1# -*- coding: utf-8 -*- # 2# Copyright 2020 Google LLC. All Rights Reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15"""Declarative hooks for Cloud Identity Groups CLI.""" 16from __future__ import absolute_import 17from __future__ import division 18from __future__ import unicode_literals 19 20import collections 21 22from apitools.base.py import encoding 23from apitools.base.py import exceptions as apitools_exceptions 24 25from googlecloudsdk.api_lib.identity import cloudidentity_client as ci_client 26from googlecloudsdk.calliope import base 27from googlecloudsdk.calliope import exceptions 28from googlecloudsdk.command_lib.organizations import org_utils 29import six 30 31 32# request hooks 33def SetParent(unused_ref, args, request): 34 """Set obfuscated customer id to request.group.parent or request.parent. 35 36 Args: 37 unused_ref: A string representing the operation reference. Unused and may be 38 None. 39 args: The argparse namespace. 40 request: The request to modify. 41 42 Returns: 43 The updated request. 44 """ 45 46 version = GetApiVersion(args) 47 messages = ci_client.GetMessages(version) 48 49 group = getattr(request, 'group', None) 50 if group is None: 51 request.group = messages.Group() 52 53 request.group.parent = GetCustomerId(args) 54 55 return request 56 57 58def SetEntityKey(unused_ref, args, request): 59 """Set EntityKey to request.group.groupKey. 60 61 Args: 62 unused_ref: unused. 63 args: The argparse namespace. 64 request: The request to modify. 65 66 Returns: 67 The updated request. 68 """ 69 70 if hasattr(args, 'email'): 71 version = GetApiVersion(args) 72 messages = ci_client.GetMessages(version) 73 request.group.groupKey = messages.EntityKey(id=args.email) 74 75 return request 76 77 78def SetLabels(unused_ref, args, request): 79 """Set Labels to request.group.labels. 80 81 Args: 82 unused_ref: unused. 83 args: The argparse namespace. 84 request: The request to modify. 85 86 Returns: 87 The updated request. 88 """ 89 90 if args.IsSpecified('labels'): 91 if hasattr(request.group, 'labels'): 92 request.group.labels = ReformatLabels(args, args.labels) 93 else: 94 version = GetApiVersion(args) 95 messages = ci_client.GetMessages(version) 96 request.group = messages.Group(labels=ReformatLabels(args, args.labels)) 97 98 return request 99 100 101def SetResourceName(unused_ref, args, request): 102 """Set resource name to request.name. 103 104 Args: 105 unused_ref: unused. 106 args: The argparse namespace. 107 request: The request to modify. 108 109 Returns: 110 The updated request. 111 """ 112 113 if args.IsSpecified('email'): 114 version = GetApiVersion(args) 115 request.name = ConvertEmailToResourceName(version, args.email, '--email') 116 117 return request 118 119 120def SetPageSize(unused_ref, args, request): 121 """Set page size to request.pageSize. 122 123 Args: 124 unused_ref: unused. 125 args: The argparse namespace. 126 request: The request to modify. 127 128 Returns: 129 The updated request. 130 """ 131 132 if args.IsSpecified('page_size'): 133 request.pageSize = int(args.page_size) 134 135 return request 136 137 138def SetGroupUpdateMask(unused_ref, args, request): 139 """Set the update mask on the request based on the args. 140 141 Args: 142 unused_ref: unused. 143 args: The argparse namespace. 144 request: The request to modify. 145 146 Returns: 147 The updated request. 148 Raises: 149 InvalidArgumentException: If no fields are specified to update. 150 """ 151 update_mask = [] 152 153 if (args.IsSpecified('display_name') or 154 args.IsSpecified('clear_display_name')): 155 update_mask.append('display_name') 156 157 if (args.IsSpecified('description') or args.IsSpecified('clear_description')): 158 update_mask.append('description') 159 160 if hasattr(args, 'labels'): 161 if args.IsSpecified('labels'): 162 update_mask.append('labels') 163 164 if hasattr(args, 'add_posix_group'): 165 if (args.IsSpecified('add_posix_group') or 166 args.IsSpecified('remove_posix_groups') or 167 args.IsSpecified('clear_posix_groups')): 168 update_mask.append('posix_groups') 169 170 if args.IsSpecified('dynamic_user_query'): 171 update_mask.append('dynamic_group_metadata') 172 173 if not update_mask: 174 raise exceptions.InvalidArgumentException( 175 'Must specify at least one field mask.') 176 177 request.updateMask = ','.join(update_mask) 178 179 return request 180 181 182def GenerateQuery(unused_ref, args, request): 183 """Generate and set the query on the request based on the args. 184 185 Args: 186 unused_ref: unused. 187 args: The argparse namespace. 188 request: The request to modify. 189 190 Returns: 191 The updated request. 192 """ 193 customer_id = GetCustomerId(args) 194 labels = FilterLabels(args.labels) 195 labels_str = ','.join(labels) 196 request.query = 'parent==\"{0}\" && \"{1}\" in labels'.format( 197 customer_id, labels_str) 198 199 return request 200 201 202def UpdateDisplayName(unused_ref, args, request): 203 """Update displayName. 204 205 Args: 206 unused_ref: unused. 207 args: The argparse namespace. 208 request: The request to modify. 209 210 Returns: 211 The updated request. 212 """ 213 214 if args.IsSpecified('clear_display_name'): 215 request.group.displayName = '' 216 elif args.IsSpecified('display_name'): 217 request.group.displayName = args.display_name 218 219 return request 220 221 222def UpdateDescription(unused_ref, args, request): 223 """Update description. 224 225 Args: 226 unused_ref: unused. 227 args: The argparse namespace. 228 request: The request to modify. 229 230 Returns: 231 The updated request. 232 """ 233 234 if args.IsSpecified('clear_description'): 235 request.group.description = '' 236 elif args.IsSpecified('description'): 237 request.group.description = args.description 238 239 return request 240 241 242def UpdatePosixGroups(unused_ref, args, request): 243 """Update posix groups. 244 245 When adding posix groups, the posix groups in the request will be combined 246 with the current posix groups. When removing groups, the current list of 247 posix groups is retrieved and if any value in args.remove_posix_groups 248 matches either a name or gid in a current posix group, it will be removed 249 from the list and the remaining posix groups will be added to the update 250 request. 251 252 Args: 253 unused_ref: unused. 254 args: The argparse namespace. 255 request: The request to modify. 256 257 Returns: 258 The updated request. 259 """ 260 version = GetApiVersion(args) 261 group = ci_client.GetGroup(version, request.name) 262 if args.IsSpecified('add_posix_group'): 263 request.group.posixGroups = request.group.posixGroups + group.posixGroups 264 elif args.IsSpecified('remove_posix_groups'): 265 if request.group is None: 266 request.group = group 267 for pg in list(group.posixGroups): 268 if (six.text_type(pg.gid) in args.remove_posix_groups or 269 pg.name in args.remove_posix_groups): 270 group.posixGroups.remove(pg) 271 request.group.posixGroups = group.posixGroups 272 273 return request 274 275 276# processor hooks 277def SetDynamicUserQuery(unused_ref, args, request): 278 """Add DynamicGroupUserQuery to DynamicGroupQueries object list. 279 280 Args: 281 unused_ref: unused. 282 args: The argparse namespace. 283 request: The request to modify. 284 285 Returns: 286 The updated dynamic group queries. 287 """ 288 289 queries = [] 290 291 if args.IsSpecified('dynamic_user_query'): 292 dg_user_query = args.dynamic_user_query 293 version = GetApiVersion(args) 294 messages = ci_client.GetMessages(version) 295 resource_type = messages.DynamicGroupQuery.ResourceTypeValueValuesEnum 296 new_dynamic_group_query = messages.DynamicGroupQuery( 297 resourceType=resource_type.USER, query=dg_user_query) 298 queries.append(new_dynamic_group_query) 299 dynamic_group_metadata = messages.DynamicGroupMetadata(queries=queries) 300 301 if hasattr(request.group, 'dynamicGroupMetadata'): 302 request.group.dynamicGroupMetadata = dynamic_group_metadata 303 else: 304 request.group = messages.Group( 305 dynamicGroupMetadata=dynamic_group_metadata) 306 307 return request 308 309 310def ReformatLabels(args, labels): 311 """Reformat label list to encoded labels message. 312 313 Reformatting labels will be done within following two steps, 314 1. Filter label strings in a label list. 315 2. Convert the filtered label list to OrderedDict. 316 3. Encode the OrderedDict format of labels to group.labels message. 317 318 Args: 319 args: The argparse namespace. 320 labels: list of label strings. e.g. 321 ["cloudidentity.googleapis.com/security=", 322 "cloudidentity.googleapis.com/groups.discussion_forum"] 323 324 Returns: 325 Encoded labels message. 326 327 Raises: 328 InvalidArgumentException: If invalid labels string is input. 329 """ 330 331 # Filter label strings in a label list. 332 filtered_labels = FilterLabels(labels) 333 334 # Convert the filtered label list to OrderedDict. 335 labels_dict = collections.OrderedDict() 336 for label in filtered_labels: 337 if '=' in label: 338 split_label = label.split('=') 339 labels_dict[split_label[0]] = split_label[1] 340 else: 341 labels_dict[label] = '' 342 343 # Encode the OrderedDict format of labels to group.labels message. 344 version = GetApiVersion(args) 345 messages = ci_client.GetMessages(version) 346 return encoding.DictToMessage(labels_dict, messages.Group.LabelsValue) 347 348 349# private methods 350def ConvertOrgArgToObfuscatedCustomerId(org_arg): 351 """Convert organization argument to obfuscated customer id. 352 353 Args: 354 org_arg: organization argument 355 356 Returns: 357 Obfuscated customer id 358 359 Example: 360 org_id: 12345 361 organization_obj: 362 { 363 owner: { 364 directoryCustomerId: A08w1n5gg 365 } 366 } 367 """ 368 organization_obj = org_utils.GetOrganization(org_arg) 369 if organization_obj: 370 return organization_obj.owner.directoryCustomerId 371 else: 372 raise org_utils.UnknownOrganizationError(org_arg, metavar='ORGANIZATION') 373 374 375def ConvertEmailToResourceName(version, email, arg_name): 376 """Convert email to resource name. 377 378 Args: 379 version: Release track information 380 email: group email 381 arg_name: argument/parameter name 382 383 Returns: 384 Group Id (e.g. groups/11zu0gzc3tkdgn2) 385 386 """ 387 try: 388 return ci_client.LookupGroupName(version, email).name 389 except (apitools_exceptions.HttpForbiddenError, 390 apitools_exceptions.HttpNotFoundError): 391 # If there is no group exists (or deleted) for the given group email, 392 # print out an error message. 393 error_msg = ('There is no such a group associated with the specified ' 394 'argument:' + email) 395 raise exceptions.InvalidArgumentException(arg_name, error_msg) 396 397 398def FilterLabels(labels): 399 """Filter label strings in label list. 400 401 Filter labels (list of strings) with the following conditions, 402 1. If 'label' has 'key' and 'value' OR 'key' only, then add the label to 403 filtered label list. (e.g. 'label_key=label_value', 'label_key') 404 2. If 'label' has an equal sign but no 'value', then add the 'key' to filtered 405 label list. (e.g. 'label_key=' ==> 'label_key') 406 3. If 'label' has invalid format of string, throw an InvalidArgumentException. 407 (e.g. 'label_key=value1=value2') 408 409 Args: 410 labels: list of label strings. 411 412 Returns: 413 Filtered label list. 414 415 Raises: 416 InvalidArgumentException: If invalid labels string is input. 417 """ 418 419 if not labels: 420 raise exceptions.InvalidArgumentException( 421 'labels', 'labels can not be an empty string') 422 423 # Convert a comma separated string to a list of strings. 424 label_list = labels.split(',') 425 426 filtered_labels = [] 427 for label in label_list: 428 if '=' in label: 429 split_label = label.split('=') 430 431 # Catch invalid format like 'key=value1=value2' 432 if len(split_label) > 2: 433 raise exceptions.InvalidArgumentException( 434 'labels', 435 'Invalid format of label string has been input. Label: ' + label) 436 437 if split_label[1]: 438 filtered_labels.append(label) # Valid format #1: 'key=value' 439 else: 440 filtered_labels.append(split_label[0]) # Valid format #2: 'key' 441 442 else: 443 filtered_labels.append(label) 444 445 return filtered_labels 446 447 448def GetApiVersion(args): 449 """Return release track information. 450 451 Args: 452 args: The argparse namespace. 453 454 Returns: 455 Release track. 456 457 Raises: 458 UnsupportedReleaseTrackError: If invalid release track is input. 459 """ 460 461 release_track = args.calliope_command.ReleaseTrack() 462 463 if release_track == base.ReleaseTrack.ALPHA: 464 return 'v1alpha1' 465 elif release_track == base.ReleaseTrack.BETA: 466 return 'v1beta1' 467 elif release_track == base.ReleaseTrack.GA: 468 return 'v1' 469 else: 470 raise UnsupportedReleaseTrackError(release_track) 471 472 473def GetCustomerId(args): 474 """Return customer_id. 475 476 Args: 477 args: The argparse namespace. 478 479 Returns: 480 customer_id. 481 482 """ 483 484 if hasattr(args, 'customer') and args.IsSpecified('customer'): 485 customer_id = args.customer 486 elif hasattr(args, 'organization') and args.IsSpecified('organization'): 487 customer_id = ConvertOrgArgToObfuscatedCustomerId(args.organization) 488 return 'customerId/' + customer_id 489 490 491class UnsupportedReleaseTrackError(Exception): 492 """Raised when requesting an api for an unsupported release track.""" 493