From 244398e096dfd8db74c74ed340f59ed0d7824a5f Mon Sep 17 00:00:00 2001 From: Jay Lee Date: Thu, 10 Sep 2020 11:25:59 -0400 Subject: [PATCH] Initial support for delegated admin service accounts (DASA) Google now allows GCP service accounts to be granted delegated admin status for a G Suite domain. To use this, admins can grant the service account email address delegated admin rights in the admin console and then set some environment variables for GAM to use: OAUTHFILE=oauth2service.json GA_DOMAIN=example.com # your primary domain name in Google CUSTOMER_ID=1d80dfc # admin.google.com > Account > Account settings > Customer ID --- src/gam/__init__.py | 81 +++++++++++----------------- src/gam/auth/__init__.py | 19 ++++++- src/gam/gapi/directory/orgunits.py | 4 +- src/gam/gapi/directory/privileges.py | 25 +++++---- src/gam/gapi/directory/roles.py | 72 +++++++++++++++++++++++++ 5 files changed, 135 insertions(+), 66 deletions(-) create mode 100644 src/gam/gapi/directory/roles.py diff --git a/src/gam/__init__.py b/src/gam/__init__.py index 06f20254..a96ef7fb 100755 --- a/src/gam/__init__.py +++ b/src/gam/__init__.py @@ -64,6 +64,7 @@ from gam.gapi.directory import mobiledevices as gapi_directory_mobiledevices from gam.gapi.directory import orgunits as gapi_directory_orgunits from gam.gapi.directory import privileges as gapi_directory_privileges from gam.gapi.directory import resource as gapi_directory_resource +from gam.gapi.directory import roles as gapi_directory_roles from gam.gapi import siteverification as gapi_siteverification from gam.gapi import errors as gapi_errors from gam.gapi import reports as gapi_reports @@ -843,14 +844,14 @@ def getOauth2TxtStorageCredentials(): return None -def getValidOauth2TxtCredentials(force_refresh=False): +def getValidOauth2TxtCredentials(force_refresh=False, api=None): """Gets OAuth2 credentials which are guaranteed to be fresh and valid.""" try: - credentials = auth.get_admin_credentials() + credentials = auth.get_admin_credentials(api) except gam.auth.oauth.InvalidCredentialsFileError: doRequestOAuth() # Make a new request which should store new creds. - return getValidOauth2TxtCredentials(force_refresh=force_refresh) - + return getValidOauth2TxtCredentials(force_refresh=force_refresh, + api=api) if credentials.expired or force_refresh: request = transport.create_request() credentials.refresh(request) @@ -922,7 +923,7 @@ def getService(api, http): def buildGAPIObject(api): GM_Globals[GM_CURRENT_API_USER] = None - credentials = getValidOauth2TxtCredentials() + credentials = getValidOauth2TxtCredentials(api=getAPIVersion(api)[0]) credentials.user_agent = GAM_INFO http = transport.AuthorizedHttp( credentials, transport.create_http(cache=GM_Globals[GM_CACHE_DIR])) @@ -1647,7 +1648,8 @@ def doCreateAdmin(): ', '.join(['customer', 'org_unit']), body['scopeType']) if body['scopeType'] == 'ORG_UNIT': - orgUnit, orgUnitId = gapi_directory_orgunits.getOrgUnitId(sys.argv[6], cd) + orgUnit, orgUnitId = gapi_directory_orgunits.getOrgUnitId( + sys.argv[6], cd) body['orgUnitId'] = orgUnitId[3:] scope = f'ORG_UNIT {orgUnit}' else: @@ -1659,37 +1661,6 @@ def doCreateAdmin(): body=body) -def doPrintAdminRoles(): - cd = buildGAPIObject('directory') - todrive = False - titles = [ - 'roleId', 'roleName', 'roleDescription', 'isSuperAdminRole', - 'isSystemRole' - ] - fields = f'nextPageToken,items({",".join(titles)})' - csvRows = [] - i = 3 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'todrive': - todrive = True - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], - 'gam print adminroles') - roles = gapi.get_all_pages(cd.roles(), - 'list', - 'items', - customer=GC_Values[GC_CUSTOMER_ID], - fields=fields) - for role in roles: - role_attrib = {} - for key, value in list(role.items()): - role_attrib[key] = value - csvRows.append(role_attrib) - display.write_csv_file(csvRows, titles, 'Admin Roles', todrive) - - def doPrintAdmins(): cd = buildGAPIObject('directory') roleId = None @@ -1731,7 +1702,9 @@ def doPrintAdmins(): admin_attrib['role'] = role_from_roleid(value) elif key == 'orgUnitId': value = f'id:{value}' - admin_attrib['orgUnit'] = gapi_directory_orgunits.orgunit_from_orgunitid(value) + admin_attrib[ + 'orgUnit'] = gapi_directory_orgunits.orgunit_from_orgunitid( + value) admin_attrib[key] = value csvRows.append(admin_attrib) display.write_csv_file(csvRows, titles, 'Admins', todrive) @@ -6517,7 +6490,8 @@ def getUserAttributes(i, cd, updateCmd): body['agreedToTerms'] = getBoolean(sys.argv[i + 1], myarg) i += 2 elif myarg in ['org', 'ou']: - body['orgUnitPath'] = gapi_directory_orgunits.getOrgUnitItem(sys.argv[i + 1], pathOnly=True) + body['orgUnitPath'] = gapi_directory_orgunits.getOrgUnitItem( + sys.argv[i + 1], pathOnly=True) i += 2 elif myarg in ['language', 'languages']: i += 1 @@ -8799,6 +8773,7 @@ def doGetUserInfo(user_email=None): 'list', 'groups', userKey=user_email, + customer=GC_Values[GC_CUSTOMER_ID], fields='groups(name,email),nextPageToken', throw_reasons=throw_reasons) if groups: @@ -9108,7 +9083,8 @@ def doUndeleteUser(): while i < len(sys.argv): myarg = sys.argv[i].lower() if myarg in ['ou', 'org']: - orgUnit = gapi_directory_orgunits.makeOrgUnitPathAbsolute(sys.argv[i + 1]) + orgUnit = gapi_directory_orgunits.makeOrgUnitPathAbsolute( + sys.argv[i + 1]) i += 2 else: controlflow.invalid_argument_exit(sys.argv[i], 'gam undelete user') @@ -9911,8 +9887,9 @@ def getUsersToModify(entity_type=None, users = [] for member in members: if ((not groupUserMembersOnly and not includeDerivedMembership) or - (member['type'] == 'USER')) and gapi_directory_groups._checkMemberRoleIsSuspended( - member, validRoles, checkSuspended): + (member['type'] == 'USER') + ) and gapi_directory_groups._checkMemberRoleIsSuspended( + member, validRoles, checkSuspended): users.append(member.get('email', member['id'])) elif entity_type in ['cigroup']: got_uids = False @@ -9934,15 +9911,15 @@ def getUsersToModify(entity_type=None, ) page_message = gapi.got_total_items_msg(f'{member_type_message}', '...') - members = gapi.get_all_pages( - ci.groups().memberships(), - 'list', - 'memberships', - page_message=page_message, - parent=parent, - fields=fields) + members = gapi.get_all_pages(ci.groups().memberships(), + 'list', + 'memberships', + page_message=page_message, + parent=parent, + fields=fields) if member_type: - members = gapi_cloudidentity_groups.filter_members_to_roles(members, [member_type]) + members = gapi_cloudidentity_groups.filter_members_to_roles( + members, [member_type]) users = [] for member in members: users.append(member['memberKey']['id']) @@ -11225,6 +11202,8 @@ def ProcessGAMCommand(args): doCreateAlertFeedback() elif argument in ['gcpfolder']: createGCPFolder() + elif argument in ['adminrole']: + gapi_directory_roles.create() else: controlflow.invalid_argument_exit(argument, 'gam create') sys.exit(0) @@ -11458,7 +11437,7 @@ def ProcessGAMCommand(args): elif argument == 'admins': doPrintAdmins() elif argument in ['roles', 'adminroles']: - doPrintAdminRoles() + gapi_directory_roles.print_() elif argument in ['guardian', 'guardians']: doPrintShowGuardians(True) elif argument in ['matters', 'vaultmatters']: diff --git a/src/gam/auth/__init__.py b/src/gam/auth/__init__.py index 9cd50320..818cd2b3 100644 --- a/src/gam/auth/__init__.py +++ b/src/gam/auth/__init__.py @@ -1,5 +1,11 @@ """Authentication/Credentials general purpose and convenience methods.""" +import json +import os +import time + +from google.auth.jwt import Credentials as JWTCredentials + from gam.auth import oauth from gam.var import _FN_OAUTH2_TXT from gam.var import GC_OAUTH2_TXT @@ -20,7 +26,16 @@ def get_admin_credentials_filename(): return DEFAULT_OAUTH_STORAGE_FILE -def get_admin_credentials(): +def get_admin_credentials(api=None): """Gets oauth.Credentials that are authenticated as the domain's admin user.""" credential_file = get_admin_credentials_filename() - return oauth.Credentials.from_credentials_file(credential_file) + if not os.path.isfile(credential_file): + raise oauth.InvalidCredentialsFileError + with open(credential_file, 'r') as f: + creds_data = json.load(f) + if 'token' in creds_data: + return oauth.Credentials.from_credentials_file(credential_file) + elif 'private_key' in creds_data: + audience = f'https://{api}.googleapis.com/' + return JWTCredentials.from_service_account_info(creds_data, + audience=audience) diff --git a/src/gam/gapi/directory/orgunits.py b/src/gam/gapi/directory/orgunits.py index a92f2d1c..bd405704 100644 --- a/src/gam/gapi/directory/orgunits.py +++ b/src/gam/gapi/directory/orgunits.py @@ -404,7 +404,7 @@ def getOrgUnitId(orgUnit, cd=None): def buildOrgUnitIdToNameMap(): - cd = buildGAPIObject('directory') + cd = gapi_directory.build() result = gapi.call(cd.orgunits(), 'list', customerId=GC_Values[GC_CUSTOMER_ID], @@ -420,5 +420,3 @@ def orgunit_from_orgunitid(orgunitid): if not GM_Globals[GM_MAP_ORGUNIT_ID_TO_NAME]: buildOrgUnitIdToNameMap() return GM_Globals[GM_MAP_ORGUNIT_ID_TO_NAME].get(orgunitid, orgunitid) - - diff --git a/src/gam/gapi/directory/privileges.py b/src/gam/gapi/directory/privileges.py index 11a658bb..51d71c23 100644 --- a/src/gam/gapi/directory/privileges.py +++ b/src/gam/gapi/directory/privileges.py @@ -3,6 +3,7 @@ from gam import display from gam import gapi from gam.gapi import directory as gapi_directory + def flatten_privilege_list(privs, parent=None): flat_privs = [] for priv in privs: @@ -10,18 +11,22 @@ def flatten_privilege_list(privs, parent=None): if parent: priv['parent'] = parent if priv.get('childPrivileges'): - children = flatten_privilege_list(priv['childPrivileges'], parent=priv['privilegeName']) - priv['children'] = ' '.join([child['privilegeName'] for child in children]) - del(priv['childPrivileges']) + children = flatten_privilege_list(priv['childPrivileges'], + parent=priv['privilegeName']) + priv['children'] = ' '.join( + [child['privilegeName'] for child in children]) + del (priv['childPrivileges']) flat_privs = flat_privs + children flat_privs.append(priv) return flat_privs - -def print_(): - cd = gapi_directory.build() - privs = gapi.call(cd.privileges(), 'list', - customer=GC_Values[GC_CUSTOMER_ID]) - privs = flatten_privilege_list(privs.get('items', [])) - display.print_json(privs) \ No newline at end of file +def print_(return_only=False): + cd = gapi_directory.build() + privs = gapi.call(cd.privileges(), + 'list', + customer=GC_Values[GC_CUSTOMER_ID]) + privs = flatten_privilege_list(privs.get('items', [])) + if return_only: + return privs + display.print_json(privs) diff --git a/src/gam/gapi/directory/roles.py b/src/gam/gapi/directory/roles.py new file mode 100644 index 00000000..3357f45d --- /dev/null +++ b/src/gam/gapi/directory/roles.py @@ -0,0 +1,72 @@ +import sys + +from gam.var import GC_Values, GC_CUSTOMER_ID +from gam import display +from gam import gapi +from gam.gapi import directory as gapi_directory +from gam.gapi.directory import privileges as gapi_directory_privileges + + +def create(): + cd = gapi_directory.build() + body = {'privileges': []} + all_privileges = gapi_directory_privileges.print_(return_only=True) + i = 3 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'privileges': + privs = sys.argv[i + 1] + if privs == 'all': + body['rolePrivileges'] = all_privileges + elif privs == 'all_ou': + body['rolePrivileges'] = [ + p for p in all_privileges if p.get('isOuScopable') + ] + else: + # Known broken, need to get serviceName in here also... + body['rolePrivileges'] = [{ + 'privilegeName': p + } for p in sys.argv[i + 1].split(',')] + i += 2 + elif myarg == 'name': + body['roleName'] = sys.argv[i + 1] + i += 2 + else: + controlflow.invalid_argument_exit(sys.argv[i], + 'gam create adminrole') + print(f'Creating role {body["roleName"]}') + gapi.call(cd.roles(), + 'insert', + customer=GC_Values[GC_CUSTOMER_ID], + body=body) + + +def print_(): + cd = gapi_directory.build() + todrive = False + titles = [ + 'roleId', 'roleName', 'roleDescription', 'isSuperAdminRole', + 'isSystemRole' + ] + fields = f'nextPageToken,items({",".join(titles)})' + csvRows = [] + i = 3 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'todrive': + todrive = True + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], + 'gam print adminroles') + roles = gapi.get_all_pages(cd.roles(), + 'list', + 'items', + customer=GC_Values[GC_CUSTOMER_ID], + fields=fields) + for role in roles: + role_attrib = {} + for key, value in list(role.items()): + role_attrib[key] = value + csvRows.append(role_attrib) + display.write_csv_file(csvRows, titles, 'Admin Roles', todrive)