Files
GoogleDriveManagement/src/gam/gapi/cloudidentity/devices.py
2022-08-31 14:10:22 -04:00

571 lines
23 KiB
Python

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}')