From 119538c10c51d5a29ba4bd1e5088052bfdb24f9a Mon Sep 17 00:00:00 2001 From: Jay Lee Date: Thu, 7 Apr 2022 00:48:40 +0000 Subject: [PATCH] Support for CAA levels --- src/gam/__init__.py | 11 +- src/gam/gapi/caa.py | 244 +++++++++++++++++++++++++++ src/gam/gapi/cloudresourcemanager.py | 31 ++++ src/project-apis.txt | 2 + 4 files changed, 287 insertions(+), 1 deletion(-) create mode 100644 src/gam/gapi/caa.py create mode 100644 src/gam/gapi/cloudresourcemanager.py diff --git a/src/gam/__init__.py b/src/gam/__init__.py index 70af5925..67079beb 100755 --- a/src/gam/__init__.py +++ b/src/gam/__init__.py @@ -55,6 +55,7 @@ from gam import auth from gam import controlflow from gam import display from gam import fileutils +from gam.gapi import caa as gapi_caa from gam.gapi import calendar as gapi_calendar from gam.gapi import cloudidentity as gapi_cloudidentity from gam.gapi import cbcm as gapi_cbcm @@ -848,7 +849,7 @@ def _getSvcAcctData(): controlflow.system_error_exit(6, None) GM_Globals[GM_OAUTH2SERVICE_JSON_DATA] = json.loads(json_string) -jwt_apis = ['chat'] # APIs which can handle OAuthless JWT tokens +jwt_apis = ['chat', 'accesscontextmanager'] # APIs which can handle OAuthless JWT tokens def getSvcAcctCredentials(scopes, act_as, api=None): try: _getSvcAcctData() @@ -11374,6 +11375,8 @@ def ProcessGAMCommand(args): gapi_directory_printers.create() elif argument in ['chatmessage']: gapi_chat.create_message() + elif argument in ['caalevel']: + gapi_caa.create_access_level() else: controlflow.invalid_argument_exit(argument, 'gam create') sys.exit(0) @@ -11438,6 +11441,8 @@ def ProcessGAMCommand(args): gapi_directory_printers.update() elif argument in ['chatmessage']: gapi_chat.update_message() + elif argument in ['caalevel']: + gapi_caa.update_access_level() else: controlflow.invalid_argument_exit(argument, 'gam update') sys.exit(0) @@ -11578,6 +11583,8 @@ def ProcessGAMCommand(args): gapi_chromepolicy.delete_policy() elif argument == 'chatmessage': gapi_chat.delete_message() + elif argument == 'caalevel': + gapi_caa.delete_access_level() else: controlflow.invalid_argument_exit(argument, 'gam delete') sys.exit(0) @@ -11699,6 +11706,8 @@ def ProcessGAMCommand(args): gapi_chat.print_spaces() elif argument in ['chatmembers']: gapi_chat.print_members() + elif argument in ['caalevels']: + gapi_caa.print_access_levels() else: controlflow.invalid_argument_exit(argument, 'gam print') sys.exit(0) diff --git a/src/gam/gapi/caa.py b/src/gam/gapi/caa.py new file mode 100644 index 00000000..d4abd8b8 --- /dev/null +++ b/src/gam/gapi/caa.py @@ -0,0 +1,244 @@ +import string +import sys + +import googleapiclient.errors + +import gam +from gam.var import * +from gam import controlflow +from gam import display +from gam import fileutils +from gam import gapi +from gam import utils +from gam.gapi import errors as gapi_errors +from gam.gapi import cloudresourcemanager as gapi_crm + +THROW_REASONS = [gapi_errors.ErrorReason.FOUR_O_THREE] +def build(): + return gam.buildGAPIServiceObject('accesscontextmanager', + act_as=None) + + +def get_access_policy(caa=None): + if not caa: + build() + parent = gapi_crm.get_org_id() + try: + aps = gapi.get_all_pages(caa.accessPolicies(), + 'list', + 'accessPolicies', + throw_reasons=THROW_REASONS, + parent=parent, + fields='accessPolicies/name') + except googleapiclient.errors.HttpError: + controlflow.system_error_exit(2, 'Your service account needs the Access Context Manager Reader or Editor role for your organization.') + if len(aps) != 1: + print(f'expected 1 access policy, got {len(aps)}.') + return + return aps[0]['name'] + + +def print_access_levels(): + caa = build() + ap_name = get_access_policy(caa) + try: + levels = gapi.get_all_pages(caa.accessPolicies().accessLevels(), + 'list', + 'accessLevels', + throw_reasons=THROW_REASONS, + parent=ap_name, + accessLevelFormat='CEL', fields='*') + except googleapiclient.errors.HttpError: + controlflow.system_error_exit(2, 'Your service account needs the Access Context Manager Reader or Editor role for your organization.') + for level in levels: + display.print_json(level) + print() + + +def build_os_constraints(constraints): + consts_obj = [] + constraints = constraints.upper().split(',') + valid_os_types = ['DESKTOP_MAC', 'DESKTOP_WINDOWS', 'DESKTOP_LINUX', 'DESKTOP_CHROMEOS', 'ANDROID', 'IOS'] + for constraint in constraints: + new_const = {} + new_const['osType'], new_const['minimumVersion'] = constraint.split(':') + if new_const['osType'] == 'VERIFIED_DESKTOP_CHROME_OS': + new_const['osType'] = 'DESKTOP_CHROME_OS' + new_const['requireVerifiedChromeOs'] = True + if new_const['osType'] not in valid_os_types: + controlflow.system_error_exit(2, f'expected os type of {", ".join(valid_os_types)} got {new_const["osType"]}') + consts_obj.append(new_const) + return consts_obj + + +def build_device_policy(i, schemas): + device_policy = {} + while True: + myarg = sys.argv[i].replace('_', '').lower() + if myarg == 'requirescreenlock': + device_policy['requireScreenLock'] = gam.getBoolean(sys.argv[i+1], myarg) + i += 2 + elif myarg == 'allowedencryptionstatuses': + allowed_statuses = gapi.get_enum_values_minus_unspecified(schemas["DevicePolicy"]["properties"]["allowedEncryptionStatuses"]["items"]["enum"]) + device_policy['allowedEncryptionStatuses'] = sys.argv[i+1].upper().split(',') + for status in device_policy['allowedEncryptionStatuses']: + if status not in allowed_statuses: + controlflow.system_error_exit(2, f'expected encryption status of {", ".join(allowed_statuses)} got {status}') + i += 2 + elif myarg == 'osconstraints': + device_policy['osConstraints'] = build_os_constraints(sys.argv[i+1]) + i += 2 + elif myarg == 'alloweddevicemanagementlevels': + allowed_levels = gapi.get_enum_values_minus_unspecified(schemas["DevicePolicy"]["properties"]["allowedDeviceManagementLevels"]["items"]["enum"]) + device_policy['allowedDeviceManagementLevels'] = sys.argv[i+1].upper().split(',') + for level in device_policy['allowedDeviceManagementLevels']: + if level == 'ADVANCED': + level == 'COMPLETE' + if level not in allowed_levels: + controlflow.system_error_exit(2, f'expected device management level of {", ".join(allowed_levels)} got {level}') + i += 2 + elif myarg == 'requireadminapproval': + device_policy['requireAdminApproval'] = gam.getBoolean(sys.argv[i+1], myarg) + i += 2 + elif myarg == 'requirecorpowned': + device_policy['requireCorpOwned'] = gam.getBoolean(sys.argv[i+1], myarg) + i += 2 + elif myarg == 'enddevicepolicy': + i += 1 + break + else: + controlflow.invalid_argument_exit(myarg, 'gam create/update caalevel') + return i, device_policy + + +def build_condition(i, schemas): + condition = {} + while True: + myarg = sys.argv[i].replace('_', '').lower() + if myarg == 'ipsubnetworks': + condition['ipSubnetworks'] = sys.argv[i+1].split(',') + i += 2 + elif myarg == 'devicepolicy': + i += 1 + i, condition['devicePolicy'] = build_device_policy(i, schemas) + elif myarg == 'requiredaccesslevels': + condition['requiredaccesslevels'] = sys.argv[i+1].split(',') + i += 2 + elif myarg == 'negate': + condition['negate'] = gam.getBoolean(sys.argv[i+1], myarg) + i += 2 + elif myarg == 'members': + condition['members'] = sys.argv[i+1].split(',') + i += 2 + elif myarg == 'regions': + condition['regions'] = sys.argv[i+1].split(',') + i += 2 + elif myarg == 'endcondition': + i += 1 + break + else: + controlflow.invalid_argument_exit(myarg, 'gam create/update caalevel') + return i, condition + + +def build_basic_level(i, schemas): + basic_level = {'conditions': []} + valid_functions = gapi.get_enum_values_minus_unspecified(schemas['BasicLevel']['properties']['combiningFunction']['enum']) + while i < len(sys.argv): + myarg = sys.argv[i].replace('_', '').lower() + if myarg == 'combiningfunction': + combiningFunction = sys.argv[i+1].upper() + if combiningFunction not in valid_functions: + controlflow.system_error_exit(2, f'expected combining function of {",".join(valid_functions)} got {combiningFunction}') + basic_level['combiningFunction'] = combiningFunction + i += 2 + elif myarg == 'condition': + i += 1 + i, condition = build_condition(i, schemas) + basic_level['conditions'].append(condition) + else: + controlflow.invalid_argument_exit(myarg, 'gam create/update caalevel') + return i, basic_level + + +def create_access_level(): + caa = build() + ap_name = get_access_policy(caa) + title = sys.argv[3].replace(' ', '_') + custom = {'expr': {'expression': sys.argv[4], 'title': 'expr'}} + allowed_title_chars = string.ascii_letters + string.digits + '_' + name = ''.join([c for c in title if c in allowed_title_chars])[:49] + name = f'{ap_name}/accessLevels/{name}' + body = { + 'name': name, + 'title': title, + } + i = 4 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'basic': + schemas = caa._rootDesc['schemas'] + i += 1 + i, body['basic'] = build_basic_level(i, schemas) + elif myarg == 'custom': + body['custom'] = {'expr': {'expression': sys.argv[i+1], 'title': 'expr'}} + i += 2 + else: + controlflow.invalid_argument_exit(myarg, 'gam create caalevel') + print(f'Creating access level {name}...') + try: + gapi.call(caa.accessPolicies().accessLevels(), + 'create', + throw_reasons=THROW_REASONS, + parent=ap_name, + body=body) + except googleapiclient.errors.HttpError: + controlflow.system_error_exit(2, 'Your service account needs the Access Context Manager Editor role for your organization.') + + +def update_access_level(): + caa = build() + name = sys.argv[3] + if not name.startswith('accessPolicies/'): + ap_name = get_access_policy(caa) + name = f'{ap_name}/accessLevels/{name}' + body = {} + i = 4 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'basic': + schemas = caa._rootDesc['schemas'] + i += 1 + i, body['basic'] = build_basic_level(i, schemas) + elif myarg == 'custom': + body['custom'] = {'expr': {'expression': sys.argv[i+1], 'title': 'expr'}} + i += 2 + else: + controlflow.invalid_argument_exit(myarg, 'gam update caalevel') + updateMask = ','.join(body.keys()) + print(f'Updating access level {name}...') + try: + gapi.call(caa.accessPolicies().accessLevels(), + 'patch', + throw_reasons=THROW_REASONS, + name=name, + updateMask=updateMask, + body=body) + except googleapiclient.errors.HttpError: + controlflow.system_error_exit(2, 'Your service account needs the Access Context Manager Editor role for your organization.') + + +def delete_access_level(): + caa = build() + name = sys.argv[3] + if not name.startswith('accessPolicies/'): + ap_name = get_access_policy(caa) + name = f'{ap_name}/accessLevels/{name}' + print('Deleting access level {name}...') + try: + gapi.call(caa.accessPolicies().accessLevels(), + 'delete', + name=name) + except googleapiclient.errors.HttpError: + controlflow.system_error_exit(2, 'Your service account needs the Access Context Manager Editor role for your organization.') + diff --git a/src/gam/gapi/cloudresourcemanager.py b/src/gam/gapi/cloudresourcemanager.py new file mode 100644 index 00000000..19322e3e --- /dev/null +++ b/src/gam/gapi/cloudresourcemanager.py @@ -0,0 +1,31 @@ +import string +import sys + +import googleapiclient.errors + +import gam +from gam.var import * +from gam import controlflow +from gam import display +from gam import fileutils +from gam import gapi +from gam import utils +from gam.gapi import errors as gapi_errors +from gam.gapi.directory import customer as gapi_directory_customer + +def build(): + return gam.buildGAPIServiceObject('cloudresourcemanager', + act_as=None) + + +def get_org_id(): + gapi_directory_customer.setTrueCustomerId() + crm = build() + query = f'directorycustomerid:{GC_Values[GC_CUSTOMER_ID]}' + orgs = gapi.get_all_pages(crm.organizations(), + 'search', + 'organizations', + query=query) + if len(orgs) < 1: + controlflow.system_error_exit(2, 'Your service account needs permission to read org id') + return orgs[0]['name'] diff --git a/src/project-apis.txt b/src/project-apis.txt index ac1677e6..9b102f54 100644 --- a/src/project-apis.txt +++ b/src/project-apis.txt @@ -1,3 +1,4 @@ +accesscontextmanager.googleapis.com. admin.googleapis.com alertcenter.googleapis.com calendar-json.googleapis.com @@ -6,6 +7,7 @@ chromemanagement.googleapis.com chromepolicy.googleapis.com classroom.googleapis.com cloudidentity.googleapis.com +cloudresourcemanager.googleapis.com contacts.googleapis.com drive.googleapis.com driveactivity.googleapis.com