Compare commits

...

100 Commits

Author SHA1 Message Date
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
Jay Lee
3aefe21f16 Update build.yml 2023-01-13 11:54:03 -05:00
Jay Lee
0fc7958ccc Update build.yml 2023-01-13 11:14:35 -05:00
Jay Lee
13dc4e74c9 Update build.yml 2023-01-12 11:11:07 -05:00
Jay Lee
a17fa16841 Update var.py 2022-12-28 19:08:01 -05:00
Jay Lee
b13757f5d3 Update var.py 2022-12-28 18:49:37 -05:00
Jay Lee
b9df6f4762 Assured controls SKU 2022-12-28 18:44:20 -05:00
Jay Lee
b7d1c62486 [no ci] let other jobs keep building 2022-12-24 14:29:36 -05:00
Jay Lee
90e5f1b665 use search endpoint when cigroup query is specified 2022-12-18 17:19:39 +00:00
Jay Lee
3132fd7783 another fix for show holds 2022-12-17 23:10:58 +00:00
Jay Lee
87808902e6 cat and allow closed matters on show holds 2022-12-17 22:56:12 +00:00
Jay Lee
fb33d8186e fix Win64 PyInstaller 2022-12-17 18:10:53 +00:00
Jay Lee
8bd2e7f879 Upgrade yubkey for 5.0 release. Fixes #1587 2022-12-17 16:28:41 +00:00
Jay Lee
e66744e3f1 Update build.yml 2022-12-07 10:24:14 -05:00
Ross Scroggs
85f2979313 Fix Chrome schema enum processing (#1582)
Prefix should not include anything after ENUM_
```
        name: RollbackToTargetVersionEnum
          value:
            name: ROLLBACK_TO_TARGET_VERSION_ENUM_ROLLBACK_DISABLED
              number: 1
            name: ROLLBACK_TO_TARGET_VERSION_ENUM_ROLLBACK_AND_RESTORE_IF_POSSIBLE
              number: 3
```
2022-12-02 21:51:39 -05:00
Jay Lee
a85ee9b108 Update build.yml 2022-12-02 12:20:18 -05:00
Jay Lee
9ab2f38436 Update build.yml 2022-12-02 09:57:21 -05:00
Jay Lee
5bcdca4fcc Update build.yml 2022-12-02 09:48:53 -05:00
Jay Lee
729edb65be Ubuntu 20.04 so less users need legacy build 2022-12-01 16:25:38 -05:00
Jay Lee
db8afb769b Update build.yml 2022-12-01 14:09:56 -05:00
Jay Lee
7dfc93892c Update build.yml 2022-12-01 13:47:09 -05:00
Jay Lee
d278cb6939 Update build.yml 2022-12-01 13:14:59 -05:00
Jay Lee
bced5172d2 single spec for one file/folder 2022-12-01 18:05:55 +00:00
Jay Lee
bb5beb66a7 Update build.yml 2022-12-01 12:09:56 -05:00
Jay Lee
f849b6ddb7 --noconfirm to overwrite existing gam folder 2022-11-30 21:12:41 +00:00
Jay Lee
d2733a53a2 fix gampath 2022-11-30 21:04:36 +00:00
Jay Lee
1b1ae44f5d Merge branch 'main' of github.com:GAM-team/GAM 2022-11-30 20:51:53 +00:00
Jay Lee
8515dc2616 Switch to PyInstaller onedir for better performance 2022-11-30 20:51:31 +00:00
Jay Lee
ba7a8d8937 Update build.yml 2022-11-30 09:51:49 -05:00
Jay Lee
d543fb9917 Update build.yml 2022-11-30 09:16:22 -05:00
Ross Scroggs
f4d390b77b Use returnnameonly, it's like returnidonly in create drivefile (#1579)
* Document nameonly in create/update inboundssoprofile

* Use returnnameonly, it's like returnidonly in create drivefile
2022-11-30 09:15:36 -05:00
Jay Lee
ffbce1fd25 no ~~ 2022-11-29 15:01:55 +00:00
Jay Lee
2d78ec6edd merge 2022-11-29 14:48:07 +00:00
Jay Lee
9cacdd166f name_only argument for ssoprofile 2022-11-29 14:43:25 +00:00
Ross Scroggs
9af0a5d843 Code fix, consistency preference (#1578)
* Code fix, consistency preference

* Code cleanup

* Code cleanup for sso assignments

* Fix typo

* Shorten lines
2022-11-22 07:08:15 -05:00
Jay Lee
3313295532 Test with user displayname 2022-11-21 23:50:30 +00:00
Jay Lee
fdf6c147dc fix temperature in chrome telemetry 2022-11-21 23:30:21 +00:00
Jay Lee
323dbd5ca9 Allow setting displayName for users 2022-11-21 23:21:45 +00:00
Jay Lee
d01fd74fa3 merge 2022-11-18 16:08:13 +00:00
Jay Lee
8c33b88e3e Inbound SSO improvements 2022-11-18 16:06:14 +00:00
Jay Lee
5d11397fca Update build.yml 2022-11-16 16:05:11 -05:00
Jay Lee
995321978f Update build.yml 2022-11-16 15:58:56 -05:00
Jay Lee
448789dad0 Update build.yml 2022-11-16 15:39:41 -05:00
Jay Lee
e9ba6819ba Update vault.py 2022-11-16 15:31:07 -05:00
Jay Lee
3056c7b803 Update vault.py 2022-11-16 15:23:53 -05:00
Jay Lee
f2c28fd1f7 Update vault.py 2022-11-16 15:20:49 -05:00
Jay Lee
11e4ff1eb5 Update __init__.py 2022-11-16 15:03:23 -05:00
Jay Lee
81cd74c244 Update build.yml 2022-11-16 13:08:44 -05:00
Jay Lee
faade7c057 Update build.yml 2022-11-16 12:47:49 -05:00
Jay Lee
0032066e1d Update build.yml 2022-11-16 12:33:17 -05:00
Jay Lee
dd938baced Update build.yml 2022-11-16 12:12:01 -05:00
Jay Lee
b835b6ee36 Update build.yml 2022-11-16 11:04:06 -05:00
Jay Lee
3660d65df6 Update build.yml 2022-11-16 10:52:19 -05:00
Ross Scroggs
3e0b4125e0 Code fixup (#1577) 2022-11-16 09:30:17 -05:00
Ross Scroggs
9820a3d81e Inbound SSO documentation; org is synonym for ou and orgunit (#1576)
Are `gam info inboundssoassignment` and `gam delete inboundssoassignment` coming?

Is `gam info inboundssocredentials` coming?
2022-11-15 07:07:04 -05:00
Jay Lee
b670a4cee6 Update build.yml 2022-11-14 21:40:53 -05:00
Jay Lee
a5dd5275c8 Update build.yml 2022-11-14 20:57:35 -05:00
Jay Lee
9b6ad2fa60 prepare 6.31 2022-11-15 01:32:22 +00:00
Jay Lee
1d80028c93 Update build.yml 2022-11-14 20:21:38 -05:00
Jay Lee
a013e95fcf Windows actions doesn\'t like an argument that has / as first char 2022-11-15 00:40:43 +00:00
Jay Lee
eb4d6ece3f Update build.yml 2022-11-14 18:53:37 -05:00
Jay Lee
a50d1ef456 new credentials with inbound sso scope 2022-11-14 17:50:38 -05:00
Jay Lee
c179ed732c login and logout, not signin signout 2022-11-14 21:21:32 +00:00
Jay Lee
a85a313ebb Merge branch 'main' of github.com:GAM-team/GAM 2022-11-14 21:10:11 +00:00
Jay Lee
534ccd275d remove / that seems to break Github Actions 2022-11-14 21:09:59 +00:00
Ross Scroggs
3c3d043276 Sort fields in info group, allow gal as an alias for includeinglobaladdresslist (#1575) 2022-11-14 16:09:28 -05:00
Jay Lee
786adb7c44 remove debug 2022-11-14 21:03:00 +00:00
Jay Lee
bb6c8dc225 more debug for orgunits on windows 2022-11-14 20:49:30 +00:00
Jay Lee
a7cd88b2be Merge branch 'main' of github.com:GAM-team/GAM 2022-11-14 20:32:01 +00:00
Jay Lee
a9fad337e2 debug ou create body 2022-11-14 20:31:48 +00:00
Jay Lee
d4dc1b1589 Update build.yml 2022-11-14 15:19:36 -05:00
Jay Lee
ad94adbb53 more actions 2022-11-14 20:12:21 +00:00
Jay Lee
b692799dcb bash quote fixes 2022-11-14 19:54:31 +00:00
Jay Lee
04dcf47746 rollback shelve (leave lowmem framework) 2022-11-14 19:40:36 +00:00
Jay Lee
aebb3c44fe quick fixes 2022-11-14 19:31:16 +00:00
Jay Lee
8cf345196a Inbound SSO API first take 2022-11-14 19:23:37 +00:00
Jay Lee
173fdb2297 Merge branch 'main' of github.com:GAM-team/GAM 2022-11-02 12:26:57 +00:00
Jay Lee
120db6e7d8 Updated Actions creds 2022-11-02 12:23:01 +00:00
Jay Lee
55555506be Update decrypt.sh 2022-11-01 16:32:15 -04:00
24 changed files with 1530 additions and 251 deletions

Binary file not shown.

View File

@@ -14,4 +14,5 @@ gpg --quiet --batch --yes --decrypt --passphrase="${PASSCODE}" \
--output "${credsfile}" "${gpgfile}"
tar xvvf "${credsfile}" --directory "${gampath}"
ls -l "${gampath}"
rm -rvf "${gpgfile}"
rm -rvf "${credsfile}"

View File

@@ -22,53 +22,71 @@ jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-22.04
jid: 1
#- os: ubuntu-20.04
# jid: 1
# goal: build
# arch: x86_64
# openssl_archs: linux-x86_64
#- os: [self-hosted, linux, arm64, gcp]
# jid: 2
# goal: build
# arch: aarch64
# openssl_archs: linux-aarch64
- os: ubuntu-20.04
jid: 3
goal: build
arch: x86_64
openssl_archs: linux-x86_64
staticx: yes
- os: [self-hosted, linux, arm64, gcp]
jid: 2
jid: 4
goal: build
arch: aarch64
openssl_archs: linux-aarch64
staticx: yes
- os: macos-12
jid: 4
jid: 5
goal: build
arch: universal2
openssl_archs: darwin64-x86_64 darwin64-arm64
- os: windows-2022
jid: 5
jid: 6
goal: build
arch: Win64
openssl_archs: VC-WIN64A
- os: windows-2022
jid: 6
jid: 7
goal: build
arch: Win32
openssl_archs: VC-WIN32
- os: ubuntu-22.04
goal: test
python: "3.7"
jid: 7
arch: x86_64
- os: ubuntu-22.04
goal: test
python: "3.8"
jid: 8
arch: x86_64
- os: ubuntu-22.04
goal: test
python: "3.9"
python: "3.8"
jid: 9
arch: x86_64
- os: ubuntu-22.04
goal: test
python: "3.10"
python: "3.9"
jid: 10
arch: x86_64
- os: ubuntu-22.04
goal: test
python: "3.10"
jid: 11
arch: x86_64
- os: ubuntu-22.04
goal: test
python: "3.12.0-alpha - 3.12"
jid: 12
arch: x86_64
steps:
@@ -85,7 +103,7 @@ jobs:
path: |
bin.tar.xz
src/cpython
key: gam-${{ matrix.jid }}-20221101
key: gam-${{ matrix.jid }}-20230208
- name: Untar Cache archive
if: matrix.goal == 'build' && steps.cache-python-ssl.outputs.cache-hit == 'true'
@@ -136,7 +154,7 @@ jobs:
if: runner.os == 'macOS'
run: |
# Install latest Rust
curl -fsS -o rust.sh https://sh.rustup.rs
curl --retry 5 --retry-connrefused -fsS -o rust.sh https://sh.rustup.rs
bash ./rust.sh -y
source $HOME/.cargo/env
# needed for Rust to compile cryptography Python package for universal2
@@ -154,6 +172,7 @@ jobs:
arch: ${{ matrix.arch }}
jid: ${{ matrix.jid }}
openssl_archs: ${{ matrix.openssl_archs }}
staticx: ${{ matrix.staticx }}
run: |
echo "We are running on ${RUNNER_OS}"
LD_LIBRARY_PATH="${OPENSSL_INSTALL_PATH}/lib:${PYTHON_INSTALL_PATH}/lib"
@@ -195,6 +214,7 @@ jobs:
fi
echo "We'll run make with: ${MAKEOPT}"
echo "JID=${jid}" >> $GITHUB_ENV
echo "staticx=${staticx}" >> $GITHUB_ENV
echo "arch=${arch}" >> $GITHUB_ENV
echo "LD_LIBRARY_PATH=${LD_LIBRARY_PATH}" >> $GITHUB_ENV
echo "MAKE=${MAKE}" >> $GITHUB_ENV
@@ -387,7 +407,7 @@ jobs:
- name: Upgrade pip, wheel, etc
run: |
curl -O https://bootstrap.pypa.io/get-pip.py
curl --retry 5 --retry-connrefused -O https://bootstrap.pypa.io/get-pip.py
"${PYTHON}" get-pip.py
"${PYTHON}" -m pip install --upgrade pip
"${PYTHON}" -m pip install --upgrade wheel
@@ -418,9 +438,14 @@ jobs:
# remove pre-compiled bootloaders so we fail if bootloader compile fails
rm -rvf PyInstaller/bootloader/*-*/*
cd bootloader
if [[ "${arch}" == "Win32" ]]; then
export PYINSTALLER_BUILD_ARGS="--target-arch=32bit"
fi
case "${arch}" in
"Win32")
export PYINSTALLER_BUILD_ARGS="--target-arch=32bit"
;;
"Win64")
export PYINSTALLER_BUILD_ARGS="--target-arch=64bit"
;;
esac
echo "PyInstaller build arguments: ${PYINSTALLER_BUILD_ARGS}"
"${PYTHON}" ./waf all $PYINSTALLER_BUILD_ARGS
cd ..
@@ -430,7 +455,13 @@ jobs:
- name: Build GAM with PyInstaller
if: matrix.goal != 'test'
run: |
export gampath="./dist/gam"
if [[ "${staticx}" == "yes" ]]; then
export distpath="./dist/gam"
export gampath="${distpath}"
else
export distpath="./dist"
export gampath="${distpath}/gam"
fi
mkdir -p -v "${gampath}"
if [[ "${RUNNER_OS}" == "macOS" ]]; then
export gampath=$($PYTHON -c "import os; print(os.path.realpath('$gampath'))")
@@ -445,7 +476,11 @@ jobs:
echo "gampath=${gampath}" >> $GITHUB_ENV
echo "gam=${gam}" >> $GITHUB_ENV
echo -e "GAM: ${gam}\nGAMPATH: ${gampath}"
"${PYTHON}" -m PyInstaller --clean --distpath="${gampath}" gam.spec
# TEMP force everything back to one file.
export PYINSTALLER_BUILD_ONEFILE="yes"
export distpath="./dist/gam"
export gampath="${distpath}"
"${PYTHON}" -m PyInstaller --clean --noconfirm --distpath="${distpath}" gam.spec
- name: Copy extra package files
if: matrix.goal == 'build'
@@ -466,7 +501,7 @@ jobs:
echo "GAMVERSION=${GAMVERSION}" >> $GITHUB_ENV
- name: Linux/MacOS package
if: runner.os != 'Windows' && matrix.goal == 'build'
if: runner.os != 'Windows' && matrix.goal == 'build' && matrix.staticx != 'yes'
run: |
if [[ "${RUNNER_OS}" == "macOS" ]]; then
GAM_ARCHIVE="gam-${GAMVERSION}-macos-universal2.tar.xz"
@@ -476,14 +511,14 @@ jobs:
fi
tar -C dist/ --create --verbose --exclude-from "${GITHUB_WORKSPACE}/.github/actions/package_exclusions.txt" --file $GAM_ARCHIVE --xz gam
- name: Linux 64-bit install patchelf/staticx
if: runner.os == 'Linux' && contains(runner.arch, '64') && matrix.goal != 'test'
- name: Install StaticX
if: matrix.staticx == 'yes'
run: |
"${PYTHON}" -m pip install --upgrade patchelf-wrapper
"${PYTHON}" -m pip install --upgrade staticx
- name: Linux 64-bit Make Static
if: runner.os == 'Linux' && contains(runner.arch, '64') && matrix.goal != 'test'
- name: Make StaticX
if: matrix.staticx == 'yes'
run: |
case $RUNNER_ARCH in
X64)
@@ -496,14 +531,14 @@ jobs:
echo "ldlib=${ldlib}"
$PYTHON -m staticx -l "${ldlib}" "${gam}" "${gam}-staticx"
- name: Linux Run StaticX-ed
if: runner.os == 'Linux' && contains(runner.arch, '64') && matrix.goal != 'test'
- name: Run StaticX
if: matrix.staticx == 'yes'
run: |
"${gam}-staticx" version extended
mv -v "${gam}-staticx" "${gam}"
- name: Linux package staticx
if: runner.os == 'Linux' && contains(runner.arch, '64') && matrix.goal != 'test'
if: matrix.staticx == 'yes'
run: |
GAM_ARCHIVE="gam-${GAMVERSION}-linux-$(uname -m)-legacy.tar.xz"
tar -C dist/ --create --verbose --exclude-from "${GITHUB_WORKSPACE}/.github/actions/package_exclusions.txt" --file $GAM_ARCHIVE --xz gam
@@ -552,26 +587,38 @@ jobs:
echo "OAUTHFILE=${OAUTHFILE}" >> $GITHUB_ENV
export gam_user="gam-gha-${JID}@pdl.jaylee.us"
echo "gam_user=${gam_user}" >> $GITHUB_ENV
touch "${gampath}/lowmemory.txt"
$gam checkconn
$gam oauth info
$gam info domain
$gam oauth refresh
$gam info user
#$gam info user $gam_user grouptree
export tstamp=$($PYTHON -c "import time; print(time.time_ns())")
export newbase=gha_test_$JID_$tstamp
export newuser=$newbase@pdl.jaylee.us
export newgroup=$newbase-group@pdl.jaylee.us
export newalias=$newbase-alias@pdl.jaylee.us
export newbuilding=$newbase-building
export newresource=$newbase-resource
export newbase="gha_test_${JID}_${tstamp}"
export newuser="${newbase}@pdl.jaylee.us"
export newgroup="${newbase}-group@pdl.jaylee.us"
export newalias="${newbase}-alias@pdl.jaylee.us"
export newbuilding="${newbase}-building"
export newresource="${newbase}-resource"
export newou="aaaGithub Actions/${newbase}"
# cleanup old runs
GAM_CSV_ROW_FILTER="name:regex:gha_test_${JID}_" $gam print vaultholds | $gam csv - gam delete vaulthold "id:~~holdId~~" matter "id:~~matterId~~"
GAM_CSV_ROW_FILTER="name:regex:gha_test_${JID}_" $gam print features | $gam csv - gam delete feature ~name
GAM_CSV_ROW_FILTER="name:regex:^gha_test_${JID}_" $gam user $gam_user print shareddrives asadmin | $gam csv - gam user $gam_user delete shareddrive ~id nukefromorbit
$gam print users query "gha.jid=$JID" | $gam csv - gam delete user ~primaryEmail
GAM_CSV_ROW_FILTER="name:regex:^gha_test_${JID}_" $gam print ous fromparent "aaaGithub Actions" | $gam csv - gam delete ou ~orgUnitId
GAM_CSV_ROW_FILTER="groupKey.id:regex:^gha_test_${JID}_" $gam print cigroups | $gam csv - gam delete cigroup ~groupKey.id
GAM_CSV_ROW_FILTER="resourceId:regex:^gha_test_${JID}_" $gam print resources | $gam csv - gam delete resource ~resourceId
GAM_CSV_ROW_FILTER="buildingId:regex:^gha_test_${JID}_" $gam print buildings | $gam csv - gam delete building ~buildingId
echo "Creating OrgUnit ${newou}"
$gam create ou "${newou}"
export GAM_THREADS=5
echo email > sample.csv;
for i in {1..10}; do
echo "${newbase}-bulkuser-$i" >> sample.csv;
done
$gam create user $newuser firstname GHA lastname $JID password random recoveryphone 12125121110 recoveryemail jay0lee@gmail.com gha.jid $JID languages en+,en-GB-
$gam create user $newuser firstname GHA lastname $JID displayname "Github Actions ${JID}" password random ou "${newou}" recoveryphone 12125121110 recoveryemail jay0lee@gmail.com gha.jid $JID languages en+,en-GB-
$gam user $newuser update photo https://dummyimage.com/400x600/000/fff
$gam user $newuser get photo
$gam user $newuser delete photo
@@ -585,8 +632,8 @@ jobs:
$gam update group $newgroup add owner $gam_user
$gam update group $newgroup add member $newuser
$gam create admin $newuser _GROUPS_EDITOR_ROLE CUSTOMER # condition nonsecuritygroup
$gam csv sample.csv gam create user ~~email~~ firstname "GHA Bulk" lastname ~~email~~ gha.jid $JID
$gam csv sample.csv gam update user ~~email~~ recoveryphone 12125121110 recoveryemail jay0lee@gmail.com password random
$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 ""
$gam csv sample.csv gam user ~email add license workspaceenterpriseplus
$gam csv sample.csv gam user $gam_user sendemail recipient ~~email~~@pdl.jaylee.us subject "test message $newbase" message "GHA test message"
@@ -616,8 +663,8 @@ jobs:
$gam users "$newbase-bulkuser-7 $newbase-bulkuser-8 $newbase-bulkuser-9" modify messages query in:anywhere maxtomodify 99999 addlabel IMPORTANT addlabel STARRED doit
$gam user $newuser delete label --ALL_LABELS--
GAM_CSV_ROW_FILTER="name:regex:gha-test-${JID}" $gam print features | $gam csv - gam delete feature ~name
$gam create feature name Whiteboard-$newbase
$gam create feature name VC-$newbase
$gam create feature name Whiteboard-$newbase
$gam create building "My Building - $newbase" id $newbuilding floors 1,2,3,4,5,6,7,8,9,10,11,12,14,15 description "No 13th floor here..."
$gam create resource $newresource "Resource Calendar $tstamp" capacity 25 features Whiteboard-$newbase,VC-$newbase building $newbuilding floor 15 type Room
$gam info resource $newresource
@@ -653,8 +700,9 @@ jobs:
$gam delete hold "GHA hold $newbase" matter $matterid
$gam update matter $matterid action close
$gam update matter $matterid action delete
#$gam delete user $newuser
#$gam undelete user $newuser
# 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
$gam delete user $newuser
$gam print users query "gha.jid=$JID" | $gam csv - gam delete user ~primaryEmail
$gam print mobile
@@ -670,26 +718,32 @@ 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
#$gam print userinvitations
#$gam print userinvitations | $gam csv - gam send userinvitation ~name
$gam print userinvitations
$gam print userinvitations | $gam csv - gam send userinvitation ~name
$gam create caalevel "zzz_${newbase}" basic condition ipsubnetworks 1.1.1.1/32,2.2.2.2/32 endcondition
$gam print caalevels
$gam delete caalevel "zzz_${newbase}"
driveid=$($gam user $gam_user add shareddrive "${newbase}" | awk '{print $NF}')
echo "Created shared drive ${driveid}"
$gam user $gam_user add drivefile localfile gam.py parentid "${driveid}"
$gam user $gam_user update shareddrive "${driveid}" ou "id:03ph8a2z1t2ph5z"
$gam user $gam_user update shareddrive "${driveid}" ou "${newou}"
$gam user $gam_user show shareddrives asadmin
$gam user $gam_user update shareddrive "${driveid}" ou "aaaGithub Actions" # so we can delete our OU...
$gam user $gam_user delete shareddrive "${driveid}" nukefromorbit
echo "printer model count:"
$gam print printermodels | wc -l
#ssoprofile=$($gam create inboundssoprofile name "El Goog ${newbase}" loginurl https://www.google.com logouturl https://www.google.com changepasswordurl https://www.google.com entityid ElGoog return_name_only)
#$gam create inboundssocredential profile "id:${ssoprofile}" generate_key
#$gam create inboundssoassignment profile "id:${ssoprofile}" orgunit "${newou}" mode SAML_SSO
$gam delete ou "${newou}"
#$gam delete inboundssoprofile "id:${ssoprofile}"
#$gam print printers
#$gam 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
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
@@ -721,11 +775,6 @@ jobs:
- name: Download artifacts
uses: actions/download-artifact@v3
- name: Set datetime version string
id: dateversion
run: |
echo "::set-output name=dateversion::$(date +'%Y%m%d.%k%M%S')"
- name: VirusTotal Scan
uses: crazy-max/ghaction-virustotal@v3
with:
@@ -737,9 +786,8 @@ jobs:
name: Publish draft release
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
automatic_release_tag: ${{ steps.dateversion.outputs.dateversion }}
automatic_release_tag: latest
prerelease: false
draft: true
title: "GAM ${{ steps.dateversion.outputs.dateversion }}"
files: |
gam-binaries/*

View File

@@ -911,6 +911,12 @@ gam oauth|oauth2 refresh
gam <UserTypeEntity> check serviceaccount [scope|scopes <APIScopeURLList>]
gam yubikey [resetpiv]
gam rotate sakey yubikey yubikey_pin yubikey_slot AUTHENTICATION yubikeypin <String> yubikeyserialnumber <String>
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]
@@ -1548,6 +1561,44 @@ gam print group-members|groups-members [todrive]
[roles <GroupRoleList>] [membernames] [fields <MembersFieldNameList>]
[includederivedmembership]
<SSOProfileDisplayName> ::= <String>
<SSOProfileName> ::= id:inboundSamlSsoProfiles/<String>
<SSOProfileItem> ::= <SSOProfileDisplayName>|<SSOProfileName>
<SSOProfileItemList> ::= "<SSOProfileItem>(,<SSOProfileItem>)*"
gam create inboundssoprofile [name <SSOProfileDisplayName>]
[entityid <String>] [loginurl <URL>] [logouturl <URL>] [changepasswordurl <URL>]
[returnnameonly]
gam update inboundssoprofile <SSOProfileItem>
[entityid <String>] [loginurl <URL>] [logouturl <URL>] [changepasswordurl <URL>]
[returnnameonly]
gam delete inboundssoprofile <SSOProfileItem>
gam info inboundssoprofile <SSOProfileItem>
gam show inboundssoprofiles
gam print inboundssoprofiles [todrive]
<SSOCredentialsName> ::= [id:]inboundSamlSsoProfiles/<String>/idpCredentials/<String>
gam create inboundssocredential profile <SSOProfileItem>
(pemfile <FileName>)|(generatekey [keysize 1024|2048|4096]) [replaceolddest]
gam delete inboundssocredential <SSOCredentialsName>
gam show inboundssocredentials [profile|profiles <SSOProfileItemList>]
gam print inboundssocredentials [profile|profiles <SSOProfileItemList>] [todrive]
<SSOAssignmentSelector> ::=
groups/<String> |
group:<EmailAddress> |
orgunits/<String> |
orgunit:<OrgUnitPath>
gam create inboundssoassignment (group <GroupItem> rank <Number>)|(ou|org|orgunit <OrgUnitItem>)
(mode sso_off)|(mode saml_sso profile <SSOProfileItem>)(mode domain_wide_saml_if_enabled) [neverredirect]
gam update inboundssoassignment [(group <GroupItem> rank <Number>)|(ou|org|orgunit <OrgUnitItem>)]
[(mode sso_off)|(mode saml_sso profile <SSOProfileItem>)(mode domain_wide_saml_if_enabled)] [neverredirect]
gam info inboundssoassignment <SSOAssignmentSelector>
gam show inboundssoassignments
gam print inboundssoassignments [todrive]
gam send userinvitation <EmailAddress>
gam cancel userinvitation <EmailAddress>
gam check userinvitation|isinvitable <EmailAddress>
@@ -1636,6 +1687,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>]
@@ -1648,6 +1702,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",

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

@@ -1,53 +1,110 @@
# -*- mode: python -*-
# -*- mode: python ; coding: utf-8 -*-
from os import getenv
from sys import platform
import sys
import importlib
from PyInstaller.utils.hooks import copy_metadata
extra_files = []
extra_files = []
extra_files += copy_metadata('google-api-python-client')
extra_files += [('cbcm-v1.1beta1.json', '.')]
extra_files += [('contactdelegation-v1.json', '.')]
extra_files += [('admin-directory_v1.1beta1.json', '.')]
extra_files += [('roots.pem', '.')]
hidden_imports = [
'gam.auth.yubikey',
]
a = Analysis(['gam/__main__.py'],
hiddenimports=hidden_imports,
hookspath=None,
excludes=['FixTk', 'tcl', 'tk', '_tkinter', 'tkinter', 'Tkinter'],
datas=extra_files,
runtime_hooks=None)
a = Analysis(
['gam/__main__.py'],
pathex=[],
binaries=[],
datas=extra_files,
hiddenimports=hidden_imports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=None,
noarchive=False,
)
for d in a.datas:
if 'pyconfig' in d[0]:
a.datas.remove(d)
break
pyz = PYZ(a.pure)
if sys.platform == "darwin":
target_arch="universal2"
pyz = PYZ(a.pure,
a.zipped_data,
cipher=None)
# requires Python 3.10+ but no one should be compiling
# GAM with older versions anyway
match platform:
case "darwin":
target_arch = "universal2"
strip = True
case "win32":
target_arch = None
strip = False
case _:
target_arch = None
strip = True
name = 'gam'
debug = False
bootloader_ignore_signals = False
upx = False
console = True
disable_windowed_traceback = False
argv_emulation = False
codesign_identity = None
entitlements_file = None
if getenv('PYINSTALLER_BUILD_ONEFILE') == 'yes':
# Build one file
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name=name,
debug=debug,
bootloader_ignore_signals=bootloader_ignore_signals,
strip=strip,
upx=upx,
console=console,
disable_windowed_traceback=disable_windowed_traceback,
argv_emulation=argv_emulation,
target_arch=target_arch,
codesign_identity=codesign_identity,
entitlements_file=entitlements_file,
)
else:
target_arch=None
# Build one folder
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name=name,
debug=debug,
bootloader_ignore_signals=bootloader_ignore_signals,
strip=strip,
upx=upx,
console=console,
disable_windowed_traceback=disable_windowed_traceback,
argv_emulation=argv_emulation,
target_arch=target_arch,
codesign_identity=codesign_identity,
entitlements_file=entitlements_file,
)
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=strip,
upx=upx,
upx_exclude=[],
name=name,
)
# use strip on all non-Windows platforms
strip = not sys.platform == 'win32'
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
name='gam',
debug=False,
strip=strip,
upx=False,
target_arch=target_arch,
console=True)

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
@@ -65,6 +66,7 @@ from gam.gapi import chromemanagement as gapi_chromemanagement
from gam.gapi import chromepolicy as gapi_chromepolicy
from gam.gapi.cloudidentity import devices as gapi_cloudidentity_devices
from gam.gapi.cloudidentity import groups as gapi_cloudidentity_groups
from gam.gapi.cloudidentity import inboundsso as gapi_cloudidentity_inboundsso
from gam.gapi.cloudidentity import orgunits as gapi_cloudidentity_orgunits
from gam.gapi.cloudidentity import userinvitations as gapi_cloudidentity_userinvitations
from gam.gapi import contactdelegation as gapi_contactdelegation
@@ -781,9 +783,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)
@@ -924,14 +926,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])
@@ -939,6 +939,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)
@@ -952,6 +956,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']
@@ -1077,9 +1086,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]))
@@ -1294,7 +1304,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,
@@ -1313,34 +1325,38 @@ def doCheckServiceAccount(users):
3,
'Invalid private key in oauth2service.json. Please delete the file and then\nrecreate with "gam create project" or "gam use project"'
)
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]
key_id = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['private_key_id']
name = f'projects/-/serviceAccounts/{project}/keys/{key_id}'
key = gapi.call(iam.projects().serviceAccounts().keys(),
'get',
name=name,
throw_reasons=[gapi_errors.ErrorReason.FOUR_O_THREE])
key_created = dateutil.parser.parse(
key['validAfterTime'], ignoretz=True)
key_age = datetime.datetime.now() - key_created
key_days = key_age.days
if key_days > 30:
print(
'Your key is old. Recommend running "gam rotate sakey" to get a new key'
)
key_type = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA].get('key_type', 'default')
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]
key_id = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['private_key_id']
name = f'projects/-/serviceAccounts/{project}/keys/{key_id}'
key = gapi.call(iam.projects().serviceAccounts().keys(),
'get',
name=name,
throw_reasons=[gapi_errors.ErrorReason.FOUR_O_THREE])
key_created = dateutil.parser.parse(
key['validAfterTime'], ignoretz=True)
key_age = datetime.datetime.now() - key_created
key_days = key_age.days
if key_days > 30:
print(
'Your key is old. Recommend running "gam rotate sakey" to get a new key'
)
key_age_result = test_warn
else:
key_age_result = test_pass
except googleapiclient.errors.HttpError:
key_age_result = test_warn
else:
key_age_result = test_pass
except googleapiclient.errors.HttpError:
key_age_result = test_warn
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)
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:
@@ -4472,15 +4488,7 @@ def getImap(users):
soft_errors=True,
userId='me')
if result:
enabled = result['enabled']
if enabled:
print(
f'User: {user}, IMAP Enabled: {enabled}, autoExpunge: {result["autoExpunge"]}, expungeBehavior: {result["expungeBehavior"]}, maxFolderSize: {result["maxFolderSize"]}{currentCount(i, count)}'
)
else:
print(
f'User: {user}, IMAP Enabled: {enabled}{currentCount(i, count)}'
)
display.print_json(result)
def doPop(users):
@@ -6577,6 +6585,15 @@ def getUserAttributes(i, cd, updateCmd):
body.setdefault('name', {})
body['name']['familyName'] = sys.argv[i + 1]
i += 2
elif myarg in ['displayname']:
body.setdefault('name', {})
body['name']['displayName'] = sys.argv[i + 1]
# sigh, the API is wonky. If we set just displayName
# we get an error. But if we also "set" fullName which is
# really just a concat of first/last name and can't be set
# then it works. Go figure.
body['name']['fullName'] = sys.argv[i+1]
i += 2
elif myarg in ['username', 'email', 'primaryemail'] and updateCmd:
body['primaryEmail'] = normalizeEmailAddressOrUID(sys.argv[i + 1],
noUid=True)
@@ -7152,6 +7169,43 @@ 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()
try:
_, projectId = google.auth.default()
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,
@@ -7436,15 +7490,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]'
@@ -7726,7 +7791,7 @@ def doUpdateProjects():
_grantRotateRights(iam, sa_email, sa_email)
def _generatePrivateKeyAndPublicCert(client_id, key_size):
def _generatePrivateKeyAndPublicCert(client_id, key_size, b64enc_pub=True):
print(' Generating new private key...')
private_key = rsa.generate_private_key(public_exponent=65537,
key_size=key_size,
@@ -7770,6 +7835,8 @@ def _generatePrivateKeyAndPublicCert(client_id, key_size):
backend=default_backend())
public_cert_pem = certificate.public_bytes(
serialization.Encoding.PEM).decode()
if not b64enc_pub:
return private_pem, public_cert_pem
publicKeyData = base64.b64encode(public_cert_pem.encode())
if isinstance(publicKeyData, bytes):
publicKeyData = publicKeyData.decode()
@@ -7805,8 +7872,7 @@ 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',
@@ -7825,6 +7891,32 @@ def doShowServiceAccountKeys():
display.print_json(keys)
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'
}
try:
creds, sa_info['project_id'] = google.auth.default()
except google.auth.exceptions.DefaultCredentialsError as e:
controlflow.system_error_exit(2, e)
request = transport.create_request()
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,
@@ -7900,6 +7992,7 @@ def doCreateOrRotateServiceAccountKeys(iam=None,
yk = yubikey.YubiKey(new_data)
if 'yubikey_serial_number' not in new_data:
new_data['yubikey_serial_number'] = yk.get_serial_number()
yk = yubikey.YubiKey(new_data)
if 'yubikey_slot' not in new_data:
new_data['yubikey_slot'] = 'AUTHENTICATION'
publicKeyData = yk.get_certificate()
@@ -8155,20 +8248,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':
@@ -8194,19 +8281,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:
@@ -8935,10 +9018,16 @@ def doGetUserInfo(user_email=None):
customFieldMask=customFieldMask,
viewType=viewType)
print(f'User: {user["primaryEmail"]}')
if 'name' in user and 'givenName' in user['name']:
print(f'First Name: {user["name"]["givenName"]}')
if 'name' in user and 'familyName' in user['name']:
print(f'Last Name: {user["name"]["familyName"]}')
if 'name' in user:
names = {
'givenName': 'First Name',
'familyName': 'Last Name',
'fullName': 'Full Name',
'displayName': 'Display Name',
}
for field, description in names.items():
if field in user['name']:
print(f'{description}: {user["name"][field]}')
if 'languages' in user:
print(f"Languages: {_formatLanguagesList(user['languages'], ',')}")
if 'isAdmin' in user:
@@ -9483,7 +9572,7 @@ def doUndeleteUser():
i = 4
while i < len(sys.argv):
myarg = sys.argv[i].lower()
if myarg in ['ou', 'org']:
if myarg in ['ou', 'org', 'orgunit']:
orgUnit = gapi_directory_orgunits.makeOrgUnitPathAbsolute(
sys.argv[i + 1])
i += 2
@@ -9646,6 +9735,7 @@ USER_ARGUMENT_TO_PROPERTY_MAP = {
'changepasswordatnextlogin': ['changePasswordAtNextLogin',],
'creationtime': ['creationTime',],
'deletiontime': ['deletionTime',],
'displayname': ['displayName',],
'email': ['emails',],
'emails': ['emails',],
'externalid': ['externalIds',],
@@ -9687,6 +9777,7 @@ USER_ARGUMENT_TO_PROPERTY_MAP = {
'location': ['locations',],
'locations': ['locations',],
'name': [
'name.displayName',
'name.givenName',
'name.familyName',
'name.fullName',
@@ -10184,7 +10275,7 @@ def getUsersToModify(entity_type=None,
'org_ns',
'ou_susp',
'org_susp',
]:
]:
if entity_type in ['ou_ns', 'org_ns']:
checkSuspended = False
elif entity_type in ['ou_susp', 'org_susp']:
@@ -10589,6 +10680,11 @@ OAUTH2_SCOPES = [
'subscopes': ['readonly'],
'scopes': 'https://www.googleapis.com/auth/cloud-identity.groups'
},
{
'name': 'Cloud Identity - Inbound SSO Settings',
'subscopes': ['readonly'],
'scopes': 'https://www.googleapis.com/auth/cloud-identity.inboundsso',
},
{
'name': 'Cloud Identity - OrgUnits',
'subscopes': ['readonly'],
@@ -10720,11 +10816,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': [],
@@ -10769,6 +10871,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.
@@ -10798,6 +10901,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
@@ -10888,7 +10992,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
@@ -10900,7 +11007,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
@@ -10929,8 +11039,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."""
@@ -11418,6 +11528,8 @@ def ProcessGAMCommand(args):
i, encoding = getCharSet(i + 1)
f = fileutils.open_file(filename, encoding=encoding)
csvFile = csv.DictReader(f)
if not csvFile.fieldnames:
controlflow.system_error_exit(0, f'CSV file {filename} is empty')
if (i == len(sys.argv)) or (sys.argv[i].lower() !=
'gam') or (i + 1 == len(sys.argv)):
controlflow.system_error_exit(
@@ -11467,7 +11579,13 @@ def ProcessGAMCommand(args):
gapi_cloudidentity_groups.create()
elif argument in ['nickname', 'alias']:
doCreateAlias()
elif argument in ['org', 'ou']:
elif argument in ['inboundssoprofile', 'inboundssoprofiles']:
gapi_cloudidentity_inboundsso.create_profile()
elif argument in ['inboundssocredential', 'inboundssocredentials']:
gapi_cloudidentity_inboundsso.create_credentials()
elif argument in ['inboundssoassignment', 'inboundssoassignments']:
gapi_cloudidentity_inboundsso.create_assignment()
elif argument in ['org', 'orgunit', 'ou']:
gapi_directory_orgunits.create()
elif argument == 'resource':
gapi_directory_resource.createResourceCalendar()
@@ -11519,6 +11637,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)
@@ -11539,10 +11659,14 @@ def ProcessGAMCommand(args):
gapi_cloudidentity_groups.update()
elif argument in ['nickname', 'alias']:
doUpdateAlias()
elif argument in ['ou', 'org']:
elif argument in ['ou', 'org', 'orgunit']:
gapi_directory_orgunits.update()
elif argument == 'resource':
gapi_directory_resource.updateResourceCalendar()
elif argument in ['inboundssoprofile', 'inboundssoprofiles']:
gapi_cloudidentity_inboundsso.update_profile()
elif argument in ['inboundssoassignment', 'inboundssoasignments']:
gapi_cloudidentity_inboundsso.update_assignment()
elif argument == 'cros':
gapi_directory_cros.doUpdateCros()
elif argument == 'mobile':
@@ -11604,7 +11728,11 @@ def ProcessGAMCommand(args):
doGetAliasInfo()
elif argument == 'instance':
gapi_directory_customer.doGetCustomerInfo()
elif argument in ['org', 'ou']:
elif argument in ['inboundssoprofile', 'inboundssoprofiles']:
gapi_cloudidentity_inboundsso.info_profile()
elif argument in ['inboundssoassignment', 'inboundssoassignments']:
gapi_cloudidentity_inboundsso.info_assignment()
elif argument in ['ou', 'org', 'orgunit']:
gapi_directory_orgunits.info()
elif argument == 'resource':
gapi_directory_resource.getResourceCalendarInfo()
@@ -11679,8 +11807,12 @@ def ProcessGAMCommand(args):
gapi_cloudidentity_groups.delete()
elif argument in ['nickname', 'alias']:
doDeleteAlias()
elif argument == 'org':
elif argument in ['ou', 'org', 'orgunit']:
gapi_directory_orgunits.delete()
elif argument in ['inboundssoprofile', 'inboundssoprofiles']:
gapi_cloudidentity_inboundsso.delete_profile()
elif argument in ['inboundssocredential', 'inboundssocredentials']:
gapi_cloudidentity_inboundsso.delete_credentials()
elif argument == 'resource':
gapi_directory_resource.deleteResourceCalendar()
elif argument == 'mobile':
@@ -11772,6 +11904,12 @@ def ProcessGAMCommand(args):
gapi_directory_groups.print_members()
elif argument in ['cigroupmembers', 'cigroupsmembers']:
gapi_cloudidentity_groups.print_members()
elif argument in ['inboundssoprofile', 'inboundssoprofiles']:
gapi_cloudidentity_inboundsso.print_show_profiles()
elif argument in ['inboundssocredential', 'inboundssocredentials']:
gapi_cloudidentity_inboundsso.print_show_credentials()
elif argument in ['inboundssoassignment', 'inboundssoassignments']:
gapi_cloudidentity_inboundsso.print_show_assignments()
elif argument in ['orgs', 'ous']:
gapi_directory_orgunits.print_()
elif argument == 'privileges':
@@ -11840,6 +11978,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']:
@@ -11872,6 +12014,12 @@ def ProcessGAMCommand(args):
gapi_licensing.show()
elif argument in ['project', 'projects']:
doPrintShowProjects(False)
elif argument in ['inboundssoprofile', 'inboundssoprofiles']:
gapi_cloudidentity_inboundsso.print_show_profiles('show')
elif argument in ['inboundssocredential', 'inboundssocredentials']:
gapi_cloudidentity_inboundsso.print_show_credentials('show')
elif argument in ['inboundssoassignment', 'inboundssoassignments']:
gapi_cloudidentity_inboundsso.print_show_assignments('show')
elif argument in ['sakey', 'sakeys']:
doShowServiceAccountKeys()
elif argument in ['browsertoken', 'browsertokens']:
@@ -11964,11 +12112,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']:
@@ -12012,8 +12170,14 @@ def ProcessGAMCommand(args):
action = sys.argv[2].lower().replace('_', '')
if action == 'resetpiv':
yk = yubikey.YubiKey()
yk.serial_number = yk.get_serial_number()
yk.reset_piv()
sys.exit(0)
elif command == 'enable':
enable_what = sys.argv[2].lower().replace('_', '')
if enable_what in ['api', 'apis']:
enable_apis()
sys.exit(0)
users = getUsersToModify()
command = sys.argv[3].lower()
if command == 'print' and len(sys.argv) == 4:

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
@@ -40,7 +41,7 @@ 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 'key_type' in creds_data:
audience = f'https://{api}.googleapis.com/'
key_type = creds_data.get('key_type', 'default')
if key_type == 'default':
@@ -51,6 +52,11 @@ def get_admin_credentials(api=None):
return JWTCredentials._from_signer_and_info(yksigner,
creds_data,
audience=audience)
elif 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:

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

@@ -0,0 +1,89 @@
''' 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"
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 '''
try:
credentials, _ = google.auth.default()
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

@@ -8,7 +8,7 @@ from threading import Timer
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from smartcard.Exceptions import CardConnectionException
from ykman.device import connect_to_device
from ykman.device import list_all_devices
from ykman.piv import generate_self_signed_certificate, \
generate_chuid
from yubikit.piv import DEFAULT_MANAGEMENT_KEY, \
@@ -20,11 +20,14 @@ from yubikit.piv import DEFAULT_MANAGEMENT_KEY, \
OBJECT_ID, \
SLOT, \
TOUCH_POLICY
from yubikit.core.smartcard import ApduError
from yubikit.core.smartcard import ApduError, \
SmartCardConnection
from gam import controlflow
class YubiKey():
def __init__(self, service_account_info=None):
self.key_type = None
self.slot = None
@@ -46,12 +49,16 @@ class YubiKey():
self.pin = service_account_info.get('yubikey_pin')
self.key_id = service_account_info.get('private_key_id')
def _connect(self):
try:
conn, _, _ = connect_to_device(self.serial_number)
devices = list_all_devices()
for (device, info) in devices:
if info.serial == self.serial_number:
return device.open_connection(SmartCardConnection)
except CardConnectionException as err:
controlflow.system_error_exit(9, f'YubiKey - {err}')
return conn
def get_certificate(self):
try:
@@ -79,11 +86,22 @@ class YubiKey():
def get_serial_number(self):
try:
_, _, info = connect_to_device(self.serial_number)
return info.serial
devices = list_all_devices()
if self.serial_number:
for (device, info) in devices:
if info.serial == self.serial_number:
return info.serial
msg = f'Could not find YubiKey with serial {self.serial_number}'
controlflow.system_error_exit(3, msg)
if len(devices) > 1:
serials = ', '.join([str(info.serial) for (_, info) in devices])
msg = f'Multiple YubiKeys connected. Specify yubikey_serial_number and one of {serials}'
controlflow.system_error_exit(4, msg)
return devices[0][1].serial
except ValueError as err:
controlflow.system_error_exit(9, f'YubiKey - {err}')
def reset_piv(self):
'''Resets YubiKey PIV app and generates new key for GAM to use.'''
reply = str(input('This will wipe all PIV keys and configuration from your YubiKey. Are you sure? (y/N) ').lower().strip())
@@ -95,7 +113,9 @@ class YubiKey():
piv = PivSession(conn)
piv.reset()
rnd = SystemRandom()
pin_puk_chars = string.ascii_letters + string.digits + string.punctuation
pin_puk_chars = string.ascii_letters + \
string.digits + \
string.punctuation
new_puk = ''.join(rnd.choice(pin_puk_chars) for _ in range(8))
new_pin = ''.join(rnd.choice(pin_puk_chars) for _ in range(8))
piv.change_puk('12345678', new_puk)
@@ -155,3 +175,4 @@ class YubiKey():
if 'mplock' in globals():
mplock.release()
return signed

View File

@@ -34,7 +34,7 @@ def missing_argument_exit(argument, command):
"""Indicate that the argument is missing for the command.
Args:
argument: the missingagrument
argument: the missing argument
command: the base GAM command
"""
system_error_exit(2, f'missing argument {argument} for "{command}"')

View File

@@ -1,7 +1,6 @@
"""Methods related to execution of GAPI requests."""
import os.path
import shelve
import sys
from tempfile import TemporaryDirectory
@@ -13,7 +12,7 @@ from gam import controlflow
from gam import display
from gam.gapi import errors
from gam import transport
from gam.var import (GC_Values, GC_LOW_MEMORY, GM_Globals,
from gam.var import (GC_Values, GM_Globals,
GM_CURRENT_API_SCOPES, GM_CURRENT_API_USER,
GM_EXTRA_ARGS_DICT, GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID,
MAX_RESULTS_API_EXCEPTIONS, MESSAGE_API_ACCESS_CONFIG,
@@ -338,18 +337,7 @@ def get_all_pages(service,
kwargs['body'].update(page_key)
else:
kwargs.update(page_key)
if GC_Values[GC_LOW_MEMORY]:
td_args = {'prefix': 'GAM-'}
if sys.version_info.minor >= 10:
td_args['ignore_cleanup_errors'] = True
tempdir = TemporaryDirectory(**td_args)
tempfile = os.path.join(tempdir.name, 'gapi_pages')
all_items = shelve.open(tempfile)
# attach tempdir to all_items so we
# don't cleanup tempdir early
all_items._tempdir = tempdir
else:
all_items = []
all_items = []
page_token = None
total_items = 0
while True:

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'
@@ -286,7 +286,8 @@ def printShowCrosTelemetry(mode):
device['storageInfo']['percentDiskUsed'] = 100 - device['storageInfo']['percentDiskFree']
for cpuStatusReport in device.get('cpuStatusReport', []):
for tempInfo in cpuStatusReport.pop('cpuTemperatureInfo', []):
cpuStatusReport[f"cpuTemperatureInfo.{tempInfo['label'].strip()}"] = tempInfo['temperatureCelsius']
if 'temperatureCelsius' in tempInfo:
cpuStatusReport[f"cpuTemperatureInfo.{tempInfo['label'].strip()}"] = tempInfo['temperatureCelsius']
if showOrgUnitPath:
orgUnitId = device.get('orgUnitId')
if orgUnitId not in orgUnitIdPathMap:
@@ -306,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'
]
@@ -319,6 +411,7 @@ def printVersions():
startDate = None
endDate = None
pfilter = None
query = None
reverse = False
i = 3
while i < len(sys.argv):
@@ -349,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

@@ -167,7 +167,7 @@ def build_schemas(svc=None, sfilter=None):
for an_enum in schema['definition']['enumType']:
if an_enum['name'] == type_name:
setting_dict['enums'] = [enum['name'] for enum in an_enum['value']]
setting_dict['enum_prefix'] = utils.commonprefix(setting_dict['enums'])
setting_dict['enum_prefix'] = utils.commonprefix(setting_dict['enums'], True)
prefix_len = len(setting_dict['enum_prefix'])
setting_dict['enums'] = [enum[prefix_len:] for enum \
in setting_dict['enums'] \

View File

@@ -217,6 +217,7 @@ def print_():
gapi_directory_customer.setTrueCustomerId()
parent = f'customers/{GC_Values[GC_CUSTOMER_ID]}'
usemember = None
query = None
memberDelimiter = '\n'
todrive = False
titles = []
@@ -235,6 +236,9 @@ def print_():
elif myarg == 'delimiter':
memberDelimiter = sys.argv[i + 1]
i += 2
elif myarg == 'query':
query = sys.argv[i + 1]
i += 2
elif myarg == 'sortheaders':
sortHeaders = True
i += 1
@@ -314,14 +318,20 @@ def print_():
if entity['relationType'] == 'DIRECT':
entityList.append(gapi.call(ci.groups(), 'get', name=entity['group']))
else:
if query:
method = 'search'
kwargs = {'query': query}
else:
method = 'list'
kwargs = {'parent': parent}
entityList = gapi.get_all_pages(ci.groups(),
'list',
method,
'groups',
page_message=page_message,
message_attribute=['groupKey', 'id'],
parent=parent,
view='FULL',
pageSize=500)
pageSize=500,
**kwargs)
i = 0
count = len(entityList)
for groupEntity in entityList:
@@ -935,6 +945,12 @@ def group_email_to_id(ci, group, i=0, count=0):
return None
def group_id_to_email(ci, group_id):
return gapi.call(ci.groups(),
'get',
fields='groupKey/id',
name=group_id).get('groupKey', {}).get('id')
def membership_email_to_id(ci, parent, membership, i=0, count=0):
membership = gam.normalizeEmailAddressOrUID(membership)
try:

View File

@@ -0,0 +1,538 @@
"""Methods related to Cloud Identity Inbound (Google as SP) SAML SSO"""
from datetime import datetime
import re
import sys
import dateutil.parser
import googleapiclient
import gam
from gam.var import GC_CUSTOMER_ID, GC_Values, MY_CUSTOMER
from gam import controlflow
from gam import display
from gam import fileutils
from gam import gapi
from gam import utils
from gam.gapi import errors as gapi_errors
from gam.gapi import cloudidentity as gapi_cloudidentity
from gam.gapi import directory as gapi_directory
from gam.gapi.cloudidentity import groups as gapi_cloudidentity_groups
from gam.gapi.directory import orgunits as gapi_directory_orgunits
'''returns customer in the format inboundsso requires'''
def get_sso_customer():
customer = GC_Values[GC_CUSTOMER_ID]
return f'customers/{customer}'
'''returns org unit in the format inboundsso requires'''
def get_orgunit_id(orgunit):
ou_id = gapi_directory_orgunits.getOrgUnitId(orgunit)[1]
if ou_id.startswith('id:'):
ou_id = ou_id[3:]
return f'orgUnits/{ou_id}'
'''build Cloud Identity API'''
def build():
return gapi_cloudidentity.build('cloudidentity_beta')
'''parse cmd for profile create/update'''
def parse_profile(body, i):
name_only = False
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'name':
body['displayName'] = sys.argv[i+1]
i += 2
elif myarg == 'entityid':
body.setdefault('idpConfig', {})['entityId'] = sys.argv[i+1]
i += 2
elif myarg == 'returnnameonly':
name_only = True
i += 1
elif myarg == 'loginurl':
body.setdefault('idpConfig', {})['singleSignOnServiceUri'] = sys.argv[i+1]
i += 2
elif myarg == 'logouturl':
body.setdefault('idpConfig', {})['logoutRedirectUri'] = sys.argv[i+1]
i += 2
elif myarg == 'changepasswordurl':
body.setdefault('idpConfig', {})['changePasswordUri'] = sys.argv[i+1]
i += 2
else:
controlflow.invalid_argument_exit(myarg, 'gam create/update inboundssoprofile')
return (name_only, body)
'''convert profile nice names to unique ID'''
def profile_displayname_to_name(displayName, ci=None):
if displayName.lower().startswith('id:') or displayName.lower().startswith('uid:'):
displayName = displayName.split(':', 1)[1]
if not displayName.startswith('inboundSamlSsoProfiles/'):
displayName = f'inboundSamlSsoProfiles/{displayName}'
return displayName
if not ci:
ci = build()
customer = get_sso_customer()
_filter = f'customer=="{customer}"'
profiles = gapi.get_all_pages(ci.inboundSamlSsoProfiles(),
'list',
'inboundSamlSsoProfiles',
filter=_filter)
matches = []
for profile in profiles:
if displayName.lower() == profile.get('displayName', '').lower():
matches.append(profile)
if len(matches) == 1:
return matches[0]['name']
if len(matches) == 0:
controlflow.system_error_exit(3, f'No Inbound SSO profile matches the name {displayName}')
else:
err_text = f'Multiple profiles match {displayName}:\n\n'
for m in matches:
err_text += f' {m["name"]} {m["displayName"]}\n'
controlflow.system_error_exit(3, err_text)
'''get an assignment based on target'''
def assignment_by_target(target, ci=None):
if not ci:
ci = build()
group_pattern = r'^groups/[^/]+$'
ou_pattern = r'^orgUnits/[^/]+$'
if re.match(group_pattern, target):
target_type = 'targetGroup'
elif re.match(ou_pattern, target):
target_type = 'targetOrgUnit'
elif target.lower().startswith('group:'):
target_type = 'targetGroup'
group_email = target[6:]
target = gapi_cloudidentity_groups.group_email_to_id(
ci,
group_email)
elif target.lower().startswith('orgunit:'):
target_type = 'targetOrgUnit'
ou_name = target[8:]
target = get_orgunit_id(ou_name)
else:
controlflow.system_error_exit(3, 'assignments should be prefixed with ' +
'group:, groups/, orgunit: or orgunits/')
customer = get_sso_customer()
_filter = f'customer=="{customer}"'
assignments = gapi.get_all_pages(ci.inboundSsoAssignments(),
'list',
'inboundSsoAssignments',
filter=_filter)
for assignment in assignments:
if target_type in assignment and assignment[target_type] == target:
return assignment
controlflow.system_error_exit(3, f'No SSO profile assigned to {target_type} {target}')
'''gam create inboundssoprofile'''
def create_profile():
ci = build()
body = {
'customer': get_sso_customer(),
'displayName': 'SSO Profile'
}
name_only, body = parse_profile(body, 3)
result = gapi.call(ci.inboundSamlSsoProfiles(),
'create',
body=body)
if result.get('done'):
if name_only:
print(result['response']['name'])
else:
print(f'Created profile {result["response"]["name"]}')
display.print_json(result['response'])
else:
controlflow.system_error_exit(3, 'Create did not finish {result}')
'''gam print inboundssoprofiles'''
def print_show_profiles(action='print'):
customer = get_sso_customer()
_filter = f'customer=="{customer}"'
ci = build()
todrive = False
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'todrive':
todrive = True
i += 1
else:
controlflow.invalid_argument_exit(myarg, f'gam {action} inboundssoprofiles')
profiles = gapi.get_all_pages(ci.inboundSamlSsoProfiles(),
'list',
'inboundSamlSsoProfiles',
filter=_filter)
if action == 'show':
for profile in profiles:
display.print_json(profile)
print()
elif action == 'print':
csv_rows = []
titles = []
for profile in profiles:
row = utils.flatten_json(profile)
for item in row:
if item not in titles:
titles.append(item)
csv_rows.append(row)
display.write_csv_file(csv_rows,
titles,
'Inbound SSO Profiles',
todrive)
'''gam update inboundssoprofile'''
def update_profile():
ci = build()
name = profile_displayname_to_name(sys.argv[3], ci)
body = {}
name_only, body = parse_profile(body, 4)
updateMask = ','.join(body.keys())
result = gapi.call(ci.inboundSamlSsoProfiles(),
'patch',
name=name,
updateMask=updateMask,
body=body)
if name_only:
print(result['response']['name'])
else:
display.print_json(result)
'''gam info inboundssoprofile'''
def info_profile(return_only=False, displayName=None, ci=None):
if not ci:
ci = build()
if not displayName:
displayName = sys.argv[3]
name = profile_displayname_to_name(displayName, ci)
result = gapi.call(ci.inboundSamlSsoProfiles(),
'get',
name=name)
if return_only:
return result
display.print_json(result)
'''gam delete inboundssoprofile'''
def delete_profile():
ci = build()
name = profile_displayname_to_name(sys.argv[3], ci)
result = gapi.call(ci.inboundSamlSsoProfiles(),
'delete',
name=name)
if result.get('done'):
print(f'Deleted profile {name}.')
else:
controlflow.system_error_exit(3, 'Delete did not finish: {result}')
'''gam create inboundssocredentials'''
def create_credentials():
allowed_sizes = [1024, 2048, 4096]
ci = build()
parent = None
generate_key = False
key_size = 2048
pemData = None
replace_oldest = False
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'profile':
parent = sys.argv[i+1]
parent = profile_displayname_to_name(parent, ci)
i += 2
elif myarg == 'pemfile':
pemfile = sys.argv[i+1]
pemData = fileutils.read_file(pemfile)
i += 2
elif myarg == 'generatekey':
generate_key = True
i += 1
elif myarg == 'replaceoldest':
replace_oldest = True
i += 1
elif myarg == 'keysize':
key_size = int(sys.argv[i+1])
if key_size not in allowed_sizes:
controlflow.expected_argument_exit('key_size',
allowed_sizes,
key_size)
i += 2
else:
controlflow.invalid_argument_exit(myarg, 'gam create inboundssocredential')
if not parent:
controlflow.missing_argument_exit('profile', 'gam create inboundssocredential')
if replace_oldest:
fields='nextPageToken,idpCredentials(name,updateTime)'
current_creds = gapi.get_all_pages(
ci.inboundSamlSsoProfiles().idpCredentials(),
'list',
'idpCredentials',
parent=parent,
fields=fields)
if len(current_creds) == 2:
oldest_key = min(current_creds,
key=lambda x:x['updateTime'])
print(' deleting older key...')
delete_credentials(ci=ci,
name=oldest_key['name'])
else:
print(' profile has {len(current_creds)} credentials. We only replace if there are 2.')
if generate_key:
privKey, pemData = gam._generatePrivateKeyAndPublicCert('GAM',
key_size,
b64enc_pub=False)
timestamp = datetime.now().strftime('%Y%m%d-%I%M%S')
priv_file = f'privatekey-{timestamp}.pem'
pub_file = f'publiccert-{timestamp}.pem'
fileutils.write_file(priv_file, privKey)
print(f' Wrote private key data to {priv_file}')
fileutils.write_file(pub_file, pemData)
print(f' Wrote public certificate to {pub_file}')
if not pemData:
controlflow.system_error_exit(3, 'You must either specify "pemfile <filename>" or "generate_key"')
body = {
'pemData': pemData,
}
result = gapi.call(ci.inboundSamlSsoProfiles().idpCredentials(),
'add',
parent=parent,
body=body)
if result.get('done'):
print(f'Created credential {result["response"]["name"]}')
display.print_json(result['response'])
else:
controlflow.system_error_exit(3, 'Create did not finish {result}')
'''gam delete inboundssocredential'''
def delete_credentials(ci=None, name=None):
if not ci:
ci = build()
if not name:
name = sys.argv[3]
result = gapi.call(ci.inboundSamlSsoProfiles().idpCredentials(),
'delete',
name=name)
if result.get('done'):
print(f'Deleted credential {name}')
else:
controlflow.system_error_exit(3, 'Delete did not finish {result}')
'''gam print inboundssocredentials'''
def print_show_credentials(action='print'):
ci = build()
todrive = False
i = 3
profiles = []
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg in ['profile', 'profiles']:
profiles = [profile_displayname_to_name(profile, ci)
for profile in sys.argv[i+1].split(',')]
i += 2
elif myarg == 'todrive':
todrive = True
i += 1
else:
controlflow.invalid_argument_exit(myarg, f'gam {action} inboundssocredentials')
if not profiles:
customer = get_sso_customer()
_filter = f'customer=="{customer}"'
profiles = gapi.get_all_pages(ci.inboundSamlSsoProfiles(),
'list',
'inboundSamlSsoProfiles',
fields='inboundSamlSsoProfiles/name',
filter=_filter)
profiles = [p['name'] for p in profiles]
if action == 'print':
titles = []
csv_rows = []
credentials = []
for profile in profiles:
results = gapi.get_all_pages(ci.inboundSamlSsoProfiles().idpCredentials(),
'list',
'idpCredentials',
parent=profile)
credentials.extend(results)
if action == 'show':
for c in credentials:
display.print_json(c)
print()
elif action == 'print':
for c in credentials:
csv_row = utils.flatten_json(c)
for item in csv_row:
if item not in titles:
titles.append(item)
csv_rows.append(csv_row)
display.write_csv_file(csv_rows,
titles,
'Inbound SSO Credentials',
todrive)
'''parse command for create/update inboundssoassignment'''
def parse_assignment(body, i, ci):
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'rank':
body['rank'] = int(sys.argv[i+1])
i += 2
elif myarg == 'mode':
mode_choices = \
gapi.get_enum_values_minus_unspecified(
ci._rootDesc['schemas']['InboundSsoAssignment']['properties']['ssoMode']['enum'])
body['ssoMode'] = sys.argv[i+1].upper()
if body['ssoMode'] not in mode_choices:
controlflow.expected_argument_exit('mode',
', '.join(mode_choices),
sys.argv[i+1])
i += 2
elif myarg == 'profile':
profile_name = profile_displayname_to_name(
sys.argv[i+1],
ci)
body['samlSsoInfo'] = {
'inboundSamlSsoProfile': profile_name
}
i += 2
elif myarg == 'neverredirect':
body['signInBehavior'] = {
'redirectCondition': 'NEVER'
}
i += 1
elif myarg == 'group':
group = sys.argv[i+1]
body['targetGroup'] = gapi_cloudidentity_groups.group_email_to_id(
ci,
group)
i += 2
elif myarg in ['ou', 'org', 'orgunit']:
body['targetOrgUnit'] = get_orgunit_id(sys.argv[i+1])
i += 2
else:
controlflow.invalid_argument_exit(myarg, 'gam create/update inboundssoassignment')
return body
def update_assignment_target_names(assignment, ci, cd):
if 'targetGroup' in assignment:
assignment['targetGroupEmail'] = \
gapi_cloudidentity_groups.group_id_to_email(ci,
assignment['targetGroup'])
elif 'targetOrgUnit' in assignment:
ou_id = assignment['targetOrgUnit'].split('/')[1]
assignment['targetOrgUnitPath'] = \
gapi_directory_orgunits.orgunit_from_orgunitid(f'id:{ou_id}', cd)
'''gam create inboundssoassignment'''
def create_assignment():
ci = build()
cd = gapi_directory.build()
body = {
'customer': get_sso_customer(),
}
body = parse_assignment(body, 3, ci)
result = gapi.call(ci.inboundSsoAssignments(),
'create',
body=body)
if result.get('done'):
print(f'Created assignment {result["response"]["name"]}')
update_assignment_target_names(result['response'], ci, cd)
display.print_json(result['response'])
else:
controlflow.system_error_exit(3, 'Create did not finish {result}')
def get_assignment_name(name):
if name.startswith('id:') or name.startswith('uid:'):
name = name.split(':', 1)[1]
if not name.startswith('inboundSsoAssignments/'):
name = f'inboundSsoAssignments/{name}'
return name
'''gam update inboundssoassignment'''
def update_assignment():
ci = build()
cd = gapi_directory.build()
name = get_assignment_name(sys.argv[3])
body = parse_assignment({}, 4, ci)
updateMask = ','.join(list(body.keys()))
result = gapi.call(ci.inboundSsoAssignments(),
'patch',
name=name,
updateMask=updateMask,
body=body)
if result.get('done'):
print(f'Updated assignment {result["response"]["name"]}')
update_assignment_target_names(result['response'], ci, cd)
display.print_json(result['response'])
else:
controlflow.system_error_exit(3, 'Update did not finish {result}')
'''gam info inboundssoassignment'''
def info_assignment():
ci = build()
cd = gapi_directory.build()
assignment = assignment_by_target(sys.argv[3], ci)
update_assignment_target_names(assignment, ci, cd)
profile = assignment.get('samlSsoInfo', {}).get('inboundSamlSsoProfile')
if profile:
assignment['samlSsoInfo']['inboundSamlSsoProfile'] = \
info_profile(return_only=True, displayName=f'id:{profile}', ci=ci)
display.print_json(assignment)
'''gam print inboundssoassignments'''
def print_show_assignments(action='print'):
ci = build()
cd = gapi_directory.build()
customer = get_sso_customer()
_filter = f'customer=="{customer}"'
todrive = False
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'todrive':
todrive = True
i += 1
else:
controlflow.invalid_argument_exit(myarg,
f'gam {action} inboundssoassignments')
assignments = gapi.get_all_pages(ci.inboundSsoAssignments(),
'list',
'inboundSsoAssignments',
filter=_filter)
if action == 'show':
for assignment in assignments:
update_assignment_target_names(assignment, ci, cd)
display.print_json(assignment)
print()
elif action == 'print':
titles = []
csv_rows = []
for assignment in assignments:
update_assignment_target_names(assignment, ci, cd)
csv_row = utils.flatten_json(assignment)
for item in csv_row:
if item not in titles:
titles.append(item)
csv_rows.append(csv_row)
display.write_csv_file(csv_rows,
titles,
'Inbound SSO Assignments',
todrive)

View File

@@ -189,7 +189,7 @@ def info(group_name=None):
pass
print('')
print('Group Settings:')
for key, value in list(basic_info.items()):
for key, value in sorted(list(basic_info.items())):
if (key in ['kind', 'etag']) or ((key == 'aliases') and
(not getAliases)):
continue
@@ -199,7 +199,7 @@ def info(group_name=None):
print(f' {val}')
else:
print(f' {key}: {value}')
for key, value in list(settings.items()):
for key, value in sorted(list(settings.items())):
if key in ['kind', 'etag', 'description', 'email', 'name']:
continue
print(f' {key}: {value}')
@@ -1217,6 +1217,8 @@ GROUP_SETTINGS_LIST_PATTERN = re.compile(r'([A-Z][A-Z_]+[A-Z]?)')
def getGroupAttrValue(myarg, value, gs_object, gs_body, function):
if myarg == 'collaborative':
myarg = 'enablecollaborativeinbox'
elif myarg == 'gal':
myarg = 'includeinglobaladdresslist'
for (attrib,
params) in list(gs_object['schemas']['Groups']['properties'].items()):
if attrib in ['kind', 'etag', 'email']:

View File

@@ -2,6 +2,7 @@ import sys
from time import sleep
import gam
from gam import controlflow
from gam import display
from gam import gapi
from gam.gapi import directory as gapi_directory
@@ -18,7 +19,7 @@ def delete():
userKey=user_email,
throw_reasons=[gapi_errors.ErrorReason.CONDITION_NOT_MET])
except gam.gapi.errors.GapiConditionNotMetError as err:
display.print_error(
controlflow.system_error_exit(3,
f'{err} The user {user_email} may be (or have recently been) on Google Vault Hold and thus not eligible for deletion. You can check holds with "gam user <email> show vaultholds".'
)

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
@@ -11,6 +12,7 @@ from gam import controlflow
from gam import display
from gam import fileutils
from gam import gapi
from gam.gapi import errors as gapi_errors
from gam.gapi import storage as gapi_storage
from gam.gapi import directory as gapi_directory
from gam.gapi.directory import orgunits as gapi_directory_orgunits
@@ -336,9 +338,9 @@ def print_count():
_validate_query(query, query_discovery)
body['query'] = query
operation = gapi.call(v.matters(), 'count', matterId=matterId, body=body)
print(f'Watching operation {operation["name"]}...')
sys.stderr.write(f'Watching operation {operation["name"]}...\n')
while not operation.get('done'):
print(f' operation {operation["name"]} is not done yet. Checking again in {operation_wait} seconds')
sys.stderr.write(f' operation {operation["name"]} is not done yet. Checking again in {operation_wait} seconds\n')
sleep(operation_wait)
operation = gapi.call(v.operations(), 'get', name=operation['name'])
response = operation.get('response', {})
@@ -518,7 +520,6 @@ def getHoldInfo():
def convertExportNameToID(v, nameOrID, matterId):
nameOrID = nameOrID.lower()
cg = UID_PATTERN.match(nameOrID)
if cg:
return cg.group(1)
@@ -529,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} '
@@ -682,18 +683,23 @@ def showHoldsForUsers(users):
v = buildGAPIObject()
matterIds = _getAllMatterIds(v)
matterHolds = {}
fields = 'holds(holdId,name,accounts(accountId,email),orgUnit),nextPageToken'
for matterId in matterIds:
matterHolds[matterId] = gapi.get_all_pages(v.matters().holds(),
'list',
'holds',
fields='holds(holdId,name,accounts(accountId,email),orgUnit),nextPageToken',
matterId=matterId)
try:
matterHolds[matterId] = gapi.get_all_pages(v.matters().holds(),
'list',
'holds',
fields=fields,
throw_reasons=[gapi_errors.ErrorReason.FOUR_O_O],
matterId=matterId)
except googleapiclient.errors.HttpError:
continue
totalHolds = 0
for user in users:
user = user.lower()
orgUnits = gapi_directory_orgunits._getAllParentOrgUnitsForUser(user, cd)
for matterId in matterIds:
for hold in matterHolds[matterId]:
for matterId, holds in matterHolds.items():
for hold in holds:
if 'orgUnit' in hold:
orgUnitId = hold['orgUnit'].get('orgUnitId')
if orgUnitId in orgUnits:
@@ -791,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]
@@ -823,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)
@@ -976,10 +1013,14 @@ def printHolds():
for matterId in matterIds:
i += 1
sys.stderr.write(f'Retrieving holds for matter {matterId} ({i}/{matter_count})\n')
holds = gapi.get_all_pages(v.matters().holds(),
try:
holds = gapi.get_all_pages(v.matters().holds(),
'list',
'holds',
throw_reasons=[gapi_errors.ErrorReason.FOUR_O_O],
matterId=matterId)
except googleapiclient.errors.HttpError:
continue
for hold in holds:
display.add_row_titles_to_csv_file(
utils.flatten_json(hold, flattened={'matterId': matterId}),

View File

@@ -96,9 +96,13 @@ class _DeHTMLParser(HTMLParser):
re.sub(r'\n +', '\n', ''.join(self.__text))).strip()
def commonprefix(m):
def commonprefix(m, checkEnum=False):
'''Given a list of strings m, return string which is prefix common to all'''
s1 = min(m)
if checkEnum:
loc = s1.find('ENUM_')
if loc > 0:
return s1[:loc+5]
s2 = max(m)
for i, c in enumerate(s1):
if c != s2[i]:

View File

@@ -8,7 +8,7 @@ import platform
import re
GAM_AUTHOR = 'Jay Lee <jay0lee@gmail.com>'
GAM_VERSION = '6.30'
GAM_VERSION = '6.50'
GAM_LICENSE = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
GAM_URL = 'https://jaylee.us/gam'
@@ -127,6 +127,16 @@ SKUS = {
'aliases': ['gwetlu', 'workspaceeducationupgrade'],
'displayName': 'Google Workspace for Education: Teaching and Learning Upgrade'
},
'1010390001': {
'product': '101039',
'aliases': ['assuredcontrols'],
'displayName': 'Assured Controls',
},
'1010400001': {
'product': '101040',
'aliases': ['beyondcorp', 'beyondcorpenterprise', 'bce'],
'displayName': 'Beyond Corp Enterprise',
},
'Google-Apps': {
'product': 'Google-Apps',
'aliases': ['standard', 'free'],
@@ -290,6 +300,8 @@ PRODUCTID_NAME_MAPPINGS = {
'101035': 'Cloud Search',
'101036': 'Google Meet Global Dialing',
'101037': 'G Suite Workspace for Education',
'101039': 'Assured Controls',
'101040': 'Beyond Corp',
'Google-Apps': 'Google Workspace',
'Google-Chrome-Device-Management': 'Google Chrome Device Management',
'Google-Drive-storage': 'Google Drive Storage',
@@ -972,6 +984,8 @@ CROS_ARGUMENT_TO_PROPERTY_MAP = {
'autoupdateexpiration': ['autoUpdateExpiration',],
'bootmode': ['bootMode',],
'cpustatusreports': ['cpuStatusReports',],
'deprovisionreason': ['deprovisionReason',],
'lastDeprovisionTimestamp': ['lastDeprovisionTimestamp',],
'devicefiles': ['deviceFiles',],
'deviceid': ['deviceId',],
'dockmacaddress': ['dockMacAddress',],
@@ -979,6 +993,7 @@ CROS_ARGUMENT_TO_PROPERTY_MAP = {
'ethernetmacaddress': ['ethernetMacAddress',],
'ethernetmacaddress0': ['ethernetMacAddress0',],
'firmwareversion': ['firmwareVersion',],
'firstenrollmenttime': ['firstEnrollmentTime',],
'lastenrollmenttime': ['lastEnrollmentTime',],
'lastknownnetwork': ['lastKnownNetwork'],
'lastsync': ['lastSync',],
@@ -1036,7 +1051,10 @@ CROS_SCALAR_PROPERTY_PRINT_ORDER = [
'ethernetMacAddress0',
'macAddress',
'systemRamTotal',
'firstEnrollmentTime',
'lastEnrollmentTime',
'deprovisionReason',
'lastDeprovisionTimestamp',
'orderNumber',
'manufactureDate',
'supportEndDate',

View File

@@ -10,4 +10,4 @@ importlib.metadata; python_version < '3.8'
passlib>=1.7.2
pathvalidate
python-dateutil
yubikey-manager>=4.0.0
yubikey-manager>=5.0