Compare commits

...

63 Commits
v6.02 ... v6.05

Author SHA1 Message Date
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
14 changed files with 432 additions and 162 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

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

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

View File

@@ -18,7 +18,7 @@ env:
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: "e5dbb051bd3d53d6c2c70cbd87270eec1765da2e"
jobs:
build:
@@ -41,16 +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"
@@ -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 }}-20210504
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,17 +153,18 @@ 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"
TARGETARCH="--target-arch=32bit"
else
BITS="64"
TARGETARCH=""
fi
$python ./waf all --target-arch=${BITS}bit
#$python ./waf all $TARGETARCH
cd ..
$python setup.py install
#$pip install pyinstaller
@@ -184,6 +182,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 +343,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>
@@ -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>
@@ -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>)*
@@ -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>

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

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

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,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
@@ -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):
@@ -5417,9 +5451,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):
@@ -7220,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)
@@ -7324,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:
@@ -7709,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):
@@ -7907,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
@@ -7931,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)
@@ -7956,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
@@ -9018,31 +9056,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 +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=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 +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)
@@ -11408,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)
@@ -11544,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)
@@ -11659,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)

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

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

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

@@ -8,7 +8,7 @@ import platform
import re
GAM_AUTHOR = 'Jay Lee <jay0lee@gmail.com>'
GAM_VERSION = '6.02'
GAM_VERSION = '6.05'
GAM_LICENSE = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
GAM_URL = 'https://git.io/gam'
@@ -1520,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 = [
@@ -1535,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 = {

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