Compare commits

..

142 Commits
v6.07 ... v6.12

Author SHA1 Message Date
Jay Lee
b158496bea Update build.yml 2021-12-28 11:16:23 -05:00
Jay Lee
a79b23e090 Update build.yml 2021-12-28 10:55:38 -05:00
Jay Lee
bdb56240f0 Update build.yml 2021-12-28 10:20:45 -05:00
Jay Lee
6dddf3eb30 Update build.yml 2021-12-28 10:20:25 -05:00
Jay Lee
7bd8569151 Update build.yml 2021-12-28 09:57:14 -05:00
Jay Lee
b03c9f1e35 Update build.yml 2021-12-28 09:47:54 -05:00
Jay Lee
057b5ff760 Update build.yml 2021-12-23 13:44:04 -05:00
Jay Lee
ba512b4159 Update build.yml 2021-12-23 13:39:00 -05:00
Jay Lee
a298aea2fe Update build.yml 2021-12-23 13:32:42 -05:00
Jay Lee
f433463074 Update build.yml 2021-12-23 13:24:01 -05:00
Jay Lee
afae08d6fe Update build.yml 2021-12-23 13:16:25 -05:00
Jay Lee
7cf2a08aff Update build.yml 2021-12-23 13:04:22 -05:00
Jay Lee
7df6781985 Update build.yml 2021-12-23 12:39:57 -05:00
Jay Lee
ae0f5e62e3 Update build.yml 2021-12-23 12:36:43 -05:00
Jay Lee
14c8356c6b Update build.yml 2021-12-23 12:23:37 -05:00
Jay Lee
45ffd4a793 Update build.yml 2021-12-23 12:16:37 -05:00
Jay Lee
eb8d39025e Update build.yml 2021-12-23 12:11:36 -05:00
Jay Lee
1f739e1c63 Update build.yml 2021-12-23 09:11:12 -05:00
Jay Lee
82111236fb Update build.yml 2021-12-23 08:32:32 -05:00
Jay Lee
813a94f8d6 Update build.yml 2021-12-23 08:28:15 -05:00
Jay Lee
e83b75e2c3 Update build.yml 2021-12-23 08:24:08 -05:00
Jay Lee
ce1e880ed0 Update build.yml 2021-12-23 08:19:11 -05:00
Jay Lee
427672065e Update build.yml 2021-12-23 07:24:25 -05:00
Jay Lee
055c5d5e54 Update build.yml 2021-12-22 20:22:05 -05:00
Jay Lee
4de7794e04 Update build.yml 2021-12-22 20:18:46 -05:00
Jay Lee
79686fd8ce Update build.yml 2021-12-22 20:06:18 -05:00
Jay Lee
cc5df0198b Update build.yml 2021-12-22 19:43:50 -05:00
Jay Lee
abc6e55ba7 Update build.yml 2021-12-22 19:31:03 -05:00
Jay Lee
0c8afb7fd6 Update build.yml 2021-12-22 19:26:36 -05:00
Jay Lee
c0c2cca44e Update build.yml 2021-12-22 19:01:16 -05:00
Jay Lee
faa645cb97 Update build.yml 2021-12-22 18:59:29 -05:00
Jay Lee
725c19aafc Update build.yml 2021-12-22 18:49:19 -05:00
Jay Lee
cc3b4c974d Update build.yml 2021-12-22 18:45:23 -05:00
Jay Lee
6ce64fad72 Update build.yml 2021-12-22 18:39:01 -05:00
Jay Lee
c1af67d4a3 Update build.yml 2021-12-22 18:35:59 -05:00
Jay Lee
802cb15007 Update requirements.txt 2021-12-22 16:46:04 -05:00
Jay Lee
b34bf3e56a Update linux-before-install.sh 2021-12-22 15:58:08 -05:00
Jay Lee
bf37700088 Update build.yml 2021-12-22 15:55:47 -05:00
Jay Lee
4a43ddfc25 Update build.yml 2021-12-22 15:51:03 -05:00
Jay Lee
650a1f5154 Update build.yml 2021-12-22 15:43:24 -05:00
Jay Lee
5eda7e30b0 Update build.yml 2021-12-22 13:10:09 -05:00
Jay Lee
8a26f547e5 Update build.yml 2021-12-22 10:25:41 -05:00
Jay Lee
343088913f Update build.yml 2021-12-22 09:56:08 -05:00
Jay Lee
5a0272fd5b Merge branch 'main' of github.com:jay0lee/GAM 2021-12-22 09:54:29 -05:00
Jay Lee
dc93503625 CrOS Telemetry API 2021-12-22 09:52:48 -05:00
Ross Scroggs
6ea6c0889b Fix show filelist query issue; add driveId to drive file fields (#1461)
* Fix show filelist query issue

If the user says: query "A or B" this becomes "'me' in owners and A or B" which is the same as "('me' in owners and A) or B" which gives incorrect results. The fix makes "'me' in owners and (A or B)"

* Add driveId to list of drive file fields
2021-12-17 11:51:32 -05:00
Jay Lee
99ab72df3f GAM 6.12 2021-12-16 07:41:30 -05:00
Ross Scroggs
99bda1385e languages update; fields for gam info user; cloud identity groups update to v1 (#1459)
* languages update

The API doesn't return languages unless you specifically mention in in fields list

* languages cleanup in print users

* Add fields to gam info user

* No up for languages

* Use v1 for Cloud Identity groups; fix bug in print cigroups member

* It's an error to set preference on custom language
2021-12-16 07:40:08 -05:00
Jay Lee
7ce3b4a8c0 Update build.yml 2021-12-15 12:06:46 -05:00
Jay Lee
495722d0d6 Update build.yml 2021-12-14 20:20:09 -05:00
Jay Lee
aca31be5d7 Update build.yml 2021-12-14 19:53:11 -05:00
Jay Lee
b9b7ae8d99 Merge branch 'main' of github.com:jay0lee/GAM 2021-12-09 15:27:38 -05:00
Jay Lee
0d46c1d13a Set user preferred language 2021-12-09 15:07:57 -05:00
Ross Scroggs
6b63ecdc19 Add limittoou <OrgUnitPath> to print users to allow selection of OU w/o children (#1455) 2021-12-09 10:35:47 -05:00
Jay Lee
f9ca0323a1 Update build.yml 2021-12-07 08:18:03 -05:00
Jay Lee
c50aa4d2e8 Update build.yml 2021-12-06 16:27:53 -05:00
Jay Lee
a72ded9079 wipe cache 2021-11-24 11:17:41 -05:00
Jay Lee
cbabbee075 wipe cache 2021-11-24 11:15:24 -05:00
Jay Lee
f55a344b7a Update build.yml 2021-11-23 10:48:03 -05:00
Jay Lee
d84f8418ff Update build.yml 2021-11-23 10:39:56 -05:00
Jay Lee
30c5e92de6 Update build.yml 2021-11-23 10:15:54 -05:00
Jay Lee
5f618a7f65 Update build.yml 2021-11-23 08:41:50 -05:00
Jay Lee
3e833419db Update README.md 2021-11-23 08:33:53 -05:00
Jay Lee
0d94bae0b5 Update README.md 2021-11-23 08:33:40 -05:00
Jay Lee
f5dec96ffb Update README.md 2021-11-23 08:31:32 -05:00
Jay Lee
e91d12caaf Update macos-install.sh 2021-11-23 08:26:12 -05:00
Jay Lee
fd5a1faa58 Update gam.spec 2021-11-22 16:31:44 -05:00
Jay Lee
90a9212793 Update build.yml 2021-11-22 16:28:10 -05:00
Jay Lee
7e582ac1fc Update build.yml 2021-11-22 16:19:34 -05:00
Jay Lee
65a740569c Update build.yml 2021-11-22 16:16:28 -05:00
Jay Lee
a47ef0e1f5 Update build.yml 2021-11-22 15:41:05 -05:00
Jay Lee
b75ad006f1 Update build.yml 2021-11-22 08:26:33 -05:00
Jay Lee
dbc3f0cd83 Update var.py 2021-11-22 08:16:32 -05:00
Jay Lee
ea2750f970 Merge branch 'main' of https://github.com/jay0lee/GAM into main 2021-11-22 08:08:33 -05:00
Jay Lee
a2eb5a2483 Correct certificate not before value to UTC-1h. Fixes #1453 2021-11-22 08:08:20 -05:00
Ross Scroggs
54178543d6 Fix Row Filtering Part 3 (#1450)
Graak! Why I can't get my code translated into yours is beyond me; this time for sure.
2021-11-21 20:25:09 -05:00
Jay Lee
5436f21bc0 Use OpenSSL 3.0.0 in builds 2021-10-29 18:12:30 -04:00
Ross Scroggs
839768a2a5 Fix error handling (#1447) 2021-10-29 13:04:59 -04:00
Jay Lee
2e195d5aa1 Update build.yml 2021-10-29 11:05:57 -04:00
Ross Scroggs
66811f8eb5 Fix Row Filtering Part 2 (#1446)
```
Row Filtering
There can be multiple filters, a filter can match multiple columns (wildcard).
The semantics should be:
For row keep filters, if all filters match, the row is kept.
For row drop filters, if any filter matches, the row is dropped.

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

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

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

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

* Revert "Update requirements.txt"

This reverts commit f89f66d44c.

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

* Fix no longer required

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

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

* Fix gam print users allfields custom all to include primaryEmail

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

* Fix sync devices

* assetTag if specified is part of sync device key

* Handle missing assetTags

* Leave agreedtoterms as an undocumented option

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

* Fix DriveFileAttribute formatting

* memberKey has been replaced by preferredMemberKey

* Correct license name

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

* Update group sync to do removes before adds

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

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

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

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

Adding `pathvalidate` to requrements

* Update __init__.py

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

* Updating with sanitize_filename

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

View File

@@ -55,7 +55,7 @@ else
tar xf openssl-$BUILD_OPENSSL_VERSION.tar.gz tar xf openssl-$BUILD_OPENSSL_VERSION.tar.gz
cd openssl-$BUILD_OPENSSL_VERSION cd openssl-$BUILD_OPENSSL_VERSION
echo "Compiling OpenSSL $BUILD_OPENSSL_VERSION..." echo "Compiling OpenSSL $BUILD_OPENSSL_VERSION..."
./config shared --prefix=$HOME/ssl ./Configure --libdir=lib --prefix=$HOME/ssl
echo "Running make for OpenSSL..." echo "Running make for OpenSSL..."
make -j$cpucount -s make -j$cpucount -s
echo "Running make install for OpenSSL..." echo "Running make install for OpenSSL..."
@@ -70,7 +70,7 @@ else
cd Python-$BUILD_PYTHON_VERSION cd Python-$BUILD_PYTHON_VERSION
echo "Compiling Python $BUILD_PYTHON_VERSION..." echo "Compiling Python $BUILD_PYTHON_VERSION..."
safe_flags="--with-openssl=$HOME/ssl --enable-shared --prefix=$HOME/python --with-ensurepip=upgrade" safe_flags="--with-openssl=$HOME/ssl --enable-shared --prefix=$HOME/python --with-ensurepip=upgrade"
unsafe_flags="--enable-optimizations --with-lto" unsafe_flags="--enable-optimizations --with-lto --with-openssl=~/ssl --with-openssl-rpath=~~/ssl/lib"
if [ ! -e Makefile ]; then if [ ! -e Makefile ]; then
echo "running configure with safe and unsafe" echo "running configure with safe and unsafe"
./configure $safe_flags $unsafe_flags > /dev/null ./configure $safe_flags $unsafe_flags > /dev/null
@@ -94,19 +94,9 @@ else
python=~/python/bin/python3 python=~/python/bin/python3
pip=~/python/bin/pip3 pip=~/python/bin/pip3
if ([ "${ImageOS}" == "ubuntu16" ]) && [ "${HOSTTYPE}" == "x86_64" ]; then if ([ "${ImageOS}" == "ubuntu20" ]) && [ "${HOSTTYPE}" == "x86_64" ]; then
echo "Installing deps for StaticX..." "${python}" -m pip install --upgrade patchelf-wrapper
if [ ! -d patchelf-$PATCHELF_VERSION ]; then "${python}" -m pip install --upgrade staticx
echo "Downloading PatchELF $PATCHELF_VERSION"
wget https://github.com/NixOS/patchelf/archive/$PATCHELF_VERSION.tar.gz
tar xf $PATCHELF_VERSION.tar.gz
cd patchelf-$PATCHELF_VERSION/
./bootstrap.sh
./configure
make
sudo make install
fi
$pip install staticx
fi fi
cd $whereibelong cd $whereibelong

View File

@@ -17,10 +17,10 @@ tar -C ${distpath} --create --file $GAM_ARCHIVE --xz gam
echo "PyInstaller GAM info:" echo "PyInstaller GAM info:"
du -h $gam du -h $gam
time $gam version extended 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 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 $python -OO -m staticx $gam $gam-staticx
strip $gam-staticx #strip $gam-staticx
rm $gampath/gam rm $gampath/gam
mv $gam-staticx $gam mv $gam-staticx $gam
chmod 755 $gam chmod 755 $gam

View File

@@ -29,7 +29,7 @@ echo "installing Python $BUILD_PYTHON_VERSION..."
sudo installer -pkg ./$pyfile -target / sudo installer -pkg ./$pyfile -target /
# This fixes https://github.com/pyinstaller/pyinstaller/issues/5062 # 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 #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 # wget --quiet https://www.python.org/ftp/python/$MIN_PYTHON_VERSION/python-$MIN_PYTHON_VERSION-macosx10.9.pkg

View File

@@ -7,7 +7,7 @@ export distpath="dist/"
export gampath="${distpath}gam" export gampath="${distpath}gam"
rm -rf $gampath rm -rf $gampath
export specfile="gam.spec" export specfile="gam.spec"
$python -OO -m PyInstaller --clean --noupx --strip --distpath "${gampath}" --target-architecture $PLATFORM "${specfile}" $python -OO -m PyInstaller --distpath "${gampath}" "${specfile}"
export gam="${gampath}/gam" export gam="${gampath}/gam"
$gam version extended $gam version extended
export GAMVERSION=`$gam version simple` export GAMVERSION=`$gam version simple`

View File

@@ -12,13 +12,13 @@ defaults:
working-directory: src working-directory: src
env: env:
BUILD_PYTHON_VERSION: "3.9.6" BUILD_PYTHON_VERSION: "3.10.1"
MIN_PYTHON_VERSION: "3.9.6" MIN_PYTHON_VERSION: "3.10.1"
BUILD_OPENSSL_VERSION: "1.1.1k" BUILD_OPENSSL_VERSION: "3.0.1"
MIN_OPENSSL_VERSION: "1.1.1k" MIN_OPENSSL_VERSION: "1.1.1l"
PATCHELF_VERSION: "0.12" PATCHELF_VERSION: "0.13"
# PYINSTALLER_VERSION can be full commit hash or version like v4.20 # PYINSTALLER_VERSION can be full commit hash or version like v4.20
PYINSTALLER_VERSION: "0f2b2e921433ab5a510c7efdb21d9c1d7cfbc645" #PYINSTALLER_VERSION: "86eeca8b4ba8012ab2df19ca206cafbe263b6a81"
jobs: jobs:
build: build:
@@ -26,62 +26,61 @@ jobs:
strategy: strategy:
matrix: matrix:
include: include:
- os: ubuntu-16.04 - os: ubuntu-18.04
jid: 1 jid: 1
goal: "build" goal: "build"
gamos: "linux" gamos: "linux"
platform: "x86_64" platform: "x86_64"
- os: ubuntu-18.04 - os: ubuntu-20.04
jid: 2 jid: 2
goal: "build" goal: "build"
gamos: "linux" gamos: "linux"
platform: "x86_64" platform: "x86_64"
- os: ubuntu-20.04
jid: 3
goal: "build"
gamos: "linux"
platform: "x86_64"
- os: macos-11.0 - os: macos-11.0
jid: 12 jid: 3
goal: "build" goal: "build"
gamos: "macos" gamos: "macos"
platform: "universal2" platform: "universal2"
- os: windows-2019 - os: windows-2022
jid: 5 jid: 4
goal: "build" goal: "build"
gamos: "windows" gamos: "windows"
pyarch: "x64" pyarch: "x64"
platform: "x86_64" platform: "x86_64"
- os: windows-2019 - os: windows-2022
jid: 6 jid: 5
goal: "build" goal: "build"
gamos: "windows" gamos: "windows"
platform: "x86" platform: "x86"
pyarch: "x86" pyarch: "x86"
- os: ubuntu-20.04
goal: "test"
python: "3.6"
jid: 7
gamos: "linux"
platform: "x86_64"
- os: ubuntu-20.04 - os: ubuntu-20.04
goal: "test" goal: "test"
python: "3.7" python: "3.7"
jid: 8 jid: 6
gamos: "linux" gamos: "linux"
platform: "x86_64" platform: "x86_64"
- os: ubuntu-20.04 - os: ubuntu-20.04
goal: "test" goal: "test"
python: "3.8" python: "3.8"
jid: 9 jid: 7
gamos: "linux" gamos: "linux"
platform: "x86_64" platform: "x86_64"
- os: ubuntu-20.04 - os: ubuntu-20.04
goal: test goal: test
python: "3.10.0-beta.1" python: "3.9"
jid: 10 jid: 8
gamos: linux gamos: linux
platform: x86_64 platform: x86_64
- os: [self-hosted, linux, arm64]
jid: 9
goal: "self-build"
platform: "aarch64"
gamos: linux
- os: [self-hosted, linux, arm]
jid: 10
goal: "self-build"
platform: "armv7l"
gamos: linux
steps: steps:
@@ -97,7 +96,7 @@ jobs:
path: | path: |
~/python ~/python
~/ssl ~/ssl
key: ${{ matrix.os }}-${{ matrix.jid }}-20210628 key: ${{ matrix.os }}-${{ matrix.jid }}-20211228
- name: Set env variables - name: Set env variables
env: env:
@@ -120,7 +119,7 @@ jobs:
architecture: ${{ matrix.pyarch }} architecture: ${{ matrix.pyarch }}
- name: Install Python on Windows - name: Install Python on Windows
if: matrix.os == 'windows-2019' if: matrix.os == 'windows-2022'
run: | run: |
if ( ${Env:PLATFORM} -eq "x86_64" ) if ( ${Env:PLATFORM} -eq "x86_64" )
{ {
@@ -139,8 +138,15 @@ jobs:
Start-Process -wait -FilePath $python_file -ArgumentList "/quiet","InstallAllUsers=0","TargetDir=c:\\python","AssociateFiles=1","PrependPath=1" Start-Process -wait -FilePath $python_file -ArgumentList "/quiet","InstallAllUsers=0","TargetDir=c:\\python","AssociateFiles=1","PrependPath=1"
shell: pwsh shell: pwsh
- name: Set env variables for pre-compiled Python - name: Install packages for test
if: matrix.goal == 'test' if: matrix.goal == 'test'
run: |
echo "RUNNING: apt update..."
sudo apt-get -qq --yes update > /dev/null
sudo apt-get -qq --yes install swig libpcsclite-dev
- name: Set env variables for pre-compiled Python
if: matrix.goal != 'build'
run: | run: |
export python=$(which python3) export python=$(which python3)
export pip=$(which pip3) export pip=$(which pip3)
@@ -151,49 +157,73 @@ jobs:
echo "pip=${pip}" >> $GITHUB_ENV echo "pip=${pip}" >> $GITHUB_ENV
echo "gam=${gam}" >> $GITHUB_ENV echo "gam=${gam}" >> $GITHUB_ENV
echo "gampath=${gampath}" >> $GITHUB_ENV echo "gampath=${gampath}" >> $GITHUB_ENV
echo "RUNNING: apt update..."
sudo apt-get -qq --yes update > /dev/null
sudo apt-get -qq --yes install swig libpcsclite-dev
$pip install --upgrade pip $pip install --upgrade pip
"${python}" -V
"${pip}" -V
- name: Build and install Python, OpenSSL and PyInstaller - name: Build and install Python and OpenSSL
if: matrix.goal != 'test' && steps.cache-primes.outputs.cache-hit != 'true' if: matrix.goal == 'build' && steps.cache-primes.outputs.cache-hit != 'true'
run: | run: |
set +e set +e
source ../.github/actions/${GAMOS}-before-install.sh source ../.github/actions/${GAMOS}-before-install.sh
echo "PATH=$PATH" >> $GITHUB_ENV # keep gnutools for MacOS
echo "python=$python" >> $GITHUB_ENV echo "python=$python" >> $GITHUB_ENV
echo "pip=$pip" >> $GITHUB_ENV echo "pip=$pip" >> $GITHUB_ENV
echo "LD_LIBRARY_PATH=$LD_LIBRARY_PATH" >> $GITHUB_ENV echo "LD_LIBRARY_PATH=$LD_LIBRARY_PATH" >> $GITHUB_ENV
echo -e "Python: $python\nPip: $pip\nLD_LIB...: $LD_LIBRARY_PATH" echo -e "Python: $python\nPip: $pip\nLD_LIB...: $LD_LIBRARY_PATH"
$pip install wheel if [ $GAMOS == "macos" ]; then
export url="https://codeload.github.com/pyinstaller/pyinstaller/tar.gz/${PYINSTALLER_VERSION}" export pipoptions='--no-binary ":all:"'
echo "Downloading ${url}" echo "PATH=$PATH" >> $GITHUB_ENV # keep gnutools for MacOS
curl -o pyinstaller.tar.gz --compressed "${url}" export MACOSX_DEPLOYMENT_TARGET="10.9"
tar xf pyinstaller.tar.gz export CFLAGS="-arch arm64 -arch x86_64"
cd "pyinstaller-${PYINSTALLER_VERSION}/"
if [ $GAMOS == "windows" ]; then
# remove pre-compiled bootloaders so we fail if bootloader compile fails
rm -rf PyInstaller/bootloader/*bit
cd bootloader
if [ "${PLATFORM}" == "x86" ]; then
TARGETARCH="--target-arch=32bit"
else
TARGETARCH=""
fi fi
$python ./waf all $TARGETARCH $pip install --upgrade pip $pipoptions
cd .. $pip install wheel $pipoptions
fi
$python setup.py install
#$pip install pyinstaller
- name: Install pip requirements - name: Set Windows Powershell env variables
if: matrix.os != 'self-hosted' if: matrix.goal != 'test' && matrix.os == 'windows-2022' && matrix.platform == 'x86_64'
shell: powershell
run: |
choco install nasm --no-progress
$env:PATH="$ENV:PATH;c:\Program Files\NASM\"
cmd /c 'call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" && set MAKE=nmake && set > %temp%\vcvars.txt'
Get-Content "$env:temp\vcvars.txt" | Foreach-Object {
if ($_ -match "^(.*?)=(.*)$") {
if ($matches[1] -eq "PATH" -or $matches[1] -eq "PLATFORM") {
continue
}
Set-Content "env:\$($matches[1])" $matches[2]
Add-Content -Path $env:GITHUB_ENV -Value "$($matches[1])=$($matches[2])"
}
}
- name: Install PyInstaller
if: matrix.goal != 'test'
run: | run: |
set +e set +e
$pip list --outdated --format=freeze | grep -v '^\-e' | cut -d = -f 1 | xargs -n1 $pip install -U --force-reinstall git clone https://github.com/pyinstaller/pyinstaller.git
cd pyinstaller
# remove pre-compiled bootloaders so we fail if bootloader compile fails
rm -rf PyInstaller/bootloader/*-*/*
cd bootloader
export DefaultWindowsSDKVersion="10.0.20348.0"
if [ "${PLATFORM}" == "x86" ]; then
TARGETARCH="--target-arch=32bit"
fi
$python ./waf all $TARGETARCH
cat build/config.log
cd ..
$pip install .
$pip install --upgrade -r requirements.txt - name: Install pip requirements
run: |
set +e
if [ $GAMOS == "macos" ]; then
#export pipoptions='--no-binary ":all:"'
export MACOSX_DEPLOYMENT_TARGET="10.9"
export CFLAGS="-arch arm64 -arch x86_64"
fi
$pip list --outdated --format=freeze | grep -v '^\-e' | cut -d = -f 1 | xargs -n1 $pip install -U --force-reinstall $pipoptions
$pip install --upgrade -r requirements.txt $pipoptions
- name: Build GAM with PyInstaller - name: Build GAM with PyInstaller
if: matrix.goal != 'test' if: matrix.goal != 'test'
@@ -218,6 +248,7 @@ jobs:
- name: Basic Tests build jobs only - name: Basic Tests build jobs only
if: matrix.goal != 'test' if: matrix.goal != 'test'
run: | run: |
$pip install packaging
export vline=$($gam version | grep "Python ") export vline=$($gam version | grep "Python ")
export python_line=($vline) export python_line=($vline)
export this_python=${python_line[1]} export this_python=${python_line[1]}
@@ -230,7 +261,7 @@ jobs:
- name: Live API tests push only - name: Live API tests push only
if: github.event_name == 'push' || github.event_name == 'schedule' if: github.event_name == 'push' || github.event_name == 'schedule'
env: # Or as an environment variable env:
PASSCODE: ${{ secrets.PASSCODE }} PASSCODE: ${{ secrets.PASSCODE }}
run: | run: |
source ../.github/actions/decrypt.sh ../.github/actions/creds.tar.gpg creds.tar source ../.github/actions/decrypt.sh ../.github/actions/creds.tar.gpg creds.tar
@@ -255,10 +286,12 @@ jobs:
for i in {01..10}; do for i in {01..10}; do
echo "${newbase}-bulkuser-$i" >> sample.csv; echo "${newbase}-bulkuser-$i" >> sample.csv;
done done
$gam create user $newuser firstname GHA lastname $JID password random recoveryphone 12125121110 recoveryemail jay0lee@gmail.com gha.jid $JID $gam create user $newuser firstname GHA lastname $JID password random recoveryphone 12125121110 recoveryemail jay0lee@gmail.com gha.jid $JID languages en+,en-GB-
$gam user $gam_user sendemail recipient $newuser subject "test message $newbase" message "GHA test message" $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 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 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 user $newuser add license gsuitebusiness
$gam update group $newgroup add owner $gam_user $gam update group $newgroup add owner $gam_user
$gam update group $newgroup add member $newuser $gam update group $newgroup add member $newuser
@@ -333,7 +366,8 @@ jobs:
$gam print browsers $gam print browsers
export sn="$JID$JID$JID$JID-$(openssl rand -base64 32 | sed 's/[^a-zA-Z0-9]//g')" 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 create device serialnumber $sn devicetype android
$gam print cros allfields nolists $gam print cros allfields orderby serialnumber
#$gam show crostelemetry storagepercentonly
$gam report usageparameters customer $gam report usageparameters customer
$gam report usage customer parameters gmail:num_emails_sent,accounts:num_1day_logins $gam report usage customer parameters gmail:num_emails_sent,accounts:num_1day_logins
$gam report customer todrive $gam report customer todrive

View File

@@ -1,23 +1,38 @@
GAM is a command line tool for Google Workspace (fka G Suite) Administrators to manage domain and user settings quickly and easily. GAM is a command line tool for Google Workspace admins to manage domain and user settings quickly and easily.
![Build Status](https://github.com/jay0lee/GAM/workflows/Build%20and%20test%20GAM/badge.svg) ![Build Status](https://github.com/jay0lee/GAM/workflows/Build%20and%20test%20GAM/badge.svg)
# Quick Start # Quick Start
## Linux / MacOS ## Linux / MacOS
Open a terminal and run: Open a terminal and run:
```
```sh
bash <(curl -s -S -L https://git.io/install-gam) bash <(curl -s -S -L https://git.io/install-gam)
``` ```
this will download GAM, install it and start setup. this will download GAM, install it and start setup.
## Windows ## Windows
Download the MSI Installer from the [GitHub Releases] page. Install the MSI and you'll be prompted to setup GAM. Download the MSI Installer from the [GitHub Releases] page. Install the MSI and you'll be prompted to setup GAM.
# Documentation # Documentation
The GAM documentation is hosted in the [GitHub Wiki] The GAM documentation is hosted in the [GitHub Wiki]
# Mailing List / Discussion group # 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. 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 # Chat Room
There is a public chat room hosted in Google Chat. [Instructions to join](https://git.io/gam-chat). There is a public chat room hosted in Google Chat. [Instructions to join](https://git.io/gam-chat).
# Author # 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 [GAM release]: https://git.io/gamreleases
[GitHub Releases]: https://github.com/jay0lee/GAM/releases [GitHub Releases]: https://github.com/jay0lee/GAM/releases

View File

@@ -207,8 +207,9 @@ If an item contains spaces, it should be surrounded by ".
<Namespace> ::= <String> <Namespace> ::= <String>
<NotificationID> ::= <String> <NotificationID> ::= <String>
<NumberOfSeats> ::= <Number> <NumberOfSeats> ::= <Number>
<OrgUnitID> ::= <String> <OrgUnitID> ::= id:<String>
<OrgUnitPath> ::= /|(/<String)+ <OrgUnitPath> ::= /|(/<String)+
<OrgUnitItem> ::= <OrgUnitID>|<OrgUnitPath>
<ParameterKey> ::= <String> <ParameterKey> ::= <String>
<ParameterValue> ::= <String> <ParameterValue> ::= <String>
<Password> ::= <String> <Password> ::= <String>
@@ -222,8 +223,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 <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 <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 <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 <QueryGmail> ::= <String> See: https://support.google.com/mail/answer/7190
<QueryGroup> ::= <String> See: https://developers.google.com/admin-sdk/directory/v1/guides/search-groups <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 <QueryMobile> ::= <String> See: https://support.google.com/a/answer/7549103
<QueryTeamDrive> ::= <String> See: https://developers.google.com/drive/api/v3/search-shareddrives <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 <QueryUser> ::= <String> See: https://developers.google.com/admin-sdk/directory/v1/guides/search-users
@@ -324,6 +327,7 @@ If an item contains spaces, it should be surrounded by ".
description| description|
editable| editable|
explicitlytrashed| explicitlytrashed|
driveid|
fileextension| fileextension|
filesize| filesize|
foldercolorrgb| foldercolorrgb|
@@ -353,6 +357,7 @@ If an item contains spaces, it should be surrounded by ".
shared| shared|
sharedwithmedate|sharedwithmetime| sharedwithmedate|sharedwithmetime|
sharinguser| sharinguser|
shortcutdetails|
size| size|
spaces| spaces|
starred| starred|
@@ -591,7 +596,7 @@ Items, separated by spaces, with spaces, commas or single quotes in the items th
<GroupRoleList> ::= "<GroupRole>(,<GroupRole>)*" <GroupRoleList> ::= "<GroupRole>(,<GroupRole>)*"
<GuardianStateList> ::= "<GuardianState>(,<GuardianState>)*" <GuardianStateList> ::= "<GuardianState>(,<GuardianState>)*"
<LabelNameList> ::= "<LabelName>(,<LabelName)*" <LabelNameList> ::= "<LabelName>(,<LabelName)*"
<LanguageList> ::= "<Language>(,<Language)*" <LanguageList> ::= "<Language>[+|-](,<Language>[+|-])*"
<MatterItemList> ::= "<MatterItem>(,<MatterItem>)*" <MatterItemList> ::= "<MatterItem>(,<MatterItem>)*"
<MembersFieldNameList> ::= "<MembersFieldName>(,<MembersFieldName>)*" <MembersFieldNameList> ::= "<MembersFieldName>(,<MembersFieldName>)*"
<MobileList> ::= "<MobileId>(,<MobileId>)*" <MobileList> ::= "<MobileId>(,<MobileId>)*"
@@ -698,9 +703,10 @@ Specify a collection of Users by directly specifying them or by specifiying item
(contentrestrictions readonly true [reason <String>])| (contentrestrictions readonly true [reason <String>])|
copyrequireswriterpermission| copyrequireswriterpermission|
(lastviewedbyme <Time>)|(modifieddate|modifiedtime <Time>)|(description <String>)|(mimetype <MimeType>)| (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>)| (securityupdate <Boolean>)|
(shortcut <DriveFileID>) (shortcut <DriveFileID>)|
writerscantshare|writerscanshare
<DriveFileUpdateAttribute> ::= <DriveFileUpdateAttribute> ::=
(localfile <FileName>|-)| (localfile <FileName>|-)|
(convert)|(ocr)|(ocrlanguage <Language>)| (convert)|(ocr)|(ocrlanguage <Language>)|
@@ -709,9 +715,10 @@ Specify a collection of Users by directly specifying them or by specifiying item
(contentrestrictions readonly true [reason <String>])| (contentrestrictions readonly true [reason <String>])|
(copyrequireswriterpermission <Boolean>)| (copyrequireswriterpermission <Boolean>)|
(lastviewedbyme <Time>)|(modifieddate <Time>)|(description <String>)|(mimetype <MimeType>)| (lastviewedbyme <Time>)|(modifieddate <Time>)|(description <String>)|(mimetype <MimeType>)|
(parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|writerscantshare|writerscanshare (parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|
(securityupdate <Boolean>)| (securityupdate <Boolean>)|
(shortcut <DriveFileID>) (shortcut <DriveFileID>)|
writerscantshare|writerscanshare
<GroupSettingsAttribute> ::= <GroupSettingsAttribute> ::=
(allowexternalmembers <Boolean>)| (allowexternalmembers <Boolean>)|
(allowwebposting <Boolean>)| (allowwebposting <Boolean>)|
@@ -719,6 +726,7 @@ Specify a collection of Users by directly specifying them or by specifiying item
(customfootertext <String>)| (customfootertext <String>)|
(customreplyto <EmailAddress>)| (customreplyto <EmailAddress>)|
(defaultmessagedenynotificationtext <String>)| (defaultmessagedenynotificationtext <String>)|
(defaultsender default_self|group)|
(description <String>)| (description <String>)|
(enablecollaborativeinbox|collaborative <Boolean>)| (enablecollaborativeinbox|collaborative <Boolean>)|
(includeinglobaladdresslist|gal <Boolean>)| (includeinglobaladdresslist|gal <Boolean>)|
@@ -796,7 +804,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 field <FieldName> (type bool|date|double|email|int64|phone|string) [multivalued|multivalue] [indexed] [restricted] [range <Number> <Number>] endfield
<UserBasicAttribute> ::= <UserBasicAttribute> ::=
(agreed2terms|agreedtoterms <Boolean>)|
(changepassword|changepasswordatnextlogin <Boolean>)| (changepassword|changepasswordatnextlogin <Boolean>)|
(base64-md5|base64-sha1|crypt|sha|sha1|sha-1|md5|nohash)| (base64-md5|base64-sha1|crypt|sha|sha1|sha-1|md5|nohash)|
(customerid <String>)| (customerid <String>)|
@@ -1012,7 +1019,8 @@ gam info customer
gam create datatransfer|transfer <OldOwnerID> <DataTransferServiceList> <NewOwnerID> (<ParameterKey> <ParameterValue>)* gam create datatransfer|transfer <OldOwnerID> <DataTransferServiceList> <NewOwnerID> (<ParameterKey> <ParameterValue>)*
gam info datatransfer|transfer <TransferID> gam info datatransfer|transfer <TransferID>
gam print datatransfers|transfers [todrive] [olduser|oldowner <UserItem>] [newuser|newowner <UserItem>] [status <String>] gam print datatransfers|transfers [todrive] [olduser|oldowner <UserItem>] [newuser|newowner <UserItem>]
[status completed|failed|inprogress]
gam print transferapps gam print transferapps
@@ -1381,18 +1389,21 @@ gam print printermodels [todrive] [filter <String>]
gam create cigroup <EmailAddress> <CIGroupAttribute>* gam create cigroup <EmailAddress> <CIGroupAttribute>*
[makeowner] [alias|aliases <AliasList>] [dynamic <QueryDynamicGroup>] [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> 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> 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> 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> update [owner|manager|member] [notsuspended|suspended] [expires never|<Time>] <UserTypeEntity>
gam update cigroup <GroupItem> clear [member] [manager] [owner] [notsuspended|suspended] gam update cigroup <GroupItem> clear [member] [manager] [owner] [notsuspended|suspended]
gam delete cigroup <GroupItem> 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] gam print cigroups [todrive]
[enterprisemember <UserItem>] [enterprisemember <UserItem>]
[members|memberscount] [managers|managerscount] [owners|ownerscount] [members|memberscount] [managers|managerscount] [owners|ownerscount]
[memberrestrictions]
[delimiter <Character>] [sortheaders] [delimiter <Character>] [sortheaders]
gam info cimember <UserItem> <GroupItem> gam info cimember <UserItem> <GroupItem>
@@ -1461,7 +1472,11 @@ gam create user <EmailAddress> <UserAttribute>* [verifynotinvitable]
gam update user <UserItem> <UserAttribute>* [clearschema <SchemaName>] [clearschema <SchemaName>.<FieldName>] [verifynotinvitable] gam update user <UserItem> <UserAttribute>* [clearschema <SchemaName>] [clearschema <SchemaName>.<FieldName>] [verifynotinvitable]
gam delete user <UserItem> gam delete user <UserItem>
gam undelete user <UserItem> [org|ou <OrgUnitPath>] gam undelete user <UserItem> [org|ou <OrgUnitPath>]
gam info user [<UserItem>] [noaliases] [nogroups] [nolicenses|nolicences] [noschemas] [schemas|custom <SchemaNameList>] [userview] [skus|sku <SKUIDList>] [grouptree] gam info user [<UserItem>]
[quick] [noaliases] [nogroups] [nolicenses|nolicences] [noschemas]
[skus|sku <SKUIDList>] [grouptree]
[userview] <UserFieldName>* [fields <UserFieldNameList>]
[schemas|custom all|<SchemaNameList>]
Print fields for selected users; use domain, query/queries and deleted_only to select users to print; Print fields for selected users; use domain, query/queries and deleted_only to select users to print;
if none of these options are specified, all users are printed. if none of these options are specified, all users are printed.
@@ -1469,10 +1484,12 @@ The first column will always be primaryEmail; the remaining field names will be
otherwise, the remaining field names will appear in the order specified. otherwise, the remaining field names will appear in the order specified.
gam print users [todrive] gam print users [todrive]
([domain <DomainName>] [(query <QueryUser>)|(queries <QueryUserList>)] [deleted_only|only_deleted]) ([domain <DomainName>] [(query <QueryUser>)|(queries <QueryUserList>)]
[limittoou <OrgUnitPath>] [deleted_only|only_deleted])
[groups] [license|licenses|licence|licences] [emailpart|emailparts|username] [groups] [license|licenses|licence|licences] [emailpart|emailparts|username]
[orderby <UserOrderByFieldName> [ascending|descending]] [userview] [orderby <UserOrderByFieldName> [ascending|descending]]
[allfields|basic|full | ((<UserFieldName>* | fields <UserFieldNameList>) [schemas|custom all|<SchemaNameList>])] [userview] [allfields|basic|full | (<UserFieldName>* | fields <UserFieldNameList>)]
[schemas|custom all|<SchemaNameList>])]
[delimiter <Character>] [sortheaders] [delimiter <Character>] [sortheaders]
gam create verify|verification <DomainName> gam create verify|verification <DomainName>

View File

@@ -538,6 +538,7 @@
}, },
"CreateEnrollmentTokenRequest": { "CreateEnrollmentTokenRequest": {
"id": "CreateEnrollmentTokenRequest", "id": "CreateEnrollmentTokenRequest",
"type": "object",
"properties": { "properties": {
"org_unit_path": { "org_unit_path": {
"description": "The full path of the organizational unit or its unique ID.", "description": "The full path of the organizational unit or its unique ID.",
@@ -548,6 +549,7 @@
"type": "string" "type": "string"
}, },
"token_type": { "token_type": {
"id": "token_type",
"annotations": { "annotations": {
"required": [ "required": [
"cbcm.enrollmentTokens.create" "cbcm.enrollmentTokens.create"
@@ -559,6 +561,8 @@
} }
}, },
"MoveChromeBrowsersRequest": { "MoveChromeBrowsersRequest": {
"id": "MoveChromeBrowsersRequest",
"type": "object",
"properties": { "properties": {
"org_unit_path": { "org_unit_path": {
"annotations": { "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.", "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" gamversion="latest"
adminuser="" adminuser=""
regularuser="" 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" #gam_macos_vers="10.15.6 10.14.6 10.13.6"
while getopts "hd:a:o:b:lp:u:r:v:" OPTION while getopts "hd:a:o:b:lp:u:r:v:" OPTION
@@ -128,7 +128,7 @@ case $gamos in
this_macos_ver=$osversion this_macos_ver=$osversion
fi fi
echo "You are running MacOS $this_macos_ver" echo "You are running MacOS $this_macos_ver"
gamfile="macos-x86_64.tar.xz" gamfile="macos-universal2.tar.xz"
;; ;;
MINGW64_NT*) MINGW64_NT*)
gamos="windows" gamos="windows"

View File

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

View File

@@ -33,6 +33,11 @@ for d in a.datas:
pyz = PYZ(a.pure) pyz = PYZ(a.pure)
if sys.platform == "darwin":
target_arch="universal2"
else:
target_arch=None
exe = EXE(pyz, exe = EXE(pyz,
a.scripts, a.scripts,
a.binaries, a.binaries,
@@ -42,4 +47,5 @@ exe = EXE(pyz,
debug=False, debug=False,
strip=None, strip=None,
upx=False, upx=False,
target_arch=target_arch,
console=True) console=True)

View File

@@ -33,6 +33,7 @@ import http.client as http_client
from multiprocessing import Pool as mp_pool from multiprocessing import Pool as mp_pool
from multiprocessing import Lock as mp_lock from multiprocessing import Lock as mp_lock
from urllib.parse import quote, urlencode, urlparse from urllib.parse import quote, urlencode, urlparse
from pathvalidate import sanitize_filename
import dateutil.parser import dateutil.parser
import googleapiclient import googleapiclient
@@ -549,6 +550,7 @@ def SetGlobalVariables():
filePresentValue=4, filePresentValue=4,
fileAbsentValue=0) fileAbsentValue=0)
_getOldSignalFile(GC_NO_BROWSER, 'nobrowser.txt') _getOldSignalFile(GC_NO_BROWSER, 'nobrowser.txt')
_getOldSignalFile(GC_NO_TDEMAIL, 'notdemail.txt')
_getOldSignalFile(GC_OAUTH_BROWSER, 'oauthbrowser.txt') _getOldSignalFile(GC_OAUTH_BROWSER, 'oauthbrowser.txt')
# _getOldSignalFile(GC_NO_CACHE, u'nocache.txt') # _getOldSignalFile(GC_NO_CACHE, u'nocache.txt')
# _getOldSignalFile(GC_CACHE_DISCOVERY_ONLY, u'allcache.txt', filePresentValue=False, fileAbsentValue=True) # _getOldSignalFile(GC_CACHE_DISCOVERY_ONLY, u'allcache.txt', filePresentValue=False, fileAbsentValue=True)
@@ -728,8 +730,12 @@ def getOSPlatform():
elif myos == 'Darwin': elif myos == 'Darwin':
myos = 'MacOS' myos = 'MacOS'
mac_ver = platform.mac_ver()[0] 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 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]) pltfrm = ' '.join([codename, mac_ver])
else: else:
pltfrm = platform.platform() pltfrm = platform.platform()
@@ -1240,9 +1246,8 @@ def doCheckServiceAccount(users):
'get', 'get',
name=name, name=name,
throw_reasons=[gapi_errors.ErrorReason.FOUR_O_THREE]) 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_created = dateutil.parser.parse(
key['validAfterTime'], ignoretz=True) + datetime.timedelta(days=1) key['validAfterTime'], ignoretz=True)
key_age = datetime.datetime.now() - key_created key_age = datetime.datetime.now() - key_created
key_days = key_age.days key_days = key_age.days
if key_days > 30: if key_days > 30:
@@ -1757,8 +1762,8 @@ def doCreateAdmin():
def doPrintAdmins(): def doPrintAdmins():
cd = buildGAPIObject('directory') cd = buildGAPIObject('directory')
roleId = None roleId = None
userKey = None
todrive = False todrive = False
kwargs = {}
fields = 'nextPageToken,items(roleAssignmentId,roleId,assignedTo,scopeType,orgUnitId)' fields = 'nextPageToken,items(roleAssignmentId,roleId,assignedTo,scopeType,orgUnitId)'
titles = [ titles = [
'roleAssignmentId', 'roleId', 'role', 'assignedTo', 'assignedToUser', 'roleAssignmentId', 'roleId', 'role', 'assignedTo', 'assignedToUser',
@@ -1769,7 +1774,7 @@ def doPrintAdmins():
while i < len(sys.argv): while i < len(sys.argv):
myarg = sys.argv[i].lower() myarg = sys.argv[i].lower()
if myarg == 'user': if myarg == 'user':
userKey = normalizeEmailAddressOrUID(sys.argv[i + 1]) kwargs['userKey'] = normalizeEmailAddressOrUID(sys.argv[i + 1])
i += 2 i += 2
elif myarg == 'role': elif myarg == 'role':
roleId = getRoleId(sys.argv[i + 1]) roleId = getRoleId(sys.argv[i + 1])
@@ -1779,14 +1784,18 @@ def doPrintAdmins():
i += 1 i += 1
else: else:
controlflow.invalid_argument_exit(sys.argv[i], 'gam print admins') 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(), admins = gapi.get_all_pages(cd.roleAssignments(),
'list', 'list',
'items', 'items',
customer=GC_Values[GC_CUSTOMER_ID], customer=GC_Values[GC_CUSTOMER_ID],
userKey=userKey, fields=fields,
roleId=roleId, **kwargs)
fields=fields)
for admin in admins: for admin in admins:
if roleId and roleId != admin['roleId']:
continue
admin_attrib = {} admin_attrib = {}
for key, value in list(admin.items()): for key, value in list(admin.items()):
if key == 'assignedTo': if key == 'assignedTo':
@@ -3264,7 +3273,7 @@ def printDriveFileList(users):
'orderby', ', '.join(sorted(DRIVEFILE_ORDERBY_CHOICES_MAP)), 'orderby', ', '.join(sorted(DRIVEFILE_ORDERBY_CHOICES_MAP)),
fieldName) fieldName)
elif myarg == 'query': elif myarg == 'query':
query += f' and {sys.argv[i+1]}' query += f' and ({sys.argv[i+1]})'
i += 2 i += 2
elif myarg == 'fullquery': elif myarg == 'fullquery':
query = sys.argv[i + 1] query = sys.argv[i + 1]
@@ -4057,8 +4066,7 @@ def downloadDriveFile(users):
if targetName: if targetName:
safe_file_title = targetName safe_file_title = targetName
else: else:
safe_file_title = ''.join(c for c in result['title'] safe_file_title = sanitize_filename(result['title'])
if c in FILENAME_SAFE_CHARS)
if not safe_file_title: if not safe_file_title:
safe_file_title = fileId safe_file_title = fileId
filename = os.path.join(targetFolder, safe_file_title) filename = os.path.join(targetFolder, safe_file_title)
@@ -6669,12 +6677,12 @@ def getUserAttributes(i, cd, updateCmd):
body['changePasswordAtNextLogin'] = getBoolean( body['changePasswordAtNextLogin'] = getBoolean(
sys.argv[i + 1], myarg) sys.argv[i + 1], myarg)
i += 2 i += 2
elif myarg == 'ipwhitelisted':
body['ipWhitelisted'] = getBoolean(sys.argv[i + 1], myarg)
i += 2
elif myarg == 'agreedtoterms': elif myarg == 'agreedtoterms':
body['agreedToTerms'] = getBoolean(sys.argv[i + 1], myarg) body['agreedToTerms'] = getBoolean(sys.argv[i + 1], myarg)
i += 2 i += 2
elif myarg == 'ipwhitelisted':
body['ipWhitelisted'] = getBoolean(sys.argv[i + 1], myarg)
i += 2
elif myarg in ['org', 'ou']: elif myarg in ['org', 'ou']:
body['orgUnitPath'] = gapi_directory_orgunits.getOrgUnitItem( body['orgUnitPath'] = gapi_directory_orgunits.getOrgUnitItem(
sys.argv[i + 1], pathOnly=True) sys.argv[i + 1], pathOnly=True)
@@ -6685,13 +6693,27 @@ def getUserAttributes(i, cd, updateCmd):
i += 1 i += 1
continue continue
for language in sys.argv[i].replace(',', ' ').split(): for language in sys.argv[i].replace(',', ' ').split():
if language.lower() in LANGUAGE_CODES_MAP: lang_item = {}
appendItemToBodyList( if language[-1] == '+':
body, 'languages', suffix = '+'
{'languageCode': LANGUAGE_CODES_MAP[language.lower()]}) language = language[:-1]
lang_item['preference'] = 'preferred'
elif language[-1] == '-':
suffix = '-'
language = language[:-1]
lang_item['preference'] = 'not_preferred'
else: else:
appendItemToBodyList(body, 'languages', suffix = ''
{'customLanguage': language}) if language.lower() in LANGUAGE_CODES_MAP:
lang_item['languageCode'] = LANGUAGE_CODES_MAP[language.lower()]
else:
if suffix:
controlflow.system_error_exit(
2,
f'suffix {suffix} not allowed with customLanguage {language}'
)
lang_item['customLanguage'] = language
appendItemToBodyList(body, 'languages', lang_item)
i += 1 i += 1
elif myarg == 'gender': elif myarg == 'gender':
i += 1 i += 1
@@ -7236,6 +7258,7 @@ def enableGAMProjectAPIs(GAMProjectAPIs,
gapi_errors.ErrorReason.FORBIDDEN, gapi_errors.ErrorReason.FORBIDDEN,
gapi_errors.ErrorReason.PERMISSION_DENIED gapi_errors.ErrorReason.PERMISSION_DENIED
], ],
retry_reasons=[gapi_errors.ErrorReason.INTERNAL_SERVER_ERROR],
name=service_name) name=service_name)
print(f' API: {api}, Enabled{currentCount(j, jcount)}') print(f' API: {api}, Enabled{currentCount(j, jcount)}')
break break
@@ -7756,11 +7779,11 @@ def _generatePrivateKeyAndPublicCert(client_id, key_size):
x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, client_id)])) x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, client_id)]))
builder = builder.issuer_name( builder = builder.issuer_name(
x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, client_id)])) x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, client_id)]))
not_valid_before = datetime.datetime.today() - datetime.timedelta(days=1) # Gooogle seems to enforce the not before date strictly. Set the not before
not_valid_after = datetime.datetime.today() + datetime.timedelta( # date to be UTC one hour ago should cover any clock skew.
days=365 * 10 - 1) builder = builder.not_valid_before(datetime.datetime.utcnow() - datetime.timedelta(hours=1))
builder = builder.not_valid_before(not_valid_before) # Google uses 12/31/9999 date for end time
builder = builder.not_valid_after(not_valid_after) builder = builder.not_valid_after(datetime.datetime(9999, 12, 31, 23, 59))
builder = builder.serial_number(x509.random_serial_number()) builder = builder.serial_number(x509.random_serial_number())
builder = builder.public_key(public_key) builder = builder.public_key(public_key)
builder = builder.add_extension(x509.BasicConstraints(ca=False, builder = builder.add_extension(x509.BasicConstraints(ca=False,
@@ -7943,10 +7966,18 @@ def doCreateOrRotateServiceAccountKeys(iam=None,
name=sa_name, name=sa_name,
body={'publicKeyData': publicKeyData}) body={'publicKeyData': publicKeyData})
break break
except googleapiclient.errors.HttpError: 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.') print('WARNING: that key already exists.')
result = {'name': oldPrivateKeyId} result = {'name': oldPrivateKeyId}
break 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: except gapi_errors.GapiNotFoundError as e:
if i == max_retries: if i == max_retries:
raise e raise e
@@ -8753,6 +8784,20 @@ def _get_admin_email():
) )
return _getValueFromOAuth('email') return _getValueFromOAuth('email')
def _formatLanguagesList(propertyValue, delimiter):
languages = []
for language in propertyValue:
if 'languageCode' in language:
lang = language['languageCode']
if language.get('preference') == 'preferred':
lang += '+'
elif language.get('preference') == 'not_preferred':
lang += '-'
else:
lang = language.get('customLanguage')
languages.append(lang)
return delimiter.join(languages)
def doGetUserInfo(user_email=None): def doGetUserInfo(user_email=None):
def user_lic_result(request_id, response, exception): def user_lic_result(request_id, response, exception):
@@ -8767,6 +8812,7 @@ def doGetUserInfo(user_email=None):
i = 4 i = 4
else: else:
user_email = _get_admin_email() user_email = _get_admin_email()
fieldsList = []
getSchemas = True getSchemas = True
getAliases = True getAliases = True
getGroups = True getGroups = True
@@ -8797,10 +8843,35 @@ def doGetUserInfo(user_email=None):
getSchemas = False getSchemas = False
projection = 'basic' projection = 'basic'
i += 1 i += 1
elif myarg == 'quick':
getAliases = getCIGroups = getGroups = getLicenses = getSchemas = False
i += 1
elif myarg in ['custom', 'schemas']: elif myarg in ['custom', 'schemas']:
getSchemas = True getSchemas = True
if not fieldsList:
fieldsList = ['primaryEmail']
fieldsList.append('customSchemas')
if sys.argv[i + 1].lower() == 'all':
projection = 'full'
else:
projection = 'custom' projection = 'custom'
customFieldMask = sys.argv[i + 1] customFieldMask = sys.argv[i + 1].replace(' ', ',')
i += 2
elif myarg in USER_ARGUMENT_TO_PROPERTY_MAP:
if not fieldsList:
fieldsList = ['primaryEmail',]
fieldsList.extend(USER_ARGUMENT_TO_PROPERTY_MAP[myarg])
i += 1
elif myarg == 'fields':
if not fieldsList:
fieldsList = ['primaryEmail',]
fieldNameList = sys.argv[i + 1]
for field in fieldNameList.lower().replace(',', ' ').split():
if field in USER_ARGUMENT_TO_PROPERTY_MAP:
fieldsList.extend(USER_ARGUMENT_TO_PROPERTY_MAP[field])
else:
controlflow.invalid_argument_exit(field,
'gam info users fields')
i += 2 i += 2
elif myarg == 'userview': elif myarg == 'userview':
viewType = 'domain_public' viewType = 'domain_public'
@@ -8814,6 +8885,7 @@ def doGetUserInfo(user_email=None):
'get', 'get',
userKey=user_email, userKey=user_email,
projection=projection, projection=projection,
fields=','.join(set(fieldsList)) if fieldsList else '*',
customFieldMask=customFieldMask, customFieldMask=customFieldMask,
viewType=viewType) viewType=viewType)
print(f'User: {user["primaryEmail"]}') print(f'User: {user["primaryEmail"]}')
@@ -8822,14 +8894,7 @@ def doGetUserInfo(user_email=None):
if 'name' in user and 'familyName' in user['name']: if 'name' in user and 'familyName' in user['name']:
print(f'Last Name: {user["name"]["familyName"]}') print(f'Last Name: {user["name"]["familyName"]}')
if 'languages' in user: if 'languages' in user:
up = 'languageCode' print(f"Languages: {_formatLanguagesList(user['languages'], ',')}")
languages = [row[up] for row in user['languages'] if up in row]
if languages:
print(f'Languages: {",".join(languages)}')
up = 'customLanguage'
languages = [row[up] for row in user['languages'] if up in row]
if languages:
print(f'Custom Languages: {",".join(languages)}')
if 'isAdmin' in user: if 'isAdmin' in user:
print(f'Is a Super Admin: {user["isAdmin"]}') print(f'Is a Super Admin: {user["isAdmin"]}')
if 'isDelegatedAdmin' in user: if 'isDelegatedAdmin' in user:
@@ -9644,6 +9709,7 @@ def doPrintUsers():
customFieldMask = None customFieldMask = None
sortHeaders = getGroupFeed = getLicenseFeed = email_parts = False sortHeaders = getGroupFeed = getLicenseFeed = email_parts = False
viewType = deleted_only = orderBy = sortOrder = None viewType = deleted_only = orderBy = sortOrder = None
orgUnitPath = orgUnitPathLower = None
groupDelimiter = ' ' groupDelimiter = ' '
licenseDelimiter = ',' licenseDelimiter = ','
i = 3 i = 3
@@ -9666,12 +9732,14 @@ def doPrintUsers():
sortHeaders = True sortHeaders = True
i += 1 i += 1
elif myarg in ['custom', 'schemas']: elif myarg in ['custom', 'schemas']:
if not fieldsList:
fieldsList = ['primaryEmail']
fieldsList.append('customSchemas') fieldsList.append('customSchemas')
if sys.argv[i + 1].lower() == 'all': if sys.argv[i + 1].lower() == 'all':
projection = 'full' projection = 'full'
else: else:
projection = 'custom' projection = 'custom'
customFieldMask = sys.argv[i + 1] customFieldMask = sys.argv[i + 1].replace(' ', ',')
i += 2 i += 2
elif myarg == 'todrive': elif myarg == 'todrive':
todrive = True todrive = True
@@ -9706,19 +9774,19 @@ def doPrintUsers():
elif myarg in ['query', 'queries']: elif myarg in ['query', 'queries']:
queries = getQueries(myarg, sys.argv[i + 1]) queries = getQueries(myarg, sys.argv[i + 1])
i += 2 i += 2
elif myarg == 'limittoou':
orgUnitPath = gapi_directory_orgunits.getOrgUnitItem(sys.argv[i + 1], pathOnly=True)
orgUnitPathLower = orgUnitPath.lower()
i += 2
elif myarg in USER_ARGUMENT_TO_PROPERTY_MAP: elif myarg in USER_ARGUMENT_TO_PROPERTY_MAP:
if not fieldsList: if not fieldsList:
fieldsList = [ fieldsList = ['primaryEmail',]
'primaryEmail',
]
display.add_field_to_csv_file(myarg, USER_ARGUMENT_TO_PROPERTY_MAP, display.add_field_to_csv_file(myarg, USER_ARGUMENT_TO_PROPERTY_MAP,
fieldsList, fieldsTitles, titles) fieldsList, fieldsTitles, titles)
i += 1 i += 1
elif myarg == 'fields': elif myarg == 'fields':
if not fieldsList: if not fieldsList:
fieldsList = [ fieldsList = ['primaryEmail',]
'primaryEmail',
]
fieldNameList = sys.argv[i + 1] fieldNameList = sys.argv[i + 1]
for field in fieldNameList.lower().replace(',', ' ').split(): for field in fieldNameList.lower().replace(',', ' ').split():
if field in USER_ARGUMENT_TO_PROPERTY_MAP: if field in USER_ARGUMENT_TO_PROPERTY_MAP:
@@ -9741,10 +9809,21 @@ def doPrintUsers():
else: else:
controlflow.invalid_argument_exit(sys.argv[i], 'gam print users') controlflow.invalid_argument_exit(sys.argv[i], 'gam print users')
if fieldsList: if fieldsList:
if orgUnitPath is not None:
fieldsList.append('orgUnitPath')
fields = f'nextPageToken,users({",".join(set(fieldsList)).replace(".", "/")})' fields = f'nextPageToken,users({",".join(set(fieldsList)).replace(".", "/")})'
else: else:
fields = None fields = None
for query in queries: for query in queries:
if orgUnitPath is not None:
if query is not None and query.find(orgUnitPath) == -1:
query += f" orgUnitPath='{orgUnitPath}'"
else:
if query is None:
query = ''
else:
query += ' '
query += f"orgUnitPath='{orgUnitPath}'"
printGettingAllItems('Users', query) printGettingAllItems('Users', query)
page_message = gapi.got_total_items_first_last_msg('Users') page_message = gapi.got_total_items_first_last_msg('Users')
all_users = gapi.get_all_pages(cd.users(), all_users = gapi.get_all_pages(cd.users(),
@@ -9763,11 +9842,14 @@ def doPrintUsers():
projection=projection, projection=projection,
customFieldMask=customFieldMask) customFieldMask=customFieldMask)
for user in all_users: for user in all_users:
if orgUnitPathLower is None or orgUnitPathLower == user.get('orgUnitPath', '').lower():
if email_parts and ('primaryEmail' in user): if email_parts and ('primaryEmail' in user):
user_email = user['primaryEmail'] user_email = user['primaryEmail']
if user_email.find('@') != -1: if user_email.find('@') != -1:
user['primaryEmailLocal'], user[ user['primaryEmailLocal'], user[
'primaryEmailDomain'] = splitEmailAddress(user_email) 'primaryEmailDomain'] = splitEmailAddress(user_email)
if 'languages' in user:
user['languages'] = _formatLanguagesList(user.pop('languages'), ' ')
display.add_row_titles_to_csv_file(utils.flatten_json(user), display.add_row_titles_to_csv_file(utils.flatten_json(user),
csvRows, titles) csvRows, titles)
if sortHeaders: if sortHeaders:
@@ -10433,9 +10515,10 @@ OAUTH2_SCOPES = [
'scopes': 'https://www.googleapis.com/auth/admin.directory.device.chromebrowsers', 'scopes': 'https://www.googleapis.com/auth/admin.directory.device.chromebrowsers',
}, },
{ {
'name': 'Chrome Management API - read only', 'name': 'Chrome Management API - read only (2 scopes)',
'subscope': [], 'subscope': [],
'scopes': ['https://www.googleapis.com/auth/chrome.management.reports.readonly'], 'scopes': ['https://www.googleapis.com/auth/chrome.management.reports.readonly',
'https://www.googleapis.com/auth/chrome.management.telemetry.readonly'],
}, },
{ {
'name': 'Chrome Policy API', 'name': 'Chrome Policy API',
@@ -11621,6 +11704,8 @@ def ProcessGAMCommand(args):
gapi_cloudidentity_groups.print_() gapi_cloudidentity_groups.print_()
elif argument == 'devices': elif argument == 'devices':
gapi_cloudidentity_devices.print_() gapi_cloudidentity_devices.print_()
elif argument == 'crostelemetry':
gapi_chromemanagement.printShowCrosTelemetry()
elif argument in ['groupmembers', 'groupsmembers']: elif argument in ['groupmembers', 'groupsmembers']:
gapi_directory_groups.print_members() gapi_directory_groups.print_members()
elif argument in ['cigroupmembers', 'cigroupsmembers']: elif argument in ['cigroupmembers', 'cigroupsmembers']:
@@ -11729,6 +11814,8 @@ def ProcessGAMCommand(args):
gapi_chromepolicy.printshow_schemas() gapi_chromepolicy.printshow_schemas()
elif argument in ['chromepolicy', 'chromepolicies']: elif argument in ['chromepolicy', 'chromepolicies']:
gapi_chromepolicy.printshow_policies() gapi_chromepolicy.printshow_policies()
elif argument == 'crostelemetry':
gapi_chromemanagement.printShowCrosTelemetry(True)
else: else:
controlflow.invalid_argument_exit(argument, 'gam show') controlflow.invalid_argument_exit(argument, 'gam show')
sys.exit(0) sys.exit(0)

View File

@@ -30,7 +30,7 @@ from gam import controlflow
import gam import gam
def main(argv): def main():
freeze_support() freeze_support()
if sys.platform == 'darwin': if sys.platform == 'darwin':
# https://bugs.python.org/issue33725 in Python 3.8.0 seems # https://bugs.python.org/issue33725 in Python 3.8.0 seems
@@ -47,4 +47,4 @@ def main(argv):
# Run from command line # Run from command line
if __name__ == '__main__': if __name__ == '__main__':
main(sys.argv) main()

View File

@@ -395,7 +395,7 @@ class Credentials(google.oauth2.credentials.Credentials):
self.refresh(request) self.refresh(request)
self._id_token_data = google.oauth2.id_token.verify_oauth2_token( 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): def get_token_value(self, field):
"""Retrieves data from the OAuth ID token. """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 import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives.asymmetric import padding
from smartcard.Exceptions import CardConnectionException
from ykman.device import connect_to_device from ykman.device import connect_to_device
from ykman.piv import generate_self_signed_certificate, \ from ykman.piv import generate_self_signed_certificate, \
generate_chuid generate_chuid
@@ -46,7 +47,10 @@ class YubiKey():
self.key_id = service_account_info.get('private_key_id') self.key_id = service_account_info.get('private_key_id')
def _connect(self): def _connect(self):
try:
conn, _, _ = connect_to_device(self.serial_number) conn, _, _ = connect_to_device(self.serial_number)
except CardConnectionException as err:
controlflow.system_error_exit(9, f'YubiKey - {err}')
return conn return conn
def get_certificate(self): def get_certificate(self):
@@ -62,7 +66,7 @@ class YubiKey():
try: try:
cert = session.get_certificate(self.slot) cert = session.get_certificate(self.slot)
except ApduError as err: 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( cert_pem = cert.public_bytes(
serialization.Encoding.PEM).decode() serialization.Encoding.PEM).decode()
publicKeyData = b64encode(cert_pem.encode()) publicKeyData = b64encode(cert_pem.encode())
@@ -78,7 +82,7 @@ class YubiKey():
_, _, info = connect_to_device(self.serial_number) _, _, info = connect_to_device(self.serial_number)
return info.serial return info.serial
except ValueError as err: except ValueError as err:
controlflow.system_error_exit(9, f'YubikKey = {err}') controlflow.system_error_exit(9, f'YubiKey - {err}')
def reset_piv(self): def reset_piv(self):
'''Resets YubiKey PIV app and generates new key for GAM to use.''' '''Resets YubiKey PIV app and generates new key for GAM to use.'''
@@ -101,7 +105,7 @@ class YubiKey():
DEFAULT_MANAGEMENT_KEY) DEFAULT_MANAGEMENT_KEY)
piv.verify_pin(new_pin) 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, pubkey = piv.generate_key(SLOT.AUTHENTICATION,
KEY_TYPE.RSA2048, KEY_TYPE.RSA2048,
PIN_POLICY.ALWAYS, PIN_POLICY.ALWAYS,
@@ -123,7 +127,7 @@ class YubiKey():
piv.put_object(OBJECT_ID.CHUID, piv.put_object(OBJECT_ID.CHUID,
generate_chuid()) generate_chuid())
except ValueError as err: except ValueError as err:
controlflow.system_error_exit(8, f'Yubikey - {err}') controlflow.system_error_exit(8, f'YubiKey - {err}')
def sign(self, message): def sign(self, message):
@@ -145,7 +149,7 @@ class YubiKey():
hash_algorithm=hashes.SHA256(), hash_algorithm=hashes.SHA256(),
padding=padding.PKCS1v15()) padding=padding.PKCS1v15())
except ApduError as err: except ApduError as err:
controlflow.system_error_exit(8, f'YubiKey = {err}') controlflow.system_error_exit(8, f'YubiKey - {err}')
except ValueError as err: except ValueError as err:
controlflow.system_error_exit(9, f'YubiKey - {err}') controlflow.system_error_exit(9, f'YubiKey - {err}')
if 'mplock' in globals(): if 'mplock' in globals():

View File

@@ -154,9 +154,8 @@ def write_csv_file(csvRows, titles, list_type, todrive):
return True return True
return False return False
def rowFilterMatch(filters, columns, row): def filterMatch(filterVal, columns, row):
for c, filterVal in iter(filters.items()): for column in columns:
for column in columns[c]:
if filterVal[1] == 'regex': if filterVal[1] == 'regex':
if filterVal[2].search(str(row.get(column, ''))): if filterVal[2].search(str(row.get(column, ''))):
return True return True
@@ -178,6 +177,18 @@ def write_csv_file(csvRows, titles, list_type, todrive):
return True return True
return False return False
def rowFilterMatch(filters, columns, row):
for c, filterVal in iter(filters.items()):
if not filterMatch(filterVal, columns[c], row):
return False
return True
def rowDropFilterMatch(filters, columns, row):
for c, filterVal in iter(filters.items()):
if filterMatch(filterVal, columns[c], row):
return True
return False
if GC_Values[GC_CSV_ROW_FILTER] or GC_Values[GC_CSV_ROW_DROP_FILTER]: if GC_Values[GC_CSV_ROW_FILTER] or GC_Values[GC_CSV_ROW_DROP_FILTER]:
if GC_Values[GC_CSV_ROW_FILTER]: if GC_Values[GC_CSV_ROW_FILTER]:
keepColumns = {} keepColumns = {}
@@ -210,7 +221,7 @@ def write_csv_file(csvRows, titles, list_type, todrive):
if (((keepColumns is None) or if (((keepColumns is None) or
rowFilterMatch(GC_Values[GC_CSV_ROW_FILTER], keepColumns, row)) and rowFilterMatch(GC_Values[GC_CSV_ROW_FILTER], keepColumns, row)) and
((dropColumns is None) or ((dropColumns is None) or
not rowFilterMatch(GC_Values[GC_CSV_ROW_DROP_FILTER], dropColumns, row))): not rowDropFilterMatch(GC_Values[GC_CSV_ROW_DROP_FILTER], dropColumns, row))):
rows.append(row) rows.append(row)
csvRows = rows csvRows = rows
@@ -231,7 +242,14 @@ def write_csv_file(csvRows, titles, list_type, todrive):
'No columns selected with GAM_CSV_HEADER_FILTER and GAM_CSV_HEADER_DROP_FILTER\n' 'No columns selected with GAM_CSV_HEADER_FILTER and GAM_CSV_HEADER_DROP_FILTER\n'
) )
return 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: if todrive:
write_to = io.StringIO() write_to = io.StringIO()
else: else:
@@ -239,8 +257,7 @@ def write_csv_file(csvRows, titles, list_type, todrive):
writer = csv.DictWriter(write_to, writer = csv.DictWriter(write_to,
fieldnames=titles, fieldnames=titles,
dialect='nixstdout', dialect='nixstdout',
extrasaction='ignore', extrasaction='ignore')
quoting=csv.QUOTE_MINIMAL)
try: try:
writer.writerow(dict((item, item) for item in writer.fieldnames)) writer.writerow(dict((item, item) for item in writer.fieldnames))
writer.writerows(csvRows) writer.writerows(csvRows)
@@ -283,6 +300,7 @@ and follow recommend steps to authorize GAM for Drive access.''')
if GC_Values[GC_NO_BROWSER]: if GC_Values[GC_NO_BROWSER]:
msg_txt = f'Drive file uploaded to:\n {file_url}' msg_txt = f'Drive file uploaded to:\n {file_url}'
msg_subj = f'{GC_Values[GC_DOMAIN]} - {list_type}' msg_subj = f'{GC_Values[GC_DOMAIN]} - {list_type}'
if not GC_Values[GC_NO_TDEMAIL]:
gam.send_email(msg_subj, msg_txt) gam.send_email(msg_subj, msg_txt)
print(msg_txt) print(msg_txt)
else: else:

View File

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

View File

@@ -9,6 +9,7 @@ from gam.var import YYYYMMDD_FORMAT
from gam import controlflow from gam import controlflow
from gam import display from gam import display
from gam import gapi from gam import gapi
from gam import utils
from gam.gapi.directory import orgunits as gapi_directory_orgunits from gam.gapi.directory import orgunits as gapi_directory_orgunits
from gam.gapi.directory.cros import _getFilterDate from gam.gapi.directory.cros import _getFilterDate
@@ -201,6 +202,79 @@ def printAppDevices():
display.write_csv_file(csvRows, titles, 'Chrome Installed Application Devices', todrive) display.write_csv_file(csvRows, titles, 'Chrome Installed Application Devices', todrive)
def printShowCrosTelemetry(show=False):
cm = build()
parent = _get_customerid()
todrive = False
filter_ = None
readMask = []
diskpercentonly = False
supported_readmask_values = list(cm._rootDesc['schemas']['GoogleChromeManagementV1TelemetryDevice']['properties'].keys())
supported_readmask_values.sort()
supported_readmask_map = {item.lower():item for item in supported_readmask_values}
listLimit = 0
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'fields':
field_list = sys.argv[i+1].lower().split(',')
for field_item in field_list:
if field_item not in supported_readmask_map:
controlflow.expected_argument_exit('fields',
', '.join(supported_readmask_values),
field_item)
else:
readMask.append(supported_readmask_map[field_item])
i += 2
elif myarg == 'filter':
filter_ = sys.argv[i+1]
i += 2
elif myarg == 'todrive':
todrive = True
i += 1
elif myarg == 'storagepercentonly':
diskpercentonly = True
i += 1
else:
msg = f'{myarg} is not a valid argument to "gam print crostelemetry"'
controlflow.system_error_exit(3, msg)
if not readMask:
readMask = ','.join(supported_readmask_values)
else:
if 'deviceId' not in readMask:
readMask.append('deviceId')
readMask = ','.join(readMask)
gam.printGettingAllItems('Chrome Device Telemetry...', filter_)
page_message = gapi.got_total_items_msg('Chrome Device Telemetry', '...\n')
devices = gapi.get_all_pages(cm.customers().telemetry().devices(),
'list',
'devices',
page_message=page_message,
parent=parent,
filter=filter_,
readMask=readMask)
for device in devices:
if 'totalDiskBytes' in device.get('storageInfo', {}) and 'availableDiskBytes' in device.get('storageInfo', {}):
disk_avail = int(device['storageInfo']['availableDiskBytes'])
disk_size = int(device['storageInfo']['totalDiskBytes'])
if diskpercentonly:
device['storageInfo'] = {}
device['storageInfo']['percentDiskFree'] = int((disk_avail / disk_size) * 100)
device['storageInfo']['percentDiskUsed'] = 100 - device['storageInfo']['percentDiskFree']
if show:
for device in devices:
display.print_json(device)
print()
print()
else:
csvRows = []
titles = []
for device in devices:
display.add_row_titles_to_csv_file(utils.flatten_json(device),
csvRows, titles)
display.write_csv_file(csvRows, titles, 'Telemetry Devices', todrive)
CHROME_VERSIONS_TITLES = [ CHROME_VERSIONS_TITLES = [
'version', 'count', 'channel', 'deviceOsVersion', 'system' 'version', 'count', 'channel', 'deviceOsVersion', 'system'
] ]

View File

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

View File

@@ -405,7 +405,7 @@ def sync():
controlflow.csv_field_error_exit(devicetype_column, input_file.fieldnames) controlflow.csv_field_error_exit(devicetype_column, input_file.fieldnames)
if assettag_column and assettag_column not in input_file.fieldnames: if assettag_column and assettag_column not in input_file.fieldnames:
controlflow.csv_field_error_exit(assettag_column, input_file.fieldnames) controlflow.csv_field_error_exit(assettag_column, input_file.fieldnames)
local_devices = [] local_devices = {}
for row in input_file: for row in input_file:
# upper() is very important to comparison since Google # upper() is very important to comparison since Google
# always return uppercase serials # always return uppercase serials
@@ -414,28 +414,43 @@ def sync():
local_device['deviceType'] = static_devicetype local_device['deviceType'] = static_devicetype
else: else:
local_device['deviceType'] = row[devicetype_column].strip() local_device['deviceType'] = row[devicetype_column].strip()
sndt = f"{local_device['serialNumber']}-{local_device['deviceType']}"
if assettag_column: if assettag_column:
local_device['assetTag'] = row[assettag_column].strip() 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) fileutils.close_file(f)
page_message = gapi.got_total_items_msg('Company Devices', '...\n') page_message = gapi.got_total_items_msg('Company Devices', '...\n')
device_fields = ['serialNumber', 'deviceType', 'lastSyncTime', 'name'] device_fields = ['serialNumber', 'deviceType', 'lastSyncTime', 'name']
if assettag_column: if assettag_column:
device_fields.append('assetTag') device_fields.append('assetTag')
fields = f'nextPageToken,devices({",".join(device_fields)})' 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, customer=customer, page_message=page_message,
pageSize=100, filter=device_filter, view='COMPANY_INVENTORY', fields=fields) pageSize=100, filter=device_filter, view='COMPANY_INVENTORY', fields=fields)
remote_device_map = {} for remote_device in result:
for remote_device in remote_devices:
sn = remote_device['serialNumber'] sn = remote_device['serialNumber']
last_sync = remote_device.pop('lastSyncTime', NEVER_TIME_NOMS) last_sync = remote_device.pop('lastSyncTime', NEVER_TIME_NOMS)
name = remote_device.pop('name') 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: if last_sync == NEVER_TIME_NOMS:
remote_device_map[sn]['unassigned'] = True remote_device_map[sndt]['unassigned'] = True
devices_to_add = [device for device in local_devices if device not in remote_devices] devices_to_add = []
missing_devices = [device for device in remote_devices if device not in local_devices] 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...') print(f'Need to add {len(devices_to_add)} and remove {len(missing_devices)} devices...')
for add_device in devices_to_add: for add_device in devices_to_add:
print(f'Creating {add_device["serialNumber"]}') print(f'Creating {add_device["serialNumber"]}')
@@ -447,8 +462,11 @@ def sync():
print(f' {add_device["serialNumber"]} already exists') print(f' {add_device["serialNumber"]} already exists')
for missing_device in missing_devices: for missing_device in missing_devices:
sn = missing_device['serialNumber'] sn = missing_device['serialNumber']
name = remote_device_map[sn]['name'] sndt = f"{sn}-{missing_device['deviceType']}"
unassigned = remote_device_map[sn].get('unassigned') 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 action = unassigned_missing_action if unassigned else assigned_missing_action
if action == 'donothing': if action == 'donothing':
pass pass

View File

@@ -3,7 +3,7 @@ import sys
import googleapiclient import googleapiclient
import gam import gam
from gam.var import * from gam.var import * # pylint: disable=unused-wildcard-import
from gam import controlflow from gam import controlflow
from gam import display from gam import display
from gam import gapi from gam import gapi
@@ -12,6 +12,14 @@ from gam.gapi import errors as gapi_errors
from gam.gapi import cloudidentity as gapi_cloudidentity from gam.gapi import cloudidentity as gapi_cloudidentity
from gam.gapi.directory import customer as gapi_directory_customer from gam.gapi.directory import customer as gapi_directory_customer
# This allows easy switching between v1 and v1beta1
# v1
CIGROUP_API_BETA = 'cloudidentity'
CIGROUP_MEMBERKEY = 'preferredMemberKey'
# v1beta1
#CIGROUP_API_BETA = 'cloudidentity_beta'
#CIGROUP_MEMBERKEY = 'memberKey'
def create(): def create():
ci = gapi_cloudidentity.build() ci = gapi_cloudidentity.build()
@@ -73,9 +81,10 @@ def delete():
def info(): def info():
ci = gapi_cloudidentity.build('cloudidentity_beta') ci = gapi_cloudidentity.build(CIGROUP_API_BETA)
group = gam.normalizeEmailAddressOrUID(sys.argv[3]) group = gam.normalizeEmailAddressOrUID(sys.argv[3])
getUsers = True getUsers = True
getSecuritySettings = True
showJoinDate = True showJoinDate = True
showUpdateDate = False showUpdateDate = False
showMemberTree = False showMemberTree = False
@@ -94,11 +103,20 @@ def info():
elif myarg == 'membertree': elif myarg == 'membertree':
showMemberTree = True showMemberTree = True
i += 1 i += 1
elif myarg in ['nosecurity', 'nosecuritysettings']:
getSecuritySettings = False
else: else:
controlflow.invalid_argument_exit(myarg, 'gam info cigroup') controlflow.invalid_argument_exit(myarg, 'gam info cigroup')
name = group_email_to_id(ci, group) name = group_email_to_id(ci, group)
basic_info = gapi.call(ci.groups(), 'get', name=name) basic_info = gapi.call(ci.groups(), 'get', name=name)
display.print_json(basic_info) 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 getUsers and not showMemberTree:
if not showJoinDate and not showUpdateDate: if not showJoinDate and not showUpdateDate:
view = 'BASIC' view = 'BASIC'
@@ -116,7 +134,7 @@ def info():
print(' Members:') print(' Members:')
for member in members: for member in members:
role = get_single_role(member.get('roles', [])).lower() role = get_single_role(member.get('roles', [])).lower()
email = member.get('memberKey', {}).get('id') email = member.get(CIGROUP_MEMBERKEY, {}).get('id')
member_type = member.get('type', 'USER').lower() member_type = member.get('type', 'USER').lower()
jc_string = '' jc_string = ''
if showJoinDate: if showJoinDate:
@@ -145,7 +163,7 @@ def print_member_tree(ci, group_id, cached_group_members, spaces, show_role):
for member in cached_group_members[group_id]: for member in cached_group_members[group_id]:
member_id = member.get('name', '') member_id = member.get('name', '')
member_id = member_id.split('/')[-1] member_id = member_id.split('/')[-1]
email = member.get('memberKey', {}).get('id') email = member.get(CIGROUP_MEMBERKEY, {}).get('id')
member_type = member.get('type', 'USER').lower() member_type = member.get('type', 'USER').lower()
if show_role: if show_role:
role = get_single_role(member.get('roles', [])).lower() role = get_single_role(member.get('roles', [])).lower()
@@ -187,9 +205,15 @@ GROUP_ROLES_MAP = {
def print_(): def print_():
ci = gapi_cloudidentity.build('cloudidentity_beta') ci = gapi_cloudidentity.build(CIGROUP_API_BETA)
i = 3 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() gapi_directory_customer.setTrueCustomerId()
parent = f'customers/{GC_Values[GC_CUSTOMER_ID]}' parent = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
usemember = None usemember = None
@@ -232,6 +256,15 @@ def print_():
if myarg == 'managerscount': if myarg == 'managerscount':
managersCountOnly = True managersCountOnly = True
i += 1 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: else:
controlflow.invalid_argument_exit(sys.argv[i], 'gam print cigroups') controlflow.invalid_argument_exit(sys.argv[i], 'gam print cigroups')
if roles: if roles:
@@ -310,12 +343,12 @@ def print_():
) )
page_message = gapi.got_total_items_first_last_msg('Members') page_message = gapi.got_total_items_first_last_msg('Members')
validRoles, _, _ = gam._getRoleVerification( validRoles, _, _ = gam._getRoleVerification(
'.'.join(roles), 'nextPageToken,members(email,id,role)') ','.join(roles), 'nextPageToken,members(email,id,role)')
groupMembers = gapi.get_all_pages(ci.groups().memberships(), groupMembers = gapi.get_all_pages(ci.groups().memberships(),
'list', 'list',
'memberships', 'memberships',
page_message=page_message, page_message=page_message,
message_attribute=['memberKey', 'id'], message_attribute=[CIGROUP_MEMBERKEY, 'id'],
soft_errors=True, soft_errors=True,
parent=groupKey_id, parent=groupKey_id,
view='BASIC') view='BASIC')
@@ -329,7 +362,7 @@ def print_():
ownersList = [] ownersList = []
ownersCount = 0 ownersCount = 0
for member in groupMembers: for member in groupMembers:
member_email = member['memberKey']['id'] member_email = member[CIGROUP_MEMBERKEY]['id']
role = get_single_role(member.get('roles', [])) role = get_single_role(member.get('roles', []))
if not validRoles or role in validRoles: if not validRoles or role in validRoles:
if role == ROLE_MEMBER: if role == ROLE_MEMBER:
@@ -363,6 +396,16 @@ def print_():
group['OwnersCount'] = ownersCount group['OwnersCount'] = ownersCount
if not ownersCountOnly: if not ownersCountOnly:
group['Owners'] = memberDelimiter.join(ownersList) 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) csvRows.append(group)
if sortHeaders: if sortHeaders:
display.sort_csv_titles([ display.sort_csv_titles([
@@ -412,7 +455,7 @@ def _get_groups_list(ci=None, member=None, parent=None):
def get_membership_graph(member): def get_membership_graph(member):
ci = gapi_cloudidentity.build('cloudidentity_beta') ci = gapi_cloudidentity.build(CIGROUP_API_BETA)
query = f"member_key_id == '{member}' && 'cloudidentity.googleapis.com/groups.discussion_forum' in labels" query = f"member_key_id == '{member}' && 'cloudidentity.googleapis.com/groups.discussion_forum' in labels"
result = gapi.call(ci.groups().memberships(), result = gapi.call(ci.groups().memberships(),
'getMembershipGraph', 'getMembershipGraph',
@@ -422,7 +465,7 @@ def get_membership_graph(member):
def print_members(): def print_members():
ci = gapi_cloudidentity.build('cloudidentity_beta') ci = gapi_cloudidentity.build(CIGROUP_API_BETA)
todrive = False todrive = False
gapi_directory_customer.setTrueCustomerId() gapi_directory_customer.setTrueCustomerId()
parent = f'customers/{GC_Values[GC_CUSTOMER_ID]}' parent = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
@@ -479,8 +522,8 @@ def print_members():
view='FULL', view='FULL',
pageSize=500, pageSize=500,
page_message=page_message, page_message=page_message,
message_attribute=['memberKey', 'id']) message_attribute=[CIGROUP_MEMBERKEY, 'id'])
#fields='nextPageToken,memberships(memberKey,roles,createTime,updateTime)') #fields=f'nextPageToken,memberships({CIGROUP_MEMBERKEY},roles,createTime,updateTime)')
if roles: if roles:
group_members = filter_members_to_roles(group_members, roles) group_members = filter_members_to_roles(group_members, roles)
for member in group_members: for member in group_members:
@@ -538,7 +581,7 @@ def update():
] ]
return (role, expireTime, users_email) return (role, expireTime, users_email)
ci = gapi_cloudidentity.build('cloudidentity_beta') ci = gapi_cloudidentity.build(CIGROUP_API_BETA)
group = sys.argv[3] group = sys.argv[3]
myarg = sys.argv[4].lower() myarg = sys.argv[4].lower()
items = [] items = []
@@ -565,7 +608,7 @@ def update():
items.append(item) items.append(item)
elif len(users_email) > 0: elif len(users_email) > 0:
body = { body = {
'memberKey': { CIGROUP_MEMBERKEY: {
'id': users_email[0] 'id': users_email[0]
}, },
'roles': [{ 'roles': [{
@@ -785,12 +828,12 @@ def update():
page_message=page_message, page_message=page_message,
throw_reasons=gapi_errors.MEMBERS_THROW_REASONS, throw_reasons=gapi_errors.MEMBERS_THROW_REASONS,
parent=parent, parent=parent,
fields='nextPageToken,memberships(memberKey,roles)') fields=f'nextPageToken,memberships({CIGROUP_MEMBERKEY},roles)')
result = filter_members_to_roles(result, roles) result = filter_members_to_roles(result, roles)
if not result: if not result:
print('Group already has 0 members') print('Group already has 0 members')
return return
users_email = [member['memberKey']['id'] for member in result] users_email = [member[CIGROUP_MEMBERKEY]['id'] for member in result]
sys.stderr.write( sys.stderr.write(
f'Group: {group}, Will remove {len(users_email)} {", ".join(roles).lower()}s.\n' f'Group: {group}, Will remove {len(users_email)} {", ".join(roles).lower()}s.\n'
) )
@@ -808,6 +851,7 @@ def update():
else: else:
i = 4 i = 4
body = {} body = {}
sec_body = {}
while i < len(sys.argv): while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '') myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'name': if myarg == 'name':
@@ -830,9 +874,21 @@ def update():
}] }]
} }
i += 2 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: else:
controlflow.invalid_argument_exit(sys.argv[i], controlflow.invalid_argument_exit(sys.argv[i],
'gam update cigroup') 'gam update cigroup')
if body:
updateMask = ','.join(body.keys()) updateMask = ','.join(body.keys())
name = group_email_to_id(ci, group) name = group_email_to_id(ci, group)
print(f'Updating group {group}') print(f'Updating group {group}')
@@ -841,6 +897,18 @@ def update():
updateMask=updateMask, updateMask=updateMask,
name=name, name=name,
body=body) 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): def group_email_to_id(ci, group, i=0, count=0):

View File

@@ -266,6 +266,8 @@ GROUP_ATTRIBUTES_ARGUMENT_TO_PROPERTY_MAP = {
'customReplyTo', 'customReplyTo',
'defaultmessagedenynotificationtext': 'defaultmessagedenynotificationtext':
'defaultMessageDenyNotificationText', 'defaultMessageDenyNotificationText',
'defaultsender':
'defaultSender',
'enablecollaborativeinbox': 'enablecollaborativeinbox':
'enableCollaborativeInbox', 'enableCollaborativeInbox',
'favoriterepliesontop': 'favoriterepliesontop':
@@ -979,6 +981,9 @@ def update():
sys.stderr.write( sys.stderr.write(
f'Group: {group}, Will add {len(to_add)} and remove {len(to_remove)} {role}s.\n' 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: for user in to_add:
item = ['gam', 'update', 'group', group, 'add'] item = ['gam', 'update', 'group', group, 'add']
if role: if role:
@@ -987,9 +992,6 @@ def update():
item.append(delivery) item.append(delivery)
item.append(user) item.append(user)
items.append(item) items.append(item)
for user in to_remove:
items.append(
['gam', 'update', 'group', group, 'remove', user])
elif myarg in ['delete', 'remove']: elif myarg in ['delete', 'remove']:
_, users_email, _ = _getRoleAndUsers() _, users_email, _ = _getRoleAndUsers()
if not exists(cd, group): 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()): params) in list(gs_object['schemas']['Groups']['properties'].items()):
if attrib in ['kind', 'etag', 'email']: if attrib in ['kind', 'etag', 'email']:
continue continue
if myarg == attrib.lower(): if myarg == attrib.lower().replace('_', ''):
if params['type'] == 'integer': if params['type'] == 'integer':
try: try:
if value[-1:].upper() == 'M': if value[-1:].upper() == 'M':

View File

@@ -60,6 +60,10 @@ class GapiGroupNotFoundError(Exception):
pass pass
class GapiInternalServerError(Exception):
pass
class GapiInvalidError(Exception): class GapiInvalidError(Exception):
pass pass
@@ -125,6 +129,7 @@ class ErrorReason(Enum):
GATEWAY_TIMEOUT = 'gatewayTimeout' GATEWAY_TIMEOUT = 'gatewayTimeout'
GROUP_NOT_FOUND = 'groupNotFound' GROUP_NOT_FOUND = 'groupNotFound'
INTERNAL_ERROR = 'internalError' INTERNAL_ERROR = 'internalError'
INTERNAL_SERVER_ERROR = 'internalServerError'
INVALID = 'invalid' INVALID = 'invalid'
INVALID_ARGUMENT = 'invalidArgument' INVALID_ARGUMENT = 'invalidArgument'
INVALID_MEMBER = 'invalidMember' INVALID_MEMBER = 'invalidMember'
@@ -199,6 +204,8 @@ ERROR_REASON_TO_EXCEPTION = {
GapiGatewayTimeoutError, GapiGatewayTimeoutError,
ErrorReason.GROUP_NOT_FOUND: ErrorReason.GROUP_NOT_FOUND:
GapiGroupNotFoundError, GapiGroupNotFoundError,
ErrorReason.INTERNAL_SERVER_ERROR:
GapiInternalServerError,
ErrorReason.INVALID: ErrorReason.INVALID:
GapiInvalidError, GapiInvalidError,
ErrorReason.INVALID_ARGUMENT: 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: if 'Requested entity was not found' in message or 'does not exist' in message:
error = _create_http_error_dict(404, ErrorReason.NOT_FOUND.value, error = _create_http_error_dict(404, ErrorReason.NOT_FOUND.value,
message) 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: else:
if 'error_description' in error: if 'error_description' in error:
if error['error_description'] == 'Invalid Value': if error['error_description'] == 'Invalid Value':

View File

@@ -8,7 +8,7 @@ import platform
import re import re
GAM_AUTHOR = 'Jay Lee <jay0lee@gmail.com>' GAM_AUTHOR = 'Jay Lee <jay0lee@gmail.com>'
GAM_VERSION = '6.07' GAM_VERSION = '6.12'
GAM_LICENSE = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)' GAM_LICENSE = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
GAM_URL = 'https://git.io/gam' GAM_URL = 'https://git.io/gam'
@@ -124,7 +124,7 @@ SKUS = {
'Google-Apps': { 'Google-Apps': {
'product': 'Google-Apps', 'product': 'Google-Apps',
'aliases': ['standard', 'free'], 'aliases': ['standard', 'free'],
'displayName': 'G Suite Free/Standard' 'displayName': 'G Suite Legacy'
}, },
'Google-Apps-For-Business': { 'Google-Apps-For-Business': {
'product': 'Google-Apps', 'product': 'Google-Apps',
@@ -458,6 +458,7 @@ DRIVEFILE_FIELDS_CHOICES_MAP = {
'createddate': 'createdDate', 'createddate': 'createdDate',
'createdtime': 'createdDate', 'createdtime': 'createdDate',
'description': 'description', 'description': 'description',
'driveid': 'driveId',
'editable': 'editable', 'editable': 'editable',
'explicitlytrashed': 'explicitlyTrashed', 'explicitlytrashed': 'explicitlyTrashed',
'fileextension': 'fileExtension', 'fileextension': 'fileExtension',
@@ -500,6 +501,7 @@ DRIVEFILE_FIELDS_CHOICES_MAP = {
'sharedwithmedate': 'sharedWithMeDate', 'sharedwithmedate': 'sharedWithMeDate',
'sharedwithmetime': 'sharedWithMeDate', 'sharedwithmetime': 'sharedWithMeDate',
'sharinguser': 'sharingUser', 'sharinguser': 'sharingUser',
'shortcutdetails': 'shortcutDetails',
'spaces': 'spaces', 'spaces': 'spaces',
'thumbnaillink': 'thumbnailLink', 'thumbnaillink': 'thumbnailLink',
'title': 'title', 'title': 'title',
@@ -616,6 +618,7 @@ GOOGLEDOC_VALID_EXTENSIONS_MAP = {
} }
MACOS_CODENAMES = { MACOS_CODENAMES = {
10: {
6: 'Snow Leopard', 6: 'Snow Leopard',
7: 'Lion', 7: 'Lion',
8: 'Mountain Lion', 8: 'Mountain Lion',
@@ -625,8 +628,12 @@ MACOS_CODENAMES = {
12: 'Sierra', 12: 'Sierra',
13: 'High Sierra', 13: 'High Sierra',
14: 'Mojave', 14: 'Mojave',
15: 'Catalina' 15: 'Catalina',
} 16: 'Big Sur'
},
11: 'Big Sur',
12: 'Monterey',
}
_MICROSOFT_FORMATS_LIST = [{ _MICROSOFT_FORMATS_LIST = [{
'mime': 'mime':
@@ -891,7 +898,6 @@ RT_TAG_REPLACE_PATTERN = re.compile(r'{(.*?)}')
LOWERNUMERIC_CHARS = string.ascii_lowercase + string.digits LOWERNUMERIC_CHARS = string.ascii_lowercase + string.digits
ALPHANUMERIC_CHARS = LOWERNUMERIC_CHARS + string.ascii_uppercase ALPHANUMERIC_CHARS = LOWERNUMERIC_CHARS + string.ascii_uppercase
URL_SAFE_CHARS = ALPHANUMERIC_CHARS + '-._~' URL_SAFE_CHARS = ALPHANUMERIC_CHARS + '-._~'
FILENAME_SAFE_CHARS = ALPHANUMERIC_CHARS + '-_.() '
FILTER_ADD_LABEL_TO_ARGUMENT_MAP = { FILTER_ADD_LABEL_TO_ARGUMENT_MAP = {
'IMPORTANT': 'important', 'IMPORTANT': 'important',
@@ -1106,7 +1112,8 @@ GROUP_SETTINGS_LIST_ATTRIBUTES = set([
'whoCanUnmarkFavoriteReplyOnAnyTopic', 'whoCanUnmarkFavoriteReplyOnAnyTopic',
'whoCanViewGroup', 'whoCanViewGroup',
'whoCanViewMembership', 'whoCanViewMembership',
# Miscellaneous hoices # Miscellaneous choices
'default_sender',
'messageModerationLevel', 'messageModerationLevel',
'replyTo', 'replyTo',
'spamModerationLevel', 'spamModerationLevel',
@@ -1241,10 +1248,12 @@ GC_DOMAIN = 'domain'
GC_DRIVE_DIR = 'drive_dir' GC_DRIVE_DIR = 'drive_dir'
# Enable Delegated Admin Service Accounts # Enable Delegated Admin Service Accounts
GC_ENABLE_DASA = 'enabledasa' 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 # and doRequestOAuth prints a link and waits for the verification code when
# oauth2.txt is being created # oauth2.txt is being created
GC_NO_BROWSER = 'no_browser' 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. # oauth_browser forces usage of web server OAuth flow that proved problematic.
GC_OAUTH_BROWSER = 'oauth_browser' GC_OAUTH_BROWSER = 'oauth_browser'
# Disable GAM API caching # Disable GAM API caching
@@ -1299,6 +1308,7 @@ GC_Defaults = {
GC_DRIVE_DIR: '', GC_DRIVE_DIR: '',
GC_ENABLE_DASA: False, GC_ENABLE_DASA: False,
GC_NO_BROWSER: False, GC_NO_BROWSER: False,
GC_NO_TDEMAIL: False,
GC_NO_CACHE: False, GC_NO_CACHE: False,
GC_NO_SHORT_URLS: False, GC_NO_SHORT_URLS: False,
GC_NO_UPDATE_CHECK: False, GC_NO_UPDATE_CHECK: False,
@@ -1384,6 +1394,9 @@ GC_VAR_INFO = {
GC_NO_BROWSER: { GC_NO_BROWSER: {
GC_VAR_TYPE: GC_TYPE_BOOLEAN GC_VAR_TYPE: GC_TYPE_BOOLEAN
}, },
GC_NO_TDEMAIL: {
GC_VAR_TYPE: GC_TYPE_BOOLEAN
},
GC_NO_CACHE: { GC_NO_CACHE: {
GC_VAR_TYPE: GC_TYPE_BOOLEAN GC_VAR_TYPE: GC_TYPE_BOOLEAN
}, },

View File

@@ -1,12 +1,13 @@
yubikey-manager>=4.0.0
cryptography cryptography
distro; sys_platform == 'linux' distro; sys_platform == 'linux'
filelock filelock
google-api-python-client>=2.1 google-api-python-client>=2.1
google-auth-httplib2 google-auth-httplib2
google-auth-oauthlib>=0.4.1 google-auth-oauthlib>=0.4.1
google-auth>=1.11.2 google-auth>=2.3.2
httplib2>=0.17.0 httplib2>=0.17.0
importlib.metadata; python_version < '3.8' importlib.metadata; python_version < '3.8'
passlib>=1.7.2 passlib>=1.7.2
python-dateutil 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 #!/usr/bin/env python3
#from packaging import version from packaging import version
from distutils.version import LooseVersion
import sys import sys
a = sys.argv[1] a = sys.argv[1]
b = sys.argv[2] b = sys.argv[2]
#result = version.parse(a) >= version.parse(b) result = version.parse(a) >= version.parse(b)
result = LooseVersion(a) >= LooseVersion(b)
if result: if result:
print('OK: %s is equal or newer than %s' % (a, b)) print('OK: %s is equal or newer than %s' % (a, b))
else: else: