Compare commits

..

55 Commits
v6.31 ... v6.5

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
18 changed files with 848 additions and 204 deletions

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
@@ -667,6 +702,7 @@ jobs:
$gam update matter $matterid action delete
# 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
@@ -696,17 +732,18 @@ jobs:
$gam user $gam_user delete shareddrive "${driveid}" nukefromorbit
echo "printer model count:"
$gam print printermodels | wc -l
$gam create inboundssoprofile name "El Goog ${newbase}" loginurl https://www.google.com logouturl https://www.google.com changepasswordurl https://www.google.com entityid ElGoog
$gam create inboundssocredential profile "El Goog ${newbase}" generate_key
$gam create inboundssoassignment profile "El Goog ${newbase}" orgunit "${newou}" mode SAML_SSO
#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

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]
@@ -1555,8 +1568,10 @@ gam print group-members|groups-members [todrive]
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
@@ -1672,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>]
@@ -1684,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
@@ -782,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)
@@ -925,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])
@@ -940,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)
@@ -953,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']
@@ -1078,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]))
@@ -1295,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,
@@ -1314,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:
@@ -4473,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):
@@ -7162,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,
@@ -7446,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]'
@@ -7817,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',
@@ -7837,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,
@@ -7912,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()
@@ -8167,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':
@@ -8206,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:
@@ -10745,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': [],
@@ -10794,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.
@@ -10823,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
@@ -10913,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
@@ -10925,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
@@ -10954,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."""
@@ -11552,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)
@@ -11891,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']:
@@ -12021,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']:
@@ -12069,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

@@ -219,7 +219,7 @@ def printShowCrosTelemetry(mode):
i = 3
if mode == 'info':
if i >= len(sys.argv):
controlflow.system_error_exit(3, f'<SerialNumber> required for "gam info crostelemetry"')
controlflow.system_error_exit(3, '<SerialNumber> required for "gam info crostelemetry"')
filter_ = f'serialNumber={sys.argv[i]}'
i += 1
mode = 'show'
@@ -307,6 +307,97 @@ def printShowCrosTelemetry(mode):
display.write_csv_file(csvRows, titles, 'Telemetry Devices', todrive)
CHROME_AUES_TITLES = [
'model', 'count', 'aueMonth', 'aueYear', 'expired'
]
def printAUEs():
cm = build()
customer = _get_customerid()
todrive = False
titles = CHROME_AUES_TITLES
csvRows = []
orgunit = None
minAueDate = None
maxAueDate = None
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'todrive':
todrive = True
i += 1
elif myarg in ['ou', 'org', 'orgunit']:
orgunit = _get_orgunit(sys.argv[i+1])
i += 2
elif myarg == 'minauedate':
minAueDate = _getFilterDate(sys.argv[i + 1]).strftime(YYYYMMDD_FORMAT)
i += 2
elif myarg == 'maxauedate':
maxAueDate = _getFilterDate(sys.argv[i + 1]).strftime(YYYYMMDD_FORMAT)
i += 2
else:
msg = f'{myarg} is not a valid argument to "gam print chromeaues"'
controlflow.system_error_exit(3, msg)
if orgunit:
orgUnitPath = gapi_directory_orgunits.orgunit_from_orgunitid(orgunit, None)
query = f'orgUnitPath={orgUnitPath}'
titles.append('orgUnitPath')
else:
orgUnitPath = '/'
query = None
gam.printGettingAllItems('Chrome Auto Update Expirations', query)
aues = gapi.call(cm.customers().reports(),
'countChromeDevicesReachingAutoExpirationDate',
customer=customer, orgUnitId=orgunit,
minAueDate=minAueDate, maxAueDate=maxAueDate).get('deviceAueCountReports', [])
for aue in sorted(aues, key=lambda k: k.get('model', 'Unknown')):
if orgunit:
aue['orgUnitPath'] = orgUnitPath
csvRows.append(aue)
display.write_csv_file(csvRows, titles, 'Chrome AUEs', todrive)
CHROME_NEEDSATTN_TITLES = [
'noRecentPolicySyncCount', 'noRecentUserActivityCount', 'pendingUpdate',
'osVersionNotCompliantCount', 'unsupportedPolicyCount'
]
def printNeedsAttn():
cm = build()
customer = _get_customerid()
todrive = False
titles = CHROME_NEEDSATTN_TITLES[:]
csvRows = []
orgunit = None
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'todrive':
todrive = True
i += 1
elif myarg in ['ou', 'org', 'orgunit']:
orgunit = _get_orgunit(sys.argv[i+1])
i += 2
else:
msg = f'{myarg} is not a valid argument to "gam print chromeneedsattn"'
controlflow.system_error_exit(3, msg)
if orgunit:
orgUnitPath = gapi_directory_orgunits.orgunit_from_orgunitid(orgunit, None)
query = f'orgUnitPath={orgUnitPath}'
titles.append('orgUnitPath')
else:
orgUnitPath = '/'
query = None
gam.printGettingAllItems('Chrome Devices Needing Attention', query)
result = gapi.call(cm.customers().reports(),
'countChromeDevicesThatNeedAttention',
customer=customer, orgUnitId=orgunit, readMask=','.join(CHROME_NEEDSATTN_TITLES))
for field in CHROME_NEEDSATTN_TITLES:
result.setdefault(field, 0)
if orgunit:
result['orgUnitPath'] = orgUnitPath
csvRows.append(result)
display.write_csv_file(csvRows, titles, 'Chrome Devices Needing Attention', todrive)
CHROME_VERSIONS_TITLES = [
'version', 'count', 'channel', 'deviceOsVersion', 'system'
]
@@ -320,6 +411,7 @@ def printVersions():
startDate = None
endDate = None
pfilter = None
query = None
reverse = False
i = 3
while i < len(sys.argv):
@@ -350,12 +442,18 @@ def printVersions():
else:
pfilter = ''
pfilter += f'last_active_date>={startDate}'
query = pfilter
if orgunit:
orgUnitPath = gapi_directory_orgunits.orgunit_from_orgunitid(orgunit, None)
if query:
query += ' AND '
else:
query = ''
query += f'orgUnitPath={orgUnitPath}'
titles.append('orgUnitPath')
else:
orgUnitPath = '/'
gam.printGettingAllItems('Chrome Versions', pfilter)
gam.printGettingAllItems('Chrome Versions', query)
page_message = gapi.got_total_items_msg('Chrome Versions', '...\n')
versions = gapi.get_all_pages(cm.customers().reports(),
'countChromeVersions',

View File

@@ -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:

View File

@@ -40,6 +40,7 @@ def build():
'''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':
@@ -48,6 +49,9 @@ def parse_profile(body, i):
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
@@ -59,7 +63,7 @@ def parse_profile(body, i):
i += 2
else:
controlflow.invalid_argument_exit(myarg, 'gam create/update inboundssoprofile')
return body
return (name_only, body)
'''convert profile nice names to unique ID'''
@@ -134,16 +138,20 @@ def create_profile():
'customer': get_sso_customer(),
'displayName': 'SSO Profile'
}
body = parse_profile(body, 3)
name_only, body = parse_profile(body, 3)
result = gapi.call(ci.inboundSamlSsoProfiles(),
'create',
body=body)
if result.get('done'):
print(f'Created profile {result["response"]["name"]}')
display.print_json(result['response'])
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()
@@ -187,14 +195,17 @@ def update_profile():
ci = build()
name = profile_displayname_to_name(sys.argv[3], ci)
body = {}
body = parse_profile(body, 4)
name_only, body = parse_profile(body, 4)
updateMask = ','.join(body.keys())
result = gapi.call(ci.inboundSamlSsoProfiles(),
'patch',
name=name,
updateMask=updateMask,
body=body)
display.print_json(result)
if name_only:
print(result['response']['name'])
else:
display.print_json(result)
'''gam info inboundssoprofile'''

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
@@ -337,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', {})
@@ -519,7 +520,6 @@ def getHoldInfo():
def convertExportNameToID(v, nameOrID, matterId):
nameOrID = nameOrID.lower()
cg = UID_PATTERN.match(nameOrID)
if cg:
return cg.group(1)
@@ -530,7 +530,7 @@ def convertExportNameToID(v, nameOrID, matterId):
matterId=matterId,
fields=fields)
for export in exports:
if export['name'].lower() == nameOrID:
if export['name'].lower() == nameOrID.lower():
return export['id']
controlflow.system_error_exit(
4, f'could not find export name {nameOrID} '
@@ -683,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:
@@ -792,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]
@@ -824,24 +867,17 @@ def downloadExport():
for s_file in export['cloudStorageSink']['files']:
bucket = s_file['bucketName']
s_object = s_file['objectName']
filename = os.path.join(targetFolder, s_object.replace('/', '-'))
print(f'saving to {filename}')
request = s.objects().get_media(bucket=bucket, object=s_object)
f = fileutils.open_file(filename, 'wb')
downloader = googleapiclient.http.MediaIoBaseDownload(f, request)
done = False
while not done:
status, done = downloader.next_chunk()
sys.stdout.write(f' Downloaded: {status.progress():>7.2%}\r')
sys.stdout.flush()
sys.stdout.write('\n Download complete. Flushing to disk...\n')
fileutils.close_file(f, True)
if verifyFiles:
expected_hash = s_file['md5Hash']
sys.stdout.write(f' Verifying file hash is {expected_hash}...')
sys.stdout.flush()
utils.md5_matches_file(filename, expected_hash, True)
print('VERIFIED')
else:
expected_hash = None
local_file = s_object.replace('/', '-').replace(':', '-')
filename = os.path.join(targetFolder, local_file)
gapi_storage.get_cloud_storage_object(s,
bucket,
s_object,
local_file=filename,
expectedMd5=expected_hash)
if extractFiles and re.search(r'\.zip$', filename):
gam.extract_nested_zip(filename, targetFolder)

View File

@@ -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.31'
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