Compare commits

...

115 Commits
v6.02 ... v6.07

Author SHA1 Message Date
Jay Lee
a3d560a8a2 YubiKey improvements and PIV reset 2021-07-27 09:24:34 -04:00
Jay Lee
ed20fe252e Use with conn so Yubikey connections close sooner 2021-07-26 14:46:58 -04:00
Jay Lee
375e36ff96 State what we don't like about invalid JSON 2021-07-26 14:45:26 -04:00
Jay Lee
e7108b108e Update build.yml 2021-07-23 13:31:06 -04:00
Jay Lee
6d59daad19 Update build.yml 2021-07-23 13:27:53 -04:00
Jay Lee
21c693921b Update build.yml 2021-07-23 13:13:34 -04:00
Jay Lee
7bcd5fbed7 Update build.yml 2021-07-23 13:06:22 -04:00
Jay Lee
7104970e17 Update build.yml 2021-07-23 13:01:20 -04:00
Jay Lee
1a2950b580 Update build.yml 2021-07-23 12:59:34 -04:00
Jay Lee
085b24e1c5 Update build.yml 2021-07-23 12:55:39 -04:00
Jay Lee
8688ce6328 Update build.yml 2021-07-23 12:52:35 -04:00
Jay Lee
fbdfed81e7 Update build.yml 2021-07-23 12:49:21 -04:00
Ross Scroggs
94fe20607e Updates for CRM v3 changes (#1401) 2021-07-22 19:19:10 -04:00
Ross Scroggs
6c62483e8e Updates for CRM v3 changes (#1400) 2021-07-21 17:27:46 -04:00
Ross Scroggs
54689129c6 Update gam print|show|update chromepolicy to handle the following special case policies: (#1399)
```
chrome.users.AutoUpdateCheckPeriodNew autoupdatecheckperiodminutesnew
chrome.users.BrowserSwitcherDelayDuration browserswitcherdelayduration
chrome.users.FetchKeepaliveDurationSecondsOnShutdown fetchkeepalivedurationsecondsonshutdown
chrome.users.MaxInvalidationFetchDelay maxinvalidationfetchdelay
chrome.users.PrintingMaxSheetsAllowed printingmaxsheetsallowednullable
chrome.users.PrintJobHistoryExpirationPeriodNew printjobhistoryexpirationperioddaysnew
chrome.users.SecurityTokenSessionSettings securitytokensessionnotificationseconds
chrome.users.SessionLength sessiondurationlimit
chrome.users.UpdatesSuppressed updatessuppresseddurationmin
chrome.users.UpdatesSuppressed updatessuppressedstarttime
```
2021-07-20 17:25:17 -04:00
Ross Scroggs
e9e8dd5a82 Fix call to be compatible with CRM v3 (#1398) 2021-07-19 19:31:08 -04:00
Jay Lee
00e764b118 Migrate to Resource Manager API v3 2021-07-16 10:14:58 -04:00
Jay Lee
cee7eb970a Merge branch 'main' of https://github.com/jay0lee/GAM 2021-07-13 10:45:21 -04:00
Jay Lee
daed17fac8 exclude null character, max out passwd length on random 2021-07-13 10:44:58 -04:00
Ross Scroggs
8708f4f93f Fix page_args_in_body, update namespace handling in show chromepolicies (#1393)
When page_args_in_body is true you have to add body to kwargs to ensure a place for pageToken

Allow setting a list of namespaces that override the defaults for printerid (not likely) and appid.
2021-07-08 08:55:18 -04:00
Jay Lee
c7c1bfbeba retry wait for mailbox if user doesn't exist 2021-07-07 11:03:04 -04:00
Jay Lee
0418438b6f increase rounds to Google max 2021-07-07 10:53:34 -04:00
Jay Lee
a2ea4d036e improve random password generator 2021-07-07 10:47:34 -04:00
Jay Lee
dc7a29908f updates to allow listing/setting extension policy 2021-07-02 13:36:21 -04:00
Jay Lee
794db5d2a4 More APIs now work with discovery v2 URL 2021-07-01 14:47:40 -04:00
Jay Lee
e5f9db129b Improve printing of app/extension/printer policy 2021-06-30 11:18:29 -04:00
Jay Lee
a6aecf4e9d undo version in exe 2021-06-29 11:22:33 -04:00
Jay Lee
b59bc4ec90 Merge branch 'main' of https://github.com/jay0lee/GAM 2021-06-29 11:04:17 -04:00
Jay Lee
41920f7865 add version info to Windows exe 2021-06-29 11:02:44 -04:00
Jay Lee
4630bf5681 Update var.py 2021-06-29 08:13:59 -04:00
Ross Scroggs
1c78ebd20e Add groupidfllert <String> to gam report <ActivityApplicationName> (#1390) 2021-06-28 21:34:50 -04:00
Jay Lee
80d17cfda3 Update windows-install.sh 2021-06-28 17:28:10 -04:00
Jay Lee
a154007927 Update windows-install.sh 2021-06-28 17:22:57 -04:00
Jay Lee
bd8274cc27 Update windows-install.sh 2021-06-28 17:13:42 -04:00
Jay Lee
fb08991c05 Update windows-before-install.sh 2021-06-28 17:06:47 -04:00
Jay Lee
7c1f06fdf7 Update build.yml 2021-06-28 16:57:11 -04:00
Jay Lee
93b38b9f95 Update build.yml 2021-06-28 16:44:40 -04:00
Jay Lee
7ffc97d301 Update build.yml 2021-06-28 16:40:43 -04:00
Jay Lee
280301f258 Update build.yml 2021-06-28 16:36:04 -04:00
Jay Lee
40daf38f80 Update build.yml 2021-06-28 16:32:41 -04:00
Jay Lee
d24925cd5f Update build.yml 2021-06-28 16:30:55 -04:00
Ross Scroggs
cd42d54b43 Fix typo, document new drive fields (#1389)
* Fix typo, document new drive fields

* Document new drive attribute
2021-06-28 15:40:22 -04:00
Jay Lee
53d8ecb6bc Update build.yml 2021-06-28 15:39:47 -04:00
Jay Lee
98e87d0297 Update build.yml 2021-06-28 15:38:04 -04:00
Jay Lee
400b4af769 Update macos-before-install.sh 2021-06-28 15:35:40 -04:00
Jay Lee
368701afb1 Update build.yml 2021-06-28 15:33:58 -04:00
Jay Lee
a501b89ecd resource key support 2021-06-25 16:52:14 -04:00
Jay Lee
91cddd72e5 Update build.yml 2021-06-17 14:54:09 -04:00
Jay Lee
8a1f0c9dbf GAM 6.06 2021-06-17 14:47:47 -04:00
Jay Lee
e3e5318b4f Update build.yml 2021-06-17 14:33:08 -04:00
Jay Lee
b060664c9f Update build.yml 2021-06-17 14:29:38 -04:00
Jay Lee
83fbf0e8ac Update build.yml 2021-06-17 14:23:13 -04:00
Jay Lee
537a926618 Update build.yml 2021-06-11 09:49:39 -04:00
Jay Lee
f791a59b1d GAM 6.05 2021-06-11 09:30:42 -04:00
Jay Lee
0b8e41f993 cleanup 2021-06-11 09:29:32 -04:00
Ross Scroggs
f540fa2a38 Unescape \r and \n in chatmessage text so multiline messages can be created from command line (#1387)
* Unescape \r and \n in chatmessage text so multiline messages can be created

* Bring gam report activity list up to date
2021-06-10 09:33:51 -04:00
Ross Scroggs
2d7bc2f34a Check that required arguments are present (#1386)
* Check that required arguments are present

* Correct chat message documentation
2021-06-07 15:56:09 -04:00
Jay Lee
c2dea0a4d7 add a few new user attribute types 2021-06-04 15:55:50 -04:00
Jay Lee
42cbfbf8ed Update macos-install.sh 2021-06-02 14:02:37 -04:00
Jay Lee
137e79b012 Update macos-install.sh 2021-06-02 13:49:34 -04:00
Jay Lee
5849ed3ecc Update build.yml 2021-06-02 10:04:30 -04:00
Ross Scroggs
d3dc1e1197 Add chat commands (#1384) 2021-05-27 21:07:55 -04:00
Jay Lee
c20f0bef44 GAM 6.04 2021-05-26 13:12:28 -04:00
Jay Lee
c572b6b182 fix win zip and MSI 2021-05-26 11:59:57 -04:00
Jay Lee
a1392dbf86 fix archive path 2021-05-26 10:53:06 -04:00
Jay Lee
4e719bab5e few build / install cleanups 2021-05-25 12:59:57 -04:00
Jay Lee
34b51ea64a GAM 6.03 2021-05-25 12:49:10 -04:00
Jay Lee
5a2a72f530 fix importlib_metadata on Python < 3.8 2021-05-25 12:27:52 -04:00
Jay Lee
2ea80c41ab retry a few more key operations 2021-05-25 11:49:44 -04:00
Jay Lee
6f987958e8 Print versions for more libraries 2021-05-25 10:39:14 -04:00
Jay Lee
ae4007aad5 re-enable legacy linux build 2021-05-25 10:09:47 -04:00
Jay Lee
c4401f8bd4 refine Chat calls 2021-05-25 09:32:02 -04:00
Jay Lee
0e7472de50 Initial Support for Google Chat API 2021-05-24 16:37:39 -04:00
Jay Lee
e998c78609 name artificat zip 2021-05-24 12:30:18 -04:00
Jay Lee
c30b92cd38 fix msi build 2021-05-24 11:23:45 -04:00
Jay Lee
2bf2d2aef7 try src/ 2021-05-24 11:07:39 -04:00
Jay Lee
cdc04b0803 fix gampath on win/mac 2021-05-24 10:48:42 -04:00
Jay Lee
5f5875acc1 fix distpath linux 2021-05-24 10:11:40 -04:00
Jay Lee
d306c5e0a3 try with extensions 2021-05-24 10:04:52 -04:00
Jay Lee
19a815cffe One file 2021-05-24 09:54:57 -04:00
Jay Lee
da0c559293 artifact wildcard attempt #3 2021-05-24 09:33:42 -04:00
Jay Lee
a2c91ef7b3 artifact wildcard attempt #2 2021-05-24 09:24:51 -04:00
Jay Lee
722b94ca32 no --universal2 argument yet 2021-05-24 09:00:18 -04:00
Jay Lee
299742fe03 reverting Actions changes for universal2 attempt 2021-05-24 08:56:25 -04:00
Ross Scroggs
3964cbf911 Add dynamic option to update cigroup (#1381)
Update print|show teamdrive documentation
2021-05-17 17:09:34 -04:00
Jay Lee
63e4947ad5 fix version 2021-05-12 08:36:10 -04:00
Jay Lee
e3cb13a414 youtoo 2021-05-11 14:46:24 -04:00
Jay Lee
01fec79d78 --universal2 2021-05-11 14:39:25 -04:00
Jay Lee
a7043a1359 disable rebuild of bootloader 2021-05-11 14:00:06 -04:00
Jay Lee
91a93ecd62 only set target arch on 32-bit Win 2021-05-11 13:20:40 -04:00
Jay Lee
c52fdf6395 preserve gam builds as Action artifact 2021-05-11 12:56:22 -04:00
Jay Lee
1d1dad4b30 switch to PyInstaller branch for Apple M1 support 2021-05-11 12:53:14 -04:00
Jay Lee
f07a57e478 no strip 2021-05-11 10:28:13 -04:00
Jay Lee
ebacd9b4b4 standardize pyinstaller arguments 2021-05-11 10:22:29 -04:00
Jay Lee
f010e59597 remove -F on Windows 2021-05-11 10:15:10 -04:00
Jay Lee
a184d7a8e0 list gampath 2021-05-11 09:15:16 -04:00
Jay Lee
807f54c549 wheel 2021-05-11 09:02:50 -04:00
Jay Lee
24684abc1d wheel 2021-05-11 08:56:17 -04:00
Jay Lee
1f1a49976c upgrade PyInstaller 2021-05-11 08:52:08 -04:00
Jay Lee
562fda3079 disable staticx for now 2021-05-10 09:30:53 -04:00
Jay Lee
05642f3c14 try w/o readlink 2021-05-10 09:06:53 -04:00
Jay Lee
251e2774aa no mkdir 2021-05-10 08:59:30 -04:00
Jay Lee
2089589d34 fix distpath 2021-05-10 08:53:19 -04:00
Jay Lee
c48b135c43 more debug decrypt 2021-05-10 08:44:45 -04:00
Jay Lee
70121a6ebf more output 2021-05-07 11:14:03 -04:00
Jay Lee
c23e53585a fix gam binary paths 2021-05-07 10:59:05 -04:00
Jay Lee
89e964163e fix dist paths 2021-05-07 10:55:42 -04:00
Jay Lee
0357774ba6 Test moving back to one-dir instead of one-file for PyInstaller 2021-05-07 10:48:36 -04:00
Ross Scroggs
93cf750249 Code cleanup; display role for group members (#1379)
* Code cleanup; display role for group members

* Standardize member and membertree output

Should dates be added to membergtree output?

* Use member_id to get subgroup, avoid call to convert email to id

* Only show role on top-level members

* Use v1beta1 for info user grouptree

* Update groups.py
2021-05-07 09:07:44 -04:00
Ross Scroggs
b712f7a344 Cloud Identity v1 only uses preferredMemberKey (#1378)
* Cloud Identity v1 only uses preferredMemberKey

* Document print labels

* Cleanup/bug fix info user grouptree; fix todrive for print labels

* Standardize write_csv_file call in print labels

* Use cloudidentity_beta for calls that process memberKey

* Code cleanup
2021-05-06 08:10:36 -04:00
Jay Lee
4159a5cbb8 test on Python 3.10-beta 2021-05-04 12:27:52 -04:00
Jay Lee
2e78a291d4 test on Python 3.10 2021-05-04 12:22:22 -04:00
Jay Lee
3f1705c2a5 test on Python 3.10 2021-05-04 12:20:45 -04:00
Jay Lee
bb1f5f7059 Merge branch 'main' of https://github.com/jay0lee/GAM into main 2021-05-04 11:57:33 -04:00
Jay Lee
75b7d0c419 Mmebership oops 2021-05-04 11:57:19 -04:00
23 changed files with 843 additions and 371 deletions

View File

@@ -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}"

View File

@@ -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

View File

@@ -22,11 +22,7 @@ cd ~
# Use official Python.org version of Python which is backwards compatible
# with older MacOS versions
if [ "$PLATFORM" == "x86_64" ]; then
export pyfile=python-$BUILD_PYTHON_VERSION-macosx10.9.pkg
else
export pyfile=python-$BUILD_PYTHON_VERSION-macos11.pkg
fi
export pyfile=python-$BUILD_PYTHON_VERSION-macos11.pkg
wget https://www.python.org/ftp/python/$BUILD_PYTHON_VERSION/$pyfile
echo "installing Python $BUILD_PYTHON_VERSION..."

View File

@@ -1,18 +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
export specfile="gam.spec"
$python -OO -m PyInstaller --clean --noupx --strip -F --distpath "${gampath}" "${specfile}"
$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

View File

@@ -13,8 +13,8 @@ echo "This is a ${BITS}-bit build for ${PLATFORM}"
export mypath=$(pwd)
cd ~
export python="python"
export pip="pip"
export python="c:\python\python.exe"
export pip="c:\python\scripts\pip.exe"
# pyscard needs swig, keep these two together
choco install $CHOCOPTIONS swig

View File

@@ -4,11 +4,10 @@ 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
/c/python/scripts/pyinstaller --clean --noupx --distpath $gampath gam.spec
export gam="${gampath}/gam"
echo "running compiled GAM..."
$gam version
@@ -18,8 +17,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..."

View File

@@ -12,13 +12,13 @@ defaults:
working-directory: src
env:
BUILD_PYTHON_VERSION: "3.9.5"
MIN_PYTHON_VERSION: "3.9.5"
BUILD_PYTHON_VERSION: "3.9.6"
MIN_PYTHON_VERSION: "3.9.6"
BUILD_OPENSSL_VERSION: "1.1.1k"
MIN_OPENSSL_VERSION: "1.1.1k"
PATCHELF_VERSION: "0.12"
# PYINSTALLER_VERSION can be full commit hash or version like v4.20
PYINSTALLER_VERSION: "e20e74c03768d432d48665b8ef1e02511b16e4be"
PYINSTALLER_VERSION: "0f2b2e921433ab5a510c7efdb21d9c1d7cfbc645"
jobs:
build:
@@ -41,21 +41,6 @@ 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"
@@ -65,7 +50,6 @@ jobs:
jid: 5
goal: "build"
gamos: "windows"
python: 3.9.5
pyarch: "x64"
platform: "x86_64"
- os: windows-2019
@@ -73,7 +57,6 @@ jobs:
goal: "build"
gamos: "windows"
platform: "x86"
python: 3.9.5
pyarch: "x86"
- os: ubuntu-20.04
goal: "test"
@@ -93,6 +76,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 +97,7 @@ jobs:
path: |
~/python
~/ssl
key: ${{ matrix.os }}-${{ matrix.jid }}-20210504
key: ${{ matrix.os }}-${{ matrix.jid }}-20210628
- name: Set env variables
env:
@@ -123,13 +112,33 @@ jobs:
echo "PLATFORM=${PLATFORM}" >> $GITHUB_ENV
uname -a
- name: Use pre-compiled Python for testing and Windows
- name: Use pre-compiled Python for testing
if: matrix.python != ''
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python }}
architecture: ${{ matrix.pyarch }}
- name: Install Python on Windows
if: matrix.os == 'windows-2019'
run: |
if ( ${Env:PLATFORM} -eq "x86_64" )
{
Set-Variable -name py_arch -value "-amd64"
}
else
{
Set-Variable -name py_arch -value ""
}
Write-Output "py_arch: $py_arch"
Set-Variable -name python_file -value "python-${Env:BUILD_PYTHON_VERSION}${py_arch}.exe"
Write-Output "python_file: $python_file"
Set-Variable -name python_url -value "https://www.python.org/ftp/python/${Env:BUILD_PYTHON_VERSION}/${python_file}"
Write-Output "python_url: $python_url"
Invoke-WebRequest -Uri $python_url -OutFile $python_file
Start-Process -wait -FilePath $python_file -ArgumentList "/quiet","InstallAllUsers=0","TargetDir=c:\\python","AssociateFiles=1","PrependPath=1"
shell: pwsh
- name: Set env variables for pre-compiled Python
if: matrix.goal == 'test'
run: |
@@ -145,6 +154,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 +166,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 +200,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}"
@@ -344,3 +361,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

View File

@@ -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>
@@ -203,6 +204,7 @@ If an item contains spaces, it should be surrounded by ".
<MaximumNumberOfSeats> ::= <Number>
<MobileID> ::= <String>
<Name> ::= <String>
<Namespace> ::= <String>
<NotificationID> ::= <String>
<NumberOfSeats> ::= <Number>
<OrgUnitID> ::= <String>
@@ -223,6 +225,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>
@@ -331,6 +334,7 @@ If an item contains spaces, it should be surrounded by ".
lastmodifyinguser|
lastmodifyingusername|
lastviewedbyme|lastviewedbymedate|lastviewedbymetime|lastviewedbyuser|
linksharemetadata|
md5|md5checksum|md5sum|
mime|mimetype|
modifiedbyme|modifiedbymedate|modifiedbymetime|modifiedbyuser|
@@ -343,6 +347,7 @@ If an item contains spaces, it should be surrounded by ".
parents|
permissions|
quotabytesused|quotaused|
resourcekey|
restricted|
shareable|
shared|
@@ -590,6 +595,7 @@ Items, separated by spaces, with spaces, commas or single quotes in the items th
<MatterItemList> ::= "<MatterItem>(,<MatterItem>)*"
<MembersFieldNameList> ::= "<MembersFieldName>(,<MembersFieldName>)*"
<MobileList> ::= "<MobileId>(,<MobileId>)*"
<NamespaceList> ::= "<Namespace>(,<Namespace)*"
<OrgUnitList> ::= "<OrgUnitPath>(,<OrgUnitPath>)*"
<PrinterIDList> ::= "<PrinterID>)(,<PrinterID>)*"
<ProductIDList> ::= "(<ProductID>|SKUID>)(,<ProductID>|SKUID>)*"
@@ -693,6 +699,7 @@ Specify a collection of Users by directly specifying them or by specifiying item
copyrequireswriterpermission|
(lastviewedbyme <Time>)|(modifieddate|modifiedtime <Time>)|(description <String>)|(mimetype <MimeType>)|
(parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|writerscantshare|writerscanshare
(securityupdate <Boolean>)|
(shortcut <DriveFileID>)
<DriveFileUpdateAttribute> ::=
(localfile <FileName>|-)|
@@ -703,6 +710,7 @@ Specify a collection of Users by directly specifying them or by specifiying item
(copyrequireswriterpermission <Boolean>)|
(lastviewedbyme <Time>)|(modifieddate <Time>)|(description <String>)|(mimetype <MimeType>)|
(parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|writerscantshare|writerscanshare
(securityupdate <Boolean>)|
(shortcut <DriveFileID>)
<GroupSettingsAttribute> ::=
(allowexternalmembers <Boolean>)|
@@ -841,6 +849,8 @@ An argument containing instances of ~~xxx~~ has xxx replaced by the value of fie
Example: gam csv Users.csv gam update user "~primaryEmail" address type work unstructured "~~Street~~, ~~City~~, ~~State~~ ~~ZIP~~"
Each user (~primaryEmail, e.g. foo@bar.com) would have their work address updated
gam create gcpfolder <String>
gam create project [<EmailAddress>] [<ProjectID>]
gam create project [admin <EmailAddress>] [project <ProjectID>] [parent <String>]
gam use project [<EmailAddress>] [<ProjectID>]
@@ -894,23 +904,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|
@@ -949,6 +963,7 @@ gam report <ActivityApplicationName> [todrive]
[(user all|<UserItem>)|(orgunit|org|ou <OrgUnitPath>)]
[start <Time>] [end <Time>]
[filter|filters <String>] [event <String>] [ip <String>]
[groupidfilter <String>]
gam create admin <UserItem> <RoleItem> customer|(org_unit <OrgUnitItem>)
gam delete admin <RoleAssignmentId>
@@ -1166,6 +1181,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|
@@ -1294,7 +1317,7 @@ gam print chromehistory releases [todrive]
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 chromepolicy ou|org|orgunit <OrgUnitItem> [(printerid <PrinterID>)|(appid <AppID>)] [namespace <NamespaceList>]
gam show chromeschema [filter <String>]
<DeviceID> ::= devices/<String>
@@ -1365,7 +1388,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 +1461,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 +1623,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>)*
@@ -1690,8 +1714,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>

View File

@@ -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*)

View File

@@ -5,8 +5,6 @@ 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')]
@@ -34,6 +32,7 @@ for d in a.datas:
pyz = PYZ(a.pure)
exe = EXE(pyz,
a.scripts,
a.binaries,
@@ -43,4 +42,4 @@ exe = EXE(pyz,
debug=False,
strip=None,
upx=False,
console=True )
console=True)

View File

@@ -6,11 +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
@@ -38,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
@@ -53,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
@@ -761,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'
@@ -784,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}'
@@ -822,27 +841,39 @@ 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
except (ValueError, KeyError):
except (ValueError, KeyError) as err:
printLine(MESSAGE_INSTRUCTIONS_OAUTH2SERVICE_JSON)
controlflow.invalid_json_exit(GC_Values[GC_OAUTH2SERVICE_JSON])
controlflow.invalid_json_exit(GC_Values[GC_OAUTH2SERVICE_JSON], err)
def getAPIVersion(api):
@@ -868,8 +899,8 @@ def readDiscoveryFile(api_version):
try:
discovery = json.loads(json_string)
return (disc_file, discovery)
except ValueError:
controlflow.invalid_json_exit(disc_file)
except ValueError as err:
controlflow.invalid_json_exit(disc_file, err)
def getOauth2TxtStorageCredentials():
@@ -1087,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):
@@ -1439,8 +1473,8 @@ def addDelegates(users, i):
body={'delegateEmail': delegate})
def gen_sha512_hash(password):
return sha512_crypt.hash(password, rounds=5000)
def gen_sha512_hash(password, rounds=10000):
return sha512_crypt.hash(password, rounds=rounds)
def printShowDelegates(users, csvFormat):
@@ -3685,6 +3719,10 @@ def getDriveFileAttribute(i, body, parameters, myarg, update=False):
body['mimeType'] = MIMETYPE_GA_SHORTCUT
body['shortcutDetails'] = {'targetId': sys.argv[i+1]}
i += 2
elif myarg == 'securityupdate':
body['linkShareMetadata'] = {'securityUpdateEnabled': getBoolean(
sys.argv[i+1], f'gam <users> {operation} drivefile'), 'securityUpdateEligible': True}
i += 2
else:
controlflow.invalid_argument_exit(
myarg, f"gam <users> {operation} drivefile")
@@ -5417,9 +5455,9 @@ def printShowLabels(users, show=True):
label['email'] = user
if not show:
display.write_csv_file(labels,
titles,
list_type='Gmail Labels',
todrive=False)
titles,
'Gmail Labels',
todrive)
def showGmailProfile(users):
@@ -7090,9 +7128,14 @@ def getUserAttributes(i, cd, updateCmd):
controlflow.invalid_argument_exit(
sys.argv[i], f"gam {['create', 'update'][updateCmd]} user")
if need_password:
# generate a password with unicode chars that are not allowed in
# passwords. We expect "password random nohash" to fail but no one
# should be using that. Our goal here is to purposefully block login
# with this password.
pass_chars = [chr(i) for i in range(1, 55296)]
rnd = SystemRandom()
body['password'] = ''.join(
rnd.choice(PASSWORD_SAFE_CHARS) for _ in range(100))
rnd.choice(pass_chars) for _ in range(4096))
if 'password' in body and need_to_hash_password:
body['password'] = gen_sha512_hash(body['password'])
body['hashFunction'] = 'crypt'
@@ -7115,12 +7158,7 @@ def getCRMService(login_hint):
login_hint=login_hint,
use_console_flow=not GC_Values[GC_OAUTH_BROWSER])
httpc = transport.AuthorizedHttp(creds, transport.create_http())
return getService('cloudresourcemanagerv1', httpc), httpc
# Ugh, v2 doesn't contain all the operations of v1 so we need to use both here.
def getCRM2Service(httpc):
return getService('cloudresourcemanager', httpc)
return getService('cloudresourcemanager', httpc), httpc
def getGAMProjectFile(filepath):
@@ -7220,19 +7258,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)
@@ -7324,11 +7362,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:
@@ -7413,10 +7452,10 @@ def _getProjects(crm, pfilter):
try:
return gapi.get_all_pages(
crm.projects(),
'list',
'search',
'projects',
throw_reasons=[gapi_errors.ErrorReason.BAD_REQUEST],
filter=pfilter)
query=pfilter)
except gapi_errors.GapiBadRequestError as e:
controlflow.system_error_exit(2, f'Project: {pfilter}, {str(e)}')
@@ -7478,23 +7517,15 @@ def _getLoginHintProjectId(createCmd):
f'Invalid Project ID: {projectId}, expected <{PROJECTID_FORMAT_REQUIRED}>'
)
crm, httpObj = getCRMService(login_hint)
if parent and not parent.startswith(
'organizations/') and not parent.startswith('folders/'):
crm2 = getCRM2Service(httpObj)
parent = convertGCPFolderNameToID(parent, crm2)
if parent:
parent_type, parent_id = parent.split('/')
if parent_type[-1] == 's':
parent_type = parent_type[:
-1] # folders > folder, organizations > organization
parent = {'type': parent_type, 'id': parent_id}
if parent and not parent.startswith('organizations/') and not parent.startswith('folders/'):
parent = convertGCPFolderNameToID(parent, crm)
projects = _getProjects(crm, f'id:{projectId}')
if not createCmd:
if not projects:
controlflow.system_error_exit(
2,
f'User: {login_hint}, Project ID: {projectId}, Does not exist')
if projects[0]['lifecycleState'] != 'ACTIVE':
if projects[0]['state'] != 'ACTIVE':
controlflow.system_error_exit(
2, f'User: {login_hint}, Project ID: {projectId}, Not active')
else:
@@ -7507,17 +7538,11 @@ def _getLoginHintProjectId(createCmd):
PROJECTID_FILTER_REQUIRED = 'gam|<ProjectID>|(filter <String>)'
def convertGCPFolderNameToID(parent, crm2):
# crm2.folders() is broken requiring pageToken, etc in body, not URL.
# for now just use gapi.get_items and if user has that many folders they'll
# just need to be specific.
folders = gapi.get_items(crm2.folders(),
'search',
items='folders',
body={
'pageSize': 1000,
'query': f'displayName="{parent}"'
})
def convertGCPFolderNameToID(parent, crm):
folders = gapi.get_all_pages(crm.folders(),
'search',
'folders',
query=f'displayName="{parent}"')
if not folders:
controlflow.system_error_exit(
1, f'ERROR: No folder found matching displayName={parent}')
@@ -7531,15 +7556,14 @@ def convertGCPFolderNameToID(parent, crm2):
def createGCPFolder():
displayName = sys.argv[3]
login_hint = _getValidateLoginHint()
_, httpObj = getCRMService(login_hint)
crm2 = getCRM2Service(httpObj)
gapi.call(crm2.folders(),
'create',
body={
'name': sys.argv[3],
'displayName': sys.argv[3]
})
login_domain = login_hint.split('@')[-1]
crm, _ = getCRMService(login_hint)
organization = getGCPOrg(crm, login_domain)
result = gapi.call(crm.folders(), 'create',
body={'parent': organization, 'displayName': displayName})
print(f'User: {login_hint}, Folder: {displayName}, GCP Folder Name: {result["name"]}, Created')
def _getLoginHintProjects(printShowCmd):
@@ -7593,16 +7617,31 @@ def _checkForExistingProjectFiles():
)
def getGCPOrg(crm, domain):
resp = gapi.call(crm.organizations(),
'search',
query=f'domain:{domain}')
try:
organization = resp['organizations'][0]['name']
print(f'Your organization name is {organization}')
return organization
except (KeyError, IndexError):
controlflow.system_error_exit(
3,
'you have no rights to create projects for your organization and you don\'t seem to be a super admin! Sorry, there\'s nothing more I can do.'
)
def doCreateProject():
_checkForExistingProjectFiles()
crm, httpObj, login_hint, projectId, parent = _getLoginHintProjectId(True)
login_domain = login_hint[login_hint.find('@') + 1:]
body = {'projectId': projectId, 'name': 'GAM Project'}
body = {'projectId': projectId, 'displayName': 'GAM Project'}
if parent:
body['parent'] = parent
while True:
create_again = False
print(f'Creating project "{body["name"]}"...')
print(f'Creating project "{body["displayName"]}"...')
create_operation = gapi.call(crm.projects(), 'create', body=body)
operation_name = create_operation['name']
time.sleep(8) # Google recommends always waiting at least 5 seconds
@@ -7617,18 +7656,7 @@ def doCreateProject():
'Hmm... Looks like you have no rights to your Google Cloud Organization.'
)
print('Attempting to fix that...')
getorg = gapi.call(
crm.organizations(),
'search',
body={'filter': f'domain:{login_domain}'})
try:
organization = getorg['organizations'][0]['name']
print(f'Your organization name is {organization}')
except (KeyError, IndexError):
controlflow.system_error_exit(
3,
'you have no rights to create projects for your organization and you don\'t seem to be a super admin! Sorry, there\'s nothing more I can do.'
)
organization = getGCPOrg(crm, login_domain)
org_policy = gapi.call(crm.organizations(),
'getIamPolicy',
resource=organization)
@@ -7709,7 +7737,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):
@@ -7861,7 +7889,7 @@ def doCreateOrRotateServiceAccountKeys(iam=None,
i += 1
elif myarg == 'yubikeyslot':
new_data['yubikey_slot'] = sys.argv[i+1].upper()
i =+ 2
i += 2
elif myarg == 'yubikeypin':
new_data['yubikey_pin'] = input('Enter your YubiKey PIN: ')
i += 1
@@ -7884,6 +7912,10 @@ def doCreateOrRotateServiceAccountKeys(iam=None,
new_data['yubikey_key_type'] = f'RSA{local_key_size}'
new_data.pop('private_key', None)
yk = yubikey.YubiKey(new_data)
if 'yubikey_serial_number' not in new_data:
new_data['yubikey_serial_number'] = yk.get_serial_number()
if 'yubikey_slot' not in new_data:
new_data['yubikey_slot'] = 'AUTHENTICATION'
publicKeyData = yk.get_certificate()
elif local_key_size:
# Generate private key locally, store in file
@@ -7907,6 +7939,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
@@ -7931,6 +7964,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)
@@ -7956,6 +7990,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
@@ -8014,7 +8049,7 @@ def doDelProjects():
gapi.call(crm.projects(),
'delete',
throw_reasons=[gapi_errors.ErrorReason.FORBIDDEN],
projectId=projectId)
name=project['name'])
print(f' Project: {projectId} Deleted{currentCount(i, count)}')
except gapi_errors.GapiForbiddenError as e:
print(
@@ -8028,8 +8063,9 @@ def doPrintShowProjects(csvFormat):
csvRows = []
todrive = False
titles = [
'User', 'projectId', 'projectNumber', 'name', 'createTime',
'lifecycleState'
'User', 'projectId', 'name', 'displayName',
'createTime', 'updateTime', 'deleteTime',
'state'
]
while i < len(sys.argv):
myarg = sys.argv[i].lower()
@@ -8046,19 +8082,19 @@ def doPrintShowProjects(csvFormat):
for project in projects:
i += 1
print(f' Project: {project["projectId"]}{currentCount(i, count)}')
print(f' projectNumber: {project["projectNumber"]}')
print(f' name: {project["name"]}')
print(f' createTime: {project["createTime"]}')
print(f' lifecycleState: {project["lifecycleState"]}')
print(f' displayName: {project["displayName"]}')
for field in ['createTime', 'updateTime', 'deleteTime']:
if field in project:
print(f' {field}: {project[field]}')
print(f' state: {project["state"]}')
jcount = len(project.get('labels', []))
if jcount > 0:
print(' labels:')
for k, v in list(project['labels'].items()):
print(f' {k}: {v}')
if 'parent' in project:
print(' parent:')
print(f' type: {project["parent"]["type"]}')
print(f' id: {project["parent"]["id"]}')
print(f' parent: {project["parent"]}')
else:
for project in projects:
display.add_row_titles_to_csv_file(
@@ -9018,31 +9054,31 @@ def doGetUserInfo(user_email=None):
print('No access to show user groups.')
elif getCIGroups:
memberships = gapi_cloudidentity_groups.get_membership_graph(user_email)
print('\nGroup Mmebership Tree:')
group_name_mapping = {}
group_displayname_mapping = {}
groups = memberships.get('groups', [])
for group in groups:
group_name = group.get('name')
group_key = group.get('groupKey', {})
group_email = group_key.get('id', '')
group_display_name = group.get('displayName', '')
group_name_mapping[group_name] = group_email
group_displayname_mapping[group_email] = group_display_name
edges = []
seen_group_count = {}
groups_with_multi_memberships = []
for adj in memberships.get('adjacencyList', []):
group_name = adj.get('group', '')
group_email = group_name_mapping[group_name]
for edge in adj.get('edges', []):
seen_group_count[group_email] = seen_group_count.get(group_email, 0) + 1
member_email = edge.get('preferredMemberKey', {}).get('id')
edges.append((member_email, group_email))
print_group_map(user_email, group_displayname_mapping, seen_group_count, edges, spaces=3, direct=True)
if max(seen_group_count.values()) > 1:
print()
print(' * user has multiple direct or inherited memberships in group')
print('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:')
@@ -9059,19 +9095,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=3, direct=False):
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]
if direct:
direction = 'direct'
else:
direction = 'inherited'
output = f'{" " * spaces}{group_display_name} <{a_child}> ({direction})'
if seen_group_count[a_child] > 1:
output += ' *'
print(output)
print_group_map(a_child, group_name_mappings, seen_group_count, edges, spaces+2)
print_group_map(a_child, group_name_mappings, seen_group_count, edges, spaces+2, 'inherited')
def doGetAliasInfo(alias_email=None):
cd = buildGAPIObject('directory')
@@ -11346,6 +11378,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)
@@ -11408,6 +11442,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)
@@ -11544,6 +11580,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)
@@ -11659,6 +11697,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)
@@ -11809,6 +11851,12 @@ def ProcessGAMCommand(args):
elif command == 'getcommand':
gapi_directory_cros.get_command()
sys.exit(0)
elif command in ['yubikey']:
action = sys.argv[2].lower().replace('_', '')
if action == 'resetpiv':
yk = yubikey.YubiKey()
yk.reset_piv()
sys.exit(0)
users = getUsersToModify()
command = sys.argv[3].lower()
if command == 'print' and len(sys.argv) == 4:

View File

@@ -1,72 +1,151 @@
from base64 import b64encode
import datetime
from secrets import SystemRandom
import string
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 ykman.piv import generate_self_signed_certificate, \
generate_chuid
from yubikit.piv import DEFAULT_MANAGEMENT_KEY, \
InvalidPinError, \
KEY_TYPE, \
MANAGEMENT_KEY_TYPE, \
PIN_POLICY, \
PivSession, \
OBJECT_ID, \
SLOT, \
TOUCH_POLICY
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 __init__(self, service_account_info=None):
self.key_type = None
self.slot = None
self.serial_number = None
self.pin = None
self.key_id = None
if 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 _connect(self):
conn, _, _ = connect_to_device(self.serial_number)
return conn
def get_certificate(self):
try:
conn, _, _ = connect_to_device(self.serial_number)
session = PivSession(conn)
if self.pin:
conn = self._connect()
with conn:
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:
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}')
cert = session.get_certificate(self.slot)
except ApduError as err:
controlflow.system_error_exit(9, f'Yubikey = {err}')
cert_pem = cert.public_bytes(
serialization.Encoding.PEM).decode()
publicKeyData = b64encode(cert_pem.encode())
if isinstance(publicKeyData, bytes):
publicKeyData = publicKeyData.decode()
return publicKeyData
except ValueError as err:
controlflow.system_error_exit(9, f'YubiKey - {err}')
def get_serial_number(self):
try:
_, _, info = connect_to_device(self.serial_number)
return info.serial
except ValueError as err:
controlflow.system_error_exit(9, f'YubikKey = {err}')
def reset_piv(self):
'''Resets YubiKey PIV app and generates new key for GAM to use.'''
reply = str(input('This will wipe all PIV keys and configuration from your YubiKey. Are you sure? (y/N) ').lower().strip())
if reply != 'y':
sys.exit(1)
try:
conn = self._connect()
with conn:
piv = PivSession(conn)
piv.reset()
rnd = SystemRandom()
pin_puk_chars = string.ascii_letters + string.digits + string.punctuation
new_puk = ''.join(rnd.choice(pin_puk_chars) for _ in range(8))
new_pin = ''.join(rnd.choice(pin_puk_chars) for _ in range(8))
piv.change_puk('12345678', new_puk)
piv.change_pin('123456', new_pin)
print(f'PIN set to: {new_pin}')
piv.authenticate(MANAGEMENT_KEY_TYPE.TDES,
DEFAULT_MANAGEMENT_KEY)
piv.verify_pin(new_pin)
print('Yubikey is generating a non-exportable private key...')
pubkey = piv.generate_key(SLOT.AUTHENTICATION,
KEY_TYPE.RSA2048,
PIN_POLICY.ALWAYS,
TOUCH_POLICY.NEVER)
now = datetime.datetime.utcnow()
valid_to = now + datetime.timedelta(days=36500)
subject = 'CN=GAM Created Key'
piv.authenticate(MANAGEMENT_KEY_TYPE.TDES,
DEFAULT_MANAGEMENT_KEY)
piv.verify_pin(new_pin)
cert = generate_self_signed_certificate(piv,
SLOT.AUTHENTICATION,
pubkey,
subject,
now,
valid_to)
piv.put_certificate(SLOT.AUTHENTICATION,
cert)
piv.put_object(OBJECT_ID.CHUID,
generate_chuid())
except ValueError as err:
controlflow.system_error_exit(8, 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:
conn = self._connect()
with conn:
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:
session.verify_pin(self.pin)
except InvalidPinError as err:
controlflow.system_error_exit(7, f'YubiKey - {err}')
try:
signed = session.sign(slot=self.slot,
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 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():

View File

@@ -65,9 +65,12 @@ def csv_field_error_exit(field_name, field_names):
','.join(field_names)))
def invalid_json_exit(file_name):
def invalid_json_exit(file_name, err=None):
"""Raises a system exit when invalid JSON content is encountered."""
system_error_exit(17, MESSAGE_INVALID_JSON.format(file_name))
err_msg = MESSAGE_INVALID_JSON.format(file_name)
if err:
err_msg += f'\n\n{err}'
system_error_exit(17, err_msg)
def wait_on_failure(current_attempt_num,

View File

@@ -281,6 +281,7 @@ def get_all_pages(service,
soft_errors=False,
throw_reasons=None,
retry_reasons=None,
page_args_in_body=False,
**kwargs):
"""Aggregates and returns all pages of a Google service function response.
@@ -311,15 +312,22 @@ def get_all_pages(service,
retry_reasons: A list of Google HTTP error reason strings indicating which
error should be retried, using exponential backoff techniques, when the
error reason is encountered.
page_args_in_body: Some APIs like Chrome Policy want pageToken and pageSize
in the body.
**kwargs: Additional params to pass to the request method.
Returns:
A list of all items received from all paged responses.
"""
if 'maxResults' not in kwargs and 'pageSize' not in kwargs:
if page_args_in_body:
kwargs.setdefault('body', {})
if 'maxResults' not in kwargs and 'pageSize' not in kwargs and 'pageSize' not in kwargs.get('body', {}):
page_key = _get_max_page_size_for_api_call(service, function, **kwargs)
if page_key:
kwargs.update(page_key)
if page_args_in_body:
kwargs['body'].update(page_key)
else:
kwargs.update(page_key)
all_items = []
page_token = None
total_items = 0
@@ -334,7 +342,10 @@ def get_all_pages(service,
if not page_token:
finalize_page_message(page_message)
return all_items
kwargs['pageToken'] = page_token
if page_args_in_body:
kwargs['body']['pageToken'] = page_token
else:
kwargs['pageToken'] = page_token
# TODO: Make this private once all execution related items that use this method

207
src/gam/gapi/chat.py Normal file
View 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"]}')

View File

@@ -39,6 +39,8 @@ def printshow_policies():
orgunit = None
printer_id = None
app_id = None
body = {}
namespaces = []
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
@@ -51,67 +53,85 @@ def printshow_policies():
elif myarg == 'appid':
app_id = sys.argv[i+1]
i += 2
elif myarg == 'namespace':
namespaces.extend(sys.argv[i+1].replace(',', ' ').split())
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,
}
}
body['policyTargetKey'] = {'targetResource': orgunit}
if printer_id:
body['policyTargetKey']['additionalTargetKeys'] = {'printer_id': printer_id}
namespaces = ['chrome.printers']
if not namespaces:
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:
if not namespaces:
namespaces = ['chrome.users.apps',
'chrome.devices.managedGuest.apps',
'chrome.devices.kiosk.apps']
elif not namespaces:
namespaces = [
'chrome.users',
# Not yet implemented:
# 'chrome.devices',
# 'chrome.devices.managedGuest',
# 'chrome.devices.kiosk',
'chrome.users.apps',
'chrome.devices',
'chrome.devices.kiosk',
'chrome.devices.managedGuest',
]
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)
print(f'Organizational Unit: {orgunitPath}')
for namespace in namespaces:
spacing = ' '
body['policySchemaFilter'] = f'{namespace}.*'
try:
policies = gapi.get_all_pages(svc.customers().policies(), 'resolve',
items='resolvedPolicies',
throw_reasons=throw_reasons,
customer=customer,
body=body)
body=body,
page_args_in_body=True)
except googleapiclient.errors.HttpError:
policies = []
for policy in sorted(policies, key=lambda k: k.get('value', {}).get('policySchema', '')):
# sort policies first by app/printer id then by schema name
policies = sorted(policies,
key=lambda k: (
list(k.get('targetKey', {}).get('additionalTargetKeys', {}).values()),
k.get('value', {}).get('policySchema', '')))
printed_ids = []
for policy in policies:
print()
name = policy.get('value', {}).get('policySchema', '')
schema = CHROME_SCHEMA_TYPE_MESSAGE.get(name)
print(name)
for key, val in policy['targetKey'].get('additionalTargetKeys', {}).items():
additional_id = f'{key} - {val}'
if additional_id not in printed_ids:
print(f' {additional_id}')
printed_ids.append(additional_id)
spacing = ' '
print(f'{spacing}{name}')
values = policy.get('value', {}).get('value', {})
for setting, value in values.items():
# Handle TYPE_MESSAGE fields with durations or counts as a special case
# Handle TYPE_MESSAGE fields with durations, values, counts and timeOfDay as special cases
schema = CHROME_SCHEMA_TYPE_MESSAGE.get(name, {}).get(setting.lower())
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']
vtype = schema['type']
if vtype in {'duration', 'value'}:
value = value.get(vtype, '')
if value:
if value.endswith('s'):
value = value[:-1]
value = int(value) // schema['scale']
elif vtype == 'count':
pass
else: ##timeOfDay
hours = value.get(vtype, {}).get('hours', 0)
minutes = value.get(vtype, {}).get('minutes', 0)
value = f'{hours:02}:{minutes:02}'
elif isinstance(value, str) and value.find('_ENUM_') != -1:
value = value.split('_ENUM_')[-1]
print(f' {setting}: {value}')
print(f'{spacing}{setting}: {value}')
def build_schemas(svc=None, sfilter=None):
@@ -254,21 +274,45 @@ def delete_policy():
CHROME_SCHEMA_TYPE_MESSAGE = {
'chrome.users.SessionLength':
{'field': 'sessiondurationlimit', 'casedField': 'sessionDurationLimit',
'type': 'duration', 'minVal': 1, 'maxVal': 1440, 'scale': 60},
'chrome.users.AutoUpdateCheckPeriodNew': {
'autoupdatecheckperiodminutesnew':
{'casedField': 'autoUpdateCheckPeriodMinutesNew',
'type': 'duration', 'minVal': 1, 'maxVal': 720, 'scale': 60}},
'chrome.users.BrowserSwitcherDelayDuration':
{'field': 'browserswitcherdelayduration', 'casedField': 'browserSwitcherDelayDuration',
'type': 'duration', 'minVal': 0, 'maxVal': 30, 'scale': 1},
{'browserswitcherdelayduration':
{'casedField': 'browserSwitcherDelayDuration',
'type': 'duration', 'minVal': 0, 'maxVal': 30, 'scale': 1}},
'chrome.users.FetchKeepaliveDurationSecondsOnShutdown':
{'fetchkeepalivedurationsecondsonshutdown':
{'casedField': 'fetchKeepaliveDurationSecondsOnShutdown',
'type': 'duration', 'minVal': 0, 'maxVal': 5, '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},
{'maxinvalidationfetchdelay':
{'casedField': 'maxInvalidationFetchDelay',
'type': 'duration', 'minVal': 1, 'maxVal': 30, 'scale': 1, 'default': 10}},
'chrome.users.PrintingMaxSheetsAllowed':
{'field': 'printingmaxsheetsallowednullable', 'casedField': 'printingMaxSheetsAllowedNullable',
'type': 'value', 'minVal': 1, 'maxVal': None, 'scale': 1},
{'printingmaxsheetsallowednullable':
{'casedField': 'printingMaxSheetsAllowedNullable',
'type': 'value', 'minVal': 1, 'maxVal': None, 'scale': 1}},
'chrome.users.PrintJobHistoryExpirationPeriodNew':
{'printjobhistoryexpirationperioddaysnew':
{'casedField': 'printJobHistoryExpirationPeriodDaysNew',
'type': 'duration', 'minVal': -1, 'maxVal': None, 'scale': 86400}},
'chrome.users.SecurityTokenSessionSettings':
{'securitytokensessionnotificationseconds':
{'casedField': 'securityTokenSessionNotificationSeconds',
'type': 'duration', 'minVal': 0, 'maxVal': 9999, 'scale': 1}},
'chrome.users.SessionLength':
{'sessiondurationlimit':
{'casedField': 'sessionDurationLimit',
'type': 'duration', 'minVal': 1, 'maxVal': 1440, 'scale': 60}},
'chrome.users.UpdatesSuppressed':
{'updatessuppresseddurationmin':
{'casedField': 'updatesSuppressedDurationMin',
'type': 'count', 'minVal': 1, 'maxVal': 1440, 'scale': 1},
'updatessuppressedstarttime':
{'casedField': 'updatesSuppressedStartTime',
'type': 'timeOfDay'}},
}
@@ -302,19 +346,39 @@ 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
# Handle TYPE_MESSAGE fields with durations, values, counts and timeOfDay as special cases
schema = CHROME_SCHEMA_TYPE_MESSAGE.get(schemaName, {}).get(field)
if schema:
i += 1
casedField = schema['casedField']
vtype = schema['type']
if vtype != 'timeOfDay':
if 'default' not in schema:
value = gam.getInteger(sys.argv[i], casedField,
minVal=schema['minVal'], maxVal=schema['maxVal'])*schema['scale']
i += 1
elif i < len(sys.argv) and sys.argv[i].isdigit():
value = gam.getInteger(sys.argv[i], casedField,
minVal=schema['minVal'], maxVal=schema['maxVal'])*schema['scale']
i += 1
else: # Handle empty value for fields with default
value = schema['default']*schema['scale']
if i < len(sys.argv) and not sys.argv[i]:
i += 1
else:
value = utils.get_hhmm(sys.argv[i])
i += 1
if vtype == 'duration':
body['requests'][-1]['policyValue']['value'][casedField] = {vtype: f'{value}s'}
elif vtype == 'value':
body['requests'][-1]['policyValue']['value'][casedField] = {vtype: value}
elif vtype == 'count':
body['requests'][-1]['policyValue']['value'][casedField] = value
else: ##timeOfDay
hours, minutes = value.split(':')
body['requests'][-1]['policyValue']['value'][casedField] = {vtype: {'hours': hours, 'minutes': minutes}}
body['requests'][-1]['updateMask'] += f'{casedField},'
continue
expected_fields = ', '.join(schemas[myarg]['settings'])
if field not in expected_fields:
msg = f'Expected {myarg} field of {expected_fields}. Got {field}.'

View File

@@ -13,12 +13,8 @@ from gam.gapi import cloudidentity as gapi_cloudidentity
from gam.gapi.directory import customer as gapi_directory_customer
def build():
return gapi_cloudidentity.build('cloudidentity')
def create():
ci = build()
ci = gapi_cloudidentity.build()
initialGroupConfig = 'EMPTY'
gapi_directory_customer.setTrueCustomerId()
parent = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
@@ -48,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],
@@ -70,7 +65,7 @@ def create():
def delete():
ci = build()
ci = gapi_cloudidentity.build()
group = sys.argv[3]
name = group_email_to_id(ci, group)
print(f'Deleting group {group}')
@@ -78,7 +73,7 @@ def delete():
def info():
ci = build()
ci = gapi_cloudidentity.build('cloudidentity_beta')
group = gam.normalizeEmailAddressOrUID(sys.argv[3])
getUsers = True
showJoinDate = True
@@ -118,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')
@@ -129,47 +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(' Member tree:')
global cached_group_members
print(' Membership Tree:')
cached_group_members = {}
print_member_tree(ci, name)
print_member_tree(ci, name, cached_group_members, 2, True)
def print_member_tree(ci, group_id, spaces=2):
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]
if member_id.isdigit():
member_type = 'user'
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:
member_type = 'group'
member_email = member.get('preferredMemberKey', {}).get('id')
relation_type = member.get('relationType', '').lower()
if member_type == 'user':
print(f'{" " * spaces}{member_email} - user')
elif member_type == 'group':
print(f'{" " * spaces}{member_email} - group')
group_id = group_email_to_id(ci, member_email)
print_member_tree(ci, group_id, spaces + 2)
else:
print(f'unknown member type: {member_type} for {member_email}')
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 = build()
ci = gapi_cloudidentity.build()
member = gam.normalizeEmailAddressOrUID(sys.argv[3])
group = gam.normalizeEmailAddressOrUID(sys.argv[4])
group_name = gapi.call(ci.groups(),
@@ -199,7 +187,7 @@ GROUP_ROLES_MAP = {
def print_():
ci = build()
ci = gapi_cloudidentity.build('cloudidentity_beta')
i = 3
members = membersCountOnly = managers = managersCountOnly = owners = ownersCountOnly = False
gapi_directory_customer.setTrueCustomerId()
@@ -287,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':
@@ -342,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:
@@ -385,7 +373,7 @@ def print_():
def _get_groups_list(ci=None, member=None, parent=None):
if not ci:
ci = build()
ci = gapi_cloudidentity.build()
if not parent:
gapi_directory_customer.setTrueCustomerId()
parent = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
@@ -407,7 +395,7 @@ def _get_groups_list(ci=None, member=None, parent=None):
except googleapiclient.errors.HttpError:
controlflow.system_error_exit(
2,
f'enterprisemember requires Enterprise license')
'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(
@@ -424,7 +412,7 @@ def _get_groups_list(ci=None, member=None, parent=None):
def get_membership_graph(member):
ci = build()
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',
@@ -434,7 +422,7 @@ def get_membership_graph(member):
def print_members():
ci = build()
ci = gapi_cloudidentity.build('cloudidentity_beta')
todrive = False
gapi_directory_customer.setTrueCustomerId()
parent = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
@@ -550,7 +538,7 @@ def update():
]
return (role, expireTime, users_email)
ci = build()
ci = gapi_cloudidentity.build('cloudidentity_beta')
group = sys.argv[3]
myarg = sys.argv[4].lower()
items = []
@@ -834,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')

View File

@@ -3,6 +3,7 @@ from time import sleep
import gam
from gam import gapi
from gam.gapi import directory as gapi_directory
from gam.gapi import errors as gapi_errors
def get_primary(email):
@@ -53,10 +54,16 @@ def wait_for_mailbox(users):
i += 1
user = gam.normalizeEmailAddressOrUID(user)
while True:
result = gapi.call(cd.users(),
'get',
'fields=isMailboxSetup',
userKey=user)
try:
result = gapi.call(cd.users(),
'get',
'fields=isMailboxSetup',
userKey=user,
throw_reasons=[gapi_errors.ErrorReason.USER_NOT_FOUND])
except gapi_errors.GapiUserNotFoundError:
print(f'{user} mailboxIsSetup: False (user does not exist yet)')
sleep(3)
continue
mailbox_is_setup = result.get('isMailboxSetup')
print(f'{user} mailboxIsSetup: {mailbox_is_setup}')
if mailbox_is_setup:

View File

@@ -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'

View File

@@ -285,7 +285,7 @@ def showReport():
customerId = GC_Values[GC_CUSTOMER_ID]
if customerId == MY_CUSTOMER:
customerId = None
filters = parameters = actorIpAddress = startTime = endTime = eventName = orgUnitId = None
filters = parameters = actorIpAddress = groupIdFilter = startTime = endTime = eventName = orgUnitId = None
tryDate = datetime.date.today().strftime(YYYYMMDD_FORMAT)
to_drive = False
userKey = 'all'
@@ -330,6 +330,9 @@ def showReport():
elif myarg == 'ip':
actorIpAddress = sys.argv[i + 1]
i += 2
elif myarg == 'groupidfilter':
groupIdFilter = sys.argv[i + 1]
i += 2
elif myarg == 'todrive':
to_drive = True
i += 1
@@ -489,7 +492,8 @@ def showReport():
endTime=endTime,
eventName=eventName,
filters=filters,
orgUnitID=orgUnitId)
orgUnitID=orgUnitId,
groupIdFilter=groupIdFilter)
if activities:
titles = ['name']
csvRows = []

View File

@@ -254,6 +254,18 @@ def get_delta_time(argstr):
return deltaTime
def get_hhmm(argstr):
argstr = argstr.strip()
if argstr:
try:
dateTime = datetime.datetime.strptime(argstr, HHMM_FORMAT)
return argstr
except ValueError:
controlflow.system_error_exit(
2, f'expected a <{HHMM_FORMAT_REQUIRED}>; got {argstr}')
controlflow.system_error_exit(2, f'expected a <{HHMM_FORMAT_REQUIRED}>')
def get_yyyymmdd(argstr, minLen=1, returnTimeStamp=False, returnDateTime=False):
argstr = argstr.strip()
if argstr:

View File

@@ -8,7 +8,7 @@ import platform
import re
GAM_AUTHOR = 'Jay Lee <jay0lee@gmail.com>'
GAM_VERSION = '6.02'
GAM_VERSION = '6.07'
GAM_LICENSE = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
GAM_URL = 'https://git.io/gam'
@@ -286,12 +286,8 @@ PRODUCTID_NAME_MAPPINGS = {
# Legacy APIs that use v1 discovery. Newer APIs should all use v2.
V1_DISCOVERY_APIS = {
'admin',
'calendar',
'drive',
'oauth2',
'reseller',
'siteVerification',
}
API_NAME_MAPPING = {
@@ -299,7 +295,7 @@ API_NAME_MAPPING = {
'reports': 'admin',
'datatransfer': 'admin',
'drive3': 'drive',
'cloudresourcemanagerv1': 'cloudresourcemanager',
'calendar': 'calendar-json',
'cloudidentity_beta': 'cloudidentity',
}
@@ -313,8 +309,7 @@ API_VER_MAPPING = {
'classroom': 'v1',
'cloudidentity': 'v1',
'cloudidentity_beta': 'v1beta1',
'cloudresourcemanager': 'v2',
'cloudresourcemanagerv1': 'v1',
'cloudresourcemanager': 'v3',
'contactdelegation': 'v1',
'datatransfer': 'datatransfer_v1',
'directory': 'directory_v1',
@@ -478,6 +473,7 @@ DRIVEFILE_FIELDS_CHOICES_MAP = {
'lastviewedbymedate': 'lastViewedByMeDate',
'lastviewedbymetime': 'lastViewedByMeDate',
'lastviewedbyuser': 'lastViewedByMeDate',
'linksharemetadata': 'linkShareMetadata',
'md5': 'md5Checksum',
'md5checksum': 'md5Checksum',
'md5sum': 'md5Checksum',
@@ -496,6 +492,7 @@ DRIVEFILE_FIELDS_CHOICES_MAP = {
'owners': 'owners',
'parents': 'parents',
'permissions': 'permissions',
'resourcekey': 'resourceKey',
'quotabytesused': 'quotaBytesUsed',
'quotaused': 'quotaBytesUsed',
'shareable': 'shareable',
@@ -894,7 +891,6 @@ RT_TAG_REPLACE_PATTERN = re.compile(r'{(.*?)}')
LOWERNUMERIC_CHARS = string.ascii_lowercase + string.digits
ALPHANUMERIC_CHARS = LOWERNUMERIC_CHARS + string.ascii_uppercase
URL_SAFE_CHARS = ALPHANUMERIC_CHARS + '-._~'
PASSWORD_SAFE_CHARS = ALPHANUMERIC_CHARS + string.punctuation + ' '
FILENAME_SAFE_CHARS = ALPHANUMERIC_CHARS + '-_.() '
FILTER_ADD_LABEL_TO_ARGUMENT_MAP = {
@@ -1520,7 +1516,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 = [
@@ -1535,7 +1531,7 @@ USER_RELATION_TYPES = [
]
USER_WEBSITE_TYPES = [
'app_install_page', 'blog', 'ftp', 'home', 'home_page', 'other', 'profile',
'reservations', 'work'
'reservations', 'resume', 'work'
]
WEBCOLOR_MAP = {
@@ -1937,6 +1933,9 @@ DELTA_DATE_FORMAT_REQUIRED = '(+|-)<Number>(d|w|y)'
DELTA_TIME_PATTERN = re.compile(r'^([+-])(\d+)([mhdwy])$')
DELTA_TIME_FORMAT_REQUIRED = '(+|-)<Number>(m|h|d|w|y)'
HHMM_FORMAT = '%H:%M'
HHMM_FORMAT_REQUIRED = 'hh:mm'
YYYYMMDD_FORMAT = '%Y-%m-%d'
YYYYMMDD_FORMAT_REQUIRED = 'yyyy-mm-dd'

View File

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