From bd38b7479fe316b02c93ba48a825b7ec788cda34 Mon Sep 17 00:00:00 2001 From: Jay Lee Date: Sat, 13 Mar 2021 10:02:53 -0500 Subject: [PATCH] Chrome Policy rough draft, further customer_id standardization --- src/gam/__init__.py | 18 ++- src/gam/gapi/cbcm.py | 36 +++-- src/gam/gapi/chromepolicy.py | 246 +++++++++++++++++++++++++++++ src/gam/gapi/directory/customer.py | 41 +++-- src/gam/utils.py | 10 ++ src/gam/var.py | 1 + src/project-apis.txt | 1 + 7 files changed, 327 insertions(+), 26 deletions(-) create mode 100644 src/gam/gapi/chromepolicy.py diff --git a/src/gam/__init__.py b/src/gam/__init__.py index de22ad6c..181e7f0d 100755 --- a/src/gam/__init__.py +++ b/src/gam/__init__.py @@ -53,6 +53,7 @@ from gam import fileutils from gam.gapi import calendar as gapi_calendar from gam.gapi import cloudidentity as gapi_cloudidentity from gam.gapi import cbcm as gapi_cbcm +from gam.gapi import chromepolicy as gapi_chromepolicy from gam.gapi.cloudidentity import devices as gapi_cloudidentity_devices from gam.gapi.cloudidentity import groups as gapi_cloudidentity_groups from gam.gapi.cloudidentity import userinvitations as gapi_cloudidentity_userinvitations @@ -933,7 +934,7 @@ def getService(api, http): controlflow.wait_on_failure(n, retries, str(e)) continue controlflow.system_error_exit(17, str(e)) - except (http_client.ResponseNotReady, socket.error, + except (http_client.ResponseNotReady, OSError, googleapiclient.errors.HttpError) as e: if n != retries: controlflow.wait_on_failure(n, retries, str(e)) @@ -10247,6 +10248,11 @@ OAUTH2_SCOPES = [ 'subscopes': ['readonly'], 'scopes': 'https://www.googleapis.com/auth/admin.directory.device.chromebrowsers', }, + { + 'name': 'Chrome Policy API', + 'subscope': ['readonly'], + 'scopes': ['https://www.googleapis.com/auth/chrome.management.policy'], + }, { 'name': 'Classroom API - counts as 5 scopes', @@ -11242,6 +11248,8 @@ def ProcessGAMCommand(args): gapi_cloudidentity_devices.update_state() elif argument in ['browser', 'browsers']: gapi_cbcm.update() + elif argument == 'chromepolicy': + gapi_chromepolicy.update_policy() elif argument in ['printer']: gapi_directory_printers.update() else: @@ -11376,6 +11384,8 @@ def ProcessGAMCommand(args): gapi_cbcm.delete() elif argument in ['printer']: gapi_directory_printers.delete() + elif argument == 'chromepolicy': + gapi_chromepolicy.delete_policy() else: controlflow.invalid_argument_exit(argument, 'gam delete') sys.exit(0) @@ -11483,6 +11493,10 @@ def ProcessGAMCommand(args): gapi_directory_printers.print_models() elif argument in ['printers']: gapi_directory_printers.print_() + elif argument == 'chromeschema': + gapi_chromepolicy.print_schemas() + elif argument == 'chromepolicy': + gapi_chromepolicy.print_policies() else: controlflow.invalid_argument_exit(argument, 'gam print') sys.exit(0) @@ -11985,7 +11999,7 @@ def ProcessGAMCommand(args): sys.exit(2) except KeyboardInterrupt: sys.exit(50) - except socket.error as e: + except OSError as e: controlflow.system_error_exit(3, str(e)) except MemoryError: controlflow.system_error_exit(99, MESSAGE_GAM_OUT_OF_MEMORY) diff --git a/src/gam/gapi/cbcm.py b/src/gam/gapi/cbcm.py index 5cbf8c96..e84956c2 100644 --- a/src/gam/gapi/cbcm.py +++ b/src/gam/gapi/cbcm.py @@ -14,6 +14,14 @@ from gam.gapi.directory import orgunits as gapi_directory_orgunits from gam import utils +def _get_customerid(): + ''' returns customer id without C prefix''' + customer_id = GC_Values[GC_CUSTOMER_ID] + if customer_id != MY_CUSTOMER and customer_id[0] == 'C': + customer_id = customer_id[1:] + return customer_id + + def build(): return gam.buildGAPIObject('cbcm') @@ -21,8 +29,9 @@ def build(): def delete(): cbcm = build() device_id = sys.argv[3] + customer_id = _get_customerid() gapi.call(cbcm.chromebrowsers(), 'delete', deviceId=device_id, - customer=GC_Values[GC_CUSTOMER_ID]) + customer=customer_id) print(f'Deleted browser {device_id}') @@ -31,6 +40,7 @@ def info(): device_id = sys.argv[3] projection = 'BASIC' fields = None + customer_id = _get_customerid() i = 4 while i < len(sys.argv): myarg = sys.argv[i].lower().replace('_', '') @@ -43,7 +53,7 @@ def info(): else: controlflow.invalid_argument_exit(sys.argv[i], 'gam info browser') browser = gapi.call(cbcm.chromebrowsers(), 'get', - customer=GC_Values[GC_CUSTOMER_ID], + customer=customer_id, fields=fields, deviceId=device_id, projection=projection) display.print_json(browser) @@ -52,6 +62,7 @@ def info(): def move(): cbcm = build() body = {'resource_ids': []} + customer_id = _get_customerid() i = 3 resource_ids = [] batch_size = 600 @@ -65,7 +76,7 @@ def move(): page_message = gapi.got_total_items_msg('Browsers', '...\n') browsers = gapi.get_all_pages(cbcm.chromebrowsers(), 'list', 'browsers', page_message=page_message, - customer=GC_Values[GC_CUSTOMER_ID], + customer=customer_id, query=query, projection='BASIC', fields='browsers(deviceId),nextPageToken') ids = [browser['deviceId'] for browser in browsers] @@ -115,11 +126,12 @@ def move(): print(f' moving {len(body["resource_ids"])} browsers to ' \ f'{body["org_unit_path"]}') gapi.call(cbcm.chromebrowsers(), 'moveChromeBrowsersToOu', - customer=GC_Values[GC_CUSTOMER_ID], body=body) + customer=customer_id, body=body) def print_(): cbcm = build() + customer_id = _get_customerid() projection = 'BASIC' orgUnitPath = query = None fields = None @@ -157,7 +169,7 @@ def print_(): page_message = gapi.got_total_items_msg('Browsers', '...\n') browsers = gapi.get_all_pages(cbcm.chromebrowsers(), 'list', 'browsers', page_message=page_message, - customer=GC_Values[GC_CUSTOMER_ID], + customer=customer_id, orgUnitPath=orgUnitPath, query=query, projection=projection, fields=fields) for browser in browsers: @@ -181,6 +193,7 @@ attribute_fields = ','.join(list(attributes.values())) def update(): cbcm = build() + customer_id = _get_customerid() device_id = sys.argv[3] body = {'deviceId': device_id} i = 4 @@ -193,17 +206,18 @@ def update(): controlflow.invalid_argument_exit(sys.argv[i], 'gam update browser') browser = gapi.call(cbcm.chromebrowsers(), 'get', deviceId=device_id, - customer=GC_Values[GC_CUSTOMER_ID], + customer=customer_id, projection='BASIC', fields=attribute_fields) browser.update(body) result = gapi.call(cbcm.chromebrowsers(), 'update', deviceId=device_id, - customer=GC_Values[GC_CUSTOMER_ID], body=browser, + customer=customer_id, body=browser, projection='BASIC', fields="deviceId") print(f'Updated browser {result["deviceId"]}') def createtoken(): cbcm = build() + customer_id = _get_customerid() body = {'token_type': 'CHROME_BROWSER'} i = 3 while i < len(sys.argv): @@ -218,20 +232,22 @@ def createtoken(): controlflow.invalid_argument_exit(sys.argv[i], 'gam create browsertoken') browser = gapi.call(cbcm.enrollmentTokens(), 'create', - customer=GC_Values[GC_CUSTOMER_ID], body=body) + customer=customer_id, body=body) print(f'Created browser enrollment token {browser["token"]}') def revoketoken(): cbcm = build() + customer_id = _get_customerid() token_permanent_id = sys.argv[3] gapi.call(cbcm.enrollmentTokens(), 'revoke', tokenPermanentId=token_permanent_id, - customer=GC_Values[GC_CUSTOMER_ID]) + customer=customer_id) print(f'Deleted browser enrollment token {token_permanent_id}') def printshowtokens(csvFormat): cbcm = build() + customer_id = _get_customerid() query = None fields = None if csvFormat: @@ -263,7 +279,7 @@ def printshowtokens(csvFormat): page_message = gapi.got_total_items_msg('Chrome Browser Enrollment Tokens', '...\n') browsers = gapi.get_all_pages(cbcm.enrollmentTokens(), 'list', 'chromeEnrollmentTokens', page_message=page_message, - customer=GC_Values[GC_CUSTOMER_ID], + customer=customer_id, query=query, fields=fields) if not csvFormat: count = len(browsers) diff --git a/src/gam/gapi/chromepolicy.py b/src/gam/gapi/chromepolicy.py new file mode 100644 index 00000000..29517714 --- /dev/null +++ b/src/gam/gapi/chromepolicy.py @@ -0,0 +1,246 @@ +"""Chrome Browser Cloud Management API calls""" + +import json +import os.path +import sys + +import gam +from gam.var import * +from gam import controlflow +from gam import display +from gam import fileutils +from gam import gapi +from gam.gapi import errors as gapi_errors +from gam.gapi.directory import orgunits as gapi_directory_orgunits +from gam import utils + +import googleapiclient.errors + + +def _get_customerid(): + customer = GC_Values[GC_CUSTOMER_ID] + if customer != MY_CUSTOMER and customer[0] != 'C': + customer = 'C' + customer + return f'customers/{customer}' + + +def _get_orgunit(orgunit): + if orgunit.startswith('orgunits/'): + return orgunit + _, orgunitid = gapi_directory_orgunits.getOrgUnitId(orgunit) + return f'orgunits/{orgunitid[3:]}' + + +def build(): + return gam.buildGAPIObject('chromepolicy') + + +def print_policies(): + cp = build() + customer = _get_customerid() + if len(sys.argv) < 4: + orgunit = '/' + else: + orgunit = sys.argv[3] + orgunit = _get_orgunit(orgunit) + namespaces = [ + 'chrome.users', +# 'chrome.users.apps', +# 'chrome.devices', +# 'chrome.devices.managedGuest', +# 'chrome.devices.managedGuest.apps', +# 'chrome.devices.kiosk', +# 'chrome.devices.kiosk.apps', +# 'chrome.printers', + ] + body = { + 'policyTargetKey': { + 'targetResource': orgunit, + + } + } + throw_reasons = [gapi_errors.ErrorReason.FOUR_O_O,] + for namespace in namespaces: + body['policySchemaFilter'] = f'{namespace}.*' + try: + policies = gapi.get_all_pages(cp.customers().policies(), 'resolve', + items='resolvedPolicies', + throw_reasons=throw_reasons, + customer=customer, + body=body) + except googleapiclient.errors.HttpError as err: + policies = [] + for policy in policies: + #print(json.dumps(policy, indent=2)) + #print() + name = policy.get('value', {}).get('policySchema', '') + print(name) + values = policy.get('value', {}).get('value', {}) + for setting, value in values.items(): + if type(value) is str and value.find('_ENUM_') != -1: + value = value.split('_ENUM_')[-1] + print(f' {setting}: {value}') + print() + +def build_schemas(cp=None): + if not cp: + cp = build() + parent = _get_customerid() + schemas = gapi.get_all_pages(cp.customers().policySchemas(), 'list', + items='policySchemas', parent=parent) + schema_objects = {} + for schema in schemas: + schema_name = schema.get('name', '').split('/')[-1] + #print(schema) + #continue + schema_dict = { + 'name': schema_name, + 'description': schema.get('policyDescription', ''), + 'settings': {}, + } + for mt in schema.get('definition', {}).get('messageType', {}): + for setting in mt.get('field', {}): + setting_name = setting.get('name', '') + setting_dict = { + 'name': setting_name, + 'constraints': None, + 'descriptions': [], + 'type': setting.get('type'), + } + if setting_dict['type'] == 'TYPE_STRING' and setting.get('label') == 'LABEL_REPEATED': + setting_dict['type'] = 'TYPE_LIST' + if setting_dict['type'] == 'TYPE_ENUM': + type_name = setting['typeName'] + 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']) + prefix_len = len(setting_dict['enum_prefix']) + setting_dict['enums'] = [enum[prefix_len:] for enum in setting_dict['enums'] if not enum.endswith('UNSPECIFIED')] + break + for fd in schema.get('fieldDescriptions', []): + if fd.get('field') == setting_name: + setting_dict['descriptions'] = [d['description'] for d in fd.get('knownValueDescriptions', [])] + break + elif setting_dict['type'] == 'TYPE_MESSAGE': + print(setting_dict) + continue + else: + setting_dict['enums'] = None + for fd in schema.get('fieldDescriptions', []): + if fd.get('field') == setting_name: + if 'knownValueDescriptions' in fd: + setting_dict['descriptions'] = fd['knownValueDescriptions'] + elif 'description' in fd: + setting_dict['descriptions'] = [fd['description']] + schema_dict['settings'][setting_name.lower()] = setting_dict + schema_objects[schema_name.lower()] = schema_dict + for obj in schema_objects.values(): + print(json.dumps(obj, indent=2)) + return schema_objects + +def print_schemas(): + cp = build() + schemas = build_schemas(cp) + for val in schemas.values(): + print(f'{val.get("name")} - {val.get("description")}') + for v in val['settings'].values(): + vtype = v.get('type') + print(f' {v.get("name")}: {vtype}') + if vtype == 'TYPE_ENUM': + enums = v.get('enums', []) + descriptions = v.get('descriptions', []) + #print(' ', ', '.join(v.get('enums'))) + for i in range(len(v.get('enums', []))): + print(f' {enums[i]} - {descriptions[i]}') + elif vtype == 'TYPE_BOOL': + pvs = v.get('descriptions') + for pv in pvs: + if type(pv) is dict: + pvalue = pv.get('value') + pdescription = pv.get('description') + print(f' {pvalue} - {pdescription}') + elif type(pv) is list: + print(f' {pv[0]}') + else: + print(f' {v.get("descriptions")[0]}') + print() + + +def delete_policy(): + cp = build() + customer = _get_customerid() + schemas = build_schemas(cp) + orgunit = None + i = 3 + body = {'requests': []} + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'orgunit': + orgunit = _get_orgunit(sys.argv[i+1]) + i += 2 + elif myarg in schemas: + body['requests'].append({'policySchema': schemas[myarg].name}) + i += 1 + else: + controlflow.system_error_exit(3, f'{myarg} is not a valid argument to "gam delete chromepolicy"') + if not orgunit: + controlflow.system_error_exit(3, 'You must specify an orgunit.') + for request in body['requests']: + request['policyTargetKey'] = {'targetResource': orgunit} + gapi.call(cp.customers().policies().orgunits(), 'batchInherit', customer=customer, body=body) + + +def update_policy(): + cp = build() + customer = _get_customerid() + schemas = build_schemas(cp) + i = 3 + body = {'requests': []} + orgunit = None + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'orgunit': + orgunit = _get_orgunit(sys.argv[i+1]) + i += 2 + elif myarg in schemas: + body['requests'].append({'policyValue': {'policySchema': schemas[myarg].name, + 'value': {}}, + 'updateMask': ''}) + i += 1 + while i < len(sys.argv): + field = sys.argv[i].lower() + if field == 'orgunit' or '.' in field: + break # field is actually a new policy name or orgunit + expected_fields = ', '.join([setting for setting in schemas[myarg].settings]) + if field not in expected_fields: + controlflow.system_error_exit(4, f'Expected {myarg} field of {expected_fields}. Got {field}.') + value = sys.argv[i+1] + vtype = schemas[myarg].settings[field].type + if vtype in ['TYPE_INT64', 'TYPE_INT32', 'TYPE_UINT64']: + if not value.isnumeric(): + controlflow.system_error_exit(7, f'Value for {myarg} {field} must be a number, got {value}') + value = int(value) + elif vtype in ['TYPE_BOOL']: + value = gam.getBoolean(value, field) + elif vtype in ['TYPE_ENUM']: + value = value.upper() + enum_values = schemas[myarg].settings[field].enums + if value not in enum_values: + expected_enums = ', '.join(enum_values) + controlflow.system_error_exit(8, f'Expected {myarg} {field} value to be one of {expected_enums}, got {value}') + prefix = schemas[myarg].settings[field].enum_prefix + value = f'{prefix}{value}' + elif vtype in ['TYPE_LIST']: + value = value.split(',') + body['requests'][-1]['policyValue']['value'][field] = value + body['requests'][-1]['updateMask'] += f'{field},' + i += 2 + else: + controlflow.system_error_exit(4, f'{myarg} is not a valid argument to "gam update chromepolicy"') + if not orgunit: + controlflow.system_error_exit(3, 'You must specify an orgunit') + for request in body['requests']: + request['policyTargetKey'] = {'targetResource': orgunit} + gapi.call(cp.customers().policies().orgunits(), 'batchModify', customer=customer, body=body) + diff --git a/src/gam/gapi/directory/customer.py b/src/gam/gapi/directory/customer.py index 6dc919d9..572b0b5b 100644 --- a/src/gam/gapi/directory/customer.py +++ b/src/gam/gapi/directory/customer.py @@ -8,18 +8,25 @@ from gam.gapi import directory as gapi_directory from gam.gapi import reports as gapi_reports +def _get_customerid(): + customer = GC_Values[GC_CUSTOMER_ID] + if customer != MY_CUSTOMER and customer[0] != 'C': + customer = 'C' + customer + return customer + def doGetCustomerInfo(): cd = gapi_directory.build() + customer_id = _get_customerid() customer_info = gapi.call(cd.customers(), 'get', - customerKey=GC_Values[GC_CUSTOMER_ID]) + customerKey=customer_id) print(f'Customer ID: {customer_info["id"]}') print(f'Primary Domain: {customer_info["customerDomain"]}') try: result = gapi.call( cd.domains(), 'get', - customer=customer_info['id'], + customer=customer_id, domainName=customer_info['customerDomain'], fields='verified', throw_reasons=[gapi.errors.ErrorReason.DOMAIN_NOT_FOUND]) @@ -35,7 +42,7 @@ def doGetCustomerInfo(): domains = gapi.get_items(cd.domains(), 'list', 'domains', - customer=GC_Values[GC_CUSTOMER_ID], + customer=customer_id, fields='domains(creationTime)') for domain in domains: creation_timestamp = int(domain['creationTime']) / 1000 @@ -67,9 +74,9 @@ def doGetCustomerInfo(): } parameters = ','.join(list(user_counts_map)) tryDate = datetime.date.today().strftime(YYYYMMDD_FORMAT) - customerId = GC_Values[GC_CUSTOMER_ID] - if customerId == MY_CUSTOMER: - customerId = None + reports_customer_id = customer_id + if reports_customer_id == MY_CUSTOMER: + reports_customer_id = None rep = gapi_reports.build() usage = None throw_reasons = [ @@ -80,7 +87,7 @@ def doGetCustomerInfo(): result = gapi.call(rep.customerUsageReports(), 'get', throw_reasons=throw_reasons, - customerId=customerId, + customerId=reports_customer_id, date=tryDate, parameters=parameters) except gapi.errors.GapiInvalidError as e: @@ -111,6 +118,7 @@ def doGetCustomerInfo(): def doUpdateCustomer(): cd = gapi_directory.build() body = {} + customer_id = _get_customerid() i = 3 while i < len(sys.argv): myarg = sys.argv[i].lower().replace('_', '') @@ -136,14 +144,19 @@ def doUpdateCustomer(): 'update customer"') gapi.call(cd.customers(), 'patch', - customerKey=GC_Values[GC_CUSTOMER_ID], + customerKey=customer_id, body=body) print('Updated customer') -def setTrueCustomerId(): - if GC_Values[GC_CUSTOMER_ID] == MY_CUSTOMER: - cd = gapi_directory.build() - GC_Values[GC_CUSTOMER_ID] = gapi.call(cd.customers(), 'get', - customerKey=GC_Values[GC_CUSTOMER_ID], - fields='id').get('id', GC_Values[GC_CUSTOMER_ID]) +def setTrueCustomerId(cd=None): + customer_id = GC_Values[GC_CUSTOMER_ID] + if customer_id == MY_CUSTOMER: + if not cd: + cd = gapi_directory.build() + result = gapi.call(cd.customers(), + 'get', + customerKey=customer_id, + fields='id') + GC_Values[GC_CUSTOMER_ID] = result.get('id', + customer_id) diff --git a/src/gam/utils.py b/src/gam/utils.py index e94b18c2..d8af6180 100644 --- a/src/gam/utils.py +++ b/src/gam/utils.py @@ -59,6 +59,16 @@ class _DeHTMLParser(HTMLParser): re.sub(r'\n +', '\n', ''.join(self.__text))).strip() +def commonprefix(m): + '''Given a list of strings m, return string which is prefix common to all''' + s1 = min(m) + s2 = max(m) + for i, c in enumerate(s1): + if c != s2[i]: + return s1[:i] + return s1 + + def dehtml(text): try: parser = _DeHTMLParser() diff --git a/src/gam/var.py b/src/gam/var.py index 3b6850ed..7ec0f3d6 100644 --- a/src/gam/var.py +++ b/src/gam/var.py @@ -297,6 +297,7 @@ API_VER_MAPPING = { 'driveactivity': 'v2', 'calendar': 'v3', 'cbcm': 'v1.1beta1', + 'chromepolicy': 'v1', 'classroom': 'v1', 'cloudidentity': 'v1', 'cloudidentity_beta': 'v1beta1', diff --git a/src/project-apis.txt b/src/project-apis.txt index 114306a7..95805b33 100644 --- a/src/project-apis.txt +++ b/src/project-apis.txt @@ -2,6 +2,7 @@ admin.googleapis.com alertcenter.googleapis.com calendar-json.googleapis.com chat.googleapis.com +chromepolicy.googleapis.com classroom.googleapis.com cloudidentity.googleapis.com contacts.googleapis.com