mirror of
https://github.com/GAM-team/GAM.git
synced 2026-06-05 14:51:39 +00:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d405767fb0 | ||
|
|
8d7c6d3835 | ||
|
|
e362591b7a | ||
|
|
ee5f4b73e8 | ||
|
|
0d15eb2898 | ||
|
|
4af50206ad | ||
|
|
c596937006 | ||
|
|
17eb61e1eb | ||
|
|
a333185e84 | ||
|
|
f6863ae2d6 | ||
|
|
36830250b5 | ||
|
|
4ca1c3537b | ||
|
|
eeab09eacb | ||
|
|
af16967257 | ||
|
|
75e2bf5a9a | ||
|
|
4db3bc409b | ||
|
|
32ccf414ea | ||
|
|
615e48fffc | ||
|
|
93bf3fce29 | ||
|
|
899601569a | ||
|
|
b1805b64a2 | ||
|
|
58190343b1 | ||
|
|
99d48b1939 | ||
|
|
82b66d53cb | ||
|
|
3200de56cc | ||
|
|
0a627d5c79 | ||
|
|
22399deb79 | ||
|
|
6a77617e3b | ||
|
|
2868ef99ae | ||
|
|
21557f9892 | ||
|
|
d2385ae62d | ||
|
|
a84efef389 | ||
|
|
310bcd1585 | ||
|
|
753f44deb2 | ||
|
|
df1f0f8f09 | ||
|
|
45e1b50674 | ||
|
|
0a2b048fb1 | ||
|
|
e3c5dca09d | ||
|
|
88339b7214 | ||
|
|
1f2bb18bc1 | ||
|
|
74977a6154 | ||
|
|
00413fe7a4 | ||
|
|
9bb9d331ad | ||
|
|
f022ffdff4 | ||
|
|
28dade2a34 | ||
|
|
7378b9d843 | ||
|
|
71075e95bf | ||
|
|
108990cf06 | ||
|
|
ebfdf4b052 | ||
|
|
dbf4073216 | ||
|
|
83214eaaf8 | ||
|
|
1100fdd456 | ||
|
|
481bfa5440 | ||
|
|
30282c7fbb | ||
|
|
382bc71b21 | ||
|
|
f3fba97652 | ||
|
|
7f51e35bd4 | ||
|
|
95beb8e62a | ||
|
|
1a9de867f9 | ||
|
|
b42946bbe1 | ||
|
|
40b2fd09ff |
2
.github/actions/linux-before-install.sh
vendored
2
.github/actions/linux-before-install.sh
vendored
@@ -94,7 +94,7 @@ else
|
||||
python=~/python/bin/python3
|
||||
pip=~/python/bin/pip3
|
||||
|
||||
if ([ "${ImageOS}" == "ubuntu16" ]) && [ "${HOSTTYPE}" == "x86_64" ]; then
|
||||
if ([ "${ImageOS}" == "ubuntu20" ]) && [ "${HOSTTYPE}" == "x86_64" ]; then
|
||||
echo "Installing deps for StaticX..."
|
||||
if [ ! -d patchelf-$PATCHELF_VERSION ]; then
|
||||
echo "Downloading PatchELF $PATCHELF_VERSION"
|
||||
|
||||
6
.github/actions/linux-install.sh
vendored
6
.github/actions/linux-install.sh
vendored
@@ -17,10 +17,10 @@ tar -C ${distpath} --create --file $GAM_ARCHIVE --xz gam
|
||||
echo "PyInstaller GAM info:"
|
||||
du -h $gam
|
||||
time $gam version extended
|
||||
if ([ "${ImageOS}" == "ubuntu16" ]) && [ "${HOSTTYPE}" == "x86_64" ]; then
|
||||
if ([ "${ImageOS}" == "ubuntu20" ]) && [ "${HOSTTYPE}" == "x86_64" ]; then
|
||||
GAM_LEGACY_ARCHIVE=gam-${GAMVERSION}-${GAMOS}-${PLATFORM}-legacy.tar.xz
|
||||
$python -OO -m staticx -l /lib/x86_64-linux-gnu/libresolv.so.2 -l /lib/x86_64-linux-gnu/libnss_dns.so.2 $gam $gam-staticx
|
||||
strip $gam-staticx
|
||||
$python -OO -m staticx $gam $gam-staticx
|
||||
#strip $gam-staticx
|
||||
rm $gampath/gam
|
||||
mv $gam-staticx $gam
|
||||
chmod 755 $gam
|
||||
|
||||
2
.github/actions/macos-before-install.sh
vendored
2
.github/actions/macos-before-install.sh
vendored
@@ -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
|
||||
|
||||
48
.github/workflows/build.yml
vendored
48
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
||||
27
README.md
27
README.md
@@ -1,23 +1,46 @@
|
||||
GAM is a command line tool for Google Workspace (fka G Suite) Administrators to manage domain and user settings quickly and easily.
|
||||
|
||||

|
||||
|
||||
# Quick Start
|
||||
|
||||
## Linux / MacOS
|
||||
|
||||
Open a terminal and run:
|
||||
```
|
||||
|
||||
```sh
|
||||
bash <(curl -s -S -L https://git.io/install-gam)
|
||||
```
|
||||
|
||||
this will download GAM, install it and start setup.
|
||||
|
||||
To install with `pip`, run
|
||||
|
||||
```sh
|
||||
pip install git+https://github.com/jay0lee/GAM.git#subdirectory=src
|
||||
```
|
||||
|
||||
This will only download and install GAM. To start setup, simply invoke the `gam` CLI.
|
||||
|
||||
## Windows
|
||||
|
||||
Download the MSI Installer from the [GitHub Releases] page. Install the MSI and you'll be prompted to setup GAM.
|
||||
|
||||
# Documentation
|
||||
|
||||
The GAM documentation is hosted in the [GitHub Wiki]
|
||||
|
||||
# Mailing List / Discussion group
|
||||
|
||||
The GAM mailing list / discussion group is hosted on [Google Groups]. You can join the list and interact via email, or just post from the web itself.
|
||||
|
||||
# Chat Room
|
||||
|
||||
There is a public chat room hosted in Google Chat. [Instructions to join](https://git.io/gam-chat).
|
||||
|
||||
# Author
|
||||
GAM is maintained by <a href="mailto:jay0lee@gmail.com">Jay Lee</a>. Please direct "how do I?" questions to [Google Groups].
|
||||
|
||||
GAM is maintained by [Jay Lee](mailto:jay0lee@gmail.com). Please direct "how do I?" questions to [Google Groups].
|
||||
|
||||
[GAM release]: https://git.io/gamreleases
|
||||
[GitHub Releases]: https://github.com/jay0lee/GAM/releases
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -368,7 +368,7 @@
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
},
|
||||
"path": "{customer}/chrome/enrollmentTokens",
|
||||
"request": {
|
||||
"$ref": "CreateEnrollmentTokenRequest"
|
||||
@@ -379,7 +379,7 @@
|
||||
"scopes": [
|
||||
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers"
|
||||
]
|
||||
},
|
||||
},
|
||||
"revoke": {
|
||||
"description": "Revokes a browser enrollment token in a domain.",
|
||||
"flatPath": "{customer}/chrome/enrollmentTokens/{tokenPermanentId}:revoke",
|
||||
@@ -387,7 +387,7 @@
|
||||
"id": "cbcm.enrollmentTokens.revoke",
|
||||
"parameterOrder": [
|
||||
"customer",
|
||||
"tokenPermanentId"
|
||||
"tokenPermanentId"
|
||||
],
|
||||
"parameters": {
|
||||
"customer": {
|
||||
@@ -402,12 +402,12 @@
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
},
|
||||
"path": "{customer}/chrome/enrollmentTokens/{tokenPermanentId}:revoke",
|
||||
"scopes": [
|
||||
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -491,23 +491,23 @@
|
||||
"description": "Immutable ID of the G Suite account.",
|
||||
"type": "string"
|
||||
},
|
||||
"orgUnitPath": {
|
||||
"orgUnitPath": {
|
||||
"description": "The full path of the organizational unit or its unique ID.",
|
||||
"type": "string"
|
||||
},
|
||||
"creatorId": {
|
||||
"creatorId": {
|
||||
"description": "Creator ID.",
|
||||
"type": "string"
|
||||
},
|
||||
"createTime": {
|
||||
"createTime": {
|
||||
"description": "Creation Time.",
|
||||
"type": "string"
|
||||
},
|
||||
"revokerId": {
|
||||
"revokerId": {
|
||||
"description": "Revoker ID.",
|
||||
"type": "string"
|
||||
},
|
||||
"revokeTime": {
|
||||
"revokeTime": {
|
||||
"description": "Revoke Time",
|
||||
"type": "string"
|
||||
}
|
||||
@@ -538,16 +538,18 @@
|
||||
},
|
||||
"CreateEnrollmentTokenRequest": {
|
||||
"id": "CreateEnrollmentTokenRequest",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"org_unit_path": {
|
||||
"org_unit_path": {
|
||||
"description": "The full path of the organizational unit or its unique ID.",
|
||||
"type": "string"
|
||||
},
|
||||
"expire_time": {
|
||||
"expire_time": {
|
||||
"description": "Expiration Time.",
|
||||
"type": "string"
|
||||
},
|
||||
"token_type": {
|
||||
"token_type": {
|
||||
"id": "token_type",
|
||||
"annotations": {
|
||||
"required": [
|
||||
"cbcm.enrollmentTokens.create"
|
||||
@@ -559,6 +561,8 @@
|
||||
}
|
||||
},
|
||||
"MoveChromeBrowsersRequest": {
|
||||
"id": "MoveChromeBrowsersRequest",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"org_unit_path": {
|
||||
"annotations": {
|
||||
@@ -576,7 +580,10 @@
|
||||
]
|
||||
},
|
||||
"description": "List of unique device IDs of Chrome Browser Devices to move. A maximum of 600 browsers may be moved per request.",
|
||||
"type": "array"
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ upgrade_only=false
|
||||
gamversion="latest"
|
||||
adminuser=""
|
||||
regularuser=""
|
||||
gam_glibc_vers="2.31 2.27 2.23"
|
||||
gam_glibc_vers="2.31 2.27"
|
||||
#gam_macos_vers="10.15.6 10.14.6 10.13.6"
|
||||
|
||||
while getopts "hd:a:o:b:lp:u:r:v:" OPTION
|
||||
@@ -128,7 +128,7 @@ case $gamos in
|
||||
this_macos_ver=$osversion
|
||||
fi
|
||||
echo "You are running MacOS $this_macos_ver"
|
||||
gamfile="macos-x86_64.tar.xz"
|
||||
gamfile="macos-universal2.tar.xz"
|
||||
;;
|
||||
MINGW64_NT*)
|
||||
gamos="windows"
|
||||
|
||||
@@ -8,4 +8,4 @@ from gam.__main__ import main
|
||||
|
||||
# Run from command line
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv)
|
||||
main()
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -30,7 +30,7 @@ from gam import controlflow
|
||||
import gam
|
||||
|
||||
|
||||
def main(argv):
|
||||
def main():
|
||||
freeze_support()
|
||||
if sys.platform == 'darwin':
|
||||
# https://bugs.python.org/issue33725 in Python 3.8.0 seems
|
||||
@@ -47,4 +47,4 @@ def main(argv):
|
||||
|
||||
# Run from command line
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv)
|
||||
main()
|
||||
|
||||
@@ -395,7 +395,7 @@ class Credentials(google.oauth2.credentials.Credentials):
|
||||
self.refresh(request)
|
||||
|
||||
self._id_token_data = google.oauth2.id_token.verify_oauth2_token(
|
||||
self.id_token, request)
|
||||
self.id_token, request, clock_skew_in_seconds=10)
|
||||
|
||||
def get_token_value(self, field):
|
||||
"""Retrieves data from the OAuth ID token.
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -405,7 +405,7 @@ def sync():
|
||||
controlflow.csv_field_error_exit(devicetype_column, input_file.fieldnames)
|
||||
if assettag_column and assettag_column not in input_file.fieldnames:
|
||||
controlflow.csv_field_error_exit(assettag_column, input_file.fieldnames)
|
||||
local_devices = []
|
||||
local_devices = {}
|
||||
for row in input_file:
|
||||
# upper() is very important to comparison since Google
|
||||
# always return uppercase serials
|
||||
@@ -414,28 +414,43 @@ def sync():
|
||||
local_device['deviceType'] = static_devicetype
|
||||
else:
|
||||
local_device['deviceType'] = row[devicetype_column].strip()
|
||||
sndt = f"{local_device['serialNumber']}-{local_device['deviceType']}"
|
||||
if assettag_column:
|
||||
local_device['assetTag'] = row[assettag_column].strip()
|
||||
local_devices.append(local_device)
|
||||
sndt += f"-{local_device['assetTag']}"
|
||||
local_devices[sndt] = local_device
|
||||
fileutils.close_file(f)
|
||||
page_message = gapi.got_total_items_msg('Company Devices', '...\n')
|
||||
device_fields = ['serialNumber', 'deviceType', 'lastSyncTime', 'name']
|
||||
if assettag_column:
|
||||
device_fields.append('assetTag')
|
||||
fields = f'nextPageToken,devices({",".join(device_fields)})'
|
||||
remote_devices = gapi.get_all_pages(ci.devices(), 'list', 'devices',
|
||||
remote_devices = {}
|
||||
remote_device_map = {}
|
||||
result = gapi.get_all_pages(ci.devices(), 'list', 'devices',
|
||||
customer=customer, page_message=page_message,
|
||||
pageSize=100, filter=device_filter, view='COMPANY_INVENTORY', fields=fields)
|
||||
remote_device_map = {}
|
||||
for remote_device in remote_devices:
|
||||
for remote_device in result:
|
||||
sn = remote_device['serialNumber']
|
||||
last_sync = remote_device.pop('lastSyncTime', NEVER_TIME_NOMS)
|
||||
name = remote_device.pop('name')
|
||||
remote_device_map[sn] = {'name': name}
|
||||
sndt = f"{remote_device['serialNumber']}-{remote_device['deviceType']}"
|
||||
if assettag_column:
|
||||
if 'assetTag' not in remote_device:
|
||||
remote_device['assetTag'] = ''
|
||||
sndt += f"-{remote_device['assetTag']}"
|
||||
remote_devices[sndt] = remote_device
|
||||
remote_device_map[sndt] = {'name': name}
|
||||
if last_sync == NEVER_TIME_NOMS:
|
||||
remote_device_map[sn]['unassigned'] = True
|
||||
devices_to_add = [device for device in local_devices if device not in remote_devices]
|
||||
missing_devices = [device for device in remote_devices if device not in local_devices]
|
||||
remote_device_map[sndt]['unassigned'] = True
|
||||
devices_to_add = []
|
||||
for sndt, device in iter(local_devices.items()):
|
||||
if sndt not in remote_devices:
|
||||
devices_to_add.append(device)
|
||||
missing_devices = []
|
||||
for sndt, device in iter(remote_devices.items()):
|
||||
if sndt not in local_devices:
|
||||
missing_devices.append(device)
|
||||
print(f'Need to add {len(devices_to_add)} and remove {len(missing_devices)} devices...')
|
||||
for add_device in devices_to_add:
|
||||
print(f'Creating {add_device["serialNumber"]}')
|
||||
@@ -447,8 +462,11 @@ def sync():
|
||||
print(f' {add_device["serialNumber"]} already exists')
|
||||
for missing_device in missing_devices:
|
||||
sn = missing_device['serialNumber']
|
||||
name = remote_device_map[sn]['name']
|
||||
unassigned = remote_device_map[sn].get('unassigned')
|
||||
sndt = f"{sn}-{missing_device['deviceType']}"
|
||||
if assettag_column:
|
||||
sndt += f"-{missing_device['assetTag']}"
|
||||
name = remote_device_map[sndt]['name']
|
||||
unassigned = remote_device_map[sndt].get('unassigned')
|
||||
action = unassigned_missing_action if unassigned else assigned_missing_action
|
||||
if action == 'donothing':
|
||||
pass
|
||||
|
||||
@@ -3,7 +3,7 @@ import sys
|
||||
import googleapiclient
|
||||
|
||||
import gam
|
||||
from gam.var import *
|
||||
from gam.var import * # pylint: disable=unused-wildcard-import
|
||||
from gam import controlflow
|
||||
from gam import display
|
||||
from gam import gapi
|
||||
@@ -76,6 +76,7 @@ def info():
|
||||
ci = gapi_cloudidentity.build('cloudidentity_beta')
|
||||
group = gam.normalizeEmailAddressOrUID(sys.argv[3])
|
||||
getUsers = True
|
||||
getSecuritySettings = True
|
||||
showJoinDate = True
|
||||
showUpdateDate = False
|
||||
showMemberTree = False
|
||||
@@ -94,11 +95,20 @@ def info():
|
||||
elif myarg == 'membertree':
|
||||
showMemberTree = True
|
||||
i += 1
|
||||
elif myarg in ['nosecurity', 'nosecuritysettings']:
|
||||
getSecuritySettings = False
|
||||
else:
|
||||
controlflow.invalid_argument_exit(myarg, 'gam info cigroup')
|
||||
name = group_email_to_id(ci, group)
|
||||
basic_info = gapi.call(ci.groups(), 'get', name=name)
|
||||
display.print_json(basic_info)
|
||||
if getSecuritySettings:
|
||||
sec_info = gapi.call(ci.groups(),
|
||||
'getSecuritySettings',
|
||||
name=f'{name}/securitySettings',
|
||||
readMask='*')
|
||||
print(' Security settings:')
|
||||
display.print_json(sec_info, spacing=' ')
|
||||
if getUsers and not showMemberTree:
|
||||
if not showJoinDate and not showUpdateDate:
|
||||
view = 'BASIC'
|
||||
@@ -116,7 +126,7 @@ def info():
|
||||
print(' Members:')
|
||||
for member in members:
|
||||
role = get_single_role(member.get('roles', [])).lower()
|
||||
email = member.get('memberKey', {}).get('id')
|
||||
email = member.get('preferredMemberKey', {}).get('id')
|
||||
member_type = member.get('type', 'USER').lower()
|
||||
jc_string = ''
|
||||
if showJoinDate:
|
||||
@@ -145,7 +155,7 @@ def print_member_tree(ci, group_id, cached_group_members, spaces, show_role):
|
||||
for member in cached_group_members[group_id]:
|
||||
member_id = member.get('name', '')
|
||||
member_id = member_id.split('/')[-1]
|
||||
email = member.get('memberKey', {}).get('id')
|
||||
email = member.get('preferredMemberKey', {}).get('id')
|
||||
member_type = member.get('type', 'USER').lower()
|
||||
if show_role:
|
||||
role = get_single_role(member.get('roles', [])).lower()
|
||||
@@ -189,7 +199,13 @@ GROUP_ROLES_MAP = {
|
||||
def print_():
|
||||
ci = gapi_cloudidentity.build('cloudidentity_beta')
|
||||
i = 3
|
||||
members = membersCountOnly = managers = managersCountOnly = owners = ownersCountOnly = False
|
||||
members = False
|
||||
membersCountOnly = False
|
||||
managers = False
|
||||
managersCountOnly = False
|
||||
owners = False
|
||||
ownersCountOnly = False
|
||||
memberRestrictions = False
|
||||
gapi_directory_customer.setTrueCustomerId()
|
||||
parent = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
|
||||
usemember = None
|
||||
@@ -232,6 +248,15 @@ def print_():
|
||||
if myarg == 'managerscount':
|
||||
managersCountOnly = True
|
||||
i += 1
|
||||
elif myarg in ['memberrestrictions']:
|
||||
memberRestrictions = True
|
||||
display.add_titles_to_csv_file(
|
||||
['memberRestrictionQuery',],
|
||||
titles)
|
||||
display.add_titles_to_csv_file(
|
||||
['memberRestrictionEvaluation',],
|
||||
titles)
|
||||
i += 1
|
||||
else:
|
||||
controlflow.invalid_argument_exit(sys.argv[i], 'gam print cigroups')
|
||||
if roles:
|
||||
@@ -315,7 +340,7 @@ def print_():
|
||||
'list',
|
||||
'memberships',
|
||||
page_message=page_message,
|
||||
message_attribute=['memberKey', 'id'],
|
||||
message_attribute=['preferredMemberKey', 'id'],
|
||||
soft_errors=True,
|
||||
parent=groupKey_id,
|
||||
view='BASIC')
|
||||
@@ -329,7 +354,7 @@ def print_():
|
||||
ownersList = []
|
||||
ownersCount = 0
|
||||
for member in groupMembers:
|
||||
member_email = member['memberKey']['id']
|
||||
member_email = member['preferredMemberKey']['id']
|
||||
role = get_single_role(member.get('roles', []))
|
||||
if not validRoles or role in validRoles:
|
||||
if role == ROLE_MEMBER:
|
||||
@@ -363,6 +388,16 @@ def print_():
|
||||
group['OwnersCount'] = ownersCount
|
||||
if not ownersCountOnly:
|
||||
group['Owners'] = memberDelimiter.join(ownersList)
|
||||
if memberRestrictions:
|
||||
name = f'{groupKey_id}/securitySettings'
|
||||
print(f'Getting member restrictions for {groupEmail} ({i}/{count}')
|
||||
sec_info = gapi.call(ci.groups(),
|
||||
'getSecuritySettings',
|
||||
name=name,
|
||||
readMask='*')
|
||||
if 'memberRestriction' in sec_info:
|
||||
group['memberRestrictionQuery'] = sec_info['memberRestriction'].get('query', '')
|
||||
group['memberRestrictionEvaluation'] = sec_info['memberRestriction'].get('evaluation', {}).get('state', '')
|
||||
csvRows.append(group)
|
||||
if sortHeaders:
|
||||
display.sort_csv_titles([
|
||||
@@ -479,8 +514,8 @@ def print_members():
|
||||
view='FULL',
|
||||
pageSize=500,
|
||||
page_message=page_message,
|
||||
message_attribute=['memberKey', 'id'])
|
||||
#fields='nextPageToken,memberships(memberKey,roles,createTime,updateTime)')
|
||||
message_attribute=['preferredMemberKey', 'id'])
|
||||
#fields='nextPageToken,memberships(preferredMemberKey,roles,createTime,updateTime)')
|
||||
if roles:
|
||||
group_members = filter_members_to_roles(group_members, roles)
|
||||
for member in group_members:
|
||||
@@ -565,7 +600,7 @@ def update():
|
||||
items.append(item)
|
||||
elif len(users_email) > 0:
|
||||
body = {
|
||||
'memberKey': {
|
||||
'preferredMemberKey': {
|
||||
'id': users_email[0]
|
||||
},
|
||||
'roles': [{
|
||||
@@ -785,12 +820,12 @@ def update():
|
||||
page_message=page_message,
|
||||
throw_reasons=gapi_errors.MEMBERS_THROW_REASONS,
|
||||
parent=parent,
|
||||
fields='nextPageToken,memberships(memberKey,roles)')
|
||||
fields='nextPageToken,memberships(preferredMemberKey,roles)')
|
||||
result = filter_members_to_roles(result, roles)
|
||||
if not result:
|
||||
print('Group already has 0 members')
|
||||
return
|
||||
users_email = [member['memberKey']['id'] for member in result]
|
||||
users_email = [member['preferredMemberKey']['id'] for member in result]
|
||||
sys.stderr.write(
|
||||
f'Group: {group}, Will remove {len(users_email)} {", ".join(roles).lower()}s.\n'
|
||||
)
|
||||
@@ -808,6 +843,7 @@ def update():
|
||||
else:
|
||||
i = 4
|
||||
body = {}
|
||||
sec_body = {}
|
||||
while i < len(sys.argv):
|
||||
myarg = sys.argv[i].lower().replace('_', '')
|
||||
if myarg == 'name':
|
||||
@@ -830,17 +866,41 @@ def update():
|
||||
}]
|
||||
}
|
||||
i += 2
|
||||
elif myarg in ['memberrestriction', 'memberrestrictions']:
|
||||
query = sys.argv[i + 1]
|
||||
member_types = {
|
||||
'USER': '1',
|
||||
'SERVICE_ACCOUNT': '2',
|
||||
'GROUP': '3',
|
||||
}
|
||||
for key, val in member_types.items():
|
||||
query = query.replace(key, val)
|
||||
sec_body['memberRestriction'] = {'query': query}
|
||||
i += 2
|
||||
else:
|
||||
controlflow.invalid_argument_exit(sys.argv[i],
|
||||
'gam update cigroup')
|
||||
updateMask = ','.join(body.keys())
|
||||
name = group_email_to_id(ci, group)
|
||||
print(f'Updating group {group}')
|
||||
gapi.call(ci.groups(),
|
||||
'patch',
|
||||
updateMask=updateMask,
|
||||
name=name,
|
||||
body=body)
|
||||
if body:
|
||||
updateMask = ','.join(body.keys())
|
||||
name = group_email_to_id(ci, group)
|
||||
print(f'Updating group {group}')
|
||||
gapi.call(ci.groups(),
|
||||
'patch',
|
||||
updateMask=updateMask,
|
||||
name=name,
|
||||
body=body)
|
||||
if sec_body:
|
||||
updateMask = 'member_restriction.query'
|
||||
# it seems like a bug that API requires /securitySettings
|
||||
# appended to name. We'll see if Google servers change this
|
||||
# at some point.
|
||||
name = f'{group_email_to_id(ci, group)}/securitySettings'
|
||||
print(f'Updating group {group} security settings')
|
||||
gapi.call(ci.groups(),
|
||||
'updateSecuritySettings',
|
||||
name=name,
|
||||
updateMask=updateMask,
|
||||
body=sec_body)
|
||||
|
||||
|
||||
def group_email_to_id(ci, group, i=0, count=0):
|
||||
|
||||
@@ -266,6 +266,8 @@ GROUP_ATTRIBUTES_ARGUMENT_TO_PROPERTY_MAP = {
|
||||
'customReplyTo',
|
||||
'defaultmessagedenynotificationtext':
|
||||
'defaultMessageDenyNotificationText',
|
||||
'defaultsender':
|
||||
'defaultSender',
|
||||
'enablecollaborativeinbox':
|
||||
'enableCollaborativeInbox',
|
||||
'favoriterepliesontop':
|
||||
@@ -979,6 +981,9 @@ def update():
|
||||
sys.stderr.write(
|
||||
f'Group: {group}, Will add {len(to_add)} and remove {len(to_remove)} {role}s.\n'
|
||||
)
|
||||
for user in to_remove:
|
||||
items.append(
|
||||
['gam', 'update', 'group', group, 'remove', user])
|
||||
for user in to_add:
|
||||
item = ['gam', 'update', 'group', group, 'add']
|
||||
if role:
|
||||
@@ -987,9 +992,6 @@ def update():
|
||||
item.append(delivery)
|
||||
item.append(user)
|
||||
items.append(item)
|
||||
for user in to_remove:
|
||||
items.append(
|
||||
['gam', 'update', 'group', group, 'remove', user])
|
||||
elif myarg in ['delete', 'remove']:
|
||||
_, users_email, _ = _getRoleAndUsers()
|
||||
if not exists(cd, group):
|
||||
@@ -1219,7 +1221,7 @@ def getGroupAttrValue(myarg, value, gs_object, gs_body, function):
|
||||
params) in list(gs_object['schemas']['Groups']['properties'].items()):
|
||||
if attrib in ['kind', 'etag', 'email']:
|
||||
continue
|
||||
if myarg == attrib.lower():
|
||||
if myarg == attrib.lower().replace('_', ''):
|
||||
if params['type'] == 'integer':
|
||||
try:
|
||||
if value[-1:].upper() == 'M':
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -4,9 +4,10 @@ filelock
|
||||
google-api-python-client>=2.1
|
||||
google-auth-httplib2
|
||||
google-auth-oauthlib>=0.4.1
|
||||
google-auth>=1.11.2
|
||||
google-auth>=2.3.2
|
||||
httplib2>=0.17.0
|
||||
importlib.metadata; python_version < '3.8'
|
||||
passlib>=1.7.2
|
||||
python-dateutil
|
||||
yubikey-manager>=4.0.0
|
||||
pathvalidate
|
||||
|
||||
49
src/setup.cfg
Normal file
49
src/setup.cfg
Normal file
@@ -0,0 +1,49 @@
|
||||
[metadata]
|
||||
name = GAM for Google Workspace
|
||||
version = 6.0.7
|
||||
description = Command line management for Google Workspaces
|
||||
long_description = file: readme.md
|
||||
long_description_content_type = text/markdown
|
||||
url = https://github.com/jay0lee/GAM
|
||||
author = Jay Lee
|
||||
author_email = jay0lee@gmail.com
|
||||
license = Apache
|
||||
license_files = LICENSE
|
||||
keywords = google, oauth2, gsuite, google-apps, google-admin-sdk, google-drive, google-cloud, google-calendar, gam, google-api, oauth2-client, google-workspace
|
||||
classifiers =
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3 :: Only
|
||||
Programming Language :: Python :: 3.6
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
License :: OSI Approved :: Apache License
|
||||
|
||||
[options]
|
||||
packages = find:
|
||||
python_requires = >=3.6
|
||||
install_requires =
|
||||
cryptography
|
||||
distro; sys_platform == 'linux'
|
||||
filelock
|
||||
google-api-python-client >= 2.1
|
||||
google-auth-httplib2
|
||||
google-auth-oauthlib >= 0.4.1
|
||||
google-auth >= 1.11.2
|
||||
httplib2 >= 0.17.0
|
||||
importlib.metadata; python_version < '3.8'
|
||||
passlib >= 1.7.2
|
||||
python-dateutil
|
||||
yubikey-manager >= 4.0.0
|
||||
pathvalidate
|
||||
|
||||
# used during pip install .[test]
|
||||
[options.extras_require]
|
||||
test = pre-commit
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
gam = gam.__main__:main
|
||||
|
||||
[bdist_wheel]
|
||||
universal = True
|
||||
3
src/setup.py
Normal file
3
src/setup.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from setuptools import setup
|
||||
|
||||
setup()
|
||||
@@ -1,13 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
#from packaging import version
|
||||
from distutils.version import LooseVersion
|
||||
from packaging import version
|
||||
import sys
|
||||
|
||||
a = sys.argv[1]
|
||||
b = sys.argv[2]
|
||||
#result = version.parse(a) >= version.parse(b)
|
||||
result = LooseVersion(a) >= LooseVersion(b)
|
||||
result = version.parse(a) >= version.parse(b)
|
||||
if result:
|
||||
print('OK: %s is equal or newer than %s' % (a, b))
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user