mirror of
https://github.com/GAM-team/GAM.git
synced 2026-06-04 22:31:38 +00:00
Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41a6c11c55 | ||
|
|
57d908e369 | ||
|
|
64274fdb33 | ||
|
|
da919fd189 | ||
|
|
cfa25f12d3 | ||
|
|
05bc1c1263 | ||
|
|
939c79c37f | ||
|
|
d352ddeea1 | ||
|
|
72a683f2b1 | ||
|
|
784399f345 | ||
|
|
710be4371b | ||
|
|
eece358aec | ||
|
|
b43ada4f83 | ||
|
|
9030af4faf | ||
|
|
38b424b62e | ||
|
|
1d9bf0b1aa | ||
|
|
d3b7700c07 | ||
|
|
d9513e159f | ||
|
|
6ddfdf2514 | ||
|
|
478804bd5c | ||
|
|
b61165a753 | ||
|
|
b3814ae7be | ||
|
|
019c363a74 | ||
|
|
da5f80e704 | ||
|
|
b37b10e669 | ||
|
|
8ca92eda39 | ||
|
|
81dbbc36db | ||
|
|
7065101b87 | ||
|
|
00c302e545 | ||
|
|
703530ce7f | ||
|
|
7ac15042d8 | ||
|
|
a80ec52027 | ||
|
|
4da4132220 | ||
|
|
8682e66eb0 | ||
|
|
34bf205d37 | ||
|
|
d6c2c6a2c3 | ||
|
|
f45639e6e2 | ||
|
|
82968e29bf | ||
|
|
5d3d571545 | ||
|
|
6999c13877 | ||
|
|
82a551e88f | ||
|
|
1b1a0c876c | ||
|
|
b262c4a898 | ||
|
|
22d1055d82 | ||
|
|
fe38565a9a | ||
|
|
a25d14e83f | ||
|
|
15b21dd8d7 | ||
|
|
caedcde49b | ||
|
|
8091e23e00 | ||
|
|
08e1090b15 | ||
|
|
f76b5cb2eb | ||
|
|
edc4311dcb | ||
|
|
a613bff664 | ||
|
|
8f875d2a9c | ||
|
|
fb60e0b389 |
2
.github/actions/macos-before-install.sh
vendored
2
.github/actions/macos-before-install.sh
vendored
@@ -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
|
||||
|
||||
6
.github/actions/macos-install.sh
vendored
6
.github/actions/macos-install.sh
vendored
@@ -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
|
||||
|
||||
30
.github/workflows/build.yml
vendored
30
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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'})
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
229
src/gam/gapi/chromehistory.py
Normal file
229
src/gam/gapi/chromehistory.py
Normal 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)
|
||||
265
src/gam/gapi/chromemanagement.py
Normal file
265
src/gam/gapi/chromemanagement.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
486
src/versionhistory-v1.json
Normal 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/"
|
||||
}
|
||||
Reference in New Issue
Block a user