Compare commits

...

215 Commits
v6.05 ... v6.13

Author SHA1 Message Date
Jay Lee
a1e6459dc1 GAM 6.13 2022-01-12 09:23:04 -05:00
Jay Lee
31a3dcd2f7 Update gam.spec 2022-01-12 08:50:29 -05:00
Jay Lee
f0120fef63 Update build.yml 2022-01-11 14:54:03 -05:00
Jay Lee
5095e6af14 Update build.yml 2022-01-11 13:25:35 -05:00
Janosh Riebesell
19f21a9453 pyupgrade --py37-plus **/*.py (#1445) 2022-01-11 11:05:02 -05:00
Jay Lee
676908daca Update build.yml 2022-01-11 09:03:43 -05:00
Jay Lee
66a5d0472d Update build.yml 2022-01-11 08:58:29 -05:00
Jay Lee
cc30e307e9 Update build.yml 2022-01-11 08:47:21 -05:00
Jay Lee
57e3eb5c8e Update macos-before-install.sh 2022-01-11 08:45:07 -05:00
Jay Lee
b855f6876c Update build.yml 2022-01-11 08:41:00 -05:00
Jay Lee
8edc06ba41 Update build.yml 2022-01-11 08:33:42 -05:00
Jay Lee
69aa31566b Update build.yml 2022-01-11 08:23:10 -05:00
Jay Lee
19d3483209 Update build.yml 2022-01-10 12:58:46 -05:00
Jay Lee
c1c7e65a3c Update build.yml 2022-01-10 10:19:39 -05:00
Jay Lee
2d0044de95 Update build.yml 2022-01-10 10:13:54 -05:00
Jay Lee
418e3af903 Update build.yml 2022-01-10 07:05:20 -05:00
Jay Lee
c3ddeae3f3 Make TLS 1.3 the default minimum.
Admins can still downgrade to 1.2 if they need to. Most should not need to.
2022-01-02 14:38:27 -05:00
Jay Lee
a9f0e5ba16 Python 3.6 is EOL, require 3.7 2022-01-02 14:33:04 -05:00
Ross Scroggs
8bf8d45ebe Make gam info crostelemetry <SerialNumber> (#1464)
* Make gam info crostelemetry <SerialNumber>

* Handle missing <SerialNumber> for gam info crostelemetry
2021-12-29 14:48:57 -05:00
Ross Scroggs
1777c762b3 Make print crostelemetry consistent print cros (#1463)
* Make print crostelemetry consistent print cros

Strip newline from cpuStatusReport.cpuTemperature.label
Replace list of label/temperatureCelsius pairs with
cpuStatusReport.cpuTemperature.label = temperaureCelsius

* Document print crostelemetry

* Update GamCommands.txt

* More work on print crostelemetry
2021-12-29 12:23:52 -05:00
Jay Lee
0b1337070e Update gam-install.sh 2021-12-28 12:45:37 -05:00
Jay Lee
b158496bea Update build.yml 2021-12-28 11:16:23 -05:00
Jay Lee
a79b23e090 Update build.yml 2021-12-28 10:55:38 -05:00
Jay Lee
bdb56240f0 Update build.yml 2021-12-28 10:20:45 -05:00
Jay Lee
6dddf3eb30 Update build.yml 2021-12-28 10:20:25 -05:00
Jay Lee
7bd8569151 Update build.yml 2021-12-28 09:57:14 -05:00
Jay Lee
b03c9f1e35 Update build.yml 2021-12-28 09:47:54 -05:00
Jay Lee
057b5ff760 Update build.yml 2021-12-23 13:44:04 -05:00
Jay Lee
ba512b4159 Update build.yml 2021-12-23 13:39:00 -05:00
Jay Lee
a298aea2fe Update build.yml 2021-12-23 13:32:42 -05:00
Jay Lee
f433463074 Update build.yml 2021-12-23 13:24:01 -05:00
Jay Lee
afae08d6fe Update build.yml 2021-12-23 13:16:25 -05:00
Jay Lee
7cf2a08aff Update build.yml 2021-12-23 13:04:22 -05:00
Jay Lee
7df6781985 Update build.yml 2021-12-23 12:39:57 -05:00
Jay Lee
ae0f5e62e3 Update build.yml 2021-12-23 12:36:43 -05:00
Jay Lee
14c8356c6b Update build.yml 2021-12-23 12:23:37 -05:00
Jay Lee
45ffd4a793 Update build.yml 2021-12-23 12:16:37 -05:00
Jay Lee
eb8d39025e Update build.yml 2021-12-23 12:11:36 -05:00
Jay Lee
1f739e1c63 Update build.yml 2021-12-23 09:11:12 -05:00
Jay Lee
82111236fb Update build.yml 2021-12-23 08:32:32 -05:00
Jay Lee
813a94f8d6 Update build.yml 2021-12-23 08:28:15 -05:00
Jay Lee
e83b75e2c3 Update build.yml 2021-12-23 08:24:08 -05:00
Jay Lee
ce1e880ed0 Update build.yml 2021-12-23 08:19:11 -05:00
Jay Lee
427672065e Update build.yml 2021-12-23 07:24:25 -05:00
Jay Lee
055c5d5e54 Update build.yml 2021-12-22 20:22:05 -05:00
Jay Lee
4de7794e04 Update build.yml 2021-12-22 20:18:46 -05:00
Jay Lee
79686fd8ce Update build.yml 2021-12-22 20:06:18 -05:00
Jay Lee
cc5df0198b Update build.yml 2021-12-22 19:43:50 -05:00
Jay Lee
abc6e55ba7 Update build.yml 2021-12-22 19:31:03 -05:00
Jay Lee
0c8afb7fd6 Update build.yml 2021-12-22 19:26:36 -05:00
Jay Lee
c0c2cca44e Update build.yml 2021-12-22 19:01:16 -05:00
Jay Lee
faa645cb97 Update build.yml 2021-12-22 18:59:29 -05:00
Jay Lee
725c19aafc Update build.yml 2021-12-22 18:49:19 -05:00
Jay Lee
cc3b4c974d Update build.yml 2021-12-22 18:45:23 -05:00
Jay Lee
6ce64fad72 Update build.yml 2021-12-22 18:39:01 -05:00
Jay Lee
c1af67d4a3 Update build.yml 2021-12-22 18:35:59 -05:00
Jay Lee
802cb15007 Update requirements.txt 2021-12-22 16:46:04 -05:00
Jay Lee
b34bf3e56a Update linux-before-install.sh 2021-12-22 15:58:08 -05:00
Jay Lee
bf37700088 Update build.yml 2021-12-22 15:55:47 -05:00
Jay Lee
4a43ddfc25 Update build.yml 2021-12-22 15:51:03 -05:00
Jay Lee
650a1f5154 Update build.yml 2021-12-22 15:43:24 -05:00
Jay Lee
5eda7e30b0 Update build.yml 2021-12-22 13:10:09 -05:00
Jay Lee
8a26f547e5 Update build.yml 2021-12-22 10:25:41 -05:00
Jay Lee
343088913f Update build.yml 2021-12-22 09:56:08 -05:00
Jay Lee
5a0272fd5b Merge branch 'main' of github.com:jay0lee/GAM 2021-12-22 09:54:29 -05:00
Jay Lee
dc93503625 CrOS Telemetry API 2021-12-22 09:52:48 -05:00
Ross Scroggs
6ea6c0889b Fix show filelist query issue; add driveId to drive file fields (#1461)
* Fix show filelist query issue

If the user says: query "A or B" this becomes "'me' in owners and A or B" which is the same as "('me' in owners and A) or B" which gives incorrect results. The fix makes "'me' in owners and (A or B)"

* Add driveId to list of drive file fields
2021-12-17 11:51:32 -05:00
Jay Lee
99ab72df3f GAM 6.12 2021-12-16 07:41:30 -05:00
Ross Scroggs
99bda1385e languages update; fields for gam info user; cloud identity groups update to v1 (#1459)
* languages update

The API doesn't return languages unless you specifically mention in in fields list

* languages cleanup in print users

* Add fields to gam info user

* No up for languages

* Use v1 for Cloud Identity groups; fix bug in print cigroups member

* It's an error to set preference on custom language
2021-12-16 07:40:08 -05:00
Jay Lee
7ce3b4a8c0 Update build.yml 2021-12-15 12:06:46 -05:00
Jay Lee
495722d0d6 Update build.yml 2021-12-14 20:20:09 -05:00
Jay Lee
aca31be5d7 Update build.yml 2021-12-14 19:53:11 -05:00
Jay Lee
b9b7ae8d99 Merge branch 'main' of github.com:jay0lee/GAM 2021-12-09 15:27:38 -05:00
Jay Lee
0d46c1d13a Set user preferred language 2021-12-09 15:07:57 -05:00
Ross Scroggs
6b63ecdc19 Add limittoou <OrgUnitPath> to print users to allow selection of OU w/o children (#1455) 2021-12-09 10:35:47 -05:00
Jay Lee
f9ca0323a1 Update build.yml 2021-12-07 08:18:03 -05:00
Jay Lee
c50aa4d2e8 Update build.yml 2021-12-06 16:27:53 -05:00
Jay Lee
a72ded9079 wipe cache 2021-11-24 11:17:41 -05:00
Jay Lee
cbabbee075 wipe cache 2021-11-24 11:15:24 -05:00
Jay Lee
f55a344b7a Update build.yml 2021-11-23 10:48:03 -05:00
Jay Lee
d84f8418ff Update build.yml 2021-11-23 10:39:56 -05:00
Jay Lee
30c5e92de6 Update build.yml 2021-11-23 10:15:54 -05:00
Jay Lee
5f618a7f65 Update build.yml 2021-11-23 08:41:50 -05:00
Jay Lee
3e833419db Update README.md 2021-11-23 08:33:53 -05:00
Jay Lee
0d94bae0b5 Update README.md 2021-11-23 08:33:40 -05:00
Jay Lee
f5dec96ffb Update README.md 2021-11-23 08:31:32 -05:00
Jay Lee
e91d12caaf Update macos-install.sh 2021-11-23 08:26:12 -05:00
Jay Lee
fd5a1faa58 Update gam.spec 2021-11-22 16:31:44 -05:00
Jay Lee
90a9212793 Update build.yml 2021-11-22 16:28:10 -05:00
Jay Lee
7e582ac1fc Update build.yml 2021-11-22 16:19:34 -05:00
Jay Lee
65a740569c Update build.yml 2021-11-22 16:16:28 -05:00
Jay Lee
a47ef0e1f5 Update build.yml 2021-11-22 15:41:05 -05:00
Jay Lee
b75ad006f1 Update build.yml 2021-11-22 08:26:33 -05:00
Jay Lee
dbc3f0cd83 Update var.py 2021-11-22 08:16:32 -05:00
Jay Lee
ea2750f970 Merge branch 'main' of https://github.com/jay0lee/GAM into main 2021-11-22 08:08:33 -05:00
Jay Lee
a2eb5a2483 Correct certificate not before value to UTC-1h. Fixes #1453 2021-11-22 08:08:20 -05:00
Ross Scroggs
54178543d6 Fix Row Filtering Part 3 (#1450)
Graak! Why I can't get my code translated into yours is beyond me; this time for sure.
2021-11-21 20:25:09 -05:00
Jay Lee
5436f21bc0 Use OpenSSL 3.0.0 in builds 2021-10-29 18:12:30 -04:00
Ross Scroggs
839768a2a5 Fix error handling (#1447) 2021-10-29 13:04:59 -04:00
Jay Lee
2e195d5aa1 Update build.yml 2021-10-29 11:05:57 -04:00
Ross Scroggs
66811f8eb5 Fix Row Filtering Part 2 (#1446)
```
Row Filtering
There can be multiple filters, a filter can match multiple columns (wildcard).
The semantics should be:
For row keep filters, if all filters match, the row is kept.
For row drop filters, if any filter matches, the row is dropped.

For an individual filter that specifies multiple columns, there is a match if any column matches.

Prior to PR 1433, the semantics for keep/drop were reversed; the semantics for multiple columns was correct.

PR 1433 corrected the semantics for keep/drop but broke the semantics for multiple columns.

This PR corrects the semantics for multiple columns.
```
2021-10-29 10:24:12 -04:00
Jay Lee
a92326790d Update build.yml 2021-10-29 10:19:09 -04:00
Ross Scroggs
d405767fb0 Update requirements.txt to get latest library versions (#1444)
* Update requirements.txt

* Revert "Update requirements.txt"

This reverts commit f89f66d44c.

* Update to fixed google oauth library
2021-10-26 14:45:34 -04:00
Ross Scroggs
8d7c6d3835 MacOS codesign fix no longer needed; MacOS 12 = Monterey (#1441)
* Updated 3.9 to 3.10, is this still needed?

* Fix no longer required

* MacOS 12 is Monterey
2021-10-26 12:56:47 -04:00
Jay Lee
e362591b7a pin google-auth to 2.0.2
Need https://github.com/googleapis/google-auth-library-python/issues/889 fixed.
2021-10-21 19:32:39 -04:00
Jay Lee
ee5f4b73e8 Update var.py 2021-10-21 18:43:34 -04:00
Jay Lee
0d15eb2898 Workaround Python 3.10.0 CSV escape issue. Fixes #1437 2021-10-21 10:41:20 -04:00
Jay Lee
4af50206ad need lists to repro 2021-10-21 08:19:32 -04:00
Jay Lee
c596937006 Update build.yml 2021-10-21 08:13:17 -04:00
Jay Lee
17eb61e1eb Merge branch 'main' of https://github.com/jay0lee/GAM into main 2021-10-21 08:06:39 -04:00
Jay Lee
a333185e84 repro issue #1438 2021-10-21 08:06:26 -04:00
Jay Lee
f6863ae2d6 Update var.py 2021-10-20 13:57:11 -04:00
Ross Scroggs
36830250b5 Handle spurious Google error when enabling project APIs (#1436) 2021-10-20 13:48:41 -04:00
Jay Lee
4ca1c3537b Merge branch 'main' of https://github.com/jay0lee/GAM into main 2021-10-18 08:50:25 -04:00
Jay Lee
eeab09eacb fix deprecated package in a_atleast_b.py 2021-10-18 08:50:13 -04:00
Ross Scroggs
af16967257 Fix Row Filtering (#1433)
When multiple filter expressions are defined:
GAM_CSV_ROW_FILTER - should match only if all expressions match
GAM_CSV_ROW_DROP_FILTER - should match if any expression matches

Currently, the opposite is true
2021-10-14 20:12:51 -04:00
Jay Lee
75e2bf5a9a Update build.yml 2021-10-14 19:22:57 -04:00
Ross Scroggs
4db3bc409b Document member restrictions; fix print users (#1430)
* Document member restrictions

* Fix gam print users allfields custom all to include primaryEmail

If you really want everything say: gam print users full
2021-10-06 14:22:27 -04:00
Jay Lee
32ccf414ea Update gam-install.sh 2021-10-06 08:01:29 -04:00
Jay Lee
615e48fffc Update gam-install.sh 2021-10-05 20:18:07 -04:00
Jay Lee
93bf3fce29 Merge branch 'main' of https://github.com/jay0lee/GAM into main 2021-10-05 18:05:52 -04:00
Jay Lee
899601569a Group member restrictions 2021-10-05 18:05:28 -04:00
Jay Lee
b1805b64a2 Update build.yml 2021-10-05 17:58:20 -04:00
Jay Lee
58190343b1 Update linux-install.sh 2021-10-05 16:49:53 -04:00
Jay Lee
99d48b1939 Update linux-before-install.sh 2021-10-05 16:49:36 -04:00
Jay Lee
82b66d53cb Update linux-install.sh 2021-10-05 09:08:55 -04:00
Ross Scroggs
3200de56cc Several fixes/updates (#1426)
* agreedToTerms is now read-only

* Fix sync devices

* assetTag if specified is part of sync device key

* Handle missing assetTags

* Leave agreedtoterms as an undocumented option

* More assetTag processing, the field is not returned from the API if it's empty

* Fix DriveFileAttribute formatting

* memberKey has been replaced by preferredMemberKey

* Correct license name

* If notdemail.txt is present, write_csv_file will not send an email
2021-10-05 08:37:09 -04:00
Jay Lee
0a627d5c79 Update build.yml 2021-10-05 08:29:19 -04:00
Jay Lee
22399deb79 Update build.yml 2021-10-05 08:22:59 -04:00
Jay Lee
6a77617e3b Update build.yml 2021-10-04 18:22:32 -04:00
Jay Lee
2868ef99ae Update build.yml 2021-10-04 18:11:54 -04:00
Jay Lee
21557f9892 Update linux-install.sh 2021-09-30 18:54:46 -04:00
Jay Lee
d2385ae62d Update linux-before-install.sh 2021-09-30 18:54:19 -04:00
Jay Lee
a84efef389 Update build.yml 2021-09-30 18:51:55 -04:00
Jay Lee
310bcd1585 Merge branch 'main' of https://github.com/jay0lee/GAM into main 2021-09-27 08:21:54 -04:00
Jay Lee
753f44deb2 Fix some missing types in cbcm JSON, formatting 2021-09-27 08:21:08 -04:00
Jay Lee
df1f0f8f09 Update build.yml 2021-09-16 08:15:21 -04:00
Jay Lee
45e1b50674 Update build.yml 2021-09-10 14:41:08 -04:00
Jay Lee
0a2b048fb1 Update build.yml 2021-09-10 14:40:21 -04:00
Ross Scroggs
e3c5dca09d Three updates (#1421)
* Initialize pageToken for each namespace

* Update group sync to do removes before adds

This gets around problem when  a group contains a primary address and a sync is performed with an alias. With adds first you get a duplicate error; with removes first the primary address in the group is replaced with the alias.

* Add defaultsender to group settings
2021-09-09 13:06:50 -04:00
Jay Lee
88339b7214 Update build.yml 2021-08-31 13:59:04 -04:00
Jay Lee
1f2bb18bc1 GAM 6.08 2021-08-31 13:58:04 -04:00
Jay Lee
74977a6154 Update build.yml 2021-08-31 10:49:42 -04:00
Jay Lee
00413fe7a4 Update build.yml 2021-08-31 10:46:03 -04:00
Jay Lee
9bb9d331ad Update build.yml 2021-08-31 09:58:07 -04:00
Jay Lee
f022ffdff4 Update build.yml 2021-08-31 09:25:13 -04:00
Jay Lee
28dade2a34 Update build.yml 2021-08-31 09:18:23 -04:00
Jay Lee
7378b9d843 Update build.yml 2021-08-31 09:10:22 -04:00
Jay Lee
71075e95bf Update build.yml 2021-08-31 08:58:45 -04:00
Janosh Riebesell
108990cf06 Fix pip license error + add pip install command to readme (#1419)
* fix pip license error, add pip install to readme

* fix warning: the 'license_file' option is deprecated, use 'license_files' instead
2021-08-31 08:51:51 -04:00
Jay Lee
ebfdf4b052 Update build.yml 2021-08-31 08:49:55 -04:00
Jay Lee
dbf4073216 fix gam.py also 2021-08-27 12:10:54 -04:00
Jay Lee
83214eaaf8 attempt fixes for pip installable 2021-08-27 12:10:15 -04:00
Janosh Riebesell
1100fdd456 Make GAM pip-installable (#1417)
* wip: make pip-installable

* resolve @jay0lee's comments
2021-08-27 11:24:02 -04:00
Jay Lee
481bfa5440 Update build.yml 2021-08-27 10:16:34 -04:00
Jay Lee
30282c7fbb OpenSSL 1.1.1l not "i" 2021-08-27 09:44:01 -04:00
Jay Lee
382bc71b21 Update build.yml 2021-08-24 14:58:58 -04:00
Ross Scroggs
f3fba97652 Add shortcutDetails to drive file fields (#1413) 2021-08-23 16:05:15 -04:00
Yaroslav Nakonechnikov
7f51e35bd4 Pathvalidate (#1408)
* Update requirements.txt

Adding `pathvalidate` to requrements

* Update __init__.py

Adding `pathvalidate` to make  correct filename on other then ascii encodings.

* Updating with sanitize_filename

* Removing unused variable.
2021-08-23 16:04:11 -04:00
Ross Scroggs
95beb8e62a Update getting MacOS version (#1409) 2021-08-14 16:59:07 -04:00
Ross Scroggs
1a9de867f9 Work around API restriction that roleId and userKey are mutually exclusive (#1406) 2021-08-10 06:30:52 -04:00
Jay Lee
b42946bbe1 Update build.yml 2021-08-04 17:02:24 -04:00
Jay Lee
40b2fd09ff small service account improvements 2021-08-04 16:58:07 -04:00
Jay Lee
a3d560a8a2 YubiKey improvements and PIV reset 2021-07-27 09:24:34 -04:00
Jay Lee
ed20fe252e Use with conn so Yubikey connections close sooner 2021-07-26 14:46:58 -04:00
Jay Lee
375e36ff96 State what we don't like about invalid JSON 2021-07-26 14:45:26 -04:00
Jay Lee
e7108b108e Update build.yml 2021-07-23 13:31:06 -04:00
Jay Lee
6d59daad19 Update build.yml 2021-07-23 13:27:53 -04:00
Jay Lee
21c693921b Update build.yml 2021-07-23 13:13:34 -04:00
Jay Lee
7bcd5fbed7 Update build.yml 2021-07-23 13:06:22 -04:00
Jay Lee
7104970e17 Update build.yml 2021-07-23 13:01:20 -04:00
Jay Lee
1a2950b580 Update build.yml 2021-07-23 12:59:34 -04:00
Jay Lee
085b24e1c5 Update build.yml 2021-07-23 12:55:39 -04:00
Jay Lee
8688ce6328 Update build.yml 2021-07-23 12:52:35 -04:00
Jay Lee
fbdfed81e7 Update build.yml 2021-07-23 12:49:21 -04:00
Ross Scroggs
94fe20607e Updates for CRM v3 changes (#1401) 2021-07-22 19:19:10 -04:00
Ross Scroggs
6c62483e8e Updates for CRM v3 changes (#1400) 2021-07-21 17:27:46 -04:00
Ross Scroggs
54689129c6 Update gam print|show|update chromepolicy to handle the following special case policies: (#1399)
```
chrome.users.AutoUpdateCheckPeriodNew autoupdatecheckperiodminutesnew
chrome.users.BrowserSwitcherDelayDuration browserswitcherdelayduration
chrome.users.FetchKeepaliveDurationSecondsOnShutdown fetchkeepalivedurationsecondsonshutdown
chrome.users.MaxInvalidationFetchDelay maxinvalidationfetchdelay
chrome.users.PrintingMaxSheetsAllowed printingmaxsheetsallowednullable
chrome.users.PrintJobHistoryExpirationPeriodNew printjobhistoryexpirationperioddaysnew
chrome.users.SecurityTokenSessionSettings securitytokensessionnotificationseconds
chrome.users.SessionLength sessiondurationlimit
chrome.users.UpdatesSuppressed updatessuppresseddurationmin
chrome.users.UpdatesSuppressed updatessuppressedstarttime
```
2021-07-20 17:25:17 -04:00
Ross Scroggs
e9e8dd5a82 Fix call to be compatible with CRM v3 (#1398) 2021-07-19 19:31:08 -04:00
Jay Lee
00e764b118 Migrate to Resource Manager API v3 2021-07-16 10:14:58 -04:00
Jay Lee
cee7eb970a Merge branch 'main' of https://github.com/jay0lee/GAM 2021-07-13 10:45:21 -04:00
Jay Lee
daed17fac8 exclude null character, max out passwd length on random 2021-07-13 10:44:58 -04:00
Ross Scroggs
8708f4f93f Fix page_args_in_body, update namespace handling in show chromepolicies (#1393)
When page_args_in_body is true you have to add body to kwargs to ensure a place for pageToken

Allow setting a list of namespaces that override the defaults for printerid (not likely) and appid.
2021-07-08 08:55:18 -04:00
Jay Lee
c7c1bfbeba retry wait for mailbox if user doesn't exist 2021-07-07 11:03:04 -04:00
Jay Lee
0418438b6f increase rounds to Google max 2021-07-07 10:53:34 -04:00
Jay Lee
a2ea4d036e improve random password generator 2021-07-07 10:47:34 -04:00
Jay Lee
dc7a29908f updates to allow listing/setting extension policy 2021-07-02 13:36:21 -04:00
Jay Lee
794db5d2a4 More APIs now work with discovery v2 URL 2021-07-01 14:47:40 -04:00
Jay Lee
e5f9db129b Improve printing of app/extension/printer policy 2021-06-30 11:18:29 -04:00
Jay Lee
a6aecf4e9d undo version in exe 2021-06-29 11:22:33 -04:00
Jay Lee
b59bc4ec90 Merge branch 'main' of https://github.com/jay0lee/GAM 2021-06-29 11:04:17 -04:00
Jay Lee
41920f7865 add version info to Windows exe 2021-06-29 11:02:44 -04:00
Jay Lee
4630bf5681 Update var.py 2021-06-29 08:13:59 -04:00
Ross Scroggs
1c78ebd20e Add groupidfllert <String> to gam report <ActivityApplicationName> (#1390) 2021-06-28 21:34:50 -04:00
Jay Lee
80d17cfda3 Update windows-install.sh 2021-06-28 17:28:10 -04:00
Jay Lee
a154007927 Update windows-install.sh 2021-06-28 17:22:57 -04:00
Jay Lee
bd8274cc27 Update windows-install.sh 2021-06-28 17:13:42 -04:00
Jay Lee
fb08991c05 Update windows-before-install.sh 2021-06-28 17:06:47 -04:00
Jay Lee
7c1f06fdf7 Update build.yml 2021-06-28 16:57:11 -04:00
Jay Lee
93b38b9f95 Update build.yml 2021-06-28 16:44:40 -04:00
Jay Lee
7ffc97d301 Update build.yml 2021-06-28 16:40:43 -04:00
Jay Lee
280301f258 Update build.yml 2021-06-28 16:36:04 -04:00
Jay Lee
40daf38f80 Update build.yml 2021-06-28 16:32:41 -04:00
Jay Lee
d24925cd5f Update build.yml 2021-06-28 16:30:55 -04:00
Ross Scroggs
cd42d54b43 Fix typo, document new drive fields (#1389)
* Fix typo, document new drive fields

* Document new drive attribute
2021-06-28 15:40:22 -04:00
Jay Lee
53d8ecb6bc Update build.yml 2021-06-28 15:39:47 -04:00
Jay Lee
98e87d0297 Update build.yml 2021-06-28 15:38:04 -04:00
Jay Lee
400b4af769 Update macos-before-install.sh 2021-06-28 15:35:40 -04:00
Jay Lee
368701afb1 Update build.yml 2021-06-28 15:33:58 -04:00
Jay Lee
a501b89ecd resource key support 2021-06-25 16:52:14 -04:00
Jay Lee
91cddd72e5 Update build.yml 2021-06-17 14:54:09 -04:00
Jay Lee
8a1f0c9dbf GAM 6.06 2021-06-17 14:47:47 -04:00
Jay Lee
e3e5318b4f Update build.yml 2021-06-17 14:33:08 -04:00
Jay Lee
b060664c9f Update build.yml 2021-06-17 14:29:38 -04:00
Jay Lee
83fbf0e8ac Update build.yml 2021-06-17 14:23:13 -04:00
44 changed files with 1222 additions and 532 deletions

View File

@@ -55,7 +55,7 @@ else
tar xf openssl-$BUILD_OPENSSL_VERSION.tar.gz
cd openssl-$BUILD_OPENSSL_VERSION
echo "Compiling OpenSSL $BUILD_OPENSSL_VERSION..."
./config shared --prefix=$HOME/ssl
./Configure --libdir=lib --prefix=$HOME/ssl
echo "Running make for OpenSSL..."
make -j$cpucount -s
echo "Running make install for OpenSSL..."
@@ -70,7 +70,7 @@ else
cd Python-$BUILD_PYTHON_VERSION
echo "Compiling Python $BUILD_PYTHON_VERSION..."
safe_flags="--with-openssl=$HOME/ssl --enable-shared --prefix=$HOME/python --with-ensurepip=upgrade"
unsafe_flags="--enable-optimizations --with-lto"
unsafe_flags="--enable-optimizations --with-lto --with-openssl=~/ssl --with-openssl-rpath=~~/ssl/lib"
if [ ! -e Makefile ]; then
echo "running configure with safe and unsafe"
./configure $safe_flags $unsafe_flags > /dev/null
@@ -94,19 +94,9 @@ else
python=~/python/bin/python3
pip=~/python/bin/pip3
if ([ "${ImageOS}" == "ubuntu16" ]) && [ "${HOSTTYPE}" == "x86_64" ]; then
echo "Installing deps for StaticX..."
if [ ! -d patchelf-$PATCHELF_VERSION ]; then
echo "Downloading PatchELF $PATCHELF_VERSION"
wget https://github.com/NixOS/patchelf/archive/$PATCHELF_VERSION.tar.gz
tar xf $PATCHELF_VERSION.tar.gz
cd patchelf-$PATCHELF_VERSION/
./bootstrap.sh
./configure
make
sudo make install
fi
$pip install staticx
if ([ "${ImageOS}" == "ubuntu20" ]) && [ "${HOSTTYPE}" == "x86_64" ]; then
"${python}" -m pip install --upgrade patchelf-wrapper
"${python}" -m pip install --upgrade staticx
fi
cd $whereibelong

View File

@@ -17,10 +17,10 @@ tar -C ${distpath} --create --file $GAM_ARCHIVE --xz gam
echo "PyInstaller GAM info:"
du -h $gam
time $gam version extended
if ([ "${ImageOS}" == "ubuntu16" ]) && [ "${HOSTTYPE}" == "x86_64" ]; then
if ([ "${ImageOS}" == "ubuntu20" ]) && [ "${HOSTTYPE}" == "x86_64" ]; then
GAM_LEGACY_ARCHIVE=gam-${GAMVERSION}-${GAMOS}-${PLATFORM}-legacy.tar.xz
$python -OO -m staticx -l /lib/x86_64-linux-gnu/libresolv.so.2 -l /lib/x86_64-linux-gnu/libnss_dns.so.2 $gam $gam-staticx
strip $gam-staticx
$python -OO -m staticx $gam $gam-staticx
#strip $gam-staticx
rm $gampath/gam
mv $gam-staticx $gam
chmod 755 $gam

View File

@@ -22,18 +22,14 @@ cd ~
# Use official Python.org version of Python which is backwards compatible
# with older MacOS versions
if [ "$PLATFORM" == "x86_64" ]; then
export pyfile=python-$BUILD_PYTHON_VERSION-macosx10.9.pkg
else
export pyfile=python-$BUILD_PYTHON_VERSION-macos11.pkg
fi
export pyfile=python-$BUILD_PYTHON_VERSION-macos11.pkg
wget https://www.python.org/ftp/python/$BUILD_PYTHON_VERSION/$pyfile
echo "installing Python $BUILD_PYTHON_VERSION..."
sudo installer -pkg ./$pyfile -target /
# This fixes https://github.com/pyinstaller/pyinstaller/issues/5062
codesign --remove-signature /Library/Frameworks/Python.framework/Versions/3.9/Python
#codesign --remove-signature /Library/Frameworks/Python.framework/Versions/3.10/Python
#if [ ! -f python-$MIN_PYTHON_VERSION-macosx10.9.pkg ]; then
# wget --quiet https://www.python.org/ftp/python/$MIN_PYTHON_VERSION/python-$MIN_PYTHON_VERSION-macosx10.9.pkg
@@ -58,8 +54,8 @@ cd ~
export python=/usr/local/bin/python3
export pip=/usr/local/bin/pip3
SSLVER=$($openssl version)
SSLRESULT=$?
#SSLVER=$($openssl version)
#SSLRESULT=$?
PYVER=$($python -V)
PYRESULT=$?

View File

@@ -7,7 +7,7 @@ export distpath="dist/"
export gampath="${distpath}gam"
rm -rf $gampath
export specfile="gam.spec"
$python -OO -m PyInstaller --clean --noupx --strip --distpath "${gampath}" --target-architecture $PLATFORM "${specfile}"
$python -OO -m PyInstaller --distpath "${gampath}" "${specfile}"
export gam="${gampath}/gam"
$gam version extended
export GAMVERSION=`$gam version simple`

View File

@@ -13,8 +13,8 @@ echo "This is a ${BITS}-bit build for ${PLATFORM}"
export mypath=$(pwd)
cd ~
export python="python"
export pip="pip"
export python="c:\python\python.exe"
export pip="c:\python\scripts\pip.exe"
# pyscard needs swig, keep these two together
choco install $CHOCOPTIONS swig

View File

@@ -7,9 +7,7 @@ echo "compiling GAM with pyinstaller..."
export distpath="dist/"
export gampath="${distpath}gam"
rm -rf $gampath
#mkdir -p $gampath
#export gampath=$(readlink -e $gampath)
pyinstaller --clean --noupx --distpath $gampath gam.spec
/c/python/scripts/pyinstaller --clean --noupx --distpath $gampath gam.spec
export gam="${gampath}/gam"
echo "running compiled GAM..."
$gam version

View File

@@ -12,13 +12,11 @@ defaults:
working-directory: src
env:
BUILD_PYTHON_VERSION: "3.9.5"
MIN_PYTHON_VERSION: "3.9.5"
BUILD_OPENSSL_VERSION: "1.1.1k"
MIN_OPENSSL_VERSION: "1.1.1k"
PATCHELF_VERSION: "0.12"
# PYINSTALLER_VERSION can be full commit hash or version like v4.20
PYINSTALLER_VERSION: "e5dbb051bd3d53d6c2c70cbd87270eec1765da2e"
BUILD_PYTHON_VERSION: "3.10.1"
MIN_PYTHON_VERSION: "3.10.1"
BUILD_OPENSSL_VERSION: "3.0.1"
MIN_OPENSSL_VERSION: "1.1.1l"
PATCHELF_VERSION: "0.13"
jobs:
build:
@@ -26,69 +24,61 @@ jobs:
strategy:
matrix:
include:
- os: ubuntu-16.04
- os: ubuntu-18.04
jid: 1
goal: "build"
gamos: "linux"
platform: "x86_64"
- os: ubuntu-18.04
- os: ubuntu-20.04
jid: 2
goal: "build"
gamos: "linux"
platform: "x86_64"
- os: ubuntu-20.04
jid: 3
goal: "build"
gamos: "linux"
platform: "x86_64"
- os: macos-10.15
jid: 4
goal: "build"
gamos: "macos"
platform: "x86_64"
- os: macos-11.0
jid: 12
jid: 3
goal: "build"
gamos: "macos"
platform: "universal2"
- os: windows-2019
- os: windows-2022
jid: 4
goal: "build"
gamos: "windows"
pyarch: "x64"
platform: "x86_64"
- os: windows-2022
jid: 5
goal: "build"
gamos: "windows"
python: 3.9.5
pyarch: "x64"
platform: "x86_64"
- os: windows-2019
jid: 6
goal: "build"
gamos: "windows"
platform: "x86"
python: 3.9.5
pyarch: "x86"
- os: ubuntu-20.04
goal: "test"
python: "3.6"
jid: 7
gamos: "linux"
platform: "x86_64"
- os: ubuntu-20.04
goal: "test"
python: "3.7"
jid: 8
jid: 6
gamos: "linux"
platform: "x86_64"
- os: ubuntu-20.04
goal: "test"
python: "3.8"
jid: 9
jid: 7
gamos: "linux"
platform: "x86_64"
- os: ubuntu-20.04
goal: test
python: "3.10.0-beta.1"
jid: 10
python: "3.9"
jid: 8
gamos: linux
platform: x86_64
- os: [self-hosted, linux, arm64]
jid: 9
goal: "self-build"
platform: "aarch64"
gamos: linux
- os: [self-hosted, linux, arm]
jid: 10
goal: "self-build"
platform: "armv7l"
gamos: linux
steps:
@@ -104,7 +94,7 @@ jobs:
path: |
~/python
~/ssl
key: ${{ matrix.os }}-${{ matrix.jid }}-20210611
key: ${{ matrix.os }}-${{ matrix.jid }}-20220111-03
- name: Set env variables
env:
@@ -119,15 +109,42 @@ jobs:
echo "PLATFORM=${PLATFORM}" >> $GITHUB_ENV
uname -a
- name: Use pre-compiled Python for testing and Windows
- name: Use pre-compiled Python for testing
if: matrix.python != ''
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python }}
architecture: ${{ matrix.pyarch }}
- name: Set env variables for pre-compiled Python
- name: Install Python on Windows
if: matrix.os == 'windows-2022'
run: |
if ( ${Env:PLATFORM} -eq "x86_64" )
{
Set-Variable -name py_arch -value "-amd64"
}
else
{
Set-Variable -name py_arch -value ""
}
Write-Output "py_arch: $py_arch"
Set-Variable -name python_file -value "python-${Env:BUILD_PYTHON_VERSION}${py_arch}.exe"
Write-Output "python_file: $python_file"
Set-Variable -name python_url -value "https://www.python.org/ftp/python/${Env:BUILD_PYTHON_VERSION}/${python_file}"
Write-Output "python_url: $python_url"
Invoke-WebRequest -Uri $python_url -OutFile $python_file
Start-Process -wait -FilePath $python_file -ArgumentList "/quiet","InstallAllUsers=0","TargetDir=c:\\python","AssociateFiles=1","PrependPath=1"
shell: pwsh
- name: Install packages for test
if: matrix.goal == 'test'
run: |
echo "RUNNING: apt update..."
sudo apt-get -qq --yes update > /dev/null
sudo apt-get -qq --yes install swig libpcsclite-dev
- name: Set env variables for pre-compiled Python
if: matrix.goal != 'build'
run: |
export python=$(which python3)
export pip=$(which pip3)
@@ -138,44 +155,84 @@ jobs:
echo "pip=${pip}" >> $GITHUB_ENV
echo "gam=${gam}" >> $GITHUB_ENV
echo "gampath=${gampath}" >> $GITHUB_ENV
echo "RUNNING: apt update..."
sudo apt-get -qq --yes update > /dev/null
sudo apt-get -qq --yes install swig libpcsclite-dev
$pip install --upgrade pip
"${python}" -V
"${pip}" -V
- name: Build and install Python, OpenSSL and PyInstaller
if: matrix.goal != 'test' && steps.cache-primes.outputs.cache-hit != 'true'
- name: Build and install Python and OpenSSL
if: matrix.goal == 'build' && steps.cache-primes.outputs.cache-hit != 'true'
run: |
set +e
source ../.github/actions/${GAMOS}-before-install.sh
echo "PATH=$PATH" >> $GITHUB_ENV # keep gnutools for MacOS
echo "python=$python" >> $GITHUB_ENV
echo "pip=$pip" >> $GITHUB_ENV
echo "LD_LIBRARY_PATH=$LD_LIBRARY_PATH" >> $GITHUB_ENV
echo -e "Python: $python\nPip: $pip\nLD_LIB...: $LD_LIBRARY_PATH"
$pip install wheel
export url="https://codeload.github.com/pyinstaller/pyinstaller/tar.gz/${PYINSTALLER_VERSION}"
echo "Downloading ${url}"
curl -o pyinstaller.tar.gz --compressed "${url}"
tar xf pyinstaller.tar.gz
cd "pyinstaller-${PYINSTALLER_VERSION}/bootloader"
if [ "${PLATFORM}" == "x86" ]; then
TARGETARCH="--target-arch=32bit"
else
TARGETARCH=""
if [ $GAMOS == "macos" ]; then
brew install openssl@1.1 rust
export LDFLAGS="-L$(brew --prefix openssl@1.1)/lib"
export pipoptions='--no-binary :all:'
echo "PATH=$PATH" >> $GITHUB_ENV # keep gnutools for MacOS
export MACOSX_DEPLOYMENT_TARGET="10.9"
export CFLAGS="-arch arm64 -arch x86_64 -I$(brew --prefix openssl@1.1)/include"
echo "pipoptions=${pipoptions}" >> $GITHUB_ENV
echo "MACOSX_DEPLOYMENT_TARGET=${MACOSX_DEPLOYMENT_TARGET}" >> $GITHUB_ENV
echo "CFLAGS=${CFLAGS}" >> $GITHUB_ENV
echo "LDFLAGS=${LDFLAGS}" >> $GITHUB_ENV
fi
#$python ./waf all $TARGETARCH
cd ..
$python setup.py install
#$pip install pyinstaller
echo -e "Python: $python\nPip: $pip\nLD_LIB...: $LD_LIBRARY_PATH"
$pip install --upgrade pip $pipoptions
$pip install --upgrade wheel $pipoptions
- name: Install pip requirements
if: matrix.os != 'self-hosted'
- name: Set Windows Powershell env variables
if: matrix.goal != 'test' && matrix.os == 'windows-2022' && matrix.platform == 'x86_64'
shell: powershell
run: |
choco install nasm --no-progress
$env:PATH="$ENV:PATH;c:\Program Files\NASM\"
cmd /c 'call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" && set MAKE=nmake && set > %temp%\vcvars.txt'
Get-Content "$env:temp\vcvars.txt" | Foreach-Object {
if ($_ -match "^(.*?)=(.*)$") {
if ($matches[1] -eq "PATH" -or $matches[1] -eq "PLATFORM") {
continue
}
Set-Content "env:\$($matches[1])" $matches[2]
Add-Content -Path $env:GITHUB_ENV -Value "$($matches[1])=$($matches[2])"
}
}
- name: Install PyInstaller
if: matrix.goal != 'test'
run: |
set +e
$pip list --outdated --format=freeze | grep -v '^\-e' | cut -d = -f 1 | xargs -n1 $pip install -U --force-reinstall
git clone https://github.com/pyinstaller/pyinstaller.git
cd pyinstaller
# use latest pyinstaller tag version
git fetch --tags
git checkout $(git describe --tags $(git rev-list --tags --max-count=1))
# remove pre-compiled bootloaders so we fail if bootloader compile fails
rm -rf PyInstaller/bootloader/*-*/*
cd bootloader
export DefaultWindowsSDKVersion="10.0.20348.0"
if [ "${PLATFORM}" == "x86" ]; then
TARGETARCH="32bit"
else
TARGETARCH="64bit"
fi
if [ $GAMOS == "macos" ]; then
UNIVERsAL="--universal2"
fi
$python ./waf all --target-arch=$TARGETARCH $UNIVERSAL
cat build/config.log
cd ..
$pip install . $pipoptions
$pip install --upgrade -r requirements.txt
- name: Install pip requirements
run: |
set +e
$pip list --outdated --format=freeze | grep -v '^\-e' | cut -d = -f 1 | xargs -n1 $pip install -U --force-reinstall $pipoptions
$pip install --upgrade -r requirements.txt $pipoptions
# yubikey-manager holds cryptography to old version, force upgrade
$pip install --upgrade --no-deps cryptography $pipoptions
- name: Build GAM with PyInstaller
if: matrix.goal != 'test'
@@ -200,6 +257,7 @@ jobs:
- name: Basic Tests build jobs only
if: matrix.goal != 'test'
run: |
$pip install packaging
export vline=$($gam version | grep "Python ")
export python_line=($vline)
export this_python=${python_line[1]}
@@ -212,7 +270,7 @@ jobs:
- name: Live API tests push only
if: github.event_name == 'push' || github.event_name == 'schedule'
env: # Or as an environment variable
env:
PASSCODE: ${{ secrets.PASSCODE }}
run: |
source ../.github/actions/decrypt.sh ../.github/actions/creds.tar.gpg creds.tar
@@ -237,17 +295,19 @@ jobs:
for i in {01..10}; do
echo "${newbase}-bulkuser-$i" >> sample.csv;
done
$gam create user $newuser firstname GHA lastname $JID password random recoveryphone 12125121110 recoveryemail jay0lee@gmail.com gha.jid $JID
$gam create user $newuser firstname GHA lastname $JID password random recoveryphone 12125121110 recoveryemail jay0lee@gmail.com gha.jid $JID languages en+,en-GB-
$gam user $gam_user sendemail recipient $newuser subject "test message $newbase" message "GHA test message"
$gam user $gam_user sendemail recipient exchange@pdl.jaylee.us subject "test ${tstamp}" message "test message"
$gam create group $newgroup name "GHA $JID group" description "This is a description" isarchived true
$gam user $newuser add license gsuitebusiness
$gam update cigroup $newgroup memberrestriction 'member.type == 1 || member.customer_id == groupCustomerId()'
$gam info cigroup $newgroup
$gam user $newuser add license workspaceenterpriseplus
$gam update group $newgroup add owner $gam_user
$gam update group $newgroup add member $newuser
$gam csv sample.csv gam create user ~~email~~ firstname "GHA Bulk" lastname ~~email~~ gha.jid $JID
$gam csv sample.csv gam update user ~~email~~ recoveryphone 12125121110 recoveryemail jay0lee@gmail.com password random
$gam csv sample.csv gam update user ~~email~~ recoveryphone "" recoveryemail ""
$gam csv sample.csv gam user ~email add license gsuitebusiness
$gam csv sample.csv gam user ~email add license workspaceenterpriseplus
$gam csv sample.csv gam user $gam_user sendemail recipient ~~email~~@pdl.jaylee.us subject "test message $newbase" message "GHA test message"
$gam csv sample.csv gam update group $newgroup add member ~email
$gam info group $newgroup
@@ -302,6 +362,7 @@ jobs:
$gam delete building $newbuilding
$gam delete group $newgroup
$gam create alias $newalias user $newuser
$gam user $newuser delete license workspaceenterpriseplus
$gam whatis $newuser
$gam user $gam_user show tokens
$gam print exports matter $matterid | $gam csv - gam download export $matterid id:~~id~~
@@ -315,7 +376,8 @@ jobs:
$gam print browsers
export sn="$JID$JID$JID$JID-$(openssl rand -base64 32 | sed 's/[^a-zA-Z0-9]//g')"
$gam create device serialnumber $sn devicetype android
$gam print cros allfields nolists
$gam print cros allfields orderby serialnumber
#$gam show crostelemetry storagepercentonly
$gam report usageparameters customer
$gam report usage customer parameters gmail:num_emails_sent,accounts:num_1day_logins
$gam report customer todrive

View File

@@ -1,23 +1,38 @@
GAM is a command line tool for Google Workspace (fka G Suite) Administrators to manage domain and user settings quickly and easily.
GAM is a command line tool for Google Workspace admins to manage domain and user settings quickly and easily.
![Build Status](https://github.com/jay0lee/GAM/workflows/Build%20and%20test%20GAM/badge.svg)
# Quick Start
## Linux / MacOS
Open a terminal and run:
```
```sh
bash <(curl -s -S -L https://git.io/install-gam)
```
this will download GAM, install it and start setup.
## Windows
Download the MSI Installer from the [GitHub Releases] page. Install the MSI and you'll be prompted to setup GAM.
# Documentation
The GAM documentation is hosted in the [GitHub Wiki]
# Mailing List / Discussion group
The GAM mailing list / discussion group is hosted on [Google Groups]. You can join the list and interact via email, or just post from the web itself.
# Chat Room
There is a public chat room hosted in Google Chat. [Instructions to join](https://git.io/gam-chat).
# Author
GAM is maintained by <a href="mailto:jay0lee@gmail.com">Jay Lee</a>. Please direct "how do I?" questions to [Google Groups].
GAM is maintained by [Jay Lee](mailto:jay0lee@gmail.com). Please direct "how do I?" questions to [Google Groups].
[GAM release]: https://git.io/gamreleases
[GitHub Releases]: https://github.com/jay0lee/GAM/releases

View File

@@ -204,10 +204,12 @@ If an item contains spaces, it should be surrounded by ".
<MaximumNumberOfSeats> ::= <Number>
<MobileID> ::= <String>
<Name> ::= <String>
<Namespace> ::= <String>
<NotificationID> ::= <String>
<NumberOfSeats> ::= <Number>
<OrgUnitID> ::= <String>
<OrgUnitID> ::= id:<String>
<OrgUnitPath> ::= /|(/<String)+
<OrgUnitItem> ::= <OrgUnitID>|<OrgUnitPath>
<ParameterKey> ::= <String>
<ParameterValue> ::= <String>
<Password> ::= <String>
@@ -221,8 +223,10 @@ If an item contains spaces, it should be surrounded by ".
<QueryContact> ::= <String> See: https://developers.google.com/google-apps/contacts/v3/reference#contacts-query-parameters-reference
<QueryCrOS> ::= <String> See: https://support.google.com/chrome/a/answer/1698333?hl=en
<QueryDriveFile> ::= <String> See: https://developers.google.com/drive/v2/web/search-parameters
<QueryDynamicGroup> ::= <String> See: https://cloud.google.com/identity/docs/reference/rest/v1beta1/groups#dynamicgroupquery
<QueryGmail> ::= <String> See: https://support.google.com/mail/answer/7190
<QueryGroup> ::= <String> See: https://developers.google.com/admin-sdk/directory/v1/guides/search-groups
<QueryMemberRestrictions> ::= <String> See: https://cloud.google.com/identity/docs/reference/rest/v1beta1/SecuritySettings#MemberRestriction
<QueryMobile> ::= <String> See: https://support.google.com/a/answer/7549103
<QueryTeamDrive> ::= <String> See: https://developers.google.com/drive/api/v3/search-shareddrives
<QueryUser> ::= <String> See: https://developers.google.com/admin-sdk/directory/v1/guides/search-users
@@ -312,6 +316,25 @@ If an item contains spaces, it should be surrounded by ".
<CrOSOrderByFieldName> ::=
lastsync|location|notes|serialnumber|status|supportenddate|user
<CrOSTelemetryFieldName> ::=
batteryinfo|
batterystatusreport|
cpuinfo|
cpustatusreport|
customer|
deviceid|
graphicsinfo|
graphicsstatusreport|
memoryinfo|
memorystatusreport|
name|
networkstatusreport|
orgunitid|
osupdatestatus|
serialnumber|
storageinfo|
storagestatusreport
<DriveFieldName> ::=
appdatacontents|
cancomment|
@@ -323,6 +346,7 @@ If an item contains spaces, it should be surrounded by ".
description|
editable|
explicitlytrashed|
driveid|
fileextension|
filesize|
foldercolorrgb|
@@ -333,6 +357,7 @@ If an item contains spaces, it should be surrounded by ".
lastmodifyinguser|
lastmodifyingusername|
lastviewedbyme|lastviewedbymedate|lastviewedbymetime|lastviewedbyuser|
linksharemetadata|
md5|md5checksum|md5sum|
mime|mimetype|
modifiedbyme|modifiedbymedate|modifiedbymetime|modifiedbyuser|
@@ -345,11 +370,13 @@ If an item contains spaces, it should be surrounded by ".
parents|
permissions|
quotabytesused|quotaused|
resourcekey|
restricted|
shareable|
shared|
sharedwithmedate|sharedwithmetime|
sharinguser|
shortcutdetails|
size|
spaces|
starred|
@@ -575,6 +602,7 @@ Items, separated by spaces, with spaces, commas or single quotes in the items th
<CourseStateList> ::= "<CourseState>(,<CourseState>)*"
<CrOSFieldNameList> ::= "<CrOSFieldName>(,<CrOSFieldName>)*"
<CrOSIDList> ::= "<CrOSID>(,<CrOSID>)*"
<CrOSTelemetryFieldNameList> ::= "<CrOSFieldName>(,<CrOSFieldName>)*"
<DriveFileList> ::= "<DriveFileItem>(,<DriveFileItem>)*"
<EmailAddressList> ::= "<EmailAddress>(,<EmailAddress>)*"
<EmailItemList> ::= "<EmailItem>(,<EmailItem>)*"
@@ -588,10 +616,11 @@ Items, separated by spaces, with spaces, commas or single quotes in the items th
<GroupRoleList> ::= "<GroupRole>(,<GroupRole>)*"
<GuardianStateList> ::= "<GuardianState>(,<GuardianState>)*"
<LabelNameList> ::= "<LabelName>(,<LabelName)*"
<LanguageList> ::= "<Language>(,<Language)*"
<LanguageList> ::= "<Language>[+|-](,<Language>[+|-])*"
<MatterItemList> ::= "<MatterItem>(,<MatterItem>)*"
<MembersFieldNameList> ::= "<MembersFieldName>(,<MembersFieldName>)*"
<MobileList> ::= "<MobileId>(,<MobileId>)*"
<NamespaceList> ::= "<Namespace>(,<Namespace)*"
<OrgUnitList> ::= "<OrgUnitPath>(,<OrgUnitPath>)*"
<PrinterIDList> ::= "<PrinterID>)(,<PrinterID>)*"
<ProductIDList> ::= "(<ProductID>|SKUID>)(,<ProductID>|SKUID>)*"
@@ -694,8 +723,10 @@ Specify a collection of Users by directly specifying them or by specifiying item
(contentrestrictions readonly true [reason <String>])|
copyrequireswriterpermission|
(lastviewedbyme <Time>)|(modifieddate|modifiedtime <Time>)|(description <String>)|(mimetype <MimeType>)|
(parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|writerscantshare|writerscanshare
(shortcut <DriveFileID>)
(parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|
(securityupdate <Boolean>)|
(shortcut <DriveFileID>)|
writerscantshare|writerscanshare
<DriveFileUpdateAttribute> ::=
(localfile <FileName>|-)|
(convert)|(ocr)|(ocrlanguage <Language>)|
@@ -704,8 +735,10 @@ Specify a collection of Users by directly specifying them or by specifiying item
(contentrestrictions readonly true [reason <String>])|
(copyrequireswriterpermission <Boolean>)|
(lastviewedbyme <Time>)|(modifieddate <Time>)|(description <String>)|(mimetype <MimeType>)|
(parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|writerscantshare|writerscanshare
(shortcut <DriveFileID>)
(parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|
(securityupdate <Boolean>)|
(shortcut <DriveFileID>)|
writerscantshare|writerscanshare
<GroupSettingsAttribute> ::=
(allowexternalmembers <Boolean>)|
(allowwebposting <Boolean>)|
@@ -713,6 +746,7 @@ Specify a collection of Users by directly specifying them or by specifiying item
(customfootertext <String>)|
(customreplyto <EmailAddress>)|
(defaultmessagedenynotificationtext <String>)|
(defaultsender default_self|group)|
(description <String>)|
(enablecollaborativeinbox|collaborative <Boolean>)|
(includeinglobaladdresslist|gal <Boolean>)|
@@ -790,7 +824,6 @@ Specify a collection of Users by directly specifying them or by specifiying item
field <FieldName> (type bool|date|double|email|int64|phone|string) [multivalued|multivalue] [indexed] [restricted] [range <Number> <Number>] endfield
<UserBasicAttribute> ::=
(agreed2terms|agreedtoterms <Boolean>)|
(changepassword|changepasswordatnextlogin <Boolean>)|
(base64-md5|base64-sha1|crypt|sha|sha1|sha-1|md5|nohash)|
(customerid <String>)|
@@ -843,6 +876,8 @@ An argument containing instances of ~~xxx~~ has xxx replaced by the value of fie
Example: gam csv Users.csv gam update user "~primaryEmail" address type work unstructured "~~Street~~, ~~City~~, ~~State~~ ~~ZIP~~"
Each user (~primaryEmail, e.g. foo@bar.com) would have their work address updated
gam create gcpfolder <String>
gam create project [<EmailAddress>] [<ProjectID>]
gam create project [admin <EmailAddress>] [project <ProjectID>] [parent <String>]
gam use project [<EmailAddress>] [<ProjectID>]
@@ -955,6 +990,7 @@ gam report <ActivityApplicationName> [todrive]
[(user all|<UserItem>)|(orgunit|org|ou <OrgUnitPath>)]
[start <Time>] [end <Time>]
[filter|filters <String>] [event <String>] [ip <String>]
[groupidfilter <String>]
gam create admin <UserItem> <RoleItem> customer|(org_unit <OrgUnitItem>)
gam delete admin <RoleAssignmentId>
@@ -1003,7 +1039,8 @@ gam info customer
gam create datatransfer|transfer <OldOwnerID> <DataTransferServiceList> <NewOwnerID> (<ParameterKey> <ParameterValue>)*
gam info datatransfer|transfer <TransferID>
gam print datatransfers|transfers [todrive] [olduser|oldowner <UserItem>] [newuser|newowner <UserItem>] [status <String>]
gam print datatransfers|transfers [todrive] [olduser|oldowner <UserItem>] [newuser|newowner <UserItem>]
[status completed|failed|inprogress]
gam print transferapps
@@ -1248,6 +1285,18 @@ The listlimit <Number> argument limits the number of recent users, time ranges a
The start <Date> and end <Date> arguments filter the time ranges.
Delimiter defaults to comma.
gam info crostelemetry <SerialNumber>
<CrOSTelemetryFieldName>* [fields <CrOSTelemetryFieldNameList>]
[storagepercentonly] [showorgunitpath]
gam show crostelemetry
[(ou|org|orgunit <OrgUnitItem>)|(cros_sn <SerialNumber>)|(filter <String>)]
<CrOSTelemetryFieldName>* [fields <CrOSTelemetryFieldNameList>]
[storagepercentonly] [showorgunitpath]
gam print crostelemetry [todrive]
[(ou|org|orgunit <OrgUnitItem>)|(cros_sn <SerialNumber>)|(filter <String>)]
<CrOSTelemetryFieldName>* [fields <CrOSTelemetryFieldNameList>]
[storagepercentonly] [showorgunitpath]
gam print chromeapps [todrive]
[ou|org|orgunit <OrgUnitItem>]
[filter <String>]
@@ -1308,7 +1357,7 @@ gam print chromehistory releases [todrive]
gam delete chromepolicy <SchemaName>+ ou|org|orgunit <OrgUnitItem> [(printerid <PrinterID>)|(appid <AppID>)]
gam update chromepolicy (<SchemaName> (<Field> <Value>)+)+ ou|org|orgunit <OrgUnitItem> [(printerid <PrinterID>)|(appid <AppID>)]
gam show chromepolicy ou|org|orgunit <OrgUnitItem> [(printerid <PrinterID>)|(appid <AppID>)]
gam show chromepolicy ou|org|orgunit <OrgUnitItem> [(printerid <PrinterID>)|(appid <AppID>)] [namespace <NamespaceList>]
gam show chromeschema [filter <String>]
<DeviceID> ::= devices/<String>
@@ -1372,18 +1421,21 @@ gam print printermodels [todrive] [filter <String>]
gam create cigroup <EmailAddress> <CIGroupAttribute>*
[makeowner] [alias|aliases <AliasList>] [dynamic <QueryDynamicGroup>]
gam update cigroup <GroupItem> [email <EmailAddress>] <CIGroupAttribute>* [security]
gam update cigroup <GroupItem> [email <EmailAddress>] <CIGroupAttribute>*
[security] [dynamic <QueryDynamicGroup>]
[memberrestrictions <QueryMemberRestrictions>]
gam update cigroup <GroupItem> add [owner|manager|member] [notsuspended|suspended] [expires never|<Time>] <UserTypeEntity>
gam update cigroup <GroupItem> delete|remove [owner|manager|member] [notsuspended|suspended] <UserTypeEntity>
gam update cigroup <GroupItem> sync [owner|manager|member] [notsuspended|suspended] [expires never|<Time>] <UserTypeEntity>
gam update cigroup <GroupItem> update [owner|manager|member] [notsuspended|suspended] [expires never|<Time>] <UserTypeEntity>
gam update cigroup <GroupItem> clear [member] [manager] [owner] [notsuspended|suspended]
gam delete cigroup <GroupItem>
gam info cigroup <GroupItem> [nousers] [nojoindate] [showupdatedate] [membertree]
gam info cigroup <GroupItem> [nousers] [nojoindate] [showupdatedate] [membertree] [nosecurity|nosecuritysettings]
gam print cigroups [todrive]
[enterprisemember <UserItem>]
[members|memberscount] [managers|managerscount] [owners|ownerscount]
[memberrestrictions]
[delimiter <Character>] [sortheaders]
gam info cimember <UserItem> <GroupItem>
@@ -1452,7 +1504,11 @@ gam create user <EmailAddress> <UserAttribute>* [verifynotinvitable]
gam update user <UserItem> <UserAttribute>* [clearschema <SchemaName>] [clearschema <SchemaName>.<FieldName>] [verifynotinvitable]
gam delete user <UserItem>
gam undelete user <UserItem> [org|ou <OrgUnitPath>]
gam info user [<UserItem>] [noaliases] [nogroups] [nolicenses|nolicences] [noschemas] [schemas|custom <SchemaNameList>] [userview] [skus|sku <SKUIDList>] [grouptree]
gam info user [<UserItem>]
[quick] [noaliases] [nogroups] [nolicenses|nolicences] [noschemas]
[skus|sku <SKUIDList>] [grouptree]
[userview] <UserFieldName>* [fields <UserFieldNameList>]
[schemas|custom all|<SchemaNameList>]
Print fields for selected users; use domain, query/queries and deleted_only to select users to print;
if none of these options are specified, all users are printed.
@@ -1460,10 +1516,12 @@ The first column will always be primaryEmail; the remaining field names will be
otherwise, the remaining field names will appear in the order specified.
gam print users [todrive]
([domain <DomainName>] [(query <QueryUser>)|(queries <QueryUserList>)] [deleted_only|only_deleted])
([domain <DomainName>] [(query <QueryUser>)|(queries <QueryUserList>)]
[limittoou <OrgUnitPath>] [deleted_only|only_deleted])
[groups] [license|licenses|licence|licences] [emailpart|emailparts|username]
[orderby <UserOrderByFieldName> [ascending|descending]] [userview]
[allfields|basic|full | ((<UserFieldName>* | fields <UserFieldNameList>) [schemas|custom all|<SchemaNameList>])]
[orderby <UserOrderByFieldName> [ascending|descending]]
[userview] [allfields|basic|full | (<UserFieldName>* | fields <UserFieldNameList>)]
[schemas|custom all|<SchemaNameList>])]
[delimiter <Character>] [sortheaders]
gam create verify|verification <DomainName>

View File

@@ -368,7 +368,7 @@
"required": true,
"type": "string"
}
},
},
"path": "{customer}/chrome/enrollmentTokens",
"request": {
"$ref": "CreateEnrollmentTokenRequest"
@@ -379,7 +379,7 @@
"scopes": [
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers"
]
},
},
"revoke": {
"description": "Revokes a browser enrollment token in a domain.",
"flatPath": "{customer}/chrome/enrollmentTokens/{tokenPermanentId}:revoke",
@@ -387,7 +387,7 @@
"id": "cbcm.enrollmentTokens.revoke",
"parameterOrder": [
"customer",
"tokenPermanentId"
"tokenPermanentId"
],
"parameters": {
"customer": {
@@ -402,12 +402,12 @@
"required": true,
"type": "string"
}
},
},
"path": "{customer}/chrome/enrollmentTokens/{tokenPermanentId}:revoke",
"scopes": [
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers"
]
}
}
}
}
},
@@ -491,23 +491,23 @@
"description": "Immutable ID of the G Suite account.",
"type": "string"
},
"orgUnitPath": {
"orgUnitPath": {
"description": "The full path of the organizational unit or its unique ID.",
"type": "string"
},
"creatorId": {
"creatorId": {
"description": "Creator ID.",
"type": "string"
},
"createTime": {
"createTime": {
"description": "Creation Time.",
"type": "string"
},
"revokerId": {
"revokerId": {
"description": "Revoker ID.",
"type": "string"
},
"revokeTime": {
"revokeTime": {
"description": "Revoke Time",
"type": "string"
}
@@ -538,16 +538,18 @@
},
"CreateEnrollmentTokenRequest": {
"id": "CreateEnrollmentTokenRequest",
"type": "object",
"properties": {
"org_unit_path": {
"org_unit_path": {
"description": "The full path of the organizational unit or its unique ID.",
"type": "string"
},
"expire_time": {
"expire_time": {
"description": "Expiration Time.",
"type": "string"
},
"token_type": {
"token_type": {
"id": "token_type",
"annotations": {
"required": [
"cbcm.enrollmentTokens.create"
@@ -559,6 +561,8 @@
}
},
"MoveChromeBrowsersRequest": {
"id": "MoveChromeBrowsersRequest",
"type": "object",
"properties": {
"org_unit_path": {
"annotations": {
@@ -576,7 +580,10 @@
]
},
"description": "List of unique device IDs of Chrome Browser Devices to move. A maximum of 600 browsers may be moved per request.",
"type": "array"
"type": "array",
"items": {
"type": "string"
}
}
}
}

View File

@@ -28,7 +28,7 @@ upgrade_only=false
gamversion="latest"
adminuser=""
regularuser=""
gam_glibc_vers="2.31 2.27 2.23"
gam_glibc_vers="2.31 2.27"
#gam_macos_vers="10.15.6 10.14.6 10.13.6"
while getopts "hd:a:o:b:lp:u:r:v:" OPTION
@@ -114,9 +114,10 @@ case $gamos in
done
case $gamarch in
x86_64) gamfile="linux-x86_64-$useglibc.tar.xz";;
arm64|aarch64) gamfile="linux-arm64-$useglibc.tar.xz";;
arm64|aarch64) gamfile="linux-arm64-glibc2.28.tar.xz";;
arm|armv7l) gamfile="linux-armv7l-glibc2.28.tar.xz";;
*)
echo_red "ERROR: this installer currently only supports x86_64 and arm64 Linux. Looks like you're running on $gamarch. Exiting."
echo_red "ERROR: this installer currently only supports x86_64, arm and arm64 Linux. Looks like you're running on $gamarch. Exiting."
exit
esac
;;
@@ -128,7 +129,7 @@ case $gamos in
this_macos_ver=$osversion
fi
echo "You are running MacOS $this_macos_ver"
gamfile="macos-x86_64.tar.xz"
gamfile="macos-universal2.tar.xz"
;;
MINGW64_NT*)
gamos="windows"

View File

@@ -1,5 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Provides backwards compatibility for calling gam as a single .py file"""
import sys
@@ -8,4 +7,4 @@ from gam.__main__ import main
# Run from command line
if __name__ == '__main__':
main(sys.argv)
main()

View File

@@ -5,8 +5,6 @@ import sys
import importlib
from PyInstaller.utils.hooks import copy_metadata
sys.modules['FixTk'] = None
# dynamically determine where httplib2/cacerts.txt lives
proot = os.path.dirname(importlib.import_module('httplib2').__file__)
extra_files = [(os.path.join(proot, 'cacerts.txt'), 'httplib2')]
@@ -35,6 +33,13 @@ for d in a.datas:
pyz = PYZ(a.pure)
# TODO: fix universal2
target_arch = None
#if sys.platform == "darwin":
# target_arch="universal2"
#else:
# target_arch=None
exe = EXE(pyz,
a.scripts,
a.binaries,
@@ -44,4 +49,5 @@ exe = EXE(pyz,
debug=False,
strip=None,
upx=False,
target_arch=target_arch,
console=True)

View File

@@ -33,6 +33,7 @@ import http.client as http_client
from multiprocessing import Pool as mp_pool
from multiprocessing import Lock as mp_lock
from urllib.parse import quote, urlencode, urlparse
from pathvalidate import sanitize_filename
import dateutil.parser
import googleapiclient
@@ -549,6 +550,7 @@ def SetGlobalVariables():
filePresentValue=4,
fileAbsentValue=0)
_getOldSignalFile(GC_NO_BROWSER, 'nobrowser.txt')
_getOldSignalFile(GC_NO_TDEMAIL, 'notdemail.txt')
_getOldSignalFile(GC_OAUTH_BROWSER, 'oauthbrowser.txt')
# _getOldSignalFile(GC_NO_CACHE, u'nocache.txt')
# _getOldSignalFile(GC_CACHE_DISCOVERY_ONLY, u'allcache.txt', filePresentValue=False, fileAbsentValue=True)
@@ -728,8 +730,12 @@ def getOSPlatform():
elif myos == 'Darwin':
myos = 'MacOS'
mac_ver = platform.mac_ver()[0]
major_ver = int(mac_ver.split('.')[0]) # macver 10.14.6 == major_ver 10
minor_ver = int(mac_ver.split('.')[1]) # macver 10.14.6 == minor_ver 14
codename = MACOS_CODENAMES.get(minor_ver, '')
if major_ver == 10:
codename = MACOS_CODENAMES[major_ver].get(minor_ver, '')
else:
codename = MACOS_CODENAMES.get(major_ver, '')
pltfrm = ' '.join([codename, mac_ver])
else:
pltfrm = platform.platform()
@@ -768,12 +774,12 @@ def doGAMVersion(checkForArgs=True):
cpu_bits = struct.calcsize('P') * 8
api_client_ver = lib_version('google-api-python-client')
print(
(f'GAM {GAM_VERSION} - {GAM_URL} - {GM_Globals[GM_GAM_TYPE]}\n'
f'GAM {GAM_VERSION} - {GAM_URL} - {GM_Globals[GM_GAM_TYPE]}\n'
f'{GAM_AUTHOR}\n'
f'Python {pyversion} {cpu_bits}-bit {sys.version_info.releaselevel}\n'
f'google-api-python-client {api_client_ver}\n'
f'{getOSPlatform()} {platform.machine()}\n'
f'Path: {GM_Globals[GM_GAM_PATH]}'))
f'Path: {GM_Globals[GM_GAM_PATH]}')
if sys.platform.startswith('win') and \
cpu_bits == 32 and \
platform.machine().find('64') != -1:
@@ -871,9 +877,9 @@ def getSvcAcctCredentials(scopes, act_as, api=None):
GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID] = GM_Globals[
GM_OAUTH2SERVICE_JSON_DATA]['client_id']
return credentials
except (ValueError, KeyError):
except (ValueError, KeyError) as err:
printLine(MESSAGE_INSTRUCTIONS_OAUTH2SERVICE_JSON)
controlflow.invalid_json_exit(GC_Values[GC_OAUTH2SERVICE_JSON])
controlflow.invalid_json_exit(GC_Values[GC_OAUTH2SERVICE_JSON], err)
def getAPIVersion(api):
@@ -899,8 +905,8 @@ def readDiscoveryFile(api_version):
try:
discovery = json.loads(json_string)
return (disc_file, discovery)
except ValueError:
controlflow.invalid_json_exit(disc_file)
except ValueError as err:
controlflow.invalid_json_exit(disc_file, err)
def getOauth2TxtStorageCredentials():
@@ -1240,9 +1246,8 @@ def doCheckServiceAccount(users):
'get',
name=name,
throw_reasons=[gapi_errors.ErrorReason.FOUR_O_THREE])
# Both Google and GAM set key valid after to day before creation
key_created = dateutil.parser.parse(
key['validAfterTime'], ignoretz=True) + datetime.timedelta(days=1)
key['validAfterTime'], ignoretz=True)
key_age = datetime.datetime.now() - key_created
key_days = key_age.days
if key_days > 30:
@@ -1473,8 +1478,8 @@ def addDelegates(users, i):
body={'delegateEmail': delegate})
def gen_sha512_hash(password):
return sha512_crypt.hash(password, rounds=5000)
def gen_sha512_hash(password, rounds=10000):
return sha512_crypt.hash(password, rounds=rounds)
def printShowDelegates(users, csvFormat):
@@ -1757,8 +1762,8 @@ def doCreateAdmin():
def doPrintAdmins():
cd = buildGAPIObject('directory')
roleId = None
userKey = None
todrive = False
kwargs = {}
fields = 'nextPageToken,items(roleAssignmentId,roleId,assignedTo,scopeType,orgUnitId)'
titles = [
'roleAssignmentId', 'roleId', 'role', 'assignedTo', 'assignedToUser',
@@ -1769,7 +1774,7 @@ def doPrintAdmins():
while i < len(sys.argv):
myarg = sys.argv[i].lower()
if myarg == 'user':
userKey = normalizeEmailAddressOrUID(sys.argv[i + 1])
kwargs['userKey'] = normalizeEmailAddressOrUID(sys.argv[i + 1])
i += 2
elif myarg == 'role':
roleId = getRoleId(sys.argv[i + 1])
@@ -1779,14 +1784,18 @@ def doPrintAdmins():
i += 1
else:
controlflow.invalid_argument_exit(sys.argv[i], 'gam print admins')
if roleId and not kwargs:
kwargs['roleId'] = roleId
roleId = None
admins = gapi.get_all_pages(cd.roleAssignments(),
'list',
'items',
customer=GC_Values[GC_CUSTOMER_ID],
userKey=userKey,
roleId=roleId,
fields=fields)
fields=fields,
**kwargs)
for admin in admins:
if roleId and roleId != admin['roleId']:
continue
admin_attrib = {}
for key, value in list(admin.items()):
if key == 'assignedTo':
@@ -3264,7 +3273,7 @@ def printDriveFileList(users):
'orderby', ', '.join(sorted(DRIVEFILE_ORDERBY_CHOICES_MAP)),
fieldName)
elif myarg == 'query':
query += f' and {sys.argv[i+1]}'
query += f' and ({sys.argv[i+1]})'
i += 2
elif myarg == 'fullquery':
query = sys.argv[i + 1]
@@ -3719,6 +3728,10 @@ def getDriveFileAttribute(i, body, parameters, myarg, update=False):
body['mimeType'] = MIMETYPE_GA_SHORTCUT
body['shortcutDetails'] = {'targetId': sys.argv[i+1]}
i += 2
elif myarg == 'securityupdate':
body['linkShareMetadata'] = {'securityUpdateEnabled': getBoolean(
sys.argv[i+1], f'gam <users> {operation} drivefile'), 'securityUpdateEligible': True}
i += 2
else:
controlflow.invalid_argument_exit(
myarg, f"gam <users> {operation} drivefile")
@@ -4053,8 +4066,7 @@ def downloadDriveFile(users):
if targetName:
safe_file_title = targetName
else:
safe_file_title = ''.join(c for c in result['title']
if c in FILENAME_SAFE_CHARS)
safe_file_title = sanitize_filename(result['title'])
if not safe_file_title:
safe_file_title = fileId
filename = os.path.join(targetFolder, safe_file_title)
@@ -4086,7 +4098,7 @@ def downloadDriveFile(users):
for sheet in spreadsheet['sheets']:
if sheet['properties']['title'].lower(
) == csvSheetTitleLower:
spreadsheetUrl = '{0}?format=csv&id={1}&gid={2}'.format(
spreadsheetUrl = '{}?format=csv&id={}&gid={}'.format(
re.sub('/edit.*$', '/export',
spreadsheet['spreadsheetUrl']),
fileId, sheet['properties']['sheetId'])
@@ -4111,7 +4123,7 @@ def downloadDriveFile(users):
while not done:
status, done = downloader.next_chunk()
if showProgress:
print('Downloaded: {0:>7.2%}'.format(
print('Downloaded: {:>7.2%}'.format(
status.progress()))
else:
_, content = drive._http.request(uri=spreadsheetUrl,
@@ -4123,7 +4135,7 @@ def downloadDriveFile(users):
fileutils.close_file(fh)
fileDownloaded = True
break
except (IOError, httplib2.HttpLib2Error) as e:
except (OSError, httplib2.HttpLib2Error) as e:
display.print_error(str(e))
GM_Globals[GM_SYSEXITRC] = 6
fileDownloadFailed = True
@@ -6665,12 +6677,12 @@ def getUserAttributes(i, cd, updateCmd):
body['changePasswordAtNextLogin'] = getBoolean(
sys.argv[i + 1], myarg)
i += 2
elif myarg == 'ipwhitelisted':
body['ipWhitelisted'] = getBoolean(sys.argv[i + 1], myarg)
i += 2
elif myarg == 'agreedtoterms':
body['agreedToTerms'] = getBoolean(sys.argv[i + 1], myarg)
i += 2
elif myarg == 'ipwhitelisted':
body['ipWhitelisted'] = getBoolean(sys.argv[i + 1], myarg)
i += 2
elif myarg in ['org', 'ou']:
body['orgUnitPath'] = gapi_directory_orgunits.getOrgUnitItem(
sys.argv[i + 1], pathOnly=True)
@@ -6681,13 +6693,27 @@ def getUserAttributes(i, cd, updateCmd):
i += 1
continue
for language in sys.argv[i].replace(',', ' ').split():
if language.lower() in LANGUAGE_CODES_MAP:
appendItemToBodyList(
body, 'languages',
{'languageCode': LANGUAGE_CODES_MAP[language.lower()]})
lang_item = {}
if language[-1] == '+':
suffix = '+'
language = language[:-1]
lang_item['preference'] = 'preferred'
elif language[-1] == '-':
suffix = '-'
language = language[:-1]
lang_item['preference'] = 'not_preferred'
else:
appendItemToBodyList(body, 'languages',
{'customLanguage': language})
suffix = ''
if language.lower() in LANGUAGE_CODES_MAP:
lang_item['languageCode'] = LANGUAGE_CODES_MAP[language.lower()]
else:
if suffix:
controlflow.system_error_exit(
2,
f'suffix {suffix} not allowed with customLanguage {language}'
)
lang_item['customLanguage'] = language
appendItemToBodyList(body, 'languages', lang_item)
i += 1
elif myarg == 'gender':
i += 1
@@ -7124,9 +7150,14 @@ def getUserAttributes(i, cd, updateCmd):
controlflow.invalid_argument_exit(
sys.argv[i], f"gam {['create', 'update'][updateCmd]} user")
if need_password:
# generate a password with unicode chars that are not allowed in
# passwords. We expect "password random nohash" to fail but no one
# should be using that. Our goal here is to purposefully block login
# with this password.
pass_chars = [chr(i) for i in range(1, 55296)]
rnd = SystemRandom()
body['password'] = ''.join(
rnd.choice(PASSWORD_SAFE_CHARS) for _ in range(100))
rnd.choice(pass_chars) for _ in range(4096))
if 'password' in body and need_to_hash_password:
body['password'] = gen_sha512_hash(body['password'])
body['hashFunction'] = 'crypt'
@@ -7149,12 +7180,7 @@ def getCRMService(login_hint):
login_hint=login_hint,
use_console_flow=not GC_Values[GC_OAUTH_BROWSER])
httpc = transport.AuthorizedHttp(creds, transport.create_http())
return getService('cloudresourcemanagerv1', httpc), httpc
# Ugh, v2 doesn't contain all the operations of v1 so we need to use both here.
def getCRM2Service(httpc):
return getService('cloudresourcemanager', httpc)
return getService('cloudresourcemanager', httpc), httpc
def getGAMProjectFile(filepath):
@@ -7232,6 +7258,7 @@ def enableGAMProjectAPIs(GAMProjectAPIs,
gapi_errors.ErrorReason.FORBIDDEN,
gapi_errors.ErrorReason.PERMISSION_DENIED
],
retry_reasons=[gapi_errors.ErrorReason.INTERNAL_SERVER_ERROR],
name=service_name)
print(f' API: {api}, Enabled{currentCount(j, jcount)}')
break
@@ -7448,10 +7475,10 @@ def _getProjects(crm, pfilter):
try:
return gapi.get_all_pages(
crm.projects(),
'list',
'search',
'projects',
throw_reasons=[gapi_errors.ErrorReason.BAD_REQUEST],
filter=pfilter)
query=pfilter)
except gapi_errors.GapiBadRequestError as e:
controlflow.system_error_exit(2, f'Project: {pfilter}, {str(e)}')
@@ -7513,23 +7540,15 @@ def _getLoginHintProjectId(createCmd):
f'Invalid Project ID: {projectId}, expected <{PROJECTID_FORMAT_REQUIRED}>'
)
crm, httpObj = getCRMService(login_hint)
if parent and not parent.startswith(
'organizations/') and not parent.startswith('folders/'):
crm2 = getCRM2Service(httpObj)
parent = convertGCPFolderNameToID(parent, crm2)
if parent:
parent_type, parent_id = parent.split('/')
if parent_type[-1] == 's':
parent_type = parent_type[:
-1] # folders > folder, organizations > organization
parent = {'type': parent_type, 'id': parent_id}
if parent and not parent.startswith('organizations/') and not parent.startswith('folders/'):
parent = convertGCPFolderNameToID(parent, crm)
projects = _getProjects(crm, f'id:{projectId}')
if not createCmd:
if not projects:
controlflow.system_error_exit(
2,
f'User: {login_hint}, Project ID: {projectId}, Does not exist')
if projects[0]['lifecycleState'] != 'ACTIVE':
if projects[0]['state'] != 'ACTIVE':
controlflow.system_error_exit(
2, f'User: {login_hint}, Project ID: {projectId}, Not active')
else:
@@ -7542,17 +7561,11 @@ def _getLoginHintProjectId(createCmd):
PROJECTID_FILTER_REQUIRED = 'gam|<ProjectID>|(filter <String>)'
def convertGCPFolderNameToID(parent, crm2):
# crm2.folders() is broken requiring pageToken, etc in body, not URL.
# for now just use gapi.get_items and if user has that many folders they'll
# just need to be specific.
folders = gapi.get_items(crm2.folders(),
'search',
items='folders',
body={
'pageSize': 1000,
'query': f'displayName="{parent}"'
})
def convertGCPFolderNameToID(parent, crm):
folders = gapi.get_all_pages(crm.folders(),
'search',
'folders',
query=f'displayName="{parent}"')
if not folders:
controlflow.system_error_exit(
1, f'ERROR: No folder found matching displayName={parent}')
@@ -7566,15 +7579,14 @@ def convertGCPFolderNameToID(parent, crm2):
def createGCPFolder():
displayName = sys.argv[3]
login_hint = _getValidateLoginHint()
_, httpObj = getCRMService(login_hint)
crm2 = getCRM2Service(httpObj)
gapi.call(crm2.folders(),
'create',
body={
'name': sys.argv[3],
'displayName': sys.argv[3]
})
login_domain = login_hint.split('@')[-1]
crm, _ = getCRMService(login_hint)
organization = getGCPOrg(crm, login_domain)
result = gapi.call(crm.folders(), 'create',
body={'parent': organization, 'displayName': displayName})
print(f'User: {login_hint}, Folder: {displayName}, GCP Folder Name: {result["name"]}, Created')
def _getLoginHintProjects(printShowCmd):
@@ -7628,16 +7640,31 @@ def _checkForExistingProjectFiles():
)
def getGCPOrg(crm, domain):
resp = gapi.call(crm.organizations(),
'search',
query=f'domain:{domain}')
try:
organization = resp['organizations'][0]['name']
print(f'Your organization name is {organization}')
return organization
except (KeyError, IndexError):
controlflow.system_error_exit(
3,
'you have no rights to create projects for your organization and you don\'t seem to be a super admin! Sorry, there\'s nothing more I can do.'
)
def doCreateProject():
_checkForExistingProjectFiles()
crm, httpObj, login_hint, projectId, parent = _getLoginHintProjectId(True)
login_domain = login_hint[login_hint.find('@') + 1:]
body = {'projectId': projectId, 'name': 'GAM Project'}
body = {'projectId': projectId, 'displayName': 'GAM Project'}
if parent:
body['parent'] = parent
while True:
create_again = False
print(f'Creating project "{body["name"]}"...')
print(f'Creating project "{body["displayName"]}"...')
create_operation = gapi.call(crm.projects(), 'create', body=body)
operation_name = create_operation['name']
time.sleep(8) # Google recommends always waiting at least 5 seconds
@@ -7652,18 +7679,7 @@ def doCreateProject():
'Hmm... Looks like you have no rights to your Google Cloud Organization.'
)
print('Attempting to fix that...')
getorg = gapi.call(
crm.organizations(),
'search',
body={'filter': f'domain:{login_domain}'})
try:
organization = getorg['organizations'][0]['name']
print(f'Your organization name is {organization}')
except (KeyError, IndexError):
controlflow.system_error_exit(
3,
'you have no rights to create projects for your organization and you don\'t seem to be a super admin! Sorry, there\'s nothing more I can do.'
)
organization = getGCPOrg(crm, login_domain)
org_policy = gapi.call(crm.organizations(),
'getIamPolicy',
resource=organization)
@@ -7763,11 +7779,11 @@ def _generatePrivateKeyAndPublicCert(client_id, key_size):
x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, client_id)]))
builder = builder.issuer_name(
x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, client_id)]))
not_valid_before = datetime.datetime.today() - datetime.timedelta(days=1)
not_valid_after = datetime.datetime.today() + datetime.timedelta(
days=365 * 10 - 1)
builder = builder.not_valid_before(not_valid_before)
builder = builder.not_valid_after(not_valid_after)
# Gooogle seems to enforce the not before date strictly. Set the not before
# date to be UTC one hour ago should cover any clock skew.
builder = builder.not_valid_before(datetime.datetime.utcnow() - datetime.timedelta(hours=1))
# Google uses 12/31/9999 date for end time
builder = builder.not_valid_after(datetime.datetime(9999, 12, 31, 23, 59))
builder = builder.serial_number(x509.random_serial_number())
builder = builder.public_key(public_key)
builder = builder.add_extension(x509.BasicConstraints(ca=False,
@@ -7896,7 +7912,7 @@ def doCreateOrRotateServiceAccountKeys(iam=None,
i += 1
elif myarg == 'yubikeyslot':
new_data['yubikey_slot'] = sys.argv[i+1].upper()
i =+ 2
i += 2
elif myarg == 'yubikeypin':
new_data['yubikey_pin'] = input('Enter your YubiKey PIN: ')
i += 1
@@ -7919,6 +7935,10 @@ def doCreateOrRotateServiceAccountKeys(iam=None,
new_data['yubikey_key_type'] = f'RSA{local_key_size}'
new_data.pop('private_key', None)
yk = yubikey.YubiKey(new_data)
if 'yubikey_serial_number' not in new_data:
new_data['yubikey_serial_number'] = yk.get_serial_number()
if 'yubikey_slot' not in new_data:
new_data['yubikey_slot'] = 'AUTHENTICATION'
publicKeyData = yk.get_certificate()
elif local_key_size:
# Generate private key locally, store in file
@@ -7946,10 +7966,18 @@ def doCreateOrRotateServiceAccountKeys(iam=None,
name=sa_name,
body={'publicKeyData': publicKeyData})
break
except googleapiclient.errors.HttpError:
print('WARNING: that key already exists.')
result = {'name': oldPrivateKeyId}
break
except googleapiclient.errors.HttpError as err:
if hasattr(err, 'error_details') and \
err.error_details == 'The given public key already exists.':
print('WARNING: that key already exists.')
result = {'name': oldPrivateKeyId}
break
elif hasattr(err, 'error_details'):
controlflow.system_error_exit(
4, err.error_details)
else:
controlflow.system_error_exit(
4, err)
except gapi_errors.GapiNotFoundError as e:
if i == max_retries:
raise e
@@ -8052,7 +8080,7 @@ def doDelProjects():
gapi.call(crm.projects(),
'delete',
throw_reasons=[gapi_errors.ErrorReason.FORBIDDEN],
projectId=projectId)
name=project['name'])
print(f' Project: {projectId} Deleted{currentCount(i, count)}')
except gapi_errors.GapiForbiddenError as e:
print(
@@ -8066,8 +8094,9 @@ def doPrintShowProjects(csvFormat):
csvRows = []
todrive = False
titles = [
'User', 'projectId', 'projectNumber', 'name', 'createTime',
'lifecycleState'
'User', 'projectId', 'name', 'displayName',
'createTime', 'updateTime', 'deleteTime',
'state'
]
while i < len(sys.argv):
myarg = sys.argv[i].lower()
@@ -8084,19 +8113,19 @@ def doPrintShowProjects(csvFormat):
for project in projects:
i += 1
print(f' Project: {project["projectId"]}{currentCount(i, count)}')
print(f' projectNumber: {project["projectNumber"]}')
print(f' name: {project["name"]}')
print(f' createTime: {project["createTime"]}')
print(f' lifecycleState: {project["lifecycleState"]}')
print(f' displayName: {project["displayName"]}')
for field in ['createTime', 'updateTime', 'deleteTime']:
if field in project:
print(f' {field}: {project[field]}')
print(f' state: {project["state"]}')
jcount = len(project.get('labels', []))
if jcount > 0:
print(' labels:')
for k, v in list(project['labels'].items()):
print(f' {k}: {v}')
if 'parent' in project:
print(' parent:')
print(f' type: {project["parent"]["type"]}')
print(f' id: {project["parent"]["id"]}')
print(f' parent: {project["parent"]}')
else:
for project in projects:
display.add_row_titles_to_csv_file(
@@ -8755,6 +8784,20 @@ def _get_admin_email():
)
return _getValueFromOAuth('email')
def _formatLanguagesList(propertyValue, delimiter):
languages = []
for language in propertyValue:
if 'languageCode' in language:
lang = language['languageCode']
if language.get('preference') == 'preferred':
lang += '+'
elif language.get('preference') == 'not_preferred':
lang += '-'
else:
lang = language.get('customLanguage')
languages.append(lang)
return delimiter.join(languages)
def doGetUserInfo(user_email=None):
def user_lic_result(request_id, response, exception):
@@ -8769,6 +8812,7 @@ def doGetUserInfo(user_email=None):
i = 4
else:
user_email = _get_admin_email()
fieldsList = []
getSchemas = True
getAliases = True
getGroups = True
@@ -8799,10 +8843,35 @@ def doGetUserInfo(user_email=None):
getSchemas = False
projection = 'basic'
i += 1
elif myarg == 'quick':
getAliases = getCIGroups = getGroups = getLicenses = getSchemas = False
i += 1
elif myarg in ['custom', 'schemas']:
getSchemas = True
projection = 'custom'
customFieldMask = sys.argv[i + 1]
if not fieldsList:
fieldsList = ['primaryEmail']
fieldsList.append('customSchemas')
if sys.argv[i + 1].lower() == 'all':
projection = 'full'
else:
projection = 'custom'
customFieldMask = sys.argv[i + 1].replace(' ', ',')
i += 2
elif myarg in USER_ARGUMENT_TO_PROPERTY_MAP:
if not fieldsList:
fieldsList = ['primaryEmail',]
fieldsList.extend(USER_ARGUMENT_TO_PROPERTY_MAP[myarg])
i += 1
elif myarg == 'fields':
if not fieldsList:
fieldsList = ['primaryEmail',]
fieldNameList = sys.argv[i + 1]
for field in fieldNameList.lower().replace(',', ' ').split():
if field in USER_ARGUMENT_TO_PROPERTY_MAP:
fieldsList.extend(USER_ARGUMENT_TO_PROPERTY_MAP[field])
else:
controlflow.invalid_argument_exit(field,
'gam info users fields')
i += 2
elif myarg == 'userview':
viewType = 'domain_public'
@@ -8816,6 +8885,7 @@ def doGetUserInfo(user_email=None):
'get',
userKey=user_email,
projection=projection,
fields=','.join(set(fieldsList)) if fieldsList else '*',
customFieldMask=customFieldMask,
viewType=viewType)
print(f'User: {user["primaryEmail"]}')
@@ -8824,14 +8894,7 @@ def doGetUserInfo(user_email=None):
if 'name' in user and 'familyName' in user['name']:
print(f'Last Name: {user["name"]["familyName"]}')
if 'languages' in user:
up = 'languageCode'
languages = [row[up] for row in user['languages'] if up in row]
if languages:
print(f'Languages: {",".join(languages)}')
up = 'customLanguage'
languages = [row[up] for row in user['languages'] if up in row]
if languages:
print(f'Custom Languages: {",".join(languages)}')
print(f"Languages: {_formatLanguagesList(user['languages'], ',')}")
if 'isAdmin' in user:
print(f'Is a Super Admin: {user["isAdmin"]}')
if 'isDelegatedAdmin' in user:
@@ -9646,6 +9709,7 @@ def doPrintUsers():
customFieldMask = None
sortHeaders = getGroupFeed = getLicenseFeed = email_parts = False
viewType = deleted_only = orderBy = sortOrder = None
orgUnitPath = orgUnitPathLower = None
groupDelimiter = ' '
licenseDelimiter = ','
i = 3
@@ -9668,12 +9732,14 @@ def doPrintUsers():
sortHeaders = True
i += 1
elif myarg in ['custom', 'schemas']:
if not fieldsList:
fieldsList = ['primaryEmail']
fieldsList.append('customSchemas')
if sys.argv[i + 1].lower() == 'all':
projection = 'full'
else:
projection = 'custom'
customFieldMask = sys.argv[i + 1]
customFieldMask = sys.argv[i + 1].replace(' ', ',')
i += 2
elif myarg == 'todrive':
todrive = True
@@ -9708,19 +9774,19 @@ def doPrintUsers():
elif myarg in ['query', 'queries']:
queries = getQueries(myarg, sys.argv[i + 1])
i += 2
elif myarg == 'limittoou':
orgUnitPath = gapi_directory_orgunits.getOrgUnitItem(sys.argv[i + 1], pathOnly=True)
orgUnitPathLower = orgUnitPath.lower()
i += 2
elif myarg in USER_ARGUMENT_TO_PROPERTY_MAP:
if not fieldsList:
fieldsList = [
'primaryEmail',
]
fieldsList = ['primaryEmail',]
display.add_field_to_csv_file(myarg, USER_ARGUMENT_TO_PROPERTY_MAP,
fieldsList, fieldsTitles, titles)
i += 1
elif myarg == 'fields':
if not fieldsList:
fieldsList = [
'primaryEmail',
]
fieldsList = ['primaryEmail',]
fieldNameList = sys.argv[i + 1]
for field in fieldNameList.lower().replace(',', ' ').split():
if field in USER_ARGUMENT_TO_PROPERTY_MAP:
@@ -9743,10 +9809,21 @@ def doPrintUsers():
else:
controlflow.invalid_argument_exit(sys.argv[i], 'gam print users')
if fieldsList:
if orgUnitPath is not None:
fieldsList.append('orgUnitPath')
fields = f'nextPageToken,users({",".join(set(fieldsList)).replace(".", "/")})'
else:
fields = None
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}'"
printGettingAllItems('Users', query)
page_message = gapi.got_total_items_first_last_msg('Users')
all_users = gapi.get_all_pages(cd.users(),
@@ -9765,13 +9842,16 @@ def doPrintUsers():
projection=projection,
customFieldMask=customFieldMask)
for user in all_users:
if email_parts and ('primaryEmail' in user):
user_email = user['primaryEmail']
if user_email.find('@') != -1:
user['primaryEmailLocal'], user[
'primaryEmailDomain'] = splitEmailAddress(user_email)
display.add_row_titles_to_csv_file(utils.flatten_json(user),
csvRows, titles)
if orgUnitPathLower is None or orgUnitPathLower == user.get('orgUnitPath', '').lower():
if email_parts and ('primaryEmail' in user):
user_email = user['primaryEmail']
if user_email.find('@') != -1:
user['primaryEmailLocal'], user[
'primaryEmailDomain'] = splitEmailAddress(user_email)
if 'languages' in user:
user['languages'] = _formatLanguagesList(user.pop('languages'), ' ')
display.add_row_titles_to_csv_file(utils.flatten_json(user),
csvRows, titles)
if sortHeaders:
display.sort_csv_titles([
'primaryEmail',
@@ -10435,9 +10515,10 @@ OAUTH2_SCOPES = [
'scopes': 'https://www.googleapis.com/auth/admin.directory.device.chromebrowsers',
},
{
'name': 'Chrome Management API - read only',
'name': 'Chrome Management API - read only (2 scopes)',
'subscope': [],
'scopes': ['https://www.googleapis.com/auth/chrome.management.reports.readonly'],
'scopes': ['https://www.googleapis.com/auth/chrome.management.reports.readonly',
'https://www.googleapis.com/auth/chrome.management.telemetry.readonly'],
},
{
'name': 'Chrome Policy API',
@@ -11471,6 +11552,8 @@ def ProcessGAMCommand(args):
gapi_directory_resource.getResourceCalendarInfo()
elif argument == 'cros':
gapi_directory_cros.doGetCrosInfo()
elif argument == 'crostelemetry':
gapi_chromemanagement.printShowCrosTelemetry('info')
elif argument == 'mobile':
gapi_directory_mobiledevices.info()
elif argument in ['verify', 'verification']:
@@ -11623,6 +11706,8 @@ def ProcessGAMCommand(args):
gapi_cloudidentity_groups.print_()
elif argument == 'devices':
gapi_cloudidentity_devices.print_()
elif argument == 'crostelemetry':
gapi_chromemanagement.printShowCrosTelemetry('print')
elif argument in ['groupmembers', 'groupsmembers']:
gapi_directory_groups.print_members()
elif argument in ['cigroupmembers', 'cigroupsmembers']:
@@ -11731,6 +11816,8 @@ def ProcessGAMCommand(args):
gapi_chromepolicy.printshow_schemas()
elif argument in ['chromepolicy', 'chromepolicies']:
gapi_chromepolicy.printshow_policies()
elif argument == 'crostelemetry':
gapi_chromemanagement.printShowCrosTelemetry('show')
else:
controlflow.invalid_argument_exit(argument, 'gam show')
sys.exit(0)
@@ -11853,6 +11940,12 @@ def ProcessGAMCommand(args):
elif command == 'getcommand':
gapi_directory_cros.get_command()
sys.exit(0)
elif command in ['yubikey']:
action = sys.argv[2].lower().replace('_', '')
if action == 'resetpiv':
yk = yubikey.YubiKey()
yk.reset_piv()
sys.exit(0)
users = getUsersToModify()
command = sys.argv[3].lower()
if command == 'print' and len(sys.argv) == 4:

View File

@@ -1,5 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# GAM
#
@@ -30,21 +29,21 @@ from gam import controlflow
import gam
def main(argv):
def main():
freeze_support()
if sys.platform == 'darwin':
# https://bugs.python.org/issue33725 in Python 3.8.0 seems
# to break parallel operations with errors about extra -b
# command line arguments
set_start_method('fork')
if sys.version_info[0] < 3 or sys.version_info[1] < 6:
if sys.version_info[0] < 3 or sys.version_info[1] < 7:
controlflow.system_error_exit(
5,
f'GAM requires Python 3.6 or newer. You are running %s.%s.%s. Please upgrade your Python version or use one of the binary GAM downloads.'
f'GAM requires Python 3.7 or newer. You are running %s.%s.%s. Please upgrade your Python version or use one of the binary GAM downloads.'
% sys.version_info[:3])
sys.exit(gam.ProcessGAMCommand(sys.argv))
# Run from command line
if __name__ == '__main__':
main(sys.argv)
main()

View File

@@ -37,7 +37,7 @@ def get_admin_credentials(api=None):
credential_file = get_admin_credentials_filename()
if not os.path.isfile(credential_file):
raise oauth.InvalidCredentialsFileError
with open(credential_file, 'r') as f:
with open(credential_file) as f:
creds_data = json.load(f)
# Validate that enable DASA matches content of authorization file
if GC_Values[GC_ENABLE_DASA] and 'private_key_id' in creds_data:

View File

@@ -115,7 +115,7 @@ class Credentials(google.oauth2.credentials.Credentials):
Raises:
TypeError: If id_token_data is not the required dict type.
"""
super(Credentials, self).__init__(token=token,
super().__init__(token=token,
refresh_token=refresh_token,
id_token=id_token,
token_uri=token_uri,
@@ -161,9 +161,9 @@ class Credentials(google.oauth2.credentials.Credentials):
ValueError: If missing fields are detected in the info.
"""
# We need all of these keys
keys_needed = set(('client_id', 'client_secret'))
keys_needed = {'client_id', 'client_secret'}
# We need 1 or more of these keys
keys_need_one_of = set(('refresh_token', 'auth_token', 'token'))
keys_need_one_of = {'refresh_token', 'auth_token', 'token'}
missing = keys_needed.difference(info.keys())
has_one_of = set(info) & keys_need_one_of
if missing or not has_one_of:
@@ -395,7 +395,7 @@ class Credentials(google.oauth2.credentials.Credentials):
self.refresh(request)
self._id_token_data = google.oauth2.id_token.verify_oauth2_token(
self.id_token, request)
self.id_token, request, clock_skew_in_seconds=10)
def get_token_value(self, field):
"""Retrieves data from the OAuth ID token.
@@ -472,7 +472,7 @@ class Credentials(google.oauth2.credentials.Credentials):
def _locked_refresh(self, request):
"""Refreshes the credential's access token while the file lock is held."""
assert self._lock.is_locked
super(Credentials, self).refresh(request)
super().refresh(request)
def write(self):
"""Writes credentials to disk."""
@@ -523,12 +523,12 @@ class _ShortURLFlow(google_auth_oauthlib.flow.InstalledAppFlow):
def authorization_url(self, http=None, **kwargs):
"""Gets a shortened authorization URL."""
long_url, state = super(_ShortURLFlow, self).authorization_url(**kwargs)
long_url, state = super().authorization_url(**kwargs)
short_url = utils.shorten_url(long_url)
return short_url, state
class _FileLikeThreadLock(object):
class _FileLikeThreadLock:
"""A threading.lock which has the same interface as filelock.Filelock."""
def __init__(self):

View File

@@ -38,7 +38,7 @@ class CredentialsTest(unittest.TestCase):
'client_id': self.fake_client_id,
'client_secret': self.fake_client_secret,
}
super(CredentialsTest, self).setUp()
super().setUp()
def tearDown(self):
# Remove any credential files that may have been created.
@@ -46,7 +46,7 @@ class CredentialsTest(unittest.TestCase):
os.remove(self.fake_filename)
if os.path.exists('%s.lock' % self.fake_filename):
os.remove('%s.lock' % self.fake_filename)
super(CredentialsTest, self).tearDown()
super().tearDown()
def test_from_authorized_user_info_only_required_info(self):
creds = oauth.Credentials.from_authorized_user_info(
@@ -592,7 +592,7 @@ class ShortUrlFlowTest(unittest.TestCase):
}
self.long_url = 'http://example.com/some/long/url'
self.short_url = 'http://ex.co/short'
super(ShortUrlFlowTest, self).setUp()
super().setUp()
@patch.object(oauth.google_auth_oauthlib.flow.InstalledAppFlow,
'authorization_url')

View File

@@ -1,72 +1,155 @@
from base64 import b64encode
import datetime
from secrets import SystemRandom
import string
import sys
from threading import Timer
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from smartcard.Exceptions import CardConnectionException
from ykman.device import connect_to_device
from yubikit.piv import KEY_TYPE, SLOT, InvalidPinError, PivSession
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, \
SLOT, \
TOUCH_POLICY
from yubikit.core.smartcard import ApduError
from gam import controlflow
class YubiKey():
def __init__(self, service_account_info):
key_type = service_account_info.get('yubikey_key_type', 'RSA2048')
def __init__(self, service_account_info=None):
self.key_type = None
self.slot = None
self.serial_number = None
self.pin = None
self.key_id = None
if service_account_info:
key_type = service_account_info.get('yubikey_key_type', 'RSA2048')
try:
self.key_type = getattr(KEY_TYPE, key_type.upper())
except AttributeError:
controlflow.system_error_exit(6, f'{key_type} is not a valid value for yubikey_key_type')
slot = service_account_info.get('yubikey_slot', 'AUTHENTICATION')
try:
self.slot = getattr(SLOT, slot.upper())
except AttributeError:
controlflow.system_error_exit(6, f'{slot} is not a valid value for yubikey_slot')
self.serial_number = service_account_info.get('yubikey_serial_number')
self.pin = service_account_info.get('yubikey_pin')
self.key_id = service_account_info.get('private_key_id')
def _connect(self):
try:
self.key_type = getattr(KEY_TYPE, key_type.upper())
except AttributeError:
controlflow.system_error_exit(6, f'{key_type} is not a valid value for yubikey_key_type')
slot = service_account_info.get('yubikey_slot', 'AUTHENTICATION')
try:
self.slot = getattr(SLOT, slot.upper())
except AttributeError:
controlflow.system_error_exit(6, f'{slot} is not a valid value for yubikey_slot')
self.serial_number = service_account_info.get('yubikey_serial_number')
self.pin = service_account_info.get('yubikey_pin')
self.key_id = service_account_info.get('private_key_id')
conn, _, _ = connect_to_device(self.serial_number)
except CardConnectionException as err:
controlflow.system_error_exit(9, f'YubiKey - {err}')
return conn
def get_certificate(self):
try:
conn, _, _ = connect_to_device(self.serial_number)
session = PivSession(conn)
if self.pin:
conn = self._connect()
with conn:
session = PivSession(conn)
if self.pin:
try:
session.verify_pin(self.pin)
except InvalidPinError as err:
controlflow.system_error_exit(7, f'YubiKey - {err}')
try:
session.verify_pin(self.pin)
except InvalidPinError as err:
controlflow.system_error_exit(7, f'YubiKey - {err}')
try:
cert = session.get_certificate(self.slot)
cert_pem = cert.public_bytes(
serialization.Encoding.PEM).decode()
publicKeyData = b64encode(cert_pem.encode())
if isinstance(publicKeyData, bytes):
publicKeyData = publicKeyData.decode()
return publicKeyData
except ApduError as err:
controlflow.system_error_exit(8, f'YubiKey - {err}')
cert = session.get_certificate(self.slot)
except ApduError as err:
controlflow.system_error_exit(9, f'YubiKey - {err}')
cert_pem = cert.public_bytes(
serialization.Encoding.PEM).decode()
publicKeyData = b64encode(cert_pem.encode())
if isinstance(publicKeyData, bytes):
publicKeyData = publicKeyData.decode()
return publicKeyData
except ValueError as err:
controlflow.system_error_exit(9, f'YubiKey - {err}')
def get_serial_number(self):
try:
_, _, info = connect_to_device(self.serial_number)
return info.serial
except ValueError as err:
controlflow.system_error_exit(9, f'YubiKey - {err}')
def reset_piv(self):
'''Resets YubiKey PIV app and generates new key for GAM to use.'''
reply = str(input('This will wipe all PIV keys and configuration from your YubiKey. Are you sure? (y/N) ').lower().strip())
if reply != 'y':
sys.exit(1)
try:
conn = self._connect()
with conn:
piv = PivSession(conn)
piv.reset()
rnd = SystemRandom()
pin_puk_chars = string.ascii_letters + string.digits + string.punctuation
new_puk = ''.join(rnd.choice(pin_puk_chars) for _ in range(8))
new_pin = ''.join(rnd.choice(pin_puk_chars) for _ in range(8))
piv.change_puk('12345678', new_puk)
piv.change_pin('123456', new_pin)
print(f'PIN set to: {new_pin}')
piv.authenticate(MANAGEMENT_KEY_TYPE.TDES,
DEFAULT_MANAGEMENT_KEY)
piv.verify_pin(new_pin)
print('YubiKey is generating a non-exportable private key...')
pubkey = piv.generate_key(SLOT.AUTHENTICATION,
KEY_TYPE.RSA2048,
PIN_POLICY.ALWAYS,
TOUCH_POLICY.NEVER)
now = datetime.datetime.utcnow()
valid_to = now + datetime.timedelta(days=36500)
subject = 'CN=GAM Created Key'
piv.authenticate(MANAGEMENT_KEY_TYPE.TDES,
DEFAULT_MANAGEMENT_KEY)
piv.verify_pin(new_pin)
cert = generate_self_signed_certificate(piv,
SLOT.AUTHENTICATION,
pubkey,
subject,
now,
valid_to)
piv.put_certificate(SLOT.AUTHENTICATION,
cert)
piv.put_object(OBJECT_ID.CHUID,
generate_chuid())
except ValueError as err:
controlflow.system_error_exit(8, f'YubiKey - {err}')
def sign(self, message):
if 'mplock' in globals():
mplock.acquire()
try:
conn, _, _ = connect_to_device(self.serial_number)
session = PivSession(conn)
if self.pin:
conn = self._connect()
with conn:
session = PivSession(conn)
if self.pin:
try:
session.verify_pin(self.pin)
except InvalidPinError as err:
controlflow.system_error_exit(7, f'YubiKey - {err}')
try:
session.verify_pin(self.pin)
except InvalidPinError as err:
controlflow.system_error_exit(7, f'YubiKey - {err}')
try:
signed = session.sign(slot=self.slot,
signed = session.sign(slot=self.slot,
key_type=self.key_type,
message=message,
hash_algorithm=hashes.SHA256(),
padding=padding.PKCS1v15())
except ApduError as err:
controlflow.system_error_exit(8, f'YubiKey = {err}')
except ApduError as err:
controlflow.system_error_exit(8, f'YubiKey - {err}')
except ValueError as err:
controlflow.system_error_exit(9, f'YubiKey - {err}')
if 'mplock' in globals():

View File

@@ -65,9 +65,12 @@ def csv_field_error_exit(field_name, field_names):
','.join(field_names)))
def invalid_json_exit(file_name):
def invalid_json_exit(file_name, err=None):
"""Raises a system exit when invalid JSON content is encountered."""
system_error_exit(17, MESSAGE_INVALID_JSON.format(file_name))
err_msg = MESSAGE_INVALID_JSON.format(file_name)
if err:
err_msg += f'\n\n{err}'
system_error_exit(17, err_msg)
def wait_on_failure(current_attempt_num,
@@ -89,8 +92,8 @@ def wait_on_failure(current_attempt_num,
wait_on_fail = min(2**current_attempt_num,
60) + float(random.randint(1, 1000)) / 1000
if current_attempt_num > error_print_threshold:
sys.stderr.write((f'Temporary error: {error_message}, Backing off: '
sys.stderr.write(f'Temporary error: {error_message}, Backing off: '
f'{int(wait_on_fail)} seconds, Retry: '
f'{current_attempt_num}/{total_num_retries}\n'))
f'{current_attempt_num}/{total_num_retries}\n')
sys.stderr.flush()
time.sleep(wait_on_fail)

View File

@@ -154,28 +154,39 @@ def write_csv_file(csvRows, titles, list_type, todrive):
return True
return False
def filterMatch(filterVal, columns, row):
for column in columns:
if filterVal[1] == 'regex':
if filterVal[2].search(str(row.get(column, ''))):
return True
elif filterVal[1] == 'notregex':
if not filterVal[2].search(str(row.get(column, ''))):
return True
elif filterVal[1] in ['date', 'time']:
if rowDateTimeFilterMatch(
filterVal[1] == 'date', row.get(column, ''),
filterVal[2], filterVal[3]):
return True
elif filterVal[1] == 'count':
if rowCountFilterMatch(
row.get(column, 0), filterVal[2], filterVal[3]):
return True
else: #boolean
if rowBooleanFilterMatch(
row.get(column, False), filterVal[2]):
return True
return False
def rowFilterMatch(filters, columns, row):
for c, filterVal in iter(filters.items()):
for column in columns[c]:
if filterVal[1] == 'regex':
if filterVal[2].search(str(row.get(column, ''))):
return True
elif filterVal[1] == 'notregex':
if not filterVal[2].search(str(row.get(column, ''))):
return True
elif filterVal[1] in ['date', 'time']:
if rowDateTimeFilterMatch(
filterVal[1] == 'date', row.get(column, ''),
filterVal[2], filterVal[3]):
return True
elif filterVal[1] == 'count':
if rowCountFilterMatch(
row.get(column, 0), filterVal[2], filterVal[3]):
return True
else: #boolean
if rowBooleanFilterMatch(
row.get(column, False), filterVal[2]):
return True
if not filterMatch(filterVal, columns[c], row):
return False
return True
def rowDropFilterMatch(filters, columns, row):
for c, filterVal in iter(filters.items()):
if filterMatch(filterVal, columns[c], row):
return True
return False
if GC_Values[GC_CSV_ROW_FILTER] or GC_Values[GC_CSV_ROW_DROP_FILTER]:
@@ -210,7 +221,7 @@ def write_csv_file(csvRows, titles, list_type, todrive):
if (((keepColumns is None) or
rowFilterMatch(GC_Values[GC_CSV_ROW_FILTER], keepColumns, row)) and
((dropColumns is None) or
not rowFilterMatch(GC_Values[GC_CSV_ROW_DROP_FILTER], dropColumns, row))):
not rowDropFilterMatch(GC_Values[GC_CSV_ROW_DROP_FILTER], dropColumns, row))):
rows.append(row)
csvRows = rows
@@ -231,7 +242,14 @@ def write_csv_file(csvRows, titles, list_type, todrive):
'No columns selected with GAM_CSV_HEADER_FILTER and GAM_CSV_HEADER_DROP_FILTER\n'
)
return
csv.register_dialect('nixstdout', lineterminator='\n')
nixstdout_dialect = {'lineterminator': '\n',
'quoting': csv.QUOTE_MINIMAL}
# fix issue with Python 3.10.0 and no escape char
# 3.10.1+ may fix this within Python so hopefully
# this is short-lived.
if sys.version_info.minor >= 10:
nixstdout_dialect['escapechar'] = '\\'
csv.register_dialect('nixstdout', **nixstdout_dialect)
if todrive:
write_to = io.StringIO()
else:
@@ -239,12 +257,11 @@ def write_csv_file(csvRows, titles, list_type, todrive):
writer = csv.DictWriter(write_to,
fieldnames=titles,
dialect='nixstdout',
extrasaction='ignore',
quoting=csv.QUOTE_MINIMAL)
extrasaction='ignore')
try:
writer.writerow(dict((item, item) for item in writer.fieldnames))
writer.writerow({item: item for item in writer.fieldnames})
writer.writerows(csvRows)
except IOError as e:
except OSError as e:
controlflow.system_error_exit(6, e)
if todrive:
admin_email = gam._get_admin_email()
@@ -283,7 +300,8 @@ and follow recommend steps to authorize GAM for Drive access.''')
if GC_Values[GC_NO_BROWSER]:
msg_txt = f'Drive file uploaded to:\n {file_url}'
msg_subj = f'{GC_Values[GC_DOMAIN]} - {list_type}'
gam.send_email(msg_subj, msg_txt)
if not GC_Values[GC_NO_TDEMAIL]:
gam.send_email(msg_subj, msg_txt)
print(msg_txt)
else:
webbrowser.open(file_url)
@@ -291,12 +309,12 @@ and follow recommend steps to authorize GAM for Drive access.''')
def print_error(message):
"""Prints a one-line error message to stderr in a standard format."""
sys.stderr.write('\n{0}{1}\n'.format(ERROR_PREFIX, message))
sys.stderr.write(f'\n{ERROR_PREFIX}{message}\n')
def print_warning(message):
"""Prints a one-line warning message to stderr in a standard format."""
sys.stderr.write('\n{0}{1}\n'.format(WARNING_PREFIX, message))
sys.stderr.write(f'\n{WARNING_PREFIX}{message}\n')
def print_json(object_value, spacing=''):

View File

@@ -59,7 +59,7 @@ def open_file(filename,
# Open a file on disk
f = _open_file(filename, mode, newline=newline, encoding=encoding)
if strip_utf_bom:
utf_bom = u'\ufeff'
utf_bom = '\ufeff'
has_bom = False
if 'b' in mode:
@@ -79,7 +79,7 @@ def open_file(filename,
return f
except IOError as e:
except OSError as e:
controlflow.system_error_exit(6, e)
@@ -101,7 +101,7 @@ def close_file(f, force_flush=False):
try:
f.close()
return True
except IOError as e:
except OSError as e:
display.print_error(e)
return False
@@ -140,7 +140,7 @@ def read_file(filename,
encoding=encoding) as f:
return f.read()
except IOError as e:
except OSError as e:
if continue_on_error:
if display_errors:
display.print_warning(e)
@@ -174,7 +174,7 @@ def write_file(filename,
f.write(data)
return True
except IOError as e:
except OSError as e:
if continue_on_error:
if display_errors:
display.print_error(e)

View File

@@ -13,7 +13,7 @@ class FileutilsTest(unittest.TestCase):
def setUp(self):
self.fake_path = '/some/path/to/file'
super(FileutilsTest, self).setUp()
super().setUp()
@patch.object(fileutils.sys, 'stdin')
def test_open_file_stdin(self, mock_stdin):
@@ -63,7 +63,7 @@ class FileutilsTest(unittest.TestCase):
self.assertEqual(fileutils.UTF8_SIG, mock_open.call_args[1]['encoding'])
def test_open_file_strips_utf_bom_in_utf(self):
bom_prefixed_data = u'\ufefffoobar'
bom_prefixed_data = '\ufefffoobar'
fake_file = io.StringIO(bom_prefixed_data)
mock_open = MagicMock(spec=open, return_value=fake_file)
with patch.object(fileutils, 'open', mock_open):
@@ -89,7 +89,7 @@ class FileutilsTest(unittest.TestCase):
self.assertEqual('foobar', f.read())
def test_open_file_strips_utf_bom_in_binary(self):
bom_prefixed_data = u'\ufefffoobar'.encode('UTF-8')
bom_prefixed_data = '\ufefffoobar'.encode()
fake_file = io.BytesIO(bom_prefixed_data)
mock_open = MagicMock(spec=open, return_value=fake_file)
with patch.object(fileutils, 'open', mock_open):

View File

@@ -281,6 +281,7 @@ def get_all_pages(service,
soft_errors=False,
throw_reasons=None,
retry_reasons=None,
page_args_in_body=False,
**kwargs):
"""Aggregates and returns all pages of a Google service function response.
@@ -311,15 +312,22 @@ def get_all_pages(service,
retry_reasons: A list of Google HTTP error reason strings indicating which
error should be retried, using exponential backoff techniques, when the
error reason is encountered.
page_args_in_body: Some APIs like Chrome Policy want pageToken and pageSize
in the body.
**kwargs: Additional params to pass to the request method.
Returns:
A list of all items received from all paged responses.
"""
if 'maxResults' not in kwargs and 'pageSize' not in kwargs:
if page_args_in_body:
kwargs.setdefault('body', {})
if 'maxResults' not in kwargs and 'pageSize' not in kwargs and 'pageSize' not in kwargs.get('body', {}):
page_key = _get_max_page_size_for_api_call(service, function, **kwargs)
if page_key:
kwargs.update(page_key)
if page_args_in_body:
kwargs['body'].update(page_key)
else:
kwargs.update(page_key)
all_items = []
page_token = None
total_items = 0
@@ -334,7 +342,10 @@ def get_all_pages(service,
if not page_token:
finalize_page_message(page_message)
return all_items
kwargs['pageToken'] = page_token
if page_args_in_body:
kwargs['body']['pageToken'] = page_token
else:
kwargs['pageToken'] = page_token
# TODO: Make this private once all execution related items that use this method
@@ -348,7 +359,7 @@ def handle_oauth_token_error(e, soft_errors):
returns to the caller.
"""
token_error = str(e).replace('.', '')
if token_error in errors.OAUTH2_TOKEN_ERRORS or e.startswith(
if token_error in errors.OAUTH2_TOKEN_ERRORS or token_error.startswith(
'Invalid response'):
if soft_errors:
return

View File

@@ -80,7 +80,7 @@ class GapiTest(unittest.TestCase):
]
self.empty_items_response = {'items': []}
super(GapiTest, self).setUp()
super().setUp()
def test_call_returns_basic_200_response(self):
response = gapi.call(self.mock_service, self.mock_method_name)

View File

@@ -9,6 +9,8 @@ from gam.var import YYYYMMDD_FORMAT
from gam import controlflow
from gam import display
from gam import gapi
from gam import utils
from gam.gapi import directory as gapi_directory
from gam.gapi.directory import orgunits as gapi_directory_orgunits
from gam.gapi.directory.cros import _getFilterDate
@@ -201,6 +203,109 @@ def printAppDevices():
display.write_csv_file(csvRows, titles, 'Chrome Installed Application Devices', todrive)
def printShowCrosTelemetry(mode):
cm = build()
cd = None
parent = _get_customerid()
todrive = False
filter_ = None
readMask = []
orgUnitIdPathMap = {}
diskpercentonly = False
showOrgUnitPath = False
supported_readmask_values = list(cm._rootDesc['schemas']['GoogleChromeManagementV1TelemetryDevice']['properties'].keys())
supported_readmask_values.sort()
supported_readmask_map = {item.lower():item for item in supported_readmask_values}
i = 3
if mode == 'info':
if i >= len(sys.argv):
controlflow.system_error_exit(3, f'<SerialNumber> required for "gam info crostelemetry"')
filter_ = f'serialNumber={sys.argv[i]}'
i += 1
mode = 'show'
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'fields':
field_list = sys.argv[i+1].lower().split(',')
for field_item in field_list:
if field_item not in supported_readmask_map:
controlflow.expected_argument_exit('fields',
', '.join(supported_readmask_values),
field_item)
else:
readMask.append(supported_readmask_map[field_item])
i += 2
elif myarg in supported_readmask_map:
readMask.append(supported_readmask_map[myarg])
i += 1
elif myarg == 'filter':
filter_ = sys.argv[i+1]
i += 2
elif myarg in ['ou', 'org', 'orgunit']:
_, orgUnitId = gapi_directory_orgunits.getOrgUnitId(sys.argv[i + 1], None)
filter_ = f'orgUnitId={orgUnitId[3:]}'
i += 2
elif myarg == 'crossn':
filter_ = f'serialNumber={sys.argv[i + 1]}'
i += 2
elif myarg == 'todrive':
todrive = True
i += 1
elif myarg == 'showorgunitpath':
showOrgUnitPath = True
cd = gapi_directory.build()
i += 1
elif myarg == 'storagepercentonly':
diskpercentonly = True
i += 1
else:
msg = f'{myarg} is not a valid argument to "gam print crostelemetry"'
controlflow.system_error_exit(3, msg)
if not readMask:
readMask = ','.join(supported_readmask_values)
else:
if 'deviceId' not in readMask:
readMask.append('deviceId')
readMask = ','.join(readMask)
gam.printGettingAllItems('Chrome Device Telemetry...', filter_)
page_message = gapi.got_total_items_msg('Chrome Device Telemetry', '...\n')
devices = gapi.get_all_pages(cm.customers().telemetry().devices(),
'list',
'devices',
page_message=page_message,
parent=parent,
filter=filter_,
readMask=readMask)
for device in devices:
if 'totalDiskBytes' in device.get('storageInfo', {}) and 'availableDiskBytes' in device.get('storageInfo', {}):
disk_avail = int(device['storageInfo']['availableDiskBytes'])
disk_size = int(device['storageInfo']['totalDiskBytes'])
if diskpercentonly:
device['storageInfo'] = {}
device['storageInfo']['percentDiskFree'] = int((disk_avail / disk_size) * 100)
device['storageInfo']['percentDiskUsed'] = 100 - device['storageInfo']['percentDiskFree']
for cpuStatusReport in device.get('cpuStatusReport', []):
for tempInfo in cpuStatusReport.pop('cpuTemperatureInfo', []):
cpuStatusReport[f"cpuTemperatureInfo.{tempInfo['label'].strip()}"] = tempInfo['temperatureCelsius']
if showOrgUnitPath:
orgUnitId = device.get('orgUnitId')
if orgUnitId not in orgUnitIdPathMap:
orgUnitIdPathMap[orgUnitId] = gapi_directory_orgunits.orgunit_from_orgunitid(orgUnitId, cd)
device['orgUnitPath'] = orgUnitIdPathMap[orgUnitId]
if mode == 'show':
for device in devices:
display.print_json(device)
print()
print()
else:
csvRows = []
titles = []
for device in devices:
display.add_row_titles_to_csv_file(utils.flatten_json(device),
csvRows, titles)
display.write_csv_file(csvRows, titles, 'Telemetry Devices', todrive)
CHROME_VERSIONS_TITLES = [
'version', 'count', 'channel', 'deviceOsVersion', 'system'
]

View File

@@ -39,6 +39,8 @@ def printshow_policies():
orgunit = None
printer_id = None
app_id = None
body = {}
namespaces = []
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
@@ -51,67 +53,86 @@ def printshow_policies():
elif myarg == 'appid':
app_id = sys.argv[i+1]
i += 2
elif myarg == 'namespace':
namespaces.extend(sys.argv[i+1].replace(',', ' ').split())
i += 2
else:
msg = f'{myarg} is not a valid argument to "gam print chromepolicy"'
controlflow.system_error_exit(3, msg)
if not orgunit:
controlflow.system_error_exit(3, 'You must specify an orgunit')
body = {
'policyTargetKey': {
'targetResource': orgunit,
}
}
body['policyTargetKey'] = {'targetResource': orgunit}
if printer_id:
body['policyTargetKey']['additionalTargetKeys'] = {'printer_id': printer_id}
namespaces = ['chrome.printers']
if not namespaces:
namespaces = ['chrome.printers']
elif app_id:
body['policyTargetKey']['additionalTargetKeys'] = {'app_id': app_id}
namespaces = ['chrome.users.apps',
'chrome.devices.managedGuest.apps',
'chrome.devices.kiosk.apps']
else:
if not namespaces:
namespaces = ['chrome.users.apps',
'chrome.devices.managedGuest.apps',
'chrome.devices.kiosk.apps']
elif not namespaces:
namespaces = [
'chrome.users',
# Not yet implemented:
# 'chrome.devices',
# 'chrome.devices.managedGuest',
# 'chrome.devices.kiosk',
'chrome.users.apps',
'chrome.devices',
'chrome.devices.kiosk',
'chrome.devices.managedGuest',
]
throw_reasons = [gapi_errors.ErrorReason.FOUR_O_O,]
orgunitPath = gapi_directory_orgunits.orgunit_from_orgunitid(orgunit[9:], None)
header = f'Organizational Unit: {orgunitPath}'
if printer_id:
header += f', printerid: {printer_id}'
elif app_id:
header += f', appid: {app_id}'
print(header)
print(f'Organizational Unit: {orgunitPath}')
for namespace in namespaces:
spacing = ' '
body['policySchemaFilter'] = f'{namespace}.*'
body['pageToken'] = None
try:
policies = gapi.get_all_pages(svc.customers().policies(), 'resolve',
items='resolvedPolicies',
throw_reasons=throw_reasons,
customer=customer,
body=body)
body=body,
page_args_in_body=True)
except googleapiclient.errors.HttpError:
policies = []
for policy in sorted(policies, key=lambda k: k.get('value', {}).get('policySchema', '')):
# sort policies first by app/printer id then by schema name
policies = sorted(policies,
key=lambda k: (
list(k.get('targetKey', {}).get('additionalTargetKeys', {}).values()),
k.get('value', {}).get('policySchema', '')))
printed_ids = []
for policy in policies:
print()
name = policy.get('value', {}).get('policySchema', '')
schema = CHROME_SCHEMA_TYPE_MESSAGE.get(name)
print(name)
for key, val in policy['targetKey'].get('additionalTargetKeys', {}).items():
additional_id = f'{key} - {val}'
if additional_id not in printed_ids:
print(f' {additional_id}')
printed_ids.append(additional_id)
spacing = ' '
print(f'{spacing}{name}')
values = policy.get('value', {}).get('value', {})
for setting, value in values.items():
# Handle TYPE_MESSAGE fields with durations or counts as a special case
# Handle TYPE_MESSAGE fields with durations, values, counts and timeOfDay as special cases
schema = CHROME_SCHEMA_TYPE_MESSAGE.get(name, {}).get(setting.lower())
if schema and setting == schema['casedField']:
value = value.get(schema['type'], '')
if value:
if value.endswith('s'):
value = value[:-1]
value = int(value) // schema['scale']
vtype = schema['type']
if vtype in {'duration', 'value'}:
value = value.get(vtype, '')
if value:
if value.endswith('s'):
value = value[:-1]
value = int(value) // schema['scale']
elif vtype == 'count':
pass
else: ##timeOfDay
hours = value.get(vtype, {}).get('hours', 0)
minutes = value.get(vtype, {}).get('minutes', 0)
value = f'{hours:02}:{minutes:02}'
elif isinstance(value, str) and value.find('_ENUM_') != -1:
value = value.split('_ENUM_')[-1]
print(f' {setting}: {value}')
print(f'{spacing}{setting}: {value}')
def build_schemas(svc=None, sfilter=None):
@@ -254,21 +275,45 @@ def delete_policy():
CHROME_SCHEMA_TYPE_MESSAGE = {
'chrome.users.SessionLength':
{'field': 'sessiondurationlimit', 'casedField': 'sessionDurationLimit',
'type': 'duration', 'minVal': 1, 'maxVal': 1440, 'scale': 60},
'chrome.users.AutoUpdateCheckPeriodNew': {
'autoupdatecheckperiodminutesnew':
{'casedField': 'autoUpdateCheckPeriodMinutesNew',
'type': 'duration', 'minVal': 1, 'maxVal': 720, 'scale': 60}},
'chrome.users.BrowserSwitcherDelayDuration':
{'field': 'browserswitcherdelayduration', 'casedField': 'browserSwitcherDelayDuration',
'type': 'duration', 'minVal': 0, 'maxVal': 30, 'scale': 1},
{'browserswitcherdelayduration':
{'casedField': 'browserSwitcherDelayDuration',
'type': 'duration', 'minVal': 0, 'maxVal': 30, 'scale': 1}},
'chrome.users.FetchKeepaliveDurationSecondsOnShutdown':
{'fetchkeepalivedurationsecondsonshutdown':
{'casedField': 'fetchKeepaliveDurationSecondsOnShutdown',
'type': 'duration', 'minVal': 0, 'maxVal': 5, 'scale': 1}},
'chrome.users.MaxInvalidationFetchDelay':
{'field': 'maxinvalidationfetchdelay', 'casedField': 'maxInvalidationFetchDelay',
'type': 'duration', 'minVal': 1, 'maxVal': 30, 'scale': 1},
'chrome.users.SecurityTokenSessionSettings':
{'field': 'securitytokensessionnotificationseconds', 'casedField': 'securityTokenSessionNotificationSeconds',
'type': 'duration', 'minVal': 0, 'maxVal': 9999, 'scale': 1},
{'maxinvalidationfetchdelay':
{'casedField': 'maxInvalidationFetchDelay',
'type': 'duration', 'minVal': 1, 'maxVal': 30, 'scale': 1, 'default': 10}},
'chrome.users.PrintingMaxSheetsAllowed':
{'field': 'printingmaxsheetsallowednullable', 'casedField': 'printingMaxSheetsAllowedNullable',
'type': 'value', 'minVal': 1, 'maxVal': None, 'scale': 1},
{'printingmaxsheetsallowednullable':
{'casedField': 'printingMaxSheetsAllowedNullable',
'type': 'value', 'minVal': 1, 'maxVal': None, 'scale': 1}},
'chrome.users.PrintJobHistoryExpirationPeriodNew':
{'printjobhistoryexpirationperioddaysnew':
{'casedField': 'printJobHistoryExpirationPeriodDaysNew',
'type': 'duration', 'minVal': -1, 'maxVal': None, 'scale': 86400}},
'chrome.users.SecurityTokenSessionSettings':
{'securitytokensessionnotificationseconds':
{'casedField': 'securityTokenSessionNotificationSeconds',
'type': 'duration', 'minVal': 0, 'maxVal': 9999, 'scale': 1}},
'chrome.users.SessionLength':
{'sessiondurationlimit':
{'casedField': 'sessionDurationLimit',
'type': 'duration', 'minVal': 1, 'maxVal': 1440, 'scale': 60}},
'chrome.users.UpdatesSuppressed':
{'updatessuppresseddurationmin':
{'casedField': 'updatesSuppressedDurationMin',
'type': 'count', 'minVal': 1, 'maxVal': 1440, 'scale': 1},
'updatessuppressedstarttime':
{'casedField': 'updatesSuppressedStartTime',
'type': 'timeOfDay'}},
}
@@ -302,19 +347,39 @@ def update_policy():
field = sys.argv[i].lower()
if field in ['ou', 'org', 'orgunit', 'printerid', 'appid'] or '.' in field:
break # field is actually a new policy, orgunit or app/printer id
# Handle TYPE_MESSAGE fields with durations or counts as a special case
schema = CHROME_SCHEMA_TYPE_MESSAGE.get(schemaName)
if schema and field == schema['field']:
casedField = schema['casedField']
value = gam.getInteger(sys.argv[i+1], casedField,
minVal=schema['minVal'], maxVal=schema['maxVal'])*schema['scale']
if schema['type'] == 'duration':
body['requests'][-1]['policyValue']['value'][casedField] = {schema['type']: f'{value}s'}
else:
body['requests'][-1]['policyValue']['value'][casedField] = {schema['type']: value}
body['requests'][-1]['updateMask'] += f'{casedField},'
i += 2
continue
# Handle TYPE_MESSAGE fields with durations, values, counts and timeOfDay as special cases
schema = CHROME_SCHEMA_TYPE_MESSAGE.get(schemaName, {}).get(field)
if schema:
i += 1
casedField = schema['casedField']
vtype = schema['type']
if vtype != 'timeOfDay':
if 'default' not in schema:
value = gam.getInteger(sys.argv[i], casedField,
minVal=schema['minVal'], maxVal=schema['maxVal'])*schema['scale']
i += 1
elif i < len(sys.argv) and sys.argv[i].isdigit():
value = gam.getInteger(sys.argv[i], casedField,
minVal=schema['minVal'], maxVal=schema['maxVal'])*schema['scale']
i += 1
else: # Handle empty value for fields with default
value = schema['default']*schema['scale']
if i < len(sys.argv) and not sys.argv[i]:
i += 1
else:
value = utils.get_hhmm(sys.argv[i])
i += 1
if vtype == 'duration':
body['requests'][-1]['policyValue']['value'][casedField] = {vtype: f'{value}s'}
elif vtype == 'value':
body['requests'][-1]['policyValue']['value'][casedField] = {vtype: value}
elif vtype == 'count':
body['requests'][-1]['policyValue']['value'][casedField] = value
else: ##timeOfDay
hours, minutes = value.split(':')
body['requests'][-1]['policyValue']['value'][casedField] = {vtype: {'hours': hours, 'minutes': minutes}}
body['requests'][-1]['updateMask'] += f'{casedField},'
continue
expected_fields = ', '.join(schemas[myarg]['settings'])
if field not in expected_fields:
msg = f'Expected {myarg} field of {expected_fields}. Got {field}.'

View File

@@ -405,7 +405,7 @@ def sync():
controlflow.csv_field_error_exit(devicetype_column, input_file.fieldnames)
if assettag_column and assettag_column not in input_file.fieldnames:
controlflow.csv_field_error_exit(assettag_column, input_file.fieldnames)
local_devices = []
local_devices = {}
for row in input_file:
# upper() is very important to comparison since Google
# always return uppercase serials
@@ -414,28 +414,43 @@ def sync():
local_device['deviceType'] = static_devicetype
else:
local_device['deviceType'] = row[devicetype_column].strip()
sndt = f"{local_device['serialNumber']}-{local_device['deviceType']}"
if assettag_column:
local_device['assetTag'] = row[assettag_column].strip()
local_devices.append(local_device)
sndt += f"-{local_device['assetTag']}"
local_devices[sndt] = local_device
fileutils.close_file(f)
page_message = gapi.got_total_items_msg('Company Devices', '...\n')
device_fields = ['serialNumber', 'deviceType', 'lastSyncTime', 'name']
if assettag_column:
device_fields.append('assetTag')
fields = f'nextPageToken,devices({",".join(device_fields)})'
remote_devices = gapi.get_all_pages(ci.devices(), 'list', 'devices',
remote_devices = {}
remote_device_map = {}
result = gapi.get_all_pages(ci.devices(), 'list', 'devices',
customer=customer, page_message=page_message,
pageSize=100, filter=device_filter, view='COMPANY_INVENTORY', fields=fields)
remote_device_map = {}
for remote_device in remote_devices:
for remote_device in result:
sn = remote_device['serialNumber']
last_sync = remote_device.pop('lastSyncTime', NEVER_TIME_NOMS)
name = remote_device.pop('name')
remote_device_map[sn] = {'name': name}
sndt = f"{remote_device['serialNumber']}-{remote_device['deviceType']}"
if assettag_column:
if 'assetTag' not in remote_device:
remote_device['assetTag'] = ''
sndt += f"-{remote_device['assetTag']}"
remote_devices[sndt] = remote_device
remote_device_map[sndt] = {'name': name}
if last_sync == NEVER_TIME_NOMS:
remote_device_map[sn]['unassigned'] = True
devices_to_add = [device for device in local_devices if device not in remote_devices]
missing_devices = [device for device in remote_devices if device not in local_devices]
remote_device_map[sndt]['unassigned'] = True
devices_to_add = []
for sndt, device in iter(local_devices.items()):
if sndt not in remote_devices:
devices_to_add.append(device)
missing_devices = []
for sndt, device in iter(remote_devices.items()):
if sndt not in local_devices:
missing_devices.append(device)
print(f'Need to add {len(devices_to_add)} and remove {len(missing_devices)} devices...')
for add_device in devices_to_add:
print(f'Creating {add_device["serialNumber"]}')
@@ -447,8 +462,11 @@ def sync():
print(f' {add_device["serialNumber"]} already exists')
for missing_device in missing_devices:
sn = missing_device['serialNumber']
name = remote_device_map[sn]['name']
unassigned = remote_device_map[sn].get('unassigned')
sndt = f"{sn}-{missing_device['deviceType']}"
if assettag_column:
sndt += f"-{missing_device['assetTag']}"
name = remote_device_map[sndt]['name']
unassigned = remote_device_map[sndt].get('unassigned')
action = unassigned_missing_action if unassigned else assigned_missing_action
if action == 'donothing':
pass

View File

@@ -3,7 +3,7 @@ import sys
import googleapiclient
import gam
from gam.var import *
from gam.var import * # pylint: disable=unused-wildcard-import
from gam import controlflow
from gam import display
from gam import gapi
@@ -12,6 +12,14 @@ from gam.gapi import errors as gapi_errors
from gam.gapi import cloudidentity as gapi_cloudidentity
from gam.gapi.directory import customer as gapi_directory_customer
# This allows easy switching between v1 and v1beta1
# v1
CIGROUP_API_BETA = 'cloudidentity'
CIGROUP_MEMBERKEY = 'preferredMemberKey'
# v1beta1
#CIGROUP_API_BETA = 'cloudidentity_beta'
#CIGROUP_MEMBERKEY = 'memberKey'
def create():
ci = gapi_cloudidentity.build()
@@ -73,9 +81,10 @@ def delete():
def info():
ci = gapi_cloudidentity.build('cloudidentity_beta')
ci = gapi_cloudidentity.build(CIGROUP_API_BETA)
group = gam.normalizeEmailAddressOrUID(sys.argv[3])
getUsers = True
getSecuritySettings = True
showJoinDate = True
showUpdateDate = False
showMemberTree = False
@@ -94,11 +103,20 @@ def info():
elif myarg == 'membertree':
showMemberTree = True
i += 1
elif myarg in ['nosecurity', 'nosecuritysettings']:
getSecuritySettings = False
else:
controlflow.invalid_argument_exit(myarg, 'gam info cigroup')
name = group_email_to_id(ci, group)
basic_info = gapi.call(ci.groups(), 'get', name=name)
display.print_json(basic_info)
if getSecuritySettings:
sec_info = gapi.call(ci.groups(),
'getSecuritySettings',
name=f'{name}/securitySettings',
readMask='*')
print(' Security settings:')
display.print_json(sec_info, spacing=' ')
if getUsers and not showMemberTree:
if not showJoinDate and not showUpdateDate:
view = 'BASIC'
@@ -116,7 +134,7 @@ def info():
print(' Members:')
for member in members:
role = get_single_role(member.get('roles', [])).lower()
email = member.get('memberKey', {}).get('id')
email = member.get(CIGROUP_MEMBERKEY, {}).get('id')
member_type = member.get('type', 'USER').lower()
jc_string = ''
if showJoinDate:
@@ -145,7 +163,7 @@ def print_member_tree(ci, group_id, cached_group_members, spaces, show_role):
for member in cached_group_members[group_id]:
member_id = member.get('name', '')
member_id = member_id.split('/')[-1]
email = member.get('memberKey', {}).get('id')
email = member.get(CIGROUP_MEMBERKEY, {}).get('id')
member_type = member.get('type', 'USER').lower()
if show_role:
role = get_single_role(member.get('roles', [])).lower()
@@ -187,9 +205,15 @@ GROUP_ROLES_MAP = {
def print_():
ci = gapi_cloudidentity.build('cloudidentity_beta')
ci = gapi_cloudidentity.build(CIGROUP_API_BETA)
i = 3
members = membersCountOnly = managers = managersCountOnly = owners = ownersCountOnly = False
members = False
membersCountOnly = False
managers = False
managersCountOnly = False
owners = False
ownersCountOnly = False
memberRestrictions = False
gapi_directory_customer.setTrueCustomerId()
parent = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
usemember = None
@@ -232,6 +256,15 @@ def print_():
if myarg == 'managerscount':
managersCountOnly = True
i += 1
elif myarg in ['memberrestrictions']:
memberRestrictions = True
display.add_titles_to_csv_file(
['memberRestrictionQuery',],
titles)
display.add_titles_to_csv_file(
['memberRestrictionEvaluation',],
titles)
i += 1
else:
controlflow.invalid_argument_exit(sys.argv[i], 'gam print cigroups')
if roles:
@@ -310,12 +343,12 @@ def print_():
)
page_message = gapi.got_total_items_first_last_msg('Members')
validRoles, _, _ = gam._getRoleVerification(
'.'.join(roles), 'nextPageToken,members(email,id,role)')
','.join(roles), 'nextPageToken,members(email,id,role)')
groupMembers = gapi.get_all_pages(ci.groups().memberships(),
'list',
'memberships',
page_message=page_message,
message_attribute=['memberKey', 'id'],
message_attribute=[CIGROUP_MEMBERKEY, 'id'],
soft_errors=True,
parent=groupKey_id,
view='BASIC')
@@ -329,7 +362,7 @@ def print_():
ownersList = []
ownersCount = 0
for member in groupMembers:
member_email = member['memberKey']['id']
member_email = member[CIGROUP_MEMBERKEY]['id']
role = get_single_role(member.get('roles', []))
if not validRoles or role in validRoles:
if role == ROLE_MEMBER:
@@ -363,6 +396,16 @@ def print_():
group['OwnersCount'] = ownersCount
if not ownersCountOnly:
group['Owners'] = memberDelimiter.join(ownersList)
if memberRestrictions:
name = f'{groupKey_id}/securitySettings'
print(f'Getting member restrictions for {groupEmail} ({i}/{count}')
sec_info = gapi.call(ci.groups(),
'getSecuritySettings',
name=name,
readMask='*')
if 'memberRestriction' in sec_info:
group['memberRestrictionQuery'] = sec_info['memberRestriction'].get('query', '')
group['memberRestrictionEvaluation'] = sec_info['memberRestriction'].get('evaluation', {}).get('state', '')
csvRows.append(group)
if sortHeaders:
display.sort_csv_titles([
@@ -412,7 +455,7 @@ def _get_groups_list(ci=None, member=None, parent=None):
def get_membership_graph(member):
ci = gapi_cloudidentity.build('cloudidentity_beta')
ci = gapi_cloudidentity.build(CIGROUP_API_BETA)
query = f"member_key_id == '{member}' && 'cloudidentity.googleapis.com/groups.discussion_forum' in labels"
result = gapi.call(ci.groups().memberships(),
'getMembershipGraph',
@@ -422,7 +465,7 @@ def get_membership_graph(member):
def print_members():
ci = gapi_cloudidentity.build('cloudidentity_beta')
ci = gapi_cloudidentity.build(CIGROUP_API_BETA)
todrive = False
gapi_directory_customer.setTrueCustomerId()
parent = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
@@ -479,8 +522,8 @@ def print_members():
view='FULL',
pageSize=500,
page_message=page_message,
message_attribute=['memberKey', 'id'])
#fields='nextPageToken,memberships(memberKey,roles,createTime,updateTime)')
message_attribute=[CIGROUP_MEMBERKEY, 'id'])
#fields=f'nextPageToken,memberships({CIGROUP_MEMBERKEY},roles,createTime,updateTime)')
if roles:
group_members = filter_members_to_roles(group_members, roles)
for member in group_members:
@@ -538,7 +581,7 @@ def update():
]
return (role, expireTime, users_email)
ci = gapi_cloudidentity.build('cloudidentity_beta')
ci = gapi_cloudidentity.build(CIGROUP_API_BETA)
group = sys.argv[3]
myarg = sys.argv[4].lower()
items = []
@@ -565,7 +608,7 @@ def update():
items.append(item)
elif len(users_email) > 0:
body = {
'memberKey': {
CIGROUP_MEMBERKEY: {
'id': users_email[0]
},
'roles': [{
@@ -785,12 +828,12 @@ def update():
page_message=page_message,
throw_reasons=gapi_errors.MEMBERS_THROW_REASONS,
parent=parent,
fields='nextPageToken,memberships(memberKey,roles)')
fields=f'nextPageToken,memberships({CIGROUP_MEMBERKEY},roles)')
result = filter_members_to_roles(result, roles)
if not result:
print('Group already has 0 members')
return
users_email = [member['memberKey']['id'] for member in result]
users_email = [member[CIGROUP_MEMBERKEY]['id'] for member in result]
sys.stderr.write(
f'Group: {group}, Will remove {len(users_email)} {", ".join(roles).lower()}s.\n'
)
@@ -808,6 +851,7 @@ def update():
else:
i = 4
body = {}
sec_body = {}
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'name':
@@ -830,17 +874,41 @@ def update():
}]
}
i += 2
elif myarg in ['memberrestriction', 'memberrestrictions']:
query = sys.argv[i + 1]
member_types = {
'USER': '1',
'SERVICE_ACCOUNT': '2',
'GROUP': '3',
}
for key, val in member_types.items():
query = query.replace(key, val)
sec_body['memberRestriction'] = {'query': query}
i += 2
else:
controlflow.invalid_argument_exit(sys.argv[i],
'gam update cigroup')
updateMask = ','.join(body.keys())
name = group_email_to_id(ci, group)
print(f'Updating group {group}')
gapi.call(ci.groups(),
'patch',
updateMask=updateMask,
name=name,
body=body)
if body:
updateMask = ','.join(body.keys())
name = group_email_to_id(ci, group)
print(f'Updating group {group}')
gapi.call(ci.groups(),
'patch',
updateMask=updateMask,
name=name,
body=body)
if sec_body:
updateMask = 'member_restriction.query'
# it seems like a bug that API requires /securitySettings
# appended to name. We'll see if Google servers change this
# at some point.
name = f'{group_email_to_id(ci, group)}/securitySettings'
print(f'Updating group {group} security settings')
gapi.call(ci.groups(),
'updateSecuritySettings',
name=name,
updateMask=updateMask,
body=sec_body)
def group_email_to_id(ci, group, i=0, count=0):

View File

@@ -755,11 +755,11 @@ def doPrintCrosDevices():
cros['autoUpdateExpiration'])
row = {}
for attrib in cros:
if attrib not in set([
if attrib not in {
'kind', 'etag', 'tpmVersionInfo', 'recentUsers',
'activeTimeRanges', 'deviceFiles', 'cpuStatusReports',
'diskVolumeReports', 'systemRamFreeReports'
]):
}:
row[attrib] = cros[attrib]
if selectedLists.get('activeTimeRanges'):
timergs = cros.get('activeTimeRanges', [])

View File

@@ -266,6 +266,8 @@ GROUP_ATTRIBUTES_ARGUMENT_TO_PROPERTY_MAP = {
'customReplyTo',
'defaultmessagedenynotificationtext':
'defaultMessageDenyNotificationText',
'defaultsender':
'defaultSender',
'enablecollaborativeinbox':
'enableCollaborativeInbox',
'favoriterepliesontop':
@@ -979,6 +981,9 @@ def update():
sys.stderr.write(
f'Group: {group}, Will add {len(to_add)} and remove {len(to_remove)} {role}s.\n'
)
for user in to_remove:
items.append(
['gam', 'update', 'group', group, 'remove', user])
for user in to_add:
item = ['gam', 'update', 'group', group, 'add']
if role:
@@ -987,9 +992,6 @@ def update():
item.append(delivery)
item.append(user)
items.append(item)
for user in to_remove:
items.append(
['gam', 'update', 'group', group, 'remove', user])
elif myarg in ['delete', 'remove']:
_, users_email, _ = _getRoleAndUsers()
if not exists(cd, group):
@@ -1219,7 +1221,7 @@ def getGroupAttrValue(myarg, value, gs_object, gs_body, function):
params) in list(gs_object['schemas']['Groups']['properties'].items()):
if attrib in ['kind', 'etag', 'email']:
continue
if myarg == attrib.lower():
if myarg == attrib.lower().replace('_', ''):
if params['type'] == 'integer':
try:
if value[-1:].upper() == 'M':

View File

@@ -299,7 +299,7 @@ def update():
def orgUnitPathQuery(path, checkSuspended):
query = "orgUnitPath='{0}'".format(path.replace(
query = "orgUnitPath='{}'".format(path.replace(
"'", "\\'")) if path != '/' else ''
if checkSuspended is not None:
query += f' isSuspended={checkSuspended}'

View File

@@ -3,6 +3,7 @@ from time import sleep
import gam
from gam import gapi
from gam.gapi import directory as gapi_directory
from gam.gapi import errors as gapi_errors
def get_primary(email):
@@ -53,10 +54,16 @@ def wait_for_mailbox(users):
i += 1
user = gam.normalizeEmailAddressOrUID(user)
while True:
result = gapi.call(cd.users(),
'get',
'fields=isMailboxSetup',
userKey=user)
try:
result = gapi.call(cd.users(),
'get',
'fields=isMailboxSetup',
userKey=user,
throw_reasons=[gapi_errors.ErrorReason.USER_NOT_FOUND])
except gapi_errors.GapiUserNotFoundError:
print(f'{user} mailboxIsSetup: False (user does not exist yet)')
sleep(3)
continue
mailbox_is_setup = result.get('isMailboxSetup')
print(f'{user} mailboxIsSetup: {mailbox_is_setup}')
if mailbox_is_setup:

View File

@@ -60,6 +60,10 @@ class GapiGroupNotFoundError(Exception):
pass
class GapiInternalServerError(Exception):
pass
class GapiInvalidError(Exception):
pass
@@ -125,6 +129,7 @@ class ErrorReason(Enum):
GATEWAY_TIMEOUT = 'gatewayTimeout'
GROUP_NOT_FOUND = 'groupNotFound'
INTERNAL_ERROR = 'internalError'
INTERNAL_SERVER_ERROR = 'internalServerError'
INVALID = 'invalid'
INVALID_ARGUMENT = 'invalidArgument'
INVALID_MEMBER = 'invalidMember'
@@ -199,6 +204,8 @@ ERROR_REASON_TO_EXCEPTION = {
GapiGatewayTimeoutError,
ErrorReason.GROUP_NOT_FOUND:
GapiGroupNotFoundError,
ErrorReason.INTERNAL_SERVER_ERROR:
GapiInternalServerError,
ErrorReason.INVALID:
GapiInvalidError,
ErrorReason.INVALID_ARGUMENT:
@@ -336,6 +343,10 @@ def get_gapi_error_detail(e,
if 'Requested entity was not found' in message or 'does not exist' in message:
error = _create_http_error_dict(404, ErrorReason.NOT_FOUND.value,
message)
elif http_status == 500:
if 'Failed to convert server response to JSON' in message:
error = _create_http_error_dict(500, ErrorReason.INTERNAL_SERVER_ERROR.value,
message)
else:
if 'error_description' in error:
if error['error_description'] == 'Invalid Value':

View File

@@ -285,7 +285,7 @@ def showReport():
customerId = GC_Values[GC_CUSTOMER_ID]
if customerId == MY_CUSTOMER:
customerId = None
filters = parameters = actorIpAddress = startTime = endTime = eventName = orgUnitId = None
filters = parameters = actorIpAddress = groupIdFilter = startTime = endTime = eventName = orgUnitId = None
tryDate = datetime.date.today().strftime(YYYYMMDD_FORMAT)
to_drive = False
userKey = 'all'
@@ -330,6 +330,9 @@ def showReport():
elif myarg == 'ip':
actorIpAddress = sys.argv[i + 1]
i += 2
elif myarg == 'groupidfilter':
groupIdFilter = sys.argv[i + 1]
i += 2
elif myarg == 'todrive':
to_drive = True
i += 1
@@ -489,7 +492,8 @@ def showReport():
endTime=endTime,
eventName=eventName,
filters=filters,
orgUnitID=orgUnitId)
orgUnitID=orgUnitId,
groupIdFilter=groupIdFilter)
if activities:
titles = ['name']
csvRows = []
@@ -500,9 +504,9 @@ def showReport():
purge_parameters = True
for event in events:
for item in event.get('parameters', []):
if set(item) == set(['value', 'name']):
if set(item) == {'value', 'name'}:
event[item['name']] = item['value']
elif set(item) == set(['intValue', 'name']):
elif set(item) == {'intValue', 'name'}:
if item['name'] in ['start_time', 'end_time']:
val = item.get('intValue')
if val is not None:
@@ -513,9 +517,9 @@ def showReport():
val-62135683200).isoformat()
else:
event[item['name']] = item['intValue']
elif set(item) == set(['boolValue', 'name']):
elif set(item) == {'boolValue', 'name'}:
event[item['name']] = item['boolValue']
elif set(item) == set(['multiValue', 'name']):
elif set(item) == {'multiValue', 'name'}:
event[item['name']] = ' '.join(item['multiValue'])
elif item['name'] == 'scope_data':
parts = {}

View File

@@ -790,7 +790,7 @@ def downloadExport():
done = False
while not done:
status, done = downloader.next_chunk()
sys.stdout.write(' Downloaded: {0:>7.2%}\r'.format(
sys.stdout.write(' Downloaded: {:>7.2%}\r'.format(
status.progress()))
sys.stdout.flush()
sys.stdout.write('\n Download complete. Flushing to disk...\n')

View File

@@ -90,7 +90,7 @@ class Request(google_auth_httplib2.Request):
@_force_user_agent(GAM_USER_AGENT)
def __call__(self, *args, **kwargs):
"""Inserts the GAM user-agent header in requests."""
return super(Request, self).__call__(*args, **kwargs)
return super().__call__(*args, **kwargs)
class AuthorizedHttp(google_auth_httplib2.AuthorizedHttp):
@@ -99,4 +99,4 @@ class AuthorizedHttp(google_auth_httplib2.AuthorizedHttp):
@_force_user_agent(GAM_USER_AGENT)
def request(self, *args, **kwargs):
"""Inserts the GAM user-agent header in requests."""
return super(AuthorizedHttp, self).request(*args, **kwargs)
return super().request(*args, **kwargs)

View File

@@ -15,7 +15,7 @@ class CreateHttpTest(unittest.TestCase):
def setUp(self):
SetGlobalVariables()
super(CreateHttpTest, self).setUp()
super().setUp()
def test_create_http_sets_default_values_on_http(self):
http = transport.create_http()
@@ -56,7 +56,7 @@ class TransportTest(unittest.TestCase):
self.mock_content)
self.mock_credentials = MagicMock()
self.test_uri = 'http://example.com'
super(TransportTest, self).setUp()
super().setUp()
@patch.object(transport, 'create_http')
def test_create_request_uses_default_http(self, mock_create_http):

View File

@@ -1,7 +1,3 @@
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import datetime
import re
import sys
@@ -32,7 +28,7 @@ class LazyLoader(types.ModuleType):
self._local_name = local_name
self._parent_module_globals = parent_module_globals
super(LazyLoader, self).__init__(name)
super().__init__(name)
def _load(self):
# Import the target module and insert it into the parent's namespace
@@ -123,7 +119,7 @@ def dehtml(text):
def indentMultiLineText(message, n=0):
return message.replace('\n', '\n{0}'.format(' ' * n)).rstrip()
return message.replace('\n', '\n{}'.format(' ' * n)).rstrip()
def flatten_json(structure, key='', path='', flattened=None, listLimit=None):
@@ -254,6 +250,18 @@ def get_delta_time(argstr):
return deltaTime
def get_hhmm(argstr):
argstr = argstr.strip()
if argstr:
try:
dateTime = datetime.datetime.strptime(argstr, HHMM_FORMAT)
return argstr
except ValueError:
controlflow.system_error_exit(
2, f'expected a <{HHMM_FORMAT_REQUIRED}>; got {argstr}')
controlflow.system_error_exit(2, f'expected a <{HHMM_FORMAT_REQUIRED}>')
def get_yyyymmdd(argstr, minLen=1, returnTimeStamp=False, returnDateTime=False):
argstr = argstr.strip()
if argstr:

View File

@@ -8,7 +8,7 @@ import platform
import re
GAM_AUTHOR = 'Jay Lee <jay0lee@gmail.com>'
GAM_VERSION = '6.05'
GAM_VERSION = '6.13'
GAM_LICENSE = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
GAM_URL = 'https://git.io/gam'
@@ -124,7 +124,7 @@ SKUS = {
'Google-Apps': {
'product': 'Google-Apps',
'aliases': ['standard', 'free'],
'displayName': 'G Suite Free/Standard'
'displayName': 'G Suite Legacy'
},
'Google-Apps-For-Business': {
'product': 'Google-Apps',
@@ -286,12 +286,8 @@ PRODUCTID_NAME_MAPPINGS = {
# Legacy APIs that use v1 discovery. Newer APIs should all use v2.
V1_DISCOVERY_APIS = {
'admin',
'calendar',
'drive',
'oauth2',
'reseller',
'siteVerification',
}
API_NAME_MAPPING = {
@@ -299,7 +295,7 @@ API_NAME_MAPPING = {
'reports': 'admin',
'datatransfer': 'admin',
'drive3': 'drive',
'cloudresourcemanagerv1': 'cloudresourcemanager',
'calendar': 'calendar-json',
'cloudidentity_beta': 'cloudidentity',
}
@@ -313,8 +309,7 @@ API_VER_MAPPING = {
'classroom': 'v1',
'cloudidentity': 'v1',
'cloudidentity_beta': 'v1beta1',
'cloudresourcemanager': 'v2',
'cloudresourcemanagerv1': 'v1',
'cloudresourcemanager': 'v3',
'contactdelegation': 'v1',
'datatransfer': 'datatransfer_v1',
'directory': 'directory_v1',
@@ -463,6 +458,7 @@ DRIVEFILE_FIELDS_CHOICES_MAP = {
'createddate': 'createdDate',
'createdtime': 'createdDate',
'description': 'description',
'driveid': 'driveId',
'editable': 'editable',
'explicitlytrashed': 'explicitlyTrashed',
'fileextension': 'fileExtension',
@@ -478,6 +474,7 @@ DRIVEFILE_FIELDS_CHOICES_MAP = {
'lastviewedbymedate': 'lastViewedByMeDate',
'lastviewedbymetime': 'lastViewedByMeDate',
'lastviewedbyuser': 'lastViewedByMeDate',
'linksharemetadata': 'linkShareMetadata',
'md5': 'md5Checksum',
'md5checksum': 'md5Checksum',
'md5sum': 'md5Checksum',
@@ -496,6 +493,7 @@ DRIVEFILE_FIELDS_CHOICES_MAP = {
'owners': 'owners',
'parents': 'parents',
'permissions': 'permissions',
'resourcekey': 'resourceKey',
'quotabytesused': 'quotaBytesUsed',
'quotaused': 'quotaBytesUsed',
'shareable': 'shareable',
@@ -503,6 +501,7 @@ DRIVEFILE_FIELDS_CHOICES_MAP = {
'sharedwithmedate': 'sharedWithMeDate',
'sharedwithmetime': 'sharedWithMeDate',
'sharinguser': 'sharingUser',
'shortcutdetails': 'shortcutDetails',
'spaces': 'spaces',
'thumbnaillink': 'thumbnailLink',
'title': 'title',
@@ -619,17 +618,22 @@ GOOGLEDOC_VALID_EXTENSIONS_MAP = {
}
MACOS_CODENAMES = {
6: 'Snow Leopard',
7: 'Lion',
8: 'Mountain Lion',
9: 'Mavericks',
10: 'Yosemite',
11: 'El Capitan',
12: 'Sierra',
13: 'High Sierra',
14: 'Mojave',
15: 'Catalina'
}
10: {
6: 'Snow Leopard',
7: 'Lion',
8: 'Mountain Lion',
9: 'Mavericks',
10: 'Yosemite',
11: 'El Capitan',
12: 'Sierra',
13: 'High Sierra',
14: 'Mojave',
15: 'Catalina',
16: 'Big Sur'
},
11: 'Big Sur',
12: 'Monterey',
}
_MICROSOFT_FORMATS_LIST = [{
'mime':
@@ -894,8 +898,6 @@ RT_TAG_REPLACE_PATTERN = re.compile(r'{(.*?)}')
LOWERNUMERIC_CHARS = string.ascii_lowercase + string.digits
ALPHANUMERIC_CHARS = LOWERNUMERIC_CHARS + string.ascii_uppercase
URL_SAFE_CHARS = ALPHANUMERIC_CHARS + '-._~'
PASSWORD_SAFE_CHARS = ALPHANUMERIC_CHARS + string.punctuation + ' '
FILENAME_SAFE_CHARS = ALPHANUMERIC_CHARS + '-_.() '
FILTER_ADD_LABEL_TO_ARGUMENT_MAP = {
'IMPORTANT': 'important',
@@ -1074,7 +1076,7 @@ COLLABORATIVE_INBOX_ATTRIBUTES = [
'favoriteRepliesOnTop',
]
GROUP_SETTINGS_LIST_ATTRIBUTES = set([
GROUP_SETTINGS_LIST_ATTRIBUTES = {
# ACL choices
'whoCanAdd',
'whoCanApproveMembers',
@@ -1110,12 +1112,13 @@ GROUP_SETTINGS_LIST_ATTRIBUTES = set([
'whoCanUnmarkFavoriteReplyOnAnyTopic',
'whoCanViewGroup',
'whoCanViewMembership',
# Miscellaneous hoices
# Miscellaneous choices
'default_sender',
'messageModerationLevel',
'replyTo',
'spamModerationLevel',
])
GROUP_SETTINGS_BOOLEAN_ATTRIBUTES = set([
}
GROUP_SETTINGS_BOOLEAN_ATTRIBUTES = {
'allowExternalMembers',
'allowGoogleCommunication',
'allowWebPosting',
@@ -1128,7 +1131,7 @@ GROUP_SETTINGS_BOOLEAN_ATTRIBUTES = set([
'membersCanPostAsTheGroup',
'sendMessageDenyNotification',
'showInGroupDirectory',
])
}
#
# Global variables
@@ -1245,10 +1248,12 @@ GC_DOMAIN = 'domain'
GC_DRIVE_DIR = 'drive_dir'
# Enable Delegated Admin Service Accounts
GC_ENABLE_DASA = 'enabledasa'
# If no_browser is False, writeCSVfile won't open a browser when todrive is set
# If no_browser is True, writeCSVfile won't open a browser when todrive is set
# and doRequestOAuth prints a link and waits for the verification code when
# oauth2.txt is being created
GC_NO_BROWSER = 'no_browser'
# If no_tdemail is True, writeCSVfile won't send an email
GC_NO_TDEMAIL = 'no_tdemail'
# oauth_browser forces usage of web server OAuth flow that proved problematic.
GC_OAUTH_BROWSER = 'oauth_browser'
# Disable GAM API caching
@@ -1286,7 +1291,7 @@ GC_TLS_MAX_VERSION = 'tls_max_ver'
# Path to certificate authority file for validating TLS hosts
GC_CA_FILE = 'ca_file'
TLS_MIN = 'TLSv1_2' if hasattr(ssl.SSLContext(), 'minimum_version') else None
TLS_MIN = 'TLSv1_3' if hasattr(ssl.SSLContext(), 'minimum_version') else None
GC_Defaults = {
GC_ADMIN_EMAIL: '',
GC_AUTO_BATCH_MIN: 0,
@@ -1303,6 +1308,7 @@ GC_Defaults = {
GC_DRIVE_DIR: '',
GC_ENABLE_DASA: False,
GC_NO_BROWSER: False,
GC_NO_TDEMAIL: False,
GC_NO_CACHE: False,
GC_NO_SHORT_URLS: False,
GC_NO_UPDATE_CHECK: False,
@@ -1388,6 +1394,9 @@ GC_VAR_INFO = {
GC_NO_BROWSER: {
GC_VAR_TYPE: GC_TYPE_BOOLEAN
},
GC_NO_TDEMAIL: {
GC_VAR_TYPE: GC_TYPE_BOOLEAN
},
GC_NO_CACHE: {
GC_VAR_TYPE: GC_TYPE_BOOLEAN
},
@@ -1937,6 +1946,9 @@ DELTA_DATE_FORMAT_REQUIRED = '(+|-)<Number>(d|w|y)'
DELTA_TIME_PATTERN = re.compile(r'^([+-])(\d+)([mhdwy])$')
DELTA_TIME_FORMAT_REQUIRED = '(+|-)<Number>(m|h|d|w|y)'
HHMM_FORMAT = '%H:%M'
HHMM_FORMAT_REQUIRED = 'hh:mm'
YYYYMMDD_FORMAT = '%Y-%m-%d'
YYYYMMDD_FORMAT_REQUIRED = 'yyyy-mm-dd'

View File

@@ -1,12 +1,13 @@
yubikey-manager>=4.0.0
cryptography
distro; sys_platform == 'linux'
filelock
google-api-python-client>=2.1
google-auth-httplib2
google-auth-oauthlib>=0.4.1
google-auth>=1.11.2
google-auth>=2.3.2
httplib2>=0.17.0
importlib.metadata; python_version < '3.8'
passlib>=1.7.2
python-dateutil
yubikey-manager>=4.0.0
pathvalidate

49
src/setup.cfg Normal file
View File

@@ -0,0 +1,49 @@
[metadata]
name = GAM for Google Workspace
version = 6.0.7
description = Command line management for Google Workspaces
long_description = file: readme.md
long_description_content_type = text/markdown
url = https://github.com/jay0lee/GAM
author = Jay Lee
author_email = jay0lee@gmail.com
license = Apache
license_files = LICENSE
keywords = google, oauth2, gsuite, google-apps, google-admin-sdk, google-drive, google-cloud, google-calendar, gam, google-api, oauth2-client, google-workspace
classifiers =
Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
License :: OSI Approved :: Apache License
[options]
packages = find:
python_requires = >=3.6
install_requires =
cryptography
distro; sys_platform == 'linux'
filelock
google-api-python-client >= 2.1
google-auth-httplib2
google-auth-oauthlib >= 0.4.1
google-auth >= 1.11.2
httplib2 >= 0.17.0
importlib.metadata; python_version < '3.8'
passlib >= 1.7.2
python-dateutil
yubikey-manager >= 4.0.0
pathvalidate
# used during pip install .[test]
[options.extras_require]
test = pre-commit
[options.entry_points]
console_scripts =
gam = gam.__main__:main
[bdist_wheel]
universal = True

3
src/setup.py Normal file
View File

@@ -0,0 +1,3 @@
from setuptools import setup
setup()

View File

@@ -1,15 +1,13 @@
#!/usr/bin/env python3
#from packaging import version
from distutils.version import LooseVersion
from packaging import version
import sys
a = sys.argv[1]
b = sys.argv[2]
#result = version.parse(a) >= version.parse(b)
result = LooseVersion(a) >= LooseVersion(b)
result = version.parse(a) >= version.parse(b)
if result:
print('OK: %s is equal or newer than %s' % (a, b))
print(f'OK: {a} is equal or newer than {b}')
else:
print('ERROR: %s is older than %s' % (a, b))
print(f'ERROR: {a} is older than {b}')
sys.exit(not result)