import csv import sys import googleapiclient 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 cloudidentity as gapi_cloudidentity from gam.gapi.directory import customer as gapi_directory_customer def _get_device_customerid(): customer = GC_Values[GC_CUSTOMER_ID] if customer.startswith('C'): customer = customer[1:] return f'customers/{customer}' def create(): ci = gapi_cloudidentity.build_dwd() customer = _get_device_customerid() device_types = gapi.get_enum_values_minus_unspecified( ci._rootDesc['schemas']['GoogleAppsCloudidentityDevicesV1Device']['properties']['deviceType']['enum']) body = {'deviceType': '', 'serialNumber': ''} i = 3 while i < len(sys.argv): myarg = sys.argv[i].lower().replace('_', '') if myarg == 'serialnumber': body['serialNumber'] = sys.argv[i+1] i += 2 elif myarg == 'devicetype': body['deviceType'] = sys.argv[i+1].upper() if body['deviceType'] not in device_types: controlflow.expected_argument_exit('device_type', ', '.join(device_types), sys.argv[i+1]) i += 2 elif myarg in {'assettag', 'assetid'}: body['assetTag'] = sys.argv[i+1] i += 2 else: controlflow.invalid_argument_exit(sys.argv[i], 'gam create device') if not body['serialNumber'] or not body['deviceType']: controlflow.system_error_exit( 3, 'serial_number and device_type are required arguments for "gam create device".') result = gapi.call(ci.devices(), 'create', customer=customer, body=body) print(f'Created device {result["response"]["name"]}') def _parse_action(action): kwargs = {} i = 3 name = sys.argv[i] if name == 'id': i += 1 name = sys.argv[i] i += 1 if not name.startswith('devices/'): name = f'devices/{name}' customer = _get_device_customerid() # bah, inconsistencies in API if action == 'delete': kwargs['customer'] = customer else: kwargs['body'] = {'customer': customer} while i < len(sys.argv): myarg = sys.argv[i].lower().replace('_', '') if action == 'wipe' and myarg == 'removeresetlock': kwargs['body']['removeResetLock'] = True i += 1 else: controlflow.invalid_argument_exit(sys.argv[i], f'gam {action} device') return name, kwargs def info(): ci = gapi_cloudidentity.build_dwd() customer = _get_device_customerid() _, name = _get_deviceuser_name() device = gapi.call(ci.devices(), 'get', name=name, customer=customer) device_users = gapi.get_all_pages(ci.devices().deviceUsers(), 'list', 'deviceUsers', parent=name, customer=customer) for device_user in device_users: parent = device_user['name'] device_user['client_states'] = gapi.get_all_pages( ci.devices().deviceUsers().clientStates(), 'list', 'clientStates', parent=parent, customer=customer) display.print_json(device) print('Device Users:') display.print_json(device_users) def _generic_action(action, device_user=False): ci = gapi_cloudidentity.build_dwd() customer = _get_device_customerid() name, kwargs = _parse_action(action) if device_user: endpoint = ci.devices().deviceUsers() else: endpoint = ci.devices() op = gapi.call(endpoint, action, name=name, **kwargs) print(op) def delete(): _generic_action('delete') def cancel_wipe(): _generic_action('cancelWipe') def wipe(): _generic_action('wipe') def approve_user(): _generic_action('approve', True) def block_user(): _generic_action('block', True) def cancel_wipe_user(): _generic_action('cancelWipe', True) def delete_user(): _generic_action('delete', True) def wipe_user(): _generic_action('wipe', True) def _get_deviceuser_name(): i = 3 name = sys.argv[i] if name == 'id': i += 1 name = sys.argv[i] if not name.startswith('devices/'): name = f'devices/{name}' return (i+1, name) def info_state(): ci = gapi_cloudidentity.build_dwd() gapi_directory_customer.setTrueCustomerId() customer = _get_device_customerid() customer_id = customer[10:] client_id = f'{customer_id}-gam' i, deviceuser = _get_deviceuser_name() while i < len(sys.argv): myarg = sys.argv[i].lower().replace('_', '') if myarg == 'clientid': client_id = f'{customer_id}-{sys.argv[i+1]}' i += 2 else: controlflow.invalid_argument_exit(sys.argv[i], 'gam info deviceuserstate') name = f'{deviceuser}/clientStates/{client_id}' result = gapi.call(ci.devices().deviceUsers().clientStates(), 'get', name=name, customer=customer) display.print_json(result) def update_state(): ci = gapi_cloudidentity.build_dwd() gapi_directory_customer.setTrueCustomerId() customer = _get_device_customerid() customer_id = customer[10:] client_id = f'{customer_id}-gam' body = {} i, deviceuser = _get_deviceuser_name() while i < len(sys.argv): myarg = sys.argv[i].lower().replace('_', '') if myarg == 'clientid': client_id = f'{customer_id}-{sys.argv[i+1]}' i += 2 elif myarg in ['assettag', 'assettags']: body['assetTags'] = gam.shlexSplitList(sys.argv[i+1]) if body['assetTags'] == ['clear']: # TODO: this doesn't work to clear # existing values. Figure out why. body['assetTags'] = [None] i += 2 elif myarg in ['compliantstate', 'compliancestate']: comp_states = gapi.get_enum_values_minus_unspecified( ci._rootDesc['schemas']['GoogleAppsCloudidentityDevicesV1ClientState']['properties']['complianceState']['enum']) body['complianceState'] = sys.argv[i+1].upper() if body['complianceState'] not in comp_states: controlflow.expected_argument_exit('compliant_state', ', '.join(comp_states), sys.argv[i+1]) i += 2 elif myarg == 'customid': body['customId'] = sys.argv[i+1] i += 2 elif myarg == 'healthscore': health_scores = gapi.get_enum_values_minus_unspecified( ci._rootDesc['schemas']['GoogleAppsCloudidentityDevicesV1ClientState']['properties']['healthScore']['enum']) body['healthScore'] = sys.argv[i+1].upper() if body['healthScore'] == 'CLEAR': body['healthScore'] = None if body['healthScore'] and body['healthScore'] not in health_scores: controlflow.expected_argument_exit('health_score', ', '.join(health_scores), sys.argv[i+1]) i += 2 elif myarg == 'customvalue': allowed_types = ['bool', 'number', 'string'] value_type = sys.argv[i+1].lower() if value_type not in allowed_types: controlflow.expected_argument_exit('custom_value', ', '.join(allowed_types), sys.argv[i+1]) key = sys.argv[i+2] value = sys.argv[i+3] if value_type == 'bool': value = gam.getBoolean(value, key) elif value_type == 'number': value = int(value) body.setdefault('keyValuePairs', {}) body['keyValuePairs'][key] = {f'{value_type}Value': value} i += 4 elif myarg in ['managedstate']: managed_states = gapi.get_enum_values_minus_unspecified( ci._rootDesc['schemas']['GoogleAppsCloudidentityDevicesV1ClientState']['properties']['managed']['enum']) body['managed'] = sys.argv[i+1].upper() if body['managed'] == 'CLEAR': body['managed'] = None if body['managed'] and body['managed'] not in managed_states: controlflow.expected_argument_exit('managed_state', ', '.join(managed_states), sys.argv[i+1]) i += 2 elif myarg in ['scorereason']: body['scoreReason'] = sys.argv[i+1] if body['scoreReason'] == 'clear': body['scoreReason'] = None i += 2 else: controlflow.invalid_argument_exit(sys.argv[i], 'gam update deviceuserstate') name = f'{deviceuser}/clientStates/{client_id}' updateMask = ','.join(body.keys()) result = gapi.call(ci.devices().deviceUsers().clientStates(), 'patch', name=name, customer=customer, updateMask=updateMask, body=body) display.print_json(result) def print_(): # This function is rather messy thanks to # https://github.com/GAM-team/GAM/issues/1534 # I'd prefer to keep it all in this function for now but if: # - we find other list() operations that also hit this bug OR # - it looks like this issue is going to exist on Google's side # for a long time. # I'll enterain some cleanup here to "functionalize" (yuck) all of this. ci = gapi_cloudidentity.build_dwd() customer = _get_device_customerid() parent = 'devices/-' device_filter = None get_device_users = True view = None orderByList = [] # default sort order needed by our 1 hour bug workaround orderBy = 'create_time' titles = [] csvRows = [] todrive = False sortHeaders = False i = 3 while i < len(sys.argv): myarg = sys.argv[i].lower().replace('_', '') if myarg in ['filter', 'query']: device_filter = sys.argv[i+1] i += 2 elif myarg == 'company': view = 'COMPANY_INVENTORY' i += 1 elif myarg == 'personal': view = 'USER_ASSIGNED_DEVICES' i += 1 elif myarg == 'nocompanydevices': view = 'USER_ASSIGNED_DEVICES' i += 1 elif myarg == 'nopersonaldevices': view = 'COMPANY_INVENTORY' i += 1 elif myarg == 'nodeviceusers': get_device_users = False i += 1 elif myarg == 'todrive': todrive = True i += 1 elif myarg == 'orderby': fieldName = sys.argv[i + 1].lower() i += 2 if fieldName in DEVICE_ORDERBY_CHOICES_MAP: fieldName = DEVICE_ORDERBY_CHOICES_MAP[fieldName] orderBy = '' if i < len(sys.argv): orderBy = sys.argv[i].lower() if orderBy in SORTORDER_CHOICES_MAP: orderBy = SORTORDER_CHOICES_MAP[orderBy] i += 1 if orderBy != 'DESCENDING': orderByList.append(fieldName) else: orderByList.append(f'{fieldName} desc') else: controlflow.expected_argument_exit( 'orderby', ', '.join(sorted(DEVICE_ORDERBY_CHOICES_MAP)), fieldName) elif myarg == 'sortheaders': sortHeaders = True i += 1 else: controlflow.invalid_argument_exit(sys.argv[i], 'gam print devices') view_name_map = { None: 'Devices', 'COMPANY_INVENTORY': 'Company Devices', 'USER_ASSIGNED_DEVICES': 'Personal Devices', } if orderByList: orderBy = ','.join(orderByList) custom_device_filter = bool(device_filter) # we store the devices in a dict keyed by name which is a unique ID. # that way when we get duplicate devices we just overwrite the name # with the latest copy we saw. devices = {} page_message = gapi.got_total_items_msg(view_name_map[view], '...\n') pageToken = None newest_device_date = '' total_items = 0 while True: try: a_page = gapi.call(ci.devices(), 'list', customer=customer, pageSize=100, pageToken=pageToken, filter=device_filter, view=view, orderBy=orderBy, throw_reasons=[gapi_errors.ErrorReason.FOUR_O_O]) except googleapiclient.errors.HttpError: sys.stderr.write('WARNING: GAM hit Google internal bug 237397223. Please file a Google Support ticket stating that you are encountering this bug.\n') if orderBy != 'create_time' or custom_device_filter: controlflow.system_error_exit(5, 'GAM workaround for this issue only works if filter and orderby arguments are not used.\n') sys.stderr.write(f' attempting to work around the bug by filtering for devices created on or after the newest we\'ve seen ({newest_device_date})...') device_filter = f'register:{newest_device_date}..' pageToken = None continue for dev in a_page.get('devices', []): total_items += 1 devices[dev['name']] = dev dev_date = dev.get('createTime', '') # remove the Z dev_date = dev_date[:-1] # remove microseconds dev_date = dev_date.split('.')[0] if dev_date > newest_device_date: newest_device_date = dev_date pageToken = a_page.get('nextPageToken') if not pageToken: break sys.stderr.write(page_message.replace('%%total_items%%', str(total_items))) if get_device_users: page_message = gapi.got_total_items_msg('Device Users', '...\n') pageToken = None newest_deviceuser_date = '' total_items = 0 device_users = {} if not custom_device_filter: device_filter = None while True: try: a_page = gapi.call(ci.devices().deviceUsers(), 'list', customer=customer, parent=parent, pageSize=20, orderBy=orderBy, filter=device_filter, pageToken=pageToken, throw_reasons=[gapi_errors.ErrorReason.FOUR_O_O]) except googleapiclient.errors.HttpError: sys.stderr.write('WARNING: GAM hit Google internal bug 237397223. Please file a Google Support ticket stating that you are encountering this bug.\n') if orderBy != 'create_time' or custom_device_filter: controlflow.system_error_exit(5, 'GAM workaround for this issue only works if filter and orderby arguments are not used.\n') sys.stderr.write(f' attempting to work around the bug by filtering for device users created on or after the newest we\'ve seen ({newest_deviceuser_date})...') device_filter = f'register:{newest_deviceuser_date}..' pageToken = None continue for device_user in a_page.get('deviceUsers', []): total_items += 1 dev_date = device_user.get('createTime', '') # remove the Z dev_date = dev_date[:-1] # remove microseconds dev_date = dev_date.split('.')[0] if dev_date > newest_deviceuser_date: newest_deviceuser_date = dev_date deviceuser_name = device_user['name'] device_users[deviceuser_name] = device_user pageToken = a_page.get('nextPageToken') if not pageToken: break sys.stderr.write(page_message.replace('%%total_items%%', str(total_items))) for deviceuser_name, device_user in device_users.items(): device_id = deviceuser_name.split('/')[1] device_name = f'devices/{device_id}' if 'users' not in devices[device_name]: devices[device_name]['users'] = [] devices[device_name]['users'].append(device_user) for device in devices.values(): device = utils.flatten_json(device) for a_key in device: if a_key not in titles: titles.append(a_key) csvRows.append(device) if sortHeaders: display.sort_csv_titles(['name',], titles) display.write_csv_file(csvRows, titles, 'Devices', todrive) def sync(): ci = gapi_cloudidentity.build_dwd() device_types = gapi.get_enum_values_minus_unspecified( ci._rootDesc['schemas']['GoogleAppsCloudidentityDevicesV1Device']['properties']['deviceType']['enum']) customer = _get_device_customerid() device_filter = None csv_file = None serialnumber_column = 'serialNumber' devicetype_column = 'deviceType' static_devicetype = None assettag_column = None unassigned_missing_action = 'delete' assigned_missing_action = 'donothing' missing_actions = ['delete', 'wipe', 'donothing'] i = 3 while i < len(sys.argv): myarg = sys.argv[i].lower().replace('_', '') if myarg in ['filter', 'query']: device_filter = sys.argv[i+1] i += 2 elif myarg == 'csvfile': csv_file = sys.argv[i+1] i += 2 elif myarg == 'serialnumbercolumn': serialnumber_column = sys.argv[i+1] i += 2 elif myarg == 'devicetypecolumn': devicetype_column = sys.argv[i+1] i += 2 elif myarg == 'staticdevicetype': static_devicetype = sys.argv[i+1].upper() if static_devicetype not in device_types: controlflow.expected_argument_exit('device_type', ', '.join(device_types), sys.argv[i+1]) i += 2 elif myarg in {'assettagcolumn', 'assetidcolumn'}: assettag_column = sys.argv[i+1] i += 2 elif myarg == 'unassignedmissingaction': unassigned_missing_action = sys.argv[i+1].lower().replace('_', '') if unassigned_missing_action not in missing_actions: controlflow.expected_argument_exit('unassigned_missing_action', ', '.join(missing_actions), sys.argv[i+1]) i += 2 elif myarg == 'assignedmissingaction': assigned_missing_action = sys.argv[i+1].lower().replace('_', '') if assigned_missing_action not in missing_actions: controlflow.expected_argument_exit('assigned_missing_action', ', '.join(missing_actions), sys.argv[i+1]) i += 2 else: controlflow.invalid_argument_exit(sys.argv[i], 'gam sync devices') if not csv_file: controlflow.system_error_exit( 3, 'csvfile is a required argument for "gam sync devices".') f = fileutils.open_file(csv_file) input_file = csv.DictReader(f, restval='') if serialnumber_column not in input_file.fieldnames: controlflow.csv_field_error_exit(serialnumber_column, input_file.fieldnames) if not static_devicetype and devicetype_column not in input_file.fieldnames: controlflow.csv_field_error_exit(devicetype_column, input_file.fieldnames) if assettag_column and assettag_column not in input_file.fieldnames: controlflow.csv_field_error_exit(assettag_column, input_file.fieldnames) local_devices = {} for row in input_file: # upper() is very important to comparison since Google # always return uppercase serials local_device = {'serialNumber': row[serialnumber_column].strip().upper()} if static_devicetype: local_device['deviceType'] = static_devicetype else: local_device['deviceType'] = row[devicetype_column].strip() sndt = f"{local_device['serialNumber']}-{local_device['deviceType']}" if assettag_column: local_device['assetTag'] = row[assettag_column].strip() sndt += f"-{local_device['assetTag']}" local_devices[sndt] = local_device fileutils.close_file(f) page_message = gapi.got_total_items_msg('Company Devices', '...\n') device_fields = ['serialNumber', 'deviceType', 'lastSyncTime', 'name'] if assettag_column: device_fields.append('assetTag') fields = f'nextPageToken,devices({",".join(device_fields)})' remote_devices = {} remote_device_map = {} result = gapi.get_all_pages(ci.devices(), 'list', 'devices', customer=customer, page_message=page_message, pageSize=100, filter=device_filter, view='COMPANY_INVENTORY', fields=fields) for remote_device in result: sn = remote_device['serialNumber'] last_sync = remote_device.pop('lastSyncTime', NEVER_TIME_NOMS) name = remote_device.pop('name') sndt = f"{remote_device['serialNumber']}-{remote_device['deviceType']}" if assettag_column: if 'assetTag' not in remote_device: remote_device['assetTag'] = '' sndt += f"-{remote_device['assetTag']}" remote_devices[sndt] = remote_device remote_device_map[sndt] = {'name': name} if last_sync == NEVER_TIME_NOMS: remote_device_map[sndt]['unassigned'] = True devices_to_add = [] for sndt, device in iter(local_devices.items()): if sndt not in remote_devices: devices_to_add.append(device) missing_devices = [] for sndt, device in iter(remote_devices.items()): if sndt not in local_devices: missing_devices.append(device) print(f'Need to add {len(devices_to_add)} and remove {len(missing_devices)} devices...') for add_device in devices_to_add: print(f'Creating {add_device["serialNumber"]}') try: result = gapi.call(ci.devices(), 'create', customer=customer, throw_reasons=[gapi_errors.ErrorReason.FOUR_O_NINE], body=add_device) print(f' created {result["response"]["deviceType"]} device {result["response"]["name"]} with serial {result["response"]["serialNumber"]}') except googleapiclient.errors.HttpError: print(f' {add_device["serialNumber"]} already exists') for missing_device in missing_devices: sn = missing_device['serialNumber'] sndt = f"{sn}-{missing_device['deviceType']}" if assettag_column: sndt += f"-{missing_device['assetTag']}" name = remote_device_map[sndt]['name'] unassigned = remote_device_map[sndt].get('unassigned') action = unassigned_missing_action if unassigned else assigned_missing_action if action == 'donothing': pass else: if action == 'delete': kwargs = {'customer': customer} else: kwargs = {'body': {'customer': customer}} gapi.call(ci.devices(), action, name=name, **kwargs) print(f'{action}d {sn}')