mirror of
https://github.com/GAM-team/GAM.git
synced 2026-07-06 13:51:36 +00:00
Compare commits
178 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82dcc4de6a | ||
|
|
f7a426f65a | ||
|
|
a94ef78066 | ||
|
|
62d738f5c2 | ||
|
|
1c56a0a608 | ||
|
|
dc3976bdda | ||
|
|
454778b190 | ||
|
|
5e78c93b71 | ||
|
|
3aefe21f16 | ||
|
|
0fc7958ccc | ||
|
|
13dc4e74c9 | ||
|
|
a17fa16841 | ||
|
|
b13757f5d3 | ||
|
|
b9df6f4762 | ||
|
|
b7d1c62486 | ||
|
|
90e5f1b665 | ||
|
|
3132fd7783 | ||
|
|
87808902e6 | ||
|
|
fb33d8186e | ||
|
|
8bd2e7f879 | ||
|
|
e66744e3f1 | ||
|
|
85f2979313 | ||
|
|
a85ee9b108 | ||
|
|
9ab2f38436 | ||
|
|
5bcdca4fcc | ||
|
|
729edb65be | ||
|
|
db8afb769b | ||
|
|
7dfc93892c | ||
|
|
d278cb6939 | ||
|
|
bced5172d2 | ||
|
|
bb5beb66a7 | ||
|
|
f849b6ddb7 | ||
|
|
d2733a53a2 | ||
|
|
1b1ae44f5d | ||
|
|
8515dc2616 | ||
|
|
ba7a8d8937 | ||
|
|
d543fb9917 | ||
|
|
f4d390b77b | ||
|
|
ffbce1fd25 | ||
|
|
2d78ec6edd | ||
|
|
9cacdd166f | ||
|
|
9af0a5d843 | ||
|
|
3313295532 | ||
|
|
fdf6c147dc | ||
|
|
323dbd5ca9 | ||
|
|
d01fd74fa3 | ||
|
|
8c33b88e3e | ||
|
|
5d11397fca | ||
|
|
995321978f | ||
|
|
448789dad0 | ||
|
|
e9ba6819ba | ||
|
|
3056c7b803 | ||
|
|
f2c28fd1f7 | ||
|
|
11e4ff1eb5 | ||
|
|
81cd74c244 | ||
|
|
faade7c057 | ||
|
|
0032066e1d | ||
|
|
dd938baced | ||
|
|
b835b6ee36 | ||
|
|
3660d65df6 | ||
|
|
3e0b4125e0 | ||
|
|
9820a3d81e | ||
|
|
b670a4cee6 | ||
|
|
a5dd5275c8 | ||
|
|
9b6ad2fa60 | ||
|
|
1d80028c93 | ||
|
|
a013e95fcf | ||
|
|
eb4d6ece3f | ||
|
|
a50d1ef456 | ||
|
|
c179ed732c | ||
|
|
a85a313ebb | ||
|
|
534ccd275d | ||
|
|
3c3d043276 | ||
|
|
786adb7c44 | ||
|
|
bb6c8dc225 | ||
|
|
a7cd88b2be | ||
|
|
a9fad337e2 | ||
|
|
d4dc1b1589 | ||
|
|
ad94adbb53 | ||
|
|
b692799dcb | ||
|
|
04dcf47746 | ||
|
|
aebb3c44fe | ||
|
|
8cf345196a | ||
|
|
173fdb2297 | ||
|
|
120db6e7d8 | ||
|
|
55555506be | ||
|
|
41965e962d | ||
|
|
30fdd00d65 | ||
|
|
37e3fd904d | ||
|
|
dc22b024b8 | ||
|
|
f412d5ad4c | ||
|
|
24cfe807e6 | ||
|
|
6a721ac2c1 | ||
|
|
4a4b22dfba | ||
|
|
6d4524c153 | ||
|
|
d7b2f82a4a | ||
|
|
844a2fe1e8 | ||
|
|
baf822c685 | ||
|
|
f3169a631c | ||
|
|
d171db36bc | ||
|
|
34c7576cd5 | ||
|
|
f859d0678b | ||
|
|
0986cb3fd9 | ||
|
|
645fd9a135 | ||
|
|
9582e6840a | ||
|
|
a8a9cfb2ab | ||
|
|
5519b33a08 | ||
|
|
976ef0252e | ||
|
|
e6829d0804 | ||
|
|
9f985a7b26 | ||
|
|
a628aeb1a8 | ||
|
|
d81c80b150 | ||
|
|
63ee016691 | ||
|
|
4935385572 | ||
|
|
30069d3039 | ||
|
|
3ef8a5a762 | ||
|
|
b12fda5007 | ||
|
|
26925c30c1 | ||
|
|
4085816fa3 | ||
|
|
7e36e5abe6 | ||
|
|
2037189148 | ||
|
|
c7781e66e1 | ||
|
|
8843675ad4 | ||
|
|
c05a1ea6b4 | ||
|
|
d9a5ac849b | ||
|
|
51d4c29dd5 | ||
|
|
c2bb9cbdaf | ||
|
|
d185765831 | ||
|
|
f57f311f16 | ||
|
|
4c81849c60 | ||
|
|
156c8319d9 | ||
|
|
b8de3310d0 | ||
|
|
f28cf664cb | ||
|
|
02b876155a | ||
|
|
97bd1f71c3 | ||
|
|
8be4445f0d | ||
|
|
550cf47db4 | ||
|
|
05d32eec08 | ||
|
|
59c181eeda | ||
|
|
dd5fd2a2c3 | ||
|
|
6ab8fbf538 | ||
|
|
509919da84 | ||
|
|
04bd5f36a0 | ||
|
|
801f5b7861 | ||
|
|
09d86e1220 | ||
|
|
6110aa1d32 | ||
|
|
11e6c80dbf | ||
|
|
1f32536ff7 | ||
|
|
7979206f21 | ||
|
|
f7901790ad | ||
|
|
7fae16f962 | ||
|
|
1dd76012f8 | ||
|
|
8fd3f4ee7d | ||
|
|
e30b8ed53e | ||
|
|
e0960d9113 | ||
|
|
35dda1cd34 | ||
|
|
ef2253fe58 | ||
|
|
ecea3aed7e | ||
|
|
2e81cae271 | ||
|
|
080eede356 | ||
|
|
fe37c687e4 | ||
|
|
27efef1d9b | ||
|
|
52aa1ac0da | ||
|
|
b5c23fdb83 | ||
|
|
0b16c9aef4 | ||
|
|
3be97acd9c | ||
|
|
8df8e6797f | ||
|
|
156ba44656 | ||
|
|
1b3663d60c | ||
|
|
8f0ea2f6a5 | ||
|
|
5e34b12e5c | ||
|
|
d124575a91 | ||
|
|
f5364ab4d0 | ||
|
|
b5580c5649 | ||
|
|
e9200ea8fb | ||
|
|
2e0c280ea6 | ||
|
|
3948a414b5 | ||
|
|
2c83068605 |
BIN
.github/actions/creds.tar.gpg
vendored
BIN
.github/actions/creds.tar.gpg
vendored
Binary file not shown.
BIN
.github/actions/creds.tar.xz.gpg
vendored
Normal file
BIN
.github/actions/creds.tar.xz.gpg
vendored
Normal file
Binary file not shown.
3
.github/actions/decrypt.sh
vendored
3
.github/actions/decrypt.sh
vendored
@@ -14,4 +14,5 @@ gpg --quiet --batch --yes --decrypt --passphrase="${PASSCODE}" \
|
|||||||
--output "${credsfile}" "${gpgfile}"
|
--output "${credsfile}" "${gpgfile}"
|
||||||
|
|
||||||
tar xvvf "${credsfile}" --directory "${gampath}"
|
tar xvvf "${credsfile}" --directory "${gampath}"
|
||||||
ls -l "${gampath}"
|
rm -rvf "${gpgfile}"
|
||||||
|
rm -rvf "${credsfile}"
|
||||||
|
|||||||
314
.github/workflows/build.yml
vendored
314
.github/workflows/build.yml
vendored
@@ -22,71 +22,98 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: ubuntu-22.04
|
- os: ubuntu-20.04
|
||||||
jid: 1
|
jid: 1
|
||||||
goal: build
|
goal: build
|
||||||
arch: x86_64
|
arch: x86_64
|
||||||
openssl_archs: linux-x86_64
|
openssl_archs: linux-x86_64
|
||||||
- os: [self-hosted, linux, arm64]
|
- os: [self-hosted, linux, arm64, gcp]
|
||||||
jid: 2
|
jid: 2
|
||||||
goal: build
|
goal: build
|
||||||
arch: aarch64
|
arch: aarch64
|
||||||
openssl_archs: linux-aarch64
|
openssl_archs: linux-aarch64
|
||||||
- os: macos-12
|
- os: ubuntu-20.04
|
||||||
|
jid: 3
|
||||||
|
goal: build
|
||||||
|
arch: x86_64
|
||||||
|
openssl_archs: linux-x86_64
|
||||||
|
staticx: yes
|
||||||
|
- os: [self-hosted, linux, arm64, gcp]
|
||||||
jid: 4
|
jid: 4
|
||||||
goal: build
|
goal: build
|
||||||
|
arch: aarch64
|
||||||
|
openssl_archs: linux-aarch64
|
||||||
|
staticx: yes
|
||||||
|
- os: macos-12
|
||||||
|
jid: 5
|
||||||
|
goal: build
|
||||||
arch: universal2
|
arch: universal2
|
||||||
openssl_archs: darwin64-x86_64 darwin64-arm64
|
openssl_archs: darwin64-x86_64 darwin64-arm64
|
||||||
- os: windows-2022
|
- os: windows-2022
|
||||||
jid: 5
|
jid: 6
|
||||||
goal: build
|
goal: build
|
||||||
arch: Win64
|
arch: Win64
|
||||||
openssl_archs: VC-WIN64A
|
openssl_archs: VC-WIN64A
|
||||||
- os: windows-2022
|
- os: windows-2022
|
||||||
jid: 6
|
jid: 7
|
||||||
goal: build
|
goal: build
|
||||||
arch: Win32
|
arch: Win32
|
||||||
openssl_archs: VC-WIN32
|
openssl_archs: VC-WIN32
|
||||||
- os: ubuntu-22.04
|
- os: ubuntu-22.04
|
||||||
goal: test
|
goal: test
|
||||||
python: "3.7"
|
python: "3.7"
|
||||||
jid: 7
|
|
||||||
arch: x86_64
|
|
||||||
- os: ubuntu-22.04
|
|
||||||
goal: test
|
|
||||||
python: "3.8"
|
|
||||||
jid: 8
|
jid: 8
|
||||||
arch: x86_64
|
arch: x86_64
|
||||||
- os: ubuntu-22.04
|
- os: ubuntu-22.04
|
||||||
goal: test
|
goal: test
|
||||||
python: "3.9"
|
python: "3.8"
|
||||||
jid: 9
|
jid: 9
|
||||||
arch: x86_64
|
arch: x86_64
|
||||||
- os: ubuntu-22.04
|
- os: ubuntu-22.04
|
||||||
goal: test
|
goal: test
|
||||||
python: "3.11-dev"
|
python: "3.9"
|
||||||
jid: 10
|
jid: 10
|
||||||
arch: x86_64
|
arch: x86_64
|
||||||
|
- os: ubuntu-22.04
|
||||||
|
goal: test
|
||||||
|
python: "3.10"
|
||||||
|
jid: 11
|
||||||
|
arch: x86_64
|
||||||
|
- os: ubuntu-22.04
|
||||||
|
goal: test
|
||||||
|
python: "3.12.0-alpha - 3.12"
|
||||||
|
jid: 12
|
||||||
|
arch: x86_64
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@master
|
|
||||||
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Cache multiple paths
|
- name: Cache multiple paths
|
||||||
uses: actions/cache@v2
|
if: matrix.goal == 'build'
|
||||||
|
uses: actions/cache@v3
|
||||||
id: cache-python-ssl
|
id: cache-python-ssl
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
bin
|
bin.tar.xz
|
||||||
key: gam-${{ matrix.jid }}-20220802
|
src/cpython
|
||||||
|
key: gam-${{ matrix.jid }}-20230208
|
||||||
|
|
||||||
|
- name: Untar Cache archive
|
||||||
|
if: matrix.goal == 'build' && steps.cache-python-ssl.outputs.cache-hit == 'true'
|
||||||
|
working-directory: ${{ github.workspace }}
|
||||||
|
run: |
|
||||||
|
tar xvvf bin.tar.xz
|
||||||
|
|
||||||
- name: Use pre-compiled Python for testing
|
- name: Use pre-compiled Python for testing
|
||||||
if: matrix.python != ''
|
if: matrix.python != ''
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python }}
|
python-version: ${{ matrix.python }}
|
||||||
|
|
||||||
@@ -117,17 +144,17 @@ jobs:
|
|||||||
sudo apt-get -qq --yes update
|
sudo apt-get -qq --yes update
|
||||||
sudo apt-get -qq --yes install swig libpcsclite-dev
|
sudo apt-get -qq --yes install swig libpcsclite-dev
|
||||||
|
|
||||||
- name: MacOS remove Homebrew
|
#- name: MacOS remove Homebrew
|
||||||
if: runner.os == 'macOS'
|
# if: runner.os == 'macOS'
|
||||||
run: |
|
# run: |
|
||||||
# remove everything except the libraries needed by yubikey-manager
|
# # remove everything except the libraries needed by yubikey-manager
|
||||||
brew uninstall $(brew list | grep -v 'pcre\|swig\|pcsc-lite')
|
# brew uninstall $(brew list | grep -v 'pcre\|swig\|pcsc-lite')
|
||||||
|
|
||||||
- name: MacOS install tools
|
- name: MacOS install tools
|
||||||
if: runner.os == 'macOS'
|
if: runner.os == 'macOS'
|
||||||
run: |
|
run: |
|
||||||
# Install latest Rust
|
# Install latest Rust
|
||||||
curl -fsS -o rust.sh https://sh.rustup.rs
|
curl --retry 5 --retry-connrefused -fsS -o rust.sh https://sh.rustup.rs
|
||||||
bash ./rust.sh -y
|
bash ./rust.sh -y
|
||||||
source $HOME/.cargo/env
|
source $HOME/.cargo/env
|
||||||
# needed for Rust to compile cryptography Python package for universal2
|
# needed for Rust to compile cryptography Python package for universal2
|
||||||
@@ -145,8 +172,10 @@ jobs:
|
|||||||
arch: ${{ matrix.arch }}
|
arch: ${{ matrix.arch }}
|
||||||
jid: ${{ matrix.jid }}
|
jid: ${{ matrix.jid }}
|
||||||
openssl_archs: ${{ matrix.openssl_archs }}
|
openssl_archs: ${{ matrix.openssl_archs }}
|
||||||
|
staticx: ${{ matrix.staticx }}
|
||||||
run: |
|
run: |
|
||||||
echo "We are running on ${RUNNER_OS}"
|
echo "We are running on ${RUNNER_OS}"
|
||||||
|
LD_LIBRARY_PATH="${OPENSSL_INSTALL_PATH}/lib:${PYTHON_INSTALL_PATH}/lib"
|
||||||
if [[ "${arch}" == "Win64" ]]; then
|
if [[ "${arch}" == "Win64" ]]; then
|
||||||
PYEXTERNALS_PATH="amd64"
|
PYEXTERNALS_PATH="amd64"
|
||||||
PYBUILDRELEASE_ARCH="x64"
|
PYBUILDRELEASE_ARCH="x64"
|
||||||
@@ -178,21 +207,22 @@ jobs:
|
|||||||
MAKE=nmake
|
MAKE=nmake
|
||||||
MAKEOPT=""
|
MAKEOPT=""
|
||||||
PERL="c:\strawberry\perl\bin\perl.exe"
|
PERL="c:\strawberry\perl\bin\perl.exe"
|
||||||
echo "PYTHON=${PYTHON_INSTALL_PATH}\python.exe" >> $GITHUB_ENV
|
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${PYTHON_SOURCE_PATH}/PCbuild/${PYEXTERNALS_PATH}"
|
||||||
|
echo "PYTHON=${PYTHON_SOURCE_PATH}/PCbuild/${PYEXTERNALS_PATH}/python.exe" >> $GITHUB_ENV
|
||||||
echo "GAM_ARCHIVE_ARCH=${GAM_ARCHIVE_ARCH}" >> $GITHUB_ENV
|
echo "GAM_ARCHIVE_ARCH=${GAM_ARCHIVE_ARCH}" >> $GITHUB_ENV
|
||||||
echo "WIX_ARCH=${WIX_ARCH}" >> $GITHUB_ENV
|
echo "WIX_ARCH=${WIX_ARCH}" >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
echo "We'll run make with: ${MAKEOPT}"
|
echo "We'll run make with: ${MAKEOPT}"
|
||||||
echo "JID=${jid}" >> $GITHUB_ENV
|
echo "JID=${jid}" >> $GITHUB_ENV
|
||||||
|
echo "staticx=${staticx}" >> $GITHUB_ENV
|
||||||
echo "arch=${arch}" >> $GITHUB_ENV
|
echo "arch=${arch}" >> $GITHUB_ENV
|
||||||
|
echo "LD_LIBRARY_PATH=${LD_LIBRARY_PATH}" >> $GITHUB_ENV
|
||||||
echo "MAKE=${MAKE}" >> $GITHUB_ENV
|
echo "MAKE=${MAKE}" >> $GITHUB_ENV
|
||||||
echo "MAKEOPT=${MAKEOPT}" >> $GITHUB_ENV
|
echo "MAKEOPT=${MAKEOPT}" >> $GITHUB_ENV
|
||||||
echo "PERL=${PERL}" >> $GITHUB_ENV
|
echo "PERL=${PERL}" >> $GITHUB_ENV
|
||||||
echo "PYEXTERNALS_PATH=${PYEXTERNALS_PATH}" >> $GITHUB_ENV
|
echo "PYEXTERNALS_PATH=${PYEXTERNALS_PATH}" >> $GITHUB_ENV
|
||||||
echo "PYBUILDRELEASE_ARCH=${PYBUILDRELEASE_ARCH}" >> $GITHUB_ENV
|
echo "PYBUILDRELEASE_ARCH=${PYBUILDRELEASE_ARCH}" >> $GITHUB_ENV
|
||||||
echo "openssl_archs=${openssl_archs}" >> $GITHUB_ENV
|
echo "openssl_archs=${openssl_archs}" >> $GITHUB_ENV
|
||||||
echo "LD_LIBRARY_PATH=${OPENSSL_INSTALL_PATH}/lib:${PYTHON_INSTALL_PATH}/lib" >> $GITHUB_ENV
|
|
||||||
#echo "PATH=${PATH}:${PYTHON_INSTALL_PATH}/scripts" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Get latest stable OpenSSL source
|
- name: Get latest stable OpenSSL source
|
||||||
if: matrix.goal == 'build' && steps.cache-python-ssl.outputs.cache-hit != 'true'
|
if: matrix.goal == 'build' && steps.cache-python-ssl.outputs.cache-hit != 'true'
|
||||||
@@ -330,7 +360,7 @@ jobs:
|
|||||||
$env:OPENSSL_EXT_TARGET_PATH = "${env:OPENSSL_EXT_PATH}${env:PYEXTERNALS_PATH}"
|
$env:OPENSSL_EXT_TARGET_PATH = "${env:OPENSSL_EXT_PATH}${env:PYEXTERNALS_PATH}"
|
||||||
echo "Copying our OpenSSL to ${env:OPENSSL_EXT_TARGET_PATH}"
|
echo "Copying our OpenSSL to ${env:OPENSSL_EXT_TARGET_PATH}"
|
||||||
mkdir "${env:OPENSSL_EXT_TARGET_PATH}\include\openssl\"
|
mkdir "${env:OPENSSL_EXT_TARGET_PATH}\include\openssl\"
|
||||||
Copy-Item -Path "${env:GITHUB_WORKSPACE}/src/openssl-${env:openssl_archs}\LICENSE.txt" -Destination "${env:OPENSSL_EXT_TARGET_PATH}\LICENSE"
|
Copy-Item -Path "${env:GITHUB_WORKSPACE}/src/openssl-${env:openssl_archs}\LICENSE.txt" -Destination "${env:OPENSSL_EXT_TARGET_PATH}\LICENSE" -Verbose
|
||||||
cp -v "$env:OPENSSL_INSTALL_PATH\lib\*" "${env:OPENSSL_EXT_TARGET_PATH}"
|
cp -v "$env:OPENSSL_INSTALL_PATH\lib\*" "${env:OPENSSL_EXT_TARGET_PATH}"
|
||||||
cp -v "$env:OPENSSL_INSTALL_PATH\bin\*" "${env:OPENSSL_EXT_TARGET_PATH}"
|
cp -v "$env:OPENSSL_INSTALL_PATH\bin\*" "${env:OPENSSL_EXT_TARGET_PATH}"
|
||||||
cp -v "$env:OPENSSL_INSTALL_PATH\include\openssl\*" "${env:OPENSSL_EXT_TARGET_PATH}\include\openssl\"
|
cp -v "$env:OPENSSL_INSTALL_PATH\include\openssl\*" "${env:OPENSSL_EXT_TARGET_PATH}\include\openssl\"
|
||||||
@@ -350,22 +380,10 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cd "${env:PYTHON_SOURCE_PATH}"
|
cd "${env:PYTHON_SOURCE_PATH}"
|
||||||
# We need out custom openssl.props which uses OpenSSL 3 DLL names
|
# We need out custom openssl.props which uses OpenSSL 3 DLL names
|
||||||
Copy-Item -Path "${env:GITHUB_WORKSPACE}\src\tools\openssl.props" -Destination PCBuild\
|
Copy-Item -Path "${env:GITHUB_WORKSPACE}\src\tools\openssl.props" -Destination PCBuild\ -Verbose
|
||||||
echo "Building for ${env:PYBUILDRELEASE_ARCH}..."
|
echo "Building for ${env:PYBUILDRELEASE_ARCH}..."
|
||||||
PCBuild\build.bat -m --pgo -c Release -p "${env:PYBUILDRELEASE_ARCH}"
|
PCBuild\build.bat -m --pgo -c Release -p "${env:PYBUILDRELEASE_ARCH}"
|
||||||
|
|
||||||
- name: Windows Install Python
|
|
||||||
if: matrix.goal == 'build' && runner.os == 'Windows' && steps.cache-python-ssl.outputs.cache-hit != 'true'
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
cd "${env:PYTHON_SOURCE_PATH}"
|
|
||||||
mkdir "${env:PYTHON_INSTALL_PATH}\lib"
|
|
||||||
mkdir "${env:PYTHON_INSTALL_PATH}\include"
|
|
||||||
Copy-Item -Path "PCBuild\${env:PYEXTERNALS_PATH}\*" "${env:PYTHON_INSTALL_PATH}\"
|
|
||||||
Copy-Item -Path "${env:PYTHON_SOURCE_PATH}\Lib\*" "${env:PYTHON_INSTALL_PATH}\lib\" -recurse
|
|
||||||
Copy-Item -Path "${env:PYTHON_SOURCE_PATH}\Include\*" "${env:PYTHON_INSTALL_PATH}\include\" -recurse
|
|
||||||
Copy-Item -Path "${env:PYTHON_SOURCE_PATH}\PC\*.h" "${env:PYTHON_INSTALL_PATH}\include\"
|
|
||||||
|
|
||||||
- name: Mac/Linux Build Python
|
- name: Mac/Linux Build Python
|
||||||
if: matrix.goal == 'build' && runner.os != 'Windows' && steps.cache-python-ssl.outputs.cache-hit != 'true'
|
if: matrix.goal == 'build' && runner.os != 'Windows' && steps.cache-python-ssl.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -379,6 +397,9 @@ jobs:
|
|||||||
cd "${PYTHON_SOURCE_PATH}"
|
cd "${PYTHON_SOURCE_PATH}"
|
||||||
$MAKE altinstall
|
$MAKE altinstall
|
||||||
$MAKE bininstall
|
$MAKE bininstall
|
||||||
|
export PATH="${PATH}:${PYTHON_INSTALL_PATH}/bin"
|
||||||
|
echo "PATH=${PATH}" >> $GITHUB_ENV
|
||||||
|
echo "PATH: ${PATH}"
|
||||||
|
|
||||||
- name: Run Python
|
- name: Run Python
|
||||||
run: |
|
run: |
|
||||||
@@ -386,12 +407,27 @@ jobs:
|
|||||||
|
|
||||||
- name: Upgrade pip, wheel, etc
|
- name: Upgrade pip, wheel, etc
|
||||||
run: |
|
run: |
|
||||||
curl -O https://bootstrap.pypa.io/get-pip.py
|
curl --retry 5 --retry-connrefused -O https://bootstrap.pypa.io/get-pip.py
|
||||||
"${PYTHON}" get-pip.py
|
"${PYTHON}" get-pip.py
|
||||||
"${PYTHON}" -m pip install --upgrade pip
|
"${PYTHON}" -m pip install --upgrade pip
|
||||||
"${PYTHON}" -m pip install --upgrade wheel
|
"${PYTHON}" -m pip install --upgrade wheel
|
||||||
"${PYTHON}" -m pip install --upgrade setuptools
|
"${PYTHON}" -m pip install --upgrade setuptools
|
||||||
|
|
||||||
|
- name: Install pip requirements
|
||||||
|
run: |
|
||||||
|
if [[ "${RUNNER_OS}" == "macOS" ]]; then
|
||||||
|
"${PYTHON}" -m pip install --upgrade cffi ${PIP_ARGS}
|
||||||
|
"${PYTHON}" -m pip download --only-binary :all: \
|
||||||
|
--dest . \
|
||||||
|
--no-cache \
|
||||||
|
--no-deps \
|
||||||
|
--platform macosx_10_15_universal2 \
|
||||||
|
cryptography
|
||||||
|
"${PYTHON}" -m pip install --force-reinstall --no-deps cryptography*.whl
|
||||||
|
fi
|
||||||
|
"${PYTHON}" -m pip install --upgrade -r requirements.txt ${PIP_ARGS}
|
||||||
|
"${PYTHON}" -m pip list
|
||||||
|
|
||||||
- name: Install PyInstaller
|
- name: Install PyInstaller
|
||||||
if: matrix.goal == 'build'
|
if: matrix.goal == 'build'
|
||||||
run: |
|
run: |
|
||||||
@@ -402,40 +438,37 @@ jobs:
|
|||||||
# remove pre-compiled bootloaders so we fail if bootloader compile fails
|
# remove pre-compiled bootloaders so we fail if bootloader compile fails
|
||||||
rm -rvf PyInstaller/bootloader/*-*/*
|
rm -rvf PyInstaller/bootloader/*-*/*
|
||||||
cd bootloader
|
cd bootloader
|
||||||
if [[ "${arch}" == "Win32" ]]; then
|
case "${arch}" in
|
||||||
|
"Win32")
|
||||||
export PYINSTALLER_BUILD_ARGS="--target-arch=32bit"
|
export PYINSTALLER_BUILD_ARGS="--target-arch=32bit"
|
||||||
fi
|
;;
|
||||||
|
"Win64")
|
||||||
|
export PYINSTALLER_BUILD_ARGS="--target-arch=64bit"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
echo "PyInstaller build arguments: ${PYINSTALLER_BUILD_ARGS}"
|
echo "PyInstaller build arguments: ${PYINSTALLER_BUILD_ARGS}"
|
||||||
"${PYTHON}" ./waf all $PYINSTALLER_BUILD_ARGS
|
"${PYTHON}" ./waf all $PYINSTALLER_BUILD_ARGS
|
||||||
cd ../..
|
cd ..
|
||||||
echo "---- Installing PyInstaller ----"
|
echo "---- Installing PyInstaller ----"
|
||||||
"${PYTHON}" -m pip install pyinstaller
|
"${PYTHON}" -m pip install .
|
||||||
|
|
||||||
- name: Install pip requirements
|
|
||||||
run: |
|
|
||||||
if [[ "${RUNNER_OS}" == "macOS" ]]; then
|
|
||||||
for package in cryptography; do
|
|
||||||
"${PYTHON}" -m pip install --upgrade cffi ${PIP_ARGS}
|
|
||||||
"${PYTHON}" -m pip download --only-binary :all: \
|
|
||||||
--dest . \
|
|
||||||
--no-cache \
|
|
||||||
--no-deps \
|
|
||||||
--platform macosx_10_15_universal2 \
|
|
||||||
$package
|
|
||||||
"${PYTHON}" -m pip install --force-reinstall --no-deps $package*.whl
|
|
||||||
done
|
|
||||||
find $PYTHON_INSTALL_PATH/lib/python3.10/site-packages -type f -name "*.so" -exec du -sh "{}" \;
|
|
||||||
fi
|
|
||||||
"${PYTHON}" -m pip install --upgrade -r requirements.txt ${PIP_ARGS}
|
|
||||||
"${PYTHON}" -m pip list
|
|
||||||
|
|
||||||
- name: Build GAM with PyInstaller
|
- name: Build GAM with PyInstaller
|
||||||
if: matrix.goal != 'test'
|
if: matrix.goal != 'test'
|
||||||
run: |
|
run: |
|
||||||
export gampath="./dist/gam"
|
if [[ "${staticx}" == "yes" ]]; then
|
||||||
|
export distpath="./dist/gam"
|
||||||
|
export gampath="${distpath}"
|
||||||
|
else
|
||||||
|
export distpath="./dist"
|
||||||
|
export gampath="${distpath}/gam"
|
||||||
|
fi
|
||||||
mkdir -p -v "${gampath}"
|
mkdir -p -v "${gampath}"
|
||||||
if [[ "${RUNNER_OS}" == "macOS" ]]; then
|
if [[ "${RUNNER_OS}" == "macOS" ]]; then
|
||||||
export gampath=$($PYTHON -c "import os; print(os.path.realpath('$gampath'))")
|
export gampath=$($PYTHON -c "import os; print(os.path.realpath('$gampath'))")
|
||||||
|
elif [[ "${RUNNER_OS}" == "Windows" ]]; then
|
||||||
|
# Work around issue where PyInstaller picks up python3.dll from other Python versions
|
||||||
|
# https://github.com/pyinstaller/pyinstaller/issues/7102
|
||||||
|
export PATH="/usr/bin"
|
||||||
else
|
else
|
||||||
export gampath=$(realpath "${gampath}")
|
export gampath=$(realpath "${gampath}")
|
||||||
fi
|
fi
|
||||||
@@ -443,7 +476,21 @@ jobs:
|
|||||||
echo "gampath=${gampath}" >> $GITHUB_ENV
|
echo "gampath=${gampath}" >> $GITHUB_ENV
|
||||||
echo "gam=${gam}" >> $GITHUB_ENV
|
echo "gam=${gam}" >> $GITHUB_ENV
|
||||||
echo -e "GAM: ${gam}\nGAMPATH: ${gampath}"
|
echo -e "GAM: ${gam}\nGAMPATH: ${gampath}"
|
||||||
"${PYTHON}" -m PyInstaller --clean --distpath="${gampath}" gam.spec
|
# TEMP force everything back to one file.
|
||||||
|
export PYINSTALLER_BUILD_ONEFILE="yes"
|
||||||
|
export distpath="./dist/gam"
|
||||||
|
export gampath="${distpath}"
|
||||||
|
"${PYTHON}" -m PyInstaller --clean --noconfirm --distpath="${distpath}" gam.spec
|
||||||
|
|
||||||
|
- name: Copy extra package files
|
||||||
|
if: matrix.goal == 'build'
|
||||||
|
run: |
|
||||||
|
cp -v roots.pem $gampath
|
||||||
|
cp -v LICENSE $gampath
|
||||||
|
cp -v GamCommands.txt $gampath
|
||||||
|
if [[ "${RUNNER_OS}" == "Windows" ]]; then
|
||||||
|
cp -v gam-setup.bat $gampath
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Basic Tests all jobs
|
- name: Basic Tests all jobs
|
||||||
run: |
|
run: |
|
||||||
@@ -454,10 +501,8 @@ jobs:
|
|||||||
echo "GAMVERSION=${GAMVERSION}" >> $GITHUB_ENV
|
echo "GAMVERSION=${GAMVERSION}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Linux/MacOS package
|
- name: Linux/MacOS package
|
||||||
if: runner.os != 'Windows' && matrix.goal == 'build'
|
if: runner.os != 'Windows' && matrix.goal == 'build' && matrix.staticx != 'yes'
|
||||||
run: |
|
run: |
|
||||||
cp -v LICENSE $gampath
|
|
||||||
cp -v GamCommands.txt $gampath
|
|
||||||
if [[ "${RUNNER_OS}" == "macOS" ]]; then
|
if [[ "${RUNNER_OS}" == "macOS" ]]; then
|
||||||
GAM_ARCHIVE="gam-${GAMVERSION}-macos-universal2.tar.xz"
|
GAM_ARCHIVE="gam-${GAMVERSION}-macos-universal2.tar.xz"
|
||||||
elif [[ "${RUNNER_OS}" == "Linux" ]]; then
|
elif [[ "${RUNNER_OS}" == "Linux" ]]; then
|
||||||
@@ -466,14 +511,14 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
tar -C dist/ --create --verbose --exclude-from "${GITHUB_WORKSPACE}/.github/actions/package_exclusions.txt" --file $GAM_ARCHIVE --xz gam
|
tar -C dist/ --create --verbose --exclude-from "${GITHUB_WORKSPACE}/.github/actions/package_exclusions.txt" --file $GAM_ARCHIVE --xz gam
|
||||||
|
|
||||||
- name: Linux 64-bit install patchelf/staticx
|
- name: Install StaticX
|
||||||
if: runner.os == 'Linux' && contains(runner.arch, '64') && matrix.goal != 'test'
|
if: matrix.staticx == 'yes'
|
||||||
run: |
|
run: |
|
||||||
"${PYTHON}" -m pip install --upgrade patchelf-wrapper
|
"${PYTHON}" -m pip install --upgrade patchelf-wrapper
|
||||||
"${PYTHON}" -m pip install --upgrade staticx
|
"${PYTHON}" -m pip install --upgrade staticx
|
||||||
|
|
||||||
- name: Linux 64-bit Make Static
|
- name: Make StaticX
|
||||||
if: runner.os == 'Linux' && contains(runner.arch, '64') && matrix.goal != 'test'
|
if: matrix.staticx == 'yes'
|
||||||
run: |
|
run: |
|
||||||
case $RUNNER_ARCH in
|
case $RUNNER_ARCH in
|
||||||
X64)
|
X64)
|
||||||
@@ -486,14 +531,14 @@ jobs:
|
|||||||
echo "ldlib=${ldlib}"
|
echo "ldlib=${ldlib}"
|
||||||
$PYTHON -m staticx -l "${ldlib}" "${gam}" "${gam}-staticx"
|
$PYTHON -m staticx -l "${ldlib}" "${gam}" "${gam}-staticx"
|
||||||
|
|
||||||
- name: Linux Run StaticX-ed
|
- name: Run StaticX
|
||||||
if: runner.os == 'Linux' && contains(runner.arch, '64') && matrix.goal != 'test'
|
if: matrix.staticx == 'yes'
|
||||||
run: |
|
run: |
|
||||||
"${gam}-staticx" version extended
|
"${gam}-staticx" version extended
|
||||||
mv -v "${gam}-staticx" "${gam}"
|
mv -v "${gam}-staticx" "${gam}"
|
||||||
|
|
||||||
- name: Linux package staticx
|
- name: Linux package staticx
|
||||||
if: runner.os == 'Linux' && contains(runner.arch, '64') && matrix.goal != 'test'
|
if: matrix.staticx == 'yes'
|
||||||
run: |
|
run: |
|
||||||
GAM_ARCHIVE="gam-${GAMVERSION}-linux-$(uname -m)-legacy.tar.xz"
|
GAM_ARCHIVE="gam-${GAMVERSION}-linux-$(uname -m)-legacy.tar.xz"
|
||||||
tar -C dist/ --create --verbose --exclude-from "${GITHUB_WORKSPACE}/.github/actions/package_exclusions.txt" --file $GAM_ARCHIVE --xz gam
|
tar -C dist/ --create --verbose --exclude-from "${GITHUB_WORKSPACE}/.github/actions/package_exclusions.txt" --file $GAM_ARCHIVE --xz gam
|
||||||
@@ -501,9 +546,6 @@ jobs:
|
|||||||
- name: Windows package
|
- name: Windows package
|
||||||
if: runner.os == 'Windows' && matrix.goal != 'test'
|
if: runner.os == 'Windows' && matrix.goal != 'test'
|
||||||
run: |
|
run: |
|
||||||
cp -v LICENSE $gampath
|
|
||||||
cp -v GamCommands.txt $gampath
|
|
||||||
cp -v gam-setup.bat $gampath
|
|
||||||
cd dist/
|
cd dist/
|
||||||
GAM_ARCHIVE="../gam-${GAMVERSION}-windows-${GAM_ARCHIVE_ARCH}.zip"
|
GAM_ARCHIVE="../gam-${GAMVERSION}-windows-${GAM_ARCHIVE_ARCH}.zip"
|
||||||
/c/Program\ Files/7-Zip/7z.exe a -tzip $GAM_ARCHIVE gam "-xr@${GITHUB_WORKSPACE}/.github/actions/package_exclusions.txt" -bb3
|
/c/Program\ Files/7-Zip/7z.exe a -tzip $GAM_ARCHIVE gam "-xr@${GITHUB_WORKSPACE}/.github/actions/package_exclusions.txt" -bb3
|
||||||
@@ -540,7 +582,7 @@ jobs:
|
|||||||
if [[ "${RUNNER_OS}" == "macOS" ]]; then
|
if [[ "${RUNNER_OS}" == "macOS" ]]; then
|
||||||
brew install gnupg
|
brew install gnupg
|
||||||
fi
|
fi
|
||||||
source ../.github/actions/decrypt.sh ../.github/actions/creds.tar.gpg creds.tar
|
source ../.github/actions/decrypt.sh ../.github/actions/creds.tar.xz.gpg creds.tar.xz
|
||||||
export OAUTHFILE="oauth2.txt-gam-gha-${JID}"
|
export OAUTHFILE="oauth2.txt-gam-gha-${JID}"
|
||||||
echo "OAUTHFILE=${OAUTHFILE}" >> $GITHUB_ENV
|
echo "OAUTHFILE=${OAUTHFILE}" >> $GITHUB_ENV
|
||||||
export gam_user="gam-gha-${JID}@pdl.jaylee.us"
|
export gam_user="gam-gha-${JID}@pdl.jaylee.us"
|
||||||
@@ -550,20 +592,33 @@ jobs:
|
|||||||
$gam info domain
|
$gam info domain
|
||||||
$gam oauth refresh
|
$gam oauth refresh
|
||||||
$gam info user
|
$gam info user
|
||||||
#$gam info user $gam_user grouptree
|
|
||||||
export tstamp=$($PYTHON -c "import time; print(time.time_ns())")
|
export tstamp=$($PYTHON -c "import time; print(time.time_ns())")
|
||||||
export newbase=gha_test_$JID_$tstamp
|
export newbase="gha_test_${JID}_${tstamp}"
|
||||||
export newuser=$newbase@pdl.jaylee.us
|
export newuser="${newbase}@pdl.jaylee.us"
|
||||||
export newgroup=$newbase-group@pdl.jaylee.us
|
export newgroup="${newbase}-group@pdl.jaylee.us"
|
||||||
export newalias=$newbase-alias@pdl.jaylee.us
|
export newalias="${newbase}-alias@pdl.jaylee.us"
|
||||||
export newbuilding=$newbase-building
|
export newbuilding="${newbase}-building"
|
||||||
export newresource=$newbase-resource
|
export newresource="${newbase}-resource"
|
||||||
|
export newou="aaaGithub Actions/${newbase}"
|
||||||
|
|
||||||
|
# cleanup old runs
|
||||||
|
GAM_CSV_ROW_FILTER="name:regex:gha_test_${JID}_" $gam print vaultholds | $gam csv - gam delete vaulthold "id:~~holdId~~" matter "id:~~matterId~~"
|
||||||
|
GAM_CSV_ROW_FILTER="name:regex:gha_test_${JID}_" $gam print features | $gam csv - gam delete feature ~name
|
||||||
|
GAM_CSV_ROW_FILTER="name:regex:^gha_test_${JID}_" $gam user $gam_user print shareddrives asadmin | $gam csv - gam user $gam_user delete shareddrive ~id nukefromorbit
|
||||||
|
$gam print users query "gha.jid=$JID" | $gam csv - gam delete user ~primaryEmail
|
||||||
|
GAM_CSV_ROW_FILTER="name:regex:^gha_test_${JID}_" $gam print ous fromparent "aaaGithub Actions" | $gam csv - gam delete ou ~orgUnitId
|
||||||
|
GAM_CSV_ROW_FILTER="groupKey.id:regex:^gha_test_${JID}_" $gam print cigroups | $gam csv - gam delete cigroup ~groupKey.id
|
||||||
|
GAM_CSV_ROW_FILTER="resourceId:regex:^gha_test_${JID}_" $gam print resources | $gam csv - gam delete resource ~resourceId
|
||||||
|
GAM_CSV_ROW_FILTER="buildingId:regex:^gha_test_${JID}_" $gam print buildings | $gam csv - gam delete building ~buildingId
|
||||||
|
|
||||||
|
echo "Creating OrgUnit ${newou}"
|
||||||
|
$gam create ou "${newou}"
|
||||||
export GAM_THREADS=5
|
export GAM_THREADS=5
|
||||||
echo email > sample.csv;
|
echo email > sample.csv;
|
||||||
for i in {1..10}; do
|
for i in {1..10}; do
|
||||||
echo "${newbase}-bulkuser-$i" >> sample.csv;
|
echo "${newbase}-bulkuser-$i" >> sample.csv;
|
||||||
done
|
done
|
||||||
$gam create user $newuser firstname GHA lastname $JID password random recoveryphone 12125121110 recoveryemail jay0lee@gmail.com gha.jid $JID languages en+,en-GB-
|
$gam create user $newuser firstname GHA lastname $JID displayname "Github Actions ${JID}" password random ou "${newou}" recoveryphone 12125121110 recoveryemail jay0lee@gmail.com gha.jid $JID languages en+,en-GB-
|
||||||
$gam user $newuser update photo https://dummyimage.com/400x600/000/fff
|
$gam user $newuser update photo https://dummyimage.com/400x600/000/fff
|
||||||
$gam user $newuser get photo
|
$gam user $newuser get photo
|
||||||
$gam user $newuser delete photo
|
$gam user $newuser delete photo
|
||||||
@@ -577,8 +632,8 @@ jobs:
|
|||||||
$gam update group $newgroup add owner $gam_user
|
$gam update group $newgroup add owner $gam_user
|
||||||
$gam update group $newgroup add member $newuser
|
$gam update group $newgroup add member $newuser
|
||||||
$gam create admin $newuser _GROUPS_EDITOR_ROLE CUSTOMER # condition nonsecuritygroup
|
$gam create admin $newuser _GROUPS_EDITOR_ROLE CUSTOMER # condition nonsecuritygroup
|
||||||
$gam csv sample.csv gam create user ~~email~~ firstname "GHA Bulk" lastname ~~email~~ gha.jid $JID
|
$gam csv sample.csv gam create user ~~email~~ firstname "GHA Bulk" lastname ~~email~~ gha.jid $JID ou "${newou}"
|
||||||
$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 12125121110 recoveryemail jay0lee@gmail.com password random displayname "GitHub Actions Bulk ${JID}"
|
||||||
$gam csv sample.csv gam update user ~~email~~ recoveryphone "" recoveryemail ""
|
$gam csv sample.csv gam update user ~~email~~ recoveryphone "" recoveryemail ""
|
||||||
$gam csv sample.csv gam user ~email add license workspaceenterpriseplus
|
$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 user $gam_user sendemail recipient ~~email~~@pdl.jaylee.us subject "test message $newbase" message "GHA test message"
|
||||||
@@ -608,8 +663,8 @@ jobs:
|
|||||||
$gam users "$newbase-bulkuser-7 $newbase-bulkuser-8 $newbase-bulkuser-9" modify messages query in:anywhere maxtomodify 99999 addlabel IMPORTANT addlabel STARRED doit
|
$gam users "$newbase-bulkuser-7 $newbase-bulkuser-8 $newbase-bulkuser-9" modify messages query in:anywhere maxtomodify 99999 addlabel IMPORTANT addlabel STARRED doit
|
||||||
$gam user $newuser delete label --ALL_LABELS--
|
$gam user $newuser delete label --ALL_LABELS--
|
||||||
GAM_CSV_ROW_FILTER="name:regex:gha-test-${JID}" $gam print features | $gam csv - gam delete feature ~name
|
GAM_CSV_ROW_FILTER="name:regex:gha-test-${JID}" $gam print features | $gam csv - gam delete feature ~name
|
||||||
$gam create feature name Whiteboard-$newbase
|
|
||||||
$gam create feature name VC-$newbase
|
$gam create feature name VC-$newbase
|
||||||
|
$gam create feature name Whiteboard-$newbase
|
||||||
$gam create building "My Building - $newbase" id $newbuilding floors 1,2,3,4,5,6,7,8,9,10,11,12,14,15 description "No 13th floor here..."
|
$gam create building "My Building - $newbase" id $newbuilding floors 1,2,3,4,5,6,7,8,9,10,11,12,14,15 description "No 13th floor here..."
|
||||||
$gam create resource $newresource "Resource Calendar $tstamp" capacity 25 features Whiteboard-$newbase,VC-$newbase building $newbuilding floor 15 type Room
|
$gam create resource $newresource "Resource Calendar $tstamp" capacity 25 features Whiteboard-$newbase,VC-$newbase building $newbuilding floor 15 type Room
|
||||||
$gam info resource $newresource
|
$gam info resource $newresource
|
||||||
@@ -645,8 +700,9 @@ jobs:
|
|||||||
$gam delete hold "GHA hold $newbase" matter $matterid
|
$gam delete hold "GHA hold $newbase" matter $matterid
|
||||||
$gam update matter $matterid action close
|
$gam update matter $matterid action close
|
||||||
$gam update matter $matterid action delete
|
$gam update matter $matterid action delete
|
||||||
#$gam delete user $newuser
|
# shakes off vault hold on user so we can delete
|
||||||
#$gam undelete user $newuser
|
$gam print users query "email:${newuser}" orgunitpath | $gam csv - gam update user ~primaryEmail ou ~orgUnitPath
|
||||||
|
$gam user $newuser show holds
|
||||||
$gam delete user $newuser
|
$gam delete user $newuser
|
||||||
$gam print users query "gha.jid=$JID" | $gam csv - gam delete user ~primaryEmail
|
$gam print users query "gha.jid=$JID" | $gam csv - gam delete user ~primaryEmail
|
||||||
$gam print mobile
|
$gam print mobile
|
||||||
@@ -662,44 +718,76 @@ jobs:
|
|||||||
$gam report users fields accounts:is_less_secure_apps_access_allowed,gmail:last_imap_time,gmail:last_pop_time filters "accounts:last_login_time>2019-01-01T00:00:00.000Z" todrive
|
$gam report users fields accounts:is_less_secure_apps_access_allowed,gmail:last_imap_time,gmail:last_pop_time filters "accounts:last_login_time>2019-01-01T00:00:00.000Z" todrive
|
||||||
$gam report admin start -3d todrive
|
$gam report admin start -3d todrive
|
||||||
$gam print devices nopersonaldevices nodeviceusers filter "serial:$JID$JID$JID$JID-" | $gam csv - gam delete device id ~name
|
$gam print devices nopersonaldevices nodeviceusers filter "serial:$JID$JID$JID$JID-" | $gam csv - gam delete device id ~name
|
||||||
#$gam print userinvitations
|
$gam print userinvitations
|
||||||
#$gam print userinvitations | $gam csv - gam send userinvitation ~name
|
$gam print userinvitations | $gam csv - gam send userinvitation ~name
|
||||||
$gam create caalevel "zzz_${newbase}" basic condition ipsubnetworks 1.1.1.1/32,2.2.2.2/32 endcondition
|
$gam create caalevel "zzz_${newbase}" basic condition ipsubnetworks 1.1.1.1/32,2.2.2.2/32 endcondition
|
||||||
$gam print caalevels
|
$gam print caalevels
|
||||||
$gam delete caalevel "zzz_${newbase}"
|
$gam delete caalevel "zzz_${newbase}"
|
||||||
driveid=$($gam user $gam_user add shareddrive "${newbase}" | awk '{print $NF}')
|
driveid=$($gam user $gam_user add shareddrive "${newbase}" | awk '{print $NF}')
|
||||||
echo "Created shared drive ${driveid}"
|
echo "Created shared drive ${driveid}"
|
||||||
$gam user $gam_user add drivefile localfile gam.py parentid "${driveid}"
|
$gam user $gam_user add drivefile localfile gam.py parentid "${driveid}"
|
||||||
$gam user $gam_user update shareddrive "${driveid}" ou "id:03ph8a2z1t2ph5z"
|
$gam user $gam_user update shareddrive "${driveid}" ou "${newou}"
|
||||||
$gam user $gam_user show shareddrives asadmin
|
$gam user $gam_user show shareddrives asadmin
|
||||||
|
$gam user $gam_user update shareddrive "${driveid}" ou "aaaGithub Actions" # so we can delete our OU...
|
||||||
$gam user $gam_user delete shareddrive "${driveid}" nukefromorbit
|
$gam user $gam_user delete shareddrive "${driveid}" nukefromorbit
|
||||||
echo "printer model count:"
|
echo "printer model count:"
|
||||||
$gam print printermodels | wc -l
|
$gam print printermodels | wc -l
|
||||||
|
#ssoprofile=$($gam create inboundssoprofile name "El Goog ${newbase}" loginurl https://www.google.com logouturl https://www.google.com changepasswordurl https://www.google.com entityid ElGoog return_name_only)
|
||||||
|
#$gam create inboundssocredential profile "id:${ssoprofile}" generate_key
|
||||||
|
#$gam create inboundssoassignment profile "id:${ssoprofile}" orgunit "${newou}" mode SAML_SSO
|
||||||
|
$gam delete ou "${newou}"
|
||||||
|
#$gam delete inboundssoprofile "id:${ssoprofile}"
|
||||||
#$gam print printers
|
#$gam print printers
|
||||||
#$gam create printer displayname "${newbase}" uri ipp://localhost:631 driverless description "made by $(gam_user)" ou /
|
#$gam create printer displayname "${newbase}" uri ipp://localhost:631 driverless description "made by $(gam_user)" ou /
|
||||||
#export CUSTOMER_ID="C01wfv983"
|
export CUSTOMER_ID="C01wfv983"
|
||||||
#export GA_DOMAIN="pdl.jaylee.us"
|
export GA_DOMAIN="pdl.jaylee.us"
|
||||||
#touch $gampath/enabledasa.txt
|
touch $gampath/enabledasa.txt
|
||||||
#echo "using delegated admin service account"
|
echo "using delegated admin service account"
|
||||||
#$gam print users
|
$gam print users
|
||||||
|
|
||||||
# - name: Upload to Google Drive, build only.
|
|
||||||
# if: github.event_name == 'push' && matrix.goal != 'test'
|
|
||||||
# run: |
|
|
||||||
# ls gam-$GAMVERSION-*
|
|
||||||
# for gamfile in gam-$GAMVERSION-*; do
|
|
||||||
# echo "Uploading file ${gamfile} to Google Drive..."
|
|
||||||
# fileid=$($gam user $gam_user add drivefile localfile $gamfile drivefilename $GAMVERSION-${GITHUB_SHA:0:7}-$gamfile parentid 1N2zbO33qzUQFsGM49-m9AQC1ijzd_ru1 returnidonly)
|
|
||||||
# echo "file uploaded as ${fileid}, setting ACL..."
|
|
||||||
# $gam user $gam_user add drivefileacl $fileid anyone role reader withlink
|
|
||||||
# done
|
|
||||||
|
|
||||||
- name: Archive production artifacts
|
- name: Archive production artifacts
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
if: github.event_name == 'push' && matrix.goal != 'test'
|
if: (github.event_name == 'push' || github.event_name == 'schedule') && matrix.goal != 'test'
|
||||||
with:
|
with:
|
||||||
name: gam-binaries
|
name: gam-binaries
|
||||||
path: |
|
path: |
|
||||||
src/*.tar.xz
|
src/*.tar.xz
|
||||||
src/*.zip
|
src/*.zip
|
||||||
src/*.msi
|
src/*.msi
|
||||||
|
|
||||||
|
- name: Tar Cache archive
|
||||||
|
if: matrix.goal == 'build' && steps.cache-python-ssl.outputs.cache-hit != 'true'
|
||||||
|
working-directory: ${{ github.workspace }}
|
||||||
|
run: |
|
||||||
|
tar cJvvf bin.tar.xz bin/
|
||||||
|
|
||||||
|
publish:
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Download artifacts
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
|
||||||
|
- name: VirusTotal Scan
|
||||||
|
uses: crazy-max/ghaction-virustotal@v3
|
||||||
|
with:
|
||||||
|
vt_api_key: ${{ secrets.VT_API_KEY }}
|
||||||
|
files: |
|
||||||
|
gam-binaries/*
|
||||||
|
|
||||||
|
- uses: "marvinpinto/action-automatic-releases@latest"
|
||||||
|
name: Publish draft release
|
||||||
|
with:
|
||||||
|
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
automatic_release_tag: latest
|
||||||
|
prerelease: false
|
||||||
|
draft: true
|
||||||
|
files: |
|
||||||
|
gam-binaries/*
|
||||||
|
|||||||
36
.github/workflows/get-roots.yml
vendored
Normal file
36
.github/workflows/get-roots.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
name: Check for Google Root CA Updates
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
schedule:
|
||||||
|
- cron: '23 23 * * *'
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
working-directory: src
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-apis:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@master
|
||||||
|
with:
|
||||||
|
persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal token
|
||||||
|
fetch-depth: 0 # otherwise, you will failed to push refs to dest repo
|
||||||
|
|
||||||
|
- name: Check for updates
|
||||||
|
run: curl -o ./roots.pem -vvvv https://pki.goog/roots.pem
|
||||||
|
|
||||||
|
- name: Commit file
|
||||||
|
run: |
|
||||||
|
git config --local user.email "action@github.com"
|
||||||
|
git config --local user.name "GitHub Action"
|
||||||
|
git add roots.pem
|
||||||
|
git diff --quiet && git diff --staged --quiet || git commit -am '[ci skip] Updated roots.pem'
|
||||||
|
|
||||||
|
- name: Push changes
|
||||||
|
uses: ad-m/github-push-action@master
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -1392,6 +1392,13 @@ gam print chromeappdevices [todrive]
|
|||||||
[start <Date>] [end <Date>]
|
[start <Date>] [end <Date>]
|
||||||
[orderby deviceid|machine]
|
[orderby deviceid|machine]
|
||||||
|
|
||||||
|
gam print chromeaues [todrive]
|
||||||
|
[ou|org|orgunit <OrgUnitItem>]
|
||||||
|
[minauedate <Date>] [maxauedate <Date>]
|
||||||
|
|
||||||
|
gam print chromeneedsattn [todrive]
|
||||||
|
[ou|org|orgunit <OrgUnitItem>]
|
||||||
|
|
||||||
gam print chromeversions [todrive]
|
gam print chromeversions [todrive]
|
||||||
[ou|org|orgunit <OrgUnitItem>]
|
[ou|org|orgunit <OrgUnitItem>]
|
||||||
[start <Date>] [end <Date>] [recentfirst]
|
[start <Date>] [end <Date>] [recentfirst]
|
||||||
@@ -1548,6 +1555,44 @@ gam print group-members|groups-members [todrive]
|
|||||||
[roles <GroupRoleList>] [membernames] [fields <MembersFieldNameList>]
|
[roles <GroupRoleList>] [membernames] [fields <MembersFieldNameList>]
|
||||||
[includederivedmembership]
|
[includederivedmembership]
|
||||||
|
|
||||||
|
<SSOProfileDisplayName> ::= <String>
|
||||||
|
<SSOProfileName> ::= id:inboundSamlSsoProfiles/<String>
|
||||||
|
<SSOProfileItem> ::= <SSOProfileDisplayName>|<SSOProfileName>
|
||||||
|
<SSOProfileItemList> ::= "<SSOProfileItem>(,<SSOProfileItem>)*"
|
||||||
|
|
||||||
|
gam create inboundssoprofile [name <SSOProfileDisplayName>]
|
||||||
|
[entityid <String>] [loginurl <URL>] [logouturl <URL>] [changepasswordurl <URL>]
|
||||||
|
[returnnameonly]
|
||||||
|
gam update inboundssoprofile <SSOProfileItem>
|
||||||
|
[entityid <String>] [loginurl <URL>] [logouturl <URL>] [changepasswordurl <URL>]
|
||||||
|
[returnnameonly]
|
||||||
|
gam delete inboundssoprofile <SSOProfileItem>
|
||||||
|
gam info inboundssoprofile <SSOProfileItem>
|
||||||
|
gam show inboundssoprofiles
|
||||||
|
gam print inboundssoprofiles [todrive]
|
||||||
|
|
||||||
|
<SSOCredentialsName> ::= [id:]inboundSamlSsoProfiles/<String>/idpCredentials/<String>
|
||||||
|
|
||||||
|
gam create inboundssocredential profile <SSOProfileItem>
|
||||||
|
(pemfile <FileName>)|(generatekey [keysize 1024|2048|4096]) [replaceolddest]
|
||||||
|
gam delete inboundssocredential <SSOCredentialsName>
|
||||||
|
gam show inboundssocredentials [profile|profiles <SSOProfileItemList>]
|
||||||
|
gam print inboundssocredentials [profile|profiles <SSOProfileItemList>] [todrive]
|
||||||
|
|
||||||
|
<SSOAssignmentSelector> ::=
|
||||||
|
groups/<String> |
|
||||||
|
group:<EmailAddress> |
|
||||||
|
orgunits/<String> |
|
||||||
|
orgunit:<OrgUnitPath>
|
||||||
|
|
||||||
|
gam create inboundssoassignment (group <GroupItem> rank <Number>)|(ou|org|orgunit <OrgUnitItem>)
|
||||||
|
(mode sso_off)|(mode saml_sso profile <SSOProfileItem>)(mode domain_wide_saml_if_enabled) [neverredirect]
|
||||||
|
gam update inboundssoassignment [(group <GroupItem> rank <Number>)|(ou|org|orgunit <OrgUnitItem>)]
|
||||||
|
[(mode sso_off)|(mode saml_sso profile <SSOProfileItem>)(mode domain_wide_saml_if_enabled)] [neverredirect]
|
||||||
|
gam info inboundssoassignment <SSOAssignmentSelector>
|
||||||
|
gam show inboundssoassignments
|
||||||
|
gam print inboundssoassignments [todrive]
|
||||||
|
|
||||||
gam send userinvitation <EmailAddress>
|
gam send userinvitation <EmailAddress>
|
||||||
gam cancel userinvitation <EmailAddress>
|
gam cancel userinvitation <EmailAddress>
|
||||||
gam check userinvitation|isinvitable <EmailAddress>
|
gam check userinvitation|isinvitable <EmailAddress>
|
||||||
@@ -1636,6 +1681,9 @@ gam show guardian|guardians [invitedguardian <EmailAddress>] [student <StudentIt
|
|||||||
gam print guardian|guardians [todrive] [invitedguardian <EmailAddress>] [student <StudentItem>] [invitations [states <GuardianStateList>]] [<UserTypeEntity>]
|
gam print guardian|guardians [todrive] [invitedguardian <EmailAddress>] [student <StudentItem>] [invitations [states <GuardianStateList>]] [<UserTypeEntity>]
|
||||||
gam cancel guardianinvitation|guardianinvitations <GuardianInvitationID> <StudentItem>
|
gam cancel guardianinvitation|guardianinvitations <GuardianInvitationID> <StudentItem>
|
||||||
|
|
||||||
|
gam download storagebucket <URL>
|
||||||
|
gam copy storagebucket sourcebucket <URL> targetbucket <URL> [sourceprefix <String>] [targetprefix <String>]
|
||||||
|
|
||||||
gam create vaultexport|export matter <MatterItem> [name <name>] corpus <drive|mail|groups|hangouts_chat>
|
gam create vaultexport|export matter <MatterItem> [name <name>] corpus <drive|mail|groups|hangouts_chat>
|
||||||
(accounts <EmailAddressList>) | (orgunit|ou <OrgUnitPath>) | (teamdrives <TeamDriveList>) | (rooms <ChatRoomList>) | everyone
|
(accounts <EmailAddressList>) | (orgunit|ou <OrgUnitPath>) | (teamdrives <TeamDriveList>) | (rooms <ChatRoomList>) | everyone
|
||||||
[scope <all_data|held_data|unprocessed_data>]
|
[scope <all_data|held_data|unprocessed_data>]
|
||||||
@@ -1648,6 +1696,7 @@ gam delete export <MatterItem> <ExportItem>
|
|||||||
gam info export <MatterItem> <ExportItem>
|
gam info export <MatterItem> <ExportItem>
|
||||||
gam print exports [todrive] [matters <MatterItemList>]
|
gam print exports [todrive] [matters <MatterItemList>]
|
||||||
gam download export <MatterItem> <ExportItem> [noverify] [noextract] [targetfolder <FilePath>]
|
gam download export <MatterItem> <ExportItem> [noverify] [noextract] [targetfolder <FilePath>]
|
||||||
|
gam copy export <MatterItem> <ExportItem> targetbucket <URL> [targetprefix <String>]
|
||||||
|
|
||||||
gam create vaulthold|hold corpus drive|groups|mail matter <MatterItem> [name <String>] [query <QueryVaultCorpus>]
|
gam create vaulthold|hold corpus drive|groups|mail matter <MatterItem> [name <String>] [query <QueryVaultCorpus>]
|
||||||
[(accounts|groups|users <EmailItemList>) | (orgunit|ou <OrgUnit>)]
|
[(accounts|groups|users <EmailItemList>) | (orgunit|ou <OrgUnit>)]
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"basePath": "",
|
"basePath": "",
|
||||||
"baseUrl": "https://www.googleapis.com/admin/directory/v1.1beta1/customer/",
|
"baseUrl": "https://admin.googleapis.com/admin/directory/v1.1beta1/customer/",
|
||||||
"batchPath": "batch",
|
"batchPath": "batch",
|
||||||
"canonicalName": "cbcm",
|
"canonicalName": "cbcm",
|
||||||
"discoveryVersion": "v1",
|
"discoveryVersion": "v1",
|
||||||
|
|||||||
121
src/gam.spec
121
src/gam.spec
@@ -1,55 +1,110 @@
|
|||||||
# -*- mode: python -*-
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
from os import getenv
|
||||||
|
from sys import platform
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import importlib
|
|
||||||
from PyInstaller.utils.hooks import copy_metadata
|
from PyInstaller.utils.hooks import copy_metadata
|
||||||
|
|
||||||
# dynamically determine where httplib2/cacerts.txt lives
|
|
||||||
proot = os.path.dirname(importlib.import_module('httplib2').__file__)
|
|
||||||
extra_files = [(os.path.join(proot, 'cacerts.txt'), 'httplib2')]
|
|
||||||
|
|
||||||
|
extra_files = []
|
||||||
extra_files += copy_metadata('google-api-python-client')
|
extra_files += copy_metadata('google-api-python-client')
|
||||||
extra_files += [('cbcm-v1.1beta1.json', '.')]
|
extra_files += [('cbcm-v1.1beta1.json', '.')]
|
||||||
extra_files += [('contactdelegation-v1.json', '.')]
|
extra_files += [('contactdelegation-v1.json', '.')]
|
||||||
extra_files += [('admin-directory_v1.1beta1.json', '.')]
|
extra_files += [('admin-directory_v1.1beta1.json', '.')]
|
||||||
|
extra_files += [('roots.pem', '.')]
|
||||||
hidden_imports = [
|
hidden_imports = [
|
||||||
'gam.auth.yubikey',
|
'gam.auth.yubikey',
|
||||||
]
|
]
|
||||||
|
a = Analysis(
|
||||||
a = Analysis(['gam/__main__.py'],
|
['gam/__main__.py'],
|
||||||
hiddenimports=hidden_imports,
|
pathex=[],
|
||||||
hookspath=None,
|
binaries=[],
|
||||||
excludes=['FixTk', 'tcl', 'tk', '_tkinter', 'tkinter', 'Tkinter'],
|
|
||||||
datas=extra_files,
|
datas=extra_files,
|
||||||
runtime_hooks=None)
|
hiddenimports=hidden_imports,
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
win_no_prefer_redirects=False,
|
||||||
|
win_private_assemblies=False,
|
||||||
|
cipher=None,
|
||||||
|
noarchive=False,
|
||||||
|
)
|
||||||
for d in a.datas:
|
for d in a.datas:
|
||||||
if 'pyconfig' in d[0]:
|
if 'pyconfig' in d[0]:
|
||||||
a.datas.remove(d)
|
a.datas.remove(d)
|
||||||
break
|
break
|
||||||
|
pyz = PYZ(a.pure,
|
||||||
|
a.zipped_data,
|
||||||
pyz = PYZ(a.pure)
|
cipher=None)
|
||||||
|
# requires Python 3.10+ but no one should be compiling
|
||||||
|
# GAM with older versions anyway
|
||||||
if sys.platform == "darwin":
|
match platform:
|
||||||
target_arch="universal2"
|
case "darwin":
|
||||||
else:
|
target_arch = "universal2"
|
||||||
target_arch=None
|
strip = True
|
||||||
|
case "win32":
|
||||||
# use strip on all non-Windows platforms
|
target_arch = None
|
||||||
strip = not sys.platform == 'win32'
|
strip = False
|
||||||
|
case _:
|
||||||
exe = EXE(pyz,
|
target_arch = None
|
||||||
|
strip = True
|
||||||
|
name = 'gam'
|
||||||
|
debug = False
|
||||||
|
bootloader_ignore_signals = False
|
||||||
|
upx = False
|
||||||
|
console = True
|
||||||
|
disable_windowed_traceback = False
|
||||||
|
argv_emulation = False
|
||||||
|
codesign_identity = None
|
||||||
|
entitlements_file = None
|
||||||
|
if getenv('PYINSTALLER_BUILD_ONEFILE') == 'yes':
|
||||||
|
# Build one file
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
a.scripts,
|
a.scripts,
|
||||||
a.binaries,
|
a.binaries,
|
||||||
a.zipfiles,
|
a.zipfiles,
|
||||||
a.datas,
|
a.datas,
|
||||||
name='gam',
|
[],
|
||||||
debug=False,
|
name=name,
|
||||||
|
debug=debug,
|
||||||
|
bootloader_ignore_signals=bootloader_ignore_signals,
|
||||||
strip=strip,
|
strip=strip,
|
||||||
upx=False,
|
upx=upx,
|
||||||
|
console=console,
|
||||||
|
disable_windowed_traceback=disable_windowed_traceback,
|
||||||
|
argv_emulation=argv_emulation,
|
||||||
target_arch=target_arch,
|
target_arch=target_arch,
|
||||||
console=True)
|
codesign_identity=codesign_identity,
|
||||||
|
entitlements_file=entitlements_file,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Build one folder
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
[],
|
||||||
|
exclude_binaries=True,
|
||||||
|
name=name,
|
||||||
|
debug=debug,
|
||||||
|
bootloader_ignore_signals=bootloader_ignore_signals,
|
||||||
|
strip=strip,
|
||||||
|
upx=upx,
|
||||||
|
console=console,
|
||||||
|
disable_windowed_traceback=disable_windowed_traceback,
|
||||||
|
argv_emulation=argv_emulation,
|
||||||
|
target_arch=target_arch,
|
||||||
|
codesign_identity=codesign_identity,
|
||||||
|
entitlements_file=entitlements_file,
|
||||||
|
)
|
||||||
|
coll = COLLECT(
|
||||||
|
exe,
|
||||||
|
a.binaries,
|
||||||
|
a.zipfiles,
|
||||||
|
a.datas,
|
||||||
|
strip=strip,
|
||||||
|
upx=upx,
|
||||||
|
upx_exclude=[],
|
||||||
|
name=name,
|
||||||
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,9 @@
|
|||||||
<Component Id="gamcommands_txt" Guid="58ff9c45-a7c9-4e22-8845-a9a92610c1f3">
|
<Component Id="gamcommands_txt" Guid="58ff9c45-a7c9-4e22-8845-a9a92610c1f3">
|
||||||
<File Name="gamcommands.txt" KeyPath="yes" />
|
<File Name="gamcommands.txt" KeyPath="yes" />
|
||||||
</Component>
|
</Component>
|
||||||
|
<Component Id="roots_pem" Guid="18ff9c45-a3c9-4e22-8445-a8a92610c1f3">
|
||||||
|
<File Name="roots.pem" KeyPath="yes" />
|
||||||
|
</Component>
|
||||||
</ComponentGroup>
|
</ComponentGroup>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ from gam.gapi import chromemanagement as gapi_chromemanagement
|
|||||||
from gam.gapi import chromepolicy as gapi_chromepolicy
|
from gam.gapi import chromepolicy as gapi_chromepolicy
|
||||||
from gam.gapi.cloudidentity import devices as gapi_cloudidentity_devices
|
from gam.gapi.cloudidentity import devices as gapi_cloudidentity_devices
|
||||||
from gam.gapi.cloudidentity import groups as gapi_cloudidentity_groups
|
from gam.gapi.cloudidentity import groups as gapi_cloudidentity_groups
|
||||||
|
from gam.gapi.cloudidentity import inboundsso as gapi_cloudidentity_inboundsso
|
||||||
from gam.gapi.cloudidentity import orgunits as gapi_cloudidentity_orgunits
|
from gam.gapi.cloudidentity import orgunits as gapi_cloudidentity_orgunits
|
||||||
from gam.gapi.cloudidentity import userinvitations as gapi_cloudidentity_userinvitations
|
from gam.gapi.cloudidentity import userinvitations as gapi_cloudidentity_userinvitations
|
||||||
from gam.gapi import contactdelegation as gapi_contactdelegation
|
from gam.gapi import contactdelegation as gapi_contactdelegation
|
||||||
@@ -553,9 +554,12 @@ def SetGlobalVariables():
|
|||||||
'debug.gam',
|
'debug.gam',
|
||||||
filePresentValue=4,
|
filePresentValue=4,
|
||||||
fileAbsentValue=0)
|
fileAbsentValue=0)
|
||||||
|
_getOldSignalFile(GC_LOW_MEMORY, 'lowmemory.txt')
|
||||||
_getOldSignalFile(GC_NO_BROWSER, 'nobrowser.txt')
|
_getOldSignalFile(GC_NO_BROWSER, 'nobrowser.txt')
|
||||||
_getOldSignalFile(GC_NO_TDEMAIL, 'notdemail.txt')
|
_getOldSignalFile(GC_NO_TDEMAIL, 'notdemail.txt')
|
||||||
_getOldSignalFile(GC_OAUTH_BROWSER, 'oauthbrowser.txt')
|
# oauthbrowser.txt is deprecated as we now always
|
||||||
|
# use the localhost flow.
|
||||||
|
#_getOldSignalFile(GC_OAUTH_BROWSER, 'oauthbrowser.txt')
|
||||||
# _getOldSignalFile(GC_NO_CACHE, u'nocache.txt')
|
# _getOldSignalFile(GC_NO_CACHE, u'nocache.txt')
|
||||||
# _getOldSignalFile(GC_CACHE_DISCOVERY_ONLY, u'allcache.txt', filePresentValue=False, fileAbsentValue=True)
|
# _getOldSignalFile(GC_CACHE_DISCOVERY_ONLY, u'allcache.txt', filePresentValue=False, fileAbsentValue=True)
|
||||||
_getOldSignalFile(GC_NO_CACHE,
|
_getOldSignalFile(GC_NO_CACHE,
|
||||||
@@ -631,18 +635,19 @@ TIME_OFFSET_UNITS = [('day', 86400), ('hour', 3600), ('minute', 60),
|
|||||||
|
|
||||||
|
|
||||||
def getLocalGoogleTimeOffset(testLocation='admin.googleapis.com'):
|
def getLocalGoogleTimeOffset(testLocation='admin.googleapis.com'):
|
||||||
|
# Try with http first, if time is close (<MAX_LOCAL_GOOGLE_TIME_OFFSET seconds),
|
||||||
|
# retry with https
|
||||||
|
badhttp = transport.create_http()
|
||||||
|
for prot in ['http', 'https']:
|
||||||
localUTC = datetime.datetime.now(datetime.timezone.utc)
|
localUTC = datetime.datetime.now(datetime.timezone.utc)
|
||||||
try:
|
try:
|
||||||
# we disable SSL verify so we can still get time even if clock
|
|
||||||
# is way off. This could be spoofed / MitM but we'll fail for those
|
|
||||||
# situations everywhere else but here.
|
|
||||||
badhttp = transport.create_http()
|
|
||||||
badhttp.disable_ssl_certificate_validation = True
|
|
||||||
googleUTC = dateutil.parser.parse(
|
googleUTC = dateutil.parser.parse(
|
||||||
badhttp.request('https://' + testLocation, 'HEAD')[0]['date'])
|
badhttp.request(f'{prot}://' + testLocation, 'HEAD')[0]['date'])
|
||||||
except (httplib2.ServerNotFoundError, RuntimeError, ValueError) as e:
|
except (httplib2.ServerNotFoundError, RuntimeError, ValueError) as e:
|
||||||
controlflow.system_error_exit(4, str(e))
|
controlflow.system_error_exit(4, str(e))
|
||||||
offset = remainder = int(abs((localUTC - googleUTC).total_seconds()))
|
offset = remainder = int(abs((localUTC - googleUTC).total_seconds()))
|
||||||
|
if offset < MAX_LOCAL_GOOGLE_TIME_OFFSET and prot == 'http':
|
||||||
|
continue
|
||||||
timeoff = []
|
timeoff = []
|
||||||
for tou in TIME_OFFSET_UNITS:
|
for tou in TIME_OFFSET_UNITS:
|
||||||
uval, remainder = divmod(remainder, tou[1])
|
uval, remainder = divmod(remainder, tou[1])
|
||||||
@@ -777,8 +782,9 @@ def checkConnection():
|
|||||||
success_count = 0
|
success_count = 0
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
try_count += 1
|
try_count += 1
|
||||||
check_line = f'Checking {host} ({try_count}/{host_count})...'
|
ip = socket.gethostbyname(host)
|
||||||
sys.stdout.write(f'{check_line:<60}')
|
check_line = f'Checking {host} ({ip}) ({try_count}/{host_count})...'
|
||||||
|
sys.stdout.write(f'{check_line:<80}')
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
try:
|
try:
|
||||||
httpc.request(f'https://{host}/', 'HEAD', headers=headers)
|
httpc.request(f'https://{host}/', 'HEAD', headers=headers)
|
||||||
@@ -1308,6 +1314,10 @@ def doCheckServiceAccount(users):
|
|||||||
3,
|
3,
|
||||||
'Invalid private key in oauth2service.json. Please delete the file and then\nrecreate with "gam create project" or "gam use project"'
|
'Invalid private key in oauth2service.json. Please delete the file and then\nrecreate with "gam create project" or "gam use project"'
|
||||||
)
|
)
|
||||||
|
key_type = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA].get('key_type', 'default')
|
||||||
|
if key_type == 'yubikey':
|
||||||
|
printPassFail('Skipping age check. YubiKey rotation not necessary.', test_pass)
|
||||||
|
else:
|
||||||
print(
|
print(
|
||||||
'Checking key age. Google recommends rotating keys on a routine basis...'
|
'Checking key age. Google recommends rotating keys on a routine basis...'
|
||||||
)
|
)
|
||||||
@@ -4467,15 +4477,7 @@ def getImap(users):
|
|||||||
soft_errors=True,
|
soft_errors=True,
|
||||||
userId='me')
|
userId='me')
|
||||||
if result:
|
if result:
|
||||||
enabled = result['enabled']
|
display.print_json(result)
|
||||||
if enabled:
|
|
||||||
print(
|
|
||||||
f'User: {user}, IMAP Enabled: {enabled}, autoExpunge: {result["autoExpunge"]}, expungeBehavior: {result["expungeBehavior"]}, maxFolderSize: {result["maxFolderSize"]}{currentCount(i, count)}'
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
f'User: {user}, IMAP Enabled: {enabled}{currentCount(i, count)}'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def doPop(users):
|
def doPop(users):
|
||||||
@@ -6572,6 +6574,15 @@ def getUserAttributes(i, cd, updateCmd):
|
|||||||
body.setdefault('name', {})
|
body.setdefault('name', {})
|
||||||
body['name']['familyName'] = sys.argv[i + 1]
|
body['name']['familyName'] = sys.argv[i + 1]
|
||||||
i += 2
|
i += 2
|
||||||
|
elif myarg in ['displayname']:
|
||||||
|
body.setdefault('name', {})
|
||||||
|
body['name']['displayName'] = sys.argv[i + 1]
|
||||||
|
# sigh, the API is wonky. If we set just displayName
|
||||||
|
# we get an error. But if we also "set" fullName which is
|
||||||
|
# really just a concat of first/last name and can't be set
|
||||||
|
# then it works. Go figure.
|
||||||
|
body['name']['fullName'] = sys.argv[i+1]
|
||||||
|
i += 2
|
||||||
elif myarg in ['username', 'email', 'primaryemail'] and updateCmd:
|
elif myarg in ['username', 'email', 'primaryemail'] and updateCmd:
|
||||||
body['primaryEmail'] = normalizeEmailAddressOrUID(sys.argv[i + 1],
|
body['primaryEmail'] = normalizeEmailAddressOrUID(sys.argv[i + 1],
|
||||||
noUid=True)
|
noUid=True)
|
||||||
@@ -7128,7 +7139,7 @@ def getCRMService(login_hint):
|
|||||||
scopes,
|
scopes,
|
||||||
'online',
|
'online',
|
||||||
login_hint=login_hint,
|
login_hint=login_hint,
|
||||||
use_console_flow=not GC_Values[GC_OAUTH_BROWSER])
|
open_browser=not GC_Values[GC_NO_BROWSER])
|
||||||
httpc = transport.AuthorizedHttp(creds, transport.create_http())
|
httpc = transport.AuthorizedHttp(creds, transport.create_http())
|
||||||
return getService('cloudresourcemanager', httpc), httpc
|
return getService('cloudresourcemanager', httpc), httpc
|
||||||
|
|
||||||
@@ -7284,7 +7295,7 @@ def _createClientSecretsOauth2service(httpObj, projectId, login_hint):
|
|||||||
'code':
|
'code':
|
||||||
'ThisIsAnInvalidCodeOnlyBeingUsedToTestIfClientAndSecretAreValid',
|
'ThisIsAnInvalidCodeOnlyBeingUsedToTestIfClientAndSecretAreValid',
|
||||||
'redirect_uri':
|
'redirect_uri':
|
||||||
'urn:ietf:wg:oauth:2.0:oob',
|
'http://127.0.0.1:8080/',
|
||||||
'grant_type':
|
'grant_type':
|
||||||
'authorization_code'
|
'authorization_code'
|
||||||
}
|
}
|
||||||
@@ -7431,12 +7442,21 @@ def _getCurrentProjectID():
|
|||||||
|
|
||||||
def _getProjects(crm, pfilter):
|
def _getProjects(crm, pfilter):
|
||||||
try:
|
try:
|
||||||
return gapi.get_all_pages(
|
projects = gapi.get_all_pages(
|
||||||
crm.projects(),
|
crm.projects(),
|
||||||
'search',
|
'search',
|
||||||
'projects',
|
'projects',
|
||||||
throw_reasons=[gapi_errors.ErrorReason.BAD_REQUEST],
|
throw_reasons=[gapi_errors.ErrorReason.BAD_REQUEST],
|
||||||
query=pfilter)
|
query=pfilter)
|
||||||
|
if projects:
|
||||||
|
return projects
|
||||||
|
if pfilter.startswith('id:'):
|
||||||
|
pfilter = pfilter[3:]
|
||||||
|
return [gapi.call(
|
||||||
|
crm.projects(),
|
||||||
|
'get',
|
||||||
|
name=f'projects/{pfilter}',
|
||||||
|
throw_reasons=[gapi_errors.ErrorReason.BAD_REQUEST])]
|
||||||
except gapi_errors.GapiBadRequestError as e:
|
except gapi_errors.GapiBadRequestError as e:
|
||||||
controlflow.system_error_exit(2, f'Project: {pfilter}, {str(e)}')
|
controlflow.system_error_exit(2, f'Project: {pfilter}, {str(e)}')
|
||||||
|
|
||||||
@@ -7721,7 +7741,7 @@ def doUpdateProjects():
|
|||||||
_grantRotateRights(iam, sa_email, sa_email)
|
_grantRotateRights(iam, sa_email, sa_email)
|
||||||
|
|
||||||
|
|
||||||
def _generatePrivateKeyAndPublicCert(client_id, key_size):
|
def _generatePrivateKeyAndPublicCert(client_id, key_size, b64enc_pub=True):
|
||||||
print(' Generating new private key...')
|
print(' Generating new private key...')
|
||||||
private_key = rsa.generate_private_key(public_exponent=65537,
|
private_key = rsa.generate_private_key(public_exponent=65537,
|
||||||
key_size=key_size,
|
key_size=key_size,
|
||||||
@@ -7765,6 +7785,8 @@ def _generatePrivateKeyAndPublicCert(client_id, key_size):
|
|||||||
backend=default_backend())
|
backend=default_backend())
|
||||||
public_cert_pem = certificate.public_bytes(
|
public_cert_pem = certificate.public_bytes(
|
||||||
serialization.Encoding.PEM).decode()
|
serialization.Encoding.PEM).decode()
|
||||||
|
if not b64enc_pub:
|
||||||
|
return private_pem, public_cert_pem
|
||||||
publicKeyData = base64.b64encode(public_cert_pem.encode())
|
publicKeyData = base64.b64encode(public_cert_pem.encode())
|
||||||
if isinstance(publicKeyData, bytes):
|
if isinstance(publicKeyData, bytes):
|
||||||
publicKeyData = publicKeyData.decode()
|
publicKeyData = publicKeyData.decode()
|
||||||
@@ -7895,6 +7917,7 @@ def doCreateOrRotateServiceAccountKeys(iam=None,
|
|||||||
yk = yubikey.YubiKey(new_data)
|
yk = yubikey.YubiKey(new_data)
|
||||||
if 'yubikey_serial_number' not in new_data:
|
if 'yubikey_serial_number' not in new_data:
|
||||||
new_data['yubikey_serial_number'] = yk.get_serial_number()
|
new_data['yubikey_serial_number'] = yk.get_serial_number()
|
||||||
|
yk = yubikey.YubiKey(new_data)
|
||||||
if 'yubikey_slot' not in new_data:
|
if 'yubikey_slot' not in new_data:
|
||||||
new_data['yubikey_slot'] = 'AUTHENTICATION'
|
new_data['yubikey_slot'] = 'AUTHENTICATION'
|
||||||
publicKeyData = yk.get_certificate()
|
publicKeyData = yk.get_certificate()
|
||||||
@@ -8930,10 +8953,16 @@ def doGetUserInfo(user_email=None):
|
|||||||
customFieldMask=customFieldMask,
|
customFieldMask=customFieldMask,
|
||||||
viewType=viewType)
|
viewType=viewType)
|
||||||
print(f'User: {user["primaryEmail"]}')
|
print(f'User: {user["primaryEmail"]}')
|
||||||
if 'name' in user and 'givenName' in user['name']:
|
if 'name' in user:
|
||||||
print(f'First Name: {user["name"]["givenName"]}')
|
names = {
|
||||||
if 'name' in user and 'familyName' in user['name']:
|
'givenName': 'First Name',
|
||||||
print(f'Last Name: {user["name"]["familyName"]}')
|
'familyName': 'Last Name',
|
||||||
|
'fullName': 'Full Name',
|
||||||
|
'displayName': 'Display Name',
|
||||||
|
}
|
||||||
|
for field, description in names.items():
|
||||||
|
if field in user['name']:
|
||||||
|
print(f'{description}: {user["name"][field]}')
|
||||||
if 'languages' in user:
|
if 'languages' in user:
|
||||||
print(f"Languages: {_formatLanguagesList(user['languages'], ',')}")
|
print(f"Languages: {_formatLanguagesList(user['languages'], ',')}")
|
||||||
if 'isAdmin' in user:
|
if 'isAdmin' in user:
|
||||||
@@ -9478,7 +9507,7 @@ def doUndeleteUser():
|
|||||||
i = 4
|
i = 4
|
||||||
while i < len(sys.argv):
|
while i < len(sys.argv):
|
||||||
myarg = sys.argv[i].lower()
|
myarg = sys.argv[i].lower()
|
||||||
if myarg in ['ou', 'org']:
|
if myarg in ['ou', 'org', 'orgunit']:
|
||||||
orgUnit = gapi_directory_orgunits.makeOrgUnitPathAbsolute(
|
orgUnit = gapi_directory_orgunits.makeOrgUnitPathAbsolute(
|
||||||
sys.argv[i + 1])
|
sys.argv[i + 1])
|
||||||
i += 2
|
i += 2
|
||||||
@@ -9641,6 +9670,7 @@ USER_ARGUMENT_TO_PROPERTY_MAP = {
|
|||||||
'changepasswordatnextlogin': ['changePasswordAtNextLogin',],
|
'changepasswordatnextlogin': ['changePasswordAtNextLogin',],
|
||||||
'creationtime': ['creationTime',],
|
'creationtime': ['creationTime',],
|
||||||
'deletiontime': ['deletionTime',],
|
'deletiontime': ['deletionTime',],
|
||||||
|
'displayname': ['displayName',],
|
||||||
'email': ['emails',],
|
'email': ['emails',],
|
||||||
'emails': ['emails',],
|
'emails': ['emails',],
|
||||||
'externalid': ['externalIds',],
|
'externalid': ['externalIds',],
|
||||||
@@ -9682,6 +9712,7 @@ USER_ARGUMENT_TO_PROPERTY_MAP = {
|
|||||||
'location': ['locations',],
|
'location': ['locations',],
|
||||||
'locations': ['locations',],
|
'locations': ['locations',],
|
||||||
'name': [
|
'name': [
|
||||||
|
'name.displayName',
|
||||||
'name.givenName',
|
'name.givenName',
|
||||||
'name.familyName',
|
'name.familyName',
|
||||||
'name.fullName',
|
'name.fullName',
|
||||||
@@ -10542,7 +10573,7 @@ def doRequestOAuth(login_hint=None, scopes=None):
|
|||||||
access_type='offline',
|
access_type='offline',
|
||||||
login_hint=login_hint,
|
login_hint=login_hint,
|
||||||
credentials_file=GC_Values[GC_OAUTH2_TXT],
|
credentials_file=GC_Values[GC_OAUTH2_TXT],
|
||||||
use_console_flow=not GC_Values[GC_OAUTH_BROWSER])
|
open_browser=not GC_Values[GC_NO_BROWSER])
|
||||||
creds.write()
|
creds.write()
|
||||||
except gam.auth.oauth.InvalidClientSecretsFileError:
|
except gam.auth.oauth.InvalidClientSecretsFileError:
|
||||||
controlflow.system_error_exit(14, missing_client_secrets_message)
|
controlflow.system_error_exit(14, missing_client_secrets_message)
|
||||||
@@ -10584,6 +10615,11 @@ OAUTH2_SCOPES = [
|
|||||||
'subscopes': ['readonly'],
|
'subscopes': ['readonly'],
|
||||||
'scopes': 'https://www.googleapis.com/auth/cloud-identity.groups'
|
'scopes': 'https://www.googleapis.com/auth/cloud-identity.groups'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'name': 'Cloud Identity - Inbound SSO Settings',
|
||||||
|
'subscopes': ['readonly'],
|
||||||
|
'scopes': 'https://www.googleapis.com/auth/cloud-identity.inboundsso',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'name': 'Cloud Identity - OrgUnits',
|
'name': 'Cloud Identity - OrgUnits',
|
||||||
'subscopes': ['readonly'],
|
'subscopes': ['readonly'],
|
||||||
@@ -10593,7 +10629,6 @@ OAUTH2_SCOPES = [
|
|||||||
'name': 'Cloud Identity - User Invitations',
|
'name': 'Cloud Identity - User Invitations',
|
||||||
'subscopes': ['readonly'],
|
'subscopes': ['readonly'],
|
||||||
'scopes': 'https://www.googleapis.com/auth/cloud-identity.userinvitations',
|
'scopes': 'https://www.googleapis.com/auth/cloud-identity.userinvitations',
|
||||||
'offByDefault': True,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Contact Delegation',
|
'name': 'Contact Delegation',
|
||||||
@@ -10716,10 +10751,16 @@ OAUTH2_SCOPES = [
|
|||||||
'subscopes': ['readonly'],
|
'subscopes': ['readonly'],
|
||||||
'scopes': 'https://www.googleapis.com/auth/ediscovery'
|
'scopes': 'https://www.googleapis.com/auth/ediscovery'
|
||||||
},
|
},
|
||||||
|
# off by default to avoid reauth issues with GCP APIs
|
||||||
|
# and since many admins never use Vault API.
|
||||||
{
|
{
|
||||||
'name': 'Cloud Storage (Vault Export - read only)',
|
'name': 'Cloud Storage - Vault/Takeout Download/Copy',
|
||||||
'subscopes': [],
|
'subscopes': ['readonly'],
|
||||||
'scopes': 'https://www.googleapis.com/auth/devstorage.read_only'
|
'offByDefault': True,
|
||||||
|
'restricted_scopes': {
|
||||||
|
'readonly': 'https://www.googleapis.com/auth/devstorage.read_only'
|
||||||
|
},
|
||||||
|
'scopes': 'https://www.googleapis.com/auth/devstorage.read_write'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'User Profile (Email address - read only)',
|
'name': 'User Profile (Email address - read only)',
|
||||||
@@ -10765,6 +10806,7 @@ class ScopeMenuOption():
|
|||||||
is_required=False,
|
is_required=False,
|
||||||
is_selected=False,
|
is_selected=False,
|
||||||
supported_restrictions=None,
|
supported_restrictions=None,
|
||||||
|
restricted_scopes=None,
|
||||||
restriction=None):
|
restriction=None):
|
||||||
"""A data structure for storing and toggling feature/API scope attributes.
|
"""A data structure for storing and toggling feature/API scope attributes.
|
||||||
|
|
||||||
@@ -10794,6 +10836,7 @@ class ScopeMenuOption():
|
|||||||
self._restriction = None
|
self._restriction = None
|
||||||
|
|
||||||
self.scopes = oauth_scopes
|
self.scopes = oauth_scopes
|
||||||
|
self.restricted_scopes = restricted_scopes
|
||||||
self.description = description
|
self.description = description
|
||||||
self.is_required = is_required
|
self.is_required = is_required
|
||||||
# Required scopes must be selected
|
# Required scopes must be selected
|
||||||
@@ -10884,6 +10927,9 @@ class ScopeMenuOption():
|
|||||||
effective_scopes = []
|
effective_scopes = []
|
||||||
for scope in self.scopes:
|
for scope in self.scopes:
|
||||||
if self.is_restricted:
|
if self.is_restricted:
|
||||||
|
if self.restricted_scopes.get(self._restriction):
|
||||||
|
scope = self.restricted_scopes.get(self._restriction)
|
||||||
|
else:
|
||||||
scope = f'{scope}.{self._restriction}'
|
scope = f'{scope}.{self._restriction}'
|
||||||
effective_scopes.append(scope)
|
effective_scopes.append(scope)
|
||||||
return effective_scopes
|
return effective_scopes
|
||||||
@@ -10896,7 +10942,10 @@ class ScopeMenuOption():
|
|||||||
name: Some description of the API/feature.
|
name: Some description of the API/feature.
|
||||||
subscopes: A list of compatible scope restrictions such as 'action' or
|
subscopes: A list of compatible scope restrictions such as 'action' or
|
||||||
'readonly'. Each scope in the scopes list must support this
|
'readonly'. Each scope in the scopes list must support this
|
||||||
restriction text appended to the end of its normal scope text.
|
restriction text appended to the end of its normal scope text or
|
||||||
|
be defined in the restricted_scopes attribute.
|
||||||
|
restricted_scopes: A dict of scopes to be used for restrictions. If not
|
||||||
|
defined then {scope}.{subscope} is used.
|
||||||
scopes: A list of scopes that are required for the API/feature.
|
scopes: A list of scopes that are required for the API/feature.
|
||||||
offByDefault: A bool indicating whether this feature/scope should be off
|
offByDefault: A bool indicating whether this feature/scope should be off
|
||||||
by default (when no prior selection has been made). Default is False
|
by default (when no prior selection has been made). Default is False
|
||||||
@@ -10925,8 +10974,8 @@ class ScopeMenuOption():
|
|||||||
description=scope_definition.get('name'),
|
description=scope_definition.get('name'),
|
||||||
is_selected=not scope_definition.get('offByDefault'),
|
is_selected=not scope_definition.get('offByDefault'),
|
||||||
supported_restrictions=scope_definition.get('subscopes', []),
|
supported_restrictions=scope_definition.get('subscopes', []),
|
||||||
is_required=scope_definition.get('required', False))
|
is_required=scope_definition.get('required', False),
|
||||||
|
restricted_scopes=scope_definition.get('restricted_scopes', {}))
|
||||||
|
|
||||||
class ScopeSelectionMenu():
|
class ScopeSelectionMenu():
|
||||||
"""A text menu which prompts the user to select the scopes to authorize."""
|
"""A text menu which prompts the user to select the scopes to authorize."""
|
||||||
@@ -11241,7 +11290,7 @@ def run_batch(items):
|
|||||||
)
|
)
|
||||||
pool.close()
|
pool.close()
|
||||||
pool.join()
|
pool.join()
|
||||||
pool = mp_pool(num_worker_threads, init_gam_worker, maxtasksperchild=200, initargs=(1,))
|
pool = mp_pool(num_worker_threads, init_gam_worker, maxtasksperchild=200, initargs=(l,))
|
||||||
sys.stderr.write(
|
sys.stderr.write(
|
||||||
'commit-batch - running processes finished, proceeding\n')
|
'commit-batch - running processes finished, proceeding\n')
|
||||||
continue
|
continue
|
||||||
@@ -11414,6 +11463,8 @@ def ProcessGAMCommand(args):
|
|||||||
i, encoding = getCharSet(i + 1)
|
i, encoding = getCharSet(i + 1)
|
||||||
f = fileutils.open_file(filename, encoding=encoding)
|
f = fileutils.open_file(filename, encoding=encoding)
|
||||||
csvFile = csv.DictReader(f)
|
csvFile = csv.DictReader(f)
|
||||||
|
if not csvFile.fieldnames:
|
||||||
|
controlflow.system_error_exit(0, f'CSV file {filename} is empty')
|
||||||
if (i == len(sys.argv)) or (sys.argv[i].lower() !=
|
if (i == len(sys.argv)) or (sys.argv[i].lower() !=
|
||||||
'gam') or (i + 1 == len(sys.argv)):
|
'gam') or (i + 1 == len(sys.argv)):
|
||||||
controlflow.system_error_exit(
|
controlflow.system_error_exit(
|
||||||
@@ -11463,7 +11514,13 @@ def ProcessGAMCommand(args):
|
|||||||
gapi_cloudidentity_groups.create()
|
gapi_cloudidentity_groups.create()
|
||||||
elif argument in ['nickname', 'alias']:
|
elif argument in ['nickname', 'alias']:
|
||||||
doCreateAlias()
|
doCreateAlias()
|
||||||
elif argument in ['org', 'ou']:
|
elif argument in ['inboundssoprofile', 'inboundssoprofiles']:
|
||||||
|
gapi_cloudidentity_inboundsso.create_profile()
|
||||||
|
elif argument in ['inboundssocredential', 'inboundssocredentials']:
|
||||||
|
gapi_cloudidentity_inboundsso.create_credentials()
|
||||||
|
elif argument in ['inboundssoassignment', 'inboundssoassignments']:
|
||||||
|
gapi_cloudidentity_inboundsso.create_assignment()
|
||||||
|
elif argument in ['org', 'orgunit', 'ou']:
|
||||||
gapi_directory_orgunits.create()
|
gapi_directory_orgunits.create()
|
||||||
elif argument == 'resource':
|
elif argument == 'resource':
|
||||||
gapi_directory_resource.createResourceCalendar()
|
gapi_directory_resource.createResourceCalendar()
|
||||||
@@ -11535,10 +11592,14 @@ def ProcessGAMCommand(args):
|
|||||||
gapi_cloudidentity_groups.update()
|
gapi_cloudidentity_groups.update()
|
||||||
elif argument in ['nickname', 'alias']:
|
elif argument in ['nickname', 'alias']:
|
||||||
doUpdateAlias()
|
doUpdateAlias()
|
||||||
elif argument in ['ou', 'org']:
|
elif argument in ['ou', 'org', 'orgunit']:
|
||||||
gapi_directory_orgunits.update()
|
gapi_directory_orgunits.update()
|
||||||
elif argument == 'resource':
|
elif argument == 'resource':
|
||||||
gapi_directory_resource.updateResourceCalendar()
|
gapi_directory_resource.updateResourceCalendar()
|
||||||
|
elif argument in ['inboundssoprofile', 'inboundssoprofiles']:
|
||||||
|
gapi_cloudidentity_inboundsso.update_profile()
|
||||||
|
elif argument in ['inboundssoassignment', 'inboundssoasignments']:
|
||||||
|
gapi_cloudidentity_inboundsso.update_assignment()
|
||||||
elif argument == 'cros':
|
elif argument == 'cros':
|
||||||
gapi_directory_cros.doUpdateCros()
|
gapi_directory_cros.doUpdateCros()
|
||||||
elif argument == 'mobile':
|
elif argument == 'mobile':
|
||||||
@@ -11600,7 +11661,11 @@ def ProcessGAMCommand(args):
|
|||||||
doGetAliasInfo()
|
doGetAliasInfo()
|
||||||
elif argument == 'instance':
|
elif argument == 'instance':
|
||||||
gapi_directory_customer.doGetCustomerInfo()
|
gapi_directory_customer.doGetCustomerInfo()
|
||||||
elif argument in ['org', 'ou']:
|
elif argument in ['inboundssoprofile', 'inboundssoprofiles']:
|
||||||
|
gapi_cloudidentity_inboundsso.info_profile()
|
||||||
|
elif argument in ['inboundssoassignment', 'inboundssoassignments']:
|
||||||
|
gapi_cloudidentity_inboundsso.info_assignment()
|
||||||
|
elif argument in ['ou', 'org', 'orgunit']:
|
||||||
gapi_directory_orgunits.info()
|
gapi_directory_orgunits.info()
|
||||||
elif argument == 'resource':
|
elif argument == 'resource':
|
||||||
gapi_directory_resource.getResourceCalendarInfo()
|
gapi_directory_resource.getResourceCalendarInfo()
|
||||||
@@ -11675,8 +11740,12 @@ def ProcessGAMCommand(args):
|
|||||||
gapi_cloudidentity_groups.delete()
|
gapi_cloudidentity_groups.delete()
|
||||||
elif argument in ['nickname', 'alias']:
|
elif argument in ['nickname', 'alias']:
|
||||||
doDeleteAlias()
|
doDeleteAlias()
|
||||||
elif argument == 'org':
|
elif argument in ['ou', 'org', 'orgunit']:
|
||||||
gapi_directory_orgunits.delete()
|
gapi_directory_orgunits.delete()
|
||||||
|
elif argument in ['inboundssoprofile', 'inboundssoprofiles']:
|
||||||
|
gapi_cloudidentity_inboundsso.delete_profile()
|
||||||
|
elif argument in ['inboundssocredential', 'inboundssocredentials']:
|
||||||
|
gapi_cloudidentity_inboundsso.delete_credentials()
|
||||||
elif argument == 'resource':
|
elif argument == 'resource':
|
||||||
gapi_directory_resource.deleteResourceCalendar()
|
gapi_directory_resource.deleteResourceCalendar()
|
||||||
elif argument == 'mobile':
|
elif argument == 'mobile':
|
||||||
@@ -11768,6 +11837,12 @@ def ProcessGAMCommand(args):
|
|||||||
gapi_directory_groups.print_members()
|
gapi_directory_groups.print_members()
|
||||||
elif argument in ['cigroupmembers', 'cigroupsmembers']:
|
elif argument in ['cigroupmembers', 'cigroupsmembers']:
|
||||||
gapi_cloudidentity_groups.print_members()
|
gapi_cloudidentity_groups.print_members()
|
||||||
|
elif argument in ['inboundssoprofile', 'inboundssoprofiles']:
|
||||||
|
gapi_cloudidentity_inboundsso.print_show_profiles()
|
||||||
|
elif argument in ['inboundssocredential', 'inboundssocredentials']:
|
||||||
|
gapi_cloudidentity_inboundsso.print_show_credentials()
|
||||||
|
elif argument in ['inboundssoassignment', 'inboundssoassignments']:
|
||||||
|
gapi_cloudidentity_inboundsso.print_show_assignments()
|
||||||
elif argument in ['orgs', 'ous']:
|
elif argument in ['orgs', 'ous']:
|
||||||
gapi_directory_orgunits.print_()
|
gapi_directory_orgunits.print_()
|
||||||
elif argument == 'privileges':
|
elif argument == 'privileges':
|
||||||
@@ -11836,6 +11911,10 @@ def ProcessGAMCommand(args):
|
|||||||
gapi_chromemanagement.printApps()
|
gapi_chromemanagement.printApps()
|
||||||
elif argument in ['chromeappdevices']:
|
elif argument in ['chromeappdevices']:
|
||||||
gapi_chromemanagement.printAppDevices()
|
gapi_chromemanagement.printAppDevices()
|
||||||
|
elif argument in ['chromeaues']:
|
||||||
|
gapi_chromemanagement.printAUEs()
|
||||||
|
elif argument in ['chromeneedsattn']:
|
||||||
|
gapi_chromemanagement.printNeedsAttn()
|
||||||
elif argument in ['chromeversions']:
|
elif argument in ['chromeversions']:
|
||||||
gapi_chromemanagement.printVersions()
|
gapi_chromemanagement.printVersions()
|
||||||
elif argument in ['chromehistory']:
|
elif argument in ['chromehistory']:
|
||||||
@@ -11868,6 +11947,12 @@ def ProcessGAMCommand(args):
|
|||||||
gapi_licensing.show()
|
gapi_licensing.show()
|
||||||
elif argument in ['project', 'projects']:
|
elif argument in ['project', 'projects']:
|
||||||
doPrintShowProjects(False)
|
doPrintShowProjects(False)
|
||||||
|
elif argument in ['inboundssoprofile', 'inboundssoprofiles']:
|
||||||
|
gapi_cloudidentity_inboundsso.print_show_profiles('show')
|
||||||
|
elif argument in ['inboundssocredential', 'inboundssocredentials']:
|
||||||
|
gapi_cloudidentity_inboundsso.print_show_credentials('show')
|
||||||
|
elif argument in ['inboundssoassignment', 'inboundssoassignments']:
|
||||||
|
gapi_cloudidentity_inboundsso.print_show_assignments('show')
|
||||||
elif argument in ['sakey', 'sakeys']:
|
elif argument in ['sakey', 'sakeys']:
|
||||||
doShowServiceAccountKeys()
|
doShowServiceAccountKeys()
|
||||||
elif argument in ['browsertoken', 'browsertokens']:
|
elif argument in ['browsertoken', 'browsertokens']:
|
||||||
@@ -11960,11 +12045,21 @@ def ProcessGAMCommand(args):
|
|||||||
argument = sys.argv[2].lower()
|
argument = sys.argv[2].lower()
|
||||||
if argument in ['export', 'vaultexport']:
|
if argument in ['export', 'vaultexport']:
|
||||||
gapi_vault.downloadExport()
|
gapi_vault.downloadExport()
|
||||||
elif argument in ['storagebucket']:
|
elif argument in ['storagebucket', 'bucket']:
|
||||||
gapi_storage.download_bucket()
|
gapi_storage.download_bucket()
|
||||||
else:
|
else:
|
||||||
controlflow.invalid_argument_exit(argument, 'gam download')
|
controlflow.invalid_argument_exit(argument, 'gam download')
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
elif command == 'copy':
|
||||||
|
argument = sys.argv[2].lower().replace('_', '')
|
||||||
|
if argument in ['export', 'vaultexport']:
|
||||||
|
gapi_vault.copyExport()
|
||||||
|
elif argument in ['storagebucket', 'bucket']:
|
||||||
|
gapi_storage.copy_bucket()
|
||||||
|
else:
|
||||||
|
controlflow.invalid_argument_exit(argument, 'gam copy')
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
elif command == 'rotate':
|
elif command == 'rotate':
|
||||||
argument = sys.argv[2].lower()
|
argument = sys.argv[2].lower()
|
||||||
if argument in ['sakey', 'sakeys']:
|
if argument in ['sakey', 'sakeys']:
|
||||||
@@ -12008,6 +12103,7 @@ def ProcessGAMCommand(args):
|
|||||||
action = sys.argv[2].lower().replace('_', '')
|
action = sys.argv[2].lower().replace('_', '')
|
||||||
if action == 'resetpiv':
|
if action == 'resetpiv':
|
||||||
yk = yubikey.YubiKey()
|
yk = yubikey.YubiKey()
|
||||||
|
yk.serial_number = yk.get_serial_number()
|
||||||
yk.reset_piv()
|
yk.reset_piv()
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
users = getUsersToModify()
|
users = getUsersToModify()
|
||||||
|
|||||||
@@ -272,6 +272,7 @@ class Credentials(google.oauth2.credentials.Credentials):
|
|||||||
access_type='offline',
|
access_type='offline',
|
||||||
login_hint=None,
|
login_hint=None,
|
||||||
filename=None,
|
filename=None,
|
||||||
|
open_browser=True,
|
||||||
use_console_flow=False):
|
use_console_flow=False):
|
||||||
"""Runs an OAuth Flow from client secrets to generate credentials.
|
"""Runs an OAuth Flow from client secrets to generate credentials.
|
||||||
|
|
||||||
@@ -291,8 +292,11 @@ class Credentials(google.oauth2.credentials.Credentials):
|
|||||||
login_hint: String, The email address that will be displayed on the Google
|
login_hint: String, The email address that will be displayed on the Google
|
||||||
login page as a hint for the user to login to the correct account.
|
login page as a hint for the user to login to the correct account.
|
||||||
filename: String, the path to a file to use to save the credentials.
|
filename: String, the path to a file to use to save the credentials.
|
||||||
use_console_flow: Boolean, True if the authentication flow should be run
|
use_console_flow: OBSOLETE: Boolean, True if the authentication flow
|
||||||
strictly from a console; False to launch a browser for authentication.
|
should be run strictly from a console; False to launch a browser
|
||||||
|
for authentication.
|
||||||
|
open_browser: Boolean: whether or not GAM should try to open the browser
|
||||||
|
automatically.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Credentials
|
Credentials
|
||||||
@@ -312,12 +316,11 @@ class Credentials(google.oauth2.credentials.Credentials):
|
|||||||
flow = _ShortURLFlow.from_client_config(client_config,
|
flow = _ShortURLFlow.from_client_config(client_config,
|
||||||
scopes,
|
scopes,
|
||||||
autogenerate_code_verifier=True)
|
autogenerate_code_verifier=True)
|
||||||
flow_kwargs = {'access_type': access_type}
|
flow_kwargs = {'access_type': access_type,
|
||||||
|
'open_browser': open_browser}
|
||||||
if login_hint:
|
if login_hint:
|
||||||
flow_kwargs['login_hint'] = login_hint
|
flow_kwargs['login_hint'] = login_hint
|
||||||
|
flow.run_dual(**flow_kwargs)
|
||||||
flow.run_dual(use_console_flow,
|
|
||||||
**flow_kwargs)
|
|
||||||
return cls.from_google_oauth2_credentials(flow.credentials,
|
return cls.from_google_oauth2_credentials(flow.credentials,
|
||||||
filename=filename)
|
filename=filename)
|
||||||
|
|
||||||
@@ -328,6 +331,7 @@ class Credentials(google.oauth2.credentials.Credentials):
|
|||||||
access_type='offline',
|
access_type='offline',
|
||||||
login_hint=None,
|
login_hint=None,
|
||||||
credentials_file=None,
|
credentials_file=None,
|
||||||
|
open_browser=True,
|
||||||
use_console_flow=False):
|
use_console_flow=False):
|
||||||
"""Runs an OAuth Flow from secrets stored on disk to generate credentials.
|
"""Runs an OAuth Flow from secrets stored on disk to generate credentials.
|
||||||
|
|
||||||
@@ -348,8 +352,11 @@ class Credentials(google.oauth2.credentials.Credentials):
|
|||||||
login page as a hint for the user to login to the correct account.
|
login page as a hint for the user to login to the correct account.
|
||||||
credentials_file: String, the path to a file to use to save the
|
credentials_file: String, the path to a file to use to save the
|
||||||
credentials.
|
credentials.
|
||||||
use_console_flow: Boolean, True if the authentication flow should be run
|
use_console_flow: OBSOLETE: Boolean, True if the authentication flow
|
||||||
strictly from a console; False to launch a browser for authentication.
|
should be run strictly from a console; False to launch a browser for
|
||||||
|
authentication.
|
||||||
|
open_browser: Boolean, whether or not GAM should try to open the browser
|
||||||
|
directly.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
InvalidClientSecretsFileError: If the client secrets file cannot be
|
InvalidClientSecretsFileError: If the client secrets file cannot be
|
||||||
@@ -378,14 +385,13 @@ class Credentials(google.oauth2.credentials.Credentials):
|
|||||||
raise InvalidClientSecretsFileFormatError(
|
raise InvalidClientSecretsFileFormatError(
|
||||||
f'Could not extract Client ID or Client Secret from file {client_secrets_file}'
|
f'Could not extract Client ID or Client Secret from file {client_secrets_file}'
|
||||||
)
|
)
|
||||||
|
|
||||||
return cls.from_client_secrets(client_id,
|
return cls.from_client_secrets(client_id,
|
||||||
client_secret,
|
client_secret,
|
||||||
scopes,
|
scopes,
|
||||||
access_type=access_type,
|
access_type=access_type,
|
||||||
login_hint=login_hint,
|
login_hint=login_hint,
|
||||||
filename=credentials_file,
|
filename=credentials_file,
|
||||||
use_console_flow=use_console_flow)
|
open_browser=open_browser)
|
||||||
|
|
||||||
def _fetch_id_token_data(self):
|
def _fetch_id_token_data(self):
|
||||||
"""Fetches verification details from Google for the OAuth2.0 token.
|
"""Fetches verification details from Google for the OAuth2.0 token.
|
||||||
@@ -482,7 +488,11 @@ class Credentials(google.oauth2.credentials.Credentials):
|
|||||||
def _locked_refresh(self, request):
|
def _locked_refresh(self, request):
|
||||||
"""Refreshes the credential's access token while the file lock is held."""
|
"""Refreshes the credential's access token while the file lock is held."""
|
||||||
assert self._lock.is_locked
|
assert self._lock.is_locked
|
||||||
|
try:
|
||||||
super().refresh(request)
|
super().refresh(request)
|
||||||
|
except google.auth.exceptions.RefreshError as e:
|
||||||
|
controlflow.system_error_exit(9, str(e))
|
||||||
|
|
||||||
|
|
||||||
def write(self):
|
def write(self):
|
||||||
"""Writes credentials to disk."""
|
"""Writes credentials to disk."""
|
||||||
@@ -595,7 +605,6 @@ class _ShortURLFlow(google_auth_oauthlib.flow.InstalledAppFlow):
|
|||||||
|
|
||||||
|
|
||||||
def run_dual(self,
|
def run_dual(self,
|
||||||
use_console_flow,
|
|
||||||
authorization_prompt_message='',
|
authorization_prompt_message='',
|
||||||
console_prompt_message='',
|
console_prompt_message='',
|
||||||
web_success_message='',
|
web_success_message='',
|
||||||
@@ -605,7 +614,7 @@ class _ShortURLFlow(google_auth_oauthlib.flow.InstalledAppFlow):
|
|||||||
mgr = multiprocessing.Manager()
|
mgr = multiprocessing.Manager()
|
||||||
d = mgr.dict()
|
d = mgr.dict()
|
||||||
d['trailing_slash'] = redirect_uri_trailing_slash
|
d['trailing_slash'] = redirect_uri_trailing_slash
|
||||||
d['open_browser'] = use_console_flow
|
d['open_browser'] = open_browser
|
||||||
http_client = multiprocessing.Process(target=_wait_for_http_client,
|
http_client = multiprocessing.Process(target=_wait_for_http_client,
|
||||||
args=(d,))
|
args=(d,))
|
||||||
user_input = multiprocessing.Process(target=_wait_for_user_input,
|
user_input = multiprocessing.Process(target=_wait_for_user_input,
|
||||||
@@ -637,8 +646,8 @@ class _ShortURLFlow(google_auth_oauthlib.flow.InstalledAppFlow):
|
|||||||
code = parsed_params.get('code', [None])[0]
|
code = parsed_params.get('code', [None])[0]
|
||||||
try:
|
try:
|
||||||
fetch_args = {'code': code}
|
fetch_args = {'code': code}
|
||||||
if GC_Values.get('GC_CA_FILE'):
|
if GC_Values.get(GC_CA_FILE):
|
||||||
fetch_args['verify'] = GC_Values.get('GC_CA_FILE')
|
fetch_args['verify'] = GC_Values.get(GC_CA_FILE)
|
||||||
self.fetch_token(**fetch_args)
|
self.fetch_token(**fetch_args)
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from threading import Timer
|
|||||||
from cryptography.hazmat.primitives import hashes, serialization
|
from cryptography.hazmat.primitives import hashes, serialization
|
||||||
from cryptography.hazmat.primitives.asymmetric import padding
|
from cryptography.hazmat.primitives.asymmetric import padding
|
||||||
from smartcard.Exceptions import CardConnectionException
|
from smartcard.Exceptions import CardConnectionException
|
||||||
from ykman.device import connect_to_device
|
from ykman.device import list_all_devices
|
||||||
from ykman.piv import generate_self_signed_certificate, \
|
from ykman.piv import generate_self_signed_certificate, \
|
||||||
generate_chuid
|
generate_chuid
|
||||||
from yubikit.piv import DEFAULT_MANAGEMENT_KEY, \
|
from yubikit.piv import DEFAULT_MANAGEMENT_KEY, \
|
||||||
@@ -20,11 +20,14 @@ from yubikit.piv import DEFAULT_MANAGEMENT_KEY, \
|
|||||||
OBJECT_ID, \
|
OBJECT_ID, \
|
||||||
SLOT, \
|
SLOT, \
|
||||||
TOUCH_POLICY
|
TOUCH_POLICY
|
||||||
from yubikit.core.smartcard import ApduError
|
from yubikit.core.smartcard import ApduError, \
|
||||||
|
SmartCardConnection
|
||||||
from gam import controlflow
|
from gam import controlflow
|
||||||
|
|
||||||
|
|
||||||
class YubiKey():
|
class YubiKey():
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, service_account_info=None):
|
def __init__(self, service_account_info=None):
|
||||||
self.key_type = None
|
self.key_type = None
|
||||||
self.slot = None
|
self.slot = None
|
||||||
@@ -46,12 +49,16 @@ class YubiKey():
|
|||||||
self.pin = service_account_info.get('yubikey_pin')
|
self.pin = service_account_info.get('yubikey_pin')
|
||||||
self.key_id = service_account_info.get('private_key_id')
|
self.key_id = service_account_info.get('private_key_id')
|
||||||
|
|
||||||
|
|
||||||
def _connect(self):
|
def _connect(self):
|
||||||
try:
|
try:
|
||||||
conn, _, _ = connect_to_device(self.serial_number)
|
devices = list_all_devices()
|
||||||
|
for (device, info) in devices:
|
||||||
|
if info.serial == self.serial_number:
|
||||||
|
return device.open_connection(SmartCardConnection)
|
||||||
except CardConnectionException as err:
|
except CardConnectionException as err:
|
||||||
controlflow.system_error_exit(9, f'YubiKey - {err}')
|
controlflow.system_error_exit(9, f'YubiKey - {err}')
|
||||||
return conn
|
|
||||||
|
|
||||||
def get_certificate(self):
|
def get_certificate(self):
|
||||||
try:
|
try:
|
||||||
@@ -79,11 +86,22 @@ class YubiKey():
|
|||||||
|
|
||||||
def get_serial_number(self):
|
def get_serial_number(self):
|
||||||
try:
|
try:
|
||||||
_, _, info = connect_to_device(self.serial_number)
|
devices = list_all_devices()
|
||||||
|
if self.serial_number:
|
||||||
|
for (device, info) in devices:
|
||||||
|
if info.serial == self.serial_number:
|
||||||
return info.serial
|
return info.serial
|
||||||
|
msg = f'Could not find YubiKey with serial {self.serial_number}'
|
||||||
|
controlflow.system_error_exit(3, msg)
|
||||||
|
if len(devices) > 1:
|
||||||
|
serials = ', '.join([str(info.serial) for (_, info) in devices])
|
||||||
|
msg = f'Multiple YubiKeys connected. Specify yubikey_serial_number and one of {serials}'
|
||||||
|
controlflow.system_error_exit(4, msg)
|
||||||
|
return devices[0][1].serial
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
controlflow.system_error_exit(9, f'YubiKey - {err}')
|
controlflow.system_error_exit(9, f'YubiKey - {err}')
|
||||||
|
|
||||||
|
|
||||||
def reset_piv(self):
|
def reset_piv(self):
|
||||||
'''Resets YubiKey PIV app and generates new key for GAM to use.'''
|
'''Resets YubiKey PIV app and generates new key for GAM to use.'''
|
||||||
reply = str(input('This will wipe all PIV keys and configuration from your YubiKey. Are you sure? (y/N) ').lower().strip())
|
reply = str(input('This will wipe all PIV keys and configuration from your YubiKey. Are you sure? (y/N) ').lower().strip())
|
||||||
@@ -95,7 +113,9 @@ class YubiKey():
|
|||||||
piv = PivSession(conn)
|
piv = PivSession(conn)
|
||||||
piv.reset()
|
piv.reset()
|
||||||
rnd = SystemRandom()
|
rnd = SystemRandom()
|
||||||
pin_puk_chars = string.ascii_letters + string.digits + string.punctuation
|
pin_puk_chars = string.ascii_letters + \
|
||||||
|
string.digits + \
|
||||||
|
string.punctuation
|
||||||
new_puk = ''.join(rnd.choice(pin_puk_chars) for _ in range(8))
|
new_puk = ''.join(rnd.choice(pin_puk_chars) for _ in range(8))
|
||||||
new_pin = ''.join(rnd.choice(pin_puk_chars) for _ in range(8))
|
new_pin = ''.join(rnd.choice(pin_puk_chars) for _ in range(8))
|
||||||
piv.change_puk('12345678', new_puk)
|
piv.change_puk('12345678', new_puk)
|
||||||
@@ -155,3 +175,4 @@ class YubiKey():
|
|||||||
if 'mplock' in globals():
|
if 'mplock' in globals():
|
||||||
mplock.release()
|
mplock.release()
|
||||||
return signed
|
return signed
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ def missing_argument_exit(argument, command):
|
|||||||
"""Indicate that the argument is missing for the command.
|
"""Indicate that the argument is missing for the command.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
argument: the missingagrument
|
argument: the missing argument
|
||||||
command: the base GAM command
|
command: the base GAM command
|
||||||
"""
|
"""
|
||||||
system_error_exit(2, f'missing argument {argument} for "{command}"')
|
system_error_exit(2, f'missing argument {argument} for "{command}"')
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
"""Methods related to execution of GAPI requests."""
|
"""Methods related to execution of GAPI requests."""
|
||||||
|
|
||||||
|
import os.path
|
||||||
import sys
|
import sys
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
import googleapiclient.errors
|
import googleapiclient.errors
|
||||||
import google.auth.exceptions
|
import google.auth.exceptions
|
||||||
@@ -10,7 +12,8 @@ from gam import controlflow
|
|||||||
from gam import display
|
from gam import display
|
||||||
from gam.gapi import errors
|
from gam.gapi import errors
|
||||||
from gam import transport
|
from gam import transport
|
||||||
from gam.var import (GM_Globals, GM_CURRENT_API_SCOPES, GM_CURRENT_API_USER,
|
from gam.var import (GC_Values, GM_Globals,
|
||||||
|
GM_CURRENT_API_SCOPES, GM_CURRENT_API_USER,
|
||||||
GM_EXTRA_ARGS_DICT, GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID,
|
GM_EXTRA_ARGS_DICT, GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID,
|
||||||
MAX_RESULTS_API_EXCEPTIONS, MESSAGE_API_ACCESS_CONFIG,
|
MAX_RESULTS_API_EXCEPTIONS, MESSAGE_API_ACCESS_CONFIG,
|
||||||
MESSAGE_API_ACCESS_DENIED, MESSAGE_SERVICE_NOT_APPLICABLE)
|
MESSAGE_API_ACCESS_DENIED, MESSAGE_SERVICE_NOT_APPLICABLE)
|
||||||
@@ -238,8 +241,13 @@ def process_page(page, items, all_items, total_items, page_message, message_attr
|
|||||||
page_items = page.get(items, [])
|
page_items = page.get(items, [])
|
||||||
num_page_items = len(page_items)
|
num_page_items = len(page_items)
|
||||||
total_items += num_page_items
|
total_items += num_page_items
|
||||||
if all_items is not None:
|
if type(all_items) is list:
|
||||||
all_items.extend(page_items)
|
all_items.extend(page_items)
|
||||||
|
elif all_items is not None:
|
||||||
|
i = len(all_items)
|
||||||
|
for item in page_items:
|
||||||
|
all_items[str(i)] = item
|
||||||
|
i += 1
|
||||||
else:
|
else:
|
||||||
page_token = None
|
page_token = None
|
||||||
num_page_items = 0
|
num_page_items = 0
|
||||||
@@ -273,6 +281,7 @@ def finalize_page_message(page_message):
|
|||||||
sys.stderr.write('\r\n')
|
sys.stderr.write('\r\n')
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
|
|
||||||
|
|
||||||
def get_all_pages(service,
|
def get_all_pages(service,
|
||||||
function,
|
function,
|
||||||
items='items',
|
items='items',
|
||||||
@@ -341,13 +350,14 @@ def get_all_pages(service,
|
|||||||
page_token, total_items = process_page(page, items, all_items, total_items, page_message, message_attribute)
|
page_token, total_items = process_page(page, items, all_items, total_items, page_message, message_attribute)
|
||||||
if not page_token:
|
if not page_token:
|
||||||
finalize_page_message(page_message)
|
finalize_page_message(page_message)
|
||||||
|
if type(all_items) is not list:
|
||||||
|
all_items = all_items.values()
|
||||||
return all_items
|
return all_items
|
||||||
if page_args_in_body:
|
if page_args_in_body:
|
||||||
kwargs['body']['pageToken'] = page_token
|
kwargs['body']['pageToken'] = page_token
|
||||||
else:
|
else:
|
||||||
kwargs['pageToken'] = page_token
|
kwargs['pageToken'] = page_token
|
||||||
|
|
||||||
|
|
||||||
# TODO: Make this private once all execution related items that use this method
|
# TODO: Make this private once all execution related items that use this method
|
||||||
# have been brought into this file
|
# have been brought into this file
|
||||||
def handle_oauth_token_error(e, soft_errors):
|
def handle_oauth_token_error(e, soft_errors):
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ def printShowCrosTelemetry(mode):
|
|||||||
i = 3
|
i = 3
|
||||||
if mode == 'info':
|
if mode == 'info':
|
||||||
if i >= len(sys.argv):
|
if i >= len(sys.argv):
|
||||||
controlflow.system_error_exit(3, f'<SerialNumber> required for "gam info crostelemetry"')
|
controlflow.system_error_exit(3, '<SerialNumber> required for "gam info crostelemetry"')
|
||||||
filter_ = f'serialNumber={sys.argv[i]}'
|
filter_ = f'serialNumber={sys.argv[i]}'
|
||||||
i += 1
|
i += 1
|
||||||
mode = 'show'
|
mode = 'show'
|
||||||
@@ -286,6 +286,7 @@ def printShowCrosTelemetry(mode):
|
|||||||
device['storageInfo']['percentDiskUsed'] = 100 - device['storageInfo']['percentDiskFree']
|
device['storageInfo']['percentDiskUsed'] = 100 - device['storageInfo']['percentDiskFree']
|
||||||
for cpuStatusReport in device.get('cpuStatusReport', []):
|
for cpuStatusReport in device.get('cpuStatusReport', []):
|
||||||
for tempInfo in cpuStatusReport.pop('cpuTemperatureInfo', []):
|
for tempInfo in cpuStatusReport.pop('cpuTemperatureInfo', []):
|
||||||
|
if 'temperatureCelsius' in tempInfo:
|
||||||
cpuStatusReport[f"cpuTemperatureInfo.{tempInfo['label'].strip()}"] = tempInfo['temperatureCelsius']
|
cpuStatusReport[f"cpuTemperatureInfo.{tempInfo['label'].strip()}"] = tempInfo['temperatureCelsius']
|
||||||
if showOrgUnitPath:
|
if showOrgUnitPath:
|
||||||
orgUnitId = device.get('orgUnitId')
|
orgUnitId = device.get('orgUnitId')
|
||||||
@@ -306,6 +307,97 @@ def printShowCrosTelemetry(mode):
|
|||||||
display.write_csv_file(csvRows, titles, 'Telemetry Devices', todrive)
|
display.write_csv_file(csvRows, titles, 'Telemetry Devices', todrive)
|
||||||
|
|
||||||
|
|
||||||
|
CHROME_AUES_TITLES = [
|
||||||
|
'model', 'count', 'aueMonth', 'aueYear', 'expired'
|
||||||
|
]
|
||||||
|
def printAUEs():
|
||||||
|
cm = build()
|
||||||
|
customer = _get_customerid()
|
||||||
|
todrive = False
|
||||||
|
titles = CHROME_AUES_TITLES
|
||||||
|
csvRows = []
|
||||||
|
orgunit = None
|
||||||
|
minAueDate = None
|
||||||
|
maxAueDate = None
|
||||||
|
i = 3
|
||||||
|
while i < len(sys.argv):
|
||||||
|
myarg = sys.argv[i].lower().replace('_', '')
|
||||||
|
if myarg == 'todrive':
|
||||||
|
todrive = True
|
||||||
|
i += 1
|
||||||
|
elif myarg in ['ou', 'org', 'orgunit']:
|
||||||
|
orgunit = _get_orgunit(sys.argv[i+1])
|
||||||
|
i += 2
|
||||||
|
elif myarg == 'minauedate':
|
||||||
|
minAueDate = _getFilterDate(sys.argv[i + 1]).strftime(YYYYMMDD_FORMAT)
|
||||||
|
i += 2
|
||||||
|
elif myarg == 'maxauedate':
|
||||||
|
maxAueDate = _getFilterDate(sys.argv[i + 1]).strftime(YYYYMMDD_FORMAT)
|
||||||
|
i += 2
|
||||||
|
else:
|
||||||
|
msg = f'{myarg} is not a valid argument to "gam print chromeaues"'
|
||||||
|
controlflow.system_error_exit(3, msg)
|
||||||
|
if orgunit:
|
||||||
|
orgUnitPath = gapi_directory_orgunits.orgunit_from_orgunitid(orgunit, None)
|
||||||
|
query = f'orgUnitPath={orgUnitPath}'
|
||||||
|
titles.append('orgUnitPath')
|
||||||
|
else:
|
||||||
|
orgUnitPath = '/'
|
||||||
|
query = None
|
||||||
|
gam.printGettingAllItems('Chrome Auto Update Expirations', query)
|
||||||
|
aues = gapi.call(cm.customers().reports(),
|
||||||
|
'countChromeDevicesReachingAutoExpirationDate',
|
||||||
|
customer=customer, orgUnitId=orgunit,
|
||||||
|
minAueDate=minAueDate, maxAueDate=maxAueDate).get('deviceAueCountReports', [])
|
||||||
|
for aue in sorted(aues, key=lambda k: k.get('model', 'Unknown')):
|
||||||
|
if orgunit:
|
||||||
|
aue['orgUnitPath'] = orgUnitPath
|
||||||
|
csvRows.append(aue)
|
||||||
|
display.write_csv_file(csvRows, titles, 'Chrome AUEs', todrive)
|
||||||
|
|
||||||
|
|
||||||
|
CHROME_NEEDSATTN_TITLES = [
|
||||||
|
'noRecentPolicySyncCount', 'noRecentUserActivityCount', 'pendingUpdate',
|
||||||
|
'osVersionNotCompliantCount', 'unsupportedPolicyCount'
|
||||||
|
]
|
||||||
|
def printNeedsAttn():
|
||||||
|
cm = build()
|
||||||
|
customer = _get_customerid()
|
||||||
|
todrive = False
|
||||||
|
titles = CHROME_NEEDSATTN_TITLES[:]
|
||||||
|
csvRows = []
|
||||||
|
orgunit = None
|
||||||
|
i = 3
|
||||||
|
while i < len(sys.argv):
|
||||||
|
myarg = sys.argv[i].lower().replace('_', '')
|
||||||
|
if myarg == 'todrive':
|
||||||
|
todrive = True
|
||||||
|
i += 1
|
||||||
|
elif myarg in ['ou', 'org', 'orgunit']:
|
||||||
|
orgunit = _get_orgunit(sys.argv[i+1])
|
||||||
|
i += 2
|
||||||
|
else:
|
||||||
|
msg = f'{myarg} is not a valid argument to "gam print chromeneedsattn"'
|
||||||
|
controlflow.system_error_exit(3, msg)
|
||||||
|
if orgunit:
|
||||||
|
orgUnitPath = gapi_directory_orgunits.orgunit_from_orgunitid(orgunit, None)
|
||||||
|
query = f'orgUnitPath={orgUnitPath}'
|
||||||
|
titles.append('orgUnitPath')
|
||||||
|
else:
|
||||||
|
orgUnitPath = '/'
|
||||||
|
query = None
|
||||||
|
gam.printGettingAllItems('Chrome Devices Needing Attention', query)
|
||||||
|
result = gapi.call(cm.customers().reports(),
|
||||||
|
'countChromeDevicesThatNeedAttention',
|
||||||
|
customer=customer, orgUnitId=orgunit, readMask=','.join(CHROME_NEEDSATTN_TITLES))
|
||||||
|
for field in CHROME_NEEDSATTN_TITLES:
|
||||||
|
result.setdefault(field, 0)
|
||||||
|
if orgunit:
|
||||||
|
result['orgUnitPath'] = orgUnitPath
|
||||||
|
csvRows.append(result)
|
||||||
|
display.write_csv_file(csvRows, titles, 'Chrome Devices Needing Attention', todrive)
|
||||||
|
|
||||||
|
|
||||||
CHROME_VERSIONS_TITLES = [
|
CHROME_VERSIONS_TITLES = [
|
||||||
'version', 'count', 'channel', 'deviceOsVersion', 'system'
|
'version', 'count', 'channel', 'deviceOsVersion', 'system'
|
||||||
]
|
]
|
||||||
@@ -319,6 +411,7 @@ def printVersions():
|
|||||||
startDate = None
|
startDate = None
|
||||||
endDate = None
|
endDate = None
|
||||||
pfilter = None
|
pfilter = None
|
||||||
|
query = None
|
||||||
reverse = False
|
reverse = False
|
||||||
i = 3
|
i = 3
|
||||||
while i < len(sys.argv):
|
while i < len(sys.argv):
|
||||||
@@ -349,12 +442,18 @@ def printVersions():
|
|||||||
else:
|
else:
|
||||||
pfilter = ''
|
pfilter = ''
|
||||||
pfilter += f'last_active_date>={startDate}'
|
pfilter += f'last_active_date>={startDate}'
|
||||||
|
query = pfilter
|
||||||
if orgunit:
|
if orgunit:
|
||||||
orgUnitPath = gapi_directory_orgunits.orgunit_from_orgunitid(orgunit, None)
|
orgUnitPath = gapi_directory_orgunits.orgunit_from_orgunitid(orgunit, None)
|
||||||
|
if query:
|
||||||
|
query += ' AND '
|
||||||
|
else:
|
||||||
|
query = ''
|
||||||
|
query += f'orgUnitPath={orgUnitPath}'
|
||||||
titles.append('orgUnitPath')
|
titles.append('orgUnitPath')
|
||||||
else:
|
else:
|
||||||
orgUnitPath = '/'
|
orgUnitPath = '/'
|
||||||
gam.printGettingAllItems('Chrome Versions', pfilter)
|
gam.printGettingAllItems('Chrome Versions', query)
|
||||||
page_message = gapi.got_total_items_msg('Chrome Versions', '...\n')
|
page_message = gapi.got_total_items_msg('Chrome Versions', '...\n')
|
||||||
versions = gapi.get_all_pages(cm.customers().reports(),
|
versions = gapi.get_all_pages(cm.customers().reports(),
|
||||||
'countChromeVersions',
|
'countChromeVersions',
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ def build_schemas(svc=None, sfilter=None):
|
|||||||
for an_enum in schema['definition']['enumType']:
|
for an_enum in schema['definition']['enumType']:
|
||||||
if an_enum['name'] == type_name:
|
if an_enum['name'] == type_name:
|
||||||
setting_dict['enums'] = [enum['name'] for enum in an_enum['value']]
|
setting_dict['enums'] = [enum['name'] for enum in an_enum['value']]
|
||||||
setting_dict['enum_prefix'] = utils.commonprefix(setting_dict['enums'])
|
setting_dict['enum_prefix'] = utils.commonprefix(setting_dict['enums'], True)
|
||||||
prefix_len = len(setting_dict['enum_prefix'])
|
prefix_len = len(setting_dict['enum_prefix'])
|
||||||
setting_dict['enums'] = [enum[prefix_len:] for enum \
|
setting_dict['enums'] = [enum[prefix_len:] for enum \
|
||||||
in setting_dict['enums'] \
|
in setting_dict['enums'] \
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ def _parse_action(action):
|
|||||||
def info():
|
def info():
|
||||||
ci = gapi_cloudidentity.build_dwd()
|
ci = gapi_cloudidentity.build_dwd()
|
||||||
customer = _get_device_customerid()
|
customer = _get_device_customerid()
|
||||||
name = _get_device_name()
|
_, name = _get_deviceuser_name()
|
||||||
device = gapi.call(ci.devices(), 'get', name=name, customer=customer)
|
device = gapi.call(ci.devices(), 'get', name=name, customer=customer)
|
||||||
device_users = gapi.get_all_pages(ci.devices().deviceUsers(), 'list',
|
device_users = gapi.get_all_pages(ci.devices().deviceUsers(), 'list',
|
||||||
'deviceUsers', parent=name, customer=customer)
|
'deviceUsers', parent=name, customer=customer)
|
||||||
|
|||||||
@@ -217,6 +217,7 @@ def print_():
|
|||||||
gapi_directory_customer.setTrueCustomerId()
|
gapi_directory_customer.setTrueCustomerId()
|
||||||
parent = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
|
parent = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
|
||||||
usemember = None
|
usemember = None
|
||||||
|
query = None
|
||||||
memberDelimiter = '\n'
|
memberDelimiter = '\n'
|
||||||
todrive = False
|
todrive = False
|
||||||
titles = []
|
titles = []
|
||||||
@@ -235,6 +236,9 @@ def print_():
|
|||||||
elif myarg == 'delimiter':
|
elif myarg == 'delimiter':
|
||||||
memberDelimiter = sys.argv[i + 1]
|
memberDelimiter = sys.argv[i + 1]
|
||||||
i += 2
|
i += 2
|
||||||
|
elif myarg == 'query':
|
||||||
|
query = sys.argv[i + 1]
|
||||||
|
i += 2
|
||||||
elif myarg == 'sortheaders':
|
elif myarg == 'sortheaders':
|
||||||
sortHeaders = True
|
sortHeaders = True
|
||||||
i += 1
|
i += 1
|
||||||
@@ -314,14 +318,20 @@ def print_():
|
|||||||
if entity['relationType'] == 'DIRECT':
|
if entity['relationType'] == 'DIRECT':
|
||||||
entityList.append(gapi.call(ci.groups(), 'get', name=entity['group']))
|
entityList.append(gapi.call(ci.groups(), 'get', name=entity['group']))
|
||||||
else:
|
else:
|
||||||
|
if query:
|
||||||
|
method = 'search'
|
||||||
|
kwargs = {'query': query}
|
||||||
|
else:
|
||||||
|
method = 'list'
|
||||||
|
kwargs = {'parent': parent}
|
||||||
entityList = gapi.get_all_pages(ci.groups(),
|
entityList = gapi.get_all_pages(ci.groups(),
|
||||||
'list',
|
method,
|
||||||
'groups',
|
'groups',
|
||||||
page_message=page_message,
|
page_message=page_message,
|
||||||
message_attribute=['groupKey', 'id'],
|
message_attribute=['groupKey', 'id'],
|
||||||
parent=parent,
|
|
||||||
view='FULL',
|
view='FULL',
|
||||||
pageSize=500)
|
pageSize=500,
|
||||||
|
**kwargs)
|
||||||
i = 0
|
i = 0
|
||||||
count = len(entityList)
|
count = len(entityList)
|
||||||
for groupEntity in entityList:
|
for groupEntity in entityList:
|
||||||
@@ -935,6 +945,12 @@ def group_email_to_id(ci, group, i=0, count=0):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def group_id_to_email(ci, group_id):
|
||||||
|
return gapi.call(ci.groups(),
|
||||||
|
'get',
|
||||||
|
fields='groupKey/id',
|
||||||
|
name=group_id).get('groupKey', {}).get('id')
|
||||||
|
|
||||||
def membership_email_to_id(ci, parent, membership, i=0, count=0):
|
def membership_email_to_id(ci, parent, membership, i=0, count=0):
|
||||||
membership = gam.normalizeEmailAddressOrUID(membership)
|
membership = gam.normalizeEmailAddressOrUID(membership)
|
||||||
try:
|
try:
|
||||||
|
|||||||
538
src/gam/gapi/cloudidentity/inboundsso.py
Normal file
538
src/gam/gapi/cloudidentity/inboundsso.py
Normal file
@@ -0,0 +1,538 @@
|
|||||||
|
"""Methods related to Cloud Identity Inbound (Google as SP) SAML SSO"""
|
||||||
|
from datetime import datetime
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import dateutil.parser
|
||||||
|
import googleapiclient
|
||||||
|
|
||||||
|
import gam
|
||||||
|
from gam.var import GC_CUSTOMER_ID, GC_Values, MY_CUSTOMER
|
||||||
|
from gam import controlflow
|
||||||
|
from gam import display
|
||||||
|
from gam import fileutils
|
||||||
|
from gam import gapi
|
||||||
|
from gam import utils
|
||||||
|
from gam.gapi import errors as gapi_errors
|
||||||
|
from gam.gapi import cloudidentity as gapi_cloudidentity
|
||||||
|
from gam.gapi import directory as gapi_directory
|
||||||
|
from gam.gapi.cloudidentity import groups as gapi_cloudidentity_groups
|
||||||
|
from gam.gapi.directory import orgunits as gapi_directory_orgunits
|
||||||
|
|
||||||
|
'''returns customer in the format inboundsso requires'''
|
||||||
|
def get_sso_customer():
|
||||||
|
customer = GC_Values[GC_CUSTOMER_ID]
|
||||||
|
return f'customers/{customer}'
|
||||||
|
|
||||||
|
|
||||||
|
'''returns org unit in the format inboundsso requires'''
|
||||||
|
def get_orgunit_id(orgunit):
|
||||||
|
ou_id = gapi_directory_orgunits.getOrgUnitId(orgunit)[1]
|
||||||
|
if ou_id.startswith('id:'):
|
||||||
|
ou_id = ou_id[3:]
|
||||||
|
return f'orgUnits/{ou_id}'
|
||||||
|
|
||||||
|
|
||||||
|
'''build Cloud Identity API'''
|
||||||
|
def build():
|
||||||
|
return gapi_cloudidentity.build('cloudidentity_beta')
|
||||||
|
|
||||||
|
|
||||||
|
'''parse cmd for profile create/update'''
|
||||||
|
def parse_profile(body, i):
|
||||||
|
name_only = False
|
||||||
|
while i < len(sys.argv):
|
||||||
|
myarg = sys.argv[i].lower().replace('_', '')
|
||||||
|
if myarg == 'name':
|
||||||
|
body['displayName'] = sys.argv[i+1]
|
||||||
|
i += 2
|
||||||
|
elif myarg == 'entityid':
|
||||||
|
body.setdefault('idpConfig', {})['entityId'] = sys.argv[i+1]
|
||||||
|
i += 2
|
||||||
|
elif myarg == 'returnnameonly':
|
||||||
|
name_only = True
|
||||||
|
i += 1
|
||||||
|
elif myarg == 'loginurl':
|
||||||
|
body.setdefault('idpConfig', {})['singleSignOnServiceUri'] = sys.argv[i+1]
|
||||||
|
i += 2
|
||||||
|
elif myarg == 'logouturl':
|
||||||
|
body.setdefault('idpConfig', {})['logoutRedirectUri'] = sys.argv[i+1]
|
||||||
|
i += 2
|
||||||
|
elif myarg == 'changepasswordurl':
|
||||||
|
body.setdefault('idpConfig', {})['changePasswordUri'] = sys.argv[i+1]
|
||||||
|
i += 2
|
||||||
|
else:
|
||||||
|
controlflow.invalid_argument_exit(myarg, 'gam create/update inboundssoprofile')
|
||||||
|
return (name_only, body)
|
||||||
|
|
||||||
|
|
||||||
|
'''convert profile nice names to unique ID'''
|
||||||
|
def profile_displayname_to_name(displayName, ci=None):
|
||||||
|
if displayName.lower().startswith('id:') or displayName.lower().startswith('uid:'):
|
||||||
|
displayName = displayName.split(':', 1)[1]
|
||||||
|
if not displayName.startswith('inboundSamlSsoProfiles/'):
|
||||||
|
displayName = f'inboundSamlSsoProfiles/{displayName}'
|
||||||
|
return displayName
|
||||||
|
if not ci:
|
||||||
|
ci = build()
|
||||||
|
customer = get_sso_customer()
|
||||||
|
_filter = f'customer=="{customer}"'
|
||||||
|
profiles = gapi.get_all_pages(ci.inboundSamlSsoProfiles(),
|
||||||
|
'list',
|
||||||
|
'inboundSamlSsoProfiles',
|
||||||
|
filter=_filter)
|
||||||
|
matches = []
|
||||||
|
for profile in profiles:
|
||||||
|
if displayName.lower() == profile.get('displayName', '').lower():
|
||||||
|
matches.append(profile)
|
||||||
|
if len(matches) == 1:
|
||||||
|
return matches[0]['name']
|
||||||
|
if len(matches) == 0:
|
||||||
|
controlflow.system_error_exit(3, f'No Inbound SSO profile matches the name {displayName}')
|
||||||
|
else:
|
||||||
|
err_text = f'Multiple profiles match {displayName}:\n\n'
|
||||||
|
for m in matches:
|
||||||
|
err_text += f' {m["name"]} {m["displayName"]}\n'
|
||||||
|
controlflow.system_error_exit(3, err_text)
|
||||||
|
|
||||||
|
|
||||||
|
'''get an assignment based on target'''
|
||||||
|
def assignment_by_target(target, ci=None):
|
||||||
|
if not ci:
|
||||||
|
ci = build()
|
||||||
|
group_pattern = r'^groups/[^/]+$'
|
||||||
|
ou_pattern = r'^orgUnits/[^/]+$'
|
||||||
|
if re.match(group_pattern, target):
|
||||||
|
target_type = 'targetGroup'
|
||||||
|
elif re.match(ou_pattern, target):
|
||||||
|
target_type = 'targetOrgUnit'
|
||||||
|
elif target.lower().startswith('group:'):
|
||||||
|
target_type = 'targetGroup'
|
||||||
|
group_email = target[6:]
|
||||||
|
target = gapi_cloudidentity_groups.group_email_to_id(
|
||||||
|
ci,
|
||||||
|
group_email)
|
||||||
|
elif target.lower().startswith('orgunit:'):
|
||||||
|
target_type = 'targetOrgUnit'
|
||||||
|
ou_name = target[8:]
|
||||||
|
target = get_orgunit_id(ou_name)
|
||||||
|
else:
|
||||||
|
controlflow.system_error_exit(3, 'assignments should be prefixed with ' +
|
||||||
|
'group:, groups/, orgunit: or orgunits/')
|
||||||
|
customer = get_sso_customer()
|
||||||
|
_filter = f'customer=="{customer}"'
|
||||||
|
assignments = gapi.get_all_pages(ci.inboundSsoAssignments(),
|
||||||
|
'list',
|
||||||
|
'inboundSsoAssignments',
|
||||||
|
filter=_filter)
|
||||||
|
for assignment in assignments:
|
||||||
|
if target_type in assignment and assignment[target_type] == target:
|
||||||
|
return assignment
|
||||||
|
controlflow.system_error_exit(3, f'No SSO profile assigned to {target_type} {target}')
|
||||||
|
|
||||||
|
|
||||||
|
'''gam create inboundssoprofile'''
|
||||||
|
def create_profile():
|
||||||
|
ci = build()
|
||||||
|
body = {
|
||||||
|
'customer': get_sso_customer(),
|
||||||
|
'displayName': 'SSO Profile'
|
||||||
|
}
|
||||||
|
name_only, body = parse_profile(body, 3)
|
||||||
|
result = gapi.call(ci.inboundSamlSsoProfiles(),
|
||||||
|
'create',
|
||||||
|
body=body)
|
||||||
|
if result.get('done'):
|
||||||
|
if name_only:
|
||||||
|
print(result['response']['name'])
|
||||||
|
else:
|
||||||
|
print(f'Created profile {result["response"]["name"]}')
|
||||||
|
display.print_json(result['response'])
|
||||||
|
else:
|
||||||
|
controlflow.system_error_exit(3, 'Create did not finish {result}')
|
||||||
|
|
||||||
|
|
||||||
|
'''gam print inboundssoprofiles'''
|
||||||
|
def print_show_profiles(action='print'):
|
||||||
|
customer = get_sso_customer()
|
||||||
|
_filter = f'customer=="{customer}"'
|
||||||
|
ci = build()
|
||||||
|
todrive = False
|
||||||
|
i = 3
|
||||||
|
while i < len(sys.argv):
|
||||||
|
myarg = sys.argv[i].lower().replace('_', '')
|
||||||
|
if myarg == 'todrive':
|
||||||
|
todrive = True
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
controlflow.invalid_argument_exit(myarg, f'gam {action} inboundssoprofiles')
|
||||||
|
|
||||||
|
profiles = gapi.get_all_pages(ci.inboundSamlSsoProfiles(),
|
||||||
|
'list',
|
||||||
|
'inboundSamlSsoProfiles',
|
||||||
|
filter=_filter)
|
||||||
|
if action == 'show':
|
||||||
|
for profile in profiles:
|
||||||
|
display.print_json(profile)
|
||||||
|
print()
|
||||||
|
elif action == 'print':
|
||||||
|
csv_rows = []
|
||||||
|
titles = []
|
||||||
|
for profile in profiles:
|
||||||
|
row = utils.flatten_json(profile)
|
||||||
|
for item in row:
|
||||||
|
if item not in titles:
|
||||||
|
titles.append(item)
|
||||||
|
csv_rows.append(row)
|
||||||
|
display.write_csv_file(csv_rows,
|
||||||
|
titles,
|
||||||
|
'Inbound SSO Profiles',
|
||||||
|
todrive)
|
||||||
|
|
||||||
|
|
||||||
|
'''gam update inboundssoprofile'''
|
||||||
|
def update_profile():
|
||||||
|
ci = build()
|
||||||
|
name = profile_displayname_to_name(sys.argv[3], ci)
|
||||||
|
body = {}
|
||||||
|
name_only, body = parse_profile(body, 4)
|
||||||
|
updateMask = ','.join(body.keys())
|
||||||
|
result = gapi.call(ci.inboundSamlSsoProfiles(),
|
||||||
|
'patch',
|
||||||
|
name=name,
|
||||||
|
updateMask=updateMask,
|
||||||
|
body=body)
|
||||||
|
if name_only:
|
||||||
|
print(result['response']['name'])
|
||||||
|
else:
|
||||||
|
display.print_json(result)
|
||||||
|
|
||||||
|
|
||||||
|
'''gam info inboundssoprofile'''
|
||||||
|
def info_profile(return_only=False, displayName=None, ci=None):
|
||||||
|
if not ci:
|
||||||
|
ci = build()
|
||||||
|
if not displayName:
|
||||||
|
displayName = sys.argv[3]
|
||||||
|
name = profile_displayname_to_name(displayName, ci)
|
||||||
|
result = gapi.call(ci.inboundSamlSsoProfiles(),
|
||||||
|
'get',
|
||||||
|
name=name)
|
||||||
|
if return_only:
|
||||||
|
return result
|
||||||
|
display.print_json(result)
|
||||||
|
|
||||||
|
'''gam delete inboundssoprofile'''
|
||||||
|
def delete_profile():
|
||||||
|
ci = build()
|
||||||
|
name = profile_displayname_to_name(sys.argv[3], ci)
|
||||||
|
result = gapi.call(ci.inboundSamlSsoProfiles(),
|
||||||
|
'delete',
|
||||||
|
name=name)
|
||||||
|
if result.get('done'):
|
||||||
|
print(f'Deleted profile {name}.')
|
||||||
|
else:
|
||||||
|
controlflow.system_error_exit(3, 'Delete did not finish: {result}')
|
||||||
|
|
||||||
|
|
||||||
|
'''gam create inboundssocredentials'''
|
||||||
|
def create_credentials():
|
||||||
|
allowed_sizes = [1024, 2048, 4096]
|
||||||
|
ci = build()
|
||||||
|
parent = None
|
||||||
|
generate_key = False
|
||||||
|
key_size = 2048
|
||||||
|
pemData = None
|
||||||
|
replace_oldest = False
|
||||||
|
i = 3
|
||||||
|
while i < len(sys.argv):
|
||||||
|
myarg = sys.argv[i].lower().replace('_', '')
|
||||||
|
if myarg == 'profile':
|
||||||
|
parent = sys.argv[i+1]
|
||||||
|
parent = profile_displayname_to_name(parent, ci)
|
||||||
|
i += 2
|
||||||
|
elif myarg == 'pemfile':
|
||||||
|
pemfile = sys.argv[i+1]
|
||||||
|
pemData = fileutils.read_file(pemfile)
|
||||||
|
i += 2
|
||||||
|
elif myarg == 'generatekey':
|
||||||
|
generate_key = True
|
||||||
|
i += 1
|
||||||
|
elif myarg == 'replaceoldest':
|
||||||
|
replace_oldest = True
|
||||||
|
i += 1
|
||||||
|
elif myarg == 'keysize':
|
||||||
|
key_size = int(sys.argv[i+1])
|
||||||
|
if key_size not in allowed_sizes:
|
||||||
|
controlflow.expected_argument_exit('key_size',
|
||||||
|
allowed_sizes,
|
||||||
|
key_size)
|
||||||
|
i += 2
|
||||||
|
else:
|
||||||
|
controlflow.invalid_argument_exit(myarg, 'gam create inboundssocredential')
|
||||||
|
if not parent:
|
||||||
|
controlflow.missing_argument_exit('profile', 'gam create inboundssocredential')
|
||||||
|
if replace_oldest:
|
||||||
|
fields='nextPageToken,idpCredentials(name,updateTime)'
|
||||||
|
current_creds = gapi.get_all_pages(
|
||||||
|
ci.inboundSamlSsoProfiles().idpCredentials(),
|
||||||
|
'list',
|
||||||
|
'idpCredentials',
|
||||||
|
parent=parent,
|
||||||
|
fields=fields)
|
||||||
|
if len(current_creds) == 2:
|
||||||
|
oldest_key = min(current_creds,
|
||||||
|
key=lambda x:x['updateTime'])
|
||||||
|
print(' deleting older key...')
|
||||||
|
delete_credentials(ci=ci,
|
||||||
|
name=oldest_key['name'])
|
||||||
|
else:
|
||||||
|
print(' profile has {len(current_creds)} credentials. We only replace if there are 2.')
|
||||||
|
if generate_key:
|
||||||
|
privKey, pemData = gam._generatePrivateKeyAndPublicCert('GAM',
|
||||||
|
key_size,
|
||||||
|
b64enc_pub=False)
|
||||||
|
timestamp = datetime.now().strftime('%Y%m%d-%I%M%S')
|
||||||
|
priv_file = f'privatekey-{timestamp}.pem'
|
||||||
|
pub_file = f'publiccert-{timestamp}.pem'
|
||||||
|
fileutils.write_file(priv_file, privKey)
|
||||||
|
print(f' Wrote private key data to {priv_file}')
|
||||||
|
fileutils.write_file(pub_file, pemData)
|
||||||
|
print(f' Wrote public certificate to {pub_file}')
|
||||||
|
if not pemData:
|
||||||
|
controlflow.system_error_exit(3, 'You must either specify "pemfile <filename>" or "generate_key"')
|
||||||
|
body = {
|
||||||
|
'pemData': pemData,
|
||||||
|
}
|
||||||
|
result = gapi.call(ci.inboundSamlSsoProfiles().idpCredentials(),
|
||||||
|
'add',
|
||||||
|
parent=parent,
|
||||||
|
body=body)
|
||||||
|
if result.get('done'):
|
||||||
|
print(f'Created credential {result["response"]["name"]}')
|
||||||
|
display.print_json(result['response'])
|
||||||
|
else:
|
||||||
|
controlflow.system_error_exit(3, 'Create did not finish {result}')
|
||||||
|
|
||||||
|
|
||||||
|
'''gam delete inboundssocredential'''
|
||||||
|
def delete_credentials(ci=None, name=None):
|
||||||
|
if not ci:
|
||||||
|
ci = build()
|
||||||
|
if not name:
|
||||||
|
name = sys.argv[3]
|
||||||
|
result = gapi.call(ci.inboundSamlSsoProfiles().idpCredentials(),
|
||||||
|
'delete',
|
||||||
|
name=name)
|
||||||
|
if result.get('done'):
|
||||||
|
print(f'Deleted credential {name}')
|
||||||
|
else:
|
||||||
|
controlflow.system_error_exit(3, 'Delete did not finish {result}')
|
||||||
|
|
||||||
|
|
||||||
|
'''gam print inboundssocredentials'''
|
||||||
|
def print_show_credentials(action='print'):
|
||||||
|
ci = build()
|
||||||
|
todrive = False
|
||||||
|
i = 3
|
||||||
|
profiles = []
|
||||||
|
while i < len(sys.argv):
|
||||||
|
myarg = sys.argv[i].lower().replace('_', '')
|
||||||
|
if myarg in ['profile', 'profiles']:
|
||||||
|
profiles = [profile_displayname_to_name(profile, ci)
|
||||||
|
for profile in sys.argv[i+1].split(',')]
|
||||||
|
i += 2
|
||||||
|
elif myarg == 'todrive':
|
||||||
|
todrive = True
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
controlflow.invalid_argument_exit(myarg, f'gam {action} inboundssocredentials')
|
||||||
|
if not profiles:
|
||||||
|
customer = get_sso_customer()
|
||||||
|
_filter = f'customer=="{customer}"'
|
||||||
|
profiles = gapi.get_all_pages(ci.inboundSamlSsoProfiles(),
|
||||||
|
'list',
|
||||||
|
'inboundSamlSsoProfiles',
|
||||||
|
fields='inboundSamlSsoProfiles/name',
|
||||||
|
filter=_filter)
|
||||||
|
profiles = [p['name'] for p in profiles]
|
||||||
|
if action == 'print':
|
||||||
|
titles = []
|
||||||
|
csv_rows = []
|
||||||
|
credentials = []
|
||||||
|
for profile in profiles:
|
||||||
|
results = gapi.get_all_pages(ci.inboundSamlSsoProfiles().idpCredentials(),
|
||||||
|
'list',
|
||||||
|
'idpCredentials',
|
||||||
|
parent=profile)
|
||||||
|
credentials.extend(results)
|
||||||
|
if action == 'show':
|
||||||
|
for c in credentials:
|
||||||
|
display.print_json(c)
|
||||||
|
print()
|
||||||
|
elif action == 'print':
|
||||||
|
for c in credentials:
|
||||||
|
csv_row = utils.flatten_json(c)
|
||||||
|
for item in csv_row:
|
||||||
|
if item not in titles:
|
||||||
|
titles.append(item)
|
||||||
|
csv_rows.append(csv_row)
|
||||||
|
display.write_csv_file(csv_rows,
|
||||||
|
titles,
|
||||||
|
'Inbound SSO Credentials',
|
||||||
|
todrive)
|
||||||
|
|
||||||
|
'''parse command for create/update inboundssoassignment'''
|
||||||
|
def parse_assignment(body, i, ci):
|
||||||
|
while i < len(sys.argv):
|
||||||
|
myarg = sys.argv[i].lower().replace('_', '')
|
||||||
|
if myarg == 'rank':
|
||||||
|
body['rank'] = int(sys.argv[i+1])
|
||||||
|
i += 2
|
||||||
|
elif myarg == 'mode':
|
||||||
|
mode_choices = \
|
||||||
|
gapi.get_enum_values_minus_unspecified(
|
||||||
|
ci._rootDesc['schemas']['InboundSsoAssignment']['properties']['ssoMode']['enum'])
|
||||||
|
body['ssoMode'] = sys.argv[i+1].upper()
|
||||||
|
if body['ssoMode'] not in mode_choices:
|
||||||
|
controlflow.expected_argument_exit('mode',
|
||||||
|
', '.join(mode_choices),
|
||||||
|
sys.argv[i+1])
|
||||||
|
i += 2
|
||||||
|
elif myarg == 'profile':
|
||||||
|
profile_name = profile_displayname_to_name(
|
||||||
|
sys.argv[i+1],
|
||||||
|
ci)
|
||||||
|
body['samlSsoInfo'] = {
|
||||||
|
'inboundSamlSsoProfile': profile_name
|
||||||
|
}
|
||||||
|
i += 2
|
||||||
|
elif myarg == 'neverredirect':
|
||||||
|
body['signInBehavior'] = {
|
||||||
|
'redirectCondition': 'NEVER'
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
elif myarg == 'group':
|
||||||
|
group = sys.argv[i+1]
|
||||||
|
body['targetGroup'] = gapi_cloudidentity_groups.group_email_to_id(
|
||||||
|
ci,
|
||||||
|
group)
|
||||||
|
i += 2
|
||||||
|
elif myarg in ['ou', 'org', 'orgunit']:
|
||||||
|
body['targetOrgUnit'] = get_orgunit_id(sys.argv[i+1])
|
||||||
|
i += 2
|
||||||
|
else:
|
||||||
|
controlflow.invalid_argument_exit(myarg, 'gam create/update inboundssoassignment')
|
||||||
|
return body
|
||||||
|
|
||||||
|
|
||||||
|
def update_assignment_target_names(assignment, ci, cd):
|
||||||
|
if 'targetGroup' in assignment:
|
||||||
|
assignment['targetGroupEmail'] = \
|
||||||
|
gapi_cloudidentity_groups.group_id_to_email(ci,
|
||||||
|
assignment['targetGroup'])
|
||||||
|
elif 'targetOrgUnit' in assignment:
|
||||||
|
ou_id = assignment['targetOrgUnit'].split('/')[1]
|
||||||
|
assignment['targetOrgUnitPath'] = \
|
||||||
|
gapi_directory_orgunits.orgunit_from_orgunitid(f'id:{ou_id}', cd)
|
||||||
|
|
||||||
|
|
||||||
|
'''gam create inboundssoassignment'''
|
||||||
|
def create_assignment():
|
||||||
|
ci = build()
|
||||||
|
cd = gapi_directory.build()
|
||||||
|
body = {
|
||||||
|
'customer': get_sso_customer(),
|
||||||
|
}
|
||||||
|
body = parse_assignment(body, 3, ci)
|
||||||
|
result = gapi.call(ci.inboundSsoAssignments(),
|
||||||
|
'create',
|
||||||
|
body=body)
|
||||||
|
if result.get('done'):
|
||||||
|
print(f'Created assignment {result["response"]["name"]}')
|
||||||
|
update_assignment_target_names(result['response'], ci, cd)
|
||||||
|
display.print_json(result['response'])
|
||||||
|
else:
|
||||||
|
controlflow.system_error_exit(3, 'Create did not finish {result}')
|
||||||
|
|
||||||
|
|
||||||
|
def get_assignment_name(name):
|
||||||
|
if name.startswith('id:') or name.startswith('uid:'):
|
||||||
|
name = name.split(':', 1)[1]
|
||||||
|
if not name.startswith('inboundSsoAssignments/'):
|
||||||
|
name = f'inboundSsoAssignments/{name}'
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
'''gam update inboundssoassignment'''
|
||||||
|
def update_assignment():
|
||||||
|
ci = build()
|
||||||
|
cd = gapi_directory.build()
|
||||||
|
name = get_assignment_name(sys.argv[3])
|
||||||
|
body = parse_assignment({}, 4, ci)
|
||||||
|
updateMask = ','.join(list(body.keys()))
|
||||||
|
result = gapi.call(ci.inboundSsoAssignments(),
|
||||||
|
'patch',
|
||||||
|
name=name,
|
||||||
|
updateMask=updateMask,
|
||||||
|
body=body)
|
||||||
|
if result.get('done'):
|
||||||
|
print(f'Updated assignment {result["response"]["name"]}')
|
||||||
|
update_assignment_target_names(result['response'], ci, cd)
|
||||||
|
display.print_json(result['response'])
|
||||||
|
else:
|
||||||
|
controlflow.system_error_exit(3, 'Update did not finish {result}')
|
||||||
|
|
||||||
|
|
||||||
|
'''gam info inboundssoassignment'''
|
||||||
|
def info_assignment():
|
||||||
|
ci = build()
|
||||||
|
cd = gapi_directory.build()
|
||||||
|
assignment = assignment_by_target(sys.argv[3], ci)
|
||||||
|
update_assignment_target_names(assignment, ci, cd)
|
||||||
|
profile = assignment.get('samlSsoInfo', {}).get('inboundSamlSsoProfile')
|
||||||
|
if profile:
|
||||||
|
assignment['samlSsoInfo']['inboundSamlSsoProfile'] = \
|
||||||
|
info_profile(return_only=True, displayName=f'id:{profile}', ci=ci)
|
||||||
|
display.print_json(assignment)
|
||||||
|
|
||||||
|
|
||||||
|
'''gam print inboundssoassignments'''
|
||||||
|
def print_show_assignments(action='print'):
|
||||||
|
ci = build()
|
||||||
|
cd = gapi_directory.build()
|
||||||
|
customer = get_sso_customer()
|
||||||
|
_filter = f'customer=="{customer}"'
|
||||||
|
todrive = False
|
||||||
|
i = 3
|
||||||
|
while i < len(sys.argv):
|
||||||
|
myarg = sys.argv[i].lower().replace('_', '')
|
||||||
|
if myarg == 'todrive':
|
||||||
|
todrive = True
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
controlflow.invalid_argument_exit(myarg,
|
||||||
|
f'gam {action} inboundssoassignments')
|
||||||
|
assignments = gapi.get_all_pages(ci.inboundSsoAssignments(),
|
||||||
|
'list',
|
||||||
|
'inboundSsoAssignments',
|
||||||
|
filter=_filter)
|
||||||
|
if action == 'show':
|
||||||
|
for assignment in assignments:
|
||||||
|
update_assignment_target_names(assignment, ci, cd)
|
||||||
|
display.print_json(assignment)
|
||||||
|
print()
|
||||||
|
elif action == 'print':
|
||||||
|
titles = []
|
||||||
|
csv_rows = []
|
||||||
|
for assignment in assignments:
|
||||||
|
update_assignment_target_names(assignment, ci, cd)
|
||||||
|
csv_row = utils.flatten_json(assignment)
|
||||||
|
for item in csv_row:
|
||||||
|
if item not in titles:
|
||||||
|
titles.append(item)
|
||||||
|
csv_rows.append(csv_row)
|
||||||
|
display.write_csv_file(csv_rows,
|
||||||
|
titles,
|
||||||
|
'Inbound SSO Assignments',
|
||||||
|
todrive)
|
||||||
@@ -25,7 +25,7 @@ def _reduce_name(name):
|
|||||||
|
|
||||||
def is_invitable_user(email):
|
def is_invitable_user(email):
|
||||||
'''return email isInvitableUser'''
|
'''return email isInvitableUser'''
|
||||||
svc = gapi_cloudidentity.build_dwd('cloudidentity')
|
svc = gapi_cloudidentity.build('cloudidentity')
|
||||||
customer = _get_customerid()
|
customer = _get_customerid()
|
||||||
encoded_email = quote_plus(email)
|
encoded_email = quote_plus(email)
|
||||||
name = f'{customer}/userinvitations/{encoded_email}'
|
name = f'{customer}/userinvitations/{encoded_email}'
|
||||||
@@ -35,7 +35,7 @@ def is_invitable_user(email):
|
|||||||
|
|
||||||
def _generic_action(action):
|
def _generic_action(action):
|
||||||
'''generic function to call actionable APIs'''
|
'''generic function to call actionable APIs'''
|
||||||
svc = gapi_cloudidentity.build_dwd('cloudidentity')
|
svc = gapi_cloudidentity.build('cloudidentity')
|
||||||
customer = _get_customerid()
|
customer = _get_customerid()
|
||||||
email = sys.argv[3].lower()
|
email = sys.argv[3].lower()
|
||||||
encoded_email = quote_plus(email)
|
encoded_email = quote_plus(email)
|
||||||
@@ -55,7 +55,7 @@ def _generic_action(action):
|
|||||||
|
|
||||||
def _generic_get(get_type):
|
def _generic_get(get_type):
|
||||||
'''generic function to call read data APIs'''
|
'''generic function to call read data APIs'''
|
||||||
svc = gapi_cloudidentity.build_dwd('cloudidentity')
|
svc = gapi_cloudidentity.build('cloudidentity')
|
||||||
customer = _get_customerid()
|
customer = _get_customerid()
|
||||||
email = sys.argv[3].lower()
|
email = sys.argv[3].lower()
|
||||||
encoded_email = quote_plus(email)
|
encoded_email = quote_plus(email)
|
||||||
@@ -75,7 +75,7 @@ def bulk_is_invitable(emails):
|
|||||||
if response.get('isInvitableUser'):
|
if response.get('isInvitableUser'):
|
||||||
rows.append({'invitableUsers': request_id})
|
rows.append({'invitableUsers': request_id})
|
||||||
|
|
||||||
svc = gapi_cloudidentity.build_dwd('cloudidentity')
|
svc = gapi_cloudidentity.build('cloudidentity')
|
||||||
customer = _get_customerid()
|
customer = _get_customerid()
|
||||||
todrive = False
|
todrive = False
|
||||||
#batch_size = 1000
|
#batch_size = 1000
|
||||||
@@ -139,7 +139,7 @@ USERINVITATION_STATE_CHOICES_MAP = {
|
|||||||
|
|
||||||
def print_():
|
def print_():
|
||||||
'''gam print userinvitations'''
|
'''gam print userinvitations'''
|
||||||
svc = gapi_cloudidentity.build_dwd('cloudidentity')
|
svc = gapi_cloudidentity.build('cloudidentity')
|
||||||
customer = _get_customerid()
|
customer = _get_customerid()
|
||||||
todrive = False
|
todrive = False
|
||||||
titles = ['name', 'state', 'updateTime']
|
titles = ['name', 'state', 'updateTime']
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ def get_org_id():
|
|||||||
gapi_directory_customer.setTrueCustomerId()
|
gapi_directory_customer.setTrueCustomerId()
|
||||||
crm = build()
|
crm = build()
|
||||||
query = f'directorycustomerid:{GC_Values[GC_CUSTOMER_ID]}'
|
query = f'directorycustomerid:{GC_Values[GC_CUSTOMER_ID]}'
|
||||||
orgs = gapi.get_all_pages(crm.organizations(),
|
results = gapi.call(crm.organizations(),
|
||||||
'search',
|
'search',
|
||||||
'organizations',
|
pageSize=1,
|
||||||
|
fields='organizations/name',
|
||||||
query=query)
|
query=query)
|
||||||
if len(orgs) < 1:
|
orgs = results.get('organizations')
|
||||||
|
if not orgs:
|
||||||
# return nothing and let calling API deal with it
|
# return nothing and let calling API deal with it
|
||||||
# since caller knows what GCP role would serve best
|
# since caller knows what GCP role would serve best
|
||||||
return
|
return
|
||||||
return orgs[0]['name']
|
return orgs[0].get('name')
|
||||||
|
|||||||
@@ -21,36 +21,36 @@ def doGetCustomerInfo():
|
|||||||
'get',
|
'get',
|
||||||
customerKey=customer_id)
|
customerKey=customer_id)
|
||||||
print(f'Customer ID: {customer_info["id"]}')
|
print(f'Customer ID: {customer_info["id"]}')
|
||||||
print(f'Primary Domain: {customer_info["customerDomain"]}')
|
fields = 'domains(creationTime,domainName,isPrimary,verified)'
|
||||||
try:
|
try:
|
||||||
result = gapi.call(
|
domains = gapi.call(
|
||||||
cd.domains(),
|
cd.domains(),
|
||||||
'get',
|
|
||||||
customer=customer_id,
|
|
||||||
domainName=customer_info['customerDomain'],
|
|
||||||
fields='verified',
|
|
||||||
throw_reasons=[gapi.errors.ErrorReason.DOMAIN_NOT_FOUND])
|
|
||||||
except gapi.errors.GapiDomainNotFoundError:
|
|
||||||
result = {'verified': False}
|
|
||||||
print(f'Primary Domain Verified: {result["verified"]}')
|
|
||||||
# If customer has changed primary domain customerCreationTime is date
|
|
||||||
# of current primary being added, not customer create date.
|
|
||||||
# We should also get all domains and use oldest date
|
|
||||||
customer_creation = customer_info['customerCreationTime']
|
|
||||||
date_format = '%Y-%m-%dT%H:%M:%S.%fZ'
|
|
||||||
oldest = datetime.datetime.strptime(customer_creation, date_format)
|
|
||||||
domains = gapi.get_items(cd.domains(),
|
|
||||||
'list',
|
'list',
|
||||||
'domains',
|
fields=fields,
|
||||||
customer=customer_id,
|
customer=customer_id,
|
||||||
fields='domains(creationTime)')
|
throw_reasons=[gapi.errors.ErrorReason.DOMAIN_NOT_FOUND]).get('domains', [])
|
||||||
|
for domain in domains:
|
||||||
|
if domain.get('isPrimary'):
|
||||||
|
primary_domain = domain
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
primary_domain = {}
|
||||||
|
except gapi.errors.GapiDomainNotFoundError:
|
||||||
|
primary_domain = {}
|
||||||
|
print(f'Primary Domain: {primary_domain.get("domainName", "Unknown")}')
|
||||||
|
print(f'Primary Domain Verified: {primary_domain.get("verified", "Unknown")}')
|
||||||
|
# we'll assume creation time is time of oldest domain customer has
|
||||||
|
oldest = 'Unknown'
|
||||||
for domain in domains:
|
for domain in domains:
|
||||||
creation_timestamp = int(domain['creationTime']) / 1000
|
creation_timestamp = int(domain['creationTime']) / 1000
|
||||||
domain_creation = datetime.datetime.fromtimestamp(creation_timestamp)
|
domain_creation = datetime.datetime.fromtimestamp(creation_timestamp)
|
||||||
if domain_creation < oldest:
|
if oldest == 'Unknown' or domain_creation < oldest:
|
||||||
oldest = domain_creation
|
oldest = domain_creation
|
||||||
print(f'Customer Creation Time: {oldest.strftime(date_format)}')
|
if oldest != 'Unknown':
|
||||||
customer_language = customer_info.get('language', 'Unset (defaults to en)')
|
date_format = '%Y-%m-%dT%H:%M:%S.%fZ'
|
||||||
|
oldest = oldest.strftime(date_format)
|
||||||
|
print(f'Customer Creation Time: {oldest}')
|
||||||
|
customer_language = customer_info.get('language', 'Unset or Unknown (defaults to en)')
|
||||||
print(f'Default Language: {customer_language}')
|
print(f'Default Language: {customer_language}')
|
||||||
if 'postalAddress' in customer_info:
|
if 'postalAddress' in customer_info:
|
||||||
print('Address:')
|
print('Address:')
|
||||||
@@ -59,7 +59,7 @@ def doGetCustomerInfo():
|
|||||||
print(f' {field}: {customer_info["postalAddress"][field]}')
|
print(f' {field}: {customer_info["postalAddress"][field]}')
|
||||||
if 'phoneNumber' in customer_info:
|
if 'phoneNumber' in customer_info:
|
||||||
print(f'Phone: {customer_info["phoneNumber"]}')
|
print(f'Phone: {customer_info["phoneNumber"]}')
|
||||||
print(f'Admin Secondary Email: {customer_info["alternateEmail"]}')
|
print(f'Admin Secondary Email: {customer_info.get("alternateEmail", "Unknown")}')
|
||||||
user_counts_map = {
|
user_counts_map = {
|
||||||
'accounts:num_users': 'Total Users',
|
'accounts:num_users': 'Total Users',
|
||||||
'accounts:gsuite_basic_total_licenses': 'G Suite Basic Licenses',
|
'accounts:gsuite_basic_total_licenses': 'G Suite Basic Licenses',
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ def info(group_name=None):
|
|||||||
pass
|
pass
|
||||||
print('')
|
print('')
|
||||||
print('Group Settings:')
|
print('Group Settings:')
|
||||||
for key, value in list(basic_info.items()):
|
for key, value in sorted(list(basic_info.items())):
|
||||||
if (key in ['kind', 'etag']) or ((key == 'aliases') and
|
if (key in ['kind', 'etag']) or ((key == 'aliases') and
|
||||||
(not getAliases)):
|
(not getAliases)):
|
||||||
continue
|
continue
|
||||||
@@ -199,7 +199,7 @@ def info(group_name=None):
|
|||||||
print(f' {val}')
|
print(f' {val}')
|
||||||
else:
|
else:
|
||||||
print(f' {key}: {value}')
|
print(f' {key}: {value}')
|
||||||
for key, value in list(settings.items()):
|
for key, value in sorted(list(settings.items())):
|
||||||
if key in ['kind', 'etag', 'description', 'email', 'name']:
|
if key in ['kind', 'etag', 'description', 'email', 'name']:
|
||||||
continue
|
continue
|
||||||
print(f' {key}: {value}')
|
print(f' {key}: {value}')
|
||||||
@@ -1217,6 +1217,8 @@ GROUP_SETTINGS_LIST_PATTERN = re.compile(r'([A-Z][A-Z_]+[A-Z]?)')
|
|||||||
def getGroupAttrValue(myarg, value, gs_object, gs_body, function):
|
def getGroupAttrValue(myarg, value, gs_object, gs_body, function):
|
||||||
if myarg == 'collaborative':
|
if myarg == 'collaborative':
|
||||||
myarg = 'enablecollaborativeinbox'
|
myarg = 'enablecollaborativeinbox'
|
||||||
|
elif myarg == 'gal':
|
||||||
|
myarg = 'includeinglobaladdresslist'
|
||||||
for (attrib,
|
for (attrib,
|
||||||
params) in list(gs_object['schemas']['Groups']['properties'].items()):
|
params) in list(gs_object['schemas']['Groups']['properties'].items()):
|
||||||
if attrib in ['kind', 'etag', 'email']:
|
if attrib in ['kind', 'etag', 'email']:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import sys
|
|||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
import gam
|
import gam
|
||||||
|
from gam import controlflow
|
||||||
from gam import display
|
from gam import display
|
||||||
from gam import gapi
|
from gam import gapi
|
||||||
from gam.gapi import directory as gapi_directory
|
from gam.gapi import directory as gapi_directory
|
||||||
@@ -18,7 +19,7 @@ def delete():
|
|||||||
userKey=user_email,
|
userKey=user_email,
|
||||||
throw_reasons=[gapi_errors.ErrorReason.CONDITION_NOT_MET])
|
throw_reasons=[gapi_errors.ErrorReason.CONDITION_NOT_MET])
|
||||||
except gam.gapi.errors.GapiConditionNotMetError as err:
|
except gam.gapi.errors.GapiConditionNotMetError as err:
|
||||||
display.print_error(
|
controlflow.system_error_exit(3,
|
||||||
f'{err} The user {user_email} may be (or have recently been) on Google Vault Hold and thus not eligible for deletion. You can check holds with "gam user <email> show vaultholds".'
|
f'{err} The user {user_email} may be (or have recently been) on Google Vault Hold and thus not eligible for deletion. You can check holds with "gam user <email> show vaultholds".'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -426,6 +426,7 @@ def showReport():
|
|||||||
titles = ['name', 'value', 'client_id']
|
titles = ['name', 'value', 'client_id']
|
||||||
csvRows = []
|
csvRows = []
|
||||||
auth_apps = list()
|
auth_apps = list()
|
||||||
|
usage = list(usage)
|
||||||
for item in usage[0]['parameters']:
|
for item in usage[0]['parameters']:
|
||||||
if 'name' not in item:
|
if 'name' not in item:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -2,20 +2,150 @@ import base64
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
import googleapiclient
|
import googleapiclient
|
||||||
|
from pathvalidate import sanitize_filepath
|
||||||
|
|
||||||
import gam
|
import gam
|
||||||
|
from gam.gapi import errors as gapi_errors
|
||||||
from gam.var import *
|
from gam.var import *
|
||||||
|
from gam import controlflow
|
||||||
from gam import fileutils
|
from gam import fileutils
|
||||||
from gam import gapi
|
from gam import gapi
|
||||||
from gam import utils
|
from gam import utils
|
||||||
|
|
||||||
|
|
||||||
def build_gapi():
|
def build():
|
||||||
return gam.buildGAPIObject('storage')
|
return gam.buildGAPIObject('storage')
|
||||||
|
|
||||||
|
|
||||||
|
def copy_bucket():
|
||||||
|
s = build()
|
||||||
|
source_bucket = None
|
||||||
|
target_bucket = None
|
||||||
|
prefix = None
|
||||||
|
target_prefix = ''
|
||||||
|
i = 3
|
||||||
|
while i < len(sys.argv):
|
||||||
|
myarg = sys.argv[i].lower().replace('_', '')
|
||||||
|
if myarg == 'sourcebucket':
|
||||||
|
source_bucket = sys.argv[i+1]
|
||||||
|
i += 2
|
||||||
|
elif myarg == 'targetbucket':
|
||||||
|
target_bucket = sys.argv[i+1]
|
||||||
|
i += 2
|
||||||
|
elif myarg == 'sourceprefix':
|
||||||
|
prefix = sys.argv[i+1]
|
||||||
|
i += 2
|
||||||
|
elif myarg == 'targetprefix':
|
||||||
|
target_prefix = sys.argv[i+1]
|
||||||
|
i += 2
|
||||||
|
else:
|
||||||
|
controlflow.invalid_argument_exit(sys.argv[i],
|
||||||
|
'gam copy storagebucket')
|
||||||
|
if not target_bucket:
|
||||||
|
controlflow.missing_argument_exit('target_bucket', 'gam copy storagebucket')
|
||||||
|
if not source_bucket:
|
||||||
|
controlflow.missing_argument_exit('source_bucket', 'gam copy storagebucket')
|
||||||
|
page_message = gapi.got_total_items_msg('Storage Objects', '...\n')
|
||||||
|
objects = gapi.get_all_pages(s.objects(),
|
||||||
|
'list',
|
||||||
|
items='items',
|
||||||
|
page_message=page_message,
|
||||||
|
prefix=prefix,
|
||||||
|
bucket=source_bucket,
|
||||||
|
fields='items(name,bucket,md5Hash),nextPageToken')
|
||||||
|
copy_objects(objects,
|
||||||
|
target_bucket,
|
||||||
|
target_prefix)
|
||||||
|
|
||||||
|
|
||||||
|
def copy_objects(objects,
|
||||||
|
target_bucket,
|
||||||
|
target_prefix):
|
||||||
|
"""Copies objects to target_bucket.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
objects: list of object dicts
|
||||||
|
[
|
||||||
|
{
|
||||||
|
bucket: source bucket,
|
||||||
|
name: source object name,
|
||||||
|
(optional) md5Hash: source file hash value
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
target_bucket: target bucket id
|
||||||
|
target_prefix: prefix name to prepend to target object
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def process_rewrite(request_id, response, exception):
|
||||||
|
file_ptr = int(request_id)
|
||||||
|
if exception:
|
||||||
|
# Poor man's backoff/retry
|
||||||
|
if exception.status_code == 429 or exception.status_code > 499:
|
||||||
|
print(f'Temporary error {exception.status_code}. Sleeping 10 seconds...')
|
||||||
|
time.sleep(10)
|
||||||
|
next_batch.add(s.objects().rewrite(**files_to_copy[file_ptr]['method']),
|
||||||
|
request_id=request_id)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
raise exception
|
||||||
|
file_count = file_ptr + 1
|
||||||
|
source_displayname = files_to_copy[file_ptr]['source_displayname']
|
||||||
|
target_displayname = files_to_copy[file_ptr]['target_displayname']
|
||||||
|
if response.get('done'):
|
||||||
|
source_md5 = files_to_copy[file_ptr]['md5Hash']
|
||||||
|
target_md5 = response['resource']['md5Hash']
|
||||||
|
if source_md5 != target_md5:
|
||||||
|
controlflow.system_error_exit(99, f'Target file {target_displayname} checksum {target_md5} does not match source {source_md5}. This should not happen')
|
||||||
|
else:
|
||||||
|
print(f'[ {file_count} / {total_files} ] 100% VERIFIED - finished copying:\n source: {source_displayname}\n dest: {target_displayname}')
|
||||||
|
else:
|
||||||
|
total_bytes = float(response.get('objectSize'))
|
||||||
|
done_bytes = float(response.get('totalBytesRewritten'))
|
||||||
|
pct = (done_bytes / total_bytes) * 100
|
||||||
|
print(f'[ {file_count} / {total_files} ] {pct:.2f}%\n source: {source_displayname}\n dest:{target_displayname}')
|
||||||
|
files_to_copy[file_ptr]['method']['rewriteToken'] = response.get('rewriteToken')
|
||||||
|
next_batch.add(s.objects().rewrite(**files_to_copy[file_ptr]['method']),
|
||||||
|
request_id=request_id)
|
||||||
|
|
||||||
|
s = build()
|
||||||
|
sbatch = s.new_batch_http_request(callback=process_rewrite)
|
||||||
|
files_to_copy = []
|
||||||
|
for object_ in objects:
|
||||||
|
files_to_copy.append(
|
||||||
|
{
|
||||||
|
'md5Hash': object_['md5Hash'],
|
||||||
|
'source_displayname': f'{object_["bucket"]}:{object_["name"]}',
|
||||||
|
'target_displayname': f'{target_bucket}:{target_prefix}{object_["name"]}',
|
||||||
|
'method': {
|
||||||
|
'destinationBucket': target_bucket,
|
||||||
|
'destinationObject': f'{target_prefix}{object_["name"]}',
|
||||||
|
'sourceBucket': object_['bucket'],
|
||||||
|
'sourceObject': object_['name'],
|
||||||
|
# 'maxBytesRewrittenPerCall': 1048576, # uncomment to easily test multiple rewrite API calls per object
|
||||||
|
},
|
||||||
|
})
|
||||||
|
i = 0
|
||||||
|
total_files = len(files_to_copy)
|
||||||
|
for file in files_to_copy:
|
||||||
|
while len(sbatch._order) == 100:
|
||||||
|
next_batch = s.new_batch_http_request(callback=process_rewrite)
|
||||||
|
sbatch.execute()
|
||||||
|
sbatch = next_batch
|
||||||
|
sbatch.add(s.objects().rewrite(**file['method']),
|
||||||
|
request_id=str(i))
|
||||||
|
i += 1
|
||||||
|
while len(sbatch._order) > 0:
|
||||||
|
next_batch = s.new_batch_http_request(callback=process_rewrite)
|
||||||
|
sbatch.execute()
|
||||||
|
sbatch = next_batch
|
||||||
|
print('All done!')
|
||||||
|
|
||||||
|
|
||||||
def get_cloud_storage_object(s,
|
def get_cloud_storage_object(s,
|
||||||
bucket,
|
bucket,
|
||||||
object_,
|
object_,
|
||||||
@@ -23,11 +153,12 @@ def get_cloud_storage_object(s,
|
|||||||
expectedMd5=None):
|
expectedMd5=None):
|
||||||
if not local_file:
|
if not local_file:
|
||||||
local_file = object_
|
local_file = object_
|
||||||
|
local_file = sanitize_filepath(local_file, platform='auto')
|
||||||
if os.path.exists(local_file):
|
if os.path.exists(local_file):
|
||||||
sys.stdout.write(' File already exists. ')
|
sys.stdout.write(f'File {local_file} already exists.')
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
if expectedMd5:
|
if expectedMd5:
|
||||||
sys.stdout.write(f'Verifying {expectedMd5} hash...')
|
sys.stdout.write(f' verifying {expectedMd5} hash...')
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
if utils.md5_matches_file(local_file, expectedMd5, False):
|
if utils.md5_matches_file(local_file, expectedMd5, False):
|
||||||
print('VERIFIED')
|
print('VERIFIED')
|
||||||
@@ -35,7 +166,7 @@ def get_cloud_storage_object(s,
|
|||||||
print('not verified. Downloading again and over-writing...')
|
print('not verified. Downloading again and over-writing...')
|
||||||
else:
|
else:
|
||||||
return # nothing to verify, just assume we're good.
|
return # nothing to verify, just assume we're good.
|
||||||
print(f'saving to {local_file}')
|
print(f'Saving to {local_file}')
|
||||||
request = s.objects().get_media(bucket=bucket, object=object_)
|
request = s.objects().get_media(bucket=bucket, object=object_)
|
||||||
file_path = os.path.dirname(local_file)
|
file_path = os.path.dirname(local_file)
|
||||||
if not os.path.exists(file_path):
|
if not os.path.exists(file_path):
|
||||||
@@ -60,7 +191,7 @@ def get_cloud_storage_object(s,
|
|||||||
|
|
||||||
def download_bucket():
|
def download_bucket():
|
||||||
bucket = sys.argv[3]
|
bucket = sys.argv[3]
|
||||||
s = build_gapi()
|
s = build()
|
||||||
page_message = gapi.got_total_items_msg('Files', '...')
|
page_message = gapi.got_total_items_msg('Files', '...')
|
||||||
fields = 'nextPageToken,items(name,id,md5Hash)'
|
fields = 'nextPageToken,items(name,id,md5Hash)'
|
||||||
objects = gapi.get_all_pages(s.objects(),
|
objects = gapi.get_all_pages(s.objects(),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from base64 import b64encode
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
@@ -11,6 +12,7 @@ from gam import controlflow
|
|||||||
from gam import display
|
from gam import display
|
||||||
from gam import fileutils
|
from gam import fileutils
|
||||||
from gam import gapi
|
from gam import gapi
|
||||||
|
from gam.gapi import errors as gapi_errors
|
||||||
from gam.gapi import storage as gapi_storage
|
from gam.gapi import storage as gapi_storage
|
||||||
from gam.gapi import directory as gapi_directory
|
from gam.gapi import directory as gapi_directory
|
||||||
from gam.gapi.directory import orgunits as gapi_directory_orgunits
|
from gam.gapi.directory import orgunits as gapi_directory_orgunits
|
||||||
@@ -336,9 +338,9 @@ def print_count():
|
|||||||
_validate_query(query, query_discovery)
|
_validate_query(query, query_discovery)
|
||||||
body['query'] = query
|
body['query'] = query
|
||||||
operation = gapi.call(v.matters(), 'count', matterId=matterId, body=body)
|
operation = gapi.call(v.matters(), 'count', matterId=matterId, body=body)
|
||||||
print(f'Watching operation {operation["name"]}...')
|
sys.stderr.write(f'Watching operation {operation["name"]}...\n')
|
||||||
while not operation.get('done'):
|
while not operation.get('done'):
|
||||||
print(f' operation {operation["name"]} is not done yet. Checking again in {operation_wait} seconds')
|
sys.stderr.write(f' operation {operation["name"]} is not done yet. Checking again in {operation_wait} seconds\n')
|
||||||
sleep(operation_wait)
|
sleep(operation_wait)
|
||||||
operation = gapi.call(v.operations(), 'get', name=operation['name'])
|
operation = gapi.call(v.operations(), 'get', name=operation['name'])
|
||||||
response = operation.get('response', {})
|
response = operation.get('response', {})
|
||||||
@@ -518,7 +520,6 @@ def getHoldInfo():
|
|||||||
|
|
||||||
|
|
||||||
def convertExportNameToID(v, nameOrID, matterId):
|
def convertExportNameToID(v, nameOrID, matterId):
|
||||||
nameOrID = nameOrID.lower()
|
|
||||||
cg = UID_PATTERN.match(nameOrID)
|
cg = UID_PATTERN.match(nameOrID)
|
||||||
if cg:
|
if cg:
|
||||||
return cg.group(1)
|
return cg.group(1)
|
||||||
@@ -529,7 +530,7 @@ def convertExportNameToID(v, nameOrID, matterId):
|
|||||||
matterId=matterId,
|
matterId=matterId,
|
||||||
fields=fields)
|
fields=fields)
|
||||||
for export in exports:
|
for export in exports:
|
||||||
if export['name'].lower() == nameOrID:
|
if export['name'].lower() == nameOrID.lower():
|
||||||
return export['id']
|
return export['id']
|
||||||
controlflow.system_error_exit(
|
controlflow.system_error_exit(
|
||||||
4, f'could not find export name {nameOrID} '
|
4, f'could not find export name {nameOrID} '
|
||||||
@@ -682,18 +683,23 @@ def showHoldsForUsers(users):
|
|||||||
v = buildGAPIObject()
|
v = buildGAPIObject()
|
||||||
matterIds = _getAllMatterIds(v)
|
matterIds = _getAllMatterIds(v)
|
||||||
matterHolds = {}
|
matterHolds = {}
|
||||||
|
fields = 'holds(holdId,name,accounts(accountId,email),orgUnit),nextPageToken'
|
||||||
for matterId in matterIds:
|
for matterId in matterIds:
|
||||||
|
try:
|
||||||
matterHolds[matterId] = gapi.get_all_pages(v.matters().holds(),
|
matterHolds[matterId] = gapi.get_all_pages(v.matters().holds(),
|
||||||
'list',
|
'list',
|
||||||
'holds',
|
'holds',
|
||||||
fields='holds(holdId,name,accounts(accountId,email),orgUnit),nextPageToken',
|
fields=fields,
|
||||||
|
throw_reasons=[gapi_errors.ErrorReason.FOUR_O_O],
|
||||||
matterId=matterId)
|
matterId=matterId)
|
||||||
|
except googleapiclient.errors.HttpError:
|
||||||
|
continue
|
||||||
totalHolds = 0
|
totalHolds = 0
|
||||||
for user in users:
|
for user in users:
|
||||||
user = user.lower()
|
user = user.lower()
|
||||||
orgUnits = gapi_directory_orgunits._getAllParentOrgUnitsForUser(user, cd)
|
orgUnits = gapi_directory_orgunits._getAllParentOrgUnitsForUser(user, cd)
|
||||||
for matterId in matterIds:
|
for matterId, holds in matterHolds.items():
|
||||||
for hold in matterHolds[matterId]:
|
for hold in holds:
|
||||||
if 'orgUnit' in hold:
|
if 'orgUnit' in hold:
|
||||||
orgUnitId = hold['orgUnit'].get('orgUnitId')
|
orgUnitId = hold['orgUnit'].get('orgUnitId')
|
||||||
if orgUnitId in orgUnits:
|
if orgUnitId in orgUnits:
|
||||||
@@ -791,11 +797,49 @@ def getMatterInfo():
|
|||||||
display.print_json(result)
|
display.print_json(result)
|
||||||
|
|
||||||
|
|
||||||
|
def copyExport():
|
||||||
|
v = buildGAPIObject()
|
||||||
|
s = gapi_storage.build()
|
||||||
|
matterId = getMatterItem(v, sys.argv[3])
|
||||||
|
exportId = convertExportNameToID(v, sys.argv[4], matterId)
|
||||||
|
target_bucket = None
|
||||||
|
target_prefix = ''
|
||||||
|
i = 5
|
||||||
|
while i < len(sys.argv):
|
||||||
|
myarg = sys.argv[i].lower().replace('_', '')
|
||||||
|
if myarg == 'targetbucket':
|
||||||
|
target_bucket = sys.argv[i+1]
|
||||||
|
i += 2
|
||||||
|
elif myarg == 'targetprefix':
|
||||||
|
target_prefix = sys.argv[i+1]
|
||||||
|
i += 2
|
||||||
|
else:
|
||||||
|
controlflow.invalid_argument_exit(sys.argv[i],
|
||||||
|
'gam copy export')
|
||||||
|
if not target_bucket:
|
||||||
|
controlflow.missing_argument_exit('target_bucket', 'gam copy export')
|
||||||
|
export = gapi.call(v.matters().exports(),
|
||||||
|
'get',
|
||||||
|
matterId=matterId,
|
||||||
|
exportId=exportId)
|
||||||
|
objects = []
|
||||||
|
for s_file in export['cloudStorageSink']['files']:
|
||||||
|
# Convert to md5Hash format Storage API uses
|
||||||
|
# because OF COURSE they differ
|
||||||
|
md5Hash = b64encode(bytes.fromhex(s_file['md5Hash'])).decode()
|
||||||
|
objects.append({'bucket': s_file['bucketName'],
|
||||||
|
'name': s_file['objectName'],
|
||||||
|
'md5Hash': md5Hash})
|
||||||
|
gapi_storage.copy_objects(objects,
|
||||||
|
target_bucket,
|
||||||
|
target_prefix)
|
||||||
|
|
||||||
|
|
||||||
def downloadExport():
|
def downloadExport():
|
||||||
verifyFiles = True
|
verifyFiles = True
|
||||||
extractFiles = True
|
extractFiles = True
|
||||||
v = buildGAPIObject()
|
v = buildGAPIObject()
|
||||||
s = gapi_storage.build_gapi()
|
s = gapi_storage.build()
|
||||||
matterId = getMatterItem(v, sys.argv[3])
|
matterId = getMatterItem(v, sys.argv[3])
|
||||||
exportId = convertExportNameToID(v, sys.argv[4], matterId)
|
exportId = convertExportNameToID(v, sys.argv[4], matterId)
|
||||||
targetFolder = GC_Values[GC_DRIVE_DIR]
|
targetFolder = GC_Values[GC_DRIVE_DIR]
|
||||||
@@ -823,24 +867,17 @@ def downloadExport():
|
|||||||
for s_file in export['cloudStorageSink']['files']:
|
for s_file in export['cloudStorageSink']['files']:
|
||||||
bucket = s_file['bucketName']
|
bucket = s_file['bucketName']
|
||||||
s_object = s_file['objectName']
|
s_object = s_file['objectName']
|
||||||
filename = os.path.join(targetFolder, s_object.replace('/', '-'))
|
|
||||||
print(f'saving to {filename}')
|
|
||||||
request = s.objects().get_media(bucket=bucket, object=s_object)
|
|
||||||
f = fileutils.open_file(filename, 'wb')
|
|
||||||
downloader = googleapiclient.http.MediaIoBaseDownload(f, request)
|
|
||||||
done = False
|
|
||||||
while not done:
|
|
||||||
status, done = downloader.next_chunk()
|
|
||||||
sys.stdout.write(f' Downloaded: {status.progress():>7.2%}\r')
|
|
||||||
sys.stdout.flush()
|
|
||||||
sys.stdout.write('\n Download complete. Flushing to disk...\n')
|
|
||||||
fileutils.close_file(f, True)
|
|
||||||
if verifyFiles:
|
if verifyFiles:
|
||||||
expected_hash = s_file['md5Hash']
|
expected_hash = s_file['md5Hash']
|
||||||
sys.stdout.write(f' Verifying file hash is {expected_hash}...')
|
else:
|
||||||
sys.stdout.flush()
|
expected_hash = None
|
||||||
utils.md5_matches_file(filename, expected_hash, True)
|
local_file = s_object.replace('/', '-').replace(':', '-')
|
||||||
print('VERIFIED')
|
filename = os.path.join(targetFolder, local_file)
|
||||||
|
gapi_storage.get_cloud_storage_object(s,
|
||||||
|
bucket,
|
||||||
|
s_object,
|
||||||
|
local_file=filename,
|
||||||
|
expectedMd5=expected_hash)
|
||||||
if extractFiles and re.search(r'\.zip$', filename):
|
if extractFiles and re.search(r'\.zip$', filename):
|
||||||
gam.extract_nested_zip(filename, targetFolder)
|
gam.extract_nested_zip(filename, targetFolder)
|
||||||
|
|
||||||
@@ -976,10 +1013,14 @@ def printHolds():
|
|||||||
for matterId in matterIds:
|
for matterId in matterIds:
|
||||||
i += 1
|
i += 1
|
||||||
sys.stderr.write(f'Retrieving holds for matter {matterId} ({i}/{matter_count})\n')
|
sys.stderr.write(f'Retrieving holds for matter {matterId} ({i}/{matter_count})\n')
|
||||||
|
try:
|
||||||
holds = gapi.get_all_pages(v.matters().holds(),
|
holds = gapi.get_all_pages(v.matters().holds(),
|
||||||
'list',
|
'list',
|
||||||
'holds',
|
'holds',
|
||||||
|
throw_reasons=[gapi_errors.ErrorReason.FOUR_O_O],
|
||||||
matterId=matterId)
|
matterId=matterId)
|
||||||
|
except googleapiclient.errors.HttpError:
|
||||||
|
continue
|
||||||
for hold in holds:
|
for hold in holds:
|
||||||
display.add_row_titles_to_csv_file(
|
display.add_row_titles_to_csv_file(
|
||||||
utils.flatten_json(hold, flattened={'matterId': matterId}),
|
utils.flatten_json(hold, flattened={'matterId': matterId}),
|
||||||
|
|||||||
@@ -96,9 +96,13 @@ class _DeHTMLParser(HTMLParser):
|
|||||||
re.sub(r'\n +', '\n', ''.join(self.__text))).strip()
|
re.sub(r'\n +', '\n', ''.join(self.__text))).strip()
|
||||||
|
|
||||||
|
|
||||||
def commonprefix(m):
|
def commonprefix(m, checkEnum=False):
|
||||||
'''Given a list of strings m, return string which is prefix common to all'''
|
'''Given a list of strings m, return string which is prefix common to all'''
|
||||||
s1 = min(m)
|
s1 = min(m)
|
||||||
|
if checkEnum:
|
||||||
|
loc = s1.find('ENUM_')
|
||||||
|
if loc > 0:
|
||||||
|
return s1[:loc+5]
|
||||||
s2 = max(m)
|
s2 = max(m)
|
||||||
for i, c in enumerate(s1):
|
for i, c in enumerate(s1):
|
||||||
if c != s2[i]:
|
if c != s2[i]:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import platform
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
GAM_AUTHOR = 'Jay Lee <jay0lee@gmail.com>'
|
GAM_AUTHOR = 'Jay Lee <jay0lee@gmail.com>'
|
||||||
GAM_VERSION = '6.23'
|
GAM_VERSION = '6.40'
|
||||||
GAM_LICENSE = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
|
GAM_LICENSE = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
|
||||||
|
|
||||||
GAM_URL = 'https://jaylee.us/gam'
|
GAM_URL = 'https://jaylee.us/gam'
|
||||||
@@ -127,6 +127,16 @@ SKUS = {
|
|||||||
'aliases': ['gwetlu', 'workspaceeducationupgrade'],
|
'aliases': ['gwetlu', 'workspaceeducationupgrade'],
|
||||||
'displayName': 'Google Workspace for Education: Teaching and Learning Upgrade'
|
'displayName': 'Google Workspace for Education: Teaching and Learning Upgrade'
|
||||||
},
|
},
|
||||||
|
'1010390001': {
|
||||||
|
'product': '101039',
|
||||||
|
'aliases': ['assuredcontrols'],
|
||||||
|
'displayName': 'Assured Controls',
|
||||||
|
},
|
||||||
|
'1010400001': {
|
||||||
|
'product': '101040',
|
||||||
|
'aliases': ['beyondcorp', 'beyondcorpenterprise', 'bce'],
|
||||||
|
'displayName': 'Beyond Corp Enterprise',
|
||||||
|
},
|
||||||
'Google-Apps': {
|
'Google-Apps': {
|
||||||
'product': 'Google-Apps',
|
'product': 'Google-Apps',
|
||||||
'aliases': ['standard', 'free'],
|
'aliases': ['standard', 'free'],
|
||||||
@@ -206,7 +216,7 @@ SKUS = {
|
|||||||
},
|
},
|
||||||
'1010020030': {
|
'1010020030': {
|
||||||
'product': 'Google-Apps',
|
'product': 'Google-Apps',
|
||||||
'aliases': ['workspacefrontline', 'workspacefrontlineworker'],
|
'aliases': ['wsflw', 'workspacefrontline', 'workspacefrontlineworker'],
|
||||||
'displayName': 'Workspace Frontline'
|
'displayName': 'Workspace Frontline'
|
||||||
},
|
},
|
||||||
'1010340002': {
|
'1010340002': {
|
||||||
@@ -290,6 +300,8 @@ PRODUCTID_NAME_MAPPINGS = {
|
|||||||
'101035': 'Cloud Search',
|
'101035': 'Cloud Search',
|
||||||
'101036': 'Google Meet Global Dialing',
|
'101036': 'Google Meet Global Dialing',
|
||||||
'101037': 'G Suite Workspace for Education',
|
'101037': 'G Suite Workspace for Education',
|
||||||
|
'101039': 'Assured Controls',
|
||||||
|
'101040': 'Beyond Corp',
|
||||||
'Google-Apps': 'Google Workspace',
|
'Google-Apps': 'Google Workspace',
|
||||||
'Google-Chrome-Device-Management': 'Google Chrome Device Management',
|
'Google-Chrome-Device-Management': 'Google Chrome Device Management',
|
||||||
'Google-Drive-storage': 'Google Drive Storage',
|
'Google-Drive-storage': 'Google Drive Storage',
|
||||||
@@ -648,6 +660,7 @@ MACOS_CODENAMES = {
|
|||||||
},
|
},
|
||||||
11: 'Big Sur',
|
11: 'Big Sur',
|
||||||
12: 'Monterey',
|
12: 'Monterey',
|
||||||
|
13: 'Ventura',
|
||||||
}
|
}
|
||||||
|
|
||||||
_MICROSOFT_FORMATS_LIST = [{
|
_MICROSOFT_FORMATS_LIST = [{
|
||||||
@@ -971,6 +984,8 @@ CROS_ARGUMENT_TO_PROPERTY_MAP = {
|
|||||||
'autoupdateexpiration': ['autoUpdateExpiration',],
|
'autoupdateexpiration': ['autoUpdateExpiration',],
|
||||||
'bootmode': ['bootMode',],
|
'bootmode': ['bootMode',],
|
||||||
'cpustatusreports': ['cpuStatusReports',],
|
'cpustatusreports': ['cpuStatusReports',],
|
||||||
|
'deprovisionreason': ['deprovisionReason',],
|
||||||
|
'lastDeprovisionTimestamp': ['lastDeprovisionTimestamp',],
|
||||||
'devicefiles': ['deviceFiles',],
|
'devicefiles': ['deviceFiles',],
|
||||||
'deviceid': ['deviceId',],
|
'deviceid': ['deviceId',],
|
||||||
'dockmacaddress': ['dockMacAddress',],
|
'dockmacaddress': ['dockMacAddress',],
|
||||||
@@ -978,6 +993,7 @@ CROS_ARGUMENT_TO_PROPERTY_MAP = {
|
|||||||
'ethernetmacaddress': ['ethernetMacAddress',],
|
'ethernetmacaddress': ['ethernetMacAddress',],
|
||||||
'ethernetmacaddress0': ['ethernetMacAddress0',],
|
'ethernetmacaddress0': ['ethernetMacAddress0',],
|
||||||
'firmwareversion': ['firmwareVersion',],
|
'firmwareversion': ['firmwareVersion',],
|
||||||
|
'firstenrollmenttime': ['firstEnrollmentTime',],
|
||||||
'lastenrollmenttime': ['lastEnrollmentTime',],
|
'lastenrollmenttime': ['lastEnrollmentTime',],
|
||||||
'lastknownnetwork': ['lastKnownNetwork'],
|
'lastknownnetwork': ['lastKnownNetwork'],
|
||||||
'lastsync': ['lastSync',],
|
'lastsync': ['lastSync',],
|
||||||
@@ -1035,7 +1051,10 @@ CROS_SCALAR_PROPERTY_PRINT_ORDER = [
|
|||||||
'ethernetMacAddress0',
|
'ethernetMacAddress0',
|
||||||
'macAddress',
|
'macAddress',
|
||||||
'systemRamTotal',
|
'systemRamTotal',
|
||||||
|
'firstEnrollmentTime',
|
||||||
'lastEnrollmentTime',
|
'lastEnrollmentTime',
|
||||||
|
'deprovisionReason',
|
||||||
|
'lastDeprovisionTimestamp',
|
||||||
'orderNumber',
|
'orderNumber',
|
||||||
'manufactureDate',
|
'manufactureDate',
|
||||||
'supportEndDate',
|
'supportEndDate',
|
||||||
@@ -1202,6 +1221,7 @@ _DEFAULT_CHARSET = UTF8
|
|||||||
_FN_CLIENT_SECRETS_JSON = 'client_secrets.json'
|
_FN_CLIENT_SECRETS_JSON = 'client_secrets.json'
|
||||||
_FN_OAUTH2SERVICE_JSON = 'oauth2service.json'
|
_FN_OAUTH2SERVICE_JSON = 'oauth2service.json'
|
||||||
_FN_OAUTH2_TXT = 'oauth2.txt'
|
_FN_OAUTH2_TXT = 'oauth2.txt'
|
||||||
|
_FN_ROOTS_PEM = 'roots.pem'
|
||||||
#
|
#
|
||||||
GM_Globals = {
|
GM_Globals = {
|
||||||
GM_SYSEXITRC: 0,
|
GM_SYSEXITRC: 0,
|
||||||
@@ -1269,6 +1289,9 @@ GC_ENABLE_DASA = 'enabledasa'
|
|||||||
# and doRequestOAuth prints a link and waits for the verification code when
|
# and doRequestOAuth prints a link and waits for the verification code when
|
||||||
# oauth2.txt is being created
|
# oauth2.txt is being created
|
||||||
GC_NO_BROWSER = 'no_browser'
|
GC_NO_BROWSER = 'no_browser'
|
||||||
|
# If low memory is True, GAM tries to save RAM by writing pages to disk
|
||||||
|
# temporarily
|
||||||
|
GC_LOW_MEMORY = 'low_memory'
|
||||||
# If no_tdemail is True, writeCSVfile won't send an email
|
# If no_tdemail is True, writeCSVfile won't send an email
|
||||||
GC_NO_TDEMAIL = 'no_tdemail'
|
GC_NO_TDEMAIL = 'no_tdemail'
|
||||||
# oauth_browser forces usage of web server OAuth flow that proved problematic.
|
# oauth_browser forces usage of web server OAuth flow that proved problematic.
|
||||||
@@ -1324,6 +1347,7 @@ GC_Defaults = {
|
|||||||
GC_DOMAIN: '',
|
GC_DOMAIN: '',
|
||||||
GC_DRIVE_DIR: '',
|
GC_DRIVE_DIR: '',
|
||||||
GC_ENABLE_DASA: False,
|
GC_ENABLE_DASA: False,
|
||||||
|
GC_LOW_MEMORY: False,
|
||||||
GC_NO_BROWSER: False,
|
GC_NO_BROWSER: False,
|
||||||
GC_NO_TDEMAIL: False,
|
GC_NO_TDEMAIL: False,
|
||||||
GC_NO_CACHE: False,
|
GC_NO_CACHE: False,
|
||||||
@@ -1343,7 +1367,7 @@ GC_Defaults = {
|
|||||||
GC_CSV_ROW_DROP_FILTER: '',
|
GC_CSV_ROW_DROP_FILTER: '',
|
||||||
GC_TLS_MIN_VERSION: TLS_MIN,
|
GC_TLS_MIN_VERSION: TLS_MIN,
|
||||||
GC_TLS_MAX_VERSION: None,
|
GC_TLS_MAX_VERSION: None,
|
||||||
GC_CA_FILE: None,
|
GC_CA_FILE: _FN_ROOTS_PEM,
|
||||||
}
|
}
|
||||||
|
|
||||||
GC_Values = {}
|
GC_Values = {}
|
||||||
@@ -1408,6 +1432,9 @@ GC_VAR_INFO = {
|
|||||||
GC_ENABLE_DASA: {
|
GC_ENABLE_DASA: {
|
||||||
GC_VAR_TYPE: GC_TYPE_BOOLEAN
|
GC_VAR_TYPE: GC_TYPE_BOOLEAN
|
||||||
},
|
},
|
||||||
|
GC_LOW_MEMORY: {
|
||||||
|
GC_VAR_TYPE: GC_TYPE_BOOLEAN
|
||||||
|
},
|
||||||
GC_NO_BROWSER: {
|
GC_NO_BROWSER: {
|
||||||
GC_VAR_TYPE: GC_TYPE_BOOLEAN
|
GC_VAR_TYPE: GC_TYPE_BOOLEAN
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,4 +10,4 @@ importlib.metadata; python_version < '3.8'
|
|||||||
passlib>=1.7.2
|
passlib>=1.7.2
|
||||||
pathvalidate
|
pathvalidate
|
||||||
python-dateutil
|
python-dateutil
|
||||||
yubikey-manager>=4.0.0
|
yubikey-manager>=5.0
|
||||||
|
|||||||
1130
src/roots.pem
Normal file
1130
src/roots.pem
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
[metadata]
|
[metadata]
|
||||||
name = GAM for Google Workspace
|
name = GAM for Google Workspace
|
||||||
version = 6.0.23
|
version = attr: gam.var.GAM_VERSION
|
||||||
description = Command line management for Google Workspaces
|
description = Command line management for Google Workspaces
|
||||||
long_description = file: readme.md
|
long_description = file: readme.md
|
||||||
long_description_content_type = text/markdown
|
long_description_content_type = text/markdown
|
||||||
@@ -37,6 +37,9 @@ install_requires =
|
|||||||
yubikey-manager >= 4.0.0
|
yubikey-manager >= 4.0.0
|
||||||
pathvalidate
|
pathvalidate
|
||||||
|
|
||||||
|
[options.package_data]
|
||||||
|
* = *.pem
|
||||||
|
|
||||||
# used during pip install .[test]
|
# used during pip install .[test]
|
||||||
[options.extras_require]
|
[options.extras_require]
|
||||||
test = pre-commit
|
test = pre-commit
|
||||||
|
|||||||
Reference in New Issue
Block a user