diff --git a/README.md b/README.md index 0883547..752367d 100644 --- a/README.md +++ b/README.md @@ -61,8 +61,7 @@ The `master` branch should be automatically deployed to the CH Kubernetes cluste ## Google Serivce Account Dienst2 requires a Google Service account to access Group and Member data via the directory API. A Google Serivce Account can be created in the [Google Cloud Console](https://console.cloud.google.com/apis/credentials). The service account should be a "Domain-wide Delegation" account with the following scopes: -- https://www.googleapis.com/auth/admin.directory.group.readonly -- https://www.googleapis.com/auth/admin.directory.group.member.readonly +- https://www.googleapis.com/auth/cloud-identity.groups.readonly The scopes can be defined in Google Admin Console -> Security -> API controls -> Domain-wide delegation. diff --git a/ldb/google.py b/ldb/google.py index 5b514fe..26e9844 100644 --- a/ldb/google.py +++ b/ldb/google.py @@ -1,6 +1,7 @@ +from urllib.parse import urlencode + from google.oauth2 import service_account from googleapiclient.discovery import build -from ldb.hardcoded_google_groups import get_indirect_groups import environ @@ -17,19 +18,49 @@ def get_google_service(scopes=[]): delegated_credentials = credentials.with_subject( env.str("GOOGLE_SERVICE_ACCOUNT_DELEGATED_USER") ) - return build("admin", "directory_v1", credentials=delegated_credentials) + return build("cloudidentity", "v1", credentials=delegated_credentials) + + +# Source: https://cloud.google.com/identity/docs/how-to/query-memberships#searching_for_all_group_memberships_of_a_member +def search_transitive_groups(service, member, page_size): + groups = [] + next_page_token = '' + while True: + query_params = urlencode( + { + "query": "member_key_id == '{}' && 'cloudidentity.googleapis.com/groups.discussion_forum' in labels".format( + member), + "page_size": page_size, + "page_token": next_page_token + } + ) + request = service.groups().memberships().searchTransitiveGroups(parent='groups/-') + request.uri += "&" + query_params + response = request.execute() + + if 'memberships' in response: + groups += response['memberships'] + + if 'nextPageToken' in response: + next_page_token = response['nextPageToken'] + else: + next_page_token = '' + if len(next_page_token) == 0: + break -def get_groups_by_user_key(userKey, domains=["ch.tudelft.nl"], indirect=False) -> list: + return groups + + +def get_groups_by_user_key(userKey) -> list: """ - Returns all Google Groups that a member is a DIRECT member of + Returns all Google Groups that a member is a direct or indirectmember of :param userKey: Email or immutable ID of the user if only those groups are to be listed, the given user is a member of. If it's an ID, it should match with the ID of the user object. :param domains: Domains to search for groups. Ensure that these are set to prevent group name attacks by using other domains. - :param indirect: Whether to include indirect groups :return: List of group email addresses @@ -41,21 +72,16 @@ def get_groups_by_user_key(userKey, domains=["ch.tudelft.nl"], indirect=False) - service = get_google_service( [ - "https://www.googleapis.com/auth/admin.directory.group.readonly", - "https://www.googleapis.com/auth/admin.directory.group.member.readonly", + "https://www.googleapis.com/auth/cloud-identity.groups.readonly", ] ) groups: list = [] - for domain in domains: - data = service.groups().list(userKey=userKey, domain=domain).execute() - if "groups" in data: - for group in data["groups"]: - groups.append(group["email"]) - - if indirect: - indirect_groups = get_indirect_groups(groups) - groups.extend(indirect_groups) + + transitive_groups = search_transitive_groups(service, userKey, 50) + for group in transitive_groups: + print("group:", group) + groups.append(group["groupKey"]['id']) # Replace "@ch.tudelft.nl" from the group names # 1. Replace "-commissie@ch.tudelft.nl" with "" diff --git a/ldb/hardcoded_google_groups.py b/ldb/hardcoded_google_groups.py deleted file mode 100644 index 8d75c2d..0000000 --- a/ldb/hardcoded_google_groups.py +++ /dev/null @@ -1,119 +0,0 @@ -import datetime - -""" -TODO: This file contains hardcoded Google Groups that are not retrieved from Google. -This is because Dienst2 currently is not able to cache the Google Groups. -This is a temporary solution to continue with the cloud migration. -Please implement cachin ASAP. -""" - - -def get_book_year() -> int: - # Get the current book year. The year starts at the 1st of September - now = datetime.datetime.now() - - if now.month < 9: - return now.year - 1 - - return now.year - - -def get_board_year() -> int: - """ - Return the current board year. - Under the assumption that the board year starts at the 1st of September and that - from now on only complete years are made. - """ - return get_book_year() - 1956 - - -YEAR = str(get_book_year()) -BOARD_YEAR = str(get_board_year()) -HARDCODED_PARENT_GROUPS = { - "bestuur-group@ch.tudelft.nl": ["bestuur" + BOARD_YEAR + "@ch.tudelft.nl"], - "vc": ["vc_" + YEAR + "@ch.tudelft.nl"], - "galacie@ch.tudelft.nl": ["galacie" + YEAR + "@ch.tudelft.nl"], - "hackdelft-commissie@ch.tudelft.nl": ["hackdelft_" + YEAR + "@ch.tudelft.nl"], - "icom-commissie@ch.tudelft.nl": ["icom_" + YEAR + "@ch.tudelft.nl"], -} - -SENAAT_GROUP = "senaat@ch.tudelft.nl" -HARDCODED_COMMITTEE_GROUPS = [ - "akcie", - "annucie", - "choco", - "coh", - "comma", - "dies", - "eiweiw", - "facie", - "flitcie", - "lancie", - "machazine", - "maphya", - "match", - "sjaarcie", - "symposium", - "wiewie", - "wifi", - "wocky", -] - - -def get_parent_committee_group(group: str) -> str: - """ - Returns the parent committee group of a group - :param group: The group to get the parent committee group of - :return: The parent committee group - - This function assigns for example choco_2022@ch.tudelft.nl to choco@ch.tudelft.nl - in the year 2022-2023. - """ - - # Split the group name on the underscore and the @ - group_name = group.split("@")[0] - if len(group_name.split("_")) < 2: - return None - committee = group_name.split("_")[0] - year = group_name.split("_")[1] - - if year == YEAR and committee in HARDCODED_COMMITTEE_GROUPS: - return committee + "@" + group.split("@")[1] - - return None - - -def get_indirect_groups(groups: list) -> list: - """ - Returns all Google Groups that a member is an INDIRECT member of - - :param groups: List of groups to get the indirect groups of - :return: List of indirect group email addresses - """ - - indirect_groups = [] - groups = groups.copy() - - while len(groups) > 0: - group = groups.pop() - - # TODO: enable retrieval of parent groups via Google. - # parent_group = get_parent_group(group) - # if parent_group is not None: - # groups.append(parent_group) - - # Check if group begins with "bestuur" or "board" - if group.startswith("bestuur") or group.startswith("board"): - if group.endswith("@ch.tudelft.nl"): - indirect_groups.append(SENAAT_GROUP) - - # Check if group has a committee parent group - if get_parent_committee_group(group) is not None: - indirect_groups.append(get_parent_committee_group(group)) - - # Check if group is in values of HARDCODED_PARENT_GROUPS - for key, value in HARDCODED_PARENT_GROUPS.items(): - if group in value: - indirect_groups.append(key) - - return indirect_groups diff --git a/ldb/viewsets.py b/ldb/viewsets.py index 7f63191..880f09f 100644 --- a/ldb/viewsets.py +++ b/ldb/viewsets.py @@ -63,8 +63,7 @@ def google_groups(self, request, pk=None): # Retrieve the groups from the Directory API try: google_groups = get_groups_by_user_key( - person.google_username + "@ch.tudelft.nl", indirect=True - ) + person.google_username + "@ch.tudelft.nl") except HttpError as e: if e.resp.status == 404: return Response(google_groups)