Compare commits

..

47 Commits
v6.06 ... v6.07

Author SHA1 Message Date
Jay Lee
a3d560a8a2 YubiKey improvements and PIV reset 2021-07-27 09:24:34 -04:00
Jay Lee
ed20fe252e Use with conn so Yubikey connections close sooner 2021-07-26 14:46:58 -04:00
Jay Lee
375e36ff96 State what we don't like about invalid JSON 2021-07-26 14:45:26 -04:00
Jay Lee
e7108b108e Update build.yml 2021-07-23 13:31:06 -04:00
Jay Lee
6d59daad19 Update build.yml 2021-07-23 13:27:53 -04:00
Jay Lee
21c693921b Update build.yml 2021-07-23 13:13:34 -04:00
Jay Lee
7bcd5fbed7 Update build.yml 2021-07-23 13:06:22 -04:00
Jay Lee
7104970e17 Update build.yml 2021-07-23 13:01:20 -04:00
Jay Lee
1a2950b580 Update build.yml 2021-07-23 12:59:34 -04:00
Jay Lee
085b24e1c5 Update build.yml 2021-07-23 12:55:39 -04:00
Jay Lee
8688ce6328 Update build.yml 2021-07-23 12:52:35 -04:00
Jay Lee
fbdfed81e7 Update build.yml 2021-07-23 12:49:21 -04:00
Ross Scroggs
94fe20607e Updates for CRM v3 changes (#1401) 2021-07-22 19:19:10 -04:00
Ross Scroggs
6c62483e8e Updates for CRM v3 changes (#1400) 2021-07-21 17:27:46 -04:00
Ross Scroggs
54689129c6 Update gam print|show|update chromepolicy to handle the following special case policies: (#1399)
```
chrome.users.AutoUpdateCheckPeriodNew autoupdatecheckperiodminutesnew
chrome.users.BrowserSwitcherDelayDuration browserswitcherdelayduration
chrome.users.FetchKeepaliveDurationSecondsOnShutdown fetchkeepalivedurationsecondsonshutdown
chrome.users.MaxInvalidationFetchDelay maxinvalidationfetchdelay
chrome.users.PrintingMaxSheetsAllowed printingmaxsheetsallowednullable
chrome.users.PrintJobHistoryExpirationPeriodNew printjobhistoryexpirationperioddaysnew
chrome.users.SecurityTokenSessionSettings securitytokensessionnotificationseconds
chrome.users.SessionLength sessiondurationlimit
chrome.users.UpdatesSuppressed updatessuppresseddurationmin
chrome.users.UpdatesSuppressed updatessuppressedstarttime
```
2021-07-20 17:25:17 -04:00
Ross Scroggs
e9e8dd5a82 Fix call to be compatible with CRM v3 (#1398) 2021-07-19 19:31:08 -04:00
Jay Lee
00e764b118 Migrate to Resource Manager API v3 2021-07-16 10:14:58 -04:00
Jay Lee
cee7eb970a Merge branch 'main' of https://github.com/jay0lee/GAM 2021-07-13 10:45:21 -04:00
Jay Lee
daed17fac8 exclude null character, max out passwd length on random 2021-07-13 10:44:58 -04:00
Ross Scroggs
8708f4f93f Fix page_args_in_body, update namespace handling in show chromepolicies (#1393)
When page_args_in_body is true you have to add body to kwargs to ensure a place for pageToken

Allow setting a list of namespaces that override the defaults for printerid (not likely) and appid.
2021-07-08 08:55:18 -04:00
Jay Lee
c7c1bfbeba retry wait for mailbox if user doesn't exist 2021-07-07 11:03:04 -04:00
Jay Lee
0418438b6f increase rounds to Google max 2021-07-07 10:53:34 -04:00
Jay Lee
a2ea4d036e improve random password generator 2021-07-07 10:47:34 -04:00
Jay Lee
dc7a29908f updates to allow listing/setting extension policy 2021-07-02 13:36:21 -04:00
Jay Lee
794db5d2a4 More APIs now work with discovery v2 URL 2021-07-01 14:47:40 -04:00
Jay Lee
e5f9db129b Improve printing of app/extension/printer policy 2021-06-30 11:18:29 -04:00
Jay Lee
a6aecf4e9d undo version in exe 2021-06-29 11:22:33 -04:00
Jay Lee
b59bc4ec90 Merge branch 'main' of https://github.com/jay0lee/GAM 2021-06-29 11:04:17 -04:00
Jay Lee
41920f7865 add version info to Windows exe 2021-06-29 11:02:44 -04:00
Jay Lee
4630bf5681 Update var.py 2021-06-29 08:13:59 -04:00
Ross Scroggs
1c78ebd20e Add groupidfllert <String> to gam report <ActivityApplicationName> (#1390) 2021-06-28 21:34:50 -04:00
Jay Lee
80d17cfda3 Update windows-install.sh 2021-06-28 17:28:10 -04:00
Jay Lee
a154007927 Update windows-install.sh 2021-06-28 17:22:57 -04:00
Jay Lee
bd8274cc27 Update windows-install.sh 2021-06-28 17:13:42 -04:00
Jay Lee
fb08991c05 Update windows-before-install.sh 2021-06-28 17:06:47 -04:00
Jay Lee
7c1f06fdf7 Update build.yml 2021-06-28 16:57:11 -04:00
Jay Lee
93b38b9f95 Update build.yml 2021-06-28 16:44:40 -04:00
Jay Lee
7ffc97d301 Update build.yml 2021-06-28 16:40:43 -04:00
Jay Lee
280301f258 Update build.yml 2021-06-28 16:36:04 -04:00
Jay Lee
40daf38f80 Update build.yml 2021-06-28 16:32:41 -04:00
Jay Lee
d24925cd5f Update build.yml 2021-06-28 16:30:55 -04:00
Ross Scroggs
cd42d54b43 Fix typo, document new drive fields (#1389)
* Fix typo, document new drive fields

* Document new drive attribute
2021-06-28 15:40:22 -04:00
Jay Lee
53d8ecb6bc Update build.yml 2021-06-28 15:39:47 -04:00
Jay Lee
98e87d0297 Update build.yml 2021-06-28 15:38:04 -04:00
Jay Lee
400b4af769 Update macos-before-install.sh 2021-06-28 15:35:40 -04:00
Jay Lee
368701afb1 Update build.yml 2021-06-28 15:33:58 -04:00
Jay Lee
a501b89ecd resource key support 2021-06-25 16:52:14 -04:00
15 changed files with 408 additions and 211 deletions

View File

@@ -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..."

View File

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

View File

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

View File

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

View File

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

View File

@@ -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')]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = []

View File

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

View File

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