Compare commits

..

71 Commits
v6.07 ... v6.11

Author SHA1 Message Date
Jay Lee
b75ad006f1 Update build.yml 2021-11-22 08:26:33 -05:00
Jay Lee
dbc3f0cd83 Update var.py 2021-11-22 08:16:32 -05:00
Jay Lee
ea2750f970 Merge branch 'main' of https://github.com/jay0lee/GAM into main 2021-11-22 08:08:33 -05:00
Jay Lee
a2eb5a2483 Correct certificate not before value to UTC-1h. Fixes #1453 2021-11-22 08:08:20 -05:00
Ross Scroggs
54178543d6 Fix Row Filtering Part 3 (#1450)
Graak! Why I can't get my code translated into yours is beyond me; this time for sure.
2021-11-21 20:25:09 -05:00
Jay Lee
5436f21bc0 Use OpenSSL 3.0.0 in builds 2021-10-29 18:12:30 -04:00
Ross Scroggs
839768a2a5 Fix error handling (#1447) 2021-10-29 13:04:59 -04:00
Jay Lee
2e195d5aa1 Update build.yml 2021-10-29 11:05:57 -04:00
Ross Scroggs
66811f8eb5 Fix Row Filtering Part 2 (#1446)
```
Row Filtering
There can be multiple filters, a filter can match multiple columns (wildcard).
The semantics should be:
For row keep filters, if all filters match, the row is kept.
For row drop filters, if any filter matches, the row is dropped.

For an individual filter that specifies multiple columns, there is a match if any column matches.

Prior to PR 1433, the semantics for keep/drop were reversed; the semantics for multiple columns was correct.

PR 1433 corrected the semantics for keep/drop but broke the semantics for multiple columns.

This PR corrects the semantics for multiple columns.
```
2021-10-29 10:24:12 -04:00
Jay Lee
a92326790d Update build.yml 2021-10-29 10:19:09 -04:00
Ross Scroggs
d405767fb0 Update requirements.txt to get latest library versions (#1444)
* Update requirements.txt

* Revert "Update requirements.txt"

This reverts commit f89f66d44c.

* Update to fixed google oauth library
2021-10-26 14:45:34 -04:00
Ross Scroggs
8d7c6d3835 MacOS codesign fix no longer needed; MacOS 12 = Monterey (#1441)
* Updated 3.9 to 3.10, is this still needed?

* Fix no longer required

* MacOS 12 is Monterey
2021-10-26 12:56:47 -04:00
Jay Lee
e362591b7a pin google-auth to 2.0.2
Need https://github.com/googleapis/google-auth-library-python/issues/889 fixed.
2021-10-21 19:32:39 -04:00
Jay Lee
ee5f4b73e8 Update var.py 2021-10-21 18:43:34 -04:00
Jay Lee
0d15eb2898 Workaround Python 3.10.0 CSV escape issue. Fixes #1437 2021-10-21 10:41:20 -04:00
Jay Lee
4af50206ad need lists to repro 2021-10-21 08:19:32 -04:00
Jay Lee
c596937006 Update build.yml 2021-10-21 08:13:17 -04:00
Jay Lee
17eb61e1eb Merge branch 'main' of https://github.com/jay0lee/GAM into main 2021-10-21 08:06:39 -04:00
Jay Lee
a333185e84 repro issue #1438 2021-10-21 08:06:26 -04:00
Jay Lee
f6863ae2d6 Update var.py 2021-10-20 13:57:11 -04:00
Ross Scroggs
36830250b5 Handle spurious Google error when enabling project APIs (#1436) 2021-10-20 13:48:41 -04:00
Jay Lee
4ca1c3537b Merge branch 'main' of https://github.com/jay0lee/GAM into main 2021-10-18 08:50:25 -04:00
Jay Lee
eeab09eacb fix deprecated package in a_atleast_b.py 2021-10-18 08:50:13 -04:00
Ross Scroggs
af16967257 Fix Row Filtering (#1433)
When multiple filter expressions are defined:
GAM_CSV_ROW_FILTER - should match only if all expressions match
GAM_CSV_ROW_DROP_FILTER - should match if any expression matches

Currently, the opposite is true
2021-10-14 20:12:51 -04:00
Jay Lee
75e2bf5a9a Update build.yml 2021-10-14 19:22:57 -04:00
Ross Scroggs
4db3bc409b Document member restrictions; fix print users (#1430)
* Document member restrictions

* Fix gam print users allfields custom all to include primaryEmail

If you really want everything say: gam print users full
2021-10-06 14:22:27 -04:00
Jay Lee
32ccf414ea Update gam-install.sh 2021-10-06 08:01:29 -04:00
Jay Lee
615e48fffc Update gam-install.sh 2021-10-05 20:18:07 -04:00
Jay Lee
93bf3fce29 Merge branch 'main' of https://github.com/jay0lee/GAM into main 2021-10-05 18:05:52 -04:00
Jay Lee
899601569a Group member restrictions 2021-10-05 18:05:28 -04:00
Jay Lee
b1805b64a2 Update build.yml 2021-10-05 17:58:20 -04:00
Jay Lee
58190343b1 Update linux-install.sh 2021-10-05 16:49:53 -04:00
Jay Lee
99d48b1939 Update linux-before-install.sh 2021-10-05 16:49:36 -04:00
Jay Lee
82b66d53cb Update linux-install.sh 2021-10-05 09:08:55 -04:00
Ross Scroggs
3200de56cc Several fixes/updates (#1426)
* agreedToTerms is now read-only

* Fix sync devices

* assetTag if specified is part of sync device key

* Handle missing assetTags

* Leave agreedtoterms as an undocumented option

* More assetTag processing, the field is not returned from the API if it's empty

* Fix DriveFileAttribute formatting

* memberKey has been replaced by preferredMemberKey

* Correct license name

* If notdemail.txt is present, write_csv_file will not send an email
2021-10-05 08:37:09 -04:00
Jay Lee
0a627d5c79 Update build.yml 2021-10-05 08:29:19 -04:00
Jay Lee
22399deb79 Update build.yml 2021-10-05 08:22:59 -04:00
Jay Lee
6a77617e3b Update build.yml 2021-10-04 18:22:32 -04:00
Jay Lee
2868ef99ae Update build.yml 2021-10-04 18:11:54 -04:00
Jay Lee
21557f9892 Update linux-install.sh 2021-09-30 18:54:46 -04:00
Jay Lee
d2385ae62d Update linux-before-install.sh 2021-09-30 18:54:19 -04:00
Jay Lee
a84efef389 Update build.yml 2021-09-30 18:51:55 -04:00
Jay Lee
310bcd1585 Merge branch 'main' of https://github.com/jay0lee/GAM into main 2021-09-27 08:21:54 -04:00
Jay Lee
753f44deb2 Fix some missing types in cbcm JSON, formatting 2021-09-27 08:21:08 -04:00
Jay Lee
df1f0f8f09 Update build.yml 2021-09-16 08:15:21 -04:00
Jay Lee
45e1b50674 Update build.yml 2021-09-10 14:41:08 -04:00
Jay Lee
0a2b048fb1 Update build.yml 2021-09-10 14:40:21 -04:00
Ross Scroggs
e3c5dca09d Three updates (#1421)
* Initialize pageToken for each namespace

* Update group sync to do removes before adds

This gets around problem when  a group contains a primary address and a sync is performed with an alias. With adds first you get a duplicate error; with removes first the primary address in the group is replaced with the alias.

* Add defaultsender to group settings
2021-09-09 13:06:50 -04:00
Jay Lee
88339b7214 Update build.yml 2021-08-31 13:59:04 -04:00
Jay Lee
1f2bb18bc1 GAM 6.08 2021-08-31 13:58:04 -04:00
Jay Lee
74977a6154 Update build.yml 2021-08-31 10:49:42 -04:00
Jay Lee
00413fe7a4 Update build.yml 2021-08-31 10:46:03 -04:00
Jay Lee
9bb9d331ad Update build.yml 2021-08-31 09:58:07 -04:00
Jay Lee
f022ffdff4 Update build.yml 2021-08-31 09:25:13 -04:00
Jay Lee
28dade2a34 Update build.yml 2021-08-31 09:18:23 -04:00
Jay Lee
7378b9d843 Update build.yml 2021-08-31 09:10:22 -04:00
Jay Lee
71075e95bf Update build.yml 2021-08-31 08:58:45 -04:00
Janosh Riebesell
108990cf06 Fix pip license error + add pip install command to readme (#1419)
* fix pip license error, add pip install to readme

* fix warning: the 'license_file' option is deprecated, use 'license_files' instead
2021-08-31 08:51:51 -04:00
Jay Lee
ebfdf4b052 Update build.yml 2021-08-31 08:49:55 -04:00
Jay Lee
dbf4073216 fix gam.py also 2021-08-27 12:10:54 -04:00
Jay Lee
83214eaaf8 attempt fixes for pip installable 2021-08-27 12:10:15 -04:00
Janosh Riebesell
1100fdd456 Make GAM pip-installable (#1417)
* wip: make pip-installable

* resolve @jay0lee's comments
2021-08-27 11:24:02 -04:00
Jay Lee
481bfa5440 Update build.yml 2021-08-27 10:16:34 -04:00
Jay Lee
30282c7fbb OpenSSL 1.1.1l not "i" 2021-08-27 09:44:01 -04:00
Jay Lee
382bc71b21 Update build.yml 2021-08-24 14:58:58 -04:00
Ross Scroggs
f3fba97652 Add shortcutDetails to drive file fields (#1413) 2021-08-23 16:05:15 -04:00
Yaroslav Nakonechnikov
7f51e35bd4 Pathvalidate (#1408)
* Update requirements.txt

Adding `pathvalidate` to requrements

* Update __init__.py

Adding `pathvalidate` to make  correct filename on other then ascii encodings.

* Updating with sanitize_filename

* Removing unused variable.
2021-08-23 16:04:11 -04:00
Ross Scroggs
95beb8e62a Update getting MacOS version (#1409) 2021-08-14 16:59:07 -04:00
Ross Scroggs
1a9de867f9 Work around API restriction that roleId and userKey are mutually exclusive (#1406) 2021-08-10 06:30:52 -04:00
Jay Lee
b42946bbe1 Update build.yml 2021-08-04 17:02:24 -04:00
Jay Lee
40b2fd09ff small service account improvements 2021-08-04 16:58:07 -04:00
25 changed files with 403 additions and 169 deletions

View File

@@ -55,7 +55,7 @@ else
tar xf openssl-$BUILD_OPENSSL_VERSION.tar.gz
cd openssl-$BUILD_OPENSSL_VERSION
echo "Compiling OpenSSL $BUILD_OPENSSL_VERSION..."
./config shared --prefix=$HOME/ssl
./Configure --libdir=lib --prefix=$HOME/ssl
echo "Running make for OpenSSL..."
make -j$cpucount -s
echo "Running make install for OpenSSL..."
@@ -70,7 +70,7 @@ else
cd Python-$BUILD_PYTHON_VERSION
echo "Compiling Python $BUILD_PYTHON_VERSION..."
safe_flags="--with-openssl=$HOME/ssl --enable-shared --prefix=$HOME/python --with-ensurepip=upgrade"
unsafe_flags="--enable-optimizations --with-lto"
unsafe_flags="--enable-optimizations --with-lto --with-openssl=~/ssl --with-openssl-rpath=~~/ssl/lib"
if [ ! -e Makefile ]; then
echo "running configure with safe and unsafe"
./configure $safe_flags $unsafe_flags > /dev/null
@@ -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"

View File

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

View File

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

View File

@@ -12,13 +12,13 @@ defaults:
working-directory: src
env:
BUILD_PYTHON_VERSION: "3.9.6"
MIN_PYTHON_VERSION: "3.9.6"
BUILD_OPENSSL_VERSION: "1.1.1k"
MIN_OPENSSL_VERSION: "1.1.1k"
PATCHELF_VERSION: "0.12"
BUILD_PYTHON_VERSION: "3.10.0"
MIN_PYTHON_VERSION: "3.10.0"
BUILD_OPENSSL_VERSION: "3.0.0"
MIN_OPENSSL_VERSION: "1.1.1l"
PATCHELF_VERSION: "0.13"
# PYINSTALLER_VERSION can be full commit hash or version like v4.20
PYINSTALLER_VERSION: "0f2b2e921433ab5a510c7efdb21d9c1d7cfbc645"
PYINSTALLER_VERSION: "6eae2c7cf93a968ddc054339e0cb3063f90d0e64"
jobs:
build:
@@ -26,34 +26,29 @@ 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-11.0
jid: 12
jid: 3
goal: "build"
gamos: "macos"
platform: "universal2"
- os: windows-2019
jid: 5
jid: 4
goal: "build"
gamos: "windows"
pyarch: "x64"
platform: "x86_64"
- os: windows-2019
jid: 6
jid: 5
goal: "build"
gamos: "windows"
platform: "x86"
@@ -61,25 +56,25 @@ jobs:
- 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
@@ -97,7 +92,7 @@ jobs:
path: |
~/python
~/ssl
key: ${{ matrix.os }}-${{ matrix.jid }}-20210628
key: ${{ matrix.os }}-${{ matrix.jid }}-20211122
- name: Set env variables
env:
@@ -166,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}"
@@ -184,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
@@ -218,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]}
@@ -259,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
@@ -333,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

View File

@@ -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.
![Build Status](https://github.com/jay0lee/GAM/workflows/Build%20and%20test%20GAM/badge.svg)
# 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

View File

@@ -222,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
@@ -353,6 +355,7 @@ If an item contains spaces, it should be surrounded by ".
shared|
sharedwithmedate|sharedwithmetime|
sharinguser|
shortcutdetails|
size|
spaces|
starred|
@@ -698,9 +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
(parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|
(securityupdate <Boolean>)|
(shortcut <DriveFileID>)
(shortcut <DriveFileID>)|
writerscantshare|writerscanshare
<DriveFileUpdateAttribute> ::=
(localfile <FileName>|-)|
(convert)|(ocr)|(ocrlanguage <Language>)|
@@ -709,9 +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
(parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|
(securityupdate <Boolean>)|
(shortcut <DriveFileID>)
(shortcut <DriveFileID>)|
writerscantshare|writerscanshare
<GroupSettingsAttribute> ::=
(allowexternalmembers <Boolean>)|
(allowwebposting <Boolean>)|
@@ -719,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>)|
@@ -796,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>)|
@@ -1381,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>

View File

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

View File

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

View File

@@ -8,4 +8,4 @@ from gam.__main__ import main
# Run from command line
if __name__ == '__main__':
main(sys.argv)
main()

View File

@@ -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()
@@ -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:
@@ -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':
@@ -4057,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)
@@ -6669,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)
@@ -7236,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
@@ -7756,11 +7765,11 @@ 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)
# Gooogle seems to enforce the not before date strictly. Set the not before
# date to be UTC one hour ago should cover any clock skew.
builder = builder.not_valid_before(datetime.datetime.utcnow() - datetime.timedelta(hours=1))
# 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,
@@ -7943,10 +7952,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
@@ -9666,6 +9683,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'

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ 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 ykman.piv import generate_self_signed_certificate, \
generate_chuid
@@ -46,7 +47,10 @@ class YubiKey():
self.key_id = service_account_info.get('private_key_id')
def _connect(self):
conn, _, _ = connect_to_device(self.serial_number)
try:
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):
@@ -62,7 +66,7 @@ class YubiKey():
try:
cert = session.get_certificate(self.slot)
except ApduError as err:
controlflow.system_error_exit(9, f'Yubikey = {err}')
controlflow.system_error_exit(9, f'YubiKey - {err}')
cert_pem = cert.public_bytes(
serialization.Encoding.PEM).decode()
publicKeyData = b64encode(cert_pem.encode())
@@ -78,7 +82,7 @@ class YubiKey():
_, _, info = connect_to_device(self.serial_number)
return info.serial
except ValueError as err:
controlflow.system_error_exit(9, f'YubikKey = {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.'''
@@ -101,7 +105,7 @@ class YubiKey():
DEFAULT_MANAGEMENT_KEY)
piv.verify_pin(new_pin)
print('Yubikey is generating a non-exportable private key...')
print('YubiKey is generating a non-exportable private key...')
pubkey = piv.generate_key(SLOT.AUTHENTICATION,
KEY_TYPE.RSA2048,
PIN_POLICY.ALWAYS,
@@ -123,7 +127,7 @@ class YubiKey():
piv.put_object(OBJECT_ID.CHUID,
generate_chuid())
except ValueError as err:
controlflow.system_error_exit(8, f'Yubikey - {err}')
controlflow.system_error_exit(8, f'YubiKey - {err}')
def sign(self, message):
@@ -145,7 +149,7 @@ class YubiKey():
hash_algorithm=hashes.SHA256(),
padding=padding.PKCS1v15())
except ApduError as err:
controlflow.system_error_exit(8, f'YubiKey = {err}')
controlflow.system_error_exit(8, f'YubiKey - {err}')
except ValueError as err:
controlflow.system_error_exit(9, f'YubiKey - {err}')
if 'mplock' in globals():

View File

@@ -154,28 +154,39 @@ def write_csv_file(csvRows, titles, list_type, todrive):
return True
return False
def filterMatch(filterVal, columns, row):
for column in columns:
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
elif filterVal[1] in ['date', 'time']:
if rowDateTimeFilterMatch(
filterVal[1] == 'date', row.get(column, ''),
filterVal[2], filterVal[3]):
return True
elif filterVal[1] == 'count':
if rowCountFilterMatch(
row.get(column, 0), filterVal[2], filterVal[3]):
return True
else: #boolean
if rowBooleanFilterMatch(
row.get(column, False), filterVal[2]):
return True
return False
def rowFilterMatch(filters, columns, row):
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
elif filterVal[1] in ['date', 'time']:
if rowDateTimeFilterMatch(
filterVal[1] == 'date', row.get(column, ''),
filterVal[2], filterVal[3]):
return True
elif filterVal[1] == 'count':
if rowCountFilterMatch(
row.get(column, 0), filterVal[2], filterVal[3]):
return True
else: #boolean
if rowBooleanFilterMatch(
row.get(column, False), filterVal[2]):
return True
if not filterMatch(filterVal, columns[c], row):
return False
return True
def rowDropFilterMatch(filters, columns, row):
for c, filterVal in iter(filters.items()):
if filterMatch(filterVal, columns[c], row):
return True
return False
if GC_Values[GC_CSV_ROW_FILTER] or GC_Values[GC_CSV_ROW_DROP_FILTER]:
@@ -210,7 +221,7 @@ def write_csv_file(csvRows, titles, list_type, todrive):
if (((keepColumns is None) or
rowFilterMatch(GC_Values[GC_CSV_ROW_FILTER], keepColumns, row)) and
((dropColumns is None) or
not rowFilterMatch(GC_Values[GC_CSV_ROW_DROP_FILTER], dropColumns, row))):
not rowDropFilterMatch(GC_Values[GC_CSV_ROW_DROP_FILTER], dropColumns, row))):
rows.append(row)
csvRows = rows
@@ -231,7 +242,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 +257,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 +300,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)

View File

@@ -359,7 +359,7 @@ def handle_oauth_token_error(e, soft_errors):
returns to the caller.
"""
token_error = str(e).replace('.', '')
if token_error in errors.OAUTH2_TOKEN_ERRORS or e.startswith(
if token_error in errors.OAUTH2_TOKEN_ERRORS or token_error.startswith(
'Invalid response'):
if soft_errors:
return

View File

@@ -86,6 +86,7 @@ def printshow_policies():
for namespace in namespaces:
spacing = ' '
body['policySchemaFilter'] = f'{namespace}.*'
body['pageToken'] = None
try:
policies = gapi.get_all_pages(svc.customers().policies(), 'resolve',
items='resolvedPolicies',

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ import platform
import re
GAM_AUTHOR = 'Jay Lee <jay0lee@gmail.com>'
GAM_VERSION = '6.07'
GAM_VERSION = '6.11'
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',
@@ -500,6 +500,7 @@ DRIVEFILE_FIELDS_CHOICES_MAP = {
'sharedwithmedate': 'sharedWithMeDate',
'sharedwithmetime': 'sharedWithMeDate',
'sharinguser': 'sharingUser',
'shortcutdetails': 'shortcutDetails',
'spaces': 'spaces',
'thumbnaillink': 'thumbnailLink',
'title': 'title',
@@ -616,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':
@@ -891,7 +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 + '-._~'
FILENAME_SAFE_CHARS = ALPHANUMERIC_CHARS + '-_.() '
FILTER_ADD_LABEL_TO_ARGUMENT_MAP = {
'IMPORTANT': 'important',
@@ -1106,7 +1111,8 @@ GROUP_SETTINGS_LIST_ATTRIBUTES = set([
'whoCanUnmarkFavoriteReplyOnAnyTopic',
'whoCanViewGroup',
'whoCanViewMembership',
# Miscellaneous hoices
# Miscellaneous choices
'default_sender',
'messageModerationLevel',
'replyTo',
'spamModerationLevel',
@@ -1241,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
@@ -1299,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,
@@ -1384,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
},

View File

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

@@ -0,0 +1,3 @@
from setuptools import setup
setup()

View File

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