mirror of
https://github.com/GAM-team/GAM.git
synced 2026-06-04 06:11:39 +00:00
Compare commits
108 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d405767fb0 | ||
|
|
8d7c6d3835 | ||
|
|
e362591b7a | ||
|
|
ee5f4b73e8 | ||
|
|
0d15eb2898 | ||
|
|
4af50206ad | ||
|
|
c596937006 | ||
|
|
17eb61e1eb | ||
|
|
a333185e84 | ||
|
|
f6863ae2d6 | ||
|
|
36830250b5 | ||
|
|
4ca1c3537b | ||
|
|
eeab09eacb | ||
|
|
af16967257 | ||
|
|
75e2bf5a9a | ||
|
|
4db3bc409b | ||
|
|
32ccf414ea | ||
|
|
615e48fffc | ||
|
|
93bf3fce29 | ||
|
|
899601569a | ||
|
|
b1805b64a2 | ||
|
|
58190343b1 | ||
|
|
99d48b1939 | ||
|
|
82b66d53cb | ||
|
|
3200de56cc | ||
|
|
0a627d5c79 | ||
|
|
22399deb79 | ||
|
|
6a77617e3b | ||
|
|
2868ef99ae | ||
|
|
21557f9892 | ||
|
|
d2385ae62d | ||
|
|
a84efef389 | ||
|
|
310bcd1585 | ||
|
|
753f44deb2 | ||
|
|
df1f0f8f09 | ||
|
|
45e1b50674 | ||
|
|
0a2b048fb1 | ||
|
|
e3c5dca09d | ||
|
|
88339b7214 | ||
|
|
1f2bb18bc1 | ||
|
|
74977a6154 | ||
|
|
00413fe7a4 | ||
|
|
9bb9d331ad | ||
|
|
f022ffdff4 | ||
|
|
28dade2a34 | ||
|
|
7378b9d843 | ||
|
|
71075e95bf | ||
|
|
108990cf06 | ||
|
|
ebfdf4b052 | ||
|
|
dbf4073216 | ||
|
|
83214eaaf8 | ||
|
|
1100fdd456 | ||
|
|
481bfa5440 | ||
|
|
30282c7fbb | ||
|
|
382bc71b21 | ||
|
|
f3fba97652 | ||
|
|
7f51e35bd4 | ||
|
|
95beb8e62a | ||
|
|
1a9de867f9 | ||
|
|
b42946bbe1 | ||
|
|
40b2fd09ff | ||
|
|
a3d560a8a2 | ||
|
|
ed20fe252e | ||
|
|
375e36ff96 | ||
|
|
e7108b108e | ||
|
|
6d59daad19 | ||
|
|
21c693921b | ||
|
|
7bcd5fbed7 | ||
|
|
7104970e17 | ||
|
|
1a2950b580 | ||
|
|
085b24e1c5 | ||
|
|
8688ce6328 | ||
|
|
fbdfed81e7 | ||
|
|
94fe20607e | ||
|
|
6c62483e8e | ||
|
|
54689129c6 | ||
|
|
e9e8dd5a82 | ||
|
|
00e764b118 | ||
|
|
cee7eb970a | ||
|
|
daed17fac8 | ||
|
|
8708f4f93f | ||
|
|
c7c1bfbeba | ||
|
|
0418438b6f | ||
|
|
a2ea4d036e | ||
|
|
dc7a29908f | ||
|
|
794db5d2a4 | ||
|
|
e5f9db129b | ||
|
|
a6aecf4e9d | ||
|
|
b59bc4ec90 | ||
|
|
41920f7865 | ||
|
|
4630bf5681 | ||
|
|
1c78ebd20e | ||
|
|
80d17cfda3 | ||
|
|
a154007927 | ||
|
|
bd8274cc27 | ||
|
|
fb08991c05 | ||
|
|
7c1f06fdf7 | ||
|
|
93b38b9f95 | ||
|
|
7ffc97d301 | ||
|
|
280301f258 | ||
|
|
40daf38f80 | ||
|
|
d24925cd5f | ||
|
|
cd42d54b43 | ||
|
|
53d8ecb6bc | ||
|
|
98e87d0297 | ||
|
|
400b4af769 | ||
|
|
368701afb1 | ||
|
|
a501b89ecd |
2
.github/actions/linux-before-install.sh
vendored
2
.github/actions/linux-before-install.sh
vendored
@@ -94,7 +94,7 @@ else
|
||||
python=~/python/bin/python3
|
||||
pip=~/python/bin/pip3
|
||||
|
||||
if ([ "${ImageOS}" == "ubuntu16" ]) && [ "${HOSTTYPE}" == "x86_64" ]; then
|
||||
if ([ "${ImageOS}" == "ubuntu20" ]) && [ "${HOSTTYPE}" == "x86_64" ]; then
|
||||
echo "Installing deps for StaticX..."
|
||||
if [ ! -d patchelf-$PATCHELF_VERSION ]; then
|
||||
echo "Downloading PatchELF $PATCHELF_VERSION"
|
||||
|
||||
6
.github/actions/linux-install.sh
vendored
6
.github/actions/linux-install.sh
vendored
@@ -17,10 +17,10 @@ tar -C ${distpath} --create --file $GAM_ARCHIVE --xz gam
|
||||
echo "PyInstaller GAM info:"
|
||||
du -h $gam
|
||||
time $gam version extended
|
||||
if ([ "${ImageOS}" == "ubuntu16" ]) && [ "${HOSTTYPE}" == "x86_64" ]; then
|
||||
if ([ "${ImageOS}" == "ubuntu20" ]) && [ "${HOSTTYPE}" == "x86_64" ]; then
|
||||
GAM_LEGACY_ARCHIVE=gam-${GAMVERSION}-${GAMOS}-${PLATFORM}-legacy.tar.xz
|
||||
$python -OO -m staticx -l /lib/x86_64-linux-gnu/libresolv.so.2 -l /lib/x86_64-linux-gnu/libnss_dns.so.2 $gam $gam-staticx
|
||||
strip $gam-staticx
|
||||
$python -OO -m staticx $gam $gam-staticx
|
||||
#strip $gam-staticx
|
||||
rm $gampath/gam
|
||||
mv $gam-staticx $gam
|
||||
chmod 755 $gam
|
||||
|
||||
8
.github/actions/macos-before-install.sh
vendored
8
.github/actions/macos-before-install.sh
vendored
@@ -22,18 +22,14 @@ 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..."
|
||||
sudo installer -pkg ./$pyfile -target /
|
||||
|
||||
# This fixes https://github.com/pyinstaller/pyinstaller/issues/5062
|
||||
codesign --remove-signature /Library/Frameworks/Python.framework/Versions/3.9/Python
|
||||
#codesign --remove-signature /Library/Frameworks/Python.framework/Versions/3.10/Python
|
||||
|
||||
#if [ ! -f python-$MIN_PYTHON_VERSION-macosx10.9.pkg ]; then
|
||||
# wget --quiet https://www.python.org/ftp/python/$MIN_PYTHON_VERSION/python-$MIN_PYTHON_VERSION-macosx10.9.pkg
|
||||
|
||||
4
.github/actions/windows-before-install.sh
vendored
4
.github/actions/windows-before-install.sh
vendored
@@ -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
|
||||
|
||||
4
.github/actions/windows-install.sh
vendored
4
.github/actions/windows-install.sh
vendored
@@ -7,9 +7,7 @@ echo "compiling GAM with pyinstaller..."
|
||||
export distpath="dist/"
|
||||
export gampath="${distpath}gam"
|
||||
rm -rf $gampath
|
||||
#mkdir -p $gampath
|
||||
#export gampath=$(readlink -e $gampath)
|
||||
pyinstaller --clean --noupx --distpath $gampath gam.spec
|
||||
/c/python/scripts/pyinstaller --clean --noupx --distpath $gampath gam.spec
|
||||
export gam="${gampath}/gam"
|
||||
echo "running compiled GAM..."
|
||||
$gam version
|
||||
|
||||
77
.github/workflows/build.yml
vendored
77
.github/workflows/build.yml
vendored
@@ -12,13 +12,13 @@ defaults:
|
||||
working-directory: src
|
||||
|
||||
env:
|
||||
BUILD_PYTHON_VERSION: "3.9.5"
|
||||
MIN_PYTHON_VERSION: "3.9.5"
|
||||
BUILD_OPENSSL_VERSION: "1.1.1k"
|
||||
MIN_OPENSSL_VERSION: "1.1.1k"
|
||||
PATCHELF_VERSION: "0.12"
|
||||
BUILD_PYTHON_VERSION: "3.10.0"
|
||||
MIN_PYTHON_VERSION: "3.10.0"
|
||||
BUILD_OPENSSL_VERSION: "1.1.1l"
|
||||
MIN_OPENSSL_VERSION: "1.1.1l"
|
||||
PATCHELF_VERSION: "0.13"
|
||||
# PYINSTALLER_VERSION can be full commit hash or version like v4.20
|
||||
PYINSTALLER_VERSION: "000275e409640320cdd995a7f077abfdece86749"
|
||||
PYINSTALLER_VERSION: "1e6a8d53f150cf24b574c32085f3745cbd2afaa6"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -26,67 +26,55 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-16.04
|
||||
- os: ubuntu-18.04
|
||||
jid: 1
|
||||
goal: "build"
|
||||
gamos: "linux"
|
||||
platform: "x86_64"
|
||||
- os: ubuntu-18.04
|
||||
- os: ubuntu-20.04
|
||||
jid: 2
|
||||
goal: "build"
|
||||
gamos: "linux"
|
||||
platform: "x86_64"
|
||||
- os: ubuntu-20.04
|
||||
jid: 3
|
||||
goal: "build"
|
||||
gamos: "linux"
|
||||
platform: "x86_64"
|
||||
- os: macos-10.15
|
||||
jid: 4
|
||||
goal: "build"
|
||||
gamos: "macos"
|
||||
platform: "x86_64"
|
||||
- os: macos-11.0
|
||||
jid: 12
|
||||
jid: 3
|
||||
goal: "build"
|
||||
gamos: "macos"
|
||||
platform: "universal2"
|
||||
- os: windows-2019
|
||||
jid: 5
|
||||
jid: 4
|
||||
goal: "build"
|
||||
gamos: "windows"
|
||||
python: 3.9.5
|
||||
pyarch: "x64"
|
||||
platform: "x86_64"
|
||||
- os: windows-2019
|
||||
jid: 6
|
||||
jid: 5
|
||||
goal: "build"
|
||||
gamos: "windows"
|
||||
platform: "x86"
|
||||
python: 3.9.5
|
||||
pyarch: "x86"
|
||||
- os: ubuntu-20.04
|
||||
goal: "test"
|
||||
python: "3.6"
|
||||
jid: 7
|
||||
jid: 6
|
||||
gamos: "linux"
|
||||
platform: "x86_64"
|
||||
- os: ubuntu-20.04
|
||||
goal: "test"
|
||||
python: "3.7"
|
||||
jid: 8
|
||||
jid: 7
|
||||
gamos: "linux"
|
||||
platform: "x86_64"
|
||||
- os: ubuntu-20.04
|
||||
goal: "test"
|
||||
python: "3.8"
|
||||
jid: 9
|
||||
jid: 8
|
||||
gamos: "linux"
|
||||
platform: "x86_64"
|
||||
- os: ubuntu-20.04
|
||||
goal: test
|
||||
python: "3.10.0-beta.1"
|
||||
jid: 10
|
||||
python: "3.9"
|
||||
jid: 9
|
||||
gamos: linux
|
||||
platform: x86_64
|
||||
|
||||
@@ -104,7 +92,7 @@ jobs:
|
||||
path: |
|
||||
~/python
|
||||
~/ssl
|
||||
key: ${{ matrix.os }}-${{ matrix.jid }}-20210611
|
||||
key: ${{ matrix.os }}-${{ matrix.jid }}-20211014
|
||||
|
||||
- name: Set env variables
|
||||
env:
|
||||
@@ -119,13 +107,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: |
|
||||
@@ -153,6 +161,7 @@ jobs:
|
||||
echo "pip=$pip" >> $GITHUB_ENV
|
||||
echo "LD_LIBRARY_PATH=$LD_LIBRARY_PATH" >> $GITHUB_ENV
|
||||
echo -e "Python: $python\nPip: $pip\nLD_LIB...: $LD_LIBRARY_PATH"
|
||||
$pip install --upgrade pip
|
||||
$pip install wheel
|
||||
export url="https://codeload.github.com/pyinstaller/pyinstaller/tar.gz/${PYINSTALLER_VERSION}"
|
||||
echo "Downloading ${url}"
|
||||
@@ -171,7 +180,8 @@ jobs:
|
||||
$python ./waf all $TARGETARCH
|
||||
cd ..
|
||||
fi
|
||||
$python setup.py install
|
||||
$pip install .
|
||||
#$python setup.py install
|
||||
#$pip install pyinstaller
|
||||
|
||||
- name: Install pip requirements
|
||||
@@ -205,6 +215,7 @@ jobs:
|
||||
- name: Basic Tests build jobs only
|
||||
if: matrix.goal != 'test'
|
||||
run: |
|
||||
$pip install packaging
|
||||
export vline=$($gam version | grep "Python ")
|
||||
export python_line=($vline)
|
||||
export this_python=${python_line[1]}
|
||||
@@ -246,6 +257,8 @@ jobs:
|
||||
$gam user $gam_user sendemail recipient $newuser subject "test message $newbase" message "GHA test message"
|
||||
$gam user $gam_user sendemail recipient exchange@pdl.jaylee.us subject "test ${tstamp}" message "test message"
|
||||
$gam create group $newgroup name "GHA $JID group" description "This is a description" isarchived true
|
||||
$gam update cigroup $newgroup memberrestriction 'member.type == 1 || member.customer_id == groupCustomerId()'
|
||||
$gam info cigroup $newgroup
|
||||
$gam user $newuser add license gsuitebusiness
|
||||
$gam update group $newgroup add owner $gam_user
|
||||
$gam update group $newgroup add member $newuser
|
||||
@@ -320,7 +333,7 @@ jobs:
|
||||
$gam print browsers
|
||||
export sn="$JID$JID$JID$JID-$(openssl rand -base64 32 | sed 's/[^a-zA-Z0-9]//g')"
|
||||
$gam create device serialnumber $sn devicetype android
|
||||
$gam print cros allfields nolists
|
||||
$gam print cros allfields orderby serialnumber
|
||||
$gam report usageparameters customer
|
||||
$gam report usage customer parameters gmail:num_emails_sent,accounts:num_1day_logins
|
||||
$gam report customer todrive
|
||||
|
||||
27
README.md
27
README.md
@@ -1,23 +1,46 @@
|
||||
GAM is a command line tool for Google Workspace (fka G Suite) Administrators to manage domain and user settings quickly and easily.
|
||||
|
||||

|
||||
|
||||
# Quick Start
|
||||
|
||||
## Linux / MacOS
|
||||
|
||||
Open a terminal and run:
|
||||
```
|
||||
|
||||
```sh
|
||||
bash <(curl -s -S -L https://git.io/install-gam)
|
||||
```
|
||||
|
||||
this will download GAM, install it and start setup.
|
||||
|
||||
To install with `pip`, run
|
||||
|
||||
```sh
|
||||
pip install git+https://github.com/jay0lee/GAM.git#subdirectory=src
|
||||
```
|
||||
|
||||
This will only download and install GAM. To start setup, simply invoke the `gam` CLI.
|
||||
|
||||
## Windows
|
||||
|
||||
Download the MSI Installer from the [GitHub Releases] page. Install the MSI and you'll be prompted to setup GAM.
|
||||
|
||||
# Documentation
|
||||
|
||||
The GAM documentation is hosted in the [GitHub Wiki]
|
||||
|
||||
# Mailing List / Discussion group
|
||||
|
||||
The GAM mailing list / discussion group is hosted on [Google Groups]. You can join the list and interact via email, or just post from the web itself.
|
||||
|
||||
# Chat Room
|
||||
|
||||
There is a public chat room hosted in Google Chat. [Instructions to join](https://git.io/gam-chat).
|
||||
|
||||
# Author
|
||||
GAM is maintained by <a href="mailto:jay0lee@gmail.com">Jay Lee</a>. Please direct "how do I?" questions to [Google Groups].
|
||||
|
||||
GAM is maintained by [Jay Lee](mailto:jay0lee@gmail.com). Please direct "how do I?" questions to [Google Groups].
|
||||
|
||||
[GAM release]: https://git.io/gamreleases
|
||||
[GitHub Releases]: https://github.com/jay0lee/GAM/releases
|
||||
|
||||
@@ -204,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>
|
||||
@@ -221,8 +222,10 @@ If an item contains spaces, it should be surrounded by ".
|
||||
<QueryContact> ::= <String> See: https://developers.google.com/google-apps/contacts/v3/reference#contacts-query-parameters-reference
|
||||
<QueryCrOS> ::= <String> See: https://support.google.com/chrome/a/answer/1698333?hl=en
|
||||
<QueryDriveFile> ::= <String> See: https://developers.google.com/drive/v2/web/search-parameters
|
||||
<QueryDynamicGroup> ::= <String> See: https://cloud.google.com/identity/docs/reference/rest/v1beta1/groups#dynamicgroupquery
|
||||
<QueryGmail> ::= <String> See: https://support.google.com/mail/answer/7190
|
||||
<QueryGroup> ::= <String> See: https://developers.google.com/admin-sdk/directory/v1/guides/search-groups
|
||||
<QueryMemberRestrictions> ::= <String> See: https://cloud.google.com/identity/docs/reference/rest/v1beta1/SecuritySettings#MemberRestriction
|
||||
<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
|
||||
@@ -333,6 +336,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|
|
||||
@@ -345,11 +349,13 @@ If an item contains spaces, it should be surrounded by ".
|
||||
parents|
|
||||
permissions|
|
||||
quotabytesused|quotaused|
|
||||
resourcekey|
|
||||
restricted|
|
||||
shareable|
|
||||
shared|
|
||||
sharedwithmedate|sharedwithmetime|
|
||||
sharinguser|
|
||||
shortcutdetails|
|
||||
size|
|
||||
spaces|
|
||||
starred|
|
||||
@@ -592,6 +598,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>)*"
|
||||
@@ -694,8 +701,10 @@ Specify a collection of Users by directly specifying them or by specifiying item
|
||||
(contentrestrictions readonly true [reason <String>])|
|
||||
copyrequireswriterpermission|
|
||||
(lastviewedbyme <Time>)|(modifieddate|modifiedtime <Time>)|(description <String>)|(mimetype <MimeType>)|
|
||||
(parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|writerscantshare|writerscanshare
|
||||
(shortcut <DriveFileID>)
|
||||
(parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|
|
||||
(securityupdate <Boolean>)|
|
||||
(shortcut <DriveFileID>)|
|
||||
writerscantshare|writerscanshare
|
||||
<DriveFileUpdateAttribute> ::=
|
||||
(localfile <FileName>|-)|
|
||||
(convert)|(ocr)|(ocrlanguage <Language>)|
|
||||
@@ -704,8 +713,10 @@ Specify a collection of Users by directly specifying them or by specifiying item
|
||||
(contentrestrictions readonly true [reason <String>])|
|
||||
(copyrequireswriterpermission <Boolean>)|
|
||||
(lastviewedbyme <Time>)|(modifieddate <Time>)|(description <String>)|(mimetype <MimeType>)|
|
||||
(parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|writerscantshare|writerscanshare
|
||||
(shortcut <DriveFileID>)
|
||||
(parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|
|
||||
(securityupdate <Boolean>)|
|
||||
(shortcut <DriveFileID>)|
|
||||
writerscantshare|writerscanshare
|
||||
<GroupSettingsAttribute> ::=
|
||||
(allowexternalmembers <Boolean>)|
|
||||
(allowwebposting <Boolean>)|
|
||||
@@ -713,6 +724,7 @@ Specify a collection of Users by directly specifying them or by specifiying item
|
||||
(customfootertext <String>)|
|
||||
(customreplyto <EmailAddress>)|
|
||||
(defaultmessagedenynotificationtext <String>)|
|
||||
(defaultsender default_self|group)|
|
||||
(description <String>)|
|
||||
(enablecollaborativeinbox|collaborative <Boolean>)|
|
||||
(includeinglobaladdresslist|gal <Boolean>)|
|
||||
@@ -790,7 +802,6 @@ Specify a collection of Users by directly specifying them or by specifiying item
|
||||
field <FieldName> (type bool|date|double|email|int64|phone|string) [multivalued|multivalue] [indexed] [restricted] [range <Number> <Number>] endfield
|
||||
|
||||
<UserBasicAttribute> ::=
|
||||
(agreed2terms|agreedtoterms <Boolean>)|
|
||||
(changepassword|changepasswordatnextlogin <Boolean>)|
|
||||
(base64-md5|base64-sha1|crypt|sha|sha1|sha-1|md5|nohash)|
|
||||
(customerid <String>)|
|
||||
@@ -843,6 +854,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>]
|
||||
@@ -955,6 +968,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>
|
||||
@@ -1308,7 +1322,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>
|
||||
@@ -1372,18 +1386,21 @@ gam print printermodels [todrive] [filter <String>]
|
||||
|
||||
gam create cigroup <EmailAddress> <CIGroupAttribute>*
|
||||
[makeowner] [alias|aliases <AliasList>] [dynamic <QueryDynamicGroup>]
|
||||
gam update cigroup <GroupItem> [email <EmailAddress>] <CIGroupAttribute>* [security]
|
||||
gam update cigroup <GroupItem> [email <EmailAddress>] <CIGroupAttribute>*
|
||||
[security] [dynamic <QueryDynamicGroup>]
|
||||
[memberrestrictions <QueryMemberRestrictions>]
|
||||
gam update cigroup <GroupItem> add [owner|manager|member] [notsuspended|suspended] [expires never|<Time>] <UserTypeEntity>
|
||||
gam update cigroup <GroupItem> delete|remove [owner|manager|member] [notsuspended|suspended] <UserTypeEntity>
|
||||
gam update cigroup <GroupItem> sync [owner|manager|member] [notsuspended|suspended] [expires never|<Time>] <UserTypeEntity>
|
||||
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] [membertree]
|
||||
gam info cigroup <GroupItem> [nousers] [nojoindate] [showupdatedate] [membertree] [nosecurity|nosecuritysettings]
|
||||
|
||||
gam print cigroups [todrive]
|
||||
[enterprisemember <UserItem>]
|
||||
[members|memberscount] [managers|managerscount] [owners|ownerscount]
|
||||
[memberrestrictions]
|
||||
[delimiter <Character>] [sortheaders]
|
||||
|
||||
gam info cimember <UserItem> <GroupItem>
|
||||
|
||||
@@ -368,7 +368,7 @@
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
},
|
||||
"path": "{customer}/chrome/enrollmentTokens",
|
||||
"request": {
|
||||
"$ref": "CreateEnrollmentTokenRequest"
|
||||
@@ -379,7 +379,7 @@
|
||||
"scopes": [
|
||||
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers"
|
||||
]
|
||||
},
|
||||
},
|
||||
"revoke": {
|
||||
"description": "Revokes a browser enrollment token in a domain.",
|
||||
"flatPath": "{customer}/chrome/enrollmentTokens/{tokenPermanentId}:revoke",
|
||||
@@ -387,7 +387,7 @@
|
||||
"id": "cbcm.enrollmentTokens.revoke",
|
||||
"parameterOrder": [
|
||||
"customer",
|
||||
"tokenPermanentId"
|
||||
"tokenPermanentId"
|
||||
],
|
||||
"parameters": {
|
||||
"customer": {
|
||||
@@ -402,12 +402,12 @@
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
},
|
||||
"path": "{customer}/chrome/enrollmentTokens/{tokenPermanentId}:revoke",
|
||||
"scopes": [
|
||||
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -491,23 +491,23 @@
|
||||
"description": "Immutable ID of the G Suite account.",
|
||||
"type": "string"
|
||||
},
|
||||
"orgUnitPath": {
|
||||
"orgUnitPath": {
|
||||
"description": "The full path of the organizational unit or its unique ID.",
|
||||
"type": "string"
|
||||
},
|
||||
"creatorId": {
|
||||
"creatorId": {
|
||||
"description": "Creator ID.",
|
||||
"type": "string"
|
||||
},
|
||||
"createTime": {
|
||||
"createTime": {
|
||||
"description": "Creation Time.",
|
||||
"type": "string"
|
||||
},
|
||||
"revokerId": {
|
||||
"revokerId": {
|
||||
"description": "Revoker ID.",
|
||||
"type": "string"
|
||||
},
|
||||
"revokeTime": {
|
||||
"revokeTime": {
|
||||
"description": "Revoke Time",
|
||||
"type": "string"
|
||||
}
|
||||
@@ -538,16 +538,18 @@
|
||||
},
|
||||
"CreateEnrollmentTokenRequest": {
|
||||
"id": "CreateEnrollmentTokenRequest",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"org_unit_path": {
|
||||
"org_unit_path": {
|
||||
"description": "The full path of the organizational unit or its unique ID.",
|
||||
"type": "string"
|
||||
},
|
||||
"expire_time": {
|
||||
"expire_time": {
|
||||
"description": "Expiration Time.",
|
||||
"type": "string"
|
||||
},
|
||||
"token_type": {
|
||||
"token_type": {
|
||||
"id": "token_type",
|
||||
"annotations": {
|
||||
"required": [
|
||||
"cbcm.enrollmentTokens.create"
|
||||
@@ -559,6 +561,8 @@
|
||||
}
|
||||
},
|
||||
"MoveChromeBrowsersRequest": {
|
||||
"id": "MoveChromeBrowsersRequest",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"org_unit_path": {
|
||||
"annotations": {
|
||||
@@ -576,7 +580,10 @@
|
||||
]
|
||||
},
|
||||
"description": "List of unique device IDs of Chrome Browser Devices to move. A maximum of 600 browsers may be moved per request.",
|
||||
"type": "array"
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ upgrade_only=false
|
||||
gamversion="latest"
|
||||
adminuser=""
|
||||
regularuser=""
|
||||
gam_glibc_vers="2.31 2.27 2.23"
|
||||
gam_glibc_vers="2.31 2.27"
|
||||
#gam_macos_vers="10.15.6 10.14.6 10.13.6"
|
||||
|
||||
while getopts "hd:a:o:b:lp:u:r:v:" OPTION
|
||||
@@ -128,7 +128,7 @@ case $gamos in
|
||||
this_macos_ver=$osversion
|
||||
fi
|
||||
echo "You are running MacOS $this_macos_ver"
|
||||
gamfile="macos-x86_64.tar.xz"
|
||||
gamfile="macos-universal2.tar.xz"
|
||||
;;
|
||||
MINGW64_NT*)
|
||||
gamos="windows"
|
||||
|
||||
@@ -8,4 +8,4 @@ from gam.__main__ import main
|
||||
|
||||
# Run from command line
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv)
|
||||
main()
|
||||
|
||||
@@ -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')]
|
||||
|
||||
@@ -33,6 +33,7 @@ import http.client as http_client
|
||||
from multiprocessing import Pool as mp_pool
|
||||
from multiprocessing import Lock as mp_lock
|
||||
from urllib.parse import quote, urlencode, urlparse
|
||||
from pathvalidate import sanitize_filename
|
||||
import dateutil.parser
|
||||
|
||||
import googleapiclient
|
||||
@@ -549,6 +550,7 @@ def SetGlobalVariables():
|
||||
filePresentValue=4,
|
||||
fileAbsentValue=0)
|
||||
_getOldSignalFile(GC_NO_BROWSER, 'nobrowser.txt')
|
||||
_getOldSignalFile(GC_NO_TDEMAIL, 'notdemail.txt')
|
||||
_getOldSignalFile(GC_OAUTH_BROWSER, 'oauthbrowser.txt')
|
||||
# _getOldSignalFile(GC_NO_CACHE, u'nocache.txt')
|
||||
# _getOldSignalFile(GC_CACHE_DISCOVERY_ONLY, u'allcache.txt', filePresentValue=False, fileAbsentValue=True)
|
||||
@@ -728,8 +730,12 @@ def getOSPlatform():
|
||||
elif myos == 'Darwin':
|
||||
myos = 'MacOS'
|
||||
mac_ver = platform.mac_ver()[0]
|
||||
major_ver = int(mac_ver.split('.')[0]) # macver 10.14.6 == major_ver 10
|
||||
minor_ver = int(mac_ver.split('.')[1]) # macver 10.14.6 == minor_ver 14
|
||||
codename = MACOS_CODENAMES.get(minor_ver, '')
|
||||
if major_ver == 10:
|
||||
codename = MACOS_CODENAMES[major_ver].get(minor_ver, '')
|
||||
else:
|
||||
codename = MACOS_CODENAMES.get(major_ver, '')
|
||||
pltfrm = ' '.join([codename, mac_ver])
|
||||
else:
|
||||
pltfrm = platform.platform()
|
||||
@@ -871,9 +877,9 @@ def getSvcAcctCredentials(scopes, act_as, api=None):
|
||||
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):
|
||||
@@ -899,8 +905,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():
|
||||
@@ -1240,9 +1246,8 @@ def doCheckServiceAccount(users):
|
||||
'get',
|
||||
name=name,
|
||||
throw_reasons=[gapi_errors.ErrorReason.FOUR_O_THREE])
|
||||
# Both Google and GAM set key valid after to day before creation
|
||||
key_created = dateutil.parser.parse(
|
||||
key['validAfterTime'], ignoretz=True) + datetime.timedelta(days=1)
|
||||
key['validAfterTime'], ignoretz=True)
|
||||
key_age = datetime.datetime.now() - key_created
|
||||
key_days = key_age.days
|
||||
if key_days > 30:
|
||||
@@ -1473,8 +1478,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):
|
||||
@@ -1757,8 +1762,8 @@ def doCreateAdmin():
|
||||
def doPrintAdmins():
|
||||
cd = buildGAPIObject('directory')
|
||||
roleId = None
|
||||
userKey = None
|
||||
todrive = False
|
||||
kwargs = {}
|
||||
fields = 'nextPageToken,items(roleAssignmentId,roleId,assignedTo,scopeType,orgUnitId)'
|
||||
titles = [
|
||||
'roleAssignmentId', 'roleId', 'role', 'assignedTo', 'assignedToUser',
|
||||
@@ -1769,7 +1774,7 @@ def doPrintAdmins():
|
||||
while i < len(sys.argv):
|
||||
myarg = sys.argv[i].lower()
|
||||
if myarg == 'user':
|
||||
userKey = normalizeEmailAddressOrUID(sys.argv[i + 1])
|
||||
kwargs['userKey'] = normalizeEmailAddressOrUID(sys.argv[i + 1])
|
||||
i += 2
|
||||
elif myarg == 'role':
|
||||
roleId = getRoleId(sys.argv[i + 1])
|
||||
@@ -1779,14 +1784,18 @@ def doPrintAdmins():
|
||||
i += 1
|
||||
else:
|
||||
controlflow.invalid_argument_exit(sys.argv[i], 'gam print admins')
|
||||
if roleId and not kwargs:
|
||||
kwargs['roleId'] = roleId
|
||||
roleId = None
|
||||
admins = gapi.get_all_pages(cd.roleAssignments(),
|
||||
'list',
|
||||
'items',
|
||||
customer=GC_Values[GC_CUSTOMER_ID],
|
||||
userKey=userKey,
|
||||
roleId=roleId,
|
||||
fields=fields)
|
||||
fields=fields,
|
||||
**kwargs)
|
||||
for admin in admins:
|
||||
if roleId and roleId != admin['roleId']:
|
||||
continue
|
||||
admin_attrib = {}
|
||||
for key, value in list(admin.items()):
|
||||
if key == 'assignedTo':
|
||||
@@ -3719,6 +3728,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")
|
||||
@@ -4053,8 +4066,7 @@ def downloadDriveFile(users):
|
||||
if targetName:
|
||||
safe_file_title = targetName
|
||||
else:
|
||||
safe_file_title = ''.join(c for c in result['title']
|
||||
if c in FILENAME_SAFE_CHARS)
|
||||
safe_file_title = sanitize_filename(result['title'])
|
||||
if not safe_file_title:
|
||||
safe_file_title = fileId
|
||||
filename = os.path.join(targetFolder, safe_file_title)
|
||||
@@ -6665,12 +6677,12 @@ def getUserAttributes(i, cd, updateCmd):
|
||||
body['changePasswordAtNextLogin'] = getBoolean(
|
||||
sys.argv[i + 1], myarg)
|
||||
i += 2
|
||||
elif myarg == 'ipwhitelisted':
|
||||
body['ipWhitelisted'] = getBoolean(sys.argv[i + 1], myarg)
|
||||
i += 2
|
||||
elif myarg == 'agreedtoterms':
|
||||
body['agreedToTerms'] = getBoolean(sys.argv[i + 1], myarg)
|
||||
i += 2
|
||||
elif myarg == 'ipwhitelisted':
|
||||
body['ipWhitelisted'] = getBoolean(sys.argv[i + 1], myarg)
|
||||
i += 2
|
||||
elif myarg in ['org', 'ou']:
|
||||
body['orgUnitPath'] = gapi_directory_orgunits.getOrgUnitItem(
|
||||
sys.argv[i + 1], pathOnly=True)
|
||||
@@ -7124,9 +7136,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'
|
||||
@@ -7149,12 +7166,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):
|
||||
@@ -7232,6 +7244,7 @@ def enableGAMProjectAPIs(GAMProjectAPIs,
|
||||
gapi_errors.ErrorReason.FORBIDDEN,
|
||||
gapi_errors.ErrorReason.PERMISSION_DENIED
|
||||
],
|
||||
retry_reasons=[gapi_errors.ErrorReason.INTERNAL_SERVER_ERROR],
|
||||
name=service_name)
|
||||
print(f' API: {api}, Enabled{currentCount(j, jcount)}')
|
||||
break
|
||||
@@ -7448,10 +7461,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)}')
|
||||
|
||||
@@ -7513,23 +7526,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:
|
||||
@@ -7542,17 +7547,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}')
|
||||
@@ -7566,15 +7565,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):
|
||||
@@ -7628,16 +7626,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
|
||||
@@ -7652,18 +7665,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)
|
||||
@@ -7763,11 +7765,9 @@ def _generatePrivateKeyAndPublicCert(client_id, key_size):
|
||||
x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, client_id)]))
|
||||
builder = builder.issuer_name(
|
||||
x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, client_id)]))
|
||||
not_valid_before = datetime.datetime.today() - datetime.timedelta(days=1)
|
||||
not_valid_after = datetime.datetime.today() + datetime.timedelta(
|
||||
days=365 * 10 - 1)
|
||||
builder = builder.not_valid_before(not_valid_before)
|
||||
builder = builder.not_valid_after(not_valid_after)
|
||||
builder = builder.not_valid_before(datetime.datetime.today())
|
||||
# Google uses 12/31/9999 date for end time
|
||||
builder = builder.not_valid_after(datetime.datetime(9999, 12, 31, 23, 59))
|
||||
builder = builder.serial_number(x509.random_serial_number())
|
||||
builder = builder.public_key(public_key)
|
||||
builder = builder.add_extension(x509.BasicConstraints(ca=False,
|
||||
@@ -7896,7 +7896,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
|
||||
@@ -7919,6 +7919,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
|
||||
@@ -7946,10 +7950,18 @@ def doCreateOrRotateServiceAccountKeys(iam=None,
|
||||
name=sa_name,
|
||||
body={'publicKeyData': publicKeyData})
|
||||
break
|
||||
except googleapiclient.errors.HttpError:
|
||||
print('WARNING: that key already exists.')
|
||||
result = {'name': oldPrivateKeyId}
|
||||
break
|
||||
except googleapiclient.errors.HttpError as err:
|
||||
if hasattr(err, 'error_details') and \
|
||||
err.error_details == 'The given public key already exists.':
|
||||
print('WARNING: that key already exists.')
|
||||
result = {'name': oldPrivateKeyId}
|
||||
break
|
||||
elif hasattr(err, 'error_details'):
|
||||
controlflow.system_error_exit(
|
||||
4, err.error_details)
|
||||
else:
|
||||
controlflow.system_error_exit(
|
||||
4, err)
|
||||
except gapi_errors.GapiNotFoundError as e:
|
||||
if i == max_retries:
|
||||
raise e
|
||||
@@ -8052,7 +8064,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(
|
||||
@@ -8066,8 +8078,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()
|
||||
@@ -8084,19 +8097,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(
|
||||
@@ -9668,6 +9681,8 @@ def doPrintUsers():
|
||||
sortHeaders = True
|
||||
i += 1
|
||||
elif myarg in ['custom', 'schemas']:
|
||||
if not fieldsList:
|
||||
fieldsList = ['primaryEmail']
|
||||
fieldsList.append('customSchemas')
|
||||
if sys.argv[i + 1].lower() == 'all':
|
||||
projection = 'full'
|
||||
@@ -11853,6 +11868,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:
|
||||
|
||||
@@ -30,7 +30,7 @@ from gam import controlflow
|
||||
import gam
|
||||
|
||||
|
||||
def main(argv):
|
||||
def main():
|
||||
freeze_support()
|
||||
if sys.platform == 'darwin':
|
||||
# https://bugs.python.org/issue33725 in Python 3.8.0 seems
|
||||
@@ -47,4 +47,4 @@ def main(argv):
|
||||
|
||||
# Run from command line
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv)
|
||||
main()
|
||||
|
||||
@@ -395,7 +395,7 @@ class Credentials(google.oauth2.credentials.Credentials):
|
||||
self.refresh(request)
|
||||
|
||||
self._id_token_data = google.oauth2.id_token.verify_oauth2_token(
|
||||
self.id_token, request)
|
||||
self.id_token, request, clock_skew_in_seconds=10)
|
||||
|
||||
def get_token_value(self, field):
|
||||
"""Retrieves data from the OAuth ID token.
|
||||
|
||||
@@ -1,72 +1,155 @@
|
||||
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 smartcard.Exceptions import CardConnectionException
|
||||
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')
|
||||
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):
|
||||
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')
|
||||
conn, _, _ = connect_to_device(self.serial_number)
|
||||
except CardConnectionException as err:
|
||||
controlflow.system_error_exit(9, f'YubiKey - {err}')
|
||||
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'YubiKey - {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():
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -158,25 +158,25 @@ def write_csv_file(csvRows, titles, list_type, todrive):
|
||||
for c, filterVal in iter(filters.items()):
|
||||
for column in columns[c]:
|
||||
if filterVal[1] == 'regex':
|
||||
if filterVal[2].search(str(row.get(column, ''))):
|
||||
return True
|
||||
elif filterVal[1] == 'notregex':
|
||||
if not filterVal[2].search(str(row.get(column, ''))):
|
||||
return True
|
||||
return False
|
||||
elif filterVal[1] == 'notregex':
|
||||
if filterVal[2].search(str(row.get(column, ''))):
|
||||
return False
|
||||
elif filterVal[1] in ['date', 'time']:
|
||||
if rowDateTimeFilterMatch(
|
||||
if not rowDateTimeFilterMatch(
|
||||
filterVal[1] == 'date', row.get(column, ''),
|
||||
filterVal[2], filterVal[3]):
|
||||
return True
|
||||
return False
|
||||
elif filterVal[1] == 'count':
|
||||
if rowCountFilterMatch(
|
||||
if not rowCountFilterMatch(
|
||||
row.get(column, 0), filterVal[2], filterVal[3]):
|
||||
return True
|
||||
return False
|
||||
else: #boolean
|
||||
if rowBooleanFilterMatch(
|
||||
if not rowBooleanFilterMatch(
|
||||
row.get(column, False), filterVal[2]):
|
||||
return True
|
||||
return False
|
||||
return False
|
||||
return True
|
||||
|
||||
if GC_Values[GC_CSV_ROW_FILTER] or GC_Values[GC_CSV_ROW_DROP_FILTER]:
|
||||
if GC_Values[GC_CSV_ROW_FILTER]:
|
||||
@@ -231,7 +231,14 @@ def write_csv_file(csvRows, titles, list_type, todrive):
|
||||
'No columns selected with GAM_CSV_HEADER_FILTER and GAM_CSV_HEADER_DROP_FILTER\n'
|
||||
)
|
||||
return
|
||||
csv.register_dialect('nixstdout', lineterminator='\n')
|
||||
nixstdout_dialect = {'lineterminator': '\n',
|
||||
'quoting': csv.QUOTE_MINIMAL}
|
||||
# fix issue with Python 3.10.0 and no escape char
|
||||
# 3.10.1+ may fix this within Python so hopefully
|
||||
# this is short-lived.
|
||||
if sys.version_info.minor >= 10:
|
||||
nixstdout_dialect['escapechar'] = '\\'
|
||||
csv.register_dialect('nixstdout', **nixstdout_dialect)
|
||||
if todrive:
|
||||
write_to = io.StringIO()
|
||||
else:
|
||||
@@ -239,8 +246,7 @@ def write_csv_file(csvRows, titles, list_type, todrive):
|
||||
writer = csv.DictWriter(write_to,
|
||||
fieldnames=titles,
|
||||
dialect='nixstdout',
|
||||
extrasaction='ignore',
|
||||
quoting=csv.QUOTE_MINIMAL)
|
||||
extrasaction='ignore')
|
||||
try:
|
||||
writer.writerow(dict((item, item) for item in writer.fieldnames))
|
||||
writer.writerows(csvRows)
|
||||
@@ -283,7 +289,8 @@ and follow recommend steps to authorize GAM for Drive access.''')
|
||||
if GC_Values[GC_NO_BROWSER]:
|
||||
msg_txt = f'Drive file uploaded to:\n {file_url}'
|
||||
msg_subj = f'{GC_Values[GC_DOMAIN]} - {list_type}'
|
||||
gam.send_email(msg_subj, msg_txt)
|
||||
if not GC_Values[GC_NO_TDEMAIL]:
|
||||
gam.send_email(msg_subj, msg_txt)
|
||||
print(msg_txt)
|
||||
else:
|
||||
webbrowser.open(file_url)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,86 @@ 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}.*'
|
||||
body['pageToken'] = None
|
||||
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 +275,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 +347,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}.'
|
||||
|
||||
@@ -405,7 +405,7 @@ def sync():
|
||||
controlflow.csv_field_error_exit(devicetype_column, input_file.fieldnames)
|
||||
if assettag_column and assettag_column not in input_file.fieldnames:
|
||||
controlflow.csv_field_error_exit(assettag_column, input_file.fieldnames)
|
||||
local_devices = []
|
||||
local_devices = {}
|
||||
for row in input_file:
|
||||
# upper() is very important to comparison since Google
|
||||
# always return uppercase serials
|
||||
@@ -414,28 +414,43 @@ def sync():
|
||||
local_device['deviceType'] = static_devicetype
|
||||
else:
|
||||
local_device['deviceType'] = row[devicetype_column].strip()
|
||||
sndt = f"{local_device['serialNumber']}-{local_device['deviceType']}"
|
||||
if assettag_column:
|
||||
local_device['assetTag'] = row[assettag_column].strip()
|
||||
local_devices.append(local_device)
|
||||
sndt += f"-{local_device['assetTag']}"
|
||||
local_devices[sndt] = local_device
|
||||
fileutils.close_file(f)
|
||||
page_message = gapi.got_total_items_msg('Company Devices', '...\n')
|
||||
device_fields = ['serialNumber', 'deviceType', 'lastSyncTime', 'name']
|
||||
if assettag_column:
|
||||
device_fields.append('assetTag')
|
||||
fields = f'nextPageToken,devices({",".join(device_fields)})'
|
||||
remote_devices = gapi.get_all_pages(ci.devices(), 'list', 'devices',
|
||||
remote_devices = {}
|
||||
remote_device_map = {}
|
||||
result = gapi.get_all_pages(ci.devices(), 'list', 'devices',
|
||||
customer=customer, page_message=page_message,
|
||||
pageSize=100, filter=device_filter, view='COMPANY_INVENTORY', fields=fields)
|
||||
remote_device_map = {}
|
||||
for remote_device in remote_devices:
|
||||
for remote_device in result:
|
||||
sn = remote_device['serialNumber']
|
||||
last_sync = remote_device.pop('lastSyncTime', NEVER_TIME_NOMS)
|
||||
name = remote_device.pop('name')
|
||||
remote_device_map[sn] = {'name': name}
|
||||
sndt = f"{remote_device['serialNumber']}-{remote_device['deviceType']}"
|
||||
if assettag_column:
|
||||
if 'assetTag' not in remote_device:
|
||||
remote_device['assetTag'] = ''
|
||||
sndt += f"-{remote_device['assetTag']}"
|
||||
remote_devices[sndt] = remote_device
|
||||
remote_device_map[sndt] = {'name': name}
|
||||
if last_sync == NEVER_TIME_NOMS:
|
||||
remote_device_map[sn]['unassigned'] = True
|
||||
devices_to_add = [device for device in local_devices if device not in remote_devices]
|
||||
missing_devices = [device for device in remote_devices if device not in local_devices]
|
||||
remote_device_map[sndt]['unassigned'] = True
|
||||
devices_to_add = []
|
||||
for sndt, device in iter(local_devices.items()):
|
||||
if sndt not in remote_devices:
|
||||
devices_to_add.append(device)
|
||||
missing_devices = []
|
||||
for sndt, device in iter(remote_devices.items()):
|
||||
if sndt not in local_devices:
|
||||
missing_devices.append(device)
|
||||
print(f'Need to add {len(devices_to_add)} and remove {len(missing_devices)} devices...')
|
||||
for add_device in devices_to_add:
|
||||
print(f'Creating {add_device["serialNumber"]}')
|
||||
@@ -447,8 +462,11 @@ def sync():
|
||||
print(f' {add_device["serialNumber"]} already exists')
|
||||
for missing_device in missing_devices:
|
||||
sn = missing_device['serialNumber']
|
||||
name = remote_device_map[sn]['name']
|
||||
unassigned = remote_device_map[sn].get('unassigned')
|
||||
sndt = f"{sn}-{missing_device['deviceType']}"
|
||||
if assettag_column:
|
||||
sndt += f"-{missing_device['assetTag']}"
|
||||
name = remote_device_map[sndt]['name']
|
||||
unassigned = remote_device_map[sndt].get('unassigned')
|
||||
action = unassigned_missing_action if unassigned else assigned_missing_action
|
||||
if action == 'donothing':
|
||||
pass
|
||||
|
||||
@@ -3,7 +3,7 @@ import sys
|
||||
import googleapiclient
|
||||
|
||||
import gam
|
||||
from gam.var import *
|
||||
from gam.var import * # pylint: disable=unused-wildcard-import
|
||||
from gam import controlflow
|
||||
from gam import display
|
||||
from gam import gapi
|
||||
@@ -76,6 +76,7 @@ def info():
|
||||
ci = gapi_cloudidentity.build('cloudidentity_beta')
|
||||
group = gam.normalizeEmailAddressOrUID(sys.argv[3])
|
||||
getUsers = True
|
||||
getSecuritySettings = True
|
||||
showJoinDate = True
|
||||
showUpdateDate = False
|
||||
showMemberTree = False
|
||||
@@ -94,11 +95,20 @@ def info():
|
||||
elif myarg == 'membertree':
|
||||
showMemberTree = True
|
||||
i += 1
|
||||
elif myarg in ['nosecurity', 'nosecuritysettings']:
|
||||
getSecuritySettings = False
|
||||
else:
|
||||
controlflow.invalid_argument_exit(myarg, 'gam info cigroup')
|
||||
name = group_email_to_id(ci, group)
|
||||
basic_info = gapi.call(ci.groups(), 'get', name=name)
|
||||
display.print_json(basic_info)
|
||||
if getSecuritySettings:
|
||||
sec_info = gapi.call(ci.groups(),
|
||||
'getSecuritySettings',
|
||||
name=f'{name}/securitySettings',
|
||||
readMask='*')
|
||||
print(' Security settings:')
|
||||
display.print_json(sec_info, spacing=' ')
|
||||
if getUsers and not showMemberTree:
|
||||
if not showJoinDate and not showUpdateDate:
|
||||
view = 'BASIC'
|
||||
@@ -116,7 +126,7 @@ def info():
|
||||
print(' Members:')
|
||||
for member in members:
|
||||
role = get_single_role(member.get('roles', [])).lower()
|
||||
email = member.get('memberKey', {}).get('id')
|
||||
email = member.get('preferredMemberKey', {}).get('id')
|
||||
member_type = member.get('type', 'USER').lower()
|
||||
jc_string = ''
|
||||
if showJoinDate:
|
||||
@@ -145,7 +155,7 @@ def print_member_tree(ci, group_id, cached_group_members, spaces, show_role):
|
||||
for member in cached_group_members[group_id]:
|
||||
member_id = member.get('name', '')
|
||||
member_id = member_id.split('/')[-1]
|
||||
email = member.get('memberKey', {}).get('id')
|
||||
email = member.get('preferredMemberKey', {}).get('id')
|
||||
member_type = member.get('type', 'USER').lower()
|
||||
if show_role:
|
||||
role = get_single_role(member.get('roles', [])).lower()
|
||||
@@ -189,7 +199,13 @@ GROUP_ROLES_MAP = {
|
||||
def print_():
|
||||
ci = gapi_cloudidentity.build('cloudidentity_beta')
|
||||
i = 3
|
||||
members = membersCountOnly = managers = managersCountOnly = owners = ownersCountOnly = False
|
||||
members = False
|
||||
membersCountOnly = False
|
||||
managers = False
|
||||
managersCountOnly = False
|
||||
owners = False
|
||||
ownersCountOnly = False
|
||||
memberRestrictions = False
|
||||
gapi_directory_customer.setTrueCustomerId()
|
||||
parent = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
|
||||
usemember = None
|
||||
@@ -232,6 +248,15 @@ def print_():
|
||||
if myarg == 'managerscount':
|
||||
managersCountOnly = True
|
||||
i += 1
|
||||
elif myarg in ['memberrestrictions']:
|
||||
memberRestrictions = True
|
||||
display.add_titles_to_csv_file(
|
||||
['memberRestrictionQuery',],
|
||||
titles)
|
||||
display.add_titles_to_csv_file(
|
||||
['memberRestrictionEvaluation',],
|
||||
titles)
|
||||
i += 1
|
||||
else:
|
||||
controlflow.invalid_argument_exit(sys.argv[i], 'gam print cigroups')
|
||||
if roles:
|
||||
@@ -315,7 +340,7 @@ def print_():
|
||||
'list',
|
||||
'memberships',
|
||||
page_message=page_message,
|
||||
message_attribute=['memberKey', 'id'],
|
||||
message_attribute=['preferredMemberKey', 'id'],
|
||||
soft_errors=True,
|
||||
parent=groupKey_id,
|
||||
view='BASIC')
|
||||
@@ -329,7 +354,7 @@ def print_():
|
||||
ownersList = []
|
||||
ownersCount = 0
|
||||
for member in groupMembers:
|
||||
member_email = member['memberKey']['id']
|
||||
member_email = member['preferredMemberKey']['id']
|
||||
role = get_single_role(member.get('roles', []))
|
||||
if not validRoles or role in validRoles:
|
||||
if role == ROLE_MEMBER:
|
||||
@@ -363,6 +388,16 @@ def print_():
|
||||
group['OwnersCount'] = ownersCount
|
||||
if not ownersCountOnly:
|
||||
group['Owners'] = memberDelimiter.join(ownersList)
|
||||
if memberRestrictions:
|
||||
name = f'{groupKey_id}/securitySettings'
|
||||
print(f'Getting member restrictions for {groupEmail} ({i}/{count}')
|
||||
sec_info = gapi.call(ci.groups(),
|
||||
'getSecuritySettings',
|
||||
name=name,
|
||||
readMask='*')
|
||||
if 'memberRestriction' in sec_info:
|
||||
group['memberRestrictionQuery'] = sec_info['memberRestriction'].get('query', '')
|
||||
group['memberRestrictionEvaluation'] = sec_info['memberRestriction'].get('evaluation', {}).get('state', '')
|
||||
csvRows.append(group)
|
||||
if sortHeaders:
|
||||
display.sort_csv_titles([
|
||||
@@ -479,8 +514,8 @@ def print_members():
|
||||
view='FULL',
|
||||
pageSize=500,
|
||||
page_message=page_message,
|
||||
message_attribute=['memberKey', 'id'])
|
||||
#fields='nextPageToken,memberships(memberKey,roles,createTime,updateTime)')
|
||||
message_attribute=['preferredMemberKey', 'id'])
|
||||
#fields='nextPageToken,memberships(preferredMemberKey,roles,createTime,updateTime)')
|
||||
if roles:
|
||||
group_members = filter_members_to_roles(group_members, roles)
|
||||
for member in group_members:
|
||||
@@ -565,7 +600,7 @@ def update():
|
||||
items.append(item)
|
||||
elif len(users_email) > 0:
|
||||
body = {
|
||||
'memberKey': {
|
||||
'preferredMemberKey': {
|
||||
'id': users_email[0]
|
||||
},
|
||||
'roles': [{
|
||||
@@ -785,12 +820,12 @@ def update():
|
||||
page_message=page_message,
|
||||
throw_reasons=gapi_errors.MEMBERS_THROW_REASONS,
|
||||
parent=parent,
|
||||
fields='nextPageToken,memberships(memberKey,roles)')
|
||||
fields='nextPageToken,memberships(preferredMemberKey,roles)')
|
||||
result = filter_members_to_roles(result, roles)
|
||||
if not result:
|
||||
print('Group already has 0 members')
|
||||
return
|
||||
users_email = [member['memberKey']['id'] for member in result]
|
||||
users_email = [member['preferredMemberKey']['id'] for member in result]
|
||||
sys.stderr.write(
|
||||
f'Group: {group}, Will remove {len(users_email)} {", ".join(roles).lower()}s.\n'
|
||||
)
|
||||
@@ -808,6 +843,7 @@ def update():
|
||||
else:
|
||||
i = 4
|
||||
body = {}
|
||||
sec_body = {}
|
||||
while i < len(sys.argv):
|
||||
myarg = sys.argv[i].lower().replace('_', '')
|
||||
if myarg == 'name':
|
||||
@@ -830,17 +866,41 @@ def update():
|
||||
}]
|
||||
}
|
||||
i += 2
|
||||
elif myarg in ['memberrestriction', 'memberrestrictions']:
|
||||
query = sys.argv[i + 1]
|
||||
member_types = {
|
||||
'USER': '1',
|
||||
'SERVICE_ACCOUNT': '2',
|
||||
'GROUP': '3',
|
||||
}
|
||||
for key, val in member_types.items():
|
||||
query = query.replace(key, val)
|
||||
sec_body['memberRestriction'] = {'query': query}
|
||||
i += 2
|
||||
else:
|
||||
controlflow.invalid_argument_exit(sys.argv[i],
|
||||
'gam update cigroup')
|
||||
updateMask = ','.join(body.keys())
|
||||
name = group_email_to_id(ci, group)
|
||||
print(f'Updating group {group}')
|
||||
gapi.call(ci.groups(),
|
||||
'patch',
|
||||
updateMask=updateMask,
|
||||
name=name,
|
||||
body=body)
|
||||
if body:
|
||||
updateMask = ','.join(body.keys())
|
||||
name = group_email_to_id(ci, group)
|
||||
print(f'Updating group {group}')
|
||||
gapi.call(ci.groups(),
|
||||
'patch',
|
||||
updateMask=updateMask,
|
||||
name=name,
|
||||
body=body)
|
||||
if sec_body:
|
||||
updateMask = 'member_restriction.query'
|
||||
# it seems like a bug that API requires /securitySettings
|
||||
# appended to name. We'll see if Google servers change this
|
||||
# at some point.
|
||||
name = f'{group_email_to_id(ci, group)}/securitySettings'
|
||||
print(f'Updating group {group} security settings')
|
||||
gapi.call(ci.groups(),
|
||||
'updateSecuritySettings',
|
||||
name=name,
|
||||
updateMask=updateMask,
|
||||
body=sec_body)
|
||||
|
||||
|
||||
def group_email_to_id(ci, group, i=0, count=0):
|
||||
|
||||
@@ -266,6 +266,8 @@ GROUP_ATTRIBUTES_ARGUMENT_TO_PROPERTY_MAP = {
|
||||
'customReplyTo',
|
||||
'defaultmessagedenynotificationtext':
|
||||
'defaultMessageDenyNotificationText',
|
||||
'defaultsender':
|
||||
'defaultSender',
|
||||
'enablecollaborativeinbox':
|
||||
'enableCollaborativeInbox',
|
||||
'favoriterepliesontop':
|
||||
@@ -979,6 +981,9 @@ def update():
|
||||
sys.stderr.write(
|
||||
f'Group: {group}, Will add {len(to_add)} and remove {len(to_remove)} {role}s.\n'
|
||||
)
|
||||
for user in to_remove:
|
||||
items.append(
|
||||
['gam', 'update', 'group', group, 'remove', user])
|
||||
for user in to_add:
|
||||
item = ['gam', 'update', 'group', group, 'add']
|
||||
if role:
|
||||
@@ -987,9 +992,6 @@ def update():
|
||||
item.append(delivery)
|
||||
item.append(user)
|
||||
items.append(item)
|
||||
for user in to_remove:
|
||||
items.append(
|
||||
['gam', 'update', 'group', group, 'remove', user])
|
||||
elif myarg in ['delete', 'remove']:
|
||||
_, users_email, _ = _getRoleAndUsers()
|
||||
if not exists(cd, group):
|
||||
@@ -1219,7 +1221,7 @@ def getGroupAttrValue(myarg, value, gs_object, gs_body, function):
|
||||
params) in list(gs_object['schemas']['Groups']['properties'].items()):
|
||||
if attrib in ['kind', 'etag', 'email']:
|
||||
continue
|
||||
if myarg == attrib.lower():
|
||||
if myarg == attrib.lower().replace('_', ''):
|
||||
if params['type'] == 'integer':
|
||||
try:
|
||||
if value[-1:].upper() == 'M':
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -60,6 +60,10 @@ class GapiGroupNotFoundError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class GapiInternalServerError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class GapiInvalidError(Exception):
|
||||
pass
|
||||
|
||||
@@ -125,6 +129,7 @@ class ErrorReason(Enum):
|
||||
GATEWAY_TIMEOUT = 'gatewayTimeout'
|
||||
GROUP_NOT_FOUND = 'groupNotFound'
|
||||
INTERNAL_ERROR = 'internalError'
|
||||
INTERNAL_SERVER_ERROR = 'internalServerError'
|
||||
INVALID = 'invalid'
|
||||
INVALID_ARGUMENT = 'invalidArgument'
|
||||
INVALID_MEMBER = 'invalidMember'
|
||||
@@ -199,6 +204,8 @@ ERROR_REASON_TO_EXCEPTION = {
|
||||
GapiGatewayTimeoutError,
|
||||
ErrorReason.GROUP_NOT_FOUND:
|
||||
GapiGroupNotFoundError,
|
||||
ErrorReason.INTERNAL_SERVER_ERROR:
|
||||
GapiInternalServerError,
|
||||
ErrorReason.INVALID:
|
||||
GapiInvalidError,
|
||||
ErrorReason.INVALID_ARGUMENT:
|
||||
@@ -336,6 +343,10 @@ def get_gapi_error_detail(e,
|
||||
if 'Requested entity was not found' in message or 'does not exist' in message:
|
||||
error = _create_http_error_dict(404, ErrorReason.NOT_FOUND.value,
|
||||
message)
|
||||
elif http_status == 500:
|
||||
if 'Failed to convert server response to JSON' in message:
|
||||
error = _create_http_error_dict(500, ErrorReason.INTERNAL_SERVER_ERROR.value,
|
||||
message)
|
||||
else:
|
||||
if 'error_description' in error:
|
||||
if error['error_description'] == 'Invalid Value':
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -8,7 +8,7 @@ import platform
|
||||
import re
|
||||
|
||||
GAM_AUTHOR = 'Jay Lee <jay0lee@gmail.com>'
|
||||
GAM_VERSION = '6.06'
|
||||
GAM_VERSION = '6.10'
|
||||
GAM_LICENSE = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
|
||||
|
||||
GAM_URL = 'https://git.io/gam'
|
||||
@@ -124,7 +124,7 @@ SKUS = {
|
||||
'Google-Apps': {
|
||||
'product': 'Google-Apps',
|
||||
'aliases': ['standard', 'free'],
|
||||
'displayName': 'G Suite Free/Standard'
|
||||
'displayName': 'G Suite Legacy'
|
||||
},
|
||||
'Google-Apps-For-Business': {
|
||||
'product': 'Google-Apps',
|
||||
@@ -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',
|
||||
@@ -503,6 +500,7 @@ DRIVEFILE_FIELDS_CHOICES_MAP = {
|
||||
'sharedwithmedate': 'sharedWithMeDate',
|
||||
'sharedwithmetime': 'sharedWithMeDate',
|
||||
'sharinguser': 'sharingUser',
|
||||
'shortcutdetails': 'shortcutDetails',
|
||||
'spaces': 'spaces',
|
||||
'thumbnaillink': 'thumbnailLink',
|
||||
'title': 'title',
|
||||
@@ -619,17 +617,22 @@ GOOGLEDOC_VALID_EXTENSIONS_MAP = {
|
||||
}
|
||||
|
||||
MACOS_CODENAMES = {
|
||||
6: 'Snow Leopard',
|
||||
7: 'Lion',
|
||||
8: 'Mountain Lion',
|
||||
9: 'Mavericks',
|
||||
10: 'Yosemite',
|
||||
11: 'El Capitan',
|
||||
12: 'Sierra',
|
||||
13: 'High Sierra',
|
||||
14: 'Mojave',
|
||||
15: 'Catalina'
|
||||
}
|
||||
10: {
|
||||
6: 'Snow Leopard',
|
||||
7: 'Lion',
|
||||
8: 'Mountain Lion',
|
||||
9: 'Mavericks',
|
||||
10: 'Yosemite',
|
||||
11: 'El Capitan',
|
||||
12: 'Sierra',
|
||||
13: 'High Sierra',
|
||||
14: 'Mojave',
|
||||
15: 'Catalina',
|
||||
16: 'Big Sur'
|
||||
},
|
||||
11: 'Big Sur',
|
||||
12: 'Monterey',
|
||||
}
|
||||
|
||||
_MICROSOFT_FORMATS_LIST = [{
|
||||
'mime':
|
||||
@@ -894,8 +897,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 = {
|
||||
'IMPORTANT': 'important',
|
||||
@@ -1110,7 +1111,8 @@ GROUP_SETTINGS_LIST_ATTRIBUTES = set([
|
||||
'whoCanUnmarkFavoriteReplyOnAnyTopic',
|
||||
'whoCanViewGroup',
|
||||
'whoCanViewMembership',
|
||||
# Miscellaneous hoices
|
||||
# Miscellaneous choices
|
||||
'default_sender',
|
||||
'messageModerationLevel',
|
||||
'replyTo',
|
||||
'spamModerationLevel',
|
||||
@@ -1245,10 +1247,12 @@ GC_DOMAIN = 'domain'
|
||||
GC_DRIVE_DIR = 'drive_dir'
|
||||
# Enable Delegated Admin Service Accounts
|
||||
GC_ENABLE_DASA = 'enabledasa'
|
||||
# If no_browser is False, writeCSVfile won't open a browser when todrive is set
|
||||
# If no_browser is True, writeCSVfile won't open a browser when todrive is set
|
||||
# and doRequestOAuth prints a link and waits for the verification code when
|
||||
# oauth2.txt is being created
|
||||
GC_NO_BROWSER = 'no_browser'
|
||||
# If no_tdemail is True, writeCSVfile won't send an email
|
||||
GC_NO_TDEMAIL = 'no_tdemail'
|
||||
# oauth_browser forces usage of web server OAuth flow that proved problematic.
|
||||
GC_OAUTH_BROWSER = 'oauth_browser'
|
||||
# Disable GAM API caching
|
||||
@@ -1303,6 +1307,7 @@ GC_Defaults = {
|
||||
GC_DRIVE_DIR: '',
|
||||
GC_ENABLE_DASA: False,
|
||||
GC_NO_BROWSER: False,
|
||||
GC_NO_TDEMAIL: False,
|
||||
GC_NO_CACHE: False,
|
||||
GC_NO_SHORT_URLS: False,
|
||||
GC_NO_UPDATE_CHECK: False,
|
||||
@@ -1388,6 +1393,9 @@ GC_VAR_INFO = {
|
||||
GC_NO_BROWSER: {
|
||||
GC_VAR_TYPE: GC_TYPE_BOOLEAN
|
||||
},
|
||||
GC_NO_TDEMAIL: {
|
||||
GC_VAR_TYPE: GC_TYPE_BOOLEAN
|
||||
},
|
||||
GC_NO_CACHE: {
|
||||
GC_VAR_TYPE: GC_TYPE_BOOLEAN
|
||||
},
|
||||
@@ -1937,6 +1945,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'
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@ filelock
|
||||
google-api-python-client>=2.1
|
||||
google-auth-httplib2
|
||||
google-auth-oauthlib>=0.4.1
|
||||
google-auth>=1.11.2
|
||||
google-auth>=2.3.2
|
||||
httplib2>=0.17.0
|
||||
importlib.metadata; python_version < '3.8'
|
||||
passlib>=1.7.2
|
||||
python-dateutil
|
||||
yubikey-manager>=4.0.0
|
||||
pathvalidate
|
||||
|
||||
49
src/setup.cfg
Normal file
49
src/setup.cfg
Normal file
@@ -0,0 +1,49 @@
|
||||
[metadata]
|
||||
name = GAM for Google Workspace
|
||||
version = 6.0.7
|
||||
description = Command line management for Google Workspaces
|
||||
long_description = file: readme.md
|
||||
long_description_content_type = text/markdown
|
||||
url = https://github.com/jay0lee/GAM
|
||||
author = Jay Lee
|
||||
author_email = jay0lee@gmail.com
|
||||
license = Apache
|
||||
license_files = LICENSE
|
||||
keywords = google, oauth2, gsuite, google-apps, google-admin-sdk, google-drive, google-cloud, google-calendar, gam, google-api, oauth2-client, google-workspace
|
||||
classifiers =
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3 :: Only
|
||||
Programming Language :: Python :: 3.6
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
License :: OSI Approved :: Apache License
|
||||
|
||||
[options]
|
||||
packages = find:
|
||||
python_requires = >=3.6
|
||||
install_requires =
|
||||
cryptography
|
||||
distro; sys_platform == 'linux'
|
||||
filelock
|
||||
google-api-python-client >= 2.1
|
||||
google-auth-httplib2
|
||||
google-auth-oauthlib >= 0.4.1
|
||||
google-auth >= 1.11.2
|
||||
httplib2 >= 0.17.0
|
||||
importlib.metadata; python_version < '3.8'
|
||||
passlib >= 1.7.2
|
||||
python-dateutil
|
||||
yubikey-manager >= 4.0.0
|
||||
pathvalidate
|
||||
|
||||
# used during pip install .[test]
|
||||
[options.extras_require]
|
||||
test = pre-commit
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
gam = gam.__main__:main
|
||||
|
||||
[bdist_wheel]
|
||||
universal = True
|
||||
3
src/setup.py
Normal file
3
src/setup.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from setuptools import setup
|
||||
|
||||
setup()
|
||||
@@ -1,13 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
#from packaging import version
|
||||
from distutils.version import LooseVersion
|
||||
from packaging import version
|
||||
import sys
|
||||
|
||||
a = sys.argv[1]
|
||||
b = sys.argv[2]
|
||||
#result = version.parse(a) >= version.parse(b)
|
||||
result = LooseVersion(a) >= LooseVersion(b)
|
||||
result = version.parse(a) >= version.parse(b)
|
||||
if result:
|
||||
print('OK: %s is equal or newer than %s' % (a, b))
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user