Compare commits

...

125 Commits

Author SHA1 Message Date
Jay Lee
eef2b95948 Only run publish on new tag starting with v 2023-04-07 09:49:02 -04:00
Jay Lee
7012bef28d package and test staticx builds as part of normal flow 2023-04-06 13:27:51 -04:00
Jay Lee
b3b44d144e GAM 6.54 2023-04-06 12:44:30 -04:00
Jay Lee
841eba79a3 Update build.yml 2023-04-06 11:02:55 -04:00
Jay Lee
77234f9e3d Update build.yml 2023-04-06 09:59:53 -04:00
Jay Lee
14478d7831 Update build.yml 2023-04-06 09:07:32 -04:00
Jay Lee
50aa7d937e Update build.yml 2023-04-06 08:07:05 -04:00
Jay Lee
2c7e01e003 Update build.yml 2023-04-06 07:01:35 -04:00
Jay Lee
a6ce5f04aa Update build.yml 2023-04-06 05:37:24 -04:00
Jay Lee
8bc6814b42 Use TLS 1.2 for SignJWT 2023-04-06 09:36:23 +00:00
Jay Lee
024177b0c7 Update __main__.py 2023-04-06 05:18:05 -04:00
Jay Lee
b7faa0acae Update build.yml 2023-04-06 05:15:35 -04:00
Ross Scroggs
0dbdbc7a13 Two updates (#1618)
* Update documentation: gam delete inboundssoassignment <SSOAssignmentSelector>

* tsv is a valid Google Doc extension
2023-04-06 05:13:50 -04:00
Jay Lee
08271e60bf Update __init__.py 2023-04-06 05:05:08 -04:00
Jay Lee
ec74698001 Update build.yml 2023-04-06 04:23:28 -04:00
Jay Lee
6cecacd334 Update __init__.py 2023-04-05 20:27:35 -04:00
Jay Lee
c3d27900e1 Update __main__.py 2023-04-05 20:16:10 -04:00
Jay Lee
f10df3607f Update __init__.py 2023-04-05 20:01:49 -04:00
Jay Lee
416be24722 Update build.yml 2023-04-05 14:55:21 -04:00
Jay Lee
e53b4a2285 backout retries 2023-04-05 18:51:03 +00:00
Jay Lee
a88320b1b2 backout sign change 2023-04-05 18:48:47 +00:00
Jay Lee
76f9a144ac debug network issues 2023-04-05 18:41:20 +00:00
Jay Lee
a673772cc1 add request to createsignjwtserviceaccount 2023-04-05 18:36:17 +00:00
Jay Lee
9e6d8195eb scope is a list, not str 2023-04-05 18:26:04 +00:00
Jay Lee
91d97c4a2c Merge branch 'main' of https://github.com/GAM-team/GAM 2023-04-05 17:18:22 +00:00
Jay Lee
5e1df9263b use transport and bump retries 2023-04-05 17:18:10 +00:00
Jay Lee
e54921ad71 rebuild for Python 3.11.3 2023-04-05 10:49:09 -04:00
Jay Lee
1b8d0877f3 retire soon-to-be EoL Python 3.7. Hello walrus operator... 2023-04-05 14:33:17 +00:00
Jay Lee
a4e962560c Update build.yml 2023-04-05 08:48:29 -04:00
Jay Lee
be7d3ceb15 [no ci] make cigroup a security group 2023-04-05 08:36:55 -04:00
Jay Lee
1e652d5725 Update build.yml 2023-04-05 08:28:50 -04:00
Jay Lee
1e7e5422be sso v1 and delete assignments 2023-04-05 12:27:28 +00:00
Jay Lee
723e9e2bb1 Update build.yml 2023-04-04 14:20:05 -04:00
Jay Lee
1f572cc95b Update build.yml 2023-04-04 14:13:19 -04:00
Jay Lee
fb63eea4a0 Update build.yml 2023-04-04 12:50:18 -04:00
Jay Lee
7efb37010d Update build.yml 2023-04-04 12:47:46 -04:00
Jay Lee
6372af8d8a Update build.yml 2023-04-04 12:18:35 -04:00
Jay Lee
0b823ea43e Update build.yml 2023-04-04 12:15:24 -04:00
Jay Lee
cebb92199f Update build.yml 2023-04-04 12:12:42 -04:00
Jay Lee
6deabf8a66 Update build.yml 2023-04-04 12:10:55 -04:00
Jay Lee
5de74a51e0 Update build.yml 2023-04-04 12:02:11 -04:00
Jay Lee
85d6305874 Update build.yml 2023-04-04 11:57:30 -04:00
Jay Lee
30d685a6f7 Update build.yml 2023-04-04 11:54:42 -04:00
Jay Lee
fcc8a58839 Update build.yml 2023-04-04 11:26:26 -04:00
Jay Lee
5a608a9b62 Update build.yml 2023-04-04 11:19:04 -04:00
Jay Lee
eb9c127a10 Update build.yml 2023-04-03 12:46:49 -04:00
Jay Lee
ed55690ff3 Update build.yml 2023-04-03 12:30:45 -04:00
Jay Lee
502afa5213 Update __init__.py 2023-04-03 12:13:34 -04:00
Jay Lee
24185d66ce Update cbcm-v1.1beta1.json 2023-04-03 11:55:41 -04:00
Jay Lee
181ba65c63 Update build.yml 2023-04-03 11:13:01 -04:00
Jay Lee
702f36a529 Update build.yml 2023-04-03 11:06:52 -04:00
Jay Lee
e2f73bf858 Update build.yml 2023-04-03 09:37:47 -04:00
Jay Lee
7265e8c6f4 Update build.yml 2023-04-03 09:26:12 -04:00
Jay Lee
b8b9808e94 Update build.yml 2023-04-03 09:02:34 -04:00
Jay Lee
7639773c40 Update build.yml 2023-04-03 08:52:54 -04:00
Jay Lee
6ab7370149 Update build.yml 2023-04-03 08:47:58 -04:00
Jay Lee
73994fe603 Update build.yml 2023-04-03 08:39:56 -04:00
Jay Lee
3fa646723d Update build.yml 2023-04-03 08:31:36 -04:00
Jay Lee
eb08b1fbdc Update build.yml 2023-04-03 08:11:44 -04:00
Jay Lee
93ac820005 Update build.yml 2023-04-03 08:03:49 -04:00
Jay Lee
c100e25ab9 Update build.yml 2023-04-03 08:01:09 -04:00
Jay Lee
716489ceed key_type is a GAMism, use type to identify SA file. 2023-04-03 08:00:12 -04:00
Jay Lee
07d5f5e52c Update build.yml 2023-04-02 17:00:18 -04:00
Jay Lee
b889debd5e Update build.yml 2023-04-02 16:57:04 -04:00
Jay Lee
b273fe1f68 Update build.yml 2023-04-02 16:54:56 -04:00
Jay Lee
376cd6e83f Update signjwt.py 2023-04-02 15:58:04 -04:00
Jay Lee
e8cb1a7b9f Update __init__.py 2023-04-02 15:53:17 -04:00
Jay Lee
9f0c5beae7 Update __init__.py 2023-04-02 15:49:02 -04:00
Jay Lee
0ea2f16322 Update build.yml 2023-04-02 15:08:49 -04:00
Jay Lee
13ca2e8d93 Update build.yml 2023-04-02 15:03:27 -04:00
Jay Lee
3833256c8c Update build.yml 2023-04-02 14:40:50 -04:00
Jay Lee
30521612b2 fix permissions 2023-04-02 14:34:56 -04:00
Jay Lee
d069cfc309 Use WIF for service account credentials 2023-04-02 14:33:15 -04:00
Jay Lee
27461b067a Update var.py 2023-03-31 09:17:10 -04:00
Jay Lee
017712742b Merge branch 'main' of https://github.com/GAM-team/GAM 2023-03-30 17:37:23 +00:00
Jay Lee
afce21a1bd Add steps to trust GAM client_ID 2023-03-30 17:34:59 +00:00
Jay Lee
030e2e270f Another attempt at fixing cryptography 2023-03-30 10:14:24 -04:00
Jay Lee
c69a86b535 use constraints.txt to prevent any downgrades during pip install 2023-03-28 08:49:03 -04:00
Jay Lee
b64e4cf3dc [no ci] that didn't work, we'll try rolling forward... 2023-03-28 08:43:57 -04:00
Jay Lee
a2e06adbbe pin cryptography to 39.0.2 for time being 2023-03-28 08:21:46 -04:00
Jay Lee
43b3397541 Rebuild for cryptography 2023-03-25 10:00:29 -04:00
Jay Lee
bd0bb1542c AppSheet licenses 2023-03-22 12:54:27 +00:00
Jay Lee
a92a07f9c0 Update var.py 2023-03-16 12:35:11 -04:00
Ross Scroggs
42ed5509ee Updates/cleanup (#1614)
* Handle socks error when checking local time offset

* Keep pylint happy

* Avoid trap when authentication flow blows up

* Fix bug that causes trap on create project
2023-03-16 12:31:20 -04:00
Jay Lee
a6582503f2 Update var.py 2023-03-15 18:31:35 -04:00
Jay Lee
7aecb889d2 Allow setting sakey validity_hours 2023-03-15 20:41:25 +00:00
Jay Lee
c273f87cc7 clear cache to pickup OpenSSL 3.1.0 2023-03-14 10:08:02 -04:00
Jay Lee
76d00c993a Update build.yml 2023-03-06 10:13:17 -05:00
Jay Lee
013b47e6e7 Update build.yml 2023-03-06 09:43:50 -05:00
Jay Lee
9f1e9934ff Update build.yml 2023-03-06 09:29:08 -05:00
Jay Lee
48b218bd9c Update build.yml 2023-03-06 08:40:59 -05:00
Jay Lee
af5baa4f3a Update build.yml 2023-03-06 08:37:37 -05:00
Jay Lee
a2cf38d904 Update build.yml 2023-03-06 07:40:01 -05:00
Jay Lee
185522d943 Update build.yml 2023-03-05 15:29:27 -05:00
Jay Lee
a42e4dd080 openssl security level 2 2023-03-05 15:28:02 -05:00
Ross Scroggs
3a5486889f Update chromepolicy.py (#1610)
split of an empty string returns [''], the API wants []
2023-03-04 11:33:22 -05:00
Jay Lee
1a1f100902 completely disalbe TLS 1.0/1.1 2023-03-03 07:09:46 -05:00
Ross Scroggs
c67b214298 Allow user to optionally specify serial number on resetpiv (#1607) 2023-02-27 11:17:20 -05:00
Ross Scroggs
3ad1d5c661 Give error on invalid subargument; handle no YubiKeys in resetpiv (#1606) 2023-02-27 10:37:22 -05:00
Jay Lee
13400d9bde Update build.yml 2023-02-24 14:20:37 -05:00
Ross Scroggs
048e8dfef5 Add gam version checkrc (#1605)
* Add gam version checkrc

* Fix code

* gam version checkrc cleanup
2023-02-24 10:58:23 -05:00
Ross Scroggs
aaf7a89192 Fix documenataion error (#1604) 2023-02-24 09:45:22 -05:00
Jay Lee
e3ee9135ff Update build.yml 2023-02-23 16:30:23 -05:00
Ross Scroggs
a774fc0beb GCP cleanup (#1602) 2023-02-23 11:44:52 -05:00
Jay Lee
f3429bd537 Update build.yml 2023-02-23 08:50:03 -05:00
Jay Lee
37876acfda Update var.py 2023-02-23 08:17:22 -05:00
Jay Lee
2a6dd0d1a2 fix building iamcredentials 2023-02-22 17:30:10 +00:00
Jay Lee
b0626dd37a improve on gam enable apis 2023-02-17 22:07:36 +00:00
Jay Lee
ed0ed8d7fc fix Id 2023-02-17 20:33:47 +00:00
Jay Lee
d67d999930 enable APIs command for signjwt 2023-02-17 20:32:29 +00:00
Jay Lee
ac79cff6b9 create signjwtserviceaccount 2023-02-17 19:39:02 +00:00
Jay Lee
50aadc6ea7 allow forcing OAuth for service account 2023-02-17 15:40:36 +00:00
Jay Lee
9036d114ed signjwt key_type for key-less service account auth 2023-02-17 15:17:01 +00:00
Jay Lee
75c19104ae fix ipv6 with checkconn 2023-02-15 17:22:34 +00:00
Jay Lee
d9b7f88287 6.42 - build shared drive restrictions dynamically 2023-02-13 21:51:41 +00:00
Jay Lee
ae28c09560 6.41 - fixes #1600 2023-02-11 13:40:59 +00:00
Jay Lee
6ffc738a51 Update gam-install.sh 2023-02-11 08:12:35 -05:00
Jay Lee
82dcc4de6a rebuild to get Python 3.11.2 2023-02-08 10:35:58 -05:00
Jay Lee
f7a426f65a rebuild for OpenSSL 3.0.8 2023-02-07 12:25:31 -05:00
Jay Lee
a94ef78066 fix Vault download filenames 2023-02-06 21:43:33 +00:00
Ross Scroggs
62d738f5c2 copy storagebucket/vault cleanup (#1599) 2023-02-06 15:50:39 -05:00
Jay Lee
1c56a0a608 Update var.py 2023-02-06 09:57:47 -05:00
Jay Lee
dc3976bdda gam copy vaultexport/storagebucket commands 2023-02-06 13:33:26 +00:00
Ross Scroggs
454778b190 print chromeaues/chromeversions cleanup, add print chromeneedsattn (#1598)
* print chromeaues/chromeversions cleanup, add print chromeneedsattn

* Fix typo

* Define new ChromeOS fields
2023-02-03 14:10:35 -05:00
Ross Scroggs
5e78c93b71 Added gam print chromeaues (#1597) 2023-01-30 19:42:28 -05:00
19 changed files with 860 additions and 191 deletions

View File

@@ -6,13 +6,17 @@ on:
schedule:
- cron: '37 22 * * *'
permissions:
contents: read
id-token: write
defaults:
run:
shell: bash
working-directory: src
env:
OPENSSL_CONFIG_OPTS: no-fips
OPENSSL_CONFIG_OPTS: no-fips --api=3.0.0
OPENSSL_INSTALL_PATH: ${{ github.workspace }}/bin/ssl
OPENSSL_SOURCE_PATH: ${{ github.workspace }}/src/openssl
PYTHON_INSTALL_PATH: ${{ github.workspace }}/bin/python
@@ -62,11 +66,6 @@ jobs:
goal: build
arch: Win32
openssl_archs: VC-WIN32
- os: ubuntu-22.04
goal: test
python: "3.7"
jid: 8
arch: x86_64
- os: ubuntu-22.04
goal: test
python: "3.8"
@@ -95,6 +94,13 @@ jobs:
persist-credentials: false
fetch-depth: 0
- id: auth
name: Authenticate to Google Cloud
uses: google-github-actions/auth@v1
with:
workload_identity_provider: projects/297925809119/locations/global/workloadIdentityPools/gha-pool/providers/gha-provider
service_account: github-actions-testing-for-gam@gam-project-wyo-lub-ivl.iam.gserviceaccount.com
- name: Cache multiple paths
if: matrix.goal == 'build'
uses: actions/cache@v3
@@ -103,7 +109,7 @@ jobs:
path: |
bin.tar.xz
src/cpython
key: gam-${{ matrix.jid }}-20221207
key: gam-${{ matrix.jid }}-20230405
- name: Untar Cache archive
if: matrix.goal == 'build' && steps.cache-python-ssl.outputs.cache-hit == 'true'
@@ -117,6 +123,17 @@ jobs:
with:
python-version: ${{ matrix.python }}
- name: Set cURL retry flag
run: |
curl_version=$(curl --version | head -n 1 | awk '{ print $2 }')
echo "cURL is ${curl_version}"
if [ "$curl_version" == "7.68.0" ]; then
export curl_retry="--retry 5 --retry-connrefused"
else
export curl_retry="--retry 5 --retry-all-errors"
fi
echo "curl_retry=${curl_retry}" >> $GITHUB_ENV
- name: Set env variables for test
if: matrix.goal == 'test'
env:
@@ -144,17 +161,11 @@ jobs:
sudo apt-get -qq --yes update
sudo apt-get -qq --yes install swig libpcsclite-dev
#- name: MacOS remove Homebrew
# if: runner.os == 'macOS'
# run: |
# # remove everything except the libraries needed by yubikey-manager
# brew uninstall $(brew list | grep -v 'pcre\|swig\|pcsc-lite')
- name: MacOS install tools
if: runner.os == 'macOS'
run: |
# Install latest Rust
curl --retry 5 --retry-connrefused -fsS -o rust.sh https://sh.rustup.rs
curl $curl_retry -fsS -o rust.sh https://sh.rustup.rs
bash ./rust.sh -y
source $HOME/.cargo/env
# needed for Rust to compile cryptography Python package for universal2
@@ -175,7 +186,7 @@ jobs:
staticx: ${{ matrix.staticx }}
run: |
echo "We are running on ${RUNNER_OS}"
LD_LIBRARY_PATH="${OPENSSL_INSTALL_PATH}/lib:${PYTHON_INSTALL_PATH}/lib"
LD_LIBRARY_PATH="${OPENSSL_INSTALL_PATH}/lib:${PYTHON_INSTALL_PATH}/lib:/usr/local/lib"
if [[ "${arch}" == "Win64" ]]; then
PYEXTERNALS_PATH="amd64"
PYBUILDRELEASE_ARCH="x64"
@@ -197,7 +208,7 @@ jobs:
PERL=perl
echo "MACOSX_DEPLOYMENT_TARGET=10.15" >> $GITHUB_ENV
echo "PYTHON=${PYTHON_INSTALL_PATH}/bin/python3" >> $GITHUB_ENV
echo "PIP_ARGS=--no-binary=:all:" >> $GITHUB_ENV
#echo "PIP_ARGS=--no-binary=:all:" >> $GITHUB_ENV
elif [[ "${RUNNER_OS}" == "Linux" ]]; then
MAKE=make
MAKEOPT="-j$(nproc)"
@@ -298,7 +309,7 @@ jobs:
rm -rf ${GITHUB_WORKSPACE}/bin/ssl-darwin64-arm64
echo "LDFLAGS=-L${OPENSSL_INSTALL_PATH}/lib" >> $GITHUB_ENV
echo "CRYPTOGRAPHY_SUPPRESS_LINK_FLAGS=1" >> $GITHUB_ENV
echo "CFLAGS=-I${OPENSSL_INSTALL_PATH}/include -arch arm64 -arch x86_64" >> $GITHUB_ENV
echo "CFLAGS=-I${OPENSSL_INSTALL_PATH}/include -arch arm64 -arch x86_64 ${CFLAGS}" >> $GITHUB_ENV
echo "ARCHFLAGS=-arch x86_64 -arch arm64" >> $GITHUB_ENV
else
cd "${GITHUB_WORKSPACE}/src/openssl-${openssl_archs}"
@@ -310,6 +321,7 @@ jobs:
if: matrix.goal == 'build'
run: |
"${OPENSSL_INSTALL_PATH}/bin/openssl" version
"${OPENSSL_INSTALL_PATH}/bin/openssl" version -f
file "${OPENSSL_INSTALL_PATH}/bin/openssl"
- name: Get latest stable Python source
@@ -407,7 +419,7 @@ jobs:
- name: Upgrade pip, wheel, etc
run: |
curl --retry 5 --retry-connrefused -O https://bootstrap.pypa.io/get-pip.py
curl $curl_retry -O https://bootstrap.pypa.io/get-pip.py
"${PYTHON}" get-pip.py
"${PYTHON}" -m pip install --upgrade pip
"${PYTHON}" -m pip install --upgrade wheel
@@ -415,8 +427,21 @@ jobs:
- name: Install pip requirements
run: |
echo "before anything..."
"${PYTHON}" -m pip list
if [[ "${RUNNER_OS}" == "macOS" ]]; then
"${PYTHON}" -m pip install --upgrade cffi ${PIP_ARGS}
# cffi is a dep of cryptography and doesn't ship
# a universal2 wheel so we must build one ourself :-/
export CFLAGS="-arch x86_64 -arch arm64"
export ARCHFLAGS="-arch x86_64 -arch arm64"
"${PYTHON}" -m pip install --upgrade --force-reinstall --no-binary :all: \
--no-cache-dir --no-deps --use-pep517 \
--use-feature=no-binary-enable-wheel-cache \
cffi
echo "before cryptography..."
"${PYTHON}" -m pip list
# cryptography has a universal2 wheel but getting it installed
# on x86-64 MacOS is a royal pain in the keester.
"${PYTHON}" -m pip download --only-binary :all: \
--dest . \
--no-cache \
@@ -424,8 +449,16 @@ jobs:
--platform macosx_10_15_universal2 \
cryptography
"${PYTHON}" -m pip install --force-reinstall --no-deps cryptography*.whl
echo "after cryptography..."
"${PYTHON}" -m pip list
"${PYTHON}" -m pip install --upgrade --no-binary :all: -r requirements.txt
else
"${PYTHON}" -m pip install --upgrade -r requirements.txt
echo "after requirements..."
"${PYTHON}" -m pip list
"${PYTHON}" -m pip install --force-reinstall --no-deps --upgrade cryptography
fi
"${PYTHON}" -m pip install --upgrade -r requirements.txt ${PIP_ARGS}
echo "after everything..."
"${PYTHON}" -m pip list
- name: Install PyInstaller
@@ -492,25 +525,6 @@ jobs:
cp -v gam-setup.bat $gampath
fi
- name: Basic Tests all jobs
run: |
$PYTHON -m unittest discover --start-directory ./ --pattern "*_test.py" --buffer
$gam version extended
export GAMVERSION=$($gam version simple)
echo "GAM Version ${GAMVERSION}"
echo "GAMVERSION=${GAMVERSION}" >> $GITHUB_ENV
- name: Linux/MacOS package
if: runner.os != 'Windows' && matrix.goal == 'build' && matrix.staticx != 'yes'
run: |
if [[ "${RUNNER_OS}" == "macOS" ]]; then
GAM_ARCHIVE="gam-${GAMVERSION}-macos-universal2.tar.xz"
elif [[ "${RUNNER_OS}" == "Linux" ]]; then
this_glibc_ver=$(ldd --version | awk '/ldd/{print $NF}')
GAM_ARCHIVE="gam-${GAMVERSION}-linux-$(arch)-glibc${this_glibc_ver}.tar.xz"
fi
tar -C dist/ --create --verbose --exclude-from "${GITHUB_WORKSPACE}/.github/actions/package_exclusions.txt" --file $GAM_ARCHIVE --xz gam
- name: Install StaticX
if: matrix.staticx == 'yes'
run: |
@@ -530,17 +544,24 @@ jobs:
esac
echo "ldlib=${ldlib}"
$PYTHON -m staticx -l "${ldlib}" "${gam}" "${gam}-staticx"
- name: Run StaticX
if: matrix.staticx == 'yes'
run: |
"${gam}-staticx" version extended
mv -v "${gam}-staticx" "${gam}"
- name: Linux package staticx
if: matrix.staticx == 'yes'
- name: Basic Tests all jobs
run: |
GAM_ARCHIVE="gam-${GAMVERSION}-linux-$(uname -m)-legacy.tar.xz"
$PYTHON -m unittest discover --start-directory ./ --pattern "*_test.py" --buffer
$gam version extended
export GAMVERSION=$($gam version simple)
echo "GAM Version ${GAMVERSION}"
echo "GAMVERSION=${GAMVERSION}" >> $GITHUB_ENV
- name: Linux/MacOS package
if: runner.os != 'Windows' && matrix.goal == 'build'
run: |
if [[ "${RUNNER_OS}" == "macOS" ]]; then
GAM_ARCHIVE="gam-${GAMVERSION}-macos-universal2.tar.xz"
elif [[ "${RUNNER_OS}" == "Linux" ]]; then
this_glibc_ver=$(ldd --version | awk '/ldd/{print $NF}')
GAM_ARCHIVE="gam-${GAMVERSION}-linux-$(arch)-glibc${this_glibc_ver}.tar.xz"
fi
tar -C dist/ --create --verbose --exclude-from "${GITHUB_WORKSPACE}/.github/actions/package_exclusions.txt" --file $GAM_ARCHIVE --xz gam
- name: Windows package
@@ -583,14 +604,20 @@ jobs:
brew install gnupg
fi
source ../.github/actions/decrypt.sh ../.github/actions/creds.tar.xz.gpg creds.tar.xz
rm $gampath/oauth2service.json
export OAUTHFILE="oauth2.txt-gam-gha-${JID}"
echo "OAUTHFILE=${OAUTHFILE}" >> $GITHUB_ENV
export gam_user="gam-gha-${JID}@pdl.jaylee.us"
echo "gam_user=${gam_user}" >> $GITHUB_ENV
$gam checkconn
$gam oauth info
$gam create signjwtserviceaccount
export CUSTOMER_ID="C03uzfv2s"
export GA_DOMAIN="pdl.jaylee.us"
export GA_ADMIN_EMAIL="$gam_user"
touch "${gampath}/enabledasa.txt"
#$gam oauth info
$gam info domain
$gam oauth refresh
#$gam oauth refresh
$gam info user
export tstamp=$($PYTHON -c "import time; print(time.time_ns())")
export newbase="gha_test_${JID}_${tstamp}"
@@ -602,7 +629,9 @@ jobs:
export newou="aaaGithub Actions/${newbase}"
# cleanup old runs
rm "${gampath}/enabledasa.txt"
GAM_CSV_ROW_FILTER="name:regex:gha_test_${JID}_" $gam print vaultholds | $gam csv - gam delete vaulthold "id:~~holdId~~" matter "id:~~matterId~~"
touch "${gampath}/enabledasa.txt"
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
@@ -622,20 +651,28 @@ jobs:
$gam user $newuser update photo https://dummyimage.com/400x600/000/fff
$gam user $newuser get photo
$gam user $newuser delete photo
$gam create alias $newalias user $newuser
$gam create group $newgroup name "GHA $JID group" description "This is a description" isarchived true
$gam user $gam_user sendemail recipient $newuser subject "test message $newbase" message "GHA test message"
$gam user $gam_user sendemail recipient exchange@pdl.jaylee.us subject "test ${tstamp}" message "test message"
rm "${gampath}/enabledasa.txt"
$gam user $newuser add license workspaceenterpriseplus
$gam print privileges
$gam update cigroup $newgroup memberrestriction 'member.type == 1 || member.customer_id == groupCustomerId()'
touch "${gampath}/enabledasa.txt"
$gam update cigroup $newgroup security memberrestriction 'member.type == 1 || member.customer_id == groupCustomerId()'
$gam info cigroup $newgroup
$gam update group $newgroup add owner $gam_user
$gam update group $newgroup add member $newuser
rm "${gampath}/enabledasa.txt"
$gam create admin $newuser _GROUPS_EDITOR_ROLE CUSTOMER # condition nonsecuritygroup
GAM_CSV_ROW_FILTER="assignedToUser:regex:${newuser}" $gam print admins | $gam csv - gam delete admin "~roleAssignmentId"
touch "${gampath}/enabledasa.txt"
$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 displayname "GitHub Actions Bulk ${JID}"
$gam csv sample.csv gam update user ~~email~~ recoveryphone "" recoveryemail ""
rm "${gampath}/enabledasa.txt"
$gam csv sample.csv gam user ~email add license workspaceenterpriseplus
touch "${gampath}/enabledasa.txt"
$gam csv sample.csv gam user $gam_user sendemail recipient ~~email~~@pdl.jaylee.us subject "test message $newbase" message "GHA test message"
$gam csv sample.csv gam update group $newgroup add member ~email
$gam info group $newgroup
@@ -679,6 +716,7 @@ jobs:
endtime=$($PYTHON -c "import datetime; print((datetime.datetime.now() + datetime.timedelta(hours=2)).strftime('%Y-%m-%dT%H:%M:%S.%f+00:00'))")
$gam calendar $gam_user addevent summary "GHA test event" start "${starttime}" end "${endtime}" attendee $newgroup hangoutsmeet guestscanmodify true sendupdates all
$gam calendar $gam_user printevents after -0d
rm "${gampath}/enabledasa.txt"
matterid=uid:$($gam create vaultmatter name "GHA matter $newbase" description "test matter" collaborators $newuser | head -1 | cut -d ' ' -f 3)
$gam create vaulthold matter $matterid name "GHA hold $newbase" corpus mail accounts $newuser
$gam print vaultmatters matterstate open
@@ -686,16 +724,21 @@ jobs:
$gam print vaultcount matter $matterid corpus mail everyone todrive
$gam create vaultexport matter $matterid name "GHA export $newbase" corpus mail accounts $newuser use_new_export false
$gam print exports matter $matterid | $gam csv - gam info export $matterid id:~~id~~
touch "${gampath}/enabledasa.txt"
$gam csv sample.csv gam user ~email add calendar id:$newresource
$gam delete resource $newresource
$gam delete feature Whiteboard-$newbase
$gam delete feature VC-$newbase
$gam delete building $newbuilding
$gam delete group $newgroup
$gam create alias $newalias user $newuser
rm "${gampath}/enabledasa.txt"
echo start
$gam user $newuser delete license workspaceenterpriseplus
echo finish
touch "${gampath}/enabledasa.txt"
$gam whatis $newuser
$gam user $gam_user show tokens
rm "${gampath}/enabledasa.txt"
$gam print exports matter $matterid | $gam csv - gam download export $matterid id:~~id~~
$gam delete hold "GHA hold $newbase" matter $matterid
$gam update matter $matterid action close
@@ -703,13 +746,14 @@ jobs:
# shakes off vault hold on user so we can delete
$gam print users query "email:${newuser}" orgunitpath | $gam csv - gam update user ~primaryEmail ou ~orgUnitPath
$gam user $newuser show holds
export sn="$JID$JID$JID$JID-$(openssl rand -base64 32 | sed 's/[^a-zA-Z0-9]//g')"
$gam create device serialnumber $sn devicetype android
touch "${gampath}/enabledasa.txt"
$gam delete user $newuser
$gam print users query "gha.jid=$JID" | $gam csv - gam delete user ~primaryEmail
$gam print mobile
$gam print devices
$gam print browsers
export sn="$JID$JID$JID$JID-$(openssl rand -base64 32 | sed 's/[^a-zA-Z0-9]//g')"
$gam create device serialnumber $sn devicetype android
$gam print cros allfields orderby serialnumber
$gam show crostelemetry storagepercentonly
$gam report usageparameters customer
@@ -718,8 +762,10 @@ 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 admin start -3d todrive
$gam print devices nopersonaldevices nodeviceusers filter "serial:$JID$JID$JID$JID-" | $gam csv - gam delete device id ~name
rm "${gampath}/enabledasa.txt"
$gam print userinvitations
$gam print userinvitations | $gam csv - gam send userinvitation ~name
touch "${gampath}/enabledasa.txt"
$gam create caalevel "zzz_${newbase}" basic condition ipsubnetworks 1.1.1.1/32,2.2.2.2/32 endcondition
$gam print caalevels
$gam delete caalevel "zzz_${newbase}"
@@ -731,19 +777,17 @@ jobs:
$gam user $gam_user update shareddrive "${driveid}" ou "aaaGithub Actions" # so we can delete our OU...
$gam user $gam_user delete shareddrive "${driveid}" nukefromorbit
echo "printer model count:"
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 inboundssoassignment "orgunit:${newou}"
$gam delete inboundssoprofile "id:${ssoprofile}"
$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 print printers
printerid=$($gam create printer displayname "${newbase}" uri ipp://localhost:631 driverless description "made by $(gam_user)" ou "${newou}" | grep 'id: [a-z,0-9]*$' | cut -d' ' -f3)
$gam info printer "$printerid"
$gam delete printer "$printerid"
$gam delete ou "${newou}"
#$gam delete inboundssoprofile "id:${ssoprofile}"
#$gam print printers
#$gam create printer displayname "${newbase}" uri ipp://localhost:631 driverless description "made by $(gam_user)" ou /
export CUSTOMER_ID="C01wfv983"
export GA_DOMAIN="pdl.jaylee.us"
touch $gampath/enabledasa.txt
echo "using delegated admin service account"
$gam print users
- name: Archive production artifacts
uses: actions/upload-artifact@v3
@@ -762,7 +806,7 @@ jobs:
tar cJvvf bin.tar.xz bin/
publish:
if: github.event_name == 'push'
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v')
runs-on: ubuntu-latest
needs: build
steps:

View File

@@ -911,6 +911,12 @@ gam oauth|oauth2 refresh
gam <UserTypeEntity> check serviceaccount [scope|scopes <APIScopeURLList>]
gam yubikey resetpiv [yubikeyserialnumber <Number>]
gam rotate sakey yubikey yubikey_pin yubikey_slot AUTHENTICATION yubikeyserialnumber <Number>
gam create [gcpserviceaccount|signjwtserviceaccount]
gam enable apis [auto|manual]
gam whatis <EmailItem>
<ResoldCustomerAttribute> ::=
@@ -1392,6 +1398,13 @@ gam print chromeappdevices [todrive]
[start <Date>] [end <Date>]
[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]
[ou|org|orgunit <OrgUnitItem>]
[start <Date>] [end <Date>] [recentfirst]
@@ -1582,6 +1595,7 @@ gam create inboundssoassignment (group <GroupItem> rank <Number>)|(ou|org|orguni
(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 delete inboundssoassignment <SSOAssignmentSelector>
gam info inboundssoassignment <SSOAssignmentSelector>
gam show inboundssoassignments
gam print inboundssoassignments [todrive]
@@ -1674,6 +1688,9 @@ gam show guardian|guardians [invitedguardian <EmailAddress>] [student <StudentIt
gam print guardian|guardians [todrive] [invitedguardian <EmailAddress>] [student <StudentItem>] [invitations [states <GuardianStateList>]] [<UserTypeEntity>]
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>
(accounts <EmailAddressList>) | (orgunit|ou <OrgUnitPath>) | (teamdrives <TeamDriveList>) | (rooms <ChatRoomList>) | everyone
[scope <all_data|held_data|unprocessed_data>]
@@ -1686,6 +1703,7 @@ gam delete export <MatterItem> <ExportItem>
gam info export <MatterItem> <ExportItem>
gam print exports [todrive] [matters <MatterItemList>]
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>]
[(accounts|groups|users <EmailItemList>) | (orgunit|ou <OrgUnit>)]

View File

@@ -12,7 +12,7 @@
}
},
"basePath": "",
"baseUrl": "https://www.googleapis.com/admin/directory/v1.1beta1/customer/",
"baseUrl": "https://admin.googleapis.com/admin/directory/v1.1beta1/customer/",
"batchPath": "batch",
"canonicalName": "cbcm",
"discoveryVersion": "v1",
@@ -412,7 +412,7 @@
}
},
"revision": "20201203",
"rootUrl": "https://www.googleapis.com/admin/directory/v1.1beta1/customer/",
"rootUrl": "https://admin.googleapis.com/admin/directory/v1.1beta1/customer/",
"schemas": {
"ChromeBrowser": {
"id": "ChromeBrowser",

View File

@@ -28,7 +28,7 @@ upgrade_only=false
gamversion="latest"
adminuser=""
regularuser=""
gam_glibc_vers="2.35"
gam_glibc_vers="2.31"
while getopts "hd:a:o:b:lp:u:r:v:" OPTION
do

View File

@@ -51,6 +51,7 @@ from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
import gam.auth.oauth
from gam.auth import signjwt
from gam import auth
from gam import controlflow
from gam import display
@@ -588,7 +589,7 @@ def SetGlobalVariables():
GM_Globals[GM_ENABLEDASA_TXT] = os.path.join(
GC_Values[GC_CONFIG_DIR], FN_ENABLEDASA_TXT)
if not GC_Values[GC_NO_UPDATE_CHECK]:
doGAMCheckForUpdates()
doGAMCheckForUpdates(forceCheck=0)
# domain must be set and customer_id must be set and != my_customer when enable_dasa = true
if GC_Values[GC_ENABLE_DASA]:
@@ -635,8 +636,10 @@ TIME_OFFSET_UNITS = [('day', 86400), ('hour', 3600), ('minute', 60),
def getLocalGoogleTimeOffset(testLocation='admin.googleapis.com'):
# If local time is well off, it breaks https because the server certificate
# will be seen as too old or new and thus invalid; http doesn't have that issue.
# Try with http first, if time is close (<MAX_LOCAL_GOOGLE_TIME_OFFSET seconds),
# retry with https
# retry with https as it should be OK
badhttp = transport.create_http()
for prot in ['http', 'https']:
localUTC = datetime.datetime.now(datetime.timezone.utc)
@@ -645,6 +648,12 @@ def getLocalGoogleTimeOffset(testLocation='admin.googleapis.com'):
badhttp.request(f'{prot}://' + testLocation, 'HEAD')[0]['date'])
except (httplib2.ServerNotFoundError, RuntimeError, ValueError) as e:
controlflow.system_error_exit(4, str(e))
except httplib2.socks.HTTPError as e:
# If user has specified an HTTPS proxy, the http request will probably fail as httplib2
# turns a GET into a CONNECT which is not valid for an http address
if prot == 'http':
continue
handleServerError(e)
offset = remainder = int(abs((localUTC - googleUTC).total_seconds()))
if offset < MAX_LOCAL_GOOGLE_TIME_OFFSET and prot == 'http':
continue
@@ -659,7 +668,7 @@ def getLocalGoogleTimeOffset(testLocation='admin.googleapis.com'):
return (offset, nicetime)
def doGAMCheckForUpdates(forceCheck=False):
def doGAMCheckForUpdates(forceCheck=0):
def _gamLatestVersionNotAvailable():
if forceCheck:
@@ -702,6 +711,8 @@ def doGAMCheckForUpdates(forceCheck=False):
print(
f'Version Check:\n Current: {current_version}\n Latest: {latest_version}'
)
if forceCheck < 0:
sys.exit(1 if latest_version > current_version else 0)
if latest_version <= current_version:
fileutils.write_file(GM_Globals[GM_LAST_UPDATE_CHECK_TXT],
str(now_time),
@@ -782,9 +793,9 @@ def checkConnection():
success_count = 0
for host in hosts:
try_count += 1
ip = socket.gethostbyname(host)
ip = socket.getaddrinfo(host, None)[0][-1][0] # works with ipv6
check_line = f'Checking {host} ({ip}) ({try_count}/{host_count})...'
sys.stdout.write(f'{check_line:<80}')
sys.stdout.write(f'{check_line:<100}')
sys.stdout.flush()
try:
httpc.request(f'https://{host}/', 'HEAD', headers=headers)
@@ -821,14 +832,18 @@ def checkConnection():
controlflow.system_error_exit(3, createYellowText('Some hosts failed to connect! Please follow the recommendations for those hosts to correct any issues and try again.'))
def doGAMVersion(checkForArgs=True):
force_check = extended = simple = timeOffset = False
forceCheck = 0
extended = simple = timeOffset = False
testLocation = 'admin.googleapis.com'
if checkForArgs:
i = 2
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'check':
force_check = True
forceCheck = 1
i += 1
elif myarg == 'checkrc':
forceCheck = -1
i += 1
elif myarg == 'simple':
simple = True
@@ -868,8 +883,8 @@ def doGAMVersion(checkForArgs=True):
(testLocation, nicetime))
if offset > MAX_LOCAL_GOOGLE_TIME_OFFSET:
controlflow.system_error_exit(4, 'Please fix your system time.')
if force_check:
doGAMCheckForUpdates(forceCheck=True)
if forceCheck:
doGAMCheckForUpdates(forceCheck)
if extended:
print(ssl.OPENSSL_VERSION)
libs = ['cryptography',
@@ -925,14 +940,12 @@ def _getSvcAcctData():
controlflow.system_error_exit(6, None)
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA] = json.loads(json_string)
jwt_apis = ['chat',
'cloudresourcemanager',
'accesscontextmanager'] # APIs which can handle OAuthless JWT tokens
def getSvcAcctCredentials(scopes, act_as, api=None):
def getSvcAcctCredentials(scopes, act_as, api=None, force_oauth=False):
try:
_getSvcAcctData()
sign_method = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA].get('key_type', 'default')
if act_as or api not in jwt_apis:
if act_as or force_oauth:
# DwD means we need to go about things differently...
if sign_method == 'default':
credentials = google.oauth2.service_account.Credentials.from_service_account_info(
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
@@ -940,6 +953,10 @@ def getSvcAcctCredentials(scopes, act_as, api=None):
yksigner = yubikey.YubiKey(GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
credentials = google.oauth2.service_account.Credentials._from_signer_and_info(yksigner,
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
elif sign_method == 'signjwt':
sjsigner = signjwt.SignJwt(GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
credentials = signjwt.Credentials._from_signer_and_info(sjsigner.sign,
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
credentials = credentials.with_scopes(scopes)
if act_as:
credentials = credentials.with_subject(act_as)
@@ -953,6 +970,11 @@ def getSvcAcctCredentials(scopes, act_as, api=None):
credentials = JWTCredentials._from_signer_and_info(yksigner,
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA],
audience=audience)
elif sign_method == 'signjwt':
sjsigner = signjwt.SignJwt(GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
credentials = signjwt.JWTCredentials._from_signer_and_info(sjsigner,
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA],
audience=audience)
credentials.project_id = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['project_id']
GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID] = GM_Globals[
GM_OAUTH2SERVICE_JSON_DATA]['client_id']
@@ -1078,9 +1100,10 @@ def getService(api, httpObj):
controlflow.invalid_json_exit(disc_file)
def buildGAPIObject(api):
def buildGAPIObject(api, credentials=None):
GM_Globals[GM_CURRENT_API_USER] = None
credentials = getValidOauth2TxtCredentials(api=getAPIVersion(api)[0])
if not credentials:
credentials = getValidOauth2TxtCredentials(api=getAPIVersion(api)[0])
credentials.user_agent = GAM_INFO
httpObj = transport.AuthorizedHttp(
credentials, transport.create_http(cache=GM_Globals[GM_CACHE_DIR]))
@@ -1295,7 +1318,9 @@ def doCheckServiceAccount(users):
# We are explicitly not doing DwD here, just confirming service account can auth
auth_error = ''
try:
credentials = getSvcAcctCredentials([USERINFO_EMAIL_SCOPE], None)
credentials = getSvcAcctCredentials([USERINFO_EMAIL_SCOPE],
None,
force_oauth=True)
request = transport.create_request()
credentials.refresh(request)
sa_token_info = gapi.call(oa2,
@@ -1315,12 +1340,10 @@ def doCheckServiceAccount(users):
'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:
if key_type == 'default':
print(
'Checking key age. Google recommends rotating keys on a routine basis...'
)
)
try:
iam = buildGAPIServiceObject('iam', None)
project = GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID]
@@ -1346,6 +1369,8 @@ def doCheckServiceAccount(users):
key_days = 'UNKNOWN'
print('Unable to check key age, please run "gam update project"')
printPassFail(f'Key is {key_days} days old', key_age_result)
else:
printPassFail(f'Skipping age check. {key_type} rotation not necessary.', test_pass)
if not check_scopes:
for _, scopes in list(API_SCOPE_MAPPING.items()):
for scope in scopes:
@@ -7158,6 +7183,45 @@ def getGAMProjectFile(filepath):
return c.decode(UTF8)
def enable_apis():
a_or_m = None
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower()
if myarg in ['auto', 'manual']:
a_or_m = myarg[0]
i += 1
else:
controlflow.invalid_argument_exit(sys.argv[i],
'gam enable apis')
GAMProjectAPIs = getGAMProjectFile('project-apis.txt').splitlines()
request = signjwt.get_request()
try:
_, projectId = google.auth.default(scopes=signjwt._IAM_SCOPES,
request=request)
except google.auth.exceptions.DefaultCredentialsError as e:
projectId = input('Please enter your project ID: ')
while a_or_m not in ['a', 'm']:
a_or_m = input('Do you want to enable projects [a]utomatically or [m]anually? (a/m): ').strip().lower()
if a_or_m in ['a', 'm']:
break
print('Please enter A or M....')
if a_or_m == 'a':
login_hint = _getValidateLoginHint()
_, httpObj = getCRMService(login_hint)
enableGAMProjectAPIs(GAMProjectAPIs,
httpObj,
projectId=projectId,
checkEnabled=True)
else:
chunk_size = 20
print('Using an account with project access, please use ALL of these URLs to enable 20 APIs at a time:\n\n')
for chunk in range(0, len(GAMProjectAPIs), chunk_size):
apiid = ",".join(GAMProjectAPIs[chunk:chunk+chunk_size])
url = f'https://console.cloud.google.com/apis/enableflow?apiid={apiid}&project={projectId}'
print(f' {url}\n\n')
def enableGAMProjectAPIs(GAMProjectAPIs,
httpObj,
projectId,
@@ -7364,7 +7428,7 @@ def _createClientSecretsOauth2service(httpObj, projectId, login_hint):
while True:
print(f'''Please go to:
{console_url}
{console_url}
1. Choose "Desktop App" or "Other" for "Application type".
2. Enter a desired value for "Name" or leave as is.
@@ -7403,6 +7467,24 @@ def _createClientSecretsOauth2service(httpObj, projectId, login_hint):
fileutils.write_file(GC_Values[GC_CLIENT_SECRETS_JSON],
cs_data,
continue_on_error=False)
print(f'''
Now it's important to mark the GAM Client ID as trusted by your Workspace instance.
1. Please go to:
https://admin.google.com/ac/owl/list?tab=configuredApps
2. Click on: Add app > OAuth App Name Or Client ID.
3. Enter the following Client ID value:
{client_id}
4. Search for the ID, select the GAM app, check the box and press Select.
5. Keep the default scope or select a preferred scope that includes your GAM admin.
6. Press Continue
7. Select Trusted radio button, Continue and Finish.
''')
input('Press Enter when complete.')
print('That\'s it! Your GAM Project is created and ready to use.')
@@ -7442,15 +7524,26 @@ def _getCurrentProjectID():
def _getProjects(crm, pfilter):
try:
return gapi.get_all_pages(
projects = gapi.get_all_pages(
crm.projects(),
'search',
'projects',
throw_reasons=[gapi_errors.ErrorReason.BAD_REQUEST],
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,
gapi_errors.ErrorReason.FOUR_O_THREE])]
except gapi_errors.GapiBadRequestError as e:
controlflow.system_error_exit(2, f'Project: {pfilter}, {str(e)}')
except googleapiclient.errors.HttpError:
return []
PROJECTID_PATTERN = re.compile(r'^[a-z][a-z0-9-]{4,28}[a-z0-9]$')
PROJECTID_FORMAT_REQUIRED = '[a-z][a-z0-9-]{4,28}[a-z0-9]'
@@ -7732,7 +7825,7 @@ def doUpdateProjects():
_grantRotateRights(iam, sa_email, sa_email)
def _generatePrivateKeyAndPublicCert(client_id, key_size, b64enc_pub=True):
def _generatePrivateKeyAndPublicCert(client_id, key_size, b64enc_pub=True, validity_hours=0):
print(' Generating new private key...')
private_key = rsa.generate_private_key(public_exponent=65537,
key_size=key_size,
@@ -7749,10 +7842,16 @@ def _generatePrivateKeyAndPublicCert(client_id, key_size, b64enc_pub=True):
builder = builder.issuer_name(
x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, client_id)]))
# Gooogle seems to enforce the not before date strictly. Set the not before
# date to be UTC one hour ago should cover any clock skew.
builder = builder.not_valid_before(datetime.datetime.utcnow() - datetime.timedelta(hours=1))
# Google uses 12/31/9999 date for end time
builder = builder.not_valid_after(datetime.datetime(9999, 12, 31, 23, 59))
# date to be UTC two minutes ago which should cover any clock skew.
now = datetime.datetime.utcnow()
builder = builder.not_valid_before(now - datetime.timedelta(minutes=2))
# Google defaults to 12/31/9999 date for end time if there's no
# policy to restrict key age
if validity_hours:
expires = now + datetime.timedelta(hours=validity_hours) - datetime.timedelta(minutes=2)
builder = builder.not_valid_after(expires)
else:
builder = builder.not_valid_after(datetime.datetime(9999, 12, 31, 23, 59))
builder = builder.serial_number(x509.random_serial_number())
builder = builder.public_key(public_key)
builder = builder.add_extension(x509.BasicConstraints(ca=False,
@@ -7813,12 +7912,12 @@ def doShowServiceAccountKeys():
else:
controlflow.invalid_argument_exit(myarg, 'gam show sakeys')
name = f'projects/-/serviceAccounts/{GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID]}'
currentPrivateKeyId = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA][
'private_key_id']
currentPrivateKeyId = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA].get('private_key_id')
keys = gapi.get_items(iam.projects().serviceAccounts().keys(),
'list',
'keys',
name=name,
fields='*',
keyTypes=keyTypes)
if not keys:
print('No keys')
@@ -7833,11 +7932,60 @@ def doShowServiceAccountKeys():
display.print_json(keys)
def getYubiKeySerialNumber(new_data, serial_number):
try:
new_data['yubikey_serial_number'] = int(serial_number)
except ValueError:
controlflow.system_error_exit(
3,
'yubikey_serial_number must be a number')
def doResetYubiKeyPIV():
new_data = {}
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'yubikeyserialnumber':
getYubiKeySerialNumber(new_data, sys.argv[i+1])
i += 2
else:
controlflow.invalid_argument_exit(myarg, 'gam yubikey resetpiv')
yk = yubikey.YubiKey(new_data)
yk.serial_number = yk.get_serial_number()
yk.reset_piv()
def create_signjwt_serviceaccount():
i = 3
if i < len(sys.argv):
controlflow.invalid_argument_exit(sys.argv[i], f'gam create {sys.argv[2]}')
_checkForExistingProjectFiles()
sa_info = {
'type': 'service_account',
'key_type': 'signjwt',
'token_uri': 'https://oauth2.googleapis.com/token'
}
request = signjwt.get_request()
try:
creds, sa_info['project_id'] = google.auth.default(scopes=signjwt._IAM_SCOPES,
request=request)
except google.auth.exceptions.DefaultCredentialsError as e:
controlflow.system_error_exit(2, e)
creds.refresh(request)
sa_info['client_email'] = creds.service_account_email
oa2 = buildGAPIObjectNoAuthentication('oauth2')
token_info = gapi.call(oa2, 'tokeninfo', access_token=creds.token)
sa_info['client_id'] = token_info['issued_to']
sa_output = json.dumps(sa_info, indent=4, sort_keys=True)
fileutils.write_file(GC_Values[GC_OAUTH2SERVICE_JSON],
sa_output,
continue_on_error=False)
def doCreateOrRotateServiceAccountKeys(iam=None,
project_id=None,
client_email=None,
client_id=None):
local_key_size = 2048
validity_hours = 0
mode = 'retainexisting'
body = {}
if iam:
@@ -7888,12 +8036,10 @@ def doCreateOrRotateServiceAccountKeys(iam=None,
new_data['yubikey_pin'] = input('Enter your YubiKey PIN: ')
i += 1
elif myarg == 'yubikeyserialnumber':
try:
new_data['yubikey_serial_number'] = int(sys.argv[i+1])
except ValueError:
controlflow.system_error_exit(
3,
'yubikey_serial_number must be a number')
getYubiKeySerialNumber(new_data, sys.argv[i+1])
i += 2
elif myarg == 'validityhours':
validity_hours = int(sys.argv[i + 1])
i += 2
elif myarg in ['retainnone', 'retainexisting', 'replacecurrent']:
mode = myarg
@@ -7915,7 +8061,7 @@ def doCreateOrRotateServiceAccountKeys(iam=None,
elif local_key_size:
# Generate private key locally, store in file
new_data['private_key'], publicKeyData = _generatePrivateKeyAndPublicCert(
sa_name, local_key_size)
sa_name, local_key_size, validity_hours=validity_hours)
new_data['key_type'] = 'default'
for key in list(new_data):
if key.startswith('yubikey_'):
@@ -8164,20 +8310,14 @@ def doCreateSharedDrive(users):
print(f'Created Shared Drive {body["name"]} with id {result["id"]}')
TEAMDRIVE_RESTRICTIONS_MAP = {
'adminmanagedrestrictions': 'adminManagedRestrictions',
'copyrequireswriterpermission': 'copyRequiresWriterPermission',
'domainusersonly': 'domainUsersOnly',
'teammembersonly': 'teamMembersOnly',
}
def doUpdateSharedDrive(users):
i, driveId = getSharedDriveId(5)
body = {}
useDomainAdminAccess = False
change_hide = None
orgUnit = None
_, d = buildDrive3GAPIObject(_get_admin_email())
restrictions_map = {r.lower(): r for r in d._rootDesc['schemas']['Drive']['properties']['restrictions']['properties'].keys()}
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'name':
@@ -8203,19 +8343,15 @@ def doUpdateSharedDrive(users):
elif myarg == 'asadmin':
useDomainAdminAccess = True
i += 1
# elif myarg in ['ou', 'org', 'orgunit']:
# body['orgUnitId'] = gapi_directory_orgunits.getOrgUnitId(sys.argv[i + 1])
# i += 2
elif myarg in ['hidden']:
if getBoolean(sys.argv[i+1], myarg):
change_hide = 'hide'
else:
change_hide = 'unhide'
i += 2
elif myarg in TEAMDRIVE_RESTRICTIONS_MAP:
elif myarg in restrictions_map:
body.setdefault('restrictions', {})
body['restrictions'][
TEAMDRIVE_RESTRICTIONS_MAP[myarg]] = getBoolean(
body['restrictions'][restrictions_map[myarg]] = getBoolean(
sys.argv[i + 1], myarg)
i += 2
else:
@@ -10742,11 +10878,17 @@ OAUTH2_SCOPES = [
'subscopes': ['readonly'],
'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)',
'subscopes': [],
'scopes': 'https://www.googleapis.com/auth/devstorage.read_only'
},
'name': 'Cloud Storage - Vault/Takeout Download/Copy',
'subscopes': ['readonly'],
'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)',
'subscopes': [],
@@ -10791,6 +10933,7 @@ class ScopeMenuOption():
is_required=False,
is_selected=False,
supported_restrictions=None,
restricted_scopes=None,
restriction=None):
"""A data structure for storing and toggling feature/API scope attributes.
@@ -10820,6 +10963,7 @@ class ScopeMenuOption():
self._restriction = None
self.scopes = oauth_scopes
self.restricted_scopes = restricted_scopes
self.description = description
self.is_required = is_required
# Required scopes must be selected
@@ -10910,7 +11054,10 @@ class ScopeMenuOption():
effective_scopes = []
for scope in self.scopes:
if self.is_restricted:
scope = f'{scope}.{self._restriction}'
if self.restricted_scopes.get(self._restriction):
scope = self.restricted_scopes.get(self._restriction)
else:
scope = f'{scope}.{self._restriction}'
effective_scopes.append(scope)
return effective_scopes
@@ -10922,7 +11069,10 @@ class ScopeMenuOption():
name: Some description of the API/feature.
subscopes: A list of compatible scope restrictions such as 'action' or
'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.
offByDefault: A bool indicating whether this feature/scope should be off
by default (when no prior selection has been made). Default is False
@@ -10951,8 +11101,8 @@ class ScopeMenuOption():
description=scope_definition.get('name'),
is_selected=not scope_definition.get('offByDefault'),
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():
"""A text menu which prompts the user to select the scopes to authorize."""
@@ -11549,6 +11699,8 @@ def ProcessGAMCommand(args):
gapi_chat.create_message()
elif argument in ['caalevel']:
gapi_caa.create_access_level()
elif argument in ['gcpserviceaccount', 'signjwtserviceaccount']:
create_signjwt_serviceaccount()
else:
controlflow.invalid_argument_exit(argument, 'gam create')
sys.exit(0)
@@ -11723,6 +11875,8 @@ def ProcessGAMCommand(args):
gapi_cloudidentity_inboundsso.delete_profile()
elif argument in ['inboundssocredential', 'inboundssocredentials']:
gapi_cloudidentity_inboundsso.delete_credentials()
elif argument in ['inboundssoassignment', 'inboundssoassignments']:
gapi_cloudidentity_inboundsso.delete_assignment()
elif argument == 'resource':
gapi_directory_resource.deleteResourceCalendar()
elif argument == 'mobile':
@@ -11787,6 +11941,8 @@ def ProcessGAMCommand(args):
argument = sys.argv[2].lower()
if argument in ['browsertoken', 'browserokens']:
gapi_cbcm.revoketoken()
else:
controlflow.invalid_argument_exit(argument, 'gam revoke')
sys.exit(0)
elif command in ['close', 'reopen']:
# close and reopen will have to be split apart if either takes a new argument
@@ -11888,6 +12044,10 @@ def ProcessGAMCommand(args):
gapi_chromemanagement.printApps()
elif argument in ['chromeappdevices']:
gapi_chromemanagement.printAppDevices()
elif argument in ['chromeaues']:
gapi_chromemanagement.printAUEs()
elif argument in ['chromeneedsattn']:
gapi_chromemanagement.printNeedsAttn()
elif argument in ['chromeversions']:
gapi_chromemanagement.printVersions()
elif argument in ['chromehistory']:
@@ -11947,6 +12107,8 @@ def ProcessGAMCommand(args):
argument = sys.argv[2].lower()
if argument in ['browser', 'browsers']:
gapi_cbcm.move()
else:
controlflow.invalid_argument_exit(argument, 'gam move')
sys.exit(0)
elif command in ['oauth', 'oauth2']:
argument = sys.argv[2].lower()
@@ -12018,11 +12180,21 @@ def ProcessGAMCommand(args):
argument = sys.argv[2].lower()
if argument in ['export', 'vaultexport']:
gapi_vault.downloadExport()
elif argument in ['storagebucket']:
elif argument in ['storagebucket', 'bucket']:
gapi_storage.download_bucket()
else:
controlflow.invalid_argument_exit(argument, 'gam download')
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':
argument = sys.argv[2].lower()
if argument in ['sakey', 'sakeys']:
@@ -12034,6 +12206,8 @@ def ProcessGAMCommand(args):
argument = sys.argv[2].lower()
if argument in ['isinvitable', 'userinvitation', 'userinvitations']:
gapi_cloudidentity_userinvitations.check()
else:
controlflow.invalid_argument_exit(argument, 'gam check')
sys.exit(0)
elif command in ['cancelwipe', 'wipe', 'approve', 'block', 'sync']:
target = sys.argv[2].lower().replace('_', '')
@@ -12053,6 +12227,8 @@ def ProcessGAMCommand(args):
gapi_cloudidentity_devices.approve_user()
elif command == 'block':
gapi_cloudidentity_devices.block_user()
else:
controlflow.invalid_argument_exit(target, f'gam {command}')
sys.exit(0)
elif command in ['issuecommand', 'getcommand']:
target = sys.argv[2].lower().replace('_', '')
@@ -12061,13 +12237,22 @@ def ProcessGAMCommand(args):
gapi_directory_cros.issue_command()
elif command == 'getcommand':
gapi_directory_cros.get_command()
else:
controlflow.invalid_argument_exit(target, f'gam {command}')
sys.exit(0)
elif command in ['yubikey']:
action = sys.argv[2].lower().replace('_', '')
if action == 'resetpiv':
yk = yubikey.YubiKey()
yk.serial_number = yk.get_serial_number()
yk.reset_piv()
doResetYubiKeyPIV()
else:
controlflow.invalid_argument_exit(action, f'gam yubikey')
sys.exit(0)
elif command == 'enable':
enable_what = sys.argv[2].lower().replace('_', '')
if enable_what in ['api', 'apis']:
enable_apis()
else:
controlflow.invalid_argument_exit(enable_what, 'gam enable')
sys.exit(0)
users = getUsersToModify()
command = sys.argv[3].lower()

View File

@@ -22,25 +22,33 @@ For more information, see https://jaylee.us/gam
"""
import sys
from multiprocessing import freeze_support
from multiprocessing import set_start_method
from gam import controlflow
import gam
# Note that this file (and only this file) should remain compatible
# with older Python versions so we can return a meaningful error
# instead of a syntax error.
def main():
required_ver = (3, 8, 0)
if sys.version_info[:3] < required_ver:
err_result = ('ERROR: GAM requires Python %s.%s.%s or newer. You are '
'running %s.%s.%s. Please upgrade your Python version '
'or use one of the binary GAM downloads.\n' %
(required_ver[0],
required_ver[1],
required_ver[2],
sys.version_info[0],
sys.version_info[1],
sys.version_info[2]))
sys.stderr.write(err_result)
sys.exit(5)
from multiprocessing import freeze_support
freeze_support()
if sys.platform == 'darwin':
# https://bugs.python.org/issue33725 in Python 3.8.0 seems
# to break parallel operations with errors about extra -b
# command line arguments
from multiprocessing import set_start_method
set_start_method('fork')
if sys.version_info[0] < 3 or sys.version_info[1] < 7:
controlflow.system_error_exit(
5,
f'GAM requires Python 3.7 or newer. You are running %s.%s.%s. Please upgrade your Python version or use one of the binary GAM downloads.'
% sys.version_info[:3])
import gam
sys.exit(gam.ProcessGAMCommand(sys.argv))

View File

@@ -9,6 +9,7 @@ import gam
from gam import utils
from gam.auth import oauth
from gam.auth import signjwt
from gam.var import _FN_OAUTH2_TXT
from gam.var import _FN_OAUTH2SERVICE_JSON
from gam.var import GC_OAUTH2_TXT
@@ -28,9 +29,11 @@ def get_admin_credentials_filename():
# some custom name in it. Otherwise, just use the default name.
if GC_Values[GC_ENABLE_DASA]:
return GC_Values[GC_OAUTH2SERVICE_JSON] if GC_Values[GC_OAUTH2SERVICE_JSON] else _FN_OAUTH2SERVICE_JSON
else:
return GC_Values[GC_OAUTH2_TXT] if GC_Values[GC_OAUTH2_TXT] else _FN_OAUTH2_TXT
return GC_Values[GC_OAUTH2_TXT] if GC_Values[GC_OAUTH2_TXT] else _FN_OAUTH2_TXT
APIS_NEEDING_ACCESS_TOKEN = {
'cbcm': ['https://www.googleapis.com/auth/admin.directory.device.chromebrowsers']
}
def get_admin_credentials(api=None):
"""Gets oauth.Credentials that are authenticated as the domain's admin user."""
@@ -40,17 +43,27 @@ def get_admin_credentials(api=None):
with open(credential_file) as f:
creds_data = json.load(f)
# Validate that enable DASA matches content of authorization file
if GC_Values[GC_ENABLE_DASA] and 'private_key_id' in creds_data:
if GC_Values[GC_ENABLE_DASA] and creds_data.get('type') == 'service_account':
if api in APIS_NEEDING_ACCESS_TOKEN:
return gam.getSvcAcctCredentials(scopes=APIS_NEEDING_ACCESS_TOKEN[api],
act_as=None,
api=None,
force_oauth=True)
audience = f'https://{api}.googleapis.com/'
key_type = creds_data.get('key_type', 'default')
if key_type == 'default':
return JWTCredentials.from_service_account_info(creds_data,
audience=audience)
elif key_type == 'yubikey':
if key_type == 'yubikey':
yksigner = yubikey.YubiKey(creds_data)
return JWTCredentials._from_signer_and_info(yksigner,
creds_data,
audience=audience)
if key_type == 'signjwt':
sjsigner = signjwt.SignJwt(creds_data)
return signjwt.JWTCredentials._from_signer_and_info(sjsigner,
creds_data,
audience=audience)
elif not GC_Values[GC_ENABLE_DASA] and 'token' in creds_data:
return oauth.Credentials.from_credentials_file(credential_file)
else:

View File

@@ -50,6 +50,7 @@ MESSAGE_LOCAL_SERVER_SUCCESS = ('The authentication flow has completed. You may'
' close this browser window and return to GAM.')
MESSAGE_AUTHENTICATION_COMPLETE = ('\nThe authentication flow has completed.\n')
MESSAGE_AUTHENTICATION_FAILED = ('\nThe authentication flow failed, reissue command')
class CredentialsError(Exception):
@@ -629,15 +630,22 @@ class _ShortURLFlow(google_auth_oauthlib.flow.InstalledAppFlow):
print(MESSAGE_CONSOLE_AUTHORIZATION_PROMPT.format(url=d['auth_url']))
user_input.start()
userInput = False
while True:
alive = 2
while alive > 0:
sleep(0.1)
if not http_client.is_alive():
user_input.terminate()
break
elif not user_input.is_alive():
if 'code' in d:
user_input.terminate()
break
alive -= 1
if not user_input.is_alive():
userInput = True
http_client.terminate()
break
if 'code' in d:
http_client.terminate()
break
alive -= 1
if 'code' not in d:
controlflow.system_error_exit(8, MESSAGE_AUTHENTICATION_FAILED)
while True:
code = d['code']
if code.startswith('http'):

98
src/gam/auth/signjwt.py Normal file
View File

@@ -0,0 +1,98 @@
''' Use Google Application Default Credentials '''
import datetime
import json
import google.auth
from google.auth._helpers import datetime_to_secs, scopes_to_string, utcnow
import google.oauth2.service_account
import gam
from gam import controlflow
from gam import gapi
from gam import transport
from gam.var import GM_Globals, GM_CACHE_DIR
_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
_GOOGLE_OAUTH2_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token"
_IAM_SCOPES = ['https://www.googleapis.com/auth/iam']
# Some Workforce Identity Federation endpoints such as GitHub Actions
# only allow TLS 1.2 as of April 2023.
def get_request():
httpc = transport.create_http(override_min_tls='TLSv1_2')
return transport.create_request(httpc)
class JWTCredentials(google.auth.jwt.Credentials):
''' Class used for DASA '''
def _make_jwt(self):
now = utcnow()
lifetime = datetime.timedelta(seconds=self._token_lifetime)
expiry = now + lifetime
payload = {
"iss": self._issuer,
"sub": self._subject,
"iat": datetime_to_secs(now),
"exp": datetime_to_secs(expiry),
}
if self._audience:
payload["aud"] = self._audience
payload.update(self._additional_claims)
jwt = self._signer.sign(payload)
return jwt, expiry
class Credentials(google.oauth2.service_account.Credentials):
''' Class used for DwD '''
def _make_authorization_grant_assertion(self):
now = utcnow()
lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS)
expiry = now + lifetime
payload = {
"iat": datetime_to_secs(now),
"exp": datetime_to_secs(expiry),
"iss": self._service_account_email,
"aud": _GOOGLE_OAUTH2_TOKEN_ENDPOINT,
"scope": scopes_to_string(self._scopes or ()),
}
payload.update(self._additional_claims)
# The subject can be a user email for domain-wide delegation.
if self._subject:
payload.setdefault("sub", self._subject)
token = self._signer(payload)
return token
class SignJwt(google.auth.crypt.Signer):
''' Signer class for SignJWT '''
def __init__(self, service_account_info):
self.service_account_email = service_account_info['client_email']
self.name = f'projects/-/serviceAccounts/{self.service_account_email}'
self._key_id = None
@property # type: ignore
def key_id(self):
return self._key_id
def sign(self, message):
''' Call IAM Credentials SignJWT API to get our signed JWT '''
request = get_request()
try:
credentials, _ = google.auth.default(scopes=_IAM_SCOPES,
request=request)
except google.auth.exceptions.DefaultCredentialsError as e:
controlflow.system_error_exit(2, e)
httpObj = transport.AuthorizedHttp(
credentials,
transport.create_http(cache=GM_Globals[GM_CACHE_DIR]))
iamc = gam.getService('iamcredentials', httpObj)
response = gapi.call(iamc.projects().serviceAccounts(),
'signJwt',
name=self.name,
body={'payload': json.dumps(message)})
signed_jwt = response.get('signedJwt')
return signed_jwt

View File

@@ -87,6 +87,9 @@ class YubiKey():
def get_serial_number(self):
try:
devices = list_all_devices()
if not devices:
msg = f'Could not find any YubiKey'
controlflow.system_error_exit(3, msg)
if self.serial_number:
for (device, info) in devices:
if info.serial == self.serial_number:

View File

@@ -219,7 +219,7 @@ def printShowCrosTelemetry(mode):
i = 3
if mode == 'info':
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]}'
i += 1
mode = 'show'
@@ -307,6 +307,97 @@ def printShowCrosTelemetry(mode):
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 = [
'version', 'count', 'channel', 'deviceOsVersion', 'system'
]
@@ -320,6 +411,7 @@ def printVersions():
startDate = None
endDate = None
pfilter = None
query = None
reverse = False
i = 3
while i < len(sys.argv):
@@ -350,12 +442,18 @@ def printVersions():
else:
pfilter = ''
pfilter += f'last_active_date>={startDate}'
query = pfilter
if orgunit:
orgUnitPath = gapi_directory_orgunits.orgunit_from_orgunitid(orgunit, None)
if query:
query += ' AND '
else:
query = ''
query += f'orgUnitPath={orgUnitPath}'
titles.append('orgUnitPath')
else:
orgUnitPath = '/'
gam.printGettingAllItems('Chrome Versions', pfilter)
gam.printGettingAllItems('Chrome Versions', query)
page_message = gapi.got_total_items_msg('Chrome Versions', '...\n')
versions = gapi.get_all_pages(cm.customers().reports(),
'countChromeVersions',

View File

@@ -408,7 +408,7 @@ def update_policy():
f'{expected_enums}, got {value}'
controlflow.system_error_exit(8, msg)
elif vtype in ['TYPE_LIST']:
value = value.split(',')
value = value.split(',') if value else []
if myarg == 'chrome.users.chromebrowserupdates' and \
cased_field == 'targetVersionPrefixSetting':
mg = re.compile(r'^([a-z]+)-(\d+)$').match(value)

View File

@@ -35,7 +35,7 @@ def get_orgunit_id(orgunit):
'''build Cloud Identity API'''
def build():
return gapi_cloudidentity.build('cloudidentity_beta')
return gapi_cloudidentity.build('cloudidentity')
'''parse cmd for profile create/update'''
@@ -484,6 +484,16 @@ def update_assignment():
controlflow.system_error_exit(3, 'Update did not finish {result}')
'''gam delete inboundssoassignment'''
def delete_assignment():
ci = build()
assignment = assignment_by_target(sys.argv[3], ci).get('name')
print(f'Deleting Inbound SSO Assignmnet {assignment}...')
gapi.call(ci.inboundSsoAssignments(),
'delete',
name=assignment)
'''gam info inboundssoassignment'''
def info_assignment():
ci = build()

View File

@@ -2,20 +2,150 @@ import base64
import os
import re
import sys
import time
import googleapiclient
from pathvalidate import sanitize_filepath
import gam
from gam.gapi import errors as gapi_errors
from gam.var import *
from gam import controlflow
from gam import fileutils
from gam import gapi
from gam import utils
def build_gapi():
def build():
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,
bucket,
object_,
@@ -23,11 +153,12 @@ def get_cloud_storage_object(s,
expectedMd5=None):
if not local_file:
local_file = object_
local_file = sanitize_filepath(local_file, platform='auto')
if os.path.exists(local_file):
sys.stdout.write(' File already exists. ')
sys.stdout.write(f'File {local_file} already exists.')
sys.stdout.flush()
if expectedMd5:
sys.stdout.write(f'Verifying {expectedMd5} hash...')
sys.stdout.write(f' verifying {expectedMd5} hash...')
sys.stdout.flush()
if utils.md5_matches_file(local_file, expectedMd5, False):
print('VERIFIED')
@@ -35,7 +166,7 @@ def get_cloud_storage_object(s,
print('not verified. Downloading again and over-writing...')
else:
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_)
file_path = os.path.dirname(local_file)
if not os.path.exists(file_path):
@@ -60,7 +191,7 @@ def get_cloud_storage_object(s,
def download_bucket():
bucket = sys.argv[3]
s = build_gapi()
s = build()
page_message = gapi.got_total_items_msg('Files', '...')
fields = 'nextPageToken,items(name,id,md5Hash)'
objects = gapi.get_all_pages(s.objects(),

View File

@@ -1,3 +1,4 @@
from base64 import b64encode
import datetime
import json
import sys
@@ -519,7 +520,6 @@ def getHoldInfo():
def convertExportNameToID(v, nameOrID, matterId):
nameOrID = nameOrID.lower()
cg = UID_PATTERN.match(nameOrID)
if cg:
return cg.group(1)
@@ -530,7 +530,7 @@ def convertExportNameToID(v, nameOrID, matterId):
matterId=matterId,
fields=fields)
for export in exports:
if export['name'].lower() == nameOrID:
if export['name'].lower() == nameOrID.lower():
return export['id']
controlflow.system_error_exit(
4, f'could not find export name {nameOrID} '
@@ -797,11 +797,49 @@ def getMatterInfo():
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():
verifyFiles = True
extractFiles = True
v = buildGAPIObject()
s = gapi_storage.build_gapi()
s = gapi_storage.build()
matterId = getMatterItem(v, sys.argv[3])
exportId = convertExportNameToID(v, sys.argv[4], matterId)
targetFolder = GC_Values[GC_DRIVE_DIR]
@@ -829,24 +867,17 @@ def downloadExport():
for s_file in export['cloudStorageSink']['files']:
bucket = s_file['bucketName']
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:
expected_hash = s_file['md5Hash']
sys.stdout.write(f' Verifying file hash is {expected_hash}...')
sys.stdout.flush()
utils.md5_matches_file(filename, expected_hash, True)
print('VERIFIED')
else:
expected_hash = None
local_file = s_object.replace('/', '-').replace(':', '-')
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):
gam.extract_nested_zip(filename, targetFolder)

View File

@@ -9,6 +9,8 @@ from gam.var import GC_TLS_MAX_VERSION
from gam.var import GC_TLS_MIN_VERSION
from gam.var import GC_Values
# Bump default retries
#httplib2.RETRIES = 5
def create_http(cache=None,
timeout=None,

View File

@@ -8,7 +8,7 @@ import platform
import re
GAM_AUTHOR = 'Jay Lee <jay0lee@gmail.com>'
GAM_VERSION = '6.31'
GAM_VERSION = '6.54'
GAM_LICENSE = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
GAM_URL = 'https://jaylee.us/gam'
@@ -62,6 +62,21 @@ SKUS = {
'aliases': ['cloudsearch'],
'displayName': 'Google Cloud Search',
},
'1010380001': {
'product': '101038',
'aliases': ['appsheetcore'],
'displayName': 'AppSheet Core',
},
'1010380002': {
'product': '101038',
'aliases': ['appsheetstandard', 'appsheetenterprisestandard'],
'displayName': 'AppSheet Enterprise Standard',
},
'1010380003': {
'product': '101038',
'aliases': ['appsheetplus', 'appsheetenterpriseplus'],
'displayName': 'AppSheet Enterprise Plus',
},
'1010310002': {
'product': '101031',
'aliases': ['gsefe', 'e4e', 'gsuiteenterpriseeducation'],
@@ -300,6 +315,7 @@ PRODUCTID_NAME_MAPPINGS = {
'101035': 'Cloud Search',
'101036': 'Google Meet Global Dialing',
'101037': 'G Suite Workspace for Education',
'101038': 'AppSheet',
'101039': 'Assured Controls',
'101040': 'Beyond Corp',
'Google-Apps': 'Google Workspace',
@@ -641,7 +657,7 @@ GOOGLEDOC_VALID_EXTENSIONS_MAP = {
'.docx', '.html', '.odt', '.pdf', '.rtf', '.txt', '.zip'
],
MIMETYPE_GA_PRESENTATION: ['.pdf', '.pptx', '.odp', '.txt'],
MIMETYPE_GA_SPREADSHEET: ['.csv', '.ods', '.pdf', '.xlsx', '.zip'],
MIMETYPE_GA_SPREADSHEET: ['.csv', '.ods', '.pdf', '.tsv', '.xlsx', '.zip'],
}
MACOS_CODENAMES = {
@@ -984,6 +1000,8 @@ CROS_ARGUMENT_TO_PROPERTY_MAP = {
'autoupdateexpiration': ['autoUpdateExpiration',],
'bootmode': ['bootMode',],
'cpustatusreports': ['cpuStatusReports',],
'deprovisionreason': ['deprovisionReason',],
'lastDeprovisionTimestamp': ['lastDeprovisionTimestamp',],
'devicefiles': ['deviceFiles',],
'deviceid': ['deviceId',],
'dockmacaddress': ['dockMacAddress',],
@@ -991,6 +1009,7 @@ CROS_ARGUMENT_TO_PROPERTY_MAP = {
'ethernetmacaddress': ['ethernetMacAddress',],
'ethernetmacaddress0': ['ethernetMacAddress0',],
'firmwareversion': ['firmwareVersion',],
'firstenrollmenttime': ['firstEnrollmentTime',],
'lastenrollmenttime': ['lastEnrollmentTime',],
'lastknownnetwork': ['lastKnownNetwork'],
'lastsync': ['lastSync',],
@@ -1048,7 +1067,10 @@ CROS_SCALAR_PROPERTY_PRINT_ORDER = [
'ethernetMacAddress0',
'macAddress',
'systemRamTotal',
'firstEnrollmentTime',
'lastEnrollmentTime',
'deprovisionReason',
'lastDeprovisionTimestamp',
'orderNumber',
'manufactureDate',
'supportEndDate',
@@ -1325,7 +1347,7 @@ GC_TLS_MAX_VERSION = 'tls_max_ver'
# Path to certificate authority file for validating TLS hosts
GC_CA_FILE = 'ca_file'
TLS_MIN = 'TLSv1_3' if hasattr(ssl.SSLContext(), 'minimum_version') else None
TLS_MIN = 'TLSv1_3'
GC_Defaults = {
GC_ADMIN_EMAIL: '',
GC_AUTO_BATCH_MIN: 0,

View File

@@ -6,7 +6,6 @@ google-auth-httplib2
google-auth-oauthlib>=0.4.1
google-auth>=2.3.2
httplib2>=0.17.0
importlib.metadata; python_version < '3.8'
passlib>=1.7.2
pathvalidate
python-dateutil

View File

@@ -13,15 +13,15 @@ keywords = google, oauth2, gsuite, google-apps, google-admin-sdk, google-drive,
classifiers =
Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
License :: OSI Approved :: Apache License
[options]
packages = find:
python_requires = >= 3.7
python_requires = >= 3.8
install_requires =
cryptography
distro; sys_platform == 'linux'
@@ -31,11 +31,10 @@ install_requires =
google-auth-oauthlib >= 0.4.6
google-auth >= 2.3.3
httplib2 >= 0.20.2
importlib.metadata; python_version < '3.8'
passlib >= 1.7.4
python-dateutil
yubikey-manager >= 4.0.0
pathvalidate
python-dateutil
yubikey-manager >= 5.0
[options.package_data]
* = *.pem