mirror of
https://github.com/GAM-team/GAM.git
synced 2026-06-05 06:41:38 +00:00
Compare commits
89 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91cddd72e5 | ||
|
|
8a1f0c9dbf | ||
|
|
e3e5318b4f | ||
|
|
b060664c9f | ||
|
|
83fbf0e8ac | ||
|
|
537a926618 | ||
|
|
f791a59b1d | ||
|
|
0b8e41f993 | ||
|
|
f540fa2a38 | ||
|
|
2d7bc2f34a | ||
|
|
c2dea0a4d7 | ||
|
|
42cbfbf8ed | ||
|
|
137e79b012 | ||
|
|
5849ed3ecc | ||
|
|
d3dc1e1197 | ||
|
|
c20f0bef44 | ||
|
|
c572b6b182 | ||
|
|
a1392dbf86 | ||
|
|
4e719bab5e | ||
|
|
34b51ea64a | ||
|
|
5a2a72f530 | ||
|
|
2ea80c41ab | ||
|
|
6f987958e8 | ||
|
|
ae4007aad5 | ||
|
|
c4401f8bd4 | ||
|
|
0e7472de50 | ||
|
|
e998c78609 | ||
|
|
c30b92cd38 | ||
|
|
2bf2d2aef7 | ||
|
|
cdc04b0803 | ||
|
|
5f5875acc1 | ||
|
|
d306c5e0a3 | ||
|
|
19a815cffe | ||
|
|
da0c559293 | ||
|
|
a2c91ef7b3 | ||
|
|
722b94ca32 | ||
|
|
299742fe03 | ||
|
|
3964cbf911 | ||
|
|
63e4947ad5 | ||
|
|
e3cb13a414 | ||
|
|
01fec79d78 | ||
|
|
a7043a1359 | ||
|
|
91a93ecd62 | ||
|
|
c52fdf6395 | ||
|
|
1d1dad4b30 | ||
|
|
f07a57e478 | ||
|
|
ebacd9b4b4 | ||
|
|
f010e59597 | ||
|
|
a184d7a8e0 | ||
|
|
807f54c549 | ||
|
|
24684abc1d | ||
|
|
1f1a49976c | ||
|
|
562fda3079 | ||
|
|
05642f3c14 | ||
|
|
251e2774aa | ||
|
|
2089589d34 | ||
|
|
c48b135c43 | ||
|
|
70121a6ebf | ||
|
|
c23e53585a | ||
|
|
89e964163e | ||
|
|
0357774ba6 | ||
|
|
93cf750249 | ||
|
|
b712f7a344 | ||
|
|
4159a5cbb8 | ||
|
|
2e78a291d4 | ||
|
|
3f1705c2a5 | ||
|
|
bb1f5f7059 | ||
|
|
75b7d0c419 | ||
|
|
41a6c11c55 | ||
|
|
57d908e369 | ||
|
|
64274fdb33 | ||
|
|
da919fd189 | ||
|
|
cfa25f12d3 | ||
|
|
05bc1c1263 | ||
|
|
939c79c37f | ||
|
|
d352ddeea1 | ||
|
|
72a683f2b1 | ||
|
|
784399f345 | ||
|
|
710be4371b | ||
|
|
eece358aec | ||
|
|
b43ada4f83 | ||
|
|
9030af4faf | ||
|
|
38b424b62e | ||
|
|
1d9bf0b1aa | ||
|
|
d3b7700c07 | ||
|
|
d9513e159f | ||
|
|
6ddfdf2514 | ||
|
|
478804bd5c | ||
|
|
b61165a753 |
3
.github/actions/decrypt.sh
vendored
3
.github/actions/decrypt.sh
vendored
@@ -13,4 +13,5 @@ fi
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="${PASSCODE}" \
|
||||
--output "${credsfile}" "${gpgfile}"
|
||||
|
||||
tar xf "${credsfile}" --directory "${gampath}"
|
||||
tar xvvf "${credsfile}" --directory "${gampath}"
|
||||
ls -l "${gampath}"
|
||||
|
||||
12
.github/actions/linux-install.sh
vendored
12
.github/actions/linux-install.sh
vendored
@@ -1,8 +1,10 @@
|
||||
export gampath="dist/gam"
|
||||
export distpath="dist/"
|
||||
export gampath="${distpath}gam"
|
||||
rm -rf $gampath
|
||||
mkdir -p $gampath
|
||||
export gampath=$(readlink -e $gampath)
|
||||
$python -OO -m PyInstaller --clean --noupx --strip -F --distpath $gampath gam.spec
|
||||
#mkdir -p $gampath
|
||||
#export gampath=$(readlink -e $gampath)
|
||||
$pip install wheel
|
||||
$python -OO -m PyInstaller --clean --noupx --strip --distpath $gampath gam.spec
|
||||
export gam="${gampath}/gam"
|
||||
export GAMVERSION=`$gam version simple`
|
||||
cp LICENSE $gampath
|
||||
@@ -11,7 +13,7 @@ this_glibc_ver=$(ldd --version | awk '/ldd/{print $NF}')
|
||||
GAM_ARCHIVE="gam-${GAMVERSION}-${GAMOS}-${PLATFORM}-glibc${this_glibc_ver}.tar.xz"
|
||||
rm $gampath/lastupdatecheck.txt
|
||||
# tar will cd to dist and tar up gam/
|
||||
tar -C dist/ --create --file $GAM_ARCHIVE --xz gam
|
||||
tar -C ${distpath} --create --file $GAM_ARCHIVE --xz gam
|
||||
echo "PyInstaller GAM info:"
|
||||
du -h $gam
|
||||
time $gam version extended
|
||||
|
||||
2
.github/actions/macos-before-install.sh
vendored
2
.github/actions/macos-before-install.sh
vendored
@@ -25,7 +25,7 @@ cd ~
|
||||
if [ "$PLATFORM" == "x86_64" ]; then
|
||||
export pyfile=python-$BUILD_PYTHON_VERSION-macosx10.9.pkg
|
||||
else
|
||||
export pyfile=python-$BUILD_PYTHON_VERSION-macos11.0.pkg
|
||||
export pyfile=python-$BUILD_PYTHON_VERSION-macos11.pkg
|
||||
fi
|
||||
|
||||
wget https://www.python.org/ftp/python/$BUILD_PYTHON_VERSION/$pyfile
|
||||
|
||||
19
.github/actions/macos-install.sh
vendored
19
.github/actions/macos-install.sh
vendored
@@ -1,22 +1,19 @@
|
||||
echo "MacOS Version Info According to Python:"
|
||||
python -c "import platform; print(platform.mac_ver())"
|
||||
echo "Xcode versionn:"
|
||||
macver=$(python -c "import platform; print(platform.mac_ver()[0])")
|
||||
echo $macver
|
||||
echo "Xcode version:"
|
||||
xcodebuild -version
|
||||
export gampath=dist/gam
|
||||
export distpath="dist/"
|
||||
export gampath="${distpath}gam"
|
||||
rm -rf $gampath
|
||||
if [ "$PLATFORM" == "x86_64" ]; then
|
||||
export specfile="gam.spec"
|
||||
else
|
||||
export specfile="gam-universal2.spec"
|
||||
fi
|
||||
$python -OO -m PyInstaller --clean --noupx --strip -F --distpath "${gampath}" "${specfile}"
|
||||
export specfile="gam.spec"
|
||||
$python -OO -m PyInstaller --clean --noupx --strip --distpath "${gampath}" --target-architecture $PLATFORM "${specfile}"
|
||||
export gam="${gampath}/gam"
|
||||
$gam version extended
|
||||
export GAMVERSION=`$gam version simple`
|
||||
cp LICENSE "${gampath}"
|
||||
cp GamCommands.txt "${gampath}"
|
||||
MACOSVERSION=$(defaults read loginwindow SystemVersionStampAsString)
|
||||
GAM_ARCHIVE="gam-${GAMVERSION}-${GAMOS}-${PLATFORM}-MacOS${MACOSVERSION}.tar.xz"
|
||||
GAM_ARCHIVE="gam-${GAMVERSION}-${GAMOS}-${PLATFORM}.tar.xz"
|
||||
rm "${gampath}/lastupdatecheck.txt"
|
||||
# tar will cd to dist/ and tar up gam/
|
||||
tar -C dist/ --create --file $GAM_ARCHIVE --xz gam
|
||||
|
||||
16
.github/actions/windows-install.sh
vendored
16
.github/actions/windows-install.sh
vendored
@@ -4,11 +4,12 @@ elif [[ "$PLATFORM" == "x86" ]]; then
|
||||
export WIX_BITS="x86"
|
||||
fi
|
||||
echo "compiling GAM with pyinstaller..."
|
||||
export gampath="dist/gam"
|
||||
export distpath="dist/"
|
||||
export gampath="${distpath}gam"
|
||||
rm -rf $gampath
|
||||
mkdir -p $gampath
|
||||
export gampath=$(readlink -e $gampath)
|
||||
pyinstaller --clean --noupx -F --distpath $gampath gam.spec
|
||||
#mkdir -p $gampath
|
||||
#export gampath=$(readlink -e $gampath)
|
||||
pyinstaller --clean --noupx --distpath $gampath gam.spec
|
||||
export gam="${gampath}/gam"
|
||||
echo "running compiled GAM..."
|
||||
$gam version
|
||||
@@ -18,8 +19,11 @@ cp LICENSE $gampath
|
||||
cp GamCommands.txt $gampath
|
||||
cp gam-setup.bat $gampath
|
||||
GAM_ARCHIVE=gam-$GAMVERSION-$GAMOS-$PLATFORM.zip
|
||||
/c/Program\ Files/7-Zip/7z.exe a -tzip $GAM_ARCHIVE $gampath -xr!.svn
|
||||
|
||||
cwd=$(pwd)
|
||||
cd "${distpath}"
|
||||
/c/Program\ Files/7-Zip/7z.exe a -tzip $GAM_ARCHIVE gam -xr!.svn
|
||||
mv "${GAM_ARCHIVE}" "${cwd}"
|
||||
cd "${cwd}"
|
||||
echo "Running WIX candle $WIX_BITS..."
|
||||
/c/Program\ Files\ \(x86\)/WiX\ Toolset\ v3.11/bin/candle.exe -arch $WIX_BITS gam.wxs
|
||||
echo "Done with WIX candle..."
|
||||
|
||||
76
.github/workflows/build.yml
vendored
76
.github/workflows/build.yml
vendored
@@ -12,13 +12,13 @@ defaults:
|
||||
working-directory: src
|
||||
|
||||
env:
|
||||
BUILD_PYTHON_VERSION: "3.9.4"
|
||||
MIN_PYTHON_VERSION: "3.9.4"
|
||||
BUILD_PYTHON_VERSION: "3.9.5"
|
||||
MIN_PYTHON_VERSION: "3.9.5"
|
||||
BUILD_OPENSSL_VERSION: "1.1.1k"
|
||||
MIN_OPENSSL_VERSION: "1.1.1i"
|
||||
MIN_OPENSSL_VERSION: "1.1.1k"
|
||||
PATCHELF_VERSION: "0.12"
|
||||
# PYINSTALLER_VERSION can be full commit hash or version like v4.20
|
||||
PYINSTALLER_VERSION: "227eac14955c02db21d4702429896d4b74beed5e"
|
||||
PYINSTALLER_VERSION: "000275e409640320cdd995a7f077abfdece86749"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -41,31 +41,21 @@ jobs:
|
||||
goal: "build"
|
||||
gamos: "linux"
|
||||
platform: "x86_64"
|
||||
# - os: [self-hosted, linux, ARM]
|
||||
# jid: 10
|
||||
# goal: "build"
|
||||
# gamos: "linux"
|
||||
# platform: "arm"
|
||||
# - os: [self-hosted, linux, ARM64]
|
||||
# jid: 11
|
||||
# goal: "build"
|
||||
# gamos: "linux"
|
||||
# platform: "arm64"
|
||||
- os: macos-10.15
|
||||
jid: 4
|
||||
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.4
|
||||
python: 3.9.5
|
||||
pyarch: "x64"
|
||||
platform: "x86_64"
|
||||
- os: windows-2019
|
||||
@@ -73,7 +63,7 @@ jobs:
|
||||
goal: "build"
|
||||
gamos: "windows"
|
||||
platform: "x86"
|
||||
python: 3.9.4
|
||||
python: 3.9.5
|
||||
pyarch: "x86"
|
||||
- os: ubuntu-20.04
|
||||
goal: "test"
|
||||
@@ -93,6 +83,12 @@ jobs:
|
||||
jid: 9
|
||||
gamos: "linux"
|
||||
platform: "x86_64"
|
||||
- os: ubuntu-20.04
|
||||
goal: test
|
||||
python: "3.10.0-beta.1"
|
||||
jid: 10
|
||||
gamos: linux
|
||||
platform: x86_64
|
||||
|
||||
steps:
|
||||
|
||||
@@ -108,7 +104,7 @@ jobs:
|
||||
path: |
|
||||
~/python
|
||||
~/ssl
|
||||
key: ${{ matrix.os }}-${{ matrix.jid }}-20210407
|
||||
key: ${{ matrix.os }}-${{ matrix.jid }}-20210611
|
||||
|
||||
- name: Set env variables
|
||||
env:
|
||||
@@ -145,6 +141,7 @@ jobs:
|
||||
echo "RUNNING: apt update..."
|
||||
sudo apt-get -qq --yes update > /dev/null
|
||||
sudo apt-get -qq --yes install swig libpcsclite-dev
|
||||
$pip install --upgrade pip
|
||||
|
||||
- name: Build and install Python, OpenSSL and PyInstaller
|
||||
if: matrix.goal != 'test' && steps.cache-primes.outputs.cache-hit != 'true'
|
||||
@@ -156,18 +153,24 @@ 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"
|
||||
$pip install wheel
|
||||
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
|
||||
cd "pyinstaller-${PYINSTALLER_VERSION}/bootloader"
|
||||
if [ "${PLATFORM}" == "x86" ]; then
|
||||
BITS="32"
|
||||
else
|
||||
BITS="64"
|
||||
cd "pyinstaller-${PYINSTALLER_VERSION}/"
|
||||
if [ $GAMOS == "windows" ]; then
|
||||
# remove pre-compiled bootloaders so we fail if bootloader compile fails
|
||||
rm -rf PyInstaller/bootloader/*bit
|
||||
cd bootloader
|
||||
if [ "${PLATFORM}" == "x86" ]; then
|
||||
TARGETARCH="--target-arch=32bit"
|
||||
else
|
||||
TARGETARCH=""
|
||||
fi
|
||||
$python ./waf all $TARGETARCH
|
||||
cd ..
|
||||
fi
|
||||
$python ./waf all --target-arch=${BITS}bit
|
||||
cd ..
|
||||
$python setup.py install
|
||||
#$pip install pyinstaller
|
||||
|
||||
@@ -184,6 +187,7 @@ jobs:
|
||||
run: |
|
||||
set +e
|
||||
source ../.github/actions/${GAMOS}-install.sh
|
||||
ls -alRF $gampath
|
||||
echo "gampath=$gampath" >> $GITHUB_ENV
|
||||
echo "gam=$gam" >> $GITHUB_ENV
|
||||
echo -e "GAM: ${gam}\nGAMPATH: ${gampath}\nGAMVERSION: ${GAMVERSION}"
|
||||
@@ -225,6 +229,7 @@ jobs:
|
||||
$gam info domain
|
||||
$gam oauth refresh
|
||||
$gam info user
|
||||
#$gam info user $gam_user grouptree
|
||||
export tstamp=$(date +%s%3N)
|
||||
export newbase=gha-test-$JID-$tstamp
|
||||
export newuser=$newbase@pdl.jaylee.us
|
||||
@@ -251,6 +256,7 @@ jobs:
|
||||
$gam csv sample.csv gam user $gam_user sendemail recipient ~~email~~@pdl.jaylee.us subject "test message $newbase" message "GHA test message"
|
||||
$gam csv sample.csv gam update group $newgroup add member ~email
|
||||
$gam info group $newgroup
|
||||
$gam info cigroup $newgroup membertree
|
||||
$gam user $gam_user check serviceaccount
|
||||
# confirm mailbox is provisoned before continuing
|
||||
$gam user $newuser waitformailbox
|
||||
@@ -322,7 +328,7 @@ jobs:
|
||||
$gam report admin start -3d todrive
|
||||
$gam print devices nopersonaldevices nodeviceusers filter "serial:$JID$JID$JID$JID-" | $gam csv - gam delete device id ~name
|
||||
$gam print userinvitations
|
||||
$gam print userinvitations | $gam csv - gam create userinvitation ~name
|
||||
$gam print userinvitations | $gam csv - gam send userinvitation ~name
|
||||
export CUSTOMER_ID="C01wfv983"
|
||||
export GA_DOMAIN="pdl.jaylee.us"
|
||||
touch $gampath/enabledasa.txt
|
||||
@@ -342,3 +348,13 @@ jobs:
|
||||
echo "file uploaded as ${fileid}, setting ACL..."
|
||||
$gam user $gam_user add drivefileacl $fileid anyone role reader withlink
|
||||
done
|
||||
|
||||
- name: Archive production artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
if: github.event_name == 'push' && matrix.goal != 'test'
|
||||
with:
|
||||
name: gam-binaries
|
||||
path: |
|
||||
src/*.tar.xz
|
||||
src/*.zip
|
||||
src/*.msi
|
||||
|
||||
@@ -158,6 +158,7 @@ If an item contains spaces, it should be surrounded by ".
|
||||
<CalendarColorIndex> ::= <Number in range 1-24>
|
||||
<CalendarItem> ::= <EmailAddress>|<String>
|
||||
<ChatRoom> ::= <String>
|
||||
<ChatSpace> ::= <String>
|
||||
<ClientID> ::= <String>
|
||||
<ColorValue> ::= <ColorName>|<ColorHex>
|
||||
<CollaboratorItem> ::= <EmailAddress>|<UniqueID>|<String>
|
||||
@@ -223,6 +224,7 @@ 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
|
||||
<QueryTeamDrive> ::= <String> See: https://developers.google.com/drive/api/v3/search-shareddrives
|
||||
<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>
|
||||
@@ -685,7 +687,7 @@ Specify a collection of Users by directly specifying them or by specifiying item
|
||||
(name <String>)
|
||||
|
||||
<DriveFileAddAttribute> ::=
|
||||
(localfile <FileName>)|
|
||||
(localfile <FileName>|-)|
|
||||
(convert)|(ocr)|(ocrlanguage <Language>)|
|
||||
(restricted|restrict)|(starred|star)|(trashed|trash)|(viewed|view)|
|
||||
(contentrestrictions readonly false)|
|
||||
@@ -695,7 +697,7 @@ Specify a collection of Users by directly specifying them or by specifiying item
|
||||
(parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|writerscantshare|writerscanshare
|
||||
(shortcut <DriveFileID>)
|
||||
<DriveFileUpdateAttribute> ::=
|
||||
(localfile <FileName>)|
|
||||
(localfile <FileName>|-)|
|
||||
(convert)|(ocr)|(ocrlanguage <Language>)|
|
||||
(restricted|restrict <Boolean>)|(starred|star <Boolean>)|(trashed|trash <Boolean>)|(viewed|view <Boolean>)|
|
||||
(contentrestrictions readonly false)|
|
||||
@@ -894,23 +896,27 @@ gam delete resoldsubscription <CustomerID> <SKUID> cancel|downgrade|transfer_to_
|
||||
gam info resoldsubscriptions <CustomerID> [customer_auth_token <String>]
|
||||
|
||||
<ActivityApplicationName> ::=
|
||||
access|accesstransparency|
|
||||
access_transparency|
|
||||
admin|
|
||||
calendar|calendars|
|
||||
calendar|
|
||||
chat|
|
||||
drive|doc|docs|
|
||||
enterprisegroups|groupsenterprise|
|
||||
chrome|
|
||||
context_aware_access|
|
||||
data_studio|
|
||||
drive|
|
||||
gcp|
|
||||
google+|gplus|
|
||||
group|groups|
|
||||
hangoutsmeet|meet|
|
||||
gplus|
|
||||
groups|
|
||||
groups_enterprise|
|
||||
jamboard|
|
||||
login|logins|
|
||||
keep|
|
||||
login|
|
||||
meet|
|
||||
mobile|
|
||||
oauthtoken|token|tokens|
|
||||
rules|
|
||||
saml|
|
||||
useraccounts
|
||||
token|
|
||||
user_accounts
|
||||
|
||||
<ReportsApp> ::=
|
||||
accounts|
|
||||
@@ -1166,6 +1172,14 @@ gam print browsertokens [todrive]
|
||||
[fields <BrowserTokenFieldNameList>]
|
||||
[sortheaders]
|
||||
|
||||
gam print chatspaces [todrive]
|
||||
gam print chatmembers space <ChatSpace> [todrive]
|
||||
gam create chatmessage space <ChatSpace> [thread <String>]
|
||||
(text <String>)|(textfile <FileName> [charset <CharSet>])
|
||||
gam delete chatmessage name <String>
|
||||
gam update chatmessage name <String>
|
||||
(text <String>)|(textfile <FileName> [charset <CharSet>])
|
||||
|
||||
<CrOSAction> ::=
|
||||
deprovision_same_model_replace|
|
||||
deprovision_different_model_replace|
|
||||
@@ -1365,7 +1379,7 @@ gam update cigroup <GroupItem> sync [owner|manager|member] [notsuspended|suspend
|
||||
gam update cigroup <GroupItem> update [owner|manager|member] [notsuspended|suspended] [expires never|<Time>] <UserTypeEntity>
|
||||
gam update cigroup <GroupItem> clear [member] [manager] [owner] [notsuspended|suspended]
|
||||
gam delete cigroup <GroupItem>
|
||||
gam info cigroup <GroupItem> [nousers] [nojoindate] [showupdatedate]
|
||||
gam info cigroup <GroupItem> [nousers] [nojoindate] [showupdatedate] [membertree]
|
||||
|
||||
gam print cigroups [todrive]
|
||||
[enterprisemember <UserItem>]
|
||||
@@ -1438,7 +1452,7 @@ 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>]
|
||||
gam info user [<UserItem>] [noaliases] [nogroups] [nolicenses|nolicences] [noschemas] [schemas|custom <SchemaNameList>] [userview] [skus|sku <SKUIDList>] [grouptree]
|
||||
|
||||
Print fields for selected users; use domain, query/queries and deleted_only to select users to print;
|
||||
if none of these options are specified, all users are printed.
|
||||
@@ -1600,6 +1614,7 @@ gam <UserTypeEntity> update labelsettings <LabelName> [name <Name>] [messagelist
|
||||
gam <UserTypeEntity> update label|labels [search <RegularExpression>] [replace <LabelReplacement>] [merge]
|
||||
gam <UserTypeEntity> delete|del label|labels <LabelName>|regex:<RegularExpression>|--ALL_LABELS--
|
||||
gam <UserTypeEntity> show labels|label [onlyuser] [showcounts]
|
||||
gam <UserTypeEntity> print labels|label [todrive] [onlyuser] [showcounts]
|
||||
|
||||
gam <UserTypeEntity> delete messages query <QueryGmail> [doit] [max_to_delete|max_to_process <Number>]
|
||||
gam <UserTypeEntity> modify messages query <QueryGmail> [doit] [max_to_modify|max_to_process <Number>] (addlabel <LabelName>)* (removelabel <LabelName>)*
|
||||
@@ -1623,9 +1638,9 @@ gam <UserTypeEntity> sendemail [recipient|to <EmailAddress>] [from <EmailAddress
|
||||
[subject <String>] [(message <String>)|(file <FileName> [charset <Charset>])]
|
||||
(header <String> <String>)*
|
||||
|
||||
gam <UserTypeEntity> create|add delegate|delegates <EmailAddress>
|
||||
gam <UserTypeEntity> delegate|delegates to <EmailAddress>
|
||||
gam <UserTypeEntity> delete|del delegate|delegates <EmailAddress>
|
||||
gam <UserTypeEntity> create|add delegate|delegates [convertalias] <EmailAddress>
|
||||
gam <UserTypeEntity> delegate|delegates to [convertalias] <EmailAddress>
|
||||
gam <UserTypeEntity> delete|del delegate|delegates [convertalias] <EmailAddress>
|
||||
gam <UserTypeEntity> show delegates|delegate [csv]
|
||||
gam <UserTypeEntity> print delegates [todrive]
|
||||
|
||||
@@ -1690,8 +1705,8 @@ gam <UserTypeEntity> update teamdrive <TeamDriveID> [asadmin] [name <Name>]
|
||||
(<TeamDriveRestrictionsSubfieldName> <Boolean>)*
|
||||
gam <UserTypeEntity> delete teamdrive <TeamDriveID>
|
||||
gam <UserTypeEntity> show teamdriveinfo <TeamDriveID> [asadmin]
|
||||
gam <UserTypeEntity> show teamdrives [asadmin]
|
||||
gam <UserTypeEntity> print teamdrives [todrive] [asadmin]
|
||||
gam <UserTypeEntity> show teamdrives [query <QueryTeamDrive>] [asadmin]
|
||||
gam <UserTypeEntity> print teamdrives [query <QueryTeamDrive>] [todrive] [asadmin]
|
||||
gam <UserTypeEntity> show teamdrivethemes
|
||||
|
||||
gam <UserTypeEntity> vacation <FalseValues>
|
||||
|
||||
@@ -29,7 +29,7 @@ gamversion="latest"
|
||||
adminuser=""
|
||||
regularuser=""
|
||||
gam_glibc_vers="2.31 2.27 2.23"
|
||||
gam_macos_vers="10.15.6 10.14.6 10.13.6"
|
||||
#gam_macos_vers="10.15.6 10.14.6 10.13.6"
|
||||
|
||||
while getopts "hd:a:o:b:lp:u:r:v:" OPTION
|
||||
do
|
||||
@@ -128,18 +128,6 @@ case $gamos in
|
||||
this_macos_ver=$osversion
|
||||
fi
|
||||
echo "You are running MacOS $this_macos_ver"
|
||||
use_macos_ver=""
|
||||
for gam_macos_ver in $gam_macos_vers; do
|
||||
if version_gt $this_macos_ver $gam_macos_ver; then
|
||||
use_macos_ver="MacOS$gam_macos_ver"
|
||||
echo_green "Using GAM compiled on $use_macos_ver"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ "$use_macos_ver" == "" ]; then
|
||||
echo_red "Sorry, you need to be running at least MacOS $gam_macos_ver to run GAM"
|
||||
exit
|
||||
fi
|
||||
gamfile="macos-x86_64.tar.xz"
|
||||
;;
|
||||
MINGW64_NT*)
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
# -*- mode: python -*-
|
||||
|
||||
import sys
|
||||
|
||||
import importlib
|
||||
from PyInstaller.utils.hooks import copy_metadata
|
||||
|
||||
sys.modules['FixTk'] = None
|
||||
|
||||
# dynamically determine where httplib2/cacerts.txt lives
|
||||
proot = os.path.dirname(importlib.import_module('httplib2').__file__)
|
||||
extra_files = [(os.path.join(proot, 'cacerts.txt'), 'httplib2')]
|
||||
|
||||
extra_files += copy_metadata('google-api-python-client')
|
||||
extra_files += [('cbcm-v1.1beta1.json', '.')]
|
||||
|
||||
a = Analysis(['gam/__main__.py'],
|
||||
hiddenimports=[],
|
||||
hookspath=None,
|
||||
excludes=['FixTk', 'tcl', 'tk', '_tkinter', 'tkinter', 'Tkinter'],
|
||||
datas=extra_files,
|
||||
runtime_hooks=None)
|
||||
|
||||
for d in a.datas:
|
||||
if 'pyconfig' in d[0]:
|
||||
a.datas.remove(d)
|
||||
break
|
||||
|
||||
|
||||
pyz = PYZ(a.pure)
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
name='gam',
|
||||
debug=False,
|
||||
strip=None,
|
||||
upx=False,
|
||||
console=True )
|
||||
|
||||
app = BUNDLE(exe,
|
||||
name='gam.app',
|
||||
icon=None,
|
||||
bundle_identifier=None,
|
||||
info_plist={'LSArchitecturePriority': 'arm64,x86_64'})
|
||||
@@ -34,6 +34,7 @@ for d in a.datas:
|
||||
|
||||
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
@@ -43,4 +44,4 @@ exe = EXE(pyz,
|
||||
debug=False,
|
||||
strip=None,
|
||||
upx=False,
|
||||
console=True )
|
||||
console=True)
|
||||
|
||||
@@ -6,10 +6,14 @@ import configparser
|
||||
import csv
|
||||
import datetime
|
||||
from email import message_from_string
|
||||
try:
|
||||
from importlib.metadata import version as lib_version
|
||||
except ImportError:
|
||||
from importlib_metadata import version as lib_version
|
||||
import io
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
import pkg_resources
|
||||
import platform
|
||||
from pathlib import Path
|
||||
import random
|
||||
@@ -37,6 +41,7 @@ import googleapiclient.errors
|
||||
import googleapiclient.http
|
||||
import google.oauth2.service_account
|
||||
import httplib2
|
||||
from google.auth.jwt import Credentials as JWTCredentials
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
@@ -52,6 +57,7 @@ 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 chat as gapi_chat
|
||||
from gam.gapi import chromehistory as gapi_chromehistory
|
||||
from gam.gapi import chromemanagement as gapi_chromemanagement
|
||||
from gam.gapi import chromepolicy as gapi_chromepolicy
|
||||
@@ -618,7 +624,7 @@ TIME_OFFSET_UNITS = [('day', 86400), ('hour', 3600), ('minute', 60),
|
||||
('second', 1)]
|
||||
|
||||
|
||||
def getLocalGoogleTimeOffset(testLocation='www.googleapis.com'):
|
||||
def getLocalGoogleTimeOffset(testLocation='admin.googleapis.com'):
|
||||
localUTC = datetime.datetime.now(datetime.timezone.utc)
|
||||
try:
|
||||
# we disable SSL verify so we can still get time even if clock
|
||||
@@ -732,7 +738,7 @@ def getOSPlatform():
|
||||
|
||||
def doGAMVersion(checkForArgs=True):
|
||||
force_check = extended = simple = timeOffset = False
|
||||
testLocation = 'www.googleapis.com'
|
||||
testLocation = 'admin.googleapis.com'
|
||||
if checkForArgs:
|
||||
i = 2
|
||||
while i < len(sys.argv):
|
||||
@@ -760,8 +766,7 @@ def doGAMVersion(checkForArgs=True):
|
||||
return
|
||||
pyversion = platform.python_version()
|
||||
cpu_bits = struct.calcsize('P') * 8
|
||||
api_client_ver = pkg_resources.get_distribution(
|
||||
'google-api-python-client').version
|
||||
api_client_ver = lib_version('google-api-python-client')
|
||||
print(
|
||||
(f'GAM {GAM_VERSION} - {GAM_URL} - {GM_Globals[GM_GAM_TYPE]}\n'
|
||||
f'{GAM_AUTHOR}\n'
|
||||
@@ -783,6 +788,21 @@ def doGAMVersion(checkForArgs=True):
|
||||
doGAMCheckForUpdates(forceCheck=True)
|
||||
if extended:
|
||||
print(ssl.OPENSSL_VERSION)
|
||||
libs = ['cryptography',
|
||||
'filelock',
|
||||
'google-auth-httplib2',
|
||||
'google-auth-oauthlib',
|
||||
'google-auth',
|
||||
'httplib2',
|
||||
'passlib',
|
||||
'python-dateutil',
|
||||
'yubikey-manager',
|
||||
]
|
||||
for lib in libs:
|
||||
try:
|
||||
print(f'{lib} {lib_version(lib)}')
|
||||
except:
|
||||
pass
|
||||
tls_ver, cipher_name, used_ip = _getServerTLSUsed(testLocation)
|
||||
print(
|
||||
f'{testLocation} ({used_ip}) connects using {tls_ver} {cipher_name}'
|
||||
@@ -821,21 +841,33 @@ def _getSvcAcctData():
|
||||
controlflow.system_error_exit(6, None)
|
||||
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA] = json.loads(json_string)
|
||||
|
||||
|
||||
def getSvcAcctCredentials(scopes, act_as):
|
||||
jwt_apis = ['chat'] # APIs which can handle OAuthless JWT tokens
|
||||
def getSvcAcctCredentials(scopes, act_as, api=None):
|
||||
try:
|
||||
_getSvcAcctData()
|
||||
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)
|
||||
if act_as or api not in jwt_apis:
|
||||
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)
|
||||
else:
|
||||
audience = f'https://{api}.googleapis.com/'
|
||||
if sign_method == 'default':
|
||||
credentials = JWTCredentials.from_service_account_info(GM_Globals[GM_OAUTH2SERVICE_JSON_DATA],
|
||||
audience=audience)
|
||||
elif sign_method == 'yubikey':
|
||||
yksigner = yubikey.YubiKey(GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
|
||||
credentials = JWTCredentials._from_signer_and_info(yksigner,
|
||||
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA],
|
||||
audience=audience)
|
||||
credentials.project_id = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['project_id']
|
||||
GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID] = GM_Globals[
|
||||
GM_OAUTH2SERVICE_JSON_DATA]['client_id']
|
||||
return credentials
|
||||
@@ -1086,14 +1118,17 @@ def convertEmailAddressToUID(emailAddressOrUID, cd=None, email_type='user'):
|
||||
return normalizedEmailAddressOrUID
|
||||
|
||||
|
||||
def buildGAPIServiceObject(api, act_as, showAuthError=True):
|
||||
def buildGAPIServiceObject(api, act_as, showAuthError=True, scopes=None):
|
||||
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'])
|
||||
if scopes:
|
||||
GM_Globals[GM_CURRENT_API_SCOPES] = scopes
|
||||
else:
|
||||
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)
|
||||
act_as, api)
|
||||
request = transport.create_request(httpObj)
|
||||
retries = 3
|
||||
for n in range(1, retries + 1):
|
||||
@@ -1168,7 +1203,7 @@ def doCheckServiceAccount(users):
|
||||
time_status = test_fail
|
||||
printPassFail(
|
||||
MESSAGE_YOUR_SYSTEM_TIME_DIFFERS_FROM_GOOGLE_BY %
|
||||
('www.googleapis.com', nicetime), time_status)
|
||||
('admin.googleapis.com', nicetime), time_status)
|
||||
oa2 = getService('oauth2', transport.create_http())
|
||||
print('Service Account Private Key Authentication:')
|
||||
# We are explicitly not doing DwD here, just confirming service account can auth
|
||||
@@ -1414,7 +1449,13 @@ def addDelegates(users, i):
|
||||
if sys.argv[i].lower() != 'to':
|
||||
controlflow.missing_argument_exit('to', 'gam <users> delegate')
|
||||
i += 1
|
||||
convertAlias = False
|
||||
if sys.argv[i].lower().replace('_', '') == 'convertalias':
|
||||
convertAlias = True
|
||||
i += 1
|
||||
delegate = normalizeEmailAddressOrUID(sys.argv[i], noUid=True)
|
||||
if convertAlias:
|
||||
delegate = gapi_directory_users.get_primary(delegate)
|
||||
i = 0
|
||||
count = len(users)
|
||||
for delegator in users:
|
||||
@@ -1493,7 +1534,14 @@ def printShowDelegates(users, csvFormat):
|
||||
|
||||
|
||||
def deleteDelegate(users):
|
||||
delegate = normalizeEmailAddressOrUID(sys.argv[5], noUid=True)
|
||||
convertAlias = False
|
||||
i = 5
|
||||
if sys.argv[i].lower().replace('_', '') == 'convertalias':
|
||||
convertAlias = True
|
||||
i += 1
|
||||
delegate = normalizeEmailAddressOrUID(sys.argv[i], noUid=True)
|
||||
if convertAlias:
|
||||
delegate = gapi_directory_users.get_primary(delegate)
|
||||
i = 0
|
||||
count = len(users)
|
||||
for user in users:
|
||||
@@ -3569,14 +3617,19 @@ def getDriveFileAttribute(i, body, parameters, myarg, update=False):
|
||||
operation = 'update' if update else 'add'
|
||||
if myarg == 'localfile':
|
||||
parameters[DFA_LOCALFILEPATH] = sys.argv[i + 1]
|
||||
parameters[DFA_LOCALFILENAME] = os.path.basename(
|
||||
parameters[DFA_LOCALFILEPATH])
|
||||
body.setdefault('title', parameters[DFA_LOCALFILENAME])
|
||||
body['mimeType'] = mimetypes.guess_type(
|
||||
parameters[DFA_LOCALFILEPATH])[0]
|
||||
if body['mimeType'] is None:
|
||||
body['mimeType'] = 'application/octet-stream'
|
||||
parameters[DFA_LOCALMIMETYPE] = body['mimeType']
|
||||
if parameters[DFA_LOCALFILEPATH] != '-':
|
||||
parameters[DFA_LOCALFILENAME] = os.path.basename(
|
||||
parameters[DFA_LOCALFILEPATH])
|
||||
body.setdefault('title', parameters[DFA_LOCALFILENAME])
|
||||
body['mimeType'] = mimetypes.guess_type(
|
||||
parameters[DFA_LOCALFILEPATH])[0]
|
||||
if body['mimeType'] is None:
|
||||
body['mimeType'] = 'application/octet-stream'
|
||||
parameters[DFA_LOCALMIMETYPE] = body['mimeType']
|
||||
else:
|
||||
parameters[DFA_LOCALFILENAME] = '-'
|
||||
if body.get('mimeType') is None:
|
||||
body['mimeType'] = 'application/octet-stream'
|
||||
i += 2
|
||||
elif myarg == 'convert':
|
||||
parameters[DFA_CONVERT] = True
|
||||
@@ -3672,6 +3725,22 @@ def getDriveFileAttribute(i, body, parameters, myarg, update=False):
|
||||
return i
|
||||
|
||||
|
||||
def get_media_body(parameters, body):
|
||||
if parameters[DFA_LOCALFILEPATH] != '-':
|
||||
media_body = googleapiclient.http.MediaFileUpload(parameters[DFA_LOCALFILEPATH], mimetype=parameters[DFA_LOCALMIMETYPE], resumable=True)
|
||||
else:
|
||||
if body['mimeType'] == MIMETYPE_GA_SPREADSHEET:
|
||||
mimetype = 'text/csv'
|
||||
elif body['mimeType'] == MIMETYPE_GA_DOCUMENT:
|
||||
mimetype = 'text/plain'
|
||||
else:
|
||||
mimetype = 'application/octet-stream'
|
||||
media_body = googleapiclient.http.MediaIoBaseUpload(io.BytesIO(sys.stdin.buffer.read()), mimetype, resumable=True)
|
||||
if media_body.size() == 0:
|
||||
media_body = None
|
||||
return media_body
|
||||
|
||||
|
||||
def has_multiple_parents(body):
|
||||
return len(body.get('parents', [])) > 1
|
||||
|
||||
@@ -3714,6 +3783,8 @@ def doUpdateDriveFile(users):
|
||||
2,
|
||||
'you cannot specify multiple file identifiers. Choose one of id, drivefilename, query.'
|
||||
)
|
||||
if operation == 'update' and parameters[DFA_LOCALFILEPATH]:
|
||||
media_body = get_media_body(parameters, body)
|
||||
for user in users:
|
||||
user, drive = buildDriveGAPIObject(user)
|
||||
if not drive:
|
||||
@@ -3734,11 +3805,6 @@ def doUpdateDriveFile(users):
|
||||
print(f'No files to {operation} for {user}')
|
||||
continue
|
||||
if operation == 'update':
|
||||
if parameters[DFA_LOCALFILEPATH]:
|
||||
media_body = googleapiclient.http.MediaFileUpload(
|
||||
parameters[DFA_LOCALFILEPATH],
|
||||
mimetype=parameters[DFA_LOCALMIMETYPE],
|
||||
resumable=True)
|
||||
for fileId in fileIdSelection['fileIds']:
|
||||
if media_body:
|
||||
result = gapi.call(drive.files(),
|
||||
@@ -3804,6 +3870,8 @@ def createDriveFile(users):
|
||||
i += 1
|
||||
else:
|
||||
i = getDriveFileAttribute(i, body, parameters, myarg, False)
|
||||
if parameters[DFA_LOCALFILEPATH]:
|
||||
media_body = get_media_body(parameters, body)
|
||||
for user in users:
|
||||
user, drive = buildDriveGAPIObject(user)
|
||||
if not drive:
|
||||
@@ -3817,11 +3885,6 @@ def createDriveFile(users):
|
||||
if has_multiple_parents(body):
|
||||
sys.stderr.write(f"Multiple parents ({len(body['parents'])}) specified for {user}, only one is allowed.\n")
|
||||
continue
|
||||
if parameters[DFA_LOCALFILEPATH]:
|
||||
media_body = googleapiclient.http.MediaFileUpload(
|
||||
parameters[DFA_LOCALFILEPATH],
|
||||
mimetype=parameters[DFA_LOCALMIMETYPE],
|
||||
resumable=True)
|
||||
result = gapi.call(drive.files(),
|
||||
'insert',
|
||||
convert=parameters[DFA_CONVERT],
|
||||
@@ -5324,20 +5387,27 @@ def gmail_del_result(request_id, response, exception):
|
||||
print(exception)
|
||||
|
||||
|
||||
def showLabels(users):
|
||||
def printShowLabels(users, show=True):
|
||||
i = 5
|
||||
onlyUser = showCounts = False
|
||||
onlyUser = False
|
||||
showCounts = False
|
||||
todrive = False
|
||||
while i < len(sys.argv):
|
||||
myarg = sys.argv[i].lower().replace('_', '')
|
||||
if myarg == 'onlyuser':
|
||||
onlyUser = True
|
||||
i += 1
|
||||
elif myarg == 'todrive':
|
||||
todrive = True
|
||||
i += 1
|
||||
elif myarg == 'showcounts':
|
||||
showCounts = True
|
||||
i += 1
|
||||
else:
|
||||
controlflow.invalid_argument_exit(sys.argv[i],
|
||||
'gam <users> show labels')
|
||||
'gam <users> show labels')
|
||||
if not show:
|
||||
titles = ['email']
|
||||
for user in users:
|
||||
user, gmail = buildGmailGAPIObject(user)
|
||||
if not gmail:
|
||||
@@ -5345,28 +5415,45 @@ def showLabels(users):
|
||||
labels = gapi.call(gmail.users().labels(),
|
||||
'list',
|
||||
userId=user,
|
||||
soft_errors=True)
|
||||
if labels:
|
||||
for label in labels['labels']:
|
||||
if onlyUser and (label['type'] == 'system'):
|
||||
continue
|
||||
soft_errors=True).get('labels', [])
|
||||
i = 0
|
||||
for label in labels:
|
||||
i += 1
|
||||
if onlyUser and (label['type'] == 'system'):
|
||||
continue
|
||||
if showCounts:
|
||||
if i >= 50 and not i % 50:
|
||||
# show label get count for greater than 100 labels
|
||||
# every 100 labels
|
||||
sys.stderr.write('\r')
|
||||
sys.stderr.flush()
|
||||
sys.stderr.write(f'Getting counts for label {i} of {len(labels)}')
|
||||
counts = gapi.call(
|
||||
gmail.users().labels(),
|
||||
'get',
|
||||
userId=user,
|
||||
id=label['id'],
|
||||
fields=
|
||||
'messagesTotal,messagesUnread,threadsTotal,threadsUnread'
|
||||
)
|
||||
label.update(counts)
|
||||
if show:
|
||||
print(label['name'])
|
||||
for a_key in label:
|
||||
if a_key == 'name':
|
||||
continue
|
||||
print(f' {a_key}: {label[a_key]}')
|
||||
if showCounts:
|
||||
counts = gapi.call(
|
||||
gmail.users().labels(),
|
||||
'get',
|
||||
userId=user,
|
||||
id=label['id'],
|
||||
fields=
|
||||
'messagesTotal,messagesUnread,threadsTotal,threadsUnread'
|
||||
)
|
||||
for a_key in counts:
|
||||
print(f' {a_key}: {counts[a_key]}')
|
||||
print('')
|
||||
else:
|
||||
for key in label:
|
||||
if key not in titles:
|
||||
titles.append(key)
|
||||
label['email'] = user
|
||||
if not show:
|
||||
display.write_csv_file(labels,
|
||||
titles,
|
||||
'Gmail Labels',
|
||||
todrive)
|
||||
|
||||
|
||||
def showGmailProfile(users):
|
||||
@@ -7167,19 +7254,19 @@ def enableGAMProjectAPIs(GAMProjectAPIs,
|
||||
return status
|
||||
|
||||
|
||||
def _grantSARotateRights(iam, sa_email):
|
||||
print(f'Giving service account {sa_email} rights to rotate own private key')
|
||||
def _grantRotateRights(iam, service_account, email, account_type='serviceAccount'):
|
||||
print(f'Giving account {email} rights to rotate {service_account} private key')
|
||||
body = {
|
||||
'policy': {
|
||||
'bindings': [{
|
||||
'role': 'roles/iam.serviceAccountKeyAdmin',
|
||||
'members': [f'serviceAccount:{sa_email}']
|
||||
'members': [f'{account_type}:{email}']
|
||||
}]
|
||||
}
|
||||
}
|
||||
gapi.call(iam.projects().serviceAccounts(),
|
||||
'setIamPolicy',
|
||||
resource=f'projects/-/serviceAccounts/{sa_email}',
|
||||
resource=f'projects/-/serviceAccounts/{service_account}',
|
||||
body=body)
|
||||
|
||||
|
||||
@@ -7271,11 +7358,12 @@ def _createClientSecretsOauth2service(httpObj, projectId, login_hint):
|
||||
})
|
||||
GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID] = service_account[
|
||||
'uniqueId']
|
||||
sa_email = service_account['name'].rsplit('/', 1)[-1]
|
||||
doCreateOrRotateServiceAccountKeys(iam,
|
||||
project_id=service_account['projectId'],
|
||||
client_email=service_account['email'],
|
||||
client_id=service_account['uniqueId'])
|
||||
_grantSARotateRights(iam, service_account['name'].rsplit('/', 1)[-1])
|
||||
_grantRotateRights(iam, sa_email, sa_email)
|
||||
console_url = f'https://console.cloud.google.com/apis/credentials/oauthclient?project={projectId}'
|
||||
while True:
|
||||
print(f'''Please go to:
|
||||
@@ -7656,7 +7744,7 @@ def doUpdateProjects():
|
||||
iam = getService('iam', httpObj)
|
||||
_getSvcAcctData() # needed to read in GM_OAUTH2SERVICE_JSON_DATA
|
||||
sa_email = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['client_email']
|
||||
_grantSARotateRights(iam, sa_email)
|
||||
_grantRotateRights(iam, sa_email, sa_email)
|
||||
|
||||
|
||||
def _generatePrivateKeyAndPublicCert(client_id, key_size):
|
||||
@@ -7854,6 +7942,7 @@ def doCreateOrRotateServiceAccountKeys(iam=None,
|
||||
iam.projects().serviceAccounts().keys(),
|
||||
'upload',
|
||||
throw_reasons=throw_reasons,
|
||||
retry_reasons=[gapi_errors.ErrorReason.FOUR_O_THREE],
|
||||
name=sa_name,
|
||||
body={'publicKeyData': publicKeyData})
|
||||
break
|
||||
@@ -7878,6 +7967,7 @@ def doCreateOrRotateServiceAccountKeys(iam=None,
|
||||
result = gapi.call(iam.projects().serviceAccounts().keys(),
|
||||
'create',
|
||||
name=sa_name,
|
||||
retry_reasons=[gapi_errors.ErrorReason.FOUR_O_THREE],
|
||||
body=body)
|
||||
new_data_str = base64.b64decode(
|
||||
result['privateKeyData']).decode(UTF8)
|
||||
@@ -7903,6 +7993,7 @@ def doCreateOrRotateServiceAccountKeys(iam=None,
|
||||
print(f' Revoking existing key {keyName} for service account')
|
||||
gapi.call(iam.projects().serviceAccounts().keys(),
|
||||
'delete',
|
||||
retry_reasons=[gapi_errors.ErrorReason.FOUR_O_THREE],
|
||||
name=key['name'])
|
||||
if mode != 'retainnone':
|
||||
break
|
||||
@@ -8147,6 +8238,7 @@ def printShowTeamDrives(users, csvFormat):
|
||||
controlflow.invalid_argument_exit(
|
||||
myarg, f"gam {['show', 'print'][csvFormat]} teamdrives")
|
||||
tds = []
|
||||
titles = []
|
||||
for user in users:
|
||||
sys.stderr.write(f'Getting Team Drives for {user}\n')
|
||||
user, drive = buildDrive3GAPIObject(user)
|
||||
@@ -8162,20 +8254,21 @@ def printShowTeamDrives(users, csvFormat):
|
||||
if not results:
|
||||
continue
|
||||
for td in results:
|
||||
if 'id' not in td:
|
||||
continue
|
||||
if 'name' not in td:
|
||||
td['name'] = '<Unknown Team Drive>'
|
||||
this_td = {'id': td['id'], 'name': td['name']}
|
||||
if this_td in tds:
|
||||
continue
|
||||
tds.append({'id': td['id'], 'name': td['name']})
|
||||
td = utils.flatten_json(td)
|
||||
for key in td:
|
||||
if key not in titles:
|
||||
titles.append(key)
|
||||
tds.append(td)
|
||||
if csvFormat:
|
||||
titles = ['name', 'id']
|
||||
display.write_csv_file(tds, titles, 'Team Drives', todrive)
|
||||
else:
|
||||
for td in tds:
|
||||
print(f'Name: {td["name"]} ID: {td["id"]}')
|
||||
name = td.pop('name')
|
||||
my_id = td.pop('id')
|
||||
print(f'Name: {name} ID: {my_id}')
|
||||
display.print_json(td)
|
||||
print()
|
||||
|
||||
|
||||
|
||||
def doDeleteTeamDrive(users):
|
||||
@@ -8413,7 +8506,7 @@ def doWhatIs():
|
||||
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')
|
||||
sys.stderr.write(f'{email} is not a group alias...\n')
|
||||
if gapi_cloudidentity_userinvitations.is_invitable_user(email):
|
||||
sys.stderr.write(f'{email} is an unmanaged account\n\n')
|
||||
else:
|
||||
@@ -8676,7 +8769,11 @@ def doGetUserInfo(user_email=None):
|
||||
i = 4
|
||||
else:
|
||||
user_email = _get_admin_email()
|
||||
getSchemas = getAliases = getGroups = getLicenses = True
|
||||
getSchemas = True
|
||||
getAliases = True
|
||||
getGroups = True
|
||||
getCIGroups = False
|
||||
getLicenses = True
|
||||
projection = 'full'
|
||||
customFieldMask = viewType = None
|
||||
skus = sorted(SKUS)
|
||||
@@ -8688,6 +8785,10 @@ def doGetUserInfo(user_email=None):
|
||||
elif myarg == 'nogroups':
|
||||
getGroups = False
|
||||
i += 1
|
||||
elif myarg == 'grouptree':
|
||||
getCIGroups = True
|
||||
getGroups = False
|
||||
i += 1
|
||||
elif myarg in ['nolicenses', 'nolicences']:
|
||||
getLicenses = False
|
||||
i += 1
|
||||
@@ -8953,6 +9054,34 @@ def doGetUserInfo(user_email=None):
|
||||
print(f' {group["name"]} <{group["email"]}>')
|
||||
except gapi.errors.GapiForbiddenError:
|
||||
print('No access to show user groups.')
|
||||
elif getCIGroups:
|
||||
memberships = gapi_cloudidentity_groups.get_membership_graph(user_email)
|
||||
print('Group Membership Tree:')
|
||||
if memberships:
|
||||
group_name_mapping = {}
|
||||
group_displayname_mapping = {}
|
||||
groups = memberships.get('groups', [])
|
||||
for group in groups:
|
||||
group_name = group.get('name')
|
||||
group_key = group.get('groupKey', {})
|
||||
group_email = group_key.get('id', '')
|
||||
group_display_name = group.get('displayName', '')
|
||||
group_name_mapping[group_name] = group_email
|
||||
group_displayname_mapping[group_email] = group_display_name
|
||||
edges = []
|
||||
seen_group_count = {}
|
||||
for adj in memberships.get('adjacencyList', []):
|
||||
group_name = adj.get('group', '')
|
||||
group_email = group_name_mapping[group_name]
|
||||
for edge in adj.get('edges', []):
|
||||
seen_group_count[group_email] = seen_group_count.get(group_email, 0) + 1
|
||||
member_email = edge.get('memberKey', {}).get('id')
|
||||
edges.append((member_email, group_email))
|
||||
print_group_map(user_email, group_displayname_mapping, seen_group_count, edges, 3, 'direct')
|
||||
if seen_group_count and max(seen_group_count.values()) > 1:
|
||||
print()
|
||||
print(' * user has multiple direct or inherited memberships in group')
|
||||
print()
|
||||
if getLicenses:
|
||||
print('Licenses:')
|
||||
lic = buildGAPIObject('licensing')
|
||||
@@ -8968,6 +9097,15 @@ def doGetUserInfo(user_email=None):
|
||||
for user_license in user_licenses:
|
||||
print(f' {gapi_licensing._formatSKUIdDisplayName(user_license)}')
|
||||
|
||||
def print_group_map(parent, group_name_mappings, seen_group_count, edges, spaces, direction):
|
||||
for a_parent, a_child in edges:
|
||||
if a_parent == parent:
|
||||
group_display_name = group_name_mappings[a_child]
|
||||
output = f'{" " * spaces}{group_display_name} <{a_child}> ({direction})'
|
||||
if seen_group_count[a_child] > 1:
|
||||
output += ' *'
|
||||
print(output)
|
||||
print_group_map(a_child, group_name_mappings, seen_group_count, edges, spaces+2, 'inherited')
|
||||
|
||||
def doGetAliasInfo(alias_email=None):
|
||||
cd = buildGAPIObject('directory')
|
||||
@@ -11242,6 +11380,8 @@ def ProcessGAMCommand(args):
|
||||
gapi_cbcm.createtoken()
|
||||
elif argument in ['printer']:
|
||||
gapi_directory_printers.create()
|
||||
elif argument in ['chatmessage']:
|
||||
gapi_chat.create_message()
|
||||
else:
|
||||
controlflow.invalid_argument_exit(argument, 'gam create')
|
||||
sys.exit(0)
|
||||
@@ -11304,6 +11444,8 @@ def ProcessGAMCommand(args):
|
||||
gapi_chromepolicy.update_policy()
|
||||
elif argument in ['printer']:
|
||||
gapi_directory_printers.update()
|
||||
elif argument in ['chatmessage']:
|
||||
gapi_chat.update_message()
|
||||
else:
|
||||
controlflow.invalid_argument_exit(argument, 'gam update')
|
||||
sys.exit(0)
|
||||
@@ -11440,6 +11582,8 @@ def ProcessGAMCommand(args):
|
||||
gapi_directory_printers.delete()
|
||||
elif argument == 'chromepolicy':
|
||||
gapi_chromepolicy.delete_policy()
|
||||
elif argument == 'chatmessage':
|
||||
gapi_chat.delete_message()
|
||||
else:
|
||||
controlflow.invalid_argument_exit(argument, 'gam delete')
|
||||
sys.exit(0)
|
||||
@@ -11555,6 +11699,10 @@ def ProcessGAMCommand(args):
|
||||
gapi_chromemanagement.printVersions()
|
||||
elif argument in ['chromehistory']:
|
||||
gapi_chromehistory.printHistory()
|
||||
elif argument in ['chatspaces']:
|
||||
gapi_chat.print_spaces()
|
||||
elif argument in ['chatmembers']:
|
||||
gapi_chat.print_members()
|
||||
else:
|
||||
controlflow.invalid_argument_exit(argument, 'gam print')
|
||||
sys.exit(0)
|
||||
@@ -11726,7 +11874,7 @@ def ProcessGAMCommand(args):
|
||||
elif command == 'show':
|
||||
showWhat = sys.argv[4].lower()
|
||||
if showWhat in ['labels', 'label']:
|
||||
showLabels(users)
|
||||
printShowLabels(users)
|
||||
elif showWhat == 'profile':
|
||||
showProfile(users)
|
||||
elif showWhat == 'calendars':
|
||||
@@ -11815,6 +11963,8 @@ def ProcessGAMCommand(args):
|
||||
printShowTeamDrives(users, True)
|
||||
elif printWhat in ['contactdelegate', 'contactdelegates']:
|
||||
gapi_contactdelegation.print_(users, True)
|
||||
elif printWhat in ['labels']:
|
||||
printShowLabels(users, show=False)
|
||||
else:
|
||||
controlflow.invalid_argument_exit(printWhat,
|
||||
'gam <users> print')
|
||||
|
||||
@@ -8,6 +8,7 @@ from unittest.mock import patch
|
||||
from gam import SetGlobalVariables
|
||||
import gam.gapi as gapi
|
||||
from gam.gapi import errors
|
||||
import httplib2
|
||||
|
||||
|
||||
def create_http_error(status, reason, message):
|
||||
@@ -21,10 +22,10 @@ def create_http_error(status, reason, message):
|
||||
Returns:
|
||||
googleapiclient.errors.HttpError
|
||||
"""
|
||||
response = {
|
||||
response = httplib2.Response({
|
||||
'status': status,
|
||||
'content-type': 'application/json',
|
||||
}
|
||||
})
|
||||
content = {
|
||||
'error': {
|
||||
'code': status,
|
||||
|
||||
207
src/gam/gapi/chat.py
Normal file
207
src/gam/gapi/chat.py
Normal file
@@ -0,0 +1,207 @@
|
||||
import sys
|
||||
|
||||
import googleapiclient.errors
|
||||
|
||||
import gam
|
||||
from gam.var import *
|
||||
from gam import controlflow
|
||||
from gam import display
|
||||
from gam import fileutils
|
||||
from gam import gapi
|
||||
from gam import utils
|
||||
from gam.gapi import errors as gapi_errors
|
||||
|
||||
# Chat scope isn't in discovery doc so need to manually set
|
||||
CHAT_SCOPES = ['https://www.googleapis.com/auth/chat.bot']
|
||||
|
||||
|
||||
def build():
|
||||
return gam.buildGAPIServiceObject('chat',
|
||||
act_as=None,
|
||||
scopes=CHAT_SCOPES)
|
||||
|
||||
|
||||
THROW_REASONS = [
|
||||
gapi_errors.ErrorReason.FOUR_O_FOUR, # Chat API not configured
|
||||
]
|
||||
|
||||
def _chat_error_handler(chat, err):
|
||||
if err.status_code == 404:
|
||||
project_id = chat._http.credentials.project_id
|
||||
url = f'https://console.cloud.google.com/apis/api/chat.googleapis.com/hangouts-chat?project={project_id}'
|
||||
print('ERROR: you need to configure Google Chat for your API project. Please go to:')
|
||||
print()
|
||||
print(f' {url}')
|
||||
print()
|
||||
print('and complete all fields.')
|
||||
else:
|
||||
raise err
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def print_spaces():
|
||||
chat = build()
|
||||
todrive = False
|
||||
i =3
|
||||
while i < len(sys.argv):
|
||||
myarg = sys.argv[i].lower()
|
||||
if myarg == 'todrive':
|
||||
todrive = True
|
||||
i += 1
|
||||
else:
|
||||
controlflow.invalid_argument_exit(myarg, 'gam print chatspaces')
|
||||
try:
|
||||
spaces = gapi.get_all_pages(chat.spaces(), 'list', 'spaces', throw_reasons=THROW_REASONS)
|
||||
except googleapiclient.errors.HttpError as err:
|
||||
_chat_error_handler(chat, err)
|
||||
if not spaces:
|
||||
print('Bot not added to any Chat rooms or users yet.')
|
||||
else:
|
||||
display.write_csv_file(spaces, spaces[0].keys(), 'Chat Spaces', todrive)
|
||||
|
||||
|
||||
def print_members():
|
||||
chat = build()
|
||||
space = None
|
||||
todrive = False
|
||||
i = 3
|
||||
while i < len(sys.argv):
|
||||
myarg = sys.argv[i].lower()
|
||||
if myarg == 'space':
|
||||
space = sys.argv[i+1]
|
||||
if space[:7] != 'spaces/':
|
||||
space = f'spaces/{space}'
|
||||
i += 2
|
||||
elif myarg == 'todrive':
|
||||
todrive = True
|
||||
i += 1
|
||||
else:
|
||||
controlflow.invalid_argument_exit(myarg, "gam print chatmembers")
|
||||
if not space:
|
||||
controlflow.system_error_exit(2,
|
||||
'space <ChatSpace> is required.')
|
||||
try:
|
||||
results = gapi.get_all_pages(chat.spaces().members(), 'list', 'memberships', parent=space)
|
||||
except googleapiclient.errors.HttpError as err:
|
||||
_chat_error_handler(chat, err)
|
||||
members = []
|
||||
titles = []
|
||||
for result in results:
|
||||
member = utils.flatten_json(result)
|
||||
for key in member:
|
||||
if key not in titles:
|
||||
titles.append(key)
|
||||
members.append(utils.flatten_json(result))
|
||||
display.write_csv_file(members, titles, 'Chat Members', todrive)
|
||||
|
||||
|
||||
def create_message():
|
||||
chat = build()
|
||||
body = {}
|
||||
i = 3
|
||||
while i < len(sys.argv):
|
||||
myarg = sys.argv[i].lower()
|
||||
if myarg == 'text':
|
||||
body['text'] = sys.argv[i+1].replace('\\r', '\r').replace('\\n', '\n')
|
||||
i += 2
|
||||
elif myarg == 'textfile':
|
||||
filename = sys.argv[i + 1]
|
||||
i, encoding = gam.getCharSet(i + 2)
|
||||
body['text'] = fileutils.read_file(filename, encoding=encoding)
|
||||
elif myarg == 'space':
|
||||
space = sys.argv[i+1]
|
||||
if space[:7] != 'spaces/':
|
||||
space = f'spaces/{space}'
|
||||
i += 2
|
||||
elif myarg == 'thread':
|
||||
body['thread'] = {'name': sys.argv[i+1]}
|
||||
i += 2
|
||||
else:
|
||||
controlflow.invalid_argument_exit(myarg, "gam create chat")
|
||||
if not space:
|
||||
controlflow.system_error_exit(2,
|
||||
'space <ChatSpace> is required.')
|
||||
if 'text' not in body:
|
||||
controlflow.system_error_exit(2,
|
||||
'text <String> or textfile <FileName> is required.')
|
||||
if len(body['text']) > 4096:
|
||||
body['text'] = body['text'][:4095]
|
||||
print('WARNING: trimmed message longer than 4k to be 4k in length.')
|
||||
try:
|
||||
resp = gapi.call(chat.spaces().messages(),
|
||||
'create',
|
||||
parent=space,
|
||||
body=body,
|
||||
throw_reasons=THROW_REASONS)
|
||||
except googleapiclient.errors.HttpError as err:
|
||||
_chat_error_handler(chat, err)
|
||||
if 'thread' in body:
|
||||
print(f'responded to thread {resp["thread"]["name"]}')
|
||||
else:
|
||||
print(f'started new thread {resp["thread"]["name"]}')
|
||||
print(f'message {resp["name"]}')
|
||||
|
||||
def delete_message():
|
||||
chat = build()
|
||||
name = None
|
||||
i = 3
|
||||
while i < len(sys.argv):
|
||||
myarg = sys.argv[i].lower()
|
||||
if myarg == 'name':
|
||||
name = sys.argv[i+1]
|
||||
i += 2
|
||||
else:
|
||||
controlflow.invalid_argument_exit(myarg, "gam delete chat")
|
||||
if not name:
|
||||
controlflow.system_error_exit(2,
|
||||
'name <String> is required.')
|
||||
try:
|
||||
gapi.call(chat.spaces().messages(),
|
||||
'delete',
|
||||
name=name)
|
||||
except googleapiclient.errors.HttpError as err:
|
||||
_chat_error_handler(chat, err)
|
||||
|
||||
|
||||
def update_message():
|
||||
chat = build()
|
||||
body = {}
|
||||
name = None
|
||||
updateMask = 'text'
|
||||
i = 3
|
||||
while i < len(sys.argv):
|
||||
myarg = sys.argv[i].lower()
|
||||
if myarg == 'text':
|
||||
body['text'] = sys.argv[i+1].replace('\\r', '\r').replace('\\n', '\n')
|
||||
i += 2
|
||||
elif myarg == 'textfile':
|
||||
filename = sys.argv[i + 1]
|
||||
i, encoding = gam.getCharSet(i + 2)
|
||||
body['text'] = fileutils.read_file(filename, encoding=encoding)
|
||||
elif myarg == 'name':
|
||||
name = sys.argv[i+1]
|
||||
i += 2
|
||||
else:
|
||||
controlflow.invalid_argument_exit(myarg, "gam update chat")
|
||||
if not name:
|
||||
controlflow.system_error_exit(2,
|
||||
'name <String> is required.')
|
||||
if 'text' not in body:
|
||||
controlflow.system_error_exit(2,
|
||||
'text <String> or textfile <FileName> is required.')
|
||||
if len(body['text']) > 4096:
|
||||
body['text'] = body['text'][:4095]
|
||||
print('WARNING: trimmed message longer than 4k to be 4k in length.')
|
||||
try:
|
||||
resp = gapi.call(chat.spaces().messages(),
|
||||
'update',
|
||||
name=name,
|
||||
updateMask=updateMask,
|
||||
body=body)
|
||||
except googleapiclient.errors.HttpError as err:
|
||||
_chat_error_handler(chat, err)
|
||||
if 'thread' in body:
|
||||
print(f'updated response to thread {resp["thread"]["name"]}')
|
||||
else:
|
||||
print(f'updated message on thread {resp["thread"]["name"]}')
|
||||
print(f'message {resp["name"]}')
|
||||
@@ -98,10 +98,18 @@ def printshow_policies():
|
||||
for policy in sorted(policies, key=lambda k: k.get('value', {}).get('policySchema', '')):
|
||||
print()
|
||||
name = policy.get('value', {}).get('policySchema', '')
|
||||
schema = CHROME_SCHEMA_TYPE_MESSAGE.get(name)
|
||||
print(name)
|
||||
values = policy.get('value', {}).get('value', {})
|
||||
for setting, value in values.items():
|
||||
if isinstance(value, str) and value.find('_ENUM_') != -1:
|
||||
# Handle TYPE_MESSAGE fields with durations or counts as a special case
|
||||
if schema and setting == schema['casedField']:
|
||||
value = value.get(schema['type'], '')
|
||||
if value:
|
||||
if value.endswith('s'):
|
||||
value = value[:-1]
|
||||
value = int(value) // schema['scale']
|
||||
elif isinstance(value, str) and value.find('_ENUM_') != -1:
|
||||
value = value.split('_ENUM_')[-1]
|
||||
print(f' {setting}: {value}')
|
||||
|
||||
@@ -245,6 +253,25 @@ def delete_policy():
|
||||
gapi.call(svc.customers().policies().orgunits(), 'batchInherit', customer=customer, body=body)
|
||||
|
||||
|
||||
CHROME_SCHEMA_TYPE_MESSAGE = {
|
||||
'chrome.users.SessionLength':
|
||||
{'field': 'sessiondurationlimit', 'casedField': 'sessionDurationLimit',
|
||||
'type': 'duration', 'minVal': 1, 'maxVal': 1440, 'scale': 60},
|
||||
'chrome.users.BrowserSwitcherDelayDuration':
|
||||
{'field': 'browserswitcherdelayduration', 'casedField': 'browserSwitcherDelayDuration',
|
||||
'type': 'duration', 'minVal': 0, 'maxVal': 30, 'scale': 1},
|
||||
'chrome.users.MaxInvalidationFetchDelay':
|
||||
{'field': 'maxinvalidationfetchdelay', 'casedField': 'maxInvalidationFetchDelay',
|
||||
'type': 'duration', 'minVal': 1, 'maxVal': 30, 'scale': 1},
|
||||
'chrome.users.SecurityTokenSessionSettings':
|
||||
{'field': 'securitytokensessionnotificationseconds', 'casedField': 'securityTokenSessionNotificationSeconds',
|
||||
'type': 'duration', 'minVal': 0, 'maxVal': 9999, 'scale': 1},
|
||||
'chrome.users.PrintingMaxSheetsAllowed':
|
||||
{'field': 'printingmaxsheetsallowednullable', 'casedField': 'printingMaxSheetsAllowedNullable',
|
||||
'type': 'value', 'minVal': 1, 'maxVal': None, 'scale': 1},
|
||||
}
|
||||
|
||||
|
||||
def update_policy():
|
||||
svc = build()
|
||||
customer = _get_customerid()
|
||||
@@ -266,7 +293,8 @@ def update_policy():
|
||||
app_id = sys.argv[i+1]
|
||||
i += 2
|
||||
elif myarg in schemas:
|
||||
body['requests'].append({'policyValue': {'policySchema': schemas[myarg]['name'],
|
||||
schemaName = schemas[myarg]['name']
|
||||
body['requests'].append({'policyValue': {'policySchema': schemaName,
|
||||
'value': {}},
|
||||
'updateMask': ''})
|
||||
i += 1
|
||||
@@ -274,6 +302,19 @@ def update_policy():
|
||||
field = sys.argv[i].lower()
|
||||
if field in ['ou', 'org', 'orgunit', 'printerid', 'appid'] or '.' in field:
|
||||
break # field is actually a new policy, orgunit or app/printer id
|
||||
# Handle TYPE_MESSAGE fields with durations or counts as a special case
|
||||
schema = CHROME_SCHEMA_TYPE_MESSAGE.get(schemaName)
|
||||
if schema and field == schema['field']:
|
||||
casedField = schema['casedField']
|
||||
value = gam.getInteger(sys.argv[i+1], casedField,
|
||||
minVal=schema['minVal'], maxVal=schema['maxVal'])*schema['scale']
|
||||
if schema['type'] == 'duration':
|
||||
body['requests'][-1]['policyValue']['value'][casedField] = {schema['type']: f'{value}s'}
|
||||
else:
|
||||
body['requests'][-1]['policyValue']['value'][casedField] = {schema['type']: value}
|
||||
body['requests'][-1]['updateMask'] += f'{casedField},'
|
||||
i += 2
|
||||
continue
|
||||
expected_fields = ', '.join(schemas[myarg]['settings'])
|
||||
if field not in expected_fields:
|
||||
msg = f'Expected {myarg} field of {expected_fields}. Got {field}.'
|
||||
@@ -290,14 +331,17 @@ def update_policy():
|
||||
value = gam.getBoolean(value, field)
|
||||
elif vtype in ['TYPE_ENUM']:
|
||||
value = value.upper()
|
||||
prefix = schemas[myarg]['settings'][field]['enum_prefix']
|
||||
enum_values = schemas[myarg]['settings'][field]['enums']
|
||||
if value not in enum_values:
|
||||
if value in enum_values:
|
||||
value = f'{prefix}{value}'
|
||||
elif value.replace(prefix, '') in enum_values:
|
||||
pass
|
||||
else:
|
||||
expected_enums = ', '.join(enum_values)
|
||||
msg = f'Expected {myarg} {field} value to be one of ' \
|
||||
f'{expected_enums}, got {value}'
|
||||
controlflow.system_error_exit(8, msg)
|
||||
prefix = schemas[myarg]['settings'][field]['enum_prefix']
|
||||
value = f'{prefix}{value}'
|
||||
elif vtype in ['TYPE_LIST']:
|
||||
value = value.split(',')
|
||||
if myarg == 'chrome.users.chromebrowserupdates' and \
|
||||
|
||||
@@ -14,7 +14,7 @@ from gam.gapi.directory import customer as gapi_directory_customer
|
||||
|
||||
|
||||
def create():
|
||||
ci = gapi_cloudidentity.build('cloudidentity_beta')
|
||||
ci = gapi_cloudidentity.build()
|
||||
initialGroupConfig = 'EMPTY'
|
||||
gapi_directory_customer.setTrueCustomerId()
|
||||
parent = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
|
||||
@@ -44,7 +44,6 @@ def create():
|
||||
body['additionalGroupKeys'].append({'id': alias})
|
||||
i += 2
|
||||
elif myarg in ['dynamic']:
|
||||
# As of 2020/06/25 this doesn't work (yet?)
|
||||
body['dynamicGroupMetadata'] = {
|
||||
'queries': [{
|
||||
'query': sys.argv[i + 1],
|
||||
@@ -66,7 +65,7 @@ def create():
|
||||
|
||||
|
||||
def delete():
|
||||
ci = gapi_cloudidentity.build('cloudidentity_beta')
|
||||
ci = gapi_cloudidentity.build()
|
||||
group = sys.argv[3]
|
||||
name = group_email_to_id(ci, group)
|
||||
print(f'Deleting group {group}')
|
||||
@@ -79,6 +78,7 @@ def info():
|
||||
getUsers = True
|
||||
showJoinDate = True
|
||||
showUpdateDate = False
|
||||
showMemberTree = False
|
||||
i = 4
|
||||
while i < len(sys.argv):
|
||||
myarg = sys.argv[i].lower().replace('_', '')
|
||||
@@ -91,12 +91,15 @@ def info():
|
||||
elif myarg == 'showupdatedate':
|
||||
showUpdateDate = True
|
||||
i += 1
|
||||
elif myarg == 'membertree':
|
||||
showMemberTree = True
|
||||
i += 1
|
||||
else:
|
||||
controlflow.invalid_argument_exit(myarg, 'gam info cigroup')
|
||||
name = group_email_to_id(ci, group)
|
||||
basic_info = gapi.call(ci.groups(), 'get', name=name)
|
||||
display.print_json(basic_info)
|
||||
if getUsers:
|
||||
if getUsers and not showMemberTree:
|
||||
if not showJoinDate and not showUpdateDate:
|
||||
view = 'BASIC'
|
||||
pageSize = 1000
|
||||
@@ -110,10 +113,11 @@ def info():
|
||||
fields='*',
|
||||
pageSize=pageSize,
|
||||
view=view)
|
||||
print('Members:')
|
||||
print(' Members:')
|
||||
for member in members:
|
||||
role = get_single_role(member.get('roles', [])).lower()
|
||||
email = member.get('memberKey', {}).get('id')
|
||||
member_type = member.get('type', 'USER').lower()
|
||||
jc_string = ''
|
||||
if showJoinDate:
|
||||
joined = member.get('createTime', 'Unknown')
|
||||
@@ -121,15 +125,39 @@ def info():
|
||||
if showUpdateDate:
|
||||
updated = member.get('updateTime', 'Unknown')
|
||||
jc_string += f' updated {updated}'
|
||||
print(
|
||||
f'{role}: {email}{jc_string}'
|
||||
# f' {member.get("role", ROLE_MEMBER).lower()}: {member.get("email", member["id"])} ({member["type"].lower()})'
|
||||
)
|
||||
print(f' {role}: {email} ({member_type}){jc_string}')
|
||||
print(f'Total {len(members)} users in group')
|
||||
elif showMemberTree:
|
||||
print(' Membership Tree:')
|
||||
cached_group_members = {}
|
||||
print_member_tree(ci, name, cached_group_members, 2, True)
|
||||
|
||||
|
||||
def print_member_tree(ci, group_id, cached_group_members, spaces, show_role):
|
||||
if not group_id in cached_group_members:
|
||||
cached_group_members[group_id] = gapi.get_all_pages(ci.groups().memberships(),
|
||||
'list',
|
||||
'memberships',
|
||||
parent=group_id,
|
||||
view='FULL',
|
||||
fields='*',
|
||||
pageSize=1000)
|
||||
for member in cached_group_members[group_id]:
|
||||
member_id = member.get('name', '')
|
||||
member_id = member_id.split('/')[-1]
|
||||
email = member.get('memberKey', {}).get('id')
|
||||
member_type = member.get('type', 'USER').lower()
|
||||
if show_role:
|
||||
role = get_single_role(member.get('roles', [])).lower()
|
||||
print(f'{" " * spaces}{role}: {email} ({member_type})')
|
||||
else:
|
||||
print(f'{" " * spaces}{email} ({member_type})')
|
||||
if member_type == 'group':
|
||||
print_member_tree(ci, f'groups/{member_id}', cached_group_members, spaces + 2, False)
|
||||
|
||||
|
||||
def info_member():
|
||||
ci = gapi_cloudidentity.build('cloudidentity_beta')
|
||||
ci = gapi_cloudidentity.build()
|
||||
member = gam.normalizeEmailAddressOrUID(sys.argv[3])
|
||||
group = gam.normalizeEmailAddressOrUID(sys.argv[4])
|
||||
group_name = gapi.call(ci.groups(),
|
||||
@@ -247,7 +275,7 @@ def print_():
|
||||
except googleapiclient.errors.HttpError:
|
||||
controlflow.system_error_exit(
|
||||
2,
|
||||
f'enterprisemember requires Enterprise license')
|
||||
'enterprisemember requires Enterprise license')
|
||||
entityList = []
|
||||
for entity in result:
|
||||
if entity['relationType'] == 'DIRECT':
|
||||
@@ -302,7 +330,7 @@ def print_():
|
||||
ownersCount = 0
|
||||
for member in groupMembers:
|
||||
member_email = member['memberKey']['id']
|
||||
role = get_single_role(member.get('roles'))
|
||||
role = get_single_role(member.get('roles', []))
|
||||
if not validRoles or role in validRoles:
|
||||
if role == ROLE_MEMBER:
|
||||
if members:
|
||||
@@ -343,6 +371,56 @@ def print_():
|
||||
display.write_csv_file(csvRows, titles, 'Groups', todrive)
|
||||
|
||||
|
||||
def _get_groups_list(ci=None, member=None, parent=None):
|
||||
if not ci:
|
||||
ci = gapi_cloudidentity.build()
|
||||
if not parent:
|
||||
gapi_directory_customer.setTrueCustomerId()
|
||||
parent = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
|
||||
gam.printGettingAllItems('Groups', member)
|
||||
page_message = gapi.got_total_items_first_last_msg('Groups')
|
||||
if member:
|
||||
fields = 'nextPageToken,memberships(groupKey(id),relationType)'
|
||||
try:
|
||||
groups_to_get = gapi.get_all_pages(ci.groups().memberships(),
|
||||
'searchTransitiveGroups',
|
||||
'memberships',
|
||||
throw_reasons=[gapi_errors.ErrorReason.FOUR_O_O],
|
||||
message_attribute=['groupKey', 'id'],
|
||||
page_message=page_message,
|
||||
parent='groups/-',
|
||||
query=member,
|
||||
pageSize=1000,
|
||||
fields=fields)
|
||||
except googleapiclient.errors.HttpError:
|
||||
controlflow.system_error_exit(
|
||||
2,
|
||||
'enterprisemember requires Enterprise license')
|
||||
return [group['groupKey']['id'] for group in groups_to_get if group['relationType'] == 'DIRECT']
|
||||
else:
|
||||
groups_to_get = gapi.get_all_pages(
|
||||
ci.groups(),
|
||||
'list',
|
||||
'groups',
|
||||
message_attribute=['groupKey', 'id'],
|
||||
page_message=page_message,
|
||||
parent=parent,
|
||||
view='BASIC',
|
||||
pageSize=1000,
|
||||
fields='nextPageToken,groups(groupKey(id))')
|
||||
return [group['groupKey']['id'] for group in groups_to_get]
|
||||
|
||||
|
||||
def get_membership_graph(member):
|
||||
ci = gapi_cloudidentity.build('cloudidentity_beta')
|
||||
query = f"member_key_id == '{member}' && 'cloudidentity.googleapis.com/groups.discussion_forum' in labels"
|
||||
result = gapi.call(ci.groups().memberships(),
|
||||
'getMembershipGraph',
|
||||
parent='groups/-',
|
||||
query=query)
|
||||
return result.get('response')
|
||||
|
||||
|
||||
def print_members():
|
||||
ci = gapi_cloudidentity.build('cloudidentity_beta')
|
||||
todrive = False
|
||||
@@ -381,36 +459,7 @@ def print_members():
|
||||
controlflow.invalid_argument_exit(sys.argv[i],
|
||||
'gam print cigroup-members')
|
||||
if not groups_to_get:
|
||||
gam.printGettingAllItems('Groups', usemember)
|
||||
page_message = gapi.got_total_items_first_last_msg('Groups')
|
||||
if usemember:
|
||||
try:
|
||||
groups_to_get = gapi.get_all_pages(ci.groups().memberships(),
|
||||
'searchTransitiveGroups',
|
||||
'memberships',
|
||||
throw_reasons=[gapi_errors.ErrorReason.FOUR_O_O],
|
||||
message_attribute=['groupKey', 'id'],
|
||||
page_message=page_message,
|
||||
parent='groups/-', query=usemember,
|
||||
pageSize=1000,
|
||||
fields='nextPageToken,memberships(groupKey(id),relationType)')
|
||||
except googleapiclient.errors.HttpError:
|
||||
controlflow.system_error_exit(
|
||||
2,
|
||||
f'enterprisemember requires Enterprise license')
|
||||
groups_to_get = [group['groupKey']['id'] for group in groups_to_get if group['relationType'] == 'DIRECT']
|
||||
else:
|
||||
groups_to_get = gapi.get_all_pages(
|
||||
ci.groups(),
|
||||
'list',
|
||||
'groups',
|
||||
message_attribute=['groupKey', 'id'],
|
||||
page_message=page_message,
|
||||
parent=parent,
|
||||
view='BASIC',
|
||||
pageSize=1000,
|
||||
fields='nextPageToken,groups(groupKey(id))')
|
||||
groups_to_get = [group['groupKey']['id'] for group in groups_to_get]
|
||||
groups_to_get = _get_groups_list(ci, usemember, parent)
|
||||
i = 0
|
||||
count = len(groups_to_get)
|
||||
for group_email in groups_to_get:
|
||||
@@ -773,6 +822,14 @@ def update():
|
||||
'cloudidentity.googleapis.com/groups.discussion_forum': ''
|
||||
}
|
||||
i += 1
|
||||
elif myarg in ['dynamic']:
|
||||
body['dynamicGroupMetadata'] = {
|
||||
'queries': [{
|
||||
'query': sys.argv[i + 1],
|
||||
'resourceType': 'USER'
|
||||
}]
|
||||
}
|
||||
i += 2
|
||||
else:
|
||||
controlflow.invalid_argument_exit(sys.argv[i],
|
||||
'gam update cigroup')
|
||||
|
||||
@@ -119,6 +119,7 @@ class ErrorReason(Enum):
|
||||
FIVE_O_THREE = '503'
|
||||
FOUR_O_NINE = '409'
|
||||
FOUR_O_O = '400'
|
||||
FOUR_O_FOUR = '404'
|
||||
FOUR_O_THREE = '403'
|
||||
FOUR_TWO_NINE = '429'
|
||||
GATEWAY_TIMEOUT = 'gatewayTimeout'
|
||||
|
||||
@@ -7,6 +7,7 @@ from unittest.mock import patch
|
||||
|
||||
import googleapiclient.errors
|
||||
from gam.gapi import errors
|
||||
import httplib2
|
||||
|
||||
|
||||
def create_simple_http_error(status, reason, message):
|
||||
@@ -15,10 +16,10 @@ def create_simple_http_error(status, reason, message):
|
||||
|
||||
|
||||
def create_http_error(status, content):
|
||||
response = {
|
||||
response = httplib2.Response({
|
||||
'status': status,
|
||||
'content-type': 'application/json',
|
||||
}
|
||||
})
|
||||
content_as_bytes = json.dumps(content).encode('UTF-8')
|
||||
return googleapiclient.errors.HttpError(response, content_as_bytes)
|
||||
|
||||
@@ -73,6 +74,7 @@ class ErrorsTest(unittest.TestCase):
|
||||
def test_get_gapi_error_extracts_user_not_found(self):
|
||||
err = create_simple_http_error(404, 'notFound',
|
||||
'Resource Not Found: userKey.')
|
||||
print(err)
|
||||
http_status, reason, message = errors.get_gapi_error_detail(err)
|
||||
self.assertEqual(http_status, 404)
|
||||
self.assertEqual(reason, errors.ErrorReason.USER_NOT_FOUND.value)
|
||||
@@ -158,7 +160,7 @@ class ErrorsTest(unittest.TestCase):
|
||||
|
||||
def test_get_gapi_error_extracts_single_error_with_message(self):
|
||||
status_code = 999
|
||||
response = {'status': status_code}
|
||||
response = httplib2.Response({'status': status_code})
|
||||
# This error does not have an "errors" key describing each error.
|
||||
content = {'error': {'code': status_code, 'message': 'unknown error'}}
|
||||
content_as_bytes = json.dumps(content).encode('UTF-8')
|
||||
@@ -172,7 +174,7 @@ class ErrorsTest(unittest.TestCase):
|
||||
def test_get_gapi_error_exits_code_4_on_malformed_error_with_unknown_description(
|
||||
self):
|
||||
status_code = 999
|
||||
response = {'status': status_code}
|
||||
response = httplib2.Response({'status': status_code})
|
||||
# This error only has an error_description_field and an unknown description.
|
||||
content = {'error_description': 'something errored'}
|
||||
content_as_bytes = json.dumps(content).encode('UTF-8')
|
||||
@@ -184,7 +186,7 @@ class ErrorsTest(unittest.TestCase):
|
||||
|
||||
def test_get_gapi_error_exits_on_invalid_error_description(self):
|
||||
status_code = 400
|
||||
response = {'status': status_code}
|
||||
response = httplib2.Response({'status': status_code})
|
||||
content = {'error_description': 'Invalid Value'}
|
||||
content_as_bytes = json.dumps(content).encode('UTF-8')
|
||||
err = googleapiclient.errors.HttpError(response, content_as_bytes)
|
||||
@@ -196,7 +198,7 @@ class ErrorsTest(unittest.TestCase):
|
||||
|
||||
def test_get_gapi_error_exits_code_4_on_unexpected_error_contents(self):
|
||||
status_code = 900
|
||||
response = {'status': status_code}
|
||||
response = httplib2.Response({'status': status_code})
|
||||
content = {'notErrorContentThatIsExpected': 'foo'}
|
||||
content_as_bytes = json.dumps(content).encode('UTF-8')
|
||||
err = googleapiclient.errors.HttpError(response, content_as_bytes)
|
||||
|
||||
@@ -8,7 +8,7 @@ import platform
|
||||
import re
|
||||
|
||||
GAM_AUTHOR = 'Jay Lee <jay0lee@gmail.com>'
|
||||
GAM_VERSION = '6.01'
|
||||
GAM_VERSION = '6.06'
|
||||
GAM_LICENSE = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
|
||||
|
||||
GAM_URL = 'https://git.io/gam'
|
||||
@@ -56,6 +56,11 @@ SKUS = {
|
||||
'aliases': ['identitypremium', 'cloudidentitypremium'],
|
||||
'displayName': 'Cloud Identity Premium'
|
||||
},
|
||||
'1010350001': {
|
||||
'product': '101035',
|
||||
'aliases': ['cloudsearch'],
|
||||
'displayName': 'Google Cloud Search',
|
||||
},
|
||||
'1010310002': {
|
||||
'product': '101031',
|
||||
'aliases': ['gsefe', 'e4e', 'gsuiteenterpriseeducation'],
|
||||
@@ -271,6 +276,7 @@ PRODUCTID_NAME_MAPPINGS = {
|
||||
'101031': 'G Suite Workspace for Education',
|
||||
'101033': 'Google Voice',
|
||||
'101034': 'G Suite Archived',
|
||||
'101035': 'Cloud Search',
|
||||
'101037': 'G Suite Workspace for Education',
|
||||
'Google-Apps': 'Google Workspace',
|
||||
'Google-Chrome-Device-Management': 'Google Chrome Device Management',
|
||||
@@ -1514,7 +1520,7 @@ USER_EXTERNALID_TYPES = [
|
||||
]
|
||||
USER_GENDER_TYPES = ['female', 'male', 'unknown']
|
||||
USER_IM_TYPES = ['home', 'work', 'other']
|
||||
USER_KEYWORD_TYPES = ['occupation', 'outlook']
|
||||
USER_KEYWORD_TYPES = ['occupation', 'outlook', 'mission']
|
||||
USER_LOCATION_TYPES = ['default', 'desk']
|
||||
USER_ORGANIZATION_TYPES = ['domain_only', 'school', 'unknown', 'work']
|
||||
USER_PHONE_TYPES = [
|
||||
@@ -1529,7 +1535,7 @@ USER_RELATION_TYPES = [
|
||||
]
|
||||
USER_WEBSITE_TYPES = [
|
||||
'app_install_page', 'blog', 'ftp', 'home', 'home_page', 'other', 'profile',
|
||||
'reservations', 'work'
|
||||
'reservations', 'resume', 'work'
|
||||
]
|
||||
|
||||
WEBCOLOR_MAP = {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
cryptography
|
||||
distro; sys_platform == 'linux'
|
||||
filelock
|
||||
google-api-python-client==2.0.2
|
||||
google-api-python-client>=2.1
|
||||
google-auth-httplib2
|
||||
google-auth-oauthlib>=0.4.1
|
||||
google-auth>=1.11.2
|
||||
httplib2>=0.17.0
|
||||
importlib.metadata; python_version < '3.8'
|
||||
passlib>=1.7.2
|
||||
python-dateutil
|
||||
yubikey-manager>=4.0.0
|
||||
|
||||
Reference in New Issue
Block a user