From 72a683f2b1362a8411614c266913994145a7bec7 Mon Sep 17 00:00:00 2001 From: Jay Lee Date: Tue, 4 May 2021 08:12:35 -0400 Subject: [PATCH] Merge branches (#1377) * Fix tests with apiclient >= 2.1 * disable MacOS 11 job * info user grouptree and info cigroup membertree * build updates --- .github/workflows/build.yml | 12 +-- src/gam/__init__.py | 51 +++++++++- src/gam/gapi/cloudidentity/groups.py | 137 +++++++++++++++++++-------- 3 files changed, 155 insertions(+), 45 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index df1eba87..8324771b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,10 +12,10 @@ defaults: working-directory: src env: - BUILD_PYTHON_VERSION: "3.9.4" - MIN_PYTHON_VERSION: "3.9.4" + BUILD_PYTHON_VERSION: "3.9.5" + MIN_PYTHON_VERSION: "3.9.5" BUILD_OPENSSL_VERSION: "1.1.1k" - MIN_OPENSSL_VERSION: "1.1.1i" + MIN_OPENSSL_VERSION: "1.1.1k" PATCHELF_VERSION: "0.12" # PYINSTALLER_VERSION can be full commit hash or version like v4.20 PYINSTALLER_VERSION: "e20e74c03768d432d48665b8ef1e02511b16e4be" @@ -65,7 +65,7 @@ jobs: jid: 5 goal: "build" gamos: "windows" - python: 3.9.4 + python: 3.9 5 pyarch: "x64" platform: "x86_64" - os: windows-2019 @@ -73,7 +73,7 @@ jobs: goal: "build" gamos: "windows" platform: "x86" - python: 3.9.4 + python: 3.9.5 pyarch: "x86" - os: ubuntu-20.04 goal: "test" @@ -108,7 +108,7 @@ jobs: path: | ~/python ~/ssl - key: ${{ matrix.os }}-${{ matrix.jid }}-20210407 + key: ${{ matrix.os }}-${{ matrix.jid }}-20210504 - name: Set env variables env: diff --git a/src/gam/__init__.py b/src/gam/__init__.py index 5f1a989b..07df3a67 100755 --- a/src/gam/__init__.py +++ b/src/gam/__init__.py @@ -8731,7 +8731,11 @@ def doGetUserInfo(user_email=None): i = 4 else: user_email = _get_admin_email() - getSchemas = getAliases = getGroups = getLicenses = True + getSchemas = True + getAliases = True + getGroups = True + getCIGroups = False + getLicenses = True projection = 'full' customFieldMask = viewType = None skus = sorted(SKUS) @@ -8743,6 +8747,10 @@ def doGetUserInfo(user_email=None): elif myarg == 'nogroups': getGroups = False i += 1 + elif myarg == 'grouptree': + getCIGroups = True + getGroups = False + i += 1 elif myarg in ['nolicenses', 'nolicences']: getLicenses = False i += 1 @@ -9008,6 +9016,34 @@ def doGetUserInfo(user_email=None): print(f' {group["name"]} <{group["email"]}>') except gapi.errors.GapiForbiddenError: print('No access to show user groups.') + elif getCIGroups: + memberships = gapi_cloudidentity_groups.get_membership_graph(user_email) + print('\nGroup Mmebership Tree:') + group_name_mapping = {} + group_displayname_mapping = {} + groups = memberships.get('groups', []) + for group in groups: + group_name = group.get('name') + group_key = group.get('groupKey', {}) + group_email = group_key.get('id', '') + group_display_name = group.get('displayName', '') + group_name_mapping[group_name] = group_email + group_displayname_mapping[group_email] = group_display_name + edges = [] + seen_group_count = {} + groups_with_multi_memberships = [] + for adj in memberships.get('adjacencyList', []): + group_name = adj.get('group', '') + group_email = group_name_mapping[group_name] + for edge in adj.get('edges', []): + seen_group_count[group_email] = seen_group_count.get(group_email, 0) + 1 + member_email = edge.get('preferredMemberKey', {}).get('id') + edges.append((member_email, group_email)) + print_group_map(user_email, group_displayname_mapping, seen_group_count, edges, spaces=3, direct=True) + if max(seen_group_count.values()) > 1: + print() + print(' * user has multiple direct or inherited memberships in group') + print() if getLicenses: print('Licenses:') lic = buildGAPIObject('licensing') @@ -9023,6 +9059,19 @@ def doGetUserInfo(user_email=None): for user_license in user_licenses: print(f' {gapi_licensing._formatSKUIdDisplayName(user_license)}') +def print_group_map(parent, group_name_mappings, seen_group_count, edges, spaces=3, direct=False): + for a_parent, a_child in edges: + if a_parent == parent: + group_display_name = group_name_mappings[a_child] + if direct: + direction = 'direct' + else: + direction = 'inherited' + output = f'{" " * spaces}{group_display_name} <{a_child}> ({direction})' + if seen_group_count[a_child] > 1: + output += ' *' + print(output) + print_group_map(a_child, group_name_mappings, seen_group_count, edges, spaces+2) def doGetAliasInfo(alias_email=None): cd = buildGAPIObject('directory') diff --git a/src/gam/gapi/cloudidentity/groups.py b/src/gam/gapi/cloudidentity/groups.py index d21f0afa..05016053 100644 --- a/src/gam/gapi/cloudidentity/groups.py +++ b/src/gam/gapi/cloudidentity/groups.py @@ -13,8 +13,12 @@ from gam.gapi import cloudidentity as gapi_cloudidentity from gam.gapi.directory import customer as gapi_directory_customer +def build(): + return gapi_cloudidentity.build('cloudidentity') + + def create(): - ci = gapi_cloudidentity.build('cloudidentity_beta') + ci = build() initialGroupConfig = 'EMPTY' gapi_directory_customer.setTrueCustomerId() parent = f'customers/{GC_Values[GC_CUSTOMER_ID]}' @@ -66,7 +70,7 @@ def create(): def delete(): - ci = gapi_cloudidentity.build('cloudidentity_beta') + ci = build() group = sys.argv[3] name = group_email_to_id(ci, group) print(f'Deleting group {group}') @@ -74,11 +78,12 @@ def delete(): def info(): - ci = gapi_cloudidentity.build('cloudidentity_beta') + ci = build() group = gam.normalizeEmailAddressOrUID(sys.argv[3]) getUsers = True showJoinDate = True showUpdateDate = False + showMemberTree = False i = 4 while i < len(sys.argv): myarg = sys.argv[i].lower().replace('_', '') @@ -91,12 +96,15 @@ def info(): elif myarg == 'showupdatedate': showUpdateDate = True i += 1 + elif myarg == 'membertree': + showMemberTree = True + i += 1 else: controlflow.invalid_argument_exit(myarg, 'gam info cigroup') name = group_email_to_id(ci, group) basic_info = gapi.call(ci.groups(), 'get', name=name) display.print_json(basic_info) - if getUsers: + if getUsers and not showMemberTree: if not showJoinDate and not showUpdateDate: view = 'BASIC' pageSize = 1000 @@ -126,10 +134,42 @@ def info(): # f' {member.get("role", ROLE_MEMBER).lower()}: {member.get("email", member["id"])} ({member["type"].lower()})' ) print(f'Total {len(members)} users in group') + elif showMemberTree: + print(' Member tree:') + global cached_group_members + cached_group_members = {} + print_member_tree(ci, name) + + +def print_member_tree(ci, group_id, spaces=2): + if not group_id in cached_group_members: + cached_group_members[group_id] = gapi.get_all_pages(ci.groups().memberships(), + 'list', + 'memberships', + parent=group_id, + fields='*', + pageSize=1000) + for member in cached_group_members[group_id]: + member_id = member.get('name', '') + member_id = member_id.split('/')[-1] + if member_id.isdigit(): + member_type = 'user' + else: + member_type = 'group' + member_email = member.get('preferredMemberKey', {}).get('id') + relation_type = member.get('relationType', '').lower() + if member_type == 'user': + print(f'{" " * spaces}{member_email} - user') + elif member_type == 'group': + print(f'{" " * spaces}{member_email} - group') + group_id = group_email_to_id(ci, member_email) + print_member_tree(ci, group_id, spaces + 2) + else: + print(f'unknown member type: {member_type} for {member_email}') def info_member(): - ci = gapi_cloudidentity.build('cloudidentity_beta') + ci = build() member = gam.normalizeEmailAddressOrUID(sys.argv[3]) group = gam.normalizeEmailAddressOrUID(sys.argv[4]) group_name = gapi.call(ci.groups(), @@ -159,7 +199,7 @@ GROUP_ROLES_MAP = { def print_(): - ci = gapi_cloudidentity.build('cloudidentity_beta') + ci = build() i = 3 members = membersCountOnly = managers = managersCountOnly = owners = ownersCountOnly = False gapi_directory_customer.setTrueCustomerId() @@ -343,8 +383,58 @@ def print_(): display.write_csv_file(csvRows, titles, 'Groups', todrive) +def _get_groups_list(ci=None, member=None, parent=None): + if not ci: + ci = build() + if not parent: + gapi_directory_customer.setTrueCustomerId() + parent = f'customers/{GC_Values[GC_CUSTOMER_ID]}' + gam.printGettingAllItems('Groups', member) + page_message = gapi.got_total_items_first_last_msg('Groups') + if member: + fields = 'nextPageToken,memberships(groupKey(id),relationType)' + try: + groups_to_get = gapi.get_all_pages(ci.groups().memberships(), + 'searchTransitiveGroups', + 'memberships', + throw_reasons=[gapi_errors.ErrorReason.FOUR_O_O], + message_attribute=['groupKey', 'id'], + page_message=page_message, + parent='groups/-', + query=member, + pageSize=1000, + fields=fields) + except googleapiclient.errors.HttpError: + controlflow.system_error_exit( + 2, + f'enterprisemember requires Enterprise license') + return [group['groupKey']['id'] for group in groups_to_get if group['relationType'] == 'DIRECT'] + else: + groups_to_get = gapi.get_all_pages( + ci.groups(), + 'list', + 'groups', + message_attribute=['groupKey', 'id'], + page_message=page_message, + parent=parent, + view='BASIC', + pageSize=1000, + fields='nextPageToken,groups(groupKey(id))') + return [group['groupKey']['id'] for group in groups_to_get] + + +def get_membership_graph(member): + ci = build() + query = f"member_key_id == '{member}' && 'cloudidentity.googleapis.com/groups.discussion_forum' in labels" + result = gapi.call(ci.groups().memberships(), + 'getMembershipGraph', + parent='groups/-', + query=query) + return result.get('response') + + def print_members(): - ci = gapi_cloudidentity.build('cloudidentity_beta') + ci = build() todrive = False gapi_directory_customer.setTrueCustomerId() parent = f'customers/{GC_Values[GC_CUSTOMER_ID]}' @@ -381,36 +471,7 @@ def print_members(): controlflow.invalid_argument_exit(sys.argv[i], 'gam print cigroup-members') if not groups_to_get: - gam.printGettingAllItems('Groups', usemember) - page_message = gapi.got_total_items_first_last_msg('Groups') - if usemember: - try: - groups_to_get = gapi.get_all_pages(ci.groups().memberships(), - 'searchTransitiveGroups', - 'memberships', - throw_reasons=[gapi_errors.ErrorReason.FOUR_O_O], - message_attribute=['groupKey', 'id'], - page_message=page_message, - parent='groups/-', query=usemember, - pageSize=1000, - fields='nextPageToken,memberships(groupKey(id),relationType)') - except googleapiclient.errors.HttpError: - controlflow.system_error_exit( - 2, - f'enterprisemember requires Enterprise license') - groups_to_get = [group['groupKey']['id'] for group in groups_to_get if group['relationType'] == 'DIRECT'] - else: - groups_to_get = gapi.get_all_pages( - ci.groups(), - 'list', - 'groups', - message_attribute=['groupKey', 'id'], - page_message=page_message, - parent=parent, - view='BASIC', - pageSize=1000, - fields='nextPageToken,groups(groupKey(id))') - groups_to_get = [group['groupKey']['id'] for group in groups_to_get] + groups_to_get = _get_groups_list(ci, usemember, parent) i = 0 count = len(groups_to_get) for group_email in groups_to_get: @@ -489,7 +550,7 @@ def update(): ] return (role, expireTime, users_email) - ci = gapi_cloudidentity.build('cloudidentity_beta') + ci = build() group = sys.argv[3] myarg = sys.argv[4].lower() items = []