mirror of
https://github.com/GAM-team/GAM.git
synced 2026-06-04 06:11:39 +00:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c09743f34 | ||
|
|
1dab9633aa | ||
|
|
57222b1a77 | ||
|
|
b54fef185c | ||
|
|
c36a71fe9e | ||
|
|
3491bd2286 | ||
|
|
36bc8837a7 | ||
|
|
395e01c1c9 | ||
|
|
b6528c4bf5 | ||
|
|
d361825165 | ||
|
|
17bc6c9f44 | ||
|
|
4058399672 | ||
|
|
e9c11f8b8d | ||
|
|
a08865b0cd | ||
|
|
8782865da4 | ||
|
|
8d79fdfe24 | ||
|
|
d86be2014c | ||
|
|
700bdfe8f8 | ||
|
|
0c50f4a794 | ||
|
|
5cf154d70b | ||
|
|
a13101ca8a | ||
|
|
1c8a3a3c57 | ||
|
|
4fc4a21909 | ||
|
|
061fead29e | ||
|
|
36887ba658 | ||
|
|
cecd9dc7ae | ||
|
|
4a952cbd16 | ||
|
|
624ed5f57c | ||
|
|
0925ff8bd4 | ||
|
|
2aecfc24dc | ||
|
|
d5d17676cc | ||
|
|
aa62ac4f3c | ||
|
|
e555e5440d | ||
|
|
122f5c3c0d | ||
|
|
4423a1ed0b | ||
|
|
e0f8789a8a | ||
|
|
944e2ed5f4 | ||
|
|
0e7c3bee6a | ||
|
|
86e827ee66 | ||
|
|
3491267f9d | ||
|
|
4b97accc21 | ||
|
|
44daf499b1 | ||
|
|
d5ac0b6b8a | ||
|
|
ce1697baff | ||
|
|
10350fbe2e |
2
.github/ISSUE_TEMPLATE/za-bug-report.md
vendored
2
.github/ISSUE_TEMPLATE/za-bug-report.md
vendored
@@ -11,7 +11,7 @@ The issue tracker is for reporting product deficiencies. "How do I?" questions s
|
||||
|
||||
Please confirm the following:
|
||||
* I have upgraded to the latest GAM release from https://github.com/GAM-team/GAM/releases and I still have this issue.
|
||||
* I am typing the command as described in the GAM Wiki at https://github.com/jay0lee/gam/wiki
|
||||
* I am typing the command as described in the GAM Wiki at https://github.com/GAM-team/GAM/wiki.
|
||||
|
||||
Full steps to reproduce the issue:
|
||||
1.
|
||||
|
||||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -153,7 +153,7 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
cache.tar.xz
|
||||
key: gam-${{ matrix.jid }}-20251031
|
||||
key: gam-${{ matrix.jid }}-20251202
|
||||
|
||||
- name: Untar Cache archive
|
||||
if: matrix.goal == 'build' && steps.cache-python-ssl.outputs.cache-hit == 'true'
|
||||
|
||||
@@ -3268,6 +3268,11 @@ gam print course-participants [todrive <ToDriveAttribute>*]
|
||||
[formatjson [quotechar <Character>]]
|
||||
[showitemcountonly]
|
||||
|
||||
gam print course-counts students|teachers [todrive <ToDriveAttribute>*]
|
||||
(course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>])
|
||||
[mincount <Integer>]
|
||||
[formatjson [quotechar <Character>]]
|
||||
|
||||
<CourseAnnouncementFieldName> ::=
|
||||
alternatelink|
|
||||
assigneemode|
|
||||
@@ -4446,22 +4451,21 @@ gam print mobile [todrive <ToDriveAttribute>*]
|
||||
<OrgUnitFieldName> ::=
|
||||
description|
|
||||
id|orgunitid|
|
||||
inherit|blockinheritance|
|
||||
name|
|
||||
parentid|parentorgunitid|
|
||||
parent|parentorgunitpath|
|
||||
path|orgunitpath
|
||||
<OrgUnitFieldNameList> ::= "<OrgUnitFieldName>(,<OrgUnitFieldName>)*"
|
||||
|
||||
gam create|add org|ou <OrgUnitPath> [description <String>] [parent <OrgUnitItem>] [inherit|(blockinheritance False)] [buildpath]
|
||||
gam update org|ou <OrgUnitItem> [name <String>] [description <String>] [parent <OrgUnitItem>] [inherit|(blockinheritance False)]
|
||||
gam create|add org|ou <OrgUnitPath> [description <String>] [parent <OrgUnitItem>] [buildpath]
|
||||
gam update org|ou <OrgUnitItem> [name <String>] [description <String>] [parent <OrgUnitItem>]
|
||||
gam update org|ou <OrgUnitItem> add|move <CrOSTypeEntity> [quickcrosmove [<Boolean>]]
|
||||
gam update org|ou <OrgUnitItem> add|move <UserTypeEntity>
|
||||
gam update org|ou <OrgUnitItem> sync <CrOSTypeEntity> [removetoou <OrgUnitItem>] [quickcrosmove [<Boolean>]]
|
||||
gam update org|ou <OrgUnitItem> sync <UserTypeEntity> [removetoou <OrgUnitItem>]
|
||||
gam delete org|ou <OrgUnitItem>
|
||||
|
||||
gam update orgs|ous <OrgUnitEntity> [name <String>] [description <String>] [parent <OrgUnitItem>] [inherit|(blockinheritance False)]
|
||||
gam update orgs|ous <OrgUnitEntity> [name <String>] [description <String>] [parent <OrgUnitItem>]
|
||||
gam update orgs|ous <OrgUnitEntity> add|move <CrOSTypeEntity> [quickcrosmove [<Boolean>]]
|
||||
gam update orgs|ous <OrgUnitEntity> add|move <UserTypeEntity>
|
||||
gam update orgs|ous <OrgUnitEntity> sync <CrOSTypeEntity> [removetoou <OrgUnitItem>] [quickcrosmove [<Boolean>]]
|
||||
@@ -5181,11 +5185,13 @@ gam copy shareddriveacls <SharedDriveEntity> to <SharedDriveEntity>
|
||||
[adminaccess|asadmin]
|
||||
[showpermissionsmessages [<Boolean>]]
|
||||
[excludepermissionsfromdomains|includepermissionsfromdomains <DomainNameList>]
|
||||
(mappermissionsemail <EmailAddress> <EmailAddress>)* [mappermissionsemailfile <CSVFileInput> endcsv]
|
||||
(mappermissionsdomain <DomainName> <DomainName>)*
|
||||
gam sync shareddriveacls <SharedDriveEntity> with <SharedDriveEntity>
|
||||
[adminaccess|asadmin]
|
||||
[showpermissionsmessages [<Boolean>]]
|
||||
[excludepermissionsfromdomains|includepermissionsfromdomains <DomainNameList>]
|
||||
(mappermissionsemail <EmailAddress> <EmailAddress>)* [mappermissionsemailfile <CSVFileInput> endcsv]
|
||||
(mappermissionsdomain <DomainName> <DomainName>)*
|
||||
|
||||
gam print shareddriveacls [todrive <ToDriveAttribute>*]
|
||||
@@ -5210,11 +5216,13 @@ gam <UserTypeEntity> copy shareddriveacls <SharedDriveEntity> to <SharedDriveEnt
|
||||
[adminaccess|asadmin]
|
||||
[showpermissionsmessages [<Boolean>]]
|
||||
[excludepermissionsfromdomains|includepermissionsfromdomains <DomainNameList>]
|
||||
(mappermissionsemail <EmailAddress> <EmailAddress>)* [mappermissionsemailfile <CSVFileInput> endcsv]
|
||||
(mappermissionsdomain <DomainName> <DomainName>)*
|
||||
gam <UserTypeEntity> sync shareddriveacls <SharedDriveEntity> with <SharedDriveEntity>
|
||||
[adminaccess|asadmin]
|
||||
[showpermissionsmessages [<Boolean>]]
|
||||
[excludepermissionsfromdomains|includepermissionsfromdomains <DomainNameList>]
|
||||
(mappermissionsemail <EmailAddress> <EmailAddress>)* [mappermissionsemailfile <CSVFileInput> endcsv]
|
||||
(mappermissionsdomain <DomainName> <DomainName>)*
|
||||
|
||||
gam <UserTypeEntity> print shareddriveacls [todrive <ToDriveAttribute>*]
|
||||
@@ -6088,7 +6096,7 @@ gam <UserTypeEntity> show calendars
|
||||
[formatjson]
|
||||
gam <UserTypeEntity> print calendars [todrive <ToDriveAttribute>*]
|
||||
[primary] <CalendarSelectProperty>* [noprimary] [nogroups] [noresources] [nosystem] [nousers]
|
||||
[fields <CalendarListFieldList>] [permissions]
|
||||
[fields <CalendarListFieldList>] [permissions] [oneitemperrow]
|
||||
[formatjson] [delimiter <Character>] [quotechar <Character>]
|
||||
|
||||
Display a users calendar settings
|
||||
@@ -6305,7 +6313,7 @@ gam <UserTypeEntity> print events <UserCalendarEntity> [<EventEntity>] <EventDis
|
||||
|
||||
gam <UserTypeEntity> update calattendees <UserCalendarEntity> <EventEntity> [anyorganizer]
|
||||
[<EventNotificationAttribute>] [splitupdate] [dryrun|doit]
|
||||
(csv|csvfile <CSVFileInput>)
|
||||
(csv|csvfile <CSVFileInput> endcsv)
|
||||
(delete <EmailAddress>)*
|
||||
(deleteentity <EmailAddressEntity>)*
|
||||
(add <EmailAddress>)*
|
||||
@@ -6804,6 +6812,7 @@ gam <UserTypeEntity> copy drivefile <DriveFileEntity>
|
||||
[copypermissionroles <DriveFileACLRoleList>]
|
||||
[copypermissiontypes <DriveFileACLTypeList>]
|
||||
[excludepermissionsfromdomains|includepermissionsfromdomains <DomainNameList>]
|
||||
(mappermissionsemail <EmailAddress> <EmailAddress>)* [mappermissionsemailfile <CSVFileInput> endcsv]
|
||||
(mappermissionsdomain <DomainName> <DomainName>)*
|
||||
[copysheetprotectedranges [<Boolean>]]
|
||||
[copysheetprotectedrangesinheritedpermissions [<Boolean>]]
|
||||
@@ -6830,8 +6839,8 @@ gam <UserTypeEntity> move drivefile <DriveFileEntity> [newfilename <DriveFileNam
|
||||
[copysubfolderinheritedpermissions [<Boolean>]]
|
||||
[copysubfoldernoniheritedpermissions never|always|syncallfolders|syncupdatedfolders]
|
||||
[excludepermissionsfromdomains|includepermissionsfromdomains <DomainNameList>]
|
||||
(mappermissionsemail <EmailAddress> <EmailAddress>)* [mappermissionsemailfile <CSVFileInput> endcsv]
|
||||
(mappermissionsdomain <DomainName> <DomainName>)*
|
||||
[updatefilepermissions [<Boolean>]]
|
||||
[retainsourcefolders [<Boolean>]]
|
||||
[sendemailifrequired [<Boolean>]]
|
||||
[verifyorganizer [<Boolean>]]
|
||||
@@ -7873,9 +7882,9 @@ gam <UserTypeEntity> show messages|threads
|
||||
[labelids <LabelIDList>]
|
||||
[quick|notquick] [max_to_show <Number>] [includespamtrash])|(ids <MessageIDEntity>)
|
||||
[labelmatchpattern <REMatchPattern>] [sendermatchpattern <REMatchPattern>]
|
||||
[countsonly|positivecountsonly] [useronly]
|
||||
[countsonly|positivecountsonly]
|
||||
[headers all|<SMTPHeaderList>] [dateheaderformat iso|rfc2822|<String>] [dateheaderconverttimezone [<Boolean>]]
|
||||
[showlabels] [delimiter <Character>] [showbody] [showhtml] [showdate] [showsize] [showsnippet]
|
||||
[showlabels] [useronly] [delimiter <Character>] [showbody] [showhtml] [showdate] [showsize] [showsnippet]
|
||||
[maxmessagesperthread <Number>]
|
||||
[[attachmentnamepattern <REMatchPattern>]
|
||||
[showattachments [noshowtextplain]]
|
||||
@@ -7886,9 +7895,9 @@ gam <UserTypeEntity> print messages|threads [todrive <ToDriveAttribute>*]
|
||||
[labelids <LabelIDList>]
|
||||
[quick|notquick] [max_to_print <Number>] [includespamtrash])|(ids <MessageIDEntity>)
|
||||
[labelmatchpattern <REMatchPattern>] [sendermatchpattern <REMatchPattern>]
|
||||
[countsonly|positivecountsonly] [useronly]
|
||||
[countsonly|positivecountsonly]
|
||||
[headers all|<SMTPHeaderList>] [dateheaderformat iso|rfc2822|<String> [dateheaderconverttimezone [<Boolean>]]]
|
||||
[showlabels] [delimiter <Character>] [showbody] [showhtml] [showdate] [showsize] [showsnippet]
|
||||
[showlabels] [useronly] [delimiter <Character>] [showbody] [showhtml] [showdate] [showsize] [showsnippet]
|
||||
[maxmessagesperthread <Number>]
|
||||
[convertcrnl] [delimiter <Character>]
|
||||
[[attachmentnamepattern <REMatchPattern>]
|
||||
|
||||
@@ -1,6 +1,86 @@
|
||||
7.29.04
|
||||
|
||||
Updated `gam delete chromepolicy chrome.users.apps.InstallType ou <OrgUnitItem> appid <AppID>`
|
||||
to allow deleting an app, i.e., explicitly remove it from management. `<OrgUnitItem>` must specify where it was added for management.
|
||||
|
||||
7.29.03
|
||||
|
||||
Remove debugging message from `gam <UserTypeEntity> move drivefile <DriveFileEntity>`.
|
||||
|
||||
7.29.02
|
||||
|
||||
Fixed bug in `gam <UserTypeEntity> move drivefile <DriveFileEntity>` where the following options
|
||||
were only applied to top level files or folders re-created in the destination. Now, domain
|
||||
and email address mappings apply to all moved files/folders.
|
||||
```
|
||||
excludepermissionsfromdomains <DomainNameList>
|
||||
includepermissionsfromdomains <DomainNameList>
|
||||
mappermissionsdomain <DomainName> <DomainName>
|
||||
mappermissionsemail <EmailAddress> <EmailAddress>
|
||||
mappermissionsemailfile <CSVFileInput> endcsv
|
||||
```
|
||||
|
||||
Upgraded to Python 3.14.1.
|
||||
|
||||
7.29.01
|
||||
|
||||
Added option `oneitemperrow` to `gam <UserTypeEntity> print calendars ... permissions` to have each of a
|
||||
calendar's permissions displayed on a separate row with all of the other calendar fields.
|
||||
|
||||
Updated `gam yubikey reset_piv` to handle YubiKey firmware updates that caused an error.
|
||||
|
||||
7.29.00
|
||||
|
||||
Added options `mappermissionsemail <EmailAddress> <EmailAddress>` and ` mappermissionsemailfile <CSVFileInput> endcsv`
|
||||
to these commands:
|
||||
```
|
||||
gam [<UserTypeEntity>] copy shareddriveacls <SharedDriveEntity> to <SharedDriveEntity>
|
||||
gam [<UserTypeEntity>] sync shareddriveacls <SharedDriveEntity> with <SharedDriveEntity>
|
||||
gam <UserTypeEntity> copy drivefile <DriveFileEntity>
|
||||
gam <UserTypeEntity> move drivefile <DriveFileEntity>
|
||||
```
|
||||
When `mappermissionsemail <EmailAddress> <EmailAddress>` is specifed, an ACL that references the first `<EmailAddress>`
|
||||
in the source will be modified to reference the second `<EmailAddress>` in the destination.
|
||||
|
||||
Bulk permission email address mapping can be specified with `mappermissionsemailfile <CSVFileInput> endcsv`.
|
||||
`<CSVFileInput>` must include these columns: `sourceEmail` and `destinationEmail`.
|
||||
|
||||
These options will be most useful with inter-workspace Shared Drive copies and moves.
|
||||
|
||||
7.28.13
|
||||
|
||||
Added option `addcsvdata <FieldName> <String>` to `gam <UserTypeEntity> print messages`
|
||||
that adds additional columns of data to the CSV file output.
|
||||
|
||||
7.28.12
|
||||
|
||||
Updated `gam delete project` to handle the following error:
|
||||
```
|
||||
ERROR: 400: failedPrecondition - Project not active
|
||||
```
|
||||
|
||||
7.28.11
|
||||
|
||||
Removed all options/fields referencing inheritance from `gam create|update|info|print org` as this option/field is deprecated.
|
||||
|
||||
7.28.10
|
||||
|
||||
Added a command `gam print course-counts` that dsplays the count of the number of courses in which a teacher or student is a participant.
|
||||
|
||||
* See: https://github.com/GAM-team/GAM/wiki/Classroom-Membership#display-course-counts-for-teachers-students
|
||||
|
||||
7.28.09
|
||||
|
||||
Fixed bug in `gam print cigroups ... descriptionmatchpattern [not] <REMatchPattern>` that caused a trap.
|
||||
|
||||
7.28.08
|
||||
|
||||
Updated `gam <UserTypeEntity> print|show chatmessages` to cache the sender UID to email address
|
||||
map so that each sender UID only has to be looked up once; this improves performance.
|
||||
|
||||
7.28.07
|
||||
|
||||
Fixed bug in 'gam report users ... aggregatebydate|aggregatebyuser` where `accounts:used_quota_in_percentage` was incorrectly displayed.
|
||||
Fixed bug in `gam report users ... aggregatebydate|aggregatebyuser` where `accounts:used_quota_in_percentage` was incorrectly displayed.
|
||||
|
||||
7.28.06
|
||||
|
||||
@@ -17315,7 +17395,7 @@ Current version of Gam with drive_v3_name_names = false
|
||||
Owner,title,parents,parents.0.id,parents.1.id
|
||||
testuser@domain.com,TestFile,2,PPPP1111,PPPP2222
|
||||
|
||||
Current version of Gam with drive_v3_name_names = true
|
||||
Current version of Gam with drive_v3_name_names = true
|
||||
Owner,name,parents
|
||||
testuser@domain.com,TestFile,PPPP1111 PPPP2222
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ https://github.com/GAM-team/GAM/wiki
|
||||
"""
|
||||
|
||||
__author__ = 'GAM Team <google-apps-manager@googlegroups.com>'
|
||||
__version__ = '7.28.07'
|
||||
__version__ = '7.29.04'
|
||||
__license__ = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
|
||||
|
||||
#pylint: disable=wrong-import-position
|
||||
@@ -12068,10 +12068,10 @@ def doDeleteProject():
|
||||
projectId = project['projectId']
|
||||
try:
|
||||
callGAPI(crm.projects(), 'delete',
|
||||
throwReasons=[GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
|
||||
throwReasons=[GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
|
||||
name=project['name'])
|
||||
entityActionPerformed([Ent.PROJECT, projectId])
|
||||
except (GAPI.forbidden, GAPI.permissionDenied) as e:
|
||||
except (GAPI.forbidden, GAPI.permissionDenied, GAPI.failedPrecondition) as e:
|
||||
entityActionFailedWarning([Ent.PROJECT, projectId], str(e))
|
||||
Ind.Decrement()
|
||||
|
||||
@@ -17603,23 +17603,7 @@ def doShowTransferApps():
|
||||
Ind.Decrement()
|
||||
Ind.Decrement()
|
||||
|
||||
def _getOrgInheritance(myarg, body):
|
||||
if myarg == 'noinherit':
|
||||
Cmd.Backup()
|
||||
deprecatedArgumentExit(myarg)
|
||||
elif myarg == 'inherit':
|
||||
body['blockInheritance'] = False
|
||||
elif myarg in {'blockinheritance', 'inheritanceblocked'}:
|
||||
location = Cmd.Location()-1
|
||||
if getBoolean():
|
||||
Cmd.SetLocation(location)
|
||||
deprecatedArgumentExit(myarg)
|
||||
body['blockInheritance'] = False
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
# gam create org|ou <String> [description <String>] [parent <OrgUnitItem>] [inherit|(blockinheritance False)] [buildpath]
|
||||
# gam create org|ou <String> [description <String>] [parent <OrgUnitItem>] [buildpath]
|
||||
def doCreateOrg():
|
||||
|
||||
def _createOrg(body, parentPath, fullPath):
|
||||
@@ -17648,8 +17632,6 @@ def doCreateOrg():
|
||||
body['description'] = getStringWithCRsNLs()
|
||||
elif myarg == 'parent':
|
||||
parent = getOrgUnitItem()
|
||||
elif _getOrgInheritance(myarg, body):
|
||||
pass
|
||||
elif myarg == 'buildpath':
|
||||
buildPath = True
|
||||
else:
|
||||
@@ -17933,8 +17915,6 @@ def _doUpdateOrgs(entityList):
|
||||
body['parentOrgUnitId'] = parent
|
||||
else:
|
||||
body['parentOrgUnitPath'] = parent
|
||||
elif _getOrgInheritance(myarg, body):
|
||||
pass
|
||||
else:
|
||||
unknownArgumentExit()
|
||||
i = 0
|
||||
@@ -17956,7 +17936,7 @@ def _doUpdateOrgs(entityList):
|
||||
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
|
||||
checkEntityAFDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, orgUnitPath)
|
||||
|
||||
# gam update orgs|ous <OrgUnitEntity> [name <String>] [description <String>] [parent <OrgUnitItem>] [inherit|(blockinheritance False)]
|
||||
# gam update orgs|ous <OrgUnitEntity> [name <String>] [description <String>] [parent <OrgUnitItem>]
|
||||
# gam update orgs|ous <OrgUnitEntity> add|move <CrosTypeEntity> [quickcrosmove [<Boolean>]]
|
||||
# gam update orgs|ous <OrgUnitEntity> add|move <UserTypeEntity>
|
||||
# gam update orgs|ous <OrgUnitEntity> sync <CrosTypeEntity> [removetoou <OrgUnitItem>] [quickcrosmove [<Boolean>]]
|
||||
@@ -17964,7 +17944,7 @@ def _doUpdateOrgs(entityList):
|
||||
def doUpdateOrgs():
|
||||
_doUpdateOrgs(getEntityList(Cmd.OB_ORGUNIT_ENTITY, shlexSplit=True))
|
||||
|
||||
# gam update org|ou <OrgUnitItem> [name <String>] [description <String>] [parent <OrgUnitItem>] [inherit|(blockinheritance False)]
|
||||
# gam update org|ou <OrgUnitItem> [name <String>] [description <String>] [parent <OrgUnitItem>]
|
||||
# gam update org|ou <OrgUnitItem> add|move <CrosTypeEntity> [quickcrosmove [<Boolean>]]
|
||||
# gam update org|ou <OrgUnitItem> add|move <UserTypeEntity>
|
||||
# gam update org|ou <OrgUnitItem> sync <CrosTypeEntity> [removetoou <OrgUnitItem>] [quickcrosmove [<Boolean>]]
|
||||
@@ -18096,9 +18076,6 @@ def doInfoOrgs():
|
||||
_doInfoOrgs(getEntityList(Cmd.OB_ORGUNIT_ENTITY, shlexSplit=True))
|
||||
|
||||
ORG_ARGUMENT_TO_FIELD_MAP = {
|
||||
'blockinheritance': 'blockInheritance',
|
||||
'inheritanceblocked': 'blockInheritance',
|
||||
'inherit': 'blockInheritance',
|
||||
'description': 'description',
|
||||
'id': 'orgUnitId',
|
||||
'name': 'name',
|
||||
@@ -28051,10 +28028,14 @@ def printShowChatMembers(users):
|
||||
def doPrintShowChatMembers():
|
||||
printShowChatMembers([None])
|
||||
|
||||
def _getChatSenderEmail(cd, sender):
|
||||
def _getChatSenderEmail(cd, sender, chatSenders):
|
||||
if sender['type'] == 'HUMAN':
|
||||
_, senderUid = sender['name'].split('/')
|
||||
sender['email'], _ = convertUIDtoEmailAddressWithType(f'uid:{senderUid}', cd, None, emailTypes=['user'])
|
||||
if senderUid in chatSenders:
|
||||
sender['email'] = chatSenders[senderUid]
|
||||
else:
|
||||
sender['email'], _ = convertUIDtoEmailAddressWithType(f'uid:{senderUid}', cd, None, emailTypes=['user'])
|
||||
chatSenders[senderUid] = sender['email']
|
||||
|
||||
def trimChatMessageIfRequired(body):
|
||||
if 'text' in body:
|
||||
@@ -28269,6 +28250,7 @@ def infoChatMessage(users):
|
||||
FJQC.GetFormatJSON(myarg)
|
||||
if not name:
|
||||
missingArgumentExit('name')
|
||||
chatSenders = {}
|
||||
fields = getFieldsFromFieldsList(fieldsList)
|
||||
i, count, users = getEntityArgument(users)
|
||||
for user in users:
|
||||
@@ -28280,7 +28262,7 @@ def infoChatMessage(users):
|
||||
message = callGAPI(chat.spaces().messages(), 'get',
|
||||
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
|
||||
name=name, fields=fields)
|
||||
_getChatSenderEmail(cd, message['sender'])
|
||||
_getChatSenderEmail(cd, message['sender'], chatSenders)
|
||||
if not FJQC.formatJSON:
|
||||
entityPerformAction(kvList, i, count)
|
||||
Ind.Increment()
|
||||
@@ -28328,6 +28310,7 @@ def printShowChatMessages(users):
|
||||
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
||||
if not parentList:
|
||||
missingArgumentExit('space')
|
||||
chatSenders = {}
|
||||
fields = getItemFieldsFromFieldsList('messages', fieldsList)
|
||||
i, count, users = getEntityArgument(users)
|
||||
for user in users:
|
||||
@@ -28361,7 +28344,7 @@ def printShowChatMessages(users):
|
||||
fields=fields)
|
||||
for message in messages:
|
||||
if 'sender' in message:
|
||||
_getChatSenderEmail(cd, message['sender'])
|
||||
_getChatSenderEmail(cd, message['sender'], chatSenders)
|
||||
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
||||
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
|
||||
continue
|
||||
@@ -29136,6 +29119,25 @@ def doDeleteChromePolicy():
|
||||
return
|
||||
kvList = setPolicyKVList([entityType, targetName, Ent.CHROME_POLICY, ','.join(schemaNameList)], printer_id, app_id)
|
||||
updatePolicyRequests(body, targetResource, printer_id, app_id)
|
||||
if orgUnit and app_id:
|
||||
for request in body['requests']:
|
||||
if request['policySchema'] == 'chrome.users.apps.InstallType':
|
||||
# Deleting an app must be done at the Organizational Unit at which the app was explicitly added for management.
|
||||
# When calling resolve, the field addedSourceKey contains the Organization Unit where it was added for management.
|
||||
# In other words, delete should only be called for apps where the Organizational Unit in addedSourceKey is equal to the one in policyTargetKey.
|
||||
# In order to delete an app (explicitly remove it from management) you should send a batchInherit request in which
|
||||
# the policySchema is the schema for the given app type, with an asterisk (*) in place of a specific policy.
|
||||
try:
|
||||
result = callGAPI(cp.customers().policies(), 'resolve', 'resolvedPolicies',
|
||||
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.INVALID_ARGUMENT,
|
||||
GAPI.SERVICE_NOT_AVAILABLE, GAPI.QUOTA_EXCEEDED],
|
||||
customer=customer, body={'policySchemaFilter': request['policySchema'], 'policyTargetKey': request['policyTargetKey']})
|
||||
if result:
|
||||
policy = result['resolvedPolicies'][0]
|
||||
if request['policyTargetKey']['targetResource'] == policy['addedSourceKey'].get('targetResource', ''):
|
||||
request['policySchema'] = 'chrome.users.apps.*'
|
||||
except (GAPI.notFound, GAPI.permissionDenied, GAPI.invalidArgument, GAPI.serviceNotAvailable, GAPI.quotaExceeded) as e:
|
||||
continue
|
||||
try:
|
||||
callGAPI(service, function,
|
||||
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED,
|
||||
@@ -34694,10 +34696,10 @@ def checkGroupMatchPatterns(groupEmail, group, matchPatterns):
|
||||
return False
|
||||
else: # field in {'name', 'displayName', 'description'}:
|
||||
if not matchp['not']:
|
||||
if not matchp['pattern'].match(group[field]):
|
||||
if not matchp['pattern'].match(group.get(field, '')):
|
||||
return False
|
||||
else:
|
||||
if matchp['pattern'].match(group[field]):
|
||||
if matchp['pattern'].match(group.get(field, '')):
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -50284,6 +50286,64 @@ def doPrintCourseParticipants():
|
||||
csvPF.SetSortTitles(COURSE_PARTICIPANTS_SORT_TITLES)
|
||||
csvPF.writeCSVfile('Course Participants')
|
||||
|
||||
COURSE_COUNTS_MEMBER_ARGUMENTS = ['students', 'teachers']
|
||||
COURSE_COUNTS_KEY_TITLE = {'students': 'Student', 'teachers': 'Teacher'}
|
||||
|
||||
# gam print course-counts students|teachers [todrive <ToDriveAttribute>*]
|
||||
# (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>])
|
||||
# [mincount <Integer>]
|
||||
# [formatjson [quotechar <Character>]]
|
||||
def doPrintCourseCounts():
|
||||
croom = buildGAPIObject(API.CLASSROOM)
|
||||
courseSelectionParameters = _initCourseSelectionParameters()
|
||||
courseShowProperties = _initCourseShowProperties()
|
||||
courseShowProperties['members'] = getChoice(COURSE_COUNTS_MEMBER_ARGUMENTS)
|
||||
keyTitle = COURSE_COUNTS_KEY_TITLE[courseShowProperties['members']]
|
||||
csvPF = CSVPrintFile([keyTitle, 'CourseCount'])
|
||||
FJQC = FormatJSONQuoteChar(csvPF)
|
||||
minCount = 0
|
||||
useOwnerAccess = GC.Values[GC.USE_COURSE_OWNER_ACCESS]
|
||||
while Cmd.ArgumentsRemaining():
|
||||
myarg = getArgument()
|
||||
if myarg == 'todrive':
|
||||
csvPF.GetTodriveParameters()
|
||||
elif _getCourseSelectionParameters(myarg, courseSelectionParameters):
|
||||
pass
|
||||
elif myarg == 'mincount':
|
||||
minCount = getInteger(minVal=0)
|
||||
else:
|
||||
FJQC.GetFormatJSONQuoteChar(myarg, False)
|
||||
coursesInfo = _getCoursesInfo(croom, courseSelectionParameters, courseShowProperties, useOwnerAccess)
|
||||
if not coursesInfo:
|
||||
coursesInfo = []
|
||||
teachersFields = 'nextPageToken,teachers(profile(emailAddress))'
|
||||
studentsFields = 'nextPageToken,students(profile(emailAddress))'
|
||||
courseCounts = {}
|
||||
count = len(coursesInfo)
|
||||
i = 0
|
||||
for course in coursesInfo:
|
||||
i += 1
|
||||
courseId = course['id']
|
||||
ocroom = _getCourseOwnerSA(croom, course, useOwnerAccess)
|
||||
if not ocroom:
|
||||
continue
|
||||
_, teachers, students = _getCourseAliasesMembers(croom, ocroom, courseId, courseShowProperties, teachersFields, studentsFields, True, i, count)
|
||||
members = teachers if courseShowProperties['members'] == 'teachers' else students
|
||||
for member in members:
|
||||
memberKey = member['profile'].get('emailAddress', '')
|
||||
courseCounts.setdefault(memberKey, 0)
|
||||
courseCounts[memberKey] += 1
|
||||
if not FJQC.formatJSON:
|
||||
for key, count in sorted(courseCounts.items()):
|
||||
if count >= minCount:
|
||||
csvPF.WriteRow({keyTitle: key, 'CourseCount': count})
|
||||
else:
|
||||
csvPF.SetJSONTitles(['JSON'])
|
||||
for key, count in sorted(courseCounts.items()):
|
||||
if count >= minCount:
|
||||
csvPF.WriteRow({'JSON': {keyTitle: key, 'CourseCount': count}})
|
||||
csvPF.writeCSVfile(f'{keyTitle} Course Counts')
|
||||
|
||||
def _batchAddItemsToCourse(croom, courseId, i, count, addItems, addType):
|
||||
def _addIdToResponse(response, riItem):
|
||||
if addType == Ent.COURSE_ANNOUNCEMENT:
|
||||
@@ -53010,7 +53070,7 @@ CALENDAR_EXCLUDE_DOMAINS = {
|
||||
|
||||
# gam <UserTypeEntity> print calendars <UserCalendarEntity> [todrive <ToDriveAttribute>*]
|
||||
# [primary] <CalendarSelectProperty>* [noprimary] [nogroups] [noresources] [nosystem] [nousers]
|
||||
# [fields <CalendarFieldList>] [permissions]
|
||||
# [fields <CalendarFieldList>] [permissions] [oneitemperrow]
|
||||
# [formatjson [quotechar <Character>]] [delimiter <Character>]
|
||||
# gam <UserTypeEntity> show calendars <UserCalendarEntity>
|
||||
# [primary] <CalendarSelectProperty>* [noprimary] [nogroups] [noresources] [nosystem] [nousers]
|
||||
@@ -53018,7 +53078,7 @@ CALENDAR_EXCLUDE_DOMAINS = {
|
||||
# [formatjson]
|
||||
def printShowCalendars(users):
|
||||
acls = []
|
||||
getCalPermissions = noPrimary = primaryOnly = False
|
||||
getCalPermissions = oneItemPerRow = noPrimary = primaryOnly = False
|
||||
excludes = set()
|
||||
excludeDomains = set()
|
||||
csvPF = CSVPrintFile(['primaryEmail', 'calendarId'], 'sortall') if Act.csvFormat() else None
|
||||
@@ -53032,6 +53092,8 @@ def printShowCalendars(users):
|
||||
csvPF.GetTodriveParameters()
|
||||
elif myarg in [Cmd.ARG_ACLS, Cmd.ARG_CALENDARACLS, Cmd.ARG_PERMISSIONS]:
|
||||
getCalPermissions = True
|
||||
elif myarg == 'oneitemperrow':
|
||||
oneItemPerRow = True
|
||||
elif myarg == 'allcalendars':
|
||||
pass
|
||||
elif myarg == 'primary':
|
||||
@@ -53107,17 +53169,35 @@ def printShowCalendars(users):
|
||||
Ind.Decrement()
|
||||
else:
|
||||
if calendars:
|
||||
for calendar in calendars:
|
||||
row = {'primaryEmail': user, 'calendarId': calendar['id']}
|
||||
if getCalPermissions:
|
||||
flattenJSON({'permissions': _getCalendarPermissions(cal, calendar)}, flattened=row)
|
||||
flattenJSON(calendar, flattened=row, simpleLists=CALENDAR_SIMPLE_LISTS, delimiter=delimiter)
|
||||
if not FJQC.formatJSON:
|
||||
row.pop('id')
|
||||
csvPF.WriteRowTitles(row)
|
||||
elif csvPF.CheckRowTitles(row):
|
||||
csvPF.WriteRowNoFilter({'primaryEmail': user, 'calendarId': calendar['id'],
|
||||
'JSON': json.dumps(cleanJSON(calendar), ensure_ascii=False, sort_keys=True)})
|
||||
if not getCalPermissions or not oneItemPerRow:
|
||||
for calendar in calendars:
|
||||
row = {'primaryEmail': user, 'calendarId': calendar['id']}
|
||||
if getCalPermissions:
|
||||
calPerms = _getCalendarPermissions(cal, calendar)
|
||||
flattenJSON({'permissions': calPerms}, flattened=row)
|
||||
flattenJSON(calendar, flattened=row, simpleLists=CALENDAR_SIMPLE_LISTS, delimiter=delimiter)
|
||||
if not FJQC.formatJSON:
|
||||
row.pop('id')
|
||||
csvPF.WriteRowTitles(row)
|
||||
elif csvPF.CheckRowTitles(row):
|
||||
if getCalPermissions:
|
||||
calendar.update({'permissions': calPerms})
|
||||
csvPF.WriteRowNoFilter({'primaryEmail': user, 'calendarId': calendar['id'],
|
||||
'JSON': json.dumps(cleanJSON(calendar), ensure_ascii=False, sort_keys=True)})
|
||||
else:
|
||||
for calendar in calendars:
|
||||
baserow = {'primaryEmail': user, 'calendarId': calendar['id']}
|
||||
flattenJSON(calendar, flattened=baserow, simpleLists=CALENDAR_SIMPLE_LISTS, delimiter=delimiter)
|
||||
for permission in _getCalendarPermissions(cal, calendar):
|
||||
row = baserow.copy()
|
||||
flattenJSON({'permission': permission}, flattened=row)
|
||||
if not FJQC.formatJSON:
|
||||
row.pop('id')
|
||||
csvPF.WriteRowTitles(row)
|
||||
elif csvPF.CheckRowTitles(row):
|
||||
calendar.update({'permission': permission})
|
||||
csvPF.WriteRowNoFilter({'primaryEmail': user, 'calendarId': calendar['id'],
|
||||
'JSON': json.dumps(cleanJSON(calendar), ensure_ascii=False, sort_keys=True)})
|
||||
elif GC.Values[GC.CSV_OUTPUT_USERS_AUDIT]:
|
||||
csvPF.WriteRowNoFilter({'primaryEmail': user})
|
||||
if csvPF:
|
||||
@@ -53589,7 +53669,7 @@ def emptyCalendarTrash(users):
|
||||
|
||||
# gam <UserTypeEntity> update calattendees <UserCalendarEntity> <EventEntity> [anyorganizer]
|
||||
# [<EventNotificationAttribute>] [splitupdate] [doit]
|
||||
# (<CSVFileSelector>)
|
||||
# (csv|csvfile <CSVFileInput> endcsv)
|
||||
# (delete <EmailAddress>)*
|
||||
# (deleteentity <EmailAddressEntity>)*
|
||||
# (add <EmailAddress>)*
|
||||
@@ -61086,6 +61166,7 @@ def initCopyMoveOptions(copyCmd):
|
||||
'noCopyNonInheritedPermissions': COPY_NONINHERITED_PERMISSIONS_NEVER,
|
||||
'excludePermissionsFromDomains': set(),
|
||||
'includePermissionsFromDomains': set(),
|
||||
'mapPermissionsEmails': {},
|
||||
'mapPermissionsDomains': {},
|
||||
'copySheetProtectedRangesInheritedPermissions': False,
|
||||
'copySheetProtectedRangesNonInheritedPermissions': COPY_NONINHERITED_PERMISSIONS_NEVER,
|
||||
@@ -61162,6 +61243,18 @@ def getCopyMoveOptions(myarg, copyMoveOptions):
|
||||
elif myarg == 'includepermissionsfromdomains':
|
||||
copyMoveOptions['includePermissionsFromDomains'] = set(getString(Cmd.OB_DOMAIN_NAME_LIST).lower().replace(',', ' ').split())
|
||||
copyMoveOptions['excludePermissionsFromDomains'] = set()
|
||||
elif myarg == 'mappermissionsemail':
|
||||
sourceEmail = getEmailAddress(noUid=True).lower()
|
||||
copyMoveOptions['mapPermissionsEmails'][sourceEmail] = getEmailAddress(noUid=True).lower()
|
||||
elif myarg == 'mappermissionsemailfile':
|
||||
csvInputLocation = Cmd.Location()
|
||||
f, csvFile, _ = openCSVFileReader(getString(Cmd.OB_FILE_NAME))
|
||||
if 'sourceEmail' not in csvFile.fieldnames or 'destinationEmail' not in csvFile.fieldnames:
|
||||
Cmd.SetLocation(csvInputLocation)
|
||||
usageErrorExit(Msg.MAP_PERMISSIONS_EMAIL_FILE_HEADERS_REQUIRED.format(myarg))
|
||||
for row in csvFile:
|
||||
copyMoveOptions['mapPermissionsEmails'][row['sourceEmail']] = row['destinationEmail']
|
||||
closeFile(f)
|
||||
elif myarg == 'mappermissionsdomain':
|
||||
oldDomain = getString(Cmd.OB_DOMAIN_NAME).lower()
|
||||
copyMoveOptions['mapPermissionsDomains'][oldDomain] = getString(Cmd.OB_DOMAIN_NAME).lower()
|
||||
@@ -61323,9 +61416,9 @@ def _copyPermissions(drive, user, i, count, j, jcount,
|
||||
if permissionType in {'group', 'user'}:
|
||||
atLoc = emailAddress.find('@')
|
||||
if atLoc > 0:
|
||||
domain = emailAddress[atLoc+1:]
|
||||
domain = emailAddress[atLoc+1:].lower()
|
||||
elif permissionType == 'domain':
|
||||
domain = permission.get('domain', '')
|
||||
domain = permission.get('domain', '').lower()
|
||||
if role not in copyMoveOptions['copyPermissionRoles']:
|
||||
notCopiedMessage = f'role {role} not selected'
|
||||
elif permissionType not in copyMoveOptions['copyPermissionTypes']:
|
||||
@@ -61375,7 +61468,12 @@ def _copyPermissions(drive, user, i, count, j, jcount,
|
||||
|
||||
def mapPermissionsDomains(srcPerm):
|
||||
if 'emailAddress' in srcPerm:
|
||||
email, domain = srcPerm['emailAddress'].lower().split('@', 1)
|
||||
sourceEmail = srcPerm['emailAddress'].lower()
|
||||
destEmail = copyMoveOptions['mapPermissionsEmails'].get(sourceEmail, None)
|
||||
if destEmail:
|
||||
srcPerm['emailAddress'] = destEmail
|
||||
return True
|
||||
email, domain = sourceEmail.split('@', 1)
|
||||
if domain in copyMoveOptions['mapPermissionsDomains']:
|
||||
srcPerm['emailAddress'] = f"{email}@{copyMoveOptions['mapPermissionsDomains'][domain]}"
|
||||
return True
|
||||
@@ -61389,6 +61487,7 @@ def _copyPermissions(drive, user, i, count, j, jcount,
|
||||
sourcePerms = getPermissions(fileId)
|
||||
if sourcePerms is None:
|
||||
return
|
||||
Ind.Increment()
|
||||
copySourcePerms = {}
|
||||
deleteTargetPermIds = set()
|
||||
updateTargetPerms = {}
|
||||
@@ -61397,18 +61496,19 @@ def _copyPermissions(drive, user, i, count, j, jcount,
|
||||
if isPermissionCopyable(kvList, permission):
|
||||
copySourcePerms[permissionId] = permission
|
||||
if copyMoveOptions[copyNonInherited] == COPY_NONINHERITED_PERMISSIONS_ALWAYS:
|
||||
if copyMoveOptions['mapPermissionsDomains']:
|
||||
if copyMoveOptions['mapPermissionsDomains'] or copyMoveOptions['mapPermissionsEmails']:
|
||||
for permissionId in getNonInheritedPermissions(copySourcePerms):
|
||||
mapPermissionsDomains(copySourcePerms[permissionId])
|
||||
elif copyMoveOptions[copyNonInherited] in {COPY_NONINHERITED_PERMISSIONS_SYNC_ALL_FOLDERS,
|
||||
COPY_NONINHERITED_PERMISSIONS_SYNC_UPDATED_FOLDERS}:
|
||||
targetPerms = getPermissions(newFileId)
|
||||
if targetPerms is None:
|
||||
Ind.Decrement()
|
||||
return
|
||||
sourceNonInheritedPermIDs = getNonInheritedPermissions(copySourcePerms)
|
||||
targetNonInheritedPermIDs = getNonInheritedPermissions(targetPerms)
|
||||
# Permissions in Source only
|
||||
if copyMoveOptions['mapPermissionsDomains']:
|
||||
if copyMoveOptions['mapPermissionsDomains'] or copyMoveOptions['mapPermissionsEmails']:
|
||||
for permissionId in sourceNonInheritedPermIDs-targetNonInheritedPermIDs:
|
||||
mapPermissionsDomains(copySourcePerms[permissionId])
|
||||
# Permissions in Target only
|
||||
@@ -61416,7 +61516,7 @@ def _copyPermissions(drive, user, i, count, j, jcount,
|
||||
# Permissions in Source and Target
|
||||
for permissionId in targetNonInheritedPermIDs&sourceNonInheritedPermIDs:
|
||||
srcPerm = copySourcePerms[permissionId]
|
||||
if copyMoveOptions['mapPermissionsDomains'] and mapPermissionsDomains(srcPerm):
|
||||
if (copyMoveOptions['mapPermissionsDomains'] or copyMoveOptions['mapPermissionsEmails']) and mapPermissionsDomains(srcPerm):
|
||||
deleteTargetPermIds.add(permissionId)
|
||||
continue
|
||||
tgtPerm = targetPerms[permissionId]
|
||||
@@ -61441,7 +61541,6 @@ def _copyPermissions(drive, user, i, count, j, jcount,
|
||||
deleteUpdateKwargs = {'useDomainAdminAccess': copyMoveOptions['useDomainAdminAccess']}
|
||||
if entityType != Ent.SHAREDDRIVE:
|
||||
deleteUpdateKwargs['enforceExpansiveAccess'] = copyMoveOptions['enforceExpansiveAccess']
|
||||
Ind.Increment()
|
||||
action = Act.Get()
|
||||
Act.Set(Act.COPY)
|
||||
kcount = len(copySourcePerms)
|
||||
@@ -61797,6 +61896,7 @@ copyReturnItemMap = {
|
||||
# [copypermissionroles <DriveFileACLRoleList>]
|
||||
# [copypermissiontypes <DriveFileACLTypeList>]
|
||||
# [excludepermissionsfromdomains|includepermissionsfromdomains <DomainNameList>]
|
||||
# (mappermissionsemail <EmailAddress> <EmailAddress>)* [mappermissionsemailfile <CSVFileInput> endcsv]
|
||||
# (mappermissionsdomain <DomainName> <DomainName>)*
|
||||
# [copysheetprotectedranges [<Boolean>]]
|
||||
# [copysheetprotectedrangesinheritedpermissions [<Boolean>]]
|
||||
@@ -62471,9 +62571,9 @@ def _updateMoveFilePermissions(drive, user, i, count,
|
||||
if permission['type'] in {'group', 'user'}:
|
||||
atLoc = permission.get('emailAddress', '').find('@')
|
||||
if atLoc > 0:
|
||||
domain = permission['emailAddress'][atLoc+1:]
|
||||
domain = permission['emailAddress'][atLoc+1:].lower()
|
||||
elif permission['type'] == 'domain':
|
||||
domain = permission.get('domain', '')
|
||||
domain = permission.get('domain', '').lower()
|
||||
if domain and domain in copyMoveOptions['excludePermissionsFromDomains']:
|
||||
notMovedMessage = f'domain {domain} excluded'
|
||||
elif domain and copyMoveOptions['includePermissionsFromDomains'] and domain not in copyMoveOptions['includePermissionsFromDomains']:
|
||||
@@ -62489,13 +62589,24 @@ def _updateMoveFilePermissions(drive, user, i, count,
|
||||
|
||||
def mapPermissionsDomains(kvList, permission):
|
||||
if 'emailAddress' in permission:
|
||||
email, domain = permission['emailAddress'].lower().split('@', 1)
|
||||
if domain in copyMoveOptions['mapPermissionsDomains']:
|
||||
sourceEmail = permission['emailAddress'].lower()
|
||||
destEmail = copyMoveOptions['mapPermissionsEmails'].get(sourceEmail, None)
|
||||
if destEmail:
|
||||
deleteSourcePerms[permission['id']] = permission.copy()
|
||||
if copyMoveOptions['showPermissionMessages']:
|
||||
notMovedMessage = f"domain {domain} mapped to {copyMoveOptions['mapPermissionsDomains'][domain]}"
|
||||
notMovedMessage = f"email {sourceEmail} mapped to {destEmail}"
|
||||
entityActionNotPerformedWarning(kvList, notMovedMessage, 0, 0)
|
||||
permission['emailAddress'] = f"{email}@{copyMoveOptions['mapPermissionsDomains'][domain]}"
|
||||
permission['emailAddress'] = destEmail
|
||||
addSourcePerms[permission['id']] = permission
|
||||
return True
|
||||
email, domain = sourceEmail.split('@', 1)
|
||||
if domain in copyMoveOptions['mapPermissionsDomains']:
|
||||
destEmail = f"{email}@{copyMoveOptions['mapPermissionsDomains'][domain]}"
|
||||
deleteSourcePerms[permission['id']] = permission.copy()
|
||||
if copyMoveOptions['showPermissionMessages']:
|
||||
notMovedMessage = f"email {sourceEmail} mapped to {destEmail}"
|
||||
entityActionNotPerformedWarning(kvList, notMovedMessage, 0, 0)
|
||||
permission['emailAddress'] = destEmail
|
||||
addSourcePerms[permission['id']] = permission
|
||||
return True
|
||||
elif 'domain' in permission:
|
||||
@@ -62513,14 +62624,14 @@ def _updateMoveFilePermissions(drive, user, i, count,
|
||||
sourcePerms = getPermissions(fileId)
|
||||
if sourcePerms is None:
|
||||
return
|
||||
Ind.Increment()
|
||||
deleteSourcePerms = {}
|
||||
addSourcePerms = {}
|
||||
for permissionId, permission in sourcePerms.items():
|
||||
kvList = permissionKVList(user, entityType, fileTitle, permission)
|
||||
if isPermissionDeletable(kvList, permission):
|
||||
pass
|
||||
elif copyMoveOptions['mapPermissionsDomains']:
|
||||
if permission.get('permissionDetails', {}).get('inherited', False):
|
||||
continue
|
||||
if (not isPermissionDeletable(kvList, permission) and
|
||||
(copyMoveOptions['mapPermissionsDomains'] or copyMoveOptions['mapPermissionsEmails'])):
|
||||
mapPermissionsDomains(kvList, permission)
|
||||
action = Act.Get()
|
||||
kcount = len(deleteSourcePerms)
|
||||
@@ -62547,7 +62658,6 @@ def _updateMoveFilePermissions(drive, user, i, count,
|
||||
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
||||
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
||||
_incrStatistic(statistics, stat)
|
||||
Ind.Decrement()
|
||||
Act.Set(action)
|
||||
return
|
||||
kcount = len(addSourcePerms)
|
||||
@@ -62594,12 +62704,41 @@ def _updateMoveFilePermissions(drive, user, i, count,
|
||||
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
||||
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
||||
_incrStatistic(statistics, stat)
|
||||
Ind.Decrement()
|
||||
Act.Set(action)
|
||||
return
|
||||
Ind.Decrement()
|
||||
Act.Set(action)
|
||||
|
||||
def _recursiveUpdateMovePermissions(drive, user, i, count,
|
||||
fileId, fileTitle,
|
||||
statistics, copyMoveOptions, sourceSearchArgs):
|
||||
_updateMoveFilePermissions(drive, user, i, count,
|
||||
Ent.DRIVE_FOLDER, fileId, fileTitle,
|
||||
statistics, STAT_FOLDER_PERMISSIONS_FAILED, copyMoveOptions)
|
||||
sourceChildren = callGAPIpages(drive.files(), 'list', 'files',
|
||||
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
||||
retryReasons=[GAPI.UNKNOWN_ERROR],
|
||||
q=WITH_PARENTS.format(fileId),
|
||||
orderBy='folder desc,name,modifiedTime desc',
|
||||
fields='nextPageToken,files(id,name,mimeType)',
|
||||
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS], **sourceSearchArgs)
|
||||
kcount = len(sourceChildren)
|
||||
if kcount > 0:
|
||||
Ind.Increment()
|
||||
k = 0
|
||||
for child in sourceChildren:
|
||||
k += 1
|
||||
childId = child['id']
|
||||
childName = child['name']
|
||||
if child['mimeType'] == MIMETYPE_GA_FOLDER:
|
||||
_recursiveUpdateMovePermissions(drive, user, i, count,
|
||||
childId, childName,
|
||||
statistics, copyMoveOptions, sourceSearchArgs)
|
||||
else:
|
||||
_updateMoveFilePermissions(drive, user, i, count,
|
||||
Ent.DRIVE_FILE, childId, childName,
|
||||
statistics, STAT_FILE_PERMISSIONS_FAILED, copyMoveOptions)
|
||||
Ind.Decrement()
|
||||
|
||||
# gam <UserTypeEntity> move drivefile <DriveFileEntity> [newfilename <DriveFileName>]
|
||||
# [summary [<Boolean>]] [showpermissionsmessages [<Boolean>]]
|
||||
# [<DriveFileParentAttribute>]
|
||||
@@ -62620,8 +62759,8 @@ def _updateMoveFilePermissions(drive, user, i, count,
|
||||
# [copypermissiontypes <DriveFileACLTypeList>]
|
||||
# [synctopfoldernoniheritedpermissions [<Boolean>]] [syncsubfoldernoninheritedpermissions [<Boolean>]]
|
||||
# [excludepermissionsfromdomains|includepermissionsfromdomains <DomainNameList>]
|
||||
# (mappermissionsemail <EmailAddress> <EmailAddress>)* [mappermissionsemailfile <CSVFileInput> endcsv]
|
||||
# (mappermissionsdomain <DomainName> <DomainName>)*
|
||||
# [updatefilepermissions [<Boolean>]]
|
||||
# [retainsourcefolders [<Boolean>]]
|
||||
# [sendemailifrequired [<Boolean>]]
|
||||
# [verifyorganizer [<Boolean>]]
|
||||
@@ -62657,7 +62796,7 @@ def moveDriveFile(users):
|
||||
copyFolderNonInheritedPermissions,
|
||||
False)
|
||||
source.pop('oldparents', None)
|
||||
return (newParentId, newParentName, True)
|
||||
return (newParentId, newParentName, True, True)
|
||||
# Merge parent folders
|
||||
if atTop and copyMoveOptions['sourceIsMyDriveSharedDrive']:
|
||||
pass
|
||||
@@ -62686,11 +62825,11 @@ def moveDriveFile(users):
|
||||
['copySubFolderInheritedPermissions', 'copyTopFolderInheritedPermissions'][atTop],
|
||||
copyFolderNonInheritedPermissions,
|
||||
False)
|
||||
return (newFolderId, newFolderName, True)
|
||||
return (newFolderId, newFolderName, True, True)
|
||||
entityActionFailedWarning(kvList+[Ent.DRIVE_FOLDER, newParentNameId], Msg.NOT_WRITABLE, j, jcount)
|
||||
_incrStatistic(statistics, STAT_FOLDER_NOT_WRITABLE)
|
||||
copyMoveOptions['retainSourceFolders'] = True
|
||||
return (None, None, False)
|
||||
return (None, None, False, False)
|
||||
elif copyMoveOptions['duplicateFolders'] == DUPLICATE_FOLDER_UNIQUE_NAME:
|
||||
newFolderName = _getUniqueFilename(newFolderName, sourceMimeType, targetChildren)
|
||||
elif copyMoveOptions['duplicateFolders'] == DUPLICATE_FOLDER_SKIP:
|
||||
@@ -62701,7 +62840,7 @@ def moveDriveFile(users):
|
||||
Msg.DUPLICATE, j, jcount)
|
||||
_incrStatistic(statistics, STAT_FOLDER_DUPLICATE)
|
||||
copyMoveOptions['retainSourceFolders'] = True
|
||||
return (None, None, False)
|
||||
return (None, None, False, False)
|
||||
# Update parents on: not retain and MD->MD, SD->MD, SD->SD
|
||||
if atTop and copyMoveOptions['sourceIsMyDriveSharedDrive']:
|
||||
pass
|
||||
@@ -62730,7 +62869,7 @@ def moveDriveFile(users):
|
||||
[Ent.DRIVE_FOLDER, newParentNameId, Ent.DRIVE_FOLDER, f'{newFolderName}({folderId})'],
|
||||
j, jcount)
|
||||
_incrStatistic(statistics, STAT_FILE_COPIED_MOVED)
|
||||
return (None, None, False)
|
||||
return (None, None, False, True)
|
||||
except (GAPI.badRequest, GAPI.insufficientParentPermissions, GAPI.fileOwnerNotMemberOfTeamDrive, GAPI.fileOwnerNotMemberOfWriterDomain,
|
||||
GAPI.fileWriterTeamDriveMoveInDisabled, GAPI.targetUserRoleLimitedByLicenseRestriction,
|
||||
GAPI.cannotMoveTrashedItemIntoTeamDrive, GAPI.cannotMoveTrashedItemOutOfTeamDrive,
|
||||
@@ -62740,8 +62879,8 @@ def moveDriveFile(users):
|
||||
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
||||
_incrStatistic(statistics, STAT_FILE_FAILED)
|
||||
copyMoveOptions['retainSourceFolders'] = True
|
||||
return (None, None, False)
|
||||
# Create new parent on: retain or MD->SD
|
||||
return (None, None, False, False)
|
||||
# Create new parent on: retain or MD->SD
|
||||
source.pop('oldparents', None)
|
||||
body = source.copy()
|
||||
body.pop('capabilities', None)
|
||||
@@ -62775,7 +62914,7 @@ def moveDriveFile(users):
|
||||
['copySubFolderInheritedPermissions', 'copyTopFolderInheritedPermissions'][atTop],
|
||||
['copySubFolderNonInheritedPermissions', 'copyTopFolderNonInheritedPermissions'][atTop],
|
||||
True)
|
||||
return (newFolderId, newFolderName, False)
|
||||
return (newFolderId, newFolderName, False, True)
|
||||
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions,
|
||||
GAPI.internalError, GAPI.storageQuotaExceeded, GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep,
|
||||
GAPI.badRequest, GAPI.targetUserRoleLimitedByLicenseRestriction) as e:
|
||||
@@ -62784,7 +62923,7 @@ def moveDriveFile(users):
|
||||
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
||||
_incrStatistic(statistics, STAT_FOLDER_FAILED)
|
||||
copyMoveOptions['retainSourceFolders'] = True
|
||||
return (None, None, False)
|
||||
return (None, None, False, False)
|
||||
|
||||
def _makeMoveShortcut(drive, user, k, kcount, entityType, childId, childName, newParentId, newParentName):
|
||||
kvList = [Ent.USER, user, entityType, f'{childName}({childId})']
|
||||
@@ -62834,7 +62973,7 @@ def moveDriveFile(users):
|
||||
def _moveFile(drive, user, i, count, k, kcount, entityType, childId, childName, newChildName, newParentId, newParentName, removeParents, body):
|
||||
kvList = [Ent.USER, user, entityType, f'{childName}({childId})']
|
||||
newParentNameId = f'{newParentName}({newParentId})'
|
||||
if updateFilePermissions:
|
||||
if updateMovePermissions:
|
||||
_updateMoveFilePermissions(drive, user, i, count,
|
||||
entityType, childId, childName,
|
||||
statistics, STAT_FILE_PERMISSIONS_FAILED, copyMoveOptions)
|
||||
@@ -62879,11 +63018,17 @@ def moveDriveFile(users):
|
||||
def _recursiveFolderMove(drive, user, i, count, j, jcount,
|
||||
source, targetChildren, newFolderName, newParentId, newParentName, mergeParentModifiedTime, atTop):
|
||||
folderId = source['id']
|
||||
newFolderId, newFolderName, existingTargetFolder = _cloneFolderMove(drive, user, i, count, j, jcount,
|
||||
source, targetChildren, newFolderName,
|
||||
newParentId, newParentName, mergeParentModifiedTime,
|
||||
statistics, copyMoveOptions, atTop)
|
||||
newFolderId, newFolderName, existingTargetFolder, status = _cloneFolderMove(drive, user, i, count, j, jcount,
|
||||
source, targetChildren, newFolderName,
|
||||
newParentId, newParentName, mergeParentModifiedTime,
|
||||
statistics, copyMoveOptions, atTop)
|
||||
if not status:
|
||||
return
|
||||
if newFolderId is None:
|
||||
if updateMovePermissions:
|
||||
_recursiveUpdateMovePermissions(drive, user, i, count,
|
||||
folderId, source['name'],
|
||||
statistics, copyMoveOptions, sourceSearchArgs)
|
||||
return
|
||||
movedFiles[newFolderId] = 1
|
||||
sourceChildren = callGAPIpages(drive.files(), 'list', 'files',
|
||||
@@ -62970,7 +63115,7 @@ def moveDriveFile(users):
|
||||
parentBody = {}
|
||||
parentParms = initDriveFileAttributes()
|
||||
copyMoveOptions = initCopyMoveOptions(False)
|
||||
newParentsSpecified = updateFilePermissions = False
|
||||
newParentsSpecified = False
|
||||
movedFiles = {}
|
||||
verifyOrganizer = True
|
||||
while Cmd.ArgumentsRemaining():
|
||||
@@ -62979,12 +63124,13 @@ def moveDriveFile(users):
|
||||
pass
|
||||
elif getDriveFileParentAttribute(myarg, parentParms):
|
||||
newParentsSpecified = True
|
||||
elif myarg == 'updatefilepermissions':
|
||||
updateFilePermissions = getBoolean()
|
||||
elif myarg == 'verifyorganizer':
|
||||
verifyOrganizer = getBoolean()
|
||||
else:
|
||||
unknownArgumentExit()
|
||||
updateMovePermissions = (copyMoveOptions['excludePermissionsFromDomains'] or copyMoveOptions['includePermissionsFromDomains'] or
|
||||
copyMoveOptions['mapPermissionsDomains'] or copyMoveOptions['mapPermissionsEmails'])
|
||||
|
||||
i, count, users = getEntityArgument(users)
|
||||
for user in users:
|
||||
i += 1
|
||||
@@ -68094,10 +68240,16 @@ def doPrintShowOrgunitSharedDrives():
|
||||
csvPF.writeCSVfile('OrgUnit {orgUnitPath} SharedDrives')
|
||||
|
||||
# gam [<UserTypeEntity>] copy shareddriveacls <SharedDriveEntity> to <SharedDriveEntity>
|
||||
# [asadmin]
|
||||
# [showpermissionsmessages [<Boolean>]]
|
||||
# [excludepermissionsfromdomains|includepermissionsfromdomains <DomainNameList>]
|
||||
# (mappermissionsemail <EmailAddress> <EmailAddress>)* [mappermissionsemailfile <CSVFileInput> endcsv]
|
||||
# (mappermissionsdomain <DomainName> <DomainName>)*
|
||||
# gam [<UserTypeEntity>] sync shareddriveacls <SharedDriveEntity> with <SharedDriveEntity>
|
||||
# [asadmin]
|
||||
# [showpermissionsmessages [<Boolean>]]
|
||||
# [excludepermissionsfromdomains|includepermissionsfromdomains <DomainNameList>]
|
||||
# (mappermissionsemail <EmailAddress> <EmailAddress>)* [mappermissionsemailfile <CSVFileInput> endcsv]
|
||||
# (mappermissionsdomain <DomainName> <DomainName>)*
|
||||
def copySyncSharedDriveACLs(users, useDomainAdminAccess=False):
|
||||
copyMoveOptions = initCopyMoveOptions(True)
|
||||
@@ -68125,13 +68277,15 @@ def copySyncSharedDriveACLs(users, useDomainAdminAccess=False):
|
||||
if not drive:
|
||||
continue
|
||||
if not srcFileIdEntity.get('shareddrivename'):
|
||||
srcFileIdEntity['shareddrivename'] = _getSharedDriveNameFromId(drive, srcFileIdEntity['shareddrive']['driveId'])
|
||||
srcFileIdEntity['shareddrivename'] = _getSharedDriveNameFromId(drive, srcFileIdEntity['shareddrive']['driveId'],
|
||||
useDomainAdminAccess)
|
||||
if tgtFileIdEntity.get('shareddrivename'):
|
||||
if not _convertSharedDriveNameToId(drive, user, i, count, tgtFileIdEntity, useDomainAdminAccess):
|
||||
continue
|
||||
tgtFileIdEntity['shareddrive']['corpora'] = 'drive'
|
||||
else:
|
||||
tgtFileIdEntity['shareddrivename'] = _getSharedDriveNameFromId(drive, tgtFileIdEntity['shareddrive']['driveId'])
|
||||
tgtFileIdEntity['shareddrivename'] = _getSharedDriveNameFromId(drive, tgtFileIdEntity['shareddrive']['driveId'],
|
||||
useDomainAdminAccess)
|
||||
statistics = _initStatistics()
|
||||
copyMoveOptions['sourceDriveId'] = srcFileIdEntity['shareddrive']['driveId']
|
||||
copyMoveOptions['destDriveId'] = tgtFileIdEntity['shareddrive']['driveId']
|
||||
@@ -73549,6 +73703,8 @@ def printShowMessagesThreads(users, entityType):
|
||||
row[f'Attachments{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{i}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}mimeType'] = attachment[1]
|
||||
row[f'Attachments{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{i}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}size'] = attachment[2]
|
||||
row[f'Attachments{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{i}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}charset'] = attachment[3]
|
||||
if addCSVData:
|
||||
row.update(addCSVData)
|
||||
csvPF.WriteRowTitles(row)
|
||||
if checkMax:
|
||||
parameters['messagesProcessed'] += 1
|
||||
@@ -74037,7 +74193,7 @@ def printShowMessagesThreads(users, entityType):
|
||||
# [quick|notquick] [max_to_print <Number>] [includespamtrash])|(ids <MessageIDEntity>)
|
||||
# [labelmatchpattern <REMatchPattern>] [sendermatchpattern <REMatchPattern>]
|
||||
# [headers all|<SMTPHeaderList>] [dateheaderformat iso|rfc2822|<String>] [dateheaderconverttimezone [<Boolean>]]
|
||||
# [showlabels] [showbody] [showhtml] [showdate] [showsize] [showsnippet]
|
||||
# [showlabels] [useronly] [delimiter <Character>] [showbody] [showhtml] [showdate] [showsize] [showsnippet]
|
||||
# [convertcrnl] [delimiter <Character>]
|
||||
# [countsonly|positivecountsonly] [useronly]
|
||||
# [[attachmentnamepattern <REMatchPattern>]
|
||||
@@ -74049,8 +74205,8 @@ def printShowMessagesThreads(users, entityType):
|
||||
# [quick|notquick] [max_to_show <Number>] [includespamtrash])|(ids <MessageIDEntity>)
|
||||
# [labelmatchpattern <REMatchPattern>] [sendermatchpattern <REMatchPattern>]
|
||||
# [headers all|<SMTPHeaderList>] [dateheaderformat iso|rfc2822|<String>] [dateheaderconverttimezone [<Boolean>]]
|
||||
# [showlabels] [showbody] [showhtml] [showdate] [showsize] [showsnippet]
|
||||
# [countsonly|positivecountsonly] [useronly]
|
||||
# [showlabels] [useronly] [showbody] [showhtml] [showdate] [showsize] [showsnippet]
|
||||
# [countsonly|positivecountsonly]
|
||||
# [[attachmentnamepattern <REMatchPattern>]
|
||||
# [showattachments [noshowtextplain]]
|
||||
# [saveattachments [targetfolder <FilePath>] [overwrite [<Boolean>]]]
|
||||
@@ -78796,6 +78952,7 @@ MAIN_COMMANDS_WITH_OBJECTS = {
|
||||
Cmd.ARG_COURSE: doPrintCourses,
|
||||
Cmd.ARG_COURSES: doPrintCourses,
|
||||
Cmd.ARG_COURSEANNOUNCEMENTS: doPrintCourseAnnouncements,
|
||||
Cmd.ARG_COURSECOUNTS: doPrintCourseCounts,
|
||||
Cmd.ARG_COURSEMATERIALS: doPrintCourseMaterials,
|
||||
Cmd.ARG_COURSEPARTICIPANTS: doPrintCourseParticipants,
|
||||
Cmd.ARG_COURSESTUDENTGROUP: doPrintCourseStudentGroups,
|
||||
@@ -79122,6 +79279,7 @@ MAIN_COMMANDS_OBJ_ALIASES = {
|
||||
Cmd.ARG_CLASSIFICATIONLABELPERMISSIONS: Cmd.ARG_DRIVELABELPERMISSION,
|
||||
Cmd.ARG_CLASS: Cmd.ARG_COURSE,
|
||||
Cmd.ARG_CLASSES: Cmd.ARG_COURSES,
|
||||
Cmd.ARG_CLASSCOUNTS: Cmd.ARG_COURSECOUNTS,
|
||||
Cmd.ARG_CLASSPARTICIPANTS: Cmd.ARG_COURSEPARTICIPANTS,
|
||||
Cmd.ARG_CLASSROOMINVITATIONS: Cmd.ARG_CLASSROOMINVITATION,
|
||||
Cmd.ARG_CONTACTS: Cmd.ARG_CONTACT,
|
||||
|
||||
@@ -788,6 +788,7 @@ class GamCLArgs():
|
||||
ARG_CLASSIFICATIONLABELPERMISSIONS = 'classificationlabelpermissions'
|
||||
ARG_CLASS = 'class'
|
||||
ARG_CLASSES = 'classes'
|
||||
ARG_CLASSCOUNTS = 'classcounts'
|
||||
ARG_CLASSPARTICIPANTS = 'classparticipants'
|
||||
ARG_CLASSROOMINVITATION = 'classroominvitation'
|
||||
ARG_CLASSROOMINVITATIONS = 'classroominvitations'
|
||||
@@ -806,6 +807,7 @@ class GamCLArgs():
|
||||
ARG_COURSE = 'course'
|
||||
ARG_COURSES = 'courses'
|
||||
ARG_COURSEANNOUNCEMENTS = 'courseannouncements'
|
||||
ARG_COURSECOUNTS = 'coursecounts'
|
||||
ARG_COURSEMATERIALS = 'coursematerials'
|
||||
ARG_COURSEPARTICIPANTS = 'courseparticipants'
|
||||
ARG_COURSESTUDENTGROUP = 'coursestudentgroup'
|
||||
|
||||
@@ -355,6 +355,7 @@ LESS_THAN_1_SECOND = 'less than 1 second'
|
||||
LIST_CHROMEOS_INVALID_INPUT_PAGE_TOKEN_RETRY = 'List ChromeOSdevices Invalid Input: pageToken retry'
|
||||
LOGGING_INITIALIZATION_ERROR = 'Logging initialization error: {0}'
|
||||
LOOKING_UP_GOOGLE_UNIQUE_ID = 'Looking up Google Unique ID'
|
||||
MAP_PERMISSIONS_EMAIL_FILE_HEADERS_REQUIRED = '{0} <CSVFileInput> requires headers "sourceEmail" and "destinationEmail"'
|
||||
MARKED_AS = 'Marked as'
|
||||
MATCHED_THE_FOLLOWING = 'Matched the following'
|
||||
MATTER_NOT_OPEN = 'Matter needs to be open, current state is: {0}'
|
||||
|
||||
@@ -19,12 +19,11 @@
|
||||
"""YubiKey"""
|
||||
|
||||
import base64
|
||||
from datetime import datetime, timedelta
|
||||
from secrets import SystemRandom
|
||||
import string
|
||||
import sys
|
||||
|
||||
import arrow
|
||||
|
||||
from gam import mplock
|
||||
|
||||
from gam import systemErrorExit
|
||||
@@ -41,7 +40,6 @@ from ykman.piv import generate_self_signed_certificate, generate_chuid
|
||||
from yubikit.piv import DEFAULT_MANAGEMENT_KEY, \
|
||||
InvalidPinError, \
|
||||
KEY_TYPE, \
|
||||
MANAGEMENT_KEY_TYPE, \
|
||||
PIN_POLICY, \
|
||||
PivSession, \
|
||||
OBJECT_ID, \
|
||||
@@ -149,17 +147,17 @@ class YubiKey():
|
||||
piv.change_puk('12345678', new_puk)
|
||||
piv.change_pin('123456', new_pin)
|
||||
writeStdout(Msg.YUBIKEY_PIN_SET_TO.format(new_pin))
|
||||
piv.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY)
|
||||
piv.authenticate(piv.management_key_type, DEFAULT_MANAGEMENT_KEY)
|
||||
piv.verify_pin(new_pin)
|
||||
writeStdout(Msg.YUBIKEY_GENERATING_NONEXPORTABLE_PRIVATE_KEY)
|
||||
pubkey = piv.generate_key(SLOT.AUTHENTICATION,
|
||||
KEY_TYPE.RSA2048,
|
||||
PIN_POLICY.ALWAYS,
|
||||
TOUCH_POLICY.NEVER)
|
||||
now = arrow.utcnow()
|
||||
valid_to = now.shift(days=36500)
|
||||
now = datetime.utcnow()
|
||||
valid_to = now + timedelta(days=3650)
|
||||
subject = 'CN=GAM Created Key'
|
||||
piv.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY)
|
||||
piv.authenticate(piv.management_key_type, DEFAULT_MANAGEMENT_KEY)
|
||||
piv.verify_pin(new_pin)
|
||||
cert = generate_self_signed_certificate(piv,
|
||||
SLOT.AUTHENTICATION,
|
||||
|
||||
@@ -394,9 +394,9 @@ gam show projects [[admin] <EmailAddress>] [all|<ProjectIDEntity>]
|
||||
* `<EmailAddress>` - A Google Workspace admin/GCP project manager; if omitted, you will be prompted for the address
|
||||
|
||||
Use these options to select projects.
|
||||
* `all` - All projects accessible by the administrator; this is the default
|
||||
* `all` - All projects accessible by the administrator
|
||||
* `current` - The project referenced in `client_secrets.json`
|
||||
* `gam` - Projects accessible by the administrator that were created by Gam, i.e, their project ID begins with `gam-project-`
|
||||
* `gam` - Projects accessible by the administrator that were created by Gam, i.e, their project ID begins with `gam-project-`; this is the default
|
||||
* `<ProjectID>` - A Google API project ID
|
||||
* `filter <String>` - A filter to select projects accessible by the administrator; see the API documentation
|
||||
* `states all|active|deleterequested` - Limit display to projects based on state; the default is `active`
|
||||
@@ -412,9 +412,9 @@ gam print projects [[admin] <EmailAddress>] [all|<ProjectIDEntity>] [todrive <To
|
||||
* `<EmailAddress>` - A Google Workspace admin/GCP project manager; if omitted, you will be prompted for the address
|
||||
|
||||
Use these options to select projects.
|
||||
* `all` - All projects accessible by the administrator; this is the default
|
||||
* `all` - All projects accessible by the administrator
|
||||
* `current` - The project referenced in `client_secrets.json`
|
||||
* `gam` - Projects accessible by the administrator that were created by Gam, i.e, their project ID begins with `gam-project-`
|
||||
* `gam` - Projects accessible by the administrator that were created by Gam, i.e, their project ID begins with `gam-project-`; this is the default
|
||||
* `<ProjectID>` - A Google API project ID
|
||||
* `filter <String>` - A filter to select projects accessible by the administrator; see the API documentation
|
||||
* `states all|active|deleterequested` - Limit display to projects based on state; the default is `active`
|
||||
|
||||
@@ -379,7 +379,7 @@ Drive files with `shareMode` `Each student will get a copy` don't seem to be abl
|
||||
|
||||
## Delete courses
|
||||
Classes can only be deleted when they are in the ARCHIVED state; to delete a class, you can update its state to ARCHIVED
|
||||
and then delete it or you can specify that it be archived as parot of the delete command.
|
||||
and then delete it or you can specify that it be archived as part of the delete command.
|
||||
```
|
||||
gam delete course <CourseID> [archived]
|
||||
gam delete courses <CourseEntity> [archived]
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
- [Bulk membership changes](#bulk-membership-changes)
|
||||
- [Display course membership](#display-course-membership)
|
||||
- [Display course membership counts](#display-course-membership-counts)
|
||||
- [Display course counts for teachers-students](#display-course-counts-for-teachers-students)
|
||||
|
||||
## API documentation
|
||||
* [Google Classroom API](https://developers.google.com/classroom/reference/rest)
|
||||
@@ -167,3 +168,46 @@ $count = & gam print course-participants teacher asmith states active show stude
|
||||
Windows Command Prompt
|
||||
for /f "delims=" %a in ('gam print course-participants teacher asmith states active show students showitemcountonly') do set count=%a
|
||||
```
|
||||
|
||||
## Display course counts for teachers-students
|
||||
You can get a count of the number of courses in which a teacher or student is a participant.
|
||||
```
|
||||
gam print course-counts students|teachers [todrive <ToDriveAttribute>*]
|
||||
(course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>])
|
||||
[mincount <Integer>]
|
||||
[formatjson [quotechar <Character>]]
|
||||
```
|
||||
By default, the `print course-counts` command displays participant counts about all courses.
|
||||
|
||||
To get participant counts for a specific set of courses, use the following option; it can be repeated to select multiple courses.
|
||||
* `(course|class <CourseID>)*` - Display courses with the specified `<CourseID>`.
|
||||
|
||||
To get participant counts for courses based on their having a particular participant, use the following options. Both options can be specified.
|
||||
* `teacher <UserItem>` - Display courses with the specified teacher.
|
||||
* `student <UserItem>` - Display courses with the specified student.
|
||||
|
||||
To get participant counts for courses based on their state, use the following option. This option can be combined with the `teacher` and `student` options.
|
||||
By default, all course states are selected.
|
||||
* `states <CourseStateList>` - Display courses with any of the specified states.
|
||||
|
||||
By default, all count values are displayed, use `mincount <Integer>` to limit the display to those counts
|
||||
greater that or equal to the specified `<Integer>`.
|
||||
|
||||
By default, Gam displays the counts as columns of fields; the following option causes the output to be in JSON format,
|
||||
* `formatjson` - Display the fields in JSON format.
|
||||
|
||||
By default, when writing CSV files, Gam uses a quote character of double quote `"`. The quote character is used to enclose columns that contain
|
||||
the quote character itself, the column delimiter (comma by default) and new-line characters. Any quote characters within the column are doubled.
|
||||
When using the `formatjson` option, double quotes are used extensively in the data resulting in hard to read/process output.
|
||||
The `quotechar <Character>` option allows you to choose an alternate quote character, single quote for instance, that makes for readable/processable output.
|
||||
`quotechar` defaults to `gam.cfg/csv_output_quote_char`. When uploading CSV files to Google, double quote `"` should be used.
|
||||
|
||||
### Example
|
||||
For teachers in all active courses, print the number of classes for which they are a participant.
|
||||
```
|
||||
gam print coursecounts teachers states active
|
||||
```
|
||||
For teachers in all active courses, print the number of classes (if it is >= 5) for which they are a participant.
|
||||
```
|
||||
gam print coursecounts teachers states active mincount 5
|
||||
```
|
||||
|
||||
@@ -10,9 +10,84 @@ Add the `-s` option to the end of the above commands to suppress creating the `g
|
||||
|
||||
See [Downloads-Installs-GAM7](https://github.com/GAM-team/GAM/wiki/Downloads-Installs) for Windows or other options, including manual installation
|
||||
|
||||
### 7.29.03
|
||||
|
||||
Remove debugging message from `gam <UserTypeEntity> move drivefile <DriveFileEntity>`.
|
||||
|
||||
### 7.29.02
|
||||
|
||||
Fixed bug in `gam <UserTypeEntity> move drivefile <DriveFileEntity>` where the following options
|
||||
were only applied to top level files or folders re-created in the destination. Now, domain
|
||||
and email address mappings apply to all moved files/folders.
|
||||
```
|
||||
excludepermissionsfromdomains <DomainNameList>
|
||||
includepermissionsfromdomains <DomainNameList>
|
||||
mappermissionsdomain <DomainName> <DomainName>
|
||||
mappermissionsemail <EmailAddress> <EmailAddress>
|
||||
mappermissionsemailfile <CSVFileInput> endcsv
|
||||
```
|
||||
|
||||
Upgraded to Python 3.14.1.
|
||||
|
||||
### 7.29.01
|
||||
|
||||
Added option `oneitemperrow` to `gam <UserTypeEntity> print calendars ... permissions` to have each of a
|
||||
calendar's permissions displayed on a separate row with all of the other calendar fields.
|
||||
|
||||
Updated `gam yubikey reset_piv` to handle YubiKey firmware updates that caused an error.
|
||||
|
||||
### 7.29.00
|
||||
|
||||
Added options `mappermissionsemail <EmailAddress> <EmailAddress>` and ` mappermissionsemailfile <CSVFileInput> endcsv`
|
||||
to these commands:
|
||||
```
|
||||
gam [<UserTypeEntity>] copy shareddriveacls <SharedDriveEntity> to <SharedDriveEntity>
|
||||
gam [<UserTypeEntity>] sync shareddriveacls <SharedDriveEntity> with <SharedDriveEntity>
|
||||
gam <UserTypeEntity> copy drivefile <DriveFileEntity>
|
||||
gam <UserTypeEntity> move drivefile <DriveFileEntity>
|
||||
```
|
||||
When `mappermissionsemail <EmailAddress> <EmailAddress>` is specifed, an ACL that references the first `<EmailAddress>`
|
||||
in the source will be modified to reference the second `<EmailAddress>` in the destination.
|
||||
|
||||
Bulk permission email address mapping can be specified with `mappermissionsemailfile <CSVFileInput> endcsv`.
|
||||
`<CSVFileInput>` must include these columns: `sourceEmail` and `destinationEmail`.
|
||||
|
||||
These options will be most useful with inter-workspace Shared Drive copies and moves.
|
||||
|
||||
### 7.28.13
|
||||
|
||||
Added option `addcsvdata <FieldName> <String>` to `gam <UserTypeEntity> print messages`
|
||||
that adds additional columns of data to the CSV file output.
|
||||
|
||||
### 7.28.12
|
||||
|
||||
Updated `gam delete project` to handle the following error:
|
||||
```
|
||||
ERROR: 400: failedPrecondition - Project not active
|
||||
```
|
||||
|
||||
### 7.28.11
|
||||
|
||||
Removed all options/fields referencing inheritance from `gam create|update|info|print org` as this option/field is deprecated.
|
||||
|
||||
### 7.28.10
|
||||
|
||||
Added a command `gam print course-counts` that dsplays the count of the number of courses in which a teacher or student is a participant.
|
||||
|
||||
* See: https://github.com/GAM-team/GAM/wiki/Classroom-Membership#display-course-counts-for-teachers-students
|
||||
|
||||
### 7.28.09
|
||||
|
||||
Fixed bug in `gam print cigroups ... descriptionmatchpattern [not] <REMatchPattern>` that caused a trap.
|
||||
|
||||
### 7.28.08
|
||||
|
||||
Updated `gam <UserTypeEntity> print|show chatmessages` to cache the sender UID to email address
|
||||
map so that each sender UID only has to be looked up once; this improves performance.
|
||||
|
||||
### 7.28.07
|
||||
|
||||
Fixed bug in 'gam report users ... aggregatebydate|aggregatebyuser` where `accounts:used_quota_in_percentage` was incorrectly displayed.
|
||||
Fixed bug in `gam report users ... aggregatebydate|aggregatebyuser` where `accounts:used_quota_in_percentage` was incorrectly displayed.
|
||||
|
||||
### 7.28.06
|
||||
|
||||
|
||||
@@ -252,9 +252,9 @@ writes the credentials into the file oauth2.txt.
|
||||
admin@server:/Users/admin$ rm -f /Users/admin/GAMConfig/oauth2.txt
|
||||
admin@server:/Users/admin$ gam version
|
||||
WARNING: Config File: /Users/admin/GAMConfig/gam.cfg, Section: DEFAULT, Item: oauth2_txt, Value: /Users/admin/GAMConfig/oauth2.txt, Not Found
|
||||
GAM 7.28.07 - https://github.com/GAM-team/GAM - pyinstaller
|
||||
GAM 7.29.03 - https://github.com/GAM-team/GAM - pyinstaller
|
||||
GAM Team <google-apps-manager@googlegroups.com>
|
||||
Python 3.14.0 64-bit final
|
||||
Python 3.14.1 64-bit final
|
||||
macOS Tahoe 26.1 x86_64
|
||||
Path: /Users/admin/bin/gam7
|
||||
Config File: /Users/admin/GAMConfig/gam.cfg, Section: DEFAULT, customer_id: my_customer, domain: domain.com
|
||||
@@ -990,9 +990,9 @@ writes the credentials into the file oauth2.txt.
|
||||
C:\>del C:\GAMConfig\oauth2.txt
|
||||
C:\>gam version
|
||||
WARNING: Config File: C:\GAMConfig\gam.cfg, Section: DEFAULT, Item: oauth2_txt, Value: C:\GAMConfig\oauth2.txt, Not Found
|
||||
GAM 7.28.07 - https://github.com/GAM-team/GAM - pythonsource
|
||||
GAM 7.29.03 - https://github.com/GAM-team/GAM - pythonsource
|
||||
GAM Team <google-apps-manager@googlegroups.com>
|
||||
Python 3.14.0 64-bit final
|
||||
Python 3.14.1 64-bit final
|
||||
Windows 11 10.0.26200 AMD64
|
||||
Path: C:\GAM7
|
||||
Config File: C:\GAMConfig\gam.cfg, Section: DEFAULT, customer_id: my_customer, domain: domain.com
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
<OrgUnitFieldName> ::=
|
||||
description|
|
||||
id|orgunitid|
|
||||
inherit|blockinheritance|
|
||||
name|
|
||||
parentid|parentorgunitid|
|
||||
parent|parentorgunitpath|
|
||||
@@ -73,18 +72,15 @@ For quoting rules, see: [List Quoting Rules](Command-Line-Parsing)
|
||||
Create, update and delete organization units.
|
||||
```
|
||||
gam create org|ou <OrgUnitPath> [description <String>]
|
||||
[parent <OrgUnitItem>] [inherit|(blockinheritance False)]
|
||||
[parent <OrgUnitItem>]
|
||||
[buildpath]
|
||||
gam update org|ou <OrgUnitPath> [name <String>] [description <String>]
|
||||
[parent <OrgUnitItem>] [inherit|(blockinheritance False)]
|
||||
[parent <OrgUnitItem>]
|
||||
gam delete org|ou <OrgUnitPath>
|
||||
gam update orgs|ous <OrgUnitEntity> [name <String>] [description <String>]
|
||||
[parent <OrgUnitItem>] [inherit|(blockinheritance False)]
|
||||
[parent <OrgUnitItem>]
|
||||
gam delete orgs|ous <OrgUnitEntity>
|
||||
```
|
||||
Inheritance specifies whether sub-OUs of the specified OU inherit its settings.
|
||||
* `inherit|blockinheritance false` - Sub-OUs inherit settings from the specified OU; this is the default
|
||||
|
||||
## Add users to an organizational unit
|
||||
When adding users to an OU, Gam uses a batch method to speed up processing.
|
||||
|
||||
|
||||
@@ -14,5 +14,10 @@ Thank you.
|
||||
* Korey Rideout - https://chatgpt.com/g/g-PTxxnVPMG-gam-assist-now-turbocharged-with-gam7
|
||||
* Paul Ogier (Taming.Tech) - GAM7 Course on Udemy https://taming.tech/GAMCourse
|
||||
* Paul Ogier (Taming.Tech) - GAM7 Tutorials https://www.youtube.com/watch?v=g9LDeyXQNLI&list=PL_dLiK09pJVhKJxZHNk9CHK0q5hkZ856w
|
||||
* Paul Ogier (Taming.Tech) - Installation videos
|
||||
* GAM7 Windows Install - https://www.youtube.com/watch?v=l8pTF5UWz7o&list=PL_dLiK09pJVhKJxZHNk9CHK0q5hkZ856w&index=4
|
||||
* GAM7 macOS Install - https://www.youtube.com/watch?v=eaLWpxhC91w&list=PL_dLiK09pJVhKJxZHNk9CHK0q5hkZ856w&index=5
|
||||
* GAM7 Ubuntu Linux Install - https://www.youtube.com/watch?v=zBwhNTxGlcM&list=PL_dLiK09pJVhKJxZHNk9CHK0q5hkZ856w&index=7
|
||||
* GAM7 ChromeOS Install - https://www.youtube.com/watch?v=BNWQh8GqvgY&list=PL_dLiK09pJVhKJxZHNk9CHK0q5hkZ856w&index=6
|
||||
* Paul Ogier (Taming.Tech) - https://taming.tech/taming-gam-a-practical-guide-to-gam-and-gamadv-xtd3/
|
||||
* Steve Larsen - https://docs.google.com/spreadsheets/d/1MzzA-u-cmoQcJnQOovCnZcEKMjvOyFhfkdFdf10X_GI/edit
|
||||
|
||||
@@ -52,11 +52,12 @@ config csv_output_row_filter "'\"accounts:used_quota_in_mb\":count>15000'"
|
||||
chrome|
|
||||
classroom|
|
||||
contextawareaccess|
|
||||
gplus|currents|google+|
|
||||
datastudio|
|
||||
drive|doc|docs|
|
||||
gcp|cloud|
|
||||
geminiinworkspaceapps|gemini|geminiforworkspace|
|
||||
gmail|
|
||||
gplus|currents|google+|
|
||||
groups|group|
|
||||
groupsenterprise|enterprisegroups|
|
||||
jamboard|
|
||||
|
||||
@@ -80,6 +80,15 @@
|
||||
<ColorValue> ::= <ColorName>|<ColorHex>
|
||||
```
|
||||
```
|
||||
<CSVFileInput> ::=
|
||||
((<FileName> [charset <Charset>] )|
|
||||
(gsheet <UserGoogleSheet>)|
|
||||
(gdoc <UserGoogleDoc>)|
|
||||
(gcscsv <StorageBucketObjectName>)|
|
||||
(gcsdoc <StorageBucketObjectName>))
|
||||
[warnifnodata] [columndelimiter <Character>] [noescapechar <Boolean>] [quotechar <Character>]
|
||||
[endcsv|(fields <FieldNameList>)]
|
||||
|
||||
<JSONData> ::= (json [charset <Charset>] <String>) | (json file <FileName> [charset <Charset>]) |
|
||||
|
||||
<OrganizerType> ::= user|group
|
||||
@@ -94,7 +103,8 @@
|
||||
<REMatchPattern> ::= <RegularExpression>
|
||||
<RESearchPattern> ::= <RegularExpression>
|
||||
<RESubstitution> ::= <String>>
|
||||
|
||||
```
|
||||
```
|
||||
<DriveFileOrderByFieldName> ::=
|
||||
createddate|createdtime|
|
||||
folder|
|
||||
@@ -649,15 +659,23 @@ When deleting permissions from JSON data, permissions with role `owner` true are
|
||||
These commands are used to transfer ACLs from one Shared Drive to another.
|
||||
* `copy` - Copy all ACLs from the source Shared Drive to the target Shared Drive. The role of an existing ACL in the target Shared Drive will never be reduced.
|
||||
* `sync` - Add/delete/update ACLs in the target Shared Drive to match those in the source Shared Drive.
|
||||
|
||||
* `<UserTypeEntity>` - Not Specified
|
||||
* All Shared Drives must be in the same workspace as the admin in `oauth2.txt`.
|
||||
* `<UserTypeEntity>` - Specified
|
||||
* `adminaccess|asadmin` specified - All Shared Drives must be in the same workspace as `<UserTypeEntity>`.
|
||||
* `adminaccess|asadmin` not specified - Shared Drives can be in separate workspaces if `<UserTypeEntity>` in a manager of all of them.
|
||||
```
|
||||
gam [<UserTypeEntity>] copy shareddriveacls <SharedDriveEntity> to <SharedDriveEntity>
|
||||
[showpermissionsmessages [<Boolean>]]
|
||||
[excludepermissionsfromdomains|includepermissionsfromdomains <DomainNameList>]
|
||||
(mappermissionsemail <EmailAddress> <EmailAddress)* [mappermissionsemailfile <CSVFileInput> endcsv]
|
||||
(mappermissionsdomain <DomainName> <DomainName>)*
|
||||
[adminaccess|asadmin]
|
||||
gam [<UserTypeEntity>] sync shareddriveacls <SharedDriveEntity> with <SharedDriveEntity>
|
||||
[showpermissionsmessages [<Boolean>]]
|
||||
[excludepermissionsfromdomains|includepermissionsfromdomains <DomainNameList>]
|
||||
(mappermissionsemail <EmailAddress> <EmailAddress)* [mappermissionsemailfile <CSVFileInput> endcsv]
|
||||
(mappermissionsdomain <DomainName> <DomainName>)*
|
||||
[adminaccess|asadmin]
|
||||
```
|
||||
@@ -665,8 +683,14 @@ When `excludepermissionsfromdomains <DomainNameList>` is specified, any ACL that
|
||||
|
||||
When `includepermissionsfromdomains <DomainNameList>` is specified, only ACLs that reference a domain in `<DomainNameList>` will be copied.
|
||||
|
||||
When `mappermissionsemail <EmailAddress> <EmailAddress>` is specifed, an ACL that references the first `<EmailAddress>` will be modified
|
||||
to reference the second `<EmailAddress>` when copied; the original ACL is not modified. The option can be repeated if multiple email addresses are to be mapped.
|
||||
|
||||
Bulk permission email address mapping can be specified with `mappermissionsemailfile <CSVFileInput> endcsv`.
|
||||
`<CSVFileInput>` must include these columns: `sourceEmail` and `destinationEmail`.
|
||||
|
||||
When `mappermissionsdomain <DomainName> <DomainName>` is specifed, any ACL that references the first `<DomainName>` will be modified
|
||||
to reference the second `<DonainName>` when copied; the original ACL is not modified. The option can be repeated if multiple domain names are to me mapped.
|
||||
to reference the second `<DomainName>` when copied; the original ACL is not modified. The option can be repeated if multiple domain names are to be mapped.
|
||||
|
||||
## Display Shared Drive access
|
||||
|
||||
|
||||
@@ -718,7 +718,7 @@ properly processed.
|
||||
```
|
||||
gam <UserTypeEntity> update calattendees <UserCalendarEntity> <EventEntity> [anyorganizer]
|
||||
[<EventNotificationAttribute>] [splitupdate] [dryrun|doit]
|
||||
(csv|csvfile <CSVFileInput>)
|
||||
(csv|csvfile <CSVFileInput> endcsv)
|
||||
(delete <EmailAddress>)*
|
||||
(deleteentity <EmailAddressEntity>)*
|
||||
(add <EmailAddress>)*
|
||||
|
||||
@@ -288,11 +288,10 @@ By default, Gam displays the information as an indented list of keys and values.
|
||||
gam <UserTypeEntity> print calendars [todrive <ToDriveAttribute>*]
|
||||
[primary] <CalendarSelectProperty>*
|
||||
[noprimary] [nogroups] [noresources] [nosystem] [nousers]
|
||||
[fields <CalendarListFieldList>] [permissions]
|
||||
[fields <CalendarListFieldList>] [permissions] [oneitemperrow]
|
||||
[formatjson] [delimiter <Character>] [quotechar <Character>]
|
||||
```
|
||||
By default, information for all visible, non-deleted calendars is shown.
|
||||
* `permissions` adds permission information for user owned calendars to the output.
|
||||
* `primary` - Limits the selection to the user's primary calendar
|
||||
* `<CalendarSelectProperty>`
|
||||
* `minaccessrole <CalendarACLRole>`- Limits the selection to those calendars where the user's role is at least `<CalendarACLRole>`
|
||||
@@ -303,6 +302,8 @@ By default, information for all visible, non-deleted calendars is shown.
|
||||
* `noresources` - Do not display resource calendars, email address ends in "@resource.calendar.google.com"
|
||||
* `nosystem` - Do not display system calendars, email address ends in "@group.v.calendar.google.com"
|
||||
* `nousers` - Do not display users calendars, email address ends in `domain` value from `gam.cfg`.
|
||||
* `permissions` Adds permission information for user owned calendars to the output.
|
||||
* `oneitemperrow` - Each permission is output on a separate row with all of the other calendar fields.
|
||||
|
||||
By default, list items are separated by the `csv_output_field_delimiter' from `gam.cfg`.
|
||||
* `delimiter <Character>` - Separate list items with `<Character>`
|
||||
|
||||
@@ -26,6 +26,15 @@
|
||||
* [`<UserTypeEntity>`](Collections-of-Users)
|
||||
|
||||
```
|
||||
<CSVFileInput> ::=
|
||||
((<FileName> [charset <Charset>] )|
|
||||
(gsheet <UserGoogleSheet>)|
|
||||
(gdoc <UserGoogleDoc>)|
|
||||
(gcscsv <StorageBucketObjectName>)|
|
||||
(gcsdoc <StorageBucketObjectName>))
|
||||
[warnifnodata] [columndelimiter <Character>] [noescapechar <Boolean>] [quotechar <Character>]
|
||||
[endcsv|(fields <FieldNameList>)]
|
||||
|
||||
<RegularExpression> ::= <String>
|
||||
See: https://docs.python.org/3/library/re.html
|
||||
<REMatchPattern> ::= <RegularExpression>
|
||||
@@ -38,7 +47,8 @@
|
||||
<EmailAddress> ::= <String>@<DomainName>
|
||||
<UniqueID> ::= id:<String>
|
||||
<UserItem> ::= <EmailAddress>|<UniqueID>|<String>
|
||||
|
||||
```
|
||||
```
|
||||
<DriveFileACLRole> ::=
|
||||
manager|organizer|owner|
|
||||
contentmanager|fileorganizer|
|
||||
@@ -114,6 +124,7 @@ gam <UserTypeEntity> copy drivefile <DriveFileEntity>
|
||||
[copysheetprotectedrangesinheritedpermissions [<Boolean>]]
|
||||
[copysheetprotectedrangesnoninheritedpermissions [<Boolean>]]
|
||||
[excludepermissionsfromdomains|includepermissionsfromdomains <DomainNameList>]
|
||||
(mappermissionsemail <EmailAddress> <EmailAddress)* [mappermissionsemailfile <CSVFileInput> endcsv]
|
||||
(mappermissionsdomain <DomainName> <DomainName>)*
|
||||
[sendemailifrequired [<Boolean>]]
|
||||
[verifyorganizer [<Boolean>]]
|
||||
@@ -313,8 +324,14 @@ When `excludepermissionsfromdomains <DomainNameList>` is specified, any ACL that
|
||||
|
||||
When `includepermissionsfromdomains <DomainNameList>` is specified, only ACLs that reference a domain in `<DomainNameList>` will be copied.
|
||||
|
||||
When `mappermissionsemail <EmailAddress> <EmailAddress>` is specifed, an ACL that references the first `<EmailAddress>` will be modified
|
||||
to reference the second `<EmailAddress>` when copied; the original ACL is not modified. The option can be repeated if multiple email addresses are to be mapped.
|
||||
|
||||
Bulk permission email address mapping can be specified with `mappermissionsemailfile <CSVFileInput> endcsv`.
|
||||
`<CSVFileInput>` must include these columns: `sourceEmail` and `destinationEmail`.
|
||||
|
||||
When `mappermissionsdomain <DomainName> <DomainName>` is specified, any ACL that references the first `<DomainName>` will be modified
|
||||
to reference the second `<DomainName>` when copied; the original ACL is not modified. The option can be repeated if multiple domain names are to me mapped.
|
||||
to reference the second `<DomainName>` when copied; the original ACL is not modified. The option can be repeated if multiple domain names are to be mapped.
|
||||
|
||||
When copying an ACL that references a non Google account, an error is generated unless an email is sent to the account;
|
||||
by default, no email notifications are sent. The `sendemailifrequired` options instructs GAM to send an email notification in this case.
|
||||
@@ -542,8 +559,8 @@ gam <UserTypeEntity> move drivefile <DriveFileEntity> [newfilename <DriveFileNam
|
||||
[copysubfolderinheritedpermissions [<Boolean>]]
|
||||
[copysubfoldernoninheritedpermissions never|always|syncallfolders|syncupdatedfolders]
|
||||
[excludepermissionsfromdomains|includepermissionsfromdomains <DomainNameList>]
|
||||
(mappermissionsemail <EmailAddress> <EmailAddress)* [mappermissionsemailfile <CSVFileInput> endcsv]
|
||||
(mappermissionsdomain <DomainName> <DomainName>)*
|
||||
[updatefilepermissions [<Boolean>]]
|
||||
[retainsourcefolders [<Boolean>]]
|
||||
[sendemailifrequired [<Boolean>]]
|
||||
[verifyorganizer [<Boolean>]]
|
||||
@@ -666,8 +683,14 @@ When `excludepermissionsfromdomains <DomainNameList>` is specified, any ACL that
|
||||
|
||||
When `includepermissionsfromdomains <DomainNameList>` is specified, only ACLs that reference a domain in `<DomainNameList>` will be copied.
|
||||
|
||||
When `mappermissionsemail <EmailAddress> <EmailAddress>` is specifed, an ACL that references the first `<EmailAddress>` will be modified
|
||||
to reference the second `<EmailAddress>` when copied; the original ACL is not modified. The option can be repeated if multiple email addresses are to be mapped.
|
||||
|
||||
Bulk permission email address mapping can ge specified with `mappermissionsemailfile <CSVFileInput> endcsv`.
|
||||
`<CSVFileInput>` must include these columns: `sourceEmail` and `destinationEmail`.
|
||||
|
||||
When `mappermissionsdomain <DomainName> <DomainName>` is specified, any ACL that references the first `<DomainName>` will be modified
|
||||
to reference the second `<DomainName>` when copied; the original ACL is not modified. The option can be repeated if multiple domain names are to me mapped.
|
||||
to reference the second `<DomainName>` when copied; the original ACL is not modified. The option can be repeated if multiple domain names are to be mapped.
|
||||
|
||||
When copying an ACL that references a non Google account, an error is generated unless an email is sent to the account;
|
||||
by default, no email notifications are sent. The `sendemailifrequired` options instructs GAM to send an email notification in this case.
|
||||
@@ -684,8 +707,14 @@ When `excludepermissionsfromdomains <DomainNameList>` is specified, any ACL that
|
||||
|
||||
When `includepermissionsfromdomains <DomainNameList>` is specified, any ACLs that references a domain not in `<DomainNameList>` will be removed.
|
||||
|
||||
When `mappermissionsemail <EmailAddress> <EmailAddress>` is specifed, an ACL that references the first `<EmailAddress>` will be removed;
|
||||
a new ACL with the same properties referencing the second `<EmailAddess>` will be created. The option can be repeated if multiple domain names are to be mapped.
|
||||
|
||||
Bulk email address mapping can ge specified with `mappermissionsemailfile <CSVFileInput> endcsv`.
|
||||
`<CSVFileInput>` must include these columns: `sourceEmail` and `destinationEmail`.
|
||||
|
||||
When `mappermissionsdomain <DomainName> <DomainName>` is specified, any ACL that references the first `<DomainName>` will be removed;
|
||||
a new ACL with the same properties referencing the second `<DomainName>` will be created. The option can be repeated if multiple domain names are to me mapped.
|
||||
a new ACL with the same properties referencing the second `<DomainName>` will be created. The option can be repeated if multiple domain names are to be mapped.
|
||||
|
||||
When creating an ACL that references a non Google account, an error is generated unless an email is sent to the account;
|
||||
by default, no email notifications are sent. The `sendemailifrequired` options instructs GAM to send an email notification in this case.
|
||||
|
||||
@@ -234,7 +234,6 @@ The option `updatesheetprotectedranges` only applies to items in `<DriveFileEnti
|
||||
* ACLs with role reader or commenter will be removed from existing protected ranges
|
||||
* ACLs with role writer or higher will be added to existing protected ranges
|
||||
|
||||
`
|
||||
`enforceexpansiveaccess` defaults to the value of `gam.cfg/enforce_expansive_access` that controls
|
||||
the ability to update inherited ACLs.
|
||||
* False - Inherited ACLs can be updated
|
||||
@@ -261,7 +260,7 @@ The option `updatesheetprotectedranges` only applies to items in `<DriveFileEnti
|
||||
* ACLs with any role will be removed from existing protected ranges
|
||||
|
||||
`enforceexpansiveaccess` defaults to the value of `gam.cfg/enforce_expansive_access` that controls
|
||||
the ability to delete delete inherited ACLs.
|
||||
the ability to delete inherited ACLs.
|
||||
* False - Inherited ACLs can be deleted
|
||||
* True = Inherited ACLs can not be deleted
|
||||
|
||||
@@ -308,7 +307,7 @@ gam <UserTypeEntity> delete permissions <DriveFileEntity> <DriveFilePermissionID
|
||||
[enforceexpansiveaccess [<Boolean>]]
|
||||
```
|
||||
`enforceexpansiveaccess` defaults to the value of `gam.cfg/enforce_expansive_access` that controls
|
||||
the ability to delete delete inherited ACLs.
|
||||
the ability to delete inherited ACLs.
|
||||
* False - Inherited ACLs can be deleted
|
||||
* True = Inherited ACLs can not be deleted
|
||||
|
||||
|
||||
@@ -589,9 +589,9 @@ gam <UserTypeEntity> show messages|threads
|
||||
[labelids <LabelIDList>]
|
||||
[quick|notquick] [max_to_show <Number>] [includespamtrash])|(ids <MessageIDEntity>)
|
||||
[labelmatchpattern <REMatchPattern>] [sendermatchpattern <REMatchPattern>]
|
||||
[countsonly|positivecountsonly] [useronly]
|
||||
[countsonly|positivecountsonly]
|
||||
[headers all|<SMTPHeaderList>] [dateheaderformat iso|rfc2822|<String>] [dateheaderconverttimezone [<Boolean>]]
|
||||
[showlabels] [delimiter <Character>] [showbody] [showhtml] [showdate] [showsize] [showsnippet]
|
||||
[showlabels] [useronly] [delimiter <Character>] [showbody] [showhtml] [showdate] [showsize] [showsnippet]
|
||||
[maxmessagesperthread <Number>]
|
||||
[showattachments [attachmentnamepattern <REMatchPattern>>] [noshowtextplain]]
|
||||
[saveattachments [attachmentnamepattern <REMatchPattern>>]]
|
||||
@@ -601,12 +601,13 @@ gam <UserTypeEntity> print messages|threads [todrive <ToDriveAttribute>*]
|
||||
[labelids <LabelIDList>]
|
||||
[quick|notquick] [max_to_print <Number>] [includespamtrash])|(ids <MessageIDEntity>)
|
||||
[labelmatchpattern <REMatchPattern>] [sendermatchpattern <REMatchPattern>]
|
||||
[countsonly|positivecountsonly] [useronly]
|
||||
[countsonly|positivecountsonly]
|
||||
[headers all|<SMTPHeaderList>] [dateheaderformat iso|rfc2822|<String>] [dateheaderconverttimezone [<Boolean>]]
|
||||
[showlabels] [delimiter <Character>] [showbody] [showhtml] [showdate] [showsize] [showsnippet]
|
||||
[showlabels] [useronly] [delimiter <Character>] [showbody] [showhtml] [showdate] [showsize] [showsnippet]
|
||||
[maxmessagesperthread <Number>]
|
||||
[showattachments [attachmentnamepattern <REMatchPattern>>]]
|
||||
[convertcrnl]
|
||||
(addcsvdata <FieldName> <String>)*
|
||||
```
|
||||
## Display all messages
|
||||
By default, Gam displays all messages.
|
||||
@@ -692,6 +693,7 @@ The `dateheaderconverttimezone [<Boolean>]>` option converts `<SMTPDateHeader>`
|
||||
## Print only options
|
||||
These options are valid with `print`.
|
||||
* `convertcrnl` - In the message body, convert carriage returns to `\r` and newlines to `\n`; the default value is `csv_output_convert_cr_nl` from `gam.cfg`.
|
||||
* `addcsvdata <FieldName> <String>` - Add additional columns of data from the command line to the output
|
||||
|
||||
By default, message attachment information is not displayed.
|
||||
* `showattachments` - Display attachment filename, MIME type and size
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
- [Display Shared Drive Counts](#display-shared-drive-counts)
|
||||
- [Display Shared Drive Organizers](#display-shared-drive-organizers)
|
||||
- [Manage Shared Drive access](#manage-shared-drive-access)
|
||||
- [Transfer Shared Drive access](#transfer-shared-drive-access)
|
||||
- [Display Shared Drive access](#display-shared-drive-access)
|
||||
- [Display Shared Drive access for specific Shared Drives](#display-shared-drive-access-for-specific-shared-drives)
|
||||
- [Display Shared Drive access for selected Shared Drives](#display-shared-drive-access-for-selected-shared-drives)
|
||||
@@ -72,6 +73,15 @@
|
||||
<ColorValue> ::= <ColorName>|<ColorHex>
|
||||
```
|
||||
```
|
||||
<CSVFileInput> ::=
|
||||
((<FileName> [charset <Charset>] )|
|
||||
(gsheet <UserGoogleSheet>)|
|
||||
(gdoc <UserGoogleDoc>)|
|
||||
(gcscsv <StorageBucketObjectName>)|
|
||||
(gcsdoc <StorageBucketObjectName>))
|
||||
[warnifnodata] [columndelimiter <Character>] [noescapechar <Boolean>] [quotechar <Character>]
|
||||
[endcsv|(fields <FieldNameList>)]
|
||||
|
||||
<JSONData> ::= (json [charset <Charset>] <String>) | (json file <FileName> [charset <Charset>]) |
|
||||
|
||||
<OrganizerType> ::= user|group
|
||||
@@ -86,7 +96,8 @@
|
||||
<REMatchPattern> ::= <RegularExpression>
|
||||
<RESearchPattern> ::= <RegularExpression>
|
||||
<RESubstitution> ::= <String>>
|
||||
|
||||
```
|
||||
```
|
||||
<DriveFileOrderByFieldName> ::=
|
||||
createddate|createdtime|
|
||||
folder|
|
||||
@@ -516,6 +527,40 @@ When adding permissions from JSON data, permissions with `deleted` true are neve
|
||||
|
||||
When deleting permissions from JSON data, permissions with role `owner` true are never processed.
|
||||
|
||||
## Transfer Shared Drive access
|
||||
These commands are used to transfer ACLs from one Shared Drive to another.
|
||||
* `copy` - Copy all ACLs from the source Shared Drive to the target Shared Drive. The role of an existing ACL in the target Shared Drive will never be reduced.
|
||||
* `sync` - Add/delete/update ACLs in the target Shared Drive to match those in the source Shared Drive.
|
||||
|
||||
* `adminaccess|asadmin` specified - All Shared Drives must be in the same workspace as `<UserTypeEntity>`.
|
||||
* `adminaccess|asadmin` not specified - Shared Drives can be in separate workspaces if `<UserTypeEntity>` in a manager of all of them.
|
||||
```
|
||||
gam <UserTypeEntity> copy shareddriveacls <SharedDriveEntity> to <SharedDriveEntity>
|
||||
[showpermissionsmessages [<Boolean>]]
|
||||
[excludepermissionsfromdomains|includepermissionsfromdomains <DomainNameList>]
|
||||
(mappermissionsemail <EmailAddress> <EmailAddress)* [mappermissionsemailfile <CSVFileInput> endcsv]
|
||||
(mappermissionsdomain <DomainName> <DomainName>)*
|
||||
[adminaccess|asadmin]
|
||||
gam <UserTypeEntity> sync shareddriveacls <SharedDriveEntity> with <SharedDriveEntity>
|
||||
[showpermissionsmessages [<Boolean>]]
|
||||
[excludepermissionsfromdomains|includepermissionsfromdomains <DomainNameList>]
|
||||
(mappermissionsemail <EmailAddress> <EmailAddress)* [mappermissionsemailfile <CSVFileInput> endcsv]
|
||||
(mappermissionsdomain <DomainName> <DomainName>)*
|
||||
[adminaccess|asadmin]
|
||||
```
|
||||
When `excludepermissionsfromdomains <DomainNameList>` is specified, any ACL that references a domain in `<DomainNameList>` will not be copied.
|
||||
|
||||
When `includepermissionsfromdomains <DomainNameList>` is specified, only ACLs that reference a domain in `<DomainNameList>` will be copied.
|
||||
|
||||
When `mappermissionsemail <EmailAddress> <EmailAddress>` is specifed, an ACL that references the first `<EmailAddress>` will be modified
|
||||
to reference the second `<EmailAddress>` when copied; the original ACL is not modified. The option can be repeated if multiple email addresses are to be mapped.
|
||||
|
||||
Bulk permission email address mapping can be specified with `mappermissionsemailfile <CSVFileInput> endcsv`.
|
||||
`<CSVFileInput>` must include these columns: `sourceEmail` and `destinationEmail`.
|
||||
|
||||
When `mappermissionsdomain <DomainName> <DomainName>` is specifed, any ACL that references the first `<DomainName>` will be modified
|
||||
to reference the second `<DomainName>` when copied; the original ACL is not modified. The option can be repeated if multiple domain names are to be mapped.
|
||||
|
||||
## Display Shared Drive access
|
||||
|
||||
These commands are used to display the ACLs on Shared Drives themselves, not the files/folders on the Shared Drives.
|
||||
|
||||
@@ -29,23 +29,32 @@ No, because the YubiKey generated the private key it cannot be digitally exporte
|
||||
When using domain-wide delegation with GAM7, the service account and anyone possessing the service account private key oauth2service.json file has access to the Gmail, Drive and Calendar data of ALL Workspace users in your domain. For this reason, whether using a YubiKey or not, you should take strong measures to protect the service account private key.
|
||||
|
||||
## Setup Steps
|
||||
1 .Upgrade to the [latest version of GAM7](https://github.com/GAM-team/GAM/wiki/How-to-Update-GAM7).
|
||||
2. **If you are using a new YubiKey or don't care about the PIV app data on the YubiKey**
|
||||
1. Tell GAM7 to reset and configure the PIV app data on the YubiKey. This wipes all existing keys and configuration and then configures a private key and PIN for GAM7.
|
||||
|
||||
1. Upgrade to the [latest version of GAM7](https://github.com/GAM-team/GAM/wiki/How-to-Update-GAM7).
|
||||
|
||||
2. If you are using a new YubiKey or don't care about the PIV app data on the YubiKey:
|
||||
|
||||
a. Tell GAM7 to reset and configure the PIV app data on the YubiKey. This wipes all existing keys and configuration and then configures a private key and PIN for GAM7.
|
||||
|
||||
* Single YubiKey - `gam yubikey reset_piv`
|
||||
|
||||
* Multiple YubiKeys - `gam yubikey reset_piv yubikeyserialnumber <Number>`
|
||||
2. During the PIV reset, GAM7 will print out a PIN for the private key, record this key.
|
||||
4. **If you are already using the YubiKey and wish to preserve the PIV app data and keys**
|
||||
1. You need to configure one of the PIV slots for a private key GAM7 can use.
|
||||
|
||||
b. During the PIV reset, GAM7 will print out a PIN for the private key, record this key.
|
||||
|
||||
3. OR if you are already using the YubiKey and wish to preserve the PIV app data and keys
|
||||
|
||||
a. You need to configure one of the PIV slots for a private key GAM7 can use.
|
||||
* [ykman piv keys generate](https://docs.yubico.com/software/yubikey/tools/ykman/PIV_Commands.html#ykman-piv-keys-options-command-args)
|
||||
`ykman piv keys generate -P <Text> --pin-policy ALWAYS --touch-policy NEVER --algorithm RSA2048 9a new_pubkey.txt`
|
||||
* Use `9a` for the `AUTHENTICATION` slot, `9c` for the `SIGNATURE` slot
|
||||
2. You need to generate a certificate for that slot.
|
||||
|
||||
b. You need to generate a certificate for that slot.
|
||||
* [ykman piv certificates generate](https://docs.yubico.com/software/yubikey/tools/ykman/PIV_Commands.html#ykman-piv-certificates-generate-options-slot-public-key)
|
||||
`ykman piv certificates generate -P <Text> --subject "GAM Service Account" -d 36500 9a new_pubkey.txt`
|
||||
* Use `9a` for the `AUTHENTICATION` slot, `9c` for the `SIGNATURE` slot
|
||||
|
||||
5. Now that you have a private key on your YubiKey, tell GAM7 to use that instead of the private_key stored in oauth2service.json. We can do that by rotating the key:
|
||||
4. Now that you have a private key on your YubiKey, tell GAM7 to use that instead of the private_key stored in oauth2service.json. We can do that by rotating the key:
|
||||
```
|
||||
copy oauth2service.json to oauth2service.save
|
||||
gam create sakey yubikey yubikey_pin yubikey_slot AUTHENTICATION|SIGNATURE
|
||||
@@ -58,10 +67,10 @@ copy oauth2service.json to oauth2service.yk
|
||||
copy oauth2service.save to oauth2service.json
|
||||
```
|
||||
|
||||
6. Now you should be able to run GAM7 commands like:
|
||||
5. Now you should be able to run GAM7 commands like:
|
||||
```
|
||||
gam user admin@example.com check serviceaccount
|
||||
```
|
||||
and see the YubiKey lights flash as the YubiKey interacts with GAM7 to sign the GAM7 authentication requests. If you look at the oauth2service.json file, you'll see it contains some new fields like yubikey_serial and yubikey_pin but no longer contains the private_key field where GAM7 would normally store the private key data.
|
||||
|
||||
7. As a last step, since YubiKey-stored private keys do not need to be and should not be rotated, you can remove the service account's permissions to change it's own key. Navigate to the [Cloud Console](https://console.cloud.google.com/iam-admin/serviceaccounts) select the correct project and service account and on the Permissions tab, edit and remove the "Service Account Key Admin" permission that the service account has to itself.
|
||||
6. As a last step, since YubiKey-stored private keys do not need to be and should not be rotated, you can remove the service account's permissions to change it's own key. Navigate to the [Cloud Console](https://console.cloud.google.com/iam-admin/serviceaccounts) select the correct project and service account and on the Permissions tab, edit and remove the "Service Account Key Admin" permission that the service account has to itself.
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
Print the current version of Gam with details
|
||||
```
|
||||
gam version
|
||||
GAM 7.28.07 - https://github.com/GAM-team/GAM - pyinstaller
|
||||
GAM 7.29.03 - https://github.com/GAM-team/GAM - pyinstaller
|
||||
GAM Team <google-apps-manager@googlegroups.com>
|
||||
Python 3.14.0 64-bit final
|
||||
Python 3.14.1 64-bit final
|
||||
macOS Tahoe 26.1 x86_64
|
||||
Path: /Users/Admin/bin/gam7
|
||||
Config File: /Users/admin/GAMConfig/gam.cfg, Section: DEFAULT, customer_id: my_customer, domain: domain.com
|
||||
@@ -15,9 +15,9 @@ Time: 2023-06-02T21:10:00-07:00
|
||||
Print the current version of Gam with details and time offset information
|
||||
```
|
||||
gam version timeoffset
|
||||
GAM 7.28.07 - https://github.com/GAM-team/GAM - pyinstaller
|
||||
GAM 7.29.03 - https://github.com/GAM-team/GAM - pyinstaller
|
||||
GAM Team <google-apps-manager@googlegroups.com>
|
||||
Python 3.14.0 64-bit final
|
||||
Python 3.14.1 64-bit final
|
||||
macOS Tahoe 26.1 x86_64
|
||||
Path: /Users/Admin/bin/gam7
|
||||
Config File: /Users/admin/GAMConfig/gam.cfg, Section: DEFAULT, customer_id: my_customer, domain: domain.com
|
||||
@@ -27,9 +27,9 @@ Your system time differs from www.googleapis.com by less than 1 second
|
||||
Print the current version of Gam with extended details and SSL information
|
||||
```
|
||||
gam version extended
|
||||
GAM 7.28.07 - https://github.com/GAM-team/GAM - pyinstaller
|
||||
GAM 7.29.03 - https://github.com/GAM-team/GAM - pyinstaller
|
||||
GAM Team <google-apps-manager@googlegroups.com>
|
||||
Python 3.14.0 64-bit final
|
||||
Python 3.14.1 64-bit final
|
||||
macOS Tahoe 26.1 x86_64
|
||||
Path: /Users/Admin/bin/gam7
|
||||
Config File: /Users/admin/GAMConfig/gam.cfg, Section: DEFAULT, customer_id: my_customer, domain: domain.com
|
||||
@@ -68,7 +68,7 @@ MacOS High Sierra 10.13.6 x86_64
|
||||
Path: /Users/Admin/bin/gam7
|
||||
Version Check:
|
||||
Current: 5.35.08
|
||||
Latest: 7.28.07
|
||||
Latest: 7.29.03
|
||||
echo $?
|
||||
1
|
||||
```
|
||||
@@ -76,7 +76,7 @@ echo $?
|
||||
Print the current version number without details
|
||||
```
|
||||
gam version simple
|
||||
7.28.07
|
||||
7.29.03
|
||||
```
|
||||
In Linux/MacOS you can do:
|
||||
```
|
||||
@@ -86,9 +86,9 @@ echo $VER
|
||||
Print the current version of Gam and address of this Wiki
|
||||
```
|
||||
gam help
|
||||
GAM 7.28.07 - https://github.com/GAM-team/GAM
|
||||
GAM 7.29.03 - https://github.com/GAM-team/GAM
|
||||
GAM Team <google-apps-manager@googlegroups.com>
|
||||
Python 3.14.0 64-bit final
|
||||
Python 3.14.1 64-bit final
|
||||
macOS Tahoe 26.1 x86_64
|
||||
Path: /Users/Admin/bin/gam7
|
||||
Config File: /Users/admin/GAMConfig/gam.cfg, Section: DEFAULT, customer_id: my_customer, domain: domain.com
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
- [Introduction](#introduction)
|
||||
- [Variables](#variables)
|
||||
- [Multiple Computers](#multiple-computers)
|
||||
- [Separate Staff-Student Domains](#separate-staff-student-domains)
|
||||
- [Multiple Customers and Domains](#multiple-customers-and-domains)
|
||||
- [Multiple Users-Projects on One Computer](#multiple-users-projects-on-one-computer)
|
||||
|
||||
@@ -564,10 +565,10 @@ timezone
|
||||
to your local timezone. If you are running GAM on a remote computer or on a
|
||||
cloud shell, "local" will mean the time at the remote/cloud shell computer,
|
||||
not your location, Use "+|-hh:mm" to specify the timezone at your location.
|
||||
Starting with version 7.21.00 you can use a timezone name.
|
||||
Starting with version 7.21.00 you can use a timezone name; the names are case sensitive.
|
||||
See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
Default: utc
|
||||
Range: utc|z|local|(+|-hh:mm)I<ValidTimezoneName>
|
||||
Range: utc|z|local|(+|-hh:mm)|<ValidTimezoneName>
|
||||
tls_max_version
|
||||
Allowed values: '', tlsv1_2, tlsv1.2, tlsv1_3, tlsv1.3
|
||||
The maximum TLS version to use in https connections
|
||||
@@ -621,7 +622,7 @@ todrive_timeformat
|
||||
Default: '' which selects an ISO format timestamp
|
||||
Example: %Y-%m-%dT%H:%M:%S will display as 2020-07-06T17:48:54
|
||||
todrive_timezone
|
||||
The Spreadsheet settings Timezone value.
|
||||
The Spreadsheet settings Timezone value; the values are case sensitive.
|
||||
See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
Default: ''
|
||||
todrive_upload_nodata
|
||||
@@ -816,6 +817,26 @@ Edit `gam.cfg` on the other computer
|
||||
* config_dir
|
||||
* drive_dir
|
||||
|
||||
## Separate Staff-Student Domains
|
||||
Suppose you have a single workspace with separate staff and student domains; e.g., school.org and students.school.org.
|
||||
You can simplifiy typing email addresses by making two sections.
|
||||
|
||||
```
|
||||
[DEFAULT]
|
||||
customer_id = C1234567
|
||||
[staff]
|
||||
domain = school.org
|
||||
[students]
|
||||
domain = students.school.org
|
||||
```
|
||||
To issue commands about staff users, do: `gam config staff save`
|
||||
Subsequent commands can omit `@school.org` from email addresses, GAM will supply `@school.org`.
|
||||
|
||||
To issue commands about student users, do: `gam config students save`
|
||||
Subsequent commands can omit `@students.school.org` from email addresses, GAM will supply `@students.school.org`.
|
||||
|
||||
You can always type a complete email address if desired.
|
||||
|
||||
## Multiple Customers and Domains
|
||||
There are four arguments to GAM that should simplify how you use GAM with multiple clients/domains.
|
||||
Each client/domain will have a section in gam.cfg that sets the values specific to it.
|
||||
|
||||
Reference in New Issue
Block a user