From effa972a4007f8065ecb56a7972aabce682cf0e9 Mon Sep 17 00:00:00 2001 From: Ross Scroggs Date: Wed, 13 Sep 2023 13:45:10 -0700 Subject: [PATCH] Updated print aliases|groups|group-members|users Added option `verifyorganizer []` to `gam copy|move drivefile` --- docs/Aliases.md | 27 +- docs/GamUpdates.md | 31 ++ docs/Groups-Membership.md | 17 +- docs/Groups.md | 12 +- docs/How-to-Upgrade-from-Standard-GAM.md | 8 +- docs/List-Items.md | 1 + docs/Users-Chat.md | 21 ++ docs/Users-Drive-Copy-Move.md | 10 + docs/Users.md | 23 +- docs/Version-and-Help.md | 12 +- docs/gam.cfg.md | 9 + src/GamCommands.txt | 18 +- src/GamUpdate.txt | 31 ++ src/gam/__init__.py | 365 +++++++++++++---------- src/gam/gamlib/glcfg.py | 4 + src/gam/gamlib/glglobals.py | 3 + 16 files changed, 389 insertions(+), 203 deletions(-) diff --git a/docs/Aliases.md b/docs/Aliases.md index 05827a62..47bd0718 100644 --- a/docs/Aliases.md +++ b/docs/Aliases.md @@ -21,8 +21,12 @@ * https://developers.google.com/admin-sdk/directory/v1/guides/search-users ## Definitions +See [Collections of Items](Collections-of-Items) ``` ::= (.)+ + ::= "(,)*" + ::= + | | ::= @ ::= "(,)*" ::= | | | @@ -84,7 +88,8 @@ gam info alias|aliases Display selected aliases. ``` gam print aliases [todrive *] - [domain ] [(query )|(queries )] + ([domain|domains ] [(query )|(queries )] + [limittoou ]) [user|users ] [group|groups ] [select ] [aliasmatchpattern ] @@ -94,8 +99,9 @@ gam print aliases [todrive *] (addcsvdata )* ``` By default, group and user aliases in all domains in the account are selected; these options allow selection of subsets of aliases: -* `domain ` - Limit aliases to those in `` -* `(query )|(queries )` - Print aliases for selected users +* `domain|domains ` - Limit aliases to those in the domains specified by `` +* `(query )|(queries )` - Print aliases for users/groups that match a query; each query is run against each domain +* `limittoou ` - Print aliases for users in the specified `` * `user|users ` - Print aliases for users in `` - Print aliases for users in `` * `group|groups ` - Print aliases for groups in `)|(queries )` and `aliasmatchpattern ` as desired. diff --git a/docs/GamUpdates.md b/docs/GamUpdates.md index 37672516..d0f14d92 100644 --- a/docs/GamUpdates.md +++ b/docs/GamUpdates.md @@ -10,6 +10,37 @@ Add the `-s` option to the end of the above commands to suppress creating the `g See [Downloads](https://github.com/taers232c/GAMADV-XTD3/wiki/Downloads) for Windows or other options, including manual installation. +### 6.63.14 + +Added option `verifyorganizer []` to `gam copy|move drivefile`. When a copy/move +operation involves a Shared Drive, GAM verifies that the user is an organizer. Unfortunatley, this fails +when the user is not a direct organizer but is a member of a group that is an organizer. Specifying +`verifyorganizer false` suppresses the verification. + +Updated the following commands to be able to specify a list of domains rather than a single domain: +``` +gam print aliases +gam print groups +gam print|show group-members +gam print users +``` +Added `print_agu_domains` variable to `gam.cfg` that provides a default list of domains for these commands. + +When multiple domains are specified and a query/queries are specified, an API call is made for each domain/query combination. +``` +$ gam print users domains school.org,students.school.org queries "'email:admin*','email:test*'" +Getting all Users that match query (domain=school.org, query="email:admin*"), may take some time on a large Google Workspace Account... +Got 3 Users: admin@school.org - admindirector@school.org +Getting all Users that match query (domain=school.org, query="email:test*"), may take some time on a large Google Workspace Account... +Got 20 Users: testusera@school.org - testuserx@school.org +Getting all Users that match query (domain=students.school.org, query="email:admin*"), may take some time on a large Google Workspace Account... +Got 1 User: admin@students.school.org - admin@students.school.org +Getting all Users that match query (domain=students.school.org, query="email:test*"), may take some time on a large Google Workspace Account... +Got 1 User: testuser1@students.school.org - testuser1@students.school.org +primaryEmail +... +``` + ### 6.63.13 Updated `gam print filelist ... showdrivename` and `gam show fileinfo ... showdrivename` diff --git a/docs/Groups-Membership.md b/docs/Groups-Membership.md index 57a087d3..be2f81d8 100644 --- a/docs/Groups-Membership.md +++ b/docs/Groups-Membership.md @@ -19,6 +19,7 @@ * https://developers.google.com/admin-sdk/directory/v1/reference/members ## Definitions +See [Collections of Items](Collections-of-Items) ``` ::= allmail| @@ -27,6 +28,9 @@ disabled| none|nomail ::= (.)+ + ::= "(,)*" + ::= + | | ::= @ ::= || ::= id: @@ -41,6 +45,7 @@ ::= "(,)*" ::= See: https://developers.google.com/admin-sdk/directory/v1/guides/search-groups + ::= "(,)*" ::= delivery|deliverysettings| @@ -563,7 +568,7 @@ gam info member|group-members | By default, delivery information is not displayed. ``` gam print group-members [todrive *] - [([domain ] ([member|showownedby ]|[query ]))| + [([domain|domains ] ([member|showownedby ]|[(query )|(queries )]))| (group|group_ns|group_susp )| (select )] [emailmatchpattern [not] ] [namematchpattern [not] ] @@ -581,10 +586,10 @@ gam print group-members [todrive *] [formatjson [quotechar ]] ``` By default, the group membership of all groups in the account are displayed, these options allow selection of subsets of groups: -* `domain ` - Limit display to groups in the domain `` +* `domain|domains ` - Limit display to groups in the domains specified by `` * `member ` - Limit display to groups that contain `` as a member; mutually exclusive with `query ` * `showownedby ` - Limit display to groups that contain `` as an owner; mutually exclusive with `query ` -* `query ` - Limit display to groups that match ``, matching is done at Google; mutually exclusive with `member ` +* `(query )|(queries )` - Limit groups to those that match a query; each query is run against each domain * `group ` - Limit display to the single group `` * `group_ns ` - Limit display to the single group ``, display non-suspended members * `group_susp ` - Limit display to the single group ``, display suspended members @@ -679,7 +684,7 @@ The `quotechar ` option allows you to choose an alternate quote chara ## Display group membership in hierarchical format ``` gam show group-members - [([domain ] ([member|showownedby ]|[query ]))| + [([domain|domains ] ([member|showownedby ]|[(query )|(queries )]))| (group|group_ns|group_susp )| (select )] [emailmatchpattern [not] ] [namematchpattern [not] ] @@ -692,10 +697,10 @@ gam show group-members [includederivedmembership] ``` By default, the group membership of all groups in the account are displayed, these options allow selection of subsets of groups: -* `domain ` - Limit display to groups in the domain `` +* `domain|domains ` - Limit display to groups in the domains specified by `` * `member ` - Limit display to groups that contain `` as a member; mutually exclusive with `query ` * `showownedby ` - Limit display to groups that contain `` as an owner; mutually exclusive with `query ` -* `query ` - Limit display to groups that match ``, matching is done at Google; mutually exclusive with `member ` +* `(query )|(queries )` - Limit groups to those that match a query; each query is run against each domain * `group ` - Limit display to the single group `` * `group_ns ` - Limit display to the single group ``, display non-suspended members * `group_susp ` - Limit display to the single group ``, display suspended members diff --git a/docs/Groups.md b/docs/Groups.md index 9ce75cd1..2693506f 100644 --- a/docs/Groups.md +++ b/docs/Groups.md @@ -44,8 +44,12 @@ * https://support.google.com/a/answer/167430 ## Definitions +See [Collections of Items](Collections-of-Items) ``` ::= (.)+ + ::= "(,)*" + ::= + | | ::= @ ::= id: ::= || @@ -60,6 +64,7 @@ ::= "(,)*" ::= See: https://developers.google.com/admin-sdk/directory/v1/guides/search-groups + ::= "(,)*" ::= See: https://cloud.google.com/identity/docs/reference/rest/v1/groups#dynamicgroupquery @@ -404,7 +409,7 @@ By default, Gam displays the information as an indented list of keys and values. This command displays information in CSV format. ``` gam print groups [todrive *] - [([domain ] ([member|showownedby ]|[query ]))| + [([domain|domains ] ([member|showownedby ]|[(query )|(queries )]))| (select )] [emailmatchpattern [not] ] [namematchpattern [not] ] [descriptionmatchpattern [not] ] (matchsetting [not] )* @@ -423,12 +428,11 @@ gam print groups [todrive *] [formatjson [quotechar ]] ``` By default, all groups in the account are displayed, these options allow selection of subsets of groups: -* `domain ` - Limit display to groups in the domain `` +* `domain|domains ` - Limit display to groups in the domains specified by `` * `member ` - Limit display to groups that contain `` as a member; mutually exclusive with `query ` * `showownedby ` - Limit display to groups that contain `` as an owner; mutually exclusive with `query ` -* `query ` - Limit display to groups that match , matching is done at Google; mutually exclusive with `member ` +* `(query )|(queries )` - Limit groups to those that match a query; each query is run against each domain * `select ` - Limit display to the groups specified in `` -* `showownedby ` - Limit display to groups owned by `` When using `query ` with the `name:{PREFIX}*` query, `PREFIX` must contain at least three characters. diff --git a/docs/How-to-Upgrade-from-Standard-GAM.md b/docs/How-to-Upgrade-from-Standard-GAM.md index 4eacc2c9..fbd0ebeb 100644 --- a/docs/How-to-Upgrade-from-Standard-GAM.md +++ b/docs/How-to-Upgrade-from-Standard-GAM.md @@ -174,6 +174,7 @@ Section: DEFAULT oauth2_txt = oauth2.txt ; /Users/admin/GAMConfig/oauth2.txt oauth2service_json = oauth2service.json ; /Users/admin/GAMConfig/oauth2service.json people_max_results = 100 + print_agu_domains = '' process_wait_limit = 0 quick_cros_move = false quick_info_user = false @@ -330,7 +331,7 @@ writes the credentials into the file oauth2.txt. admin@server:/Users/admin/bin/gamadv-xtd3$ rm -f /Users/admin/GAMConfig/oauth2.txt admin@server:/Users/admin/bin/gamadv-xtd3$ ./gam version WARNING: Config File: /Users/admin/GAMConfig/gam.cfg, Section: DEFAULT, Item: oauth2_txt, Value: /Users/admin/GAMConfig/oauth2.txt, Not Found -GAMADV-XTD3 6.63.13 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource +GAMADV-XTD3 6.63.14 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource Ross Scroggs Python 3.10.8 64-bit final MacOS High Sierra 10.13.6 x86_64 @@ -596,6 +597,7 @@ Section: DEFAULT oauth2_txt = oauth2.txt ; /Users/admin/GAMConfig/oauth2.txt oauth2service_json = oauth2service.json ; /Users/admin/GAMConfig/oauth2service.json people_max_results = 100 + print_agu_domains = '' process_wait_limit = 0 quick_cros_move = false quick_info_user = False @@ -794,6 +796,7 @@ Section: DEFAULT oauth2_txt = oauth2.txt ; C:\GAMConfig\oauth2.txt oauth2service_json = oauth2service.json ; C:\GAMConfig\oauth2service.json people_max_results = 100 + print_agu_domains = '' process_wait_limit = 0 quick_cros_move = false quick_info_user = False @@ -972,7 +975,7 @@ writes the credentials into the file oauth2.txt. C:\GAMADV-XTD3>del C:\GAMConfig\oauth2.txt C:\GAMADV-XTD3>gam version WARNING: Config File: C:\GAMConfig\gam.cfg, Section: DEFAULT, Item: oauth2_txt, Value: C:\GAMConfig\oauth2.txt, Not Found -GAMADV-XTD3 6.63.13 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource +GAMADV-XTD3 6.63.14 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource Ross Scroggs Python 3.11.5 64-bit final Windows-10-10.0.17134 AMD64 @@ -1240,6 +1243,7 @@ Section: DEFAULT output_dateformat = '' output_timeformat = '' people_max_results = 100 + print_agu_domains = '' process_wait_limit = 0 quick_cros_move = false quick_info_user = False diff --git a/docs/List-Items.md b/docs/List-Items.md index 64e20e03..3b4bf5f8 100644 --- a/docs/List-Items.md +++ b/docs/List-Items.md @@ -78,6 +78,7 @@ ::= "(,)*" ::= "(,)*" ::= "(,)*" + ::= "(,)*" ::= "(,)*" ::= "(,)*" ::= "(,)*" diff --git a/docs/Users-Chat.md b/docs/Users-Chat.md index 5bccbcd6..e7c6236e 100644 --- a/docs/Users-Chat.md +++ b/docs/Users-Chat.md @@ -179,6 +179,27 @@ When using the `formatjson` option, double quotes are used extensively in the da The `quotechar ` 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. +### Display information about all chat spaces +``` +# Local file +gam config auto_batch_min 1 redirect csv ./AllChatSpaces.csv multiprocess redirect stdout - multiprocess redirect stderr stdout all users print chatspaces +# Google sheet +gam config auto_batch_min 1 redirect csv - todrive * multiprocess redirect stdout - multiprocess redirect stderr stdout all users print chatspaces +``` +Add these options as desired: +``` + [types ] + [formatjson [quotechar ]] +``` +By default, Gam displays the information 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 ` 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. + ## Manage Chat Members ### Add members to a chat space ``` diff --git a/docs/Users-Drive-Copy-Move.md b/docs/Users-Drive-Copy-Move.md index 6e7f164d..7f0c0b75 100644 --- a/docs/Users-Drive-Copy-Move.md +++ b/docs/Users-Drive-Copy-Move.md @@ -92,6 +92,7 @@ gam copy drivefile [excludepermissionsfromdomains|includepermissionsfromdomains ] (mappermissionsdomain )* [sendemailifrequired []] + [verifyorganizer []] ``` The files/folders specified by `` are referred to as `source`, `target` refers to where those files are being copied. The files/folders specified by `` are referred to as `top`; when a folder is being copied recursively, the files/folders that it contains are referred as `sub`. @@ -100,6 +101,10 @@ At its simplest, you copy files/folders by giving the copy a new name and parent By default, files/folders in the Trash are copied; use `excludetrashed` to prevent these files/folders from being copied. +When a copy operation involves a Shared Drive, GAM verifies that the user is an organizer. Unfortunatley, this fails +when the user is not a direct organizer but is a member of a group that is an organizer. Specifying +`verifyorganizer false` suppresses the verification. + When copying folders, you have three modes of operation: ### Copy the top folder but none of its sub files/folders @@ -443,12 +448,17 @@ gam move drivefile [newfilename ]] [retainsourcefolders []] [sendemailifrequired []] + [verifyorganizer []] ``` The files/folders specified by `` are referred to as `source`, `target` refers to where those files are being moved. The files/folders specified by `` are referred to as `top`; when a folder is being moved, the files/folders that it contains are referred as `sub`. At its simplest, you move files/folders by giving them a new name and parent location. +When a move operation involves a Shared Drive, GAM verifies that the user is an organizer. Unfortunatley, this fails +when the user is not a direct organizer but is a member of a group that is an organizer. Specifying +`verifyorganizer false` suppresses the verification. + When moving folders, you have two modes of operation: ### Move the top folder and its sub files/folders diff --git a/docs/Users.md b/docs/Users.md index e22c5bbf..bc0b1290 100644 --- a/docs/Users.md +++ b/docs/Users.md @@ -80,6 +80,9 @@ queries "\"orgUnitPath='/Students/Middle School'\",\"orgUnitPath='/Students/Low none|nomail ::= (.)+ + ::= "(,)*" + ::= + | | ::= @ | <@> # The outer <> around @ are literal, e.g., IT Group @@ -953,8 +956,8 @@ gam print users See: https://developers.google.com/admin-sdk/directory/v1/guides/search-users ``` gam print users [todrive *] - ([domain ] [(query )|(queries )] - [limittoou |] [deleted_only|only_deleted]) + ([domain|domains ] [(query )|(queries )] + [limittoou ] [deleted_only|only_deleted]) [orderby [ascending|descending]] [groups|groupsincolumns] [license|licenses|licence|licences] [schemas|custom|customschemas all|] @@ -966,9 +969,9 @@ gam print users [todrive *] ``` By default, users in all domains in the account are selected; these options allow selection of subsets of users: -* `domain ` - Limit users to those in `` -* `(query )|(queries )` - Limit users to those that match a query -* `limittoou |` - Limit users to those in the specified `|` +* `domain|domains ` - Limit users to those in the domains specified by `` +* `(query )|(queries )` - Limit users to those that match a query; each query is run against each domain +* `limittoou |` - Limit users to those in the specified `>` * `deleted_only|only_deleted` - Only display deleted users * `issuspended ` - Limit users based on their status @@ -1060,15 +1063,15 @@ Print a CSV file with headers `domain,count` that gives the number of users in e ### Print domain counts for users in a specific domain and/or selected by a query ``` gam print users [todrive *] - ([domain ] [(query )|(queries )] - [limittoou |] [deleted_only|only_deleted]) + ([domain|domains ] [(query )|(queries )] + [limittoou ] [deleted_only|only_deleted]) [formatjson [quotechar ]] [countonly] [issuspended ] ``` By default, users in all domains in the account are selected; these options allow selection of subsets of users: -* `domain ` - Limit users to those in `` -* `(query )|(queries )` - Limit users to those that match a query -* `limittoou |` - Limit users to those in the specified `|` +* `domain|domains ` - Limit users to those in the domains specified by `` +* `(query )|(queries )` - Limit users to those that match a query; each query is run against each domain +* `limittoou |` - Limit users to those in the specified `>` * `deleted_only|only_deleted` - Only display deleted users * `issuspended ` - Limit users based on their status diff --git a/docs/Version-and-Help.md b/docs/Version-and-Help.md index c46af6ae..5e80d343 100644 --- a/docs/Version-and-Help.md +++ b/docs/Version-and-Help.md @@ -3,7 +3,7 @@ Print the current version of Gam with details ``` gam version -GAMADV-XTD3 6.63.13 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource +GAMADV-XTD3 6.63.14 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource Ross Scroggs Python 3.11.5 64-bit final MacOS Monterey 12.6.6 x86_64 @@ -15,7 +15,7 @@ Time: 2023-06-02T21:10:00-07:00 Print the current version of Gam with details and time offset information ``` gam version timeoffset -GAMADV-XTD3 6.63.13 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource +GAMADV-XTD3 6.63.14 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource Ross Scroggs Python 3.11.5 64-bit final MacOS Monterey 12.6.6 x86_64 @@ -27,7 +27,7 @@ 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 -GAMADV-XTD3 6.63.13 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource +GAMADV-XTD3 6.63.14 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource Ross Scroggs Python 3.11.5 64-bit final MacOS Monterey 12.6.6 x86_64 @@ -64,7 +64,7 @@ MacOS High Sierra 10.13.6 x86_64 Path: /Users/Admin/bin/gamadv-xtd3 Version Check: Current: 5.35.08 - Latest: 6.63.13 + Latest: 6.63.14 echo $? 1 ``` @@ -72,7 +72,7 @@ echo $? Print the current version number without details ``` gam version simple -6.63.13 +6.63.14 ``` In Linux/MacOS you can do: ``` @@ -82,7 +82,7 @@ echo $VER Print the current version of Gam and address of this Wiki ``` gam help -GAM 6.63.13 - https://github.com/taers232c/GAMADV-XTD3 +GAM 6.63.14 - https://github.com/taers232c/GAMADV-XTD3 Ross Scroggs Python 3.11.5 64-bit final MacOS Monterey 12.6.6 x86_64 diff --git a/docs/gam.cfg.md b/docs/gam.cfg.md index c45c2757..1b53f86b 100644 --- a/docs/gam.cfg.md +++ b/docs/gam.cfg.md @@ -401,6 +401,13 @@ people_max_results how many should be retrieved in each API call Default: 100 Range: 1 - 1000 +print_agu_domains + A comma separated list of domain names that are used in these commands: + gam print aliases + gam print groups + gam print|show group-members + gam print users + Default: Blank process_wait_limit When processing batch/CSV files, how long (in seconds) GAM should wait for all batch|csv processes to complete after all have been started. If the limit is reached, GAM terminates any remaining processes. @@ -633,6 +640,7 @@ Section: DEFAULT output_dateformat = '' output_timeformat = '' people_max_results = 100 + print_agu_domains = '' process_wait_limit = 0 quick_cros_move = false quick_info_user = false @@ -814,6 +822,7 @@ oauth2service_json = oauth2service.json output_dateformat = '' output_timeformat = '' people_max_results = 100 +print_agu_domains = '' process_wait_limit = 0 quick_cros_move = False quick_info_user = False diff --git a/src/GamCommands.txt b/src/GamCommands.txt index 590582dc..de43eb73 100644 --- a/src/GamCommands.txt +++ b/src/GamCommands.txt @@ -700,6 +700,7 @@ If an item contains spaces, it should be surrounded by ". ::= "(,)*" ::= "(,)*" ::= "(,)*" + ::= "(,)*" ::= "(,)*" ::= "(,)*" ::= "(,)*" @@ -1453,7 +1454,8 @@ gam delete alias|aliases gam info alias|aliases gam print alias|aliases [todrive *] - [domain ] [(query )|(queries )] + ([domain|domains ] [(query )|(queries )] + [limittoou ]) [user|users ] [group|groups ] [select ] [aliasmatchpattern ] @@ -3555,7 +3557,7 @@ gam info group|groups [memberemaildisplaypattern|memberemailskippattern ] [formatjson] gam print groups [todrive *] - [([domain ] ([member|showownedby ]|[query ]))| + [([domain|domains ] ([member|showownedby ]|[(query )|(queries )]))| (select )] [emailmatchpattern [not] ] [namematchpattern [not] ] [descriptionmatchpattern [not] ] (matchsetting [not] )* @@ -3591,7 +3593,7 @@ gam show grouptree gam info member|group-members gam info member|group-members | gam print group-members [todrive *] - [([domain ] ([member|showownedby ]|[query ]))| + [([domain|domains ] ([member|showownedby ]|[(query )|(queries )]))| (group|group_ns|group_susp )| (select )] [emailmatchpattern [not] ] [namematchpattern [not] ] @@ -3608,7 +3610,7 @@ gam print group-members [todrive *] [peoplelookup|(peoplelookupuser )] [formatjson [quotechar ]] gam show group-members - [([domain ] ([member|showownedby ]|[query ]))| + [([domain|domains ] ([member|showownedby ]|[(query )|(queries )]))| (group|group_ns|group_susp )| (select )] [emailmatchpattern [not] ] [namematchpattern [not] ] @@ -5262,7 +5264,7 @@ Print fields for selected users; use these options to select users: If none of these options are chosen, all users are selected. gam print users [todrive *] - ([domain ] [(query )|(queries )] + ([domain|domains ] [(query )|(queries )] [limittoou ] [deleted_only|only_deleted]) [orderby [ascending|descending]] [groups|groupsincolumns] @@ -5310,12 +5312,12 @@ gam print gam print users Print user domain counts for selected users; use these options to select users: - ([domain ] [(query )|(queries )] + ([domain|domains ] [(query )|(queries )] [limittoou ] [deleted_only|only_deleted]) If none of these options are chosen, all users are selected. gam print users [todrive *] - ([domain ] [(query )|(queries )] + ([domain|domains ] [(query )|(queries )] [limittoou ] [deleted_only|only_deleted]) [formatjson [quotechar ]] [countsonly|countonly] [issuspended ] @@ -6015,6 +6017,7 @@ gam copy drivefile [copysheetprotectedrangesnoninheritedpermissions []] [sendemailifrequired []] [suppressnotselectedmessages []] + [verifyorganizer []] gam move drivefile [newfilename ] [summary []] [showpermissionmessages []] @@ -6037,6 +6040,7 @@ gam move drivefile [newfilename ]] [retainsourcefolders []] [sendemailifrequired []] + [verifyorganizer []] gam get document [viewmode default|suggestions_inline|preview_suggestions_accepted|preview_without_suggestions] diff --git a/src/GamUpdate.txt b/src/GamUpdate.txt index 912594ab..2c917d95 100644 --- a/src/GamUpdate.txt +++ b/src/GamUpdate.txt @@ -2,6 +2,37 @@ Merged GAM-Team version +6.63.14 + +Added option `verifyorganizer []` to `gam copy|move drivefile`. When a copy/move +operation involves a Shared Drive, GAM verifies that the user is an organizer. Unfortunatley, this fails +when the user is not a direct organizer but is a member of a group that is an organizer. Specifying +`verifyorganizer false` suppresses the verification. + +Updated the following commands to be able to specify a list of domains rather than a single domain: +``` +gam print alias|aliases +gam print groups +gam print|show group-members +gam print users +``` +Added `print_agu_domains` variable to `gam.cfg` that provides a default list of domains for these commands. + +When multiple domains are specified and a query/queries are specified, an API call is made for each domain/query combination. +``` +$ gam print users domains school.org,students.school.org queries "'email:admin*','email:test*'" +Getting all Users that match query (domain=school.org, query="email:admin*"), may take some time on a large Google Workspace Account... +Got 3 Users: admin@school.org - admindirector@school.org +Getting all Users that match query (domain=school.org, query="email:test*"), may take some time on a large Google Workspace Account... +Got 20 Users: testusera@school.org - testuserx@school.org +Getting all Users that match query (domain=students.school.org, query="email:admin*"), may take some time on a large Google Workspace Account... +Got 1 User: admin@students.school.org - admin@students.school.org +Getting all Users that match query (domain=students.school.org, query="email:test*"), may take some time on a large Google Workspace Account... +Got 1 User: testuser1@students.school.org - testuser1@students.school.org +primaryEmail +... +``` + 6.63.13 Updated `gam print filelist ... showdrivename` and `gam show fileinfo ... showdrivename` diff --git a/src/gam/__init__.py b/src/gam/__init__.py index d22a384f..f53bb29c 100755 --- a/src/gam/__init__.py +++ b/src/gam/__init__.py @@ -2375,12 +2375,9 @@ def emptyQuery(query, entityType): def invalidQuery(query): return f'{Ent.Singular(Ent.QUERY)} ({query}) {Msg.INVALID}' -def invalidMember(kwargs): - if 'userKey' in kwargs: - badRequestWarning(Ent.GROUP, Ent.MEMBER, kwargs['userKey']) - return True - if 'query' in kwargs: - badRequestWarning(Ent.GROUP, Ent.QUERY, invalidQuery(kwargs['query'])) +def invalidMember(query): + if query: + badRequestWarning(Ent.GROUP, Ent.QUERY, invalidQuery(query)) return True return False @@ -4121,6 +4118,8 @@ def SetGlobalVariables(): GC.Values[GC.CSV_OUTPUT_ROW_DROP_FILTER_MODE] = GM.Globals[GM.CSV_OUTPUT_ROW_DROP_FILTER_MODE] if not GC.Values[GC.CSV_OUTPUT_ROW_LIMIT]: GC.Values[GC.CSV_OUTPUT_ROW_LIMIT] = GM.Globals[GM.CSV_OUTPUT_ROW_LIMIT] + if not GC.Values[GC.PRINT_AGU_DOMAINS]: + GC.Values[GC.PRINT_AGU_DOMAINS] = GM.Globals[GM.PRINT_AGU_DOMAINS] # customer_id, domain and admin_email must be set when enable_dasa = true if GC.Values[GC.ENABLE_DASA]: errors = 0 @@ -9300,7 +9299,7 @@ def terminateStdQueueHandler(mpQueue, mpQueueHandler): mpQueueHandler.join() def ProcessGAMCommandMulti(pid, numItems, logCmd, mpQueueCSVFile, mpQueueStdout, mpQueueStderr, - debugLevel, todrive, + debugLevel, todrive, printAguDomains, output_dateformat, output_timeformat, csvColumnDelimiter, csvQuoteChar, csvTimestampColumn, @@ -9339,6 +9338,7 @@ def ProcessGAMCommandMulti(pid, numItems, logCmd, mpQueueCSVFile, mpQueueStdout, GM.Globals[GM.OUTPUT_TIMEFORMAT] = output_timeformat GM.Globals[GM.NUM_BATCH_ITEMS] = numItems GM.Globals[GM.PID] = pid + GM.Globals[GM.PRINT_AGU_DOMAINS] = printAguDomains GM.Globals[GM.SAVED_STDOUT] = None GM.Globals[GM.SYSEXITRC] = 0 if mpQueueCSVFile: @@ -9508,6 +9508,7 @@ def MultiprocessGAMCommands(items, showCmds): poolProcessResults[pid] = pool.apply_async(ProcessGAMCommandMulti, [pid, numItems, logCmd, mpQueueCSVFile, mpQueueStdout, mpQueueStderr, GC.Values[GC.DEBUG_LEVEL], GM.Globals[GM.CSV_TODRIVE], + GC.Values[GC.PRINT_AGU_DOMAINS], GC.Values[GC.OUTPUT_DATEFORMAT], GC.Values[GC.OUTPUT_TIMEFORMAT], GC.Values[GC.CSV_OUTPUT_COLUMN_DELIMITER], GC.Values[GC.CSV_OUTPUT_QUOTE_CHAR], @@ -17452,11 +17453,58 @@ def infoAliases(entityList): def doInfoAliases(): infoAliases(getEntityList(Cmd.OB_EMAIL_ADDRESS_ENTITY)) +def initUserGroupDomainQueryFilters(): + if not GC.Values[GC.PRINT_AGU_DOMAINS]: + return {'list': [{'customer': GC.Values[GC.CUSTOMER_ID]}], 'queries': [None]} + return {'list': [{'domain': domain.lower()} for domain in GC.Values[GC.PRINT_AGU_DOMAINS].replace(',', ' ').split()], 'queries': [None]} + +def getUserGroupDomainQueryFilters(myarg, kwargsDict): + if myarg in {'domain', 'domains'}: + kwargsDict['list'] = [{'domain': domain.lower()} for domain in getEntityList(Cmd.OB_DOMAIN_NAME_ENTITY)] + elif myarg in {'query', 'queries'}: + kwargsDict['queries'] = getQueries(myarg) + else: + return False + return True + +def makeUserGroupDomainQueryFilters(kwargsDict): + kwargsQueries = [] + for kwargs in kwargsDict['list']: + for query in kwargsDict['queries']: + kwargsQueries.append((kwargs, query)) + return kwargsQueries + +def userFilters(kwargs, query, orgUnitPath, isSuspended): + queryTitle = '' + if kwargs.get('domain'): + queryTitle += f'domain={kwargs["domain"]}, ' + if orgUnitPath is not None: + if query is not None and query.find(orgUnitPath) == -1: + query += f" orgUnitPath='{orgUnitPath}'" + else: + if query is None: + query = '' + else: + query += ' ' + query += f"orgUnitPath='{orgUnitPath}'" + if isSuspended is not None: + if query is None: + query = '' + else: + query += ' ' + query += f'isSuspended={isSuspended}' + if query is not None: + queryTitle += f'query="{query}", ' + if queryTitle: + return query, queryTitle[:-2] + return query, queryTitle + # gam print aliases|nicknames [todrive *] -# [domain ] [(query )|(queries )] +# ([domain|domains ] [(query )|(queries )] +# [limittoou ]) # [user|users ] [group|groups ] # [select ] -# [aliasmatchpattern ] +# [issuspended ] [aliasmatchpattern ] # [shownoneditable] [nogroups] [nousers] # [onerowpertarget] [delimiter ] # [suppressnoaliasrows] @@ -17497,12 +17545,12 @@ def doPrintAliases(): userFields = ['primaryEmail', 'aliases'] groupFields = ['email', 'aliases'] oneRowPerTarget = showNonEditable = suppressNoAliasRows = False - kwargs = {'customer': GC.Values[GC.CUSTOMER_ID]} - queries = [None] + kwargsDict = initUserGroupDomainQueryFilters() getGroups = getUsers = True groups = [] users = [] aliasMatchPattern = re.compile(r'^.*$') + isSuspended = orgUnitPath = None addCSVData = {} delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER] while Cmd.ArgumentsRemaining(): @@ -17517,15 +17565,17 @@ def doPrintAliases(): getGroups = False elif myarg == 'nousers': getUsers = False - elif myarg == 'domain': - kwargs['domain'] = getString(Cmd.OB_DOMAIN_NAME).lower() - kwargs.pop('customer', None) - elif myarg in {'query', 'queries'}: - queries = getQueries(myarg) + elif myarg == 'limittoou': + orgUnitPath = getOrgUnitItem(pathOnly=True, cd=cd) + orgUnitPathLower = orgUnitPath.lower() + userFields.append('orgUnitPath') getGroups = False - getUsers = True + elif getUserGroupDomainQueryFilters(myarg, kwargsDict): + pass elif myarg == 'select': _, users = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS) + elif myarg == 'issuspended': + isSuspended = getBoolean() elif myarg in {'user','users'}: users.extend(convertEntityToList(getString(Cmd.OB_EMAIL_ADDRESS_LIST, minLen=0))) elif myarg in {'group', 'groups'}: @@ -17543,7 +17593,7 @@ def doPrintAliases(): delimiter = getCharacter() else: unknownArgumentExit() - if users or groups and queries[0] is None: + if (users or groups) and kwargsDict['queries'][0] is None: getUsers = getGroups = False if not oneRowPerTarget: titlesList = ['Alias', 'Target', 'TargetType'] @@ -17557,8 +17607,11 @@ def doPrintAliases(): if addCSVData: csvPF.AddTitles(sorted(addCSVData.keys())) if getUsers: - for query in queries: - printGettingAllAccountEntities(Ent.USER, query) + for kwargsQuery in makeUserGroupDomainQueryFilters(kwargsDict): + kwargs = kwargsQuery[0] + query = kwargsQuery[1] + query, pquery = userFilters(kwargs, query, orgUnitPath, isSuspended) + printGettingAllAccountEntities(Ent.USER, pquery) try: entityList = callGAPIpages(cd.users(), 'list', 'users', pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute='primaryEmail', @@ -17568,13 +17621,14 @@ def doPrintAliases(): fields=f'nextPageToken,users({",".join(userFields)})', maxResults=GC.Values[GC.USER_MAX_RESULTS], **kwargs) for user in entityList: - writeAliases(user, user['primaryEmail'], 'User') + if orgUnitPath is None or orgUnitPathLower == user.get('orgUnitPath', '').lower(): + writeAliases(user, user['primaryEmail'], 'User') except (GAPI.invalidOrgunit, GAPI.invalidInput): entityActionFailedWarning([Ent.ALIAS, None], invalidQuery(query)) - return + continue except GAPI.domainNotFound as e : entityActionFailedWarning([Ent.ALIAS, None, Ent.DOMAIN, kwargs['domain']], str(e)) - return + continue except (GAPI.resourceNotFound, GAPI.forbidden, GAPI.badRequest): accessErrorExit(cd) count = len(users) @@ -17593,19 +17647,24 @@ def doPrintAliases(): except (GAPI.userNotFound, GAPI.badRequest, GAPI.invalid, GAPI.forbidden, GAPI.invalidResource, GAPI.conditionNotMet) as e: entityActionFailedWarning([Ent.USER, user], str(e), i, count) if getGroups: - printGettingAllAccountEntities(Ent.GROUP) - try: - entityList = callGAPIpages(cd.groups(), 'list', 'groups', - pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute='email', - throwReasons=GAPI.GROUP_LIST_THROW_REASONS, - orderBy='email', fields=f'nextPageToken,groups({",".join(groupFields)})', **kwargs) - for group in entityList: - writeAliases(group, group['email'], 'Group') - except GAPI.domainNotFound as e : - entityActionFailedWarning([Ent.ALIAS, None, Ent.DOMAIN, kwargs['domain']], str(e)) - return - except (GAPI.resourceNotFound, GAPI.forbidden, GAPI.badRequest): - accessErrorExit(cd) + for kwargsQuery in makeUserGroupDomainQueryFilters(kwargsDict): + kwargs = kwargsQuery[0] + query = kwargsQuery[1] + query, pquery = groupFilters(kwargs, query) + printGettingAllAccountEntities(Ent.GROUP, pquery) + try: + entityList = callGAPIpages(cd.groups(), 'list', 'groups', + pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute='email', + throwReasons=GAPI.GROUP_LIST_THROW_REASONS, + query=query, orderBy='email', + fields=f'nextPageToken,groups({",".join(groupFields)})', **kwargs) + for group in entityList: + writeAliases(group, group['email'], 'Group') + except GAPI.domainNotFound as e : + entityActionFailedWarning([Ent.ALIAS, None, Ent.DOMAIN, kwargs['domain']], str(e)) + continue + except (GAPI.resourceNotFound, GAPI.forbidden, GAPI.badRequest): + accessErrorExit(cd) count = len(groups) i = 0 for group in groups: @@ -30442,40 +30501,32 @@ def infoGroups(entityList): def doInfoGroups(): infoGroups(getEntityList(Cmd.OB_GROUP_ENTITY)) -def groupFilters(kwargs): +def groupFilters(kwargs, query): queryTitle = '' if kwargs.get('domain'): - queryTitle += f'{Ent.Singular(Ent.DOMAIN)}={kwargs["domain"]}, ' - if kwargs.get('userKey'): - queryTitle += f'{Ent.Singular(Ent.MEMBER)}={kwargs["userKey"]}, ' - if kwargs.get('query'): - queryTitle += f'query="{kwargs["query"]}", ' + queryTitle += f'domain={kwargs["domain"]}, ' + if query is not None: + queryTitle += f'query="{query}", ' if queryTitle: - return queryTitle[:-2] - return queryTitle + return query, queryTitle[:-2] + return query, queryTitle -def getGroupFilters(myarg, kwargs, showOwnedBy): - if myarg == 'domain': - kwargs['domain'] = getString(Cmd.OB_DOMAIN_NAME).lower() - kwargs.pop('customer', None) +def getGroupFilters(myarg, kwargsDict, showOwnedBy): + if getUserGroupDomainQueryFilters(myarg, kwargsDict): + pass elif myarg in {'member', 'showownedby'}: emailAddressOrUID = getEmailAddress() if emailAddressOrUID != GC.Values[GC.CUSTOMER_ID].lower(): - kwargs['userKey'] = emailAddressOrUID - kwargs.pop('customer', None) + kwargsDict['queries'] = [f'memberKey={emailAddressOrUID}'] key = 'email' if emailAddressOrUID.find('@') != -1 else 'id' else: - kwargs['query'] = f'memberKey={GC.Values[GC.CUSTOMER_ID]}' + kwargsDict['queries'] = [f'memberKey={GC.Values[GC.CUSTOMER_ID]}'] key = 'id' if myarg == 'showownedby': showOwnedBy['key'] = key showOwnedBy['value'] = emailAddressOrUID - elif myarg == 'query': - kwargs['query'] = getString(Cmd.OB_QUERY) else: return False - if kwargs.get('userKey') and kwargs.get('query'): - usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format('member', 'query')) return True def checkGroupShowOwnedBy(showOwnedBy, members): @@ -30669,7 +30720,7 @@ def addMemberInfoToRow(row, groupMembers, typesSet, memberOptions, memberDisplay PRINT_GROUPS_JSON_TITLES = ['email', 'JSON'] # gam print groups [todrive *] -# [([domain ] ([member|showownedby ]|[query ]))| +# [([domain|domains ] ([member|showownedby ]|[(query )|(queries )]))| # (select )] # [emailmatchpattern [not] ] [namematchpattern [not] ] # [descriptionmatchpattern [not] ] (matchsetting [not] )* @@ -30849,7 +30900,7 @@ def doPrintGroups(): cd = buildGAPIObject(API.DIRECTORY) ci = None - kwargs = {'customer': GC.Values[GC.CUSTOMER_ID]} + kwargsDict = initUserGroupDomainQueryFilters() convertCRNL = GC.Values[GC.CSV_OUTPUT_CONVERT_CR_NL] delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER] getCloudIdentity = getSettings = showCIgroupKey = sortHeaders = False @@ -30871,7 +30922,7 @@ def doPrintGroups(): myarg = getArgument() if myarg == 'todrive': csvPF.GetTodriveParameters() - elif getGroupFilters(myarg, kwargs, showOwnedBy): + elif getGroupFilters(myarg, kwargsDict, showOwnedBy): pass elif getGroupMatchPatterns(myarg, matchPatterns, False): pass @@ -30992,40 +31043,43 @@ def doPrintGroups(): if getCloudIdentity: csvPF.AddJSONTitle('JSON-cloudIdentity') if entitySelection is None: - printGettingAllAccountEntities(Ent.GROUP, groupFilters(kwargs)) - try: - entityList = callGAPIpages(cd.groups(), 'list', 'groups', - pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute='email', - throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS, - orderBy='email', fields=cdfieldsnp, maxResults=maxResults, **kwargs) - except (GAPI.invalidMember, GAPI.invalidInput) as e: - if not invalidMember(kwargs): - entityActionFailedExit([Ent.GROUP, None], str(e)) - entityList = [] - except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest): - if kwargs.get('domain'): - badRequestWarning(Ent.GROUP, Ent.DOMAIN, kwargs['domain']) - entityList = [] - else: - accessErrorExit(cd) - if getCloudIdentity: - printGettingAllAccountEntities(Ent.CLOUD_IDENTITY_GROUP) + entityList = [] + for kwargsQuery in makeUserGroupDomainQueryFilters(kwargsDict): + kwargs = kwargsQuery[0] + query = kwargsQuery[1] + query, pquery = groupFilters(kwargs, query) + printGettingAllAccountEntities(Ent.GROUP, pquery) try: - ciGroupList = callGAPIpages(ci.groups(), 'list', 'groups', - pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute=['groupKey', 'id'], - throwReasons=GAPI.CIGROUP_LIST_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS, - parent=f'customers/{GC.Values[GC.CUSTOMER_ID]}', view='FULL', - fields=cifieldsnp, pageSize=500) - except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, - GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.invalidArgument, - GAPI.systemError, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e: - accessErrorExitNonDirectory(API.CLOUDIDENTITY_GROUPS, str(e)) - for ciGroup in ciGroupList: - key = ciGroup['groupKey']['id'] - if not showCIgroupKey: - ciGroup.pop('groupKey') - ciGroups[key] = ciGroup.copy() - del ciGroupList + entityList.extend(callGAPIpages(cd.groups(), 'list', 'groups', + pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute='email', + throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS, + orderBy='email', query=query, fields=cdfieldsnp, maxResults=maxResults, **kwargs)) + except (GAPI.invalidMember, GAPI.invalidInput) as e: + if not invalidMember(query): + entityActionFailedExit([Ent.GROUP, None], str(e)) + except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest): + if kwargs.get('domain'): + badRequestWarning(Ent.GROUP, Ent.DOMAIN, kwargs['domain']) + else: + accessErrorExit(cd) + if getCloudIdentity: + printGettingAllAccountEntities(Ent.CLOUD_IDENTITY_GROUP) + try: + ciGroupList = callGAPIpages(ci.groups(), 'list', 'groups', + pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute=['groupKey', 'id'], + throwReasons=GAPI.CIGROUP_LIST_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS, + parent=f'customers/{GC.Values[GC.CUSTOMER_ID]}', view='FULL', + fields=cifieldsnp, pageSize=500) + except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, + GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.invalidArgument, + GAPI.systemError, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e: + accessErrorExitNonDirectory(API.CLOUDIDENTITY_GROUPS, str(e)) + for ciGroup in ciGroupList: + key = ciGroup['groupKey']['id'] + if not showCIgroupKey: + ciGroup.pop('groupKey') + ciGroups[key] = ciGroup.copy() + del ciGroupList else: svcargs = dict([('groupKey', None), ('fields', cdfields)]+GM.Globals[GM.EXTRA_ARGS_LIST]) cdmethod = getattr(cd.groups(), 'get') @@ -31219,26 +31273,28 @@ def infoGroupMembers(entityList, ciGroupsAPI=False): def doInfoGroupMembers(): infoGroupMembers(getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)[1], False) -def getGroupMembersEntityList(cd, entityList, matchPatterns, fieldsList, kwargs): +def getGroupMembersEntityList(cd, entityList, matchPatterns, fieldsList, kwargsDict): if entityList is None: updateFieldsForGroupMatchPatterns(matchPatterns, fieldsList) - subTitle = groupFilters(kwargs) - printGettingAllAccountEntities(Ent.GROUP, subTitle) - try: - entityList = callGAPIpages(cd.groups(), 'list', 'groups', - pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute='email', - throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS, - orderBy='email', fields=f'nextPageToken,groups({",".join(set(fieldsList))})', **kwargs) - except (GAPI.invalidMember, GAPI.invalidInput) as e: - if not invalidMember(kwargs): - entityActionFailedExit([Ent.GROUP, None], str(e)) - entityList = [] - except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest): - if kwargs.get('domain'): - badRequestWarning(Ent.GROUP, Ent.DOMAIN, kwargs['domain']) - entityList = [] - else: - accessErrorExit(cd) + entityList = [] + for kwargsQuery in makeUserGroupDomainQueryFilters(kwargsDict): + kwargs = kwargsQuery[0] + query = kwargsQuery[1] + query, pquery = groupFilters(kwargs, query) + printGettingAllAccountEntities(Ent.GROUP, pquery) + try: + entityList.extend(callGAPIpages(cd.groups(), 'list', 'groups', + pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute='email', + throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS, + orderBy='email', query=query, fields=f'nextPageToken,groups({",".join(set(fieldsList))})', **kwargs)) + except (GAPI.invalidMember, GAPI.invalidInput) as e: + if not invalidMember(query): + entityActionFailedExit([Ent.GROUP, None], str(e)) + except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest): + if kwargs.get('domain'): + badRequestWarning(Ent.GROUP, Ent.DOMAIN, kwargs['domain']) + else: + accessErrorExit(cd) else: clearUnneededGroupMatchPatterns(matchPatterns) return entityList @@ -31347,7 +31403,7 @@ GROUPMEMBERS_FIELDS_CHOICE_MAP = { GROUPMEMBERS_DEFAULT_FIELDS = ['group', 'type', 'role', 'id', 'status', 'email'] # gam print group-members [todrive *] -# [([domain ] ([member|showownedby ]|[query ]))| +# [([domain|domains ] ([member|showownedby ]|[(query )|(queries )]))| # (group|group_ns|group_susp )| # (select )] # [emailmatchpattern [not] ] [namematchpattern [not] ] @@ -31384,7 +31440,8 @@ def doPrintGroupMembers(): peopleNames = {} memberOptions = initMemberOptions() groupColumn = True - kwargs = {'customer': GC.Values[GC.CUSTOMER_ID]} + customerKey = GC.Values[GC.CUSTOMER_ID] + kwargsDict = initUserGroupDomainQueryFilters() subTitle = f'{Msg.ALL} {Ent.Plural(Ent.GROUP)}' fieldsList = [] csvPF = CSVPrintFile('group') @@ -31401,7 +31458,7 @@ def doPrintGroupMembers(): myarg = getArgument() if myarg == 'todrive': csvPF.GetTodriveParameters() - elif getGroupFilters(myarg, kwargs, showOwnedBy): + elif getGroupFilters(myarg, kwargsDict, showOwnedBy): pass elif getGroupMatchPatterns(myarg, matchPatterns, False): pass @@ -31457,7 +31514,7 @@ def doPrintGroupMembers(): FJQC.GetFormatJSONQuoteChar(myarg, False) if not typesSet: typesSet = {Ent.TYPE_USER} if memberOptions[MEMBEROPTION_RECURSIVE] else ALL_GROUP_TYPES - entityList = getGroupMembersEntityList(cd, entityList, matchPatterns, cdfieldsList, kwargs) + entityList = getGroupMembersEntityList(cd, entityList, matchPatterns, cdfieldsList, kwargsDict) if not fieldsList: for field in GROUPMEMBERS_DEFAULT_FIELDS: csvPF.AddField(field, {field: field}, fieldsList) @@ -31493,7 +31550,6 @@ def doPrintGroupMembers(): getRolesSet.add(Ent.ROLE_OWNER) getRoles = ','.join(sorted(getRolesSet)) level = 0 - customerKey = GC.Values[GC.CUSTOMER_ID] setCustomerMemberEmail = 'email' in fieldsList i = 0 count = len(entityList) @@ -31595,7 +31651,7 @@ def doPrintGroupMembers(): csvPF.writeCSVfile(f'Group Members ({subTitle})') # gam show group-members -# [([domain ] ([member|showownedby ]|[query ]))| +# [([domain|domains ] ([member|showownedby ]|[(query )|(queries )]))| # (group|group_ns|group_susp )| # (select )] # [emailmatchpattern [not] ] [namematchpattern [not] ] @@ -31642,8 +31698,7 @@ def doShowGroupMembers(): cd = buildGAPIObject(API.DIRECTORY) ci = None - customerKey = GC.Values[GC.CUSTOMER_ID] - kwargs = {'customer': customerKey} + kwargsDict = initUserGroupDomainQueryFilters() entityList = None showOwnedBy = {} cdfieldsList = ['email'] @@ -31655,7 +31710,7 @@ def doShowGroupMembers(): includeDerivedMembership = False while Cmd.ArgumentsRemaining(): myarg = getArgument() - if getGroupFilters(myarg, kwargs, showOwnedBy): + if getGroupFilters(myarg, kwargsDict, showOwnedBy): pass elif getGroupMatchPatterns(myarg, matchPatterns, False): pass @@ -31687,7 +31742,7 @@ def doShowGroupMembers(): rolesSet = ALL_GROUP_ROLES if not typesSet: typesSet = ALL_GROUP_TYPES - entityList = getGroupMembersEntityList(cd, entityList, matchPatterns, cdfieldsList, kwargs) + entityList = getGroupMembersEntityList(cd, entityList, matchPatterns, cdfieldsList, kwargsDict) i = 0 count = len(entityList) for group in entityList: @@ -41662,7 +41717,7 @@ USERS_INDEXED_TITLES = ['addresses', 'aliases', 'nonEditableAliases', 'emails', 'phones', 'posixAccounts', 'relations', 'sshPublicKeys', 'websites'] # gam print users [todrive *] -# ([domain ] [(query )|(queries )] +# ([domain|domains ] [(query )|(queries )] # [limittoou ] [deleted_only|only_deleted])|[select ] # [groups|groupsincolumns] # [license|licenses|licence|licences|licensebyuser|licensesbyuser|licencebyuser|licencesbyuser] @@ -41804,8 +41859,7 @@ def doPrintUsers(entityList=None): 'sortHeaders': False, 'maxGroups': 0 } - kwargs = {'customer': GC.Values[GC.CUSTOMER_ID]} - queries = [None] + kwargsDict = initUserGroupDomainQueryFilters() licenses = {} lic = None skus = None @@ -41824,11 +41878,8 @@ def doPrintUsers(entityList=None): elif entityList is None and myarg == 'limittoou': orgUnitPath = getOrgUnitItem(pathOnly=True, cd=cd) orgUnitPathLower = orgUnitPath.lower() - elif myarg == 'domain': - kwargs['domain'] = getString(Cmd.OB_DOMAIN_NAME).lower() - kwargs.pop('customer', None) - elif entityList is None and myarg in {'query', 'queries'}: - queries = getQueries(myarg) + elif entityList is None and getUserGroupDomainQueryFilters(myarg, kwargsDict): + pass elif entityList is None and myarg in {'deletedonly', 'onlydeleted'}: showDeleted = True elif entityList is None and myarg == 'select': @@ -41927,23 +41978,11 @@ def doPrintUsers(entityList=None): if orgUnitPath is not None and fieldsList: fieldsList.append('orgUnitPath') fields = getItemFieldsFromFieldsList('users', fieldsList) - for query in queries: - if orgUnitPath is not None: - if query is not None and query.find(orgUnitPath) == -1: - query += f" orgUnitPath='{orgUnitPath}'" - else: - if query is None: - query = '' - else: - query += ' ' - query += f"orgUnitPath='{orgUnitPath}'" - if isSuspended is not None: - if query is None: - query = '' - else: - query += ' ' - query += f'isSuspended={isSuspended}' - printGettingAllAccountEntities(Ent.USER, query) + for kwargsQuery in makeUserGroupDomainQueryFilters(kwargsDict): + kwargs = kwargsQuery[0] + query = kwargsQuery[1] + query, pquery = userFilters(kwargs, query, orgUnitPath, isSuspended) + printGettingAllAccountEntities(Ent.USER, pquery) pageMessage = getPageMessage(showFirstLastItems=True) try: feed = yieldGAPIpages(cd.users(), 'list', 'users', @@ -41972,7 +42011,7 @@ def doPrintUsers(entityList=None): _updateDomainCounts(user['primaryEmail']) except GAPI.domainNotFound: entityActionFailedWarning([Ent.USER, None, Ent.DOMAIN, kwargs['domain']], Msg.NOT_FOUND) - return + continue except (GAPI.invalidOrgunit, GAPI.invalidInput) as e: if query and not customFieldMask: entityActionFailedWarning([Ent.USER, None], invalidQuery(query)) @@ -41982,7 +42021,7 @@ def doPrintUsers(entityList=None): entityActionFailedWarning([Ent.USER, None], f'{invalidQuery(query)} or {invalidUserSchema(customFieldMask)}') else: entityActionFailedWarning([Ent.USER, None], str(e)) - return + continue except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden): accessErrorExit(cd) else: @@ -50235,8 +50274,6 @@ class DriveFileFields(): self.fieldsList = [] self.includeLabels = [] self.parentsSubFields = {'id': False, 'isRoot': False, 'rootFolderId': None} - self.sharedDriveNames = {} - self.drive = None def SetAllParentsSubFields(self): self.parentsSubFields['id'] = self.parentsSubFields['isRoot'] = True @@ -50286,16 +50323,6 @@ class DriveFileFields(): def orderBy(self): return self.OBY.orderBy - def SharedDriveName(self, drive, driveId): - if driveId not in self.sharedDriveNames: - try: - self.sharedDriveNames[driveId] = callGAPI(drive.drives(), 'get', - throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND], - useDomainAdminAccess=False, driveId=driveId, fields='name')['name'] - except (GAPI.notFound, GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy): - self.sharedDriveNames[driveId] = TEAM_DRIVE - return self.sharedDriveNames[driveId] - def _setSkipObjects(skipObjects, skipTitles, fieldsList): for field in skipTitles: if field != 'parents': @@ -50453,9 +50480,9 @@ def showFileInfo(users): driveId = result.get('driveId') if driveId: if result['mimeType'] == MIMETYPE_GA_FOLDER and result['name'] == TEAM_DRIVE: - result['name'] = DFF.SharedDriveName(drive, driveId) + result['name'] = _getSharedDriveNameFromId(drive, driveId) if DFF.showSharedDriveNames: - result['driveName'] = DFF.SharedDriveName(drive, driveId) + result['driveName'] = _getSharedDriveNameFromId(drive, driveId) if showNoParents: result.setdefault('parents', []) if getPermissionsForSharedDrives and driveId and 'permissions' not in result: @@ -51791,7 +51818,7 @@ def printFileList(users): if not pmselect and 'permissions' in fileInfo: fileInfo['permissions'] = DLP.GetFileMatchingPermission(fileInfo) if DFF.showSharedDriveNames and driveId: - fileInfo['driveName'] = DFF.SharedDriveName(drive, driveId) + fileInfo['driveName'] = _getSharedDriveNameFromId(drive, driveId) if filepath: if not FJQC.formatJSON or not addPathsToJSON: addFilePathsToRow(drive, fileTree, fileInfo, filePathInfo, csvPF, row, fullpath=fullpath, showDepth=showDepth) @@ -54904,6 +54931,7 @@ copyReturnItemMap = { # [copysheetprotectedrangesnoninheritedpermissions []] # [sendemailifrequired []] # [suppressnotselectedmessages []] +# [verifyorganizer []] def copyDriveFile(users): def _writeCSVData(user, oldName, oldId, newName, newId, mimeType): row = {'User': user, fileNameTitle: oldName, 'id': oldId, @@ -55240,6 +55268,7 @@ def copyDriveFile(users): copyMoveOptions = initCopyMoveOptions(True) excludeTrashed = newParentsSpecified = recursive = suppressNotSelectedMessages = False maxdepth = -1 + verifyOrganizer = True while Cmd.ArgumentsRemaining(): myarg = getArgument() if getCopyMoveOptions(myarg, copyMoveOptions): @@ -55267,6 +55296,8 @@ def copyDriveFile(users): suppressNotSelectedMessages = getBoolean() elif getDriveFileCopyAttribute(myarg, copyBody, copyParameters): pass + elif myarg == 'verifyorganizer': + verifyOrganizer = getBoolean() else: unknownArgumentExit() if csvPF: @@ -55319,7 +55350,7 @@ def copyDriveFile(users): continue if copyMoveOptions['sourceDriveId']: # If copying from a Shared Drive, user has to be an organizer - if not _verifyUserIsOrganizer(drive, user, i, count, copyMoveOptions['sourceDriveId']): + if verifyOrganizer and not _verifyUserIsOrganizer(drive, user, i, count, copyMoveOptions['sourceDriveId']): _incrStatistic(statistics, STAT_USER_NOT_ORGANIZER) continue sourceSearchArgs = {'driveId': copyMoveOptions['sourceDriveId'], 'corpora': 'drive', 'includeItemsFromAllDrives': True, 'supportsAllDrives': True} @@ -55347,7 +55378,7 @@ def copyDriveFile(users): copyMoveOptions['destParentType'] = dest['destParentType'] if copyMoveOptions['destDriveId']: # If copying to a Shared Drive, user has to be an organizer - if not _verifyUserIsOrganizer(drive, user, i, count, copyMoveOptions['destDriveId']): + if verifyOrganizer and not _verifyUserIsOrganizer(drive, user, i, count, copyMoveOptions['destDriveId']): _incrStatistic(statistics, STAT_USER_NOT_ORGANIZER) continue if not parentParms[DFA_SEARCHARGS]: @@ -55680,6 +55711,7 @@ def _updateMoveFilePermissions(drive, user, i, count, # [updatefilepermissions []] # [retainsourcefolders []] # [sendemailifrequired []] +# [verifyorganizer []] def moveDriveFile(users): def _cloneFolderMove(drive, user, i, count, j, jcount, source, targetChildren, newFolderName, newParentId, newParentName, mergeParentModifiedTime, @@ -56014,6 +56046,7 @@ def moveDriveFile(users): newParentsSpecified = False movedFiles = {} updateFilePermissions = False + verifyOrganizer = True while Cmd.ArgumentsRemaining(): myarg = getArgument() if getCopyMoveOptions(myarg, copyMoveOptions): @@ -56022,6 +56055,8 @@ def moveDriveFile(users): newParentsSpecified = True elif myarg == 'updatefilepermissions': updateFilePermissions = getBoolean() + elif myarg == 'verifyorganizer': + verifyOrganizer = getBoolean() else: unknownArgumentExit() i, count, users = getEntityArgument(users) @@ -56054,7 +56089,7 @@ def moveDriveFile(users): copyMoveOptions['sourceDriveId'] = source.get('driveId') if copyMoveOptions['sourceDriveId']: # If moving from a Shared Drive, user has to be an organizer - if not _verifyUserIsOrganizer(drive, user, i, count, copyMoveOptions['sourceDriveId']): + if verifyOrganizer and not _verifyUserIsOrganizer(drive, user, i, count, copyMoveOptions['sourceDriveId']): _incrStatistic(statistics, STAT_USER_NOT_ORGANIZER) continue if source['trashed']: @@ -56099,7 +56134,7 @@ def moveDriveFile(users): continue if copyMoveOptions['destDriveId']: # If moving to a Shared Drive, user has to be an organizer - if not _verifyUserIsOrganizer(drive, user, i, count, copyMoveOptions['destDriveId']): + if verifyOrganizer and not _verifyUserIsOrganizer(drive, user, i, count, copyMoveOptions['destDriveId']): _incrStatistic(statistics, STAT_USER_NOT_ORGANIZER) continue # 3rd party shortcuts can't be moved to Shared Drives diff --git a/src/gam/gamlib/glcfg.py b/src/gam/gamlib/glcfg.py index c28553bb..77b9ec3c 100644 --- a/src/gam/gamlib/glcfg.py +++ b/src/gam/gamlib/glcfg.py @@ -198,6 +198,8 @@ OUTPUT_DATEFORMAT = 'output_dateformat' OUTPUT_TIMEFORMAT = 'output_timeformat' # When retrieving lists of people from API, how many should be retrieved in each chunk PEOPLE_MAX_RESULTS = 'people_max_results' +# Domains for print alises|groups|users +PRINT_AGU_DOMAINS = 'print_agu_domains' # Number of seconds to wait for batch/csv processes to complete PROCESS_WAIT_LIMIT = 'process_wait_limit' # Use quick method to move Chromebooks to OU @@ -363,6 +365,7 @@ Defaults = { OUTPUT_DATEFORMAT: '', OUTPUT_TIMEFORMAT: '', PEOPLE_MAX_RESULTS: '100', + PRINT_AGU_DOMAINS: '', PROCESS_WAIT_LIMIT: '0', QUICK_CROS_MOVE: FALSE, QUICK_INFO_USER: FALSE, @@ -511,6 +514,7 @@ VAR_INFO = { OUTPUT_DATEFORMAT: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)}, OUTPUT_TIMEFORMAT: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)}, PEOPLE_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (0, 1000)}, + PRINT_AGU_DOMAINS: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)}, PROCESS_WAIT_LIMIT: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (0, None)}, QUICK_CROS_MOVE: {VAR_TYPE: TYPE_BOOLEAN}, QUICK_INFO_USER: {VAR_TYPE: TYPE_BOOLEAN}, diff --git a/src/gam/gamlib/glglobals.py b/src/gam/gamlib/glglobals.py index 5b163df7..9d78d689 100644 --- a/src/gam/gamlib/glglobals.py +++ b/src/gam/gamlib/glglobals.py @@ -156,6 +156,8 @@ OUTPUT_TIMEFORMAT = 'outf' PARSER = 'pars' # Process ID PID = 'pid ' +# Domains for print alises|groups|users +PRINT_AGU_DOMAINS = 'pagu' # Check API calls rate RATE_CHECK_COUNT = 'rccn' RATE_CHECK_START = 'rcst' @@ -269,6 +271,7 @@ Globals = { OUTPUT_TIMEFORMAT: '', PARSER: None, PID: 0, + PRINT_AGU_DOMAINS: '', RATE_CHECK_COUNT: 0, RATE_CHECK_START: 0, SECTION: None,