Compare commits

...

218 Commits
v6.22 ... v6.5

Author SHA1 Message Date
Ross Scroggs
a774fc0beb GCP cleanup (#1602) 2023-02-23 11:44:52 -05:00
Jay Lee
f3429bd537 Update build.yml 2023-02-23 08:50:03 -05:00
Jay Lee
37876acfda Update var.py 2023-02-23 08:17:22 -05:00
Jay Lee
2a6dd0d1a2 fix building iamcredentials 2023-02-22 17:30:10 +00:00
Jay Lee
b0626dd37a improve on gam enable apis 2023-02-17 22:07:36 +00:00
Jay Lee
ed0ed8d7fc fix Id 2023-02-17 20:33:47 +00:00
Jay Lee
d67d999930 enable APIs command for signjwt 2023-02-17 20:32:29 +00:00
Jay Lee
ac79cff6b9 create signjwtserviceaccount 2023-02-17 19:39:02 +00:00
Jay Lee
50aadc6ea7 allow forcing OAuth for service account 2023-02-17 15:40:36 +00:00
Jay Lee
9036d114ed signjwt key_type for key-less service account auth 2023-02-17 15:17:01 +00:00
Jay Lee
75c19104ae fix ipv6 with checkconn 2023-02-15 17:22:34 +00:00
Jay Lee
d9b7f88287 6.42 - build shared drive restrictions dynamically 2023-02-13 21:51:41 +00:00
Jay Lee
ae28c09560 6.41 - fixes #1600 2023-02-11 13:40:59 +00:00
Jay Lee
6ffc738a51 Update gam-install.sh 2023-02-11 08:12:35 -05:00
Jay Lee
82dcc4de6a rebuild to get Python 3.11.2 2023-02-08 10:35:58 -05:00
Jay Lee
f7a426f65a rebuild for OpenSSL 3.0.8 2023-02-07 12:25:31 -05:00
Jay Lee
a94ef78066 fix Vault download filenames 2023-02-06 21:43:33 +00:00
Ross Scroggs
62d738f5c2 copy storagebucket/vault cleanup (#1599) 2023-02-06 15:50:39 -05:00
Jay Lee
1c56a0a608 Update var.py 2023-02-06 09:57:47 -05:00
Jay Lee
dc3976bdda gam copy vaultexport/storagebucket commands 2023-02-06 13:33:26 +00:00
Ross Scroggs
454778b190 print chromeaues/chromeversions cleanup, add print chromeneedsattn (#1598)
* print chromeaues/chromeversions cleanup, add print chromeneedsattn

* Fix typo

* Define new ChromeOS fields
2023-02-03 14:10:35 -05:00
Ross Scroggs
5e78c93b71 Added gam print chromeaues (#1597) 2023-01-30 19:42:28 -05:00
Jay Lee
3aefe21f16 Update build.yml 2023-01-13 11:54:03 -05:00
Jay Lee
0fc7958ccc Update build.yml 2023-01-13 11:14:35 -05:00
Jay Lee
13dc4e74c9 Update build.yml 2023-01-12 11:11:07 -05:00
Jay Lee
a17fa16841 Update var.py 2022-12-28 19:08:01 -05:00
Jay Lee
b13757f5d3 Update var.py 2022-12-28 18:49:37 -05:00
Jay Lee
b9df6f4762 Assured controls SKU 2022-12-28 18:44:20 -05:00
Jay Lee
b7d1c62486 [no ci] let other jobs keep building 2022-12-24 14:29:36 -05:00
Jay Lee
90e5f1b665 use search endpoint when cigroup query is specified 2022-12-18 17:19:39 +00:00
Jay Lee
3132fd7783 another fix for show holds 2022-12-17 23:10:58 +00:00
Jay Lee
87808902e6 cat and allow closed matters on show holds 2022-12-17 22:56:12 +00:00
Jay Lee
fb33d8186e fix Win64 PyInstaller 2022-12-17 18:10:53 +00:00
Jay Lee
8bd2e7f879 Upgrade yubkey for 5.0 release. Fixes #1587 2022-12-17 16:28:41 +00:00
Jay Lee
e66744e3f1 Update build.yml 2022-12-07 10:24:14 -05:00
Ross Scroggs
85f2979313 Fix Chrome schema enum processing (#1582)
Prefix should not include anything after ENUM_
```
        name: RollbackToTargetVersionEnum
          value:
            name: ROLLBACK_TO_TARGET_VERSION_ENUM_ROLLBACK_DISABLED
              number: 1
            name: ROLLBACK_TO_TARGET_VERSION_ENUM_ROLLBACK_AND_RESTORE_IF_POSSIBLE
              number: 3
```
2022-12-02 21:51:39 -05:00
Jay Lee
a85ee9b108 Update build.yml 2022-12-02 12:20:18 -05:00
Jay Lee
9ab2f38436 Update build.yml 2022-12-02 09:57:21 -05:00
Jay Lee
5bcdca4fcc Update build.yml 2022-12-02 09:48:53 -05:00
Jay Lee
729edb65be Ubuntu 20.04 so less users need legacy build 2022-12-01 16:25:38 -05:00
Jay Lee
db8afb769b Update build.yml 2022-12-01 14:09:56 -05:00
Jay Lee
7dfc93892c Update build.yml 2022-12-01 13:47:09 -05:00
Jay Lee
d278cb6939 Update build.yml 2022-12-01 13:14:59 -05:00
Jay Lee
bced5172d2 single spec for one file/folder 2022-12-01 18:05:55 +00:00
Jay Lee
bb5beb66a7 Update build.yml 2022-12-01 12:09:56 -05:00
Jay Lee
f849b6ddb7 --noconfirm to overwrite existing gam folder 2022-11-30 21:12:41 +00:00
Jay Lee
d2733a53a2 fix gampath 2022-11-30 21:04:36 +00:00
Jay Lee
1b1ae44f5d Merge branch 'main' of github.com:GAM-team/GAM 2022-11-30 20:51:53 +00:00
Jay Lee
8515dc2616 Switch to PyInstaller onedir for better performance 2022-11-30 20:51:31 +00:00
Jay Lee
ba7a8d8937 Update build.yml 2022-11-30 09:51:49 -05:00
Jay Lee
d543fb9917 Update build.yml 2022-11-30 09:16:22 -05:00
Ross Scroggs
f4d390b77b Use returnnameonly, it's like returnidonly in create drivefile (#1579)
* Document nameonly in create/update inboundssoprofile

* Use returnnameonly, it's like returnidonly in create drivefile
2022-11-30 09:15:36 -05:00
Jay Lee
ffbce1fd25 no ~~ 2022-11-29 15:01:55 +00:00
Jay Lee
2d78ec6edd merge 2022-11-29 14:48:07 +00:00
Jay Lee
9cacdd166f name_only argument for ssoprofile 2022-11-29 14:43:25 +00:00
Ross Scroggs
9af0a5d843 Code fix, consistency preference (#1578)
* Code fix, consistency preference

* Code cleanup

* Code cleanup for sso assignments

* Fix typo

* Shorten lines
2022-11-22 07:08:15 -05:00
Jay Lee
3313295532 Test with user displayname 2022-11-21 23:50:30 +00:00
Jay Lee
fdf6c147dc fix temperature in chrome telemetry 2022-11-21 23:30:21 +00:00
Jay Lee
323dbd5ca9 Allow setting displayName for users 2022-11-21 23:21:45 +00:00
Jay Lee
d01fd74fa3 merge 2022-11-18 16:08:13 +00:00
Jay Lee
8c33b88e3e Inbound SSO improvements 2022-11-18 16:06:14 +00:00
Jay Lee
5d11397fca Update build.yml 2022-11-16 16:05:11 -05:00
Jay Lee
995321978f Update build.yml 2022-11-16 15:58:56 -05:00
Jay Lee
448789dad0 Update build.yml 2022-11-16 15:39:41 -05:00
Jay Lee
e9ba6819ba Update vault.py 2022-11-16 15:31:07 -05:00
Jay Lee
3056c7b803 Update vault.py 2022-11-16 15:23:53 -05:00
Jay Lee
f2c28fd1f7 Update vault.py 2022-11-16 15:20:49 -05:00
Jay Lee
11e4ff1eb5 Update __init__.py 2022-11-16 15:03:23 -05:00
Jay Lee
81cd74c244 Update build.yml 2022-11-16 13:08:44 -05:00
Jay Lee
faade7c057 Update build.yml 2022-11-16 12:47:49 -05:00
Jay Lee
0032066e1d Update build.yml 2022-11-16 12:33:17 -05:00
Jay Lee
dd938baced Update build.yml 2022-11-16 12:12:01 -05:00
Jay Lee
b835b6ee36 Update build.yml 2022-11-16 11:04:06 -05:00
Jay Lee
3660d65df6 Update build.yml 2022-11-16 10:52:19 -05:00
Ross Scroggs
3e0b4125e0 Code fixup (#1577) 2022-11-16 09:30:17 -05:00
Ross Scroggs
9820a3d81e Inbound SSO documentation; org is synonym for ou and orgunit (#1576)
Are `gam info inboundssoassignment` and `gam delete inboundssoassignment` coming?

Is `gam info inboundssocredentials` coming?
2022-11-15 07:07:04 -05:00
Jay Lee
b670a4cee6 Update build.yml 2022-11-14 21:40:53 -05:00
Jay Lee
a5dd5275c8 Update build.yml 2022-11-14 20:57:35 -05:00
Jay Lee
9b6ad2fa60 prepare 6.31 2022-11-15 01:32:22 +00:00
Jay Lee
1d80028c93 Update build.yml 2022-11-14 20:21:38 -05:00
Jay Lee
a013e95fcf Windows actions doesn\'t like an argument that has / as first char 2022-11-15 00:40:43 +00:00
Jay Lee
eb4d6ece3f Update build.yml 2022-11-14 18:53:37 -05:00
Jay Lee
a50d1ef456 new credentials with inbound sso scope 2022-11-14 17:50:38 -05:00
Jay Lee
c179ed732c login and logout, not signin signout 2022-11-14 21:21:32 +00:00
Jay Lee
a85a313ebb Merge branch 'main' of github.com:GAM-team/GAM 2022-11-14 21:10:11 +00:00
Jay Lee
534ccd275d remove / that seems to break Github Actions 2022-11-14 21:09:59 +00:00
Ross Scroggs
3c3d043276 Sort fields in info group, allow gal as an alias for includeinglobaladdresslist (#1575) 2022-11-14 16:09:28 -05:00
Jay Lee
786adb7c44 remove debug 2022-11-14 21:03:00 +00:00
Jay Lee
bb6c8dc225 more debug for orgunits on windows 2022-11-14 20:49:30 +00:00
Jay Lee
a7cd88b2be Merge branch 'main' of github.com:GAM-team/GAM 2022-11-14 20:32:01 +00:00
Jay Lee
a9fad337e2 debug ou create body 2022-11-14 20:31:48 +00:00
Jay Lee
d4dc1b1589 Update build.yml 2022-11-14 15:19:36 -05:00
Jay Lee
ad94adbb53 more actions 2022-11-14 20:12:21 +00:00
Jay Lee
b692799dcb bash quote fixes 2022-11-14 19:54:31 +00:00
Jay Lee
04dcf47746 rollback shelve (leave lowmem framework) 2022-11-14 19:40:36 +00:00
Jay Lee
aebb3c44fe quick fixes 2022-11-14 19:31:16 +00:00
Jay Lee
8cf345196a Inbound SSO API first take 2022-11-14 19:23:37 +00:00
Jay Lee
173fdb2297 Merge branch 'main' of github.com:GAM-team/GAM 2022-11-02 12:26:57 +00:00
Jay Lee
120db6e7d8 Updated Actions creds 2022-11-02 12:23:01 +00:00
Jay Lee
55555506be Update decrypt.sh 2022-11-01 16:32:15 -04:00
Jay Lee
41965e962d rebuild to pickup OpenSSL 3.0.7 2022-11-01 14:39:36 -04:00
Jay Lee
30fdd00d65 GAM 6.30
To be released soon after OpenSSL 3.0.7...
2022-11-01 07:54:38 -04:00
Ross Scroggs
37e3fd904d Rework getting local-Google time offset (#1572) 2022-10-30 08:22:58 -04:00
Jay Lee
dc22b024b8 Try disabling check hostname on time checks 2022-10-29 11:21:00 -04:00
Jay Lee
f412d5ad4c Update build.yml 2022-10-29 10:12:33 -04:00
Jay Lee
24cfe807e6 Update build.yml 2022-10-28 09:35:42 -04:00
Jay Lee
6a721ac2c1 [no ci] 2022-10-28 09:25:08 -04:00
Jay Lee
4a4b22dfba Update build.yml 2022-10-28 08:30:26 -04:00
Ross Scroggs
6d4524c153 Update var.py (#1571) 2022-10-26 17:36:53 -04:00
Jay Lee
d7b2f82a4a Update build.yml 2022-10-26 11:39:32 -04:00
Jay Lee
844a2fe1e8 Update build.yml 2022-10-26 10:03:44 -04:00
Jay Lee
baf822c685 Update build.yml 2022-10-26 09:59:32 -04:00
Jay Lee
f3169a631c Update build.yml 2022-10-26 08:44:31 -04:00
Jay Lee
d171db36bc Update build.yml 2022-10-26 08:40:01 -04:00
Jay Lee
34c7576cd5 Update build.yml 2022-10-26 08:01:21 -04:00
Jay Lee
f859d0678b Update build.yml 2022-10-25 20:19:09 -04:00
Jay Lee
0986cb3fd9 Update build.yml 2022-10-25 19:17:16 -04:00
Jay Lee
645fd9a135 Update build.yml 2022-10-25 10:15:20 -04:00
Jay Lee
9582e6840a Update build.yml 2022-10-24 18:07:32 -04:00
Jay Lee
a8a9cfb2ab Update build.yml 2022-10-24 18:03:11 -04:00
Jay Lee
5519b33a08 Update build.yml 2022-10-24 17:54:55 -04:00
Jay Lee
976ef0252e Update build.yml 2022-10-24 17:52:13 -04:00
Jay Lee
e6829d0804 Update build.yml 2022-10-24 16:51:58 -04:00
Jay Lee
9f985a7b26 Update build.yml 2022-10-17 15:30:26 -04:00
Jay Lee
a628aeb1a8 Update build.yml 2022-10-17 14:54:35 -04:00
Jay Lee
d81c80b150 Update build.yml 2022-10-17 13:56:42 -04:00
Jay Lee
63ee016691 Update build.yml 2022-10-17 13:51:45 -04:00
Ross Scroggs
4935385572 Pass lock (l) not one (1) to initargs (#1567) 2022-10-17 09:08:42 -04:00
Jay Lee
30069d3039 Update build.yml 2022-10-12 12:00:07 -04:00
Jay Lee
3ef8a5a762 Update var.py 2022-10-12 08:10:38 -04:00
Jay Lee
b12fda5007 Update build.yml 2022-10-11 17:15:03 -04:00
Jay Lee
26925c30c1 Update build.yml 2022-10-07 11:40:59 -04:00
Jay Lee
4085816fa3 Update build.yml 2022-10-07 10:52:12 -04:00
Jay Lee
7e36e5abe6 Update build.yml 2022-10-07 10:23:26 -04:00
Jay Lee
2037189148 Update build.yml 2022-10-07 08:13:43 -04:00
Jay Lee
c7781e66e1 Update build.yml 2022-10-06 22:18:30 -04:00
Jay Lee
8843675ad4 leave Homebrew in place 2022-10-06 22:47:35 +00:00
Jay Lee
c05a1ea6b4 Honor nobrowser.txt on auth 2022-10-06 22:36:25 +00:00
Jay Lee
d9a5ac849b try again with new creds 2022-10-06 17:45:05 +00:00
Jay Lee
51d4c29dd5 update creds 2022-10-06 17:38:01 +00:00
Jay Lee
c2bb9cbdaf Catch revoke and throw nicer error 2022-10-05 17:09:39 +00:00
Lewis Lebentz
d185765831 Add Frontline Worker alias (#1566) 2022-10-05 12:40:53 -04:00
Jay Lee
f57f311f16 Update build.yml 2022-09-27 07:07:21 -04:00
Jay Lee
4c81849c60 Update build.yml 2022-09-26 16:28:42 -04:00
Jay Lee
156c8319d9 Update build.yml 2022-09-26 14:20:54 -04:00
Jay Lee
b8de3310d0 Update build.yml 2022-09-26 13:53:29 -04:00
Jay Lee
f28cf664cb Update build.yml 2022-09-26 13:46:14 -04:00
Jay Lee
02b876155a Allow 'info domain' for delegate admins 2022-09-15 20:03:03 +00:00
Jay Lee
97bd1f71c3 Merge branch 'main' of https://github.com/GAM-team/GAM 2022-09-15 14:46:53 +00:00
Jay Lee
8be4445f0d Fix crm org retrieval 2022-09-15 14:45:02 +00:00
Jay Lee
550cf47db4 Update reports.py 2022-09-15 10:22:29 -04:00
Jay Lee
05d32eec08 Update __init__.py 2022-09-14 19:17:04 -04:00
Jay Lee
59c181eeda Update __init__.py 2022-09-14 19:07:58 -04:00
Jay Lee
dd5fd2a2c3 Update __init__.py 2022-09-14 18:02:59 -04:00
Jay Lee
6ab8fbf538 test with lowmemory.txt 2022-09-14 17:49:40 -04:00
Jay Lee
509919da84 Reduce memory with shelve. Fixes #1560 2022-09-14 18:55:17 +00:00
Jay Lee
04bd5f36a0 Update build.yml 2022-09-07 10:17:17 -04:00
Jay Lee
801f5b7861 Update build.yml 2022-09-06 16:50:18 -04:00
Jay Lee
09d86e1220 Update build.yml 2022-09-02 17:13:40 -04:00
Jay Lee
6110aa1d32 Update build.yml 2022-09-02 16:41:25 -04:00
Jay Lee
11e6c80dbf Update build.yml 2022-09-02 16:31:42 -04:00
Jay Lee
1f32536ff7 Update build.yml 2022-09-02 12:15:07 -04:00
Jay Lee
7979206f21 Update build.yml 2022-09-02 09:45:10 -04:00
Jay Lee
f7901790ad Update build.yml 2022-09-02 09:44:16 -04:00
Jay Lee
7fae16f962 Update build.yml 2022-09-01 14:56:23 -04:00
Jay Lee
1dd76012f8 Update __init__.py 2022-09-01 14:34:55 -04:00
Jay Lee
8fd3f4ee7d Update setup.cfg 2022-08-31 16:27:21 -04:00
Jay Lee
e30b8ed53e Update setup.cfg 2022-08-31 16:26:06 -04:00
Jay Lee
e0960d9113 Update setup.cfg 2022-08-31 16:16:55 -04:00
Jay Lee
35dda1cd34 Update setup.cfg 2022-08-31 15:56:59 -04:00
Jay Lee
ef2253fe58 Update setup.cfg 2022-08-31 15:52:57 -04:00
Jay Lee
ecea3aed7e [no ci] 6.25 2022-08-31 15:52:22 -04:00
Jay Lee
2e81cae271 add roots.pem to MSI 2022-08-31 15:44:06 -04:00
Jay Lee
080eede356 Update devices.py 2022-08-31 14:10:22 -04:00
Jay Lee
fe37c687e4 Update build.yml 2022-08-31 11:59:10 -04:00
Jay Lee
27efef1d9b Update build.yml 2022-08-31 08:41:52 -04:00
Jay Lee
52aa1ac0da Update build.yml 2022-08-31 08:39:18 -04:00
Jay Lee
b5c23fdb83 Update gam.spec 2022-08-31 08:30:53 -04:00
Jay Lee
0b16c9aef4 Default to Google's roots.pem CA file 2022-08-31 08:29:34 -04:00
Jay Lee
3be97acd9c Update build.yml 2022-08-31 08:27:11 -04:00
GitHub Action
8df8e6797f [ci skip] Updated roots.pem 2022-08-31 12:19:58 +00:00
Jay Lee
156ba44656 Update get-roots.yml 2022-08-31 08:19:42 -04:00
Jay Lee
1b3663d60c Update get-roots.yml 2022-08-31 08:15:38 -04:00
Jay Lee
8f0ea2f6a5 Rename get-roots.yaml to get-roots.yml 2022-08-31 08:13:59 -04:00
Jay Lee
5e34b12e5c Update get-roots.yaml 2022-08-31 08:13:11 -04:00
Jay Lee
d124575a91 Update get-roots.yaml 2022-08-31 08:11:35 -04:00
Jay Lee
f5364ab4d0 Download Google roots.pem 2022-08-31 08:10:19 -04:00
Jay Lee
b5580c5649 Update var.py 2022-08-29 17:07:31 -04:00
Jay Lee
e9200ea8fb Update var.py 2022-08-29 17:03:00 -04:00
Jay Lee
2e0c280ea6 Update oauth.py 2022-08-29 17:02:41 -04:00
Ross Scroggs
3948a414b5 Back to client access for user invitations (#1553) 2022-08-24 15:24:38 -04:00
Jay Lee
2c83068605 enable user invite scope by default 2022-08-24 17:53:57 +00:00
Jay Lee
6f6ccad00b further refine gam checkconn 2022-08-22 15:16:40 +00:00
Ross Scroggs
bd18f14137 checkconnection cleanup (#1552) 2022-08-22 09:19:39 -04:00
Jay Lee
d54ca7ee43 Update build.yml 2022-08-20 09:31:46 -04:00
Jay Lee
19452c2461 Fix info customer 2022-08-20 13:10:23 +00:00
Jay Lee
4e2e96a6dd Update build.yml 2022-08-20 08:54:15 -04:00
Jay Lee
7957d131c0 Merge branch 'main' of https://github.com/GAM-team/GAM 2022-08-19 20:37:32 +00:00
Jay Lee
ca9dfaff1d gam checkconn first shot 2022-08-19 20:33:19 +00:00
Ross Scroggs
7e9475791b Handle Reports API bug (#1550)
* Handle Reports API bug

* Handle Reports API bug
2022-08-18 19:07:17 -04:00
Jay Lee
c8fb44a7c4 Update build.yml 2022-08-10 19:42:57 -04:00
Jay Lee
bb70183bc7 Update build.yml 2022-08-10 09:11:14 -04:00
Jay Lee
ff80ba1814 Update build.yml 2022-08-09 10:52:51 -04:00
Jay Lee
5d292dcaf7 Update var.py 2022-08-04 14:34:49 -04:00
Jay Lee
bcc5c4520f Update build.yml 2022-08-04 13:44:48 -04:00
Jay Lee
aa7ea59b5e Update oauth.py 2022-08-04 09:33:13 -04:00
Jay Lee
16e85d6d5c pass CA file to fetch_token so it's used by requests 2022-08-03 11:26:36 -04:00
Ross Scroggs
453e65ec53 Clean up calendar ACL documentation (#1545) 2022-08-02 10:05:03 -04:00
Jay Lee
4cbcb9418c Update build.yml 2022-08-02 07:11:48 -04:00
Jay Lee
f2120229e2 Update userinvitations.py 2022-07-20 18:52:32 -04:00
Ross Scroggs
4d2db30000 Update course field names (#1542) 2022-07-18 17:17:11 -04:00
Ross Scroggs
ca575b267b Bound sleep time in create project (#1541) 2022-07-15 13:35:53 -04:00
Jay Lee
3216666a94 retry project creation status check 10 times instead of 5 2022-07-15 11:08:36 -04:00
Ross Scroggs
4ef5606f05 Add cros_ou and cros_ou_and_children to <CrOSTypeEntity> (#1538)
* Add cros_ou and cros_ou_and_children to <CrOSTypeEntity>

Most useful here:
`gam update org|ou <OrgUnitPath> add|move <CrOSTypeEntity>`

* Update GamCommands.txt

* Code cleanup/appease pylint
2022-07-14 13:48:02 -04:00
Ross Scroggs
6122dc3353 Allow print of cros OU and children (#1537) 2022-07-14 09:53:43 -04:00
Jay Lee
14ae792091 Update build.yml 2022-07-13 14:17:14 -04:00
Ross Scroggs
9da5065700 Two updates (#1536)
New CRoS actions

Allow child privileges in create|update adminrole
2022-07-12 14:07:04 -04:00
Jay Lee
22e155998d Update gam-install.sh 2022-06-30 14:56:51 -04:00
37 changed files with 3086 additions and 441 deletions

Binary file not shown.

BIN
.github/actions/creds.tar.xz.gpg vendored Normal file

Binary file not shown.

View File

@@ -14,4 +14,5 @@ gpg --quiet --batch --yes --decrypt --passphrase="${PASSCODE}" \
--output "${credsfile}" "${gpgfile}"
tar xvvf "${credsfile}" --directory "${gampath}"
ls -l "${gampath}"
rm -rvf "${gpgfile}"
rm -rvf "${credsfile}"

View File

@@ -22,71 +22,98 @@ jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-22.04
jid: 1
#- os: ubuntu-20.04
# jid: 1
# goal: build
# arch: x86_64
# openssl_archs: linux-x86_64
#- os: [self-hosted, linux, arm64, gcp]
# jid: 2
# goal: build
# arch: aarch64
# openssl_archs: linux-aarch64
- os: ubuntu-20.04
jid: 3
goal: build
arch: x86_64
openssl_archs: linux-x86_64
- os: [self-hosted, linux, arm64]
jid: 2
staticx: yes
- os: [self-hosted, linux, arm64, gcp]
jid: 4
goal: build
arch: aarch64
openssl_archs: linux-aarch64
staticx: yes
- os: macos-12
jid: 4
jid: 5
goal: build
arch: universal2
openssl_archs: darwin64-x86_64 darwin64-arm64
- os: windows-2022
jid: 5
jid: 6
goal: build
arch: Win64
openssl_archs: VC-WIN64A
- os: windows-2022
jid: 6
jid: 7
goal: build
arch: Win32
openssl_archs: VC-WIN32
- os: ubuntu-22.04
goal: test
python: "3.7"
jid: 7
arch: x86_64
- os: ubuntu-22.04
goal: test
python: "3.8"
jid: 8
arch: x86_64
- os: ubuntu-22.04
goal: test
python: "3.9"
python: "3.8"
jid: 9
arch: x86_64
- os: ubuntu-22.04
goal: test
python: "3.11-dev"
python: "3.9"
jid: 10
arch: x86_64
- os: ubuntu-22.04
goal: test
python: "3.10"
jid: 11
arch: x86_64
- os: ubuntu-22.04
goal: test
python: "3.12.0-alpha - 3.12"
jid: 12
arch: x86_64
steps:
- uses: actions/checkout@master
- uses: actions/checkout@v3
with:
persist-credentials: false
fetch-depth: 0
- name: Cache multiple paths
uses: actions/cache@v2
if: matrix.goal == 'build'
uses: actions/cache@v3
id: cache-python-ssl
with:
path: |
bin
key: gam-${{ matrix.jid }}-20220621
bin.tar.xz
src/cpython
key: gam-${{ matrix.jid }}-20230208
- name: Untar Cache archive
if: matrix.goal == 'build' && steps.cache-python-ssl.outputs.cache-hit == 'true'
working-directory: ${{ github.workspace }}
run: |
tar xvvf bin.tar.xz
- name: Use pre-compiled Python for testing
if: matrix.python != ''
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python }}
@@ -117,17 +144,17 @@ jobs:
sudo apt-get -qq --yes update
sudo apt-get -qq --yes install swig libpcsclite-dev
- name: MacOS remove Homebrew
if: runner.os == 'macOS'
run: |
# remove everything except the libraries needed by yubikey-manager
brew uninstall $(brew list | grep -v 'pcre\|swig\|pcsc-lite')
#- name: MacOS remove Homebrew
# if: runner.os == 'macOS'
# run: |
# # remove everything except the libraries needed by yubikey-manager
# brew uninstall $(brew list | grep -v 'pcre\|swig\|pcsc-lite')
- name: MacOS install tools
if: runner.os == 'macOS'
run: |
# Install latest Rust
curl -fsS -o rust.sh https://sh.rustup.rs
curl --retry 5 --retry-connrefused -fsS -o rust.sh https://sh.rustup.rs
bash ./rust.sh -y
source $HOME/.cargo/env
# needed for Rust to compile cryptography Python package for universal2
@@ -145,8 +172,10 @@ jobs:
arch: ${{ matrix.arch }}
jid: ${{ matrix.jid }}
openssl_archs: ${{ matrix.openssl_archs }}
staticx: ${{ matrix.staticx }}
run: |
echo "We are running on ${RUNNER_OS}"
LD_LIBRARY_PATH="${OPENSSL_INSTALL_PATH}/lib:${PYTHON_INSTALL_PATH}/lib"
if [[ "${arch}" == "Win64" ]]; then
PYEXTERNALS_PATH="amd64"
PYBUILDRELEASE_ARCH="x64"
@@ -178,21 +207,22 @@ jobs:
MAKE=nmake
MAKEOPT=""
PERL="c:\strawberry\perl\bin\perl.exe"
echo "PYTHON=${PYTHON_INSTALL_PATH}\python.exe" >> $GITHUB_ENV
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${PYTHON_SOURCE_PATH}/PCbuild/${PYEXTERNALS_PATH}"
echo "PYTHON=${PYTHON_SOURCE_PATH}/PCbuild/${PYEXTERNALS_PATH}/python.exe" >> $GITHUB_ENV
echo "GAM_ARCHIVE_ARCH=${GAM_ARCHIVE_ARCH}" >> $GITHUB_ENV
echo "WIX_ARCH=${WIX_ARCH}" >> $GITHUB_ENV
fi
echo "We'll run make with: ${MAKEOPT}"
echo "JID=${jid}" >> $GITHUB_ENV
echo "staticx=${staticx}" >> $GITHUB_ENV
echo "arch=${arch}" >> $GITHUB_ENV
echo "LD_LIBRARY_PATH=${LD_LIBRARY_PATH}" >> $GITHUB_ENV
echo "MAKE=${MAKE}" >> $GITHUB_ENV
echo "MAKEOPT=${MAKEOPT}" >> $GITHUB_ENV
echo "PERL=${PERL}" >> $GITHUB_ENV
echo "PYEXTERNALS_PATH=${PYEXTERNALS_PATH}" >> $GITHUB_ENV
echo "PYBUILDRELEASE_ARCH=${PYBUILDRELEASE_ARCH}" >> $GITHUB_ENV
echo "openssl_archs=${openssl_archs}" >> $GITHUB_ENV
echo "LD_LIBRARY_PATH=${OPENSSL_INSTALL_PATH}/lib:${PYTHON_INSTALL_PATH}/lib" >> $GITHUB_ENV
#echo "PATH=${PATH}:${PYTHON_INSTALL_PATH}/scripts" >> $GITHUB_ENV
- name: Get latest stable OpenSSL source
if: matrix.goal == 'build' && steps.cache-python-ssl.outputs.cache-hit != 'true'
@@ -330,7 +360,7 @@ jobs:
$env:OPENSSL_EXT_TARGET_PATH = "${env:OPENSSL_EXT_PATH}${env:PYEXTERNALS_PATH}"
echo "Copying our OpenSSL to ${env:OPENSSL_EXT_TARGET_PATH}"
mkdir "${env:OPENSSL_EXT_TARGET_PATH}\include\openssl\"
Copy-Item -Path "${env:GITHUB_WORKSPACE}/src/openssl-${env:openssl_archs}\LICENSE.txt" -Destination "${env:OPENSSL_EXT_TARGET_PATH}\LICENSE"
Copy-Item -Path "${env:GITHUB_WORKSPACE}/src/openssl-${env:openssl_archs}\LICENSE.txt" -Destination "${env:OPENSSL_EXT_TARGET_PATH}\LICENSE" -Verbose
cp -v "$env:OPENSSL_INSTALL_PATH\lib\*" "${env:OPENSSL_EXT_TARGET_PATH}"
cp -v "$env:OPENSSL_INSTALL_PATH\bin\*" "${env:OPENSSL_EXT_TARGET_PATH}"
cp -v "$env:OPENSSL_INSTALL_PATH\include\openssl\*" "${env:OPENSSL_EXT_TARGET_PATH}\include\openssl\"
@@ -350,22 +380,10 @@ jobs:
run: |
cd "${env:PYTHON_SOURCE_PATH}"
# We need out custom openssl.props which uses OpenSSL 3 DLL names
Copy-Item -Path "${env:GITHUB_WORKSPACE}\src\tools\openssl.props" -Destination PCBuild\
Copy-Item -Path "${env:GITHUB_WORKSPACE}\src\tools\openssl.props" -Destination PCBuild\ -Verbose
echo "Building for ${env:PYBUILDRELEASE_ARCH}..."
PCBuild\build.bat -m --pgo -c Release -p "${env:PYBUILDRELEASE_ARCH}"
- name: Windows Install Python
if: matrix.goal == 'build' && runner.os == 'Windows' && steps.cache-python-ssl.outputs.cache-hit != 'true'
shell: powershell
run: |
cd "${env:PYTHON_SOURCE_PATH}"
mkdir "${env:PYTHON_INSTALL_PATH}\lib"
mkdir "${env:PYTHON_INSTALL_PATH}\include"
Copy-Item -Path "PCBuild\${env:PYEXTERNALS_PATH}\*" "${env:PYTHON_INSTALL_PATH}\"
Copy-Item -Path "${env:PYTHON_SOURCE_PATH}\Lib\*" "${env:PYTHON_INSTALL_PATH}\lib\" -recurse
Copy-Item -Path "${env:PYTHON_SOURCE_PATH}\Include\*" "${env:PYTHON_INSTALL_PATH}\include\" -recurse
Copy-Item -Path "${env:PYTHON_SOURCE_PATH}\PC\*.h" "${env:PYTHON_INSTALL_PATH}\include\"
- name: Mac/Linux Build Python
if: matrix.goal == 'build' && runner.os != 'Windows' && steps.cache-python-ssl.outputs.cache-hit != 'true'
run: |
@@ -379,6 +397,9 @@ jobs:
cd "${PYTHON_SOURCE_PATH}"
$MAKE altinstall
$MAKE bininstall
export PATH="${PATH}:${PYTHON_INSTALL_PATH}/bin"
echo "PATH=${PATH}" >> $GITHUB_ENV
echo "PATH: ${PATH}"
- name: Run Python
run: |
@@ -386,12 +407,27 @@ jobs:
- name: Upgrade pip, wheel, etc
run: |
curl -O https://bootstrap.pypa.io/get-pip.py
curl --retry 5 --retry-connrefused -O https://bootstrap.pypa.io/get-pip.py
"${PYTHON}" get-pip.py
"${PYTHON}" -m pip install --upgrade pip
"${PYTHON}" -m pip install --upgrade wheel
"${PYTHON}" -m pip install --upgrade setuptools
- name: Install pip requirements
run: |
if [[ "${RUNNER_OS}" == "macOS" ]]; then
"${PYTHON}" -m pip install --upgrade cffi ${PIP_ARGS}
"${PYTHON}" -m pip download --only-binary :all: \
--dest . \
--no-cache \
--no-deps \
--platform macosx_10_15_universal2 \
cryptography
"${PYTHON}" -m pip install --force-reinstall --no-deps cryptography*.whl
fi
"${PYTHON}" -m pip install --upgrade -r requirements.txt ${PIP_ARGS}
"${PYTHON}" -m pip list
- name: Install PyInstaller
if: matrix.goal == 'build'
run: |
@@ -402,40 +438,37 @@ jobs:
# remove pre-compiled bootloaders so we fail if bootloader compile fails
rm -rvf PyInstaller/bootloader/*-*/*
cd bootloader
if [[ "${arch}" == "Win32" ]]; then
export PYINSTALLER_BUILD_ARGS="--target-arch=32bit"
fi
case "${arch}" in
"Win32")
export PYINSTALLER_BUILD_ARGS="--target-arch=32bit"
;;
"Win64")
export PYINSTALLER_BUILD_ARGS="--target-arch=64bit"
;;
esac
echo "PyInstaller build arguments: ${PYINSTALLER_BUILD_ARGS}"
"${PYTHON}" ./waf all $PYINSTALLER_BUILD_ARGS
cd ../..
cd ..
echo "---- Installing PyInstaller ----"
"${PYTHON}" -m pip install pyinstaller
- name: Install pip requirements
run: |
if [[ "${RUNNER_OS}" == "macOS" ]]; then
for package in cryptography; do
"${PYTHON}" -m pip install --upgrade cffi ${PIP_ARGS}
"${PYTHON}" -m pip download --only-binary :all: \
--dest . \
--no-cache \
--no-deps \
--platform macosx_10_15_universal2 \
$package
"${PYTHON}" -m pip install --force-reinstall --no-deps $package*.whl
done
find $PYTHON_INSTALL_PATH/lib/python3.10/site-packages -type f -name "*.so" -exec du -sh "{}" \;
fi
"${PYTHON}" -m pip install --upgrade -r requirements.txt ${PIP_ARGS}
"${PYTHON}" -m pip list
"${PYTHON}" -m pip install .
- name: Build GAM with PyInstaller
if: matrix.goal != 'test'
run: |
export gampath="./dist/gam"
if [[ "${staticx}" == "yes" ]]; then
export distpath="./dist/gam"
export gampath="${distpath}"
else
export distpath="./dist"
export gampath="${distpath}/gam"
fi
mkdir -p -v "${gampath}"
if [[ "${RUNNER_OS}" == "macOS" ]]; then
export gampath=$($PYTHON -c "import os; print(os.path.realpath('$gampath'))")
elif [[ "${RUNNER_OS}" == "Windows" ]]; then
# Work around issue where PyInstaller picks up python3.dll from other Python versions
# https://github.com/pyinstaller/pyinstaller/issues/7102
export PATH="/usr/bin"
else
export gampath=$(realpath "${gampath}")
fi
@@ -443,8 +476,22 @@ jobs:
echo "gampath=${gampath}" >> $GITHUB_ENV
echo "gam=${gam}" >> $GITHUB_ENV
echo -e "GAM: ${gam}\nGAMPATH: ${gampath}"
"${PYTHON}" -m PyInstaller --clean --distpath="${gampath}" gam.spec
# TEMP force everything back to one file.
export PYINSTALLER_BUILD_ONEFILE="yes"
export distpath="./dist/gam"
export gampath="${distpath}"
"${PYTHON}" -m PyInstaller --clean --noconfirm --distpath="${distpath}" gam.spec
- name: Copy extra package files
if: matrix.goal == 'build'
run: |
cp -v roots.pem $gampath
cp -v LICENSE $gampath
cp -v GamCommands.txt $gampath
if [[ "${RUNNER_OS}" == "Windows" ]]; then
cp -v gam-setup.bat $gampath
fi
- name: Basic Tests all jobs
run: |
$PYTHON -m unittest discover --start-directory ./ --pattern "*_test.py" --buffer
@@ -454,10 +501,8 @@ jobs:
echo "GAMVERSION=${GAMVERSION}" >> $GITHUB_ENV
- name: Linux/MacOS package
if: runner.os != 'Windows' && matrix.goal == 'build'
if: runner.os != 'Windows' && matrix.goal == 'build' && matrix.staticx != 'yes'
run: |
cp -v LICENSE $gampath
cp -v GamCommands.txt $gampath
if [[ "${RUNNER_OS}" == "macOS" ]]; then
GAM_ARCHIVE="gam-${GAMVERSION}-macos-universal2.tar.xz"
elif [[ "${RUNNER_OS}" == "Linux" ]]; then
@@ -466,14 +511,14 @@ jobs:
fi
tar -C dist/ --create --verbose --exclude-from "${GITHUB_WORKSPACE}/.github/actions/package_exclusions.txt" --file $GAM_ARCHIVE --xz gam
- name: Linux 64-bit install patchelf/staticx
if: runner.os == 'Linux' && contains(runner.arch, '64') && matrix.goal != 'test'
- name: Install StaticX
if: matrix.staticx == 'yes'
run: |
"${PYTHON}" -m pip install --upgrade patchelf-wrapper
"${PYTHON}" -m pip install --upgrade staticx
- name: Linux 64-bit Make Static
if: runner.os == 'Linux' && contains(runner.arch, '64') && matrix.goal != 'test'
- name: Make StaticX
if: matrix.staticx == 'yes'
run: |
case $RUNNER_ARCH in
X64)
@@ -486,14 +531,14 @@ jobs:
echo "ldlib=${ldlib}"
$PYTHON -m staticx -l "${ldlib}" "${gam}" "${gam}-staticx"
- name: Linux Run StaticX-ed
if: runner.os == 'Linux' && contains(runner.arch, '64') && matrix.goal != 'test'
- name: Run StaticX
if: matrix.staticx == 'yes'
run: |
"${gam}-staticx" version extended
mv -v "${gam}-staticx" "${gam}"
- name: Linux package staticx
if: runner.os == 'Linux' && contains(runner.arch, '64') && matrix.goal != 'test'
if: matrix.staticx == 'yes'
run: |
GAM_ARCHIVE="gam-${GAMVERSION}-linux-$(uname -m)-legacy.tar.xz"
tar -C dist/ --create --verbose --exclude-from "${GITHUB_WORKSPACE}/.github/actions/package_exclusions.txt" --file $GAM_ARCHIVE --xz gam
@@ -501,9 +546,6 @@ jobs:
- name: Windows package
if: runner.os == 'Windows' && matrix.goal != 'test'
run: |
cp -v LICENSE $gampath
cp -v GamCommands.txt $gampath
cp -v gam-setup.bat $gampath
cd dist/
GAM_ARCHIVE="../gam-${GAMVERSION}-windows-${GAM_ARCHIVE_ARCH}.zip"
/c/Program\ Files/7-Zip/7z.exe a -tzip $GAM_ARCHIVE gam "-xr@${GITHUB_WORKSPACE}/.github/actions/package_exclusions.txt" -bb3
@@ -540,29 +582,43 @@ jobs:
if [[ "${RUNNER_OS}" == "macOS" ]]; then
brew install gnupg
fi
source ../.github/actions/decrypt.sh ../.github/actions/creds.tar.gpg creds.tar
source ../.github/actions/decrypt.sh ../.github/actions/creds.tar.xz.gpg creds.tar.xz
export OAUTHFILE="oauth2.txt-gam-gha-${JID}"
echo "OAUTHFILE=${OAUTHFILE}" >> $GITHUB_ENV
export gam_user="gam-gha-${JID}@pdl.jaylee.us"
echo "gam_user=${gam_user}" >> $GITHUB_ENV
$gam checkconn
$gam oauth info
$gam info domain
$gam oauth refresh
$gam info user
#$gam info user $gam_user grouptree
export tstamp=$($PYTHON -c "import time; print(time.time_ns())")
export newbase=gha_test_$JID_$tstamp
export newuser=$newbase@pdl.jaylee.us
export newgroup=$newbase-group@pdl.jaylee.us
export newalias=$newbase-alias@pdl.jaylee.us
export newbuilding=$newbase-building
export newresource=$newbase-resource
export newbase="gha_test_${JID}_${tstamp}"
export newuser="${newbase}@pdl.jaylee.us"
export newgroup="${newbase}-group@pdl.jaylee.us"
export newalias="${newbase}-alias@pdl.jaylee.us"
export newbuilding="${newbase}-building"
export newresource="${newbase}-resource"
export newou="aaaGithub Actions/${newbase}"
# cleanup old runs
GAM_CSV_ROW_FILTER="name:regex:gha_test_${JID}_" $gam print vaultholds | $gam csv - gam delete vaulthold "id:~~holdId~~" matter "id:~~matterId~~"
GAM_CSV_ROW_FILTER="name:regex:gha_test_${JID}_" $gam print features | $gam csv - gam delete feature ~name
GAM_CSV_ROW_FILTER="name:regex:^gha_test_${JID}_" $gam user $gam_user print shareddrives asadmin | $gam csv - gam user $gam_user delete shareddrive ~id nukefromorbit
$gam print users query "gha.jid=$JID" | $gam csv - gam delete user ~primaryEmail
GAM_CSV_ROW_FILTER="name:regex:^gha_test_${JID}_" $gam print ous fromparent "aaaGithub Actions" | $gam csv - gam delete ou ~orgUnitId
GAM_CSV_ROW_FILTER="groupKey.id:regex:^gha_test_${JID}_" $gam print cigroups | $gam csv - gam delete cigroup ~groupKey.id
GAM_CSV_ROW_FILTER="resourceId:regex:^gha_test_${JID}_" $gam print resources | $gam csv - gam delete resource ~resourceId
GAM_CSV_ROW_FILTER="buildingId:regex:^gha_test_${JID}_" $gam print buildings | $gam csv - gam delete building ~buildingId
echo "Creating OrgUnit ${newou}"
$gam create ou "${newou}"
export GAM_THREADS=5
echo email > sample.csv;
for i in {1..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 languages en+,en-GB-
$gam create user $newuser firstname GHA lastname $JID displayname "Github Actions ${JID}" password random ou "${newou}" recoveryphone 12125121110 recoveryemail jay0lee@gmail.com gha.jid $JID languages en+,en-GB-
$gam user $newuser update photo https://dummyimage.com/400x600/000/fff
$gam user $newuser get photo
$gam user $newuser delete photo
@@ -575,9 +631,9 @@ jobs:
$gam info cigroup $newgroup
$gam update group $newgroup add owner $gam_user
$gam update group $newgroup add member $newuser
$gam create admin $newuser _GROUPS_EDITOR_ROLE CUSTOMER condition nonsecuritygroup
$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 create admin $newuser _GROUPS_EDITOR_ROLE CUSTOMER # condition nonsecuritygroup
$gam csv sample.csv gam create user ~~email~~ firstname "GHA Bulk" lastname ~~email~~ gha.jid $JID ou "${newou}"
$gam csv sample.csv gam update user ~~email~~ recoveryphone 12125121110 recoveryemail jay0lee@gmail.com password random displayname "GitHub Actions Bulk ${JID}"
$gam csv sample.csv gam update user ~~email~~ recoveryphone "" recoveryemail ""
$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"
@@ -607,8 +663,8 @@ jobs:
$gam users "$newbase-bulkuser-7 $newbase-bulkuser-8 $newbase-bulkuser-9" modify messages query in:anywhere maxtomodify 99999 addlabel IMPORTANT addlabel STARRED doit
$gam user $newuser delete label --ALL_LABELS--
GAM_CSV_ROW_FILTER="name:regex:gha-test-${JID}" $gam print features | $gam csv - gam delete feature ~name
$gam create feature name Whiteboard-$newbase
$gam create feature name VC-$newbase
$gam create feature name Whiteboard-$newbase
$gam create building "My Building - $newbase" id $newbuilding floors 1,2,3,4,5,6,7,8,9,10,11,12,14,15 description "No 13th floor here..."
$gam create resource $newresource "Resource Calendar $tstamp" capacity 25 features Whiteboard-$newbase,VC-$newbase building $newbuilding floor 15 type Room
$gam info resource $newresource
@@ -644,8 +700,9 @@ jobs:
$gam delete hold "GHA hold $newbase" matter $matterid
$gam update matter $matterid action close
$gam update matter $matterid action delete
#$gam delete user $newuser
#$gam undelete user $newuser
# shakes off vault hold on user so we can delete
$gam print users query "email:${newuser}" orgunitpath | $gam csv - gam update user ~primaryEmail ou ~orgUnitPath
$gam user $newuser show holds
$gam delete user $newuser
$gam print users query "gha.jid=$JID" | $gam csv - gam delete user ~primaryEmail
$gam print mobile
@@ -669,36 +726,68 @@ jobs:
driveid=$($gam user $gam_user add shareddrive "${newbase}" | awk '{print $NF}')
echo "Created shared drive ${driveid}"
$gam user $gam_user add drivefile localfile gam.py parentid "${driveid}"
$gam user $gam_user update shareddrive "${driveid}" ou "id:03ph8a2z1t2ph5z"
$gam user $gam_user update shareddrive "${driveid}" ou "${newou}"
$gam user $gam_user show shareddrives asadmin
$gam user $gam_user update shareddrive "${driveid}" ou "aaaGithub Actions" # so we can delete our OU...
$gam user $gam_user delete shareddrive "${driveid}" nukefromorbit
echo "printer model count:"
$gam print printermodels | wc -l
#ssoprofile=$($gam create inboundssoprofile name "El Goog ${newbase}" loginurl https://www.google.com logouturl https://www.google.com changepasswordurl https://www.google.com entityid ElGoog return_name_only)
#$gam create inboundssocredential profile "id:${ssoprofile}" generate_key
#$gam create inboundssoassignment profile "id:${ssoprofile}" orgunit "${newou}" mode SAML_SSO
$gam delete ou "${newou}"
#$gam delete inboundssoprofile "id:${ssoprofile}"
#$gam print printers
#$gam create printer displayname "${newbase}" uri ipp://localhost:631 driverless description "made by $(gam_user)" ou /
#export CUSTOMER_ID="C01wfv983"
#export GA_DOMAIN="pdl.jaylee.us"
#touch $gampath/enabledasa.txt
#echo "using delegated admin service account"
#$gam print users
# - name: Upload to Google Drive, build only.
# if: github.event_name == 'push' && matrix.goal != 'test'
# run: |
# ls gam-$GAMVERSION-*
# for gamfile in gam-$GAMVERSION-*; do
# echo "Uploading file ${gamfile} to Google Drive..."
# fileid=$($gam user $gam_user add drivefile localfile $gamfile drivefilename $GAMVERSION-${GITHUB_SHA:0:7}-$gamfile parentid 1N2zbO33qzUQFsGM49-m9AQC1ijzd_ru1 returnidonly)
# echo "file uploaded as ${fileid}, setting ACL..."
# $gam user $gam_user add drivefileacl $fileid anyone role reader withlink
# done
export CUSTOMER_ID="C01wfv983"
export GA_DOMAIN="pdl.jaylee.us"
touch $gampath/enabledasa.txt
echo "using delegated admin service account"
$gam print users
- name: Archive production artifacts
uses: actions/upload-artifact@v2
if: github.event_name == 'push' && matrix.goal != 'test'
uses: actions/upload-artifact@v3
if: (github.event_name == 'push' || github.event_name == 'schedule') && matrix.goal != 'test'
with:
name: gam-binaries
path: |
src/*.tar.xz
src/*.zip
src/*.msi
- name: Tar Cache archive
if: matrix.goal == 'build' && steps.cache-python-ssl.outputs.cache-hit != 'true'
working-directory: ${{ github.workspace }}
run: |
tar cJvvf bin.tar.xz bin/
publish:
if: github.event_name == 'push'
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v3
with:
persist-credentials: false
fetch-depth: 0
- name: Download artifacts
uses: actions/download-artifact@v3
- name: VirusTotal Scan
uses: crazy-max/ghaction-virustotal@v3
with:
vt_api_key: ${{ secrets.VT_API_KEY }}
files: |
gam-binaries/*
- uses: "marvinpinto/action-automatic-releases@latest"
name: Publish draft release
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
automatic_release_tag: latest
prerelease: false
draft: true
files: |
gam-binaries/*

36
.github/workflows/get-roots.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: Check for Google Root CA Updates
on:
push:
pull_request:
schedule:
- cron: '23 23 * * *'
defaults:
run:
shell: bash
working-directory: src
jobs:
check-apis:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
with:
persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal token
fetch-depth: 0 # otherwise, you will failed to push refs to dest repo
- name: Check for updates
run: curl -o ./roots.pem -vvvv https://pki.goog/roots.pem
- name: Commit file
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add roots.pem
git diff --quiet && git diff --staged --quiet || git commit -am '[ci skip] Updated roots.pem'
- name: Push changes
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -653,7 +653,8 @@ Specify a collection of ChromeOS devices by directly specifying them
(crosfile <FileName>)|
(croscsvfile <FileName>:<FieldName>)|
(crosquery <QueryCrOS>)|
(crosqueries <QueryCrOSList>)
(crosqueries <QueryCrOSList>)|
(cros_ou|cros_ou_and_children <OrgUnitPath>)
## Collections of Users
@@ -871,6 +872,7 @@ Specify a collection of Users by directly specifying them or by specifying items
<UserBasicAttribute>|
<UserMultiAttribute>
gam checkconnection
gam version [check|checkrc|simple|extended] [timeoffset] [location <HostName>]
gam help
@@ -909,6 +911,12 @@ gam oauth|oauth2 refresh
gam <UserTypeEntity> check serviceaccount [scope|scopes <APIScopeURLList>]
gam yubikey [resetpiv]
gam rotate sakey yubikey yubikey_pin yubikey_slot AUTHENTICATION yubikeypin <String> yubikeyserialnumber <String>
gam create [gcpserviceaccount|signjwtserviceaccount]
gam enable apis [auto|manual]
gam whatis <EmailItem>
<ResoldCustomerAttribute> ::=
@@ -1066,9 +1074,9 @@ gam delete alias|nickname [user|group|target] <UniqueID>|<EmailAddress>
gam info alias|nickname <EmailAddress>
gam print aliases|nicknames [todrive] [shownoneditable] [nogroups] [nousers] [(query <QueryUser>)|(queries <QueryUserList)]
gam calendar <CalendarItem> add <CalendarACLRole> ([user] <EmailAddress>)|(group <EmailAddress>)|(domain <DomainName>) [sendnotifications <Boolean>]
gam calendar <CalendarItem> add <CalendarACLRole> ([user] <EmailAddress>)|(group <EmailAddress>)|(domain <DomainName>)|domain|default [sendnotifications <Boolean>]
gam calendar <CalendarItem> update <CalendarACLRole> ([user] <EmailAddress>)|(group <EmailAddress>)|(domain <DomainName>)|domain|default [sendnotifications <Boolean>]
gam calendar <CalendarItem> del|delete ([user] <EmailAddress>)|(group <EmailAddress>)|(domain <DomainName>)|domainx|default
gam calendar <CalendarItem> del|delete ([user] <EmailAddress>)|(group <EmailAddress>)|(domain <DomainName>)|domain|default
gam calendar <CalendarItem> del|delete id <CalendarACLRuleID>
gam calendar <CalendarItem> showacl
gam calendar <CalendarItem> printacl [todrive]
@@ -1303,7 +1311,9 @@ gam update chatmessage name <String>
deprovision_retiring_device|
deprovision_upgrade_transfer|
disable|
reenable
reenable|
pre_provisioned_disable|
pre_provisioned_reenable
gam update cros <CrOSEntity> action <CrOSAction> [acknowledge_device_touch_requirement]
@@ -1321,7 +1331,7 @@ gam update cros <CrOSEntity> <CrOSAttribute>+
gam info cros <CrOSEntity> [nolists] [listlimit <Number>] [start <Date>] [end <Date>]
[basic|full|allfields] <CrOSFieldName>* [fields <CrOSFieldNameList>] [downloadfile latest|<Time>] [targetfolder <FilePath>]
gam print cros [todrive] [(query <QueryCrOS>)|(queries <QueryCrOSList>)] [limittoou <OrgUnitItem>]
gam print cros [todrive] [(query <QueryCrOS>)|(queries <QueryCrOSList>)] [limittoou|cros_ou|cros_ou_and_children <OrgUnitItem>]
[orderby <CrOSOrderByFieldName> [ascending|descending]] [nolists|<CrOSListFieldName>*] [listlimit <Number>] [start <Date>] [end <Date>]
[basic|full|allfields] <CrOSFieldName>* [fields <CrOSFieldNameList>] [sortheaders]
gam <CrOSTypeEntity> print
@@ -1329,7 +1339,7 @@ gam <CrOSTypeEntity> print
Summary of printing:
gam print cros
Prints a header row and deviceId for all CrOS devices.
gam <CrOSTypeEntity> print cros
gam <CrOSTypeEntity> print
Prints no header row and deviceId for specified CrOS devices.
gam print cros ... basic|full
Prints a header row and selected fields for specified CrOS devices.
@@ -1343,7 +1353,7 @@ One set of values for all <CrOSListFieldName> fields specified will be output on
The listlimit <Number> argument limits the number of repetitions to <Number>; if not specified or <Number> equals zero, there is no limit.
The start <Date> and end <Date> arguments constrain activeTimeRanges, cpuStatusReports, deviceFiles and systemRamFreeReports to fall within the specified <Dates>.
gam print crosactivity [todrive] [(query <QueryCrOS>)|(queries <QueryCrOSList>)] [limittoou <OrgUnitItem>]
gam print crosactivity [todrive] [(query <QueryCrOS>)|(queries <QueryCrOSList>)] [limittoou|cros_ou|cros_ou_and_children <OrgUnitItem>]
[recentusers] [timeranges] [both] [devicefiles] [all] [listlimit <Number>] [start <Date>] [end <Date>] [delimiter <Character>]
The basic column headers are: deviceId,annotatedAssetId,annotatedLocation,serialNumber,orgUnitPath.
@@ -1388,6 +1398,13 @@ gam print chromeappdevices [todrive]
[start <Date>] [end <Date>]
[orderby deviceid|machine]
gam print chromeaues [todrive]
[ou|org|orgunit <OrgUnitItem>]
[minauedate <Date>] [maxauedate <Date>]
gam print chromeneedsattn [todrive]
[ou|org|orgunit <OrgUnitItem>]
gam print chromeversions [todrive]
[ou|org|orgunit <OrgUnitItem>]
[start <Date>] [end <Date>] [recentfirst]
@@ -1544,6 +1561,44 @@ gam print group-members|groups-members [todrive]
[roles <GroupRoleList>] [membernames] [fields <MembersFieldNameList>]
[includederivedmembership]
<SSOProfileDisplayName> ::= <String>
<SSOProfileName> ::= id:inboundSamlSsoProfiles/<String>
<SSOProfileItem> ::= <SSOProfileDisplayName>|<SSOProfileName>
<SSOProfileItemList> ::= "<SSOProfileItem>(,<SSOProfileItem>)*"
gam create inboundssoprofile [name <SSOProfileDisplayName>]
[entityid <String>] [loginurl <URL>] [logouturl <URL>] [changepasswordurl <URL>]
[returnnameonly]
gam update inboundssoprofile <SSOProfileItem>
[entityid <String>] [loginurl <URL>] [logouturl <URL>] [changepasswordurl <URL>]
[returnnameonly]
gam delete inboundssoprofile <SSOProfileItem>
gam info inboundssoprofile <SSOProfileItem>
gam show inboundssoprofiles
gam print inboundssoprofiles [todrive]
<SSOCredentialsName> ::= [id:]inboundSamlSsoProfiles/<String>/idpCredentials/<String>
gam create inboundssocredential profile <SSOProfileItem>
(pemfile <FileName>)|(generatekey [keysize 1024|2048|4096]) [replaceolddest]
gam delete inboundssocredential <SSOCredentialsName>
gam show inboundssocredentials [profile|profiles <SSOProfileItemList>]
gam print inboundssocredentials [profile|profiles <SSOProfileItemList>] [todrive]
<SSOAssignmentSelector> ::=
groups/<String> |
group:<EmailAddress> |
orgunits/<String> |
orgunit:<OrgUnitPath>
gam create inboundssoassignment (group <GroupItem> rank <Number>)|(ou|org|orgunit <OrgUnitItem>)
(mode sso_off)|(mode saml_sso profile <SSOProfileItem>)(mode domain_wide_saml_if_enabled) [neverredirect]
gam update inboundssoassignment [(group <GroupItem> rank <Number>)|(ou|org|orgunit <OrgUnitItem>)]
[(mode sso_off)|(mode saml_sso profile <SSOProfileItem>)(mode domain_wide_saml_if_enabled)] [neverredirect]
gam info inboundssoassignment <SSOAssignmentSelector>
gam show inboundssoassignments
gam print inboundssoassignments [todrive]
gam send userinvitation <EmailAddress>
gam cancel userinvitation <EmailAddress>
gam check userinvitation|isinvitable <EmailAddress>
@@ -1632,6 +1687,9 @@ gam show guardian|guardians [invitedguardian <EmailAddress>] [student <StudentIt
gam print guardian|guardians [todrive] [invitedguardian <EmailAddress>] [student <StudentItem>] [invitations [states <GuardianStateList>]] [<UserTypeEntity>]
gam cancel guardianinvitation|guardianinvitations <GuardianInvitationID> <StudentItem>
gam download storagebucket <URL>
gam copy storagebucket sourcebucket <URL> targetbucket <URL> [sourceprefix <String>] [targetprefix <String>]
gam create vaultexport|export matter <MatterItem> [name <name>] corpus <drive|mail|groups|hangouts_chat>
(accounts <EmailAddressList>) | (orgunit|ou <OrgUnitPath>) | (teamdrives <TeamDriveList>) | (rooms <ChatRoomList>) | everyone
[scope <all_data|held_data|unprocessed_data>]
@@ -1644,6 +1702,7 @@ gam delete export <MatterItem> <ExportItem>
gam info export <MatterItem> <ExportItem>
gam print exports [todrive] [matters <MatterItemList>]
gam download export <MatterItem> <ExportItem> [noverify] [noextract] [targetfolder <FilePath>]
gam copy export <MatterItem> <ExportItem> targetbucket <URL> [targetprefix <String>]
gam create vaulthold|hold corpus drive|groups|mail matter <MatterItem> [name <String>] [query <QueryVaultCorpus>]
[(accounts|groups|users <EmailItemList>) | (orgunit|ou <OrgUnit>)]

View File

@@ -12,7 +12,7 @@
}
},
"basePath": "",
"baseUrl": "https://www.googleapis.com/admin/directory/v1.1beta1/customer/",
"baseUrl": "https://admin.googleapis.com/admin/directory/v1.1beta1/customer/",
"batchPath": "batch",
"canonicalName": "cbcm",
"discoveryVersion": "v1",

View File

@@ -113,10 +113,9 @@ case $gamos in
done
case $gamarch in
x86_64) gamfile="linux-x86_64-$useglibc.tar.xz";;
arm64|aarch64) gamfile="linux-aarch64-glibc2.28.tar.xz";;
arm|armv7l) gamfile="linux-armv7l-glibc2.28.tar.xz";;
arm64|aarch64) gamfile="linux-aarch64-$useglibc.tar.xz";;
*)
echo_red "ERROR: this installer currently only supports x86_64, arm and arm64 Linux. Looks like you're running on $gamarch. Exiting."
echo_red "ERROR: this installer currently only supports x86_64 and arm64 Linux. Looks like you're running on $gamarch. Exiting."
exit
esac
;;

View File

@@ -1,55 +1,110 @@
# -*- mode: python -*-
# -*- mode: python ; coding: utf-8 -*-
from os import getenv
from sys import platform
import sys
import importlib
from PyInstaller.utils.hooks import copy_metadata
# 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')]
extra_files = []
extra_files += copy_metadata('google-api-python-client')
extra_files += [('cbcm-v1.1beta1.json', '.')]
extra_files += [('contactdelegation-v1.json', '.')]
extra_files += [('admin-directory_v1.1beta1.json', '.')]
extra_files += [('roots.pem', '.')]
hidden_imports = [
'gam.auth.yubikey',
]
a = Analysis(['gam/__main__.py'],
hiddenimports=hidden_imports,
hookspath=None,
excludes=['FixTk', 'tcl', 'tk', '_tkinter', 'tkinter', 'Tkinter'],
datas=extra_files,
runtime_hooks=None)
a = Analysis(
['gam/__main__.py'],
pathex=[],
binaries=[],
datas=extra_files,
hiddenimports=hidden_imports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=None,
noarchive=False,
)
for d in a.datas:
if 'pyconfig' in d[0]:
a.datas.remove(d)
break
pyz = PYZ(a.pure)
if sys.platform == "darwin":
target_arch="universal2"
pyz = PYZ(a.pure,
a.zipped_data,
cipher=None)
# requires Python 3.10+ but no one should be compiling
# GAM with older versions anyway
match platform:
case "darwin":
target_arch = "universal2"
strip = True
case "win32":
target_arch = None
strip = False
case _:
target_arch = None
strip = True
name = 'gam'
debug = False
bootloader_ignore_signals = False
upx = False
console = True
disable_windowed_traceback = False
argv_emulation = False
codesign_identity = None
entitlements_file = None
if getenv('PYINSTALLER_BUILD_ONEFILE') == 'yes':
# Build one file
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name=name,
debug=debug,
bootloader_ignore_signals=bootloader_ignore_signals,
strip=strip,
upx=upx,
console=console,
disable_windowed_traceback=disable_windowed_traceback,
argv_emulation=argv_emulation,
target_arch=target_arch,
codesign_identity=codesign_identity,
entitlements_file=entitlements_file,
)
else:
target_arch=None
# Build one folder
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name=name,
debug=debug,
bootloader_ignore_signals=bootloader_ignore_signals,
strip=strip,
upx=upx,
console=console,
disable_windowed_traceback=disable_windowed_traceback,
argv_emulation=argv_emulation,
target_arch=target_arch,
codesign_identity=codesign_identity,
entitlements_file=entitlements_file,
)
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=strip,
upx=upx,
upx_exclude=[],
name=name,
)
# use strip on all non-Windows platforms
strip = not sys.platform == 'win32'
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
name='gam',
debug=False,
strip=strip,
upx=False,
target_arch=target_arch,
console=True)

View File

@@ -55,6 +55,9 @@
<Component Id="gamcommands_txt" Guid="58ff9c45-a7c9-4e22-8845-a9a92610c1f3">
<File Name="gamcommands.txt" KeyPath="yes" />
</Component>
<Component Id="roots_pem" Guid="18ff9c45-a3c9-4e22-8445-a8a92610c1f3">
<File Name="roots.pem" KeyPath="yes" />
</Component>
</ComponentGroup>
</Fragment>

View File

@@ -51,6 +51,7 @@ from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
import gam.auth.oauth
from gam.auth import signjwt
from gam import auth
from gam import controlflow
from gam import display
@@ -65,6 +66,7 @@ from gam.gapi import chromemanagement as gapi_chromemanagement
from gam.gapi import chromepolicy as gapi_chromepolicy
from gam.gapi.cloudidentity import devices as gapi_cloudidentity_devices
from gam.gapi.cloudidentity import groups as gapi_cloudidentity_groups
from gam.gapi.cloudidentity import inboundsso as gapi_cloudidentity_inboundsso
from gam.gapi.cloudidentity import orgunits as gapi_cloudidentity_orgunits
from gam.gapi.cloudidentity import userinvitations as gapi_cloudidentity_userinvitations
from gam.gapi import contactdelegation as gapi_contactdelegation
@@ -553,9 +555,12 @@ def SetGlobalVariables():
'debug.gam',
filePresentValue=4,
fileAbsentValue=0)
_getOldSignalFile(GC_LOW_MEMORY, 'lowmemory.txt')
_getOldSignalFile(GC_NO_BROWSER, 'nobrowser.txt')
_getOldSignalFile(GC_NO_TDEMAIL, 'notdemail.txt')
_getOldSignalFile(GC_OAUTH_BROWSER, 'oauthbrowser.txt')
# oauthbrowser.txt is deprecated as we now always
# use the localhost flow.
#_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)
_getOldSignalFile(GC_NO_CACHE,
@@ -631,27 +636,28 @@ TIME_OFFSET_UNITS = [('day', 86400), ('hour', 3600), ('minute', 60),
def getLocalGoogleTimeOffset(testLocation='admin.googleapis.com'):
localUTC = datetime.datetime.now(datetime.timezone.utc)
try:
# we disable SSL verify so we can still get time even if clock
# is way off. This could be spoofed / MitM but we'll fail for those
# situations everywhere else but here.
badhttp = transport.create_http()
badhttp.disable_ssl_certificate_validation = True
googleUTC = dateutil.parser.parse(
badhttp.request('https://' + testLocation, 'HEAD')[0]['date'])
except (httplib2.ServerNotFoundError, RuntimeError, ValueError) as e:
controlflow.system_error_exit(4, str(e))
offset = remainder = int(abs((localUTC - googleUTC).total_seconds()))
timeoff = []
for tou in TIME_OFFSET_UNITS:
uval, remainder = divmod(remainder, tou[1])
if uval:
timeoff.append(f'{uval} {tou[0]}{"s" if uval != 1 else ""}')
if not timeoff:
timeoff.append('less than 1 second')
nicetime = ', '.join(timeoff)
return (offset, nicetime)
# Try with http first, if time is close (<MAX_LOCAL_GOOGLE_TIME_OFFSET seconds),
# retry with https
badhttp = transport.create_http()
for prot in ['http', 'https']:
localUTC = datetime.datetime.now(datetime.timezone.utc)
try:
googleUTC = dateutil.parser.parse(
badhttp.request(f'{prot}://' + testLocation, 'HEAD')[0]['date'])
except (httplib2.ServerNotFoundError, RuntimeError, ValueError) as e:
controlflow.system_error_exit(4, str(e))
offset = remainder = int(abs((localUTC - googleUTC).total_seconds()))
if offset < MAX_LOCAL_GOOGLE_TIME_OFFSET and prot == 'http':
continue
timeoff = []
for tou in TIME_OFFSET_UNITS:
uval, remainder = divmod(remainder, tou[1])
if uval:
timeoff.append(f'{uval} {tou[0]}{"s" if uval != 1 else ""}')
if not timeoff:
timeoff.append('less than 1 second')
nicetime = ', '.join(timeoff)
return (offset, nicetime)
def doGAMCheckForUpdates(forceCheck=False):
@@ -719,8 +725,11 @@ def doGAMCheckForUpdates(forceCheck=False):
continue_on_error=True,
display_errors=forceCheck)
return
except (httplib2.HttpLib2Error, httplib2.ServerNotFoundError, RuntimeError,
socket.timeout):
except (httplib2.HttpLib2Error,
httplib2.ServerNotFoundError,
RuntimeError,
ConnectionError,
TimeoutError):
return
@@ -746,6 +755,72 @@ def getOSPlatform():
return f'{myos} {pltfrm}'
def checkConnection():
hosts = [
'api.github.com',
'raw.githubusercontent.com',
'gam-shortn.appspot.com',
'accounts.google.com',
'oauth2.googleapis.com',
'www.googleapis.com',
]
api_hosts = []
for api in API_VER_MAPPING:
api = API_NAME_MAPPING.get(api, api)
api = f'{api}.googleapis.com'
if api not in api_hosts and api not in hosts:
api_hosts.append(api)
api_hosts.sort()
hosts.extend(api_hosts)
httpc = transport.create_http(timeout=10)
httpc.follow_redirects = False
headers = {'user-agent': GAM_INFO}
okay = createGreenText('OK')
not_okay = createRedText('ERROR')
gen_firewall = 'You may have security software or a firewall on your machine or network that is preventing GAM from making a secure connection to this host. Check your network configuration or try running GAM on a hotspot or home network to see if the problem exists only on your organization\'s network.'
host_count = len(hosts)
try_count = 0
success_count = 0
for host in hosts:
try_count += 1
ip = socket.getaddrinfo(host, None)[0][-1][0] # works with ipv6
check_line = f'Checking {host} ({ip}) ({try_count}/{host_count})...'
sys.stdout.write(f'{check_line:<100}')
sys.stdout.flush()
try:
httpc.request(f'https://{host}/', 'HEAD', headers=headers)
success_count += 1
print(okay)
except BrokenPipeError:
print(f'{not_okay}\n Broken pipe. {gen_firewall}')
except ConnectionAbortedError:
print(f'{not_okay}\n Connection aborted. {gen_firewall}')
except ConnectionRefusedError:
print(f'{not_okay}\n Connection refused. {gen_firewall}')
except ConnectionResetError:
print(f'{not_okay}\n Connection reset by peer. {gen_firewall}')
except httplib2.error.ServerNotFoundError:
print(f'{not_okay}\n Failed to find server. Your DNS is probably misconfigured.')
except ssl.SSLError as e:
if e.reason == 'SSLV3_ALERT_HANDSHAKE_FAILURE':
print(f'{not_okay}\n GAM expects to connect with TLS 1.3 or newer and that failed. If your firewall / proxy server is not compatible with TLS 1.3 then you can tell GAM to allow TLS 1.2 by setting the GAM_TLS_MIN_VERSION=TLSv1_2 environment variable.')
elif e.reason == 'CERTIFICATE_VERIFY_FAILED':
print(f'{not_okay}\n Certificate verification failed. If you are behind a firewall / proxy server that does TLS / SSL inspection you may need to point GAM at your certificate authority file by setting the GAM_CA_FILE=/path/to/your/certauth.pem environment variable.')
elif e.strerror.startswith('TLS/SSL connection has been closed'):
print(f'{not_okay}\n TLS connection was closed. {gen_firewall}')
else:
print(f'{not_okay}\n {e.reason}: {str(e)}')
except TimeoutError:
print(f'{not_okay}\n Timed out trying to connect to host. {gen_firewall}')
except Exception as e:
# include the exception class so we know what to catch in the future
print(f'{not_okay}\n {type(e).__name__} - {str(e)}')
print()
if success_count == host_count:
print(createGreenText('All hosts passed!'))
else:
controlflow.system_error_exit(3, createYellowText('Some hosts failed to connect! Please follow the recommendations for those hosts to correct any issues and try again.'))
def doGAMVersion(checkForArgs=True):
force_check = extended = simple = timeOffset = False
testLocation = 'admin.googleapis.com'
@@ -851,14 +926,12 @@ def _getSvcAcctData():
controlflow.system_error_exit(6, None)
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA] = json.loads(json_string)
jwt_apis = ['chat',
'cloudresourcemanager',
'accesscontextmanager'] # APIs which can handle OAuthless JWT tokens
def getSvcAcctCredentials(scopes, act_as, api=None):
def getSvcAcctCredentials(scopes, act_as, api=None, force_oauth=False):
try:
_getSvcAcctData()
sign_method = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA].get('key_type', 'default')
if act_as or api not in jwt_apis:
if act_as or force_oauth:
# DwD means we need to go about things differently...
if sign_method == 'default':
credentials = google.oauth2.service_account.Credentials.from_service_account_info(
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
@@ -866,6 +939,10 @@ def getSvcAcctCredentials(scopes, act_as, api=None):
yksigner = yubikey.YubiKey(GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
credentials = google.oauth2.service_account.Credentials._from_signer_and_info(yksigner,
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
elif sign_method == 'signjwt':
sjsigner = signjwt.SignJwt(GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
credentials = signjwt.Credentials._from_signer_and_info(sjsigner.sign,
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
credentials = credentials.with_scopes(scopes)
if act_as:
credentials = credentials.with_subject(act_as)
@@ -879,6 +956,11 @@ def getSvcAcctCredentials(scopes, act_as, api=None):
credentials = JWTCredentials._from_signer_and_info(yksigner,
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA],
audience=audience)
elif sign_method == 'signjwt':
sjsigner = signjwt.SignJwt(GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
credentials = signjwt.JWTCredentials._from_signer_and_info(sjsigner,
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA],
audience=audience)
credentials.project_id = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['project_id']
GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID] = GM_Globals[
GM_OAUTH2SERVICE_JSON_DATA]['client_id']
@@ -1004,9 +1086,10 @@ def getService(api, httpObj):
controlflow.invalid_json_exit(disc_file)
def buildGAPIObject(api):
def buildGAPIObject(api, credentials=None):
GM_Globals[GM_CURRENT_API_USER] = None
credentials = getValidOauth2TxtCredentials(api=getAPIVersion(api)[0])
if not credentials:
credentials = getValidOauth2TxtCredentials(api=getAPIVersion(api)[0])
credentials.user_agent = GAM_INFO
httpObj = transport.AuthorizedHttp(
credentials, transport.create_http(cache=GM_Globals[GM_CACHE_DIR]))
@@ -1221,7 +1304,9 @@ def doCheckServiceAccount(users):
# We are explicitly not doing DwD here, just confirming service account can auth
auth_error = ''
try:
credentials = getSvcAcctCredentials([USERINFO_EMAIL_SCOPE], None)
credentials = getSvcAcctCredentials([USERINFO_EMAIL_SCOPE],
None,
force_oauth=True)
request = transport.create_request()
credentials.refresh(request)
sa_token_info = gapi.call(oa2,
@@ -1240,34 +1325,38 @@ def doCheckServiceAccount(users):
3,
'Invalid private key in oauth2service.json. Please delete the file and then\nrecreate with "gam create project" or "gam use project"'
)
print(
'Checking key age. Google recommends rotating keys on a routine basis...'
)
try:
iam = buildGAPIServiceObject('iam', None)
project = GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID]
key_id = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['private_key_id']
name = f'projects/-/serviceAccounts/{project}/keys/{key_id}'
key = gapi.call(iam.projects().serviceAccounts().keys(),
'get',
name=name,
throw_reasons=[gapi_errors.ErrorReason.FOUR_O_THREE])
key_created = dateutil.parser.parse(
key['validAfterTime'], ignoretz=True)
key_age = datetime.datetime.now() - key_created
key_days = key_age.days
if key_days > 30:
print(
'Your key is old. Recommend running "gam rotate sakey" to get a new key'
)
key_type = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA].get('key_type', 'default')
if key_type == 'default':
print(
'Checking key age. Google recommends rotating keys on a routine basis...'
)
try:
iam = buildGAPIServiceObject('iam', None)
project = GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID]
key_id = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['private_key_id']
name = f'projects/-/serviceAccounts/{project}/keys/{key_id}'
key = gapi.call(iam.projects().serviceAccounts().keys(),
'get',
name=name,
throw_reasons=[gapi_errors.ErrorReason.FOUR_O_THREE])
key_created = dateutil.parser.parse(
key['validAfterTime'], ignoretz=True)
key_age = datetime.datetime.now() - key_created
key_days = key_age.days
if key_days > 30:
print(
'Your key is old. Recommend running "gam rotate sakey" to get a new key'
)
key_age_result = test_warn
else:
key_age_result = test_pass
except googleapiclient.errors.HttpError:
key_age_result = test_warn
else:
key_age_result = test_pass
except googleapiclient.errors.HttpError:
key_age_result = test_warn
key_days = 'UNKNOWN'
print('Unable to check key age, please run "gam update project"')
printPassFail(f'Key is {key_days} days old', key_age_result)
key_days = 'UNKNOWN'
print('Unable to check key age, please run "gam update project"')
printPassFail(f'Key is {key_days} days old', key_age_result)
else:
printPassFail(f'Skipping age check. {key_type} rotation not necessary.', test_pass)
if not check_scopes:
for _, scopes in list(API_SCOPE_MAPPING.items()):
for scope in scopes:
@@ -2247,6 +2336,7 @@ def doGetCourseInfo():
COURSE_ARGUMENT_TO_PROPERTY_MAP = {
'alternatelink': 'alternateLink',
'calendarid': 'calendarId',
'coursegroupemail': 'courseGroupEmail',
'coursematerialsets': 'courseMaterialSets',
'coursestate': 'courseState',
@@ -2254,7 +2344,9 @@ COURSE_ARGUMENT_TO_PROPERTY_MAP = {
'description': 'description',
'descriptionheading': 'descriptionHeading',
'enrollmentcode': 'enrollmentCode',
'gradebooksettings': 'gradebookSettings',
'guardiansenabled': 'guardiansEnabled',
'heading': 'descriptionHeading',
'id': 'id',
'name': 'name',
'ownerid': 'ownerId',
@@ -4396,15 +4488,7 @@ def getImap(users):
soft_errors=True,
userId='me')
if result:
enabled = result['enabled']
if enabled:
print(
f'User: {user}, IMAP Enabled: {enabled}, autoExpunge: {result["autoExpunge"]}, expungeBehavior: {result["expungeBehavior"]}, maxFolderSize: {result["maxFolderSize"]}{currentCount(i, count)}'
)
else:
print(
f'User: {user}, IMAP Enabled: {enabled}{currentCount(i, count)}'
)
display.print_json(result)
def doPop(users):
@@ -6501,6 +6585,15 @@ def getUserAttributes(i, cd, updateCmd):
body.setdefault('name', {})
body['name']['familyName'] = sys.argv[i + 1]
i += 2
elif myarg in ['displayname']:
body.setdefault('name', {})
body['name']['displayName'] = sys.argv[i + 1]
# sigh, the API is wonky. If we set just displayName
# we get an error. But if we also "set" fullName which is
# really just a concat of first/last name and can't be set
# then it works. Go figure.
body['name']['fullName'] = sys.argv[i+1]
i += 2
elif myarg in ['username', 'email', 'primaryemail'] and updateCmd:
body['primaryEmail'] = normalizeEmailAddressOrUID(sys.argv[i + 1],
noUid=True)
@@ -7057,7 +7150,7 @@ def getCRMService(login_hint):
scopes,
'online',
login_hint=login_hint,
use_console_flow=not GC_Values[GC_OAUTH_BROWSER])
open_browser=not GC_Values[GC_NO_BROWSER])
httpc = transport.AuthorizedHttp(creds, transport.create_http())
return getService('cloudresourcemanager', httpc), httpc
@@ -7076,6 +7169,43 @@ def getGAMProjectFile(filepath):
return c.decode(UTF8)
def enable_apis():
a_or_m = None
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower()
if myarg in ['auto', 'manual']:
a_or_m = myarg[0]
i += 1
else:
controlflow.invalid_argument_exit(sys.argv[i],
'gam enable apis')
GAMProjectAPIs = getGAMProjectFile('project-apis.txt').splitlines()
try:
_, projectId = google.auth.default()
except google.auth.exceptions.DefaultCredentialsError as e:
projectId = input('Please enter your project ID: ')
while a_or_m not in ['a', 'm']:
a_or_m = input('Do you want to enable projects [a]utomatically or [m]anually? (a/m): ').strip().lower()
if a_or_m in ['a', 'm']:
break
print('Please enter A or M....')
if a_or_m == 'a':
login_hint = _getValidateLoginHint()
_, httpObj = getCRMService(login_hint)
enableGAMProjectAPIs(GAMProjectAPIs,
httpObj,
projectId=projectId,
checkEnabled=True)
else:
chunk_size = 20
print('Using an account with project access, please use ALL of these URLs to enable 20 APIs at a time:\n\n')
for chunk in range(0, len(GAMProjectAPIs), chunk_size):
apiid = ",".join(GAMProjectAPIs[chunk:chunk+chunk_size])
url = f'https://console.cloud.google.com/apis/enableflow?apiid={apiid}&project={projectId}'
print(f' {url}\n\n')
def enableGAMProjectAPIs(GAMProjectAPIs,
httpObj,
projectId,
@@ -7213,7 +7343,7 @@ def _createClientSecretsOauth2service(httpObj, projectId, login_hint):
'code':
'ThisIsAnInvalidCodeOnlyBeingUsedToTestIfClientAndSecretAreValid',
'redirect_uri':
'urn:ietf:wg:oauth:2.0:oob',
'http://127.0.0.1:8080/',
'grant_type':
'authorization_code'
}
@@ -7360,15 +7490,26 @@ def _getCurrentProjectID():
def _getProjects(crm, pfilter):
try:
return gapi.get_all_pages(
projects = gapi.get_all_pages(
crm.projects(),
'search',
'projects',
throw_reasons=[gapi_errors.ErrorReason.BAD_REQUEST],
query=pfilter)
if projects:
return projects
if pfilter.startswith('id:'):
pfilter = pfilter[3:]
return [gapi.call(
crm.projects(),
'get',
name=f'projects/{pfilter}',
throw_reasons=[gapi_errors.ErrorReason.BAD_REQUEST,
gapi_errors.ErrorReason.FOUR_O_THREE])]
except gapi_errors.GapiBadRequestError as e:
controlflow.system_error_exit(2, f'Project: {pfilter}, {str(e)}')
except googleapiclient.errors.HttpError:
return []
PROJECTID_PATTERN = re.compile(r'^[a-z][a-z0-9-]{4,28}[a-z0-9]$')
PROJECTID_FORMAT_REQUIRED = '[a-z][a-z0-9-]{4,28}[a-z0-9]'
@@ -7555,7 +7696,7 @@ def doCreateProject():
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
for i in range(1, 5):
for i in range(1, 10):
print('Checking project status...')
status = gapi.call(crm.operations(), 'get', name=operation_name)
if 'error' in status:
@@ -7614,7 +7755,7 @@ and accept the Terms of Service (ToS). As soon as you've accepted the ToS popup,
controlflow.system_error_exit(1, status)
if status.get('done', False):
break
sleep_time = i**2
sleep_time = min(2**i, 60)
print(f'Project still being created. Sleeping {sleep_time} seconds')
time.sleep(sleep_time)
if create_again:
@@ -7650,7 +7791,7 @@ def doUpdateProjects():
_grantRotateRights(iam, sa_email, sa_email)
def _generatePrivateKeyAndPublicCert(client_id, key_size):
def _generatePrivateKeyAndPublicCert(client_id, key_size, b64enc_pub=True):
print(' Generating new private key...')
private_key = rsa.generate_private_key(public_exponent=65537,
key_size=key_size,
@@ -7694,6 +7835,8 @@ def _generatePrivateKeyAndPublicCert(client_id, key_size):
backend=default_backend())
public_cert_pem = certificate.public_bytes(
serialization.Encoding.PEM).decode()
if not b64enc_pub:
return private_pem, public_cert_pem
publicKeyData = base64.b64encode(public_cert_pem.encode())
if isinstance(publicKeyData, bytes):
publicKeyData = publicKeyData.decode()
@@ -7729,8 +7872,7 @@ def doShowServiceAccountKeys():
else:
controlflow.invalid_argument_exit(myarg, 'gam show sakeys')
name = f'projects/-/serviceAccounts/{GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID]}'
currentPrivateKeyId = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA][
'private_key_id']
currentPrivateKeyId = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA].get('private_key_id')
keys = gapi.get_items(iam.projects().serviceAccounts().keys(),
'list',
'keys',
@@ -7749,6 +7891,32 @@ def doShowServiceAccountKeys():
display.print_json(keys)
def create_signjwt_serviceaccount():
i = 3
if i < len(sys.argv):
controlflow.invalid_argument_exit(sys.argv[i], f'gam create {sys.argv[2]}')
_checkForExistingProjectFiles()
sa_info = {
'type': 'service_account',
'key_type': 'signjwt',
'token_uri': 'https://oauth2.googleapis.com/token'
}
try:
creds, sa_info['project_id'] = google.auth.default()
except google.auth.exceptions.DefaultCredentialsError as e:
controlflow.system_error_exit(2, e)
request = transport.create_request()
creds.refresh(request)
sa_info['client_email'] = creds.service_account_email
oa2 = buildGAPIObjectNoAuthentication('oauth2')
token_info = gapi.call(oa2, 'tokeninfo', access_token=creds.token)
sa_info['client_id'] = token_info['issued_to']
sa_output = json.dumps(sa_info, indent=4, sort_keys=True)
fileutils.write_file(GC_Values[GC_OAUTH2SERVICE_JSON],
sa_output,
continue_on_error=False)
def doCreateOrRotateServiceAccountKeys(iam=None,
project_id=None,
client_email=None,
@@ -7824,6 +7992,7 @@ def doCreateOrRotateServiceAccountKeys(iam=None,
yk = yubikey.YubiKey(new_data)
if 'yubikey_serial_number' not in new_data:
new_data['yubikey_serial_number'] = yk.get_serial_number()
yk = yubikey.YubiKey(new_data)
if 'yubikey_slot' not in new_data:
new_data['yubikey_slot'] = 'AUTHENTICATION'
publicKeyData = yk.get_certificate()
@@ -7854,14 +8023,13 @@ def doCreateOrRotateServiceAccountKeys(iam=None,
body={'publicKeyData': publicKeyData})
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)
if hasattr(err, 'error_details'):
if err.error_details == 'The given public key already exists.':
print('WARNING: that key already exists.')
result = {'name': oldPrivateKeyId}
break
controlflow.system_error_exit(
4, err.error_details)
else:
controlflow.system_error_exit(
4, err)
@@ -8080,20 +8248,14 @@ def doCreateSharedDrive(users):
print(f'Created Shared Drive {body["name"]} with id {result["id"]}')
TEAMDRIVE_RESTRICTIONS_MAP = {
'adminmanagedrestrictions': 'adminManagedRestrictions',
'copyrequireswriterpermission': 'copyRequiresWriterPermission',
'domainusersonly': 'domainUsersOnly',
'teammembersonly': 'teamMembersOnly',
}
def doUpdateSharedDrive(users):
i, driveId = getSharedDriveId(5)
body = {}
useDomainAdminAccess = False
change_hide = None
orgUnit = None
_, d = buildDrive3GAPIObject(_get_admin_email())
restrictions_map = {r.lower(): r for r in d._rootDesc['schemas']['Drive']['properties']['restrictions']['properties'].keys()}
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'name':
@@ -8119,19 +8281,15 @@ def doUpdateSharedDrive(users):
elif myarg == 'asadmin':
useDomainAdminAccess = True
i += 1
# elif myarg in ['ou', 'org', 'orgunit']:
# body['orgUnitId'] = gapi_directory_orgunits.getOrgUnitId(sys.argv[i + 1])
# i += 2
elif myarg in ['hidden']:
if getBoolean(sys.argv[i+1], myarg):
change_hide = 'hide'
else:
change_hide = 'unhide'
i += 2
elif myarg in TEAMDRIVE_RESTRICTIONS_MAP:
elif myarg in restrictions_map:
body.setdefault('restrictions', {})
body['restrictions'][
TEAMDRIVE_RESTRICTIONS_MAP[myarg]] = getBoolean(
body['restrictions'][restrictions_map[myarg]] = getBoolean(
sys.argv[i + 1], myarg)
i += 2
else:
@@ -8169,7 +8327,7 @@ def printShowSharedDrives(users, csvFormat):
todrive = False
useDomainAdminAccess = False
q = None
get_orgunits = True
get_orgunits = True
i = 5
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
@@ -8191,7 +8349,7 @@ def printShowSharedDrives(users, csvFormat):
tds = []
titles = []
if get_orgunits and useDomainAdminAccess:
ou_map = gapi_directory_orgunits.orgid_to_org_map()
ou_map = gapi_directory_orgunits.orgid_to_org_map()
for user in users:
sys.stderr.write(f'Getting Shared Drives for {user}\n')
user, drive = buildDrive3GAPIObject(user)
@@ -8860,10 +9018,16 @@ def doGetUserInfo(user_email=None):
customFieldMask=customFieldMask,
viewType=viewType)
print(f'User: {user["primaryEmail"]}')
if 'name' in user and 'givenName' in user['name']:
print(f'First Name: {user["name"]["givenName"]}')
if 'name' in user and 'familyName' in user['name']:
print(f'Last Name: {user["name"]["familyName"]}')
if 'name' in user:
names = {
'givenName': 'First Name',
'familyName': 'Last Name',
'fullName': 'Full Name',
'displayName': 'Display Name',
}
for field, description in names.items():
if field in user['name']:
print(f'{description}: {user["name"][field]}')
if 'languages' in user:
print(f"Languages: {_formatLanguagesList(user['languages'], ',')}")
if 'isAdmin' in user:
@@ -9408,7 +9572,7 @@ def doUndeleteUser():
i = 4
while i < len(sys.argv):
myarg = sys.argv[i].lower()
if myarg in ['ou', 'org']:
if myarg in ['ou', 'org', 'orgunit']:
orgUnit = gapi_directory_orgunits.makeOrgUnitPathAbsolute(
sys.argv[i + 1])
i += 2
@@ -9571,6 +9735,7 @@ USER_ARGUMENT_TO_PROPERTY_MAP = {
'changepasswordatnextlogin': ['changePasswordAtNextLogin',],
'creationtime': ['creationTime',],
'deletiontime': ['deletionTime',],
'displayname': ['displayName',],
'email': ['emails',],
'emails': ['emails',],
'externalid': ['externalIds',],
@@ -9612,6 +9777,7 @@ USER_ARGUMENT_TO_PROPERTY_MAP = {
'location': ['locations',],
'locations': ['locations',],
'name': [
'name.displayName',
'name.givenName',
'name.familyName',
'name.fullName',
@@ -10109,7 +10275,7 @@ def getUsersToModify(entity_type=None,
'org_ns',
'ou_susp',
'org_susp',
]:
]:
if entity_type in ['ou_ns', 'org_ns']:
checkSuspended = False
elif entity_type in ['ou_susp', 'org_susp']:
@@ -10294,8 +10460,13 @@ def getUsersToModify(entity_type=None,
elif entity_type == 'cros':
users = entity.replace(',', ' ').split()
entity = 'cros'
elif entity_type in ['crosquery', 'crosqueries', 'cros_sn']:
if entity_type == 'cros_sn':
elif entity_type in ['crosquery', 'crosqueries', 'cros_sn', 'cros_ou', 'cros_ou_and_children']:
orgUnitPath = includeChildOrgunits = None
if entity_type in {'cros_ou', 'cros_ou_and_children'}:
orgUnitPath = entity
includeChildOrgunits = entity_type == 'cros_ou_and_children'
queries = [None]
elif entity_type == 'cros_sn':
queries = [f'id:{sn}' for sn in shlexSplitList(entity)]
elif entity_type == 'crosqueries':
queries = shlexSplitList(entity)
@@ -10312,9 +10483,11 @@ def getUsersToModify(entity_type=None,
'list',
'chromeosdevices',
page_message=page_message,
query=query,
customerId=GC_Values[GC_CUSTOMER_ID],
fields='nextPageToken,chromeosdevices(deviceId)',
query=query)
orgUnitPath=orgUnitPath,
includeChildOrgunits=includeChildOrgunits,
fields='nextPageToken,chromeosdevices(deviceId)')
for member in members:
deviceId = member['deviceId']
if deviceId not in usersSet:
@@ -10465,7 +10638,7 @@ def doRequestOAuth(login_hint=None, scopes=None):
access_type='offline',
login_hint=login_hint,
credentials_file=GC_Values[GC_OAUTH2_TXT],
use_console_flow=not GC_Values[GC_OAUTH_BROWSER])
open_browser=not GC_Values[GC_NO_BROWSER])
creds.write()
except gam.auth.oauth.InvalidClientSecretsFileError:
controlflow.system_error_exit(14, missing_client_secrets_message)
@@ -10507,6 +10680,11 @@ OAUTH2_SCOPES = [
'subscopes': ['readonly'],
'scopes': 'https://www.googleapis.com/auth/cloud-identity.groups'
},
{
'name': 'Cloud Identity - Inbound SSO Settings',
'subscopes': ['readonly'],
'scopes': 'https://www.googleapis.com/auth/cloud-identity.inboundsso',
},
{
'name': 'Cloud Identity - OrgUnits',
'subscopes': ['readonly'],
@@ -10516,7 +10694,6 @@ OAUTH2_SCOPES = [
'name': 'Cloud Identity - User Invitations',
'subscopes': ['readonly'],
'scopes': 'https://www.googleapis.com/auth/cloud-identity.userinvitations',
'offByDefault': True,
},
{
'name': 'Contact Delegation',
@@ -10639,11 +10816,17 @@ OAUTH2_SCOPES = [
'subscopes': ['readonly'],
'scopes': 'https://www.googleapis.com/auth/ediscovery'
},
# off by default to avoid reauth issues with GCP APIs
# and since many admins never use Vault API.
{
'name': 'Cloud Storage (Vault Export - read only)',
'subscopes': [],
'scopes': 'https://www.googleapis.com/auth/devstorage.read_only'
},
'name': 'Cloud Storage - Vault/Takeout Download/Copy',
'subscopes': ['readonly'],
'offByDefault': True,
'restricted_scopes': {
'readonly': 'https://www.googleapis.com/auth/devstorage.read_only'
},
'scopes': 'https://www.googleapis.com/auth/devstorage.read_write'
},
{
'name': 'User Profile (Email address - read only)',
'subscopes': [],
@@ -10688,6 +10871,7 @@ class ScopeMenuOption():
is_required=False,
is_selected=False,
supported_restrictions=None,
restricted_scopes=None,
restriction=None):
"""A data structure for storing and toggling feature/API scope attributes.
@@ -10717,6 +10901,7 @@ class ScopeMenuOption():
self._restriction = None
self.scopes = oauth_scopes
self.restricted_scopes = restricted_scopes
self.description = description
self.is_required = is_required
# Required scopes must be selected
@@ -10807,7 +10992,10 @@ class ScopeMenuOption():
effective_scopes = []
for scope in self.scopes:
if self.is_restricted:
scope = f'{scope}.{self._restriction}'
if self.restricted_scopes.get(self._restriction):
scope = self.restricted_scopes.get(self._restriction)
else:
scope = f'{scope}.{self._restriction}'
effective_scopes.append(scope)
return effective_scopes
@@ -10819,7 +11007,10 @@ class ScopeMenuOption():
name: Some description of the API/feature.
subscopes: A list of compatible scope restrictions such as 'action' or
'readonly'. Each scope in the scopes list must support this
restriction text appended to the end of its normal scope text.
restriction text appended to the end of its normal scope text or
be defined in the restricted_scopes attribute.
restricted_scopes: A dict of scopes to be used for restrictions. If not
defined then {scope}.{subscope} is used.
scopes: A list of scopes that are required for the API/feature.
offByDefault: A bool indicating whether this feature/scope should be off
by default (when no prior selection has been made). Default is False
@@ -10848,8 +11039,8 @@ class ScopeMenuOption():
description=scope_definition.get('name'),
is_selected=not scope_definition.get('offByDefault'),
supported_restrictions=scope_definition.get('subscopes', []),
is_required=scope_definition.get('required', False))
is_required=scope_definition.get('required', False),
restricted_scopes=scope_definition.get('restricted_scopes', {}))
class ScopeSelectionMenu():
"""A text menu which prompts the user to select the scopes to authorize."""
@@ -11164,7 +11355,7 @@ def run_batch(items):
)
pool.close()
pool.join()
pool = mp_pool(num_worker_threads, init_gam_worker, maxtasksperchild=200, initargs=(1,))
pool = mp_pool(num_worker_threads, init_gam_worker, maxtasksperchild=200, initargs=(l,))
sys.stderr.write(
'commit-batch - running processes finished, proceeding\n')
continue
@@ -11337,6 +11528,8 @@ def ProcessGAMCommand(args):
i, encoding = getCharSet(i + 1)
f = fileutils.open_file(filename, encoding=encoding)
csvFile = csv.DictReader(f)
if not csvFile.fieldnames:
controlflow.system_error_exit(0, f'CSV file {filename} is empty')
if (i == len(sys.argv)) or (sys.argv[i].lower() !=
'gam') or (i + 1 == len(sys.argv)):
controlflow.system_error_exit(
@@ -11373,6 +11566,9 @@ def ProcessGAMCommand(args):
elif command == 'version':
doGAMVersion()
sys.exit(0)
elif command in ['checkconnection', 'checkconn']:
checkConnection()
sys.exit(0)
elif command == 'create':
argument = sys.argv[2].lower()
if argument == 'user':
@@ -11383,7 +11579,13 @@ def ProcessGAMCommand(args):
gapi_cloudidentity_groups.create()
elif argument in ['nickname', 'alias']:
doCreateAlias()
elif argument in ['org', 'ou']:
elif argument in ['inboundssoprofile', 'inboundssoprofiles']:
gapi_cloudidentity_inboundsso.create_profile()
elif argument in ['inboundssocredential', 'inboundssocredentials']:
gapi_cloudidentity_inboundsso.create_credentials()
elif argument in ['inboundssoassignment', 'inboundssoassignments']:
gapi_cloudidentity_inboundsso.create_assignment()
elif argument in ['org', 'orgunit', 'ou']:
gapi_directory_orgunits.create()
elif argument == 'resource':
gapi_directory_resource.createResourceCalendar()
@@ -11435,6 +11637,8 @@ def ProcessGAMCommand(args):
gapi_chat.create_message()
elif argument in ['caalevel']:
gapi_caa.create_access_level()
elif argument in ['gcpserviceaccount', 'signjwtserviceaccount']:
create_signjwt_serviceaccount()
else:
controlflow.invalid_argument_exit(argument, 'gam create')
sys.exit(0)
@@ -11455,10 +11659,14 @@ def ProcessGAMCommand(args):
gapi_cloudidentity_groups.update()
elif argument in ['nickname', 'alias']:
doUpdateAlias()
elif argument in ['ou', 'org']:
elif argument in ['ou', 'org', 'orgunit']:
gapi_directory_orgunits.update()
elif argument == 'resource':
gapi_directory_resource.updateResourceCalendar()
elif argument in ['inboundssoprofile', 'inboundssoprofiles']:
gapi_cloudidentity_inboundsso.update_profile()
elif argument in ['inboundssoassignment', 'inboundssoasignments']:
gapi_cloudidentity_inboundsso.update_assignment()
elif argument == 'cros':
gapi_directory_cros.doUpdateCros()
elif argument == 'mobile':
@@ -11520,7 +11728,11 @@ def ProcessGAMCommand(args):
doGetAliasInfo()
elif argument == 'instance':
gapi_directory_customer.doGetCustomerInfo()
elif argument in ['org', 'ou']:
elif argument in ['inboundssoprofile', 'inboundssoprofiles']:
gapi_cloudidentity_inboundsso.info_profile()
elif argument in ['inboundssoassignment', 'inboundssoassignments']:
gapi_cloudidentity_inboundsso.info_assignment()
elif argument in ['ou', 'org', 'orgunit']:
gapi_directory_orgunits.info()
elif argument == 'resource':
gapi_directory_resource.getResourceCalendarInfo()
@@ -11595,8 +11807,12 @@ def ProcessGAMCommand(args):
gapi_cloudidentity_groups.delete()
elif argument in ['nickname', 'alias']:
doDeleteAlias()
elif argument == 'org':
elif argument in ['ou', 'org', 'orgunit']:
gapi_directory_orgunits.delete()
elif argument in ['inboundssoprofile', 'inboundssoprofiles']:
gapi_cloudidentity_inboundsso.delete_profile()
elif argument in ['inboundssocredential', 'inboundssocredentials']:
gapi_cloudidentity_inboundsso.delete_credentials()
elif argument == 'resource':
gapi_directory_resource.deleteResourceCalendar()
elif argument == 'mobile':
@@ -11688,6 +11904,12 @@ def ProcessGAMCommand(args):
gapi_directory_groups.print_members()
elif argument in ['cigroupmembers', 'cigroupsmembers']:
gapi_cloudidentity_groups.print_members()
elif argument in ['inboundssoprofile', 'inboundssoprofiles']:
gapi_cloudidentity_inboundsso.print_show_profiles()
elif argument in ['inboundssocredential', 'inboundssocredentials']:
gapi_cloudidentity_inboundsso.print_show_credentials()
elif argument in ['inboundssoassignment', 'inboundssoassignments']:
gapi_cloudidentity_inboundsso.print_show_assignments()
elif argument in ['orgs', 'ous']:
gapi_directory_orgunits.print_()
elif argument == 'privileges':
@@ -11756,6 +11978,10 @@ def ProcessGAMCommand(args):
gapi_chromemanagement.printApps()
elif argument in ['chromeappdevices']:
gapi_chromemanagement.printAppDevices()
elif argument in ['chromeaues']:
gapi_chromemanagement.printAUEs()
elif argument in ['chromeneedsattn']:
gapi_chromemanagement.printNeedsAttn()
elif argument in ['chromeversions']:
gapi_chromemanagement.printVersions()
elif argument in ['chromehistory']:
@@ -11788,6 +12014,12 @@ def ProcessGAMCommand(args):
gapi_licensing.show()
elif argument in ['project', 'projects']:
doPrintShowProjects(False)
elif argument in ['inboundssoprofile', 'inboundssoprofiles']:
gapi_cloudidentity_inboundsso.print_show_profiles('show')
elif argument in ['inboundssocredential', 'inboundssocredentials']:
gapi_cloudidentity_inboundsso.print_show_credentials('show')
elif argument in ['inboundssoassignment', 'inboundssoassignments']:
gapi_cloudidentity_inboundsso.print_show_assignments('show')
elif argument in ['sakey', 'sakeys']:
doShowServiceAccountKeys()
elif argument in ['browsertoken', 'browsertokens']:
@@ -11880,11 +12112,21 @@ def ProcessGAMCommand(args):
argument = sys.argv[2].lower()
if argument in ['export', 'vaultexport']:
gapi_vault.downloadExport()
elif argument in ['storagebucket']:
elif argument in ['storagebucket', 'bucket']:
gapi_storage.download_bucket()
else:
controlflow.invalid_argument_exit(argument, 'gam download')
sys.exit(0)
elif command == 'copy':
argument = sys.argv[2].lower().replace('_', '')
if argument in ['export', 'vaultexport']:
gapi_vault.copyExport()
elif argument in ['storagebucket', 'bucket']:
gapi_storage.copy_bucket()
else:
controlflow.invalid_argument_exit(argument, 'gam copy')
sys.exit(0)
elif command == 'rotate':
argument = sys.argv[2].lower()
if argument in ['sakey', 'sakeys']:
@@ -11928,8 +12170,14 @@ def ProcessGAMCommand(args):
action = sys.argv[2].lower().replace('_', '')
if action == 'resetpiv':
yk = yubikey.YubiKey()
yk.serial_number = yk.get_serial_number()
yk.reset_piv()
sys.exit(0)
elif command == 'enable':
enable_what = sys.argv[2].lower().replace('_', '')
if enable_what in ['api', 'apis']:
enable_apis()
sys.exit(0)
users = getUsersToModify()
command = sys.argv[3].lower()
if command == 'print' and len(sys.argv) == 4:

View File

@@ -9,6 +9,7 @@ import gam
from gam import utils
from gam.auth import oauth
from gam.auth import signjwt
from gam.var import _FN_OAUTH2_TXT
from gam.var import _FN_OAUTH2SERVICE_JSON
from gam.var import GC_OAUTH2_TXT
@@ -40,7 +41,7 @@ def get_admin_credentials(api=None):
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:
if GC_Values[GC_ENABLE_DASA] and 'key_type' in creds_data:
audience = f'https://{api}.googleapis.com/'
key_type = creds_data.get('key_type', 'default')
if key_type == 'default':
@@ -51,6 +52,11 @@ def get_admin_credentials(api=None):
return JWTCredentials._from_signer_and_info(yksigner,
creds_data,
audience=audience)
elif key_type == 'signjwt':
sjsigner = signjwt.SignJwt(creds_data)
return signjwt.JWTCredentials._from_signer_and_info(sjsigner,
creds_data,
audience=audience)
elif not GC_Values[GC_ENABLE_DASA] and 'token' in creds_data:
return oauth.Credentials.from_credentials_file(credential_file)
else:

View File

@@ -24,7 +24,10 @@ from gam import controlflow
from gam import display
from gam import fileutils
from gam import transport
from gam.var import GM_Globals, GM_WINDOWS
from gam.var import (GC_CA_FILE,
GC_Values,
GM_Globals,
GM_WINDOWS)
from gam import utils
@@ -269,6 +272,7 @@ class Credentials(google.oauth2.credentials.Credentials):
access_type='offline',
login_hint=None,
filename=None,
open_browser=True,
use_console_flow=False):
"""Runs an OAuth Flow from client secrets to generate credentials.
@@ -288,8 +292,11 @@ class Credentials(google.oauth2.credentials.Credentials):
login_hint: String, The email address that will be displayed on the Google
login page as a hint for the user to login to the correct account.
filename: String, the path to a file to use to save the credentials.
use_console_flow: Boolean, True if the authentication flow should be run
strictly from a console; False to launch a browser for authentication.
use_console_flow: OBSOLETE: Boolean, True if the authentication flow
should be run strictly from a console; False to launch a browser
for authentication.
open_browser: Boolean: whether or not GAM should try to open the browser
automatically.
Returns:
Credentials
@@ -309,12 +316,11 @@ class Credentials(google.oauth2.credentials.Credentials):
flow = _ShortURLFlow.from_client_config(client_config,
scopes,
autogenerate_code_verifier=True)
flow_kwargs = {'access_type': access_type}
flow_kwargs = {'access_type': access_type,
'open_browser': open_browser}
if login_hint:
flow_kwargs['login_hint'] = login_hint
flow.run_dual(use_console_flow,
**flow_kwargs)
flow.run_dual(**flow_kwargs)
return cls.from_google_oauth2_credentials(flow.credentials,
filename=filename)
@@ -325,6 +331,7 @@ class Credentials(google.oauth2.credentials.Credentials):
access_type='offline',
login_hint=None,
credentials_file=None,
open_browser=True,
use_console_flow=False):
"""Runs an OAuth Flow from secrets stored on disk to generate credentials.
@@ -345,8 +352,11 @@ class Credentials(google.oauth2.credentials.Credentials):
login page as a hint for the user to login to the correct account.
credentials_file: String, the path to a file to use to save the
credentials.
use_console_flow: Boolean, True if the authentication flow should be run
strictly from a console; False to launch a browser for authentication.
use_console_flow: OBSOLETE: Boolean, True if the authentication flow
should be run strictly from a console; False to launch a browser for
authentication.
open_browser: Boolean, whether or not GAM should try to open the browser
directly.
Raises:
InvalidClientSecretsFileError: If the client secrets file cannot be
@@ -375,14 +385,13 @@ class Credentials(google.oauth2.credentials.Credentials):
raise InvalidClientSecretsFileFormatError(
f'Could not extract Client ID or Client Secret from file {client_secrets_file}'
)
return cls.from_client_secrets(client_id,
client_secret,
scopes,
access_type=access_type,
login_hint=login_hint,
filename=credentials_file,
use_console_flow=use_console_flow)
open_browser=open_browser)
def _fetch_id_token_data(self):
"""Fetches verification details from Google for the OAuth2.0 token.
@@ -479,7 +488,11 @@ 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().refresh(request)
try:
super().refresh(request)
except google.auth.exceptions.RefreshError as e:
controlflow.system_error_exit(9, str(e))
def write(self):
"""Writes credentials to disk."""
@@ -592,7 +605,6 @@ class _ShortURLFlow(google_auth_oauthlib.flow.InstalledAppFlow):
def run_dual(self,
use_console_flow,
authorization_prompt_message='',
console_prompt_message='',
web_success_message='',
@@ -602,7 +614,7 @@ class _ShortURLFlow(google_auth_oauthlib.flow.InstalledAppFlow):
mgr = multiprocessing.Manager()
d = mgr.dict()
d['trailing_slash'] = redirect_uri_trailing_slash
d['open_browser'] = use_console_flow
d['open_browser'] = open_browser
http_client = multiprocessing.Process(target=_wait_for_http_client,
args=(d,))
user_input = multiprocessing.Process(target=_wait_for_user_input,
@@ -633,7 +645,10 @@ class _ShortURLFlow(google_auth_oauthlib.flow.InstalledAppFlow):
parsed_params = parse_qs(parsed_url.query)
code = parsed_params.get('code', [None])[0]
try:
self.fetch_token(code=code)
fetch_args = {'code': code}
if GC_Values.get(GC_CA_FILE):
fetch_args['verify'] = GC_Values.get(GC_CA_FILE)
self.fetch_token(**fetch_args)
break
except Exception as e:
if not userInput:

89
src/gam/auth/signjwt.py Normal file
View File

@@ -0,0 +1,89 @@
''' Use Google Application Default Credentials '''
import datetime
import json
import google.auth
from google.auth._helpers import datetime_to_secs, scopes_to_string, utcnow
import google.oauth2.service_account
import gam
from gam import controlflow
from gam import gapi
from gam import transport
from gam.var import GM_Globals, GM_CACHE_DIR
_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
_GOOGLE_OAUTH2_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token"
class JWTCredentials(google.auth.jwt.Credentials):
''' Class used for DASA '''
def _make_jwt(self):
now = utcnow()
lifetime = datetime.timedelta(seconds=self._token_lifetime)
expiry = now + lifetime
payload = {
"iss": self._issuer,
"sub": self._subject,
"iat": datetime_to_secs(now),
"exp": datetime_to_secs(expiry),
}
if self._audience:
payload["aud"] = self._audience
payload.update(self._additional_claims)
jwt = self._signer.sign(payload)
return jwt, expiry
class Credentials(google.oauth2.service_account.Credentials):
''' Class used for DwD '''
def _make_authorization_grant_assertion(self):
now = utcnow()
lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS)
expiry = now + lifetime
payload = {
"iat": datetime_to_secs(now),
"exp": datetime_to_secs(expiry),
"iss": self._service_account_email,
"aud": _GOOGLE_OAUTH2_TOKEN_ENDPOINT,
"scope": scopes_to_string(self._scopes or ()),
}
payload.update(self._additional_claims)
# The subject can be a user email for domain-wide delegation.
if self._subject:
payload.setdefault("sub", self._subject)
token = self._signer(payload)
return token
class SignJwt(google.auth.crypt.Signer):
''' Signer class for SignJWT '''
def __init__(self, service_account_info):
self.service_account_email = service_account_info['client_email']
self.name = f'projects/-/serviceAccounts/{self.service_account_email}'
self._key_id = None
@property # type: ignore
def key_id(self):
return self._key_id
def sign(self, message):
''' Call IAM Credentials SignJWT API to get our signed JWT '''
try:
credentials, _ = google.auth.default()
except google.auth.exceptions.DefaultCredentialsError as e:
controlflow.system_error_exit(2, e)
httpObj = transport.AuthorizedHttp(
credentials,
transport.create_http(cache=GM_Globals[GM_CACHE_DIR]))
iamc = gam.getService('iamcredentials', httpObj)
response = gapi.call(iamc.projects().serviceAccounts(),
'signJwt',
name=self.name,
body={'payload': json.dumps(message)})
signed_jwt = response.get('signedJwt')
return signed_jwt

View File

@@ -8,7 +8,7 @@ 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 ykman.device import list_all_devices
from ykman.piv import generate_self_signed_certificate, \
generate_chuid
from yubikit.piv import DEFAULT_MANAGEMENT_KEY, \
@@ -20,11 +20,14 @@ from yubikit.piv import DEFAULT_MANAGEMENT_KEY, \
OBJECT_ID, \
SLOT, \
TOUCH_POLICY
from yubikit.core.smartcard import ApduError
from yubikit.core.smartcard import ApduError, \
SmartCardConnection
from gam import controlflow
class YubiKey():
def __init__(self, service_account_info=None):
self.key_type = None
self.slot = None
@@ -46,12 +49,16 @@ class YubiKey():
self.pin = service_account_info.get('yubikey_pin')
self.key_id = service_account_info.get('private_key_id')
def _connect(self):
try:
conn, _, _ = connect_to_device(self.serial_number)
devices = list_all_devices()
for (device, info) in devices:
if info.serial == self.serial_number:
return device.open_connection(SmartCardConnection)
except CardConnectionException as err:
controlflow.system_error_exit(9, f'YubiKey - {err}')
return conn
def get_certificate(self):
try:
@@ -79,11 +86,22 @@ class YubiKey():
def get_serial_number(self):
try:
_, _, info = connect_to_device(self.serial_number)
return info.serial
devices = list_all_devices()
if self.serial_number:
for (device, info) in devices:
if info.serial == self.serial_number:
return info.serial
msg = f'Could not find YubiKey with serial {self.serial_number}'
controlflow.system_error_exit(3, msg)
if len(devices) > 1:
serials = ', '.join([str(info.serial) for (_, info) in devices])
msg = f'Multiple YubiKeys connected. Specify yubikey_serial_number and one of {serials}'
controlflow.system_error_exit(4, msg)
return devices[0][1].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())
@@ -95,7 +113,9 @@ class YubiKey():
piv = PivSession(conn)
piv.reset()
rnd = SystemRandom()
pin_puk_chars = string.ascii_letters + string.digits + string.punctuation
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)
@@ -155,3 +175,4 @@ class YubiKey():
if 'mplock' in globals():
mplock.release()
return signed

View File

@@ -34,7 +34,7 @@ def missing_argument_exit(argument, command):
"""Indicate that the argument is missing for the command.
Args:
argument: the missingagrument
argument: the missing argument
command: the base GAM command
"""
system_error_exit(2, f'missing argument {argument} for "{command}"')

View File

@@ -1,6 +1,8 @@
"""Methods related to execution of GAPI requests."""
import os.path
import sys
from tempfile import TemporaryDirectory
import googleapiclient.errors
import google.auth.exceptions
@@ -10,7 +12,8 @@ from gam import controlflow
from gam import display
from gam.gapi import errors
from gam import transport
from gam.var import (GM_Globals, GM_CURRENT_API_SCOPES, GM_CURRENT_API_USER,
from gam.var import (GC_Values, GM_Globals,
GM_CURRENT_API_SCOPES, GM_CURRENT_API_USER,
GM_EXTRA_ARGS_DICT, GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID,
MAX_RESULTS_API_EXCEPTIONS, MESSAGE_API_ACCESS_CONFIG,
MESSAGE_API_ACCESS_DENIED, MESSAGE_SERVICE_NOT_APPLICABLE)
@@ -238,8 +241,13 @@ def process_page(page, items, all_items, total_items, page_message, message_attr
page_items = page.get(items, [])
num_page_items = len(page_items)
total_items += num_page_items
if all_items is not None:
if type(all_items) is list:
all_items.extend(page_items)
elif all_items is not None:
i = len(all_items)
for item in page_items:
all_items[str(i)] = item
i += 1
else:
page_token = None
num_page_items = 0
@@ -273,6 +281,7 @@ def finalize_page_message(page_message):
sys.stderr.write('\r\n')
sys.stderr.flush()
def get_all_pages(service,
function,
items='items',
@@ -341,13 +350,14 @@ def get_all_pages(service,
page_token, total_items = process_page(page, items, all_items, total_items, page_message, message_attribute)
if not page_token:
finalize_page_message(page_message)
if type(all_items) is not list:
all_items = all_items.values()
return all_items
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
# have been brought into this file
def handle_oauth_token_error(e, soft_errors):

View File

@@ -219,7 +219,7 @@ def printShowCrosTelemetry(mode):
i = 3
if mode == 'info':
if i >= len(sys.argv):
controlflow.system_error_exit(3, f'<SerialNumber> required for "gam info crostelemetry"')
controlflow.system_error_exit(3, '<SerialNumber> required for "gam info crostelemetry"')
filter_ = f'serialNumber={sys.argv[i]}'
i += 1
mode = 'show'
@@ -286,7 +286,8 @@ def printShowCrosTelemetry(mode):
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 'temperatureCelsius' in tempInfo:
cpuStatusReport[f"cpuTemperatureInfo.{tempInfo['label'].strip()}"] = tempInfo['temperatureCelsius']
if showOrgUnitPath:
orgUnitId = device.get('orgUnitId')
if orgUnitId not in orgUnitIdPathMap:
@@ -306,6 +307,97 @@ def printShowCrosTelemetry(mode):
display.write_csv_file(csvRows, titles, 'Telemetry Devices', todrive)
CHROME_AUES_TITLES = [
'model', 'count', 'aueMonth', 'aueYear', 'expired'
]
def printAUEs():
cm = build()
customer = _get_customerid()
todrive = False
titles = CHROME_AUES_TITLES
csvRows = []
orgunit = None
minAueDate = None
maxAueDate = None
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'todrive':
todrive = True
i += 1
elif myarg in ['ou', 'org', 'orgunit']:
orgunit = _get_orgunit(sys.argv[i+1])
i += 2
elif myarg == 'minauedate':
minAueDate = _getFilterDate(sys.argv[i + 1]).strftime(YYYYMMDD_FORMAT)
i += 2
elif myarg == 'maxauedate':
maxAueDate = _getFilterDate(sys.argv[i + 1]).strftime(YYYYMMDD_FORMAT)
i += 2
else:
msg = f'{myarg} is not a valid argument to "gam print chromeaues"'
controlflow.system_error_exit(3, msg)
if orgunit:
orgUnitPath = gapi_directory_orgunits.orgunit_from_orgunitid(orgunit, None)
query = f'orgUnitPath={orgUnitPath}'
titles.append('orgUnitPath')
else:
orgUnitPath = '/'
query = None
gam.printGettingAllItems('Chrome Auto Update Expirations', query)
aues = gapi.call(cm.customers().reports(),
'countChromeDevicesReachingAutoExpirationDate',
customer=customer, orgUnitId=orgunit,
minAueDate=minAueDate, maxAueDate=maxAueDate).get('deviceAueCountReports', [])
for aue in sorted(aues, key=lambda k: k.get('model', 'Unknown')):
if orgunit:
aue['orgUnitPath'] = orgUnitPath
csvRows.append(aue)
display.write_csv_file(csvRows, titles, 'Chrome AUEs', todrive)
CHROME_NEEDSATTN_TITLES = [
'noRecentPolicySyncCount', 'noRecentUserActivityCount', 'pendingUpdate',
'osVersionNotCompliantCount', 'unsupportedPolicyCount'
]
def printNeedsAttn():
cm = build()
customer = _get_customerid()
todrive = False
titles = CHROME_NEEDSATTN_TITLES[:]
csvRows = []
orgunit = None
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'todrive':
todrive = True
i += 1
elif myarg in ['ou', 'org', 'orgunit']:
orgunit = _get_orgunit(sys.argv[i+1])
i += 2
else:
msg = f'{myarg} is not a valid argument to "gam print chromeneedsattn"'
controlflow.system_error_exit(3, msg)
if orgunit:
orgUnitPath = gapi_directory_orgunits.orgunit_from_orgunitid(orgunit, None)
query = f'orgUnitPath={orgUnitPath}'
titles.append('orgUnitPath')
else:
orgUnitPath = '/'
query = None
gam.printGettingAllItems('Chrome Devices Needing Attention', query)
result = gapi.call(cm.customers().reports(),
'countChromeDevicesThatNeedAttention',
customer=customer, orgUnitId=orgunit, readMask=','.join(CHROME_NEEDSATTN_TITLES))
for field in CHROME_NEEDSATTN_TITLES:
result.setdefault(field, 0)
if orgunit:
result['orgUnitPath'] = orgUnitPath
csvRows.append(result)
display.write_csv_file(csvRows, titles, 'Chrome Devices Needing Attention', todrive)
CHROME_VERSIONS_TITLES = [
'version', 'count', 'channel', 'deviceOsVersion', 'system'
]
@@ -319,6 +411,7 @@ def printVersions():
startDate = None
endDate = None
pfilter = None
query = None
reverse = False
i = 3
while i < len(sys.argv):
@@ -349,12 +442,18 @@ def printVersions():
else:
pfilter = ''
pfilter += f'last_active_date>={startDate}'
query = pfilter
if orgunit:
orgUnitPath = gapi_directory_orgunits.orgunit_from_orgunitid(orgunit, None)
if query:
query += ' AND '
else:
query = ''
query += f'orgUnitPath={orgUnitPath}'
titles.append('orgUnitPath')
else:
orgUnitPath = '/'
gam.printGettingAllItems('Chrome Versions', pfilter)
gam.printGettingAllItems('Chrome Versions', query)
page_message = gapi.got_total_items_msg('Chrome Versions', '...\n')
versions = gapi.get_all_pages(cm.customers().reports(),
'countChromeVersions',

View File

@@ -167,7 +167,7 @@ def build_schemas(svc=None, sfilter=None):
for an_enum in schema['definition']['enumType']:
if an_enum['name'] == type_name:
setting_dict['enums'] = [enum['name'] for enum in an_enum['value']]
setting_dict['enum_prefix'] = utils.commonprefix(setting_dict['enums'])
setting_dict['enum_prefix'] = utils.commonprefix(setting_dict['enums'], True)
prefix_len = len(setting_dict['enum_prefix'])
setting_dict['enums'] = [enum[prefix_len:] for enum \
in setting_dict['enums'] \

View File

@@ -80,7 +80,7 @@ def _parse_action(action):
def info():
ci = gapi_cloudidentity.build_dwd()
customer = _get_device_customerid()
name = _get_device_name()
_, name = _get_deviceuser_name()
device = gapi.call(ci.devices(), 'get', name=name, customer=customer)
device_users = gapi.get_all_pages(ci.devices().deviceUsers(), 'list',
'deviceUsers', parent=name, customer=customer)

View File

@@ -217,6 +217,7 @@ def print_():
gapi_directory_customer.setTrueCustomerId()
parent = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
usemember = None
query = None
memberDelimiter = '\n'
todrive = False
titles = []
@@ -235,6 +236,9 @@ def print_():
elif myarg == 'delimiter':
memberDelimiter = sys.argv[i + 1]
i += 2
elif myarg == 'query':
query = sys.argv[i + 1]
i += 2
elif myarg == 'sortheaders':
sortHeaders = True
i += 1
@@ -314,14 +318,20 @@ def print_():
if entity['relationType'] == 'DIRECT':
entityList.append(gapi.call(ci.groups(), 'get', name=entity['group']))
else:
if query:
method = 'search'
kwargs = {'query': query}
else:
method = 'list'
kwargs = {'parent': parent}
entityList = gapi.get_all_pages(ci.groups(),
'list',
method,
'groups',
page_message=page_message,
message_attribute=['groupKey', 'id'],
parent=parent,
view='FULL',
pageSize=500)
pageSize=500,
**kwargs)
i = 0
count = len(entityList)
for groupEntity in entityList:
@@ -935,6 +945,12 @@ def group_email_to_id(ci, group, i=0, count=0):
return None
def group_id_to_email(ci, group_id):
return gapi.call(ci.groups(),
'get',
fields='groupKey/id',
name=group_id).get('groupKey', {}).get('id')
def membership_email_to_id(ci, parent, membership, i=0, count=0):
membership = gam.normalizeEmailAddressOrUID(membership)
try:

View File

@@ -0,0 +1,538 @@
"""Methods related to Cloud Identity Inbound (Google as SP) SAML SSO"""
from datetime import datetime
import re
import sys
import dateutil.parser
import googleapiclient
import gam
from gam.var import GC_CUSTOMER_ID, GC_Values, MY_CUSTOMER
from gam import controlflow
from gam import display
from gam import fileutils
from gam import gapi
from gam import utils
from gam.gapi import errors as gapi_errors
from gam.gapi import cloudidentity as gapi_cloudidentity
from gam.gapi import directory as gapi_directory
from gam.gapi.cloudidentity import groups as gapi_cloudidentity_groups
from gam.gapi.directory import orgunits as gapi_directory_orgunits
'''returns customer in the format inboundsso requires'''
def get_sso_customer():
customer = GC_Values[GC_CUSTOMER_ID]
return f'customers/{customer}'
'''returns org unit in the format inboundsso requires'''
def get_orgunit_id(orgunit):
ou_id = gapi_directory_orgunits.getOrgUnitId(orgunit)[1]
if ou_id.startswith('id:'):
ou_id = ou_id[3:]
return f'orgUnits/{ou_id}'
'''build Cloud Identity API'''
def build():
return gapi_cloudidentity.build('cloudidentity_beta')
'''parse cmd for profile create/update'''
def parse_profile(body, i):
name_only = False
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'name':
body['displayName'] = sys.argv[i+1]
i += 2
elif myarg == 'entityid':
body.setdefault('idpConfig', {})['entityId'] = sys.argv[i+1]
i += 2
elif myarg == 'returnnameonly':
name_only = True
i += 1
elif myarg == 'loginurl':
body.setdefault('idpConfig', {})['singleSignOnServiceUri'] = sys.argv[i+1]
i += 2
elif myarg == 'logouturl':
body.setdefault('idpConfig', {})['logoutRedirectUri'] = sys.argv[i+1]
i += 2
elif myarg == 'changepasswordurl':
body.setdefault('idpConfig', {})['changePasswordUri'] = sys.argv[i+1]
i += 2
else:
controlflow.invalid_argument_exit(myarg, 'gam create/update inboundssoprofile')
return (name_only, body)
'''convert profile nice names to unique ID'''
def profile_displayname_to_name(displayName, ci=None):
if displayName.lower().startswith('id:') or displayName.lower().startswith('uid:'):
displayName = displayName.split(':', 1)[1]
if not displayName.startswith('inboundSamlSsoProfiles/'):
displayName = f'inboundSamlSsoProfiles/{displayName}'
return displayName
if not ci:
ci = build()
customer = get_sso_customer()
_filter = f'customer=="{customer}"'
profiles = gapi.get_all_pages(ci.inboundSamlSsoProfiles(),
'list',
'inboundSamlSsoProfiles',
filter=_filter)
matches = []
for profile in profiles:
if displayName.lower() == profile.get('displayName', '').lower():
matches.append(profile)
if len(matches) == 1:
return matches[0]['name']
if len(matches) == 0:
controlflow.system_error_exit(3, f'No Inbound SSO profile matches the name {displayName}')
else:
err_text = f'Multiple profiles match {displayName}:\n\n'
for m in matches:
err_text += f' {m["name"]} {m["displayName"]}\n'
controlflow.system_error_exit(3, err_text)
'''get an assignment based on target'''
def assignment_by_target(target, ci=None):
if not ci:
ci = build()
group_pattern = r'^groups/[^/]+$'
ou_pattern = r'^orgUnits/[^/]+$'
if re.match(group_pattern, target):
target_type = 'targetGroup'
elif re.match(ou_pattern, target):
target_type = 'targetOrgUnit'
elif target.lower().startswith('group:'):
target_type = 'targetGroup'
group_email = target[6:]
target = gapi_cloudidentity_groups.group_email_to_id(
ci,
group_email)
elif target.lower().startswith('orgunit:'):
target_type = 'targetOrgUnit'
ou_name = target[8:]
target = get_orgunit_id(ou_name)
else:
controlflow.system_error_exit(3, 'assignments should be prefixed with ' +
'group:, groups/, orgunit: or orgunits/')
customer = get_sso_customer()
_filter = f'customer=="{customer}"'
assignments = gapi.get_all_pages(ci.inboundSsoAssignments(),
'list',
'inboundSsoAssignments',
filter=_filter)
for assignment in assignments:
if target_type in assignment and assignment[target_type] == target:
return assignment
controlflow.system_error_exit(3, f'No SSO profile assigned to {target_type} {target}')
'''gam create inboundssoprofile'''
def create_profile():
ci = build()
body = {
'customer': get_sso_customer(),
'displayName': 'SSO Profile'
}
name_only, body = parse_profile(body, 3)
result = gapi.call(ci.inboundSamlSsoProfiles(),
'create',
body=body)
if result.get('done'):
if name_only:
print(result['response']['name'])
else:
print(f'Created profile {result["response"]["name"]}')
display.print_json(result['response'])
else:
controlflow.system_error_exit(3, 'Create did not finish {result}')
'''gam print inboundssoprofiles'''
def print_show_profiles(action='print'):
customer = get_sso_customer()
_filter = f'customer=="{customer}"'
ci = build()
todrive = False
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'todrive':
todrive = True
i += 1
else:
controlflow.invalid_argument_exit(myarg, f'gam {action} inboundssoprofiles')
profiles = gapi.get_all_pages(ci.inboundSamlSsoProfiles(),
'list',
'inboundSamlSsoProfiles',
filter=_filter)
if action == 'show':
for profile in profiles:
display.print_json(profile)
print()
elif action == 'print':
csv_rows = []
titles = []
for profile in profiles:
row = utils.flatten_json(profile)
for item in row:
if item not in titles:
titles.append(item)
csv_rows.append(row)
display.write_csv_file(csv_rows,
titles,
'Inbound SSO Profiles',
todrive)
'''gam update inboundssoprofile'''
def update_profile():
ci = build()
name = profile_displayname_to_name(sys.argv[3], ci)
body = {}
name_only, body = parse_profile(body, 4)
updateMask = ','.join(body.keys())
result = gapi.call(ci.inboundSamlSsoProfiles(),
'patch',
name=name,
updateMask=updateMask,
body=body)
if name_only:
print(result['response']['name'])
else:
display.print_json(result)
'''gam info inboundssoprofile'''
def info_profile(return_only=False, displayName=None, ci=None):
if not ci:
ci = build()
if not displayName:
displayName = sys.argv[3]
name = profile_displayname_to_name(displayName, ci)
result = gapi.call(ci.inboundSamlSsoProfiles(),
'get',
name=name)
if return_only:
return result
display.print_json(result)
'''gam delete inboundssoprofile'''
def delete_profile():
ci = build()
name = profile_displayname_to_name(sys.argv[3], ci)
result = gapi.call(ci.inboundSamlSsoProfiles(),
'delete',
name=name)
if result.get('done'):
print(f'Deleted profile {name}.')
else:
controlflow.system_error_exit(3, 'Delete did not finish: {result}')
'''gam create inboundssocredentials'''
def create_credentials():
allowed_sizes = [1024, 2048, 4096]
ci = build()
parent = None
generate_key = False
key_size = 2048
pemData = None
replace_oldest = False
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'profile':
parent = sys.argv[i+1]
parent = profile_displayname_to_name(parent, ci)
i += 2
elif myarg == 'pemfile':
pemfile = sys.argv[i+1]
pemData = fileutils.read_file(pemfile)
i += 2
elif myarg == 'generatekey':
generate_key = True
i += 1
elif myarg == 'replaceoldest':
replace_oldest = True
i += 1
elif myarg == 'keysize':
key_size = int(sys.argv[i+1])
if key_size not in allowed_sizes:
controlflow.expected_argument_exit('key_size',
allowed_sizes,
key_size)
i += 2
else:
controlflow.invalid_argument_exit(myarg, 'gam create inboundssocredential')
if not parent:
controlflow.missing_argument_exit('profile', 'gam create inboundssocredential')
if replace_oldest:
fields='nextPageToken,idpCredentials(name,updateTime)'
current_creds = gapi.get_all_pages(
ci.inboundSamlSsoProfiles().idpCredentials(),
'list',
'idpCredentials',
parent=parent,
fields=fields)
if len(current_creds) == 2:
oldest_key = min(current_creds,
key=lambda x:x['updateTime'])
print(' deleting older key...')
delete_credentials(ci=ci,
name=oldest_key['name'])
else:
print(' profile has {len(current_creds)} credentials. We only replace if there are 2.')
if generate_key:
privKey, pemData = gam._generatePrivateKeyAndPublicCert('GAM',
key_size,
b64enc_pub=False)
timestamp = datetime.now().strftime('%Y%m%d-%I%M%S')
priv_file = f'privatekey-{timestamp}.pem'
pub_file = f'publiccert-{timestamp}.pem'
fileutils.write_file(priv_file, privKey)
print(f' Wrote private key data to {priv_file}')
fileutils.write_file(pub_file, pemData)
print(f' Wrote public certificate to {pub_file}')
if not pemData:
controlflow.system_error_exit(3, 'You must either specify "pemfile <filename>" or "generate_key"')
body = {
'pemData': pemData,
}
result = gapi.call(ci.inboundSamlSsoProfiles().idpCredentials(),
'add',
parent=parent,
body=body)
if result.get('done'):
print(f'Created credential {result["response"]["name"]}')
display.print_json(result['response'])
else:
controlflow.system_error_exit(3, 'Create did not finish {result}')
'''gam delete inboundssocredential'''
def delete_credentials(ci=None, name=None):
if not ci:
ci = build()
if not name:
name = sys.argv[3]
result = gapi.call(ci.inboundSamlSsoProfiles().idpCredentials(),
'delete',
name=name)
if result.get('done'):
print(f'Deleted credential {name}')
else:
controlflow.system_error_exit(3, 'Delete did not finish {result}')
'''gam print inboundssocredentials'''
def print_show_credentials(action='print'):
ci = build()
todrive = False
i = 3
profiles = []
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg in ['profile', 'profiles']:
profiles = [profile_displayname_to_name(profile, ci)
for profile in sys.argv[i+1].split(',')]
i += 2
elif myarg == 'todrive':
todrive = True
i += 1
else:
controlflow.invalid_argument_exit(myarg, f'gam {action} inboundssocredentials')
if not profiles:
customer = get_sso_customer()
_filter = f'customer=="{customer}"'
profiles = gapi.get_all_pages(ci.inboundSamlSsoProfiles(),
'list',
'inboundSamlSsoProfiles',
fields='inboundSamlSsoProfiles/name',
filter=_filter)
profiles = [p['name'] for p in profiles]
if action == 'print':
titles = []
csv_rows = []
credentials = []
for profile in profiles:
results = gapi.get_all_pages(ci.inboundSamlSsoProfiles().idpCredentials(),
'list',
'idpCredentials',
parent=profile)
credentials.extend(results)
if action == 'show':
for c in credentials:
display.print_json(c)
print()
elif action == 'print':
for c in credentials:
csv_row = utils.flatten_json(c)
for item in csv_row:
if item not in titles:
titles.append(item)
csv_rows.append(csv_row)
display.write_csv_file(csv_rows,
titles,
'Inbound SSO Credentials',
todrive)
'''parse command for create/update inboundssoassignment'''
def parse_assignment(body, i, ci):
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'rank':
body['rank'] = int(sys.argv[i+1])
i += 2
elif myarg == 'mode':
mode_choices = \
gapi.get_enum_values_minus_unspecified(
ci._rootDesc['schemas']['InboundSsoAssignment']['properties']['ssoMode']['enum'])
body['ssoMode'] = sys.argv[i+1].upper()
if body['ssoMode'] not in mode_choices:
controlflow.expected_argument_exit('mode',
', '.join(mode_choices),
sys.argv[i+1])
i += 2
elif myarg == 'profile':
profile_name = profile_displayname_to_name(
sys.argv[i+1],
ci)
body['samlSsoInfo'] = {
'inboundSamlSsoProfile': profile_name
}
i += 2
elif myarg == 'neverredirect':
body['signInBehavior'] = {
'redirectCondition': 'NEVER'
}
i += 1
elif myarg == 'group':
group = sys.argv[i+1]
body['targetGroup'] = gapi_cloudidentity_groups.group_email_to_id(
ci,
group)
i += 2
elif myarg in ['ou', 'org', 'orgunit']:
body['targetOrgUnit'] = get_orgunit_id(sys.argv[i+1])
i += 2
else:
controlflow.invalid_argument_exit(myarg, 'gam create/update inboundssoassignment')
return body
def update_assignment_target_names(assignment, ci, cd):
if 'targetGroup' in assignment:
assignment['targetGroupEmail'] = \
gapi_cloudidentity_groups.group_id_to_email(ci,
assignment['targetGroup'])
elif 'targetOrgUnit' in assignment:
ou_id = assignment['targetOrgUnit'].split('/')[1]
assignment['targetOrgUnitPath'] = \
gapi_directory_orgunits.orgunit_from_orgunitid(f'id:{ou_id}', cd)
'''gam create inboundssoassignment'''
def create_assignment():
ci = build()
cd = gapi_directory.build()
body = {
'customer': get_sso_customer(),
}
body = parse_assignment(body, 3, ci)
result = gapi.call(ci.inboundSsoAssignments(),
'create',
body=body)
if result.get('done'):
print(f'Created assignment {result["response"]["name"]}')
update_assignment_target_names(result['response'], ci, cd)
display.print_json(result['response'])
else:
controlflow.system_error_exit(3, 'Create did not finish {result}')
def get_assignment_name(name):
if name.startswith('id:') or name.startswith('uid:'):
name = name.split(':', 1)[1]
if not name.startswith('inboundSsoAssignments/'):
name = f'inboundSsoAssignments/{name}'
return name
'''gam update inboundssoassignment'''
def update_assignment():
ci = build()
cd = gapi_directory.build()
name = get_assignment_name(sys.argv[3])
body = parse_assignment({}, 4, ci)
updateMask = ','.join(list(body.keys()))
result = gapi.call(ci.inboundSsoAssignments(),
'patch',
name=name,
updateMask=updateMask,
body=body)
if result.get('done'):
print(f'Updated assignment {result["response"]["name"]}')
update_assignment_target_names(result['response'], ci, cd)
display.print_json(result['response'])
else:
controlflow.system_error_exit(3, 'Update did not finish {result}')
'''gam info inboundssoassignment'''
def info_assignment():
ci = build()
cd = gapi_directory.build()
assignment = assignment_by_target(sys.argv[3], ci)
update_assignment_target_names(assignment, ci, cd)
profile = assignment.get('samlSsoInfo', {}).get('inboundSamlSsoProfile')
if profile:
assignment['samlSsoInfo']['inboundSamlSsoProfile'] = \
info_profile(return_only=True, displayName=f'id:{profile}', ci=ci)
display.print_json(assignment)
'''gam print inboundssoassignments'''
def print_show_assignments(action='print'):
ci = build()
cd = gapi_directory.build()
customer = get_sso_customer()
_filter = f'customer=="{customer}"'
todrive = False
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'todrive':
todrive = True
i += 1
else:
controlflow.invalid_argument_exit(myarg,
f'gam {action} inboundssoassignments')
assignments = gapi.get_all_pages(ci.inboundSsoAssignments(),
'list',
'inboundSsoAssignments',
filter=_filter)
if action == 'show':
for assignment in assignments:
update_assignment_target_names(assignment, ci, cd)
display.print_json(assignment)
print()
elif action == 'print':
titles = []
csv_rows = []
for assignment in assignments:
update_assignment_target_names(assignment, ci, cd)
csv_row = utils.flatten_json(assignment)
for item in csv_row:
if item not in titles:
titles.append(item)
csv_rows.append(csv_row)
display.write_csv_file(csv_rows,
titles,
'Inbound SSO Assignments',
todrive)

View File

@@ -25,7 +25,7 @@ def _reduce_name(name):
def is_invitable_user(email):
'''return email isInvitableUser'''
svc = gapi_cloudidentity.build_dwd('cloudidentity_beta')
svc = gapi_cloudidentity.build('cloudidentity')
customer = _get_customerid()
encoded_email = quote_plus(email)
name = f'{customer}/userinvitations/{encoded_email}'
@@ -35,7 +35,7 @@ def is_invitable_user(email):
def _generic_action(action):
'''generic function to call actionable APIs'''
svc = gapi_cloudidentity.build_dwd('cloudidentity_beta')
svc = gapi_cloudidentity.build('cloudidentity')
customer = _get_customerid()
email = sys.argv[3].lower()
encoded_email = quote_plus(email)
@@ -55,7 +55,7 @@ def _generic_action(action):
def _generic_get(get_type):
'''generic function to call read data APIs'''
svc = gapi_cloudidentity.build_dwd('cloudidentity_beta')
svc = gapi_cloudidentity.build('cloudidentity')
customer = _get_customerid()
email = sys.argv[3].lower()
encoded_email = quote_plus(email)
@@ -75,7 +75,7 @@ def bulk_is_invitable(emails):
if response.get('isInvitableUser'):
rows.append({'invitableUsers': request_id})
svc = gapi_cloudidentity.build_dwd('cloudidentity_beta')
svc = gapi_cloudidentity.build('cloudidentity')
customer = _get_customerid()
todrive = False
#batch_size = 1000
@@ -139,7 +139,7 @@ USERINVITATION_STATE_CHOICES_MAP = {
def print_():
'''gam print userinvitations'''
svc = gapi_cloudidentity.build_dwd('cloudidentity_beta')
svc = gapi_cloudidentity.build('cloudidentity')
customer = _get_customerid()
todrive = False
titles = ['name', 'state', 'updateTime']

View File

@@ -13,12 +13,14 @@ def get_org_id():
gapi_directory_customer.setTrueCustomerId()
crm = build()
query = f'directorycustomerid:{GC_Values[GC_CUSTOMER_ID]}'
orgs = gapi.get_all_pages(crm.organizations(),
results = gapi.call(crm.organizations(),
'search',
'organizations',
pageSize=1,
fields='organizations/name',
query=query)
if len(orgs) < 1:
orgs = results.get('organizations')
if not orgs:
# return nothing and let calling API deal with it
# since caller knows what GCP role would serve best
return
return orgs[0]['name']
return orgs[0].get('name')

View File

@@ -151,12 +151,19 @@ def doUpdateCros():
elif action == 'deprovisionupgradetransfer':
action = 'deprovision'
deprovisionReason = 'upgrade_transfer'
elif action not in ['disable', 'reenable']:
elif action in ['disable', 'reenable']:
pass
elif action == 'preprovisioneddisable':
action = 'pre_provisioned_disable'
elif action == 'preprovisionedreenable':
action = 'pre_provisioned_reenable'
else:
controlflow.system_error_exit(2, f'expected action of ' \
f'deprovision_same_model_replace, ' \
f'deprovision_different_model_replace, ' \
f'deprovision_retiring_device, ' \
f'deprovision_upgrade_transfer, disable or reenable,'
f'deprovision_upgrade_transfer, disable, reenable, '\
f'pre_provisioned_disable, pre_provisioned_reenable'\
f' got {action}')
action_body = {'action': action}
if deprovisionReason:
@@ -448,7 +455,7 @@ def doPrintCrosActivity():
selectActiveTimeRanges = selectDeviceFiles = selectRecentUsers = False
listLimit = 0
delimiter = ','
orgUnitPath = None
orgUnitPath = includeChildOrgunits = None
queries = [None]
i = 3
while i < len(sys.argv):
@@ -456,8 +463,9 @@ def doPrintCrosActivity():
if myarg in ['query', 'queries']:
queries = gam.getQueries(myarg, sys.argv[i + 1])
i += 2
elif myarg == 'limittoou':
elif myarg in {'limittoou', 'crosou', 'crosouandchildren'}:
orgUnitPath = gapi_directory_orgunits.getOrgUnitItem(sys.argv[i + 1])
includeChildOrgunits = myarg == 'crosouandchildren'
i += 2
elif myarg == 'todrive':
todrive = True
@@ -524,8 +532,9 @@ def doPrintCrosActivity():
query=query,
customerId=GC_Values[GC_CUSTOMER_ID],
projection='FULL',
fields=fields,
orgUnitPath=orgUnitPath)
orgUnitPath=orgUnitPath,
includeChildOrgunits=includeChildOrgunits,
fields=fields)
for cros in all_cros:
row = {}
skip_attribs = ['recentUsers', 'activeTimeRanges', 'deviceFiles']
@@ -612,7 +621,7 @@ def doPrintCrosDevices():
csvRows = []
display.add_field_to_csv_file('deviceid', CROS_ARGUMENT_TO_PROPERTY_MAP,
fieldsList, fieldsTitles, titles)
projection = orderBy = sortOrder = orgUnitPath = None
projection = orderBy = sortOrder = orgUnitPath = includeChildOrgunits = None
queries = [None]
noLists = sortHeaders = False
selectedLists = {}
@@ -624,8 +633,9 @@ def doPrintCrosDevices():
if myarg in ['query', 'queries']:
queries = gam.getQueries(myarg, sys.argv[i + 1])
i += 2
elif myarg == 'limittoou':
elif myarg in {'limittoou', 'crosou', 'crosouandchildren'}:
orgUnitPath = gapi_directory_orgunits.getOrgUnitItem(sys.argv[i + 1])
includeChildOrgunits = myarg == 'crosouandchildren'
i += 2
elif myarg == 'todrive':
todrive = True
@@ -729,6 +739,7 @@ def doPrintCrosDevices():
customerId=GC_Values[GC_CUSTOMER_ID],
projection=projection,
orgUnitPath=orgUnitPath,
includeChildOrgunits=includeChildOrgunits,
orderBy=orderBy,
sortOrder=sortOrder,
fields=fields)

View File

@@ -21,36 +21,36 @@ def doGetCustomerInfo():
'get',
customerKey=customer_id)
print(f'Customer ID: {customer_info["id"]}')
print(f'Primary Domain: {customer_info["customerDomain"]}')
fields = 'domains(creationTime,domainName,isPrimary,verified)'
try:
result = gapi.call(
domains = gapi.call(
cd.domains(),
'get',
'list',
fields=fields,
customer=customer_id,
domainName=customer_info['customerDomain'],
fields='verified',
throw_reasons=[gapi.errors.ErrorReason.DOMAIN_NOT_FOUND])
throw_reasons=[gapi.errors.ErrorReason.DOMAIN_NOT_FOUND]).get('domains', [])
for domain in domains:
if domain.get('isPrimary'):
primary_domain = domain
break
else:
primary_domain = {}
except gapi.errors.GapiDomainNotFoundError:
result = {'verified': False}
print(f'Primary Domain Verified: {result["verified"]}')
# If customer has changed primary domain customerCreationTime is date
# of current primary being added, not customer create date.
# We should also get all domains and use oldest date
customer_creation = customer_info['customerCreationTime']
date_format = '%Y-%m-%dT%H:%M:%S.%fZ'
oldest = datetime.datetime.strptime(customer_creation, date_format)
domains = gapi.get_items(cd.domains(),
'list',
'domains',
customer=customer_id,
fields='domains(creationTime)')
primary_domain = {}
print(f'Primary Domain: {primary_domain.get("domainName", "Unknown")}')
print(f'Primary Domain Verified: {primary_domain.get("verified", "Unknown")}')
# we'll assume creation time is time of oldest domain customer has
oldest = 'Unknown'
for domain in domains:
creation_timestamp = int(domain['creationTime']) / 1000
domain_creation = datetime.datetime.fromtimestamp(creation_timestamp)
if domain_creation < oldest:
if oldest == 'Unknown' or domain_creation < oldest:
oldest = domain_creation
print(f'Customer Creation Time: {oldest.strftime(date_format)}')
customer_language = customer_info.get('language', 'Unset (defaults to en)')
if oldest != 'Unknown':
date_format = '%Y-%m-%dT%H:%M:%S.%fZ'
oldest = oldest.strftime(date_format)
print(f'Customer Creation Time: {oldest}')
customer_language = customer_info.get('language', 'Unset or Unknown (defaults to en)')
print(f'Default Language: {customer_language}')
if 'postalAddress' in customer_info:
print('Address:')
@@ -59,7 +59,7 @@ def doGetCustomerInfo():
print(f' {field}: {customer_info["postalAddress"][field]}')
if 'phoneNumber' in customer_info:
print(f'Phone: {customer_info["phoneNumber"]}')
print(f'Admin Secondary Email: {customer_info["alternateEmail"]}')
print(f'Admin Secondary Email: {customer_info.get("alternateEmail", "Unknown")}')
user_counts_map = {
'accounts:num_users': 'Total Users',
'accounts:gsuite_basic_total_licenses': 'G Suite Basic Licenses',
@@ -95,12 +95,10 @@ def doGetCustomerInfo():
continue
except gapi.errors.GapiForbiddenError:
return
warnings = result.get('warnings', [])
fullDataRequired = ['accounts']
usage = result.get('usageReports')
has_reports = bool(usage)
fullData, tryDate = gapi_reports._check_full_data_available(
warnings, tryDate, fullDataRequired, has_reports)
result, tryDate, fullDataRequired, False)
if fullData < 0:
print('No user report available.')
sys.exit(1)

View File

@@ -189,7 +189,7 @@ def info(group_name=None):
pass
print('')
print('Group Settings:')
for key, value in list(basic_info.items()):
for key, value in sorted(list(basic_info.items())):
if (key in ['kind', 'etag']) or ((key == 'aliases') and
(not getAliases)):
continue
@@ -199,7 +199,7 @@ def info(group_name=None):
print(f' {val}')
else:
print(f' {key}: {value}')
for key, value in list(settings.items()):
for key, value in sorted(list(settings.items())):
if key in ['kind', 'etag', 'description', 'email', 'name']:
continue
print(f' {key}: {value}')
@@ -1217,6 +1217,8 @@ GROUP_SETTINGS_LIST_PATTERN = re.compile(r'([A-Z][A-Z_]+[A-Z]?)')
def getGroupAttrValue(myarg, value, gs_object, gs_body, function):
if myarg == 'collaborative':
myarg = 'enablecollaborativeinbox'
elif myarg == 'gal':
myarg = 'includeinglobaladdresslist'
for (attrib,
params) in list(gs_object['schemas']['Groups']['properties'].items()):
if attrib in ['kind', 'etag', 'email']:

View File

@@ -58,22 +58,32 @@ def getRoleId(role):
def getPrivileges(body, privs, action):
all_privileges = gapi_directory_privileges.print_(return_only=True)
def expandChildPrivileges(privilege):
for childPrivilege in privilege.get('childPrivileges', []):
childPrivileges[childPrivilege['privilegeName']] = childPrivilege['serviceId']
expandChildPrivileges(childPrivilege)
allPrivileges = {}
ouPrivileges = {}
childPrivileges = {}
for privilege in gapi_directory_privileges.print_(return_only=True):
allPrivileges[privilege['privilegeName']] = privilege['serviceId']
if privilege['isOuScopable']:
ouPrivileges[privilege['privilegeName']] = privilege['serviceId']
expandChildPrivileges(privilege)
if privs == 'ALL':
body['rolePrivileges'] = [
{'privilegeName': p['privilegeName'], 'serviceId': p['serviceId']} for p in all_privileges
]
body['rolePrivileges'] = [{'privilegeName': priv, 'serviceId': v} for priv, v in allPrivileges.items()]
elif privs == 'ALL_OU':
body['rolePrivileges'] = [
{'privilegeName': p['privilegeName'], 'serviceId': p['serviceId']} for p in all_privileges if p.get('isOuScopable')
]
body['rolePrivileges'] = [{'privilegeName': priv, 'serviceId': v} for priv, v in ouPrivileges.items()]
else:
body.setdefault('rolePrivileges', [])
for priv in privs.split(','):
for p in all_privileges:
if priv == p['privilegeName']:
body['rolePrivileges'].append({'privilegeName': p['privilegeName'], 'serviceId': p['serviceId']})
break
if priv in allPrivileges:
body['rolePrivileges'].append({'privilegeName': priv, 'serviceId': allPrivileges[priv]})
elif priv in ouPrivileges:
body['rolePrivileges'].append({'privilegeName': priv, 'serviceId': ouPrivileges[priv]})
elif priv in childPrivileges:
body['rolePrivileges'].append({'privilegeName': priv, 'serviceId': childPrivileges[priv]})
else:
controlflow.invalid_argument_exit(priv,
f'gam {action} adminrole privileges')

View File

@@ -2,6 +2,7 @@ import sys
from time import sleep
import gam
from gam import controlflow
from gam import display
from gam import gapi
from gam.gapi import directory as gapi_directory
@@ -18,7 +19,7 @@ def delete():
userKey=user_email,
throw_reasons=[gapi_errors.ErrorReason.CONDITION_NOT_MET])
except gam.gapi.errors.GapiConditionNotMetError as err:
display.print_error(
controlflow.system_error_exit(3,
f'{err} The user {user_email} may be (or have recently been) on Google Vault Hold and thus not eligible for deletion. You can check holds with "gam user <email> show vaultholds".'
)

View File

@@ -85,15 +85,13 @@ def showUsageParameters():
customerId=customerId,
fields='warnings,usageReports(parameters(name))',
**kwargs)
warnings = result.get('warnings', [])
usage = result.get('usageReports')
has_reports = bool(usage)
fullData, tryDate = _check_full_data_available(
warnings, tryDate, fullDataRequired, has_reports)
result, tryDate, fullDataRequired, False)
if fullData < 0:
print('No usage parameters available.')
sys.exit(1)
if has_reports:
if usage:
for parameter in usage[0]['parameters']:
name = parameter.get('name')
if name:
@@ -350,10 +348,8 @@ def showReport():
orgUnitID=orgUnitId,
fields='warnings,usageReports',
maxResults=1)
warnings = one_page.get('warnings', [])
has_reports = bool(one_page.get('usageReports'))
fullData, tryDate = _check_full_data_available(
warnings, tryDate, fullDataRequired, has_reports)
one_page, tryDate, fullDataRequired, True)
if fullData < 0:
print('No user report available.')
sys.exit(1)
@@ -382,7 +378,7 @@ def showReport():
for user_report in usage:
if 'entity' not in user_report:
continue
row = {'email': user_report['entity']['userEmail'], 'date': tryDate}
row = {'email': user_report['entity'].get('userEmail', 'Unknown'), 'date': tryDate}
for item in user_report.get('parameters', []):
if 'name' not in item:
continue
@@ -407,10 +403,8 @@ def showReport():
customerId=customerId,
date=tryDate,
fields='warnings,usageReports')
warnings = first_page.get('warnings', [])
has_reports = bool(first_page.get('usageReports'))
fullData, tryDate = _check_full_data_available(
warnings, tryDate, fullDataRequired, has_reports)
first_page, tryDate, fullDataRequired, False)
if fullData < 0:
print('No customer report available.')
sys.exit(1)
@@ -432,6 +426,7 @@ def showReport():
titles = ['name', 'value', 'client_id']
csvRows = []
auth_apps = list()
usage = list(usage)
for item in usage[0]['parameters']:
if 'name' not in item:
continue
@@ -563,15 +558,16 @@ def _adjust_date(errMsg):
return str(match_date.group(1))
def _check_full_data_available(warnings, tryDate, fullDataRequired,
has_reports):
def _check_full_data_available(result, tryDate, fullDataRequired,
checkUserEmail):
one_day = datetime.timedelta(days=1)
tryDateTime = datetime.datetime.strptime(tryDate, YYYYMMDD_FORMAT)
# move to day before if we don't have at least one usageReport
if not has_reports:
usage = result.get('usageReports')
if not usage:
tryDateTime -= one_day
return (0, tryDateTime.strftime(YYYYMMDD_FORMAT))
for warning in warnings:
for warning in result.get('warnings', []):
if warning['code'] == 'PARTIAL_DATA_AVAILABLE':
for app in warning['data']:
if app['key'] == 'application' and \
@@ -586,4 +582,8 @@ def _check_full_data_available(warnings, tryDate, fullDataRequired,
app['value'] != 'docs' and \
(not fullDataRequired or app['value'] in fullDataRequired):
return (-1, tryDate)
if checkUserEmail:
if 'entity' not in usage[0] or 'userEmail' not in usage[0]['entity']:
tryDateTime -= one_day
return (0, tryDateTime.strftime(YYYYMMDD_FORMAT))
return (1, tryDate)

View File

@@ -2,20 +2,150 @@ import base64
import os
import re
import sys
import time
import googleapiclient
from pathvalidate import sanitize_filepath
import gam
from gam.gapi import errors as gapi_errors
from gam.var import *
from gam import controlflow
from gam import fileutils
from gam import gapi
from gam import utils
def build_gapi():
def build():
return gam.buildGAPIObject('storage')
def copy_bucket():
s = build()
source_bucket = None
target_bucket = None
prefix = None
target_prefix = ''
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'sourcebucket':
source_bucket = sys.argv[i+1]
i += 2
elif myarg == 'targetbucket':
target_bucket = sys.argv[i+1]
i += 2
elif myarg == 'sourceprefix':
prefix = sys.argv[i+1]
i += 2
elif myarg == 'targetprefix':
target_prefix = sys.argv[i+1]
i += 2
else:
controlflow.invalid_argument_exit(sys.argv[i],
'gam copy storagebucket')
if not target_bucket:
controlflow.missing_argument_exit('target_bucket', 'gam copy storagebucket')
if not source_bucket:
controlflow.missing_argument_exit('source_bucket', 'gam copy storagebucket')
page_message = gapi.got_total_items_msg('Storage Objects', '...\n')
objects = gapi.get_all_pages(s.objects(),
'list',
items='items',
page_message=page_message,
prefix=prefix,
bucket=source_bucket,
fields='items(name,bucket,md5Hash),nextPageToken')
copy_objects(objects,
target_bucket,
target_prefix)
def copy_objects(objects,
target_bucket,
target_prefix):
"""Copies objects to target_bucket.
Args:
objects: list of object dicts
[
{
bucket: source bucket,
name: source object name,
(optional) md5Hash: source file hash value
},
...
]
target_bucket: target bucket id
target_prefix: prefix name to prepend to target object
"""
def process_rewrite(request_id, response, exception):
file_ptr = int(request_id)
if exception:
# Poor man's backoff/retry
if exception.status_code == 429 or exception.status_code > 499:
print(f'Temporary error {exception.status_code}. Sleeping 10 seconds...')
time.sleep(10)
next_batch.add(s.objects().rewrite(**files_to_copy[file_ptr]['method']),
request_id=request_id)
return
else:
raise exception
file_count = file_ptr + 1
source_displayname = files_to_copy[file_ptr]['source_displayname']
target_displayname = files_to_copy[file_ptr]['target_displayname']
if response.get('done'):
source_md5 = files_to_copy[file_ptr]['md5Hash']
target_md5 = response['resource']['md5Hash']
if source_md5 != target_md5:
controlflow.system_error_exit(99, f'Target file {target_displayname} checksum {target_md5} does not match source {source_md5}. This should not happen')
else:
print(f'[ {file_count} / {total_files} ] 100% VERIFIED - finished copying:\n source: {source_displayname}\n dest: {target_displayname}')
else:
total_bytes = float(response.get('objectSize'))
done_bytes = float(response.get('totalBytesRewritten'))
pct = (done_bytes / total_bytes) * 100
print(f'[ {file_count} / {total_files} ] {pct:.2f}%\n source: {source_displayname}\n dest:{target_displayname}')
files_to_copy[file_ptr]['method']['rewriteToken'] = response.get('rewriteToken')
next_batch.add(s.objects().rewrite(**files_to_copy[file_ptr]['method']),
request_id=request_id)
s = build()
sbatch = s.new_batch_http_request(callback=process_rewrite)
files_to_copy = []
for object_ in objects:
files_to_copy.append(
{
'md5Hash': object_['md5Hash'],
'source_displayname': f'{object_["bucket"]}:{object_["name"]}',
'target_displayname': f'{target_bucket}:{target_prefix}{object_["name"]}',
'method': {
'destinationBucket': target_bucket,
'destinationObject': f'{target_prefix}{object_["name"]}',
'sourceBucket': object_['bucket'],
'sourceObject': object_['name'],
# 'maxBytesRewrittenPerCall': 1048576, # uncomment to easily test multiple rewrite API calls per object
},
})
i = 0
total_files = len(files_to_copy)
for file in files_to_copy:
while len(sbatch._order) == 100:
next_batch = s.new_batch_http_request(callback=process_rewrite)
sbatch.execute()
sbatch = next_batch
sbatch.add(s.objects().rewrite(**file['method']),
request_id=str(i))
i += 1
while len(sbatch._order) > 0:
next_batch = s.new_batch_http_request(callback=process_rewrite)
sbatch.execute()
sbatch = next_batch
print('All done!')
def get_cloud_storage_object(s,
bucket,
object_,
@@ -23,11 +153,12 @@ def get_cloud_storage_object(s,
expectedMd5=None):
if not local_file:
local_file = object_
local_file = sanitize_filepath(local_file, platform='auto')
if os.path.exists(local_file):
sys.stdout.write(' File already exists. ')
sys.stdout.write(f'File {local_file} already exists.')
sys.stdout.flush()
if expectedMd5:
sys.stdout.write(f'Verifying {expectedMd5} hash...')
sys.stdout.write(f' verifying {expectedMd5} hash...')
sys.stdout.flush()
if utils.md5_matches_file(local_file, expectedMd5, False):
print('VERIFIED')
@@ -35,7 +166,7 @@ def get_cloud_storage_object(s,
print('not verified. Downloading again and over-writing...')
else:
return # nothing to verify, just assume we're good.
print(f'saving to {local_file}')
print(f'Saving to {local_file}')
request = s.objects().get_media(bucket=bucket, object=object_)
file_path = os.path.dirname(local_file)
if not os.path.exists(file_path):
@@ -60,7 +191,7 @@ def get_cloud_storage_object(s,
def download_bucket():
bucket = sys.argv[3]
s = build_gapi()
s = build()
page_message = gapi.got_total_items_msg('Files', '...')
fields = 'nextPageToken,items(name,id,md5Hash)'
objects = gapi.get_all_pages(s.objects(),

View File

@@ -1,3 +1,4 @@
from base64 import b64encode
import datetime
import json
import sys
@@ -11,6 +12,7 @@ from gam import controlflow
from gam import display
from gam import fileutils
from gam import gapi
from gam.gapi import errors as gapi_errors
from gam.gapi import storage as gapi_storage
from gam.gapi import directory as gapi_directory
from gam.gapi.directory import orgunits as gapi_directory_orgunits
@@ -336,9 +338,9 @@ def print_count():
_validate_query(query, query_discovery)
body['query'] = query
operation = gapi.call(v.matters(), 'count', matterId=matterId, body=body)
print(f'Watching operation {operation["name"]}...')
sys.stderr.write(f'Watching operation {operation["name"]}...\n')
while not operation.get('done'):
print(f' operation {operation["name"]} is not done yet. Checking again in {operation_wait} seconds')
sys.stderr.write(f' operation {operation["name"]} is not done yet. Checking again in {operation_wait} seconds\n')
sleep(operation_wait)
operation = gapi.call(v.operations(), 'get', name=operation['name'])
response = operation.get('response', {})
@@ -518,7 +520,6 @@ def getHoldInfo():
def convertExportNameToID(v, nameOrID, matterId):
nameOrID = nameOrID.lower()
cg = UID_PATTERN.match(nameOrID)
if cg:
return cg.group(1)
@@ -529,7 +530,7 @@ def convertExportNameToID(v, nameOrID, matterId):
matterId=matterId,
fields=fields)
for export in exports:
if export['name'].lower() == nameOrID:
if export['name'].lower() == nameOrID.lower():
return export['id']
controlflow.system_error_exit(
4, f'could not find export name {nameOrID} '
@@ -682,18 +683,23 @@ def showHoldsForUsers(users):
v = buildGAPIObject()
matterIds = _getAllMatterIds(v)
matterHolds = {}
fields = 'holds(holdId,name,accounts(accountId,email),orgUnit),nextPageToken'
for matterId in matterIds:
matterHolds[matterId] = gapi.get_all_pages(v.matters().holds(),
'list',
'holds',
fields='holds(holdId,name,accounts(accountId,email),orgUnit),nextPageToken',
matterId=matterId)
try:
matterHolds[matterId] = gapi.get_all_pages(v.matters().holds(),
'list',
'holds',
fields=fields,
throw_reasons=[gapi_errors.ErrorReason.FOUR_O_O],
matterId=matterId)
except googleapiclient.errors.HttpError:
continue
totalHolds = 0
for user in users:
user = user.lower()
orgUnits = gapi_directory_orgunits._getAllParentOrgUnitsForUser(user, cd)
for matterId in matterIds:
for hold in matterHolds[matterId]:
for matterId, holds in matterHolds.items():
for hold in holds:
if 'orgUnit' in hold:
orgUnitId = hold['orgUnit'].get('orgUnitId')
if orgUnitId in orgUnits:
@@ -791,11 +797,49 @@ def getMatterInfo():
display.print_json(result)
def copyExport():
v = buildGAPIObject()
s = gapi_storage.build()
matterId = getMatterItem(v, sys.argv[3])
exportId = convertExportNameToID(v, sys.argv[4], matterId)
target_bucket = None
target_prefix = ''
i = 5
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'targetbucket':
target_bucket = sys.argv[i+1]
i += 2
elif myarg == 'targetprefix':
target_prefix = sys.argv[i+1]
i += 2
else:
controlflow.invalid_argument_exit(sys.argv[i],
'gam copy export')
if not target_bucket:
controlflow.missing_argument_exit('target_bucket', 'gam copy export')
export = gapi.call(v.matters().exports(),
'get',
matterId=matterId,
exportId=exportId)
objects = []
for s_file in export['cloudStorageSink']['files']:
# Convert to md5Hash format Storage API uses
# because OF COURSE they differ
md5Hash = b64encode(bytes.fromhex(s_file['md5Hash'])).decode()
objects.append({'bucket': s_file['bucketName'],
'name': s_file['objectName'],
'md5Hash': md5Hash})
gapi_storage.copy_objects(objects,
target_bucket,
target_prefix)
def downloadExport():
verifyFiles = True
extractFiles = True
v = buildGAPIObject()
s = gapi_storage.build_gapi()
s = gapi_storage.build()
matterId = getMatterItem(v, sys.argv[3])
exportId = convertExportNameToID(v, sys.argv[4], matterId)
targetFolder = GC_Values[GC_DRIVE_DIR]
@@ -823,24 +867,17 @@ def downloadExport():
for s_file in export['cloudStorageSink']['files']:
bucket = s_file['bucketName']
s_object = s_file['objectName']
filename = os.path.join(targetFolder, s_object.replace('/', '-'))
print(f'saving to {filename}')
request = s.objects().get_media(bucket=bucket, object=s_object)
f = fileutils.open_file(filename, 'wb')
downloader = googleapiclient.http.MediaIoBaseDownload(f, request)
done = False
while not done:
status, done = downloader.next_chunk()
sys.stdout.write(f' Downloaded: {status.progress():>7.2%}\r')
sys.stdout.flush()
sys.stdout.write('\n Download complete. Flushing to disk...\n')
fileutils.close_file(f, True)
if verifyFiles:
expected_hash = s_file['md5Hash']
sys.stdout.write(f' Verifying file hash is {expected_hash}...')
sys.stdout.flush()
utils.md5_matches_file(filename, expected_hash, True)
print('VERIFIED')
else:
expected_hash = None
local_file = s_object.replace('/', '-').replace(':', '-')
filename = os.path.join(targetFolder, local_file)
gapi_storage.get_cloud_storage_object(s,
bucket,
s_object,
local_file=filename,
expectedMd5=expected_hash)
if extractFiles and re.search(r'\.zip$', filename):
gam.extract_nested_zip(filename, targetFolder)
@@ -976,10 +1013,14 @@ def printHolds():
for matterId in matterIds:
i += 1
sys.stderr.write(f'Retrieving holds for matter {matterId} ({i}/{matter_count})\n')
holds = gapi.get_all_pages(v.matters().holds(),
try:
holds = gapi.get_all_pages(v.matters().holds(),
'list',
'holds',
throw_reasons=[gapi_errors.ErrorReason.FOUR_O_O],
matterId=matterId)
except googleapiclient.errors.HttpError:
continue
for hold in holds:
display.add_row_titles_to_csv_file(
utils.flatten_json(hold, flattened={'matterId': matterId}),

View File

@@ -96,9 +96,13 @@ class _DeHTMLParser(HTMLParser):
re.sub(r'\n +', '\n', ''.join(self.__text))).strip()
def commonprefix(m):
def commonprefix(m, checkEnum=False):
'''Given a list of strings m, return string which is prefix common to all'''
s1 = min(m)
if checkEnum:
loc = s1.find('ENUM_')
if loc > 0:
return s1[:loc+5]
s2 = max(m)
for i, c in enumerate(s1):
if c != s2[i]:

View File

@@ -8,7 +8,7 @@ import platform
import re
GAM_AUTHOR = 'Jay Lee <jay0lee@gmail.com>'
GAM_VERSION = '6.22'
GAM_VERSION = '6.50'
GAM_LICENSE = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
GAM_URL = 'https://jaylee.us/gam'
@@ -31,7 +31,8 @@ usergroup_types = [
'ou_and_child', 'ou_and_children_ns', 'ou_and_child_ns',
'ou_and_children_susp', 'ou_and_child_susp', 'query', 'queries', 'license',
'licenses', 'licence', 'licences', 'file', 'csv', 'csvfile', 'all', 'cros',
'cros_sn', 'crosquery', 'crosqueries', 'crosfile', 'croscsv', 'croscsvfile'
'cros_sn', 'crosquery', 'crosqueries', 'crosfile', 'croscsv', 'croscsvfile',
'cros_ou', 'cros_ou_and_children'
]
ERROR_PREFIX = 'ERROR: '
WARNING_PREFIX = 'WARNING: '
@@ -126,6 +127,16 @@ SKUS = {
'aliases': ['gwetlu', 'workspaceeducationupgrade'],
'displayName': 'Google Workspace for Education: Teaching and Learning Upgrade'
},
'1010390001': {
'product': '101039',
'aliases': ['assuredcontrols'],
'displayName': 'Assured Controls',
},
'1010400001': {
'product': '101040',
'aliases': ['beyondcorp', 'beyondcorpenterprise', 'bce'],
'displayName': 'Beyond Corp Enterprise',
},
'Google-Apps': {
'product': 'Google-Apps',
'aliases': ['standard', 'free'],
@@ -205,7 +216,7 @@ SKUS = {
},
'1010020030': {
'product': 'Google-Apps',
'aliases': ['workspacefrontline', 'workspacefrontlineworker'],
'aliases': ['wsflw', 'workspacefrontline', 'workspacefrontlineworker'],
'displayName': 'Workspace Frontline'
},
'1010340002': {
@@ -289,6 +300,8 @@ PRODUCTID_NAME_MAPPINGS = {
'101035': 'Cloud Search',
'101036': 'Google Meet Global Dialing',
'101037': 'G Suite Workspace for Education',
'101039': 'Assured Controls',
'101040': 'Beyond Corp',
'Google-Apps': 'Google Workspace',
'Google-Chrome-Device-Management': 'Google Chrome Device Management',
'Google-Drive-storage': 'Google Drive Storage',
@@ -647,6 +660,7 @@ MACOS_CODENAMES = {
},
11: 'Big Sur',
12: 'Monterey',
13: 'Ventura',
}
_MICROSOFT_FORMATS_LIST = [{
@@ -970,6 +984,8 @@ CROS_ARGUMENT_TO_PROPERTY_MAP = {
'autoupdateexpiration': ['autoUpdateExpiration',],
'bootmode': ['bootMode',],
'cpustatusreports': ['cpuStatusReports',],
'deprovisionreason': ['deprovisionReason',],
'lastDeprovisionTimestamp': ['lastDeprovisionTimestamp',],
'devicefiles': ['deviceFiles',],
'deviceid': ['deviceId',],
'dockmacaddress': ['dockMacAddress',],
@@ -977,6 +993,7 @@ CROS_ARGUMENT_TO_PROPERTY_MAP = {
'ethernetmacaddress': ['ethernetMacAddress',],
'ethernetmacaddress0': ['ethernetMacAddress0',],
'firmwareversion': ['firmwareVersion',],
'firstenrollmenttime': ['firstEnrollmentTime',],
'lastenrollmenttime': ['lastEnrollmentTime',],
'lastknownnetwork': ['lastKnownNetwork'],
'lastsync': ['lastSync',],
@@ -1034,7 +1051,10 @@ CROS_SCALAR_PROPERTY_PRINT_ORDER = [
'ethernetMacAddress0',
'macAddress',
'systemRamTotal',
'firstEnrollmentTime',
'lastEnrollmentTime',
'deprovisionReason',
'lastDeprovisionTimestamp',
'orderNumber',
'manufactureDate',
'supportEndDate',
@@ -1201,6 +1221,7 @@ _DEFAULT_CHARSET = UTF8
_FN_CLIENT_SECRETS_JSON = 'client_secrets.json'
_FN_OAUTH2SERVICE_JSON = 'oauth2service.json'
_FN_OAUTH2_TXT = 'oauth2.txt'
_FN_ROOTS_PEM = 'roots.pem'
#
GM_Globals = {
GM_SYSEXITRC: 0,
@@ -1268,6 +1289,9 @@ GC_ENABLE_DASA = 'enabledasa'
# and doRequestOAuth prints a link and waits for the verification code when
# oauth2.txt is being created
GC_NO_BROWSER = 'no_browser'
# If low memory is True, GAM tries to save RAM by writing pages to disk
# temporarily
GC_LOW_MEMORY = 'low_memory'
# 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.
@@ -1323,6 +1347,7 @@ GC_Defaults = {
GC_DOMAIN: '',
GC_DRIVE_DIR: '',
GC_ENABLE_DASA: False,
GC_LOW_MEMORY: False,
GC_NO_BROWSER: False,
GC_NO_TDEMAIL: False,
GC_NO_CACHE: False,
@@ -1342,7 +1367,7 @@ GC_Defaults = {
GC_CSV_ROW_DROP_FILTER: '',
GC_TLS_MIN_VERSION: TLS_MIN,
GC_TLS_MAX_VERSION: None,
GC_CA_FILE: None,
GC_CA_FILE: _FN_ROOTS_PEM,
}
GC_Values = {}
@@ -1407,6 +1432,9 @@ GC_VAR_INFO = {
GC_ENABLE_DASA: {
GC_VAR_TYPE: GC_TYPE_BOOLEAN
},
GC_LOW_MEMORY: {
GC_VAR_TYPE: GC_TYPE_BOOLEAN
},
GC_NO_BROWSER: {
GC_VAR_TYPE: GC_TYPE_BOOLEAN
},

View File

@@ -10,4 +10,4 @@ importlib.metadata; python_version < '3.8'
passlib>=1.7.2
pathvalidate
python-dateutil
yubikey-manager>=4.0.0
yubikey-manager>=5.0

1130
src/roots.pem Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[metadata]
name = GAM for Google Workspace
version = 6.0.22
version = attr: gam.var.GAM_VERSION
description = Command line management for Google Workspaces
long_description = file: readme.md
long_description_content_type = text/markdown
@@ -37,6 +37,9 @@ install_requires =
yubikey-manager >= 4.0.0
pathvalidate
[options.package_data]
* = *.pem
# used during pip install .[test]
[options.extras_require]
test = pre-commit