Compare commits

...

221 Commits
v6.21 ... v6.31

Author SHA1 Message Date
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
Jay Lee
939a1afbbf fix device user printing 2022-06-30 14:48:44 +00:00
Jay Lee
ecda9fe232 fix deviceuser list pageSize 2022-06-30 00:28:10 +00:00
Jay Lee
76eb79eb2d Merge branch 'main' of https://github.com/GAM-team/GAM 2022-06-30 00:12:51 +00:00
Jay Lee
325e597772 Workaround for #1534 2022-06-30 00:12:45 +00:00
Ross Scroggs
c6334d2bcd Allow super admin to delete any shared drive (#1533) 2022-06-27 14:50:17 -04:00
Jay Lee
72ff9ff7e9 rebuild to start using OpenSSL 3.0.4 2022-06-21 12:53:10 -04:00
Jay Lee
b45ad5dcaf Update build.yml 2022-06-17 08:23:29 -04:00
Ross Scroggs
3d14964365 Add address fields to buildings (#1529) 2022-06-10 11:16:15 -04:00
Jay Lee
d15bab5a29 Update build.yml 2022-06-06 13:43:57 -04:00
Jay Lee
c4deb29b21 Update build.yml 2022-05-31 12:20:50 -04:00
Jay Lee
0eafee3105 [no ci] upgrade test runs to Ubuntu 22.04 2022-05-31 11:54:13 -04:00
Ross Scroggs
4d116e81c8 Eliminate orgUnitPath encoding (#1527) 2022-05-31 11:24:37 -04:00
Ross Scroggs
3a41b31e19 Use i, not 6 (#1526) 2022-05-27 16:44:43 -04:00
Jay Lee
f9786468db Merge branch 'main' of https://github.com/GAM-team/GAM 2022-05-27 15:03:47 +00:00
Jay Lee
a49124d1d0 fix org_unit admin role assignment 2022-05-27 14:59:17 +00:00
Ross Scroggs
3087b4b776 Update pip version (#1525) 2022-05-26 09:58:22 -04:00
Jay Lee
aa595fe623 Update var.py 2022-05-26 08:54:27 -04:00
Jay Lee
30a571f56c Update build.yml 2022-05-25 21:39:15 -04:00
Jay Lee
7b0a8dce1e Update build.yml 2022-05-25 20:32:17 -04:00
Jay Lee
6fce941640 Update build.yml 2022-05-25 19:51:13 -04:00
Jay Lee
dbfbf61ddc Update build.yml 2022-05-25 19:18:51 -04:00
Jay Lee
5d618de296 Update build.yml 2022-05-25 17:01:00 -04:00
Ross Scroggs
3e556425ec New Oauth2 error message (#1524) 2022-05-25 16:29:18 -04:00
Jay Lee
370db726a4 Update build.yml 2022-05-21 16:46:28 -04:00
Jay Lee
12d6c8c3f5 Update build.yml 2022-05-21 16:10:52 -04:00
Jay Lee
0fdb6544cc Update build.yml 2022-05-21 16:04:52 -04:00
Jay Lee
ca52845c65 Update build.yml 2022-05-21 15:28:43 -04:00
Jay Lee
fac32f95d8 Update build.yml 2022-05-21 14:40:46 -04:00
Jay Lee
f27b37d21a Update build.yml 2022-05-21 09:09:21 -04:00
Jay Lee
e066fcc172 Make all step if conditions OS generic 2022-05-21 08:39:58 -04:00
Jay Lee
14535e814f [no ci] replace some OS context with generic OS 2022-05-21 08:14:56 -04:00
Jay Lee
1cfb873122 [no ci] undo commented out lines while troubleshooting 2022-05-21 07:47:22 -04:00
Jay Lee
de16285bf2 Update build.yml 2022-05-21 07:21:36 -04:00
Jay Lee
d01f3f31ff Update build.yml 2022-05-21 07:19:12 -04:00
Jay Lee
edec98579d Update build.yml 2022-05-20 22:10:00 -04:00
Jay Lee
ea583e98b2 Update build.yml 2022-05-20 21:39:04 -04:00
Jay Lee
c4a080e081 [no ci] document setuptools_scm issue 2022-05-20 21:31:32 -04:00
Jay Lee
4e35845ecf Update build.yml 2022-05-20 21:05:18 -04:00
Jay Lee
9d76fcbcf1 Update requirements.txt 2022-05-20 09:42:32 -04:00
Jay Lee
60290473e1 Update build.yml 2022-05-20 09:14:17 -04:00
Jay Lee
f94456ce7f Update setup.cfg 2022-05-19 19:16:53 -04:00
Jay Lee
65cda68f40 Update requirements.txt 2022-05-18 03:25:47 -04:00
Jay Lee
576731c386 Update build.yml 2022-05-17 21:22:29 -04:00
Jay Lee
0b56af76b7 Update build.yml 2022-05-17 20:46:23 -04:00
Ross Scroggs
10e6cbabcf Support displaying Chromebook OU ID (#1523) 2022-05-10 14:10:59 -04:00
Jay Lee
f33529a1d4 Update build.yml 2022-05-10 14:10:31 -04:00
Jay Lee
7a5a6a8db2 Update build.yml 2022-05-10 09:50:31 -04:00
Jay Lee
c43c4a8b07 Update build.yml 2022-05-10 08:08:13 -04:00
Jay Lee
0e0e1332c7 Update build.yml 2022-05-10 08:00:33 -04:00
Ross Scroggs
ee19c5e25f Multivalued schema files default to type work; update URLs (#1522)
* multivalued schema field defaults to type work;

* Update URLs

* Add new Product/SKU

* Revert to https://jaylee.us/gam
2022-05-08 19:42:39 -04:00
Ross Scroggs
c0c1355216 Handle missing type field in multivalued schema valus (#1520) 2022-05-04 12:41:35 -04:00
Jay Lee
da3394f3fd Update build.yml 2022-05-03 21:37:27 -04:00
Jay Lee
c856723945 Update var.py 2022-04-29 06:12:36 -04:00
Jay Lee
5e10ccdbdd Update __main__.py 2022-04-29 06:12:00 -04:00
Jay Lee
a49f645647 Update za-bug-report.md 2022-04-29 06:10:51 -04:00
Jay Lee
cf8d714ba7 Update ISSUE_TEMPLATE.txt 2022-04-29 06:10:13 -04:00
Jay Lee
17bb625d0d goodbye git.io :-/ 2022-04-29 06:08:51 -04:00
Ross Scroggs
5e7a858d55 Document delete teamdrive allowitemdeletion (#1517) 2022-04-24 08:00:06 -04:00
38 changed files with 2490 additions and 341 deletions

View File

@@ -1,8 +1,8 @@
The issue tracker is for reporting product deficiencies. "How do I?" questions should be posted to the discussion forum at https://groups.google.com/group/google-apps-manager. When in doubt, start at the discussion forum and return here only when instructed to do so.
Please confirm the following:
* I have upgraded to the latest GAM release from https://git.io/gamreleases and I still have this issue.
* I am typing the command as described in the GAM Wiki at https://github.com/jay0lee/gam/wiki
* I have upgraded to the latest GAM release from https://github.com/GAM-team/GAM/releases and I still have this issue.
* I am typing the command as described in the GAM Wiki at https://github.com/GAM-team/GAM/wiki
Full steps to reproduce the issue:
1.

View File

@@ -10,7 +10,7 @@ assignees: jay0lee
The issue tracker is for reporting product deficiencies. "How do I?" questions should be posted to the discussion forum at https://groups.google.com/group/google-apps-manager. When in doubt, start at the discussion forum and return here only when instructed to do so.
Please confirm the following:
* I have upgraded to the latest GAM release from https://git.io/gamreleases and I still have this issue.
* I have upgraded to the latest GAM release from https://github.com/GAM-team/GAM/releases and I still have this issue.
* I am typing the command as described in the GAM Wiki at https://github.com/jay0lee/gam/wiki
Full steps to reproduce the issue:

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

@@ -24,22 +24,17 @@ jobs:
strategy:
matrix:
include:
- os: ubuntu-20.04
- os: ubuntu-22.04
jid: 1
goal: build
arch: x86_64
openssl_archs: linux-x86_64
- os: [self-hosted, linux, arm64]
- os: [self-hosted, linux, arm64, gcp]
jid: 2
goal: build
arch: aarch64
openssl_archs: linux-aarch64
- os: [self-hosted, linux, arm]
jid: 3
goal: build
arch: armv7l
openssl_archs: linux-armv4
- os: macos-11
- os: macos-12
jid: 4
goal: build
arch: universal2
@@ -54,40 +49,53 @@ jobs:
goal: build
arch: Win32
openssl_archs: VC-WIN32
- os: ubuntu-20.04
- os: ubuntu-22.04
goal: test
python: "3.7"
jid: 7
arch: x86_64
- os: ubuntu-20.04
- os: ubuntu-22.04
goal: test
python: "3.8"
jid: 8
arch: x86_64
- os: ubuntu-20.04
- os: ubuntu-22.04
goal: test
python: "3.9"
jid: 9
arch: x86_64
- os: ubuntu-22.04
goal: test
python: "3.10"
jid: 10
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 }}-20220328-01
bin.tar.xz
src/cpython
key: gam-${{ matrix.jid }}-20221101
- 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 }}
@@ -111,21 +119,21 @@ jobs:
echo "ACTIONS_CACHE=${ACTIONS_CACHE}" >> $GITHUB_ENV
echo "ACTIONS_GOAL=${ACTIONS_GOAL}" >> $GITHUB_ENV
- name: Install necessary hosted Linux packages
if: matrix.os == 'ubuntu-20.04'
- name: Install necessary Github-hosted Linux packages
if: runner.os == 'Linux' && runner.arch == 'X64'
run: |
echo "RUNNING: apt update..."
sudo apt-get -qq --yes update
sudo apt-get -qq --yes install swig libpcsclite-dev
- name: MacOS remove Homebrew
if: matrix.os == 'macos-11'
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: matrix.os == 'macos-11'
if: runner.os == 'macOS'
run: |
# Install latest Rust
curl -fsS -o rust.sh https://sh.rustup.rs
@@ -136,7 +144,7 @@ jobs:
- name: Windows Configure VCode
uses: ilammy/msvc-dev-cmd@v1
if: matrix.os == 'windows-2022' && steps.cache-python-ssl.outputs.cache-hit != 'true'
if: runner.os == 'Windows' && steps.cache-python-ssl.outputs.cache-hit != 'true'
with:
arch: ${{ matrix.arch }}
@@ -148,6 +156,7 @@ jobs:
openssl_archs: ${{ matrix.openssl_archs }}
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"
@@ -179,21 +188,21 @@ 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 "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'
@@ -220,7 +229,7 @@ jobs:
- name: Windows NASM Install
uses: ilammy/setup-nasm@v1
if: matrix.goal == 'build' && matrix.os == 'windows-2022' && steps.cache-python-ssl.outputs.cache-hit != 'true'
if: matrix.goal == 'build' && runner.os == 'Windows' && steps.cache-python-ssl.outputs.cache-hit != 'true'
- name: Config OpenSSL
if: matrix.goal == 'build' && steps.cache-python-ssl.outputs.cache-hit != 'true'
@@ -232,7 +241,7 @@ jobs:
done
- name: Rename GNU link on Windows
if: matrix.goal == 'build' && matrix.os == 'windows-2022' && steps.cache-python-ssl.outputs.cache-hit != 'true'
if: matrix.goal == 'build' && runner.os == 'Windows' && steps.cache-python-ssl.outputs.cache-hit != 'true'
shell: bash
run: mv /usr/bin/link /usr/bin/gnulink
@@ -295,7 +304,7 @@ jobs:
echo "COMPILED_PYTHON_VERSION=${COMPILED_PYTHON_VERSION}" >> $GITHUB_ENV
- name: Mac/Linux Configure Python
if: matrix.goal == 'build' && matrix.os != 'windows-2022' && steps.cache-python-ssl.outputs.cache-hit != 'true'
if: matrix.goal == 'build' && runner.os != 'Windows' && steps.cache-python-ssl.outputs.cache-hit != 'true'
run: |
cd "${PYTHON_SOURCE_PATH}"
if [[ "${RUNNER_OS}" == "macOS" ]]; then
@@ -312,14 +321,14 @@ jobs:
"${extra_args[@]}"
- name: Windows Get External Python deps
if: matrix.goal == 'build' && matrix.os == 'windows-2022' && steps.cache-python-ssl.outputs.cache-hit != 'true'
if: matrix.goal == 'build' && runner.os == 'Windows' && steps.cache-python-ssl.outputs.cache-hit != 'true'
shell: powershell
run: |
cd "${env:PYTHON_SOURCE_PATH}"
PCBuild\get_externals.bat
- name: Windows overwrite external OpenSSL with local
if: matrix.goal == 'build' && matrix.os == 'windows-2022' && steps.cache-python-ssl.outputs.cache-hit != 'true'
if: matrix.goal == 'build' && runner.os == 'Windows' && steps.cache-python-ssl.outputs.cache-hit != 'true'
shell: powershell
run: |
cd "${env:PYTHON_SOURCE_PATH}"
@@ -331,14 +340,14 @@ 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\"
cp -v "$env:OPENSSL_INSTALL_PATH\include\openssl\applink.c" "${env:OPENSSL_EXT_TARGET_PATH}\include\"
- name: Windows Install sphinx-build
if: matrix.goal == 'build' && matrix.os == 'windows-2022' && steps.cache-python-ssl.outputs.cache-hit != 'true'
if: matrix.goal == 'build' && runner.os == 'Windows' && steps.cache-python-ssl.outputs.cache-hit != 'true'
shell: powershell
run: |
pip install --upgrade pip
@@ -346,40 +355,31 @@ jobs:
sphinx-build --version
- name: Windows Config/Build Python
if: matrix.goal == 'build' && matrix.os == 'windows-2022' && steps.cache-python-ssl.outputs.cache-hit != 'true'
if: matrix.goal == 'build' && runner.os == 'Windows' && steps.cache-python-ssl.outputs.cache-hit != 'true'
shell: powershell
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' && matrix.os == 'windows-2022' && 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' && matrix.os != 'windows-2022' && steps.cache-python-ssl.outputs.cache-hit != 'true'
if: matrix.goal == 'build' && runner.os != 'Windows' && steps.cache-python-ssl.outputs.cache-hit != 'true'
run: |
cd "${PYTHON_SOURCE_PATH}"
echo "Running: ${MAKE} ${MAKEOPT}"
$MAKE $MAKEOPT
- name: Mac/Linux Install Python
if: matrix.goal == 'build' && matrix.os != 'windows-2022' && steps.cache-python-ssl.outputs.cache-hit != 'true'
if: matrix.goal == 'build' && runner.os != 'Windows' && steps.cache-python-ssl.outputs.cache-hit != 'true'
run: |
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: |
@@ -392,7 +392,22 @@ jobs:
"${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: |
@@ -408,28 +423,9 @@ jobs:
fi
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: |
set +e
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'
@@ -438,6 +434,10 @@ jobs:
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
@@ -447,6 +447,16 @@ jobs:
echo -e "GAM: ${gam}\nGAMPATH: ${gampath}"
"${PYTHON}" -m PyInstaller --clean --distpath="${gampath}" 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
@@ -456,10 +466,8 @@ jobs:
echo "GAMVERSION=${GAMVERSION}" >> $GITHUB_ENV
- name: Linux/MacOS package
if: matrix.os != 'windows-2022' && matrix.goal == 'build'
if: runner.os != 'Windows' && matrix.goal == 'build'
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
@@ -468,35 +476,41 @@ jobs:
fi
tar -C dist/ --create --verbose --exclude-from "${GITHUB_WORKSPACE}/.github/actions/package_exclusions.txt" --file $GAM_ARCHIVE --xz gam
- name: Linux install patchelf/staticx
if: matrix.os == 'ubuntu-20.04' && matrix.goal != 'test'
- name: Linux 64-bit install patchelf/staticx
if: runner.os == 'Linux' && contains(runner.arch, '64') && matrix.goal != 'test'
run: |
"${PYTHON}" -m pip install --upgrade patchelf-wrapper
"${PYTHON}" -m pip install --upgrade staticx
- name: Linux Make Static
if: matrix.os == 'ubuntu-20.04' && matrix.goal != 'test'
- name: Linux 64-bit Make Static
if: runner.os == 'Linux' && contains(runner.arch, '64') && matrix.goal != 'test'
run: |
$PYTHON -m staticx "${gam}" "${gam}-staticx"
case $RUNNER_ARCH in
X64)
ldlib=/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
;;
ARM64)
ldlib=/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1
;;
esac
echo "ldlib=${ldlib}"
$PYTHON -m staticx -l "${ldlib}" "${gam}" "${gam}-staticx"
- name: Linux Run StaticX-ed
if: matrix.os == 'ubuntu-20.04' && matrix.goal != 'test'
if: runner.os == 'Linux' && contains(runner.arch, '64') && matrix.goal != 'test'
run: |
"${gam}-staticx" version extended
mv -v "${gam}-staticx" "${gam}"
- name: Linux package staticx
if: matrix.os == 'ubuntu-20.04' && matrix.goal != 'test'
if: runner.os == 'Linux' && contains(runner.arch, '64') && matrix.goal != 'test'
run: |
GAM_ARCHIVE="gam-${GAMVERSION}-linux-x86_64-legacy.tar.xz"
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
- name: Windows package
if: matrix.os == 'windows-2022' && matrix.goal != 'test'
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
@@ -533,29 +547,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
@@ -568,9 +596,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"
@@ -583,8 +611,8 @@ jobs:
$gam user $newuser imap on
$gam user $newuser show imap
$gam user $newuser show delegates
$gam user $newuser add contactdelegate "${newbase}-bulkuser-1"
$gam user $newuser print contactdelegates
#$gam user $newuser add contactdelegate "${newbase}-bulkuser-1"
#$gam user $newuser print contactdelegates
export biohazard=$(echo -e '\xe2\x98\xa3')
$gam user $newuser label "$biohazard unicode biohazard $biohazard"
$gam user $newuser show labels
@@ -600,8 +628,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
@@ -621,7 +649,7 @@ jobs:
$gam print vaultmatters matterstate open
$gam print vaultholds matter $matterid
$gam print vaultcount matter $matterid corpus mail everyone todrive
$gam create vaultexport matter $matterid name "GHA export $newbase" corpus mail accounts $newuser use_new_export true
$gam create vaultexport matter $matterid name "GHA export $newbase" corpus mail accounts $newuser use_new_export false
$gam print exports matter $matterid | $gam csv - gam info export $matterid id:~~id~~
$gam csv sample.csv gam user ~email add calendar id:$newresource
$gam delete resource $newresource
@@ -637,8 +665,8 @@ 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 delete user $newuser
$gam print users query "gha.jid=$JID" | $gam csv - gam delete user ~primaryEmail
$gam print mobile
@@ -662,11 +690,16 @@ 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
$gam create inboundssoprofile name "El Goog ${newbase}" loginurl https://www.google.com logouturl https://www.google.com changepasswordurl https://www.google.com entityid ElGoog
$gam create inboundssocredential profile "El Goog ${newbase}" generate_key
$gam create inboundssoassignment profile "El Goog ${newbase}" orgunit "${newou}" mode SAML_SSO
$gam delete ou "${newou}"
#$gam print printers
#$gam create printer displayname "${newbase}" uri ipp://localhost:631 driverless description "made by $(gam_user)" ou /
#export CUSTOMER_ID="C01wfv983"
@@ -675,23 +708,49 @@ jobs:
#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
- 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

@@ -9,7 +9,7 @@ GAM is a command line tool for Google Workspace admins to manage domain and user
Open a terminal and run:
```sh
bash <(curl -s -S -L https://git.io/install-gam)
bash <(curl -s -S -L https://gam-shortn.appspot.com/gam-install)
```
this will download GAM, install it and start setup.
@@ -28,13 +28,13 @@ The GAM mailing list / discussion group is hosted on [Google Groups]. You can j
# Chat Room
There is a public chat room hosted in Google Chat. [Instructions to join](https://git.io/gam-chat).
There is a public chat room hosted in Google Chat. [Instructions to join](https://github.com/GAM-team/GAM/wiki/GAM-Public-Chat-Room).
# Author
GAM is maintained by [Jay Lee](mailto:jay0lee@gmail.com). Please direct "how do I?" questions to [Google Groups].
[GAM release]: https://git.io/gamreleases
[GAM release]: https://github.com/GAM-team/GAM/releases
[GitHub Releases]: https://github.com/GAM-team/GAM/releases
[GitHub]: https://github.com/GAM-team/GAM/tree/master
[GitHub Wiki]: https://github.com/GAM-team/GAM/wiki/

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
@@ -682,12 +683,19 @@ Specify a collection of Users by directly specifying them or by specifying items
## Item attributes
<BuildingAttribute> ::=
(address|addresslines <String>)|
(city|locality <String>)|
(country|regioncode <String>)|
(description <String>)|
(floors <FloorNameList>)|
(id <String>)|
(language|languageCode <Language>)|
(latitude <Float>)|
(longitude <Float>)|
(name <String>)
(state|administrativearea <String>)|
(sublocality <String>)|
(zipcode|postalcode <String>)
<CalendarAttribute> ::=
(selected <Boolean>)|(hidden <Boolean>)|(summary <String>)|(colorindex|colorid <CalendarColorIndex>)|(backgroundcolor <ColorValue>)|(foregroundcolor <ColorValue>)|
@@ -864,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
@@ -1059,9 +1068,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]
@@ -1296,7 +1305,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]
@@ -1314,7 +1325,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
@@ -1322,7 +1333,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.
@@ -1336,7 +1347,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.
@@ -1537,6 +1548,42 @@ 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>]
gam update inboundssoprofile <SSOProfileItem>
[entityid <String>] [loginurl <URL>] [logouturl <URL>] [changepasswordurl <URL>]
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>
@@ -1839,7 +1886,7 @@ gam <UserTypeEntity> update teamdrive <TeamDriveID>|(name <TeamDriveName>) [asad
[(theme|themeid <String>) | ([customtheme <DriveFileID> <Float> <Float> <Float>] [color <ColorValue>])]
(<TeamDriveRestrictionsSubfieldName> <Boolean>)*
[hidden <Boolean>]
gam <UserTypeEntity> delete teamdrive <TeamDriveID>|(name <TeamDriveName>)
gam <UserTypeEntity> delete teamdrive <TeamDriveID>|(name <TeamDriveName>) [asadmin] [allowitemdeletion|nukefromorbit]
gam <UserTypeEntity> show teamdriveinfo <TeamDriveID>|(name <TeamDriveName>) [asadmin]
gam <UserTypeEntity> show teamdrives [query <QueryTeamDrive>] [asadmin]
gam <UserTypeEntity> print teamdrives [query <QueryTeamDrive>] [todrive] [asadmin]

View File

@@ -28,7 +28,7 @@ upgrade_only=false
gamversion="latest"
adminuser=""
regularuser=""
gam_glibc_vers="2.31"
gam_glibc_vers="2.35"
while getopts "hd:a:o:b:lp:u:r:v:" OPTION
do
@@ -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

@@ -5,9 +5,7 @@ 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', '.')]

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

@@ -65,6 +65,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 +554,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 +635,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 +724,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 +754,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.gethostbyname(host)
check_line = f'Checking {host} ({ip}) ({try_count}/{host_count})...'
sys.stdout.write(f'{check_line:<80}')
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'
@@ -2247,6 +2321,7 @@ def doGetCourseInfo():
COURSE_ARGUMENT_TO_PROPERTY_MAP = {
'alternatelink': 'alternateLink',
'calendarid': 'calendarId',
'coursegroupemail': 'courseGroupEmail',
'coursematerialsets': 'courseMaterialSets',
'coursestate': 'courseState',
@@ -2254,7 +2329,9 @@ COURSE_ARGUMENT_TO_PROPERTY_MAP = {
'description': 'description',
'descriptionheading': 'descriptionHeading',
'enrollmentcode': 'enrollmentCode',
'gradebooksettings': 'gradebookSettings',
'guardiansenabled': 'guardiansEnabled',
'heading': 'descriptionHeading',
'id': 'id',
'name': 'name',
'ownerid': 'ownerId',
@@ -6501,6 +6578,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)
@@ -7014,6 +7100,8 @@ def getUserAttributes(i, cd, updateCmd):
if schemaValue['type'] == 'custom':
schemaValue['customType'] = sys.argv[i]
i += 1
else:
schemaValue['type'] = 'work'
schemaValue['value'] = sys.argv[i]
if schemaValue['value'] or multivalue != 'multinonempty':
body[up][schemaName][fieldName].append(schemaValue)
@@ -7055,7 +7143,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
@@ -7211,7 +7299,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'
}
@@ -7553,7 +7641,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:
@@ -7612,7 +7700,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:
@@ -7648,7 +7736,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,
@@ -7692,6 +7780,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()
@@ -7852,14 +7942,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)
@@ -8167,7 +8256,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('_', '')
@@ -8189,7 +8278,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)
@@ -8241,6 +8330,9 @@ def doDeleteSharedDrive(users):
allowItemDeletion = True
useDomainAdminAccess = True
i += 1
elif myarg == 'asadmin':
useDomainAdminAccess = True
i += 1
else:
controlflow.invalid_argument_exit(
myarg, 'gam delete shareddrive')
@@ -8855,10 +8947,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:
@@ -9042,8 +9140,9 @@ def doGetUserInfo(user_email=None):
if isinstance(user['customSchemas'][schema][field], list):
print(f' {field}:')
for an_item in user['customSchemas'][schema][field]:
print(f' type: {an_item["type"]}')
if an_item['type'] == 'custom':
an_type = an_item.get('type', 'work')
print(f' type: {an_type}')
if an_type == 'custom':
print(
f' customType: {an_item["customType"]}')
print(f' value: {an_item["value"]}')
@@ -9402,7 +9501,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
@@ -9565,6 +9664,7 @@ USER_ARGUMENT_TO_PROPERTY_MAP = {
'changepasswordatnextlogin': ['changePasswordAtNextLogin',],
'creationtime': ['creationTime',],
'deletiontime': ['deletionTime',],
'displayname': ['displayName',],
'email': ['emails',],
'emails': ['emails',],
'externalid': ['externalIds',],
@@ -9606,6 +9706,7 @@ USER_ARGUMENT_TO_PROPERTY_MAP = {
'location': ['locations',],
'locations': ['locations',],
'name': [
'name.displayName',
'name.givenName',
'name.familyName',
'name.fullName',
@@ -10103,7 +10204,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']:
@@ -10288,8 +10389,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)
@@ -10306,9 +10412,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:
@@ -10459,7 +10567,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)
@@ -10501,6 +10609,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'],
@@ -10510,7 +10623,6 @@ OAUTH2_SCOPES = [
'name': 'Cloud Identity - User Invitations',
'subscopes': ['readonly'],
'scopes': 'https://www.googleapis.com/auth/cloud-identity.userinvitations',
'offByDefault': True,
},
{
'name': 'Contact Delegation',
@@ -11158,7 +11270,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
@@ -11331,6 +11443,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(
@@ -11367,6 +11481,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':
@@ -11377,7 +11494,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()
@@ -11449,10 +11572,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':
@@ -11514,7 +11641,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()
@@ -11589,8 +11720,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':
@@ -11682,6 +11817,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':
@@ -11782,6 +11923,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']:

View File

@@ -18,7 +18,7 @@
"""GAM is a command line tool which allows Administrators to control their Google Workspace domain and accounts.
With GAM you can programmatically create users, turn on/off services for users like POP and Forwarding and much more.
For more information, see https://git.io/gam
For more information, see https://jaylee.us/gam
"""
import sys

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:

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

@@ -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:

View File

@@ -1,9 +1,12 @@
import gam
from gam.var import GC_Values, GC_ENABLE_DASA
def build(api='cloudidentity'):
return gam.buildGAPIObject(api)
def build_dwd(api='cloudidentity'):
# If we are using DASA we don't need to use DwD.
if GC_Values[GC_ENABLE_DASA]:
return gam.buildGAPIObject(api)
admin = gam._get_admin_email()
return gam.buildGAPIServiceObject(api, admin, True)

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)
@@ -253,6 +253,13 @@ def update_state():
def print_():
# This function is rather messy thanks to
# https://github.com/GAM-team/GAM/issues/1534
# I'd prefer to keep it all in this function for now but if:
# - we find other list() operations that also hit this bug OR
# - it looks like this issue is going to exist on Google's side
# for a long time.
# I'll enterain some cleanup here to "functionalize" (yuck) all of this.
ci = gapi_cloudidentity.build_dwd()
customer = _get_device_customerid()
parent = 'devices/-'
@@ -260,6 +267,8 @@ def print_():
get_device_users = True
view = None
orderByList = []
# default sort order needed by our 1 hour bug workaround
orderBy = 'create_time'
titles = []
csvRows = []
todrive = False
@@ -319,26 +328,97 @@ def print_():
}
if orderByList:
orderBy = ','.join(orderByList)
else:
orderBy = None
devices = []
custom_device_filter = bool(device_filter)
# we store the devices in a dict keyed by name which is a unique ID.
# that way when we get duplicate devices we just overwrite the name
# with the latest copy we saw.
devices = {}
page_message = gapi.got_total_items_msg(view_name_map[view], '...\n')
devices += gapi.get_all_pages(ci.devices(), 'list', 'devices',
customer=customer, page_message=page_message,
pageSize=100, filter=device_filter, view=view, orderBy=orderBy)
pageToken = None
newest_device_date = ''
total_items = 0
while True:
try:
a_page = gapi.call(ci.devices(),
'list',
customer=customer,
pageSize=100,
pageToken=pageToken,
filter=device_filter,
view=view,
orderBy=orderBy,
throw_reasons=[gapi_errors.ErrorReason.FOUR_O_O])
except googleapiclient.errors.HttpError:
sys.stderr.write('WARNING: GAM hit Google internal bug 237397223. Please file a Google Support ticket stating that you are encountering this bug.\n')
if orderBy != 'create_time' or custom_device_filter:
controlflow.system_error_exit(5, 'GAM workaround for this issue only works if filter and orderby arguments are not used.\n')
sys.stderr.write(f' attempting to work around the bug by filtering for devices created on or after the newest we\'ve seen ({newest_device_date})...')
device_filter = f'register:{newest_device_date}..'
pageToken = None
continue
for dev in a_page.get('devices', []):
total_items += 1
devices[dev['name']] = dev
dev_date = dev.get('createTime', '')
# remove the Z
dev_date = dev_date[:-1]
# remove microseconds
dev_date = dev_date.split('.')[0]
if dev_date > newest_device_date:
newest_device_date = dev_date
pageToken = a_page.get('nextPageToken')
if not pageToken:
break
sys.stderr.write(page_message.replace('%%total_items%%', str(total_items)))
if get_device_users:
page_message = gapi.got_total_items_msg('Device Users', '...\n')
device_users = gapi.get_all_pages(ci.devices().deviceUsers(), 'list',
'deviceUsers', customer=customer, parent=parent,
page_message=page_message, pageSize=20, filter=device_filter)
for device_user in device_users:
for device in devices:
if device_user.get('name').startswith(device.get('name')):
if 'users' not in device:
device['users'] = []
device['users'].append(device_user)
break
for device in devices:
pageToken = None
newest_deviceuser_date = ''
total_items = 0
device_users = {}
if not custom_device_filter:
device_filter = None
while True:
try:
a_page = gapi.call(ci.devices().deviceUsers(),
'list',
customer=customer,
parent=parent,
pageSize=20,
orderBy=orderBy,
filter=device_filter,
pageToken=pageToken,
throw_reasons=[gapi_errors.ErrorReason.FOUR_O_O])
except googleapiclient.errors.HttpError:
sys.stderr.write('WARNING: GAM hit Google internal bug 237397223. Please file a Google Support ticket stating that you are encountering this bug.\n')
if orderBy != 'create_time' or custom_device_filter:
controlflow.system_error_exit(5, 'GAM workaround for this issue only works if filter and orderby arguments are not used.\n')
sys.stderr.write(f' attempting to work around the bug by filtering for device users created on or after the newest we\'ve seen ({newest_deviceuser_date})...')
device_filter = f'register:{newest_deviceuser_date}..'
pageToken = None
continue
for device_user in a_page.get('deviceUsers', []):
total_items += 1
dev_date = device_user.get('createTime', '')
# remove the Z
dev_date = dev_date[:-1]
# remove microseconds
dev_date = dev_date.split('.')[0]
if dev_date > newest_deviceuser_date:
newest_deviceuser_date = dev_date
deviceuser_name = device_user['name']
device_users[deviceuser_name] = device_user
pageToken = a_page.get('nextPageToken')
if not pageToken:
break
sys.stderr.write(page_message.replace('%%total_items%%', str(total_items)))
for deviceuser_name, device_user in device_users.items():
device_id = deviceuser_name.split('/')[1]
device_name = f'devices/{device_id}'
if 'users' not in devices[device_name]:
devices[device_name]['users'] = []
devices[device_name]['users'].append(device_user)
for device in devices.values():
device = utils.flatten_json(device)
for a_key in device:
if a_key not in titles:

View File

@@ -935,6 +935,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,527 @@
"""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):
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 == '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 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'
}
body = parse_profile(body, 3)
result = gapi.call(ci.inboundSamlSsoProfiles(),
'create',
body=body)
if result.get('done'):
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 = {}
body = parse_profile(body, 4)
updateMask = ','.join(body.keys())
result = gapi.call(ci.inboundSamlSsoProfiles(),
'patch',
name=name,
updateMask=updateMask,
body=body)
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:
@@ -304,6 +311,8 @@ def doGetCrosInfo():
if 'autoUpdateExpiration' in cros:
cros['autoUpdateExpiration'] = utils.formatTimestampYMD(
cros['autoUpdateExpiration'])
if 'orgUnitId' in cros:
cros['orgUnitId'] = f"id:{cros['orgUnitId']}"
_checkTPMVulnerability(cros)
for up in CROS_SCALAR_PROPERTY_PRINT_ORDER:
if up in cros:
@@ -446,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):
@@ -454,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
@@ -522,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']
@@ -610,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 = {}
@@ -622,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
@@ -727,6 +739,7 @@ def doPrintCrosDevices():
customerId=GC_Values[GC_CUSTOMER_ID],
projection=projection,
orgUnitPath=orgUnitPath,
includeChildOrgunits=includeChildOrgunits,
orderBy=orderBy,
sortOrder=sortOrder,
fields=fields)
@@ -739,6 +752,8 @@ def doPrintCrosDevices():
if 'autoUpdateExpiration' in cros:
cros['autoUpdateExpiration'] = utils.formatTimestampYMD(
cros['autoUpdateExpiration'])
if 'orgUnitId' in cros:
cros['orgUnitId'] = f"id:{cros['orgUnitId']}"
for cpuStatusReport in cros.get('cpuStatusReports', []):
tempInfos = cpuStatusReport.get('cpuTemperatureInfo', [])
for tempInfo in tempInfos:
@@ -753,6 +768,8 @@ def doPrintCrosDevices():
if 'autoUpdateExpiration' in cros:
cros['autoUpdateExpiration'] = utils.formatTimestampYMD(
cros['autoUpdateExpiration'])
if 'orgUnitId' in cros:
cros['orgUnitId'] = f"id:{cros['orgUnitId']}"
row = {}
for attrib in cros:
if attrib not in {

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

@@ -373,17 +373,20 @@ def makeOrgUnitPathRelative(path):
def encodeOrgUnitPath(path):
if path.find('+') == -1 and path.find('%') == -1:
return path
encpath = ''
for c in path:
if c == '+':
encpath += '%2B'
elif c == '%':
encpath += '%25'
else:
encpath += c
return encpath
# 6.22 - This method no longer works.
# % no longer needs encoding and + is handled incorrectly in API with or without encoding
return path
# if path.find('+') == -1 and path.find('%') == -1:
# return path
# encpath = ''
# for c in path:
# if c == '+':
# encpath += '%2B'
# elif c == '%':
# encpath += '%25'
# else:
# encpath += c
# return encpath
def getOrgUnitItem(orgUnit, pathOnly=False, absolutePath=True):

View File

@@ -227,6 +227,22 @@ def printFeatures():
display.write_csv_file(csvRows, titles, 'Features', to_drive)
BUILDING_ADDRESS_FIELD_MAP = {
'address': 'addressLines',
'addresslines': 'addressLines',
'administrativearea': 'administrativeArea',
'city': 'locality',
'country': 'regionCode',
'language': 'languageCode',
'languagecode': 'languageCode',
'locality': 'locality',
'postalcode': 'postalCode',
'regioncode': 'regionCode',
'state': 'administrativeArea',
'sublocality': 'sublocality',
'zipcode': 'postalCode',
}
def _getBuildingAttributes(args, body={}):
i = 0
while i < len(args):
@@ -253,6 +269,16 @@ def _getBuildingAttributes(args, body={}):
elif myarg == 'floors':
body['floorNames'] = args[i + 1].split(',')
i += 2
elif myarg in BUILDING_ADDRESS_FIELD_MAP:
myarg = BUILDING_ADDRESS_FIELD_MAP[myarg]
body.setdefault('address', {})
if myarg == 'addressLines':
body['address'][myarg] = args[i + 1].split('\n')
elif myarg == 'languageCode':
body['address'][myarg] = LANGUAGE_CODES_MAP.get(args[i + 1].lower(), args[i + 1])
else:
body['address'][myarg] = args[i + 1]
i += 2
else:
controlflow.invalid_argument_exit(myarg,
'gam create|update building')

View File

@@ -21,6 +21,18 @@ def create():
body['roleId'] = gapi_directory_roles.getRoleId(role)
body['scopeType'] = sys.argv[5].upper()
i = 6
if body['scopeType'] not in ['CUSTOMER', 'ORG_UNIT']:
controlflow.expected_argument_exit('scope type',
', '.join(['customer', 'org_unit']),
body['scopeType'])
if body['scopeType'] == 'ORG_UNIT':
orgUnit, orgUnitId = gapi_directory_orgunits.getOrgUnitId(
sys.argv[i], cd)
body['orgUnitId'] = orgUnitId[3:]
scope = f'ORG_UNIT {orgUnit}'
i = 7
else:
scope = 'CUSTOMER'
while i < len(sys.argv):
myarg = sys.argv[i].lower()
if myarg == 'condition':
@@ -33,17 +45,6 @@ def create():
i += 2
else:
controlflow.invalid_argument_exit(sys.argv[i], 'gam create admin')
if body['scopeType'] not in ['CUSTOMER', 'ORG_UNIT']:
controlflow.expected_argument_exit('scope type',
', '.join(['customer', 'org_unit']),
body['scopeType'])
if body['scopeType'] == 'ORG_UNIT':
orgUnit, orgUnitId = gapi_directory_orgunits.getOrgUnitId(
sys.argv[6], cd)
body['orgUnitId'] = orgUnitId[3:]
scope = f'ORG_UNIT {orgUnit}'
else:
scope = 'CUSTOMER'
print(f'Giving {user} admin role {role} for {scope}')
gapi.call(cd.roleAssignments(),
'insert',

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

@@ -233,6 +233,7 @@ ERROR_REASON_TO_EXCEPTION = {
OAUTH2_TOKEN_ERRORS = [
'access_denied',
'access_denied: Requested client not authorized',
'access_denied: Account restricted',
'internal_failure: Backend Error',
'internal_failure: None',
'invalid_grant',

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

@@ -11,6 +11,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
@@ -976,10 +977,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

@@ -8,10 +8,10 @@ import platform
import re
GAM_AUTHOR = 'Jay Lee <jay0lee@gmail.com>'
GAM_VERSION = '6.21'
GAM_VERSION = '6.31'
GAM_LICENSE = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
GAM_URL = 'https://git.io/gam'
GAM_URL = 'https://jaylee.us/gam'
GAM_INFO = (
f'GAM {GAM_VERSION} - {GAM_URL} / {GAM_AUTHOR} / '
f'Python {platform.python_version()} {sys.version_info.releaselevel} / '
@@ -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: '
@@ -116,6 +117,11 @@ SKUS = {
'aliases': ['gvpremier', 'voicepremier', 'googlevoicepremier'],
'displayName': 'Google Voice Premier'
},
'1010360001': {
'product': '101036',
'aliases': ['meetdialing','googlemeetglobaldialing'],
'displayName': 'Google Meet Global Dialing'
},
'1010370001': {
'product': '101037',
'aliases': ['gwetlu', 'workspaceeducationupgrade'],
@@ -200,7 +206,7 @@ SKUS = {
},
'1010020030': {
'product': 'Google-Apps',
'aliases': ['workspacefrontline', 'workspacefrontlineworker'],
'aliases': ['wsflw', 'workspacefrontline', 'workspacefrontlineworker'],
'displayName': 'Workspace Frontline'
},
'1010340002': {
@@ -282,6 +288,7 @@ PRODUCTID_NAME_MAPPINGS = {
'101033': 'Google Voice',
'101034': 'G Suite Archived',
'101035': 'Cloud Search',
'101036': 'Google Meet Global Dialing',
'101037': 'G Suite Workspace for Education',
'Google-Apps': 'Google Workspace',
'Google-Chrome-Device-Management': 'Google Chrome Device Management',
@@ -641,6 +648,7 @@ MACOS_CODENAMES = {
},
11: 'Big Sur',
12: 'Monterey',
13: 'Ventura',
}
_MICROSOFT_FORMATS_LIST = [{
@@ -982,6 +990,7 @@ CROS_ARGUMENT_TO_PROPERTY_MAP = {
'notes': ['notes',],
'ordernumber': ['orderNumber',],
'org': ['orgUnitPath',],
'orgunitid': ['orgUnitId',],
'orgunitpath': ['orgUnitPath',],
'osversion': ['osVersion',],
'ou': ['orgUnitPath',],
@@ -1007,6 +1016,7 @@ CROS_BASIC_FIELDS_LIST = [
]
CROS_SCALAR_PROPERTY_PRINT_ORDER = [
'orgUnitId',
'orgUnitPath',
'annotatedAssetId',
'annotatedLocation',
@@ -1193,6 +1203,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,
@@ -1260,6 +1271,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.
@@ -1315,6 +1329,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,
@@ -1334,7 +1349,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 = {}
@@ -1399,6 +1414,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
},

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.20
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