Compare commits

...

55 Commits
v6.00 ... v6.02

Author SHA1 Message Date
Ross Scroggs
41a6c11c55 Handle TYPE_MESSAGE fields with durations or counts as a special case (#1375)
* Handle TYPE_MESSAGE fields with durations or counts as a special case

* Allow schema TYPE_ENUM field values with/without common prefix
2021-05-04 10:15:36 -04:00
Jay Lee
57d908e369 disable grouptree for now 2021-05-04 09:59:21 -04:00
Jay Lee
64274fdb33 more test fixes 2021-05-04 09:19:59 -04:00
Jay Lee
da919fd189 add few tests and fix one 2021-05-04 09:17:14 -04:00
Jay Lee
cfa25f12d3 6.02, admin.googleapis.com as test, MacOS universal2 build 2021-05-04 09:12:45 -04:00
Jay Lee
05bc1c1263 just stick with staic python versions 2021-05-04 08:38:56 -04:00
Jay Lee
939c79c37f use env variable 2021-05-04 08:32:06 -04:00
Jay Lee
d352ddeea1 use env variable 2021-05-04 08:30:56 -04:00
Jay Lee
72a683f2b1 Merge branches (#1377)
* Fix tests with apiclient >= 2.1

* disable MacOS 11 job

* info user grouptree and info cigroup membertree

* build updates
2021-05-04 08:12:35 -04:00
Ross Scroggs
784399f345 Handle TYPE_MESSAGE fields with durations or counts as a special case (#1374) 2021-05-02 08:22:29 -04:00
Ross Scroggs
710be4371b Fix typo (#1373) 2021-04-30 21:00:51 -04:00
Jay Lee
eece358aec Googleapiclient test fix (#1372)
* Fix tests with apiclient >= 2.1

* disable MacOS 11 job
2021-04-26 07:35:07 -04:00
Ross Scroggs
b43ada4f83 Add Cloud Search product name (#1370) 2021-04-23 09:02:15 -04:00
Jay Lee
9030af4faf Cloud Search SKU 2021-04-21 09:46:11 -04:00
Ross Scroggs
38b424b62e Add convertalias to delegate commands to convert aliases to primary (#1368)
* Add convertalias to delegate commands to convert aliases to primary

* New PyInstaller, won't build ARM without it
2021-04-21 09:38:07 -04:00
Jay Lee
1d9bf0b1aa Merge branch 'main' of https://github.com/jay0lee/GAM into main 2021-04-20 15:57:06 -04:00
Jay Lee
d3b7700c07 re-enable MacOS universal2 build 2021-04-20 15:56:27 -04:00
Ross Scroggs
d9513e159f Added support for localfile - in gam <UserTypeEntity> create|update drivefile (#1366)
This allows commands/programs to output data to stdout which can then be uploaded to a Google Drive file.
```
generatedata | gam user user@domain.com create drivefile drivefilename test.csv localfile - mimetype gsheet
```
2021-04-20 15:43:09 -04:00
Jay Lee
6ddfdf2514 print labels with counts 2021-04-20 15:37:21 -04:00
Jay Lee
478804bd5c Show/print full teamdrives info 2021-04-15 12:25:04 -04:00
Jay Lee
b61165a753 make sure we are using primary addresses for delegation 2021-04-10 21:28:19 -04:00
Ross Scroggs
b3814ae7be Document new Google Workspave Frontline license (#1363) 2021-04-08 16:42:07 -04:00
Jay Lee
019c363a74 Merge branch 'main' of https://github.com/jay0lee/GAM into main 2021-04-08 13:10:46 -04:00
Jay Lee
da5f80e704 Workspace Frontline SKU 2021-04-08 13:10:33 -04:00
Ross Scroggs
b37b10e669 Restandardize chromehistory columns; fix chromepolicy (#1362)
* Restandardize chromehistory columns; fix chromepolicy

* Update chromehistory.py
2021-04-08 11:27:30 -04:00
Jay Lee
8ca92eda39 G Suite > Workspace in few more spots 2021-04-08 09:38:37 -04:00
Jay Lee
81dbbc36db build channel and platform maps dynamically to reduce future maintenance 2021-04-08 09:08:04 -04:00
Jay Lee
7065101b87 further refine chromehistory output 2021-04-08 08:14:00 -04:00
Jay Lee
00c302e545 further refine chromehistory output 2021-04-08 08:12:15 -04:00
Ross Scroggs
703530ce7f Standardize chrome history column order; update data transfer apps (#1361) 2021-04-08 07:50:52 -04:00
Jay Lee
7ac15042d8 Merge branch 'main' of https://github.com/jay0lee/GAM into main 2021-04-07 15:27:32 -04:00
Jay Lee
a80ec52027 add more useful columns to chromehistory 2021-04-07 15:27:28 -04:00
Ross Scroggs
4da4132220 Validate chrome.users.chromebrowserupdates targetVersionPrefixSetting channel-offset (#1359)
* Validate chrome.users.chromebrowserupdates targetVersionPrefixSetting channel-offset

* Fix typo, add extended channel

Pass extended on to maintainer of :
https://developer.chrome.com/docs/versionhistory/reference/#channel-identifiers
2021-04-07 15:09:45 -04:00
Jay Lee
8682e66eb0 Update build.yml 2021-04-07 13:01:27 -04:00
Ross Scroggs
34bf205d37 Fix indentation (#1357) 2021-04-07 12:34:31 -04:00
Jay Lee
d6c2c6a2c3 Lazy load yubikey module to avoid lib errors when not in use 2021-04-07 09:27:13 -04:00
Jay Lee
f45639e6e2 switch User Invitations to DwD for now 2021-04-06 17:42:39 -04:00
Jay Lee
82968e29bf fix tests 2021-04-06 16:44:07 -04:00
Jay Lee
5d3d571545 Merge branch 'main' of https://github.com/jay0lee/GAM into main 2021-04-06 16:35:49 -04:00
Jay Lee
6999c13877 allow Chrome pinning to relative version like 'stable-1' 2021-04-06 16:35:34 -04:00
Ross Scroggs
82a551e88f Have whatis check for unmanaged accounts (#1355)
* Have whatis check for unmanaged accounts

* Handle addition error in whatis
2021-04-06 16:29:08 -04:00
Ross Scroggs
1b1a0c876c Implement Chrome version history (#1354)
* Implement Chrome version history

* Update GamCommands.txt

* Use httpObj
2021-04-06 14:08:27 -04:00
Ross Scroggs
b262c4a898 Implement Issue #1345 (#1352)
* Implement Issue #1345

* Clean up verifynotinvitable
2021-04-06 13:26:19 -04:00
Jay Lee
22d1055d82 allow i 2021-04-06 12:37:48 -04:00
Jay Lee
fe38565a9a 3.9.4 2021-04-06 12:06:57 -04:00
Jay Lee
a25d14e83f pin to google api client 2.0.2 for now 2021-04-06 11:56:51 -04:00
Jay Lee
15b21dd8d7 Merge branch 'main' of https://github.com/jay0lee/GAM into main 2021-04-06 09:18:13 -04:00
Jay Lee
caedcde49b Update build.yml 2021-04-04 19:23:29 -04:00
Ross Scroggs
8091e23e00 Implement Chrome Management API calls (#1350)
* Implement Chrome Management API calls

* User start/end in print chromeappdevices

* Handle a Chrome version without a version field
2021-04-02 14:44:58 -04:00
Jay Lee
08e1090b15 Update build.yml 2021-04-02 14:43:51 -04:00
Jay Lee
f76b5cb2eb Merge branch 'main' of https://github.com/jay0lee/GAM into main 2021-04-02 08:11:05 -04:00
Dima Scherbakov
edc4311dcb Bump google-api-python-client requirements to v2.0.0 (#1346)
We pass static_discovery keyword arg that got introduced in v2 only.
2021-03-28 15:55:56 -04:00
Jay Lee
a613bff664 Update build.yml 2021-03-25 14:36:09 -04:00
Jay Lee
8f875d2a9c Update build.yml 2021-03-25 14:35:49 -04:00
Jay Lee
fb60e0b389 enable chromemanagement reporting api 2021-03-25 11:01:14 -04:00
24 changed files with 1625 additions and 243 deletions

View File

@@ -25,7 +25,7 @@ cd ~
if [ "$PLATFORM" == "x86_64" ]; then
export pyfile=python-$BUILD_PYTHON_VERSION-macosx10.9.pkg
else
export pyfile=python-$BUILD_PYTHON_VERSION-macos11.0.pkg
export pyfile=python-$BUILD_PYTHON_VERSION-macos11.pkg
fi
wget https://www.python.org/ftp/python/$BUILD_PYTHON_VERSION/$pyfile

View File

@@ -4,11 +4,7 @@ echo "Xcode versionn:"
xcodebuild -version
export gampath=dist/gam
rm -rf $gampath
if [ "$PLATFORM" == "x86_64" ]; then
export specfile="gam.spec"
else
export specfile="gam-universal2.spec"
fi
export specfile="gam.spec"
$python -OO -m PyInstaller --clean --noupx --strip -F --distpath "${gampath}" "${specfile}"
export gam="${gampath}/gam"
$gam version extended

View File

@@ -12,13 +12,13 @@ defaults:
working-directory: src
env:
BUILD_PYTHON_VERSION: "3.9.2"
MIN_PYTHON_VERSION: "3.9.2"
BUILD_OPENSSL_VERSION: "1.1.1j"
MIN_OPENSSL_VERSION: "1.1.1i"
BUILD_PYTHON_VERSION: "3.9.5"
MIN_PYTHON_VERSION: "3.9.5"
BUILD_OPENSSL_VERSION: "1.1.1k"
MIN_OPENSSL_VERSION: "1.1.1k"
PATCHELF_VERSION: "0.12"
# PYINSTALLER_VERSION can be full commit hash or version like v4.20
PYINSTALLER_VERSION: "227eac14955c02db21d4702429896d4b74beed5e"
PYINSTALLER_VERSION: "e20e74c03768d432d48665b8ef1e02511b16e4be"
jobs:
build:
@@ -56,16 +56,16 @@ jobs:
goal: "build"
gamos: "macos"
platform: "x86_64"
# - os: macos-11.0
# jid: 12
# goal: "build"
# gamos: "macos"
# platform: "universal2"
- os: macos-11.0
jid: 12
goal: "build"
gamos: "macos"
platform: "universal2"
- os: windows-2019
jid: 5
goal: "build"
gamos: "windows"
python: 3.9.2
python: 3.9.5
pyarch: "x64"
platform: "x86_64"
- os: windows-2019
@@ -73,7 +73,7 @@ jobs:
goal: "build"
gamos: "windows"
platform: "x86"
python: 3.9.2
python: 3.9.5
pyarch: "x86"
- os: ubuntu-20.04
goal: "test"
@@ -108,7 +108,7 @@ jobs:
path: |
~/python
~/ssl
key: ${{ matrix.os }}-${{ matrix.jid }}-20210219
key: ${{ matrix.os }}-${{ matrix.jid }}-20210504
- name: Set env variables
env:
@@ -225,6 +225,7 @@ jobs:
$gam info domain
$gam oauth refresh
$gam info user
#$gam info user $gam_user grouptree
export tstamp=$(date +%s%3N)
export newbase=gha-test-$JID-$tstamp
export newuser=$newbase@pdl.jaylee.us
@@ -251,6 +252,7 @@ jobs:
$gam csv sample.csv gam user $gam_user sendemail recipient ~~email~~@pdl.jaylee.us subject "test message $newbase" message "GHA test message"
$gam csv sample.csv gam update group $newgroup add member ~email
$gam info group $newgroup
$gam info cigroup $newgroup membertree
$gam user $gam_user check serviceaccount
# confirm mailbox is provisoned before continuing
$gam user $newuser waitformailbox
@@ -322,7 +324,7 @@ jobs:
$gam report admin start -3d todrive
$gam print devices nopersonaldevices nodeviceusers filter "serial:$JID$JID$JID$JID-" | $gam csv - gam delete device id ~name
$gam print userinvitations
$gam print userinvitations | $gam csv - gam create userinvitation ~name
$gam print userinvitations | $gam csv - gam send userinvitation ~name
export CUSTOMER_ID="C01wfv983"
export GA_DOMAIN="pdl.jaylee.us"
touch $gampath/enabledasa.txt

View File

@@ -114,7 +114,8 @@ If an item contains spaces, it should be surrounded by ".
drive8tb|8tb|googledrivestorage8tb|Google-Drive-storage-8TB|
drive16tb|16tb|googledrivestorage16tb|Google-Drive-storage-16TB|
vault|googlevault|Google-Vault|
vfe|googlevaultformeremployee|Google-Vault-Former-Employee
vfe|googlevaultformeremployee|Google-Vault-Former-Employee|
workspacefrontline|workspacefrontlineworker|1010020030
## Basic items built from primitives
@@ -684,7 +685,7 @@ Specify a collection of Users by directly specifying them or by specifiying item
(name <String>)
<DriveFileAddAttribute> ::=
(localfile <FileName>)|
(localfile <FileName>|-)|
(convert)|(ocr)|(ocrlanguage <Language>)|
(restricted|restrict)|(starred|star)|(trashed|trash)|(viewed|view)|
(contentrestrictions readonly false)|
@@ -694,7 +695,7 @@ Specify a collection of Users by directly specifying them or by specifiying item
(parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|writerscantshare|writerscanshare
(shortcut <DriveFileID>)
<DriveFileUpdateAttribute> ::=
(localfile <FileName>)|
(localfile <FileName>|-)|
(convert)|(ocr)|(ocrlanguage <Language>)|
(restricted|restrict <Boolean>)|(starred|star <Boolean>)|(trashed|trash <Boolean>)|(viewed|view <Boolean>)|
(contentrestrictions readonly false)|
@@ -989,6 +990,8 @@ gam info customer
<DataTransferService> ::=
calendar|
currents|
datastudio|"google data studio"|
googledrive|gdrive|drive|"drive and docs"
<DataTransferServiceList> ::= "<DataTransferService>(,<DataTransferService>)*"
@@ -1005,8 +1008,8 @@ gam delete org|ou <OrgUnitPath>
gam info org|ou <OrgUnitPath> [nousers|notsuspended|suspended] [children|child]
gam print orgs|ous [todrive] [toplevelonly] [from_parent <OrgUnitPath>] [allfields|(fields <OrgUnitFieldNameList>)]
gam create alias|nickname <EmailAddress> user|group|target <UniqueID>|<EmailAddress>
gam update alias|nickname <EmailAddress> user|group|target <UniqueID>|<EmailAddress>
gam create alias|nickname <EmailAddress> user|group|target <UniqueID>|<EmailAddress> [verifynotinvitable]
gam update alias|nickname <EmailAddress> user|group|target <UniqueID>|<EmailAddress> [verifynotinvitable]
gam delete alias|nickname [user|group|target] <UniqueID>|<EmailAddress>
gam info alias|nickname <EmailAddress>
gam print aliases|nicknames [todrive] [shownoneditable] [nogroups] [nousers] [(query <QueryUser>)|(queries <QueryUserList)]
@@ -1231,6 +1234,64 @@ The listlimit <Number> argument limits the number of recent users, time ranges a
The start <Date> and end <Date> arguments filter the time ranges.
Delimiter defaults to comma.
gam print chromeapps [todrive]
[ou|org|orgunit <OrgUnitItem>]
[filter <String>]
[orderby appname|apptype|installtype|numberofpermissions|totalinstallcount]
gam print chromeappdevices [todrive]
appid <AppID> apptype extension|app|theme|hostedapp|androidapp
[ou|org|orgunit <OrgUnitItem>]
[start <Date>] [end <Date>]
[orderby deviceid|machine]
gam print chromeversions [todrive]
[ou|org|orgunit <OrgUnitItem>]
[start <Date>] [end <Date>] [recentfirst]
<ChromePlatformType>> ::=
all'|
android'|
ios'|
lacros'|
linux'|
mac'|
macarm64'|
sebview'|
win'|
win64'
<ChromeChannelType> ::=
beta'|
canary'|
canaryasan'|
dev'|
stable'
<ChromeVersionsOrderByFieldName> ::=
channel|
name|
platform|
version|
<ChromeReleasesOrderByFieldName> ::=
channel|
endtime|
fraction|
name|
platform|
starttime|
version
gam print chromehistory platforms [todrive]
gam print chromehistory channels [todrive]
[platform <ChromePlatformType>]
gam print chromehistory versions [todrive]
[platform <ChromePlatformType>] [channel <ChromeChannelType>]
[filter <String>]
(orderby <ChromeVersionsOrderByFieldName> [ascending|descending])*
gam print chromehistory releases [todrive]
[platform <ChromePlatformType>] [channel <ChromeChannelType>] [version <String>]
[filter <String>]
(orderby <ChromeReleasessOrderByFieldName> [ascending|descending])*
gam delete chromepolicy <SchemaName>+ ou|org|orgunit <OrgUnitItem> [(printerid <PrinterID>)|(appid <AppID>)]
gam update chromepolicy (<SchemaName> (<Field> <Value>)+)+ ou|org|orgunit <OrgUnitItem> [(printerid <PrinterID>)|(appid <AppID>)]
gam show chromepolicy ou|org|orgunit <OrgUnitItem> [(printerid <PrinterID>)|(appid <AppID>)]
@@ -1316,8 +1377,8 @@ gam print cigroup-members|cigroups-members [todrive]
[(enterprisemember <UserItem>)|(cigroup <GroupItem>)]
[roles <GroupRoleList>]
gam create group <EmailAddress> <GroupAttribute>*
gam update group <GroupItem> [email <EmailAddress>] <GroupAttribute>*
gam create group <EmailAddress> <GroupAttribute>* [verifynotinvitable]
gam update group <GroupItem> [email <EmailAddress>] <GroupAttribute>* [verifynotinvitable]
gam update group <GroupItem> add [owner|manager|member] [notsuspended|suspended] [allmail|daily|digest|none|nomail] <UserTypeEntity>
gam update group <GroupItem> delete|remove [owner|manager|member] <UserTypeEntity>
gam update group <GroupItem> sync [owner|manager|member] [notsuspended|suspended] [allmail|daily|digest|none|nomail] <UserTypeEntity>
@@ -1373,8 +1434,8 @@ gam info schema <SchemaName>
gam show schema|schemas
gam print schema|schemas
gam create user <EmailAddress> <UserAttribute>*
gam update user <UserItem> <UserAttribute>* [clearschema <SchemaName>] [clearschema <SchemaName>.<FieldName>]
gam create user <EmailAddress> <UserAttribute>* [verifynotinvitable]
gam update user <UserItem> <UserAttribute>* [clearschema <SchemaName>] [clearschema <SchemaName>.<FieldName>] [verifynotinvitable]
gam delete user <UserItem>
gam undelete user <UserItem> [org|ou <OrgUnitPath>]
gam info user [<UserItem>] [noaliases] [nogroups] [nolicenses|nolicences] [noschemas] [schemas|custom <SchemaNameList>] [userview] [skus|sku <SKUIDList>]
@@ -1562,9 +1623,9 @@ gam <UserTypeEntity> sendemail [recipient|to <EmailAddress>] [from <EmailAddress
[subject <String>] [(message <String>)|(file <FileName> [charset <Charset>])]
(header <String> <String>)*
gam <UserTypeEntity> create|add delegate|delegates <EmailAddress>
gam <UserTypeEntity> delegate|delegates to <EmailAddress>
gam <UserTypeEntity> delete|del delegate|delegates <EmailAddress>
gam <UserTypeEntity> create|add delegate|delegates [convertalias] <EmailAddress>
gam <UserTypeEntity> delegate|delegates to [convertalias] <EmailAddress>
gam <UserTypeEntity> delete|del delegate|delegates [convertalias] <EmailAddress>
gam <UserTypeEntity> show delegates|delegate [csv]
gam <UserTypeEntity> print delegates [todrive]

View File

@@ -1,46 +0,0 @@
# -*- mode: python -*-
import sys
import importlib
from PyInstaller.utils.hooks import copy_metadata
sys.modules['FixTk'] = None
# dynamically determine where httplib2/cacerts.txt lives
proot = os.path.dirname(importlib.import_module('httplib2').__file__)
extra_files = [(os.path.join(proot, 'cacerts.txt'), 'httplib2')]
extra_files += copy_metadata('google-api-python-client')
extra_files += [('cbcm-v1.1beta1.json', '.')]
a = Analysis(['gam/__main__.py'],
hiddenimports=[],
hookspath=None,
excludes=['FixTk', 'tcl', 'tk', '_tkinter', 'tkinter', 'Tkinter'],
datas=extra_files,
runtime_hooks=None)
for d in a.datas:
if 'pyconfig' in d[0]:
a.datas.remove(d)
break
pyz = PYZ(a.pure)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
name='gam',
debug=False,
strip=None,
upx=False,
console=True )
app = BUNDLE(exe,
name='gam.app',
icon=None,
bundle_identifier=None,
info_plist={'LSArchitecturePriority': 'arm64,x86_64'})

View File

@@ -14,9 +14,14 @@ extra_files = [(os.path.join(proot, 'cacerts.txt'), 'httplib2')]
extra_files += copy_metadata('google-api-python-client')
extra_files += [('cbcm-v1.1beta1.json', '.')]
extra_files += [('contactdelegation-v1.json', '.')]
extra_files += [('versionhistory-v1.json', '.')]
hidden_imports = [
'gam.auth.yubikey',
]
a = Analysis(['gam/__main__.py'],
hiddenimports=[],
hiddenimports=hidden_imports,
hookspath=None,
excludes=['FixTk', 'tcl', 'tk', '_tkinter', 'tkinter', 'Tkinter'],
datas=extra_files,

View File

@@ -6,6 +6,7 @@ import configparser
import csv
import datetime
from email import message_from_string
import io
import json
import mimetypes
import os
@@ -46,13 +47,14 @@ from cryptography.x509.oid import NameOID
import gam.auth.oauth
from gam import auth
from gam.auth import yubikey
from gam import controlflow
from gam import display
from gam import fileutils
from gam.gapi import calendar as gapi_calendar
from gam.gapi import cloudidentity as gapi_cloudidentity
from gam.gapi import cbcm as gapi_cbcm
from gam.gapi import chromehistory as gapi_chromehistory
from gam.gapi import chromemanagement as gapi_chromemanagement
from gam.gapi import chromepolicy as gapi_chromepolicy
from gam.gapi.cloudidentity import devices as gapi_cloudidentity_devices
from gam.gapi.cloudidentity import groups as gapi_cloudidentity_groups
@@ -82,6 +84,7 @@ from gam import transport
from gam import utils
from gam.var import *
yubikey = utils.LazyLoader('yubikey', globals(), 'gam.auth.yubikey')
from passlib.hash import sha512_crypt
if platform.system() == 'Linux':
@@ -108,7 +111,7 @@ def showUsage():
print('''
Usage: gam [OPTIONS]...
GAM. Retrieve or set G Suite domain,
GAM. Retrieve or set Google Workspace domain,
user, group and alias settings. Exhaustive list of commands
can be found at: https://github.com/jay0lee/GAM/wiki
@@ -134,11 +137,11 @@ def currentCountNL(i, count):
def printGettingAllItems(items, query):
if query:
sys.stderr.write(
f'Getting all {items} in G Suite account that match query ({query}) (may take some time on a large account)...\n'
f'Getting all {items} in Google Workspace account that match query ({query}) (may take some time on a large account)...\n'
)
else:
sys.stderr.write(
f'Getting all {items} in G Suite account (may take some time on a large account)...\n'
f'Getting all {items} in Google Workspace account (may take some time on a large account)...\n'
)
@@ -616,7 +619,7 @@ TIME_OFFSET_UNITS = [('day', 86400), ('hour', 3600), ('minute', 60),
('second', 1)]
def getLocalGoogleTimeOffset(testLocation='www.googleapis.com'):
def getLocalGoogleTimeOffset(testLocation='admin.googleapis.com'):
localUTC = datetime.datetime.now(datetime.timezone.utc)
try:
# we disable SSL verify so we can still get time even if clock
@@ -730,7 +733,7 @@ def getOSPlatform():
def doGAMVersion(checkForArgs=True):
force_check = extended = simple = timeOffset = False
testLocation = 'www.googleapis.com'
testLocation = 'admin.googleapis.com'
if checkForArgs:
i = 2
while i < len(sys.argv):
@@ -892,14 +895,14 @@ def getValidOauth2TxtCredentials(force_refresh=False, api=None):
return credentials
def getService(api, http):
def getService(api, httpObj):
api, version, api_version = getAPIVersion(api)
if api in GM_Globals[GM_CURRENT_API_SERVICES] and version in GM_Globals[
GM_CURRENT_API_SERVICES][api]:
service = googleapiclient.discovery.build_from_document(
GM_Globals[GM_CURRENT_API_SERVICES][api][version], http=http)
GM_Globals[GM_CURRENT_API_SERVICES][api][version], http=httpObj)
if GM_Globals[GM_CACHE_DISCOVERY_ONLY]:
http.cache = None
httpObj.cache = None
return service
if api in V1_DISCOVERY_APIS:
discoveryServiceUrl = googleapiclient.discovery.DISCOVERY_URI
@@ -911,7 +914,7 @@ def getService(api, http):
service = googleapiclient.discovery.build(
api,
version,
http=http,
http=httpObj,
cache_discovery=False,
static_discovery=False,
discoveryServiceUrl=discoveryServiceUrl)
@@ -919,23 +922,25 @@ def getService(api, http):
GM_Globals[GM_CURRENT_API_SERVICES][api][
version] = service._rootDesc.copy()
if GM_Globals[GM_CACHE_DISCOVERY_ONLY]:
http.cache = None
httpObj.cache = None
return service
except (httplib2.ServerNotFoundError, RuntimeError) as e:
if n != retries:
http.connections = {}
httpObj.connections = {}
controlflow.wait_on_failure(n, retries, str(e))
continue
controlflow.system_error_exit(4, str(e))
except (googleapiclient.errors.InvalidJsonError, KeyError,
ValueError) as e:
http.cache = None
httpObj.cache = None
if n != retries:
controlflow.wait_on_failure(n, retries, str(e))
continue
controlflow.system_error_exit(17, str(e))
except (http_client.ResponseNotReady, OSError,
googleapiclient.errors.HttpError) as e:
if 'The request is missing a valid API key' in str(e):
break
if n != retries:
controlflow.wait_on_failure(n, retries, str(e))
continue
@@ -945,12 +950,12 @@ def getService(api, http):
disc_file, discovery = readDiscoveryFile(api_version)
try:
service = googleapiclient.discovery.build_from_document(discovery,
http=http)
http=httpObj)
GM_Globals[GM_CURRENT_API_SERVICES].setdefault(api, {})
GM_Globals[GM_CURRENT_API_SERVICES][api][
version] = service._rootDesc.copy()
if GM_Globals[GM_CACHE_DISCOVERY_ONLY]:
http.cache = None
httpObj.cache = None
return service
except (KeyError, ValueError):
controlflow.invalid_json_exit(disc_file)
@@ -960,9 +965,9 @@ def buildGAPIObject(api):
GM_Globals[GM_CURRENT_API_USER] = None
credentials = getValidOauth2TxtCredentials(api=getAPIVersion(api)[0])
credentials.user_agent = GAM_INFO
http = transport.AuthorizedHttp(
httpObj = transport.AuthorizedHttp(
credentials, transport.create_http(cache=GM_Globals[GM_CACHE_DIR]))
service = getService(api, http)
service = getService(api, httpObj)
if GC_Values[GC_DOMAIN]:
if not GC_Values[GC_CUSTOMER_ID]:
resp, result = service._http.request(
@@ -994,6 +999,12 @@ def buildGAPIObject(api):
return service
def buildGAPIObjectNoAuthentication(api):
GM_Globals[GM_CURRENT_API_USER] = None
httpObj = transport.create_http(cache=GM_Globals[GM_CACHE_DIR])
service = getService(api, httpObj)
return service
# Convert UID to email address
def convertUIDtoEmailAddress(emailAddressOrUID, cd=None, email_types=['user']):
if isinstance(email_types, str):
@@ -1077,23 +1088,23 @@ def convertEmailAddressToUID(emailAddressOrUID, cd=None, email_type='user'):
def buildGAPIServiceObject(api, act_as, showAuthError=True):
http = transport.create_http(cache=GM_Globals[GM_CACHE_DIR])
service = getService(api, http)
httpObj = transport.create_http(cache=GM_Globals[GM_CACHE_DIR])
service = getService(api, httpObj)
GM_Globals[GM_CURRENT_API_USER] = act_as
GM_Globals[GM_CURRENT_API_SCOPES] = API_SCOPE_MAPPING.get(
api, service._rootDesc['auth']['oauth2']['scopes'])
credentials = getSvcAcctCredentials(GM_Globals[GM_CURRENT_API_SCOPES],
act_as)
request = transport.create_request(http)
request = transport.create_request(httpObj)
retries = 3
for n in range(1, retries + 1):
try:
credentials.refresh(request)
service._http = transport.AuthorizedHttp(credentials, http=http)
service._http = transport.AuthorizedHttp(credentials, http=httpObj)
break
except (httplib2.ServerNotFoundError, RuntimeError) as e:
if n != retries:
http.connections = {}
httpObj.connections = {}
controlflow.wait_on_failure(n, retries, str(e))
continue
controlflow.system_error_exit(4, e)
@@ -1158,7 +1169,7 @@ def doCheckServiceAccount(users):
time_status = test_fail
printPassFail(
MESSAGE_YOUR_SYSTEM_TIME_DIFFERS_FROM_GOOGLE_BY %
('www.googleapis.com', nicetime), time_status)
('admin.googleapis.com', nicetime), time_status)
oa2 = getService('oauth2', transport.create_http())
print('Service Account Private Key Authentication:')
# We are explicitly not doing DwD here, just confirming service account can auth
@@ -1264,7 +1275,7 @@ def doCheckServiceAccount(users):
{short_url}
You will be directed to the G Suite admin console Security/API Controls/Domain-wide Delegation page
You will be directed to the Google Workspace 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.
@@ -1404,7 +1415,13 @@ def addDelegates(users, i):
if sys.argv[i].lower() != 'to':
controlflow.missing_argument_exit('to', 'gam <users> delegate')
i += 1
convertAlias = False
if sys.argv[i].lower().replace('_', '') == 'convertalias':
convertAlias = True
i += 1
delegate = normalizeEmailAddressOrUID(sys.argv[i], noUid=True)
if convertAlias:
delegate = gapi_directory_users.get_primary(delegate)
i = 0
count = len(users)
for delegator in users:
@@ -1483,7 +1500,14 @@ def printShowDelegates(users, csvFormat):
def deleteDelegate(users):
delegate = normalizeEmailAddressOrUID(sys.argv[5], noUid=True)
convertAlias = False
i = 5
if sys.argv[i].lower().replace('_', '') == 'convertalias':
convertAlias = True
i += 1
delegate = normalizeEmailAddressOrUID(sys.argv[i], noUid=True)
if convertAlias:
delegate = gapi_directory_users.get_primary(delegate)
i = 0
count = len(users)
for user in users:
@@ -3559,14 +3583,19 @@ 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(
parameters[DFA_LOCALFILEPATH])
body.setdefault('title', parameters[DFA_LOCALFILENAME])
body['mimeType'] = mimetypes.guess_type(
parameters[DFA_LOCALFILEPATH])[0]
if body['mimeType'] is None:
body['mimeType'] = 'application/octet-stream'
parameters[DFA_LOCALMIMETYPE] = body['mimeType']
if parameters[DFA_LOCALFILEPATH] != '-':
parameters[DFA_LOCALFILENAME] = os.path.basename(
parameters[DFA_LOCALFILEPATH])
body.setdefault('title', parameters[DFA_LOCALFILENAME])
body['mimeType'] = mimetypes.guess_type(
parameters[DFA_LOCALFILEPATH])[0]
if body['mimeType'] is None:
body['mimeType'] = 'application/octet-stream'
parameters[DFA_LOCALMIMETYPE] = body['mimeType']
else:
parameters[DFA_LOCALFILENAME] = '-'
if body.get('mimeType') is None:
body['mimeType'] = 'application/octet-stream'
i += 2
elif myarg == 'convert':
parameters[DFA_CONVERT] = True
@@ -3662,6 +3691,22 @@ def getDriveFileAttribute(i, body, parameters, myarg, update=False):
return i
def get_media_body(parameters, body):
if parameters[DFA_LOCALFILEPATH] != '-':
media_body = googleapiclient.http.MediaFileUpload(parameters[DFA_LOCALFILEPATH], mimetype=parameters[DFA_LOCALMIMETYPE], resumable=True)
else:
if body['mimeType'] == MIMETYPE_GA_SPREADSHEET:
mimetype = 'text/csv'
elif body['mimeType'] == MIMETYPE_GA_DOCUMENT:
mimetype = 'text/plain'
else:
mimetype = 'application/octet-stream'
media_body = googleapiclient.http.MediaIoBaseUpload(io.BytesIO(sys.stdin.buffer.read()), mimetype, resumable=True)
if media_body.size() == 0:
media_body = None
return media_body
def has_multiple_parents(body):
return len(body.get('parents', [])) > 1
@@ -3704,6 +3749,8 @@ def doUpdateDriveFile(users):
2,
'you cannot specify multiple file identifiers. Choose one of id, drivefilename, query.'
)
if operation == 'update' and parameters[DFA_LOCALFILEPATH]:
media_body = get_media_body(parameters, body)
for user in users:
user, drive = buildDriveGAPIObject(user)
if not drive:
@@ -3724,11 +3771,6 @@ def doUpdateDriveFile(users):
print(f'No files to {operation} for {user}')
continue
if operation == 'update':
if parameters[DFA_LOCALFILEPATH]:
media_body = googleapiclient.http.MediaFileUpload(
parameters[DFA_LOCALFILEPATH],
mimetype=parameters[DFA_LOCALMIMETYPE],
resumable=True)
for fileId in fileIdSelection['fileIds']:
if media_body:
result = gapi.call(drive.files(),
@@ -3794,6 +3836,8 @@ def createDriveFile(users):
i += 1
else:
i = getDriveFileAttribute(i, body, parameters, myarg, False)
if parameters[DFA_LOCALFILEPATH]:
media_body = get_media_body(parameters, body)
for user in users:
user, drive = buildDriveGAPIObject(user)
if not drive:
@@ -3807,11 +3851,6 @@ def createDriveFile(users):
if has_multiple_parents(body):
sys.stderr.write(f"Multiple parents ({len(body['parents'])}) specified for {user}, only one is allowed.\n")
continue
if parameters[DFA_LOCALFILEPATH]:
media_body = googleapiclient.http.MediaFileUpload(
parameters[DFA_LOCALFILEPATH],
mimetype=parameters[DFA_LOCALMIMETYPE],
resumable=True)
result = gapi.call(drive.files(),
'insert',
convert=parameters[DFA_CONVERT],
@@ -5314,20 +5353,27 @@ def gmail_del_result(request_id, response, exception):
print(exception)
def showLabels(users):
def printShowLabels(users, show=True):
i = 5
onlyUser = showCounts = False
onlyUser = False
showCounts = False
todrive = False
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'onlyuser':
onlyUser = True
i += 1
elif myarg == 'todrive':
todrive = True
i += 1
elif myarg == 'showcounts':
showCounts = True
i += 1
else:
controlflow.invalid_argument_exit(sys.argv[i],
'gam <users> show labels')
'gam <users> show labels')
if not show:
titles = ['email']
for user in users:
user, gmail = buildGmailGAPIObject(user)
if not gmail:
@@ -5335,28 +5381,45 @@ def showLabels(users):
labels = gapi.call(gmail.users().labels(),
'list',
userId=user,
soft_errors=True)
if labels:
for label in labels['labels']:
if onlyUser and (label['type'] == 'system'):
continue
soft_errors=True).get('labels', [])
i = 0
for label in labels:
i += 1
if onlyUser and (label['type'] == 'system'):
continue
if showCounts:
if i >= 50 and not i % 50:
# show label get count for greater than 100 labels
# every 100 labels
sys.stderr.write('\r')
sys.stderr.flush()
sys.stderr.write(f'Getting counts for label {i} of {len(labels)}')
counts = gapi.call(
gmail.users().labels(),
'get',
userId=user,
id=label['id'],
fields=
'messagesTotal,messagesUnread,threadsTotal,threadsUnread'
)
label.update(counts)
if show:
print(label['name'])
for a_key in label:
if a_key == 'name':
continue
print(f' {a_key}: {label[a_key]}')
if showCounts:
counts = gapi.call(
gmail.users().labels(),
'get',
userId=user,
id=label['id'],
fields=
'messagesTotal,messagesUnread,threadsTotal,threadsUnread'
)
for a_key in counts:
print(f' {a_key}: {counts[a_key]}')
print('')
else:
for key in label:
if key not in titles:
titles.append(key)
label['email'] = user
if not show:
display.write_csv_file(labels,
titles,
list_type='Gmail Labels',
todrive=False)
def showGmailProfile(users):
@@ -6504,6 +6567,7 @@ def getUserAttributes(i, cd, updateCmd):
need_password = True
need_to_hash_password = True
need_to_b64_decrypt_password = False
verifyNotInvitable = False
while i < len(sys.argv):
myarg = sys.argv[i].lower()
if myarg in ['firstname', 'givenname']:
@@ -7019,6 +7083,9 @@ def getUserAttributes(i, cd, updateCmd):
else:
body[up][schemaName][fieldName] = sys.argv[i]
i += 1
elif myarg == 'verifynotinvitable':
verifyNotInvitable = True
i += 1
else:
controlflow.invalid_argument_exit(
sys.argv[i], f"gam {['create', 'update'][updateCmd]} user")
@@ -7033,7 +7100,7 @@ def getUserAttributes(i, cd, updateCmd):
if body['password'].lower()[:5] in ['{md5}', '{sha}']:
body['password'] = body['password'][5:]
body['password'] = base64.b64decode(body['password']).hex()
return body
return (body, verifyNotInvitable)
def getCRMService(login_hint):
@@ -7315,7 +7382,7 @@ def _getValidateLoginHint(login_hint=None):
while True:
if not login_hint:
login_hint = input(
'\nWhat is your G Suite admin email address? ').strip()
'\nWhat is your Google Workspace admin email address? ').strip()
if login_hint.find('@') == -1 and GC_Values[GC_DOMAIN]:
login_hint = f'{login_hint}@{GC_Values[GC_DOMAIN].lower()}'
if VALIDEMAIL_PATTERN.match(login_hint):
@@ -8133,6 +8200,7 @@ def printShowTeamDrives(users, csvFormat):
controlflow.invalid_argument_exit(
myarg, f"gam {['show', 'print'][csvFormat]} teamdrives")
tds = []
titles = []
for user in users:
sys.stderr.write(f'Getting Team Drives for {user}\n')
user, drive = buildDrive3GAPIObject(user)
@@ -8148,20 +8216,21 @@ def printShowTeamDrives(users, csvFormat):
if not results:
continue
for td in results:
if 'id' not in td:
continue
if 'name' not in td:
td['name'] = '<Unknown Team Drive>'
this_td = {'id': td['id'], 'name': td['name']}
if this_td in tds:
continue
tds.append({'id': td['id'], 'name': td['name']})
td = utils.flatten_json(td)
for key in td:
if key not in titles:
titles.append(key)
tds.append(td)
if csvFormat:
titles = ['name', 'id']
display.write_csv_file(tds, titles, 'Team Drives', todrive)
else:
for td in tds:
print(f'Name: {td["name"]} ID: {td["id"]}')
name = td.pop('name')
my_id = td.pop('id')
print(f'Name: {name} ID: {my_id}')
display.print_json(td)
print()
def doDeleteTeamDrive(users):
@@ -8196,7 +8265,10 @@ def extract_nested_zip(zippedFile, toFolder, spacing=' '):
def doCreateUser():
cd = buildGAPIObject('directory')
body = getUserAttributes(3, cd, False)
body, verifyNotInvitable = getUserAttributes(3, cd, False)
if (verifyNotInvitable and
gapi_cloudidentity_userinvitations.is_invitable_user(body['primaryEmail'])):
controlflow.system_error_exit(51, f'User not created, {body["primaryEmail"]} is an unmanaged account')
print(f'Creating account for {body["primaryEmail"]}')
gapi.call(cd.users(), 'insert', body=body, fields='primaryEmail')
@@ -8212,6 +8284,15 @@ def doCreateAlias():
controlflow.expected_argument_exit(
'target type', ', '.join(['user', 'group', 'target']), target_type)
targetKey = normalizeEmailAddressOrUID(sys.argv[5])
if len(sys.argv) > 6:
myarg = sys.argv[6].lower().replace('_', '')
if myarg != 'verifynotinvitable':
controlflow.system_error_exit(
3,
f'{myarg} is not a valid argument for "gam create alias"'
)
if gapi_cloudidentity_userinvitations.is_invitable_user(body['alias']):
controlflow.system_error_exit(51, f'Alias not created, {body["alias"]} is an unmanaged account')
print(f'Creating alias {body["alias"]} for {target_type} {targetKey}')
if target_type == 'user':
gapi.call(cd.users().aliases(), 'insert', userKey=targetKey, body=body)
@@ -8241,7 +8322,7 @@ def doUpdateUser(users, i):
cd = buildGAPIObject('directory')
if users is None:
users = [normalizeEmailAddressOrUID(sys.argv[3])]
body = getUserAttributes(i, cd, True)
body, verifyNotInvitable = getUserAttributes(i, cd, True)
vfe = 'primaryEmail' in body and body['primaryEmail'][:4].lower() == 'vfe@'
for user in users:
userKey = user
@@ -8261,6 +8342,9 @@ def doUpdateUser(users, i):
'primary': False,
'address': user_primary
}]
if (verifyNotInvitable and'primaryEmail' in body and
gapi_cloudidentity_userinvitations.is_invitable_user(body['primaryEmail'])):
controlflow.system_error_exit(51, f'User {user} not updated, new primaryEmail {body["primaryEmail"]} is an unmanaged account')
sys.stdout.write(f'updating user {user}...\n')
if body:
gapi.call(cd.users(), 'update', userKey=userKey, body=body)
@@ -8295,6 +8379,15 @@ def doUpdateAlias():
controlflow.expected_argument_exit(
'target type', ', '.join(['user', 'group', 'target']), target_type)
target_email = normalizeEmailAddressOrUID(sys.argv[5])
if len(sys.argv) > 6:
myarg = sys.argv[6].lower().replace('_', '')
if myarg != 'verifynotinvitable':
controlflow.system_error_exit(
3,
f'{myarg} is not a valid argument for "gam update alias"'
)
if gapi_cloudidentity_userinvitations.is_invitable_user(alias):
controlflow.system_error_exit(51, f'Alias not updated, {alias} is an unmanaged account')
try:
gapi.call(cd.users().aliases(),
'delete',
@@ -8335,6 +8428,7 @@ def doWhatIs():
user_or_alias = gapi.call(cd.users(),
'get',
throw_reasons=[
gapi_errors.ErrorReason.USER_NOT_FOUND,
gapi_errors.ErrorReason.NOT_FOUND,
gapi_errors.ErrorReason.BAD_REQUEST,
gapi_errors.ErrorReason.INVALID
@@ -8349,28 +8443,37 @@ def doWhatIs():
sys.stderr.write(f'{email} is a user alias\n\n')
doGetAliasInfo(alias_email=email)
return
except (gapi_errors.GapiNotFoundError, gapi_errors.GapiBadRequestError,
gapi_errors.GapiInvalidError):
except (gapi_errors.GapiUserNotFoundError, gapi_errors.GapiNotFoundError,
gapi_errors.GapiBadRequestError, gapi_errors.GapiInvalidError):
sys.stderr.write(f'{email} is not a user...\n')
sys.stderr.write(f'{email} is is not a user alias...\n')
sys.stderr.write(f'{email} is not a user alias...\n')
try:
group = gapi.call(cd.groups(),
'get',
throw_reasons=[
gapi_errors.ErrorReason.GROUP_NOT_FOUND,
gapi_errors.ErrorReason.NOT_FOUND,
gapi_errors.ErrorReason.BAD_REQUEST
gapi_errors.ErrorReason.BAD_REQUEST,
gapi_errors.ErrorReason.FORBIDDEN
],
groupKey=email,
fields='id,email')
except (gapi_errors.GapiNotFoundError, gapi_errors.GapiBadRequestError):
controlflow.system_error_exit(
1, f'{email} is not a group either!\n\nDoesn\'t seem to exist!\n\n')
if (group['email'].lower() == email) or (group['id'] == email):
sys.stderr.write(f'{email} is a group\n\n')
gapi_directory_groups.info(group_name=email)
else:
if (group['email'].lower() == email) or (group['id'] == email):
sys.stderr.write(f'{email} is a group\n\n')
gapi_directory_groups.info(group_name=email)
return
sys.stderr.write(f'{email} is a group alias\n\n')
doGetAliasInfo(alias_email=email)
return
except (gapi_errors.GapiGroupNotFoundError, gapi_errors.GapiNotFoundError,
gapi_errors.GapiBadRequestError, gapi_errors.GapiForbiddenError):
sys.stderr.write(f'{email} is not a group...\n')
sys.stderr.write(f'{email} is not a group alias...\n')
if gapi_cloudidentity_userinvitations.is_invitable_user(email):
sys.stderr.write(f'{email} is an unmanaged account\n\n')
else:
controlflow.system_error_exit(
1, f'{email} doesn\'t seem to exist!\n\n')
def convertSKU2ProductId(res, sku, customerId):
@@ -8628,7 +8731,11 @@ def doGetUserInfo(user_email=None):
i = 4
else:
user_email = _get_admin_email()
getSchemas = getAliases = getGroups = getLicenses = True
getSchemas = True
getAliases = True
getGroups = True
getCIGroups = False
getLicenses = True
projection = 'full'
customFieldMask = viewType = None
skus = sorted(SKUS)
@@ -8640,6 +8747,10 @@ def doGetUserInfo(user_email=None):
elif myarg == 'nogroups':
getGroups = False
i += 1
elif myarg == 'grouptree':
getCIGroups = True
getGroups = False
i += 1
elif myarg in ['nolicenses', 'nolicences']:
getLicenses = False
i += 1
@@ -8905,6 +9016,34 @@ def doGetUserInfo(user_email=None):
print(f' {group["name"]} <{group["email"]}>')
except gapi.errors.GapiForbiddenError:
print('No access to show user groups.')
elif getCIGroups:
memberships = gapi_cloudidentity_groups.get_membership_graph(user_email)
print('\nGroup Mmebership Tree:')
group_name_mapping = {}
group_displayname_mapping = {}
groups = memberships.get('groups', [])
for group in groups:
group_name = group.get('name')
group_key = group.get('groupKey', {})
group_email = group_key.get('id', '')
group_display_name = group.get('displayName', '')
group_name_mapping[group_name] = group_email
group_displayname_mapping[group_email] = group_display_name
edges = []
seen_group_count = {}
groups_with_multi_memberships = []
for adj in memberships.get('adjacencyList', []):
group_name = adj.get('group', '')
group_email = group_name_mapping[group_name]
for edge in adj.get('edges', []):
seen_group_count[group_email] = seen_group_count.get(group_email, 0) + 1
member_email = edge.get('preferredMemberKey', {}).get('id')
edges.append((member_email, group_email))
print_group_map(user_email, group_displayname_mapping, seen_group_count, edges, spaces=3, direct=True)
if max(seen_group_count.values()) > 1:
print()
print(' * user has multiple direct or inherited memberships in group')
print()
if getLicenses:
print('Licenses:')
lic = buildGAPIObject('licensing')
@@ -8920,6 +9059,19 @@ def doGetUserInfo(user_email=None):
for user_license in user_licenses:
print(f' {gapi_licensing._formatSKUIdDisplayName(user_license)}')
def print_group_map(parent, group_name_mappings, seen_group_count, edges, spaces=3, direct=False):
for a_parent, a_child in edges:
if a_parent == parent:
group_display_name = group_name_mappings[a_child]
if direct:
direction = 'direct'
else:
direction = 'inherited'
output = f'{" " * spaces}{group_display_name} <{a_child}> ({direction})'
if seen_group_count[a_child] > 1:
output += ' *'
print(output)
print_group_map(a_child, group_name_mappings, seen_group_count, edges, spaces+2)
def doGetAliasInfo(alias_email=None):
cd = buildGAPIObject('directory')
@@ -10146,7 +10298,7 @@ def OAuthInfo():
for scope in sorted(scopes):
print(f' {scope}')
if 'email' in token_info:
print(f'G Suite Admin: {token_info["email"]}')
print(f'Google Workspace Admin: {token_info["email"]}')
if 'expires_in' in token_info:
expires = (
datetime.datetime.now() +
@@ -10248,6 +10400,11 @@ OAUTH2_SCOPES = [
'subscopes': ['readonly'],
'scopes': 'https://www.googleapis.com/auth/admin.directory.device.chromebrowsers',
},
{
'name': 'Chrome Management API - read only',
'subscope': [],
'scopes': ['https://www.googleapis.com/auth/chrome.management.reports.readonly'],
},
{
'name': 'Chrome Policy API',
'subscope': ['readonly'],
@@ -10273,7 +10430,8 @@ OAUTH2_SCOPES = [
{
'name': 'Cloud Identity - User Invitations',
'subscopes': ['readonly'],
'scopes': 'https://www.googleapis.com/auth/cloud-identity.userinvitations'
'scopes': 'https://www.googleapis.com/auth/cloud-identity.userinvitations',
'offByDefault': True,
},
{
'name': 'Contact Delegation',
@@ -11493,6 +11651,14 @@ def ProcessGAMCommand(args):
gapi_directory_printers.print_models()
elif argument in ['printers']:
gapi_directory_printers.print_()
elif argument in ['chromeapps']:
gapi_chromemanagement.printApps()
elif argument in ['chromeappdevices']:
gapi_chromemanagement.printAppDevices()
elif argument in ['chromeversions']:
gapi_chromemanagement.printVersions()
elif argument in ['chromehistory']:
gapi_chromehistory.printHistory()
else:
controlflow.invalid_argument_exit(argument, 'gam print')
sys.exit(0)
@@ -11614,7 +11780,7 @@ def ProcessGAMCommand(args):
elif command == 'check':
argument = sys.argv[2].lower()
if argument in ['isinvitable', 'userinvitation', 'userinvitations']:
gapi_cloudidentity_userinvitations.is_invitable_user()
gapi_cloudidentity_userinvitations.check()
sys.exit(0)
elif command in ['cancelwipe', 'wipe', 'approve', 'block', 'sync']:
target = sys.argv[2].lower().replace('_', '')
@@ -11664,7 +11830,7 @@ def ProcessGAMCommand(args):
elif command == 'show':
showWhat = sys.argv[4].lower()
if showWhat in ['labels', 'label']:
showLabels(users)
printShowLabels(users)
elif showWhat == 'profile':
showProfile(users)
elif showWhat == 'calendars':
@@ -11753,6 +11919,8 @@ def ProcessGAMCommand(args):
printShowTeamDrives(users, True)
elif printWhat in ['contactdelegate', 'contactdelegates']:
gapi_contactdelegation.print_(users, True)
elif printWhat in ['labels']:
printShowLabels(users, show=False)
else:
controlflow.invalid_argument_exit(printWhat,
'gam <users> print')

View File

@@ -6,8 +6,9 @@ import os
from google.auth.jwt import Credentials as JWTCredentials
import gam
from gam import utils
from gam.auth import oauth
from gam.auth import yubikey
from gam.var import _FN_OAUTH2_TXT
from gam.var import _FN_OAUTH2SERVICE_JSON
from gam.var import GC_OAUTH2_TXT
@@ -15,6 +16,7 @@ from gam.var import GC_OAUTH2SERVICE_JSON
from gam.var import GC_ENABLE_DASA
from gam.var import GC_Values
yubikey = utils.LazyLoader('yubikey', globals(), 'gam.auth.yubikey')
# 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.

View File

@@ -8,6 +8,7 @@ from unittest.mock import patch
from gam import SetGlobalVariables
import gam.gapi as gapi
from gam.gapi import errors
import httplib2
def create_http_error(status, reason, message):
@@ -21,10 +22,10 @@ def create_http_error(status, reason, message):
Returns:
googleapiclient.errors.HttpError
"""
response = {
response = httplib2.Response({
'status': status,
'content-type': 'application/json',
}
})
content = {
'error': {
'code': status,

View File

@@ -0,0 +1,229 @@
"""Chrome Version History API calls"""
import re
import sys
import gam
from gam.var import *
from gam import controlflow
from gam import display
from gam import gapi
from gam import utils
def build():
return gam.buildGAPIObjectNoAuthentication('versionhistory')
CHROME_HISTORY_ENTITY_CHOICES = {
'platforms',
'channels',
'versions',
'releases',
}
CHROME_VERSIONHISTORY_ORDERBY_CHOICE_MAP = {
'versions': {
'channel': 'channel',
'name': 'name',
'platform': 'platform',
'version': 'version'
},
'releases': {
'channel': 'channel',
'endtime': 'endtime',
'fraction': 'fraction',
'name': 'name',
'platform': 'platform',
'starttime': 'starttime',
'version': 'version'
}
}
CHROME_VERSIONHISTORY_TITLES = {
'platforms': ['platform'],
'channels': ['channel', 'platform'],
'versions': ['version', 'channel', 'platform',
'major_version', 'minor_version', 'build', 'patch'],
'releases': ['version', 'channel', 'platform',
'major_version', 'minor_version', 'build', 'patch',
'fraction', 'serving.startTime','serving.endTime']
}
def get_relative_milestone(channel='stable', minus=0):
'''
takes a channel and minus like stable and -1.
returns current given milestone number
'''
cv = build()
parent = f'chrome/platforms/all/channels/{channel}/versions/all'
releases = gapi.get_all_pages(cv.platforms().channels().versions().releases(),
'list',
'releases',
parent=parent,
fields='releases/version,nextPageToken')
milestones = []
# Note that milestones are usually sequential but some numbers
# may be skipped. For example, there was no Chrome 82 stable.
# Thus we need to do more than find the latest version and subtract.
for release in releases:
milestone = release.get('version').split('.')[0]
if milestone not in milestones:
milestones.append(milestone)
milestones.sort(reverse=True)
try:
return milestones[minus]
except IndexError:
return ''
def get_platform_map(cv=None):
'''returns dict mapping of platform choices'''
if cv is None:
cv = build()
result = gapi.get_all_pages(cv.platforms(),
'list',
'platforms',
parent='chrome')
platforms = [p.get('platformType', '').lower() for p in result]
platform_map = {'all': 'all'}
for cplatform in platforms:
key = cplatform.replace('_', '')
platform_map[key] = cplatform
return platform_map
def get_channel_map(cv=None):
'''returns dict mapping of channel choices'''
if cv is None:
cv = build()
result = gapi.get_all_pages(cv.platforms().channels(),
'list',
'channels',
parent='chrome/platforms/all')
channels = [c.get('channelType', '').lower() for c in result]
channels = list(set(channels))
channel_map = {'all': 'all'}
for channel in channels:
key = channel.replace('_', '')
channel_map[key] = channel
return channel_map
def printHistory():
cv = build()
entityType = sys.argv[3].lower().replace('_', '')
if entityType not in CHROME_HISTORY_ENTITY_CHOICES:
msg = f'{entityType} is not a valid argument to "gam print chromehistory"'
controlflow.system_error_exit(3, msg)
todrive = False
csvRows = []
cplatform = 'all'
channel = 'all'
version = 'all'
kwargs = {}
orderByList = []
i = 4
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'todrive':
todrive = True
i += 1
elif entityType != 'platforms' and myarg == 'platform':
cplatform = sys.argv[i + 1].lower().replace('_', '')
platform_map = get_platform_map(cv)
if cplatform not in platform_map:
controlflow.expected_argument_exit('platform',
', '.join(platform_map),
cplatform)
cplatform = platform_map[cplatform]
i += 2
elif entityType in {'versions', 'releases'} and myarg == 'channel':
channel = sys.argv[i + 1].lower().replace('_', '')
channel_map = get_channel_map(cv)
if channel not in channel_map:
controlflow.expected_argument_exit('channel',
', '.join(channel_map),
channel)
channel = channel_map[channel]
i += 2
elif entityType == 'releases' and myarg == 'version':
version = sys.argv[i + 1]
i += 2
elif entityType in {'versions', 'releases'} and myarg == 'orderby':
fieldName = sys.argv[i + 1].lower().replace('_', '')
i += 2
if fieldName in CHROME_VERSIONHISTORY_ORDERBY_CHOICE_MAP[entityType]:
fieldName = CHROME_VERSIONHISTORY_ORDERBY_CHOICE_MAP[entityType][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(CHROME_VERSIONHISTORY_ORDERBY_CHOICE_MAP[entityType]),
fieldName)
elif entityType in {'versions', 'releases'} and myarg == 'filter':
kwargs['filter'] = sys.argv[i + 1]
i += 2
else:
msg = f'{myarg} is not a valid argument to "gam print chromehistory {entityType}"'
controlflow.system_error_exit(3, msg)
if orderByList:
kwargs['orderBy'] = ','.join(orderByList)
if entityType == 'platforms':
svc = cv.platforms()
parent = 'chrome'
elif entityType == 'channels':
svc = cv.platforms().channels()
parent = f'chrome/platforms/{cplatform}'
elif entityType == 'versions':
svc = cv.platforms().channels().versions()
parent = f'chrome/platforms/{cplatform}/channels/{channel}'
else: #elif entityType == 'releases'
svc = cv.platforms().channels().versions().releases()
parent = f'chrome/platforms/{cplatform}/channels/{channel}/versions/{version}'
reportTitle = f'Chrome Version History {entityType.capitalize()}'
page_message = gapi.got_total_items_msg(reportTitle, '...\n')
gam.printGettingAllItems(reportTitle, None)
citems = gapi.get_all_pages(svc, 'list', entityType,
page_message=page_message,
parent=parent,
fields=f'nextPageToken,{entityType}',
**kwargs)
for citem in citems:
for key in list(citem):
if key.endswith('Type'):
newkey = key[:-4]
citem[newkey] = citem.pop(key)
if 'channel' in citem:
citem['channel'] = citem['channel'].lower()
else:
channel_match = re.search(r"\/channels\/([^/]*)", citem['name'])
if channel_match:
try:
citem['channel'] = channel_match.group(1)
except IndexError:
pass
if 'platform' in citem:
citem['platform'] = citem['platform'].lower()
else:
platform_match = re.search(r"\/platforms\/([^/]*)", citem['name'])
if platform_match:
try:
citem['platform'] = platform_match.group(1)
except IndexError:
pass
if citem.get('version', '').count('.') == 3:
citem['major_version'], \
citem['minor_version'], \
citem['build'], \
citem['patch'] = citem['version'].split('.')
citem.pop('name')
csvRows.append(utils.flatten_json(citem))
display.write_csv_file(csvRows, CHROME_VERSIONHISTORY_TITLES[entityType], reportTitle, todrive)

View File

@@ -0,0 +1,265 @@
"""Chrome Management API calls"""
import sys
import gam
from gam.var import GC_CUSTOMER_ID, GC_Values, MY_CUSTOMER
from gam.var import CROS_START_ARGUMENTS, CROS_END_ARGUMENTS
from gam.var import YYYYMMDD_FORMAT
from gam import controlflow
from gam import display
from gam import gapi
from gam.gapi.directory import orgunits as gapi_directory_orgunits
from gam.gapi.directory.cros import _getFilterDate
def _get_customerid():
customer = GC_Values[GC_CUSTOMER_ID]
if customer != MY_CUSTOMER and customer[0] != 'C':
customer = 'C' + customer
return f'customers/{customer}'
def _get_orgunit(orgunit):
if orgunit.startswith('orgunits/'):
return orgunit
_, orgunitid = gapi_directory_orgunits.getOrgUnitId(orgunit)
return f'{orgunitid[3:]}'
def build():
return gam.buildGAPIObject('chromemanagement')
CHROME_APPS_ORDERBY_CHOICE_MAP = {
'appname': 'app_name',
'apptype': 'appType',
'installtype': 'install_type',
'numberofpermissions': 'number_of_permissions',
'totalinstallcount': 'total_install_count',
}
CHROME_APPS_TITLES = [
'appId', 'displayName',
'browserDeviceCount', 'osUserCount',
'appType', 'description',
'appInstallType', 'appSource',
'disabled', 'homepageUri',
'permissions'
]
def printApps():
cm = build()
customer = _get_customerid()
todrive = False
titles = CHROME_APPS_TITLES
csvRows = []
orgunit = None
pfilter = None
orderBy = None
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'todrive':
todrive = True
i += 1
elif myarg in ['ou', 'org', 'orgunit']:
orgunit = _get_orgunit(sys.argv[i+1])
i += 2
elif myarg == 'filter':
pfilter = sys.argv[i + 1]
i += 2
elif myarg == 'orderby':
orderBy = sys.argv[i + 1].lower().replace('_', '')
if orderBy not in CHROME_APPS_ORDERBY_CHOICE_MAP:
controlflow.expected_argument_exit('orderby',
', '.join(CHROME_APPS_ORDERBY_CHOICE_MAP),
orderBy)
orderBy = CHROME_APPS_ORDERBY_CHOICE_MAP[orderBy]
i += 2
else:
msg = f'{myarg} is not a valid argument to "gam print chromeapps"'
controlflow.system_error_exit(3, msg)
if orgunit:
orgUnitPath = gapi_directory_orgunits.orgunit_from_orgunitid(orgunit, None)
titles.append('orgUnitPath')
else:
orgUnitPath = '/'
gam.printGettingAllItems('Chrome Installed Applications', pfilter)
page_message = gapi.got_total_items_msg('Chrome Installed Applications', '...\n')
apps = gapi.get_all_pages(cm.customers().reports(),
'countInstalledApps',
'installedApps',
page_message=page_message,
customer=customer, orgUnitId=orgunit,
filter=pfilter, orderBy=orderBy)
for app in apps:
if orgunit:
app['orgUnitPath'] = orgUnitPath
if 'permissions'in app:
app['permissions'] = ' '.join(app['permissions'])
csvRows.append(app)
display.write_csv_file(csvRows, titles, 'Chrome Installed Applications', todrive)
CHROME_APP_DEVICES_APPTYPE_CHOICE_MAP = {
'extension': 'EXTENSION',
'app': 'APP',
'theme': 'THEME',
'hostedapp': 'HOSTED_APP',
'androidapp': 'ANDROID_APP',
}
CHROME_APP_DEVICES_ORDERBY_CHOICE_MAP = {
'deviceid': 'deviceId',
'machine': 'machine',
}
CHROME_APP_DEVICES_TITLES = [
'appId', 'appType', 'deviceId', 'machine'
]
def printAppDevices():
cm = build()
customer = _get_customerid()
todrive = False
titles = CHROME_APP_DEVICES_TITLES
csvRows = []
orgunit = None
appId = None
appType = None
startDate = None
endDate = None
pfilter = None
orderBy = None
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'todrive':
todrive = True
i += 1
elif myarg in ['ou', 'org', 'orgunit']:
orgunit = _get_orgunit(sys.argv[i+1])
i += 2
elif myarg == 'appid':
appId = sys.argv[i + 1]
i += 2
elif myarg == 'apptype':
appType = sys.argv[i + 1].lower().replace('_', '')
if appType not in CHROME_APP_DEVICES_APPTYPE_CHOICE_MAP:
controlflow.expected_argument_exit('orderby',
', '.join(CHROME_APP_DEVICES_APPTYPE_CHOICE_MAP),
appType)
appType = CHROME_APP_DEVICES_APPTYPE_CHOICE_MAP[appType]
i += 2
elif myarg in CROS_START_ARGUMENTS:
startDate = _getFilterDate(sys.argv[i + 1]).strftime(YYYYMMDD_FORMAT)
i += 2
elif myarg in CROS_END_ARGUMENTS:
endDate = _getFilterDate(sys.argv[i + 1]).strftime(YYYYMMDD_FORMAT)
i += 2
elif myarg == 'orderby':
orderBy = sys.argv[i + 1].lower().replace('_', '')
if orderBy not in CHROME_APP_DEVICES_ORDERBY_CHOICE_MAP:
controlflow.expected_argument_exit('orderby',
', '.join(CHROME_APP_DEVICES_ORDERBY_CHOICE_MAP),
orderBy)
orderBy = CHROME_APP_DEVICES_ORDERBY_CHOICE_MAP[orderBy]
i += 2
else:
msg = f'{myarg} is not a valid argument to "gam print chromeappdevices"'
controlflow.system_error_exit(3, msg)
if not appId:
controlflow.system_error_exit(3, 'You must specify an appid')
if not appType:
controlflow.system_error_exit(3, 'You must specify an apptype')
if endDate:
pfilter = f'last_active_date<={endDate}'
if startDate:
if pfilter:
pfilter += ' AND '
else:
pfilter = ''
pfilter += f'last_active_date>={startDate}'
if orgunit:
orgUnitPath = gapi_directory_orgunits.orgunit_from_orgunitid(orgunit, None)
titles.append('orgUnitPath')
else:
orgUnitPath = '/'
gam.printGettingAllItems('Chrome Installed Application Devices', pfilter)
page_message = gapi.got_total_items_msg('Chrome Installed Application Devices', '...\n')
devices = gapi.get_all_pages(cm.customers().reports(),
'findInstalledAppDevices',
'devices',
page_message=page_message,
appId=appId, appType=appType,
customer=customer, orgUnitId=orgunit,
filter=pfilter, orderBy=orderBy)
for device in devices:
if orgunit:
device['orgUnitPath'] = orgUnitPath
device['appId'] = appId
device['appType'] = appType
csvRows.append(device)
display.write_csv_file(csvRows, titles, 'Chrome Installed Application Devices', todrive)
CHROME_VERSIONS_TITLES = [
'version', 'count', 'channel', 'deviceOsVersion', 'system'
]
def printVersions():
cm = build()
customer = _get_customerid()
todrive = False
titles = CHROME_VERSIONS_TITLES
csvRows = []
orgunit = None
startDate = None
endDate = None
pfilter = None
reverse = False
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'todrive':
todrive = True
i += 1
elif myarg in ['ou', 'org', 'orgunit']:
orgunit = _get_orgunit(sys.argv[i+1])
i += 2
elif myarg in CROS_START_ARGUMENTS:
startDate = _getFilterDate(sys.argv[i + 1]).strftime(YYYYMMDD_FORMAT)
i += 2
elif myarg in CROS_END_ARGUMENTS:
endDate = _getFilterDate(sys.argv[i + 1]).strftime(YYYYMMDD_FORMAT)
i += 2
elif myarg == 'recentfirst':
reverse = True
i += 1
else:
msg = f'{myarg} is not a valid argument to "gam print chromeversions"'
controlflow.system_error_exit(3, msg)
if endDate:
pfilter = f'last_active_date<={endDate}'
if startDate:
if pfilter:
pfilter += ' AND '
else:
pfilter = ''
pfilter += f'last_active_date>={startDate}'
if orgunit:
orgUnitPath = gapi_directory_orgunits.orgunit_from_orgunitid(orgunit, None)
titles.append('orgUnitPath')
else:
orgUnitPath = '/'
gam.printGettingAllItems('Chrome Versions', pfilter)
page_message = gapi.got_total_items_msg('Chrome Versions', '...\n')
versions = gapi.get_all_pages(cm.customers().reports(),
'countChromeVersions',
'browserVersions',
page_message=page_message,
customer=customer, orgUnitId=orgunit, filter=pfilter)
for version in sorted(versions, key=lambda k: k.get('version', 'Unknown'), reverse=reverse):
if orgunit:
version['orgUnitPath'] = orgUnitPath
if 'version' not in version:
version['version'] = 'Unknown'
csvRows.append(version)
display.write_csv_file(csvRows, titles, 'Chrome Versions', todrive)

View File

@@ -1,5 +1,6 @@
"""Chrome Browser Cloud Management API calls"""
import re
import sys
import googleapiclient.errors
@@ -9,6 +10,7 @@ from gam.var import GC_CUSTOMER_ID, GC_Values, MY_CUSTOMER
from gam import controlflow
from gam import gapi
from gam.gapi import errors as gapi_errors
from gam.gapi import chromehistory as gapi_chromehistory
from gam.gapi.directory import orgunits as gapi_directory_orgunits
from gam import utils
@@ -96,10 +98,18 @@ def printshow_policies():
for policy in sorted(policies, key=lambda k: k.get('value', {}).get('policySchema', '')):
print()
name = policy.get('value', {}).get('policySchema', '')
schema = CHROME_SCHEMA_TYPE_MESSAGE.get(name)
print(name)
values = policy.get('value', {}).get('value', {})
for setting, value in values.items():
if isinstance(value, str) and value.find('_ENUM_') != -1:
# Handle TYPE_MESSAGE fields with durations or counts as a special case
if schema and setting == schema['casedField']:
value = value.get(schema['type'], '')
if value:
if value.endswith('s'):
value = value[:-1]
value = int(value) // schema['scale']
elif isinstance(value, str) and value.find('_ENUM_') != -1:
value = value.split('_ENUM_')[-1]
print(f' {setting}: {value}')
@@ -243,6 +253,25 @@ def delete_policy():
gapi.call(svc.customers().policies().orgunits(), 'batchInherit', customer=customer, body=body)
CHROME_SCHEMA_TYPE_MESSAGE = {
'chrome.users.SessionLength':
{'field': 'sessiondurationlimit', 'casedField': 'sessionDurationLimit',
'type': 'duration', 'minVal': 1, 'maxVal': 1440, 'scale': 60},
'chrome.users.BrowserSwitcherDelayDuration':
{'field': 'browserswitcherdelayduration', 'casedField': 'browserSwitcherDelayDuration',
'type': 'duration', 'minVal': 0, 'maxVal': 30, 'scale': 1},
'chrome.users.MaxInvalidationFetchDelay':
{'field': 'maxinvalidationfetchdelay', 'casedField': 'maxInvalidationFetchDelay',
'type': 'duration', 'minVal': 1, 'maxVal': 30, 'scale': 1},
'chrome.users.SecurityTokenSessionSettings':
{'field': 'securitytokensessionnotificationseconds', 'casedField': 'securityTokenSessionNotificationSeconds',
'type': 'duration', 'minVal': 0, 'maxVal': 9999, 'scale': 1},
'chrome.users.PrintingMaxSheetsAllowed':
{'field': 'printingmaxsheetsallowednullable', 'casedField': 'printingMaxSheetsAllowedNullable',
'type': 'value', 'minVal': 1, 'maxVal': None, 'scale': 1},
}
def update_policy():
svc = build()
customer = _get_customerid()
@@ -264,7 +293,8 @@ def update_policy():
app_id = sys.argv[i+1]
i += 2
elif myarg in schemas:
body['requests'].append({'policyValue': {'policySchema': schemas[myarg]['name'],
schemaName = schemas[myarg]['name']
body['requests'].append({'policyValue': {'policySchema': schemaName,
'value': {}},
'updateMask': ''})
i += 1
@@ -272,6 +302,19 @@ def update_policy():
field = sys.argv[i].lower()
if field in ['ou', 'org', 'orgunit', 'printerid', 'appid'] or '.' in field:
break # field is actually a new policy, orgunit or app/printer id
# Handle TYPE_MESSAGE fields with durations or counts as a special case
schema = CHROME_SCHEMA_TYPE_MESSAGE.get(schemaName)
if schema and field == schema['field']:
casedField = schema['casedField']
value = gam.getInteger(sys.argv[i+1], casedField,
minVal=schema['minVal'], maxVal=schema['maxVal'])*schema['scale']
if schema['type'] == 'duration':
body['requests'][-1]['policyValue']['value'][casedField] = {schema['type']: f'{value}s'}
else:
body['requests'][-1]['policyValue']['value'][casedField] = {schema['type']: value}
body['requests'][-1]['updateMask'] += f'{casedField},'
i += 2
continue
expected_fields = ', '.join(schemas[myarg]['settings'])
if field not in expected_fields:
msg = f'Expected {myarg} field of {expected_fields}. Got {field}.'
@@ -288,16 +331,37 @@ def update_policy():
value = gam.getBoolean(value, field)
elif vtype in ['TYPE_ENUM']:
value = value.upper()
prefix = schemas[myarg]['settings'][field]['enum_prefix']
enum_values = schemas[myarg]['settings'][field]['enums']
if value not in enum_values:
if value in enum_values:
value = f'{prefix}{value}'
elif value.replace(prefix, '') in enum_values:
pass
else:
expected_enums = ', '.join(enum_values)
msg = f'Expected {myarg} {field} value to be one of ' \
f'{expected_enums}, got {value}'
controlflow.system_error_exit(8, msg)
prefix = schemas[myarg]['settings'][field]['enum_prefix']
value = f'{prefix}{value}'
elif vtype in ['TYPE_LIST']:
value = value.split(',')
if myarg == 'chrome.users.chromebrowserupdates' and \
cased_field == 'targetVersionPrefixSetting':
mg = re.compile(r'^([a-z]+)-(\d+)$').match(value)
if mg:
channel = mg.group(1).lower().replace('_', '')
minus = mg.group(2)
channel_map = gapi_chromehistory.get_channel_map(None)
if channel not in channel_map:
expected_channels = ', '.join(channel_map)
msg = f'Expected {myarg} {cased_field} channel to be one of ' \
f'{expected_channels}, got {channel}'
controlflow.system_error_exit(8, msg)
milestone = gapi_chromehistory.get_relative_milestone(
channel_map[channel], int(minus))
if not milestone:
msg = f'{myarg} {cased_field} channel {channel} offset {minus} does not exist'
controlflow.system_error_exit(8, msg)
value = f'{milestone}.'
body['requests'][-1]['policyValue']['value'][cased_field] = value
body['requests'][-1]['updateMask'] += f'{cased_field},'
i += 2

View File

@@ -13,8 +13,12 @@ from gam.gapi import cloudidentity as gapi_cloudidentity
from gam.gapi.directory import customer as gapi_directory_customer
def build():
return gapi_cloudidentity.build('cloudidentity')
def create():
ci = gapi_cloudidentity.build('cloudidentity_beta')
ci = build()
initialGroupConfig = 'EMPTY'
gapi_directory_customer.setTrueCustomerId()
parent = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
@@ -66,7 +70,7 @@ def create():
def delete():
ci = gapi_cloudidentity.build('cloudidentity_beta')
ci = build()
group = sys.argv[3]
name = group_email_to_id(ci, group)
print(f'Deleting group {group}')
@@ -74,11 +78,12 @@ def delete():
def info():
ci = gapi_cloudidentity.build('cloudidentity_beta')
ci = build()
group = gam.normalizeEmailAddressOrUID(sys.argv[3])
getUsers = True
showJoinDate = True
showUpdateDate = False
showMemberTree = False
i = 4
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
@@ -91,12 +96,15 @@ def info():
elif myarg == 'showupdatedate':
showUpdateDate = True
i += 1
elif myarg == 'membertree':
showMemberTree = 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 getUsers and not showMemberTree:
if not showJoinDate and not showUpdateDate:
view = 'BASIC'
pageSize = 1000
@@ -126,10 +134,42 @@ def info():
# f' {member.get("role", ROLE_MEMBER).lower()}: {member.get("email", member["id"])} ({member["type"].lower()})'
)
print(f'Total {len(members)} users in group')
elif showMemberTree:
print(' Member tree:')
global cached_group_members
cached_group_members = {}
print_member_tree(ci, name)
def print_member_tree(ci, group_id, spaces=2):
if not group_id in cached_group_members:
cached_group_members[group_id] = gapi.get_all_pages(ci.groups().memberships(),
'list',
'memberships',
parent=group_id,
fields='*',
pageSize=1000)
for member in cached_group_members[group_id]:
member_id = member.get('name', '')
member_id = member_id.split('/')[-1]
if member_id.isdigit():
member_type = 'user'
else:
member_type = 'group'
member_email = member.get('preferredMemberKey', {}).get('id')
relation_type = member.get('relationType', '').lower()
if member_type == 'user':
print(f'{" " * spaces}{member_email} - user')
elif member_type == 'group':
print(f'{" " * spaces}{member_email} - group')
group_id = group_email_to_id(ci, member_email)
print_member_tree(ci, group_id, spaces + 2)
else:
print(f'unknown member type: {member_type} for {member_email}')
def info_member():
ci = gapi_cloudidentity.build('cloudidentity_beta')
ci = build()
member = gam.normalizeEmailAddressOrUID(sys.argv[3])
group = gam.normalizeEmailAddressOrUID(sys.argv[4])
group_name = gapi.call(ci.groups(),
@@ -159,7 +199,7 @@ GROUP_ROLES_MAP = {
def print_():
ci = gapi_cloudidentity.build('cloudidentity_beta')
ci = build()
i = 3
members = membersCountOnly = managers = managersCountOnly = owners = ownersCountOnly = False
gapi_directory_customer.setTrueCustomerId()
@@ -343,8 +383,58 @@ def print_():
display.write_csv_file(csvRows, titles, 'Groups', todrive)
def _get_groups_list(ci=None, member=None, parent=None):
if not ci:
ci = build()
if not parent:
gapi_directory_customer.setTrueCustomerId()
parent = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
gam.printGettingAllItems('Groups', member)
page_message = gapi.got_total_items_first_last_msg('Groups')
if member:
fields = 'nextPageToken,memberships(groupKey(id),relationType)'
try:
groups_to_get = gapi.get_all_pages(ci.groups().memberships(),
'searchTransitiveGroups',
'memberships',
throw_reasons=[gapi_errors.ErrorReason.FOUR_O_O],
message_attribute=['groupKey', 'id'],
page_message=page_message,
parent='groups/-',
query=member,
pageSize=1000,
fields=fields)
except googleapiclient.errors.HttpError:
controlflow.system_error_exit(
2,
f'enterprisemember requires Enterprise license')
return [group['groupKey']['id'] for group in groups_to_get if group['relationType'] == 'DIRECT']
else:
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))')
return [group['groupKey']['id'] for group in groups_to_get]
def get_membership_graph(member):
ci = build()
query = f"member_key_id == '{member}' && 'cloudidentity.googleapis.com/groups.discussion_forum' in labels"
result = gapi.call(ci.groups().memberships(),
'getMembershipGraph',
parent='groups/-',
query=query)
return result.get('response')
def print_members():
ci = gapi_cloudidentity.build('cloudidentity_beta')
ci = build()
todrive = False
gapi_directory_customer.setTrueCustomerId()
parent = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
@@ -381,36 +471,7 @@ def print_members():
controlflow.invalid_argument_exit(sys.argv[i],
'gam print cigroup-members')
if not groups_to_get:
gam.printGettingAllItems('Groups', usemember)
page_message = gapi.got_total_items_first_last_msg('Groups')
if usemember:
try:
groups_to_get = gapi.get_all_pages(ci.groups().memberships(),
'searchTransitiveGroups',
'memberships',
throw_reasons=[gapi_errors.ErrorReason.FOUR_O_O],
message_attribute=['groupKey', 'id'],
page_message=page_message,
parent='groups/-', query=usemember,
pageSize=1000,
fields='nextPageToken,memberships(groupKey(id),relationType)')
except googleapiclient.errors.HttpError:
controlflow.system_error_exit(
2,
f'enterprisemember requires Enterprise license')
groups_to_get = [group['groupKey']['id'] for group in groups_to_get if group['relationType'] == 'DIRECT']
else:
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]
groups_to_get = _get_groups_list(ci, usemember, parent)
i = 0
count = len(groups_to_get)
for group_email in groups_to_get:
@@ -489,7 +550,7 @@ def update():
]
return (role, expireTime, users_email)
ci = gapi_cloudidentity.build('cloudidentity_beta')
ci = build()
group = sys.argv[3]
myarg = sys.argv[4].lower()
items = []

View File

@@ -23,9 +23,19 @@ def _reduce_name(name):
''' converts long name into email address'''
return name.split('/')[-1]
def is_invitable_user(email):
'''return email isInvitableUser'''
svc = gapi_cloudidentity.build_dwd('cloudidentity_beta')
customer = _get_customerid()
encoded_email = quote_plus(email)
name = f'{customer}/userinvitations/{encoded_email}'
return gapi.call(svc.customers().userinvitations(), 'isInvitableUser',
name=name)['isInvitableUser']
def _generic_action(action):
'''generic function to call actionable APIs'''
svc = gapi_cloudidentity.build('cloudidentity_beta')
svc = gapi_cloudidentity.build_dwd('cloudidentity_beta')
customer = _get_customerid()
email = sys.argv[3].lower()
encoded_email = quote_plus(email)
@@ -45,7 +55,7 @@ def _generic_action(action):
def _generic_get(get_type):
'''generic function to call read data APIs'''
svc = gapi_cloudidentity.build('cloudidentity_beta')
svc = gapi_cloudidentity.build_dwd('cloudidentity_beta')
customer = _get_customerid()
email = sys.argv[3].lower()
encoded_email = quote_plus(email)
@@ -65,7 +75,7 @@ def bulk_is_invitable(emails):
if response.get('isInvitableUser'):
rows.append({'invitableUsers': request_id})
svc = gapi_cloudidentity.build('cloudidentity_beta')
svc = gapi_cloudidentity.build_dwd('cloudidentity_beta')
customer = _get_customerid()
todrive = False
#batch_size = 1000
@@ -105,13 +115,13 @@ def get():
_generic_get('get')
def is_invitable_user():
def check():
'''gam check userinvitation <email>'''
_generic_get('isInvitableUser')
def send():
'''gam create userinvitation <email>'''
'''gam send userinvitation <email>'''
_generic_action('send')
@@ -129,7 +139,7 @@ USERINVITATION_STATE_CHOICES_MAP = {
def print_():
'''gam print userinvitations'''
svc = gapi_cloudidentity.build('cloudidentity_beta')
svc = gapi_cloudidentity.build_dwd('cloudidentity_beta')
customer = _get_customerid()
todrive = False
titles = ['name', 'state', 'updateTime']

View File

@@ -64,9 +64,9 @@ def doGetCustomerInfo():
'accounts:num_users': 'Total Users',
'accounts:gsuite_basic_total_licenses': 'G Suite Basic Licenses',
'accounts:gsuite_basic_used_licenses': 'G Suite Basic Users',
'accounts:gsuite_enterprise_total_licenses': 'G Suite Enterprise ' \
'accounts:gsuite_enterprise_total_licenses': 'Workspace Enterprise Plus ' \
'Licenses',
'accounts:gsuite_enterprise_used_licenses': 'G Suite Enterprise ' \
'accounts:gsuite_enterprise_used_licenses': 'Workspace Enterprise Plus ' \
'Users',
'accounts:gsuite_unlimited_total_licenses': 'G Suite Business ' \
'Licenses',

View File

@@ -7,8 +7,7 @@ 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.gapi.directory import customer as gapi_directory_customer
from gam import utils
from gam.gapi.cloudidentity import userinvitations as gapi_cloudidentity_userinvitations
def GroupIsAbuseOrPostmaster(emailAddr):
@@ -23,6 +22,7 @@ def create():
cd = gapi_directory.build()
body = {'email': gam.normalizeEmailAddressOrUID(sys.argv[3], noUid=True)}
gs_get_before_update = got_name = False
verifyNotInvitable = False
i = 4
gs_body = {}
gs = None
@@ -51,6 +51,9 @@ def create():
elif myarg == 'getbeforeupdate':
gs_get_before_update = True
i += 1
elif myarg == 'verifynotinvitable':
verifyNotInvitable = True
i += 1
else:
if not gs:
gs = gam.buildGAPIObject('groupssettings')
@@ -60,6 +63,10 @@ def create():
i += 2
if not got_name:
body['name'] = body['email']
if (verifyNotInvitable and
gapi_cloudidentity_userinvitations.get_is_invitable_user(body['email'])):
sys.stderr.write(f'Group not created, {body["email"]} is an unmanaged account\n')
sys.exit(51)
print(f'Creating group {body["email"]}')
gapi.call(cd.groups(), 'insert', body=body, fields='email')
if gs and not GroupIsAbuseOrPostmaster(body['email']):
@@ -1138,6 +1145,7 @@ def update():
else:
i = 4
use_cd_api = False
verifyNotInvitable = False
gs = None
gs_body = {}
cd_body = {}
@@ -1155,6 +1163,9 @@ def update():
elif myarg == 'getbeforeupdate':
gs_get_before_update = True
i += 1
elif myarg == 'verifynotinvitable':
verifyNotInvitable = True
i += 1
else:
if not gs:
gs = gam.buildGAPIObject('groupssettings')
@@ -1166,6 +1177,10 @@ def update():
if use_cd_api or (
group.find('@') == -1
): # group settings API won't take uid so we make sure cd API is used so that we can grab real email.
if (verifyNotInvitable and 'email' in cd_body and
gapi_cloudidentity_userinvitations.get_is_invitable_user(cd_body['email'])):
sys.stderr.write(f'Group {group} not updated, new email {cd_body["email"]} is an unmanaged account\n')
sys.exit(51)
group = gapi.call(cd.groups(),
'update',
groupKey=group,

View File

@@ -7,6 +7,7 @@ from unittest.mock import patch
import googleapiclient.errors
from gam.gapi import errors
import httplib2
def create_simple_http_error(status, reason, message):
@@ -15,10 +16,10 @@ def create_simple_http_error(status, reason, message):
def create_http_error(status, content):
response = {
response = httplib2.Response({
'status': status,
'content-type': 'application/json',
}
})
content_as_bytes = json.dumps(content).encode('UTF-8')
return googleapiclient.errors.HttpError(response, content_as_bytes)
@@ -73,6 +74,7 @@ class ErrorsTest(unittest.TestCase):
def test_get_gapi_error_extracts_user_not_found(self):
err = create_simple_http_error(404, 'notFound',
'Resource Not Found: userKey.')
print(err)
http_status, reason, message = errors.get_gapi_error_detail(err)
self.assertEqual(http_status, 404)
self.assertEqual(reason, errors.ErrorReason.USER_NOT_FOUND.value)
@@ -158,7 +160,7 @@ class ErrorsTest(unittest.TestCase):
def test_get_gapi_error_extracts_single_error_with_message(self):
status_code = 999
response = {'status': status_code}
response = httplib2.Response({'status': status_code})
# This error does not have an "errors" key describing each error.
content = {'error': {'code': status_code, 'message': 'unknown error'}}
content_as_bytes = json.dumps(content).encode('UTF-8')
@@ -172,7 +174,7 @@ class ErrorsTest(unittest.TestCase):
def test_get_gapi_error_exits_code_4_on_malformed_error_with_unknown_description(
self):
status_code = 999
response = {'status': status_code}
response = httplib2.Response({'status': status_code})
# This error only has an error_description_field and an unknown description.
content = {'error_description': 'something errored'}
content_as_bytes = json.dumps(content).encode('UTF-8')
@@ -184,7 +186,7 @@ class ErrorsTest(unittest.TestCase):
def test_get_gapi_error_exits_on_invalid_error_description(self):
status_code = 400
response = {'status': status_code}
response = httplib2.Response({'status': status_code})
content = {'error_description': 'Invalid Value'}
content_as_bytes = json.dumps(content).encode('UTF-8')
err = googleapiclient.errors.HttpError(response, content_as_bytes)
@@ -196,7 +198,7 @@ class ErrorsTest(unittest.TestCase):
def test_get_gapi_error_exits_code_4_on_unexpected_error_contents(self):
status_code = 900
response = {'status': status_code}
response = httplib2.Response({'status': status_code})
content = {'notErrorContentThatIsExpected': 'foo'}
content_as_bytes = json.dumps(content).encode('UTF-8')
err = googleapiclient.errors.HttpError(response, content_as_bytes)

View File

@@ -40,19 +40,19 @@ def create_http(cache=None,
return httpObj
def create_request(http=None):
def create_request(httpObj=None):
"""Creates a uniform Request object with a default http, if not provided.
Args:
http: Optional httplib2.Http compatible object to be used with the request.
httpObj: Optional httplib2.Http compatible object to be used with the request.
If not provided, a default HTTP will be used.
Returns:
Request: A google_auth_httplib2.Request compatible Request.
"""
if not http:
http = create_http()
return Request(http)
if not httpObj:
httpObj = create_http()
return Request(httpObj)
GAM_USER_AGENT = GAM_INFO

View File

@@ -64,7 +64,7 @@ class TransportTest(unittest.TestCase):
self.assertEqual(request.http, mock_create_http.return_value)
def test_create_request_uses_provided_http(self):
request = transport.create_request(http=self.mock_http)
request = transport.create_request(httpObj=self.mock_http)
self.assertEqual(request.http, self.mock_http)
def test_create_request_returns_request_with_forced_user_agent(self):

View File

@@ -1,3 +1,7 @@
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import datetime
import re
import sys
@@ -5,8 +9,10 @@ import time
from hashlib import md5
from html.entities import name2codepoint
from html.parser import HTMLParser
import importlib
import json
import dateutil.parser
import types
from gam import controlflow
from gam import fileutils
@@ -14,6 +20,41 @@ from gam import transport
from gam.var import *
class LazyLoader(types.ModuleType):
"""Lazily import a module, mainly to avoid pulling in large dependencies.
`contrib`, and `ffmpeg` are examples of modules that are large and not always
needed, and this allows them to only be loaded when they are used.
"""
# The lint error here is incorrect.
def __init__(self, local_name, parent_module_globals, name): # pylint: disable=super-on-old-class
self._local_name = local_name
self._parent_module_globals = parent_module_globals
super(LazyLoader, self).__init__(name)
def _load(self):
# Import the target module and insert it into the parent's namespace
module = importlib.import_module(self.__name__)
self._parent_module_globals[self._local_name] = module
# Update this object's dict so that if someone keeps a reference to the
# LazyLoader, lookups are efficient (__getattr__ is only called on lookups
# that fail).
self.__dict__.update(module.__dict__)
return module
def __getattr__(self, item):
module = self._load()
return getattr(module, item)
def __dir__(self):
module = self._load()
return dir(module)
class _DeHTMLParser(HTMLParser):
def __init__(self):

View File

@@ -8,7 +8,7 @@ import platform
import re
GAM_AUTHOR = 'Jay Lee <jay0lee@gmail.com>'
GAM_VERSION = '6.00'
GAM_VERSION = '6.02'
GAM_LICENSE = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
GAM_URL = 'https://git.io/gam'
@@ -56,6 +56,11 @@ SKUS = {
'aliases': ['identitypremium', 'cloudidentitypremium'],
'displayName': 'Cloud Identity Premium'
},
'1010350001': {
'product': '101035',
'aliases': ['cloudsearch'],
'displayName': 'Google Cloud Search',
},
'1010310002': {
'product': '101031',
'aliases': ['gsefe', 'e4e', 'gsuiteenterpriseeducation'],
@@ -188,6 +193,11 @@ SKUS = {
'wsentplus', 'workspaceenterpriseplus'],
'displayName': 'Workspace Enterprise Plus'
},
'1010020030': {
'product': 'Google-Apps',
'aliases': ['workspacefrontline', 'workspacefrontlineworker'],
'displayName': 'Workspace Frontline'
},
'1010340002': {
'product': '101034',
'aliases': ['gsbau', 'businessarchived', 'gsuitebusinessarchived'],
@@ -266,6 +276,7 @@ PRODUCTID_NAME_MAPPINGS = {
'101031': 'G Suite Workspace for Education',
'101033': 'Google Voice',
'101034': 'G Suite Archived',
'101035': 'Cloud Search',
'101037': 'G Suite Workspace for Education',
'Google-Apps': 'Google Workspace',
'Google-Chrome-Device-Management': 'Google Chrome Device Management',
@@ -297,6 +308,7 @@ API_VER_MAPPING = {
'driveactivity': 'v2',
'calendar': 'v3',
'cbcm': 'v1.1beta1',
'chromemanagement': 'v1',
'chromepolicy': 'v1',
'classroom': 'v1',
'cloudidentity': 'v1',
@@ -323,6 +335,7 @@ API_VER_MAPPING = {
'siteVerification': 'v1',
'storage': 'v1',
'vault': 'v1',
'versionhistory': 'v1',
}
USERINFO_EMAIL_SCOPE = 'https://www.googleapis.com/auth/userinfo.email'
@@ -335,6 +348,7 @@ API_SCOPE_MAPPING = {
],
'calendar': ['https://www.googleapis.com/auth/calendar',],
'cloudidentity': ['https://www.googleapis.com/auth/cloud-identity'],
'cloudidentity_beta': ['https://www.googleapis.com/auth/cloud-identity'],
'drive': ['https://www.googleapis.com/auth/drive',],
'drive3': ['https://www.googleapis.com/auth/drive',],
'gmail': [
@@ -381,16 +395,21 @@ ADDRESS_FIELDS_ARGUMENT_MAP = {
}
SERVICE_NAME_TO_ID_MAP = {
'Calendar': '435070579839',
'Currents': '553547912911',
'Drive and Docs': '55656082996',
'Calendar': '435070579839'
'Google Data Studio': '810260081642',
}
SERVICE_NAME_CHOICES_MAP = {
'calendar': 'Calendar',
'currents': 'Currents',
'datastudio': 'Google Data Studio',
'google data studio': 'Google Data Studio',
'drive': 'Drive and Docs',
'drive and docs': 'Drive and Docs',
'googledrive': 'Drive and Docs',
'gdrive': 'Drive and Docs',
'calendar': 'Calendar',
}
PRINTJOB_ASCENDINGORDER_MAP = {

View File

@@ -2,6 +2,7 @@ admin.googleapis.com
alertcenter.googleapis.com
calendar-json.googleapis.com
chat.googleapis.com
chromemanagement.googleapis.com
chromepolicy.googleapis.com
classroom.googleapis.com
cloudidentity.googleapis.com

View File

@@ -1,7 +1,7 @@
cryptography
distro; sys_platform == 'linux'
filelock
google-api-python-client>=1.7.10
google-api-python-client>=2.1
google-auth-httplib2
google-auth-oauthlib>=0.4.1
google-auth>=1.11.2

486
src/versionhistory-v1.json Normal file
View File

@@ -0,0 +1,486 @@
{
"revision": "20210322",
"name": "versionhistory",
"mtlsRootUrl": "https://versionhistory.mtls.googleapis.com/",
"version_module": true,
"basePath": "",
"title": "Version History API",
"kind": "discovery#restDescription",
"servicePath": "",
"ownerDomain": "google.com",
"parameters": {
"access_token": {
"location": "query",
"description": "OAuth access token.",
"type": "string"
},
"alt": {
"default": "json",
"location": "query",
"enum": [
"json",
"media",
"proto"
],
"type": "string",
"enumDescriptions": [
"Responses with Content-Type of application/json",
"Media download with context-dependent Content-Type",
"Responses with Content-Type of application/x-protobuf"
],
"description": "Data format for response."
},
"quotaUser": {
"type": "string",
"description": "Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters.",
"location": "query"
},
"$.xgafv": {
"location": "query",
"enumDescriptions": [
"v1 error format",
"v2 error format"
],
"enum": [
"1",
"2"
],
"description": "V1 error format.",
"type": "string"
},
"fields": {
"type": "string",
"description": "Selector specifying which fields to include in a partial response.",
"location": "query"
},
"key": {
"description": "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.",
"location": "query",
"type": "string"
},
"callback": {
"location": "query",
"description": "JSONP",
"type": "string"
},
"oauth_token": {
"description": "OAuth 2.0 token for the current user.",
"location": "query",
"type": "string"
},
"upload_protocol": {
"location": "query",
"type": "string",
"description": "Upload protocol for media (e.g. \"raw\", \"multipart\")."
},
"prettyPrint": {
"description": "Returns response with indentations and line breaks.",
"default": "true",
"location": "query",
"type": "boolean"
},
"uploadType": {
"description": "Legacy upload protocol for media (e.g. \"media\", \"multipart\").",
"type": "string",
"location": "query"
}
},
"ownerName": "Google",
"protocol": "rest",
"resources": {
"platforms": {
"methods": {
"list": {
"httpMethod": "GET",
"parameterOrder": [
"parent"
],
"response": {
"$ref": "ListPlatformsResponse"
},
"path": "v1/{+parent}/platforms",
"description": "Returns list of platforms that are avaialble for a given product. The resource \"product\" has no resource name in its name.",
"flatPath": "v1/{v1Id}/platforms",
"id": "versionhistory.platforms.list",
"parameters": {
"parent": {
"location": "path",
"pattern": "^[^/]+$",
"type": "string",
"required": true,
"description": "Required. The product, which owns this collection of platforms. Format: {product}"
},
"pageSize": {
"format": "int32",
"description": "Optional. Optional limit on the number of channels to include in the response. If unspecified, the server will pick an appropriate default.",
"type": "integer",
"location": "query"
},
"pageToken": {
"location": "query",
"description": "Optional. A page token, received from a previous `ListChannels` call. Provide this to retrieve the subsequent page.",
"type": "string"
}
}
}
},
"resources": {
"channels": {
"resources": {
"versions": {
"resources": {
"releases": {
"methods": {
"list": {
"id": "versionhistory.platforms.channels.versions.releases.list",
"path": "v1/{+parent}/releases",
"httpMethod": "GET",
"parameterOrder": [
"parent"
],
"response": {
"$ref": "ListReleasesResponse"
},
"flatPath": "v1/{v1Id}/platforms/{platformsId}/channels/{channelsId}/versions/{versionsId}/releases",
"parameters": {
"parent": {
"type": "string",
"required": true,
"description": "Required. The version, which owns this collection of releases. Format: {product}/platforms/{platform}/channels/{channel}/versions/{version}",
"pattern": "^[^/]+/platforms/[^/]+/channels/[^/]+/versions/[^/]+$",
"location": "path"
},
"filter": {
"type": "string",
"location": "query",
"description": "Optional. Filter string. Format is a comma separated list of All comma separated filter clauses are conjoined with a logical \"and\". Valid field_names are \"version\", \"name\", \"platform\", \"channel\", \"fraction\" \"starttime\", and \"endtime\". Valid operators are \"\u003c\", \"\u003c=\", \"=\", \"\u003e=\", and \"\u003e\". Channel comparison is done by distance from stable. must be a valid channel when filtering by channel. Ex) stable \u003c beta, beta \u003c dev, canary \u003c canary_asan. Version comparison is done numerically. Ex) 1.0.0.8 \u003c 1.0.0.10. If version is not entirely written, the version will be appended with 0 for the missing fields. Ex) version \u003e 80 becoms version \u003e 80.0.0.0 When filtering by starttime or endtime, string must be in RFC 3339 date string format. Name and platform are filtered by string comparison. Ex) \"...?filter=channel\u003c=beta, version \u003e= 80 Ex) \"...?filter=version \u003e 80, version \u003c 81 Ex) \"...?filter=starttime\u003e2020-01-01T00:00:00Z"
},
"orderBy": {
"location": "query",
"description": "Optional. Ordering string. Valid order_by strings are \"version\", \"name\", \"starttime\", \"endtime\", \"platform\", \"channel\", and \"fraction\". Optionally, you can append \"desc\" or \"asc\" to specify the sorting order. Multiple order_by strings can be used in a comma separated list. Ordering by channel will sort by distance from the stable channel (not alphabetically). A list of channels sorted in this order is: stable, beta, dev, canary, and canary_asan. Sorting by name may cause unexpected behaviour as it is a naive string sort. For example, 1.0.0.8 will be before 1.0.0.10 in descending order. If order_by is not specified the response will be sorted by starttime in descending order. Ex) \"...?order_by=starttime asc\" Ex) \"...?order_by=platform desc, channel, startime desc\"",
"type": "string"
},
"pageSize": {
"location": "query",
"format": "int32",
"description": "Optional. Optional limit on the number of releases to include in the response. If unspecified, the server will pick an appropriate default.",
"type": "integer"
},
"pageToken": {
"description": "Optional. A page token, received from a previous `ListReleases` call. Provide this to retrieve the subsequent page.",
"location": "query",
"type": "string"
}
},
"description": "Returns list of releases of the given version."
}
}
}
},
"methods": {
"list": {
"response": {
"$ref": "ListVersionsResponse"
},
"path": "v1/{+parent}/versions",
"parameters": {
"pageSize": {
"location": "query",
"format": "int32",
"description": "Optional. Optional limit on the number of versions to include in the response. If unspecified, the server will pick an appropriate default.",
"type": "integer"
},
"pageToken": {
"description": "Optional. A page token, received from a previous `ListVersions` call. Provide this to retrieve the subsequent page.",
"location": "query",
"type": "string"
},
"parent": {
"required": true,
"location": "path",
"description": "Required. The channel, which owns this collection of versions. Format: {product}/platforms/{platform}/channels/{channel}",
"pattern": "^[^/]+/platforms/[^/]+/channels/[^/]+$",
"type": "string"
},
"orderBy": {
"type": "string",
"location": "query",
"description": "Optional. Ordering string. Valid order_by strings are \"version\", \"name\", \"platform\", and \"channel\". Optionally, you can append \" desc\" or \" asc\" to specify the sorting order. Multiple order_by strings can be used in a comma separated list. Ordering by channel will sort by distance from the stable channel (not alphabetically). A list of channels sorted in this order is: stable, beta, dev, canary, and canary_asan. Sorting by name may cause unexpected behaviour as it is a naive string sort. For example, 1.0.0.8 will be before 1.0.0.10 in descending order. If order_by is not specified the response will be sorted by version in descending order. Ex) \"...?order_by=version asc\" Ex) \"...?order_by=platform desc, channel, version\""
},
"filter": {
"description": "Optional. Filter string. Format is a comma separated list of All comma separated filter clauses are conjoined with a logical \"and\". Valid field_names are \"version\", \"name\", \"platform\", and \"channel\". Valid operators are \"\u003c\", \"\u003c=\", \"=\", \"\u003e=\", and \"\u003e\". Channel comparison is done by distance from stable. Ex) stable \u003c beta, beta \u003c dev, canary \u003c canary_asan. Version comparison is done numerically. If version is not entirely written, the version will be appended with 0 in missing fields. Ex) version \u003e 80 becoms version \u003e 80.0.0.0 Name and platform are filtered by string comparison. Ex) \"...?filter=channel\u003c=beta, version \u003e= 80 Ex) \"...?filter=version \u003e 80, version \u003c 81",
"location": "query",
"type": "string"
}
},
"id": "versionhistory.platforms.channels.versions.list",
"parameterOrder": [
"parent"
],
"description": "Returns list of version for the given platform/channel.",
"flatPath": "v1/{v1Id}/platforms/{platformsId}/channels/{channelsId}/versions",
"httpMethod": "GET"
}
}
}
},
"methods": {
"list": {
"response": {
"$ref": "ListChannelsResponse"
},
"parameterOrder": [
"parent"
],
"parameters": {
"pageToken": {
"type": "string",
"location": "query",
"description": "Optional. A page token, received from a previous `ListChannels` call. Provide this to retrieve the subsequent page."
},
"parent": {
"location": "path",
"type": "string",
"required": true,
"pattern": "^[^/]+/platforms/[^/]+$",
"description": "Required. The platform, which owns this collection of channels. Format: {product}/platforms/{platform}"
},
"pageSize": {
"format": "int32",
"type": "integer",
"description": "Optional. Optional limit on the number of channels to include in the response. If unspecified, the server will pick an appropriate default.",
"location": "query"
}
},
"path": "v1/{+parent}/channels",
"httpMethod": "GET",
"flatPath": "v1/{v1Id}/platforms/{platformsId}/channels",
"id": "versionhistory.platforms.channels.list",
"description": "Returns list of channels that are available for a given platform."
}
}
}
}
}
},
"description": "Version History API - Prod",
"discoveryVersion": "v1",
"schemas": {
"Channel": {
"id": "Channel",
"type": "object",
"description": "Each Channel is owned by a Platform and owns a collection of versions. Possible Channels are listed in the Channel enum below. Not all Channels are available for every Platform (e.g. CANARY does not exist for LINUX).",
"properties": {
"name": {
"description": "Channel name. Format is \"{product}/platforms/{platform}/channels/{channel}\"",
"type": "string"
},
"channelType": {
"description": "Type of channel.",
"enumDescriptions": [
"",
"",
"",
"",
"",
"",
"",
""
],
"type": "string",
"enum": [
"CHANNEL_TYPE_UNSPECIFIED",
"STABLE",
"BETA",
"DEV",
"CANARY",
"CANARY_ASAN",
"ALL",
"EXTENDED"
]
}
}
},
"Interval": {
"description": "Represents a time interval, encoded as a Timestamp start (inclusive) and a Timestamp end (exclusive). The start must be less than or equal to the end. When the start equals the end, the interval is empty (matches no time). When both start and end are unspecified, the interval matches any time.",
"type": "object",
"id": "Interval",
"properties": {
"endTime": {
"type": "string",
"format": "google-datetime",
"description": "Optional. Exclusive end of the interval. If specified, a Timestamp matching this interval will have to be before the end."
},
"startTime": {
"format": "google-datetime",
"type": "string",
"description": "Optional. Inclusive start of the interval. If specified, a Timestamp matching this interval will have to be the same or after the start."
}
}
},
"ListVersionsResponse": {
"description": "Response message for ListVersions.",
"type": "object",
"properties": {
"versions": {
"type": "array",
"description": "The list of versions.",
"items": {
"$ref": "Version"
}
},
"nextPageToken": {
"description": "A token, which can be sent as `page_token` to retrieve the next page. If this field is omitted, there are no subsequent pages.",
"type": "string"
}
},
"id": "ListVersionsResponse"
},
"ListChannelsResponse": {
"description": "Response message for ListChannels.",
"id": "ListChannelsResponse",
"type": "object",
"properties": {
"nextPageToken": {
"description": "A token, which can be sent as `page_token` to retrieve the next page. If this field is omitted, there are no subsequent pages.",
"type": "string"
},
"channels": {
"description": "The list of channels.",
"type": "array",
"items": {
"$ref": "Channel"
}
}
}
},
"Platform": {
"properties": {
"platformType": {
"enum": [
"PLATFORM_TYPE_UNSPECIFIED",
"WIN",
"WIN64",
"MAC",
"LINUX",
"ANDROID",
"WEBVIEW",
"IOS",
"ALL",
"MAC_ARM64",
"LACROS"
],
"description": "Type of platform.",
"enumDescriptions": [
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
""
],
"type": "string"
},
"name": {
"description": "Platform name. Format is \"{product}/platforms/{platform}\"",
"type": "string"
}
},
"id": "Platform",
"type": "object",
"description": "Each Platform is owned by a Product and owns a collection of channels. Available platforms are listed in Platform enum below. Not all Channels are available for every Platform (e.g. CANARY does not exist for LINUX)."
},
"Version": {
"properties": {
"version": {
"description": "String containing just the version number. e.g. \"84.0.4147.38\"",
"type": "string"
},
"name": {
"description": "Version name. Format is \"{product}/platforms/{platform}/channels/{channel}/versions/{version}\" e.g. \"chrome/platforms/win/channels/beta/versions/84.0.4147.38\"",
"type": "string"
}
},
"id": "Version",
"type": "object",
"description": "Each Version is owned by a Channel. A Version only displays the Version number (e.g. 84.0.4147.38). A Version owns a collection of releases."
},
"ListPlatformsResponse": {
"description": "Response message for ListPlatforms.",
"id": "ListPlatformsResponse",
"type": "object",
"properties": {
"nextPageToken": {
"type": "string",
"description": "A token, which can be sent as `page_token` to retrieve the next page. If this field is omitted, there are no subsequent pages."
},
"platforms": {
"type": "array",
"items": {
"$ref": "Platform"
},
"description": "The list of platforms."
}
}
},
"ListReleasesResponse": {
"type": "object",
"id": "ListReleasesResponse",
"description": "Response message for ListReleases.",
"properties": {
"nextPageToken": {
"description": "A token, which can be sent as `page_token` to retrieve the next page. If this field is omitted, there are no subsequent pages.",
"type": "string"
},
"releases": {
"description": "The list of releases.",
"items": {
"$ref": "Release"
},
"type": "array"
}
}
},
"Release": {
"properties": {
"fraction": {
"format": "double",
"type": "number",
"description": "Rollout fraction. This fraction indicates the fraction of people that should receive this version in this release. If the fraction is not specified in ReleaseManager, the API will assume fraction is 1."
},
"version": {
"type": "string",
"description": "String containing just the version number. e.g. \"84.0.4147.38\""
},
"name": {
"type": "string",
"description": "Release name. Format is \"{product}/platforms/{platform}/channels/{channel}/versions/{version}/releases/{release}\""
},
"serving": {
"description": "Timestamp interval of when the release was live. If end_time is unspecified, the release is currently live.",
"$ref": "Interval"
}
},
"type": "object",
"description": "A Release is owned by a Version. A Release contains information about the release(s) of its parent version. This includes when the release began and ended, as well as what percentage it was released at. If the version is released again, or if the serving percentage changes, it will create another release under the version.",
"id": "Release"
}
},
"fullyEncodeReservedExpansion": true,
"documentationLink": "https://developers.chrome.com/",
"icons": {
"x16": "http://www.google.com/images/icons/product/search-16.gif",
"x32": "http://www.google.com/images/icons/product/search-32.gif"
},
"baseUrl": "https://versionhistory.googleapis.com/",
"batchPath": "batch",
"version": "v1",
"canonicalName": "Version History",
"id": "versionhistory:v1",
"rootUrl": "https://versionhistory.googleapis.com/"
}