mirror of
https://github.com/GAM-team/GAM.git
synced 2026-06-04 22:31:38 +00:00
Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3d560a8a2 | ||
|
|
ed20fe252e | ||
|
|
375e36ff96 | ||
|
|
e7108b108e | ||
|
|
6d59daad19 | ||
|
|
21c693921b | ||
|
|
7bcd5fbed7 | ||
|
|
7104970e17 | ||
|
|
1a2950b580 | ||
|
|
085b24e1c5 | ||
|
|
8688ce6328 | ||
|
|
fbdfed81e7 | ||
|
|
94fe20607e | ||
|
|
6c62483e8e | ||
|
|
54689129c6 | ||
|
|
e9e8dd5a82 | ||
|
|
00e764b118 | ||
|
|
cee7eb970a | ||
|
|
daed17fac8 | ||
|
|
8708f4f93f | ||
|
|
c7c1bfbeba | ||
|
|
0418438b6f | ||
|
|
a2ea4d036e | ||
|
|
dc7a29908f | ||
|
|
794db5d2a4 | ||
|
|
e5f9db129b | ||
|
|
a6aecf4e9d | ||
|
|
b59bc4ec90 | ||
|
|
41920f7865 | ||
|
|
4630bf5681 | ||
|
|
1c78ebd20e | ||
|
|
80d17cfda3 | ||
|
|
a154007927 | ||
|
|
bd8274cc27 | ||
|
|
fb08991c05 | ||
|
|
7c1f06fdf7 | ||
|
|
93b38b9f95 | ||
|
|
7ffc97d301 | ||
|
|
280301f258 | ||
|
|
40daf38f80 | ||
|
|
d24925cd5f | ||
|
|
cd42d54b43 | ||
|
|
53d8ecb6bc | ||
|
|
98e87d0297 | ||
|
|
400b4af769 | ||
|
|
368701afb1 | ||
|
|
a501b89ecd |
6
.github/actions/macos-before-install.sh
vendored
6
.github/actions/macos-before-install.sh
vendored
@@ -22,11 +22,7 @@ cd ~
|
||||
|
||||
# Use official Python.org version of Python which is backwards compatible
|
||||
# with older MacOS versions
|
||||
if [ "$PLATFORM" == "x86_64" ]; then
|
||||
export pyfile=python-$BUILD_PYTHON_VERSION-macosx10.9.pkg
|
||||
else
|
||||
export pyfile=python-$BUILD_PYTHON_VERSION-macos11.pkg
|
||||
fi
|
||||
export pyfile=python-$BUILD_PYTHON_VERSION-macos11.pkg
|
||||
|
||||
wget https://www.python.org/ftp/python/$BUILD_PYTHON_VERSION/$pyfile
|
||||
echo "installing Python $BUILD_PYTHON_VERSION..."
|
||||
|
||||
4
.github/actions/windows-before-install.sh
vendored
4
.github/actions/windows-before-install.sh
vendored
@@ -13,8 +13,8 @@ echo "This is a ${BITS}-bit build for ${PLATFORM}"
|
||||
export mypath=$(pwd)
|
||||
cd ~
|
||||
|
||||
export python="python"
|
||||
export pip="pip"
|
||||
export python="c:\python\python.exe"
|
||||
export pip="c:\python\scripts\pip.exe"
|
||||
|
||||
# pyscard needs swig, keep these two together
|
||||
choco install $CHOCOPTIONS swig
|
||||
|
||||
4
.github/actions/windows-install.sh
vendored
4
.github/actions/windows-install.sh
vendored
@@ -7,9 +7,7 @@ echo "compiling GAM with pyinstaller..."
|
||||
export distpath="dist/"
|
||||
export gampath="${distpath}gam"
|
||||
rm -rf $gampath
|
||||
#mkdir -p $gampath
|
||||
#export gampath=$(readlink -e $gampath)
|
||||
pyinstaller --clean --noupx --distpath $gampath gam.spec
|
||||
/c/python/scripts/pyinstaller --clean --noupx --distpath $gampath gam.spec
|
||||
export gam="${gampath}/gam"
|
||||
echo "running compiled GAM..."
|
||||
$gam version
|
||||
|
||||
37
.github/workflows/build.yml
vendored
37
.github/workflows/build.yml
vendored
@@ -12,13 +12,13 @@ defaults:
|
||||
working-directory: src
|
||||
|
||||
env:
|
||||
BUILD_PYTHON_VERSION: "3.9.5"
|
||||
MIN_PYTHON_VERSION: "3.9.5"
|
||||
BUILD_PYTHON_VERSION: "3.9.6"
|
||||
MIN_PYTHON_VERSION: "3.9.6"
|
||||
BUILD_OPENSSL_VERSION: "1.1.1k"
|
||||
MIN_OPENSSL_VERSION: "1.1.1k"
|
||||
PATCHELF_VERSION: "0.12"
|
||||
# PYINSTALLER_VERSION can be full commit hash or version like v4.20
|
||||
PYINSTALLER_VERSION: "000275e409640320cdd995a7f077abfdece86749"
|
||||
PYINSTALLER_VERSION: "0f2b2e921433ab5a510c7efdb21d9c1d7cfbc645"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -41,11 +41,6 @@ jobs:
|
||||
goal: "build"
|
||||
gamos: "linux"
|
||||
platform: "x86_64"
|
||||
- os: macos-10.15
|
||||
jid: 4
|
||||
goal: "build"
|
||||
gamos: "macos"
|
||||
platform: "x86_64"
|
||||
- os: macos-11.0
|
||||
jid: 12
|
||||
goal: "build"
|
||||
@@ -55,7 +50,6 @@ jobs:
|
||||
jid: 5
|
||||
goal: "build"
|
||||
gamos: "windows"
|
||||
python: 3.9.5
|
||||
pyarch: "x64"
|
||||
platform: "x86_64"
|
||||
- os: windows-2019
|
||||
@@ -63,7 +57,6 @@ jobs:
|
||||
goal: "build"
|
||||
gamos: "windows"
|
||||
platform: "x86"
|
||||
python: 3.9.5
|
||||
pyarch: "x86"
|
||||
- os: ubuntu-20.04
|
||||
goal: "test"
|
||||
@@ -104,7 +97,7 @@ jobs:
|
||||
path: |
|
||||
~/python
|
||||
~/ssl
|
||||
key: ${{ matrix.os }}-${{ matrix.jid }}-20210611
|
||||
key: ${{ matrix.os }}-${{ matrix.jid }}-20210628
|
||||
|
||||
- name: Set env variables
|
||||
env:
|
||||
@@ -119,13 +112,33 @@ jobs:
|
||||
echo "PLATFORM=${PLATFORM}" >> $GITHUB_ENV
|
||||
uname -a
|
||||
|
||||
- name: Use pre-compiled Python for testing and Windows
|
||||
- name: Use pre-compiled Python for testing
|
||||
if: matrix.python != ''
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
architecture: ${{ matrix.pyarch }}
|
||||
|
||||
- name: Install Python on Windows
|
||||
if: matrix.os == 'windows-2019'
|
||||
run: |
|
||||
if ( ${Env:PLATFORM} -eq "x86_64" )
|
||||
{
|
||||
Set-Variable -name py_arch -value "-amd64"
|
||||
}
|
||||
else
|
||||
{
|
||||
Set-Variable -name py_arch -value ""
|
||||
}
|
||||
Write-Output "py_arch: $py_arch"
|
||||
Set-Variable -name python_file -value "python-${Env:BUILD_PYTHON_VERSION}${py_arch}.exe"
|
||||
Write-Output "python_file: $python_file"
|
||||
Set-Variable -name python_url -value "https://www.python.org/ftp/python/${Env:BUILD_PYTHON_VERSION}/${python_file}"
|
||||
Write-Output "python_url: $python_url"
|
||||
Invoke-WebRequest -Uri $python_url -OutFile $python_file
|
||||
Start-Process -wait -FilePath $python_file -ArgumentList "/quiet","InstallAllUsers=0","TargetDir=c:\\python","AssociateFiles=1","PrependPath=1"
|
||||
shell: pwsh
|
||||
|
||||
- name: Set env variables for pre-compiled Python
|
||||
if: matrix.goal == 'test'
|
||||
run: |
|
||||
|
||||
@@ -204,6 +204,7 @@ If an item contains spaces, it should be surrounded by ".
|
||||
<MaximumNumberOfSeats> ::= <Number>
|
||||
<MobileID> ::= <String>
|
||||
<Name> ::= <String>
|
||||
<Namespace> ::= <String>
|
||||
<NotificationID> ::= <String>
|
||||
<NumberOfSeats> ::= <Number>
|
||||
<OrgUnitID> ::= <String>
|
||||
@@ -333,6 +334,7 @@ If an item contains spaces, it should be surrounded by ".
|
||||
lastmodifyinguser|
|
||||
lastmodifyingusername|
|
||||
lastviewedbyme|lastviewedbymedate|lastviewedbymetime|lastviewedbyuser|
|
||||
linksharemetadata|
|
||||
md5|md5checksum|md5sum|
|
||||
mime|mimetype|
|
||||
modifiedbyme|modifiedbymedate|modifiedbymetime|modifiedbyuser|
|
||||
@@ -345,6 +347,7 @@ If an item contains spaces, it should be surrounded by ".
|
||||
parents|
|
||||
permissions|
|
||||
quotabytesused|quotaused|
|
||||
resourcekey|
|
||||
restricted|
|
||||
shareable|
|
||||
shared|
|
||||
@@ -592,6 +595,7 @@ Items, separated by spaces, with spaces, commas or single quotes in the items th
|
||||
<MatterItemList> ::= "<MatterItem>(,<MatterItem>)*"
|
||||
<MembersFieldNameList> ::= "<MembersFieldName>(,<MembersFieldName>)*"
|
||||
<MobileList> ::= "<MobileId>(,<MobileId>)*"
|
||||
<NamespaceList> ::= "<Namespace>(,<Namespace)*"
|
||||
<OrgUnitList> ::= "<OrgUnitPath>(,<OrgUnitPath>)*"
|
||||
<PrinterIDList> ::= "<PrinterID>)(,<PrinterID>)*"
|
||||
<ProductIDList> ::= "(<ProductID>|SKUID>)(,<ProductID>|SKUID>)*"
|
||||
@@ -695,6 +699,7 @@ Specify a collection of Users by directly specifying them or by specifiying item
|
||||
copyrequireswriterpermission|
|
||||
(lastviewedbyme <Time>)|(modifieddate|modifiedtime <Time>)|(description <String>)|(mimetype <MimeType>)|
|
||||
(parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|writerscantshare|writerscanshare
|
||||
(securityupdate <Boolean>)|
|
||||
(shortcut <DriveFileID>)
|
||||
<DriveFileUpdateAttribute> ::=
|
||||
(localfile <FileName>|-)|
|
||||
@@ -705,6 +710,7 @@ Specify a collection of Users by directly specifying them or by specifiying item
|
||||
(copyrequireswriterpermission <Boolean>)|
|
||||
(lastviewedbyme <Time>)|(modifieddate <Time>)|(description <String>)|(mimetype <MimeType>)|
|
||||
(parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|writerscantshare|writerscanshare
|
||||
(securityupdate <Boolean>)|
|
||||
(shortcut <DriveFileID>)
|
||||
<GroupSettingsAttribute> ::=
|
||||
(allowexternalmembers <Boolean>)|
|
||||
@@ -843,6 +849,8 @@ An argument containing instances of ~~xxx~~ has xxx replaced by the value of fie
|
||||
Example: gam csv Users.csv gam update user "~primaryEmail" address type work unstructured "~~Street~~, ~~City~~, ~~State~~ ~~ZIP~~"
|
||||
Each user (~primaryEmail, e.g. foo@bar.com) would have their work address updated
|
||||
|
||||
gam create gcpfolder <String>
|
||||
|
||||
gam create project [<EmailAddress>] [<ProjectID>]
|
||||
gam create project [admin <EmailAddress>] [project <ProjectID>] [parent <String>]
|
||||
gam use project [<EmailAddress>] [<ProjectID>]
|
||||
@@ -955,6 +963,7 @@ gam report <ActivityApplicationName> [todrive]
|
||||
[(user all|<UserItem>)|(orgunit|org|ou <OrgUnitPath>)]
|
||||
[start <Time>] [end <Time>]
|
||||
[filter|filters <String>] [event <String>] [ip <String>]
|
||||
[groupidfilter <String>]
|
||||
|
||||
gam create admin <UserItem> <RoleItem> customer|(org_unit <OrgUnitItem>)
|
||||
gam delete admin <RoleAssignmentId>
|
||||
@@ -1308,7 +1317,7 @@ gam print chromehistory releases [todrive]
|
||||
|
||||
gam delete chromepolicy <SchemaName>+ ou|org|orgunit <OrgUnitItem> [(printerid <PrinterID>)|(appid <AppID>)]
|
||||
gam update chromepolicy (<SchemaName> (<Field> <Value>)+)+ ou|org|orgunit <OrgUnitItem> [(printerid <PrinterID>)|(appid <AppID>)]
|
||||
gam show chromepolicy ou|org|orgunit <OrgUnitItem> [(printerid <PrinterID>)|(appid <AppID>)]
|
||||
gam show chromepolicy ou|org|orgunit <OrgUnitItem> [(printerid <PrinterID>)|(appid <AppID>)] [namespace <NamespaceList>]
|
||||
gam show chromeschema [filter <String>]
|
||||
|
||||
<DeviceID> ::= devices/<String>
|
||||
|
||||
@@ -5,8 +5,6 @@ import sys
|
||||
import importlib
|
||||
from PyInstaller.utils.hooks import copy_metadata
|
||||
|
||||
sys.modules['FixTk'] = None
|
||||
|
||||
# dynamically determine where httplib2/cacerts.txt lives
|
||||
proot = os.path.dirname(importlib.import_module('httplib2').__file__)
|
||||
extra_files = [(os.path.join(proot, 'cacerts.txt'), 'httplib2')]
|
||||
|
||||
@@ -871,9 +871,9 @@ def getSvcAcctCredentials(scopes, act_as, api=None):
|
||||
GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID] = GM_Globals[
|
||||
GM_OAUTH2SERVICE_JSON_DATA]['client_id']
|
||||
return credentials
|
||||
except (ValueError, KeyError):
|
||||
except (ValueError, KeyError) as err:
|
||||
printLine(MESSAGE_INSTRUCTIONS_OAUTH2SERVICE_JSON)
|
||||
controlflow.invalid_json_exit(GC_Values[GC_OAUTH2SERVICE_JSON])
|
||||
controlflow.invalid_json_exit(GC_Values[GC_OAUTH2SERVICE_JSON], err)
|
||||
|
||||
|
||||
def getAPIVersion(api):
|
||||
@@ -899,8 +899,8 @@ def readDiscoveryFile(api_version):
|
||||
try:
|
||||
discovery = json.loads(json_string)
|
||||
return (disc_file, discovery)
|
||||
except ValueError:
|
||||
controlflow.invalid_json_exit(disc_file)
|
||||
except ValueError as err:
|
||||
controlflow.invalid_json_exit(disc_file, err)
|
||||
|
||||
|
||||
def getOauth2TxtStorageCredentials():
|
||||
@@ -1473,8 +1473,8 @@ def addDelegates(users, i):
|
||||
body={'delegateEmail': delegate})
|
||||
|
||||
|
||||
def gen_sha512_hash(password):
|
||||
return sha512_crypt.hash(password, rounds=5000)
|
||||
def gen_sha512_hash(password, rounds=10000):
|
||||
return sha512_crypt.hash(password, rounds=rounds)
|
||||
|
||||
|
||||
def printShowDelegates(users, csvFormat):
|
||||
@@ -3719,6 +3719,10 @@ def getDriveFileAttribute(i, body, parameters, myarg, update=False):
|
||||
body['mimeType'] = MIMETYPE_GA_SHORTCUT
|
||||
body['shortcutDetails'] = {'targetId': sys.argv[i+1]}
|
||||
i += 2
|
||||
elif myarg == 'securityupdate':
|
||||
body['linkShareMetadata'] = {'securityUpdateEnabled': getBoolean(
|
||||
sys.argv[i+1], f'gam <users> {operation} drivefile'), 'securityUpdateEligible': True}
|
||||
i += 2
|
||||
else:
|
||||
controlflow.invalid_argument_exit(
|
||||
myarg, f"gam <users> {operation} drivefile")
|
||||
@@ -7124,9 +7128,14 @@ def getUserAttributes(i, cd, updateCmd):
|
||||
controlflow.invalid_argument_exit(
|
||||
sys.argv[i], f"gam {['create', 'update'][updateCmd]} user")
|
||||
if need_password:
|
||||
# generate a password with unicode chars that are not allowed in
|
||||
# passwords. We expect "password random nohash" to fail but no one
|
||||
# should be using that. Our goal here is to purposefully block login
|
||||
# with this password.
|
||||
pass_chars = [chr(i) for i in range(1, 55296)]
|
||||
rnd = SystemRandom()
|
||||
body['password'] = ''.join(
|
||||
rnd.choice(PASSWORD_SAFE_CHARS) for _ in range(100))
|
||||
rnd.choice(pass_chars) for _ in range(4096))
|
||||
if 'password' in body and need_to_hash_password:
|
||||
body['password'] = gen_sha512_hash(body['password'])
|
||||
body['hashFunction'] = 'crypt'
|
||||
@@ -7149,12 +7158,7 @@ def getCRMService(login_hint):
|
||||
login_hint=login_hint,
|
||||
use_console_flow=not GC_Values[GC_OAUTH_BROWSER])
|
||||
httpc = transport.AuthorizedHttp(creds, transport.create_http())
|
||||
return getService('cloudresourcemanagerv1', httpc), httpc
|
||||
|
||||
|
||||
# Ugh, v2 doesn't contain all the operations of v1 so we need to use both here.
|
||||
def getCRM2Service(httpc):
|
||||
return getService('cloudresourcemanager', httpc)
|
||||
return getService('cloudresourcemanager', httpc), httpc
|
||||
|
||||
|
||||
def getGAMProjectFile(filepath):
|
||||
@@ -7448,10 +7452,10 @@ def _getProjects(crm, pfilter):
|
||||
try:
|
||||
return gapi.get_all_pages(
|
||||
crm.projects(),
|
||||
'list',
|
||||
'search',
|
||||
'projects',
|
||||
throw_reasons=[gapi_errors.ErrorReason.BAD_REQUEST],
|
||||
filter=pfilter)
|
||||
query=pfilter)
|
||||
except gapi_errors.GapiBadRequestError as e:
|
||||
controlflow.system_error_exit(2, f'Project: {pfilter}, {str(e)}')
|
||||
|
||||
@@ -7513,23 +7517,15 @@ def _getLoginHintProjectId(createCmd):
|
||||
f'Invalid Project ID: {projectId}, expected <{PROJECTID_FORMAT_REQUIRED}>'
|
||||
)
|
||||
crm, httpObj = getCRMService(login_hint)
|
||||
if parent and not parent.startswith(
|
||||
'organizations/') and not parent.startswith('folders/'):
|
||||
crm2 = getCRM2Service(httpObj)
|
||||
parent = convertGCPFolderNameToID(parent, crm2)
|
||||
if parent:
|
||||
parent_type, parent_id = parent.split('/')
|
||||
if parent_type[-1] == 's':
|
||||
parent_type = parent_type[:
|
||||
-1] # folders > folder, organizations > organization
|
||||
parent = {'type': parent_type, 'id': parent_id}
|
||||
if parent and not parent.startswith('organizations/') and not parent.startswith('folders/'):
|
||||
parent = convertGCPFolderNameToID(parent, crm)
|
||||
projects = _getProjects(crm, f'id:{projectId}')
|
||||
if not createCmd:
|
||||
if not projects:
|
||||
controlflow.system_error_exit(
|
||||
2,
|
||||
f'User: {login_hint}, Project ID: {projectId}, Does not exist')
|
||||
if projects[0]['lifecycleState'] != 'ACTIVE':
|
||||
if projects[0]['state'] != 'ACTIVE':
|
||||
controlflow.system_error_exit(
|
||||
2, f'User: {login_hint}, Project ID: {projectId}, Not active')
|
||||
else:
|
||||
@@ -7542,17 +7538,11 @@ def _getLoginHintProjectId(createCmd):
|
||||
PROJECTID_FILTER_REQUIRED = 'gam|<ProjectID>|(filter <String>)'
|
||||
|
||||
|
||||
def convertGCPFolderNameToID(parent, crm2):
|
||||
# crm2.folders() is broken requiring pageToken, etc in body, not URL.
|
||||
# for now just use gapi.get_items and if user has that many folders they'll
|
||||
# just need to be specific.
|
||||
folders = gapi.get_items(crm2.folders(),
|
||||
'search',
|
||||
items='folders',
|
||||
body={
|
||||
'pageSize': 1000,
|
||||
'query': f'displayName="{parent}"'
|
||||
})
|
||||
def convertGCPFolderNameToID(parent, crm):
|
||||
folders = gapi.get_all_pages(crm.folders(),
|
||||
'search',
|
||||
'folders',
|
||||
query=f'displayName="{parent}"')
|
||||
if not folders:
|
||||
controlflow.system_error_exit(
|
||||
1, f'ERROR: No folder found matching displayName={parent}')
|
||||
@@ -7566,15 +7556,14 @@ def convertGCPFolderNameToID(parent, crm2):
|
||||
|
||||
|
||||
def createGCPFolder():
|
||||
displayName = sys.argv[3]
|
||||
login_hint = _getValidateLoginHint()
|
||||
_, httpObj = getCRMService(login_hint)
|
||||
crm2 = getCRM2Service(httpObj)
|
||||
gapi.call(crm2.folders(),
|
||||
'create',
|
||||
body={
|
||||
'name': sys.argv[3],
|
||||
'displayName': sys.argv[3]
|
||||
})
|
||||
login_domain = login_hint.split('@')[-1]
|
||||
crm, _ = getCRMService(login_hint)
|
||||
organization = getGCPOrg(crm, login_domain)
|
||||
result = gapi.call(crm.folders(), 'create',
|
||||
body={'parent': organization, 'displayName': displayName})
|
||||
print(f'User: {login_hint}, Folder: {displayName}, GCP Folder Name: {result["name"]}, Created')
|
||||
|
||||
|
||||
def _getLoginHintProjects(printShowCmd):
|
||||
@@ -7628,16 +7617,31 @@ def _checkForExistingProjectFiles():
|
||||
)
|
||||
|
||||
|
||||
def getGCPOrg(crm, domain):
|
||||
resp = gapi.call(crm.organizations(),
|
||||
'search',
|
||||
query=f'domain:{domain}')
|
||||
try:
|
||||
organization = resp['organizations'][0]['name']
|
||||
print(f'Your organization name is {organization}')
|
||||
return organization
|
||||
except (KeyError, IndexError):
|
||||
controlflow.system_error_exit(
|
||||
3,
|
||||
'you have no rights to create projects for your organization and you don\'t seem to be a super admin! Sorry, there\'s nothing more I can do.'
|
||||
)
|
||||
|
||||
|
||||
def doCreateProject():
|
||||
_checkForExistingProjectFiles()
|
||||
crm, httpObj, login_hint, projectId, parent = _getLoginHintProjectId(True)
|
||||
login_domain = login_hint[login_hint.find('@') + 1:]
|
||||
body = {'projectId': projectId, 'name': 'GAM Project'}
|
||||
body = {'projectId': projectId, 'displayName': 'GAM Project'}
|
||||
if parent:
|
||||
body['parent'] = parent
|
||||
while True:
|
||||
create_again = False
|
||||
print(f'Creating project "{body["name"]}"...')
|
||||
print(f'Creating project "{body["displayName"]}"...')
|
||||
create_operation = gapi.call(crm.projects(), 'create', body=body)
|
||||
operation_name = create_operation['name']
|
||||
time.sleep(8) # Google recommends always waiting at least 5 seconds
|
||||
@@ -7652,18 +7656,7 @@ def doCreateProject():
|
||||
'Hmm... Looks like you have no rights to your Google Cloud Organization.'
|
||||
)
|
||||
print('Attempting to fix that...')
|
||||
getorg = gapi.call(
|
||||
crm.organizations(),
|
||||
'search',
|
||||
body={'filter': f'domain:{login_domain}'})
|
||||
try:
|
||||
organization = getorg['organizations'][0]['name']
|
||||
print(f'Your organization name is {organization}')
|
||||
except (KeyError, IndexError):
|
||||
controlflow.system_error_exit(
|
||||
3,
|
||||
'you have no rights to create projects for your organization and you don\'t seem to be a super admin! Sorry, there\'s nothing more I can do.'
|
||||
)
|
||||
organization = getGCPOrg(crm, login_domain)
|
||||
org_policy = gapi.call(crm.organizations(),
|
||||
'getIamPolicy',
|
||||
resource=organization)
|
||||
@@ -7896,7 +7889,7 @@ def doCreateOrRotateServiceAccountKeys(iam=None,
|
||||
i += 1
|
||||
elif myarg == 'yubikeyslot':
|
||||
new_data['yubikey_slot'] = sys.argv[i+1].upper()
|
||||
i =+ 2
|
||||
i += 2
|
||||
elif myarg == 'yubikeypin':
|
||||
new_data['yubikey_pin'] = input('Enter your YubiKey PIN: ')
|
||||
i += 1
|
||||
@@ -7919,6 +7912,10 @@ def doCreateOrRotateServiceAccountKeys(iam=None,
|
||||
new_data['yubikey_key_type'] = f'RSA{local_key_size}'
|
||||
new_data.pop('private_key', None)
|
||||
yk = yubikey.YubiKey(new_data)
|
||||
if 'yubikey_serial_number' not in new_data:
|
||||
new_data['yubikey_serial_number'] = yk.get_serial_number()
|
||||
if 'yubikey_slot' not in new_data:
|
||||
new_data['yubikey_slot'] = 'AUTHENTICATION'
|
||||
publicKeyData = yk.get_certificate()
|
||||
elif local_key_size:
|
||||
# Generate private key locally, store in file
|
||||
@@ -8052,7 +8049,7 @@ def doDelProjects():
|
||||
gapi.call(crm.projects(),
|
||||
'delete',
|
||||
throw_reasons=[gapi_errors.ErrorReason.FORBIDDEN],
|
||||
projectId=projectId)
|
||||
name=project['name'])
|
||||
print(f' Project: {projectId} Deleted{currentCount(i, count)}')
|
||||
except gapi_errors.GapiForbiddenError as e:
|
||||
print(
|
||||
@@ -8066,8 +8063,9 @@ def doPrintShowProjects(csvFormat):
|
||||
csvRows = []
|
||||
todrive = False
|
||||
titles = [
|
||||
'User', 'projectId', 'projectNumber', 'name', 'createTime',
|
||||
'lifecycleState'
|
||||
'User', 'projectId', 'name', 'displayName',
|
||||
'createTime', 'updateTime', 'deleteTime',
|
||||
'state'
|
||||
]
|
||||
while i < len(sys.argv):
|
||||
myarg = sys.argv[i].lower()
|
||||
@@ -8084,19 +8082,19 @@ def doPrintShowProjects(csvFormat):
|
||||
for project in projects:
|
||||
i += 1
|
||||
print(f' Project: {project["projectId"]}{currentCount(i, count)}')
|
||||
print(f' projectNumber: {project["projectNumber"]}')
|
||||
print(f' name: {project["name"]}')
|
||||
print(f' createTime: {project["createTime"]}')
|
||||
print(f' lifecycleState: {project["lifecycleState"]}')
|
||||
print(f' displayName: {project["displayName"]}')
|
||||
for field in ['createTime', 'updateTime', 'deleteTime']:
|
||||
if field in project:
|
||||
print(f' {field}: {project[field]}')
|
||||
print(f' state: {project["state"]}')
|
||||
jcount = len(project.get('labels', []))
|
||||
if jcount > 0:
|
||||
print(' labels:')
|
||||
for k, v in list(project['labels'].items()):
|
||||
print(f' {k}: {v}')
|
||||
if 'parent' in project:
|
||||
print(' parent:')
|
||||
print(f' type: {project["parent"]["type"]}')
|
||||
print(f' id: {project["parent"]["id"]}')
|
||||
print(f' parent: {project["parent"]}')
|
||||
else:
|
||||
for project in projects:
|
||||
display.add_row_titles_to_csv_file(
|
||||
@@ -11853,6 +11851,12 @@ def ProcessGAMCommand(args):
|
||||
elif command == 'getcommand':
|
||||
gapi_directory_cros.get_command()
|
||||
sys.exit(0)
|
||||
elif command in ['yubikey']:
|
||||
action = sys.argv[2].lower().replace('_', '')
|
||||
if action == 'resetpiv':
|
||||
yk = yubikey.YubiKey()
|
||||
yk.reset_piv()
|
||||
sys.exit(0)
|
||||
users = getUsersToModify()
|
||||
command = sys.argv[3].lower()
|
||||
if command == 'print' and len(sys.argv) == 4:
|
||||
|
||||
@@ -1,72 +1,151 @@
|
||||
from base64 import b64encode
|
||||
import datetime
|
||||
from secrets import SystemRandom
|
||||
import string
|
||||
import sys
|
||||
from threading import Timer
|
||||
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from ykman.device import connect_to_device
|
||||
from yubikit.piv import KEY_TYPE, SLOT, InvalidPinError, PivSession
|
||||
from ykman.piv import generate_self_signed_certificate, \
|
||||
generate_chuid
|
||||
from yubikit.piv import DEFAULT_MANAGEMENT_KEY, \
|
||||
InvalidPinError, \
|
||||
KEY_TYPE, \
|
||||
MANAGEMENT_KEY_TYPE, \
|
||||
PIN_POLICY, \
|
||||
PivSession, \
|
||||
OBJECT_ID, \
|
||||
SLOT, \
|
||||
TOUCH_POLICY
|
||||
from yubikit.core.smartcard import ApduError
|
||||
from gam import controlflow
|
||||
|
||||
class YubiKey():
|
||||
|
||||
def __init__(self, service_account_info):
|
||||
key_type = service_account_info.get('yubikey_key_type', 'RSA2048')
|
||||
try:
|
||||
self.key_type = getattr(KEY_TYPE, key_type.upper())
|
||||
except AttributeError:
|
||||
controlflow.system_error_exit(6, f'{key_type} is not a valid value for yubikey_key_type')
|
||||
slot = service_account_info.get('yubikey_slot', 'AUTHENTICATION')
|
||||
try:
|
||||
self.slot = getattr(SLOT, slot.upper())
|
||||
except AttributeError:
|
||||
controlflow.system_error_exit(6, f'{slot} is not a valid value for yubikey_slot')
|
||||
self.serial_number = service_account_info.get('yubikey_serial_number')
|
||||
self.pin = service_account_info.get('yubikey_pin')
|
||||
self.key_id = service_account_info.get('private_key_id')
|
||||
def __init__(self, service_account_info=None):
|
||||
self.key_type = None
|
||||
self.slot = None
|
||||
self.serial_number = None
|
||||
self.pin = None
|
||||
self.key_id = None
|
||||
if service_account_info:
|
||||
key_type = service_account_info.get('yubikey_key_type', 'RSA2048')
|
||||
try:
|
||||
self.key_type = getattr(KEY_TYPE, key_type.upper())
|
||||
except AttributeError:
|
||||
controlflow.system_error_exit(6, f'{key_type} is not a valid value for yubikey_key_type')
|
||||
slot = service_account_info.get('yubikey_slot', 'AUTHENTICATION')
|
||||
try:
|
||||
self.slot = getattr(SLOT, slot.upper())
|
||||
except AttributeError:
|
||||
controlflow.system_error_exit(6, f'{slot} is not a valid value for yubikey_slot')
|
||||
self.serial_number = service_account_info.get('yubikey_serial_number')
|
||||
self.pin = service_account_info.get('yubikey_pin')
|
||||
self.key_id = service_account_info.get('private_key_id')
|
||||
|
||||
def _connect(self):
|
||||
conn, _, _ = connect_to_device(self.serial_number)
|
||||
return conn
|
||||
|
||||
def get_certificate(self):
|
||||
try:
|
||||
conn, _, _ = connect_to_device(self.serial_number)
|
||||
session = PivSession(conn)
|
||||
if self.pin:
|
||||
conn = self._connect()
|
||||
with conn:
|
||||
session = PivSession(conn)
|
||||
if self.pin:
|
||||
try:
|
||||
session.verify_pin(self.pin)
|
||||
except InvalidPinError as err:
|
||||
controlflow.system_error_exit(7, f'YubiKey - {err}')
|
||||
try:
|
||||
session.verify_pin(self.pin)
|
||||
except InvalidPinError as err:
|
||||
controlflow.system_error_exit(7, f'YubiKey - {err}')
|
||||
try:
|
||||
cert = session.get_certificate(self.slot)
|
||||
cert_pem = cert.public_bytes(
|
||||
serialization.Encoding.PEM).decode()
|
||||
publicKeyData = b64encode(cert_pem.encode())
|
||||
if isinstance(publicKeyData, bytes):
|
||||
publicKeyData = publicKeyData.decode()
|
||||
return publicKeyData
|
||||
except ApduError as err:
|
||||
controlflow.system_error_exit(8, f'YubiKey - {err}')
|
||||
cert = session.get_certificate(self.slot)
|
||||
except ApduError as err:
|
||||
controlflow.system_error_exit(9, f'Yubikey = {err}')
|
||||
cert_pem = cert.public_bytes(
|
||||
serialization.Encoding.PEM).decode()
|
||||
publicKeyData = b64encode(cert_pem.encode())
|
||||
if isinstance(publicKeyData, bytes):
|
||||
publicKeyData = publicKeyData.decode()
|
||||
return publicKeyData
|
||||
except ValueError as err:
|
||||
controlflow.system_error_exit(9, f'YubiKey - {err}')
|
||||
|
||||
|
||||
def get_serial_number(self):
|
||||
try:
|
||||
_, _, info = connect_to_device(self.serial_number)
|
||||
return info.serial
|
||||
except ValueError as err:
|
||||
controlflow.system_error_exit(9, f'YubikKey = {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())
|
||||
if reply != 'y':
|
||||
sys.exit(1)
|
||||
try:
|
||||
conn = self._connect()
|
||||
with conn:
|
||||
piv = PivSession(conn)
|
||||
piv.reset()
|
||||
rnd = SystemRandom()
|
||||
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)
|
||||
piv.change_pin('123456', new_pin)
|
||||
print(f'PIN set to: {new_pin}')
|
||||
piv.authenticate(MANAGEMENT_KEY_TYPE.TDES,
|
||||
DEFAULT_MANAGEMENT_KEY)
|
||||
|
||||
piv.verify_pin(new_pin)
|
||||
print('Yubikey is generating a non-exportable private key...')
|
||||
pubkey = piv.generate_key(SLOT.AUTHENTICATION,
|
||||
KEY_TYPE.RSA2048,
|
||||
PIN_POLICY.ALWAYS,
|
||||
TOUCH_POLICY.NEVER)
|
||||
now = datetime.datetime.utcnow()
|
||||
valid_to = now + datetime.timedelta(days=36500)
|
||||
subject = 'CN=GAM Created Key'
|
||||
piv.authenticate(MANAGEMENT_KEY_TYPE.TDES,
|
||||
DEFAULT_MANAGEMENT_KEY)
|
||||
piv.verify_pin(new_pin)
|
||||
cert = generate_self_signed_certificate(piv,
|
||||
SLOT.AUTHENTICATION,
|
||||
pubkey,
|
||||
subject,
|
||||
now,
|
||||
valid_to)
|
||||
piv.put_certificate(SLOT.AUTHENTICATION,
|
||||
cert)
|
||||
piv.put_object(OBJECT_ID.CHUID,
|
||||
generate_chuid())
|
||||
except ValueError as err:
|
||||
controlflow.system_error_exit(8, f'Yubikey - {err}')
|
||||
|
||||
|
||||
def sign(self, message):
|
||||
if 'mplock' in globals():
|
||||
mplock.acquire()
|
||||
try:
|
||||
conn, _, _ = connect_to_device(self.serial_number)
|
||||
session = PivSession(conn)
|
||||
if self.pin:
|
||||
conn = self._connect()
|
||||
with conn:
|
||||
session = PivSession(conn)
|
||||
if self.pin:
|
||||
try:
|
||||
session.verify_pin(self.pin)
|
||||
except InvalidPinError as err:
|
||||
controlflow.system_error_exit(7, f'YubiKey - {err}')
|
||||
try:
|
||||
session.verify_pin(self.pin)
|
||||
except InvalidPinError as err:
|
||||
controlflow.system_error_exit(7, f'YubiKey - {err}')
|
||||
try:
|
||||
signed = session.sign(slot=self.slot,
|
||||
signed = session.sign(slot=self.slot,
|
||||
key_type=self.key_type,
|
||||
message=message,
|
||||
hash_algorithm=hashes.SHA256(),
|
||||
padding=padding.PKCS1v15())
|
||||
except ApduError as err:
|
||||
controlflow.system_error_exit(8, f'YubiKey = {err}')
|
||||
except ApduError as err:
|
||||
controlflow.system_error_exit(8, f'YubiKey = {err}')
|
||||
except ValueError as err:
|
||||
controlflow.system_error_exit(9, f'YubiKey - {err}')
|
||||
if 'mplock' in globals():
|
||||
|
||||
@@ -65,9 +65,12 @@ def csv_field_error_exit(field_name, field_names):
|
||||
','.join(field_names)))
|
||||
|
||||
|
||||
def invalid_json_exit(file_name):
|
||||
def invalid_json_exit(file_name, err=None):
|
||||
"""Raises a system exit when invalid JSON content is encountered."""
|
||||
system_error_exit(17, MESSAGE_INVALID_JSON.format(file_name))
|
||||
err_msg = MESSAGE_INVALID_JSON.format(file_name)
|
||||
if err:
|
||||
err_msg += f'\n\n{err}'
|
||||
system_error_exit(17, err_msg)
|
||||
|
||||
|
||||
def wait_on_failure(current_attempt_num,
|
||||
|
||||
@@ -281,6 +281,7 @@ def get_all_pages(service,
|
||||
soft_errors=False,
|
||||
throw_reasons=None,
|
||||
retry_reasons=None,
|
||||
page_args_in_body=False,
|
||||
**kwargs):
|
||||
"""Aggregates and returns all pages of a Google service function response.
|
||||
|
||||
@@ -311,15 +312,22 @@ def get_all_pages(service,
|
||||
retry_reasons: A list of Google HTTP error reason strings indicating which
|
||||
error should be retried, using exponential backoff techniques, when the
|
||||
error reason is encountered.
|
||||
page_args_in_body: Some APIs like Chrome Policy want pageToken and pageSize
|
||||
in the body.
|
||||
**kwargs: Additional params to pass to the request method.
|
||||
|
||||
Returns:
|
||||
A list of all items received from all paged responses.
|
||||
"""
|
||||
if 'maxResults' not in kwargs and 'pageSize' not in kwargs:
|
||||
if page_args_in_body:
|
||||
kwargs.setdefault('body', {})
|
||||
if 'maxResults' not in kwargs and 'pageSize' not in kwargs and 'pageSize' not in kwargs.get('body', {}):
|
||||
page_key = _get_max_page_size_for_api_call(service, function, **kwargs)
|
||||
if page_key:
|
||||
kwargs.update(page_key)
|
||||
if page_args_in_body:
|
||||
kwargs['body'].update(page_key)
|
||||
else:
|
||||
kwargs.update(page_key)
|
||||
all_items = []
|
||||
page_token = None
|
||||
total_items = 0
|
||||
@@ -334,7 +342,10 @@ def get_all_pages(service,
|
||||
if not page_token:
|
||||
finalize_page_message(page_message)
|
||||
return all_items
|
||||
kwargs['pageToken'] = page_token
|
||||
if page_args_in_body:
|
||||
kwargs['body']['pageToken'] = page_token
|
||||
else:
|
||||
kwargs['pageToken'] = page_token
|
||||
|
||||
|
||||
# TODO: Make this private once all execution related items that use this method
|
||||
|
||||
@@ -39,6 +39,8 @@ def printshow_policies():
|
||||
orgunit = None
|
||||
printer_id = None
|
||||
app_id = None
|
||||
body = {}
|
||||
namespaces = []
|
||||
i = 3
|
||||
while i < len(sys.argv):
|
||||
myarg = sys.argv[i].lower().replace('_', '')
|
||||
@@ -51,67 +53,85 @@ def printshow_policies():
|
||||
elif myarg == 'appid':
|
||||
app_id = sys.argv[i+1]
|
||||
i += 2
|
||||
elif myarg == 'namespace':
|
||||
namespaces.extend(sys.argv[i+1].replace(',', ' ').split())
|
||||
i += 2
|
||||
else:
|
||||
msg = f'{myarg} is not a valid argument to "gam print chromepolicy"'
|
||||
controlflow.system_error_exit(3, msg)
|
||||
if not orgunit:
|
||||
controlflow.system_error_exit(3, 'You must specify an orgunit')
|
||||
body = {
|
||||
'policyTargetKey': {
|
||||
'targetResource': orgunit,
|
||||
}
|
||||
}
|
||||
body['policyTargetKey'] = {'targetResource': orgunit}
|
||||
if printer_id:
|
||||
body['policyTargetKey']['additionalTargetKeys'] = {'printer_id': printer_id}
|
||||
namespaces = ['chrome.printers']
|
||||
if not namespaces:
|
||||
namespaces = ['chrome.printers']
|
||||
elif app_id:
|
||||
body['policyTargetKey']['additionalTargetKeys'] = {'app_id': app_id}
|
||||
namespaces = ['chrome.users.apps',
|
||||
'chrome.devices.managedGuest.apps',
|
||||
'chrome.devices.kiosk.apps']
|
||||
else:
|
||||
if not namespaces:
|
||||
namespaces = ['chrome.users.apps',
|
||||
'chrome.devices.managedGuest.apps',
|
||||
'chrome.devices.kiosk.apps']
|
||||
elif not namespaces:
|
||||
namespaces = [
|
||||
'chrome.users',
|
||||
# Not yet implemented:
|
||||
# 'chrome.devices',
|
||||
# 'chrome.devices.managedGuest',
|
||||
# 'chrome.devices.kiosk',
|
||||
'chrome.users.apps',
|
||||
'chrome.devices',
|
||||
'chrome.devices.kiosk',
|
||||
'chrome.devices.managedGuest',
|
||||
]
|
||||
throw_reasons = [gapi_errors.ErrorReason.FOUR_O_O,]
|
||||
orgunitPath = gapi_directory_orgunits.orgunit_from_orgunitid(orgunit[9:], None)
|
||||
header = f'Organizational Unit: {orgunitPath}'
|
||||
if printer_id:
|
||||
header += f', printerid: {printer_id}'
|
||||
elif app_id:
|
||||
header += f', appid: {app_id}'
|
||||
print(header)
|
||||
print(f'Organizational Unit: {orgunitPath}')
|
||||
for namespace in namespaces:
|
||||
spacing = ' '
|
||||
body['policySchemaFilter'] = f'{namespace}.*'
|
||||
try:
|
||||
policies = gapi.get_all_pages(svc.customers().policies(), 'resolve',
|
||||
items='resolvedPolicies',
|
||||
throw_reasons=throw_reasons,
|
||||
customer=customer,
|
||||
body=body)
|
||||
body=body,
|
||||
page_args_in_body=True)
|
||||
except googleapiclient.errors.HttpError:
|
||||
policies = []
|
||||
for policy in sorted(policies, key=lambda k: k.get('value', {}).get('policySchema', '')):
|
||||
# sort policies first by app/printer id then by schema name
|
||||
policies = sorted(policies,
|
||||
key=lambda k: (
|
||||
list(k.get('targetKey', {}).get('additionalTargetKeys', {}).values()),
|
||||
k.get('value', {}).get('policySchema', '')))
|
||||
printed_ids = []
|
||||
for policy in policies:
|
||||
print()
|
||||
name = policy.get('value', {}).get('policySchema', '')
|
||||
schema = CHROME_SCHEMA_TYPE_MESSAGE.get(name)
|
||||
print(name)
|
||||
for key, val in policy['targetKey'].get('additionalTargetKeys', {}).items():
|
||||
additional_id = f'{key} - {val}'
|
||||
if additional_id not in printed_ids:
|
||||
print(f' {additional_id}')
|
||||
printed_ids.append(additional_id)
|
||||
spacing = ' '
|
||||
print(f'{spacing}{name}')
|
||||
values = policy.get('value', {}).get('value', {})
|
||||
for setting, value in values.items():
|
||||
# Handle TYPE_MESSAGE fields with durations or counts as a special case
|
||||
# Handle TYPE_MESSAGE fields with durations, values, counts and timeOfDay as special cases
|
||||
schema = CHROME_SCHEMA_TYPE_MESSAGE.get(name, {}).get(setting.lower())
|
||||
if schema and setting == schema['casedField']:
|
||||
value = value.get(schema['type'], '')
|
||||
if value:
|
||||
if value.endswith('s'):
|
||||
value = value[:-1]
|
||||
value = int(value) // schema['scale']
|
||||
vtype = schema['type']
|
||||
if vtype in {'duration', 'value'}:
|
||||
value = value.get(vtype, '')
|
||||
if value:
|
||||
if value.endswith('s'):
|
||||
value = value[:-1]
|
||||
value = int(value) // schema['scale']
|
||||
elif vtype == 'count':
|
||||
pass
|
||||
else: ##timeOfDay
|
||||
hours = value.get(vtype, {}).get('hours', 0)
|
||||
minutes = value.get(vtype, {}).get('minutes', 0)
|
||||
value = f'{hours:02}:{minutes:02}'
|
||||
elif isinstance(value, str) and value.find('_ENUM_') != -1:
|
||||
value = value.split('_ENUM_')[-1]
|
||||
print(f' {setting}: {value}')
|
||||
print(f'{spacing}{setting}: {value}')
|
||||
|
||||
|
||||
def build_schemas(svc=None, sfilter=None):
|
||||
@@ -254,21 +274,45 @@ def delete_policy():
|
||||
|
||||
|
||||
CHROME_SCHEMA_TYPE_MESSAGE = {
|
||||
'chrome.users.SessionLength':
|
||||
{'field': 'sessiondurationlimit', 'casedField': 'sessionDurationLimit',
|
||||
'type': 'duration', 'minVal': 1, 'maxVal': 1440, 'scale': 60},
|
||||
'chrome.users.AutoUpdateCheckPeriodNew': {
|
||||
'autoupdatecheckperiodminutesnew':
|
||||
{'casedField': 'autoUpdateCheckPeriodMinutesNew',
|
||||
'type': 'duration', 'minVal': 1, 'maxVal': 720, 'scale': 60}},
|
||||
'chrome.users.BrowserSwitcherDelayDuration':
|
||||
{'field': 'browserswitcherdelayduration', 'casedField': 'browserSwitcherDelayDuration',
|
||||
'type': 'duration', 'minVal': 0, 'maxVal': 30, 'scale': 1},
|
||||
{'browserswitcherdelayduration':
|
||||
{'casedField': 'browserSwitcherDelayDuration',
|
||||
'type': 'duration', 'minVal': 0, 'maxVal': 30, 'scale': 1}},
|
||||
'chrome.users.FetchKeepaliveDurationSecondsOnShutdown':
|
||||
{'fetchkeepalivedurationsecondsonshutdown':
|
||||
{'casedField': 'fetchKeepaliveDurationSecondsOnShutdown',
|
||||
'type': 'duration', 'minVal': 0, 'maxVal': 5, 'scale': 1}},
|
||||
'chrome.users.MaxInvalidationFetchDelay':
|
||||
{'field': 'maxinvalidationfetchdelay', 'casedField': 'maxInvalidationFetchDelay',
|
||||
'type': 'duration', 'minVal': 1, 'maxVal': 30, 'scale': 1},
|
||||
'chrome.users.SecurityTokenSessionSettings':
|
||||
{'field': 'securitytokensessionnotificationseconds', 'casedField': 'securityTokenSessionNotificationSeconds',
|
||||
'type': 'duration', 'minVal': 0, 'maxVal': 9999, 'scale': 1},
|
||||
{'maxinvalidationfetchdelay':
|
||||
{'casedField': 'maxInvalidationFetchDelay',
|
||||
'type': 'duration', 'minVal': 1, 'maxVal': 30, 'scale': 1, 'default': 10}},
|
||||
'chrome.users.PrintingMaxSheetsAllowed':
|
||||
{'field': 'printingmaxsheetsallowednullable', 'casedField': 'printingMaxSheetsAllowedNullable',
|
||||
'type': 'value', 'minVal': 1, 'maxVal': None, 'scale': 1},
|
||||
{'printingmaxsheetsallowednullable':
|
||||
{'casedField': 'printingMaxSheetsAllowedNullable',
|
||||
'type': 'value', 'minVal': 1, 'maxVal': None, 'scale': 1}},
|
||||
'chrome.users.PrintJobHistoryExpirationPeriodNew':
|
||||
{'printjobhistoryexpirationperioddaysnew':
|
||||
{'casedField': 'printJobHistoryExpirationPeriodDaysNew',
|
||||
'type': 'duration', 'minVal': -1, 'maxVal': None, 'scale': 86400}},
|
||||
'chrome.users.SecurityTokenSessionSettings':
|
||||
{'securitytokensessionnotificationseconds':
|
||||
{'casedField': 'securityTokenSessionNotificationSeconds',
|
||||
'type': 'duration', 'minVal': 0, 'maxVal': 9999, 'scale': 1}},
|
||||
'chrome.users.SessionLength':
|
||||
{'sessiondurationlimit':
|
||||
{'casedField': 'sessionDurationLimit',
|
||||
'type': 'duration', 'minVal': 1, 'maxVal': 1440, 'scale': 60}},
|
||||
'chrome.users.UpdatesSuppressed':
|
||||
{'updatessuppresseddurationmin':
|
||||
{'casedField': 'updatesSuppressedDurationMin',
|
||||
'type': 'count', 'minVal': 1, 'maxVal': 1440, 'scale': 1},
|
||||
'updatessuppressedstarttime':
|
||||
{'casedField': 'updatesSuppressedStartTime',
|
||||
'type': 'timeOfDay'}},
|
||||
}
|
||||
|
||||
|
||||
@@ -302,19 +346,39 @@ def update_policy():
|
||||
field = sys.argv[i].lower()
|
||||
if field in ['ou', 'org', 'orgunit', 'printerid', 'appid'] or '.' in field:
|
||||
break # field is actually a new policy, orgunit or app/printer id
|
||||
# Handle TYPE_MESSAGE fields with durations or counts as a special case
|
||||
schema = CHROME_SCHEMA_TYPE_MESSAGE.get(schemaName)
|
||||
if schema and field == schema['field']:
|
||||
casedField = schema['casedField']
|
||||
value = gam.getInteger(sys.argv[i+1], casedField,
|
||||
minVal=schema['minVal'], maxVal=schema['maxVal'])*schema['scale']
|
||||
if schema['type'] == 'duration':
|
||||
body['requests'][-1]['policyValue']['value'][casedField] = {schema['type']: f'{value}s'}
|
||||
else:
|
||||
body['requests'][-1]['policyValue']['value'][casedField] = {schema['type']: value}
|
||||
body['requests'][-1]['updateMask'] += f'{casedField},'
|
||||
i += 2
|
||||
continue
|
||||
# Handle TYPE_MESSAGE fields with durations, values, counts and timeOfDay as special cases
|
||||
schema = CHROME_SCHEMA_TYPE_MESSAGE.get(schemaName, {}).get(field)
|
||||
if schema:
|
||||
i += 1
|
||||
casedField = schema['casedField']
|
||||
vtype = schema['type']
|
||||
if vtype != 'timeOfDay':
|
||||
if 'default' not in schema:
|
||||
value = gam.getInteger(sys.argv[i], casedField,
|
||||
minVal=schema['minVal'], maxVal=schema['maxVal'])*schema['scale']
|
||||
i += 1
|
||||
elif i < len(sys.argv) and sys.argv[i].isdigit():
|
||||
value = gam.getInteger(sys.argv[i], casedField,
|
||||
minVal=schema['minVal'], maxVal=schema['maxVal'])*schema['scale']
|
||||
i += 1
|
||||
else: # Handle empty value for fields with default
|
||||
value = schema['default']*schema['scale']
|
||||
if i < len(sys.argv) and not sys.argv[i]:
|
||||
i += 1
|
||||
else:
|
||||
value = utils.get_hhmm(sys.argv[i])
|
||||
i += 1
|
||||
if vtype == 'duration':
|
||||
body['requests'][-1]['policyValue']['value'][casedField] = {vtype: f'{value}s'}
|
||||
elif vtype == 'value':
|
||||
body['requests'][-1]['policyValue']['value'][casedField] = {vtype: value}
|
||||
elif vtype == 'count':
|
||||
body['requests'][-1]['policyValue']['value'][casedField] = value
|
||||
else: ##timeOfDay
|
||||
hours, minutes = value.split(':')
|
||||
body['requests'][-1]['policyValue']['value'][casedField] = {vtype: {'hours': hours, 'minutes': minutes}}
|
||||
body['requests'][-1]['updateMask'] += f'{casedField},'
|
||||
continue
|
||||
expected_fields = ', '.join(schemas[myarg]['settings'])
|
||||
if field not in expected_fields:
|
||||
msg = f'Expected {myarg} field of {expected_fields}. Got {field}.'
|
||||
|
||||
@@ -3,6 +3,7 @@ from time import sleep
|
||||
import gam
|
||||
from gam import gapi
|
||||
from gam.gapi import directory as gapi_directory
|
||||
from gam.gapi import errors as gapi_errors
|
||||
|
||||
|
||||
def get_primary(email):
|
||||
@@ -53,10 +54,16 @@ def wait_for_mailbox(users):
|
||||
i += 1
|
||||
user = gam.normalizeEmailAddressOrUID(user)
|
||||
while True:
|
||||
result = gapi.call(cd.users(),
|
||||
'get',
|
||||
'fields=isMailboxSetup',
|
||||
userKey=user)
|
||||
try:
|
||||
result = gapi.call(cd.users(),
|
||||
'get',
|
||||
'fields=isMailboxSetup',
|
||||
userKey=user,
|
||||
throw_reasons=[gapi_errors.ErrorReason.USER_NOT_FOUND])
|
||||
except gapi_errors.GapiUserNotFoundError:
|
||||
print(f'{user} mailboxIsSetup: False (user does not exist yet)')
|
||||
sleep(3)
|
||||
continue
|
||||
mailbox_is_setup = result.get('isMailboxSetup')
|
||||
print(f'{user} mailboxIsSetup: {mailbox_is_setup}')
|
||||
if mailbox_is_setup:
|
||||
|
||||
@@ -285,7 +285,7 @@ def showReport():
|
||||
customerId = GC_Values[GC_CUSTOMER_ID]
|
||||
if customerId == MY_CUSTOMER:
|
||||
customerId = None
|
||||
filters = parameters = actorIpAddress = startTime = endTime = eventName = orgUnitId = None
|
||||
filters = parameters = actorIpAddress = groupIdFilter = startTime = endTime = eventName = orgUnitId = None
|
||||
tryDate = datetime.date.today().strftime(YYYYMMDD_FORMAT)
|
||||
to_drive = False
|
||||
userKey = 'all'
|
||||
@@ -330,6 +330,9 @@ def showReport():
|
||||
elif myarg == 'ip':
|
||||
actorIpAddress = sys.argv[i + 1]
|
||||
i += 2
|
||||
elif myarg == 'groupidfilter':
|
||||
groupIdFilter = sys.argv[i + 1]
|
||||
i += 2
|
||||
elif myarg == 'todrive':
|
||||
to_drive = True
|
||||
i += 1
|
||||
@@ -489,7 +492,8 @@ def showReport():
|
||||
endTime=endTime,
|
||||
eventName=eventName,
|
||||
filters=filters,
|
||||
orgUnitID=orgUnitId)
|
||||
orgUnitID=orgUnitId,
|
||||
groupIdFilter=groupIdFilter)
|
||||
if activities:
|
||||
titles = ['name']
|
||||
csvRows = []
|
||||
|
||||
@@ -254,6 +254,18 @@ def get_delta_time(argstr):
|
||||
return deltaTime
|
||||
|
||||
|
||||
def get_hhmm(argstr):
|
||||
argstr = argstr.strip()
|
||||
if argstr:
|
||||
try:
|
||||
dateTime = datetime.datetime.strptime(argstr, HHMM_FORMAT)
|
||||
return argstr
|
||||
except ValueError:
|
||||
controlflow.system_error_exit(
|
||||
2, f'expected a <{HHMM_FORMAT_REQUIRED}>; got {argstr}')
|
||||
controlflow.system_error_exit(2, f'expected a <{HHMM_FORMAT_REQUIRED}>')
|
||||
|
||||
|
||||
def get_yyyymmdd(argstr, minLen=1, returnTimeStamp=False, returnDateTime=False):
|
||||
argstr = argstr.strip()
|
||||
if argstr:
|
||||
|
||||
@@ -8,7 +8,7 @@ import platform
|
||||
import re
|
||||
|
||||
GAM_AUTHOR = 'Jay Lee <jay0lee@gmail.com>'
|
||||
GAM_VERSION = '6.06'
|
||||
GAM_VERSION = '6.07'
|
||||
GAM_LICENSE = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
|
||||
|
||||
GAM_URL = 'https://git.io/gam'
|
||||
@@ -286,12 +286,8 @@ PRODUCTID_NAME_MAPPINGS = {
|
||||
|
||||
# Legacy APIs that use v1 discovery. Newer APIs should all use v2.
|
||||
V1_DISCOVERY_APIS = {
|
||||
'admin',
|
||||
'calendar',
|
||||
'drive',
|
||||
'oauth2',
|
||||
'reseller',
|
||||
'siteVerification',
|
||||
}
|
||||
|
||||
API_NAME_MAPPING = {
|
||||
@@ -299,7 +295,7 @@ API_NAME_MAPPING = {
|
||||
'reports': 'admin',
|
||||
'datatransfer': 'admin',
|
||||
'drive3': 'drive',
|
||||
'cloudresourcemanagerv1': 'cloudresourcemanager',
|
||||
'calendar': 'calendar-json',
|
||||
'cloudidentity_beta': 'cloudidentity',
|
||||
}
|
||||
|
||||
@@ -313,8 +309,7 @@ API_VER_MAPPING = {
|
||||
'classroom': 'v1',
|
||||
'cloudidentity': 'v1',
|
||||
'cloudidentity_beta': 'v1beta1',
|
||||
'cloudresourcemanager': 'v2',
|
||||
'cloudresourcemanagerv1': 'v1',
|
||||
'cloudresourcemanager': 'v3',
|
||||
'contactdelegation': 'v1',
|
||||
'datatransfer': 'datatransfer_v1',
|
||||
'directory': 'directory_v1',
|
||||
@@ -478,6 +473,7 @@ DRIVEFILE_FIELDS_CHOICES_MAP = {
|
||||
'lastviewedbymedate': 'lastViewedByMeDate',
|
||||
'lastviewedbymetime': 'lastViewedByMeDate',
|
||||
'lastviewedbyuser': 'lastViewedByMeDate',
|
||||
'linksharemetadata': 'linkShareMetadata',
|
||||
'md5': 'md5Checksum',
|
||||
'md5checksum': 'md5Checksum',
|
||||
'md5sum': 'md5Checksum',
|
||||
@@ -496,6 +492,7 @@ DRIVEFILE_FIELDS_CHOICES_MAP = {
|
||||
'owners': 'owners',
|
||||
'parents': 'parents',
|
||||
'permissions': 'permissions',
|
||||
'resourcekey': 'resourceKey',
|
||||
'quotabytesused': 'quotaBytesUsed',
|
||||
'quotaused': 'quotaBytesUsed',
|
||||
'shareable': 'shareable',
|
||||
@@ -894,7 +891,6 @@ RT_TAG_REPLACE_PATTERN = re.compile(r'{(.*?)}')
|
||||
LOWERNUMERIC_CHARS = string.ascii_lowercase + string.digits
|
||||
ALPHANUMERIC_CHARS = LOWERNUMERIC_CHARS + string.ascii_uppercase
|
||||
URL_SAFE_CHARS = ALPHANUMERIC_CHARS + '-._~'
|
||||
PASSWORD_SAFE_CHARS = ALPHANUMERIC_CHARS + string.punctuation + ' '
|
||||
FILENAME_SAFE_CHARS = ALPHANUMERIC_CHARS + '-_.() '
|
||||
|
||||
FILTER_ADD_LABEL_TO_ARGUMENT_MAP = {
|
||||
@@ -1937,6 +1933,9 @@ DELTA_DATE_FORMAT_REQUIRED = '(+|-)<Number>(d|w|y)'
|
||||
DELTA_TIME_PATTERN = re.compile(r'^([+-])(\d+)([mhdwy])$')
|
||||
DELTA_TIME_FORMAT_REQUIRED = '(+|-)<Number>(m|h|d|w|y)'
|
||||
|
||||
HHMM_FORMAT = '%H:%M'
|
||||
HHMM_FORMAT_REQUIRED = 'hh:mm'
|
||||
|
||||
YYYYMMDD_FORMAT = '%Y-%m-%d'
|
||||
YYYYMMDD_FORMAT_REQUIRED = 'yyyy-mm-dd'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user