Compare commits

..

84 Commits
v5.10 ... v5.20

Author SHA1 Message Date
Jay Lee
31f83d33f5 Merge branch 'master' of https://github.com/jay0lee/GAM 2020-09-18 07:38:45 -04:00
Ross Scroggs
597256d048 Ensure that customer_id, domain and an admin email address are present for DASA (#1242)
* Ensure that customer_id, domain and an admin email address are present for DASA

* Fix typos
2020-09-18 07:38:38 -04:00
Jay Lee
62594a2898 add id to device delete command 2020-09-17 08:16:05 -04:00
Jay Lee
00582d486c serial not serialnumber for filter 2020-09-17 08:04:18 -04:00
Jay Lee
cda626b01c minor fixes to devices, more travis testing 2020-09-17 07:55:07 -04:00
Jay Lee
7d84da1520 GAM 5.20 2020-09-16 16:54:33 -04:00
Jay Lee
11b96b488f label xcode universal as testing in travis 2020-09-16 16:48:40 -04:00
Jay Lee
1853c0ca32 Update .travis.yml 2020-09-16 16:09:19 -04:00
Jay Lee
0b8fb177c4 Update osx-install.sh 2020-09-16 16:04:57 -04:00
Jay Lee
4e80434956 simple print test for devices 2020-09-16 15:35:08 -04:00
Jay Lee
c2f53577ab Cloud Identity Devices API, 5.20 2020-09-16 15:33:55 -04:00
Jay Lee
4974150357 Update .travis.yml 2020-09-16 08:12:43 -04:00
Jay Lee
1586d97295 Update .travis.yml 2020-09-15 18:02:16 -04:00
Jay Lee
5f65898c33 fix travis paths 2020-09-15 12:11:53 -04:00
Jay Lee
88e7941db3 fix travis creds 2020-09-15 11:48:31 -04:00
Jay Lee
6c715263e0 svars-write 2020-09-15 11:46:31 -04:00
Jay Lee
7088962d44 redo travis auth setup 2020-09-15 11:40:46 -04:00
Jay Lee
429bb0957d Update svars-write.py 2020-09-15 09:16:54 -04:00
Jay Lee
424fda55dd move oauth info earlier 2020-09-14 14:20:17 -04:00
Ross Scroggs
1b26a11281 Complete adminrole commands; add signout and turnoff2sv (#1237)
* Complete create admintole

* Add update/delete to adminroles

* Update privileges options

* Separate create/update adminrole

* Sdd signout/turnoff2sv commands

* Move signout, turn_off_2sv to new users.py
2020-09-14 12:58:52 -04:00
Jay Lee
56f52c8623 Update .travis.yml 2020-09-14 11:47:07 -04:00
Ross Scroggs
908edff878 Fix error; DASA suggestions (#1236)
* userKey and customer is an invalid combination; userkey and domain is allowed

* DASA suggestions

I would not use OAUTHFILE to distinguish between normal/DASA, it seems to me that this might lead to oauth2service.json getting deleted by accident.

By using enabledasa.txt you can flip between the two modes easily.

* Update __init__.py

Is this what yuou meant?
2020-09-11 11:33:06 -04:00
Jay Lee
487e1dc4c1 Merge branch 'master' of https://github.com/jay0lee/GAM 2020-09-10 11:36:16 -04:00
Jay Lee
244398e096 Initial support for delegated admin service accounts (DASA)
Google now allows GCP service accounts to be granted delegated admin status for a G Suite domain.
To use this, admins can grant the service account email address delegated admin rights in the admin console
and then set some environment variables for GAM to use:

OAUTHFILE=oauth2service.json
GA_DOMAIN=example.com   # your primary domain name in Google
CUSTOMER_ID=1d80dfc     # admin.google.com > Account > Account settings > Customer ID
2020-09-10 11:25:59 -04:00
Ross Scroggs
fafd9e2bd8 Fix code (#1232)
* Correct argmument

* Fix code
2020-09-08 11:47:06 -04:00
djeckle
367ea4df39 Fix seats argument reference for resoldsubscription. (#1233) 2020-09-08 11:46:28 -04:00
Jay Lee
630abbd0fc merge cleanup 2020-09-06 09:53:52 -04:00
Jay Lee
fe20428a14 initial support for security and dynamic groups 2020-09-06 09:47:46 -04:00
Jay Lee
0e36681ec1 Update .travis.yml 2020-08-31 10:20:14 -04:00
Ross Scroggs
884cbc52a3 Clean up Cloud Identity groups/Fix get drivefile csvsheet (#1228)
* Clean up Cloud Identity groups

* Fix issue in get drivefile

Updated code in downloadDriveFile to handle unexpected data in spreadsheetUrl after /edit
```
https://docs.google.com/spreadsheets/d/%3Cdocidhere%3E/edit?ouid=1234567890123456&urlBuilderDomain=mydomain.edu
```
2020-08-27 14:35:48 -04:00
Jay Lee
88c17af8ef Merge branch 'master' of https://github.com/jay0lee/GAM 2020-08-27 14:33:51 -04:00
Jay Lee
549670e45f some APIs now using v2 discovery URLs 2020-08-27 14:31:47 -04:00
Jay Lee
4fa0e58e80 Update var.py 2020-08-21 08:42:01 -04:00
Jay Lee
d60b9b2b47 use new DwD page 2020-08-17 12:04:44 -04:00
Ross Scroggs
3368bd3879 Two fixes (#1226)
* Set maxtasksperchild to help avoid hangs especially on Windows

* Fix function reference
2020-08-11 12:47:03 -04:00
Jay Lee
dbc47c5420 Update .travis.yml 2020-07-20 16:02:04 -04:00
Jay Lee
f86b5a2bf3 Update .travis.yml 2020-07-20 16:01:34 -04:00
Jay Lee
0e0c126726 Python 3.8.4, update PyInstaller commit 2020-07-14 11:37:34 -04:00
Ross Scroggs
45e0e57668 Several fixes (#1218)
Have create project use http object that uses CA_FILE
Fix calls to functions in new source modules
Put deprovisionupgradetransfer back
2020-07-02 04:38:40 -04:00
Jay Lee
7ee1edbab8 fix line end 2020-06-28 20:36:28 -04:00
Jay Lee
747ad9f29a fix test create/show unicode labels 2020-06-28 19:45:20 -04:00
Jay Lee
7e128dc6c3 yet another patchelf correction 2020-06-28 19:20:14 -04:00
Jay Lee
5e1352077a one more for patchelf 2020-06-28 19:13:37 -04:00
Jay Lee
22fc54b2fa fix patchelf download/extract 2020-06-28 19:02:24 -04:00
Jay Lee
b67e068991 fix bonehead mistake 2020-06-28 18:42:24 -04:00
Jay Lee
40f5bb07d8 fix gs objects 2020-06-28 18:38:27 -04:00
Jay Lee
c1063d1967 Initial CloudIdentity Groups work, more APIs to own files 2020-06-28 17:26:21 -04:00
Ross Scroggs
964cd19949 Update CrOS actions (#1217) 2020-06-27 12:31:03 -04:00
Jay Lee
f55305a800 action deprovision_upgrade_transfer 2020-06-24 11:11:45 -04:00
Ross Scroggs
8392856ec5 Move trap statement up so it is seen before any exit command (#1215) 2020-06-24 08:16:23 -04:00
Jay Lee
01e1551838 move domain functions to own file 2020-06-19 19:34:34 -04:00
Jay Lee
d3f042433d move asps, domainaliases functions to own file 2020-06-19 19:07:51 -04:00
Jay Lee
0e5635cc2a fix project creation for @gmail.com accounts 2020-06-18 11:17:46 -04:00
Jay Lee
73677544a3 GAM 5.11 2020-06-17 14:21:49 -04:00
Jay Lee
7c46d8548e servicemanagement API to serviceusage API 2020-06-17 13:30:08 -04:00
Ross Scroggs
186381426a Update GamCommands.txt (#1211) 2020-06-06 09:35:24 -04:00
Ross Scroggs
af1e695661 Clean up csvtest (#1209)
Let your formatter redo 13382/13386, I don't understand its rules
2020-06-05 13:37:44 -04:00
Jay Lee
4ccd51269a Support base64-sha1 and base64-md5 user passwords
This allows pulling md5 and sha-1 passwords stored in OpenLDAP format into G Suite. This example commands set user password to "helloworld".

gam update user user@example.com password "{SHA}at+xg6SiyUovktq1redipHiJpaE=" base64-sha1
2020-06-04 15:59:06 -04:00
Jay Lee
560cfe225f wMerge branch 'master' of https://github.com/jay0lee/GAM 2020-06-03 13:30:56 -04:00
Jay Lee
e9e4c3d333 Add a 'gam csvtest' command to help users understand CSV batch processing 2020-06-03 13:30:50 -04:00
Ross Scroggs
dbca6e3b88 Handle 0 users in update group (#1207) 2020-06-01 16:05:26 -04:00
Jay Lee
ad465ed20c MacOS XCode 11.5 2020-06-01 15:35:08 -04:00
Jay Lee
9370f7ce15 Merge branch 'master' of https://github.com/jay0lee/GAM 2020-06-01 13:45:13 -04:00
Jay Lee
d9151a866b 3.9 dev, allow Python 3.9 to fail, Focal is now passing 2020-06-01 13:44:12 -04:00
Ross Scroggs
7937fd00d4 Clean up group_inde (#1203) 2020-05-26 15:09:15 -04:00
Eduardo
d2199a5b9c include includeDerivedMembership option for group sync (#1198)
* include includeDerivedMembership option

in command gam course xxx sync students|teachers group yyyy
I add alternative option group_inde (INclude DErived)

* Avoid interference of new includeDerivedMembership

taers232c suggestion to avoid unexpected interference
2020-05-26 14:30:11 -04:00
Ross Scroggs
6e765325c1 Fix bug where user all was not properly recognized (#1202) 2020-05-26 14:29:10 -04:00
Jay Lee
18119b3d64 disable secrets for focal 2020-05-26 09:56:41 -04:00
Jay Lee
378a7c2d6c allow focal to fail 2020-05-24 09:09:18 -04:00
Jay Lee
1270a315b2 it wasn't the unsafe flags... 2020-05-21 10:18:36 -04:00
Jay Lee
931b2cc700 temp disable Python optimizations 2020-05-21 10:00:51 -04:00
Jay Lee
e145ac0ad1 hold on ssh-server 2020-05-21 09:19:52 -04:00
Jay Lee
ab8e882e94 focal 2020-05-21 09:03:16 -04:00
Jay Lee
b66d671b74 try upgrading pyinstaller commit 2020-05-21 08:29:55 -04:00
Jay Lee
f662a13778 another attempt to fix focal 2020-05-20 11:29:17 -04:00
Jay Lee
845aa122e1 unmute apt-get upgrade so it doesn't timeout 2020-05-20 11:07:59 -04:00
Jay Lee
bb19336d06 silence apt-get again 2020-05-20 10:52:53 -04:00
Jay Lee
774948cf9d try apt-get upgrade instead of dist-upgrade 2020-05-20 10:27:18 -04:00
Jay Lee
e26e077c83 no apt-get dist-upgrade for focal 2020-05-20 10:17:02 -04:00
Jay Lee
f264ffd040 Merge branch 'master' of https://github.com/jay0lee/GAM 2020-05-20 10:02:07 -04:00
Jay Lee
7e16e4880b Update .travis.yml 2020-05-20 09:53:49 -04:00
Ross Scroggs
dd1ee6ff44 Fix bug (#1196)
```
$ gam calendar testsimple@rdschool.org addevent start 2020-05-17T08:00:00-07:00 end 2020-05-17T09:00:00-07:00  recurrence "RRULE:FREQ=WEEKLY;WKST=SU;COUNT=13;BYDAY=MO" summary "Monday Morning Meeting"
Traceback (most recent call last):
  File "/Users/Ross/Documents/GoogleApps/GAMO/gam.py", line 11, in <module>
    main(sys.argv)
  File "/Users/Ross/Documents/GoogleApps/GAMO/gam/__main__.py", line 45, in main
    sys.exit(gam.ProcessGAMCommand(sys.argv))
  File "/Users/Ross/Documents/GoogleApps/GAMO/gam/__init__.py", line 14413, in ProcessGAMCommand
    gapi_calendar.addOrUpdateEvent('add')
  File "/Users/Ross/Documents/GoogleApps/GAMO/gam/gapi/calendar.py", line 350, in addOrUpdateEvent
    sendUpdates, body = getEventAttributes(i, calendarId, cal, body, action)
  File "/Users/Ross/Documents/GoogleApps/GAMO/gam/gapi/calendar.py", line 537, in getEventAttributes
    if not timeZone:
UnboundLocalError: local variable 'timeZone' referenced before assignment
```
2020-05-16 09:56:48 -04:00
Jay Lee
90d628cc75 Remove deprecated CloudPrint commands 2020-05-15 18:35:48 -04:00
Jay Lee
d5a0b33f04 add MacOS 10.15 check 2020-05-15 08:16:21 -04:00
36 changed files with 4068 additions and 3751 deletions

View File

@@ -5,33 +5,12 @@ dist: xenial
env:
global:
- BUILD_PYTHON_VERSION=3.8.3
- MIN_PYTHON_VERSION=3.8.3
- BUILD_PYTHON_VERSION=3.8.5
- MIN_PYTHON_VERSION=3.8.5
- BUILD_OPENSSL_VERSION=1.1.1g
- MIN_OPENSSL_VERSION=1.1.1g
- PATCHELF_VERSION=0.10
- PYINSTALLER_COMMIT=3010fdfaa037e9b19e936711d0c0be9b314b03c6
- secure: "FSKvLaiqhKz21SVgAQZI3bSX34Ffyev4l+R2G//QXNDu6UVQcuFsykzw+eZEG7fkhotXr8BMDL7xIkookiL8eLwUtcd/Z95HCjPBBHcmCSQleyvuuJBxdrQ9xldmiGLzMCYiumSH9OH4uJhQ39Yjnjsa8TK+PlTci6a/BTzlYyBSyDYDf7Iv/uhfQPDHL3pNwrQPHf4fL6/jcvo+uaPcv83AVZkNzZjjyoi9Aa+uh9xlbyHg11jp44463qqxoxTdYik3pYuXRBPjknjOGcnFHqn+QOVSdRQoiwbmT8xVuYuCzTv9THhuJ//i5u7s4y3Xyl7u17B3tdm86UlMpQHy/w9EsYaSBPOU4oPNomRtOnTSugh0v9ZBwptP5XfbslII/iA+LQdzTHhchn0W0CRyDqjOMSestWlrsq5NZJtBJTYHbebllOhEI7xbj9tY+re1zFWSPMOPgHJP23ovsdk3hD9OT93AzRHInCx5IxL6QvEgRhAancRuGkf2rGP0g/vX9fQ0Il3rNMSQxHB5CyHUBtUJ9nhU79YkMDZicD0jFMEwjWJO3itAp3ynoLXRgktgQCYUfgc9SpdWKD5SXLCYnSo22JD3D1P6h2EertRHaoKRLb+CRXQC/lM8uh/W+BjA2Xe6Vut2I/72ndjM+10T7E2xk1CFyCH37a5p8cH26Fs="
- secure: "J9380tGLOZWa7dSH1y5Il8T5JQpN6ad81gI6VR1HIU0svpRdjgikyDA7ca2MKYDUYYY9yVSkTV6gCl6iIU/9+SKaYugpP+tkvdGYkC2moJdcTgYM/WOnIK9ExQ3BPhN1neGxJjPTwKo1ft27mtZ2I5vuCiBwIcnKWLnKPyW3PD+mWpfqiLuEzkHoAh6G3jC4qbcCrZDeX/knE+PzqESUEi+8k1G8gYcSDWujba9ypSsqZ8T/MXagGla6l7y2Rz+/KZTJmFHwKAA10V+xPLVqxoiqi4ar66yUqy0BamwRXPcseI+ns3Q+4lUpMqVQ5GlRy7LF1xC8myjmcAexXk0F9hg+CMzewKI8UgmQH/ZJvQZEh8s6mW26+CqA4d3zMQkWaR0WtEtpiuH7AGHCflIqvEQ6UiG7ia3B8iZfW2wl0j/kqx4OuHkS3r0pWKVVIIvCj9Ow2BHP7SpiV1AcUGsVxzwbgTh67fitna3Z3c6Uj8ccQlNr7ZIt1az6Wf3w5njijkLOiBpQSLKunTTCTSge/JzBTKUcie3RE9vzirl58gUxAt36nDtPWnory+RttMZrOkBVbTeSxp+IUe8pNwLFPHABsafXsjkfzBOtFmm+0ZXWt2Rlog5NvlemJfQUWDlsL4g+BSakzN+4sIPKzSauWDHyaEeULY7Uprkil6c5zwo="
- secure: "szcjWHPr0Bf1KCkyTrV5Fu3ADhWk+pg8YWucjXHdybmhaQIKG7iBNg8LJ5d0OBTwAg31wK4ZgyLVSa2gKrAZ3UeDjykJFsR711xDSQOod51Wrgqu4FbXDewE817DUk3Cwe1l5DCu3/fjEw4vbm8B/qb7iMTRKCq6hJd97FwT5oauP0QHNPer9JjrW4F0Hk9ttkgEU2dXWvBMsTJsDOGNI3ddABE2HskxV4T4thelDYGKBDHhUOAsRwSjXgWy77Tvz98psPIvd+6+WPYNRdRWcPDyAR3Z1O/fNjUymrQI6eMaHoSFrmhDS5lbhjINRfdUmECyfCfIFeLWWiw4g4bq7l+4HBORbei55tAIjhEsxJQoqHi0Q5dD5TFh8IiWqowkFbpvNonMSIpKtB0cyT5jU1G/jRA7MPcIvSrdzHaDkoDNHJgAeZfgjOhzTGYYD19lGIljz5BQBcNFZY2dJbja+Jr4He2CMAOBOdERa4Zn1VyNfOmd8Bn5hu0C9D2ybnSCxjXXq5TRiktR8X7WycVZYfqMZXAwP9FEHVitJ4MZEGUc7S92K5gX4wmjcJjLS+Xo/0nsduQm8PuiMjbcPM7/oGx8Xm1KuSfHdKWMBoaesPaDvRX+YcuiNstXf1DkCWl72TsFABzddlNUMl/s2YSKkCSHAJ5ILqrB28Gx89kzVlg="
- secure: "CPsDgSoZIHLyjpUbYEsx1GbB+UZcPXCEsz9qT6XRM+qMFKLlSnHxoJ7gMrJtqfjTh+1gLB3UTjQjFr+jAenqLWzaJh7Zdtpg9BOG6aXC8CAi1h0U5ZMNSA/+5lQxOXuhN8HmXI1r8xPNRFXBdZFea+072/2HJTwmgYlVTTQ8FrbXQJCzs2cFqxnVeFmuG3N49AJuoy3B+P2DMpqPzABbQt7Jf5H9Foq+16iXxhRkYA8/8H2nF7ZE8IdJuRpqhBUoPF/8Mt2QBLIlvNIdIzFEy2O7ZhwL9Dt08AG9v5c91QPzjsLok2e+5hFGaeMpGQJCE1V3uHyrOAeyZs1QYr7qBWbjQCVh0Phxz9yOA9RPfnQdjyJTq4QEj6vVYvCi5K/CGp/DnnLnGyYJKZOt7nBx02fTVI136UXqt29eF5NRkCpnUqah2s0OXkijsw5Y9LsbwiUxELKtCthESOwN8e/ZNvn/gjPfZWIaB0gupRNugL8Xo9Cx6vFmaW8wzm1IutJvo1mWvkWMvuYYjvd4aDyP6s7PFnSX7DoD/pfxoQuzjwHO2OD93nCOx0ofnNojLNooeJPKLikiwS4qKc8exWd+TlnccKSkXO8Y8S+XRgeE652YNlf+9DH79lNLeK0N0W2tRExX5JaEyJQCuvK0WZi02kxvjmUHhwLAOv9ueC2UCRs="
- secure: "JMv9mkSQBJXvUznXcyvngFaOfd2fHYEQQTH8BnS03pqAL0HkWSheG/R2HFJlJv1VJ9MDMf+wVzwMvU/kzkOT68nIgyFWnLBkT6fw+Mw7t/dG96/nOh7DPuaTVzKS0xRbMhDaqZOA9GW13TVnpVdIw79vbhQM69N9Gj80j8oM1cHgMi+fkJSDU3EN/vMJOGKSB3DTpyqAG/nYyZ38tLib8ic12za/YL3HIu8QRHa3fr37+cyrVKgGebGg++yK34p8lC1W4apiO30drmHVQhKWrWnmdcssdGmVM7NystMGGAzUwsJcmRFJuREv1LXiijDzjIRVduceeRYgPi1KHB5ZdL9vji0gM1eHYZZefhcJb6WgPDbCtbjdlCId4v+1bNfsh+dMmhM+vBZDtDEt3UC3MBeYTmklQT2w9zCXHuPBQigj7W4zLm6GxnAXife0SQMfmr736QBvSLUJWtd2tRgS+dRG/LvWxrP5Urvfgs8iRtEVZLDpRR+bSjvLs1UEKLN13KMKYuVwieHbxJn2kY7d5wmZVBYdaokl0yrTkZZ6J9xIphxM8GXJU1BnMZY9zn+Xq3jm576QYnNYUwCtUjOis00Ct4UVuuFqIqZ7bJL7GT4AHTENAk+9GtXVCo3/jnv161rxgoSdnUG6VmrpnyA9jSpcIvMVcE129oghBJ2+PkU="
- secure: "otePbb9W6CAuwzayL0dDAvCryzF2s5HHAIytvMW/xnzOuRMR03lumj9bhg56YCB/nIuTncAjToYmsMh2Acrp1Xcx8/DzqKse1K9nkfJgsB+EYeXmy8pVJnCxhHeLCzUPJjUfRoPBm+5GHAkeoviUyxenwbOFm3/Uhtay7i2ZU8K1FsECqx4FZXVh8rrGHqFGzHNimqDDvFUQM6f9a+BevEio8+/aDooaFfvVqPixDYXpi2+99DB/0hZKwcUpj6pOUUszATkAl0kJdOSWI6E7bQa63i6SfhcBNHfvbDqhpcAGs1TIXHbXwWGfySABKgT9mkq64CY3bc/QZ6HeWe6P5tJmt4mGBxDMOWMYj4qtCskQCCCiY9+sCx61Sj8W5w8exjQvvvFA088cKtbCKAx8FIil4CuYPtIjDQFkiNI2bC6BmkUen7qe4z7dSF2AZPb6CuSsBjoXL8Ezle3e1pRrkR+SYbxMSaZVcQoG8hinTqaXhlS5gXi97ZzRrJWn3tUB6JWEBeEkIfTJmrJolbLYIMcADNhKenrW8k6nV32JkvMsCgMFCIkJsIZeuQ9ZYgSZ3CXPODux/D2/4/gNa+353Fs3DTlDQxRTgoaYi9UDIRrKZAkoELFclxuGSKMCCheevQ8FMCmXfxUocwLSgjlO7g4rTJa9Kggn7VltucXiVWY="
- secure: "WKdB+WpZG/7SKpPpgM6DAwzVg0QQuxFMJEZE324pagJ74xv+GlObTLm6kU4SAZf6TuRSChTXDAD4paJXkxpq3LlXjp9aKxPASbG5PgZMnGL6bZ40c/UUqrwVy2HLnXPKSNuDBX1RidGXmH3u4YvufjguNnlOLvHYfMHIRGjiEL4ZvC38GB/3YDmID1zI6w8NzTCpFvxeNdg1qhhOBUKyt+icRUwBu8DfgLidzTrO0j5jo3UeqO/w3E9t3nnRgSiO25mBYwWySm1QBK1VS3F5iG9jo1J6GKMOFbWi3lOBoqnWotpfwID+p1vMNX35phHwmMrtoBYfMTV4A2CvQpGyQhn58qyMoKnhLz+NIOxlHN83qcu65bTMG+7ji0pgUl3jhp3ZPyWUjRYQpoVAg5f3UAnUzJgBrOUC0N60ukJVacT/kvkO10CLfz+0+eefW53r+GCkTi2m8iDezZn1olinpLs+Mt0M4VfQ52RVtq3WB6uxN7RjgtrK5XGi2zVAsvfHp+vqFQ6uu+iioiWNji88cTlMuKheo+FWwozNQzd8+4Vxe9w01mLek57Q0dpXiKWL3vMS4Qc4T9E6UzutllDyg/c61LW6Afcqw3jL4HN0UNG3zFefno47oKrmB13HcxakLEC8sdNfE1kWGvB7jqD0snXsz/8rz7rf0IqnK0kaCAc="
- secure: "Lah0Q4bWyEJXTBLZHCNkuEuU5wlDQzLF6aQwW47RyVtyRDYW8uQRQFkbb/3oj4QAAgXl0sOcDFnRaaWmlOStPIIYKKVY8Mk6ufyDCz8inWoPNJoSbdqbAi761HacGUsEfSNDrPNB7fst47UZaX7WNAO3q1QjCCGSEJQVpJE2MDAjcSJhAgLPt3JRFSOSwvqxcDLZI+wF9AM81OWNp6QisAdI4LPUJK3L4M4dLh9+apFRb1OMld++scWah1SB5Qj6tPzMvX96mp1XfWB1fJXuf8Yvks02H6ZcAXubK2HXe3iJZKVqGB67kShNN52P+wg0ZZO4OP4kZ2PXahnB7hhxIfEfWsNVy/w4ww1N6K907BfT6RDmsgNP1ZP8kpq6H4pTnWgAy9WDxjatzbNFAHtMzakGjYJNFxZJVco2FL0ipNQ7htoVAA4sUb+VbRQv4O/oZTLMNnnco19+TFJzuZuS4Rjxj/zX63gXkj/W7wou3hw+NpI/PL2hUXIc5imXSLfVQwri8Nl+6IOjV2gWR/vR1VhQqbLsTF6TZQKNI4lvbRTs1nNeqNMW1lHizI3r9Kernj2noAsxJK7wFTZ64OUkQvSNphfmVow29JYQKXbpxbmRrdnmfmtnMsxUngpx9WsTPhrprt5hIAqiBspHemrs+H3LiIml6IY/3l9bcPlc0gA="
- secure: "sNc7kC0CuH/TCZh2WwEN51GcA1fSpcliCHpFB+WX7ieQiRu3xKn2avby/T7vbvX0viXRER59arFGQF4i/dyr2g4tlZLVRYjPeiApfduKZ7Lb+vZGro3cWesfHG6Abk2VcgZZli1IDrgrHH5qAWnA9xNnKvKBL9NDM73Zj92BmlDFVEzadTii8brWvced/YP3jNXEmM5ZIufgpe2yidBB2bLWYJXb3Cf1MvzMG4tqNAtZTrI32q50mokz/uTqp3MRJ+cR8sOI+2+2xSbT0zZGLSRZf96/7FKtE0QIDxdWAe4XdlHq1CluRVk30Ju5BEn0QzoYLryCIuw3JjDl1Yksw4IA5imljZJlOmWa2l6fX0HNxMw+z0R/1d2HARA8BY7/uQKv4guV3Cf3jpsWoKSsM1WxqOqsuEFOoRQ2eQNJEaSuC6+j/vzNoj61pOuG0R9OC2PFcFCZ9fomIrZMse+7M3WIj4+mp7e+JDK8DgVdUlqkBVCx1Ospseb5pm6lDx8F5NbgqZgGXgyoWVpqZnyYOoOutezMoD6MI2wXzJaepV/L3+LD5f6q3DAa/sRAEEsBFGyMHXiPYbziEiy8Hz09Sz3inT5rzS8OLOinwAI2sHiIYHTl340XfWdYz6AlNhYLCGwwmtkntbjOj5UVW06IgBBx44ujpZSUjv7SOrACPGU="
- secure: "t9/mC8eSsxXIB2vjcZGtGISxrSY3Yd+XS4+/i1YG1mxSov3R6UdhVl2blLgrvFfVCSo61YMAzJbXKtZlXPzhLDXxpsSlWiatjNrKKo4D2unnlHsyEMJ4wfz8cJ8pKyynP7Kc1ZuaqTSMcEZgk85tlew8Zy/VH6g3Mc/7DvP4SxEDRsP+DdCplZc0vbxHDaV3iC93bwNRfy1UQypwJJ2WQviRema3Umneg8hVl2V35zbaBoy45ubCkUoCbRCDaUyoHA10GrE1OUOLsioar/dj6K2W3EhAtehHLWrUZhhl07rayrrRDQYDTdebifB/MWlvR5FjM/Dr5M4k2ciss4ol3IR5LypRLD+/YBtzeIuqkDbUkaBowY+oUj6OWlzEbzAUrUNa5mnyR2jhr8ivZUeEEWLxljsu8gWq65mzgiZt/u+kVCnMLciUv0//0nrsNsEMI9pau2ZxbcpItFVKdZYXFdmHWG+qPCgMcbgsUM3xqaIc7fNwbk2Aa6erIGqD2VkwWzD/xksweg4lsgQ0N1tXMfoWWKh4Xj/OFom+S+3w9uSx/jQma7nXm+PKQL8dIo7rqza3fz9biq5T6mhwUrqCpFIJv7mrMwaTT1UfOchjiLL3CGeO7Amv7Avmh3fhMPbypF0sws79d7ewZbyv+oSzn+pxlCdzBU1GYx6veApbzhg="
- secure: "ljVx3TgpBJ/ylMKDVmXabi9UNi5YvrRM5UBTrRk7XPu1YFYS4FI1GcGlyvYhToc4fKt4jLhX9qU+s/rZY+odO0x/HpmJglMBCrY+QcWOzuyaP1U5dCET+evuqFdEAZIzLQc4VDjL1aQLZh+OG7bjoBClVAan6a+pmW0yxBC6rNtCWTESG4rY3wOeTpoI0Q1gM7gg5Zkj4Z+yLvYeJdoKHijM7C3/R/VVTqUFqArk+Js7Qb2qTqm03SHP0ahRQA8XSfbPebSkJyX9oLbidanBEaQE6sqnp9Qh+8VGcnn7VkSu6oq2+ZXz4xlSMrH2Iv2JXl68Td51LsLo9BxaMCL68ssgTFfXPSrrcLwholNEt1pXk5nhBl1l6MZ1UwUJyBm+AXZp/4sCK9/P0rGa2d1rOcpOz7nobH7BDktqEJkrR6VzkTMx1aOwtF+JSt7SJQ1RrRdm9uKfOZZsnw17+VgVAHo/ttY0C3cRl10oaF1C/IdliDfa5gJdZ2VSZtJxyewqKwGiZrqCRv2fQyIuGsqfHXsyHVL6q1KfVcHjaXBvh0o6xZ6duieFT4FNHg7clv1qPQV+cLh4L11nugiihRTeYQtKyUnP5YIL+jlcGNM6KqKhF9RN5c+zOqWNmEcz6O8nljY5mFWdIxL6dFfE3+4wpw7snFP0PrWIWl5SmrU5ipY="
- secure: "L0ivSUmbOHNmKWVR2zeYcVR+Xr6780dYA829MyksXfg9OrieTS+qVqSODKexFW89dp4Wf8xFdT9f16xSqjA/xSuX12uiISMiTcFKP39ZnQZ//NDkx1T4pZaeNiysqT+2Ys9OaKTWxn8luFtAYp+DaluancQtEw6+M8H3jQyQZimGHl0QB6BK2A5VA2vko+7ebIdwu5Df5E7lc/LG42H+DdSFkSj7Meu5XMyPHSZRAdOE5doO++tqFKzgs2WbDRHRUP4R4LqtDlGn8+5qnQtQKUN4V9UGrKM95R34BhgUlgOqOFAjFDug71/1/rRyv9XnzKkAdTIbxV7nmxSL+APhQDqpwPr01EP9gtZBOycswV/igJYotgqkhzwNAYrmwOA5Ta8S18Ck0feDEqT20w8yKS4QwV2Ihg4q7CqDFu7i+9iSRUCd/RVrGtG0U5Zo1b3+1JcNxXH/ErUeyHPfrk4vktxfHmU+omqwkfvTRy5upn0Ycr57YHOJc/Iyur7vE07HcnBfAwV0d5KO6HJ7M1n6hmMxeyKmf+qiyQQnphySvHHAfa/9Sec7omwwKv4bn33DwFGc8GWvL6cZXWhmGhFvd+LslpZl0vZ+7Bz0tXlAg4t8V48y/ZtyoSpny0HP4UstdA43PttvZ6q2y8LUNk4LBP5btKmp27Fszuj/rldtj5g="
- secure: "Dox8JthAJqWT9eh3Jt4Morbf4pGN9OjduJXe/lYMsmFvqNg7b94P2QdumWBPmVjDq4YjDVirMejBA/TNwORgKfg7pI7MOw+qqoHpT9xyPecXi3ecyBay13e16p0GNRlc5pUu5JcU8sgCpttvM0EAw6bOuQIhnnkIFesbOvwoxGYjMzjmWMNuikR3CjKbo0LDtD5NJXT1OMSqrRuh8NM/BoKyn/kCdSaq9wI2GUMbkg09/kFJkQOvtMXPkM7dIhr/9UC0ouMIyqe/MHa8O6Y4xESdqiTql9uz1+eZfHIRrgFlHfxDvkMv87Cx5OuL+O+qeT/a+RYLCRJspMoq94IYHGg2iyEfBO2YAkogl53wiEf2KF2JdiNGT0xId7bxCJj3efTuCAXV1oqaHpJli1Mhvs7zPEtf78B4tkWEgjhGr5pBLIlbhNjS5wtTHJX4BUzoiP+wODj4h7rjPAah42nWF8XOMlboVi56sOCLjiHBOvYObqyhSfiQxoi2XHphsrZqw6H03tr4Kqd9HVmuoSvRiv+NOu24Ubr6MrrQM2/G72TrTx0/aBlt8Dx5nx2oWZ9ZMiDUR3XlvDLUi45SpY5qESXz08nRlcdS9EvUpK7C+77bNvX+A3dIhsxnxuNaf2naf+QnYYbvh7q4Qbrj4v6EMYS90Uky1JHdoc2wMua8J+w="
- secure: "Is2Lv/rxKKrXnxFns9KQYseD02tjY9qbgSteVtJavG1cLJDvkTwb6X+Thvgo4cxk5fQPiXScrQaYzHjVVuNleD+dyD1HC/8CU2Xq+tjBhPjdcccHFSbk06DpcETmLGRyMORXG9JkYlgXHLLXtu/9icppWEHgra+zvchVL2YDhofYne8FMNBb/lq0AAC2wgzAS33tW1+57HaYnzl0hf6+Q6lwqoH2/aTfGMRFDgyJ0HK+5IVfLnQJ+OuGFSrj8/0FWSggR5+EXDIddDgovFgaCghMjHYp21bzn0eIAJtuNFFultwk/UC1lT9joXTKEzgLTAh+w13yz1T3x9rNuv6FDKCotBIS/ZDtPmgvyZ8xvB4SzyULnTRSVn7YvspKR6PAO/qxGNudUD5H8tRKer4qKnKjHzSUcVBlRHb4yE6FqqY1Z9RLEcomWO43nJwb6saNHR9BYedyi0gA+EbA+P259QFClW1dWEQ2LQhDa+0VRssOqZ0BQblPFyz+e5Vc9kfAMbOuoss8fjkiYv+twXv7nT27xrVT5okfKDSiy5opZD6d36N5FibZPYiMrVx00YZdkFB+5EqQuJ7lqKUMkZJTeApLzj+h+/4aAOWd3paj5ghv7m+9ReohsNKFHyjaSy97RhMAZjzqgMMdD8rjUSKDhvNKvvQECWHlaXwL129GB0s="
- secure: "l7vWxfcu1RgXbStq76Mzz2I5Iu4e31729OyLYqulXZzft3wO+idvgQKy/JSwajiKgOxlpBuI0wrncgIUYshcRvE4yB0y9+QIMDTegJzTADtRSUyVNCIZfTgvtOvzrlW0iCdVsxLBtYcJWJVPdjF2q59ED1jahd1AuJJqX5e61gfr0eZ9+cNiHbX1u3VpmGchFNWQF4KvebE4WKs5xWEds+AtbTODdQq3H6kKQK3fTVJnbz6WMO+bEWgWI7orfSE20lku/3Q3eMLhNOcPwH8WnUoTTvDWol5Cq5NfPhkKF5aV7kIbNXkxswM7yBPAPumBiXdM1BHpfd4+0YQ/fqRtnqxw85HEYpT8dRewHimCl4IccgGCR+G0tK8RNleKL28GWrz86gRVpXMEhOU3ILwb8as3SdQWLwhXXy5uKGJmsdrCNAH4/8eu3XczO5VN2wOo7narYuBgGcl2eLW9TOLyNVKFKxbQnDLBiOybLNoOV42DCRYt7v3Eknkv1Z0dmX6n23q1z9if9kkfAFgRQWsTbNZyWeIwWuX8b2a0Zq0znS3JflSKxzqS7RHADfBOJEVM86AiGkw0XkR4o31yDrY+zOrkJ3GxfV/HpqG7LzCd9tFeexanneDCE1FJhCkDjpnc0Mdyx+1gVf+u2MkgrU0BbCf2EESBAlC/FTRvxTmk/6s="
- secure: "bvtQ348bxKwTtB8X0zMxeTsM0jEJozbS6/rzH/88Fk90a+KO8SdXen0Kj9/LahV4duMn2xTTRmxMCVj424FbcVTgBkJpIO7btqTcNASORkmK+9wGTK6Bgb9R5sHLbVrbrMYJzsMQR0MWxE8ibPLlyom+ssUIqr7HAjnRyYSDiKChGhgBfdE/0G9OE/DuQaU/ZkTuEYfc4527QNLJ8Bt1aLDdCHJLxzCojNaTHB215Tz7dmpnJjWa45BXxkMwLTpxIJuY7wA7K+2UBJfLvZv5QiPYyeUlosV7FwhCN9e90+dc2x8HPJbwe/Ysv5dm8/FfNTgTe6IkKw1z9kev9/W1tYCtqMzn9GnlIH2000ZUomhHKbc4UUl8sskF2jGr14rSz7pfaTyvXNvK7nKLKM7mZAO/cAaQvZoGEx0s6B7a7YX38NmJWcM0UUD603n4G9uVZNoxjyr8HWC6pho8MY8/u5aFKMPd+C8DaDWeSzNJDZRyi62v+t2iyyZuQUtVXtnDpv8GQW4tYFIJCkkrTm5VNNgD2x3WRsEr3LsBa3BonOmi3a1VrdIpyidDzBEWFUR59a/C0lU5J2UX5W1vu/Pnr9/8p898sDvvDbluz47OR9xoVOjtmGdt/ENJa4EOY1q/eRIN6zrt/cwwjIKgTcYOVk7DFDMXQuYlDeEFuHSJ5Xs="
- secure: "VWY3aJGMFB+E5ObJpUUXNQrnkEpA1pbfey8Z0pe9X/n5ubmDBIAfCPP7hqT/lAF/1hj5+HA6oWEa2FzeJZOL9q8cuymHzkehiX9n3f6GktirMtEoG3Hk/CiW/ZxKf9KyBNRVsqsOiXvfzMcMSU6ZrSV+4ZHIVl0zO5cehSaaiTTqVU5K474N5TCc8SAvpSRpxjZVRXy8rWF7WMaG4Xm6aa/IELccjJL8s6sahk3wT++5pzewxv4XBWf4bNA7+MNLpnzKmm2S5T/uohsw00F/j82g9xFT3qYm6vgaR4ql0aNvxd9gZ/6vdVpqBIUZmcFi9yTSFSsSSH2cNGFk3IU9Ecdh1Psi3B5pTpYzy7BbLWyKo70wBkOoY8qCwJ/wtj+2TcfR72qR8ryXgkbIoXxe04VkPqRNEezk3U+UxUvLLDEWthxjuCRCydT8FaCfYm9N+l5Un8TNmY4W9mOSu5IXKJ7oSQDrJG65Z2uZKQrWm0ToOo1nNLFclFuM+K8JWNOKysfh451LHdbmA0rkoaLUat2ttDBtQpbw3h+Hwn3SW6BfELg6WDJXHyAPNRMPSc2Grk2o7mFfTMJhHQG4U+k2bL+AHJU0nPWzuEJyMraquqKOCcHz56A+6j1PN3wpLbIC18nNPN9Q3TDPiBmwzwEg+zLu0DKTu3Oe4IRKRNTuocI="
- secure: "BSZK3v2RYsNKv/8lBZ/dKagYF+znGXIeDY271A2nCS8DXx+y0octwI5RT08+UqdD1AvhPOPjdnSXG+M2NOSLlfBoXF/0t9S39+qy3EhGgxYAcWcVDXnrOrHEOALg1cQkCA2D/CG+eeG/36SmAn6FINTLaAOilxWGGCeL0KOB5mqPPnPogaYhowIwDkj0Hfsfrw8011XzgbufNr15GIYJ+nP7f41NcgIbnNkqXY6Q1jiWs1DuKXNDr5WGPqztHtVf7RtcpVmShzptAcAMdRqCGKF7atCZhXPbd69j3qf7F533/tCSCIrszR/Wp4BKlJfVCRCk1oiEcfRvprJqeFJ+0WJCVzyD7hXfnkBg2Tb5PmzvxOFM+hsqRu4mY3UdjqXukzAe5O0O6Uo8sqzqQUIjUooRnNQ4GdPd+7wMbyBZn4PJaW6YTi/7zg7mAegqot6uyEGGpWb6iFYrf6DX75GUfTciNktv+ez0AeqYFwNBLgcOsmaq+3V+aR+dIxsxIzC/i5GCTHvMYOIp6Q2tPCuIug0tX6uxm++vMOoMLK+vG1fVQ0Cd8m6yQIWEXUu0VBs1MTlJD+vX6LsK8CGo0Dt3ZS3JmC4TGGo9CBSEkeDvnKvQeX6x3NBIFWvgt+Hjxh4S9U+vW+AaUXpV9k+NEhesC/8Ys0UGbGYsTDzHdx9TMBY="
- secure: "sskZYJ/+xmHh5GNvw3QRf10+sBGOjlub29jOTxjXIRYUyZfkhjsF8CR9NP59CBFuqgN716Mj1uEVEcx0aaepr/zWweQSnqa9lZgFD9nDYALVNh1b4oyqfhYG1sw57ZHdBsJOBF4mKBnahly/QQj11Ya25GyIS2IewwCWVNtCOJYietSssG9l0+qc+RPG5Ub/lEKA3VRrtbUuApcIYszt33DumgZ4wzkzICl1SuOy8V15vHVlkBqASJheEDa4Hia+eAMo6e7OCE5KWjl7K2MxTvtszwFi7lgMCyPCOy6DkaULACfBnQeloh42B2Qhd/ta04vuRKg6y4fKSrnegpeOAa9aGxF1ufvG9IvPsRTsu4+w63b9xsQUN9MyDgxcJe0YeUHPgPYL6mqAjCIKvL62LlIyrQ8IAteoh3MC+4Xb8crXaLGINTcLwYvLwxsHMuC/58hdQ23I4DqnyZGAS3L1IhpX4QvYN6xc7H+ptaiQttweoH5VpMAduSrmSnNmvLOc4g2PvbRES6D95moQ7qk7iX6vpu+PevL5HtQCbB8SFyFLTYWsqZaG+4NrjpIi2hLzUT35meHwrvxG/MsfeqOVlnBfa93VnX75vxOkRFNY56sOtrTJaiVZ+rQUdszAZz3KGPbyrSlz3KxV+ZKxZH6/0oFteAJttXFjWQdICnKGDSo="
- secure: "sdzU5bPs1w7Nzf3F5Gtk/iq2Kfq3zfLQNGcehLZp1gJXzNf7F6HZLOn6GaXQ/5RVlhqR5nOmzMpVQs88rZv+6PE6YqGMugTxHIQeNmXtGnuEDJSBvGT9Ok8ENxcwKwL93g9SCd9P8mTspxklOsW2Bk3No2+Zlc/aQguBcX44TwYF42KuBU4O7oS6pUg8NjnuQp2zTyhp0ouzyAudatPyu1BLci/3lbx+MehutQw1y4Om6g8tfwf950UONZQdqq9MBluu7yYb1oHkdC1J4qgwdCZkJslWIwQHCH5UE4AW9iVG0qVrLpzBGtV27Kfy/Vf8r2gMYzbiur2h2+zzWSDm8/bk8YLn7u1FBpjGJdJ0pX0ZrZr+hMV2vH3e54NvA5WyRU7tw8mZ1PoDYd/FM5KXIYbscSbSRCqTsbPgyVIHuoOWXeeSe+/Ef1ifZv3HHf6ARfUIWupKfAipxChc7QUMI/HEQ9QPsqgBaooZD9chGsWAgv+8tFxdteqkx4Yh+AuZp1rVykB/9vAamUBebQxy0oeGm65j6X1rksfjAPkfeDYB7L4Ruy7tUwPtvYrAHPoWrf9O0g/qDRsw0vdqp42CjszpIxhuPhVRDx0i0wquqw2LnIU3ejRv2R8d1SecVKBBcVsWLFvc9iR4rNIi5JnxITRtiwL1Xv3Vcgx7WwxExtE="
- secure: "f9n1KmU5NuV4jGkrhLNiPD+3Cy7t4D7Rq8YsPlmyB2A6u6IHMuOVP95IwH6Zt0cmMDBrZGthj3/0iu5kzxzqD0m135PT17wSEqsDfDMyKRZJQuYSO/ESVxnSca3afRH7Ds7/ipQVd/ljZgwEnW3JMaOQiIdbbIquNOOTeY0/wsXkreDamXZaKKqUoeadkAV4AkKhM0xcMg+Vni1i71TYPBWrZPLVAu3ZrSvU/cE5mtBUIkbr9EgsEE+WR23QCgtwxKzNxrXetBcPXDsJb98/ABgpoItm5Ko/Zk6pkib44f+iQtn7Y6j3lieELCH5Sn0uy3RrMxkl7xicB7zPYME94NEPHCmshyVsH3RxWfcBG4kauRNBCLYLl5HYF2t1lWZ6In5qlx8xN3Tf0KrbM3vzAEOnnfZt83h3q5OuWl+jzTOcv9xHmeW0lnwEEfS0nxUV3KDqLFBczcUxKBmsA2aWnJZ2HV+kls6OaWZ987m6V2pIGR2uviGT7I4ngjCSOJzbwYbvJbJqYAI97FWP/5pqv97xHLCSSJh6TBO4NMQ7Ib/W1XT6NZyPOjNGSLpd79UA3cTzU2+UYr/7RxZpAKONSAMTJh7CX451XQwgovpFZ2quXs2BzqcVpy8AlUc45ygnFOALOANAkcRP5QFhmff0jpXstjPW83/GksbtaiLhIiQ="
- PATCHELF_VERSION=0.11
- PYINSTALLER_COMMIT=ad39eb8df209d02636399ffdc44521a97886cf8c
cache:
directories:
- $HOME/.cache/pip
@@ -39,7 +18,14 @@ cache:
- $HOME/ssl
jobs:
allow_failures:
- python: nightly
fast_finish: true
include:
- os: linux
name: "Linux 64-bit Focal"
dist: focal
language: shell
- os: linux
name: "Linux 64-bit Bionic"
dist: bionic
@@ -48,6 +34,12 @@ jobs:
name: "Linux 64-bit Xenial"
dist: xenial
language: shell
- os: linux
name: "Linux ARM64 Focal"
dist: focal
language: shell
arch: arm64
filter_secrets: false
- os: linux
dist: bionic
arch: arm64
@@ -69,7 +61,12 @@ jobs:
language: python
python: 3.7
- os: linux
name: "Python nightly Source Testing"
name: "Python 3.9 dev Source Testing"
language: python
python: 3.9-dev
dist: focal
- os: linux
name: "Python trunk nightly Source Testing"
language: python
python: nightly
- os: linux
@@ -87,7 +84,11 @@ jobs:
- os: osx
name: "MacOS 10.15"
language: generic
osx_image: xcode11.4
osx_image: xcode11.7
- os: osx
name: "MacOS 10.15 Universal Testing"
language: generic
osx_image: xcode12u
- os: windows
name: "Windows 64-bit"
language: shell
@@ -128,10 +129,11 @@ script:
- export jid="$(cut -d'.' -f2 <<<"$TRAVIS_JOB_NUMBER")"
- if [ "$TRAVIS_EVENT_TYPE" != "pull_request" ]; then export e2e=true; fi
- if [ "$e2e" = true ]; then export gam_user=gam-travis-$jid@pdl.jaylee.us; fi
- if [ "$e2e" = true ]; then openssl aes-256-cbc -K $encrypted_ab10ec38326e_key -iv $encrypted_ab10ec38326e_iv -in travis/oauth2service.json.enc -out $gampath/oauth2service.json -d; fi
- if [ "$e2e" = true ]; then cat travis/cfg_template.json | $python travis/svars-write.py &> /dev/null; fi
- if [ "$e2e" = true ]; then $gam info domain; fi
- if [ "$e2e" = true ]; then openssl aes-256-cbc -K $encrypted_6294a53f809d_key -iv $encrypted_6294a53f809d_iv -in travis/creds.tar.enc -out travis/creds.tar -d; fi
- if [ "$e2e" = true ]; then tar xvf travis/creds.tar -C $gampath; fi
- if [ "$e2e" = true ]; then export OAUTHFILE=oauth2.txt-gam-travis-$jid; fi
- if [ "$e2e" = true ]; then $gam oauth info; fi
- if [ "$e2e" = true ]; then $gam info domain; fi
- if [ "$e2e" = true ]; then $gam oauth refresh; fi
- if [ "$e2e" = true ]; then $gam info user; fi
- if [ "$e2e" = true ]; then export tstamp=$(date +%s%3N);
@@ -164,7 +166,8 @@ script:
- if [ "$e2e" = true ]; then $gam user $newuser show imap; fi
- if [ "$e2e" = true ]; then $gam csv sample.csv gam user $newuser delegate to ~email; fi
- if [ "$e2e" = true ]; then $gam user $newuser show delegates; fi
- if [ "$e2e" = true ]; then $gam user $newuser label "✔ unicode checkmark ✔"; fi
- if [ "$e2d" = true ]; then export biohazard=$(echo -e '\xe2\x98\xa3'); fi
- if [ "$e2e" = true ]; then $gam user $newuser label "$biohazard unicode biohazard $biohazard"; fi
- if [ "$e2e" = true ]; then $gam user $newuser show labels; fi
- if [ "$e2e" = true ]; then $gam user $newuser show labels > labels.txt; fi
- if [ "$e2e" = true ]; then $gam user $gam_user importemail subject "Travis import $newbase" message "This is a test import" labels IMPORTANT,UNREAD,INBOX,STARRED; fi
@@ -211,6 +214,10 @@ script:
- if [ "$e2e" = true ]; then $gam delete user $newuser; fi
- if [ "$e2e" = true ]; then $gam print users query "travis.jid=$jid" | $gam csv - gam delete user ~primaryEmail; fi
- if [ "$e2e" = true ]; then $gam print mobile; fi
- if [ "$e2e" = true ]; then $gam print devices; fi
- if [ "$e2e" = true ]; then export sn="$jid$jid$jid$jid$jid-$(openssl rand -base64 32 | sed 's/[^a-zA-Z0-9]//g')"; fi
- if [ "$e2e" = true ]; then $gam create device serialnumber $sn devicetype android; fi
- if [ "$e2e" = true ]; then $gam print devices filter "serial:$jid$jid$jid$jid$jid-" | $gam csv - gam delete device id ~name; fi
- if [ "$e2e" = true ]; then $gam print cros allfields nolists; fi
- if [ "$e2e" = true ]; then $gam report usageparameters customer; fi
- if [ "$e2e" = true ]; then $gam report usage customer parameters gmail:num_emails_sent,accounts:num_1day_logins; fi

View File

@@ -780,7 +780,7 @@ Specify a collection of Users by directly specifying them or by specifiying item
<UserBasicAttribute> ::=
(agreed2terms|agreedtoterms <Boolean>)|
(changepassword|changepasswordatnextlogin <Boolean>)|
(crypt|sha|sha1|sha-1|md5|nohash)|
(base64-md5|base64-sha1|crypt|sha|sha1|sha-1|md5|nohash)|
(customerid <String>)|
(email|primaryemail|username <EmailAddress>)|
(firstname|givenname <String>)|
@@ -822,6 +822,7 @@ gam help
gam batch <FileName>|- [charset <Charset>]
gam csv <FileName>|- [charset <Charset>] gam <GAM argument list>
gam csvtest <FileName>|- [charset <Charset>] gam <GAM argument list>
You can make substitutions in <GAMArgumentList> with values from the CSV file.
An argument containing exactly ~xxx is replaced by the value of field xxx from the CSV file
@@ -942,6 +943,9 @@ gam report <ActivityApplicationName> [todrive]
gam create admin <UserItem> <RoleItem> customer|(org_unit <OrgUnitItem>)
gam delete admin <RoleAssignmentId>
gam print admins [todrive] [user <UserItem>] [role <RoleItem>]
gam create adminrole <String> privileges all|all_ou|<PrivilegesList> [description <String>]
gam update adminrole <RoleItem> [name <String>] [privileges all|all_ou|<PrivilegesList>] [description <String>]
gam delete adminrole <RoleItem>
gam print adminroles|roles [todrive]
gam create domain <DomainName>
@@ -1067,7 +1071,15 @@ gam calendar <CalendarItem> printevents <EventSelectProperty>* <EventDisplayProp
gam calendar <CalendarItem> modify <CalendarSettings>+
gam update cros <CrOSEntity> (<CrOSAttributes>+)|(action deprovision_same_model_replace|deprovision_different_model_replace|deprovision_retiring_device|disable|reenable [acknowledge_device_touch_requirement])
<CrOSAction> ::=
deprovision_same_model_replace|
deprovision_different_model_replace|
deprovision_retiring_device|
deprovision_upgrade_transfer|
disable|
reenable
gam update cros <CrOSEntity> (<CrOSAttributes>+)|(action <CrOSAction> [acknowledge_device_touch_requirement])
gam info cros <CrOSEntity> [nolists] [listlimit <Number>] [start <Date>] [end <Date>]
[basic|full|allfields] <CrOSFieldName>* [fields <CrOSFieldNameList>] [downloadfile latest|<Time>] [targetfolder <FilePath>]
@@ -1441,3 +1453,7 @@ gam <UserTypeEntity> vacation <FalseValues>
gam <UserTypeEntity> vacation <TrueValues> subject <String> (message <String>)|(file <FileName> [charset <Charset>]) (replace <Tag> <String>)* [html]
[contactsonly] [domainonly] [startdate <Date>] [enddate <Date>]
gam <UserTypeEntity> show vacation [format]
gam <UserTypeEntity> signout
gam <UserTypeEntity> turnoff2sv

View File

@@ -1,486 +0,0 @@
{
"kind": "discovery#restDescription",
"discoveryVersion": "v1",
"id": "cloudprint:v2",
"name": "cloudprint",
"version": "v2",
"revision": "20150605",
"title": "Cloud Print API",
"description": "Lets you access Cloud Print Printers",
"ownerDomain": "google.com",
"ownerName": "Google",
"icons": {
"x16": "http://www.google.com/images/icons/product/search-16.gif",
"x32": "http://www.google.com/images/icons/product/search-32.gif"
},
"documentationLink": "https://developers.google.com/cloud-print",
"protocol": "rest",
"baseUrl": "https://www.google.com/",
"basePath": "/cloudprint/",
"rootUrl": "https://www.google.com/",
"servicePath": "/cloudprint/",
"parameters": {
"prettyPrint": {
"type": "boolean",
"description": "Returns response with indentations and line breaks.",
"default": "true",
"location": "query"
}
},
"auth": {
"oauth2": {
"scopes": {
"https://www.googleapis.com/auth/cloudprint": {
"description": "Manage Cloud Print"
}
}
}
},
"schemas": {
"Job": {
"id": "Job",
"type": "object",
"description": "Job Object",
"properties": {
"title": {
"type": "string",
"description": "Job Title"
},
"id": {
"type": "string",
"description": "Unique ID"
}
}
},
"Jobs": {
"id": "Jobs",
"type": "object",
"description": "List of Jobs.",
"properties": {
"jobs": {
"type": "array",
"description": "List of job objects.",
"items": {
"$ref": "Job"
}
}
}
},
"Printer": {
"id": "Printer",
"type": "object",
"description": "Printer Object",
"properties": {
"displayName": {
"type": "string",
"description": "Display Name"
},
"id": {
"type": "string",
"description": "Unique ID"
}
}
},
"Printers": {
"id": "Printers",
"type": "object",
"description": "List of Printers.",
"properties": {
"printers": {
"type": "array",
"description": "List of printer objects.",
"items": {
"$ref": "Printer"
}
}
}
}
},
"resources": {
"jobs": {
"methods": {
"delete": {
"id": "cloudprint.jobs.delete",
"path": "deletejob",
"httpMethod": "GET",
"parameters": {
"jobid": {
"type": "string",
"location": "query",
"required": "true"
}
}
},
"fetch": {
"id": "cloudprint.jobs.fetch",
"path": "fetch",
"httpMethod": "GET",
"parameters": {
"printerid": {
"type": "string",
"required": "true",
"location": "query"
}
},
"response": {
"$ref": "Jobs"
}
},
"getticket": {
"id": "cloudprint.jobs.getticket",
"path": "ticket",
"httpMethod": "GET",
"parameters": {
"jobid": {
"type": "string",
"required": "true",
"location": "query"
},
"use_cjt": {
"type": "boolean",
"required": "true",
"location": "query"
}
}
},
"list": {
"id": "cloudprint.jobs.list",
"path": "jobs",
"httpMethod": "GET",
"parameters": {
"printerid": {
"type": "string",
"location": "query"
},
"owner": {
"type": "string",
"location": "query"
},
"status": {
"type": "string",
"location": "query"
},
"q": {
"type": "string",
"location": "query"
},
"offset": {
"type": "string",
"location": "query"
},
"limit": {
"type": "string",
"location": "query"
},
"sortorder": {
"type": "string",
"location": "query"
}
},
"response": {
"$ref": "Jobs"
}
},
"update": {
"id": "cloudprint.jobs.update",
"path": "control",
"httpMethod": "GET",
"parameters": {
"jobid": {
"type": "string",
"required": "true",
"location": "query"
},
"semantic_state_diff": {
"type": "string",
"required": "true",
"location": "query"
}
},
"response": {
"$ref": "Jobs"
}
},
"resubmit": {
"id": "cloudprint.jobs.resubmit",
"path": "resubmit",
"httpMethod": "POST",
"description": "resubmit a job to new printer.",
"parameters": {
"printerid": {
"type": "string",
"required": "true",
"location": "query"
},
"jobid": {
"type": "string",
"required": "true",
"location": "query"
},
"ticket": {
"type": "string",
"location": "query"
}
},
"response": {
"$ref": "Job"
}
},
"submit": {
"id": "cloudprint.jobs.submit",
"path": "submit",
"httpMethod": "POST",
"description": "Send a print job to cloud print.",
"request": {
"printerid": {
"type": "string",
"required": "true",
"location": "query"
},
"title": {
"type": "string",
"location": "query"
},
"ticket": {
"type": "string",
"location": "query"
},
"content": {
"type": "string",
"location": "query"
},
"contentType": {
"type": "string",
"location": "query"
},
"tag": {
"type": "string",
"location": "query"
}
},
"response": {
"$ref": "Job"
}
}
}
},
"printers": {
"methods": {
"get": {
"id": "cloudprint.printers.get",
"path": "printer",
"httpMethod": "GET",
"parameters": {
"printerid": {
"type": "string",
"required": "true",
"location": "query"
},
"extra_fields": {
"type": "string",
"location": "query"
}
},
"response": {
"$ref": "Printer"
}
},
"list": {
"id": "cloudprint.printers.list",
"path": "search",
"httpMethod": "GET",
"description": "List all printers",
"parameters": {
"q": {
"type": "string",
"description": "Query list of printers",
"location": "query"
},
"type": {
"type": "string",
"description": "limit results to printers of type",
"location": "query"
},
"connection_status": {
"type": "string",
"description": "limit results to printers with this status",
"location": "query"
},
"extra_fields": {
"type": "string",
"description": "include extra fields",
"location": "query"
}
},
"response": {
"$ref": "Printers"
}
},
"share": {
"id": "cloudprint.printers.share",
"path": "share",
"httpMethod": "GET",
"description": "Share printer with user, group or domain",
"parameters": {
"printerid": {
"type": "string",
"required": "true",
"location": "query"
},
"scope": {
"type": "string",
"location": "query"
},
"role": {
"type": "string",
"location": "query"
},
"type": {
"type": "string",
"location": "query"
},
"skip_notification": {
"type": "boolean",
"location": "query"
},
"public": {
"type": "boolean",
"location": "query"
}
}
},
"unshare": {
"id": "cloudprint.printers.unshare",
"path": "unshare",
"httpMethod": "GET",
"description": "unshare printer with user, group or domain",
"parameters": {
"printerid": {
"type": "string",
"required": "true",
"location": "query"
},
"scope": {
"type": "string",
"location": "query"
},
"public": {
"type": "string",
"location": "query"
}
}
},
"delete": {
"id": "cloudprint.printers.delete",
"path": "delete",
"httpMethod": "GET",
"description": "delete a printer",
"parameters": {
"printerid": {
"type": "string",
"required": "true",
"location": "query"
}
}
},
"update": {
"id": "cloudprint.printers.update",
"path": "update",
"httpMethod": "GET",
"description": "update a printer",
"parameters": {
"isTosAccepted": {
"type": "boolean",
"location": "query"
},
"gcpVersion": {
"type": "string",
"location": "query"
},
"setupUrl": {
"type": "string",
"location": "query"
},
"supportUrl": {
"type": "string",
"location": "query"
},
"firmware": {
"type": "string",
"location": "query"
},
"currentQuota": {
"type": "string",
"location": "query"
},
"type": {
"type": "string",
"location": "query"
},
"public": {
"type": "boolean",
"location": "query"
},
"status": {
"type": "string",
"location": "query"
},
"proxy": {
"type": "string",
"location": "query"
},
"manufacturer": {
"type": "string",
"location": "query"
},
"defaultDisplayName": {
"type": "string",
"location": "query"
},
"displayName": {
"type": "string",
"location": "query"
},
"name": {
"type": "string",
"location": "query"
},
"uuid": {
"type": "string",
"location": "query"
},
"updateUrl": {
"type": "string",
"location": "query"
},
"ownerId": {
"type": "string",
"location": "query"
},
"model": {
"type": "string",
"location": "query"
},
"description": {
"type": "string",
"location": "query"
},
"printerid": {
"type": "string",
"required": "true",
"location": "query"
},
"quotaEnabled": {
"type": "boolean",
"location": "query"
},
"dailyQuota": {
"type": "string",
"location": "query"
}
}
}
}
}
}
}

View File

@@ -29,7 +29,7 @@ gamversion="latest"
adminuser=""
regularuser=""
gam_glibc_vers="2.27 2.23"
gam_macos_vers="10.14.6 10.13.6"
gam_macos_vers="10.15.4 10.14.6 10.13.6"
while getopts "hd:a:o:b:lp:u:r:v:" OPTION
do
@@ -218,6 +218,10 @@ fi
# Temp dir for archive
#temp_archive_dir=$(mktemp -d)
temp_archive_dir=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir')
# Clean up after ourselves even if we are killed with CTRL-C
trap "rm -rf $temp_archive_dir" EXIT
echo_yellow "Downloading file $name from $browser_download_url to $temp_archive_dir."
# Save archive to temp w/o losing our path
(cd $temp_archive_dir && curl -O -L $browser_download_url)
@@ -372,6 +376,3 @@ echo_green "GAM installation and setup complete!"
if [ "$update_profile" = true ]; then
echo_green "Please restart your terminal shell or to get started right away run:\n\n$alias_line"
fi
# Clean up after ourselves even if we are killed with CTRL-C
trap "rm -rf $temp_archive_dir" EXIT

View File

@@ -7,11 +7,9 @@ from PyInstaller.utils.hooks import copy_metadata
sys.modules['FixTk'] = None
extra_files = [('cloudprint-v2.json', 'cloudprint-v2.json')]
# 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 = [(os.path.join(proot, 'cacerts.txt'), 'httplib2')]
extra_files += copy_metadata('google-api-python-client')

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,46 @@
"""Authentication/Credentials general purpose and convenience methods."""
import json
import os
from google.auth.jwt import Credentials as JWTCredentials
from gam.auth import oauth
from gam.var import _FN_OAUTH2_TXT
from gam.var import _FN_OAUTH2SERVICE_JSON
from gam.var import GC_OAUTH2_TXT
from gam.var import GC_OAUTH2SERVICE_JSON
from gam.var import GC_ENABLE_DASA
from gam.var import GC_Values
# TODO: Move logic that determines file name into this module. We should be able
# to discover the file location without accessing a private member or waiting
# for a global initialization.
DEFAULT_OAUTH_STORAGE_FILE = _FN_OAUTH2_TXT
def get_admin_credentials_filename():
"""Gets the name of the file that stores the admin account credentials."""
# If the environment globals are loaded, use the set global value. It may have
# some custom name in it. Otherwise, just use the default name.
if GC_Values[GC_OAUTH2_TXT]:
return GC_Values[GC_OAUTH2_TXT]
return DEFAULT_OAUTH_STORAGE_FILE
if GC_Values[GC_ENABLE_DASA]:
return GC_Values[GC_OAUTH2SERVICE_JSON] if GC_Values[GC_OAUTH2SERVICE_JSON] else _FN_OAUTH2SERVICE_JSON
else:
return GC_Values[GC_OAUTH2_TXT] if GC_Values[GC_OAUTH2_TXT] else _FN_OAUTH2_TXT
def get_admin_credentials():
def get_admin_credentials(api=None):
"""Gets oauth.Credentials that are authenticated as the domain's admin user."""
credential_file = get_admin_credentials_filename()
return oauth.Credentials.from_credentials_file(credential_file)
if not os.path.isfile(credential_file):
raise oauth.InvalidCredentialsFileError
with open(credential_file, 'r') as f:
creds_data = json.load(f)
# Validate that enable DASA matches content of authorization file
if GC_Values[GC_ENABLE_DASA] and 'private_key' in creds_data:
audience = f'https://{api}.googleapis.com/'
return JWTCredentials.from_service_account_info(creds_data,
audience=audience)
elif not GC_Values[GC_ENABLE_DASA] and 'token' in creds_data:
return oauth.Credentials.from_credentials_file(credential_file)
else:
raise oauth.InvalidCredentialsFileError

View File

@@ -220,7 +220,7 @@ def write_csv_file(csvRows, titles, list_type, todrive):
except IOError as e:
controlflow.system_error_exit(6, e)
if todrive:
admin_email = gam._getValueFromOAuth('email')
admin_email = gam._get_admin_email()
_, drive = gam.buildDrive3GAPIObject(admin_email)
if not drive:
print(f'''\nGAM is not authorized to create Drive files. Please run:

View File

@@ -246,9 +246,9 @@ def get_all_pages(service,
display a unique property of the first item in the current page.
LAST_ITEM_MARKER : In conjunction with `message_attribute` arg, will
display a unique property of the last item in the current page.
message_attribute: String, the name of a signature field within a single
returned item which identifies that unique item. This field is used with
`page_message` to templatize a paging status message.
message_attribute: String or list, the name of a signature field within a
single returned item which identifies that unique item. This field is used
with `page_message` to templatize a paging status message.
soft_errors: Bool, If True, writes non-fatal errors to stderr.
throw_reasons: A list of Google HTTP error reason strings indicating the
errors generated by this request should be re-thrown. All other HTTP
@@ -293,11 +293,17 @@ def get_all_pages(service,
if message_attribute:
first_item = page_items[0] if num_page_items > 0 else {}
last_item = page_items[-1] if num_page_items > 1 else first_item
show_message = show_message.replace(
FIRST_ITEM_MARKER,
str(first_item.get(message_attribute, '')))
show_message = show_message.replace(
LAST_ITEM_MARKER, str(last_item.get(message_attribute, '')))
if type(message_attribute) is str:
first_item = str(first_item.get(message_attribute, ''))
last_item = str(last_item.get(message_attribute, ''))
else:
for attr in message_attribute:
first_item = first_item.get(attr, {})
last_item = last_item.get(attr, {})
first_item = str(first_item)
last_item = str(last_item)
show_message = show_message.replace(FIRST_ITEM_MARKER, first_item)
show_message = show_message.replace(LAST_ITEM_MARKER, last_item)
sys.stderr.write('\r')
sys.stderr.flush()
sys.stderr.write(show_message)

View File

@@ -37,7 +37,7 @@ def buildCalendarDataGAPIObject(calname):
if not calname.endswith('.calendar.google.com'):
cal = gam.buildGAPIServiceObject('calendar', calendarId, False)
if cal is None:
_, cal = buildCalendarGAPIObject(gam._getValueFromOAuth('email'))
_, cal = buildCalendarGAPIObject(gam._get_admin_email())
return (calendarId, cal)
@@ -372,6 +372,7 @@ def getEventAttributes(i, calendarId, cal, body, action):
# calendars are notified of changes
sendUpdates = 'externalOnly'
action = 'update' if body else 'add'
timeZone = None
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg in ['notifyattendees', 'sendnotifications', 'sendupdates']:

View File

@@ -0,0 +1,9 @@
import gam
def build(api='cloudidentity'):
return gam.buildGAPIObject(api)
def build_dwd(api='cloudidentity'):
admin = gam._get_admin_email()
return gam.buildGAPIServiceObject(api, admin, True)

View File

@@ -0,0 +1,297 @@
import csv
import sys
import googleapiclient
import gam
from gam.var import *
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.directory import customer as gapi_directory_customer
from gam.gapi.directory import groups as gapi_directory_groups
def create():
ci = gapi_cloudidentity.build_dwd()
customer = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
device_types = gapi.get_enum_values_minus_unspecified(
ci._rootDesc['schemas']['GoogleAppsCloudidentityDevicesV1Device']['properties']['deviceType']['enum'])
body = {}
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'serialnumber':
body['serialNumber'] = sys.argv[i+1]
i += 2
elif myarg == 'devicetype':
body['deviceType'] = sys.argv[i+1].upper()
if body['deviceType'] not in device_types:
controlflow.expected_argument_exit('device_type',
', '.join(device_types),
sys.argv[i+1])
i += 2
else:
controlflow.invalid_argument_exit(sys.argv[i], 'gam create device')
if not body.get('serialNumber') or not body.get('deviceType'):
controlflow.system_error_exit(
3, 'serial_number and device_type are required arguments for "gam create device".')
result = gapi.call(ci.devices(), 'create', customer=customer, body=body)
print(f'Created device {result["response"]["name"]}')
def info():
ci = gapi_cloudidentity.build_dwd()
customer = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
name = sys.argv[3]
if not name.startswith('devices/'):
name = f'devices/{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)
display.print_json(device)
print('Device Users:')
display.print_json(device_users)
def _generic_action(action, device_user=False):
ci = gapi_cloudidentity.build_dwd()
customer = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
# bah, inconsistencies in API
if action == 'delete':
kwargs = {'customer': customer}
else:
kwargs = {'body': {'customer': customer}}
if device_user:
endpoint = ci.devices().deviceUsers()
else:
endpoint = ci.devices()
name = None
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
# The API calls it "name" but GAM will expose as "id" to avoid admin confusion.
if myarg == 'id':
name = sys.argv[i+1]
if not name.startswith('devices/'):
name = f'devices/{name}'
i += 2
else:
controlflow.invalid_argument_exit(sys.argv[i], f'gam {action} device')
if not name:
controlflow.system_error_exit(3, f'id is a required argument for "gam {action} device".')
op = gapi.call(endpoint, action, name=name, **kwargs)
print(op)
def delete():
_generic_action('delete')
def cancel_wipe():
_generic_action('cancelWipe')
def wipe():
_generic_action('wipe')
def approve_user():
_generic_action('approve', True)
def block_user():
_generic_action('block', True)
def cancel_wipe_user():
_generic_action('cancelWipe', True)
def delete_user():
_generic_action('delete', True)
def wipe_user():
_generic_action('wipe', True)
def print_():
ci = gapi_cloudidentity.build_dwd()
customer = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
parent = 'devices/-'
filter = None
get_device_users = True
get_device_views = ['COMPANY_INVENTORY', 'USER_ASSIGNED_DEVICES']
titles = []
csvRows = []
todrive = False
sortHeaders = False
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg in ['filter', 'query']:
filter = sys.argv[i+1]
i += 2
elif myarg == 'nocompanydevices':
get_device_views.remove('COMPANY_INVENTORY')
i += 1
elif myarg == 'nopersonaldevices':
get_device_views.remove('USER_ASSIGNED_DEVICES')
i += 1
elif myarg == 'todrive':
todrive = True
i += 1
elif myarg == 'sortheaders':
sortHeaders = True
i += 1
else:
controlflow.invalid_argument_exit(sys.argv[i], 'gam print devices')
view_name_map = {
'COMPANY_INVENTORY': 'Company Devices',
'USER_ASSIGNED_DEVICES': 'Personal Devices',
}
devices = []
for view in get_device_views:
view_name = view_name_map.get(view, 'Devices')
page_message = gapi.got_total_items_msg(view_name, '...\n')
devices += gapi.get_all_pages(ci.devices(), 'list', 'devices',
customer=customer, page_message=page_message,
pageSize=100, filter=filter, view=view)
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=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:
device = utils.flatten_json(device)
for a_key in device:
if a_key not in titles:
titles.append(a_key)
csvRows.append(device)
if sortHeaders:
display.sort_csv_titles(['name',], titles)
display.write_csv_file(csvRows, titles, 'Devices', todrive)
def sync():
ci = gapi_cloudidentity.build_dwd()
device_types = gapi.get_enum_values_minus_unspecified(
ci._rootDesc['schemas']['GoogleAppsCloudidentityDevicesV1Device']['properties']['deviceType']['enum'])
customer = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
filter = None
csv_file = None
serialnumber_column = 'serialNumber'
devicetype_column = 'deviceType'
static_devicetype = None
assetid_column = None
unassigned_missing_action = 'delete'
assigned_missing_action = 'donothing'
missing_actions = ['delete', 'wipe', 'donothing']
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg in ['filter', 'query']:
filter = sys.argv[i+1]
i += 2
elif myarg == 'csvfile':
csv_file = sys.argv[i+1]
i += 2
elif myarg == 'serialnumbercolumn':
serialnumber_column = sys.argv[i+1]
i += 2
elif myarg == 'devicetypecolumn':
devicetype_column = sys.argv[i+1]
i += 2
elif myarg == 'staticdevicetype':
static_devicetype = sys.argv[i+1].upper()
if static_devicetype not in device_types:
controlflow.expected_argument_exit('device_type',
', '.join(device_types),
sys.argv[i+1])
i += 2
elif myarg == 'assetidcolumn':
assetid_column = sys.argv[i+1]
i += 2
elif myarg == 'unassigned_missing_action':
unassigned_missing_action = sys.argv[i+1].lower().replace('_', '')
if unassigned_missing_action not in missing_actions:
controlflow.expected_argument_exit('unassigned_missing_action',
', '.join(missing_actions),
sys.argv[i+1])
i += 2
elif myarg == 'assigned_missing_action':
assigned_missing_action = sys.argv[i+1].lower().replace('_', '')
if assigned_missing_action not in missing_actions:
controlflow.expected_argument_exit('assigned_missing_action',
', '.join(missing_actions),
sys.argv[i+1])
i += 2
else:
controlflow.invalid_argument_exit(sys.argv[i], 'gam sync devices')
f = fileutils.open_file(csv_file)
input_file = csv.DictReader(f, restval='')
if serialnumber_column not in input_file.fieldnames:
controlflow.csv_field_error_exit(serialnumber_column, input_file.fieldnames)
if not static_devicetype and devicetype_column not in input_file.fieldnames:
controlflow.csv_field_error_exit(devicetype_column, input_file.fieldnames)
if assetid_column and assetid_column not in input_file.fieldnames:
controlflow.csv_field_error_exit(assetid_column, input_file.fieldnames)
local_devices = []
for row in input_file:
# upper() is very important to comparison since Google
# always return uppercase serials
serialnumber = row[serialnumber_column].strip().upper()
local_device = {'serialNumber': serialnumber}
if static_devicetype:
local_device['deviceType'] = static_devicetype
else:
local_device['deviceType'] = row[devicetype_column].strip()
if assetid_column:
local_device['assetTag'] = row[assetid_column].strip()
local_devices.append(local_device)
fileutils.close_file(f)
page_message = gapi.got_total_items_msg('Company Devices', '...\n')
device_fields = ['serialNumber', 'deviceType', 'lastSyncTime', 'name']
if assetid_column:
device_fields.append('assetTag')
fields = f'nextPageToken,devices({",".join(device_fields)})'
remote_devices = gapi.get_all_pages(ci.devices(), 'list', 'devices',
customer=customer, page_message=page_message,
pageSize=100, filter=filter, view='COMPANY_INVENTORY', fields=fields)
remote_device_map = {}
for remote_device in remote_devices:
sn = remote_device['serialNumber']
last_sync = remote_device.pop('lastSyncTime')
name = remote_device.pop('name')
remote_device_map[sn] = {'name': name}
if last_sync == '1970-01-01T00:00:00Z':
remote_device_map[sn]['unassigned'] = True
devices_to_add = [device for device in local_devices if device not in remote_devices]
missing_devices = [device for device in remote_devices if device not in local_devices]
print(f'Need to add {len(devices_to_add)} and remove {len(missing_devices)} devices...')
for add_device in devices_to_add:
print(f'Creating {add_device["serialNumber"]}')
try:
result = gapi.call(ci.devices(), 'create', customer=customer,
throw_reasons=[gapi_errors.ErrorReason.FOUR_O_NINE], body=add_device)
print(f' created {result["response"]["deviceType"]} device {result["response"]["name"]} with serial {result["response"]["serialNumber"]}')
except googleapiclient.errors.HttpError:
print(f' {add_device["serialNumber"]} already exists')
for missing_device in missing_devices:
sn = missing_device['serialNumber']
name = remote_device_map[sn]['name']
unassigned = remote_device_map[sn].get('unassigned')
action = unassigned_missing_action if unassigned else assigned_missing_action
if action == 'donothing':
pass
else:
if action == 'delete':
kwargs = {'customer': customer}
else:
kwargs = {'body': {'customer': customer}}
gapi.call(ci.devices(), unassigned_missing_action,
name=name, **kwargs)
print(f'{action}d {sn}')

View File

@@ -0,0 +1,752 @@
import csv
import gam
from gam.var import *
from gam import controlflow
from gam import display
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.directory import customer as gapi_directory_customer
from gam.gapi.directory import groups as gapi_directory_groups
def create():
ci = gapi_cloudidentity.build('cloudidentity_beta')
initialGroupConfig = 'EMPTY'
gapi_directory_customer.setTrueCustomerId()
parent = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
body = {
'groupKey': {
'id': gam.normalizeEmailAddressOrUID(sys.argv[3], noUid=True)
},
'parent': parent,
'labels': {
'cloudidentity.googleapis.com/groups.discussion_forum': ''
},
}
i = 4
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'name':
body['displayName'] = sys.argv[i + 1]
i += 2
elif myarg == 'description':
body['description'] = sys.argv[i + 1]
i += 2
elif myarg in ['alias', 'aliases']:
# As of 2020/06/25 this doesn't work (yet?)
aliases = sys.argv[i + 1].split(' ')
body['additionalGroupKeys'] = []
for alias in aliases:
body['additionalGroupKeys'].append({'id': alias})
i += 2
elif myarg in ['dynamic']:
# As of 2020/06/25 this doesn't work (yet?)
body['dynamicGroupMetadata'] = {
'queries': [{
'query': sys.argv[i + 1],
'resourceType': 'USER'
}]
}
i += 2
elif myarg in ['makeowner']:
initialGroupConfig = 'WITH_INITIAL_OWNER'
i += 1
else:
print('should not get here')
sys.exit(5)
print(f'Creating group {body["groupKey"]["id"]}')
gapi.call(ci.groups(),
'create',
initialGroupConfig=initialGroupConfig,
body=body)
def delete():
ci = gapi_cloudidentity.build('cloudidentity_beta')
group = sys.argv[3]
name = group_email_to_id(ci, group)
print(f'Deleting group {group}')
gapi.call(ci.groups(), 'delete', name=name)
def info():
ci = gapi_cloudidentity.build('cloudidentity_beta')
group = gam.normalizeEmailAddressOrUID(sys.argv[3])
getUsers = True
showJoinDate = True
showUpdateDate = False
i = 4
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'nousers':
getUsers = False
i += 1
elif myarg == 'nojoindate':
showJoinDate = False
i += 1
elif myarg == 'showupdatedate':
showUpdateDate = True
i += 1
else:
controlflow.invalid_argument_exit(myarg, 'gam info cigroup')
name = group_email_to_id(ci, group)
basic_info = gapi.call(ci.groups(), 'get', name=name)
display.print_json(basic_info)
if getUsers:
if not showJoinDate and not showUpdateDate:
view = 'BASIC'
pageSize = 1000
else:
view = 'FULL'
pageSize = 500
members = gapi.get_all_pages(ci.groups().memberships(),
'list',
'memberships',
parent=name,
fields='*',
pageSize=pageSize,
view=view)
print('Members:')
for member in members:
role = get_single_role(member.get('roles', [])).lower()
email = member.get('memberKey', {}).get('id')
jc_string = ''
if showJoinDate:
joined = member.get('createTime', 'Unknown')
jc_string += f' joined {joined}'
if showUpdateDate:
updated = member.get('updateTime', 'Unknown')
jc_string += f' updated {updated}'
print(
f'{role}: {email}{jc_string}'
# f' {member.get("role", ROLE_MEMBER).lower()}: {member.get("email", member["id"])} ({member["type"].lower()})'
)
print(f'Total {len(members)} users in group')
def info_member():
ci = gapi_cloudidentity.build('cloudidentity_beta')
member = gam.normalizeEmailAddressOrUID(sys.argv[3])
group = gam.normalizeEmailAddressOrUID(sys.argv[4])
group_name = gapi.call(ci.groups(),
'lookup',
groupKey_id=group,
fields='name').get('name')
member_name = gapi.call(ci.groups().memberships(),
'lookup',
parent=group_name,
memberKey_id=member,
fields='name').get('name')
member_details = gapi.call(ci.groups().memberships(),
'get',
name=member_name)
display.print_json(member_details)
UPDATE_GROUP_SUBCMDS = ['add', 'clear', 'delete', 'remove', 'sync', 'update']
GROUP_ROLES_MAP = {
'owner': ROLE_OWNER,
'owners': ROLE_OWNER,
'manager': ROLE_MANAGER,
'managers': ROLE_MANAGER,
'member': ROLE_MEMBER,
'members': ROLE_MEMBER,
}
def print_():
ci = gapi_cloudidentity.build('cloudidentity_beta')
i = 3
members = membersCountOnly = managers = managersCountOnly = owners = ownersCountOnly = False
gapi_directory_customer.setTrueCustomerId()
parent = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
memberDelimiter = '\n'
todrive = False
titles = []
csvRows = []
roles = []
sortHeaders = False
while i < len(sys.argv):
myarg = sys.argv[i].lower()
if myarg == 'todrive':
todrive = True
i += 1
elif myarg == 'delimiter':
memberDelimiter = sys.argv[i + 1]
i += 2
elif myarg == 'sortheaders':
sortHeaders = True
i += 1
elif myarg in ['members', 'memberscount']:
roles.append(ROLE_MEMBER)
members = True
if myarg == 'memberscount':
membersCountOnly = True
i += 1
elif myarg in ['owners', 'ownerscount']:
roles.append(ROLE_OWNER)
owners = True
if myarg == 'ownerscount':
ownersCountOnly = True
i += 1
elif myarg in ['managers', 'managerscount']:
roles.append(ROLE_MANAGER)
managers = True
if myarg == 'managerscount':
managersCountOnly = True
i += 1
else:
controlflow.invalid_argument_exit(sys.argv[i], 'gam print cigroups')
if roles:
if members:
display.add_titles_to_csv_file([
'MembersCount',
], titles)
if not membersCountOnly:
display.add_titles_to_csv_file([
'Members',
], titles)
if managers:
display.add_titles_to_csv_file([
'ManagersCount',
], titles)
if not managersCountOnly:
display.add_titles_to_csv_file([
'Managers',
], titles)
if owners:
display.add_titles_to_csv_file([
'OwnersCount',
], titles)
if not ownersCountOnly:
display.add_titles_to_csv_file([
'Owners',
], titles)
gam.printGettingAllItems('Groups', None)
page_message = gapi.got_total_items_first_last_msg('Groups')
entityList = gapi.get_all_pages(ci.groups(),
'list',
'groups',
page_message=page_message,
message_attribute=['groupKey', 'id'],
parent=parent,
view='FULL',
pageSize=500)
i = 0
count = len(entityList)
for groupEntity in entityList:
i += 1
groupEmail = groupEntity['groupKey']['id']
group = utils.flatten_json(groupEntity)
for a_key in group:
if a_key not in titles:
titles.append(a_key)
groupKey_id = groupEntity['name']
if roles:
sys.stderr.write(
f' Getting {roles} for {groupEmail}{gam.currentCountNL(i, count)}'
)
page_message = gapi.got_total_items_first_last_msg('Members')
validRoles, _, _ = gam._getRoleVerification(
'.'.join(roles), 'nextPageToken,members(email,id,role)')
groupMembers = gapi.get_all_pages(ci.groups().memberships(),
'list',
'memberships',
page_message=page_message,
message_attribute=['memberKey', 'id'],
soft_errors=True,
parent=groupKey_id,
view='BASIC')
if members:
membersList = []
membersCount = 0
if managers:
managersList = []
managersCount = 0
if owners:
ownersList = []
ownersCount = 0
for member in groupMembers:
member_email = member['memberKey']['id']
role = get_single_role(member.get('roles'))
if not validRoles or role in validRoles:
if role == ROLE_MEMBER:
if members:
membersCount += 1
if not membersCountOnly:
membersList.append(member_email)
elif role == ROLE_MANAGER:
if managers:
managersCount += 1
if not managersCountOnly:
managersList.append(member_email)
elif role == ROLE_OWNER:
if owners:
ownersCount += 1
if not ownersCountOnly:
ownersList.append(member_email)
elif members:
membersCount += 1
if not membersCountOnly:
membersList.append(member_email)
if members:
group['MembersCount'] = membersCount
if not membersCountOnly:
group['Members'] = memberDelimiter.join(membersList)
if managers:
group['ManagersCount'] = managersCount
if not managersCountOnly:
group['Managers'] = memberDelimiter.join(managersList)
if owners:
group['OwnersCount'] = ownersCount
if not ownersCountOnly:
group['Owners'] = memberDelimiter.join(ownersList)
csvRows.append(group)
if sortHeaders:
display.sort_csv_titles([
'Email',
], titles)
display.write_csv_file(csvRows, titles, 'Groups', todrive)
def print_members():
ci = gapi_cloudidentity.build('cloudidentity_beta')
todrive = False
gapi_directory_customer.setTrueCustomerId()
parent = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
roles = []
titles = ['group']
csvRows = []
groups_to_get = []
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'todrive':
todrive = True
i += 1
elif myarg in ['role', 'roles']:
for role in sys.argv[i + 1].lower().replace(',', ' ').split():
if role in GROUP_ROLES_MAP:
roles.append(GROUP_ROLES_MAP[role])
else:
controlflow.system_error_exit(
2,
f'{role} is not a valid role for "gam print group-members {myarg}"'
)
i += 2
elif myarg in ['cigroup', 'cigroups']:
group_email = gam.normalizeEmailAddressOrUID(sys.argv[i + 1])
groups_to_get = [group_email]
i += 2
else:
controlflow.invalid_argument_exit(sys.argv[i],
'gam print cigroup-members')
if not groups_to_get:
gam.printGettingAllItems('Groups', None)
page_message = gapi.got_total_items_first_last_msg('Groups')
groups_to_get = gapi.get_all_pages(
ci.groups(),
'list',
'groups',
message_attribute=['groupKey', 'id'],
page_message=page_message,
parent=parent,
view='BASIC',
pageSize=1000,
fields='nextPageToken,groups(groupKey(id))')
groups_to_get = [group['groupKey']['id'] for group in groups_to_get]
i = 0
count = len(groups_to_get)
for group_email in groups_to_get:
i += 1
sys.stderr.write(
f'Getting members for {group_email}{gam.currentCountNL(i, count)}')
group_id = group_email_to_id(ci, group_email)
print(f'Getting members of cigroup {group_email}...')
page_message = f' {gapi.got_total_items_first_last_msg("Members")}'
group_members = gapi.get_all_pages(
ci.groups().memberships(),
'list',
'memberships',
soft_errors=True,
parent=group_id,
view='FULL',
pageSize=500,
page_message=page_message,
message_attribute=['memberKey', 'id'])
#fields='nextPageToken,memberships(memberKey,roles,createTime,updateTime)')
if roles:
group_members = filter_members_to_roles(group_members, roles)
for member in group_members:
# reduce role to a single value
member['role'] = get_single_role(member.pop('roles'))
member = utils.flatten_json(member)
for title in member:
if title not in titles:
titles.append(title)
member['group'] = group_email
csvRows.append(member)
display.write_csv_file(csvRows, titles, 'Group Members', todrive)
def update():
# Convert foo@googlemail.com to foo@gmail.com; eliminate periods in name for foo.bar@gmail.com
def _cleanConsumerAddress(emailAddress, mapCleanToOriginal):
atLoc = emailAddress.find('@')
if atLoc > 0:
if emailAddress[atLoc + 1:] in ['gmail.com', 'googlemail.com']:
cleanEmailAddress = emailAddress[:atLoc].replace(
'.', '') + '@gmail.com'
if cleanEmailAddress != emailAddress:
mapCleanToOriginal[cleanEmailAddress] = emailAddress
return cleanEmailAddress
return emailAddress
def _getRoleAndUsers():
checkSuspended = None
role = None
i = 5
if sys.argv[i].lower() in GROUP_ROLES_MAP:
role = GROUP_ROLES_MAP[sys.argv[i].lower()]
i += 1
if sys.argv[i].lower() in ['suspended', 'notsuspended']:
checkSuspended = sys.argv[i].lower() == 'suspended'
i += 1
if sys.argv[i].lower() in usergroup_types:
users_email = gam.getUsersToModify(entity_type=sys.argv[i].lower(),
entity=sys.argv[i + 1],
checkSuspended=checkSuspended,
groupUserMembersOnly=False)
else:
users_email = [
gam.normalizeEmailAddressOrUID(sys.argv[i],
checkForCustomerId=True)
]
return (role, users_email)
ci = gapi_cloudidentity.build('cloudidentity_beta')
group = sys.argv[3]
myarg = sys.argv[4].lower()
items = []
if myarg in UPDATE_GROUP_SUBCMDS:
group = gam.normalizeEmailAddressOrUID(group)
if group.startswith('groups/'):
parent = group
else:
parent = group_email_to_id(ci, group)
if not parent:
return
if myarg == 'add':
role, users_email = _getRoleAndUsers()
if not role:
role = ROLE_MEMBER
if len(users_email) > 1:
sys.stderr.write(
f'Group: {group}, Will add {len(users_email)} {role}s.\n')
for user_email in users_email:
item = [
'gam', 'update', 'cigroup', f'id:{parent}', 'add', role,
user_email
]
items.append(item)
elif len(users_email) > 0:
body = {
'memberKey': {
'id': users_email[0]
},
'roles': [{
'name': ROLE_MEMBER
}]
}
if role != ROLE_MEMBER:
body['roles'].append({'name': role})
add_text = [f'as {role}']
for i in range(2):
try:
gapi.call(
ci.groups().memberships(),
'create',
throw_reasons=[
gapi_errors.ErrorReason.FOUR_O_NINE,
gapi_errors.ErrorReason.MEMBER_NOT_FOUND,
gapi_errors.ErrorReason.RESOURCE_NOT_FOUND,
gapi_errors.ErrorReason.INVALID_MEMBER,
gapi_errors.ErrorReason.
CYCLIC_MEMBERSHIPS_NOT_ALLOWED
],
parent=parent,
body=body)
print(
f' Group: {group}, {users_email[0]} Added {" ".join(add_text)}'
)
break
except (gapi_errors.GapiMemberNotFoundError,
gapi_errors.GapiResourceNotFoundError,
gapi_errors.GapiInvalidMemberError,
gapi_errors.GapiCyclicMembershipsNotAllowedError
) as e:
print(
f' Group: {group}, {users_email[0]} Add {" ".join(add_text)} Failed: {str(e)}'
)
break
elif myarg == 'sync':
syncMembersSet = set()
syncMembersMap = {}
role, users_email = _getRoleAndUsers()
for user_email in users_email:
if user_email in ('*', GC_Values[GC_CUSTOMER_ID]):
syncMembersSet.add(GC_Values[GC_CUSTOMER_ID])
else:
syncMembersSet.add(
_cleanConsumerAddress(user_email.lower(),
syncMembersMap))
currentMembersSet = set()
currentMembersMap = {}
for current_email in gam.getUsersToModify(
entity_type='cigroup',
entity=group,
member_type=role,
groupUserMembersOnly=False):
if current_email == GC_Values[GC_CUSTOMER_ID]:
currentMembersSet.add(current_email)
else:
currentMembersSet.add(
_cleanConsumerAddress(current_email.lower(),
currentMembersMap))
to_add = [
syncMembersMap.get(emailAddress, emailAddress)
for emailAddress in syncMembersSet - currentMembersSet
]
to_remove = [
currentMembersMap.get(emailAddress, emailAddress)
for emailAddress in currentMembersSet - syncMembersSet
]
sys.stderr.write(
f'Group: {group}, Will add {len(to_add)} and remove {len(to_remove)} {role}s.\n'
)
for user in to_add:
item = [
'gam', 'update', 'cigroup', f'id:{parent}', 'add', role,
user
]
items.append(item)
for user in to_remove:
items.append([
'gam', 'update', 'cigroup', f'id:{parent}', 'remove', user
])
elif myarg in ['delete', 'remove']:
_, users_email = _getRoleAndUsers()
if len(users_email) > 1:
sys.stderr.write(
f'Group: {group}, Will remove {len(users_email)} emails.\n')
for user_email in users_email:
items.append([
'gam', 'update', 'cigroup', f'id:{parent}', 'remove',
user_email
])
elif len(users_email) == 1:
name = membership_email_to_id(ci, parent, users_email[0])
try:
gapi.call(ci.groups().memberships(),
'delete',
throw_reasons=[
gapi_errors.ErrorReason.MEMBER_NOT_FOUND,
gapi_errors.ErrorReason.INVALID_MEMBER
],
name=name)
print(f' Group: {group}, {users_email[0]} Removed')
except (gapi_errors.GapiMemberNotFoundError,
gapi_errors.GapiInvalidMemberError) as e:
print(
f' Group: {group}, {users_email[0]} Remove Failed: {str(e)}'
)
elif myarg == 'update':
role, users_email = _getRoleAndUsers()
if not role:
role = ROLE_MEMBER
if len(users_email) > 1:
sys.stderr.write(
f'Group: {group}, Will update {len(users_email)} {role}s.\n'
)
for user_email in users_email:
item = [
'gam', 'update', 'cigroup', f'id:{parent}', 'update',
role, user_email
]
items.append(item)
elif len(users_email) > 0:
name = membership_email_to_id(ci, parent, users_email[0])
addRoles = []
removeRoles = []
new_role = {'role': role}
current_roles = gapi.call(ci.groups().memberships(),
'get',
name=name,
fields='roles').get('roles', [])
current_roles = [role['name'] for role in current_roles]
for crole in current_roles:
if crole != ROLE_MEMBER and crole != role:
removeRoles.append(crole)
if role not in current_roles:
addRoles.append({'name': role})
bodys = []
if addRoles:
bodys.append({'addRoles': addRoles})
if removeRoles:
bodys.append({'removeRoles': removeRoles})
for body in bodys:
try:
gapi.call(ci.groups().memberships(),
'modifyMembershipRoles',
throw_reasons=[
gapi_errors.ErrorReason.MEMBER_NOT_FOUND,
gapi_errors.ErrorReason.INVALID_MEMBER
],
name=name,
body=body)
except (gapi_errors.GapiMemberNotFoundError,
gapi_errors.GapiInvalidMemberError) as e:
print(
f' Group: {group}, {users_email[0]} Update to {role} Failed: {str(e)}'
)
break
print(
f' Group: {group}, {users_email[0]} Updated to {role}'
)
else: # clear
roles = []
i = 5
while i < len(sys.argv):
myarg = sys.argv[i].lower()
if myarg.upper() in [ROLE_OWNER, ROLE_MANAGER, ROLE_MEMBER]:
roles.append(myarg.upper())
i += 1
else:
controlflow.invalid_argument_exit(
sys.argv[i], 'gam update cigroup clear')
if not roles:
roles = [ROLE_MEMBER]
group = gam.normalizeEmailAddressOrUID(group)
member_type_message = f'{",".join(roles).lower()}s'
sys.stderr.write(
f'Getting {member_type_message} of {group} (may take some time for large groups)...\n'
)
page_message = gapi.got_total_items_msg(f'{member_type_message}',
'...')
try:
result = gapi.get_all_pages(
ci.groups().memberships(),
'list',
'memberships',
page_message=page_message,
throw_reasons=gapi_errors.MEMBERS_THROW_REASONS,
parent=parent,
fields='nextPageToken,memberships(memberKey,roles)')
result = filter_members_to_roles(result, roles)
if not result:
print('Group already has 0 members')
return
users_email = [member['memberKey']['id'] for member in result]
sys.stderr.write(
f'Group: {group}, Will remove {len(users_email)} {", ".join(roles).lower()}s.\n'
)
for user_email in users_email:
items.append([
'gam', 'update', 'cigroup', group, 'remove', user_email
])
except (gapi_errors.GapiGroupNotFoundError,
gapi_errors.GapiDomainNotFoundError,
gapi_errors.GapiInvalidError,
gapi_errors.GapiForbiddenError):
gam.entityUnknownWarning('Group', group, 0, 0)
if items:
gam.run_batch(items)
else:
i = 4
body = {}
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'name':
body['displayName'] = sys.argv[i + 1]
i += 2
elif myarg == 'description':
body['description'] = sys.argv[i + 1]
i += 2
elif myarg == 'security':
body['labels'] = {
'cloudidentity.googleapis.com/groups.security': '',
'cloudidentity.googleapis.com/groups.discussion_forum': ''
}
i += 1
else:
controlflow.invalid_argument_exit(sys.argv[i],
'gam update cigroup')
updateMask = ','.join(body.keys())
name = group_email_to_id(ci, group)
print(f'Updating group {group}')
gapi.call(ci.groups(),
'patch',
updateMask=updateMask,
name=name,
body=body)
def group_email_to_id(ci, group, i=0, count=0):
group = gam.normalizeEmailAddressOrUID(group)
try:
return gapi.call(ci.groups(),
'lookup',
throw_reasons=gapi_errors.GROUP_GET_THROW_REASONS,
retry_reasons=gapi_errors.GROUP_GET_RETRY_REASONS,
groupKey_id=group,
fields='name').get('name')
except (gapi_errors.GapiGroupNotFoundError,
gapi_errors.GapiDomainNotFoundError,
gapi_errors.GapiDomainCannotUseApisError,
gapi_errors.GapiForbiddenError, gapi_errors.GapiBadRequestError):
gam.entityUnknownWarning('Group', group, i, count)
return None
def membership_email_to_id(ci, parent, membership, i=0, count=0):
membership = gam.normalizeEmailAddressOrUID(membership)
try:
return gapi.call(ci.groups().memberships(),
'lookup',
throw_reasons=gapi_errors.GROUP_GET_THROW_REASONS,
retry_reasons=gapi_errors.GROUP_GET_RETRY_REASONS,
parent=parent,
memberKey_id=membership,
fields='name').get('name')
except (gapi_errors.GapiGroupNotFoundError,
gapi_errors.GapiDomainNotFoundError,
gapi_errors.GapiDomainCannotUseApisError,
gapi_errors.GapiForbiddenError, gapi_errors.GapiBadRequestError):
gam.entityUnknownWarning('Membership', membership, i, count)
return None
def get_single_role(roles):
''' returns the highest role of member '''
roles = [role.get('name') for role in roles]
if not roles:
return ROLE_MEMBER
for a_role in [ROLE_OWNER, ROLE_MANAGER, ROLE_MEMBER]:
if a_role in roles:
return a_role
return roles[0]
def filter_members_to_roles(members, roles):
filtered_members = []
for member in members:
role = get_single_role(member.get('roles', []))
if role in roles:
filtered_members.append(member)
return filtered_members

View File

@@ -1,5 +1,5 @@
import gam
def buildGAPIObject():
def build():
return gam.buildGAPIObject('directory')

View File

@@ -0,0 +1,58 @@
import sys
from gam.var import *
from gam import controlflow
from gam import gapi
from gam.gapi import directory as gapi_directory
from gam import utils
def info(users):
cd = gapi_directory.build()
for user in users:
asps = gapi.get_items(cd.asps(), 'list', 'items', userKey=user)
if asps:
print(f'Application-Specific Passwords for {user}')
for asp in asps:
if asp['creationTime'] == '0':
created_date = 'Unknown'
else:
created_date = utils.formatTimestampYMDHMS(
asp['creationTime'])
if asp['lastTimeUsed'] == '0':
used_date = 'Never'
else:
last_used = asp['lastTimeUsed']
used_date = utils.formatTimestampYMDHMS(last_used)
print(f' ID: {asp["codeId"]}\n' \
f' Name: {asp["name"]}\n' \
f' Created: {created_date}\n' \
f' Last Used: {used_date}\n')
else:
print(f' no ASPs for {user}\n')
def delete(users, cd=None, codeIdList=None):
if not cd:
cd = gapi_directory.build()
if not codeIdList:
codeIdList = sys.argv[5].lower()
if codeIdList == 'all':
allCodeIds = True
else:
allCodeIds = False
codeIds = codeIdList.replace(',', ' ').split()
for user in users:
if allCodeIds:
print(f'Getting Application Specific Passwords for {user}')
asps = gapi.get_items(cd.asps(),
'list',
'items',
userKey=user,
fields='items/codeId')
codeIds = [asp['codeId'] for asp in asps]
if not codeIds:
print('No ASPs')
for codeId in codeIds:
gapi.call(cd.asps(), 'delete', userKey=user, codeId=codeId)
print(f'deleted ASP {codeId} for {user}')

View File

@@ -7,11 +7,12 @@ from gam import display
from gam import fileutils
from gam import gapi
from gam.gapi import directory as gapi_directory
from gam.gapi.directory import orgunits as gapi_directory_orgunits
from gam import utils
def doUpdateCros():
cd = gapi_directory.buildGAPIObject()
cd = gapi_directory.build()
i, devices = getCrOSDeviceEntity(3, cd)
update_body = {}
action_body = {}
@@ -32,7 +33,7 @@ def doUpdateCros():
update_body['annotatedAssetId'] = sys.argv[i + 1]
i += 2
elif myarg in ['ou', 'org']:
orgUnitPath = gam.getOrgUnitItem(sys.argv[i + 1])
orgUnitPath = gapi_directory_orgunits.getOrgUnitItem(sys.argv[i + 1])
i += 2
elif myarg == 'action':
action = sys.argv[i + 1].lower().replace('_', '').replace('-', '')
@@ -52,11 +53,15 @@ def doUpdateCros():
elif action in ['deprovisionretiringdevice']:
action = 'deprovision'
deprovisionReason = 'retiring_device'
elif action == 'deprovisionupgradetransfer':
action = 'deprovision'
deprovisionReason = 'upgrade_transfer'
elif action not in ['disable', 'reenable']:
controlflow.system_error_exit(2, f'expected action of ' \
f'deprovision_same_model_replace, ' \
f'deprovision_different_model_replace, ' \
f'deprovision_retiring_device, disable or reenable,'
f'deprovision_retiring_device, ' \
f'deprovision_upgrade_transfer, disable or reenable,'
f' got {action}')
action_body = {'action': action}
if deprovisionReason:
@@ -120,7 +125,7 @@ def doUpdateCros():
def doGetCrosInfo():
cd = gapi_directory.buildGAPIObject()
cd = gapi_directory.build()
i, devices = getCrOSDeviceEntity(3, cd)
downloadfile = None
targetFolder = GC_Values[GC_DRIVE_DIR]
@@ -330,7 +335,7 @@ def doGetCrosInfo():
def doPrintCrosActivity():
cd = gapi_directory.buildGAPIObject()
cd = gapi_directory.build()
todrive = False
titles = [
'deviceId', 'annotatedAssetId', 'annotatedLocation', 'serialNumber',
@@ -354,7 +359,7 @@ def doPrintCrosActivity():
queries = gam.getQueries(myarg, sys.argv[i + 1])
i += 2
elif myarg == 'limittoou':
orgUnitPath = gam.getOrgUnitItem(sys.argv[i + 1])
orgUnitPath = gapi_directory_orgunits.getOrgUnitItem(sys.argv[i + 1])
i += 2
elif myarg == 'todrive':
todrive = True
@@ -501,7 +506,7 @@ def doPrintCrosDevices():
elif myarg in CROS_SYSTEM_RAM_FREE_REPORTS_ARGUMENTS:
selectedLists['systemRamFreeReports'] = True
cd = gapi_directory.buildGAPIObject()
cd = gapi_directory.build()
todrive = False
fieldsList = []
fieldsTitles = {}
@@ -522,7 +527,7 @@ def doPrintCrosDevices():
queries = gam.getQueries(myarg, sys.argv[i + 1])
i += 2
elif myarg == 'limittoou':
orgUnitPath = gam.getOrgUnitItem(sys.argv[i + 1])
orgUnitPath = gapi_directory_orgunits.getOrgUnitItem(sys.argv[i + 1])
i += 2
elif myarg == 'todrive':
todrive = True

View File

@@ -1,5 +1,6 @@
import datetime
import gam
from gam.var import *
from gam import controlflow
from gam import gapi
@@ -8,7 +9,7 @@ from gam.gapi import reports as gapi_reports
def doGetCustomerInfo():
cd = gapi_directory.buildGAPIObject()
cd = gapi_directory.build()
customer_info = gapi.call(cd.customers(),
'get',
customerKey=GC_Values[GC_CUSTOMER_ID])
@@ -69,7 +70,7 @@ def doGetCustomerInfo():
customerId = GC_Values[GC_CUSTOMER_ID]
if customerId == MY_CUSTOMER:
customerId = None
rep = gapi_reports.buildGAPIObject()
rep = gapi_reports.build()
usage = None
throw_reasons = [
gapi.errors.ErrorReason.INVALID, gapi.errors.ErrorReason.FORBIDDEN
@@ -108,7 +109,7 @@ def doGetCustomerInfo():
def doUpdateCustomer():
cd = gapi_directory.buildGAPIObject()
cd = gapi_directory.build()
body = {}
i = 3
while i < len(sys.argv):
@@ -138,3 +139,11 @@ def doUpdateCustomer():
customerKey=GC_Values[GC_CUSTOMER_ID],
body=body)
print('Updated customer')
def setTrueCustomerId():
if GC_Values[GC_CUSTOMER_ID] == MY_CUSTOMER:
cd = gapi_directory.build()
GC_Values[GC_CUSTOMER_ID] = gapi.call(cd.customers(), 'get',
customerKey=GC_Values[GC_CUSTOMER_ID],
fields='id').get('id', GC_Values[GC_CUSTOMER_ID])

View File

@@ -0,0 +1,76 @@
import sys
from gam.var import *
from gam import controlflow
from gam import display
from gam import gapi
from gam.gapi import directory as gapi_directory
from gam import utils
def create():
cd = gapi_directory.build()
body = {'domainAliasName': sys.argv[3], 'parentDomainName': sys.argv[4]}
print(f'Adding {body["domainAliasName"]} alias for ' \
f'{body["parentDomainName"]}')
gapi.call(cd.domainAliases(),
'insert',
customer=GC_Values[GC_CUSTOMER_ID],
body=body)
def delete():
cd = gapi_directory.build()
domainAliasName = sys.argv[3]
print(f'Deleting domain alias {domainAliasName}')
gapi.call(cd.domainAliases(),
'delete',
customer=GC_Values[GC_CUSTOMER_ID],
domainAliasName=domainAliasName)
def info():
cd = gapi_directory.build()
alias = sys.argv[3]
result = gapi.call(cd.domainAliases(),
'get',
customer=GC_Values[GC_CUSTOMER_ID],
domainAliasName=alias)
if 'creationTime' in result:
result['creationTime'] = utils.formatTimestampYMDHMSF(
result['creationTime'])
display.print_json(result)
def print_():
cd = gapi_directory.build()
todrive = False
titles = [
'domainAliasName',
]
csvRows = []
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower()
if myarg == 'todrive':
todrive = True
i += 1
else:
controlflow.invalid_argument_exit(sys.argv[i],
'gam print domainaliases')
results = gapi.call(cd.domainAliases(),
'list',
customer=GC_Values[GC_CUSTOMER_ID])
for domainAlias in results['domainAliases']:
domainAlias_attributes = {}
for attr in domainAlias:
if attr in ['kind', 'etag']:
continue
if attr == 'creationTime':
domainAlias[attr] = utils.formatTimestampYMDHMSF(
domainAlias[attr])
if attr not in titles:
titles.append(attr)
domainAlias_attributes[attr] = domainAlias[attr]
csvRows.append(domainAlias_attributes)
display.write_csv_file(csvRows, titles, 'Domains', todrive)

View File

@@ -0,0 +1,124 @@
import sys
from gam.var import *
from gam import controlflow
from gam import display
from gam import gapi
from gam.gapi import directory as gapi_directory
from gam.gapi.directory import customer as gapi_directory_customer
from gam import utils
def create():
cd = gapi_directory.build()
domain_name = sys.argv[3]
body = {'domainName': domain_name}
gapi.call(cd.domains(),
'insert',
customer=GC_Values[GC_CUSTOMER_ID],
body=body)
print(f'Added domain {domain_name}')
def info():
if (len(sys.argv) < 4) or (sys.argv[3] == 'logo'):
gapi_directory_customer.doGetCustomerInfo()
return
cd = gapi_directory.build()
domainName = sys.argv[3]
result = gapi.call(cd.domains(),
'get',
customer=GC_Values[GC_CUSTOMER_ID],
domainName=domainName)
if 'creationTime' in result:
result['creationTime'] = utils.formatTimestampYMDHMSF(
result['creationTime'])
if 'domainAliases' in result:
for i in range(0, len(result['domainAliases'])):
if 'creationTime' in result['domainAliases'][i]:
result['domainAliases'][i][
'creationTime'] = utils.formatTimestampYMDHMSF(
result['domainAliases'][i]['creationTime'])
display.print_json(result)
def update():
cd = gapi_directory.build()
domain_name = sys.argv[3]
i = 4
body = {}
while i < len(sys.argv):
myarg = sys.argv[i].lower()
if myarg == 'primary':
body['customerDomain'] = domain_name
i += 1
else:
controlflow.invalid_argument_exit(sys.argv[i], 'gam update domain')
gapi.call(cd.customers(),
'update',
customerKey=GC_Values[GC_CUSTOMER_ID],
body=body)
print(f'{domain_name} is now the primary domain.')
def delete():
cd = gapi_directory.build()
domainName = sys.argv[3]
print(f'Deleting domain {domainName}')
gapi.call(cd.domains(),
'delete',
customer=GC_Values[GC_CUSTOMER_ID],
domainName=domainName)
def print_():
cd = gapi_directory.build()
todrive = False
titles = [
'domainName',
]
csvRows = []
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower()
if myarg == 'todrive':
todrive = True
i += 1
else:
controlflow.invalid_argument_exit(sys.argv[i], 'gam print domains')
results = gapi.call(cd.domains(),
'list',
customer=GC_Values[GC_CUSTOMER_ID])
for domain in results.get('domains', []):
domain_attributes = {}
domain['type'] = ['secondary', 'primary'][domain['isPrimary']]
for attr in domain:
if attr in ['kind', 'etag', 'domainAliases', 'isPrimary']:
continue
if attr in [
'creationTime',
]:
domain[attr] = utils.formatTimestampYMDHMSF(domain[attr])
if attr not in titles:
titles.append(attr)
domain_attributes[attr] = domain[attr]
csvRows.append(domain_attributes)
if 'domainAliases' in domain:
for aliasdomain in domain['domainAliases']:
aliasdomain['domainName'] = aliasdomain['domainAliasName']
del aliasdomain['domainAliasName']
aliasdomain['type'] = 'alias'
aliasdomain_attributes = {}
for attr in aliasdomain:
if attr in ['kind', 'etag']:
continue
if attr in [
'creationTime',
]:
aliasdomain[attr] = utils.formatTimestampYMDHMSF(
aliasdomain[attr])
if attr not in titles:
titles.append(attr)
aliasdomain_attributes[attr] = aliasdomain[attr]
csvRows.append(aliasdomain_attributes)
display.write_csv_file(csvRows, titles, 'Domains', todrive)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,239 @@
import sys
import uuid
import gam
from gam.var import *
from gam import controlflow
from gam import display
from gam import gapi
from gam.gapi import directory as gapi_directory
from gam import utils
def delete():
cd = gapi_directory.build()
resourceId = sys.argv[3]
gapi.call(cd.mobiledevices(),
'delete',
resourceId=resourceId,
customerId=GC_Values[GC_CUSTOMER_ID])
def info():
cd = gapi_directory.build()
resourceId = sys.argv[3]
info = gapi.call(cd.mobiledevices(),
'get',
customerId=GC_Values[GC_CUSTOMER_ID],
resourceId=resourceId)
if 'deviceId' in info:
info['deviceId'] = info['deviceId'].encode('unicode-escape').decode(
UTF8)
attrib = 'securityPatchLevel'
if attrib in info and int(info[attrib]):
info[attrib] = utils.formatTimestampYMDHMS(info[attrib])
display.print_json(info)
def print_():
cd = gapi_directory.build()
todrive = False
titles = []
csvRows = []
fields = None
projection = orderBy = sortOrder = None
queries = [None]
delimiter = ' '
listLimit = 1
appsLimit = -1
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'todrive':
todrive = True
i += 1
elif myarg in ['query', 'queries']:
queries = gam.getQueries(myarg, sys.argv[i + 1])
i += 2
elif myarg == 'delimiter':
delimiter = sys.argv[i + 1]
i += 2
elif myarg == 'listlimit':
listLimit = gam.getInteger(sys.argv[i + 1], myarg, minVal=-1)
i += 2
elif myarg == 'appslimit':
appsLimit = gam.getInteger(sys.argv[i + 1], myarg, minVal=-1)
i += 2
elif myarg == 'fields':
fields = f'nextPageToken,mobiledevices({sys.argv[i+1]})'
i += 2
elif myarg == 'orderby':
orderBy = sys.argv[i + 1].lower()
validOrderBy = [
'deviceid', 'email', 'lastsync', 'model', 'name', 'os',
'status', 'type'
]
if orderBy not in validOrderBy:
controlflow.expected_argument_exit('orderby',
', '.join(validOrderBy),
orderBy)
if orderBy == 'lastsync':
orderBy = 'lastSync'
elif orderBy == 'deviceid':
orderBy = 'deviceId'
i += 2
elif myarg in SORTORDER_CHOICES_MAP:
sortOrder = SORTORDER_CHOICES_MAP[myarg]
i += 1
elif myarg in PROJECTION_CHOICES_MAP:
projection = PROJECTION_CHOICES_MAP[myarg]
i += 1
else:
controlflow.invalid_argument_exit(sys.argv[i], 'gam print mobile')
for query in queries:
gam.printGettingAllItems('Mobile Devices', query)
page_message = gapi.got_total_items_msg('Mobile Devices', '...\n')
all_mobile = gapi.get_all_pages(cd.mobiledevices(),
'list',
'mobiledevices',
page_message=page_message,
customerId=GC_Values[GC_CUSTOMER_ID],
query=query,
projection=projection,
fields=fields,
orderBy=orderBy,
sortOrder=sortOrder)
for mobile in all_mobile:
row = {}
for attrib in mobile:
if attrib in ['kind', 'etag']:
continue
if attrib in ['name', 'email', 'otherAccountsInfo']:
if attrib not in titles:
titles.append(attrib)
if listLimit > 0:
row[attrib] = delimiter.join(
mobile[attrib][0:listLimit])
elif listLimit == 0:
row[attrib] = delimiter.join(mobile[attrib])
elif attrib == 'applications':
if appsLimit >= 0:
if attrib not in titles:
titles.append(attrib)
applications = []
j = 0
for app in mobile[attrib]:
j += 1
if appsLimit and (j > appsLimit):
break
appDetails = []
for field in [
'displayName', 'packageName', 'versionName'
]:
appDetails.append(app.get(field, '<None>'))
appDetails.append(
str(app.get('versionCode', '<None>')))
permissions = app.get('permission', [])
if permissions:
appDetails.append('/'.join(permissions))
else:
appDetails.append('<None>')
applications.append('-'.join(appDetails))
row[attrib] = delimiter.join(applications)
else:
if attrib not in titles:
titles.append(attrib)
if attrib == 'deviceId':
row[attrib] = mobile[attrib].encode(
'unicode-escape').decode(UTF8)
elif attrib == 'securityPatchLevel' and int(mobile[attrib]):
row[attrib] = utils.formatTimestampYMDHMS(
mobile[attrib])
else:
row[attrib] = mobile[attrib]
csvRows.append(row)
display.sort_csv_titles(
['resourceId', 'deviceId', 'serialNumber', 'name', 'email', 'status'],
titles)
display.write_csv_file(csvRows, titles, 'Mobile', todrive)
def update():
cd = gapi_directory.build
resourceIds = sys.argv[3]
match_users = None
doit = False
if resourceIds[:6] == 'query:':
query = resourceIds[6:]
fields = 'nextPageToken,mobiledevices(resourceId,email)'
page_message = gapi.got_total_items_msg('Mobile Devices', '...\n')
devices = gapi.get_all_pages(cd.mobiledevices(),
'list',
page_message=page_message,
customerId=GC_Values[GC_CUSTOMER_ID],
items='mobiledevices',
query=query,
fields=fields)
else:
devices = [{'resourceId': resourceIds, 'email': ['not set']}]
doit = True
i = 4
body = {}
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'action':
body['action'] = sys.argv[i + 1].lower()
validActions = [
'wipe', 'wipeaccount', 'accountwipe', 'wipe_account',
'account_wipe', 'approve', 'block',
'cancel_remote_wipe_then_activate',
'cancel_remote_wipe_then_block'
]
if body['action'] not in validActions:
controlflow.expected_argument_exit('action',
', '.join(validActions),
body['action'])
if body['action'] == 'wipe':
body['action'] = 'admin_remote_wipe'
elif body['action'].replace('_',
'') in ['accountwipe', 'wipeaccount']:
body['action'] = 'admin_account_wipe'
i += 2
elif myarg in ['ifusers', 'matchusers']:
match_users = gam.getUsersToModify(entity_type=sys.argv[i + 1].lower(),
entity=sys.argv[i + 2])
i += 3
elif myarg == 'doit':
doit = True
i += 1
else:
controlflow.invalid_argument_exit(sys.argv[i], 'gam update mobile')
if body:
if doit:
print(f'Updating {len(devices)} devices')
describe_as = 'Performing'
else:
print(
f'Showing {len(devices)} changes that would be made, not actually making changes because doit argument not specified'
)
describe_as = 'Would perform'
for device in devices:
device_user = device.get('email', [''])[0]
if match_users and device_user not in match_users:
print(
f'Skipping device for user {device_user} that did not match match_users argument'
)
else:
print(
f'{describe_as} {body["action"]} on user {device_user} device {device["resourceId"]}'
)
if doit:
gapi.call(cd.mobiledevices(),
'action',
resourceId=device['resourceId'],
body=body,
customerId=GC_Values[GC_CUSTOMER_ID])

View File

@@ -0,0 +1,422 @@
import sys
import gam
from gam.var import *
from gam import controlflow
from gam import display
from gam import gapi
from gam.gapi import directory as gapi_directory
from gam.gapi import errors as gapi_errors
from gam import utils
def create():
cd = gapi_directory.build()
name = getOrgUnitItem(sys.argv[3], pathOnly=True, absolutePath=False)
parent = ''
body = {}
i = 4
while i < len(sys.argv):
myarg = sys.argv[i].lower()
if myarg == 'description':
body['description'] = sys.argv[i + 1].replace('\\n', '\n')
i += 2
elif myarg == 'parent':
parent = getOrgUnitItem(sys.argv[i + 1])
i += 2
elif myarg == 'noinherit':
body['blockInheritance'] = True
i += 1
elif myarg == 'inherit':
body['blockInheritance'] = False
i += 1
else:
controlflow.invalid_argument_exit(sys.argv[i], 'gam create org')
if parent.startswith('id:'):
parent = gapi.call(cd.orgunits(),
'get',
customerId=GC_Values[GC_CUSTOMER_ID],
orgUnitPath=parent,
fields='orgUnitPath')['orgUnitPath']
if parent == '/':
orgUnitPath = parent + name
else:
orgUnitPath = parent + '/' + name
if orgUnitPath.count('/') > 1:
body['parentOrgUnitPath'], body['name'] = orgUnitPath.rsplit('/', 1)
else:
body['parentOrgUnitPath'] = '/'
body['name'] = orgUnitPath[1:]
parent = body['parentOrgUnitPath']
gapi.call(cd.orgunits(),
'insert',
customerId=GC_Values[GC_CUSTOMER_ID],
body=body,
retry_reasons=[gapi_errors.ErrorReason.DAILY_LIMIT_EXCEEDED])
print(f'Created OrgUnit {body["name"]}')
def delete():
cd = gapi_directory.build()
name = getOrgUnitItem(sys.argv[3])
print(f'Deleting organization {name}')
gapi.call(cd.orgunits(),
'delete',
customerId=GC_Values[GC_CUSTOMER_ID],
orgUnitPath=encodeOrgUnitPath(makeOrgUnitPathRelative(name)))
def info(name=None, return_attrib=None):
cd = gapi_directory.build()
checkSuspended = None
if not name:
name = getOrgUnitItem(sys.argv[3])
get_users = True
show_children = False
i = 4
while i < len(sys.argv):
myarg = sys.argv[i].lower()
if myarg == 'nousers':
get_users = False
i += 1
elif myarg in ['children', 'child']:
show_children = True
i += 1
elif myarg in ['suspended', 'notsuspended']:
checkSuspended = myarg == 'suspended'
i += 1
else:
controlflow.invalid_argument_exit(sys.argv[i], 'gam info org')
if name == '/':
orgs = gapi.call(cd.orgunits(),
'list',
customerId=GC_Values[GC_CUSTOMER_ID],
type='children',
fields='organizationUnits/parentOrgUnitId')
if 'organizationUnits' in orgs and orgs['organizationUnits']:
name = orgs['organizationUnits'][0]['parentOrgUnitId']
else:
topLevelOrgId = getTopLevelOrgId(cd, '/')
if topLevelOrgId:
name = topLevelOrgId
else:
name = makeOrgUnitPathRelative(name)
result = gapi.call(cd.orgunits(),
'get',
customerId=GC_Values[GC_CUSTOMER_ID],
orgUnitPath=encodeOrgUnitPath(name))
if return_attrib:
return result[return_attrib]
display.print_json(result)
if get_users:
name = result['orgUnitPath']
page_message = gapi.got_total_items_first_last_msg('Users')
users = gapi.get_all_pages(
cd.users(),
'list',
'users',
page_message=page_message,
message_attribute='primaryEmail',
customer=GC_Values[GC_CUSTOMER_ID],
query=orgUnitPathQuery(name, checkSuspended),
fields='users(primaryEmail,orgUnitPath),nextPageToken')
if checkSuspended is None:
print('Users:')
elif not checkSuspended:
print('Users (Not suspended):')
else:
print('Users (Suspended):')
for user in users:
if show_children or (name.lower() == user['orgUnitPath'].lower()):
sys.stdout.write(f' {user["primaryEmail"]}')
if name.lower() != user['orgUnitPath'].lower():
print(' (child)')
else:
print('')
def print_():
print_order = [
'orgUnitPath', 'orgUnitId', 'name', 'description', 'parentOrgUnitPath',
'parentOrgUnitId', 'blockInheritance'
]
cd = gapi_directory.build()
listType = 'all'
orgUnitPath = '/'
todrive = False
fields = ['orgUnitPath', 'name', 'orgUnitId', 'parentOrgUnitId']
titles = []
csvRows = []
parentOrgIds = []
retrievedOrgIds = []
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'todrive':
todrive = True
i += 1
elif myarg == 'toplevelonly':
listType = 'children'
i += 1
elif myarg == 'fromparent':
orgUnitPath = getOrgUnitItem(sys.argv[i + 1])
i += 2
elif myarg == 'allfields':
fields = None
i += 1
elif myarg == 'fields':
fields += sys.argv[i + 1].split(',')
i += 2
else:
controlflow.invalid_argument_exit(sys.argv[i], 'gam print orgs')
gam.printGettingAllItems('Organizational Units', None)
if fields:
get_fields = ','.join(fields)
list_fields = f'organizationUnits({get_fields})'
else:
list_fields = None
get_fields = None
orgs = gapi.call(cd.orgunits(),
'list',
customerId=GC_Values[GC_CUSTOMER_ID],
type=listType,
orgUnitPath=orgUnitPath,
fields=list_fields)
if not 'organizationUnits' in orgs:
topLevelOrgId = getTopLevelOrgId(cd, orgUnitPath)
if topLevelOrgId:
parentOrgIds.append(topLevelOrgId)
orgunits = []
else:
orgunits = orgs['organizationUnits']
for row in orgunits:
retrievedOrgIds.append(row['orgUnitId'])
if row['parentOrgUnitId'] not in parentOrgIds:
parentOrgIds.append(row['parentOrgUnitId'])
missing_parents = set(parentOrgIds) - set(retrievedOrgIds)
for missing_parent in missing_parents:
try:
result = gapi.call(cd.orgunits(),
'get',
throw_reasons=['required'],
customerId=GC_Values[GC_CUSTOMER_ID],
orgUnitPath=missing_parent,
fields=get_fields)
orgunits.append(result)
except:
pass
for row in orgunits:
orgEntity = {}
for key, value in list(row.items()):
if key in ['kind', 'etag', 'etags']:
continue
if key not in titles:
titles.append(key)
orgEntity[key] = value
csvRows.append(orgEntity)
for title in titles:
if title not in print_order:
print_order.append(title)
titles = sorted(titles, key=print_order.index)
# sort results similar to how they list in admin console
csvRows.sort(key=lambda x: x['orgUnitPath'].lower(), reverse=False)
display.write_csv_file(csvRows, titles, 'Orgs', todrive)
def update():
cd = gapi_directory.build()
orgUnitPath = getOrgUnitItem(sys.argv[3])
if sys.argv[4].lower() in ['move', 'add']:
entity_type = sys.argv[5].lower()
if entity_type in usergroup_types:
users = getUsersToModify(entity_type=entity_type,
entity=sys.argv[6])
else:
entity_type = 'users'
users = getUsersToModify(entity_type=entity_type,
entity=sys.argv[5])
if (entity_type.startswith('cros')) or (
(entity_type == 'all') and (sys.argv[6].lower() == 'cros')):
for l in range(0, len(users), 50):
move_body = {'deviceIds': users[l:l + 50]}
print(
f' moving {len(move_body["deviceIds"])} devices to {orgUnitPath}'
)
gapi.call(cd.chromeosdevices(),
'moveDevicesToOu',
customerId=GC_Values[GC_CUSTOMER_ID],
orgUnitPath=orgUnitPath,
body=move_body)
else:
i = 0
count = len(users)
for user in users:
i += 1
sys.stderr.write(
f' moving {user} to {orgUnitPath}{currentCountNL(i, count)}'
)
try:
gapi.call(cd.users(),
'update',
throw_reasons=[
gapi_errors.ErrorReason.CONDITION_NOT_MET
],
userKey=user,
body={'orgUnitPath': orgUnitPath})
except gapi_errors.GapiConditionNotMetError:
pass
else:
body = {}
i = 4
while i < len(sys.argv):
myarg = sys.argv[i].lower()
if myarg == 'name':
body['name'] = sys.argv[i + 1]
i += 2
elif myarg == 'description':
body['description'] = sys.argv[i + 1].replace('\\n', '\n')
i += 2
elif myarg == 'parent':
parent = getOrgUnitItem(sys.argv[i + 1])
if parent.startswith('id:'):
body['parentOrgUnitId'] = parent
else:
body['parentOrgUnitPath'] = parent
i += 2
elif myarg == 'noinherit':
body['blockInheritance'] = True
i += 1
elif myarg == 'inherit':
body['blockInheritance'] = False
i += 1
else:
controlflow.invalid_argument_exit(sys.argv[i], 'gam update org')
gapi.call(cd.orgunits(),
'update',
customerId=GC_Values[GC_CUSTOMER_ID],
orgUnitPath=encodeOrgUnitPath(
makeOrgUnitPathRelative(orgUnitPath)),
body=body)
def orgUnitPathQuery(path, checkSuspended):
query = "orgUnitPath='{0}'".format(path.replace(
"'", "\\'")) if path != '/' else ''
if checkSuspended is not None:
query += f' isSuspended={checkSuspended}'
return query
def makeOrgUnitPathAbsolute(path):
if path == '/':
return path
if path.startswith('/'):
return path.rstrip('/')
if path.startswith('id:'):
return path
if path.startswith('uid:'):
return path[1:]
return '/' + path.rstrip('/')
def makeOrgUnitPathRelative(path):
if path == '/':
return path
if path.startswith('/'):
return path[1:].rstrip('/')
if path.startswith('id:'):
return path
if path.startswith('uid:'):
return path[1:]
return path.rstrip('/')
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
def getOrgUnitItem(orgUnit, pathOnly=False, absolutePath=True):
if pathOnly and (orgUnit.startswith('id:') or orgUnit.startswith('uid:')):
controlflow.system_error_exit(
2, f'{orgUnit} is not valid in this context')
if absolutePath:
return makeOrgUnitPathAbsolute(orgUnit)
return makeOrgUnitPathRelative(orgUnit)
def getTopLevelOrgId(cd, orgUnitPath):
try:
# create a temp org so we can learn what the top level org ID is (sigh)
temp_org = gapi.call(cd.orgunits(),
'insert',
customerId=GC_Values[GC_CUSTOMER_ID],
body={
'name': 'temp-delete-me',
'parentOrgUnitPath': orgUnitPath
},
fields='parentOrgUnitId,orgUnitId')
gapi.call(cd.orgunits(),
'delete',
customerId=GC_Values[GC_CUSTOMER_ID],
orgUnitPath=temp_org['orgUnitId'])
return temp_org['parentOrgUnitId']
except:
pass
return None
def getOrgUnitId(orgUnit, cd=None):
if cd is None:
cd = buildGAPIObject('directory')
orgUnit = getOrgUnitItem(orgUnit)
if orgUnit[:3] == 'id:':
return (orgUnit, orgUnit)
if orgUnit == '/':
result = gapi.call(cd.orgunits(),
'list',
customerId=GC_Values[GC_CUSTOMER_ID],
orgUnitPath='/',
type='children',
fields='organizationUnits(parentOrgUnitId)')
if result.get('organizationUnits', []):
return (orgUnit, result['organizationUnits'][0]['parentOrgUnitId'])
topLevelOrgId = getTopLevelOrgId(cd, '/')
if topLevelOrgId:
return (orgUnit, topLevelOrgId)
return (orgUnit, '/') #Bogus but should never happen
result = gapi.call(cd.orgunits(),
'get',
customerId=GC_Values[GC_CUSTOMER_ID],
orgUnitPath=encodeOrgUnitPath(
makeOrgUnitPathRelative(orgUnit)),
fields='orgUnitId')
return (orgUnit, result['orgUnitId'])
def buildOrgUnitIdToNameMap():
cd = gapi_directory.build()
result = gapi.call(cd.orgunits(),
'list',
customerId=GC_Values[GC_CUSTOMER_ID],
fields='organizationUnits(orgUnitPath,orgUnitId)',
type='all')
GM_Globals[GM_MAP_ORGUNIT_ID_TO_NAME] = {}
for orgUnit in result['organizationUnits']:
GM_Globals[GM_MAP_ORGUNIT_ID_TO_NAME][
orgUnit['orgUnitId']] = orgUnit['orgUnitPath']
def orgunit_from_orgunitid(orgunitid):
if not GM_Globals[GM_MAP_ORGUNIT_ID_TO_NAME]:
buildOrgUnitIdToNameMap()
return GM_Globals[GM_MAP_ORGUNIT_ID_TO_NAME].get(orgunitid, orgunitid)

View File

@@ -0,0 +1,32 @@
from gam.var import GC_Values, GC_CUSTOMER_ID
from gam import display
from gam import gapi
from gam.gapi import directory as gapi_directory
def flatten_privilege_list(privs, parent=None):
flat_privs = []
for priv in privs:
children = []
if parent:
priv['parent'] = parent
if priv.get('childPrivileges'):
children = flatten_privilege_list(priv['childPrivileges'],
parent=priv['privilegeName'])
priv['children'] = ' '.join(
[child['privilegeName'] for child in children])
del (priv['childPrivileges'])
flat_privs = flat_privs + children
flat_privs.append(priv)
return flat_privs
def print_(return_only=False):
cd = gapi_directory.build()
privs = gapi.call(cd.privileges(),
'list',
customer=GC_Values[GC_CUSTOMER_ID])
privs = flatten_privilege_list(privs.get('items', []))
if return_only:
return privs
display.print_json(privs)

View File

@@ -12,7 +12,7 @@ from gam import utils
def printBuildings():
to_drive = False
cd = gapi_directory.buildGAPIObject()
cd = gapi_directory.build()
titles = []
csvRows = []
fieldsList = ['buildingId']
@@ -67,7 +67,7 @@ def printBuildings():
def printResourceCalendars():
cd = gapi_directory.buildGAPIObject()
cd = gapi_directory.build()
todrive = False
fieldsList = []
fieldsTitles = {}
@@ -182,7 +182,7 @@ RESCAL_ARGUMENT_TO_PROPERTY_MAP = {
def printFeatures():
to_drive = False
cd = gapi_directory.buildGAPIObject()
cd = gapi_directory.build()
titles = []
csvRows = []
fieldsList = ['name']
@@ -260,7 +260,7 @@ def _getBuildingAttributes(args, body={}):
def createBuilding():
cd = gapi_directory.buildGAPIObject()
cd = gapi_directory.build()
body = {
'floorNames': ['1'],
'buildingId': str(uuid.uuid4()),
@@ -346,7 +346,7 @@ def getBuildingNameById(cd, buildingId):
def updateBuilding():
cd = gapi_directory.buildGAPIObject()
cd = gapi_directory.build()
buildingId = getBuildingByNameOrId(cd, sys.argv[3])
body = _getBuildingAttributes(sys.argv[4:])
print(f'Updating building {buildingId}...')
@@ -358,7 +358,7 @@ def updateBuilding():
def getBuildingInfo():
cd = gapi_directory.buildGAPIObject()
cd = gapi_directory.build()
buildingId = getBuildingByNameOrId(cd, sys.argv[3])
building = gapi.call(cd.resources().buildings(),
'get',
@@ -374,7 +374,7 @@ def getBuildingInfo():
def deleteBuilding():
cd = gapi_directory.buildGAPIObject()
cd = gapi_directory.build()
buildingId = getBuildingByNameOrId(cd, sys.argv[3])
print(f'Deleting building {buildingId}...')
gapi.call(cd.resources().buildings(),
@@ -397,7 +397,7 @@ def _getFeatureAttributes(args, body={}):
def createFeature():
cd = gapi_directory.buildGAPIObject()
cd = gapi_directory.build()
body = _getFeatureAttributes(sys.argv[3:])
print(f'Creating feature {body["name"]}...')
gapi.call(cd.resources().features(),
@@ -410,7 +410,7 @@ def updateFeature():
# update does not work for name and name is only field to be updated
# if additional writable fields are added to feature in the future
# we'll add support for update as well as rename
cd = gapi_directory.buildGAPIObject()
cd = gapi_directory.build()
oldName = sys.argv[3]
body = {'newName': sys.argv[5:]}
print(f'Updating feature {oldName}...')
@@ -422,7 +422,7 @@ def updateFeature():
def deleteFeature():
cd = gapi_directory.buildGAPIObject()
cd = gapi_directory.build()
featureKey = sys.argv[3]
print(f'Deleting feature {featureKey}...')
gapi.call(cd.resources().features(),
@@ -480,7 +480,7 @@ def _getResourceCalendarAttributes(cd, args, body={}):
def createResourceCalendar():
cd = gapi_directory.buildGAPIObject()
cd = gapi_directory.build()
body = {'resourceId': sys.argv[3], 'resourceName': sys.argv[4]}
body = _getResourceCalendarAttributes(cd, sys.argv[5:], body)
print(f'Creating resource {body["resourceId"]}...')
@@ -491,7 +491,7 @@ def createResourceCalendar():
def updateResourceCalendar():
cd = gapi_directory.buildGAPIObject()
cd = gapi_directory.build()
resId = sys.argv[3]
body = _getResourceCalendarAttributes(cd, sys.argv[4:])
# Use patch since it seems to work better.
@@ -506,7 +506,7 @@ def updateResourceCalendar():
def getResourceCalendarInfo():
cd = gapi_directory.buildGAPIObject()
cd = gapi_directory.build()
resId = sys.argv[3]
resource = gapi.call(cd.resources().calendars(),
'get',
@@ -526,7 +526,7 @@ def getResourceCalendarInfo():
def deleteResourceCalendar():
resId = sys.argv[3]
cd = gapi_directory.buildGAPIObject()
cd = gapi_directory.build()
print(f'Deleting resource calendar {resId}')
gapi.call(cd.resources().calendars(),
'delete',

View File

@@ -0,0 +1,124 @@
import sys
from gam.var import GC_Values, GC_CUSTOMER_ID
import gam
from gam import controlflow
from gam import display
from gam import gapi
from gam.gapi import directory as gapi_directory
from gam.gapi.directory import privileges as gapi_directory_privileges
def getPrivileges(body, privs, action):
all_privileges = gapi_directory_privileges.print_(return_only=True)
if privs == 'ALL':
body['rolePrivileges'] = [
{'privilegeName': p['privilegeName'], 'serviceId': p['serviceId']} for p in all_privileges
]
elif privs == 'ALL_OU':
body['rolePrivileges'] = [
{'privilegeName': p['privilegeName'], 'serviceId': p['serviceId']} for p in all_privileges if p.get('isOuScopable')
]
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
else:
controlflow.invalid_argument_exit(priv,
f'gam {action} adminrole privileges')
def create():
cd = gapi_directory.build()
body = {'roleName': sys.argv[3]}
i = 4
while i < len(sys.argv):
myarg = sys.argv[i].lower()
if myarg == 'privileges':
getPrivileges(body, sys.argv[i + 1].upper(), 'create')
i += 2
elif myarg == 'description':
body['roleDescription'] = sys.argv[i + 1]
i += 2
else:
controlflow.invalid_argument_exit(sys.argv[i],
'gam create adminrole')
if not body.get('rolePrivileges'):
controlflow.missing_argument_exit('privileges',
'gam create adminrole')
print(f'Creating role {body["roleName"]}')
gapi.call(cd.roles(),
'insert',
customer=GC_Values[GC_CUSTOMER_ID],
body=body)
def update():
cd = gapi_directory.build()
body = {}
roleId = gam.getRoleId(sys.argv[3])
i = 4
while i < len(sys.argv):
myarg = sys.argv[i].lower()
if myarg == 'privileges':
getPrivileges(body, sys.argv[i + 1].upper(), 'update')
i += 2
elif myarg == 'description':
body['roleDescription'] = sys.argv[i + 1]
i += 2
elif myarg == 'name':
body['roleName'] = sys.argv[i + 1]
i += 2
else:
controlflow.invalid_argument_exit(sys.argv[i],
'gam update adminrole')
print(f'Updating role {roleId}')
gapi.call(cd.roles(),
'patch',
customer=GC_Values[GC_CUSTOMER_ID],
roleId=roleId,
body=body)
def delete():
cd = gapi_directory.build()
roleId = gam.getRoleId(sys.argv[3])
print(f'Deleting role {roleId}')
gapi.call(cd.roles(),
'delete',
customer=GC_Values[GC_CUSTOMER_ID],
roleId=roleId)
def print_():
cd = gapi_directory.build()
todrive = False
titles = [
'roleId', 'roleName', 'roleDescription', 'isSuperAdminRole',
'isSystemRole'
]
fields = f'nextPageToken,items({",".join(titles)})'
csvRows = []
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower()
if myarg == 'todrive':
todrive = True
i += 1
else:
controlflow.invalid_argument_exit(sys.argv[i],
'gam print adminroles')
roles = gapi.get_all_pages(cd.roles(),
'list',
'items',
customer=GC_Values[GC_CUSTOMER_ID],
fields=fields)
for role in roles:
role_attrib = {}
for key, value in list(role.items()):
role_attrib[key] = value
csvRows.append(role_attrib)
display.write_csv_file(csvRows, titles, 'Admin Roles', todrive)

View File

@@ -0,0 +1,32 @@
import gam
from gam import gapi
from gam.gapi import directory as gapi_directory
def signout(users):
cd = gapi_directory.build()
i = 0
count = len(users)
for user in users:
i += 1
user = gam.normalizeEmailAddressOrUID(user)
print(f'Signing Out {user}{gam.currentCount(i, count)}')
gapi.call(cd.users(),
'signOut',
soft_errors=True,
userKey=user)
def turn_off_2sv(users):
cd = gapi_directory.build()
i = 0
count = len(users)
for user in users:
i += 1
user = gam.normalizeEmailAddressOrUID(user)
print(f'Turning Off 2-Step Verification for {user}{gam.currentCount(i, count)}')
gapi.call(cd.twoStepVerification(),
'turnOff',
soft_errors=True,
userKey=user)

View File

@@ -117,6 +117,7 @@ class ErrorReason(Enum):
FAILED_PRECONDITION = 'failedPrecondition'
FORBIDDEN = 'forbidden'
FOUR_O_NINE = '409'
FOUR_O_O = '400'
FOUR_O_THREE = '403'
FOUR_TWO_NINE = '429'
GATEWAY_TIMEOUT = 'gatewayTimeout'

View File

@@ -13,7 +13,7 @@ from gam import gapi
from gam import utils
def buildGAPIObject():
def build():
return gam.buildGAPIObject('reports')
@@ -41,7 +41,7 @@ REPORT_CHOICE_MAP = {
def showUsageParameters():
rep = buildGAPIObject()
rep = build()
throw_reasons = [
gapi.errors.ErrorReason.INVALID, gapi.errors.ErrorReason.BAD_REQUEST
]
@@ -56,7 +56,7 @@ def showUsageParameters():
kwargs = {}
elif report == 'user':
endpoint = rep.userUsageReport()
kwargs = {'userKey': gam._getValueFromOAuth('email')}
kwargs = {'userKey': gam._get_admin_email()}
else:
controlflow.expected_argument_exit('usageparameters',
['user', 'customer'], report)
@@ -115,7 +115,7 @@ REPORTS_PARAMETERS_SIMPLE_TYPES = [
def showUsage():
rep = buildGAPIObject()
rep = build()
throw_reasons = [
gapi.errors.ErrorReason.INVALID, gapi.errors.ErrorReason.BAD_REQUEST
]
@@ -264,7 +264,7 @@ def showUsage():
def showReport():
rep = buildGAPIObject()
rep = build()
throw_reasons = [gapi.errors.ErrorReason.INVALID]
report = sys.argv[2].lower()
report = REPORT_CHOICE_MAP.get(report.replace('_', ''), report)
@@ -316,7 +316,9 @@ def showReport():
eventName = sys.argv[i + 1]
i += 2
elif myarg == 'user':
userKey = gam.normalizeEmailAddressOrUID(sys.argv[i + 1])
userKey = sys.argv[i + 1].lower()
if userKey != 'all':
userKey = gam.normalizeEmailAddressOrUID(sys.argv[i + 1])
i += 2
elif myarg in ['filter', 'filters']:
filters = sys.argv[i + 1]

View File

@@ -0,0 +1,188 @@
import json
import sys
from urllib.parse import urlencode
import gam
from gam.var import *
from gam import controlflow
from gam import display
from gam import fileutils
from gam import gapi
from gam.gapi import directory as gapi_directory
from gam.gapi import errors as gapi_errors
from gam.gapi.directory import customer as gapi_directory_customer
from gam import transport
from gam import utils
import gam
def build():
return gam.buildGAPIObject('siteVerification')
def create():
verif = build()
a_domain = sys.argv[3]
txt_record = gapi.call(verif.webResource(),
'getToken',
body={
'site': {
'type': 'INET_DOMAIN',
'identifier': a_domain
},
'verificationMethod': 'DNS_TXT'
})
print(f'TXT Record Name: {a_domain}')
print(f'TXT Record Value: {txt_record["token"]}')
print()
cname_record = gapi.call(verif.webResource(),
'getToken',
body={
'site': {
'type': 'INET_DOMAIN',
'identifier': a_domain
},
'verificationMethod': 'DNS_CNAME'
})
cname_token = cname_record['token']
cname_list = cname_token.split(' ')
cname_subdomain = cname_list[0]
cname_value = cname_list[1]
print(f'CNAME Record Name: {cname_subdomain}.{a_domain}')
print(f'CNAME Record Value: {cname_value}')
print('')
webserver_file_record = gapi.call(
verif.webResource(),
'getToken',
body={
'site': {
'type': 'SITE',
'identifier': f'http://{a_domain}/'
},
'verificationMethod': 'FILE'
})
webserver_file_token = webserver_file_record['token']
print(f'Saving web server verification file to: {webserver_file_token}')
fileutils.write_file(webserver_file_token,
f'google-site-verification: {webserver_file_token}',
continue_on_error=True)
print(f'Verification File URL: http://{a_domain}/{webserver_file_token}')
print()
webserver_meta_record = gapi.call(
verif.webResource(),
'getToken',
body={
'site': {
'type': 'SITE',
'identifier': f'http://{a_domain}/'
},
'verificationMethod': 'META'
})
print(f'Meta URL: http://{a_domain}/')
print(f'Meta HTML Header Data: {webserver_meta_record["token"]}')
print()
def info():
verif = build()
sites = gapi.get_items(verif.webResource(), 'list', 'items')
if sites:
for site in sites:
print(f'Site: {site["site"]["identifier"]}')
print(f'Type: {site["site"]["type"]}')
print('Owners:')
for owner in site['owners']:
print(f' {owner}')
print()
else:
print('No Sites Verified.')
def update():
verif = build()
a_domain = sys.argv[3]
verificationMethod = sys.argv[4].upper()
if verificationMethod == 'CNAME':
verificationMethod = 'DNS_CNAME'
elif verificationMethod in ['TXT', 'TEXT']:
verificationMethod = 'DNS_TXT'
if verificationMethod in ['DNS_TXT', 'DNS_CNAME']:
verify_type = 'INET_DOMAIN'
identifier = a_domain
else:
verify_type = 'SITE'
identifier = f'http://{a_domain}/'
body = {
'site': {
'type': verify_type,
'identifier': identifier
},
'verificationMethod': verificationMethod
}
try:
verify_result = gapi.call(
verif.webResource(),
'insert',
throw_reasons=[gapi_errors.ErrorReason.BAD_REQUEST],
verificationMethod=verificationMethod,
body=body)
except gapi_errors.GapiBadRequestError as e:
print(f'ERROR: {str(e)}')
verify_data = gapi.call(verif.webResource(), 'getToken', body=body)
print(f'Method: {verify_data["method"]}')
print(f'Expected Token: {verify_data["token"]}')
if verify_data['method'] in ['DNS_CNAME', 'DNS_TXT']:
simplehttp = transport.create_http()
base_url = 'https://dns.google/resolve?'
query_params = {}
if verify_data['method'] == 'DNS_CNAME':
cname_token = verify_data['token']
cname_list = cname_token.split(' ')
cname_subdomain = cname_list[0]
query_params['name'] = f'{cname_subdomain}.{a_domain}'
query_params['type'] = 'cname'
else:
query_params['name'] = a_domain
query_params['type'] = 'txt'
full_url = base_url + urlencode(query_params)
(_, c) = simplehttp.request(full_url, 'GET')
result = json.loads(c)
status = result['Status']
if status == 0 and 'Answer' in result:
answers = result['Answer']
if verify_data['method'] == 'DNS_CNAME':
answer = answers[0]['data']
else:
answer = 'no matching record found'
for possible_answer in answers:
possible_answer['data'] = possible_answer['data'].strip(
'"')
if possible_answer['data'].startswith(
'google-site-verification'):
answer = possible_answer['data']
break
print(
f'Unrelated TXT record: {possible_answer["data"]}')
print(f'Found DNS Record: {answer}')
elif status == 0:
controlflow.system_error_exit(1, 'DNS record not found')
else:
controlflow.system_error_exit(
status,
DNS_ERROR_CODES_MAP.get(status, f'Unknown error {status}'))
return
print('SUCCESS!')
print(f'Verified: {verify_result["site"]["identifier"]}')
print(f'ID: {verify_result["id"]}')
print(f'Type: {verify_result["site"]["type"]}')
print('All Owners:')
try:
for owner in verify_result['owners']:
print(f' {owner}')
except KeyError:
pass
print()
print(
f'You can now add {a_domain} or it\'s subdomains as secondary or domain aliases of the {GC_Values[GC_DOMAIN]} G Suite Account.'
)

View File

@@ -8,7 +8,7 @@ import platform
import re
GAM_AUTHOR = 'Jay Lee <jay0lee@gmail.com>'
GAM_VERSION = '5.10'
GAM_VERSION = '5.20'
GAM_LICENSE = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
GAM_URL = 'https://git.io/gam'
@@ -26,17 +26,18 @@ GAM_PROJECT_FILEPATH = 'https://raw.githubusercontent.com/jay0lee/GAM/master/src
true_values = ['on', 'yes', 'enabled', 'true', '1']
false_values = ['off', 'no', 'disabled', 'false', '0']
usergroup_types = [
'user', 'users', 'group', 'group_ns', 'grooup_susp', 'ou', 'org', 'ou_ns',
'org_ns', 'ou_susp', 'org_susp', 'ou_and_children', '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'
'user', 'users', 'group', 'group_ns', 'group_susp', 'group_inde', 'ou',
'org', 'ou_ns', 'org_ns', 'ou_susp', 'org_susp', 'ou_and_children',
'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'
]
ERROR_PREFIX = 'ERROR: '
WARNING_PREFIX = 'WARNING: '
UTF8 = 'utf-8'
UTF8_SIG = 'utf-8-sig'
FN_ENABLEDASA_TXT = 'enabledasa.txt'
FN_EXTRA_ARGS_TXT = 'extra-args.txt'
FN_LAST_UPDATE_CHECK_TXT = 'lastupdatecheck.txt'
MY_CUSTOMER = 'my_customer'
@@ -222,13 +223,19 @@ V1_DISCOVERY_APIS = {
'appsactivity',
'calendar',
'drive',
'gmail',
'groupssettings',
'licensing',
'oauth2',
'reseller',
'siteVerification',
'storage',
}
API_NAME_MAPPING = {
'directory': 'admin',
'reports': 'admin',
'datatransfer': 'admin',
'drive3': 'drive',
'cloudresourcemanagerv1': 'cloudresourcemanager',
'cloudidentity_beta': 'cloudidentity',
}
API_VER_MAPPING = {
@@ -236,7 +243,8 @@ API_VER_MAPPING = {
'appsactivity': 'v1',
'calendar': 'v3',
'classroom': 'v1',
'cloudprint': 'v2',
'cloudidentity': 'v1',
'cloudidentity_beta': 'v1beta1',
'cloudresourcemanager': 'v2',
'cloudresourcemanagerv1': 'v1',
'datatransfer': 'datatransfer_v1',
@@ -253,6 +261,7 @@ API_VER_MAPPING = {
'reports': 'reports_v1',
'reseller': 'v1',
'servicemanagement': 'v1',
'serviceusage': 'v1',
'sheets': 'v4',
'siteVerification': 'v1',
'storage': 'v1',
@@ -268,6 +277,7 @@ API_SCOPE_MAPPING = {
'https://www.googleapis.com/auth/drive',
],
'calendar': ['https://www.googleapis.com/auth/calendar',],
'cloudidentity': ['https://www.googleapis.com/auth/cloud-identity',],
'drive': ['https://www.googleapis.com/auth/drive',],
'drive3': ['https://www.googleapis.com/auth/drive',],
'gmail': [
@@ -1054,6 +1064,8 @@ GM_CURRENT_API_SCOPES = 'scoc'
# Values retrieved from oauth2service.json
GM_OAUTH2SERVICE_JSON_DATA = 'oajd'
GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID = 'oaci'
# Full path to enabledasa.txt
GM_ENABLEDASA_TXT = 'enda'
# File containing time of last GAM update check
GM_LAST_UPDATE_CHECK_TXT = 'lupc'
# Dictionary mapping OrgUnit ID to Name
@@ -1093,6 +1105,7 @@ GM_Globals = {
GM_CURRENT_API_SCOPES: [],
GM_OAUTH2SERVICE_JSON_DATA: None,
GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID: None,
GM_ENABLEDASA_TXT: '',
GM_LAST_UPDATE_CHECK_TXT: '',
GM_MAP_ORGUNIT_ID_TO_NAME: None,
GM_MAP_ROLE_ID_TO_NAME: None,
@@ -1126,6 +1139,8 @@ GC_CLIENT_SECRETS_JSON = 'client_secrets_json'
GC_CONFIG_DIR = 'config_dir'
# custmerId from gam.cfg or retrieved from Google
GC_CUSTOMER_ID = 'customer_id'
# Enable Delegated Admin Service Accounts admin user
GC_ADMIN_EMAIL = 'admin_email'
# If debug_level > 0: extra_args[u'prettyPrint'] = True,
# httplib2.debuglevel = gam_debug_level, appsObj.debug = True
GC_DEBUG_LEVEL = 'debug_level'
@@ -1136,6 +1151,8 @@ GC_DECODED_ID_TOKEN = 'decoded_id_token'
GC_DOMAIN = 'domain'
# Google Drive download directory
GC_DRIVE_DIR = 'drive_dir'
# Enable Delegated Admin Service Accounts
GC_ENABLE_DASA = 'enabledasa'
# If no_browser is False, writeCSVfile won't open a browser when todrive is set
# and doRequestOAuth prints a link and waits for the verification code when
# oauth2.txt is being created
@@ -1177,6 +1194,7 @@ GC_CA_FILE = 'ca_file'
TLS_MIN = 'TLSv1_2' if hasattr(ssl.SSLContext(), 'minimum_version') else None
GC_Defaults = {
GC_ADMIN_EMAIL: '',
GC_AUTO_BATCH_MIN: 0,
GC_BATCH_SIZE: 50,
GC_CACHE_DIR: '',
@@ -1189,6 +1207,7 @@ GC_Defaults = {
GC_DECODED_ID_TOKEN: '',
GC_DOMAIN: '',
GC_DRIVE_DIR: '',
GC_ENABLE_DASA: False,
GC_NO_BROWSER: False,
GC_NO_CACHE: False,
GC_NO_SHORT_URLS: False,
@@ -1226,6 +1245,9 @@ GC_VAR_TYPE = 'type'
GC_VAR_LIMITS = 'lmit'
GC_VAR_INFO = {
GC_ADMIN_EMAIL: {
GC_VAR_TYPE: GC_TYPE_STRING
},
GC_AUTO_BATCH_MIN: {
GC_VAR_TYPE: GC_TYPE_INTEGER,
GC_VAR_LIMITS: (0, None)
@@ -1265,6 +1287,9 @@ GC_VAR_INFO = {
GC_DRIVE_DIR: {
GC_VAR_TYPE: GC_TYPE_DIRECTORY
},
GC_ENABLE_DASA: {
GC_VAR_TYPE: GC_TYPE_BOOLEAN
},
GC_NO_BROWSER: {
GC_VAR_TYPE: GC_TYPE_BOOLEAN
},

View File

@@ -1,12 +0,0 @@
{
"_class": "OAuth2Credentials",
"_module": "oauth2client.client",
"access_token": "",
"client_id": "118850122376-72t6r2666n5rbjlfebftqat5qjai2def.apps.googleusercontent.com",
"client_secret": "",
"invalid": false,
"refresh_token": "",
"token_expiry": "2010-04-17T15:18:45Z",
"token_uri": "https://accounts.google.com/o/oauth2/token",
"user_agent": ""
}

BIN
src/travis/creds.tar.enc Normal file

Binary file not shown.

View File

@@ -34,8 +34,10 @@ else
mkdir python
echo "RUNNING: apt update..."
sudo apt-get -qq --yes update > /dev/null
echo "RUNNING: apt dist-upgrade..."
sudo apt-get -qq --yes dist-upgrade > /dev/null
echo "RUNNING: apt upgrade..."
sudo apt-mark hold openssh-server
sudo apt-get --yes upgrade
sudo apt-get --yes --with-new-pkgs upgrade
echo "Installing build tools..."
sudo apt-get -qq --yes install build-essential
echo "Installing deps for python3"
@@ -72,7 +74,8 @@ else
echo "running configure with safe and unsafe"
./configure $safe_flags $unsafe_flags > /dev/null
fi
make -j$cpucount PROFILE_TASK="-m test.regrtest --pgo -j$(( $cpucount * 2 ))" -s
#make -j$cpucount PROFILE_TASK="-m test.regrtest --pgo -j$(( $cpucount * 2 ))" -s
make -j$cpucount -s
RESULT=$?
echo "First make exited with $RESULT"
if [ $RESULT != 0 ]; then
@@ -94,9 +97,10 @@ else
echo "Installing deps for StaticX..."
if [ ! -d patchelf-$PATCHELF_VERSION ]; then
echo "Downloading PatchELF $PATCHELF_VERSION"
wget https://nixos.org/releases/patchelf/patchelf-$PATCHELF_VERSION/patchelf-$PATCHELF_VERSION.tar.bz2
tar xf patchelf-$PATCHELF_VERSION.tar.bz2
cd patchelf-$PATCHELF_VERSION
wget https://github.com/NixOS/patchelf/archive/$PATCHELF_VERSION.tar.gz
tar xf $PATCHELF_VERSION.tar.gz
cd patchelf-$PATCHELF_VERSION/
./bootstrap.sh
./configure
make
sudo make install

Binary file not shown.

View File

@@ -1,6 +1,8 @@
cd src
echo "MacOS Version Info According to Python:"
python -c "import platform; print(platform.mac_ver())"
echo "Xcode versionn:"
xcodebuild -version
export gampath=dist/gam
rm -rf $gampath
$python -OO -m PyInstaller --clean --noupx --strip -F --distpath $gampath gam.spec

View File

@@ -1,12 +0,0 @@
import sys
import json
import os
cfg = json.load(sys.stdin)
cfg['client_secret'] = os.getenv('client_secret')
jid = os.getenv('jid')
cfg['refresh_token'] = os.getenv('refresh_%s' % jid)
gampath = os.getenv('gampath')
out_file = os.path.join(gampath, 'oauth2.txt')
with open(out_file, 'w') as f:
json.dump(cfg, f)