Compare commits

...

61 Commits
v6.07 ... v6.10

Author SHA1 Message Date
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
24 changed files with 377 additions and 156 deletions

View File

@@ -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: "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: "0f2b2e921433ab5a510c7efdb21d9c1d7cfbc645"
PYINSTALLER_VERSION: "1e6a8d53f150cf24b574c32085f3745cbd2afaa6"
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 }}-20211014
- 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,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,
@@ -7943,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
@@ -9666,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'

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

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

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