mirror of
https://github.com/GAM-team/GAM.git
synced 2026-06-04 22:31:38 +00:00
Compare commits
163 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1e6459dc1 | ||
|
|
31a3dcd2f7 | ||
|
|
f0120fef63 | ||
|
|
5095e6af14 | ||
|
|
19f21a9453 | ||
|
|
676908daca | ||
|
|
66a5d0472d | ||
|
|
cc30e307e9 | ||
|
|
57e3eb5c8e | ||
|
|
b855f6876c | ||
|
|
8edc06ba41 | ||
|
|
69aa31566b | ||
|
|
19d3483209 | ||
|
|
c1c7e65a3c | ||
|
|
2d0044de95 | ||
|
|
418e3af903 | ||
|
|
c3ddeae3f3 | ||
|
|
a9f0e5ba16 | ||
|
|
8bf8d45ebe | ||
|
|
1777c762b3 | ||
|
|
0b1337070e | ||
|
|
b158496bea | ||
|
|
a79b23e090 | ||
|
|
bdb56240f0 | ||
|
|
6dddf3eb30 | ||
|
|
7bd8569151 | ||
|
|
b03c9f1e35 | ||
|
|
057b5ff760 | ||
|
|
ba512b4159 | ||
|
|
a298aea2fe | ||
|
|
f433463074 | ||
|
|
afae08d6fe | ||
|
|
7cf2a08aff | ||
|
|
7df6781985 | ||
|
|
ae0f5e62e3 | ||
|
|
14c8356c6b | ||
|
|
45ffd4a793 | ||
|
|
eb8d39025e | ||
|
|
1f739e1c63 | ||
|
|
82111236fb | ||
|
|
813a94f8d6 | ||
|
|
e83b75e2c3 | ||
|
|
ce1e880ed0 | ||
|
|
427672065e | ||
|
|
055c5d5e54 | ||
|
|
4de7794e04 | ||
|
|
79686fd8ce | ||
|
|
cc5df0198b | ||
|
|
abc6e55ba7 | ||
|
|
0c8afb7fd6 | ||
|
|
c0c2cca44e | ||
|
|
faa645cb97 | ||
|
|
725c19aafc | ||
|
|
cc3b4c974d | ||
|
|
6ce64fad72 | ||
|
|
c1af67d4a3 | ||
|
|
802cb15007 | ||
|
|
b34bf3e56a | ||
|
|
bf37700088 | ||
|
|
4a43ddfc25 | ||
|
|
650a1f5154 | ||
|
|
5eda7e30b0 | ||
|
|
8a26f547e5 | ||
|
|
343088913f | ||
|
|
5a0272fd5b | ||
|
|
dc93503625 | ||
|
|
6ea6c0889b | ||
|
|
99ab72df3f | ||
|
|
99bda1385e | ||
|
|
7ce3b4a8c0 | ||
|
|
495722d0d6 | ||
|
|
aca31be5d7 | ||
|
|
b9b7ae8d99 | ||
|
|
0d46c1d13a | ||
|
|
6b63ecdc19 | ||
|
|
f9ca0323a1 | ||
|
|
c50aa4d2e8 | ||
|
|
a72ded9079 | ||
|
|
cbabbee075 | ||
|
|
f55a344b7a | ||
|
|
d84f8418ff | ||
|
|
30c5e92de6 | ||
|
|
5f618a7f65 | ||
|
|
3e833419db | ||
|
|
0d94bae0b5 | ||
|
|
f5dec96ffb | ||
|
|
e91d12caaf | ||
|
|
fd5a1faa58 | ||
|
|
90a9212793 | ||
|
|
7e582ac1fc | ||
|
|
65a740569c | ||
|
|
a47ef0e1f5 | ||
|
|
b75ad006f1 | ||
|
|
dbc3f0cd83 | ||
|
|
ea2750f970 | ||
|
|
a2eb5a2483 | ||
|
|
54178543d6 | ||
|
|
5436f21bc0 | ||
|
|
839768a2a5 | ||
|
|
2e195d5aa1 | ||
|
|
66811f8eb5 | ||
|
|
a92326790d | ||
|
|
d405767fb0 | ||
|
|
8d7c6d3835 | ||
|
|
e362591b7a | ||
|
|
ee5f4b73e8 | ||
|
|
0d15eb2898 | ||
|
|
4af50206ad | ||
|
|
c596937006 | ||
|
|
17eb61e1eb | ||
|
|
a333185e84 | ||
|
|
f6863ae2d6 | ||
|
|
36830250b5 | ||
|
|
4ca1c3537b | ||
|
|
eeab09eacb | ||
|
|
af16967257 | ||
|
|
75e2bf5a9a | ||
|
|
4db3bc409b | ||
|
|
32ccf414ea | ||
|
|
615e48fffc | ||
|
|
93bf3fce29 | ||
|
|
899601569a | ||
|
|
b1805b64a2 | ||
|
|
58190343b1 | ||
|
|
99d48b1939 | ||
|
|
82b66d53cb | ||
|
|
3200de56cc | ||
|
|
0a627d5c79 | ||
|
|
22399deb79 | ||
|
|
6a77617e3b | ||
|
|
2868ef99ae | ||
|
|
21557f9892 | ||
|
|
d2385ae62d | ||
|
|
a84efef389 | ||
|
|
310bcd1585 | ||
|
|
753f44deb2 | ||
|
|
df1f0f8f09 | ||
|
|
45e1b50674 | ||
|
|
0a2b048fb1 | ||
|
|
e3c5dca09d | ||
|
|
88339b7214 | ||
|
|
1f2bb18bc1 | ||
|
|
74977a6154 | ||
|
|
00413fe7a4 | ||
|
|
9bb9d331ad | ||
|
|
f022ffdff4 | ||
|
|
28dade2a34 | ||
|
|
7378b9d843 | ||
|
|
71075e95bf | ||
|
|
108990cf06 | ||
|
|
ebfdf4b052 | ||
|
|
dbf4073216 | ||
|
|
83214eaaf8 | ||
|
|
1100fdd456 | ||
|
|
481bfa5440 | ||
|
|
30282c7fbb | ||
|
|
382bc71b21 | ||
|
|
f3fba97652 | ||
|
|
7f51e35bd4 | ||
|
|
95beb8e62a | ||
|
|
1a9de867f9 | ||
|
|
b42946bbe1 | ||
|
|
40b2fd09ff |
20
.github/actions/linux-before-install.sh
vendored
20
.github/actions/linux-before-install.sh
vendored
@@ -55,7 +55,7 @@ else
|
||||
tar xf openssl-$BUILD_OPENSSL_VERSION.tar.gz
|
||||
cd openssl-$BUILD_OPENSSL_VERSION
|
||||
echo "Compiling OpenSSL $BUILD_OPENSSL_VERSION..."
|
||||
./config shared --prefix=$HOME/ssl
|
||||
./Configure --libdir=lib --prefix=$HOME/ssl
|
||||
echo "Running make for OpenSSL..."
|
||||
make -j$cpucount -s
|
||||
echo "Running make install for OpenSSL..."
|
||||
@@ -70,7 +70,7 @@ else
|
||||
cd Python-$BUILD_PYTHON_VERSION
|
||||
echo "Compiling Python $BUILD_PYTHON_VERSION..."
|
||||
safe_flags="--with-openssl=$HOME/ssl --enable-shared --prefix=$HOME/python --with-ensurepip=upgrade"
|
||||
unsafe_flags="--enable-optimizations --with-lto"
|
||||
unsafe_flags="--enable-optimizations --with-lto --with-openssl=~/ssl --with-openssl-rpath=~~/ssl/lib"
|
||||
if [ ! -e Makefile ]; then
|
||||
echo "running configure with safe and unsafe"
|
||||
./configure $safe_flags $unsafe_flags > /dev/null
|
||||
@@ -94,19 +94,9 @@ else
|
||||
python=~/python/bin/python3
|
||||
pip=~/python/bin/pip3
|
||||
|
||||
if ([ "${ImageOS}" == "ubuntu16" ]) && [ "${HOSTTYPE}" == "x86_64" ]; then
|
||||
echo "Installing deps for StaticX..."
|
||||
if [ ! -d patchelf-$PATCHELF_VERSION ]; then
|
||||
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
|
||||
if ([ "${ImageOS}" == "ubuntu20" ]) && [ "${HOSTTYPE}" == "x86_64" ]; then
|
||||
"${python}" -m pip install --upgrade patchelf-wrapper
|
||||
"${python}" -m pip install --upgrade staticx
|
||||
fi
|
||||
|
||||
cd $whereibelong
|
||||
|
||||
6
.github/actions/linux-install.sh
vendored
6
.github/actions/linux-install.sh
vendored
@@ -17,10 +17,10 @@ tar -C ${distpath} --create --file $GAM_ARCHIVE --xz gam
|
||||
echo "PyInstaller GAM info:"
|
||||
du -h $gam
|
||||
time $gam version extended
|
||||
if ([ "${ImageOS}" == "ubuntu16" ]) && [ "${HOSTTYPE}" == "x86_64" ]; then
|
||||
if ([ "${ImageOS}" == "ubuntu20" ]) && [ "${HOSTTYPE}" == "x86_64" ]; then
|
||||
GAM_LEGACY_ARCHIVE=gam-${GAMVERSION}-${GAMOS}-${PLATFORM}-legacy.tar.xz
|
||||
$python -OO -m staticx -l /lib/x86_64-linux-gnu/libresolv.so.2 -l /lib/x86_64-linux-gnu/libnss_dns.so.2 $gam $gam-staticx
|
||||
strip $gam-staticx
|
||||
$python -OO -m staticx $gam $gam-staticx
|
||||
#strip $gam-staticx
|
||||
rm $gampath/gam
|
||||
mv $gam-staticx $gam
|
||||
chmod 755 $gam
|
||||
|
||||
6
.github/actions/macos-before-install.sh
vendored
6
.github/actions/macos-before-install.sh
vendored
@@ -29,7 +29,7 @@ echo "installing Python $BUILD_PYTHON_VERSION..."
|
||||
sudo installer -pkg ./$pyfile -target /
|
||||
|
||||
# This fixes https://github.com/pyinstaller/pyinstaller/issues/5062
|
||||
codesign --remove-signature /Library/Frameworks/Python.framework/Versions/3.9/Python
|
||||
#codesign --remove-signature /Library/Frameworks/Python.framework/Versions/3.10/Python
|
||||
|
||||
#if [ ! -f python-$MIN_PYTHON_VERSION-macosx10.9.pkg ]; then
|
||||
# wget --quiet https://www.python.org/ftp/python/$MIN_PYTHON_VERSION/python-$MIN_PYTHON_VERSION-macosx10.9.pkg
|
||||
@@ -54,8 +54,8 @@ cd ~
|
||||
|
||||
export python=/usr/local/bin/python3
|
||||
export pip=/usr/local/bin/pip3
|
||||
SSLVER=$($openssl version)
|
||||
SSLRESULT=$?
|
||||
#SSLVER=$($openssl version)
|
||||
#SSLRESULT=$?
|
||||
PYVER=$($python -V)
|
||||
PYRESULT=$?
|
||||
|
||||
|
||||
2
.github/actions/macos-install.sh
vendored
2
.github/actions/macos-install.sh
vendored
@@ -7,7 +7,7 @@ export distpath="dist/"
|
||||
export gampath="${distpath}gam"
|
||||
rm -rf $gampath
|
||||
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"
|
||||
$gam version extended
|
||||
export GAMVERSION=`$gam version simple`
|
||||
|
||||
178
.github/workflows/build.yml
vendored
178
.github/workflows/build.yml
vendored
@@ -12,13 +12,11 @@ defaults:
|
||||
working-directory: src
|
||||
|
||||
env:
|
||||
BUILD_PYTHON_VERSION: "3.9.6"
|
||||
MIN_PYTHON_VERSION: "3.9.6"
|
||||
BUILD_OPENSSL_VERSION: "1.1.1k"
|
||||
MIN_OPENSSL_VERSION: "1.1.1k"
|
||||
PATCHELF_VERSION: "0.12"
|
||||
# PYINSTALLER_VERSION can be full commit hash or version like v4.20
|
||||
PYINSTALLER_VERSION: "0f2b2e921433ab5a510c7efdb21d9c1d7cfbc645"
|
||||
BUILD_PYTHON_VERSION: "3.10.1"
|
||||
MIN_PYTHON_VERSION: "3.10.1"
|
||||
BUILD_OPENSSL_VERSION: "3.0.1"
|
||||
MIN_OPENSSL_VERSION: "1.1.1l"
|
||||
PATCHELF_VERSION: "0.13"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -26,62 +24,61 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-16.04
|
||||
- os: ubuntu-18.04
|
||||
jid: 1
|
||||
goal: "build"
|
||||
gamos: "linux"
|
||||
platform: "x86_64"
|
||||
- os: ubuntu-18.04
|
||||
- os: ubuntu-20.04
|
||||
jid: 2
|
||||
goal: "build"
|
||||
gamos: "linux"
|
||||
platform: "x86_64"
|
||||
- os: ubuntu-20.04
|
||||
jid: 3
|
||||
goal: "build"
|
||||
gamos: "linux"
|
||||
platform: "x86_64"
|
||||
- os: macos-11.0
|
||||
jid: 12
|
||||
jid: 3
|
||||
goal: "build"
|
||||
gamos: "macos"
|
||||
platform: "universal2"
|
||||
- os: windows-2019
|
||||
jid: 5
|
||||
- os: windows-2022
|
||||
jid: 4
|
||||
goal: "build"
|
||||
gamos: "windows"
|
||||
pyarch: "x64"
|
||||
platform: "x86_64"
|
||||
- os: windows-2019
|
||||
jid: 6
|
||||
- os: windows-2022
|
||||
jid: 5
|
||||
goal: "build"
|
||||
gamos: "windows"
|
||||
platform: "x86"
|
||||
pyarch: "x86"
|
||||
- os: ubuntu-20.04
|
||||
goal: "test"
|
||||
python: "3.6"
|
||||
jid: 7
|
||||
gamos: "linux"
|
||||
platform: "x86_64"
|
||||
- os: ubuntu-20.04
|
||||
goal: "test"
|
||||
python: "3.7"
|
||||
jid: 8
|
||||
jid: 6
|
||||
gamos: "linux"
|
||||
platform: "x86_64"
|
||||
- os: ubuntu-20.04
|
||||
goal: "test"
|
||||
python: "3.8"
|
||||
jid: 9
|
||||
jid: 7
|
||||
gamos: "linux"
|
||||
platform: "x86_64"
|
||||
- os: ubuntu-20.04
|
||||
goal: test
|
||||
python: "3.10.0-beta.1"
|
||||
jid: 10
|
||||
python: "3.9"
|
||||
jid: 8
|
||||
gamos: linux
|
||||
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:
|
||||
|
||||
@@ -97,7 +94,7 @@ jobs:
|
||||
path: |
|
||||
~/python
|
||||
~/ssl
|
||||
key: ${{ matrix.os }}-${{ matrix.jid }}-20210628
|
||||
key: ${{ matrix.os }}-${{ matrix.jid }}-20220111-03
|
||||
|
||||
- name: Set env variables
|
||||
env:
|
||||
@@ -120,7 +117,7 @@ jobs:
|
||||
architecture: ${{ matrix.pyarch }}
|
||||
|
||||
- name: Install Python on Windows
|
||||
if: matrix.os == 'windows-2019'
|
||||
if: matrix.os == 'windows-2022'
|
||||
run: |
|
||||
if ( ${Env:PLATFORM} -eq "x86_64" )
|
||||
{
|
||||
@@ -139,8 +136,15 @@ jobs:
|
||||
Start-Process -wait -FilePath $python_file -ArgumentList "/quiet","InstallAllUsers=0","TargetDir=c:\\python","AssociateFiles=1","PrependPath=1"
|
||||
shell: pwsh
|
||||
|
||||
- name: Set env variables for pre-compiled Python
|
||||
- name: Install packages for 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: |
|
||||
export python=$(which python3)
|
||||
export pip=$(which pip3)
|
||||
@@ -151,49 +155,84 @@ jobs:
|
||||
echo "pip=${pip}" >> $GITHUB_ENV
|
||||
echo "gam=${gam}" >> $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
|
||||
"${python}" -V
|
||||
"${pip}" -V
|
||||
|
||||
- name: Build and install Python, OpenSSL and PyInstaller
|
||||
if: matrix.goal != 'test' && steps.cache-primes.outputs.cache-hit != 'true'
|
||||
- name: Build and install Python and OpenSSL
|
||||
if: matrix.goal == 'build' && steps.cache-primes.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
set +e
|
||||
source ../.github/actions/${GAMOS}-before-install.sh
|
||||
echo "PATH=$PATH" >> $GITHUB_ENV # keep gnutools for MacOS
|
||||
echo "python=$python" >> $GITHUB_ENV
|
||||
echo "pip=$pip" >> $GITHUB_ENV
|
||||
echo "LD_LIBRARY_PATH=$LD_LIBRARY_PATH" >> $GITHUB_ENV
|
||||
echo -e "Python: $python\nPip: $pip\nLD_LIB...: $LD_LIBRARY_PATH"
|
||||
$pip install wheel
|
||||
export url="https://codeload.github.com/pyinstaller/pyinstaller/tar.gz/${PYINSTALLER_VERSION}"
|
||||
echo "Downloading ${url}"
|
||||
curl -o pyinstaller.tar.gz --compressed "${url}"
|
||||
tar xf pyinstaller.tar.gz
|
||||
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
|
||||
$python ./waf all $TARGETARCH
|
||||
cd ..
|
||||
if [ $GAMOS == "macos" ]; then
|
||||
brew install openssl@1.1 rust
|
||||
export LDFLAGS="-L$(brew --prefix openssl@1.1)/lib"
|
||||
export pipoptions='--no-binary :all:'
|
||||
echo "PATH=$PATH" >> $GITHUB_ENV # keep gnutools for MacOS
|
||||
export MACOSX_DEPLOYMENT_TARGET="10.9"
|
||||
export CFLAGS="-arch arm64 -arch x86_64 -I$(brew --prefix openssl@1.1)/include"
|
||||
echo "pipoptions=${pipoptions}" >> $GITHUB_ENV
|
||||
echo "MACOSX_DEPLOYMENT_TARGET=${MACOSX_DEPLOYMENT_TARGET}" >> $GITHUB_ENV
|
||||
echo "CFLAGS=${CFLAGS}" >> $GITHUB_ENV
|
||||
echo "LDFLAGS=${LDFLAGS}" >> $GITHUB_ENV
|
||||
fi
|
||||
$python setup.py install
|
||||
#$pip install pyinstaller
|
||||
echo -e "Python: $python\nPip: $pip\nLD_LIB...: $LD_LIBRARY_PATH"
|
||||
$pip install --upgrade pip $pipoptions
|
||||
$pip install --upgrade wheel $pipoptions
|
||||
|
||||
- name: Install pip requirements
|
||||
if: matrix.os != 'self-hosted'
|
||||
- name: Set Windows Powershell env variables
|
||||
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: |
|
||||
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
|
||||
# use latest pyinstaller tag version
|
||||
git fetch --tags
|
||||
git checkout $(git describe --tags $(git rev-list --tags --max-count=1))
|
||||
# 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="32bit"
|
||||
else
|
||||
TARGETARCH="64bit"
|
||||
fi
|
||||
if [ $GAMOS == "macos" ]; then
|
||||
UNIVERsAL="--universal2"
|
||||
fi
|
||||
$python ./waf all --target-arch=$TARGETARCH $UNIVERSAL
|
||||
cat build/config.log
|
||||
cd ..
|
||||
$pip install . $pipoptions
|
||||
|
||||
$pip install --upgrade -r requirements.txt
|
||||
- name: Install pip requirements
|
||||
run: |
|
||||
set +e
|
||||
$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
|
||||
# yubikey-manager holds cryptography to old version, force upgrade
|
||||
$pip install --upgrade --no-deps cryptography $pipoptions
|
||||
|
||||
- name: Build GAM with PyInstaller
|
||||
if: matrix.goal != 'test'
|
||||
@@ -218,6 +257,7 @@ jobs:
|
||||
- name: Basic Tests build jobs only
|
||||
if: matrix.goal != 'test'
|
||||
run: |
|
||||
$pip install packaging
|
||||
export vline=$($gam version | grep "Python ")
|
||||
export python_line=($vline)
|
||||
export this_python=${python_line[1]}
|
||||
@@ -230,7 +270,7 @@ jobs:
|
||||
|
||||
- name: Live API tests push only
|
||||
if: github.event_name == 'push' || github.event_name == 'schedule'
|
||||
env: # Or as an environment variable
|
||||
env:
|
||||
PASSCODE: ${{ secrets.PASSCODE }}
|
||||
run: |
|
||||
source ../.github/actions/decrypt.sh ../.github/actions/creds.tar.gpg creds.tar
|
||||
@@ -255,17 +295,19 @@ jobs:
|
||||
for i in {01..10}; do
|
||||
echo "${newbase}-bulkuser-$i" >> sample.csv;
|
||||
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 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 user $newuser add license gsuitebusiness
|
||||
$gam update cigroup $newgroup memberrestriction 'member.type == 1 || member.customer_id == groupCustomerId()'
|
||||
$gam info cigroup $newgroup
|
||||
$gam user $newuser add license workspaceenterpriseplus
|
||||
$gam update group $newgroup add owner $gam_user
|
||||
$gam update group $newgroup add member $newuser
|
||||
$gam csv sample.csv gam create user ~~email~~ firstname "GHA Bulk" lastname ~~email~~ gha.jid $JID
|
||||
$gam csv sample.csv gam update user ~~email~~ recoveryphone 12125121110 recoveryemail jay0lee@gmail.com password random
|
||||
$gam csv sample.csv gam update user ~~email~~ recoveryphone "" recoveryemail ""
|
||||
$gam csv sample.csv gam user ~email add license gsuitebusiness
|
||||
$gam csv sample.csv gam user ~email add license workspaceenterpriseplus
|
||||
$gam csv sample.csv gam user $gam_user sendemail recipient ~~email~~@pdl.jaylee.us subject "test message $newbase" message "GHA test message"
|
||||
$gam csv sample.csv gam update group $newgroup add member ~email
|
||||
$gam info group $newgroup
|
||||
@@ -320,6 +362,7 @@ jobs:
|
||||
$gam delete building $newbuilding
|
||||
$gam delete group $newgroup
|
||||
$gam create alias $newalias user $newuser
|
||||
$gam user $newuser delete license workspaceenterpriseplus
|
||||
$gam whatis $newuser
|
||||
$gam user $gam_user show tokens
|
||||
$gam print exports matter $matterid | $gam csv - gam download export $matterid id:~~id~~
|
||||
@@ -333,7 +376,8 @@ jobs:
|
||||
$gam print browsers
|
||||
export sn="$JID$JID$JID$JID-$(openssl rand -base64 32 | sed 's/[^a-zA-Z0-9]//g')"
|
||||
$gam create device serialnumber $sn devicetype android
|
||||
$gam print cros allfields nolists
|
||||
$gam print cros allfields orderby serialnumber
|
||||
#$gam show crostelemetry storagepercentonly
|
||||
$gam report usageparameters customer
|
||||
$gam report usage customer parameters gmail:num_emails_sent,accounts:num_1day_logins
|
||||
$gam report customer todrive
|
||||
|
||||
21
README.md
21
README.md
@@ -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.
|
||||
|
||||

|
||||
|
||||
# Quick Start
|
||||
|
||||
## Linux / MacOS
|
||||
|
||||
Open a terminal and run:
|
||||
```
|
||||
|
||||
```sh
|
||||
bash <(curl -s -S -L https://git.io/install-gam)
|
||||
```
|
||||
|
||||
this will download GAM, install it and start setup.
|
||||
|
||||
## Windows
|
||||
|
||||
Download the MSI Installer from the [GitHub Releases] page. Install the MSI and you'll be prompted to setup GAM.
|
||||
|
||||
# Documentation
|
||||
|
||||
The GAM documentation is hosted in the [GitHub Wiki]
|
||||
|
||||
# Mailing List / Discussion group
|
||||
|
||||
The GAM mailing list / discussion group is hosted on [Google Groups]. You can join the list and interact via email, or just post from the web itself.
|
||||
|
||||
# Chat Room
|
||||
|
||||
There is a public chat room hosted in Google Chat. [Instructions to join](https://git.io/gam-chat).
|
||||
|
||||
# Author
|
||||
GAM is maintained by <a href="mailto:jay0lee@gmail.com">Jay Lee</a>. Please direct "how do I?" questions to [Google Groups].
|
||||
|
||||
GAM is maintained by [Jay Lee](mailto:jay0lee@gmail.com). Please direct "how do I?" questions to [Google Groups].
|
||||
|
||||
[GAM release]: https://git.io/gamreleases
|
||||
[GitHub Releases]: https://github.com/jay0lee/GAM/releases
|
||||
|
||||
@@ -207,8 +207,9 @@ If an item contains spaces, it should be surrounded by ".
|
||||
<Namespace> ::= <String>
|
||||
<NotificationID> ::= <String>
|
||||
<NumberOfSeats> ::= <Number>
|
||||
<OrgUnitID> ::= <String>
|
||||
<OrgUnitID> ::= id:<String>
|
||||
<OrgUnitPath> ::= /|(/<String)+
|
||||
<OrgUnitItem> ::= <OrgUnitID>|<OrgUnitPath>
|
||||
<ParameterKey> ::= <String>
|
||||
<ParameterValue> ::= <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
|
||||
<QueryCrOS> ::= <String> See: https://support.google.com/chrome/a/answer/1698333?hl=en
|
||||
<QueryDriveFile> ::= <String> See: https://developers.google.com/drive/v2/web/search-parameters
|
||||
<QueryDynamicGroup> ::= <String> See: https://cloud.google.com/identity/docs/reference/rest/v1beta1/groups#dynamicgroupquery
|
||||
<QueryGmail> ::= <String> See: https://support.google.com/mail/answer/7190
|
||||
<QueryGroup> ::= <String> See: https://developers.google.com/admin-sdk/directory/v1/guides/search-groups
|
||||
<QueryMemberRestrictions> ::= <String> See: https://cloud.google.com/identity/docs/reference/rest/v1beta1/SecuritySettings#MemberRestriction
|
||||
<QueryMobile> ::= <String> See: https://support.google.com/a/answer/7549103
|
||||
<QueryTeamDrive> ::= <String> See: https://developers.google.com/drive/api/v3/search-shareddrives
|
||||
<QueryUser> ::= <String> See: https://developers.google.com/admin-sdk/directory/v1/guides/search-users
|
||||
@@ -313,6 +316,25 @@ If an item contains spaces, it should be surrounded by ".
|
||||
<CrOSOrderByFieldName> ::=
|
||||
lastsync|location|notes|serialnumber|status|supportenddate|user
|
||||
|
||||
<CrOSTelemetryFieldName> ::=
|
||||
batteryinfo|
|
||||
batterystatusreport|
|
||||
cpuinfo|
|
||||
cpustatusreport|
|
||||
customer|
|
||||
deviceid|
|
||||
graphicsinfo|
|
||||
graphicsstatusreport|
|
||||
memoryinfo|
|
||||
memorystatusreport|
|
||||
name|
|
||||
networkstatusreport|
|
||||
orgunitid|
|
||||
osupdatestatus|
|
||||
serialnumber|
|
||||
storageinfo|
|
||||
storagestatusreport
|
||||
|
||||
<DriveFieldName> ::=
|
||||
appdatacontents|
|
||||
cancomment|
|
||||
@@ -324,6 +346,7 @@ If an item contains spaces, it should be surrounded by ".
|
||||
description|
|
||||
editable|
|
||||
explicitlytrashed|
|
||||
driveid|
|
||||
fileextension|
|
||||
filesize|
|
||||
foldercolorrgb|
|
||||
@@ -353,6 +376,7 @@ If an item contains spaces, it should be surrounded by ".
|
||||
shared|
|
||||
sharedwithmedate|sharedwithmetime|
|
||||
sharinguser|
|
||||
shortcutdetails|
|
||||
size|
|
||||
spaces|
|
||||
starred|
|
||||
@@ -578,6 +602,7 @@ Items, separated by spaces, with spaces, commas or single quotes in the items th
|
||||
<CourseStateList> ::= "<CourseState>(,<CourseState>)*"
|
||||
<CrOSFieldNameList> ::= "<CrOSFieldName>(,<CrOSFieldName>)*"
|
||||
<CrOSIDList> ::= "<CrOSID>(,<CrOSID>)*"
|
||||
<CrOSTelemetryFieldNameList> ::= "<CrOSFieldName>(,<CrOSFieldName>)*"
|
||||
<DriveFileList> ::= "<DriveFileItem>(,<DriveFileItem>)*"
|
||||
<EmailAddressList> ::= "<EmailAddress>(,<EmailAddress>)*"
|
||||
<EmailItemList> ::= "<EmailItem>(,<EmailItem>)*"
|
||||
@@ -591,7 +616,7 @@ Items, separated by spaces, with spaces, commas or single quotes in the items th
|
||||
<GroupRoleList> ::= "<GroupRole>(,<GroupRole>)*"
|
||||
<GuardianStateList> ::= "<GuardianState>(,<GuardianState>)*"
|
||||
<LabelNameList> ::= "<LabelName>(,<LabelName)*"
|
||||
<LanguageList> ::= "<Language>(,<Language)*"
|
||||
<LanguageList> ::= "<Language>[+|-](,<Language>[+|-])*"
|
||||
<MatterItemList> ::= "<MatterItem>(,<MatterItem>)*"
|
||||
<MembersFieldNameList> ::= "<MembersFieldName>(,<MembersFieldName>)*"
|
||||
<MobileList> ::= "<MobileId>(,<MobileId>)*"
|
||||
@@ -698,9 +723,10 @@ Specify a collection of Users by directly specifying them or by specifiying item
|
||||
(contentrestrictions readonly true [reason <String>])|
|
||||
copyrequireswriterpermission|
|
||||
(lastviewedbyme <Time>)|(modifieddate|modifiedtime <Time>)|(description <String>)|(mimetype <MimeType>)|
|
||||
(parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|writerscantshare|writerscanshare
|
||||
(parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|
|
||||
(securityupdate <Boolean>)|
|
||||
(shortcut <DriveFileID>)
|
||||
(shortcut <DriveFileID>)|
|
||||
writerscantshare|writerscanshare
|
||||
<DriveFileUpdateAttribute> ::=
|
||||
(localfile <FileName>|-)|
|
||||
(convert)|(ocr)|(ocrlanguage <Language>)|
|
||||
@@ -709,9 +735,10 @@ Specify a collection of Users by directly specifying them or by specifiying item
|
||||
(contentrestrictions readonly true [reason <String>])|
|
||||
(copyrequireswriterpermission <Boolean>)|
|
||||
(lastviewedbyme <Time>)|(modifieddate <Time>)|(description <String>)|(mimetype <MimeType>)|
|
||||
(parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|writerscantshare|writerscanshare
|
||||
(parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|
|
||||
(securityupdate <Boolean>)|
|
||||
(shortcut <DriveFileID>)
|
||||
(shortcut <DriveFileID>)|
|
||||
writerscantshare|writerscanshare
|
||||
<GroupSettingsAttribute> ::=
|
||||
(allowexternalmembers <Boolean>)|
|
||||
(allowwebposting <Boolean>)|
|
||||
@@ -719,6 +746,7 @@ Specify a collection of Users by directly specifying them or by specifiying item
|
||||
(customfootertext <String>)|
|
||||
(customreplyto <EmailAddress>)|
|
||||
(defaultmessagedenynotificationtext <String>)|
|
||||
(defaultsender default_self|group)|
|
||||
(description <String>)|
|
||||
(enablecollaborativeinbox|collaborative <Boolean>)|
|
||||
(includeinglobaladdresslist|gal <Boolean>)|
|
||||
@@ -796,7 +824,6 @@ Specify a collection of Users by directly specifying them or by specifiying item
|
||||
field <FieldName> (type bool|date|double|email|int64|phone|string) [multivalued|multivalue] [indexed] [restricted] [range <Number> <Number>] endfield
|
||||
|
||||
<UserBasicAttribute> ::=
|
||||
(agreed2terms|agreedtoterms <Boolean>)|
|
||||
(changepassword|changepasswordatnextlogin <Boolean>)|
|
||||
(base64-md5|base64-sha1|crypt|sha|sha1|sha-1|md5|nohash)|
|
||||
(customerid <String>)|
|
||||
@@ -1012,7 +1039,8 @@ gam info customer
|
||||
|
||||
gam create datatransfer|transfer <OldOwnerID> <DataTransferServiceList> <NewOwnerID> (<ParameterKey> <ParameterValue>)*
|
||||
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
|
||||
|
||||
@@ -1257,6 +1285,18 @@ The listlimit <Number> argument limits the number of recent users, time ranges a
|
||||
The start <Date> and end <Date> arguments filter the time ranges.
|
||||
Delimiter defaults to comma.
|
||||
|
||||
gam info crostelemetry <SerialNumber>
|
||||
<CrOSTelemetryFieldName>* [fields <CrOSTelemetryFieldNameList>]
|
||||
[storagepercentonly] [showorgunitpath]
|
||||
gam show crostelemetry
|
||||
[(ou|org|orgunit <OrgUnitItem>)|(cros_sn <SerialNumber>)|(filter <String>)]
|
||||
<CrOSTelemetryFieldName>* [fields <CrOSTelemetryFieldNameList>]
|
||||
[storagepercentonly] [showorgunitpath]
|
||||
gam print crostelemetry [todrive]
|
||||
[(ou|org|orgunit <OrgUnitItem>)|(cros_sn <SerialNumber>)|(filter <String>)]
|
||||
<CrOSTelemetryFieldName>* [fields <CrOSTelemetryFieldNameList>]
|
||||
[storagepercentonly] [showorgunitpath]
|
||||
|
||||
gam print chromeapps [todrive]
|
||||
[ou|org|orgunit <OrgUnitItem>]
|
||||
[filter <String>]
|
||||
@@ -1381,18 +1421,21 @@ gam print printermodels [todrive] [filter <String>]
|
||||
|
||||
gam create cigroup <EmailAddress> <CIGroupAttribute>*
|
||||
[makeowner] [alias|aliases <AliasList>] [dynamic <QueryDynamicGroup>]
|
||||
gam update cigroup <GroupItem> [email <EmailAddress>] <CIGroupAttribute>* [security]
|
||||
gam update cigroup <GroupItem> [email <EmailAddress>] <CIGroupAttribute>*
|
||||
[security] [dynamic <QueryDynamicGroup>]
|
||||
[memberrestrictions <QueryMemberRestrictions>]
|
||||
gam update cigroup <GroupItem> add [owner|manager|member] [notsuspended|suspended] [expires never|<Time>] <UserTypeEntity>
|
||||
gam update cigroup <GroupItem> delete|remove [owner|manager|member] [notsuspended|suspended] <UserTypeEntity>
|
||||
gam update cigroup <GroupItem> sync [owner|manager|member] [notsuspended|suspended] [expires never|<Time>] <UserTypeEntity>
|
||||
gam update cigroup <GroupItem> update [owner|manager|member] [notsuspended|suspended] [expires never|<Time>] <UserTypeEntity>
|
||||
gam update cigroup <GroupItem> clear [member] [manager] [owner] [notsuspended|suspended]
|
||||
gam delete cigroup <GroupItem>
|
||||
gam info cigroup <GroupItem> [nousers] [nojoindate] [showupdatedate] [membertree]
|
||||
gam info cigroup <GroupItem> [nousers] [nojoindate] [showupdatedate] [membertree] [nosecurity|nosecuritysettings]
|
||||
|
||||
gam print cigroups [todrive]
|
||||
[enterprisemember <UserItem>]
|
||||
[members|memberscount] [managers|managerscount] [owners|ownerscount]
|
||||
[memberrestrictions]
|
||||
[delimiter <Character>] [sortheaders]
|
||||
|
||||
gam info cimember <UserItem> <GroupItem>
|
||||
@@ -1461,7 +1504,11 @@ gam create user <EmailAddress> <UserAttribute>* [verifynotinvitable]
|
||||
gam update user <UserItem> <UserAttribute>* [clearschema <SchemaName>] [clearschema <SchemaName>.<FieldName>] [verifynotinvitable]
|
||||
gam delete user <UserItem>
|
||||
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;
|
||||
if none of these options are specified, all users are printed.
|
||||
@@ -1469,10 +1516,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.
|
||||
|
||||
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]
|
||||
[orderby <UserOrderByFieldName> [ascending|descending]] [userview]
|
||||
[allfields|basic|full | ((<UserFieldName>* | fields <UserFieldNameList>) [schemas|custom all|<SchemaNameList>])]
|
||||
[orderby <UserOrderByFieldName> [ascending|descending]]
|
||||
[userview] [allfields|basic|full | (<UserFieldName>* | fields <UserFieldNameList>)]
|
||||
[schemas|custom all|<SchemaNameList>])]
|
||||
[delimiter <Character>] [sortheaders]
|
||||
|
||||
gam create verify|verification <DomainName>
|
||||
|
||||
@@ -368,7 +368,7 @@
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
},
|
||||
"path": "{customer}/chrome/enrollmentTokens",
|
||||
"request": {
|
||||
"$ref": "CreateEnrollmentTokenRequest"
|
||||
@@ -379,7 +379,7 @@
|
||||
"scopes": [
|
||||
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers"
|
||||
]
|
||||
},
|
||||
},
|
||||
"revoke": {
|
||||
"description": "Revokes a browser enrollment token in a domain.",
|
||||
"flatPath": "{customer}/chrome/enrollmentTokens/{tokenPermanentId}:revoke",
|
||||
@@ -387,7 +387,7 @@
|
||||
"id": "cbcm.enrollmentTokens.revoke",
|
||||
"parameterOrder": [
|
||||
"customer",
|
||||
"tokenPermanentId"
|
||||
"tokenPermanentId"
|
||||
],
|
||||
"parameters": {
|
||||
"customer": {
|
||||
@@ -402,12 +402,12 @@
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
},
|
||||
"path": "{customer}/chrome/enrollmentTokens/{tokenPermanentId}:revoke",
|
||||
"scopes": [
|
||||
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -491,23 +491,23 @@
|
||||
"description": "Immutable ID of the G Suite account.",
|
||||
"type": "string"
|
||||
},
|
||||
"orgUnitPath": {
|
||||
"orgUnitPath": {
|
||||
"description": "The full path of the organizational unit or its unique ID.",
|
||||
"type": "string"
|
||||
},
|
||||
"creatorId": {
|
||||
"creatorId": {
|
||||
"description": "Creator ID.",
|
||||
"type": "string"
|
||||
},
|
||||
"createTime": {
|
||||
"createTime": {
|
||||
"description": "Creation Time.",
|
||||
"type": "string"
|
||||
},
|
||||
"revokerId": {
|
||||
"revokerId": {
|
||||
"description": "Revoker ID.",
|
||||
"type": "string"
|
||||
},
|
||||
"revokeTime": {
|
||||
"revokeTime": {
|
||||
"description": "Revoke Time",
|
||||
"type": "string"
|
||||
}
|
||||
@@ -538,16 +538,18 @@
|
||||
},
|
||||
"CreateEnrollmentTokenRequest": {
|
||||
"id": "CreateEnrollmentTokenRequest",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"org_unit_path": {
|
||||
"org_unit_path": {
|
||||
"description": "The full path of the organizational unit or its unique ID.",
|
||||
"type": "string"
|
||||
},
|
||||
"expire_time": {
|
||||
"expire_time": {
|
||||
"description": "Expiration Time.",
|
||||
"type": "string"
|
||||
},
|
||||
"token_type": {
|
||||
"token_type": {
|
||||
"id": "token_type",
|
||||
"annotations": {
|
||||
"required": [
|
||||
"cbcm.enrollmentTokens.create"
|
||||
@@ -559,6 +561,8 @@
|
||||
}
|
||||
},
|
||||
"MoveChromeBrowsersRequest": {
|
||||
"id": "MoveChromeBrowsersRequest",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"org_unit_path": {
|
||||
"annotations": {
|
||||
@@ -576,7 +580,10 @@
|
||||
]
|
||||
},
|
||||
"description": "List of unique device IDs of Chrome Browser Devices to move. A maximum of 600 browsers may be moved per request.",
|
||||
"type": "array"
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ upgrade_only=false
|
||||
gamversion="latest"
|
||||
adminuser=""
|
||||
regularuser=""
|
||||
gam_glibc_vers="2.31 2.27 2.23"
|
||||
gam_glibc_vers="2.31 2.27"
|
||||
#gam_macos_vers="10.15.6 10.14.6 10.13.6"
|
||||
|
||||
while getopts "hd:a:o:b:lp:u:r:v:" OPTION
|
||||
@@ -114,9 +114,10 @@ case $gamos in
|
||||
done
|
||||
case $gamarch in
|
||||
x86_64) gamfile="linux-x86_64-$useglibc.tar.xz";;
|
||||
arm64|aarch64) gamfile="linux-arm64-$useglibc.tar.xz";;
|
||||
arm64|aarch64) gamfile="linux-arm64-glibc2.28.tar.xz";;
|
||||
arm|armv7l) gamfile="linux-armv7l-glibc2.28.tar.xz";;
|
||||
*)
|
||||
echo_red "ERROR: this installer currently only supports x86_64 and arm64 Linux. Looks like you're running on $gamarch. Exiting."
|
||||
echo_red "ERROR: this installer currently only supports x86_64, arm and arm64 Linux. Looks like you're running on $gamarch. Exiting."
|
||||
exit
|
||||
esac
|
||||
;;
|
||||
@@ -128,7 +129,7 @@ case $gamos in
|
||||
this_macos_ver=$osversion
|
||||
fi
|
||||
echo "You are running MacOS $this_macos_ver"
|
||||
gamfile="macos-x86_64.tar.xz"
|
||||
gamfile="macos-universal2.tar.xz"
|
||||
;;
|
||||
MINGW64_NT*)
|
||||
gamos="windows"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Provides backwards compatibility for calling gam as a single .py file"""
|
||||
|
||||
import sys
|
||||
@@ -8,4 +7,4 @@ from gam.__main__ import main
|
||||
|
||||
# Run from command line
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv)
|
||||
main()
|
||||
|
||||
@@ -33,6 +33,13 @@ for d in a.datas:
|
||||
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
# TODO: fix universal2
|
||||
target_arch = None
|
||||
#if sys.platform == "darwin":
|
||||
# target_arch="universal2"
|
||||
#else:
|
||||
# target_arch=None
|
||||
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
@@ -42,4 +49,5 @@ exe = EXE(pyz,
|
||||
debug=False,
|
||||
strip=None,
|
||||
upx=False,
|
||||
target_arch=target_arch,
|
||||
console=True)
|
||||
|
||||
@@ -33,6 +33,7 @@ import http.client as http_client
|
||||
from multiprocessing import Pool as mp_pool
|
||||
from multiprocessing import Lock as mp_lock
|
||||
from urllib.parse import quote, urlencode, urlparse
|
||||
from pathvalidate import sanitize_filename
|
||||
import dateutil.parser
|
||||
|
||||
import googleapiclient
|
||||
@@ -549,6 +550,7 @@ def SetGlobalVariables():
|
||||
filePresentValue=4,
|
||||
fileAbsentValue=0)
|
||||
_getOldSignalFile(GC_NO_BROWSER, 'nobrowser.txt')
|
||||
_getOldSignalFile(GC_NO_TDEMAIL, 'notdemail.txt')
|
||||
_getOldSignalFile(GC_OAUTH_BROWSER, 'oauthbrowser.txt')
|
||||
# _getOldSignalFile(GC_NO_CACHE, u'nocache.txt')
|
||||
# _getOldSignalFile(GC_CACHE_DISCOVERY_ONLY, u'allcache.txt', filePresentValue=False, fileAbsentValue=True)
|
||||
@@ -728,8 +730,12 @@ def getOSPlatform():
|
||||
elif myos == 'Darwin':
|
||||
myos = 'MacOS'
|
||||
mac_ver = platform.mac_ver()[0]
|
||||
major_ver = int(mac_ver.split('.')[0]) # macver 10.14.6 == major_ver 10
|
||||
minor_ver = int(mac_ver.split('.')[1]) # macver 10.14.6 == minor_ver 14
|
||||
codename = MACOS_CODENAMES.get(minor_ver, '')
|
||||
if major_ver == 10:
|
||||
codename = MACOS_CODENAMES[major_ver].get(minor_ver, '')
|
||||
else:
|
||||
codename = MACOS_CODENAMES.get(major_ver, '')
|
||||
pltfrm = ' '.join([codename, mac_ver])
|
||||
else:
|
||||
pltfrm = platform.platform()
|
||||
@@ -768,12 +774,12 @@ def doGAMVersion(checkForArgs=True):
|
||||
cpu_bits = struct.calcsize('P') * 8
|
||||
api_client_ver = lib_version('google-api-python-client')
|
||||
print(
|
||||
(f'GAM {GAM_VERSION} - {GAM_URL} - {GM_Globals[GM_GAM_TYPE]}\n'
|
||||
f'GAM {GAM_VERSION} - {GAM_URL} - {GM_Globals[GM_GAM_TYPE]}\n'
|
||||
f'{GAM_AUTHOR}\n'
|
||||
f'Python {pyversion} {cpu_bits}-bit {sys.version_info.releaselevel}\n'
|
||||
f'google-api-python-client {api_client_ver}\n'
|
||||
f'{getOSPlatform()} {platform.machine()}\n'
|
||||
f'Path: {GM_Globals[GM_GAM_PATH]}'))
|
||||
f'Path: {GM_Globals[GM_GAM_PATH]}')
|
||||
if sys.platform.startswith('win') and \
|
||||
cpu_bits == 32 and \
|
||||
platform.machine().find('64') != -1:
|
||||
@@ -1240,9 +1246,8 @@ def doCheckServiceAccount(users):
|
||||
'get',
|
||||
name=name,
|
||||
throw_reasons=[gapi_errors.ErrorReason.FOUR_O_THREE])
|
||||
# Both Google and GAM set key valid after to day before creation
|
||||
key_created = dateutil.parser.parse(
|
||||
key['validAfterTime'], ignoretz=True) + datetime.timedelta(days=1)
|
||||
key['validAfterTime'], ignoretz=True)
|
||||
key_age = datetime.datetime.now() - key_created
|
||||
key_days = key_age.days
|
||||
if key_days > 30:
|
||||
@@ -1757,8 +1762,8 @@ def doCreateAdmin():
|
||||
def doPrintAdmins():
|
||||
cd = buildGAPIObject('directory')
|
||||
roleId = None
|
||||
userKey = None
|
||||
todrive = False
|
||||
kwargs = {}
|
||||
fields = 'nextPageToken,items(roleAssignmentId,roleId,assignedTo,scopeType,orgUnitId)'
|
||||
titles = [
|
||||
'roleAssignmentId', 'roleId', 'role', 'assignedTo', 'assignedToUser',
|
||||
@@ -1769,7 +1774,7 @@ def doPrintAdmins():
|
||||
while i < len(sys.argv):
|
||||
myarg = sys.argv[i].lower()
|
||||
if myarg == 'user':
|
||||
userKey = normalizeEmailAddressOrUID(sys.argv[i + 1])
|
||||
kwargs['userKey'] = normalizeEmailAddressOrUID(sys.argv[i + 1])
|
||||
i += 2
|
||||
elif myarg == 'role':
|
||||
roleId = getRoleId(sys.argv[i + 1])
|
||||
@@ -1779,14 +1784,18 @@ def doPrintAdmins():
|
||||
i += 1
|
||||
else:
|
||||
controlflow.invalid_argument_exit(sys.argv[i], 'gam print admins')
|
||||
if roleId and not kwargs:
|
||||
kwargs['roleId'] = roleId
|
||||
roleId = None
|
||||
admins = gapi.get_all_pages(cd.roleAssignments(),
|
||||
'list',
|
||||
'items',
|
||||
customer=GC_Values[GC_CUSTOMER_ID],
|
||||
userKey=userKey,
|
||||
roleId=roleId,
|
||||
fields=fields)
|
||||
fields=fields,
|
||||
**kwargs)
|
||||
for admin in admins:
|
||||
if roleId and roleId != admin['roleId']:
|
||||
continue
|
||||
admin_attrib = {}
|
||||
for key, value in list(admin.items()):
|
||||
if key == 'assignedTo':
|
||||
@@ -3264,7 +3273,7 @@ def printDriveFileList(users):
|
||||
'orderby', ', '.join(sorted(DRIVEFILE_ORDERBY_CHOICES_MAP)),
|
||||
fieldName)
|
||||
elif myarg == 'query':
|
||||
query += f' and {sys.argv[i+1]}'
|
||||
query += f' and ({sys.argv[i+1]})'
|
||||
i += 2
|
||||
elif myarg == 'fullquery':
|
||||
query = sys.argv[i + 1]
|
||||
@@ -4057,8 +4066,7 @@ def downloadDriveFile(users):
|
||||
if targetName:
|
||||
safe_file_title = targetName
|
||||
else:
|
||||
safe_file_title = ''.join(c for c in result['title']
|
||||
if c in FILENAME_SAFE_CHARS)
|
||||
safe_file_title = sanitize_filename(result['title'])
|
||||
if not safe_file_title:
|
||||
safe_file_title = fileId
|
||||
filename = os.path.join(targetFolder, safe_file_title)
|
||||
@@ -4090,7 +4098,7 @@ def downloadDriveFile(users):
|
||||
for sheet in spreadsheet['sheets']:
|
||||
if sheet['properties']['title'].lower(
|
||||
) == csvSheetTitleLower:
|
||||
spreadsheetUrl = '{0}?format=csv&id={1}&gid={2}'.format(
|
||||
spreadsheetUrl = '{}?format=csv&id={}&gid={}'.format(
|
||||
re.sub('/edit.*$', '/export',
|
||||
spreadsheet['spreadsheetUrl']),
|
||||
fileId, sheet['properties']['sheetId'])
|
||||
@@ -4115,7 +4123,7 @@ def downloadDriveFile(users):
|
||||
while not done:
|
||||
status, done = downloader.next_chunk()
|
||||
if showProgress:
|
||||
print('Downloaded: {0:>7.2%}'.format(
|
||||
print('Downloaded: {:>7.2%}'.format(
|
||||
status.progress()))
|
||||
else:
|
||||
_, content = drive._http.request(uri=spreadsheetUrl,
|
||||
@@ -4127,7 +4135,7 @@ def downloadDriveFile(users):
|
||||
fileutils.close_file(fh)
|
||||
fileDownloaded = True
|
||||
break
|
||||
except (IOError, httplib2.HttpLib2Error) as e:
|
||||
except (OSError, httplib2.HttpLib2Error) as e:
|
||||
display.print_error(str(e))
|
||||
GM_Globals[GM_SYSEXITRC] = 6
|
||||
fileDownloadFailed = True
|
||||
@@ -6669,12 +6677,12 @@ def getUserAttributes(i, cd, updateCmd):
|
||||
body['changePasswordAtNextLogin'] = getBoolean(
|
||||
sys.argv[i + 1], myarg)
|
||||
i += 2
|
||||
elif myarg == 'ipwhitelisted':
|
||||
body['ipWhitelisted'] = getBoolean(sys.argv[i + 1], myarg)
|
||||
i += 2
|
||||
elif myarg == 'agreedtoterms':
|
||||
body['agreedToTerms'] = getBoolean(sys.argv[i + 1], myarg)
|
||||
i += 2
|
||||
elif myarg == 'ipwhitelisted':
|
||||
body['ipWhitelisted'] = getBoolean(sys.argv[i + 1], myarg)
|
||||
i += 2
|
||||
elif myarg in ['org', 'ou']:
|
||||
body['orgUnitPath'] = gapi_directory_orgunits.getOrgUnitItem(
|
||||
sys.argv[i + 1], pathOnly=True)
|
||||
@@ -6685,13 +6693,27 @@ def getUserAttributes(i, cd, updateCmd):
|
||||
i += 1
|
||||
continue
|
||||
for language in sys.argv[i].replace(',', ' ').split():
|
||||
if language.lower() in LANGUAGE_CODES_MAP:
|
||||
appendItemToBodyList(
|
||||
body, 'languages',
|
||||
{'languageCode': LANGUAGE_CODES_MAP[language.lower()]})
|
||||
lang_item = {}
|
||||
if language[-1] == '+':
|
||||
suffix = '+'
|
||||
language = language[:-1]
|
||||
lang_item['preference'] = 'preferred'
|
||||
elif language[-1] == '-':
|
||||
suffix = '-'
|
||||
language = language[:-1]
|
||||
lang_item['preference'] = 'not_preferred'
|
||||
else:
|
||||
appendItemToBodyList(body, 'languages',
|
||||
{'customLanguage': language})
|
||||
suffix = ''
|
||||
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
|
||||
elif myarg == 'gender':
|
||||
i += 1
|
||||
@@ -7236,6 +7258,7 @@ def enableGAMProjectAPIs(GAMProjectAPIs,
|
||||
gapi_errors.ErrorReason.FORBIDDEN,
|
||||
gapi_errors.ErrorReason.PERMISSION_DENIED
|
||||
],
|
||||
retry_reasons=[gapi_errors.ErrorReason.INTERNAL_SERVER_ERROR],
|
||||
name=service_name)
|
||||
print(f' API: {api}, Enabled{currentCount(j, jcount)}')
|
||||
break
|
||||
@@ -7756,11 +7779,11 @@ def _generatePrivateKeyAndPublicCert(client_id, key_size):
|
||||
x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, client_id)]))
|
||||
builder = builder.issuer_name(
|
||||
x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, client_id)]))
|
||||
not_valid_before = datetime.datetime.today() - datetime.timedelta(days=1)
|
||||
not_valid_after = datetime.datetime.today() + datetime.timedelta(
|
||||
days=365 * 10 - 1)
|
||||
builder = builder.not_valid_before(not_valid_before)
|
||||
builder = builder.not_valid_after(not_valid_after)
|
||||
# Gooogle seems to enforce the not before date strictly. Set the not before
|
||||
# date to be UTC one hour ago should cover any clock skew.
|
||||
builder = builder.not_valid_before(datetime.datetime.utcnow() - datetime.timedelta(hours=1))
|
||||
# Google uses 12/31/9999 date for end time
|
||||
builder = builder.not_valid_after(datetime.datetime(9999, 12, 31, 23, 59))
|
||||
builder = builder.serial_number(x509.random_serial_number())
|
||||
builder = builder.public_key(public_key)
|
||||
builder = builder.add_extension(x509.BasicConstraints(ca=False,
|
||||
@@ -7943,10 +7966,18 @@ def doCreateOrRotateServiceAccountKeys(iam=None,
|
||||
name=sa_name,
|
||||
body={'publicKeyData': publicKeyData})
|
||||
break
|
||||
except googleapiclient.errors.HttpError:
|
||||
print('WARNING: that key already exists.')
|
||||
result = {'name': oldPrivateKeyId}
|
||||
break
|
||||
except googleapiclient.errors.HttpError as err:
|
||||
if hasattr(err, 'error_details') and \
|
||||
err.error_details == 'The given public key already exists.':
|
||||
print('WARNING: that key already exists.')
|
||||
result = {'name': oldPrivateKeyId}
|
||||
break
|
||||
elif hasattr(err, 'error_details'):
|
||||
controlflow.system_error_exit(
|
||||
4, err.error_details)
|
||||
else:
|
||||
controlflow.system_error_exit(
|
||||
4, err)
|
||||
except gapi_errors.GapiNotFoundError as e:
|
||||
if i == max_retries:
|
||||
raise e
|
||||
@@ -8753,6 +8784,20 @@ def _get_admin_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 user_lic_result(request_id, response, exception):
|
||||
@@ -8767,6 +8812,7 @@ def doGetUserInfo(user_email=None):
|
||||
i = 4
|
||||
else:
|
||||
user_email = _get_admin_email()
|
||||
fieldsList = []
|
||||
getSchemas = True
|
||||
getAliases = True
|
||||
getGroups = True
|
||||
@@ -8797,10 +8843,35 @@ def doGetUserInfo(user_email=None):
|
||||
getSchemas = False
|
||||
projection = 'basic'
|
||||
i += 1
|
||||
elif myarg == 'quick':
|
||||
getAliases = getCIGroups = getGroups = getLicenses = getSchemas = False
|
||||
i += 1
|
||||
elif myarg in ['custom', 'schemas']:
|
||||
getSchemas = True
|
||||
projection = 'custom'
|
||||
customFieldMask = sys.argv[i + 1]
|
||||
if not fieldsList:
|
||||
fieldsList = ['primaryEmail']
|
||||
fieldsList.append('customSchemas')
|
||||
if sys.argv[i + 1].lower() == 'all':
|
||||
projection = 'full'
|
||||
else:
|
||||
projection = 'custom'
|
||||
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
|
||||
elif myarg == 'userview':
|
||||
viewType = 'domain_public'
|
||||
@@ -8814,6 +8885,7 @@ def doGetUserInfo(user_email=None):
|
||||
'get',
|
||||
userKey=user_email,
|
||||
projection=projection,
|
||||
fields=','.join(set(fieldsList)) if fieldsList else '*',
|
||||
customFieldMask=customFieldMask,
|
||||
viewType=viewType)
|
||||
print(f'User: {user["primaryEmail"]}')
|
||||
@@ -8822,14 +8894,7 @@ def doGetUserInfo(user_email=None):
|
||||
if 'name' in user and 'familyName' in user['name']:
|
||||
print(f'Last Name: {user["name"]["familyName"]}')
|
||||
if 'languages' in user:
|
||||
up = 'languageCode'
|
||||
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)}')
|
||||
print(f"Languages: {_formatLanguagesList(user['languages'], ',')}")
|
||||
if 'isAdmin' in user:
|
||||
print(f'Is a Super Admin: {user["isAdmin"]}')
|
||||
if 'isDelegatedAdmin' in user:
|
||||
@@ -9644,6 +9709,7 @@ def doPrintUsers():
|
||||
customFieldMask = None
|
||||
sortHeaders = getGroupFeed = getLicenseFeed = email_parts = False
|
||||
viewType = deleted_only = orderBy = sortOrder = None
|
||||
orgUnitPath = orgUnitPathLower = None
|
||||
groupDelimiter = ' '
|
||||
licenseDelimiter = ','
|
||||
i = 3
|
||||
@@ -9666,12 +9732,14 @@ def doPrintUsers():
|
||||
sortHeaders = True
|
||||
i += 1
|
||||
elif myarg in ['custom', 'schemas']:
|
||||
if not fieldsList:
|
||||
fieldsList = ['primaryEmail']
|
||||
fieldsList.append('customSchemas')
|
||||
if sys.argv[i + 1].lower() == 'all':
|
||||
projection = 'full'
|
||||
else:
|
||||
projection = 'custom'
|
||||
customFieldMask = sys.argv[i + 1]
|
||||
customFieldMask = sys.argv[i + 1].replace(' ', ',')
|
||||
i += 2
|
||||
elif myarg == 'todrive':
|
||||
todrive = True
|
||||
@@ -9706,19 +9774,19 @@ def doPrintUsers():
|
||||
elif myarg in ['query', 'queries']:
|
||||
queries = getQueries(myarg, sys.argv[i + 1])
|
||||
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:
|
||||
if not fieldsList:
|
||||
fieldsList = [
|
||||
'primaryEmail',
|
||||
]
|
||||
fieldsList = ['primaryEmail',]
|
||||
display.add_field_to_csv_file(myarg, USER_ARGUMENT_TO_PROPERTY_MAP,
|
||||
fieldsList, fieldsTitles, titles)
|
||||
i += 1
|
||||
elif myarg == 'fields':
|
||||
if not fieldsList:
|
||||
fieldsList = [
|
||||
'primaryEmail',
|
||||
]
|
||||
fieldsList = ['primaryEmail',]
|
||||
fieldNameList = sys.argv[i + 1]
|
||||
for field in fieldNameList.lower().replace(',', ' ').split():
|
||||
if field in USER_ARGUMENT_TO_PROPERTY_MAP:
|
||||
@@ -9741,10 +9809,21 @@ def doPrintUsers():
|
||||
else:
|
||||
controlflow.invalid_argument_exit(sys.argv[i], 'gam print users')
|
||||
if fieldsList:
|
||||
if orgUnitPath is not None:
|
||||
fieldsList.append('orgUnitPath')
|
||||
fields = f'nextPageToken,users({",".join(set(fieldsList)).replace(".", "/")})'
|
||||
else:
|
||||
fields = None
|
||||
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)
|
||||
page_message = gapi.got_total_items_first_last_msg('Users')
|
||||
all_users = gapi.get_all_pages(cd.users(),
|
||||
@@ -9763,13 +9842,16 @@ def doPrintUsers():
|
||||
projection=projection,
|
||||
customFieldMask=customFieldMask)
|
||||
for user in all_users:
|
||||
if email_parts and ('primaryEmail' in user):
|
||||
user_email = user['primaryEmail']
|
||||
if user_email.find('@') != -1:
|
||||
user['primaryEmailLocal'], user[
|
||||
'primaryEmailDomain'] = splitEmailAddress(user_email)
|
||||
display.add_row_titles_to_csv_file(utils.flatten_json(user),
|
||||
csvRows, titles)
|
||||
if orgUnitPathLower is None or orgUnitPathLower == user.get('orgUnitPath', '').lower():
|
||||
if email_parts and ('primaryEmail' in user):
|
||||
user_email = user['primaryEmail']
|
||||
if user_email.find('@') != -1:
|
||||
user['primaryEmailLocal'], user[
|
||||
'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),
|
||||
csvRows, titles)
|
||||
if sortHeaders:
|
||||
display.sort_csv_titles([
|
||||
'primaryEmail',
|
||||
@@ -10433,9 +10515,10 @@ OAUTH2_SCOPES = [
|
||||
'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': [],
|
||||
'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',
|
||||
@@ -11469,6 +11552,8 @@ def ProcessGAMCommand(args):
|
||||
gapi_directory_resource.getResourceCalendarInfo()
|
||||
elif argument == 'cros':
|
||||
gapi_directory_cros.doGetCrosInfo()
|
||||
elif argument == 'crostelemetry':
|
||||
gapi_chromemanagement.printShowCrosTelemetry('info')
|
||||
elif argument == 'mobile':
|
||||
gapi_directory_mobiledevices.info()
|
||||
elif argument in ['verify', 'verification']:
|
||||
@@ -11621,6 +11706,8 @@ def ProcessGAMCommand(args):
|
||||
gapi_cloudidentity_groups.print_()
|
||||
elif argument == 'devices':
|
||||
gapi_cloudidentity_devices.print_()
|
||||
elif argument == 'crostelemetry':
|
||||
gapi_chromemanagement.printShowCrosTelemetry('print')
|
||||
elif argument in ['groupmembers', 'groupsmembers']:
|
||||
gapi_directory_groups.print_members()
|
||||
elif argument in ['cigroupmembers', 'cigroupsmembers']:
|
||||
@@ -11729,6 +11816,8 @@ def ProcessGAMCommand(args):
|
||||
gapi_chromepolicy.printshow_schemas()
|
||||
elif argument in ['chromepolicy', 'chromepolicies']:
|
||||
gapi_chromepolicy.printshow_policies()
|
||||
elif argument == 'crostelemetry':
|
||||
gapi_chromemanagement.printShowCrosTelemetry('show')
|
||||
else:
|
||||
controlflow.invalid_argument_exit(argument, 'gam show')
|
||||
sys.exit(0)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# GAM
|
||||
#
|
||||
@@ -30,21 +29,21 @@ from gam import controlflow
|
||||
import gam
|
||||
|
||||
|
||||
def main(argv):
|
||||
def main():
|
||||
freeze_support()
|
||||
if sys.platform == 'darwin':
|
||||
# https://bugs.python.org/issue33725 in Python 3.8.0 seems
|
||||
# to break parallel operations with errors about extra -b
|
||||
# command line arguments
|
||||
set_start_method('fork')
|
||||
if sys.version_info[0] < 3 or sys.version_info[1] < 6:
|
||||
if sys.version_info[0] < 3 or sys.version_info[1] < 7:
|
||||
controlflow.system_error_exit(
|
||||
5,
|
||||
f'GAM requires Python 3.6 or newer. You are running %s.%s.%s. Please upgrade your Python version or use one of the binary GAM downloads.'
|
||||
f'GAM requires Python 3.7 or newer. You are running %s.%s.%s. Please upgrade your Python version or use one of the binary GAM downloads.'
|
||||
% sys.version_info[:3])
|
||||
sys.exit(gam.ProcessGAMCommand(sys.argv))
|
||||
|
||||
|
||||
# Run from command line
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv)
|
||||
main()
|
||||
|
||||
@@ -37,7 +37,7 @@ def get_admin_credentials(api=None):
|
||||
credential_file = get_admin_credentials_filename()
|
||||
if not os.path.isfile(credential_file):
|
||||
raise oauth.InvalidCredentialsFileError
|
||||
with open(credential_file, 'r') as f:
|
||||
with open(credential_file) as f:
|
||||
creds_data = json.load(f)
|
||||
# Validate that enable DASA matches content of authorization file
|
||||
if GC_Values[GC_ENABLE_DASA] and 'private_key_id' in creds_data:
|
||||
|
||||
@@ -115,7 +115,7 @@ class Credentials(google.oauth2.credentials.Credentials):
|
||||
Raises:
|
||||
TypeError: If id_token_data is not the required dict type.
|
||||
"""
|
||||
super(Credentials, self).__init__(token=token,
|
||||
super().__init__(token=token,
|
||||
refresh_token=refresh_token,
|
||||
id_token=id_token,
|
||||
token_uri=token_uri,
|
||||
@@ -161,9 +161,9 @@ class Credentials(google.oauth2.credentials.Credentials):
|
||||
ValueError: If missing fields are detected in the info.
|
||||
"""
|
||||
# We need all of these keys
|
||||
keys_needed = set(('client_id', 'client_secret'))
|
||||
keys_needed = {'client_id', 'client_secret'}
|
||||
# We need 1 or more of these keys
|
||||
keys_need_one_of = set(('refresh_token', 'auth_token', 'token'))
|
||||
keys_need_one_of = {'refresh_token', 'auth_token', 'token'}
|
||||
missing = keys_needed.difference(info.keys())
|
||||
has_one_of = set(info) & keys_need_one_of
|
||||
if missing or not has_one_of:
|
||||
@@ -395,7 +395,7 @@ class Credentials(google.oauth2.credentials.Credentials):
|
||||
self.refresh(request)
|
||||
|
||||
self._id_token_data = google.oauth2.id_token.verify_oauth2_token(
|
||||
self.id_token, request)
|
||||
self.id_token, request, clock_skew_in_seconds=10)
|
||||
|
||||
def get_token_value(self, field):
|
||||
"""Retrieves data from the OAuth ID token.
|
||||
@@ -472,7 +472,7 @@ class Credentials(google.oauth2.credentials.Credentials):
|
||||
def _locked_refresh(self, request):
|
||||
"""Refreshes the credential's access token while the file lock is held."""
|
||||
assert self._lock.is_locked
|
||||
super(Credentials, self).refresh(request)
|
||||
super().refresh(request)
|
||||
|
||||
def write(self):
|
||||
"""Writes credentials to disk."""
|
||||
@@ -523,12 +523,12 @@ class _ShortURLFlow(google_auth_oauthlib.flow.InstalledAppFlow):
|
||||
|
||||
def authorization_url(self, http=None, **kwargs):
|
||||
"""Gets a shortened authorization URL."""
|
||||
long_url, state = super(_ShortURLFlow, self).authorization_url(**kwargs)
|
||||
long_url, state = super().authorization_url(**kwargs)
|
||||
short_url = utils.shorten_url(long_url)
|
||||
return short_url, state
|
||||
|
||||
|
||||
class _FileLikeThreadLock(object):
|
||||
class _FileLikeThreadLock:
|
||||
"""A threading.lock which has the same interface as filelock.Filelock."""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@@ -38,7 +38,7 @@ class CredentialsTest(unittest.TestCase):
|
||||
'client_id': self.fake_client_id,
|
||||
'client_secret': self.fake_client_secret,
|
||||
}
|
||||
super(CredentialsTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
def tearDown(self):
|
||||
# Remove any credential files that may have been created.
|
||||
@@ -46,7 +46,7 @@ class CredentialsTest(unittest.TestCase):
|
||||
os.remove(self.fake_filename)
|
||||
if os.path.exists('%s.lock' % self.fake_filename):
|
||||
os.remove('%s.lock' % self.fake_filename)
|
||||
super(CredentialsTest, self).tearDown()
|
||||
super().tearDown()
|
||||
|
||||
def test_from_authorized_user_info_only_required_info(self):
|
||||
creds = oauth.Credentials.from_authorized_user_info(
|
||||
@@ -592,7 +592,7 @@ class ShortUrlFlowTest(unittest.TestCase):
|
||||
}
|
||||
self.long_url = 'http://example.com/some/long/url'
|
||||
self.short_url = 'http://ex.co/short'
|
||||
super(ShortUrlFlowTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
@patch.object(oauth.google_auth_oauthlib.flow.InstalledAppFlow,
|
||||
'authorization_url')
|
||||
|
||||
@@ -7,6 +7,7 @@ from threading import Timer
|
||||
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from smartcard.Exceptions import CardConnectionException
|
||||
from ykman.device import connect_to_device
|
||||
from ykman.piv import generate_self_signed_certificate, \
|
||||
generate_chuid
|
||||
@@ -46,7 +47,10 @@ class YubiKey():
|
||||
self.key_id = service_account_info.get('private_key_id')
|
||||
|
||||
def _connect(self):
|
||||
conn, _, _ = connect_to_device(self.serial_number)
|
||||
try:
|
||||
conn, _, _ = connect_to_device(self.serial_number)
|
||||
except CardConnectionException as err:
|
||||
controlflow.system_error_exit(9, f'YubiKey - {err}')
|
||||
return conn
|
||||
|
||||
def get_certificate(self):
|
||||
@@ -62,7 +66,7 @@ class YubiKey():
|
||||
try:
|
||||
cert = session.get_certificate(self.slot)
|
||||
except ApduError as err:
|
||||
controlflow.system_error_exit(9, f'Yubikey = {err}')
|
||||
controlflow.system_error_exit(9, f'YubiKey - {err}')
|
||||
cert_pem = cert.public_bytes(
|
||||
serialization.Encoding.PEM).decode()
|
||||
publicKeyData = b64encode(cert_pem.encode())
|
||||
@@ -78,7 +82,7 @@ class YubiKey():
|
||||
_, _, info = connect_to_device(self.serial_number)
|
||||
return info.serial
|
||||
except ValueError as err:
|
||||
controlflow.system_error_exit(9, f'YubikKey = {err}')
|
||||
controlflow.system_error_exit(9, f'YubiKey - {err}')
|
||||
|
||||
def reset_piv(self):
|
||||
'''Resets YubiKey PIV app and generates new key for GAM to use.'''
|
||||
@@ -101,7 +105,7 @@ class YubiKey():
|
||||
DEFAULT_MANAGEMENT_KEY)
|
||||
|
||||
piv.verify_pin(new_pin)
|
||||
print('Yubikey is generating a non-exportable private key...')
|
||||
print('YubiKey is generating a non-exportable private key...')
|
||||
pubkey = piv.generate_key(SLOT.AUTHENTICATION,
|
||||
KEY_TYPE.RSA2048,
|
||||
PIN_POLICY.ALWAYS,
|
||||
@@ -123,7 +127,7 @@ class YubiKey():
|
||||
piv.put_object(OBJECT_ID.CHUID,
|
||||
generate_chuid())
|
||||
except ValueError as err:
|
||||
controlflow.system_error_exit(8, f'Yubikey - {err}')
|
||||
controlflow.system_error_exit(8, f'YubiKey - {err}')
|
||||
|
||||
|
||||
def sign(self, message):
|
||||
@@ -145,7 +149,7 @@ class YubiKey():
|
||||
hash_algorithm=hashes.SHA256(),
|
||||
padding=padding.PKCS1v15())
|
||||
except ApduError as err:
|
||||
controlflow.system_error_exit(8, f'YubiKey = {err}')
|
||||
controlflow.system_error_exit(8, f'YubiKey - {err}')
|
||||
except ValueError as err:
|
||||
controlflow.system_error_exit(9, f'YubiKey - {err}')
|
||||
if 'mplock' in globals():
|
||||
|
||||
@@ -92,8 +92,8 @@ def wait_on_failure(current_attempt_num,
|
||||
wait_on_fail = min(2**current_attempt_num,
|
||||
60) + float(random.randint(1, 1000)) / 1000
|
||||
if current_attempt_num > error_print_threshold:
|
||||
sys.stderr.write((f'Temporary error: {error_message}, Backing off: '
|
||||
sys.stderr.write(f'Temporary error: {error_message}, Backing off: '
|
||||
f'{int(wait_on_fail)} seconds, Retry: '
|
||||
f'{current_attempt_num}/{total_num_retries}\n'))
|
||||
f'{current_attempt_num}/{total_num_retries}\n')
|
||||
sys.stderr.flush()
|
||||
time.sleep(wait_on_fail)
|
||||
|
||||
@@ -154,28 +154,39 @@ def write_csv_file(csvRows, titles, list_type, todrive):
|
||||
return True
|
||||
return False
|
||||
|
||||
def filterMatch(filterVal, columns, row):
|
||||
for column in columns:
|
||||
if filterVal[1] == 'regex':
|
||||
if filterVal[2].search(str(row.get(column, ''))):
|
||||
return True
|
||||
elif filterVal[1] == 'notregex':
|
||||
if not filterVal[2].search(str(row.get(column, ''))):
|
||||
return True
|
||||
elif filterVal[1] in ['date', 'time']:
|
||||
if rowDateTimeFilterMatch(
|
||||
filterVal[1] == 'date', row.get(column, ''),
|
||||
filterVal[2], filterVal[3]):
|
||||
return True
|
||||
elif filterVal[1] == 'count':
|
||||
if rowCountFilterMatch(
|
||||
row.get(column, 0), filterVal[2], filterVal[3]):
|
||||
return True
|
||||
else: #boolean
|
||||
if rowBooleanFilterMatch(
|
||||
row.get(column, False), filterVal[2]):
|
||||
return True
|
||||
return False
|
||||
|
||||
def rowFilterMatch(filters, columns, row):
|
||||
for c, filterVal in iter(filters.items()):
|
||||
for column in columns[c]:
|
||||
if filterVal[1] == 'regex':
|
||||
if filterVal[2].search(str(row.get(column, ''))):
|
||||
return True
|
||||
elif filterVal[1] == 'notregex':
|
||||
if not filterVal[2].search(str(row.get(column, ''))):
|
||||
return True
|
||||
elif filterVal[1] in ['date', 'time']:
|
||||
if rowDateTimeFilterMatch(
|
||||
filterVal[1] == 'date', row.get(column, ''),
|
||||
filterVal[2], filterVal[3]):
|
||||
return True
|
||||
elif filterVal[1] == 'count':
|
||||
if rowCountFilterMatch(
|
||||
row.get(column, 0), filterVal[2], filterVal[3]):
|
||||
return True
|
||||
else: #boolean
|
||||
if rowBooleanFilterMatch(
|
||||
row.get(column, False), filterVal[2]):
|
||||
return True
|
||||
if not filterMatch(filterVal, columns[c], row):
|
||||
return False
|
||||
return True
|
||||
|
||||
def rowDropFilterMatch(filters, columns, row):
|
||||
for c, filterVal in iter(filters.items()):
|
||||
if filterMatch(filterVal, columns[c], row):
|
||||
return True
|
||||
return False
|
||||
|
||||
if GC_Values[GC_CSV_ROW_FILTER] or GC_Values[GC_CSV_ROW_DROP_FILTER]:
|
||||
@@ -210,7 +221,7 @@ def write_csv_file(csvRows, titles, list_type, todrive):
|
||||
if (((keepColumns is None) or
|
||||
rowFilterMatch(GC_Values[GC_CSV_ROW_FILTER], keepColumns, row)) and
|
||||
((dropColumns is None) or
|
||||
not rowFilterMatch(GC_Values[GC_CSV_ROW_DROP_FILTER], dropColumns, row))):
|
||||
not rowDropFilterMatch(GC_Values[GC_CSV_ROW_DROP_FILTER], dropColumns, row))):
|
||||
rows.append(row)
|
||||
csvRows = rows
|
||||
|
||||
@@ -231,7 +242,14 @@ def write_csv_file(csvRows, titles, list_type, todrive):
|
||||
'No columns selected with GAM_CSV_HEADER_FILTER and GAM_CSV_HEADER_DROP_FILTER\n'
|
||||
)
|
||||
return
|
||||
csv.register_dialect('nixstdout', lineterminator='\n')
|
||||
nixstdout_dialect = {'lineterminator': '\n',
|
||||
'quoting': csv.QUOTE_MINIMAL}
|
||||
# fix issue with Python 3.10.0 and no escape char
|
||||
# 3.10.1+ may fix this within Python so hopefully
|
||||
# this is short-lived.
|
||||
if sys.version_info.minor >= 10:
|
||||
nixstdout_dialect['escapechar'] = '\\'
|
||||
csv.register_dialect('nixstdout', **nixstdout_dialect)
|
||||
if todrive:
|
||||
write_to = io.StringIO()
|
||||
else:
|
||||
@@ -239,12 +257,11 @@ def write_csv_file(csvRows, titles, list_type, todrive):
|
||||
writer = csv.DictWriter(write_to,
|
||||
fieldnames=titles,
|
||||
dialect='nixstdout',
|
||||
extrasaction='ignore',
|
||||
quoting=csv.QUOTE_MINIMAL)
|
||||
extrasaction='ignore')
|
||||
try:
|
||||
writer.writerow(dict((item, item) for item in writer.fieldnames))
|
||||
writer.writerow({item: item for item in writer.fieldnames})
|
||||
writer.writerows(csvRows)
|
||||
except IOError as e:
|
||||
except OSError as e:
|
||||
controlflow.system_error_exit(6, e)
|
||||
if todrive:
|
||||
admin_email = gam._get_admin_email()
|
||||
@@ -283,7 +300,8 @@ and follow recommend steps to authorize GAM for Drive access.''')
|
||||
if GC_Values[GC_NO_BROWSER]:
|
||||
msg_txt = f'Drive file uploaded to:\n {file_url}'
|
||||
msg_subj = f'{GC_Values[GC_DOMAIN]} - {list_type}'
|
||||
gam.send_email(msg_subj, msg_txt)
|
||||
if not GC_Values[GC_NO_TDEMAIL]:
|
||||
gam.send_email(msg_subj, msg_txt)
|
||||
print(msg_txt)
|
||||
else:
|
||||
webbrowser.open(file_url)
|
||||
@@ -291,12 +309,12 @@ and follow recommend steps to authorize GAM for Drive access.''')
|
||||
|
||||
def print_error(message):
|
||||
"""Prints a one-line error message to stderr in a standard format."""
|
||||
sys.stderr.write('\n{0}{1}\n'.format(ERROR_PREFIX, message))
|
||||
sys.stderr.write(f'\n{ERROR_PREFIX}{message}\n')
|
||||
|
||||
|
||||
def print_warning(message):
|
||||
"""Prints a one-line warning message to stderr in a standard format."""
|
||||
sys.stderr.write('\n{0}{1}\n'.format(WARNING_PREFIX, message))
|
||||
sys.stderr.write(f'\n{WARNING_PREFIX}{message}\n')
|
||||
|
||||
|
||||
def print_json(object_value, spacing=''):
|
||||
|
||||
@@ -59,7 +59,7 @@ def open_file(filename,
|
||||
# Open a file on disk
|
||||
f = _open_file(filename, mode, newline=newline, encoding=encoding)
|
||||
if strip_utf_bom:
|
||||
utf_bom = u'\ufeff'
|
||||
utf_bom = '\ufeff'
|
||||
has_bom = False
|
||||
|
||||
if 'b' in mode:
|
||||
@@ -79,7 +79,7 @@ def open_file(filename,
|
||||
|
||||
return f
|
||||
|
||||
except IOError as e:
|
||||
except OSError as e:
|
||||
controlflow.system_error_exit(6, e)
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ def close_file(f, force_flush=False):
|
||||
try:
|
||||
f.close()
|
||||
return True
|
||||
except IOError as e:
|
||||
except OSError as e:
|
||||
display.print_error(e)
|
||||
return False
|
||||
|
||||
@@ -140,7 +140,7 @@ def read_file(filename,
|
||||
encoding=encoding) as f:
|
||||
return f.read()
|
||||
|
||||
except IOError as e:
|
||||
except OSError as e:
|
||||
if continue_on_error:
|
||||
if display_errors:
|
||||
display.print_warning(e)
|
||||
@@ -174,7 +174,7 @@ def write_file(filename,
|
||||
f.write(data)
|
||||
return True
|
||||
|
||||
except IOError as e:
|
||||
except OSError as e:
|
||||
if continue_on_error:
|
||||
if display_errors:
|
||||
display.print_error(e)
|
||||
|
||||
@@ -13,7 +13,7 @@ class FileutilsTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.fake_path = '/some/path/to/file'
|
||||
super(FileutilsTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
@patch.object(fileutils.sys, 'stdin')
|
||||
def test_open_file_stdin(self, mock_stdin):
|
||||
@@ -63,7 +63,7 @@ class FileutilsTest(unittest.TestCase):
|
||||
self.assertEqual(fileutils.UTF8_SIG, mock_open.call_args[1]['encoding'])
|
||||
|
||||
def test_open_file_strips_utf_bom_in_utf(self):
|
||||
bom_prefixed_data = u'\ufefffoobar'
|
||||
bom_prefixed_data = '\ufefffoobar'
|
||||
fake_file = io.StringIO(bom_prefixed_data)
|
||||
mock_open = MagicMock(spec=open, return_value=fake_file)
|
||||
with patch.object(fileutils, 'open', mock_open):
|
||||
@@ -89,7 +89,7 @@ class FileutilsTest(unittest.TestCase):
|
||||
self.assertEqual('foobar', f.read())
|
||||
|
||||
def test_open_file_strips_utf_bom_in_binary(self):
|
||||
bom_prefixed_data = u'\ufefffoobar'.encode('UTF-8')
|
||||
bom_prefixed_data = '\ufefffoobar'.encode()
|
||||
fake_file = io.BytesIO(bom_prefixed_data)
|
||||
mock_open = MagicMock(spec=open, return_value=fake_file)
|
||||
with patch.object(fileutils, 'open', mock_open):
|
||||
|
||||
@@ -359,7 +359,7 @@ def handle_oauth_token_error(e, soft_errors):
|
||||
returns to the caller.
|
||||
"""
|
||||
token_error = str(e).replace('.', '')
|
||||
if token_error in errors.OAUTH2_TOKEN_ERRORS or e.startswith(
|
||||
if token_error in errors.OAUTH2_TOKEN_ERRORS or token_error.startswith(
|
||||
'Invalid response'):
|
||||
if soft_errors:
|
||||
return
|
||||
|
||||
@@ -80,7 +80,7 @@ class GapiTest(unittest.TestCase):
|
||||
]
|
||||
self.empty_items_response = {'items': []}
|
||||
|
||||
super(GapiTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
def test_call_returns_basic_200_response(self):
|
||||
response = gapi.call(self.mock_service, self.mock_method_name)
|
||||
|
||||
@@ -9,6 +9,8 @@ from gam.var import YYYYMMDD_FORMAT
|
||||
from gam import controlflow
|
||||
from gam import display
|
||||
from gam import gapi
|
||||
from gam import utils
|
||||
from gam.gapi import directory as gapi_directory
|
||||
from gam.gapi.directory import orgunits as gapi_directory_orgunits
|
||||
from gam.gapi.directory.cros import _getFilterDate
|
||||
|
||||
@@ -201,6 +203,109 @@ def printAppDevices():
|
||||
display.write_csv_file(csvRows, titles, 'Chrome Installed Application Devices', todrive)
|
||||
|
||||
|
||||
def printShowCrosTelemetry(mode):
|
||||
cm = build()
|
||||
cd = None
|
||||
parent = _get_customerid()
|
||||
todrive = False
|
||||
filter_ = None
|
||||
readMask = []
|
||||
orgUnitIdPathMap = {}
|
||||
diskpercentonly = False
|
||||
showOrgUnitPath = 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}
|
||||
i = 3
|
||||
if mode == 'info':
|
||||
if i >= len(sys.argv):
|
||||
controlflow.system_error_exit(3, f'<SerialNumber> required for "gam info crostelemetry"')
|
||||
filter_ = f'serialNumber={sys.argv[i]}'
|
||||
i += 1
|
||||
mode = 'show'
|
||||
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 in supported_readmask_map:
|
||||
readMask.append(supported_readmask_map[myarg])
|
||||
i += 1
|
||||
elif myarg == 'filter':
|
||||
filter_ = sys.argv[i+1]
|
||||
i += 2
|
||||
elif myarg in ['ou', 'org', 'orgunit']:
|
||||
_, orgUnitId = gapi_directory_orgunits.getOrgUnitId(sys.argv[i + 1], None)
|
||||
filter_ = f'orgUnitId={orgUnitId[3:]}'
|
||||
i += 2
|
||||
elif myarg == 'crossn':
|
||||
filter_ = f'serialNumber={sys.argv[i + 1]}'
|
||||
i += 2
|
||||
elif myarg == 'todrive':
|
||||
todrive = True
|
||||
i += 1
|
||||
elif myarg == 'showorgunitpath':
|
||||
showOrgUnitPath = True
|
||||
cd = gapi_directory.build()
|
||||
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']
|
||||
for cpuStatusReport in device.get('cpuStatusReport', []):
|
||||
for tempInfo in cpuStatusReport.pop('cpuTemperatureInfo', []):
|
||||
cpuStatusReport[f"cpuTemperatureInfo.{tempInfo['label'].strip()}"] = tempInfo['temperatureCelsius']
|
||||
if showOrgUnitPath:
|
||||
orgUnitId = device.get('orgUnitId')
|
||||
if orgUnitId not in orgUnitIdPathMap:
|
||||
orgUnitIdPathMap[orgUnitId] = gapi_directory_orgunits.orgunit_from_orgunitid(orgUnitId, cd)
|
||||
device['orgUnitPath'] = orgUnitIdPathMap[orgUnitId]
|
||||
if mode == '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 = [
|
||||
'version', 'count', 'channel', 'deviceOsVersion', 'system'
|
||||
]
|
||||
|
||||
@@ -86,6 +86,7 @@ def printshow_policies():
|
||||
for namespace in namespaces:
|
||||
spacing = ' '
|
||||
body['policySchemaFilter'] = f'{namespace}.*'
|
||||
body['pageToken'] = None
|
||||
try:
|
||||
policies = gapi.get_all_pages(svc.customers().policies(), 'resolve',
|
||||
items='resolvedPolicies',
|
||||
|
||||
@@ -405,7 +405,7 @@ def sync():
|
||||
controlflow.csv_field_error_exit(devicetype_column, input_file.fieldnames)
|
||||
if assettag_column and assettag_column not in input_file.fieldnames:
|
||||
controlflow.csv_field_error_exit(assettag_column, input_file.fieldnames)
|
||||
local_devices = []
|
||||
local_devices = {}
|
||||
for row in input_file:
|
||||
# upper() is very important to comparison since Google
|
||||
# always return uppercase serials
|
||||
@@ -414,28 +414,43 @@ def sync():
|
||||
local_device['deviceType'] = static_devicetype
|
||||
else:
|
||||
local_device['deviceType'] = row[devicetype_column].strip()
|
||||
sndt = f"{local_device['serialNumber']}-{local_device['deviceType']}"
|
||||
if assettag_column:
|
||||
local_device['assetTag'] = row[assettag_column].strip()
|
||||
local_devices.append(local_device)
|
||||
sndt += f"-{local_device['assetTag']}"
|
||||
local_devices[sndt] = local_device
|
||||
fileutils.close_file(f)
|
||||
page_message = gapi.got_total_items_msg('Company Devices', '...\n')
|
||||
device_fields = ['serialNumber', 'deviceType', 'lastSyncTime', 'name']
|
||||
if assettag_column:
|
||||
device_fields.append('assetTag')
|
||||
fields = f'nextPageToken,devices({",".join(device_fields)})'
|
||||
remote_devices = gapi.get_all_pages(ci.devices(), 'list', 'devices',
|
||||
remote_devices = {}
|
||||
remote_device_map = {}
|
||||
result = gapi.get_all_pages(ci.devices(), 'list', 'devices',
|
||||
customer=customer, page_message=page_message,
|
||||
pageSize=100, filter=device_filter, view='COMPANY_INVENTORY', fields=fields)
|
||||
remote_device_map = {}
|
||||
for remote_device in remote_devices:
|
||||
for remote_device in result:
|
||||
sn = remote_device['serialNumber']
|
||||
last_sync = remote_device.pop('lastSyncTime', NEVER_TIME_NOMS)
|
||||
name = remote_device.pop('name')
|
||||
remote_device_map[sn] = {'name': name}
|
||||
sndt = f"{remote_device['serialNumber']}-{remote_device['deviceType']}"
|
||||
if assettag_column:
|
||||
if 'assetTag' not in remote_device:
|
||||
remote_device['assetTag'] = ''
|
||||
sndt += f"-{remote_device['assetTag']}"
|
||||
remote_devices[sndt] = remote_device
|
||||
remote_device_map[sndt] = {'name': name}
|
||||
if last_sync == NEVER_TIME_NOMS:
|
||||
remote_device_map[sn]['unassigned'] = True
|
||||
devices_to_add = [device for device in local_devices if device not in remote_devices]
|
||||
missing_devices = [device for device in remote_devices if device not in local_devices]
|
||||
remote_device_map[sndt]['unassigned'] = True
|
||||
devices_to_add = []
|
||||
for sndt, device in iter(local_devices.items()):
|
||||
if sndt not in remote_devices:
|
||||
devices_to_add.append(device)
|
||||
missing_devices = []
|
||||
for sndt, device in iter(remote_devices.items()):
|
||||
if sndt not in local_devices:
|
||||
missing_devices.append(device)
|
||||
print(f'Need to add {len(devices_to_add)} and remove {len(missing_devices)} devices...')
|
||||
for add_device in devices_to_add:
|
||||
print(f'Creating {add_device["serialNumber"]}')
|
||||
@@ -447,8 +462,11 @@ def sync():
|
||||
print(f' {add_device["serialNumber"]} already exists')
|
||||
for missing_device in missing_devices:
|
||||
sn = missing_device['serialNumber']
|
||||
name = remote_device_map[sn]['name']
|
||||
unassigned = remote_device_map[sn].get('unassigned')
|
||||
sndt = f"{sn}-{missing_device['deviceType']}"
|
||||
if assettag_column:
|
||||
sndt += f"-{missing_device['assetTag']}"
|
||||
name = remote_device_map[sndt]['name']
|
||||
unassigned = remote_device_map[sndt].get('unassigned')
|
||||
action = unassigned_missing_action if unassigned else assigned_missing_action
|
||||
if action == 'donothing':
|
||||
pass
|
||||
|
||||
@@ -3,7 +3,7 @@ import sys
|
||||
import googleapiclient
|
||||
|
||||
import gam
|
||||
from gam.var import *
|
||||
from gam.var import * # pylint: disable=unused-wildcard-import
|
||||
from gam import controlflow
|
||||
from gam import display
|
||||
from gam import gapi
|
||||
@@ -12,6 +12,14 @@ from gam.gapi import errors as gapi_errors
|
||||
from gam.gapi import cloudidentity as gapi_cloudidentity
|
||||
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():
|
||||
ci = gapi_cloudidentity.build()
|
||||
@@ -73,9 +81,10 @@ def delete():
|
||||
|
||||
|
||||
def info():
|
||||
ci = gapi_cloudidentity.build('cloudidentity_beta')
|
||||
ci = gapi_cloudidentity.build(CIGROUP_API_BETA)
|
||||
group = gam.normalizeEmailAddressOrUID(sys.argv[3])
|
||||
getUsers = True
|
||||
getSecuritySettings = True
|
||||
showJoinDate = True
|
||||
showUpdateDate = False
|
||||
showMemberTree = False
|
||||
@@ -94,11 +103,20 @@ def info():
|
||||
elif myarg == 'membertree':
|
||||
showMemberTree = True
|
||||
i += 1
|
||||
elif myarg in ['nosecurity', 'nosecuritysettings']:
|
||||
getSecuritySettings = False
|
||||
else:
|
||||
controlflow.invalid_argument_exit(myarg, 'gam info cigroup')
|
||||
name = group_email_to_id(ci, group)
|
||||
basic_info = gapi.call(ci.groups(), 'get', name=name)
|
||||
display.print_json(basic_info)
|
||||
if getSecuritySettings:
|
||||
sec_info = gapi.call(ci.groups(),
|
||||
'getSecuritySettings',
|
||||
name=f'{name}/securitySettings',
|
||||
readMask='*')
|
||||
print(' Security settings:')
|
||||
display.print_json(sec_info, spacing=' ')
|
||||
if getUsers and not showMemberTree:
|
||||
if not showJoinDate and not showUpdateDate:
|
||||
view = 'BASIC'
|
||||
@@ -116,7 +134,7 @@ def info():
|
||||
print(' Members:')
|
||||
for member in members:
|
||||
role = get_single_role(member.get('roles', [])).lower()
|
||||
email = member.get('memberKey', {}).get('id')
|
||||
email = member.get(CIGROUP_MEMBERKEY, {}).get('id')
|
||||
member_type = member.get('type', 'USER').lower()
|
||||
jc_string = ''
|
||||
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]:
|
||||
member_id = member.get('name', '')
|
||||
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()
|
||||
if show_role:
|
||||
role = get_single_role(member.get('roles', [])).lower()
|
||||
@@ -187,9 +205,15 @@ GROUP_ROLES_MAP = {
|
||||
|
||||
|
||||
def print_():
|
||||
ci = gapi_cloudidentity.build('cloudidentity_beta')
|
||||
ci = gapi_cloudidentity.build(CIGROUP_API_BETA)
|
||||
i = 3
|
||||
members = membersCountOnly = managers = managersCountOnly = owners = ownersCountOnly = False
|
||||
members = False
|
||||
membersCountOnly = False
|
||||
managers = False
|
||||
managersCountOnly = False
|
||||
owners = False
|
||||
ownersCountOnly = False
|
||||
memberRestrictions = False
|
||||
gapi_directory_customer.setTrueCustomerId()
|
||||
parent = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
|
||||
usemember = None
|
||||
@@ -232,6 +256,15 @@ def print_():
|
||||
if myarg == 'managerscount':
|
||||
managersCountOnly = True
|
||||
i += 1
|
||||
elif myarg in ['memberrestrictions']:
|
||||
memberRestrictions = True
|
||||
display.add_titles_to_csv_file(
|
||||
['memberRestrictionQuery',],
|
||||
titles)
|
||||
display.add_titles_to_csv_file(
|
||||
['memberRestrictionEvaluation',],
|
||||
titles)
|
||||
i += 1
|
||||
else:
|
||||
controlflow.invalid_argument_exit(sys.argv[i], 'gam print cigroups')
|
||||
if roles:
|
||||
@@ -310,12 +343,12 @@ def print_():
|
||||
)
|
||||
page_message = gapi.got_total_items_first_last_msg('Members')
|
||||
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(),
|
||||
'list',
|
||||
'memberships',
|
||||
page_message=page_message,
|
||||
message_attribute=['memberKey', 'id'],
|
||||
message_attribute=[CIGROUP_MEMBERKEY, 'id'],
|
||||
soft_errors=True,
|
||||
parent=groupKey_id,
|
||||
view='BASIC')
|
||||
@@ -329,7 +362,7 @@ def print_():
|
||||
ownersList = []
|
||||
ownersCount = 0
|
||||
for member in groupMembers:
|
||||
member_email = member['memberKey']['id']
|
||||
member_email = member[CIGROUP_MEMBERKEY]['id']
|
||||
role = get_single_role(member.get('roles', []))
|
||||
if not validRoles or role in validRoles:
|
||||
if role == ROLE_MEMBER:
|
||||
@@ -363,6 +396,16 @@ def print_():
|
||||
group['OwnersCount'] = ownersCount
|
||||
if not ownersCountOnly:
|
||||
group['Owners'] = memberDelimiter.join(ownersList)
|
||||
if memberRestrictions:
|
||||
name = f'{groupKey_id}/securitySettings'
|
||||
print(f'Getting member restrictions for {groupEmail} ({i}/{count}')
|
||||
sec_info = gapi.call(ci.groups(),
|
||||
'getSecuritySettings',
|
||||
name=name,
|
||||
readMask='*')
|
||||
if 'memberRestriction' in sec_info:
|
||||
group['memberRestrictionQuery'] = sec_info['memberRestriction'].get('query', '')
|
||||
group['memberRestrictionEvaluation'] = sec_info['memberRestriction'].get('evaluation', {}).get('state', '')
|
||||
csvRows.append(group)
|
||||
if sortHeaders:
|
||||
display.sort_csv_titles([
|
||||
@@ -412,7 +455,7 @@ def _get_groups_list(ci=None, member=None, parent=None):
|
||||
|
||||
|
||||
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"
|
||||
result = gapi.call(ci.groups().memberships(),
|
||||
'getMembershipGraph',
|
||||
@@ -422,7 +465,7 @@ def get_membership_graph(member):
|
||||
|
||||
|
||||
def print_members():
|
||||
ci = gapi_cloudidentity.build('cloudidentity_beta')
|
||||
ci = gapi_cloudidentity.build(CIGROUP_API_BETA)
|
||||
todrive = False
|
||||
gapi_directory_customer.setTrueCustomerId()
|
||||
parent = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
|
||||
@@ -479,8 +522,8 @@ def print_members():
|
||||
view='FULL',
|
||||
pageSize=500,
|
||||
page_message=page_message,
|
||||
message_attribute=['memberKey', 'id'])
|
||||
#fields='nextPageToken,memberships(memberKey,roles,createTime,updateTime)')
|
||||
message_attribute=[CIGROUP_MEMBERKEY, 'id'])
|
||||
#fields=f'nextPageToken,memberships({CIGROUP_MEMBERKEY},roles,createTime,updateTime)')
|
||||
if roles:
|
||||
group_members = filter_members_to_roles(group_members, roles)
|
||||
for member in group_members:
|
||||
@@ -538,7 +581,7 @@ def update():
|
||||
]
|
||||
return (role, expireTime, users_email)
|
||||
|
||||
ci = gapi_cloudidentity.build('cloudidentity_beta')
|
||||
ci = gapi_cloudidentity.build(CIGROUP_API_BETA)
|
||||
group = sys.argv[3]
|
||||
myarg = sys.argv[4].lower()
|
||||
items = []
|
||||
@@ -565,7 +608,7 @@ def update():
|
||||
items.append(item)
|
||||
elif len(users_email) > 0:
|
||||
body = {
|
||||
'memberKey': {
|
||||
CIGROUP_MEMBERKEY: {
|
||||
'id': users_email[0]
|
||||
},
|
||||
'roles': [{
|
||||
@@ -785,12 +828,12 @@ def update():
|
||||
page_message=page_message,
|
||||
throw_reasons=gapi_errors.MEMBERS_THROW_REASONS,
|
||||
parent=parent,
|
||||
fields='nextPageToken,memberships(memberKey,roles)')
|
||||
fields=f'nextPageToken,memberships({CIGROUP_MEMBERKEY},roles)')
|
||||
result = filter_members_to_roles(result, roles)
|
||||
if not result:
|
||||
print('Group already has 0 members')
|
||||
return
|
||||
users_email = [member['memberKey']['id'] for member in result]
|
||||
users_email = [member[CIGROUP_MEMBERKEY]['id'] for member in result]
|
||||
sys.stderr.write(
|
||||
f'Group: {group}, Will remove {len(users_email)} {", ".join(roles).lower()}s.\n'
|
||||
)
|
||||
@@ -808,6 +851,7 @@ def update():
|
||||
else:
|
||||
i = 4
|
||||
body = {}
|
||||
sec_body = {}
|
||||
while i < len(sys.argv):
|
||||
myarg = sys.argv[i].lower().replace('_', '')
|
||||
if myarg == 'name':
|
||||
@@ -830,17 +874,41 @@ def update():
|
||||
}]
|
||||
}
|
||||
i += 2
|
||||
elif myarg in ['memberrestriction', 'memberrestrictions']:
|
||||
query = sys.argv[i + 1]
|
||||
member_types = {
|
||||
'USER': '1',
|
||||
'SERVICE_ACCOUNT': '2',
|
||||
'GROUP': '3',
|
||||
}
|
||||
for key, val in member_types.items():
|
||||
query = query.replace(key, val)
|
||||
sec_body['memberRestriction'] = {'query': query}
|
||||
i += 2
|
||||
else:
|
||||
controlflow.invalid_argument_exit(sys.argv[i],
|
||||
'gam update cigroup')
|
||||
updateMask = ','.join(body.keys())
|
||||
name = group_email_to_id(ci, group)
|
||||
print(f'Updating group {group}')
|
||||
gapi.call(ci.groups(),
|
||||
'patch',
|
||||
updateMask=updateMask,
|
||||
name=name,
|
||||
body=body)
|
||||
if body:
|
||||
updateMask = ','.join(body.keys())
|
||||
name = group_email_to_id(ci, group)
|
||||
print(f'Updating group {group}')
|
||||
gapi.call(ci.groups(),
|
||||
'patch',
|
||||
updateMask=updateMask,
|
||||
name=name,
|
||||
body=body)
|
||||
if sec_body:
|
||||
updateMask = 'member_restriction.query'
|
||||
# it seems like a bug that API requires /securitySettings
|
||||
# appended to name. We'll see if Google servers change this
|
||||
# at some point.
|
||||
name = f'{group_email_to_id(ci, group)}/securitySettings'
|
||||
print(f'Updating group {group} security settings')
|
||||
gapi.call(ci.groups(),
|
||||
'updateSecuritySettings',
|
||||
name=name,
|
||||
updateMask=updateMask,
|
||||
body=sec_body)
|
||||
|
||||
|
||||
def group_email_to_id(ci, group, i=0, count=0):
|
||||
|
||||
@@ -755,11 +755,11 @@ def doPrintCrosDevices():
|
||||
cros['autoUpdateExpiration'])
|
||||
row = {}
|
||||
for attrib in cros:
|
||||
if attrib not in set([
|
||||
if attrib not in {
|
||||
'kind', 'etag', 'tpmVersionInfo', 'recentUsers',
|
||||
'activeTimeRanges', 'deviceFiles', 'cpuStatusReports',
|
||||
'diskVolumeReports', 'systemRamFreeReports'
|
||||
]):
|
||||
}:
|
||||
row[attrib] = cros[attrib]
|
||||
if selectedLists.get('activeTimeRanges'):
|
||||
timergs = cros.get('activeTimeRanges', [])
|
||||
|
||||
@@ -266,6 +266,8 @@ GROUP_ATTRIBUTES_ARGUMENT_TO_PROPERTY_MAP = {
|
||||
'customReplyTo',
|
||||
'defaultmessagedenynotificationtext':
|
||||
'defaultMessageDenyNotificationText',
|
||||
'defaultsender':
|
||||
'defaultSender',
|
||||
'enablecollaborativeinbox':
|
||||
'enableCollaborativeInbox',
|
||||
'favoriterepliesontop':
|
||||
@@ -979,6 +981,9 @@ def update():
|
||||
sys.stderr.write(
|
||||
f'Group: {group}, Will add {len(to_add)} and remove {len(to_remove)} {role}s.\n'
|
||||
)
|
||||
for user in to_remove:
|
||||
items.append(
|
||||
['gam', 'update', 'group', group, 'remove', user])
|
||||
for user in to_add:
|
||||
item = ['gam', 'update', 'group', group, 'add']
|
||||
if role:
|
||||
@@ -987,9 +992,6 @@ def update():
|
||||
item.append(delivery)
|
||||
item.append(user)
|
||||
items.append(item)
|
||||
for user in to_remove:
|
||||
items.append(
|
||||
['gam', 'update', 'group', group, 'remove', user])
|
||||
elif myarg in ['delete', 'remove']:
|
||||
_, users_email, _ = _getRoleAndUsers()
|
||||
if not exists(cd, group):
|
||||
@@ -1219,7 +1221,7 @@ def getGroupAttrValue(myarg, value, gs_object, gs_body, function):
|
||||
params) in list(gs_object['schemas']['Groups']['properties'].items()):
|
||||
if attrib in ['kind', 'etag', 'email']:
|
||||
continue
|
||||
if myarg == attrib.lower():
|
||||
if myarg == attrib.lower().replace('_', ''):
|
||||
if params['type'] == 'integer':
|
||||
try:
|
||||
if value[-1:].upper() == 'M':
|
||||
|
||||
@@ -299,7 +299,7 @@ def update():
|
||||
|
||||
|
||||
def orgUnitPathQuery(path, checkSuspended):
|
||||
query = "orgUnitPath='{0}'".format(path.replace(
|
||||
query = "orgUnitPath='{}'".format(path.replace(
|
||||
"'", "\\'")) if path != '/' else ''
|
||||
if checkSuspended is not None:
|
||||
query += f' isSuspended={checkSuspended}'
|
||||
|
||||
@@ -60,6 +60,10 @@ class GapiGroupNotFoundError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class GapiInternalServerError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class GapiInvalidError(Exception):
|
||||
pass
|
||||
|
||||
@@ -125,6 +129,7 @@ class ErrorReason(Enum):
|
||||
GATEWAY_TIMEOUT = 'gatewayTimeout'
|
||||
GROUP_NOT_FOUND = 'groupNotFound'
|
||||
INTERNAL_ERROR = 'internalError'
|
||||
INTERNAL_SERVER_ERROR = 'internalServerError'
|
||||
INVALID = 'invalid'
|
||||
INVALID_ARGUMENT = 'invalidArgument'
|
||||
INVALID_MEMBER = 'invalidMember'
|
||||
@@ -199,6 +204,8 @@ ERROR_REASON_TO_EXCEPTION = {
|
||||
GapiGatewayTimeoutError,
|
||||
ErrorReason.GROUP_NOT_FOUND:
|
||||
GapiGroupNotFoundError,
|
||||
ErrorReason.INTERNAL_SERVER_ERROR:
|
||||
GapiInternalServerError,
|
||||
ErrorReason.INVALID:
|
||||
GapiInvalidError,
|
||||
ErrorReason.INVALID_ARGUMENT:
|
||||
@@ -336,6 +343,10 @@ def get_gapi_error_detail(e,
|
||||
if 'Requested entity was not found' in message or 'does not exist' in message:
|
||||
error = _create_http_error_dict(404, ErrorReason.NOT_FOUND.value,
|
||||
message)
|
||||
elif http_status == 500:
|
||||
if 'Failed to convert server response to JSON' in message:
|
||||
error = _create_http_error_dict(500, ErrorReason.INTERNAL_SERVER_ERROR.value,
|
||||
message)
|
||||
else:
|
||||
if 'error_description' in error:
|
||||
if error['error_description'] == 'Invalid Value':
|
||||
|
||||
@@ -504,9 +504,9 @@ def showReport():
|
||||
purge_parameters = True
|
||||
for event in events:
|
||||
for item in event.get('parameters', []):
|
||||
if set(item) == set(['value', 'name']):
|
||||
if set(item) == {'value', 'name'}:
|
||||
event[item['name']] = item['value']
|
||||
elif set(item) == set(['intValue', 'name']):
|
||||
elif set(item) == {'intValue', 'name'}:
|
||||
if item['name'] in ['start_time', 'end_time']:
|
||||
val = item.get('intValue')
|
||||
if val is not None:
|
||||
@@ -517,9 +517,9 @@ def showReport():
|
||||
val-62135683200).isoformat()
|
||||
else:
|
||||
event[item['name']] = item['intValue']
|
||||
elif set(item) == set(['boolValue', 'name']):
|
||||
elif set(item) == {'boolValue', 'name'}:
|
||||
event[item['name']] = item['boolValue']
|
||||
elif set(item) == set(['multiValue', 'name']):
|
||||
elif set(item) == {'multiValue', 'name'}:
|
||||
event[item['name']] = ' '.join(item['multiValue'])
|
||||
elif item['name'] == 'scope_data':
|
||||
parts = {}
|
||||
|
||||
@@ -790,7 +790,7 @@ def downloadExport():
|
||||
done = False
|
||||
while not done:
|
||||
status, done = downloader.next_chunk()
|
||||
sys.stdout.write(' Downloaded: {0:>7.2%}\r'.format(
|
||||
sys.stdout.write(' Downloaded: {:>7.2%}\r'.format(
|
||||
status.progress()))
|
||||
sys.stdout.flush()
|
||||
sys.stdout.write('\n Download complete. Flushing to disk...\n')
|
||||
|
||||
@@ -90,7 +90,7 @@ class Request(google_auth_httplib2.Request):
|
||||
@_force_user_agent(GAM_USER_AGENT)
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""Inserts the GAM user-agent header in requests."""
|
||||
return super(Request, self).__call__(*args, **kwargs)
|
||||
return super().__call__(*args, **kwargs)
|
||||
|
||||
|
||||
class AuthorizedHttp(google_auth_httplib2.AuthorizedHttp):
|
||||
@@ -99,4 +99,4 @@ class AuthorizedHttp(google_auth_httplib2.AuthorizedHttp):
|
||||
@_force_user_agent(GAM_USER_AGENT)
|
||||
def request(self, *args, **kwargs):
|
||||
"""Inserts the GAM user-agent header in requests."""
|
||||
return super(AuthorizedHttp, self).request(*args, **kwargs)
|
||||
return super().request(*args, **kwargs)
|
||||
|
||||
@@ -15,7 +15,7 @@ class CreateHttpTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
SetGlobalVariables()
|
||||
super(CreateHttpTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
def test_create_http_sets_default_values_on_http(self):
|
||||
http = transport.create_http()
|
||||
@@ -56,7 +56,7 @@ class TransportTest(unittest.TestCase):
|
||||
self.mock_content)
|
||||
self.mock_credentials = MagicMock()
|
||||
self.test_uri = 'http://example.com'
|
||||
super(TransportTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
@patch.object(transport, 'create_http')
|
||||
def test_create_request_uses_default_http(self, mock_create_http):
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import datetime
|
||||
import re
|
||||
import sys
|
||||
@@ -32,7 +28,7 @@ class LazyLoader(types.ModuleType):
|
||||
self._local_name = local_name
|
||||
self._parent_module_globals = parent_module_globals
|
||||
|
||||
super(LazyLoader, self).__init__(name)
|
||||
super().__init__(name)
|
||||
|
||||
def _load(self):
|
||||
# Import the target module and insert it into the parent's namespace
|
||||
@@ -123,7 +119,7 @@ def dehtml(text):
|
||||
|
||||
|
||||
def indentMultiLineText(message, n=0):
|
||||
return message.replace('\n', '\n{0}'.format(' ' * n)).rstrip()
|
||||
return message.replace('\n', '\n{}'.format(' ' * n)).rstrip()
|
||||
|
||||
|
||||
def flatten_json(structure, key='', path='', flattened=None, listLimit=None):
|
||||
|
||||
@@ -8,7 +8,7 @@ import platform
|
||||
import re
|
||||
|
||||
GAM_AUTHOR = 'Jay Lee <jay0lee@gmail.com>'
|
||||
GAM_VERSION = '6.07'
|
||||
GAM_VERSION = '6.13'
|
||||
GAM_LICENSE = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
|
||||
|
||||
GAM_URL = 'https://git.io/gam'
|
||||
@@ -124,7 +124,7 @@ SKUS = {
|
||||
'Google-Apps': {
|
||||
'product': 'Google-Apps',
|
||||
'aliases': ['standard', 'free'],
|
||||
'displayName': 'G Suite Free/Standard'
|
||||
'displayName': 'G Suite Legacy'
|
||||
},
|
||||
'Google-Apps-For-Business': {
|
||||
'product': 'Google-Apps',
|
||||
@@ -458,6 +458,7 @@ DRIVEFILE_FIELDS_CHOICES_MAP = {
|
||||
'createddate': 'createdDate',
|
||||
'createdtime': 'createdDate',
|
||||
'description': 'description',
|
||||
'driveid': 'driveId',
|
||||
'editable': 'editable',
|
||||
'explicitlytrashed': 'explicitlyTrashed',
|
||||
'fileextension': 'fileExtension',
|
||||
@@ -500,6 +501,7 @@ DRIVEFILE_FIELDS_CHOICES_MAP = {
|
||||
'sharedwithmedate': 'sharedWithMeDate',
|
||||
'sharedwithmetime': 'sharedWithMeDate',
|
||||
'sharinguser': 'sharingUser',
|
||||
'shortcutdetails': 'shortcutDetails',
|
||||
'spaces': 'spaces',
|
||||
'thumbnaillink': 'thumbnailLink',
|
||||
'title': 'title',
|
||||
@@ -616,17 +618,22 @@ GOOGLEDOC_VALID_EXTENSIONS_MAP = {
|
||||
}
|
||||
|
||||
MACOS_CODENAMES = {
|
||||
6: 'Snow Leopard',
|
||||
7: 'Lion',
|
||||
8: 'Mountain Lion',
|
||||
9: 'Mavericks',
|
||||
10: 'Yosemite',
|
||||
11: 'El Capitan',
|
||||
12: 'Sierra',
|
||||
13: 'High Sierra',
|
||||
14: 'Mojave',
|
||||
15: 'Catalina'
|
||||
}
|
||||
10: {
|
||||
6: 'Snow Leopard',
|
||||
7: 'Lion',
|
||||
8: 'Mountain Lion',
|
||||
9: 'Mavericks',
|
||||
10: 'Yosemite',
|
||||
11: 'El Capitan',
|
||||
12: 'Sierra',
|
||||
13: 'High Sierra',
|
||||
14: 'Mojave',
|
||||
15: 'Catalina',
|
||||
16: 'Big Sur'
|
||||
},
|
||||
11: 'Big Sur',
|
||||
12: 'Monterey',
|
||||
}
|
||||
|
||||
_MICROSOFT_FORMATS_LIST = [{
|
||||
'mime':
|
||||
@@ -891,7 +898,6 @@ RT_TAG_REPLACE_PATTERN = re.compile(r'{(.*?)}')
|
||||
LOWERNUMERIC_CHARS = string.ascii_lowercase + string.digits
|
||||
ALPHANUMERIC_CHARS = LOWERNUMERIC_CHARS + string.ascii_uppercase
|
||||
URL_SAFE_CHARS = ALPHANUMERIC_CHARS + '-._~'
|
||||
FILENAME_SAFE_CHARS = ALPHANUMERIC_CHARS + '-_.() '
|
||||
|
||||
FILTER_ADD_LABEL_TO_ARGUMENT_MAP = {
|
||||
'IMPORTANT': 'important',
|
||||
@@ -1070,7 +1076,7 @@ COLLABORATIVE_INBOX_ATTRIBUTES = [
|
||||
'favoriteRepliesOnTop',
|
||||
]
|
||||
|
||||
GROUP_SETTINGS_LIST_ATTRIBUTES = set([
|
||||
GROUP_SETTINGS_LIST_ATTRIBUTES = {
|
||||
# ACL choices
|
||||
'whoCanAdd',
|
||||
'whoCanApproveMembers',
|
||||
@@ -1106,12 +1112,13 @@ GROUP_SETTINGS_LIST_ATTRIBUTES = set([
|
||||
'whoCanUnmarkFavoriteReplyOnAnyTopic',
|
||||
'whoCanViewGroup',
|
||||
'whoCanViewMembership',
|
||||
# Miscellaneous hoices
|
||||
# Miscellaneous choices
|
||||
'default_sender',
|
||||
'messageModerationLevel',
|
||||
'replyTo',
|
||||
'spamModerationLevel',
|
||||
])
|
||||
GROUP_SETTINGS_BOOLEAN_ATTRIBUTES = set([
|
||||
}
|
||||
GROUP_SETTINGS_BOOLEAN_ATTRIBUTES = {
|
||||
'allowExternalMembers',
|
||||
'allowGoogleCommunication',
|
||||
'allowWebPosting',
|
||||
@@ -1124,7 +1131,7 @@ GROUP_SETTINGS_BOOLEAN_ATTRIBUTES = set([
|
||||
'membersCanPostAsTheGroup',
|
||||
'sendMessageDenyNotification',
|
||||
'showInGroupDirectory',
|
||||
])
|
||||
}
|
||||
|
||||
#
|
||||
# Global variables
|
||||
@@ -1241,10 +1248,12 @@ GC_DOMAIN = 'domain'
|
||||
GC_DRIVE_DIR = 'drive_dir'
|
||||
# Enable Delegated Admin Service Accounts
|
||||
GC_ENABLE_DASA = 'enabledasa'
|
||||
# If no_browser is False, writeCSVfile won't open a browser when todrive is set
|
||||
# If no_browser is True, writeCSVfile won't open a browser when todrive is set
|
||||
# and doRequestOAuth prints a link and waits for the verification code when
|
||||
# oauth2.txt is being created
|
||||
GC_NO_BROWSER = 'no_browser'
|
||||
# If no_tdemail is True, writeCSVfile won't send an email
|
||||
GC_NO_TDEMAIL = 'no_tdemail'
|
||||
# oauth_browser forces usage of web server OAuth flow that proved problematic.
|
||||
GC_OAUTH_BROWSER = 'oauth_browser'
|
||||
# Disable GAM API caching
|
||||
@@ -1282,7 +1291,7 @@ GC_TLS_MAX_VERSION = 'tls_max_ver'
|
||||
# Path to certificate authority file for validating TLS hosts
|
||||
GC_CA_FILE = 'ca_file'
|
||||
|
||||
TLS_MIN = 'TLSv1_2' if hasattr(ssl.SSLContext(), 'minimum_version') else None
|
||||
TLS_MIN = 'TLSv1_3' if hasattr(ssl.SSLContext(), 'minimum_version') else None
|
||||
GC_Defaults = {
|
||||
GC_ADMIN_EMAIL: '',
|
||||
GC_AUTO_BATCH_MIN: 0,
|
||||
@@ -1299,6 +1308,7 @@ GC_Defaults = {
|
||||
GC_DRIVE_DIR: '',
|
||||
GC_ENABLE_DASA: False,
|
||||
GC_NO_BROWSER: False,
|
||||
GC_NO_TDEMAIL: False,
|
||||
GC_NO_CACHE: False,
|
||||
GC_NO_SHORT_URLS: False,
|
||||
GC_NO_UPDATE_CHECK: False,
|
||||
@@ -1384,6 +1394,9 @@ GC_VAR_INFO = {
|
||||
GC_NO_BROWSER: {
|
||||
GC_VAR_TYPE: GC_TYPE_BOOLEAN
|
||||
},
|
||||
GC_NO_TDEMAIL: {
|
||||
GC_VAR_TYPE: GC_TYPE_BOOLEAN
|
||||
},
|
||||
GC_NO_CACHE: {
|
||||
GC_VAR_TYPE: GC_TYPE_BOOLEAN
|
||||
},
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
yubikey-manager>=4.0.0
|
||||
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
|
||||
google-auth>=2.3.2
|
||||
httplib2>=0.17.0
|
||||
importlib.metadata; python_version < '3.8'
|
||||
passlib>=1.7.2
|
||||
python-dateutil
|
||||
yubikey-manager>=4.0.0
|
||||
pathvalidate
|
||||
|
||||
49
src/setup.cfg
Normal file
49
src/setup.cfg
Normal file
@@ -0,0 +1,49 @@
|
||||
[metadata]
|
||||
name = GAM for Google Workspace
|
||||
version = 6.0.7
|
||||
description = Command line management for Google Workspaces
|
||||
long_description = file: readme.md
|
||||
long_description_content_type = text/markdown
|
||||
url = https://github.com/jay0lee/GAM
|
||||
author = Jay Lee
|
||||
author_email = jay0lee@gmail.com
|
||||
license = Apache
|
||||
license_files = LICENSE
|
||||
keywords = google, oauth2, gsuite, google-apps, google-admin-sdk, google-drive, google-cloud, google-calendar, gam, google-api, oauth2-client, google-workspace
|
||||
classifiers =
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3 :: Only
|
||||
Programming Language :: Python :: 3.6
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
License :: OSI Approved :: Apache License
|
||||
|
||||
[options]
|
||||
packages = find:
|
||||
python_requires = >=3.6
|
||||
install_requires =
|
||||
cryptography
|
||||
distro; sys_platform == 'linux'
|
||||
filelock
|
||||
google-api-python-client >= 2.1
|
||||
google-auth-httplib2
|
||||
google-auth-oauthlib >= 0.4.1
|
||||
google-auth >= 1.11.2
|
||||
httplib2 >= 0.17.0
|
||||
importlib.metadata; python_version < '3.8'
|
||||
passlib >= 1.7.2
|
||||
python-dateutil
|
||||
yubikey-manager >= 4.0.0
|
||||
pathvalidate
|
||||
|
||||
# used during pip install .[test]
|
||||
[options.extras_require]
|
||||
test = pre-commit
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
gam = gam.__main__:main
|
||||
|
||||
[bdist_wheel]
|
||||
universal = True
|
||||
3
src/setup.py
Normal file
3
src/setup.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from setuptools import setup
|
||||
|
||||
setup()
|
||||
@@ -1,15 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
#from packaging import version
|
||||
from distutils.version import LooseVersion
|
||||
from packaging import version
|
||||
import sys
|
||||
|
||||
a = sys.argv[1]
|
||||
b = sys.argv[2]
|
||||
#result = version.parse(a) >= version.parse(b)
|
||||
result = LooseVersion(a) >= LooseVersion(b)
|
||||
result = version.parse(a) >= version.parse(b)
|
||||
if result:
|
||||
print('OK: %s is equal or newer than %s' % (a, b))
|
||||
print(f'OK: {a} is equal or newer than {b}')
|
||||
else:
|
||||
print('ERROR: %s is older than %s' % (a, b))
|
||||
print(f'ERROR: {a} is older than {b}')
|
||||
sys.exit(not result)
|
||||
|
||||
Reference in New Issue
Block a user