# -*- coding: utf-8 -*- # # Copyright 2020 Google LLC. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Declarative hooks for Cloud Identity Groups CLI.""" from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals import collections from apitools.base.py import encoding from apitools.base.py import exceptions as apitools_exceptions from googlecloudsdk.api_lib.identity import cloudidentity_client as ci_client from googlecloudsdk.calliope import base from googlecloudsdk.calliope import exceptions from googlecloudsdk.command_lib.organizations import org_utils import six GROUP_TYPE_MAP = { 'discussion': ['cloudidentity.googleapis.com/groups.discussion_forum'], 'dynamic': ['cloudidentity.googleapis.com/groups.dynamic'], 'security': ['cloudidentity.googleapis.com/groups.discussion_forum', 'cloudidentity.googleapis.com/groups.security'], } # request hooks def SetParent(unused_ref, args, request): """Set obfuscated customer id to request.group.parent or request.parent. Args: unused_ref: A string representing the operation reference. Unused and may be None. args: The argparse namespace. request: The request to modify. Returns: The updated request. """ version = GetApiVersion(args) messages = ci_client.GetMessages(version) group = getattr(request, 'group', None) if group is None: request.group = messages.Group() request.group.parent = GetCustomerId(args) return request def SetEntityKey(unused_ref, args, request): """Set EntityKey to request.group.groupKey. Args: unused_ref: unused. args: The argparse namespace. request: The request to modify. Returns: The updated request. """ if hasattr(args, 'email'): version = GetApiVersion(args) messages = ci_client.GetMessages(version) request.group.groupKey = messages.EntityKey(id=args.email) return request def SetLabels(unused_ref, args, request): """Set Labels to request.group.labels. Args: unused_ref: unused. args: The argparse namespace. request: The request to modify. Returns: The updated request. """ if args.IsSpecified('labels'): if hasattr(request.group, 'labels'): request.group.labels = ReformatLabels(args, args.labels) else: version = GetApiVersion(args) messages = ci_client.GetMessages(version) request.group = messages.Group(labels=ReformatLabels(args, args.labels)) return request def SetLabelsCreate(unused_ref, args, request): """Set Labels to request.group.labels for the create command. Labels will be used from args.labels if supplied, otherwise labels will be looked up based on the args.group_type argument. If neither is supplied, labels will be set based on the 'discussion' group type. Args: unused_ref: unused. args: The argparse namespace. request: The request to modify. Returns: The updated request. """ if args.IsSpecified('labels'): labels = args.labels elif args.IsKnownAndSpecified('group_type'): labels = ','.join(GROUP_TYPE_MAP[args.group_type]) else: labels = ','.join(GROUP_TYPE_MAP['discussion']) if hasattr(request.group, 'labels'): request.group.labels = ReformatLabels(args, labels) else: version = GetApiVersion(args) messages = ci_client.GetMessages(version) request.group = messages.Group(labels=ReformatLabels(args, labels)) return request def SetInitialOwner(unused_ref, args, request): """Set the initial owner. Defaults to 'empty' for dynamic groups and to 'with-initial-owner' for other group types. Args: unused_ref: unused. args: The argparse namespace. request: The request to modify. Returns: The updated request. """ if args.IsSpecified('with_initial_owner'): return request version = GetApiVersion(args) messages = ci_client.GetMessages(version) create_message = messages.CloudidentityGroupsCreateRequest config_enum = create_message.InitialGroupConfigValueValuesEnum if ((args.IsSpecified('group_type') and 'dynamic' in args.group_type) or (args.IsSpecified('labels') and 'dynamic' in args.labels)): request.initialGroupConfig = config_enum.EMPTY else: request.initialGroupConfig = config_enum.WITH_INITIAL_OWNER return request def SetResourceName(unused_ref, args, request): """Set resource name to request.name. Args: unused_ref: unused. args: The argparse namespace. request: The request to modify. Returns: The updated request. """ if args.IsSpecified('email'): version = GetApiVersion(args) request.name = ConvertEmailToResourceName(version, args.email, '--email') return request def SetPageSize(unused_ref, args, request): """Set page size to request.pageSize. Args: unused_ref: unused. args: The argparse namespace. request: The request to modify. Returns: The updated request. """ if args.IsSpecified('page_size'): request.pageSize = int(args.page_size) return request def SetGroupUpdateMask(unused_ref, args, request): """Set the update mask on the request based on the args. Args: unused_ref: unused. args: The argparse namespace. request: The request to modify. Returns: The updated request. Raises: InvalidArgumentException: If no fields are specified to update. """ update_mask = [] if (args.IsSpecified('display_name') or args.IsSpecified('clear_display_name')): update_mask.append('display_name') if (args.IsSpecified('description') or args.IsSpecified('clear_description')): update_mask.append('description') if hasattr(args, 'labels'): if args.IsSpecified('labels'): update_mask.append('labels') if hasattr(args, 'add_posix_group'): if (args.IsSpecified('add_posix_group') or args.IsSpecified('remove_posix_groups') or args.IsSpecified('clear_posix_groups')): update_mask.append('posix_groups') if args.IsSpecified('dynamic_user_query'): update_mask.append('dynamic_group_metadata') if not update_mask: raise exceptions.InvalidArgumentException( 'Must specify at least one field mask.') request.updateMask = ','.join(update_mask) return request def GenerateQuery(unused_ref, args, request): """Generate and set the query on the request based on the args. Args: unused_ref: unused. args: The argparse namespace. request: The request to modify. Returns: The updated request. """ customer_id = GetCustomerId(args) labels = FilterLabels(args.labels) labels_str = ','.join(labels) request.query = 'parent==\"{0}\" && \"{1}\" in labels'.format( customer_id, labels_str) return request def UpdateDisplayName(unused_ref, args, request): """Update displayName. Args: unused_ref: unused. args: The argparse namespace. request: The request to modify. Returns: The updated request. """ if args.IsSpecified('clear_display_name'): request.group.displayName = '' elif args.IsSpecified('display_name'): request.group.displayName = args.display_name return request def UpdateDescription(unused_ref, args, request): """Update description. Args: unused_ref: unused. args: The argparse namespace. request: The request to modify. Returns: The updated request. """ if args.IsSpecified('clear_description'): request.group.description = '' elif args.IsSpecified('description'): request.group.description = args.description return request def UpdatePosixGroups(unused_ref, args, request): """Update posix groups. When adding posix groups, the posix groups in the request will be combined with the current posix groups. When removing groups, the current list of posix groups is retrieved and if any value in args.remove_posix_groups matches either a name or gid in a current posix group, it will be removed from the list and the remaining posix groups will be added to the update request. Args: unused_ref: unused. args: The argparse namespace. request: The request to modify. Returns: The updated request. """ version = GetApiVersion(args) group = ci_client.GetGroup(version, request.name) if args.IsSpecified('add_posix_group'): request.group.posixGroups = request.group.posixGroups + group.posixGroups elif args.IsSpecified('remove_posix_groups'): if request.group is None: request.group = group for pg in list(group.posixGroups): if (six.text_type(pg.gid) in args.remove_posix_groups or pg.name in args.remove_posix_groups): group.posixGroups.remove(pg) request.group.posixGroups = group.posixGroups return request # processor hooks def SetDynamicUserQuery(unused_ref, args, request): """Add DynamicGroupUserQuery to DynamicGroupQueries object list. Args: unused_ref: unused. args: The argparse namespace. request: The request to modify. Returns: The updated dynamic group queries. """ queries = [] if args.IsSpecified('dynamic_user_query'): dg_user_query = args.dynamic_user_query version = GetApiVersion(args) messages = ci_client.GetMessages(version) resource_type = messages.DynamicGroupQuery.ResourceTypeValueValuesEnum new_dynamic_group_query = messages.DynamicGroupQuery( resourceType=resource_type.USER, query=dg_user_query) queries.append(new_dynamic_group_query) dynamic_group_metadata = messages.DynamicGroupMetadata(queries=queries) if hasattr(request.group, 'dynamicGroupMetadata'): request.group.dynamicGroupMetadata = dynamic_group_metadata else: request.group = messages.Group( dynamicGroupMetadata=dynamic_group_metadata) return request def ReformatLabels(args, labels): """Reformat label list to encoded labels message. Reformatting labels will be done within following two steps, 1. Filter label strings in a label list. 2. Convert the filtered label list to OrderedDict. 3. Encode the OrderedDict format of labels to group.labels message. Args: args: The argparse namespace. labels: list of label strings. e.g. ["cloudidentity.googleapis.com/security=", "cloudidentity.googleapis.com/groups.discussion_forum"] Returns: Encoded labels message. Raises: InvalidArgumentException: If invalid labels string is input. """ # Filter label strings in a label list. filtered_labels = FilterLabels(labels) # Convert the filtered label list to OrderedDict. labels_dict = collections.OrderedDict() for label in filtered_labels: if '=' in label: split_label = label.split('=') labels_dict[split_label[0]] = split_label[1] else: labels_dict[label] = '' # Encode the OrderedDict format of labels to group.labels message. version = GetApiVersion(args) messages = ci_client.GetMessages(version) return encoding.DictToMessage(labels_dict, messages.Group.LabelsValue) # private methods def ConvertOrgArgToObfuscatedCustomerId(org_arg): """Convert organization argument to obfuscated customer id. Args: org_arg: organization argument Returns: Obfuscated customer id Example: org_id: 12345 organization_obj: { owner: { directoryCustomerId: A08w1n5gg } } """ organization_obj = org_utils.GetOrganization(org_arg) if organization_obj: return organization_obj.owner.directoryCustomerId else: raise org_utils.UnknownOrganizationError(org_arg, metavar='ORGANIZATION') def ConvertEmailToResourceName(version, email, arg_name): """Convert email to resource name. Args: version: Release track information email: group email arg_name: argument/parameter name Returns: Group Id (e.g. groups/11zu0gzc3tkdgn2) """ try: return ci_client.LookupGroupName(version, email).name except (apitools_exceptions.HttpForbiddenError, apitools_exceptions.HttpNotFoundError): # If there is no group exists (or deleted) for the given group email, # print out an error message. error_msg = ('There is no such a group associated with the specified ' 'argument:' + email) raise exceptions.InvalidArgumentException(arg_name, error_msg) def FilterLabels(labels): """Filter label strings in label list. Filter labels (list of strings) with the following conditions, 1. If 'label' has 'key' and 'value' OR 'key' only, then add the label to filtered label list. (e.g. 'label_key=label_value', 'label_key') 2. If 'label' has an equal sign but no 'value', then add the 'key' to filtered label list. (e.g. 'label_key=' ==> 'label_key') 3. If 'label' has invalid format of string, throw an InvalidArgumentException. (e.g. 'label_key=value1=value2') Args: labels: list of label strings. Returns: Filtered label list. Raises: InvalidArgumentException: If invalid labels string is input. """ if not labels: raise exceptions.InvalidArgumentException( 'labels', 'labels can not be an empty string') # Convert a comma separated string to a list of strings. label_list = labels.split(',') filtered_labels = [] for label in label_list: if '=' in label: split_label = label.split('=') # Catch invalid format like 'key=value1=value2' if len(split_label) > 2: raise exceptions.InvalidArgumentException( 'labels', 'Invalid format of label string has been input. Label: ' + label) if split_label[1]: filtered_labels.append(label) # Valid format #1: 'key=value' else: filtered_labels.append(split_label[0]) # Valid format #2: 'key' else: filtered_labels.append(label) return filtered_labels def GetApiVersion(args): """Return release track information. Args: args: The argparse namespace. Returns: Release track. Raises: UnsupportedReleaseTrackError: If invalid release track is input. """ release_track = args.calliope_command.ReleaseTrack() if release_track == base.ReleaseTrack.ALPHA: return 'v1alpha1' elif release_track == base.ReleaseTrack.BETA: return 'v1beta1' elif release_track == base.ReleaseTrack.GA: return 'v1' else: raise UnsupportedReleaseTrackError(release_track) def GetCustomerId(args): """Return customer_id. Args: args: The argparse namespace. Returns: customer_id. """ if hasattr(args, 'customer') and args.IsSpecified('customer'): customer_id = args.customer elif hasattr(args, 'organization') and args.IsSpecified('organization'): customer_id = ConvertOrgArgToObfuscatedCustomerId(args.organization) return 'customerId/' + customer_id class UnsupportedReleaseTrackError(Exception): """Raised when requesting an api for an unsupported release track."""