mirror of
https://github.com/GAM-team/GAM.git
synced 2026-06-04 14:21:39 +00:00
Compare commits
112 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
2199fb2828 | ||
|
|
b7d052a6b3 | ||
|
|
b333816dc8 | ||
|
|
90160da042 | ||
|
|
6f2ebf8d2d | ||
|
|
a65635365e | ||
|
|
0eee6979b0 | ||
|
|
ec796e9f84 | ||
|
|
aaed2a6d86 | ||
|
|
0ea7c500e1 | ||
|
|
d90c884cf2 | ||
|
|
93700c01a8 | ||
|
|
1df5662d4f | ||
|
|
338eeba944 | ||
|
|
9651e4abb1 | ||
|
|
ed1f3400ac | ||
|
|
e9d9353fbb | ||
|
|
00adf4ca46 | ||
|
|
870fc27c72 | ||
|
|
bd38b7479f | ||
|
|
a567599eae | ||
|
|
5e6f9353c2 | ||
|
|
7de1179b7e | ||
|
|
ea7c80c3a1 | ||
|
|
f252f757f1 | ||
|
|
b27c63d0d7 | ||
|
|
bcce1a4472 | ||
|
|
9d9655512d | ||
|
|
7af75f31e4 | ||
|
|
83f02c377f | ||
|
|
ce4f74bc61 | ||
|
|
66651d0eed | ||
|
|
ec0e143361 | ||
|
|
250e0188f7 | ||
|
|
3123e472fc | ||
|
|
c12f7f1123 | ||
|
|
7e706518c5 | ||
|
|
d8ca573983 | ||
|
|
2225625cd8 | ||
|
|
89f0f01fd2 | ||
|
|
a36282d114 | ||
|
|
a8c92b7f9a | ||
|
|
f505dac8f3 | ||
|
|
8e4730a3bd | ||
|
|
b094bb344b | ||
|
|
2685aa049d | ||
|
|
b738d57433 | ||
|
|
539b870754 | ||
|
|
abeb0998ea | ||
|
|
82faddd985 | ||
|
|
b8084c270e | ||
|
|
22c7da420c | ||
|
|
45a3c89b0b | ||
|
|
8fc9e6d1ee | ||
|
|
7f0b286d8e | ||
|
|
4f664df087 | ||
|
|
dff48e3146 | ||
|
|
0fefa19f80 | ||
|
|
88e07ddbaa | ||
|
|
44a3ef0d70 | ||
|
|
5e793f171f | ||
|
|
e9bc63bee8 | ||
|
|
5636876e42 | ||
|
|
f2f7f549b0 | ||
|
|
1fc6e4f781 | ||
|
|
d641458fb4 | ||
|
|
517d44fa3c | ||
|
|
80ee0bf9a8 | ||
|
|
0934b70414 | ||
|
|
f74168e2c7 | ||
|
|
bf4a6e6cde | ||
|
|
0e09675779 | ||
|
|
40e92ca3d2 | ||
|
|
e776919bfd | ||
|
|
84bfeffe46 | ||
|
|
1360abbecb | ||
|
|
2a13accfe4 | ||
|
|
e26dac3993 |
BIN
.github/actions/creds.tar.gpg
vendored
BIN
.github/actions/creds.tar.gpg
vendored
Binary file not shown.
5
.github/actions/linux-before-install.sh
vendored
5
.github/actions/linux-before-install.sh
vendored
@@ -1,3 +1,6 @@
|
||||
echo "RUNNING: apt update..."
|
||||
sudo apt-get -qq --yes update > /dev/null
|
||||
sudo apt-get -qq --yes install swig libpcsclite-dev
|
||||
if [[ "$TRAVIS_JOB_NAME" == *"Testing" ]]; then
|
||||
export python="python"
|
||||
export pip="pip"
|
||||
@@ -32,8 +35,6 @@ else
|
||||
rm -rf python
|
||||
mkdir ssl
|
||||
mkdir python
|
||||
echo "RUNNING: apt update..."
|
||||
sudo apt-get -qq --yes update > /dev/null
|
||||
echo "RUNNING: apt upgrade..."
|
||||
sudo apt-mark hold openssh-server
|
||||
sudo apt-get --yes upgrade
|
||||
|
||||
3
.github/actions/macos-before-install.sh
vendored
3
.github/actions/macos-before-install.sh
vendored
@@ -63,6 +63,9 @@ SSLRESULT=$?
|
||||
PYVER=$($python -V)
|
||||
PYRESULT=$?
|
||||
|
||||
brew install swig
|
||||
$pip install pyscard
|
||||
|
||||
#wget --quiet https://www.python.org/ftp/python/$BUILD_PYTHON_VERSION/python-$BUILD_PYTHON_VERSION-macosx10.9.pkg
|
||||
|
||||
#if [ $SSLRESULT -ne 0 ] || [[ "$SSLVER" != "OpenSSL $BUILD_OPENSSL_VERSION "* ]] || [ $PYRESULT -ne 0 ] || [[ "$PYVER" != "Python $BUILD_PYTHON_VERSION"* ]]; then
|
||||
|
||||
4
.github/actions/windows-before-install.sh
vendored
4
.github/actions/windows-before-install.sh
vendored
@@ -16,6 +16,10 @@ cd ~
|
||||
export python="python"
|
||||
export pip="pip"
|
||||
|
||||
# pyscard needs swig, keep these two together
|
||||
choco install $CHOCOPTIONS swig
|
||||
$pip install pyscard
|
||||
|
||||
# Python
|
||||
#echo "Installing Python..."
|
||||
#export python_file=python-${BUILD_PYTHON_VERSION}${PYTHONFILE_BITS}.exe
|
||||
|
||||
52
.github/workflows/build.yml
vendored
52
.github/workflows/build.yml
vendored
@@ -12,13 +12,13 @@ defaults:
|
||||
working-directory: src
|
||||
|
||||
env:
|
||||
BUILD_PYTHON_VERSION: "3.9.1"
|
||||
MIN_PYTHON_VERSION: "3.9.1"
|
||||
BUILD_OPENSSL_VERSION: "1.1.1i"
|
||||
MIN_OPENSSL_VERSION: "1.1.1g"
|
||||
BUILD_PYTHON_VERSION: "3.9.4"
|
||||
MIN_PYTHON_VERSION: "3.9.4"
|
||||
BUILD_OPENSSL_VERSION: "1.1.1k"
|
||||
MIN_OPENSSL_VERSION: "1.1.1i"
|
||||
PATCHELF_VERSION: "0.12"
|
||||
#PYINSTALLER_COMMIT: "61d846d46bdc8b6d926bb57ae05e6c9bb884a144"
|
||||
PYINSTALLER_VERSION: "4.2"
|
||||
# PYINSTALLER_VERSION can be full commit hash or version like v4.20
|
||||
PYINSTALLER_VERSION: "227eac14955c02db21d4702429896d4b74beed5e"
|
||||
|
||||
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.1
|
||||
python: 3.9.4
|
||||
pyarch: "x64"
|
||||
platform: "x86_64"
|
||||
- os: windows-2019
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
goal: "build"
|
||||
gamos: "windows"
|
||||
platform: "x86"
|
||||
python: 3.9.1
|
||||
python: 3.9.4
|
||||
pyarch: "x86"
|
||||
- os: ubuntu-20.04
|
||||
goal: "test"
|
||||
@@ -108,7 +108,7 @@ jobs:
|
||||
path: |
|
||||
~/python
|
||||
~/ssl
|
||||
key: ${{ matrix.os }}-${{ matrix.jid }}-20210103
|
||||
key: ${{ matrix.os }}-${{ matrix.jid }}-20210407
|
||||
|
||||
- name: Set env variables
|
||||
env:
|
||||
@@ -142,6 +142,9 @@ jobs:
|
||||
echo "pip=${pip}" >> $GITHUB_ENV
|
||||
echo "gam=${gam}" >> $GITHUB_ENV
|
||||
echo "gampath=${gampath}" >> $GITHUB_ENV
|
||||
echo "RUNNING: apt update..."
|
||||
sudo apt-get -qq --yes update > /dev/null
|
||||
sudo apt-get -qq --yes install swig libpcsclite-dev
|
||||
|
||||
- name: Build and install Python, OpenSSL and PyInstaller
|
||||
if: matrix.goal != 'test' && steps.cache-primes.outputs.cache-hit != 'true'
|
||||
@@ -153,7 +156,7 @@ jobs:
|
||||
echo "pip=$pip" >> $GITHUB_ENV
|
||||
echo "LD_LIBRARY_PATH=$LD_LIBRARY_PATH" >> $GITHUB_ENV
|
||||
echo -e "Python: $python\nPip: $pip\nLD_LIB...: $LD_LIBRARY_PATH"
|
||||
export url="https://codeload.github.com/pyinstaller/pyinstaller/tar.gz/v${PYINSTALLER_VERSION}"
|
||||
export url="https://codeload.github.com/pyinstaller/pyinstaller/tar.gz/${PYINSTALLER_VERSION}"
|
||||
echo "Downloading ${url}"
|
||||
curl -o pyinstaller.tar.gz --compressed "${url}"
|
||||
tar xf pyinstaller.tar.gz
|
||||
@@ -173,6 +176,7 @@ jobs:
|
||||
run: |
|
||||
set +e
|
||||
$pip list --outdated --format=freeze | grep -v '^\-e' | cut -d = -f 1 | xargs -n1 $pip install -U --force-reinstall
|
||||
|
||||
$pip install --upgrade -r requirements.txt
|
||||
|
||||
- name: Build GAM with PyInstaller
|
||||
@@ -248,9 +252,10 @@ jobs:
|
||||
$gam csv sample.csv gam update group $newgroup add member ~email
|
||||
$gam info group $newgroup
|
||||
$gam user $gam_user check serviceaccount
|
||||
# confirm mailbox is provisoned before continuing
|
||||
$gam user $newuser waitformailbox
|
||||
$gam user $newuser imap on
|
||||
$gam user $newuser show imap
|
||||
$gam user $newuser delegate to "${newbase}-bulkuser-01"
|
||||
$gam user $newuser show delegates
|
||||
#$gam user $newuser add contactdelegate "${newbase}-bulkuser-01"
|
||||
#$gam user $newuser print contactdelegates
|
||||
@@ -262,10 +267,11 @@ jobs:
|
||||
$gam user $gam_user insertemail subject "GHA insert $newbase" file gam.py labels INBOX,UNREAD # yep body is gam code
|
||||
$gam user $gam_user sendemail subject "GHA send $gam_user $newbase" file gam.py recipient admin@pdl.jaylee.us
|
||||
$gam user $gam_user draftemail subject "GHA draft $newbase" message "Draft message test"
|
||||
$gam csvfile sample.csv:email waitformailbox
|
||||
$gam user $newuser delegate to "${newbase}-bulkuser-01"
|
||||
$gam users "$gam_user $newbase-bulkuser-01 $newbase-bulkuser-02 $newbase-bulkuser-03" delete messages query in:anywhere maxtodelete 99999 doit
|
||||
$gam users "$newbase-bulkuser-04 $newbase-bulkuser-05 $newbase-bulkuser-06" trash messages query in:anywhere maxtotrash 99999 doit
|
||||
# disabling as we see a lot of errors here
|
||||
# $gam users "$newbase-bulkuser-07 $newbase-bulkuser-08 $newbase-bulkuser-09" modify messages query in:anywhere maxtomodify 99999 addlabel IMPORTANT addlabel STARRED doit
|
||||
$gam users "$newbase-bulkuser-07 $newbase-bulkuser-08 $newbase-bulkuser-09" modify messages query in:anywhere maxtomodify 99999 addlabel IMPORTANT addlabel STARRED doit
|
||||
$gam user $newuser delete label --ALL_LABELS--
|
||||
$gam create feature name Whiteboard-$newbase
|
||||
$gam create feature name VC-$newbase
|
||||
@@ -315,6 +321,16 @@ jobs:
|
||||
$gam report users fields accounts:is_less_secure_apps_access_allowed,gmail:last_imap_time,gmail:last_pop_time filters "accounts:last_login_time>2019-01-01T00:00:00.000Z" todrive
|
||||
$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
|
||||
export CUSTOMER_ID="C01wfv983"
|
||||
export GA_DOMAIN="pdl.jaylee.us"
|
||||
touch $gampath/enabledasa.txt
|
||||
echo "printer model count:"
|
||||
$gam print printermodels | wc -l
|
||||
#$gam print printers
|
||||
#$gam create printer displayname "${newbase}" uri ipp://localhost:631 driverless description "made by $(date)"
|
||||
rm $gampath/enabledasa.txt
|
||||
|
||||
- name: Upload to Google Drive, build only.
|
||||
if: github.event_name == 'push' && matrix.goal != 'test'
|
||||
|
||||
@@ -14,6 +14,8 @@ Download the MSI Installer from the [GitHub Releases] page. Install the MSI and
|
||||
The GAM documentation is hosted in the [GitHub Wiki]
|
||||
# Mailing List / Discussion group
|
||||
The GAM mailing list / discussion group is hosted on [Google Groups]. You can join the list and interact via email, or just post from the web itself.
|
||||
# Chat Room
|
||||
There is a public chat room hosted in Google Chat. [Instructions to join](https://git.io/gam-chat).
|
||||
# Author
|
||||
GAM is maintained by <a href="mailto:jay0lee@gmail.com">Jay Lee</a>. Please direct "how do I?" questions to [Google Groups].
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ If an item contains spaces, it should be surrounded by ".
|
||||
Google-Chrome-Device-Management|
|
||||
Google-Drive-storage|
|
||||
Google-Vault|
|
||||
101001|101005|101031|101033|101034
|
||||
101001|101005|101031|101033|101034|101037
|
||||
<SKUID> ::=
|
||||
cloudidentity|identity|1010010001|
|
||||
cloudidentitypremium|identitypremium|1010050001|
|
||||
@@ -85,6 +85,13 @@ If an item contains spaces, it should be surrounded by ".
|
||||
gams|postini|gsuitegams|gsuitepostini|gsuitemessagesecurity|Google-Apps-For-Postini|
|
||||
gal|gsl|lite|gsuitelite|Google-Apps-Lite|
|
||||
gau|gsb|unlimited|gsuitebusiness|Google-Apps-Unlimited|
|
||||
gwep|workspaceeducationplus|1010310008|
|
||||
gwepstaff|workspaceeducationplusstaff|1010310009|
|
||||
gwepstudent|workspaceeducationplusstudent|1010310010|
|
||||
gwes|workspaceeducationstandard|1010310005|
|
||||
gwesstaff|workspaceeducationstandardstaff|1010310006|
|
||||
gwesstudent|workspaceeducationstandardstudent|1010310007|
|
||||
gwetlu|workspaceeducationupgrade|1010370001|
|
||||
wsentplus|workspaceenterpriseplus|gae|gse|enterprise|gsuiteenterprise|1010020020|
|
||||
wsbizplus|workspacebusinessplus|1010020025|
|
||||
wsentstan|workspaceenterprisestandard|'1010020026|
|
||||
@@ -107,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
|
||||
|
||||
@@ -140,6 +148,7 @@ If an item contains spaces, it should be surrounded by ".
|
||||
<AccessToken> ::= <String>
|
||||
<ACLScope> ::= [user:]<EmailAddress>|group:<EmailAddress>|domain[:<DomainName>]|default
|
||||
<APIScopeURL> ::= <String>
|
||||
<APPID> ::= <String>
|
||||
<ASPID> ::= <String>
|
||||
<AssetTag> ::= <String>
|
||||
<BrowserTokenPermanentID> ::= <String>
|
||||
@@ -202,6 +211,7 @@ If an item contains spaces, it should be surrounded by ".
|
||||
<ParameterValue> ::= <String>
|
||||
<Password> ::= <String>
|
||||
<PermissionID> ::= id:<String>|<EmailAddress>|anyone|anyonewithlink
|
||||
<PrinterID> ::= <String>
|
||||
<PropertyKey> ::= <String>
|
||||
<PropertyValue> ::= <String>
|
||||
<QueryBrowser> ::= <String> See: https://support.google.com/chrome/a/answer/9681204#retrieve_all_chrome_devices_for_an_account
|
||||
@@ -213,7 +223,6 @@ If an item contains spaces, it should be surrounded by ".
|
||||
<QueryGmail> ::= <String> See: https://support.google.com/mail/answer/7190
|
||||
<QueryGroup> ::= <String> See: https://developers.google.com/admin-sdk/directory/v1/guides/search-groups
|
||||
<QueryMobile> ::= <String> See: https://support.google.com/a/answer/7549103
|
||||
<QueryPrintJob> ::= <String> See: https://developers.google.com/cloud-print/docs/appInterfaces#parameters_3
|
||||
<QueryUser> ::= <String> See: https://developers.google.com/admin-sdk/directory/v1/guides/search-users
|
||||
<QueryVaultCorpus> ::= <String> See: https://developers.google.com/vault/reference/rest/v1/matters.holds#CorpusQuery
|
||||
<RequestID> ::= <String>
|
||||
@@ -482,9 +491,6 @@ If an item contains spaces, it should be surrounded by ".
|
||||
description|id|inherit|name|orgunitpath|parent|parentid|inherit
|
||||
<OrgUnitFieldNameList> ::= "<OrgUnitFieldName>(,<OrgUnitFieldName>)*"
|
||||
|
||||
<PrintJobOrderByFieldName> ::=
|
||||
create_time|status|title
|
||||
|
||||
<ResourceFieldName> ::=
|
||||
buildingid|
|
||||
capacity|
|
||||
@@ -585,8 +591,8 @@ Items, separated by spaces, with spaces, commas or single quotes in the items th
|
||||
<MembersFieldNameList> ::= "<MembersFieldName>(,<MembersFieldName>)*"
|
||||
<MobileList> ::= "<MobileId>(,<MobileId>)*"
|
||||
<OrgUnitList> ::= "<OrgUnitPath>(,<OrgUnitPath>)*"
|
||||
<PrinterIDList> ::= "<PrinterID>)(,<PrinterID>)*"
|
||||
<ProductIDList> ::= "(<ProductID>|SKUID>)(,<ProductID>|SKUID>)*"
|
||||
<PrintJobIDList> ::= "<PrintJobID>(,<PrintJobID>)*"
|
||||
<QueryCrOSList> ::= "<QueryCrOS>(,<QueryCrOS>)*"
|
||||
<QueryMobileList> ::= "<QueryMobile>(,<QueryMobile>)*"
|
||||
<QueryUserList> ::= "<QueryUser>(,<QueryUser>)*"
|
||||
@@ -686,7 +692,7 @@ Specify a collection of Users by directly specifying them or by specifiying item
|
||||
(contentrestrictions readonly true [reason <String>])|
|
||||
copyrequireswriterpermission|
|
||||
(lastviewedbyme <Time>)|(modifieddate|modifiedtime <Time>)|(description <String>)|(mimetype <MimeType>)|
|
||||
(parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|writerscantshare|
|
||||
(parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|writerscantshare|writerscanshare
|
||||
(shortcut <DriveFileID>)
|
||||
<DriveFileUpdateAttribute> ::=
|
||||
(localfile <FileName>)|
|
||||
@@ -696,7 +702,7 @@ Specify a collection of Users by directly specifying them or by specifiying item
|
||||
(contentrestrictions readonly true [reason <String>])|
|
||||
(copyrequireswriterpermission <Boolean>)|
|
||||
(lastviewedbyme <Time>)|(modifieddate <Time>)|(description <String>)|(mimetype <MimeType>)|
|
||||
(parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|writerscantshare|
|
||||
(parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|writerscantshare|writerscanshare
|
||||
(shortcut <DriveFileID>)
|
||||
<GroupSettingsAttribute> ::=
|
||||
(allowexternalmembers <Boolean>)|
|
||||
@@ -984,6 +990,8 @@ gam info customer
|
||||
|
||||
<DataTransferService> ::=
|
||||
calendar|
|
||||
currents|
|
||||
datastudio|"google data studio"|
|
||||
googledrive|gdrive|drive|"drive and docs"
|
||||
<DataTransferServiceList> ::= "<DataTransferService>(,<DataTransferService>)*"
|
||||
|
||||
@@ -1000,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)]
|
||||
@@ -1127,26 +1135,26 @@ gam info browser <DeviceID>
|
||||
[fields <BrowserFieldNameList>]
|
||||
|
||||
gam print browsers [todrive]
|
||||
[query <QueryBrowser>]
|
||||
[ou|org|orgunit <OrgUnitPath>] [query <QueryBrowser>]
|
||||
[projection basic|full]
|
||||
[fields <BrowserFieldNameList>]
|
||||
[sortheaders]
|
||||
|
||||
gam create browsertoken
|
||||
[ou|org|orgunit <OrgUnitPath>] [expire|expires <Time>]
|
||||
[ou|org|orgunit <OrgUnitPath>] [expire|expires <Time>]
|
||||
gam revoke browsertoken <BrowserTokenPermanentID>
|
||||
|
||||
<BrowserTokenFieldName> ::=
|
||||
createTime|
|
||||
creatorId|
|
||||
customerId|
|
||||
expireTime|
|
||||
orgUnitPath|
|
||||
revokeTime|
|
||||
revokerId|
|
||||
state|
|
||||
token|
|
||||
tokenPermanentId
|
||||
createTime|
|
||||
creatorId|
|
||||
customerId|
|
||||
expireTime|
|
||||
orgUnitPath|
|
||||
revokeTime|
|
||||
revokerId|
|
||||
state|
|
||||
token|
|
||||
tokenPermanentId
|
||||
<BrowserTokenFieldNameList> ::= "<BrowseTokenFieldName>(,<BrowserTokenFieldName>)*"
|
||||
|
||||
gam show browsertokens
|
||||
@@ -1226,6 +1234,69 @@ 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>)]
|
||||
gam show chromeschema [filter <String>]
|
||||
|
||||
<DeviceID> ::= devices/<String>
|
||||
<DeviceType> ::= android|chrome_os|google_sync|ios|linux|mac_os|windows
|
||||
<DeviceUserID> ::= devices/<String>/deviceUsers/<String>
|
||||
@@ -1268,6 +1339,23 @@ gam info mobile <MobileID>
|
||||
gam print mobile [todrive] [(query <QueryMobile>)|(queries <QueryMobileList>)] [basic|full] [orderby <MobileOrderByFieldName> [ascending|descending]]
|
||||
fields <MobileFieldNameList>] [delimiter <Character>] [appslimit <Number>] [listlimit <Number>]
|
||||
|
||||
<PrinterAttribute> ::=
|
||||
(description <String>)|
|
||||
(displayname <String>)|
|
||||
(makeandmodel <String>)|
|
||||
(ou|org|orgunit|orgunitid <OrgUnitItem>)|
|
||||
(ownerid <EmailAddress>)|
|
||||
(uri <String>)|
|
||||
(driverless|usedriverlessconfig)
|
||||
|
||||
gam create printer <PrinterAttribute>+
|
||||
gam update printer <PrinterID> <PrinterAttribute>+
|
||||
gam delete printer <PrinterIDList>|(file <FileName>)|(csvfile <FileName>:<FieldName>)
|
||||
|
||||
gam info printer <PrinterID>
|
||||
gam print printers [todrive] [filter <String>]
|
||||
gam print printermodels [todrive] [filter <String>]
|
||||
|
||||
gam create cigroup <EmailAddress> <CIGroupAttribute>*
|
||||
[makeowner] [alias|aliases <AliasList>] [dynamic <QueryDynamicGroup>]
|
||||
gam update cigroup <GroupItem> [email <EmailAddress>] <CIGroupAttribute>* [security]
|
||||
@@ -1289,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>
|
||||
@@ -1310,6 +1398,15 @@ gam print group-members|groups-members [todrive]
|
||||
[roles <GroupRoleList>] [membernames] [fields <MembersFieldNameList>]
|
||||
[includederivedmembership]
|
||||
|
||||
gam send userinvitation <EmailAddress>
|
||||
gam cancel userinvitation <EmailAddress>
|
||||
gam check userinvitation|isinvitable <EmailAddress>
|
||||
gam info userinvitation <EmailAddress>
|
||||
gam print userinvitations [todrive]
|
||||
[state notyetsent|invited|accepted|declined]]
|
||||
[orderby email|updatetime [ascending|descending]]
|
||||
gam <UserTypeEntity> check isinvitable [todrive]
|
||||
|
||||
gam print licenses [todrive] [(products|product <ProductIDList>)|(skus|sku <SKUIDList>)|allskus|gsuite] [countsonly]
|
||||
gam show license|licenses|licence|licences [(products|product <ProductIDList>)|(skus|sku <SKUIDList>)|allskus|gsuite]
|
||||
|
||||
@@ -1337,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>]
|
||||
|
||||
@@ -140,9 +140,13 @@ case $gamos in
|
||||
echo_red "Sorry, you need to be running at least MacOS $gam_macos_ver to run GAM"
|
||||
exit
|
||||
fi
|
||||
#gamfile="macos-x86_64-$use_macos_ver.tar.xz"
|
||||
gamfile="macos-x86_64.tar.xz"
|
||||
;;
|
||||
MINGW64_NT*)
|
||||
gamos="windows"
|
||||
echo "You are running Windows"
|
||||
gamfile="-windows-x86_64.zip"
|
||||
;;
|
||||
*)
|
||||
echo_red "Sorry, this installer currently only supports Linux and MacOS. Looks like you're runnning on $gamos. Exiting."
|
||||
exit
|
||||
@@ -155,8 +159,14 @@ else
|
||||
release_url="https://api.github.com/repos/jay0lee/GAM/releases/tags/v$gamversion"
|
||||
fi
|
||||
|
||||
echo_yellow "Checking GitHub URL $release_url for $gamversion GAM release..."
|
||||
release_json=$(curl -s $release_url 2>&1 /dev/null)
|
||||
if [ -z ${GHCLIENT+x} ]; then
|
||||
check_type="unauthenticated"
|
||||
else
|
||||
check_type="authenticated"
|
||||
fi
|
||||
|
||||
echo_yellow "Checking GitHub URL $release_url for $gamversion GAM release ($check_type)..."
|
||||
release_json=$(curl -s $GHCLIENT $release_url 2>&1 /dev/null)
|
||||
|
||||
echo_yellow "Getting file and download URL..."
|
||||
# Python is sadly the nearest to universal way to safely handle JSON with Bash
|
||||
@@ -223,14 +233,18 @@ temp_archive_dir=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir')
|
||||
# Clean up after ourselves even if we are killed with CTRL-C
|
||||
trap "rm -rf $temp_archive_dir" EXIT
|
||||
|
||||
echo_yellow "Downloading file $name from $browser_download_url to $temp_archive_dir."
|
||||
echo_yellow "Downloading file $name from $browser_download_url to $temp_archive_dir ($check_type)..."
|
||||
# Save archive to temp w/o losing our path
|
||||
(cd $temp_archive_dir && curl -O -L $browser_download_url)
|
||||
(cd $temp_archive_dir && curl -O -L $GHCLIENT $browser_download_url)
|
||||
|
||||
mkdir -p "$target_dir"
|
||||
|
||||
echo_yellow "Extracting archive to $target_dir"
|
||||
tar xf $temp_archive_dir/$name -C "$target_dir"
|
||||
if [[ "${name}" == *.tar.xz ]]; then
|
||||
tar xf $temp_archive_dir/$name -C "$target_dir"
|
||||
else
|
||||
unzip "${temp_archive_dir}/${name}" -d "${target_dir}"
|
||||
fi
|
||||
rc=$?
|
||||
if (( $rc != 0 )); then
|
||||
echo_red "ERROR: extracting the GAM archive with tar failed with error $rc. Exiting."
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -27,6 +27,7 @@ import webbrowser
|
||||
import zipfile
|
||||
import http.client as http_client
|
||||
from multiprocessing import Pool as mp_pool
|
||||
from multiprocessing import Lock as mp_lock
|
||||
from urllib.parse import quote, urlencode, urlparse
|
||||
import dateutil.parser
|
||||
|
||||
@@ -51,8 +52,12 @@ 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
|
||||
from gam.gapi.cloudidentity import userinvitations as gapi_cloudidentity_userinvitations
|
||||
from gam.gapi import contactdelegation as gapi_contactdelegation
|
||||
from gam.gapi.directory import asps as gapi_directory_asps
|
||||
from gam.gapi.directory import cros as gapi_directory_cros
|
||||
@@ -62,6 +67,7 @@ from gam.gapi.directory import domains as gapi_directory_domains
|
||||
from gam.gapi.directory import groups as gapi_directory_groups
|
||||
from gam.gapi.directory import mobiledevices as gapi_directory_mobiledevices
|
||||
from gam.gapi.directory import orgunits as gapi_directory_orgunits
|
||||
from gam.gapi.directory import printers as gapi_directory_printers
|
||||
from gam.gapi.directory import privileges as gapi_directory_privileges
|
||||
from gam.gapi.directory import resource as gapi_directory_resource
|
||||
from gam.gapi.directory import roles as gapi_directory_roles
|
||||
@@ -77,11 +83,8 @@ from gam import transport
|
||||
from gam import utils
|
||||
from gam.var import *
|
||||
|
||||
if platform.system() == 'Windows':
|
||||
# No crypt module on Win, use passlib
|
||||
from passlib.hash import sha512_crypt
|
||||
else:
|
||||
from crypt import crypt
|
||||
yubikey = utils.LazyLoader('yubikey', globals(), 'gam.auth.yubikey')
|
||||
from passlib.hash import sha512_crypt
|
||||
|
||||
if platform.system() == 'Linux':
|
||||
import distro
|
||||
@@ -107,7 +110,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
|
||||
|
||||
@@ -133,11 +136,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'
|
||||
)
|
||||
|
||||
|
||||
@@ -822,8 +825,14 @@ def _getSvcAcctData():
|
||||
def getSvcAcctCredentials(scopes, act_as):
|
||||
try:
|
||||
_getSvcAcctData()
|
||||
credentials = google.oauth2.service_account.Credentials.from_service_account_info(
|
||||
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
|
||||
sign_method = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA].get('key_type', 'default')
|
||||
if sign_method == 'default':
|
||||
credentials = google.oauth2.service_account.Credentials.from_service_account_info(
|
||||
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
|
||||
elif sign_method == 'yubikey':
|
||||
yksigner = yubikey.YubiKey(GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
|
||||
credentials = google.oauth2.service_account.Credentials._from_signer_and_info(yksigner,
|
||||
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
|
||||
credentials = credentials.with_scopes(scopes)
|
||||
if act_as:
|
||||
credentials = credentials.with_subject(act_as)
|
||||
@@ -885,14 +894,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
|
||||
@@ -904,30 +913,33 @@ def getService(api, http):
|
||||
service = googleapiclient.discovery.build(
|
||||
api,
|
||||
version,
|
||||
http=http,
|
||||
http=httpObj,
|
||||
cache_discovery=False,
|
||||
static_discovery=False,
|
||||
discoveryServiceUrl=discoveryServiceUrl)
|
||||
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 (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, socket.error,
|
||||
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
|
||||
@@ -937,12 +949,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)
|
||||
@@ -952,9 +964,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(
|
||||
@@ -986,6 +998,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):
|
||||
@@ -1069,23 +1087,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)
|
||||
@@ -1256,7 +1274,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.
|
||||
@@ -1415,9 +1433,7 @@ def addDelegates(users, i):
|
||||
|
||||
|
||||
def gen_sha512_hash(password):
|
||||
if platform.system() == 'Windows':
|
||||
return sha512_crypt.hash(password, rounds=5000)
|
||||
return crypt(password)
|
||||
return sha512_crypt.hash(password, rounds=5000)
|
||||
|
||||
|
||||
def printShowDelegates(users, csvFormat):
|
||||
@@ -1733,7 +1749,7 @@ def doPrintAdmins():
|
||||
value = f'id:{value}'
|
||||
admin_attrib[
|
||||
'orgUnit'] = gapi_directory_orgunits.orgunit_from_orgunitid(
|
||||
value)
|
||||
value, cd)
|
||||
admin_attrib[key] = value
|
||||
csvRows.append(admin_attrib)
|
||||
display.write_csv_file(csvRows, titles, 'Admins', todrive)
|
||||
@@ -2434,7 +2450,7 @@ def doPrintCourses():
|
||||
if ownerEmails is not None:
|
||||
ownerId = course['ownerId']
|
||||
if ownerId not in ownerEmails:
|
||||
ownerEmails[ownerId] = convertUIDtoEmailAddress(f'uid{ownerId}',
|
||||
ownerEmails[ownerId] = convertUIDtoEmailAddress(f'uid:{ownerId}',
|
||||
cd=cd)
|
||||
course['ownerEmail'] = ownerEmails[ownerId]
|
||||
for field in skipFieldsList:
|
||||
@@ -3275,6 +3291,7 @@ def printDriveFileList(users):
|
||||
titles.append(attrib)
|
||||
a_file[attrib] = ' '.join(f_file[attrib])
|
||||
else:
|
||||
a_file[attrib] = len(f_file[attrib])
|
||||
for j, l_attrib in enumerate(f_file[attrib]):
|
||||
for list_attrib in l_attrib:
|
||||
if list_attrib in [
|
||||
@@ -3625,6 +3642,9 @@ def getDriveFileAttribute(i, body, parameters, myarg, update=False):
|
||||
elif myarg == 'writerscantshare':
|
||||
body['writersCanShare'] = False
|
||||
i += 1
|
||||
elif myarg == 'writerscanshare':
|
||||
body['writersCanShare'] = True
|
||||
i += 1
|
||||
elif myarg == 'contentrestrictions':
|
||||
body['contentRestrictions'] = [{}]
|
||||
restriction = sys.argv[i+1].lower().replace('_', '')
|
||||
@@ -6302,7 +6322,7 @@ def doCreateOrUpdateUserSchema(updateCmd):
|
||||
if myarg == 'type':
|
||||
a_field['fieldType'] = sys.argv[i + 1].upper()
|
||||
validTypes = [
|
||||
'BOOL', 'DOUBLE', 'EMAIL', 'INT64', 'PHONE', 'STRING'
|
||||
'BOOL', 'DATE', 'DOUBLE', 'EMAIL', 'INT64', 'PHONE', 'STRING'
|
||||
]
|
||||
if a_field['fieldType'] not in validTypes:
|
||||
controlflow.expected_argument_exit(
|
||||
@@ -6494,6 +6514,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']:
|
||||
@@ -7009,6 +7030,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")
|
||||
@@ -7023,7 +7047,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):
|
||||
@@ -7305,7 +7329,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):
|
||||
@@ -7686,32 +7710,14 @@ def _generatePrivateKeyAndPublicCert(client_id, key_size):
|
||||
return private_pem, publicKeyData
|
||||
|
||||
|
||||
def _formatOAuth2ServiceData(project_id, client_email, client_id, private_key,
|
||||
private_key_id):
|
||||
quoted_email = quote(client_email)
|
||||
key_json = {
|
||||
'auth_provider_x509_cert_url':
|
||||
'https://www.googleapis.com/oauth2/v1/certs',
|
||||
'auth_uri':
|
||||
'https://accounts.google.com/o/oauth2/auth',
|
||||
'client_email':
|
||||
client_email,
|
||||
'client_id':
|
||||
client_id,
|
||||
'client_x509_cert_url':
|
||||
f'https://www.googleapis.com/robot/v1/metadata/x509/{quoted_email}',
|
||||
'private_key':
|
||||
private_key,
|
||||
'private_key_id':
|
||||
private_key_id,
|
||||
'project_id':
|
||||
project_id,
|
||||
'token_uri':
|
||||
'https://oauth2.googleapis.com/token',
|
||||
'type':
|
||||
'service_account',
|
||||
}
|
||||
return json.dumps(key_json, indent=2, sort_keys=True)
|
||||
def _formatOAuth2ServiceData(service_data):
|
||||
quoted_email = quote(service_data.get('client_email', ''))
|
||||
service_data['auth_provider_x509_cert_url'] = 'https://www.googleapis.com/oauth2/v1/certs'
|
||||
service_data['auth_uri'] = 'https://accounts.google.com/o/oauth2/auth'
|
||||
service_data['client_x509_cert_url'] = f'https://www.googleapis.com/robot/v1/metadata/x509/{quoted_email}'
|
||||
service_data['token_uri'] = 'https://oauth2.googleapis.com/token'
|
||||
service_data['type'] = 'service_account'
|
||||
return json.dumps(service_data, indent=2, sort_keys=True)
|
||||
|
||||
|
||||
def doShowServiceAccountKeys():
|
||||
@@ -7757,10 +7763,22 @@ def doCreateOrRotateServiceAccountKeys(iam=None,
|
||||
client_email=None,
|
||||
client_id=None):
|
||||
local_key_size = 2048
|
||||
mode = 'retainexisting'
|
||||
body = {}
|
||||
if iam:
|
||||
mode = 'retainexisting'
|
||||
new_data = {
|
||||
'client_email': client_email,
|
||||
'project_id': project_id,
|
||||
'client_id': client_id,
|
||||
'key_type': 'default'
|
||||
}
|
||||
else:
|
||||
_getSvcAcctData()
|
||||
# dict() ensures we have a real copy, not pointer
|
||||
new_data = dict(GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
|
||||
oldPrivateKeyId = new_data.get('private_key_id')
|
||||
# assume default key type unless we are told otherwise
|
||||
new_data['key_type'] = 'default'
|
||||
mode = 'retainnone'
|
||||
i = 3
|
||||
iam = buildGAPIServiceObject('iam', None)
|
||||
@@ -7785,38 +7803,64 @@ def doCreateOrRotateServiceAccountKeys(iam=None,
|
||||
'localkeysize must be 1024, 2048 or 4096. 1024 is weak and dangerous. 2048 is recommended. 4096 is slow.'
|
||||
)
|
||||
i += 2
|
||||
elif myarg == 'yubikey':
|
||||
new_data['key_type'] = 'yubikey'
|
||||
i += 1
|
||||
elif myarg == 'yubikeyslot':
|
||||
new_data['yubikey_slot'] = sys.argv[i+1].upper()
|
||||
i =+ 2
|
||||
elif myarg == 'yubikeypin':
|
||||
new_data['yubikey_pin'] = input('Enter your YubiKey PIN: ')
|
||||
i += 1
|
||||
elif myarg == 'yubikeyserialnumber':
|
||||
try:
|
||||
new_data['yubikey_serial_number'] = int(sys.argv[i+1])
|
||||
except ValueError:
|
||||
controlflow.system_error_exit(
|
||||
3,
|
||||
'yubikey_serial_number must be a number')
|
||||
i += 2
|
||||
elif myarg in ['retainnone', 'retainexisting', 'replacecurrent']:
|
||||
mode = myarg
|
||||
i += 1
|
||||
else:
|
||||
controlflow.invalid_argument_exit(myarg, 'gam rotate sakeys')
|
||||
currentPrivateKeyId = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA][
|
||||
'private_key_id']
|
||||
project_id = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['project_id']
|
||||
client_email = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['client_email']
|
||||
client_id = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['client_id']
|
||||
clientId = GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID]
|
||||
name = f'projects/-/serviceAccounts/{clientId}'
|
||||
if mode != 'retainexisting':
|
||||
keys = gapi.get_items(iam.projects().serviceAccounts().keys(),
|
||||
'list',
|
||||
'keys',
|
||||
name=name,
|
||||
keyTypes='USER_MANAGED')
|
||||
sa_name = f'projects/-/serviceAccounts/{new_data["client_id"]}'
|
||||
if new_data.get('key_type') == 'yubikey':
|
||||
# Use yubikey private key
|
||||
new_data['yubikey_key_type'] = f'RSA{local_key_size}'
|
||||
new_data.pop('private_key', None)
|
||||
yk = yubikey.YubiKey(new_data)
|
||||
publicKeyData = yk.get_certificate()
|
||||
elif local_key_size:
|
||||
# Generate private key locally, store in file
|
||||
new_data['private_key'], publicKeyData = _generatePrivateKeyAndPublicCert(
|
||||
sa_name, local_key_size)
|
||||
new_data['key_type'] = 'default'
|
||||
for key in list(new_data):
|
||||
if key.startswith('yubikey_'):
|
||||
new_data.pop(key, None)
|
||||
if local_key_size:
|
||||
private_key, publicKeyData = _generatePrivateKeyAndPublicCert(
|
||||
name, local_key_size)
|
||||
# Upload public cert for yubikey or local generated
|
||||
print(' Uploading new public certificate to Google...')
|
||||
throw_reasons = [
|
||||
gapi_errors.ErrorReason.FOUR_O_O,
|
||||
gapi_errors.ErrorReason.NOT_FOUND
|
||||
]
|
||||
max_retries = 10
|
||||
for i in range(1, max_retries + 1):
|
||||
try:
|
||||
result = gapi.call(
|
||||
iam.projects().serviceAccounts().keys(),
|
||||
'upload',
|
||||
throw_reasons=[gapi_errors.ErrorReason.NOT_FOUND],
|
||||
name=name,
|
||||
throw_reasons=throw_reasons,
|
||||
name=sa_name,
|
||||
body={'publicKeyData': publicKeyData})
|
||||
break
|
||||
except googleapiclient.errors.HttpError:
|
||||
print('WARNING: that key already exists.')
|
||||
result = {'name': oldPrivateKeyId}
|
||||
break
|
||||
except gapi_errors.GapiNotFoundError as e:
|
||||
if i == max_retries:
|
||||
raise e
|
||||
@@ -7826,31 +7870,36 @@ def doCreateOrRotateServiceAccountKeys(iam=None,
|
||||
f'Waiting for Service Account creation to complete. Sleeping {sleep_time} seconds\n'
|
||||
)
|
||||
time.sleep(sleep_time)
|
||||
private_key_id = result['name'].rsplit('/', 1)[-1]
|
||||
oauth2service_data = _formatOAuth2ServiceData(project_id, client_email,
|
||||
client_id, private_key,
|
||||
private_key_id)
|
||||
newPrivateKeyId = result['name'].rsplit('/', 1)[-1]
|
||||
new_data['private_key_id'] = newPrivateKeyId
|
||||
new_data_str = _formatOAuth2ServiceData(new_data)
|
||||
else:
|
||||
# Ask Google to generate private key, store locally
|
||||
result = gapi.call(iam.projects().serviceAccounts().keys(),
|
||||
'create',
|
||||
name=name,
|
||||
name=sa_name,
|
||||
body=body)
|
||||
oauth2service_data = base64.b64decode(
|
||||
new_data_str = base64.b64decode(
|
||||
result['privateKeyData']).decode(UTF8)
|
||||
private_key_id = result['name'].rsplit('/', 1)[-1]
|
||||
newPrivateKeyId = result['name'].rsplit('/', 1)[-1]
|
||||
fileutils.write_file(GC_Values[GC_OAUTH2SERVICE_JSON],
|
||||
oauth2service_data,
|
||||
new_data_str,
|
||||
continue_on_error=False)
|
||||
print(
|
||||
f' Wrote new private key {private_key_id} to {GC_Values[GC_OAUTH2SERVICE_JSON]}'
|
||||
f' Wrote new service account data for {newPrivateKeyId} to {GC_Values[GC_OAUTH2SERVICE_JSON]}'
|
||||
)
|
||||
if mode != 'retainexisting':
|
||||
keys = gapi.get_items(iam.projects().serviceAccounts().keys(),
|
||||
'list',
|
||||
'keys',
|
||||
name=sa_name,
|
||||
keyTypes='USER_MANAGED')
|
||||
count = len(keys) if mode == 'retainnone' else 1
|
||||
print(
|
||||
f' Revoking {count} existing key(s) for Service Account {clientId}')
|
||||
f' Revoking {count} existing key(s) for Service Account {new_data["client_id"]}')
|
||||
for key in keys:
|
||||
keyName = key['name'].rsplit('/', 1)[-1]
|
||||
if mode == 'retainnone' or keyName == currentPrivateKeyId:
|
||||
if (mode == 'retainnone' or keyName == oldPrivateKeyId) and keyName != newPrivateKeyId:
|
||||
print(f' Revoking existing key {keyName} for service account')
|
||||
gapi.call(iam.projects().serviceAccounts().keys(),
|
||||
'delete',
|
||||
@@ -8161,7 +8210,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')
|
||||
|
||||
@@ -8177,6 +8229,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)
|
||||
@@ -8206,7 +8267,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
|
||||
@@ -8226,6 +8287,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)
|
||||
@@ -8260,6 +8324,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',
|
||||
@@ -8300,6 +8373,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
|
||||
@@ -8314,28 +8388,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 proup 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):
|
||||
@@ -9759,12 +9842,11 @@ def getUsersToModify(entity_type=None,
|
||||
entity = sys.argv[2]
|
||||
# avoid building cd for user/users since it
|
||||
# unnnecesarily pushes user through admin auth
|
||||
if entity_type not in ['user', 'users']:
|
||||
if entity_type not in ['user', 'users'] or \
|
||||
('@' not in entity and not GC_Values[GC_DOMAIN]):
|
||||
cd = buildGAPIObject('directory')
|
||||
if entity_type == 'user':
|
||||
users = [
|
||||
entity,
|
||||
]
|
||||
users = [entity]
|
||||
elif entity_type == 'users':
|
||||
users = entity.replace(',', ' ').split()
|
||||
elif entity_type in ['group', 'group_ns', 'group_susp', 'group_inde']:
|
||||
@@ -10112,7 +10194,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() +
|
||||
@@ -10214,6 +10296,16 @@ 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'],
|
||||
'scopes': ['https://www.googleapis.com/auth/chrome.management.policy'],
|
||||
},
|
||||
{
|
||||
'name':
|
||||
'Classroom API - counts as 5 scopes',
|
||||
@@ -10231,6 +10323,12 @@ OAUTH2_SCOPES = [
|
||||
'subscopes': ['readonly'],
|
||||
'scopes': 'https://www.googleapis.com/auth/cloud-identity.groups'
|
||||
},
|
||||
{
|
||||
'name': 'Cloud Identity - User Invitations',
|
||||
'subscopes': ['readonly'],
|
||||
'scopes': 'https://www.googleapis.com/auth/cloud-identity.userinvitations',
|
||||
'offByDefault': True,
|
||||
},
|
||||
{
|
||||
'name': 'Contact Delegation',
|
||||
'subscopes': ['readonly'],
|
||||
@@ -10263,12 +10361,17 @@ OAUTH2_SCOPES = [
|
||||
'scopes': 'https://www.googleapis.com/auth/admin.directory.group'
|
||||
},
|
||||
{
|
||||
'name':
|
||||
'Directory API - Mobile Devices',
|
||||
'name': 'Directory API - Mobile Devices',
|
||||
'subscopes': ['readonly', 'action'],
|
||||
'scopes':
|
||||
'https://www.googleapis.com/auth/admin.directory.device.mobile'
|
||||
},
|
||||
{
|
||||
'name': 'Directory API - Printers',
|
||||
'subscopes': ['readonly'],
|
||||
# note - currently DASA only but admin credentials should work soon
|
||||
'scopes': 'https://www.googleapis.com/auth/admin.chrome.printers'
|
||||
},
|
||||
{
|
||||
'name': 'Directory API - Organizational Units',
|
||||
'subscopes': ['readonly'],
|
||||
@@ -10850,7 +10953,9 @@ Append an 'r' to grant read-only access or an 'a' to grant action-only access.
|
||||
)
|
||||
|
||||
|
||||
def init_gam_worker():
|
||||
def init_gam_worker(l):
|
||||
global mplock
|
||||
mplock = l
|
||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
|
||||
|
||||
@@ -10858,7 +10963,8 @@ def run_batch(items):
|
||||
if not items:
|
||||
return
|
||||
num_worker_threads = min(len(items), GC_Values[GC_NUM_THREADS])
|
||||
pool = mp_pool(num_worker_threads, init_gam_worker, maxtasksperchild=200)
|
||||
l = mp_lock()
|
||||
pool = mp_pool(num_worker_threads, init_gam_worker, maxtasksperchild=200, initargs=(l,))
|
||||
sys.stderr.write(f'Using {num_worker_threads} processes...\n')
|
||||
try:
|
||||
results = []
|
||||
@@ -10869,7 +10975,7 @@ def run_batch(items):
|
||||
)
|
||||
pool.close()
|
||||
pool.join()
|
||||
pool = mp_pool(num_worker_threads, init_gam_worker)
|
||||
pool = mp_pool(num_worker_threads, init_gam_worker, maxtasksperchild=200, initargs=(1,))
|
||||
sys.stderr.write(
|
||||
'commit-batch - running processes finished, proceeding\n')
|
||||
continue
|
||||
@@ -11134,6 +11240,8 @@ def ProcessGAMCommand(args):
|
||||
gapi_directory_roles.create()
|
||||
elif argument in ['browsertoken', 'browsertokens']:
|
||||
gapi_cbcm.createtoken()
|
||||
elif argument in ['printer']:
|
||||
gapi_directory_printers.create()
|
||||
else:
|
||||
controlflow.invalid_argument_exit(argument, 'gam create')
|
||||
sys.exit(0)
|
||||
@@ -11192,6 +11300,10 @@ def ProcessGAMCommand(args):
|
||||
gapi_cloudidentity_devices.update_state()
|
||||
elif argument in ['browser', 'browsers']:
|
||||
gapi_cbcm.update()
|
||||
elif argument == 'chromepolicy':
|
||||
gapi_chromepolicy.update_policy()
|
||||
elif argument in ['printer']:
|
||||
gapi_directory_printers.update()
|
||||
else:
|
||||
controlflow.invalid_argument_exit(argument, 'gam update')
|
||||
sys.exit(0)
|
||||
@@ -11235,6 +11347,8 @@ def ProcessGAMCommand(args):
|
||||
gapi_directory_domainaliases.info()
|
||||
elif argument in ['resoldcustomer', 'resellercustomer']:
|
||||
doGetResoldCustomer()
|
||||
elif argument in ['printer']:
|
||||
gapi_directory_printers.info()
|
||||
elif argument in [
|
||||
'resoldsubscription', 'resoldsubscriptions',
|
||||
'resellersubscription', 'resellersubscriptions'
|
||||
@@ -11254,6 +11368,8 @@ def ProcessGAMCommand(args):
|
||||
gapi_cloudidentity_devices.info_state()
|
||||
elif argument in ['browser', 'browsers']:
|
||||
gapi_cbcm.info()
|
||||
elif argument in ['userinvitation', 'userinvitations']:
|
||||
gapi_cloudidentity_userinvitations.get()
|
||||
else:
|
||||
controlflow.invalid_argument_exit(argument, 'gam info')
|
||||
sys.exit(0)
|
||||
@@ -11261,6 +11377,8 @@ def ProcessGAMCommand(args):
|
||||
argument = sys.argv[2].lower()
|
||||
if argument in ['guardianinvitation', 'guardianinvitations']:
|
||||
doCancelGuardianInvitation()
|
||||
elif argument in ['userinvitation', 'userinvitations']:
|
||||
gapi_cloudidentity_userinvitations.cancel()
|
||||
else:
|
||||
controlflow.invalid_argument_exit(argument, 'gam cancel')
|
||||
sys.exit(0)
|
||||
@@ -11318,6 +11436,10 @@ def ProcessGAMCommand(args):
|
||||
gapi_directory_roles.delete()
|
||||
elif argument in ['browser', 'browsers']:
|
||||
gapi_cbcm.delete()
|
||||
elif argument in ['printer']:
|
||||
gapi_directory_printers.delete()
|
||||
elif argument == 'chromepolicy':
|
||||
gapi_chromepolicy.delete_policy()
|
||||
else:
|
||||
controlflow.invalid_argument_exit(argument, 'gam delete')
|
||||
sys.exit(0)
|
||||
@@ -11419,9 +11541,30 @@ def ProcessGAMCommand(args):
|
||||
gapi_cbcm.printshowtokens(True)
|
||||
elif argument in ['vaultcount']:
|
||||
gapi_vault.print_count()
|
||||
elif argument in ['userinvitations']:
|
||||
gapi_cloudidentity_userinvitations.print_()
|
||||
elif argument in ['printermodels']:
|
||||
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)
|
||||
elif command == 'send':
|
||||
argument = sys.argv[2].lower()
|
||||
if argument in ['userinvitation', 'userinvitations']:
|
||||
gapi_cloudidentity_userinvitations.send()
|
||||
else:
|
||||
controlflow.invalid_argument_exit(argument, 'gam send')
|
||||
sys.exit(0)
|
||||
elif command == 'show':
|
||||
argument = sys.argv[2].lower()
|
||||
if argument in ['schema', 'schemas']:
|
||||
@@ -11436,6 +11579,10 @@ def ProcessGAMCommand(args):
|
||||
doShowServiceAccountKeys()
|
||||
elif argument in ['browsertoken', 'browsertokens']:
|
||||
gapi_cbcm.printshowtokens(False)
|
||||
elif argument in ['chromeschema', 'chromeschemas']:
|
||||
gapi_chromepolicy.printshow_schemas()
|
||||
elif argument in ['chromepolicy', 'chromepolicies']:
|
||||
gapi_chromepolicy.printshow_policies()
|
||||
else:
|
||||
controlflow.invalid_argument_exit(argument, 'gam show')
|
||||
sys.exit(0)
|
||||
@@ -11526,6 +11673,11 @@ def ProcessGAMCommand(args):
|
||||
else:
|
||||
controlflow.invalid_argument_exit(argument, 'gam rotate')
|
||||
sys.exit(0)
|
||||
elif command == 'check':
|
||||
argument = sys.argv[2].lower()
|
||||
if argument in ['isinvitable', 'userinvitation', 'userinvitations']:
|
||||
gapi_cloudidentity_userinvitations.check()
|
||||
sys.exit(0)
|
||||
elif command in ['cancelwipe', 'wipe', 'approve', 'block', 'sync']:
|
||||
target = sys.argv[2].lower().replace('_', '')
|
||||
if target in ['device', 'devices']:
|
||||
@@ -11855,6 +12007,8 @@ def ProcessGAMCommand(args):
|
||||
checkWhat = sys.argv[4].replace('_', '').lower()
|
||||
if checkWhat == 'serviceaccount':
|
||||
doCheckServiceAccount(users)
|
||||
elif checkWhat == 'isinvitable':
|
||||
gapi_cloudidentity_userinvitations.bulk_is_invitable(users)
|
||||
else:
|
||||
controlflow.invalid_argument_exit(checkWhat,
|
||||
'gam <users> check')
|
||||
@@ -11903,6 +12057,8 @@ def ProcessGAMCommand(args):
|
||||
gapi_directory_users.signout(users)
|
||||
elif command == 'turnoff2sv':
|
||||
gapi_directory_users.turn_off_2sv(users)
|
||||
elif command == 'waitformailbox':
|
||||
gapi_directory_users.wait_for_mailbox(users)
|
||||
else:
|
||||
controlflow.invalid_argument_exit(command, 'gam')
|
||||
except IndexError:
|
||||
@@ -11910,7 +12066,7 @@ def ProcessGAMCommand(args):
|
||||
sys.exit(2)
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(50)
|
||||
except socket.error as e:
|
||||
except OSError as e:
|
||||
controlflow.system_error_exit(3, str(e))
|
||||
except MemoryError:
|
||||
controlflow.system_error_exit(99, MESSAGE_GAM_OUT_OF_MEMORY)
|
||||
|
||||
@@ -5,6 +5,9 @@ import os
|
||||
|
||||
from google.auth.jwt import Credentials as JWTCredentials
|
||||
|
||||
import gam
|
||||
from gam import utils
|
||||
|
||||
from gam.auth import oauth
|
||||
from gam.var import _FN_OAUTH2_TXT
|
||||
from gam.var import _FN_OAUTH2SERVICE_JSON
|
||||
@@ -13,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.
|
||||
@@ -36,10 +40,17 @@ def get_admin_credentials(api=None):
|
||||
with open(credential_file, 'r') as f:
|
||||
creds_data = json.load(f)
|
||||
# Validate that enable DASA matches content of authorization file
|
||||
if GC_Values[GC_ENABLE_DASA] and 'private_key' in creds_data:
|
||||
if GC_Values[GC_ENABLE_DASA] and 'private_key_id' in creds_data:
|
||||
audience = f'https://{api}.googleapis.com/'
|
||||
return JWTCredentials.from_service_account_info(creds_data,
|
||||
audience=audience)
|
||||
key_type = creds_data.get('key_type', 'default')
|
||||
if key_type == 'default':
|
||||
return JWTCredentials.from_service_account_info(creds_data,
|
||||
audience=audience)
|
||||
elif key_type == 'yubikey':
|
||||
yksigner = yubikey.YubiKey(creds_data)
|
||||
return JWTCredentials._from_signer_and_info(yksigner,
|
||||
creds_data,
|
||||
audience=audience)
|
||||
elif not GC_Values[GC_ENABLE_DASA] and 'token' in creds_data:
|
||||
return oauth.Credentials.from_credentials_file(credential_file)
|
||||
else:
|
||||
|
||||
74
src/gam/auth/yubikey.py
Normal file
74
src/gam/auth/yubikey.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from base64 import b64encode
|
||||
import sys
|
||||
from threading import Timer
|
||||
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from ykman.device import connect_to_device
|
||||
from yubikit.piv import KEY_TYPE, SLOT, InvalidPinError, PivSession
|
||||
from yubikit.core.smartcard import ApduError
|
||||
from gam import controlflow
|
||||
|
||||
class YubiKey():
|
||||
|
||||
def __init__(self, service_account_info):
|
||||
key_type = service_account_info.get('yubikey_key_type', 'RSA2048')
|
||||
try:
|
||||
self.key_type = getattr(KEY_TYPE, key_type.upper())
|
||||
except AttributeError:
|
||||
controlflow.system_error_exit(6, f'{key_type} is not a valid value for yubikey_key_type')
|
||||
slot = service_account_info.get('yubikey_slot', 'AUTHENTICATION')
|
||||
try:
|
||||
self.slot = getattr(SLOT, slot.upper())
|
||||
except AttributeError:
|
||||
controlflow.system_error_exit(6, f'{slot} is not a valid value for yubikey_slot')
|
||||
self.serial_number = service_account_info.get('yubikey_serial_number')
|
||||
self.pin = service_account_info.get('yubikey_pin')
|
||||
self.key_id = service_account_info.get('private_key_id')
|
||||
|
||||
def get_certificate(self):
|
||||
try:
|
||||
conn, _, _ = connect_to_device(self.serial_number)
|
||||
session = PivSession(conn)
|
||||
if self.pin:
|
||||
try:
|
||||
session.verify_pin(self.pin)
|
||||
except InvalidPinError as err:
|
||||
controlflow.system_error_exit(7, f'YubiKey - {err}')
|
||||
try:
|
||||
cert = session.get_certificate(self.slot)
|
||||
cert_pem = cert.public_bytes(
|
||||
serialization.Encoding.PEM).decode()
|
||||
publicKeyData = b64encode(cert_pem.encode())
|
||||
if isinstance(publicKeyData, bytes):
|
||||
publicKeyData = publicKeyData.decode()
|
||||
return publicKeyData
|
||||
except ApduError as err:
|
||||
controlflow.system_error_exit(8, f'YubiKey - {err}')
|
||||
except ValueError as err:
|
||||
controlflow.system_error_exit(9, f'YubiKey - {err}')
|
||||
|
||||
def sign(self, message):
|
||||
if 'mplock' in globals():
|
||||
mplock.acquire()
|
||||
try:
|
||||
conn, _, _ = connect_to_device(self.serial_number)
|
||||
session = PivSession(conn)
|
||||
if self.pin:
|
||||
try:
|
||||
session.verify_pin(self.pin)
|
||||
except InvalidPinError as err:
|
||||
controlflow.system_error_exit(7, f'YubiKey - {err}')
|
||||
try:
|
||||
signed = session.sign(slot=self.slot,
|
||||
key_type=self.key_type,
|
||||
message=message,
|
||||
hash_algorithm=hashes.SHA256(),
|
||||
padding=padding.PKCS1v15())
|
||||
except ApduError as err:
|
||||
controlflow.system_error_exit(8, f'YubiKey = {err}')
|
||||
except ValueError as err:
|
||||
controlflow.system_error_exit(9, f'YubiKey - {err}')
|
||||
if 'mplock' in globals():
|
||||
mplock.release()
|
||||
return signed
|
||||
@@ -14,6 +14,14 @@ from gam.gapi.directory import orgunits as gapi_directory_orgunits
|
||||
from gam import utils
|
||||
|
||||
|
||||
def _get_customerid():
|
||||
''' returns customer id without C prefix'''
|
||||
customer_id = GC_Values[GC_CUSTOMER_ID]
|
||||
if customer_id[0] == 'C':
|
||||
customer_id = customer_id[1:]
|
||||
return customer_id
|
||||
|
||||
|
||||
def build():
|
||||
return gam.buildGAPIObject('cbcm')
|
||||
|
||||
@@ -21,8 +29,9 @@ def build():
|
||||
def delete():
|
||||
cbcm = build()
|
||||
device_id = sys.argv[3]
|
||||
customer_id = _get_customerid()
|
||||
gapi.call(cbcm.chromebrowsers(), 'delete', deviceId=device_id,
|
||||
customer=GC_Values[GC_CUSTOMER_ID])
|
||||
customer=customer_id)
|
||||
print(f'Deleted browser {device_id}')
|
||||
|
||||
|
||||
@@ -31,6 +40,7 @@ def info():
|
||||
device_id = sys.argv[3]
|
||||
projection = 'BASIC'
|
||||
fields = None
|
||||
customer_id = _get_customerid()
|
||||
i = 4
|
||||
while i < len(sys.argv):
|
||||
myarg = sys.argv[i].lower().replace('_', '')
|
||||
@@ -43,7 +53,7 @@ def info():
|
||||
else:
|
||||
controlflow.invalid_argument_exit(sys.argv[i], 'gam info browser')
|
||||
browser = gapi.call(cbcm.chromebrowsers(), 'get',
|
||||
customer=GC_Values[GC_CUSTOMER_ID],
|
||||
customer=customer_id,
|
||||
fields=fields, deviceId=device_id,
|
||||
projection=projection)
|
||||
display.print_json(browser)
|
||||
@@ -52,6 +62,7 @@ def info():
|
||||
def move():
|
||||
cbcm = build()
|
||||
body = {'resource_ids': []}
|
||||
customer_id = _get_customerid()
|
||||
i = 3
|
||||
resource_ids = []
|
||||
batch_size = 600
|
||||
@@ -65,7 +76,7 @@ def move():
|
||||
page_message = gapi.got_total_items_msg('Browsers', '...\n')
|
||||
browsers = gapi.get_all_pages(cbcm.chromebrowsers(), 'list',
|
||||
'browsers', page_message=page_message,
|
||||
customer=GC_Values[GC_CUSTOMER_ID],
|
||||
customer=customer_id,
|
||||
query=query, projection='BASIC',
|
||||
fields='browsers(deviceId),nextPageToken')
|
||||
ids = [browser['deviceId'] for browser in browsers]
|
||||
@@ -115,13 +126,14 @@ def move():
|
||||
print(f' moving {len(body["resource_ids"])} browsers to ' \
|
||||
f'{body["org_unit_path"]}')
|
||||
gapi.call(cbcm.chromebrowsers(), 'moveChromeBrowsersToOu',
|
||||
customer=GC_Values[GC_CUSTOMER_ID], body=body)
|
||||
customer=customer_id, body=body)
|
||||
|
||||
|
||||
def print_():
|
||||
cbcm = build()
|
||||
customer_id = _get_customerid()
|
||||
projection = 'BASIC'
|
||||
query = None
|
||||
orgUnitPath = query = None
|
||||
fields = None
|
||||
titles = []
|
||||
csv_rows = []
|
||||
@@ -133,6 +145,9 @@ def print_():
|
||||
if myarg == 'query':
|
||||
query = sys.argv[i+1]
|
||||
i += 2
|
||||
elif myarg in ['ou', 'org', 'orgunit']:
|
||||
orgUnitPath = gapi_directory_orgunits.getOrgUnitItem(sys.argv[i + 1], pathOnly=True, absolutePath=True)
|
||||
i += 2
|
||||
elif myarg == 'projection':
|
||||
projection = sys.argv[i + 1].upper()
|
||||
i += 2
|
||||
@@ -154,8 +169,8 @@ def print_():
|
||||
page_message = gapi.got_total_items_msg('Browsers', '...\n')
|
||||
browsers = gapi.get_all_pages(cbcm.chromebrowsers(), 'list',
|
||||
'browsers', page_message=page_message,
|
||||
customer=GC_Values[GC_CUSTOMER_ID],
|
||||
query=query, projection=projection,
|
||||
customer=customer_id,
|
||||
orgUnitPath=orgUnitPath, query=query, projection=projection,
|
||||
fields=fields)
|
||||
for browser in browsers:
|
||||
browser = utils.flatten_json(browser)
|
||||
@@ -178,6 +193,7 @@ attribute_fields = ','.join(list(attributes.values()))
|
||||
|
||||
def update():
|
||||
cbcm = build()
|
||||
customer_id = _get_customerid()
|
||||
device_id = sys.argv[3]
|
||||
body = {'deviceId': device_id}
|
||||
i = 4
|
||||
@@ -190,17 +206,18 @@ def update():
|
||||
controlflow.invalid_argument_exit(sys.argv[i],
|
||||
'gam update browser')
|
||||
browser = gapi.call(cbcm.chromebrowsers(), 'get', deviceId=device_id,
|
||||
customer=GC_Values[GC_CUSTOMER_ID],
|
||||
customer=customer_id,
|
||||
projection='BASIC', fields=attribute_fields)
|
||||
browser.update(body)
|
||||
result = gapi.call(cbcm.chromebrowsers(), 'update', deviceId=device_id,
|
||||
customer=GC_Values[GC_CUSTOMER_ID], body=browser,
|
||||
customer=customer_id, body=browser,
|
||||
projection='BASIC', fields="deviceId")
|
||||
print(f'Updated browser {result["deviceId"]}')
|
||||
|
||||
|
||||
def createtoken():
|
||||
cbcm = build()
|
||||
customer_id = _get_customerid()
|
||||
body = {'token_type': 'CHROME_BROWSER'}
|
||||
i = 3
|
||||
while i < len(sys.argv):
|
||||
@@ -215,22 +232,24 @@ def createtoken():
|
||||
controlflow.invalid_argument_exit(sys.argv[i],
|
||||
'gam create browsertoken')
|
||||
browser = gapi.call(cbcm.enrollmentTokens(), 'create',
|
||||
customer=GC_Values[GC_CUSTOMER_ID], body=body)
|
||||
customer=customer_id, body=body)
|
||||
print(f'Created browser enrollment token {browser["token"]}')
|
||||
|
||||
|
||||
def revoketoken():
|
||||
cbcm = build()
|
||||
customer_id = _get_customerid()
|
||||
token_permanent_id = sys.argv[3]
|
||||
gapi.call(cbcm.enrollmentTokens(), 'revoke', tokenPermanentId=token_permanent_id,
|
||||
customer=GC_Values[GC_CUSTOMER_ID])
|
||||
customer=customer_id)
|
||||
print(f'Deleted browser enrollment token {token_permanent_id}')
|
||||
|
||||
|
||||
def printshowtokens(csvFormat):
|
||||
cbcm = build()
|
||||
customer_id = _get_customerid()
|
||||
query = None
|
||||
fields = []
|
||||
fields = None
|
||||
if csvFormat:
|
||||
titles = ['token']
|
||||
csv_rows = []
|
||||
@@ -260,7 +279,7 @@ def printshowtokens(csvFormat):
|
||||
page_message = gapi.got_total_items_msg('Chrome Browser Enrollment Tokens', '...\n')
|
||||
browsers = gapi.get_all_pages(cbcm.enrollmentTokens(), 'list',
|
||||
'chromeEnrollmentTokens', page_message=page_message,
|
||||
customer=GC_Values[GC_CUSTOMER_ID],
|
||||
customer=customer_id,
|
||||
query=query, fields=fields)
|
||||
if not csvFormat:
|
||||
count = len(browsers)
|
||||
|
||||
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)
|
||||
338
src/gam/gapi/chromepolicy.py
Normal file
338
src/gam/gapi/chromepolicy.py
Normal file
@@ -0,0 +1,338 @@
|
||||
"""Chrome Browser Cloud Management API calls"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
|
||||
import googleapiclient.errors
|
||||
|
||||
import gam
|
||||
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
|
||||
|
||||
|
||||
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'orgunits/{orgunitid[3:]}'
|
||||
|
||||
|
||||
def build():
|
||||
return gam.buildGAPIObject('chromepolicy')
|
||||
|
||||
|
||||
def printshow_policies():
|
||||
svc = build()
|
||||
customer = _get_customerid()
|
||||
orgunit = None
|
||||
printer_id = None
|
||||
app_id = None
|
||||
i = 3
|
||||
while i < len(sys.argv):
|
||||
myarg = sys.argv[i].lower().replace('_', '')
|
||||
if myarg in ['ou', 'org', 'orgunit']:
|
||||
orgunit = _get_orgunit(sys.argv[i+1])
|
||||
i += 2
|
||||
elif myarg == 'printerid':
|
||||
printer_id = sys.argv[i+1]
|
||||
i += 2
|
||||
elif myarg == 'appid':
|
||||
app_id = sys.argv[i+1]
|
||||
i += 2
|
||||
else:
|
||||
msg = f'{myarg} is not a valid argument to "gam print chromepolicy"'
|
||||
controlflow.system_error_exit(3, msg)
|
||||
if not orgunit:
|
||||
controlflow.system_error_exit(3, 'You must specify an orgunit')
|
||||
body = {
|
||||
'policyTargetKey': {
|
||||
'targetResource': orgunit,
|
||||
}
|
||||
}
|
||||
if printer_id:
|
||||
body['policyTargetKey']['additionalTargetKeys'] = {'printer_id': printer_id}
|
||||
namespaces = ['chrome.printers']
|
||||
elif app_id:
|
||||
body['policyTargetKey']['additionalTargetKeys'] = {'app_id': app_id}
|
||||
namespaces = ['chrome.users.apps',
|
||||
'chrome.devices.managedGuest.apps',
|
||||
'chrome.devices.kiosk.apps']
|
||||
else:
|
||||
namespaces = [
|
||||
'chrome.users',
|
||||
# Not yet implemented:
|
||||
# 'chrome.devices',
|
||||
# 'chrome.devices.managedGuest',
|
||||
# 'chrome.devices.kiosk',
|
||||
]
|
||||
throw_reasons = [gapi_errors.ErrorReason.FOUR_O_O,]
|
||||
orgunitPath = gapi_directory_orgunits.orgunit_from_orgunitid(orgunit[9:], None)
|
||||
header = f'Organizational Unit: {orgunitPath}'
|
||||
if printer_id:
|
||||
header += f', printerid: {printer_id}'
|
||||
elif app_id:
|
||||
header += f', appid: {app_id}'
|
||||
print(header)
|
||||
for namespace in namespaces:
|
||||
body['policySchemaFilter'] = f'{namespace}.*'
|
||||
try:
|
||||
policies = gapi.get_all_pages(svc.customers().policies(), 'resolve',
|
||||
items='resolvedPolicies',
|
||||
throw_reasons=throw_reasons,
|
||||
customer=customer,
|
||||
body=body)
|
||||
except googleapiclient.errors.HttpError:
|
||||
policies = []
|
||||
for policy in sorted(policies, key=lambda k: k.get('value', {}).get('policySchema', '')):
|
||||
print()
|
||||
name = policy.get('value', {}).get('policySchema', '')
|
||||
print(name)
|
||||
values = policy.get('value', {}).get('value', {})
|
||||
for setting, value in values.items():
|
||||
if isinstance(value, str) and value.find('_ENUM_') != -1:
|
||||
value = value.split('_ENUM_')[-1]
|
||||
print(f' {setting}: {value}')
|
||||
|
||||
|
||||
def build_schemas(svc=None, sfilter=None):
|
||||
if not svc:
|
||||
svc = build()
|
||||
parent = _get_customerid()
|
||||
schemas = gapi.get_all_pages(svc.customers().policySchemas(), 'list',
|
||||
items='policySchemas', parent=parent, filter=sfilter)
|
||||
schema_objects = {}
|
||||
for schema in schemas:
|
||||
schema_name = schema.get('name', '').split('/')[-1]
|
||||
schema_dict = {
|
||||
'name': schema_name,
|
||||
'description': schema.get('policyDescription', ''),
|
||||
'settings': {},
|
||||
}
|
||||
field_descriptions = schema.get('fieldDescriptions', [])
|
||||
for mtype in schema.get('definition', {}).get('messageType', {}):
|
||||
for setting in mtype.get('field', {}):
|
||||
setting_name = setting.get('name', '')
|
||||
setting_dict = {
|
||||
'name': setting_name,
|
||||
'constraints': None,
|
||||
'descriptions': [],
|
||||
'type': setting.get('type'),
|
||||
}
|
||||
if setting_dict['type'] == 'TYPE_STRING' and \
|
||||
setting.get('label') == 'LABEL_REPEATED':
|
||||
setting_dict['type'] = 'TYPE_LIST'
|
||||
if setting_dict['type'] == 'TYPE_ENUM':
|
||||
type_name = setting['typeName']
|
||||
for an_enum in schema['definition']['enumType']:
|
||||
if an_enum['name'] == type_name:
|
||||
setting_dict['enums'] = [enum['name'] for enum in an_enum['value']]
|
||||
setting_dict['enum_prefix'] = utils.commonprefix(setting_dict['enums'])
|
||||
prefix_len = len(setting_dict['enum_prefix'])
|
||||
setting_dict['enums'] = [enum[prefix_len:] for enum \
|
||||
in setting_dict['enums'] \
|
||||
if not enum.endswith('UNSPECIFIED')]
|
||||
setting_dict['descriptions'] = ['']*len(setting_dict['enums'])
|
||||
if field_descriptions:
|
||||
for i, an in enumerate(setting_dict['enums']):
|
||||
for fdesc in field_descriptions:
|
||||
if fdesc.get('field') == setting_name:
|
||||
for d in fdesc.get('knownValueDescriptions', []):
|
||||
if d['value'][prefix_len:] == an:
|
||||
setting_dict['descriptions'][i] = d['description']
|
||||
break
|
||||
break
|
||||
break
|
||||
elif setting_dict['type'] == 'TYPE_MESSAGE':
|
||||
continue
|
||||
else:
|
||||
setting_dict['enums'] = None
|
||||
for fdesc in schema.get('fieldDescriptions', []):
|
||||
if fdesc.get('field') == setting_name:
|
||||
if 'knownValueDescriptions' in fdesc:
|
||||
setting_dict['descriptions'] = fdesc['knownValueDescriptions']
|
||||
elif 'description' in fdesc:
|
||||
setting_dict['descriptions'] = [fdesc['description']]
|
||||
schema_dict['settings'][setting_name.lower()] = setting_dict
|
||||
schema_objects[schema_name.lower()] = schema_dict
|
||||
return schema_objects
|
||||
|
||||
|
||||
def printshow_schemas():
|
||||
svc = build()
|
||||
sfilter = None
|
||||
i = 3
|
||||
while i < len(sys.argv):
|
||||
myarg = sys.argv[i].lower().replace('_', '')
|
||||
if myarg == 'filter':
|
||||
sfilter = sys.argv[i+1]
|
||||
i += 2
|
||||
else:
|
||||
msg = f'{myarg} is not a valid argument to "gam print chromeschema"'
|
||||
controlflow.system_error_exit(3, msg)
|
||||
schemas = build_schemas(svc, sfilter)
|
||||
for _, value in sorted(iter(schemas.items())):
|
||||
print(f'{value.get("name")}: {value.get("description")}')
|
||||
for val in value['settings'].values():
|
||||
vtype = val.get('type')
|
||||
print(f' {val.get("name")}: {vtype}')
|
||||
if vtype == 'TYPE_ENUM':
|
||||
enums = val.get('enums', [])
|
||||
descriptions = val.get('descriptions', [])
|
||||
for i in range(len(val.get('enums', []))):
|
||||
print(f' {enums[i]}: {descriptions[i]}')
|
||||
elif vtype == 'TYPE_BOOL':
|
||||
pvs = val.get('descriptions')
|
||||
for pvi in pvs:
|
||||
if isinstance(pvi, dict):
|
||||
pvalue = pvi.get('value')
|
||||
pdescription = pvi.get('description')
|
||||
print(f' {pvalue}: {pdescription}')
|
||||
elif isinstance(pvi, list):
|
||||
print(f' {pvi[0]}')
|
||||
else:
|
||||
description = val.get('descriptions')
|
||||
if len(description) > 0:
|
||||
print(f' {description[0]}')
|
||||
print()
|
||||
|
||||
|
||||
def delete_policy():
|
||||
svc = build()
|
||||
customer = _get_customerid()
|
||||
schemas = build_schemas(svc)
|
||||
orgunit = None
|
||||
printer_id = None
|
||||
app_id = None
|
||||
i = 3
|
||||
body = {'requests': []}
|
||||
while i < len(sys.argv):
|
||||
myarg = sys.argv[i].lower().replace('_', '')
|
||||
if myarg in ['ou', 'org', 'orgunit']:
|
||||
orgunit = _get_orgunit(sys.argv[i+1])
|
||||
i += 2
|
||||
elif myarg == 'printerid':
|
||||
printer_id = sys.argv[i+1]
|
||||
i += 2
|
||||
elif myarg == 'appid':
|
||||
app_id = sys.argv[i+1]
|
||||
i += 2
|
||||
elif myarg in schemas:
|
||||
body['requests'].append({'policySchema': schemas[myarg]['name']})
|
||||
i += 1
|
||||
else:
|
||||
msg = f'{myarg} is not a valid argument to "gam delete chromepolicy"'
|
||||
controlflow.system_error_exit(3, msg)
|
||||
if not orgunit:
|
||||
controlflow.system_error_exit(3, 'You must specify an orgunit')
|
||||
for request in body['requests']:
|
||||
request['policyTargetKey'] = {'targetResource': orgunit}
|
||||
if printer_id:
|
||||
request['policyTargetKey']['additionalTargetKeys'] = {'printer_id': printer_id}
|
||||
elif app_id:
|
||||
request['policyTargetKey']['additionalTargetKeys'] = {'app_id': app_id}
|
||||
gapi.call(svc.customers().policies().orgunits(), 'batchInherit', customer=customer, body=body)
|
||||
|
||||
|
||||
def update_policy():
|
||||
svc = build()
|
||||
customer = _get_customerid()
|
||||
schemas = build_schemas(svc)
|
||||
orgunit = None
|
||||
printer_id = None
|
||||
app_id = None
|
||||
i = 3
|
||||
body = {'requests': []}
|
||||
while i < len(sys.argv):
|
||||
myarg = sys.argv[i].lower().replace('_', '')
|
||||
if myarg in ['ou', 'org', 'orgunit']:
|
||||
orgunit = _get_orgunit(sys.argv[i+1])
|
||||
i += 2
|
||||
elif myarg == 'printerid':
|
||||
printer_id = sys.argv[i+1]
|
||||
i += 2
|
||||
elif myarg == 'appid':
|
||||
app_id = sys.argv[i+1]
|
||||
i += 2
|
||||
elif myarg in schemas:
|
||||
body['requests'].append({'policyValue': {'policySchema': schemas[myarg]['name'],
|
||||
'value': {}},
|
||||
'updateMask': ''})
|
||||
i += 1
|
||||
while i < len(sys.argv):
|
||||
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
|
||||
expected_fields = ', '.join(schemas[myarg]['settings'])
|
||||
if field not in expected_fields:
|
||||
msg = f'Expected {myarg} field of {expected_fields}. Got {field}.'
|
||||
controlflow.system_error_exit(4, msg)
|
||||
cased_field = schemas[myarg]['settings'][field]['name']
|
||||
value = sys.argv[i+1]
|
||||
vtype = schemas[myarg]['settings'][field]['type']
|
||||
if vtype in ['TYPE_INT64', 'TYPE_INT32', 'TYPE_UINT64']:
|
||||
if not value.isnumeric():
|
||||
msg = f'Value for {myarg} {field} must be a number, got {value}'
|
||||
controlflow.system_error_exit(7, msg)
|
||||
value = int(value)
|
||||
elif vtype in ['TYPE_BOOL']:
|
||||
value = gam.getBoolean(value, field)
|
||||
elif vtype in ['TYPE_ENUM']:
|
||||
value = value.upper()
|
||||
enum_values = schemas[myarg]['settings'][field]['enums']
|
||||
if value not in enum_values:
|
||||
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
|
||||
else:
|
||||
msg = f'{myarg} is not a valid argument to "gam update chromepolicy"'
|
||||
controlflow.system_error_exit(4, msg)
|
||||
if not orgunit:
|
||||
controlflow.system_error_exit(3, 'You must specify an orgunit')
|
||||
for request in body['requests']:
|
||||
request['policyTargetKey'] = {'targetResource': orgunit}
|
||||
if printer_id:
|
||||
request['policyTargetKey']['additionalTargetKeys'] = {'printer_id': printer_id}
|
||||
elif app_id:
|
||||
request['policyTargetKey']['additionalTargetKeys'] = {'app_id': app_id}
|
||||
gapi.call(svc.customers().policies().orgunits(),
|
||||
'batchModify',
|
||||
customer=customer,
|
||||
body=body)
|
||||
207
src/gam/gapi/cloudidentity/userinvitations.py
Normal file
207
src/gam/gapi/cloudidentity/userinvitations.py
Normal file
@@ -0,0 +1,207 @@
|
||||
"""Methods related to Cloud Identity User Invitation API"""
|
||||
import sys
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
import googleapiclient
|
||||
|
||||
import gam
|
||||
from gam.var import GC_CUSTOMER_ID, GC_Values, MY_CUSTOMER, SORTORDER_CHOICES_MAP
|
||||
from gam import controlflow
|
||||
from gam import display
|
||||
from gam import gapi
|
||||
from gam.gapi import errors as gapi_errors
|
||||
from gam.gapi import cloudidentity as gapi_cloudidentity
|
||||
|
||||
def _get_customerid():
|
||||
''' returns customer in "customers/(C){customer_id}' format needed for this API'''
|
||||
customer = GC_Values[GC_CUSTOMER_ID]
|
||||
if customer != MY_CUSTOMER and customer[0] != 'C':
|
||||
customer = 'C' + customer
|
||||
return f'customers/{customer}'
|
||||
|
||||
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_dwd('cloudidentity_beta')
|
||||
customer = _get_customerid()
|
||||
email = sys.argv[3].lower()
|
||||
encoded_email = quote_plus(email)
|
||||
name = f'{customer}/userinvitations/{encoded_email}'
|
||||
action_map = {
|
||||
'cancel': 'Cancelling',
|
||||
'send': 'Sending'
|
||||
}
|
||||
print_action = action_map[action]
|
||||
print(f'{print_action} user invitation...')
|
||||
result = gapi.call(svc.customers().userinvitations(), action,
|
||||
name=name)
|
||||
name = result.get('response', {}).get('name')
|
||||
if name:
|
||||
result['response']['name'] = _reduce_name(name)
|
||||
display.print_json(result)
|
||||
|
||||
def _generic_get(get_type):
|
||||
'''generic function to call read data APIs'''
|
||||
svc = gapi_cloudidentity.build_dwd('cloudidentity_beta')
|
||||
customer = _get_customerid()
|
||||
email = sys.argv[3].lower()
|
||||
encoded_email = quote_plus(email)
|
||||
name = f'{customer}/userinvitations/{encoded_email}'
|
||||
result = gapi.call(svc.customers().userinvitations(), get_type,
|
||||
name=name)
|
||||
if 'name' in result:
|
||||
result['name'] = _reduce_name(result['name'])
|
||||
display.print_json(result)
|
||||
|
||||
|
||||
# /batch is broken for Cloud Identity. Once fixed move this to using batch.
|
||||
# Current serial implementation will be SLOW...
|
||||
def bulk_is_invitable(emails):
|
||||
'''gam <users> check isinvitable'''
|
||||
def _invitation_result(request_id, response, _):
|
||||
if response.get('isInvitableUser'):
|
||||
rows.append({'invitableUsers': request_id})
|
||||
|
||||
svc = gapi_cloudidentity.build_dwd('cloudidentity_beta')
|
||||
customer = _get_customerid()
|
||||
todrive = False
|
||||
#batch_size = 1000
|
||||
#ebatch = svc.new_batch_http_request(callback=_invitation_result)
|
||||
rows = []
|
||||
throw_reasons = [gapi_errors.ErrorReason.FOUR_O_THREE]
|
||||
for email in emails:
|
||||
encoded_email = quote_plus(email)
|
||||
name = f'{customer}/userinvitations/{encoded_email}'
|
||||
endpoint = svc.customers().userinvitations()
|
||||
#if len(ebatch._order) == batch_size:
|
||||
# ebatch.execute()
|
||||
# ebatch = svc.new_batch_http_request(callback=_invitation_result)
|
||||
#req = endpoint.isInvitableUser(name=name)
|
||||
#ebatch.add(req, request_id=email)
|
||||
try:
|
||||
result = gapi.call(endpoint,
|
||||
'isInvitableUser',
|
||||
throw_reasons=throw_reasons,
|
||||
name=name)
|
||||
except googleapiclient.errors.HttpError:
|
||||
continue
|
||||
if result.get('isInvitableUser'):
|
||||
rows.append({'invitableUsers': email})
|
||||
#ebatch.execute()
|
||||
titles = ['invitableUsers']
|
||||
display.write_csv_file(rows, titles, 'Invitable Users', todrive)
|
||||
|
||||
|
||||
def cancel():
|
||||
'''gam cancel userinvitation <email>'''
|
||||
_generic_action('cancel')
|
||||
|
||||
|
||||
def get():
|
||||
'''gam info userinvitation <email>'''
|
||||
_generic_get('get')
|
||||
|
||||
|
||||
def check():
|
||||
'''gam check userinvitation <email>'''
|
||||
_generic_get('isInvitableUser')
|
||||
|
||||
|
||||
def send():
|
||||
'''gam send userinvitation <email>'''
|
||||
_generic_action('send')
|
||||
|
||||
|
||||
USERINVITATION_ORDERBY_CHOICES_MAP = {
|
||||
'email': 'email',
|
||||
'updatetime': 'update_time',
|
||||
}
|
||||
|
||||
USERINVITATION_STATE_CHOICES_MAP = {
|
||||
'accepted': 'ACCEPTED',
|
||||
'declined': 'DECLINED',
|
||||
'invited': 'INVITED',
|
||||
'notyetsent': 'NOT_YET_SENT',
|
||||
}
|
||||
|
||||
def print_():
|
||||
'''gam print userinvitations'''
|
||||
svc = gapi_cloudidentity.build_dwd('cloudidentity_beta')
|
||||
customer = _get_customerid()
|
||||
todrive = False
|
||||
titles = ['name', 'state', 'updateTime']
|
||||
rows = []
|
||||
filter_ = None
|
||||
orderByList = []
|
||||
i = 3
|
||||
while i < len(sys.argv):
|
||||
myarg = sys.argv[i].lower().replace('_', '')
|
||||
if myarg == 'state':
|
||||
state = sys.argv[i + 1].lower().replace('_', '')
|
||||
if state in USERINVITATION_STATE_CHOICES_MAP:
|
||||
filter_ = f"state=='{USERINVITATION_STATE_CHOICES_MAP[state]}'"
|
||||
else:
|
||||
controlflow.expected_argument_exit('state',
|
||||
', '.join(USERINVITATION_STATE_CHOICES_MAP),
|
||||
state)
|
||||
i += 2
|
||||
elif myarg == 'orderby':
|
||||
fieldName = sys.argv[i + 1].lower()
|
||||
i += 2
|
||||
if fieldName in USERINVITATION_ORDERBY_CHOICES_MAP:
|
||||
fieldName = USERINVITATION_ORDERBY_CHOICES_MAP[fieldName]
|
||||
orderBy = ''
|
||||
if i < len(sys.argv):
|
||||
orderBy = sys.argv[i].lower()
|
||||
if orderBy in SORTORDER_CHOICES_MAP:
|
||||
orderBy = SORTORDER_CHOICES_MAP[orderBy]
|
||||
i += 1
|
||||
if orderBy != 'DESCENDING':
|
||||
orderByList.append(fieldName)
|
||||
else:
|
||||
orderByList.append(f'{fieldName} desc')
|
||||
else:
|
||||
controlflow.expected_argument_exit(
|
||||
'orderby', ', '.join(sorted(USERINVITATION_ORDERBY_CHOICES_MAP)),
|
||||
fieldName)
|
||||
elif myarg == 'todrive':
|
||||
todrive = True
|
||||
i += 1
|
||||
else:
|
||||
controlflow.invalid_argument_exit(sys.argv[i],
|
||||
'gam print userinvitations')
|
||||
if orderByList:
|
||||
orderBy = ' '.join(orderByList)
|
||||
else:
|
||||
orderBy = None
|
||||
gam.printGettingAllItems('User Invitations', filter_)
|
||||
page_message = gapi.got_total_items_msg('User Invitations', '...\n')
|
||||
invitations = gapi.get_all_pages(svc.customers().userinvitations(),
|
||||
'list',
|
||||
'userInvitations',
|
||||
page_message=page_message,
|
||||
parent=customer,
|
||||
filter=filter_,
|
||||
orderBy=orderBy)
|
||||
for invitation in invitations:
|
||||
invitation['name'] = _reduce_name(invitation['name'])
|
||||
row = {}
|
||||
for key, val in invitation.items():
|
||||
if key not in titles:
|
||||
titles.append(key)
|
||||
row[key] = val
|
||||
rows.append(row)
|
||||
display.write_csv_file(rows, titles, 'User Invitations', todrive)
|
||||
@@ -1,5 +1,7 @@
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
import googleapiclient
|
||||
@@ -62,6 +64,8 @@ def issue_command():
|
||||
i += 1
|
||||
else:
|
||||
controlflow.invalid_argument_exit(sys.argv[i], 'gam issuecommand cros')
|
||||
if 'commandType' not in body:
|
||||
controlflow.missing_argument_exit('command <CrOSCommand>', 'gam issuecommand cros')
|
||||
if body['commandType'] == 'WIPE_USERS' and not doit:
|
||||
controlflow.system_error_exit(2, 'wipe_users command requires admin ' \
|
||||
'acknowledge user data will be destroyed with the ' \
|
||||
@@ -392,9 +396,10 @@ def doGetCrosInfo():
|
||||
temp_label = tempInfo['label'].strip()
|
||||
temperature = tempInfo['temperature']
|
||||
print(f' {temp_label}: {temperature}')
|
||||
pct_info = cpuStatusReport['cpuUtilizationPercentageInfo']
|
||||
util = ','.join([str(x) for x in pct_info])
|
||||
print(f' cpuUtilizationPercentageInfo: {util}')
|
||||
if 'cpuUtilizationPercentageInfo' in cpuStatusReport:
|
||||
pct_info = cpuStatusReport['cpuUtilizationPercentageInfo']
|
||||
util = ','.join([str(x) for x in pct_info])
|
||||
print(f' cpuUtilizationPercentageInfo: {util}')
|
||||
diskVolumeReports = cros.get('diskVolumeReports', [])
|
||||
lenDVR = len(diskVolumeReports)
|
||||
if lenDVR:
|
||||
@@ -829,16 +834,16 @@ def doPrintCrosDevices():
|
||||
if i < lenCSR:
|
||||
nrow['cpuStatusReports.reportTime'] = \
|
||||
cpuStatusReports[i]['reportTime']
|
||||
tempInfos = cpuStatusReports[i].get('cpuTemperatureInfo',
|
||||
[])
|
||||
tempInfos = cpuStatusReports[i].get('cpuTemperatureInfo', [])
|
||||
for tempInfo in tempInfos:
|
||||
label = tempInfo['label'].strip()
|
||||
base = 'cpuStatusReports.cpuTemperatureInfo.'
|
||||
nrow[f'{base}{label}'] = tempInfo['temperature']
|
||||
cpu_field = 'cpuUtilizationPercentageInfo'
|
||||
cpu_reports = cpuStatusReports[i][cpu_field]
|
||||
cpu_pcts = [str(x) for x in cpu_reports]
|
||||
nrow[f'cpuStatusReports.{cpu_field}'] = ','.join(cpu_pcts)
|
||||
if cpu_field in cpuStatusReports[i]:
|
||||
cpu_reports = cpuStatusReports[i][cpu_field]
|
||||
cpu_pcts = [str(x) for x in cpu_reports]
|
||||
nrow[f'cpuStatusReports.{cpu_field}'] = ','.join(cpu_pcts)
|
||||
if i < lenDVR:
|
||||
volumeInfo = diskVolumeReports[i]['volumeInfo']
|
||||
j = 0
|
||||
|
||||
@@ -8,18 +8,25 @@ from gam.gapi import directory as gapi_directory
|
||||
from gam.gapi import reports as gapi_reports
|
||||
|
||||
|
||||
def _get_customerid():
|
||||
customer = GC_Values[GC_CUSTOMER_ID]
|
||||
if customer != MY_CUSTOMER and customer[0] != 'C':
|
||||
customer = 'C' + customer
|
||||
return customer
|
||||
|
||||
def doGetCustomerInfo():
|
||||
cd = gapi_directory.build()
|
||||
customer_id = _get_customerid()
|
||||
customer_info = gapi.call(cd.customers(),
|
||||
'get',
|
||||
customerKey=GC_Values[GC_CUSTOMER_ID])
|
||||
customerKey=customer_id)
|
||||
print(f'Customer ID: {customer_info["id"]}')
|
||||
print(f'Primary Domain: {customer_info["customerDomain"]}')
|
||||
try:
|
||||
result = gapi.call(
|
||||
cd.domains(),
|
||||
'get',
|
||||
customer=customer_info['id'],
|
||||
customer=customer_id,
|
||||
domainName=customer_info['customerDomain'],
|
||||
fields='verified',
|
||||
throw_reasons=[gapi.errors.ErrorReason.DOMAIN_NOT_FOUND])
|
||||
@@ -35,7 +42,7 @@ def doGetCustomerInfo():
|
||||
domains = gapi.get_items(cd.domains(),
|
||||
'list',
|
||||
'domains',
|
||||
customer=GC_Values[GC_CUSTOMER_ID],
|
||||
customer=customer_id,
|
||||
fields='domains(creationTime)')
|
||||
for domain in domains:
|
||||
creation_timestamp = int(domain['creationTime']) / 1000
|
||||
@@ -57,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',
|
||||
@@ -67,9 +74,9 @@ def doGetCustomerInfo():
|
||||
}
|
||||
parameters = ','.join(list(user_counts_map))
|
||||
tryDate = datetime.date.today().strftime(YYYYMMDD_FORMAT)
|
||||
customerId = GC_Values[GC_CUSTOMER_ID]
|
||||
if customerId == MY_CUSTOMER:
|
||||
customerId = None
|
||||
reports_customer_id = customer_id
|
||||
if reports_customer_id == MY_CUSTOMER:
|
||||
reports_customer_id = None
|
||||
rep = gapi_reports.build()
|
||||
usage = None
|
||||
throw_reasons = [
|
||||
@@ -80,7 +87,7 @@ def doGetCustomerInfo():
|
||||
result = gapi.call(rep.customerUsageReports(),
|
||||
'get',
|
||||
throw_reasons=throw_reasons,
|
||||
customerId=customerId,
|
||||
customerId=reports_customer_id,
|
||||
date=tryDate,
|
||||
parameters=parameters)
|
||||
except gapi.errors.GapiInvalidError as e:
|
||||
@@ -111,6 +118,7 @@ def doGetCustomerInfo():
|
||||
def doUpdateCustomer():
|
||||
cd = gapi_directory.build()
|
||||
body = {}
|
||||
customer_id = _get_customerid()
|
||||
i = 3
|
||||
while i < len(sys.argv):
|
||||
myarg = sys.argv[i].lower().replace('_', '')
|
||||
@@ -136,14 +144,19 @@ def doUpdateCustomer():
|
||||
'update customer"')
|
||||
gapi.call(cd.customers(),
|
||||
'patch',
|
||||
customerKey=GC_Values[GC_CUSTOMER_ID],
|
||||
customerKey=customer_id,
|
||||
body=body)
|
||||
print('Updated customer')
|
||||
|
||||
|
||||
def setTrueCustomerId():
|
||||
if GC_Values[GC_CUSTOMER_ID] == MY_CUSTOMER:
|
||||
cd = gapi_directory.build()
|
||||
GC_Values[GC_CUSTOMER_ID] = gapi.call(cd.customers(), 'get',
|
||||
customerKey=GC_Values[GC_CUSTOMER_ID],
|
||||
fields='id').get('id', GC_Values[GC_CUSTOMER_ID])
|
||||
def setTrueCustomerId(cd=None):
|
||||
customer_id = GC_Values[GC_CUSTOMER_ID]
|
||||
if customer_id == MY_CUSTOMER:
|
||||
if not cd:
|
||||
cd = gapi_directory.build()
|
||||
result = gapi.call(cd.customers(),
|
||||
'get',
|
||||
customerKey=customer_id,
|
||||
fields='id')
|
||||
GC_Values[GC_CUSTOMER_ID] = result.get('id',
|
||||
customer_id)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -402,20 +402,18 @@ def getOrgUnitId(orgUnit, cd=None):
|
||||
return (orgUnit, result['orgUnitId'])
|
||||
|
||||
|
||||
def buildOrgUnitIdToNameMap():
|
||||
cd = gapi_directory.build()
|
||||
result = gapi.call(cd.orgunits(),
|
||||
'list',
|
||||
customerId=GC_Values[GC_CUSTOMER_ID],
|
||||
fields='organizationUnits(orgUnitPath,orgUnitId)',
|
||||
type='all')
|
||||
GM_Globals[GM_MAP_ORGUNIT_ID_TO_NAME] = {}
|
||||
for orgUnit in result['organizationUnits']:
|
||||
GM_Globals[GM_MAP_ORGUNIT_ID_TO_NAME][
|
||||
orgUnit['orgUnitId']] = orgUnit['orgUnitPath']
|
||||
|
||||
|
||||
def orgunit_from_orgunitid(orgunitid):
|
||||
if not GM_Globals[GM_MAP_ORGUNIT_ID_TO_NAME]:
|
||||
buildOrgUnitIdToNameMap()
|
||||
return GM_Globals[GM_MAP_ORGUNIT_ID_TO_NAME].get(orgunitid, orgunitid)
|
||||
def orgunit_from_orgunitid(orgunitid, cd=None):
|
||||
if cd is None:
|
||||
cd = gapi_directory.build()
|
||||
orgunitpath = GM_Globals[GM_MAP_ORGUNIT_ID_TO_NAME].get(orgunitid)
|
||||
if not orgunitpath:
|
||||
try:
|
||||
orgunitpath = gapi.call(cd.orgunits(),
|
||||
'get',
|
||||
customerId=GC_Values[GC_CUSTOMER_ID],
|
||||
orgUnitPath=f'id:{orgunitid}' if not orgunitid.startswith('id:') else orgunitid,
|
||||
fields='orgUnitPath')['orgUnitPath']
|
||||
except:
|
||||
orgunitpath = orgunitid
|
||||
GM_Globals[GM_MAP_ORGUNIT_ID_TO_NAME][orgunitid] = orgunitpath
|
||||
return orgunitpath
|
||||
|
||||
187
src/gam/gapi/directory/printers.py
Normal file
187
src/gam/gapi/directory/printers.py
Normal file
@@ -0,0 +1,187 @@
|
||||
'''Commands to manage directory printers.'''
|
||||
# pylint: disable=unused-wildcard-import wildcard-import
|
||||
|
||||
import sys
|
||||
|
||||
import gam
|
||||
from gam import controlflow
|
||||
from gam import display
|
||||
from gam import gapi
|
||||
from gam.var import *
|
||||
from gam.gapi import directory as gapi_directory
|
||||
from gam.gapi.directory import orgunits as gapi_directory_orgunits
|
||||
|
||||
|
||||
def _get_customerid():
|
||||
''' returns customer in "customers/C{customer}" format needed for this API'''
|
||||
customer = GC_Values[GC_CUSTOMER_ID]
|
||||
if customer != MY_CUSTOMER and customer[0] != 'C':
|
||||
customer = 'C' + customer
|
||||
return f'customers/{customer}'
|
||||
|
||||
def _get_printer_attributes(i, cdapi=None):
|
||||
'''get printer attributes for create/update commands'''
|
||||
body = {}
|
||||
while i < len(sys.argv):
|
||||
myarg = sys.argv[i].lower().replace('_', '')
|
||||
if myarg == 'description':
|
||||
body['description'] = sys.argv[i+1]
|
||||
i += 2
|
||||
elif myarg == 'displayname':
|
||||
body['displayName'] = sys.argv[i+1]
|
||||
i += 2
|
||||
elif myarg == 'makeandmodel':
|
||||
body['makeAndModel'] = sys.argv[i+1]
|
||||
i += 2
|
||||
elif myarg in ['ou', 'org', 'orgunit', 'orgunitid']:
|
||||
_, body['orgUnitId'] = gapi_directory_orgunits.getOrgUnitId(sys.argv[i+1], cdapi)
|
||||
body['orgUnitId'] = body['orgUnitId'][3:]
|
||||
i += 2
|
||||
elif myarg == 'uri':
|
||||
body['uri'] = sys.argv[i+1]
|
||||
i += 2
|
||||
elif myarg in {'driverless', 'usedriverlessconfig'}:
|
||||
body['useDriverlessConfig'] = True
|
||||
i += 1
|
||||
return body
|
||||
|
||||
|
||||
def create():
|
||||
'''gam create printer'''
|
||||
cdapi = gapi_directory.build()
|
||||
parent = _get_customerid()
|
||||
body = _get_printer_attributes(3, cdapi)
|
||||
result = gapi.call(cdapi.customers().chrome().printers(),
|
||||
'create',
|
||||
parent=parent,
|
||||
body=body)
|
||||
display.print_json(result)
|
||||
|
||||
|
||||
def delete():
|
||||
'''gam delete printer <PrinterIDList>|(file <FileName>)|(csvfile <FileName>:<FieldName>)'''
|
||||
cdapi = gapi_directory.build()
|
||||
customer_id = _get_customerid()
|
||||
printer_id = sys.argv[3]
|
||||
if printer_id.lower() not in {'file', 'csvfile'}:
|
||||
printer_ids = printer_id.replace(',', ' ').split()
|
||||
else:
|
||||
printer_ids = gam.getUsersToModify(f'cros{printer_id.lower()}', sys.argv[4])
|
||||
# max 50 per API call
|
||||
batch_size = 50
|
||||
for chunk in range(0, len(printer_ids), batch_size):
|
||||
body = {
|
||||
'printerIds': printer_ids[chunk:chunk + batch_size]
|
||||
}
|
||||
result = gapi.call(cdapi.customers().chrome().printers(),
|
||||
'batchDeletePrinters',
|
||||
parent=customer_id,
|
||||
body=body)
|
||||
for printer_id in result.get('printerIds', []):
|
||||
print(f'Deleted printer {printer_id}')
|
||||
for printer_id in result.get('failedPrinters', []):
|
||||
print(f'ERROR: failed to delete {printer_id.get("printerIds")}')
|
||||
|
||||
|
||||
def info():
|
||||
'''gam info printer'''
|
||||
cdapi = gapi_directory.build()
|
||||
customer = _get_customerid()
|
||||
printer_id = sys.argv[3]
|
||||
name = f'{customer}/chrome/printers/{printer_id}'
|
||||
printer = gapi.call(cdapi.customers().chrome().printers(),
|
||||
'get',
|
||||
name=name)
|
||||
if 'orgUnitId' in printer:
|
||||
printer['orgUnitPath'] = gapi_directory_orgunits.orgunit_from_orgunitid(
|
||||
printer['orgUnitId'], cdapi)
|
||||
display.print_json(printer)
|
||||
|
||||
|
||||
def print_():
|
||||
'''gam print printers'''
|
||||
cdapi = gapi_directory.build()
|
||||
parent = _get_customerid()
|
||||
filter_ = None
|
||||
todrive = False
|
||||
titles = []
|
||||
rows = []
|
||||
i = 3
|
||||
while i < len(sys.argv):
|
||||
myarg = sys.argv[i].lower()
|
||||
if myarg == 'filter':
|
||||
filter_ = sys.argv[i+1]
|
||||
i += 2
|
||||
elif myarg == 'todrive':
|
||||
todrive = True
|
||||
i += 1
|
||||
else:
|
||||
controlflow.invalid_argument_exit(sys.argv[i], 'gam print printermodels')
|
||||
printers = gapi.get_all_pages(cdapi.customers().chrome().printers(),
|
||||
'list',
|
||||
items='printers',
|
||||
parent=parent,
|
||||
filter=filter_)
|
||||
for printer in printers:
|
||||
if 'orgUnitId' in printer:
|
||||
printer['orgUnitPath'] = gapi_directory_orgunits.orgunit_from_orgunitid(
|
||||
printer['orgUnitId'], cdapi)
|
||||
row = {}
|
||||
for key, val in printer.items():
|
||||
if key not in titles:
|
||||
titles.append(key)
|
||||
row[key] = val
|
||||
rows.append(row)
|
||||
display.write_csv_file(rows, titles, 'Printers', todrive)
|
||||
|
||||
|
||||
def print_models():
|
||||
'''gam print printermodels'''
|
||||
cdapi = gapi_directory.build()
|
||||
parent = _get_customerid()
|
||||
filter_ = None
|
||||
todrive = False
|
||||
titles = []
|
||||
rows = []
|
||||
i = 3
|
||||
while i < len(sys.argv):
|
||||
myarg = sys.argv[i].lower()
|
||||
if myarg == 'filter':
|
||||
filter_ = sys.argv[i+1]
|
||||
i += 2
|
||||
elif myarg == 'todrive':
|
||||
todrive = True
|
||||
i += 1
|
||||
else:
|
||||
controlflow.invalid_argument_exit(sys.argv[i], 'gam print printermodels')
|
||||
models = gapi.get_all_pages(cdapi.customers().chrome().printers(),
|
||||
'listPrinterModels',
|
||||
items='printerModels',
|
||||
parent=parent,
|
||||
pageSize=10000,
|
||||
filter=filter_)
|
||||
for model in models:
|
||||
row = {}
|
||||
for key, val in model.items():
|
||||
if key not in titles:
|
||||
titles.append(key)
|
||||
row[key] = val
|
||||
rows.append(row)
|
||||
display.write_csv_file(rows, titles, 'Printer Models', todrive)
|
||||
|
||||
|
||||
def update():
|
||||
'''gam update printer'''
|
||||
cdapi = gapi_directory.build()
|
||||
customer = _get_customerid()
|
||||
printer_id = sys.argv[3]
|
||||
name = f'{customer}/chrome/printers/{printer_id}'
|
||||
body = _get_printer_attributes(4, cdapi)
|
||||
update_mask = ','.join(body)
|
||||
# note clearMask seems unnecessary. Updating field to '' clears it.
|
||||
result = gapi.call(cdapi.customers().chrome().printers(),
|
||||
'patch',
|
||||
name=name,
|
||||
updateMask=update_mask,
|
||||
body=body)
|
||||
display.print_json(result)
|
||||
@@ -1,3 +1,5 @@
|
||||
from time import sleep
|
||||
|
||||
import gam
|
||||
from gam import gapi
|
||||
from gam.gapi import directory as gapi_directory
|
||||
@@ -41,3 +43,22 @@ def turn_off_2sv(users):
|
||||
'turnOff',
|
||||
soft_errors=True,
|
||||
userKey=user)
|
||||
|
||||
def wait_for_mailbox(users):
|
||||
'''Wait until users mailbox is provisioned.'''
|
||||
cd = gapi_directory.build()
|
||||
i = 0
|
||||
count = len(users)
|
||||
for user in users:
|
||||
i += 1
|
||||
user = gam.normalizeEmailAddressOrUID(user)
|
||||
while True:
|
||||
result = gapi.call(cd.users(),
|
||||
'get',
|
||||
'fields=isMailboxSetup',
|
||||
userKey=user)
|
||||
mailbox_is_setup = result.get('isMailboxSetup')
|
||||
print(f'{user} mailboxIsSetup: {mailbox_is_setup}')
|
||||
if mailbox_is_setup:
|
||||
break
|
||||
sleep(3)
|
||||
|
||||
@@ -116,6 +116,7 @@ class ErrorReason(Enum):
|
||||
DUPLICATE = 'duplicate'
|
||||
FAILED_PRECONDITION = 'failedPrecondition'
|
||||
FORBIDDEN = 'forbidden'
|
||||
FIVE_O_THREE = '503'
|
||||
FOUR_O_NINE = '409'
|
||||
FOUR_O_O = '400'
|
||||
FOUR_O_THREE = '403'
|
||||
@@ -153,6 +154,7 @@ DEFAULT_RETRY_REASONS = [
|
||||
ErrorReason.GATEWAY_TIMEOUT,
|
||||
ErrorReason.INTERNAL_ERROR,
|
||||
ErrorReason.FOUR_TWO_NINE,
|
||||
ErrorReason.FIVE_O_THREE,
|
||||
]
|
||||
GMAIL_THROW_REASONS = [ErrorReason.SERVICE_NOT_AVAILABLE]
|
||||
GROUP_GET_THROW_REASONS = [
|
||||
|
||||
@@ -7,8 +7,17 @@ from gam import controlflow
|
||||
from gam import display
|
||||
from gam import gapi
|
||||
from gam.gapi import errors as gapi_errors
|
||||
from gam.gapi.directory import customer as gapi_directory_customer
|
||||
|
||||
|
||||
def _get_customerid():
|
||||
''' returns customerId with format C{customer_id}'''
|
||||
gapi_directory_customer.setTrueCustomerId()
|
||||
customer_id = GC_Values[GC_CUSTOMER_ID]
|
||||
if customer_id[0] != 'C':
|
||||
customer_id = 'C' + customer_id
|
||||
return customer_id
|
||||
|
||||
def build():
|
||||
return gam.buildGAPIObject('licensing')
|
||||
|
||||
@@ -127,6 +136,7 @@ def print_(returnFields=None,
|
||||
countsOnly=False,
|
||||
returnCounts=False):
|
||||
lic = build()
|
||||
customer_id = _get_customerid()
|
||||
products = []
|
||||
licenses = []
|
||||
licenseCounts = []
|
||||
@@ -193,7 +203,7 @@ def print_(returnFields=None,
|
||||
gapi_errors.ErrorReason.FORBIDDEN
|
||||
],
|
||||
page_message=page_message,
|
||||
customerId=GC_Values[GC_DOMAIN],
|
||||
customerId=customer_id,
|
||||
productId=product,
|
||||
skuId=sku,
|
||||
fields=fields)
|
||||
@@ -223,7 +233,7 @@ def print_(returnFields=None,
|
||||
gapi_errors.ErrorReason.FORBIDDEN
|
||||
],
|
||||
page_message=page_message,
|
||||
customerId=GC_Values[GC_DOMAIN],
|
||||
customerId=customer_id,
|
||||
productId=productId,
|
||||
fields=fields)
|
||||
if countsOnly:
|
||||
|
||||
@@ -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):
|
||||
@@ -59,6 +100,16 @@ class _DeHTMLParser(HTMLParser):
|
||||
re.sub(r'\n +', '\n', ''.join(self.__text))).strip()
|
||||
|
||||
|
||||
def commonprefix(m):
|
||||
'''Given a list of strings m, return string which is prefix common to all'''
|
||||
s1 = min(m)
|
||||
s2 = max(m)
|
||||
for i, c in enumerate(s1):
|
||||
if c != s2[i]:
|
||||
return s1[:i]
|
||||
return s1
|
||||
|
||||
|
||||
def dehtml(text):
|
||||
try:
|
||||
parser = _DeHTMLParser()
|
||||
|
||||
@@ -8,7 +8,7 @@ import platform
|
||||
import re
|
||||
|
||||
GAM_AUTHOR = 'Jay Lee <jay0lee@gmail.com>'
|
||||
GAM_VERSION = '5.32'
|
||||
GAM_VERSION = '6.01'
|
||||
GAM_LICENSE = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
|
||||
|
||||
GAM_URL = 'https://git.io/gam'
|
||||
@@ -59,12 +59,42 @@ SKUS = {
|
||||
'1010310002': {
|
||||
'product': '101031',
|
||||
'aliases': ['gsefe', 'e4e', 'gsuiteenterpriseeducation'],
|
||||
'displayName': 'G Suite Enterprise for Education'
|
||||
'displayName': 'Google Workspace for Education Plus - Legacy'
|
||||
},
|
||||
'1010310003': {
|
||||
'product': '101031',
|
||||
'aliases': ['gsefes', 'e4es', 'gsuiteenterpriseeducationstudent'],
|
||||
'displayName': 'G Suite Enterprise for Education (Student)'
|
||||
'displayName': 'Google Workspace for Education Plus - Legacy (Student)'
|
||||
},
|
||||
'1010310005': {
|
||||
'product': '101031',
|
||||
'aliases': ['gwes', 'workspaceeducationstandard'],
|
||||
'displayName': 'Google Workspace for Education Standard'
|
||||
},
|
||||
'1010310006': {
|
||||
'product': '101031',
|
||||
'aliases': ['gwesstaff', 'workspaceeducationstandardstaff'],
|
||||
'displayName': 'Google Workspace for Education Standard (Staff)'
|
||||
},
|
||||
'1010310007': {
|
||||
'product': '101031',
|
||||
'aliases': ['gwesstudent', 'workspaceeducationstandardstudent'],
|
||||
'displayName': 'Google Workspace for Education Standard (Extra Student)'
|
||||
},
|
||||
'1010310008': {
|
||||
'product': '101031',
|
||||
'aliases': ['gwep', 'workspaceeducationplus'],
|
||||
'displayName': 'Google Workspace for Education Plus'
|
||||
},
|
||||
'1010310009': {
|
||||
'product': '101031',
|
||||
'aliases': ['gwepstaff', 'workspaceeducationplusstaff'],
|
||||
'displayName': 'Google Workspace for Education Plus (Staff)'
|
||||
},
|
||||
'1010310010': {
|
||||
'product': '101031',
|
||||
'aliases': ['gwepstudent', 'workspaceeducationplusstudent'],
|
||||
'displayName': 'Google Workspace for Education Plus (Extra Student)'
|
||||
},
|
||||
'1010330003': {
|
||||
'product': '101033',
|
||||
@@ -81,6 +111,11 @@ SKUS = {
|
||||
'aliases': ['gvpremier', 'voicepremier', 'googlevoicepremier'],
|
||||
'displayName': 'Google Voice Premier'
|
||||
},
|
||||
'1010370001': {
|
||||
'product': '101037',
|
||||
'aliases': ['gwetlu', 'workspaceeducationupgrade'],
|
||||
'displayName': 'Google Workspace for Education: Teaching and Learning Upgrade'
|
||||
},
|
||||
'Google-Apps': {
|
||||
'product': 'Google-Apps',
|
||||
'aliases': ['standard', 'free'],
|
||||
@@ -153,6 +188,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'],
|
||||
@@ -228,9 +268,10 @@ SKUS = {
|
||||
PRODUCTID_NAME_MAPPINGS = {
|
||||
'101001': 'Cloud Identity Free',
|
||||
'101005': 'Cloud Identity Premium',
|
||||
'101031': 'G Suite Enterprise for Education',
|
||||
'101031': 'G Suite Workspace for Education',
|
||||
'101033': 'Google Voice',
|
||||
'101034': 'G Suite Archived',
|
||||
'101037': 'G Suite Workspace for Education',
|
||||
'Google-Apps': 'Google Workspace',
|
||||
'Google-Chrome-Device-Management': 'Google Chrome Device Management',
|
||||
'Google-Drive-storage': 'Google Drive Storage',
|
||||
@@ -261,6 +302,8 @@ API_VER_MAPPING = {
|
||||
'driveactivity': 'v2',
|
||||
'calendar': 'v3',
|
||||
'cbcm': 'v1.1beta1',
|
||||
'chromemanagement': 'v1',
|
||||
'chromepolicy': 'v1',
|
||||
'classroom': 'v1',
|
||||
'cloudidentity': 'v1',
|
||||
'cloudidentity_beta': 'v1beta1',
|
||||
@@ -286,6 +329,7 @@ API_VER_MAPPING = {
|
||||
'siteVerification': 'v1',
|
||||
'storage': 'v1',
|
||||
'vault': 'v1',
|
||||
'versionhistory': 'v1',
|
||||
}
|
||||
|
||||
USERINFO_EMAIL_SCOPE = 'https://www.googleapis.com/auth/userinfo.email'
|
||||
@@ -298,6 +342,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': [
|
||||
@@ -344,16 +389,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 = {
|
||||
@@ -1143,7 +1193,7 @@ GM_Globals = {
|
||||
GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID: None,
|
||||
GM_ENABLEDASA_TXT: '',
|
||||
GM_LAST_UPDATE_CHECK_TXT: '',
|
||||
GM_MAP_ORGUNIT_ID_TO_NAME: None,
|
||||
GM_MAP_ORGUNIT_ID_TO_NAME: {},
|
||||
GM_MAP_ROLE_ID_TO_NAME: None,
|
||||
GM_MAP_ROLE_NAME_TO_ID: None,
|
||||
GM_MAP_USER_ID_TO_NAME: None,
|
||||
|
||||
@@ -2,6 +2,8 @@ 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
|
||||
contacts.googleapis.com
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
cryptography
|
||||
distro; sys_platform == 'linux'
|
||||
filelock
|
||||
google-api-python-client>=1.7.10
|
||||
google-api-python-client==2.0.2
|
||||
google-auth-httplib2
|
||||
google-auth-oauthlib>=0.4.1
|
||||
google-auth>=1.11.2
|
||||
httplib2>=0.17.0
|
||||
passlib>=1.7.2; sys_platform == 'win32'
|
||||
passlib>=1.7.2
|
||||
python-dateutil
|
||||
yubikey-manager>=4.0.0
|
||||
|
||||
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