Compare commits

..

41 Commits
v5.20 ... v5.24

Author SHA1 Message Date
Jay Lee
aaf6448563 Support for Chrome OS remote device commands 2020-10-15 07:05:45 -04:00
Ross Scroggs
4a696635f5 Fix display of contentRestrictions in print filelist (#1268)
Update documentation
2020-10-09 17:55:30 -04:00
Jay Lee
beb14befca merge 2020-10-09 14:39:02 -04:00
Jay Lee
c91703364d Workspace SKU updates 2020-10-09 14:31:30 -04:00
Ross Scroggs
597cea17cd Add gshortcut to MIME type choices to be consistent (#1266)
Add third party MIME type shortcut
2020-10-09 12:39:06 -04:00
Ross Scroggs
9585f6c598 reason not valid woth readonly false; add contentrestrions to file field list (#1263) 2020-10-09 11:28:27 -04:00
Jay Lee
e356fe3e85 Update gam-install.sh 2020-10-09 11:25:35 -04:00
Ross Scroggs
55e5b86ec4 Make labels display useful (#1260)
With the existing code you get these columns:

labels.cloudidentity.googleapis.com/groups.discussion_forum,labels.cloudidentity.googleapis.com/groups.security

but there is no data in the columns so you can't tell whcih groups have which values; by translating '' to True you can.
2020-10-09 10:33:03 -04:00
Max Mathieu
bf29a56aeb Updated branding (#1261)
* Update branding

* Update branding

* Update branding

* Update branding

* Update branding

* Update var.py
2020-10-09 10:32:37 -04:00
Jay Lee
07c57d4197 add support for Drive shortcut creation 2020-10-09 10:31:21 -04:00
Jay Lee
146db31cb5 Merge branch 'master' of https://github.com/jay0lee/GAM 2020-10-09 09:11:28 -04:00
Jay Lee
14239fcd47 Support Drive content restrictions 2020-10-09 09:11:14 -04:00
Jay Lee
8dc6a17295 Update .travis.yml 2020-10-07 13:51:25 -04:00
Jay Lee
76f9a6c746 Update .travis.yml 2020-10-06 08:42:05 -04:00
Jay Lee
eb155a5690 Merge branch 'master' of https://github.com/jay0lee/GAM 2020-10-05 09:46:39 -04:00
Jay Lee
b78575aa8f Initial support for group membership expiration 2020-10-05 09:46:24 -04:00
Ross Scroggs
91a5cd5c69 Fix bug in sync devices (#1259) 2020-10-03 19:52:22 -04:00
Jay Lee
dd3e6420b6 Upgrade PyInstaller and hopefully fix Linux binary sizes 2020-10-01 09:30:29 -04:00
Jay Lee
d6a65861e0 GAM 5.22 2020-10-01 09:08:46 -04:00
Jay Lee
fe77ff3f60 stop pypy and nightly source tests that have been failing for awhile anyway 2020-10-01 08:36:00 -04:00
Jay Lee
326cccd525 Stop using batch for licensing
Licensing API seems to throw lots of errors on large batch and we weren't retrying temp errors. For now just stick with serial methods.
2020-10-01 07:36:23 -04:00
Ross Scroggs
b41ca0f0be More bug/pylint cleanup (#1258)
* More bug/pylint cleanup

* pylint cleanup groups.py

* Update GamCommands.txt
2020-09-30 16:56:42 -04:00
Jay Lee
02fa092775 fix reporting orgunit scoping 2020-09-30 16:44:48 -04:00
Ross Scroggs
57860dc5a6 Fix issue #1256 (#1257) 2020-09-30 09:43:42 -04:00
Jay Lee
e28f2fb8cd limit scope of devices listed 2020-09-29 17:23:09 -04:00
Jay Lee
0423dd4069 move device delete later 2020-09-29 17:11:14 -04:00
Ross Scroggs
5e38137916 Add view all|company|personal to print devices (#1254)
* Add view all|company|personal to print devices

Update documentation

* sync devices cleanup

* Update sync devices documentation

* Cleanup;

The advantage here is that they will be no filter errors unless you specify a particular view and a filter that doesn't match

* Simplify selection of devices to print

Default - all
Positve choices
Negative choices - backwards compatible
2020-09-29 14:47:06 -04:00
Ross Scroggs
dc90fb9c94 Update to new DwD page (#1253) 2020-09-26 10:16:08 -04:00
Jay Lee
2e2575c360 send email 2020-09-25 19:37:07 -04:00
Ross Scroggs
2732abbc93 Make sync license consistent with other license commands (#1252) 2020-09-25 08:23:07 -04:00
Jay Lee
e9d5d676a5 Update .travis.yml 2020-09-24 10:46:15 -04:00
Jay Lee
18bab4044e 'gam sync licenses' command, move licensing to separate file 2020-09-24 09:49:49 -04:00
Jay Lee
2ccc4a6932 Update .travis.yml 2020-09-24 08:06:23 -04:00
Jay Lee
d01d02e700 Update .travis.yml 2020-09-23 12:33:25 -04:00
Sean Young
56c6f6cabe Fix issue with zsh not loading gam (#1245)
Co-authored-by: syoung-quizlet <sean.young@quizlet.com>
2020-09-22 14:03:14 -04:00
Ross Scroggs
adb1e58937 GAM_ADMIN_EMAIL overrides oauth2.txt value (#1249)
pylint cleanup unused imports
2020-09-22 12:43:53 -04:00
Ross Scroggs
a59c893652 Device updates (#1247)
* Device updates

Make info device consistent with action device: id keyword is optional
Add nodeviceusers to print devices
pylint cleanup

* Fix documentation

* Sdd orderby to print devices

* Device assetTag cleanup

* Fix typo, appease pylint

* Strip C from customer ID
2020-09-21 10:14:34 -04:00
Ross Scroggs
93bcd5f43b Fix bug, handle missing required argument (#1246) 2020-09-19 12:37:40 -04:00
Ross Scroggs
d7453a7841 filter is a builtin Python function (#1244)
devices.py:118:4: W0622: Redefining built-in 'filter' (redefined-builtin)
2020-09-18 15:28:42 -04:00
Ross Scroggs
4fe3dc052a Delete duplicate code (#1243) 2020-09-18 15:00:25 -04:00
Jay Lee
c88c755785 MacOS 10.15.6, fix groups+DASA+user info 2020-09-18 10:23:55 -04:00
20 changed files with 796 additions and 462 deletions

View File

@@ -1,16 +1,16 @@
if: tag IS blank
os: linux
language: python
dist: xenial
dist: focal
env:
global:
- BUILD_PYTHON_VERSION=3.8.5
- MIN_PYTHON_VERSION=3.8.5
- BUILD_OPENSSL_VERSION=1.1.1g
- MIN_OPENSSL_VERSION=1.1.1g
- BUILD_PYTHON_VERSION=3.9.0
- MIN_PYTHON_VERSION=3.9.0
- BUILD_OPENSSL_VERSION=1.1.1h
- MIN_OPENSSL_VERSION=1.1.1h
- PATCHELF_VERSION=0.11
- PYINSTALLER_COMMIT=ad39eb8df209d02636399ffdc44521a97886cf8c
- PYINSTALLER_COMMIT=7aa19839c171d898b5cf957739083c4bb901607e
cache:
directories:
- $HOME/.cache/pip
@@ -61,18 +61,21 @@ jobs:
language: python
python: 3.7
- os: linux
name: "Python 3.9 dev Source Testing"
name: "Python 3.8 Source Testing"
language: python
python: 3.9-dev
dist: focal
python: 3.8
- os: linux
name: "Python trunk nightly Source Testing"
name: "Python 3.10 dev Source Testing"
language: python
python: nightly
- os: linux
name: "Python PyPi Source Testing"
language: python
python: pypy3
python: 3.10-dev
# - os: linux
# name: "Python trunk nightly Source Testing"
# language: python
# python: nightly
# - os: linux
# name: "Python PyPi Source Testing"
# language: python
# python: pypy3
- os: osx
name: "MacOS 10.13"
language: generic
@@ -150,6 +153,7 @@ script:
done; fi
- if [ "$e2e" = true ]; then $gam create user $newuser firstname Travis lastname $jid password random recoveryphone 12125121110 recoveryemail jay0lee@gmail.com travis.jid $jid; fi
- if [ "$e2e" = true ]; then $gam user $gam_user sendemail recipient $newuser subject "test message $newbase" message "Travis test message"; fi
- if [ "$e2e" = true ]; then $gam user $gam_user sendemail recipient exchange@pdl.jaylee.us subject "test ${tstamp}" message "test message"; fi
- if [ "$e2e" = true ]; then $gam create group $newgroup name "Travis $jid group" description "This is a description" isarchived true; fi
- if [ "$e2e" = true ]; then $gam user $newuser add license gsuitebusiness; fi
- if [ "$e2e" = true ]; then $gam update group $newgroup add owner $gam_user; fi
@@ -217,13 +221,13 @@ script:
- 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
- if [ "$e2e" = true ]; then $gam report customer todrive; fi
- if [ "$e2e" = true ]; then $gam report users fields accounts:is_less_secure_apps_access_allowed,gmail:last_imap_time,gmail:last_pop_time filters "accounts:last_login_time>2019-01-01T00:00:00.000Z" todrive; fi
- if [ "$e2e" = true ]; then $gam report admin start -3d todrive; fi
- if [ "$e2e" = true ]; then $gam print devices nopersonaldevices nodeviceusers filter "serial:$jid$jid$jid$jid$jid-" | $gam csv - gam delete device id ~name; fi
- if ([ "$e2e" = true ] && [[ "$TRAVIS_JOB_NAME" != *"Testing" ]]); then
for gamfile in gam-$GAMVERSION-*; do
fileid=$($gam user $gam_user add drivefile localfile $gamfile drivefilename $GAMVERSION-${TRAVIS_COMMIT:0:7}-$gamfile parentid 1N2zbO33qzUQFsGM49-m9AQC1ijzd_ru1 returnidonly);

View File

@@ -1,4 +1,4 @@
GAM is a command line tool for Google G Suite Administrators to manage domain and user settings quickly and easily. [![Build Status](https://travis-ci.org/jay0lee/GAM.svg?branch=master)](https://travis-ci.org/jay0lee/GAM)
GAM is a command line tool for Google Workspace (fka G Suite) Administrators to manage domain and user settings quickly and easily. [![Build Status](https://travis-ci.org/jay0lee/GAM.svg?branch=master)](https://travis-ci.org/jay0lee/GAM)
# Quick Start
## Linux / MacOS
Open a terminal and run:

View File

@@ -67,7 +67,9 @@ If an item contains spaces, it should be surrounded by ".
gpresentation|
gscript|
gsite|
gsheet|gspreadsheet
gsheet|gspreadsheet|
gshortcut|
g3pshortcut
<ProductID> ::=
Google-Apps|
Google-Chrome-Device-Management|
@@ -91,14 +93,19 @@ If an item contains spaces, it should be surrounded by ".
gams|postini|gsuitegams|gsuitepostini|gsuitemessagesecurity|Google-Apps-For-Postini|
gal|gsl|lite|gsuitelite|Google-Apps-Lite|
gau|gsb|unlimited|gsuitebusiness|Google-Apps-Unlimited|
gae|gse|enterprise|gsuiteenterprise|1010020020|
wsentplus|workspaceenterpriseplus|gae|gse|enterprise|gsuiteenterprise|1010020020|
wsbizplus|workspacebusinessplus|1010020025|
wsentstan|workspaceenterprisestandard|'1010020026|
wsbizstart|workspacebusinessstarter|1010020027|
wsbizstan|workspacebusinessstandard|1010020028|
gsefe|e4e|gsuiteenterpriseeducation|1010310002|
gsefes|e4es|gsuiteenterpriseeducationstudent|1010310003|
gsbau|businessarchived|gsuitebusinessarchived|
gseau|enterprisearchived|gsuiteenterprisearchived|
chrome|cdm|googlechromedevicemanagement|Google-Chrome-Device-Management|
coordinate|googlecoordinate|Google-Coordinate|
d4e|driveenterprise|drive4enterprise|
wsess|workspaceesentials|gsuiteessentials|essentials|d4e|driveenterprise|drive4enterprise|1010060001|
wsentess|workspaceenterpriseessentials|1010060003|
drive20gb|20gb|googledrivestorage20gb|Google-Drive-storage-20GB|
drive50gb|50gb|googledrivestorage50gb|Google-Drive-storage-50GB|
drive200gb|200gb|googledrivestorage200gb|Google-Drive-storage-200GB|
@@ -158,6 +165,9 @@ If an item contains spaces, it should be surrounded by ".
<CourseState> ::= active|archived|provisioned|declined
<CrOSID> ::= <String>
<CustomerID> ::= <String>
<DeviceID> ::= devices/<String>
<DeviceType> ::= android|chrome_os|google_sync|ios|linux|mac_os|windows
<DeviceUserID> ::= devices/<String>/deviceUsers/<String>
<DomainAlias> ::= <String>
<DriveFileACLRole> ::= commenter|contentmanager|editor|fileorganizer|organizer|owner|reader|writer
<DriveFileID> ::= <String>
@@ -305,6 +315,7 @@ If an item contains spaces, it should be surrounded by ".
appdatacontents|
cancomment|
canreadrevisions|
contentrestrictions|
copyable|
copyrequireswriterpermission|
createddate|createdtime|
@@ -678,16 +689,22 @@ Specify a collection of Users by directly specifying them or by specifiying item
(localfile <FileName>)|
(convert)|(ocr)|(ocrlanguage <Language>)|
(restricted|restrict)|(starred|star)|(trashed|trash)|(viewed|view)|
(contentrestrictions readonly false)|
(contentrestrictions readonly true [reason <String>])|
copyrequireswriterpermission|
(lastviewedbyme <Time>)|(modifieddate|modifiedtime <Time>)|(description <String>)|(mimetype <MimeType>)|
(parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|writerscantshare
(parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|writerscantshare|
(shortcut <DriveFileID>)
<DriveFileUpdateAttributes> ::=
(localfile <FileName>)|
(convert)|(ocr)|(ocrlanguage <Language>)|
(restricted|restrict <Boolean>)|(starred|star <Boolean>)|(trashed|trash <Boolean>)|(viewed|view <Boolean>)|
(contentrestrictions readonly false)|
(contentrestrictions readonly true [reason <String>])|
(copyrequireswriterpermission <Boolean>)|
(lastviewedbyme <Time>)|(modifieddate <Time>)|(description <String>)|(mimetype <MimeType>)|
(parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|writerscantshare
(parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|writerscantshare|
(shortcut <DriveFileID>)
<GroupSettingsAttribute> ::=
(allowexternalmembers <Boolean>)|
(allowwebposting <Boolean>)|
@@ -841,7 +858,7 @@ gam show projects [<EmailAddress>] [all|gam|<ProjectID>|(filter <String>)]
gam print projects [<EmailAddress>] [all|gam|<ProjectID>|(filter <String>)] [todrive]
gam rotate sakey|sakeys [retain_none|retain_existing|replace_current]
[(algorithm KEY_ALG_RSA_1024|KEY_ALG_RSA_2048)|(localkeysize 1024|2048|4096)]
[(algorithm KEY_ALG_RSA_1024|KEY_ALG_RSA_2048)|(localkeysize 1024|2048|4096)]
gam delete sakey|sakeys <ServiceAccountKeyList>+ [doit]
gam show sakey|sakeys [all|system|user]
@@ -1024,7 +1041,10 @@ The following attributes are equivalent:
(colorindex|colorid <EventColorIndex>)
(description <String>)|
(end (allday <Date>)|<Time>)|
(guestscaninviteothers <Boolean>)|
guestscantinviteothers|
(guestscanmodify <Boolean>)|
(guestscanseeothers <Boolean>)|
guestscantseeothers|
hangoutsmeet|
(location <String>)|
@@ -1127,6 +1147,34 @@ The listlimit <Number> argument limits the number of recent users, time ranges a
The start <Date> and end <Date> arguments filter the time ranges.
Delimiter defaults to comma.
<DeviceID> ::= devices/<String>
<DeviceType> ::= android|chrome_os|google_sync|ios|linux|mac_os|windows
<DeviceUserID> ::= devices/<String>/deviceUsers/<String>
<DeviceOrderbyFieldName> ::=
createtime|devicetype|lastsynctime|model|osversion|serialnumber
gam create device serialnumber <String> devicetype <DeviceType> [assetid <String>]
gam info device [id] <DeviceID>
gam delete device [id] <DeviceID>
gam cancelwipe device [id] <DeviceID>
gam wipe device [id] <DeviceID>
gam approve deviceuser [id] <DeviceUserID>
gam block deviceuser [id] <DeviceUserID>
gam delete deviceuser [id] <DeviceUserID>
gam cancelwipe deviceuser [id] <DeviceUserID>
gam wipe deviceuser [id] <DeviceUserID>
gam print devices [todrive] [filter|query <QueryDevice>]
[orderby <DeviceOrderByFieldName> [ascending|descending]]
[company|personal|nocompanydevices|nopersonaldevices]
[nodeviceusers]
gam sync devices [filter|query <QueryDevice>]
csvfile <FileName>
(devicetype_column <String>)|(static_devicetype <DeviceType>)
serialnumber_column <String>
[assettag_column <String>]
[unassigned_missing_action delete|wipe|donothing]
[assigned_missing_action delete|wipe|donothing]
gam update mobile <MobileID>|query:<QueryMobile> action <MobileAction> [doit] [if_users|match_users <UserTypeEntity>]
gam delete mobile <MobileID>
gam info mobile <MobileID>
@@ -1336,6 +1384,7 @@ gam <UserTypeEntity> delete|del group|groups
gam <UserTypeEntity> create|add license <SKUID> [product|productid <ProductID>]
gam <UserTypeEntity> update license <SKUID> [product|productid <ProductID>] [from] <SKUID>
gam <UserTypeEntity> delete|del license <SKUID> [product|productid <ProductID>]
gam <UserTypeEntity> sync license <SKUID> [product|productid <ProductID>]
gam <UserTypeEntity> update photo <FileNamePattern>
gam <UserTypeEntity> delete|del photo
@@ -1456,4 +1505,3 @@ gam <UserTypeEntity> show vacation [format]
gam <UserTypeEntity> signout
gam <UserTypeEntity> turnoff2sv

View File

@@ -28,8 +28,8 @@ upgrade_only=false
gamversion="latest"
adminuser=""
regularuser=""
gam_glibc_vers="2.27 2.23"
gam_macos_vers="10.15.4 10.14.6 10.13.6"
gam_glibc_vers="2.31 2.27 2.23"
gam_macos_vers="10.15.6 10.14.6 10.13.6"
while getopts "hd:a:o:b:lp:u:r:v:" OPTION
do
@@ -240,7 +240,7 @@ fi
# Update profile to add gam command
if [ "$update_profile" = true ]; then
alias_line="gam() { \"$target_dir/gam/gam\" \"\$@\" ; }"
alias_line="function gam() { \"$target_dir/gam/gam\" \"\$@\" ; }"
if [ "$gamos" == "linux" ]; then
update_profile "$HOME/.bash_aliases" 0 || update_profile "$HOME/.bash_profile" 0 || update_profile "$HOME/.bashrc" 0
update_profile "$HOME/.zshrc" 0
@@ -288,7 +288,7 @@ while true; do
case $yn in
[Yy]*)
if [ "$adminuser" == "" ]; then
read -p "Please enter your G Suite admin email address: " adminuser
read -p "Please enter your Google Workspace admin email address: " adminuser
fi
"$target_dir/gam/gam" create project $adminuser
rc=$?
@@ -312,7 +312,7 @@ done
admin_authorized=false
while $project_created; do
read -p "Are you ready to authorize GAM to perform G Suite management operations as your admin account? (yes or no) " yn
read -p "Are you ready to authorize GAM to perform Google Workspace management operations as your admin account? (yes or no) " yn
case $yn in
[Yy]*)
"$target_dir/gam/gam" oauth create $adminuser
@@ -337,11 +337,11 @@ done
service_account_authorized=false
while $project_created; do
read -p "Are you ready to authorize GAM to manage G Suite user data and settings? (yes or no) " yn
read -p "Are you ready to authorize GAM to manage Google Workspace user data and settings? (yes or no) " yn
case $yn in
[Yy]*)
if [ "$regularuser" == "" ]; then
read -p "Please enter the email address of a regular G Suite user: " regularuser
read -p "Please enter the email address of a regular Google Workspace user: " regularuser
fi
echo_yellow "Great! Checking service account scopes.This will fail the first time. Follow the steps to authorize and retry. It can take a few minutes for scopes to PASS after they've been authorized in the admin console."
"$target_dir/gam/gam" user $adminuser check serviceaccount

View File

@@ -27,7 +27,7 @@
@ goto createproject
)
@echo(
@set /p adminemail= "Please enter your G Suite admin email address: "
@set /p adminemail= "Please enter your Google Workspace admin email address: "
@gam create project %adminemail%
@if not ERRORLEVEL 1 goto projectdone
@echo(
@@ -37,7 +37,7 @@
:adminauth
@echo(
@set /p yn= "Are you ready to authorize GAM to perform G Suite management operations as your admin account? [y or n] "
@set /p yn= "Are you ready to authorize GAM to perform Google Workspace management operations as your admin account? [y or n] "
@if /I "%yn%"=="n" (
@ echo(
@ echo You can authorize an admin later by running:
@@ -59,7 +59,7 @@
:saauth
@echo(
@set /p yn= "Are you ready to authorize GAM to manage G Suite user data and settings? [y or n] "
@set /p yn= "Are you ready to authorize GAM to manage Google Workspace user data and settings? [y or n] "
@if /I "%yn%"=="n" (
@ echo(
@ echo You can authorize a service account later by running:
@@ -73,7 +73,7 @@
@ goto saauth
)
@echo(
@set /p regularuser= "Please enter the email address of a regular G Suite user: "
@set /p regularuser= "Please enter the email address of a regular Google Workspace user: "
@echo Great! Checking service account scopes. This will fail the first time. Follow the steps to authorize and retry. It can take a few minutes for scopes to PASS after they've been authorized in the admin console.
@gam user %regularuser% check serviceaccount
@if not ERRORLEVEL 1 goto sadone

View File

@@ -4,10 +4,7 @@ import base64
import configparser
import csv
import datetime
import difflib
from email import message_from_string
import hashlib
import io
import json
import mimetypes
import os
@@ -54,7 +51,6 @@ from gam.gapi import calendar as gapi_calendar
from gam.gapi import cloudidentity as gapi_cloudidentity
from gam.gapi.cloudidentity import devices as gapi_cloudidentity_devices
from gam.gapi.cloudidentity import groups as gapi_cloudidentity_groups
from gam.gapi import directory as gapi_directory
from gam.gapi.directory import asps as gapi_directory_asps
from gam.gapi.directory import cros as gapi_directory_cros
from gam.gapi.directory import customer as gapi_directory_customer
@@ -67,6 +63,7 @@ from gam.gapi.directory import privileges as gapi_directory_privileges
from gam.gapi.directory import resource as gapi_directory_resource
from gam.gapi.directory import roles as gapi_directory_roles
from gam.gapi.directory import users as gapi_directory_users
from gam.gapi import licensing as gapi_licensing
from gam.gapi import siteverification as gapi_siteverification
from gam.gapi import errors as gapi_errors
from gam.gapi import reports as gapi_reports
@@ -510,6 +507,7 @@ def SetGlobalVariables():
if GC_Defaults[GC_OAUTH2SERVICE_JSON].find('.') == -1:
GC_Defaults[GC_OAUTH2SERVICE_JSON] += '.json'
_getOldEnvVar(GC_CLIENT_SECRETS_JSON, 'CLIENTSECRETS')
_getOldEnvVar(GC_ADMIN_EMAIL, 'GA_ADMIN_EMAIL')
_getOldEnvVar(GC_DOMAIN, 'GA_DOMAIN')
_getOldEnvVar(GC_CUSTOMER_ID, 'CUSTOMER_ID')
_getOldEnvVar(GC_CHARSET, 'GAM_CHARSET')
@@ -1237,15 +1235,17 @@ def doCheckServiceAccount(users):
check_scopes.append(USERINFO_EMAIL_SCOPE)
long_url = ('https://admin.google.com/ac/owl/domainwidedelegation'
f'?clientScopeToAdd={",".join(check_scopes)}'
f'&clientIdToAdd={service_account}')
f'&clientIdToAdd={service_account}&overwriteClientId=true')
short_url = utils.shorten_url(long_url)
scopes_failed = f'''Some scopes failed! To authorize them, please go to:
{short_url}
You will be redirected to the G Suite admin console. The Client Name and API
Scopes fields will be pre-populated. Please click Authorize to allow these
scopes access. After authorizing it may take some time for this test to pass so
You will be directed to the G Suite admin console Security/API Controls/Domain-wide Delegation page
The "Add a new Client ID" box will open
Make sure that "Overwrite existing client ID" is checked
Please click Authorize to allow these scopes access.
After authorizing it may take some time for this test to pass so
go grab a cup of coffee and then try this command again.
'''
controlflow.system_error_exit(1, scopes_failed)
@@ -3197,10 +3197,21 @@ def printDriveFileList(users):
'kind', 'etag', 'selfLink'
]:
continue
x_attrib = f'{attrib}.{j}.{list_attrib}'
if x_attrib not in titles:
titles.append(x_attrib)
a_file[x_attrib] = l_attrib[list_attrib]
if not isinstance(l_attrib[list_attrib], dict):
x_attrib = f'{attrib}.{j}.{list_attrib}'
if x_attrib not in titles:
titles.append(x_attrib)
a_file[x_attrib] = l_attrib[list_attrib]
else:
for sl_attrib in l_attrib[list_attrib]:
if sl_attrib in [
'kind', 'etag', 'selfLink'
]:
continue
x_attrib = f'{attrib}.{j}.{list_attrib}.{sl_attrib}'
if x_attrib not in titles:
titles.append(x_attrib)
a_file[x_attrib] = l_attrib[list_attrib][sl_attrib]
elif isinstance(f_file[attrib], (str, int, bool)):
if attrib not in titles:
titles.append(attrib)
@@ -3454,6 +3465,7 @@ def initializeDriveFileAttributes():
def getDriveFileAttribute(i, body, parameters, myarg, update=False):
operation = 'update' if update else 'add'
if myarg == 'localfile':
parameters[DFA_LOCALFILEPATH] = sys.argv[i + 1]
parameters[DFA_LOCALFILENAME] = os.path.basename(
@@ -3529,9 +3541,30 @@ def getDriveFileAttribute(i, body, parameters, myarg, update=False):
elif myarg == 'writerscantshare':
body['writersCanShare'] = False
i += 1
elif myarg == 'contentrestrictions':
body['contentRestrictions'] = [{}]
restriction = sys.argv[i+1].lower().replace('_', '')
if restriction == 'readonly':
body['contentRestrictions'][0]['readOnly'] = getBoolean(
sys.argv[i+2], f'gam <users> {operation} drivefile')
i += 3
if len(sys.argv) > i and sys.argv[i].lower() == 'reason':
if body['contentRestrictions'][0]['readOnly']:
body['contentRestrictions'][0]['reason'] = sys.argv[i+1]
else:
controlflow.invalid_argument_exit(
'reason', 'contentrestrictions readonly false')
i += 2
else:
controlflow.invalid_argument_exit(
restriction, f'gam <users> {operation} drivefile')
elif myarg == 'shortcut':
body['mimeType'] = MIMETYPE_GA_SHORTCUT
body['shortcutDetails'] = {'targetId': sys.argv[i+1]}
i += 2
else:
controlflow.invalid_argument_exit(
myarg, f"gam <users> {['add', 'update'][update]} drivefile")
myarg, f"gam <users> {operation} drivefile")
return i
@@ -4308,73 +4341,6 @@ def getImap(users):
)
def getProductAndSKU(sku):
l_sku = sku.lower().replace('-', '').replace(' ', '')
for a_sku, sku_values in list(SKUS.items()):
if l_sku == a_sku.lower().replace(
'-',
'') or l_sku in sku_values['aliases'] or l_sku == sku_values[
'displayName'].lower().replace(' ', ''):
return (sku_values['product'], a_sku)
try:
product = re.search('^([A-Z,a-z]*-[A-Z,a-z]*)', sku).group(1)
except AttributeError:
product = sku
return (product, sku)
def doLicense(users, operation):
lic = buildGAPIObject('licensing')
sku = sys.argv[5]
productId, skuId = getProductAndSKU(sku)
i = 6
if len(sys.argv) > 6 and sys.argv[i].lower() in ['product', 'productid']:
productId = sys.argv[i + 1]
i += 2
for user in users:
if operation == 'delete':
print(
f'Removing license {_formatSKUIdDisplayName(skuId)} from user {user}'
)
gapi.call(lic.licenseAssignments(),
operation,
soft_errors=True,
productId=productId,
skuId=skuId,
userId=user)
elif operation == 'insert':
print(
f'Adding license {_formatSKUIdDisplayName(skuId)} to user {user}'
)
gapi.call(lic.licenseAssignments(),
operation,
soft_errors=True,
productId=productId,
skuId=skuId,
body={'userId': user})
elif operation == 'patch':
try:
old_sku = sys.argv[i]
if old_sku.lower() == 'from':
old_sku = sys.argv[i + 1]
except KeyError:
controlflow.system_error_exit(
2,
'You need to specify the user\'s old SKU as the last argument'
)
_, old_sku = getProductAndSKU(old_sku)
print(
f'Changing user {user} from license {_formatSKUIdDisplayName(old_sku)} to {_formatSKUIdDisplayName(skuId)}'
)
gapi.call(lic.licenseAssignments(),
operation,
soft_errors=True,
productId=productId,
skuId=old_sku,
userId=user,
body={'skuId': skuId})
def doPop(users):
enable = getBoolean(sys.argv[4], 'gam <users> pop')
body = {
@@ -8437,7 +8403,7 @@ def _getResoldSubscriptionAttr(arg, customerId):
arg[i + 2], 'maximumNumberOfSeats', minVal=0)
i += 1
elif myarg in ['sku', 'skuid']:
_, body['skuId'] = getProductAndSKU(arg[i + 1])
_, body['skuId'] = gapi_licensing.getProductAndSKU(arg[i + 1])
elif myarg in ['customerauthtoken', 'transfertoken']:
customerAuthToken = arg[i + 1]
else:
@@ -8510,15 +8476,13 @@ def _getValueFromOAuth(field, credentials=None):
def _get_admin_email():
if GC_Values[GC_ENABLE_DASA]:
if not GC_Values[GC_ADMIN_EMAIL]:
GC_Values[GC_ADMIN_EMAIL] = os.environ.get('GA_ADMIN_EMAIL', GC_Defaults[GC_ADMIN_EMAIL])
if not GC_Values[GC_ADMIN_EMAIL]:
controlflow.system_error_exit(
3,
f'Environment variable GA_ADMIN_EMAIL must be set when {GM_Globals[GM_ENABLEDASA_TXT]} is present'
)
if GC_Values[GC_ADMIN_EMAIL]:
return GC_Values[GC_ADMIN_EMAIL]
if GC_Values[GC_ENABLE_DASA]:
controlflow.system_error_exit(
3,
f'Environment variable GA_ADMIN_EMAIL must be set when {GM_Globals[GM_ENABLEDASA_TXT]} is present'
)
return _getValueFromOAuth('email')
def doGetUserInfo(user_email=None):
@@ -8792,6 +8756,12 @@ def doGetUserInfo(user_email=None):
print(f' {alias}')
if getGroups:
throw_reasons = [gapi_errors.ErrorReason.FORBIDDEN]
kwargs = {}
if GC_Values[GC_ENABLE_DASA]:
# Allows groups.list() to function but will limit
# returned groups to those in same domain as user
# so only do this for DASA admins
kwargs['domain'] = GC_Values[GC_DOMAIN]
try:
groups = gapi.get_all_pages(
cd.groups(),
@@ -8799,7 +8769,7 @@ def doGetUserInfo(user_email=None):
'groups',
userKey=user_email,
fields='groups(name,email),nextPageToken',
throw_reasons=throw_reasons)
throw_reasons=throw_reasons, **kwargs)
if groups:
print(f'Groups: ({len(groups)})')
for group in groups:
@@ -8812,25 +8782,14 @@ def doGetUserInfo(user_email=None):
lbatch = lic.new_batch_http_request(callback=user_lic_result)
user_licenses = []
for sku in skus:
productId, skuId = getProductAndSKU(sku)
productId, skuId = gapi_licensing.getProductAndSKU(sku)
lbatch.add(lic.licenseAssignments().get(userId=user_email,
productId=productId,
skuId=skuId,
fields='skuId'))
lbatch.execute()
for user_license in user_licenses:
print(f' {_formatSKUIdDisplayName(user_license)}')
def _skuIdToDisplayName(skuId):
return SKUS[skuId]['displayName'] if skuId in SKUS else skuId
def _formatSKUIdDisplayName(skuId):
skuIdDisplay = _skuIdToDisplayName(skuId)
if skuId == skuIdDisplay:
return skuId
return f'{skuId} ({skuIdDisplay})'
print(f' {gapi_licensing._formatSKUIdDisplayName(user_license)}')
def doGetAliasInfo(alias_email=None):
@@ -9520,13 +9479,13 @@ def doPrintUsers():
[groupname['email'] for groupname in groups])
if getLicenseFeed:
titles.append('Licenses')
licenses = doPrintLicenses(returnFields='userId,skuId')
licenses = gapi_licensing.print_(returnFields='userId,skuId')
if licenses:
for user in csvRows:
u_licenses = licenses.get(user['primaryEmail'].lower())
if u_licenses:
user['Licenses'] = licenseDelimiter.join(
[_skuIdToDisplayName(skuId) for skuId in u_licenses])
[gapi_licensing._skuIdToDisplayName(skuId) for skuId in u_licenses])
display.write_csv_file(csvRows, titles, 'Users', todrive)
@@ -9663,172 +9622,6 @@ def doPrintAliases():
display.write_csv_file(csvRows, titles, 'Aliases', todrive)
def doPrintLicenses(returnFields=None,
skus=None,
countsOnly=False,
returnCounts=False):
lic = buildGAPIObject('licensing')
products = []
licenses = []
licenseCounts = []
if not returnFields:
csvRows = []
todrive = False
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower()
if not returnCounts and myarg == 'todrive':
todrive = True
i += 1
elif myarg in ['products', 'product']:
products = sys.argv[i + 1].split(',')
i += 2
elif myarg in ['sku', 'skus']:
skus = sys.argv[i + 1].split(',')
i += 2
elif myarg == 'allskus':
skus = sorted(SKUS)
products = []
i += 1
elif myarg == 'gsuite':
skus = [
skuId for skuId in SKUS
if SKUS[skuId]['product'] in ['Google-Apps', '101031']
]
products = []
i += 1
elif myarg == 'countsonly':
countsOnly = True
i += 1
else:
controlflow.invalid_argument_exit(sys.argv[i],
'gam print licenses')
if not countsOnly:
fields = 'nextPageToken,items(productId,skuId,userId)'
titles = ['userId', 'productId', 'skuId']
else:
fields = 'nextPageToken,items(userId)'
if not returnCounts:
if skus:
titles = ['productId', 'skuId', 'licenses']
else:
titles = ['productId', 'licenses']
else:
fields = f'nextPageToken,items({returnFields})'
if skus:
for sku in skus:
if not products:
product, sku = getProductAndSKU(sku)
else:
product = products[0]
page_message = gapi.got_total_items_msg(
f'Licenses for {SKUS.get(sku, {"displayName": sku})["displayName"]}',
'...\n')
try:
licenses += gapi.get_all_pages(
lic.licenseAssignments(),
'listForProductAndSku',
'items',
throw_reasons=[
gapi_errors.ErrorReason.INVALID,
gapi_errors.ErrorReason.FORBIDDEN
],
page_message=page_message,
customerId=GC_Values[GC_DOMAIN],
productId=product,
skuId=sku,
fields=fields)
if countsOnly:
licenseCounts.append([
'Product', product, 'SKU', sku, 'Licenses',
len(licenses)
])
licenses = []
except (gapi_errors.GapiInvalidError,
gapi_errors.GapiForbiddenError):
pass
else:
if not products:
products = sorted(PRODUCTID_NAME_MAPPINGS)
for productId in products:
page_message = gapi.got_total_items_msg(
f'Licenses for {PRODUCTID_NAME_MAPPINGS.get(productId, productId)}',
'...\n')
try:
licenses += gapi.get_all_pages(
lic.licenseAssignments(),
'listForProduct',
'items',
throw_reasons=[
gapi_errors.ErrorReason.INVALID,
gapi_errors.ErrorReason.FORBIDDEN
],
page_message=page_message,
customerId=GC_Values[GC_DOMAIN],
productId=productId,
fields=fields)
if countsOnly:
licenseCounts.append(
['Product', productId, 'Licenses',
len(licenses)])
licenses = []
except (gapi_errors.GapiInvalidError,
gapi_errors.GapiForbiddenError):
pass
if countsOnly:
if returnCounts:
return licenseCounts
if skus:
for u_license in licenseCounts:
csvRows.append({
'productId': u_license[1],
'skuId': u_license[3],
'licenses': u_license[5]
})
else:
for u_license in licenseCounts:
csvRows.append({
'productId': u_license[1],
'licenses': u_license[3]
})
display.write_csv_file(csvRows, titles, 'Licenses', todrive)
return
if returnFields:
if returnFields == 'userId':
userIds = []
for u_license in licenses:
userId = u_license.get('userId', '').lower()
if userId:
userIds.append(userId)
return userIds
userSkuIds = {}
for u_license in licenses:
userId = u_license.get('userId', '').lower()
skuId = u_license.get('skuId')
if userId and skuId:
userSkuIds.setdefault(userId, [])
userSkuIds[userId].append(skuId)
return userSkuIds
for u_license in licenses:
userId = u_license.get('userId', '').lower()
skuId = u_license.get('skuId', '')
csvRows.append({
'userId': userId,
'productId': u_license.get('productId', ''),
'skuId': _skuIdToDisplayName(skuId)
})
display.write_csv_file(csvRows, titles, 'Licenses', todrive)
def doShowLicenses():
licenseCounts = doPrintLicenses(countsOnly=True, returnCounts=True)
for u_license in licenseCounts:
line = ''
for i in range(0, len(u_license), 2):
line += f'{u_license[i]}: {u_license[i+1]}, '
print(line[:-2])
def shlexSplitList(entity, dataDelimiter=' ,'):
lexer = shlex.shlex(entity, posix=True)
lexer.whitespace = dataDelimiter
@@ -10043,7 +9836,7 @@ def getUsersToModify(entity_type=None,
if not silent:
sys.stderr.write('done.\r\n')
elif entity_type in ['license', 'licenses', 'licence', 'licences']:
users = doPrintLicenses(returnFields='userId', skus=entity.split(','))
users = gapi_licensing.print_(returnFields='userId', skus=entity.split(','))
elif entity_type in ['file', 'crosfile']:
users = []
f = fileutils.open_file(entity, strip_utf_bom=True)
@@ -11440,8 +11233,6 @@ def ProcessGAMCommand(args):
gapi_cloudidentity_devices.print_()
elif argument in ['groupmembers', 'groupsmembers']:
gapi_directory_groups.print_members()
elif argument == 'devices':
gapi_cloudidentity_devices.print_()
elif argument in ['cigroupmembers', 'cigroupsmembers']:
gapi_cloudidentity_groups.print_members()
elif argument in ['orgs', 'ous']:
@@ -11457,7 +11248,7 @@ def ProcessGAMCommand(args):
elif argument == 'mobile':
gapi_directory_mobiledevices.print_()
elif argument in ['license', 'licenses', 'licence', 'licences']:
doPrintLicenses()
gapi_licensing.print_()
elif argument in ['token', 'tokens', 'oauth', '3lo']:
printShowTokens(3, None, None, True)
elif argument in ['schema', 'schemas']:
@@ -11506,7 +11297,7 @@ def ProcessGAMCommand(args):
elif argument in ['guardian', 'guardians']:
doPrintShowGuardians(False)
elif argument in ['license', 'licenses', 'licence', 'licences']:
doShowLicenses()
gapi_licensing.show()
elif argument in ['project', 'projects']:
doPrintShowProjects(False)
elif argument in ['sakey', 'sakeys']:
@@ -11615,6 +11406,14 @@ def ProcessGAMCommand(args):
elif command == 'block':
gapi_cloudidentity_devices.block_user()
sys.exit(0)
elif command in ['issuecommand', 'getcommand']:
target = sys.argv[2].lower().replace('_', '')
if target == 'cros':
if command == 'issuecommand':
gapi_directory_cros.issue_command()
elif command == 'getcommand':
gapi_directory_cros.get_command()
sys.exit(0)
users = getUsersToModify()
command = sys.argv[3].lower()
if command == 'print' and len(sys.argv) == 4:
@@ -11774,7 +11573,7 @@ def ProcessGAMCommand(args):
elif delWhat == 'photo':
deletePhoto(users)
elif delWhat in ['license', 'licence']:
doLicense(users, 'delete')
gapi_licensing.delete(users)
elif delWhat in ['backupcode', 'backupcodes', 'verificationcodes']:
doDelBackupCodes(users)
elif delWhat in ['asp', 'asps', 'applicationspecificpasswords']:
@@ -11816,7 +11615,7 @@ def ProcessGAMCommand(args):
elif addWhat == 'drivefile':
createDriveFile(users)
elif addWhat in ['license', 'licence']:
doLicense(users, 'insert')
gapi_licensing.create(users)
elif addWhat in ['drivefileacl', 'drivefileacls']:
addDriveFileACL(users)
elif addWhat in ['label', 'labels']:
@@ -11836,6 +11635,13 @@ def ProcessGAMCommand(args):
else:
controlflow.invalid_argument_exit(addWhat,
f'gam <users> {command}')
elif command == 'sync':
syncWhat = sys.argv[4].lower()
if syncWhat in ['license', 'licence']:
gapi_licensing.sync(users)
else:
controlflow.invalid_argument_exit(syncWhat,
f'gam <users> {command}')
elif command == 'update':
updateWhat = sys.argv[4].lower()
if updateWhat == 'calendar':
@@ -11845,7 +11651,7 @@ def ProcessGAMCommand(args):
elif updateWhat == 'photo':
doPhoto(users)
elif updateWhat in ['license', 'licence']:
doLicense(users, 'patch')
gapi_licensing.update(users)
elif updateWhat == 'user':
doUpdateUser(users, 5)
elif updateWhat in [

View File

@@ -16,7 +16,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""GAM is a command line tool which allows Administrators to control their G Suite domain and accounts.
"""GAM is a command line tool which allows Administrators to control their Google Workspace domain and accounts.
With GAM you can programmatically create users, turn on/off services for users like POP and Forwarding and much more.
For more information, see https://git.io/gam

View File

@@ -3,7 +3,6 @@ import sys
import googleapiclient
import gam
from gam.var import *
from gam import controlflow
from gam import display
@@ -12,16 +11,20 @@ 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 _get_device_customerid():
customer = GC_Values[GC_CUSTOMER_ID]
if customer.startswith('C'):
customer = customer[1:]
return f'customers/{customer}'
def create():
ci = gapi_cloudidentity.build_dwd()
customer = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
customer = _get_device_customerid()
device_types = gapi.get_enum_values_minus_unspecified(
ci._rootDesc['schemas']['GoogleAppsCloudidentityDevicesV1Device']['properties']['deviceType']['enum'])
body = {}
body = {'deviceType': '', 'serialNumber': ''}
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
@@ -35,20 +38,30 @@ def create():
', '.join(device_types),
sys.argv[i+1])
i += 2
elif myarg in {'assettag', 'assetid'}:
body['assetTag'] = 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.invalid_argument_exit(sys.argv[i], 'gam create device')
if not body['serialNumber'] or not body['deviceType']:
controlflow.system_error_exit(
3, 'serial_number and device_type are required arguments for "gam create device".')
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]}'
def _get_device_name():
name = sys.argv[3]
if name == 'id':
name = sys.argv[4]
if not name.startswith('devices/'):
name = f'devices/{name}'
return name
def info():
ci = gapi_cloudidentity.build_dwd()
customer = _get_device_customerid()
name = _get_device_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)
@@ -58,8 +71,9 @@ def info():
def _generic_action(action, device_user=False):
ci = gapi_cloudidentity.build_dwd()
customer = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
customer = _get_device_customerid()
name = _get_device_name()
# bah, inconsistencies in API
if action == 'delete':
kwargs = {'customer': customer}
@@ -70,31 +84,17 @@ def _generic_action(action, device_user=False):
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)
print(op)
def delete():
_generic_action('delete')
_generic_action('delete')
def cancel_wipe():
_generic_action('cancelWipe')
_generic_action('cancelWipe')
def wipe():
_generic_action('wipe')
_generic_action('wipe')
def approve_user():
_generic_action('approve', True)
@@ -113,11 +113,12 @@ def wipe_user():
def print_():
ci = gapi_cloudidentity.build_dwd()
customer = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
customer = _get_device_customerid()
parent = 'devices/-'
filter = None
device_filter = None
get_device_users = True
get_device_views = ['COMPANY_INVENTORY', 'USER_ASSIGNED_DEVICES']
view = None
orderByList = []
titles = []
csvRows = []
todrive = False
@@ -126,38 +127,69 @@ def print_():
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg in ['filter', 'query']:
filter = sys.argv[i+1]
i += 2
device_filter = sys.argv[i+1]
i += 2
elif myarg == 'company':
view = 'COMPANY_INVENTORY'
i += 1
elif myarg == 'personal':
view = 'USER_ASSIGNED_DEVICES'
i += 1
elif myarg == 'nocompanydevices':
get_device_views.remove('COMPANY_INVENTORY')
view = 'USER_ASSIGNED_DEVICES'
i += 1
elif myarg == 'nopersonaldevices':
get_device_views.remove('USER_ASSIGNED_DEVICES')
view = 'COMPANY_INVENTORY'
i += 1
elif myarg == 'nodeviceusers':
get_device_users = False
i += 1
elif myarg == 'todrive':
todrive = True
i += 1
elif myarg == 'orderby':
fieldName = sys.argv[i + 1].lower()
i += 2
if fieldName in DEVICE_ORDERBY_CHOICES_MAP:
fieldName = DEVICE_ORDERBY_CHOICES_MAP[fieldName]
orderBy = ''
if i < len(sys.argv):
orderBy = sys.argv[i].lower()
if orderBy in SORTORDER_CHOICES_MAP:
orderBy = SORTORDER_CHOICES_MAP[orderBy]
i += 1
if orderBy != 'DESCENDING':
orderByList.append(fieldName)
else:
orderByList.append(f'{fieldName} desc')
else:
controlflow.expected_argument_exit(
'orderby', ', '.join(sorted(DEVICE_ORDERBY_CHOICES_MAP)),
fieldName)
elif myarg == 'sortheaders':
sortHeaders = True
i += 1
else:
controlflow.invalid_argument_exit(sys.argv[i], 'gam print devices')
controlflow.invalid_argument_exit(sys.argv[i], 'gam print devices')
view_name_map = {
None: 'Devices',
'COMPANY_INVENTORY': 'Company Devices',
'USER_ASSIGNED_DEVICES': 'Personal Devices',
}
if orderByList:
orderBy = ','.join(orderByList)
else:
orderBy = None
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)
page_message = gapi.got_total_items_msg(view_name_map[view], '...\n')
devices += gapi.get_all_pages(ci.devices(), 'list', 'devices',
customer=customer, page_message=page_message,
pageSize=100, filter=device_filter, view=view, orderBy=orderBy)
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)
page_message=page_message, pageSize=20, filter=device_filter)
for device_user in device_users:
for device in devices:
if device_user.get('name').startswith(device.get('name')):
@@ -180,13 +212,13 @@ 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
customer = _get_device_customerid()
device_filter = None
csv_file = None
serialnumber_column = 'serialNumber'
devicetype_column = 'deviceType'
static_devicetype = None
assetid_column = None
assettag_column = None
unassigned_missing_action = 'delete'
assigned_missing_action = 'donothing'
missing_actions = ['delete', 'wipe', 'donothing']
@@ -194,7 +226,7 @@ def sync():
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg in ['filter', 'query']:
filter = sys.argv[i+1]
device_filter = sys.argv[i+1]
i += 2
elif myarg == 'csvfile':
csv_file = sys.argv[i+1]
@@ -212,17 +244,17 @@ def sync():
', '.join(device_types),
sys.argv[i+1])
i += 2
elif myarg == 'assetidcolumn':
assetid_column = sys.argv[i+1]
elif myarg in {'assettagcolumn', 'assetidcolumn'}:
assettag_column = sys.argv[i+1]
i += 2
elif myarg == 'unassigned_missing_action':
elif myarg == 'unassignedmissingaction':
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':
elif myarg == 'assignedmissingaction':
assigned_missing_action = sys.argv[i+1].lower().replace('_', '')
if assigned_missing_action not in missing_actions:
controlflow.expected_argument_exit('assigned_missing_action',
@@ -230,44 +262,46 @@ def sync():
sys.argv[i+1])
i += 2
else:
controlflow.invalid_argument_exit(sys.argv[i], 'gam sync devices')
controlflow.invalid_argument_exit(sys.argv[i], 'gam sync devices')
if not csv_file:
controlflow.system_error_exit(
3, 'csvfile is a required argument for "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)
if assettag_column and assettag_column not in input_file.fieldnames:
controlflow.csv_field_error_exit(assettag_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}
local_device = {'serialNumber': row[serialnumber_column].strip().upper()}
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()
if assettag_column:
local_device['assetTag'] = row[assettag_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:
if assettag_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)
pageSize=100, filter=device_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')
last_sync = remote_device.pop('lastSyncTime', NEVER_TIME_NOMS)
name = remote_device.pop('name')
remote_device_map[sn] = {'name': name}
if last_sync == '1970-01-01T00:00:00Z':
if last_sync == NEVER_TIME_NOMS:
remote_device_map[sn]['unassigned'] = True
devices_to_add = [device for device in local_devices if device not in remote_devices]
missing_devices = [device for device in remote_devices if device not in local_devices]
@@ -292,6 +326,6 @@ def sync():
kwargs = {'customer': customer}
else:
kwargs = {'body': {'customer': customer}}
gapi.call(ci.devices(), unassigned_missing_action,
gapi.call(ci.devices(), action,
name=name, **kwargs)
print(f'{action}d {sn}')

View File

@@ -1,5 +1,3 @@
import csv
import gam
from gam.var import *
from gam import controlflow
@@ -9,7 +7,6 @@ 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():
@@ -240,6 +237,11 @@ def print_():
for groupEntity in entityList:
i += 1
groupEmail = groupEntity['groupKey']['id']
for k, v in iter(groupEntity.pop('labels', {}).items()):
if v == '':
groupEntity[f'labels.{k}'] = True
else:
groupEntity[f'labels.{k}'] = v
group = utils.flatten_json(groupEntity)
for a_key in group:
if a_key not in titles:
@@ -307,7 +309,7 @@ def print_():
csvRows.append(group)
if sortHeaders:
display.sort_csv_titles([
'Email',
'name', 'groupKey.id'
], titles)
display.write_csv_file(csvRows, titles, 'Groups', todrive)
@@ -410,6 +412,7 @@ def update():
def _getRoleAndUsers():
checkSuspended = None
role = None
expireTime = None
i = 5
if sys.argv[i].lower() in GROUP_ROLES_MAP:
role = GROUP_ROLES_MAP[sys.argv[i].lower()]
@@ -417,6 +420,9 @@ def update():
if sys.argv[i].lower() in ['suspended', 'notsuspended']:
checkSuspended = sys.argv[i].lower() == 'suspended'
i += 1
if sys.argv[i].lower() in ['expire', 'expires']:
expireTime = sys.argv[i+1]
i += 2
if sys.argv[i].lower() in usergroup_types:
users_email = gam.getUsersToModify(entity_type=sys.argv[i].lower(),
entity=sys.argv[i + 1],
@@ -427,7 +433,7 @@ def update():
gam.normalizeEmailAddressOrUID(sys.argv[i],
checkForCustomerId=True)
]
return (role, users_email)
return (role, expireTime, users_email)
ci = gapi_cloudidentity.build('cloudidentity_beta')
group = sys.argv[3]
@@ -442,7 +448,7 @@ def update():
if not parent:
return
if myarg == 'add':
role, users_email = _getRoleAndUsers()
role, expireTime, users_email = _getRoleAndUsers()
if not role:
role = ROLE_MEMBER
if len(users_email) > 1:
@@ -451,8 +457,10 @@ def update():
for user_email in users_email:
item = [
'gam', 'update', 'cigroup', f'id:{parent}', 'add', role,
user_email
]
if expireTime:
item.extend(['expires', expireTime])
item.append(user_email)
items.append(item)
elif len(users_email) > 0:
body = {
@@ -465,6 +473,10 @@ def update():
}
if role != ROLE_MEMBER:
body['roles'].append({'name': role})
if expireTime:
for role in body['roles']:
if role['name'] == ROLE_MEMBER:
role['expiryDetail'] = {'expireTime': expireTime}
add_text = [f'as {role}']
for i in range(2):
try:
@@ -497,7 +509,7 @@ def update():
elif myarg == 'sync':
syncMembersSet = set()
syncMembersMap = {}
role, users_email = _getRoleAndUsers()
role, expireTime, 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])
@@ -530,17 +542,18 @@ def update():
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
]
item = ['gam', 'update', 'cigroup', f'id:{parent}', 'add',
role,]
if expireTime:
item.extend(['expires', expireTime])
item.append(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()
_, _, users_email = _getRoleAndUsers()
if len(users_email) > 1:
sys.stderr.write(
f'Group: {group}, Will remove {len(users_email)} emails.\n')
@@ -566,7 +579,7 @@ def update():
f' Group: {group}, {users_email[0]} Remove Failed: {str(e)}'
)
elif myarg == 'update':
role, users_email = _getRoleAndUsers()
role, expireTime, users_email = _getRoleAndUsers()
if not role:
role = ROLE_MEMBER
if len(users_email) > 1:
@@ -576,29 +589,43 @@ def update():
for user_email in users_email:
item = [
'gam', 'update', 'cigroup', f'id:{parent}', 'update',
role, user_email
]
role,]
if expireTime:
item.extend(['expires', expireTime])
item.append(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:
if crole not in {ROLE_MEMBER, role}:
removeRoles.append(crole)
if role not in current_roles:
addRoles.append({'name': role})
new_role = {'name': role}
if role == ROLE_MEMBER and expireTime:
new_role['expiryDetail'] = {'expireTime': expireTime}
expireTime = None
addRoles.append(new_role)
bodys = []
if addRoles:
bodys.append({'addRoles': addRoles})
if removeRoles:
bodys.append({'removeRoles': removeRoles})
if expireTime:
bodys.append({
'name': ROLE_MEMBER,
# Note this doesn't actually work for some reason. Only known method to change
# expire time right now is to remove/re-add member.
'expiryDetail': {
'expireTime': expireTime
}
})
for body in bodys:
try:
gapi.call(ci.groups().memberships(),

View File

@@ -1,7 +1,6 @@
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

View File

@@ -1,4 +1,7 @@
import datetime
import time
import googleapiclient
from gam.var import *
import gam
@@ -7,10 +10,94 @@ 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 orgunits as gapi_directory_orgunits
from gam import utils
def issue_command():
cd = gapi_directory.build()
i, devices = getCrOSDeviceEntity(3, cd)
body = {}
final_states = ['EXPIRED', 'CANCELLED', 'EXECUTED_BY_CLIENT']
valid_commands = gapi.get_enum_values_minus_unspecified(
cd._rootDesc['schemas']
['DirectoryChromeosdevicesIssueCommandRequest']
['properties']['commandType']['enum'])
command_map = {}
for valid_command in valid_commands:
v = valid_command.lower().replace('_', '')
command_map[v] = valid_command
times_to_check_status = 1
doit = False
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'command':
command = sys.argv[i+1].lower().replace('_', '')
if command not in command_map:
controlflow.system_error_exit(2, f'expected command of ' \
f'{", ".join(valid_commands)} got {command}')
body['commandType'] = command_map[command]
i += 2
if command == 'setvolume':
body['payload'] = {'volume': int(sys.argv[i])}
i += 1
elif myarg == 'timestocheckstatus':
times_to_check_status = int(sys.argv[i+1])
i += 2
elif myarg == 'doit':
doit = True
i += 1
else:
controlflow.invalid_argument_exit(sys.argv[i], 'gam issuecommand cros')
if body['commandType'] == 'WIPE_USERS' and not doit:
controlflow.system_error_exit(2, 'wipe_users command requires admin ' \
'acknowledge user data will be destroyed with the ' \
'doit argument')
if body['commandType'] == 'REMOTE_POWERWASH' and not doit:
controlflow.system_error_exit(2, 'remote_powerwash command requires ' \
'admin acknowledge user data will be destroyed, device will need' \
' to be reconnected to WiFi and re-enrolled with the doit argument')
for device_id in devices:
try:
result = gapi.call(cd.customer().devices().chromeos(), 'issueCommand',
customerId=GC_Values[GC_CUSTOMER_ID], deviceId=device_id,
throw_reasons=[gapi_errors.ErrorReason.FOUR_O_O],
body=body)
except googleapiclient.errors.HttpError:
controlflow.system_error_exit(4, '400 response from Google. This ' \
'usually indicates the devices was not in a state where it will' \
' accept the command. For example, reboot and take_a_screenshot' \
' require the device to be in auto-start kiosk app mode.')
display.print_json(result)
command_id = result.get('commandId')
for i in range(0, times_to_check_status):
time.sleep(2)
result = gapi.call(cd.customer().devices().chromeos().commands(), 'get',
customerId=GC_Values[GC_CUSTOMER_ID], deviceId=device_id,
commandId=command_id)
display.print_json(result)
state = result.get('state')
if state in final_states:
break
def get_command():
cd = gapi_directory.build()
i, devices = getCrOSDeviceEntity(3, cd)
command_id = None
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'commandid':
command_id = sys.argv[i+1]
i += 2
else:
controlflow.invalid_argument_exit(sys.argv[i], 'gam getcommand cros')
for device_id in devices:
result = gapi.call(cd.customer().devices().chromeos().commands(), 'get',
customerId=GC_Values[GC_CUSTOMER_ID], deviceId=device_id,
commandId=command_id)
display.print_json(result)
def doUpdateCros():
cd = gapi_directory.build()
i, devices = getCrOSDeviceEntity(3, cd)

View File

@@ -221,11 +221,11 @@ def info_member():
cd = gapi_directory.build()
memberKey = gam.normalizeEmailAddressOrUID(sys.argv[3])
groupKey = gam.normalizeEmailAddressOrUID(sys.argv[4])
info = gapi.call(cd.members(),
'get',
memberKey=memberKey,
groupKey=groupKey)
display.print_json(info)
member_info = gapi.call(cd.members(),
'get',
memberKey=memberKey,
groupKey=groupKey)
display.print_json(member_info)
GROUP_ARGUMENT_TO_PROPERTY_TITLE_MAP = {
@@ -1146,7 +1146,7 @@ def update():
i += 2
elif myarg == 'admincreated':
use_cd_api = True
cd_body['adminCreated'] = getBoolean(sys.argv[i + 1], myarg)
cd_body['adminCreated'] = gam.getBoolean(sys.argv[i + 1], myarg)
i += 2
elif myarg == 'getbeforeupdate':
gs_get_before_update = True
@@ -1242,5 +1242,3 @@ def getGroupAttrValue(myarg, value, gs_object, gs_body, function):
gs_body[attrib] = value
return
controlflow.invalid_argument_exit(myarg, f'gam {function} group')

View File

@@ -23,16 +23,16 @@ def delete():
def info():
cd = gapi_directory.build()
resourceId = sys.argv[3]
info = gapi.call(cd.mobiledevices(),
'get',
customerId=GC_Values[GC_CUSTOMER_ID],
resourceId=resourceId)
device_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(
device_info['deviceId'] = device_info['deviceId'].encode('unicode-escape').decode(
UTF8)
attrib = 'securityPatchLevel'
if attrib in info and int(info[attrib]):
info[attrib] = utils.formatTimestampYMDHMS(info[attrib])
if attrib in info and int(device_info[attrib]):
device_info[attrib] = utils.formatTimestampYMDHMS(device_info[attrib])
display.print_json(info)
@@ -161,7 +161,7 @@ def print_():
def update():
cd = gapi_directory.build
cd = gapi_directory.build()
resourceIds = sys.argv[3]
match_users = None
doit = False
@@ -235,5 +235,3 @@ def update():
resourceId=device['resourceId'],
body=body,
customerId=GC_Values[GC_CUSTOMER_ID])

View File

@@ -7,7 +7,6 @@ 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():
@@ -229,12 +228,12 @@ def update():
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])
users = gam.getUsersToModify(entity_type=entity_type,
entity=sys.argv[6])
else:
entity_type = 'users'
users = getUsersToModify(entity_type=entity_type,
entity=sys.argv[5])
users = gam.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):
@@ -253,7 +252,7 @@ def update():
for user in users:
i += 1
sys.stderr.write(
f' moving {user} to {orgUnitPath}{currentCountNL(i, count)}'
f' moving {user} to {orgUnitPath}{gam.currentCountNL(i, count)}'
)
try:
gapi.call(cd.users(),
@@ -377,7 +376,7 @@ def getTopLevelOrgId(cd, orgUnitPath):
def getOrgUnitId(orgUnit, cd=None):
if cd is None:
cd = buildGAPIObject('directory')
cd = gapi_directory.build()
orgUnit = getOrgUnitItem(orgUnit)
if orgUnit[:3] == 'id:':
return (orgUnit, orgUnit)

View File

@@ -15,7 +15,7 @@ def flatten_privilege_list(privs, parent=None):
parent=priv['privilegeName'])
priv['children'] = ' '.join(
[child['privilegeName'] for child in children])
del (priv['childPrivileges'])
del priv['childPrivileges']
flat_privs = flat_privs + children
flat_privs.append(priv)
return flat_privs

View File

@@ -28,5 +28,3 @@ def turn_off_2sv(users):
'turnOff',
soft_errors=True,
userKey=user)

299
src/gam/gapi/licensing.py Normal file
View File

@@ -0,0 +1,299 @@
import re
import sys
import gam
from gam.var import *
from gam import controlflow
from gam import display
from gam import gapi
from gam.gapi import errors as gapi_errors
def build():
return gam.buildGAPIObject('licensing')
def getProductAndSKU(sku):
l_sku = sku.lower().replace('-', '').replace(' ', '')
for a_sku, sku_values in list(SKUS.items()):
if l_sku == a_sku.lower().replace(
'-',
'') or l_sku in sku_values['aliases'] or l_sku == sku_values[
'displayName'].lower().replace(' ', ''):
return (sku_values['product'], a_sku)
try:
product = re.search('^([A-Z,a-z]*-[A-Z,a-z]*)', sku).group(1)
except AttributeError:
product = sku
return (product, sku)
def user_lic_result(request_id, response, exception):
if exception:
http_status, reason, message = gapi_errors.get_gapi_error_detail(
exception,
soft_errors=True)
print(f'ERROR: {request_id}: {http_status} - {reason} {message}')
def create(users, sku=None):
lic = build()
if not sku:
sku = sys.argv[5]
productId, skuId = getProductAndSKU(sku)
sku_name = _formatSKUIdDisplayName(skuId)
i = 6
if len(sys.argv) > 6 and sys.argv[i].lower() in ['product', 'productid']:
productId = sys.argv[i+1]
i += 2
for user in users:
print(f'Adding license {sku_name} from to {user}')
gapi.call(lic.licenseAssignments(),
'insert',
soft_errors=True,
productId=productId,
skuId=skuId,
body={'userId': user})
def delete(users, sku=None):
lic = build()
if not sku:
sku = sys.argv[5]
productId, skuId = getProductAndSKU(sku)
sku_name = _formatSKUIdDisplayName(skuId)
i = 6
if len(sys.argv) > 6 and sys.argv[i].lower() in ['product', 'productid']:
productId = sys.argv[i+1]
i += 2
for user in users:
print(f'Removing license {sku_name} from user {user}')
gapi.call(lic.licenseAssignments(),
'delete',
soft_errors=True,
productId=productId,
skuId=skuId,
userId=user)
def sync(users):
sku = sys.argv[5]
current_licenses = gam.getUsersToModify(entity_type='license',
entity=sku)
users_to_license = [user for user in users if user not in current_licenses]
users_to_unlicense = [user for user in current_licenses if user not in users]
print(f'Need to remove license from {len(users_to_unlicense)} and add to ' \
f'{len(users_to_license)} users...')
# do the remove first to free up seats
delete(users_to_unlicense, sku)
create(users_to_license, sku)
def update(users, sku=None, old_sku=None):
lic = build()
if not sku:
sku = sys.argv[5]
productId, skuId = getProductAndSKU(sku)
sku_name = _formatSKUIdDisplayName(skuId)
i = 6
if len(sys.argv) > 6 and sys.argv[i].lower() in ['product', 'productid']:
productId = sys.argv[i+1]
i += 2
if not old_sku:
try:
old_sku = sys.argv[i]
if old_sku.lower() == 'from':
old_sku = sys.argv[i + 1]
except KeyError:
controlflow.system_error_exit(
2,
'You need to specify the user\'s old SKU as the last argument'
)
_, old_sku = getProductAndSKU(old_sku)
old_sku_name = _formatSKUIdDisplayName(old_sku)
for user in users:
print(f'Changing user {user} from license {old_sku_name} to {sku_name}')
gapi.call(lic.licenseAssignments(),
'patch',
soft_errors=True,
productId=productId,
skuId=old_sku,
userId=user,
body={'skuId': skuId})
def print_(returnFields=None,
skus=None,
countsOnly=False,
returnCounts=False):
lic = build()
products = []
licenses = []
licenseCounts = []
if not returnFields:
csvRows = []
todrive = False
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower()
if not returnCounts and myarg == 'todrive':
todrive = True
i += 1
elif myarg in ['products', 'product']:
products = sys.argv[i + 1].split(',')
i += 2
elif myarg in ['sku', 'skus']:
skus = sys.argv[i + 1].split(',')
i += 2
elif myarg == 'allskus':
skus = sorted(SKUS)
products = []
i += 1
elif myarg == 'gsuite':
skus = [
skuId for skuId in SKUS
if SKUS[skuId]['product'] in ['Google-Apps', '101031']
]
products = []
i += 1
elif myarg == 'countsonly':
countsOnly = True
i += 1
else:
controlflow.invalid_argument_exit(sys.argv[i],
'gam print licenses')
if not countsOnly:
fields = 'nextPageToken,items(productId,skuId,userId)'
titles = ['userId', 'productId', 'skuId']
else:
fields = 'nextPageToken,items(userId)'
if not returnCounts:
if skus:
titles = ['productId', 'skuId', 'licenses']
else:
titles = ['productId', 'licenses']
else:
fields = f'nextPageToken,items({returnFields})'
if skus:
for sku in skus:
if not products:
product, sku = getProductAndSKU(sku)
else:
product = products[0]
page_message = gapi.got_total_items_msg(
f'Licenses for {SKUS.get(sku, {"displayName": sku})["displayName"]}',
'...\n')
try:
licenses += gapi.get_all_pages(
lic.licenseAssignments(),
'listForProductAndSku',
'items',
throw_reasons=[
gapi_errors.ErrorReason.INVALID,
gapi_errors.ErrorReason.FORBIDDEN
],
page_message=page_message,
customerId=GC_Values[GC_DOMAIN],
productId=product,
skuId=sku,
fields=fields)
if countsOnly:
licenseCounts.append([
'Product', product, 'SKU', sku, 'Licenses',
len(licenses)
])
licenses = []
except (gapi_errors.GapiInvalidError,
gapi_errors.GapiForbiddenError):
pass
else:
if not products:
products = sorted(PRODUCTID_NAME_MAPPINGS)
for productId in products:
page_message = gapi.got_total_items_msg(
f'Licenses for {PRODUCTID_NAME_MAPPINGS.get(productId, productId)}',
'...\n')
try:
licenses += gapi.get_all_pages(
lic.licenseAssignments(),
'listForProduct',
'items',
throw_reasons=[
gapi_errors.ErrorReason.INVALID,
gapi_errors.ErrorReason.FORBIDDEN
],
page_message=page_message,
customerId=GC_Values[GC_DOMAIN],
productId=productId,
fields=fields)
if countsOnly:
licenseCounts.append(
['Product', productId, 'Licenses',
len(licenses)])
licenses = []
except (gapi_errors.GapiInvalidError,
gapi_errors.GapiForbiddenError):
pass
if countsOnly:
if returnCounts:
return licenseCounts
if skus:
for u_license in licenseCounts:
csvRows.append({
'productId': u_license[1],
'skuId': u_license[3],
'licenses': u_license[5]
})
else:
for u_license in licenseCounts:
csvRows.append({
'productId': u_license[1],
'licenses': u_license[3]
})
display.write_csv_file(csvRows, titles, 'Licenses', todrive)
return
if returnFields:
if returnFields == 'userId':
userIds = []
for u_license in licenses:
userId = u_license.get('userId', '').lower()
if userId:
userIds.append(userId)
return userIds
userSkuIds = {}
for u_license in licenses:
userId = u_license.get('userId', '').lower()
skuId = u_license.get('skuId')
if userId and skuId:
userSkuIds.setdefault(userId, [])
userSkuIds[userId].append(skuId)
return userSkuIds
for u_license in licenses:
userId = u_license.get('userId', '').lower()
skuId = u_license.get('skuId', '')
csvRows.append({
'userId': userId,
'productId': u_license.get('productId', ''),
'skuId': _skuIdToDisplayName(skuId)
})
display.write_csv_file(csvRows, titles, 'Licenses', todrive)
def show():
licenseCounts = print_(countsOnly=True, returnCounts=True)
for u_license in licenseCounts:
line = ''
for i in range(0, len(u_license), 2):
line += f'{u_license[i]}: {u_license[i+1]}, '
print(line[:-2])
def _skuIdToDisplayName(skuId):
return SKUS[skuId]['displayName'] if skuId in SKUS else skuId
def _formatSKUIdDisplayName(skuId):
skuIdDisplay = _skuIdToDisplayName(skuId)
if skuId == skuIdDisplay:
return skuId
return f'{skuId} ({skuIdDisplay})'

View File

@@ -11,6 +11,7 @@ from gam import controlflow
from gam import display
from gam import gapi
from gam import utils
from gam.gapi.directory import orgunits as gapi_directory_orgunits
def build():
@@ -178,7 +179,7 @@ def showUsage():
skip_day_numbers = [dow.index(d) for d in skipdaynames if d in dow]
i += 2
elif report == 'user' and myarg in ['orgunit', 'org', 'ou']:
_, orgUnitId = gam.getOrgUnitId(sys.argv[i + 1])
_, orgUnitId = gapi_directory_orgunits.getOrgUnitId(sys.argv[i + 1])
i += 2
elif report == 'user' and myarg in usergroup_types:
users = gam.getUsersToModify(myarg, sys.argv[i + 1])
@@ -296,7 +297,7 @@ def showReport():
tryDate = utils.get_yyyymmdd(sys.argv[i + 1])
i += 2
elif myarg in ['orgunit', 'org', 'ou']:
_, orgUnitId = gam.getOrgUnitId(sys.argv[i + 1])
_, orgUnitId = gapi_directory_orgunits.getOrgUnitId(sys.argv[i + 1])
i += 2
elif myarg == 'fulldatarequired':
fullDataRequired = []

View File

@@ -184,5 +184,5 @@ def update():
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.'
f'You can now add {a_domain} or it\'s subdomains as secondary or domain aliases of the {GC_Values[GC_DOMAIN]} Google Workspace Account.'
)

View File

@@ -8,7 +8,7 @@ import platform
import re
GAM_AUTHOR = 'Jay Lee <jay0lee@gmail.com>'
GAM_VERSION = '5.20'
GAM_VERSION = '5.24'
GAM_LICENSE = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
GAM_URL = 'https://git.io/gam'
@@ -114,10 +114,44 @@ SKUS = {
'aliases': ['gau', 'gsb', 'unlimited', 'gsuitebusiness'],
'displayName': 'G Suite Business'
},
'1010020027': {
'product': 'Google-Apps',
'aliases': ['wsbizstart', 'workspacebusinessstarter'],
'displayName': 'Workspace Business Starter'
},
'1010020028': {
'product': 'Google-Apps',
'aliases': ['wsbizstan', 'workspacebusinessstandard'],
'displayName': 'Workspace Business Standard'
},
'1010020025': {
'product': 'Google-Apps',
'aliases': ['wsbizplus', 'workspacebusinessplus'],
'displayName': 'Workspace Business Plus'
},
'1010060001': {
'product': 'Google-Apps',
'aliases': [
'gsuiteessentials', 'essentials', 'd4e', 'driveenterprise',
'drive4enterprise', 'wsess', 'workspaceesentials'
],
'displayName': 'Google Workspace Essentials'
},
'1010060003': {
'product': 'Google-Apps',
'aliases': ['wsentess', 'workspaceenterpriseessentials'],
'displayName': 'Workspace Enterprise Essentials'
},
'1010020026': {
'product': 'Google-Apps',
'aliases': ['wsentstan', 'workspaceenterprisestandard'],
'displayName': 'Workspace Enterprise Standard'
},
'1010020020': {
'product': 'Google-Apps',
'aliases': ['gae', 'gse', 'enterprise', 'gsuiteenterprise'],
'displayName': 'G Suite Enterprise'
'aliases': ['gae', 'gse', 'enterprise', 'gsuiteenterprise',
'wsentplus', 'workspaceenterpriseplus'],
'displayName': 'Workspace Enterprise Plus'
},
'1010340002': {
'product': '101034',
@@ -127,15 +161,7 @@ SKUS = {
'1010340001': {
'product': '101034',
'aliases': ['gseau', 'enterprisearchived', 'gsuiteenterprisearchived'],
'displayName': 'G Suite Enterprise Archived'
},
'1010060001': {
'product': '101006',
'aliases': [
'gsuiteessentials', 'essentials', 'd4e', 'driveenterprise',
'drive4enterprise'
],
'displayName': 'G Suite Essentials'
'displayName': 'Google Workspace Enterprise Plus Archived'
},
'Google-Drive-storage-20GB': {
'product': 'Google-Drive-storage',
@@ -192,11 +218,6 @@ SKUS = {
'aliases': ['vfe', 'googlevaultformeremployee'],
'displayName': 'Google Vault Former Employee'
},
'Google-Coordinate': {
'product': 'Google-Coordinate',
'aliases': ['coordinate', 'googlecoordinate'],
'displayName': 'Google Coordinate'
},
'Google-Chrome-Device-Management': {
'product': 'Google-Chrome-Device-Management',
'aliases': ['chrome', 'cdm', 'googlechromedevicemanagement'],
@@ -210,7 +231,7 @@ PRODUCTID_NAME_MAPPINGS = {
'101031': 'G Suite Enterprise for Education',
'101033': 'Google Voice',
'101034': 'G Suite Archived',
'Google-Apps': 'G Suite',
'Google-Apps': 'Google Workspace',
'Google-Chrome-Device-Management': 'Google Chrome Device Management',
'Google-Coordinate': 'Google Coordinate',
'Google-Drive-storage': 'Google Drive Storage',
@@ -223,7 +244,6 @@ V1_DISCOVERY_APIS = {
'appsactivity',
'calendar',
'drive',
'licensing',
'oauth2',
'reseller',
'siteVerification',
@@ -367,11 +387,21 @@ CALENDAR_NOTIFICATION_TYPES_MAP = {
'agenda': 'agenda',
}
DEVICE_ORDERBY_CHOICES_MAP = {
'createtime': 'create_time',
'devicetype': 'device_type',
'lastsynctime': 'last_sync_time',
'model': 'model',
'osversion': 'os_version',
'serialnumber': 'serial_number'
}
DRIVEFILE_FIELDS_CHOICES_MAP = {
'alternatelink': 'alternateLink',
'appdatacontents': 'appDataContents',
'cancomment': 'canComment',
'canreadrevisions': 'canReadRevisions',
'contentrestrictions': 'contentRestrictions',
'copyable': 'copyable',
'copyrequireswriterpermission': 'copyRequiresWriterPermission',
'createddate': 'createdDate',
@@ -480,16 +510,18 @@ DRIVEFILE_LABEL_CHOICES_MAP = {
}
APPLICATION_VND_GOOGLE_APPS = 'application/vnd.google-apps.'
MIMETYPE_GA_DOCUMENT = APPLICATION_VND_GOOGLE_APPS + 'document'
MIMETYPE_GA_DRAWING = APPLICATION_VND_GOOGLE_APPS + 'drawing'
MIMETYPE_GA_FOLDER = APPLICATION_VND_GOOGLE_APPS + 'folder'
MIMETYPE_GA_FORM = APPLICATION_VND_GOOGLE_APPS + 'form'
MIMETYPE_GA_FUSIONTABLE = APPLICATION_VND_GOOGLE_APPS + 'fusiontable'
MIMETYPE_GA_MAP = APPLICATION_VND_GOOGLE_APPS + 'map'
MIMETYPE_GA_PRESENTATION = APPLICATION_VND_GOOGLE_APPS + 'presentation'
MIMETYPE_GA_SCRIPT = APPLICATION_VND_GOOGLE_APPS + 'script'
MIMETYPE_GA_SITES = APPLICATION_VND_GOOGLE_APPS + 'sites'
MIMETYPE_GA_SPREADSHEET = APPLICATION_VND_GOOGLE_APPS + 'spreadsheet'
MIMETYPE_GA_DOCUMENT = f'{APPLICATION_VND_GOOGLE_APPS}document'
MIMETYPE_GA_DRAWING = f'{APPLICATION_VND_GOOGLE_APPS}drawing'
MIMETYPE_GA_FOLDER = f'{APPLICATION_VND_GOOGLE_APPS}folder'
MIMETYPE_GA_FORM = f'{APPLICATION_VND_GOOGLE_APPS}form'
MIMETYPE_GA_FUSIONTABLE = f'{APPLICATION_VND_GOOGLE_APPS}fusiontable'
MIMETYPE_GA_MAP = f'{APPLICATION_VND_GOOGLE_APPS}map'
MIMETYPE_GA_PRESENTATION = f'{APPLICATION_VND_GOOGLE_APPS}presentation'
MIMETYPE_GA_SCRIPT = f'{APPLICATION_VND_GOOGLE_APPS}script'
MIMETYPE_GA_SITES = f'{APPLICATION_VND_GOOGLE_APPS}sites'
MIMETYPE_GA_SPREADSHEET = f'{APPLICATION_VND_GOOGLE_APPS}spreadsheet'
MIMETYPE_GA_SHORTCUT = f'{APPLICATION_VND_GOOGLE_APPS}shortcut'
MIMETYPE_GA_3P_SHORTCUT = f'{APPLICATION_VND_GOOGLE_APPS}drive-sdk'
MIMETYPE_CHOICES_MAP = {
'gdoc': MIMETYPE_GA_DOCUMENT,
@@ -501,9 +533,12 @@ MIMETYPE_CHOICES_MAP = {
'gfusion': MIMETYPE_GA_FUSIONTABLE,
'gpresentation': MIMETYPE_GA_PRESENTATION,
'gscript': MIMETYPE_GA_SCRIPT,
'gshortcut': MIMETYPE_GA_SHORTCUT,
'g3pshortcut': MIMETYPE_GA_3P_SHORTCUT,
'gsite': MIMETYPE_GA_SITES,
'gsheet': MIMETYPE_GA_SPREADSHEET,
'gspreadsheet': MIMETYPE_GA_SPREADSHEET,
'shortcut': MIMETYPE_GA_SHORTCUT,
}
DFA_CONVERT = 'convert'
@@ -1139,7 +1174,7 @@ 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
# Admin email address, required when enable_dasa is true, overrides oauth2.txt value otherwise
GC_ADMIN_EMAIL = 'admin_email'
# If debug_level > 0: extra_args[u'prettyPrint'] = True,
# httplib2.debuglevel = gam_debug_level, appsObj.debug = True
@@ -1350,6 +1385,7 @@ GC_VAR_INFO = {
# Google API constants
NEVER_TIME = '1970-01-01T00:00:00.000Z'
NEVER_TIME_NOMS = '1970-01-01T00:00:00Z'
ROLE_MANAGER = 'MANAGER'
ROLE_MEMBER = 'MEMBER'
ROLE_OWNER = 'OWNER'