mirror of
https://github.com/GAM-team/GAM.git
synced 2026-06-04 14:21:39 +00:00
Compare commits
125 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eef2b95948 | ||
|
|
7012bef28d | ||
|
|
b3b44d144e | ||
|
|
841eba79a3 | ||
|
|
77234f9e3d | ||
|
|
14478d7831 | ||
|
|
50aa7d937e | ||
|
|
2c7e01e003 | ||
|
|
a6ce5f04aa | ||
|
|
8bc6814b42 | ||
|
|
024177b0c7 | ||
|
|
b7faa0acae | ||
|
|
0dbdbc7a13 | ||
|
|
08271e60bf | ||
|
|
ec74698001 | ||
|
|
6cecacd334 | ||
|
|
c3d27900e1 | ||
|
|
f10df3607f | ||
|
|
416be24722 | ||
|
|
e53b4a2285 | ||
|
|
a88320b1b2 | ||
|
|
76f9a144ac | ||
|
|
a673772cc1 | ||
|
|
9e6d8195eb | ||
|
|
91d97c4a2c | ||
|
|
5e1df9263b | ||
|
|
e54921ad71 | ||
|
|
1b8d0877f3 | ||
|
|
a4e962560c | ||
|
|
be7d3ceb15 | ||
|
|
1e652d5725 | ||
|
|
1e7e5422be | ||
|
|
723e9e2bb1 | ||
|
|
1f572cc95b | ||
|
|
fb63eea4a0 | ||
|
|
7efb37010d | ||
|
|
6372af8d8a | ||
|
|
0b823ea43e | ||
|
|
cebb92199f | ||
|
|
6deabf8a66 | ||
|
|
5de74a51e0 | ||
|
|
85d6305874 | ||
|
|
30d685a6f7 | ||
|
|
fcc8a58839 | ||
|
|
5a608a9b62 | ||
|
|
eb9c127a10 | ||
|
|
ed55690ff3 | ||
|
|
502afa5213 | ||
|
|
24185d66ce | ||
|
|
181ba65c63 | ||
|
|
702f36a529 | ||
|
|
e2f73bf858 | ||
|
|
7265e8c6f4 | ||
|
|
b8b9808e94 | ||
|
|
7639773c40 | ||
|
|
6ab7370149 | ||
|
|
73994fe603 | ||
|
|
3fa646723d | ||
|
|
eb08b1fbdc | ||
|
|
93ac820005 | ||
|
|
c100e25ab9 | ||
|
|
716489ceed | ||
|
|
07d5f5e52c | ||
|
|
b889debd5e | ||
|
|
b273fe1f68 | ||
|
|
376cd6e83f | ||
|
|
e8cb1a7b9f | ||
|
|
9f0c5beae7 | ||
|
|
0ea2f16322 | ||
|
|
13ca2e8d93 | ||
|
|
3833256c8c | ||
|
|
30521612b2 | ||
|
|
d069cfc309 | ||
|
|
27461b067a | ||
|
|
017712742b | ||
|
|
afce21a1bd | ||
|
|
030e2e270f | ||
|
|
c69a86b535 | ||
|
|
b64e4cf3dc | ||
|
|
a2e06adbbe | ||
|
|
43b3397541 | ||
|
|
bd0bb1542c | ||
|
|
a92a07f9c0 | ||
|
|
42ed5509ee | ||
|
|
a6582503f2 | ||
|
|
7aecb889d2 | ||
|
|
c273f87cc7 | ||
|
|
76d00c993a | ||
|
|
013b47e6e7 | ||
|
|
9f1e9934ff | ||
|
|
48b218bd9c | ||
|
|
af5baa4f3a | ||
|
|
a2cf38d904 | ||
|
|
185522d943 | ||
|
|
a42e4dd080 | ||
|
|
3a5486889f | ||
|
|
1a1f100902 | ||
|
|
c67b214298 | ||
|
|
3ad1d5c661 | ||
|
|
13400d9bde | ||
|
|
048e8dfef5 | ||
|
|
aaf7a89192 | ||
|
|
e3ee9135ff | ||
|
|
a774fc0beb | ||
|
|
f3429bd537 | ||
|
|
37876acfda | ||
|
|
2a6dd0d1a2 | ||
|
|
b0626dd37a | ||
|
|
ed0ed8d7fc | ||
|
|
d67d999930 | ||
|
|
ac79cff6b9 | ||
|
|
50aadc6ea7 | ||
|
|
9036d114ed | ||
|
|
75c19104ae | ||
|
|
d9b7f88287 | ||
|
|
ae28c09560 | ||
|
|
6ffc738a51 | ||
|
|
82dcc4de6a | ||
|
|
f7a426f65a | ||
|
|
a94ef78066 | ||
|
|
62d738f5c2 | ||
|
|
1c56a0a608 | ||
|
|
dc3976bdda | ||
|
|
454778b190 | ||
|
|
5e78c93b71 |
176
.github/workflows/build.yml
vendored
176
.github/workflows/build.yml
vendored
@@ -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:
|
||||
|
||||
@@ -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>)]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
98
src/gam/auth/signjwt.py
Normal 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
|
||||
@@ -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:
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user