1# -*- coding: utf-8 -*- # 2# Copyright 2019 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 16"""Declarative hooks for Cloud Identity Groups Memberships CLI.""" 17from __future__ import absolute_import 18from __future__ import division 19from __future__ import unicode_literals 20 21from apitools.base.py import exceptions as apitools_exceptions 22 23from googlecloudsdk.api_lib.identity import cloudidentity_client as ci_client 24from googlecloudsdk.calliope import exceptions 25from googlecloudsdk.command_lib.identity.groups import hooks as groups_hooks 26from googlecloudsdk.core.util import times 27 28 29# request hooks 30def SetMembership(unused_ref, args, request): 31 """Set Membership in request. 32 33 Args: 34 unused_ref: unused. 35 args: The argparse namespace. 36 request: The request to modify. 37 38 Returns: 39 The updated request. 40 41 """ 42 43 version = groups_hooks.GetApiVersion(args) 44 messages = ci_client.GetMessages(version) 45 request.membership = messages.Membership() 46 47 return request 48 49 50def SetEntityKey(unused_ref, args, request): 51 """Set EntityKey in group resource. 52 53 Args: 54 unused_ref: unused. 55 args: The argparse namespace. 56 request: The request to modify. 57 58 Returns: 59 The updated request. 60 61 """ 62 63 version = groups_hooks.GetApiVersion(args) 64 messages = ci_client.GetMessages(version) 65 if hasattr(args, 'member_email') and args.IsSpecified('member_email'): 66 entity_key = messages.EntityKey(id=args.member_email) 67 request.membership.preferredMemberKey = entity_key 68 69 return request 70 71 72def SetPageSize(unused_ref, args, request): 73 """Set page size to request.pageSize. 74 75 Args: 76 unused_ref: unused. 77 args: The argparse namespace. 78 request: The request to modify. 79 80 Returns: 81 The updated request. 82 83 """ 84 85 if hasattr(args, 'page_size') and args.IsSpecified('page_size'): 86 request.pageSize = int(args.page_size) 87 88 return request 89 90 91def SetMembershipParent(unused_ref, args, request): 92 """Set resource name to request.parent. 93 94 Args: 95 unused_ref: unused. 96 args: The argparse namespace. 97 request: The request to modify. 98 99 Returns: 100 The updated request. 101 102 """ 103 104 version = groups_hooks.GetApiVersion(args) 105 if args.IsSpecified('group_email'): 106 # Resource name example: groups/03qco8b4452k99t 107 request.parent = groups_hooks.ConvertEmailToResourceName( 108 version, args.group_email, '--group-email') 109 110 return request 111 112 113def SetTransitiveMembershipParent(unused_ref, args, request): 114 """Set resource name to request.parent. 115 116 Args: 117 unused_ref: unused. 118 args: The argparse namespace. 119 request: The request to modify. 120 121 Returns: 122 The updated request. 123 124 """ 125 126 version = groups_hooks.GetApiVersion(args) 127 if hasattr(args, 'group_email') and args.IsSpecified('group_email'): 128 # Resource name example: groups/03qco8b4452k99t 129 request.parent = groups_hooks.ConvertEmailToResourceName( 130 version, args.group_email, '--group-email') 131 else: 132 # If this hook is used and no group_emails provided then set the parent to 133 # be the groups wildcard. 134 request.parent = 'groups/-' 135 return request 136 137 138def SetMembershipResourceName(unused_ref, args, request): 139 """Set membership resource name to request.name. 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 149 """ 150 151 version = groups_hooks.GetApiVersion(args) 152 name = '' 153 if args.IsSpecified('group_email') and args.IsSpecified('member_email'): 154 name = ConvertEmailToMembershipResourceName( 155 version, args, '--group-email', '--member-email') 156 else: 157 raise exceptions.InvalidArgumentException( 158 'Must specify `--group-email` and `--member-email` argument.') 159 160 request.name = name 161 162 if hasattr(request, 'membership'): 163 request.membership.name = name 164 165 return request 166 167 168def SetMembershipRoles(unused_ref, args, request): 169 """Set MembershipRoles to request.membership.roles. 170 171 Args: 172 unused_ref: unused. 173 args: The argparse namespace. 174 request: The request to modify. 175 176 Returns: 177 The updated request. 178 179 """ 180 181 version = groups_hooks.GetApiVersion(args) 182 if not hasattr(args, 'roles') or not args.IsSpecified('roles'): 183 empty_list = [] 184 request.membership.roles = ReformatMembershipRoles(version, empty_list) 185 else: 186 request.membership.roles = ReformatMembershipRoles(version, args.roles) 187 188 return request 189 190 191def SetExpiryDetail(unused_ref, args, request): 192 """Set expiration to request.membership.expiryDetail (v1alpha1) or in request.membership.roles (v1beta1). 193 194 Args: 195 unused_ref: unused. 196 args: The argparse namespace. 197 request: The request to modify. 198 199 Returns: 200 The updated request. 201 202 Raises: 203 InvalidArgumentException: If 'expiration' is specified upon following cases: 204 1. 'request.membership' doesn't have 'roles' attribute, or 205 2. multiple roles are provided. 206 207 """ 208 209 ### Pre-validations ### 210 211 # #1. In order to set Expiry Detail, there should be a role in 212 # 'request.membership' 213 if not hasattr(request.membership, 'roles'): 214 raise exceptions.InvalidArgumentException( 215 'expiration', 'roles must be specified.') 216 217 # #2. When setting 'expiration', a single role should be input. 218 if len(request.membership.roles) != 1: 219 raise exceptions.InvalidArgumentException( 220 'roles', 221 'When setting "expiration", a single role should be input.') 222 223 version = groups_hooks.GetApiVersion(args) 224 if hasattr(args, 'expiration') and args.IsSpecified('expiration'): 225 if version == 'v1alpha1': 226 request.membership.expiryDetail = ReformatExpiryDetail( 227 version, args.expiration, 'add') 228 else: 229 request.membership.roles = AddExpiryDetailInMembershipRoles( 230 version, request, args.expiration) 231 232 return request 233 234 235def SetTransitiveQuery(unused_ref, args, request): 236 """Sets query paremeters to request.query. 237 238 Args: 239 unused_ref: unused. 240 args: The argparse namespace. 241 request: The request to modify. 242 243 Returns: 244 The updated request. 245 """ 246 params = [] 247 248 if hasattr(args, 'member_email') and args.IsSpecified('member_email'): 249 params.append("member_key_id=='{}'".format(args.member_email)) 250 251 if hasattr(args, 'labels') and args.IsSpecified('labels'): 252 params.append("'{}' in labels".format(args.labels)) 253 254 request.query = '&&'.join(params) 255 256 return request 257 258 259def UpdateMembershipRoles(unused_ref, args, request): 260 """Update MembershipRoles to request.membership.roles. 261 262 Args: 263 unused_ref: unused. 264 args: The argparse namespace. 265 request: The request to modify. 266 267 Returns: 268 The updated request. 269 270 """ 271 272 version = groups_hooks.GetApiVersion(args) 273 if hasattr(args, 'roles') and args.IsSpecified('roles'): 274 request.membership.roles = ReformatMembershipRoles(version, args.roles) 275 276 return request 277 278 279def UpdateRoles(unused_ref, args, request): 280 """Update 'MembershipRoles' to request.modifyMembershipRolesRequest. 281 282 Args: 283 unused_ref: unused. 284 args: The argparse namespace. 285 request: The request to modify. 286 287 Returns: 288 The updated request. 289 290 """ 291 292 # Following logic is used only when 'add-roles' parameter is used. 293 if hasattr(args, 'add_roles') and args.IsSpecified('add_roles'): 294 # Convert a comma separated string to a list of strings. 295 role_list = args.add_roles.split(',') 296 297 # Convert a list of strings to a list of MembershipRole objects. 298 version = groups_hooks.GetApiVersion(args) 299 roles = [] 300 messages = ci_client.GetMessages(version) 301 for role in role_list: 302 membership_role = messages.MembershipRole(name=role) 303 roles.append(membership_role) 304 305 request.modifyMembershipRolesRequest = messages.ModifyMembershipRolesRequest( 306 addRoles=roles) 307 308 return request 309 310 311def SetUpdateRolesParams(unused_ref, args, request): 312 """Update 'MembershipRoles' to request.modifyMembershipRolesRequest. 313 314 Args: 315 unused_ref: A string representing the operation reference. Unused and may 316 be None. 317 args: The argparse namespace. 318 request: The request to modify. 319 320 Returns: 321 The updated request. 322 323 """ 324 325 if hasattr(args, 326 'update_roles_params') and args.IsSpecified('update_roles_params'): 327 version = groups_hooks.GetApiVersion(args) 328 messages = ci_client.GetMessages(version) 329 request.modifyMembershipRolesRequest = messages.ModifyMembershipRolesRequest( 330 updateRolesParams=ReformatUpdateRolesParams( 331 args, args.update_roles_params)) 332 333 return request 334 335 336# private methods 337def ConvertEmailToMembershipResourceName( 338 version, args, group_arg_name, member_arg_name): 339 """Convert email to membership resource name. 340 341 Args: 342 version: Release track information 343 args: The argparse namespace 344 group_arg_name: argument/parameter name related to group info 345 member_arg_name: argument/parameter name related to member info 346 347 Returns: 348 Membership Id (e.g. groups/11zu0gzc3tkdgn2/memberships/1044279104595057141) 349 350 """ 351 352 # Resource name example: groups/03qco8b4452k99t 353 group_id = groups_hooks.ConvertEmailToResourceName( 354 version, args.group_email, group_arg_name) 355 356 try: 357 return ci_client.LookupMembershipName( 358 version, group_id, args.member_email).name 359 except (apitools_exceptions.HttpForbiddenError, 360 apitools_exceptions.HttpNotFoundError): 361 # If there is no group exists (or deleted) for the given group email, 362 # print out an error message. 363 parameter_name = group_arg_name + ', ' + member_arg_name 364 error_msg = ('There is no such membership associated with the specified ' 365 'arguments: {}, {}').format(args.group_email, 366 args.member_email) 367 368 raise exceptions.InvalidArgumentException(parameter_name, error_msg) 369 370 371def ReformatExpiryDetail(version, expiration, command): 372 """Reformat expiration string to ExpiryDetail object. 373 374 Args: 375 version: Release track information 376 expiration: expiration string. 377 command: gcloud command name. 378 379 Returns: 380 ExpiryDetail object that contains the expiration data. 381 382 """ 383 384 messages = ci_client.GetMessages(version) 385 duration = 'P' + expiration 386 expiration_ts = FormatDateTime(duration) 387 388 if version == 'v1alpha1' and command == 'modify-membership-roles': 389 return messages.MembershipRoleExpiryDetail(expireTime=expiration_ts) 390 391 return messages.ExpiryDetail(expireTime=expiration_ts) 392 393 394def ReformatMembershipRoles(version, roles_list): 395 """Reformat roles string to MembershipRoles object list. 396 397 Args: 398 version: Release track information 399 roles_list: list of roles in a string format. 400 401 Returns: 402 List of MembershipRoles object. 403 404 """ 405 406 messages = ci_client.GetMessages(version) 407 roles = [] 408 if not roles_list: 409 # If no MembershipRole is provided, 'MEMBER' is used as a default value. 410 roles.append(messages.MembershipRole(name='MEMBER')) 411 return roles 412 413 for role in roles_list: 414 new_membership_role = messages.MembershipRole(name=role) 415 roles.append(new_membership_role) 416 417 return roles 418 419 420def GetUpdateMask(role_param, arg_name): 421 """Set the update mask on the request based on the role param. 422 423 Args: 424 role_param: The param that needs to be updated for a specified role. 425 arg_name: The argument name 426 427 Returns: 428 Update mask 429 430 Raises: 431 InvalidArgumentException: If no fields are specified to update. 432 433 """ 434 update_mask = [] 435 436 if role_param == 'expiration': 437 update_mask.append('expiry_detail.expire_time') 438 439 if not update_mask: 440 raise exceptions.InvalidArgumentException( 441 arg_name, 'Must specify at least one field mask.') 442 443 return ','.join(update_mask) 444 445 446def FormatDateTime(duration): 447 """Return RFC3339 string for datetime that is now + given duration. 448 449 Args: 450 duration: string ISO 8601 duration, e.g. 'P5D' for period 5 days. 451 452 Returns: 453 string timestamp 454 455 """ 456 457 # We use a format that preserves +00:00 for UTC to match timestamp format 458 # returned by container API. 459 fmt = '%Y-%m-%dT%H:%M:%S.%3f%Oz' 460 461 return times.FormatDateTime( 462 times.ParseDateTime(duration, tzinfo=times.UTC), fmt=fmt) 463 464 465def AddExpiryDetailInMembershipRoles(version, request, expiration): 466 """Add an expiration in request.membership.roles. 467 468 Args: 469 version: version 470 request: The request to modify 471 expiration: expiration date to set 472 473 Returns: 474 The updated roles. 475 476 Raises: 477 InvalidArgumentException: If 'expiration' is specified without MEMBER role. 478 479 """ 480 481 messages = ci_client.GetMessages(version) 482 roles = [] 483 has_member_role = False 484 for role in request.membership.roles: 485 if hasattr(role, 'name') and role.name == 'MEMBER': 486 has_member_role = True 487 roles.append(messages.MembershipRole( 488 name='MEMBER', 489 expiryDetail=ReformatExpiryDetail(version, expiration, 'add'))) 490 else: 491 roles.append(role) 492 493 # Checking whether the 'expiration' is specified with a MEMBER role. 494 if not has_member_role: 495 raise exceptions.InvalidArgumentException( 496 'expiration', 'Expiration date can be set with a MEMBER role only.') 497 498 return roles 499 500 501def ReformatUpdateRolesParams(args, update_roles_params): 502 """Reformat update_roles_params string. 503 504 Reformatting update_roles_params will be done by following steps, 505 1. Split the comma separated string to a list of strings. 506 2. Convert the splitted string to UpdateMembershipRolesParams message. 507 508 Args: 509 args: The argparse namespace. 510 update_roles_params: A comma separated string. 511 512 Returns: 513 A list of reformatted 'UpdateMembershipRolesParams'. 514 515 Raises: 516 InvalidArgumentException: If invalid update_roles_params string is input. 517 """ 518 519 # Split a comma separated 'update_roles_params' string. 520 update_roles_params_list = update_roles_params.split(',') 521 522 version = groups_hooks.GetApiVersion(args) 523 messages = ci_client.GetMessages(version) 524 roles_params = [] 525 arg_name = '--update-roles-params' 526 for update_roles_param in update_roles_params_list: 527 role, param_key, param_value = TokenizeUpdateRolesParams( 528 update_roles_param, arg_name) 529 530 # Membership expiry is supported only on a MEMBER role. 531 if param_key == 'expiration' and role != 'MEMBER': 532 error_msg = ('Membership Expiry is not supported on a specified role: {}.' 533 ).format(role) 534 raise exceptions.InvalidArgumentException(arg_name, error_msg) 535 536 # Instantiate MembershipRole object. 537 expiry_detail = ReformatExpiryDetail( 538 version, param_value, 'modify-membership-roles') 539 membership_role = messages.MembershipRole( 540 name=role, expiryDetail=expiry_detail) 541 542 update_mask = GetUpdateMask(param_key, arg_name) 543 544 update_membership_roles_params = messages.UpdateMembershipRolesParams( 545 fieldMask=update_mask, membershipRole=membership_role) 546 547 roles_params.append(update_membership_roles_params) 548 549 return roles_params 550 551 552def TokenizeUpdateRolesParams(update_roles_param, arg_name): 553 """Tokenize update_roles_params string. 554 555 Args: 556 update_roles_param: 'update_roles_param' string (e.g. MEMBER=expiration=3d) 557 arg_name: The argument name 558 559 Returns: 560 Tokenized strings: role (e.g. MEMBER), param_key (e.g. expiration), and 561 param_value (e.g. 3d) 562 563 Raises: 564 InvalidArgumentException: If invalid update_roles_param string is input. 565 """ 566 567 token_list = update_roles_param.split('=') 568 if len(token_list) == 3: 569 return token_list[0], token_list[1], token_list[2] 570 571 raise exceptions.InvalidArgumentException( 572 arg_name, 'Invalid format: ' + update_roles_param) 573