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"""General IAM utilities used by the Cloud SDK.""" 17 18from __future__ import absolute_import 19from __future__ import division 20from __future__ import unicode_literals 21 22import binascii 23import re 24import textwrap 25 26from apitools.base.protorpclite import messages as apitools_messages 27from apitools.base.py import encoding 28 29from googlecloudsdk.api_lib.util import apis as core_apis 30from googlecloudsdk.calliope import arg_parsers 31from googlecloudsdk.calliope import exceptions as gcloud_exceptions 32from googlecloudsdk.command_lib.iam import completers 33from googlecloudsdk.core import exceptions as core_exceptions 34from googlecloudsdk.core import log 35from googlecloudsdk.core import resources 36from googlecloudsdk.core import yaml 37from googlecloudsdk.core.console import console_io 38from googlecloudsdk.core.util import files 39import six 40 41 42# generation from proto. 43kms_message = core_apis.GetMessagesModule('cloudkms', 'v1') 44encoding.AddCustomJsonFieldMapping( 45 kms_message.CloudkmsProjectsLocationsKeyRingsGetIamPolicyRequest, 46 'options_requestedPolicyVersion', 'options.requestedPolicyVersion') 47 48encoding.AddCustomJsonFieldMapping( 49 kms_message.CloudkmsProjectsLocationsKeyRingsCryptoKeysGetIamPolicyRequest, 50 'options_requestedPolicyVersion', 'options.requestedPolicyVersion') 51 52encoding.AddCustomJsonFieldMapping( 53 kms_message.CloudkmsProjectsLocationsKeyRingsImportJobsGetIamPolicyRequest, 54 'options_requestedPolicyVersion', 'options.requestedPolicyVersion') 55secrets_message = core_apis.GetMessagesModule('secretmanager', 'v1') 56encoding.AddCustomJsonFieldMapping( 57 secrets_message.SecretmanagerProjectsSecretsGetIamPolicyRequest, 58 'options_requestedPolicyVersion', 'options.requestedPolicyVersion') 59 60msgs = core_apis.GetMessagesModule('iam', 'v1') 61encoding.AddCustomJsonFieldMapping( 62 msgs.IamProjectsServiceAccountsGetIamPolicyRequest, 63 'options_requestedPolicyVersion', 'options.requestedPolicyVersion') 64 65 66MANAGED_BY = (msgs.IamProjectsServiceAccountsKeysListRequest 67 .KeyTypesValueValuesEnum) 68CREATE_KEY_TYPES = (msgs.CreateServiceAccountKeyRequest 69 .PrivateKeyTypeValueValuesEnum) 70KEY_TYPES = (msgs.ServiceAccountKey.PrivateKeyTypeValueValuesEnum) 71PUBLIC_KEY_TYPES = ( 72 msgs.IamProjectsServiceAccountsKeysGetRequest.PublicKeyTypeValueValuesEnum) 73STAGE_TYPES = (msgs.Role.StageValueValuesEnum) 74 75SERVICE_ACCOUNTS_COLLECTION = 'iam.projects.serviceAccounts' 76 77SERVICE_ACCOUNT_FORMAT = ('table(displayName:label="DISPLAY NAME", email, ' 78 'disabled)') 79SERVICE_ACCOUNT_KEY_FORMAT = """ 80 table( 81 name.scope(keys):label=KEY_ID, 82 validAfterTime:label=CREATED_AT, 83 validBeforeTime:label=EXPIRES_AT 84 ) 85""" 86CONDITION_FORMAT_EXCEPTION = gcloud_exceptions.InvalidArgumentException( 87 'condition', 88 'condition must be either `None` or a list of key=value pairs. ' 89 'If not `None`, `expression` and `title` are required keys.\n' 90 'Example: --condition=expression=[expression],title=[title],' 91 'description=[description]') 92 93CONDITION_FILE_FORMAT_EXCEPTION = gcloud_exceptions.InvalidArgumentException( 94 'condition-from-file', 95 'condition-from-file must be a path to a YAML or JSON file containing the ' 96 'condition. `expression` and `title` are required keys. `description` is ' 97 'optional. To specify a `None` condition, use --condition=None.') 98 99MAX_LIBRARY_IAM_SUPPORTED_VERSION = 3 100 101_ALL_CONDITIONS = {'All': None} 102_NEW_CONDITION = object() 103_NONE_CONDITION = {'None': None} 104 105 106def _IsAllConditions(condition): 107 return condition == _ALL_CONDITIONS 108 109 110class IamEtagReadError(core_exceptions.Error): 111 """IamEtagReadError is raised when etag is badly formatted.""" 112 113 114class IamPolicyBindingNotFound(core_exceptions.Error): 115 """Raised when the specified IAM policy binding is not found.""" 116 117 118class IamPolicyBindingInvalidError(core_exceptions.Error): 119 """Raised when the specified IAM policy binding is invalid.""" 120 121 122class IamPolicyBindingIncompleteError(IamPolicyBindingInvalidError): 123 """Raised when the specified IAM policy binding is incomplete.""" 124 125 126def _AddMemberFlag(parser, verb, required=True): 127 """Create --member flag and add to parser.""" 128 help_str = ( 129 """\ 130The member {verb}. Should be of the form `user|group|serviceAccount:email` or 131`domain:domain`. 132 133Examples: `user:test-user@gmail.com`, `group:admins@example.com`, 134`serviceAccount:test123@example.domain.com`, or 135`domain:example.domain.com`. 136 137Can also be one of the following special values: 138* `allUsers` - Special identifier that represents anyone who is on the internet, 139 with or without a Google account. 140* `allAuthenticatedUsers` - Special identifier that represents anyone who is 141 authenticated with a Google account or a service account. 142 """ 143 ).format(verb=verb) 144 parser.add_argument('--member', required=required, help=help_str) 145 146 147def _ConditionArgDict(): 148 condition_spec = { 149 'expression': str, 150 'title': str, 151 'description': str, 152 'None': None 153 } 154 return arg_parsers.ArgDict(spec=condition_spec, allow_key_only=True) 155 156 157def _ConditionHelpText(intro): 158 """Get the help text for --condition.""" 159 160 help_text = ( 161 """\ 162{intro} 163 164When using the `--condition` flag, include the following key-value pairs: 165 166*expression*::: (Required) Condition expression that evaluates to True or False. 167This uses a subset of Common Expression Language syntax. 168 169If the condition expression includes a comma, use a different delimiter to 170separate the key-value pairs. Specify the delimiter before listing the 171key-value pairs. For example, to specify a colon (`:`) as the delimiter, do the 172following: `--condition=^:^title=TITLE:expression=EXPRESSION`. For more 173information, see https://cloud.google.com/sdk/gcloud/reference/topic/escaping. 174 175*title*::: (Required) A short string describing the purpose of the expression. 176 177*description*::: (Optional) Additional description for the expression. 178 """).format(intro=intro) 179 return help_text 180 181 182def _AddConditionFlagsForAddBindingToIamPolicy(parser): 183 """Create flags for condition and add to parser.""" 184 condition_intro = """\ 185A condition to include in the binding. When the condition is explicitly 186specified as `None` (`--condition=None`), a binding without a condition is 187added. When the condition is specified and is not `None`, `--role` cannot be a 188basic role. Basic roles are `roles/editor`, `roles/owner`, and `roles/viewer`. 189For more on conditions, refer to the conditions overview guide: 190https://cloud.google.com/iam/docs/conditions-overview""" 191 help_str_condition = _ConditionHelpText(condition_intro) 192 help_str_condition_from_file = """ 193Path to a local JSON or YAML file that defines the condition. 194To see available fields, see the help for `--condition`.""" 195 condition_group = parser.add_mutually_exclusive_group() 196 condition_group.add_argument( 197 '--condition', 198 type=_ConditionArgDict(), 199 metavar='KEY=VALUE', 200 help=help_str_condition) 201 202 condition_group.add_argument( 203 '--condition-from-file', 204 type=arg_parsers.FileContents(), 205 help=help_str_condition_from_file) 206 207 208def _AddConditionFlagsForRemoveBindingFromIamPolicy(parser, 209 condition_completer=None): 210 """Create flags for condition and add to parser.""" 211 condition_intro = """\ 212The condition of the binding that you want to remove. When the condition is 213explicitly specified as `None` (`--condition=None`), a binding without a 214condition is removed. Otherwise, only a binding with a condition that exactly 215matches the specified condition (including the optional description) is removed. 216For more on conditions, refer to the conditions overview guide: 217https://cloud.google.com/iam/docs/conditions-overview""" 218 help_str_condition = _ConditionHelpText(condition_intro) 219 help_str_condition_from_file = """ 220Path to a local JSON or YAML file that defines the condition. 221To see available fields, see the help for `--condition`.""" 222 help_str_condition_all = """ 223Remove all bindings with this role and member, irrespective of any 224conditions.""" 225 condition_group = parser.add_mutually_exclusive_group() 226 condition_group.add_argument( 227 '--condition', 228 type=_ConditionArgDict(), 229 metavar='KEY=VALUE', 230 completer=condition_completer, 231 help=help_str_condition) 232 233 condition_group.add_argument( 234 '--condition-from-file', 235 type=arg_parsers.FileContents(), 236 help=help_str_condition_from_file) 237 238 condition_group.add_argument( 239 '--all', action='store_true', help=help_str_condition_all) 240 241 242def ValidateConditionArgument(condition, exception): 243 if 'None' in condition: 244 if ('expression' in condition or 'description' in condition or 245 'title' in condition): 246 raise exception 247 else: 248 if not condition.get('expression') or not condition.get('title'): 249 raise exception 250 251 252def ValidateMutexConditionAndPrimitiveRoles(condition, role): 253 primitive_roles = ['roles/editor', 'roles/owner', 'roles/viewer'] 254 if (_ConditionIsSpecified(condition) and not _IsNoneCondition(condition) and 255 role in primitive_roles): 256 raise IamPolicyBindingInvalidError( 257 'Binding with a condition and a basic role is not allowed. ' 258 'Basic roles are `roles/editor`, `roles/owner`, ' 259 'and `roles/viewer`.') 260 261 262def ValidateAndExtractConditionMutexRole(args): 263 """Extract IAM condition from arguments and validate conditon/role mutex.""" 264 condition = ValidateAndExtractCondition(args) 265 ValidateMutexConditionAndPrimitiveRoles(condition, args.role) 266 return condition 267 268 269def ValidateAndExtractCondition(args): 270 """Extract IAM condition from arguments.""" 271 condition = None 272 if args.IsSpecified('condition'): 273 ValidateConditionArgument(args.condition, CONDITION_FORMAT_EXCEPTION) 274 condition = args.condition 275 if args.IsSpecified('condition_from_file'): 276 condition = ParseYamlOrJsonCondition(args.condition_from_file) 277 return condition 278 279 280def AddArgForPolicyFile(parser): 281 """Adds the IAM policy file argument to the given parser. 282 283 Args: 284 parser: An argparse.ArgumentParser-like object to which we add the argss. 285 286 Raises: 287 ArgumentError if one of the arguments is already defined in the parser. 288 """ 289 parser.add_argument( 290 'policy_file', 291 metavar='POLICY_FILE', 292 help="""\ 293 Path to a local JSON or YAML formatted file containing a valid policy. 294 295 The output of the `get-iam-policy` command is a valid file, as is any 296 JSON or YAML file conforming to the structure of a 297 [Policy](https://cloud.google.com/iam/reference/rest/v1/Policy). 298 """) 299 300 301def AddArgsForAddIamPolicyBinding(parser, 302 role_completer=None, 303 add_condition=False): 304 """Adds the IAM policy binding arguments for role and members. 305 306 Args: 307 parser: An argparse.ArgumentParser-like object to which we add the argss. 308 role_completer: A command_lib.iam.completers.IamRolesCompleter class to 309 complete the `--role` flag value. 310 add_condition: boolean, If true, add the flags for condition. 311 312 Raises: 313 ArgumentError if one of the arguments is already defined in the parser. 314 """ 315 316 help_text = """ 317 Role name to assign to the member. The role name is the complete path of a 318 predefined role, such as `roles/logging.viewer`, or a custom role, such as 319 `organizations/{ORGANIZATION_ID}/roles/logging.viewer`. 320 """ 321 322 parser.add_argument( 323 '--role', 324 required=True, 325 completer=role_completer, 326 help=help_text) 327 _AddMemberFlag(parser, 'to add the binding for') 328 if add_condition: 329 _AddConditionFlagsForAddBindingToIamPolicy(parser) 330 331 332# TODO (b/114447521): implement a completer for condition 333def AddArgsForRemoveIamPolicyBinding(parser, 334 role_completer=None, 335 add_condition=False, 336 condition_completer=None): 337 """Adds the IAM policy binding arguments for role and members. 338 339 Args: 340 parser: An argparse.ArgumentParser-like object to which we add the args. 341 role_completer: A command_lib.iam.completers.IamRolesCompleter class to 342 complete the --role flag value. 343 add_condition: boolean, If true, add the flags for condition. 344 condition_completer: A completer to complete the condition flag value. 345 346 Raises: 347 ArgumentError if one of the arguments is already defined in the parser. 348 """ 349 parser.add_argument( 350 '--role', 351 required=True, 352 completer=role_completer, 353 help='The role to remove the member from.') 354 _AddMemberFlag(parser, 'to remove the binding for') 355 if add_condition: 356 _AddConditionFlagsForRemoveBindingFromIamPolicy( 357 parser, condition_completer=condition_completer) 358 359 360def AddBindingToIamPolicy(binding_message_type, policy, member, role): 361 """Given an IAM policy, add new bindings as specified by args. 362 363 An IAM binding is a pair of role and member. Check if the arguments passed 364 define both the role and member attribute, create a binding out of their 365 values, and append it to the policy. 366 367 Args: 368 binding_message_type: The protorpc.Message of the Binding to create 369 policy: IAM policy to which we want to add the bindings. 370 member: The member to add to IAM policy. 371 role: The role the member should have. 372 373 Returns: 374 boolean, whether or not the policy was updated. 375 """ 376 377 # First check all bindings to see if the member is already in a binding with 378 # the same role. 379 # A policy can have multiple bindings with the same role. This is why we need 380 # to explicitly do this as a separate, first, step and check all bindings. 381 for binding in policy.bindings: 382 if binding.role == role: 383 if member in binding.members: 384 return False # Nothing to do. Member already has the role. 385 386 # Second step: check to see if a binding already exists with the same role and 387 # add the member to this binding. This is to not create new bindings with 388 # the same role. 389 for binding in policy.bindings: 390 if binding.role == role: 391 binding.members.append(member) 392 return True 393 394 # Third step: no binding was found that has the same role. Create a new one. 395 policy.bindings.append(binding_message_type( 396 members=[member], role='{0}'.format(role))) 397 return True 398 399 400def _IsNoneCondition(condition): 401 """When user specify --condition=None.""" 402 return condition is not None and 'None' in condition 403 404 405def _ConditionIsSpecified(condition): 406 """When --condition is specified.""" 407 return condition is not None 408 409 410def AddBindingToIamPolicyWithCondition(binding_message_type, 411 condition_message_type, policy, member, 412 role, condition): 413 """Given an IAM policy, add a new role/member binding with condition. 414 415 An IAM binding is a pair of role and member with an optional condition. 416 Check if the arguments passed define both the role and member attribute, 417 create a binding out of their values, and append it to the policy. 418 419 Args: 420 binding_message_type: The protorpc.Message of the Binding to create. 421 condition_message_type: the protorpc.Message of the Expr. 422 policy: IAM policy to which we want to add the bindings. 423 member: The member of the binding. 424 role: The role the member should have. 425 condition: The condition of the role/member binding. 426 427 Raises: 428 IamPolicyBindingIncompleteError: when user adds a binding without specifying 429 --condition to a policy containing conditions in the non-interactive mode. 430 """ 431 if _PolicyContainsCondition(policy) and not _ConditionIsSpecified(condition): 432 if not console_io.CanPrompt(): 433 message = ( 434 'Adding a binding without specifying a condition to a ' 435 'policy containing conditions is prohibited in non-interactive ' 436 'mode. Run the command again with `--condition=None`') 437 raise IamPolicyBindingIncompleteError(message) 438 condition = _PromptForConditionAddBindingToIamPolicy(policy) 439 ValidateConditionArgument(condition, CONDITION_FORMAT_EXCEPTION) 440 ValidateMutexConditionAndPrimitiveRoles(condition, role) 441 if (not _PolicyContainsCondition(policy) and 442 _ConditionIsSpecified(condition) and not _IsNoneCondition(condition)): 443 log.warning('Adding binding with condition to a policy without condition ' 444 'will change the behavior of add-iam-policy-binding and ' 445 'remove-iam-policy-binding commands.') 446 condition = None if _IsNoneCondition(condition) else condition 447 _AddBindingToIamPolicyWithCondition(binding_message_type, 448 condition_message_type, policy, member, 449 role, condition) 450 451 452def _ConditionsInPolicy(policy, member=None, role=None): 453 """Select conditions in bindings which have the given role and member. 454 455 Search bindings from policy and return their conditions which has the given 456 role and member if role and member are given. If member and role are not 457 given, return all conditions. Duplicates are not returned. 458 459 Args: 460 policy: IAM policy to collect conditions 461 member: member which should appear in the binding to select its condition 462 role: role which should be the role of binding to select its condition 463 464 Returns: 465 A list of conditions got selected 466 """ 467 conditions = {} 468 for binding in policy.bindings: 469 if (member is None or member in binding.members) and (role is None or 470 role == binding.role): 471 condition = binding.condition 472 conditions[_ConditionToString(condition)] = condition 473 contain_none = False 474 if 'None' in conditions: 475 contain_none = True 476 del conditions['None'] 477 conditions = [(condition_str, condition) 478 for condition_str, condition in conditions.items()] 479 conditions = sorted(conditions, key=lambda x: x[0]) 480 if contain_none: 481 conditions.append(('None', _NONE_CONDITION)) 482 return conditions 483 484 485def _ConditionToString(condition): 486 if condition is None: 487 return 'None' 488 keys = ['expression', 'title', 'description'] 489 key_values = [] 490 for key in keys: 491 if getattr(condition, key) is not None: 492 key_values.append('{key}={value}'.format( 493 key=key.upper(), value=getattr(condition, key))) 494 return ', '.join(key_values) 495 496 497def PromptChoicesForAddBindingToIamPolicy(policy): 498 """The choices in a prompt for condition when adding binding to policy. 499 500 All conditions in the policy will be returned. Two more choices (i.e. 501 `None` and `Specify a new condition`) are appended. 502 Args: 503 policy: the IAM policy which the binding is added to. 504 Returns: 505 a list of conditions appearing in policy plus the choices of `None` and 506 `Specify a new condition`. 507 """ 508 conditions = _ConditionsInPolicy(policy) 509 if conditions and conditions[-1][0] != 'None': 510 conditions.append(('None', _NONE_CONDITION)) 511 conditions.append(('Specify a new condition', _NEW_CONDITION)) 512 return conditions 513 514 515def PromptChoicesForRemoveBindingFromIamPolicy(policy, member, role): 516 """The choices in a prompt for condition when removing binding from policy. 517 518 Args: 519 policy: the IAM policy which the binding is removed from. 520 member: the member of the binding to be removed. 521 role: the role of the binding to be removed. 522 Returns: 523 a list of conditions from the policy whose bindings contain the given member 524 and role. 525 """ 526 conditions = _ConditionsInPolicy(policy, member, role) 527 if conditions: 528 conditions.append(('all conditions', _ALL_CONDITIONS)) 529 return conditions 530 531 532def _ToDictCondition(condition): 533 if isinstance(condition, dict): 534 return condition 535 return_condition = {} 536 for key in ('expression', 'title', 'description'): 537 return_condition[key] = getattr(condition, key) 538 return return_condition 539 540 541def _PromptForConditionAddBindingToIamPolicy(policy): 542 """Prompt user for a condition when adding binding.""" 543 prompt_message = ('The policy contains bindings with conditions, ' 544 'so specifying a condition is required when adding a ' 545 'binding. Please specify a condition.') 546 conditions = PromptChoicesForAddBindingToIamPolicy(policy) 547 condition_keys = [c[0] for c in conditions] 548 549 condition_index = console_io.PromptChoice( 550 condition_keys, prompt_string=prompt_message) 551 if condition_index == len(conditions) - 1: 552 return _PromptForNewCondition() 553 return _ToDictCondition(conditions[condition_index][1]) 554 555 556def _PromptForConditionRemoveBindingFromIamPolicy(policy, member, role): 557 """Prompt user for a condition when removing binding.""" 558 conditions = PromptChoicesForRemoveBindingFromIamPolicy(policy, member, role) 559 if not conditions: 560 raise IamPolicyBindingNotFound('Policy binding with the specified member ' 561 'and role not found!') 562 prompt_message = ('The policy contains bindings with conditions, ' 563 'so specifying a condition is required when removing a ' 564 'binding. Please specify a condition.') 565 condition_keys = [c[0] for c in conditions] 566 567 condition_index = console_io.PromptChoice( 568 condition_keys, prompt_string=prompt_message) 569 if condition_index == len(conditions) - 1: 570 return _ALL_CONDITIONS 571 return _ToDictCondition(conditions[condition_index][1]) 572 573 574def _PromptForNewCondition(): 575 prompt_message = ( 576 'Condition is either `None` or a list of key=value pairs. ' 577 'If not `None`, `expression` and `title` are required keys.\n' 578 'Example: --condition=expression=[expression],title=[title],' 579 'description=[description].\nSpecify the condition') 580 condition_string = console_io.PromptWithDefault(prompt_message) 581 condition_dict = _ConditionArgDict()(condition_string) 582 return condition_dict 583 584 585def _EqualConditions(binding_condition, input_condition): 586 if binding_condition is None and input_condition is None: 587 return True 588 if binding_condition is None or input_condition is None: 589 return False 590 return (binding_condition.expression == input_condition.get('expression') and 591 binding_condition.title == input_condition.get('title') and 592 binding_condition.description == input_condition.get('description')) 593 594 595def _AddBindingToIamPolicyWithCondition(binding_message_type, 596 condition_message_type, policy, member, 597 role, condition): 598 """Given an IAM policy, add a new role/member binding with condition.""" 599 for binding in policy.bindings: 600 if binding.role == role and _EqualConditions( 601 binding_condition=binding.condition, input_condition=condition): 602 if member not in binding.members: 603 binding.members.append(member) 604 return 605 606 condition_message = None 607 if condition is not None: 608 condition_message = condition_message_type( 609 expression=condition.get('expression'), 610 title=condition.get('title'), 611 description=condition.get('description')) 612 policy.bindings.append( 613 binding_message_type( 614 members=[member], role='{}'.format(role), 615 condition=condition_message)) 616 617 618def RemoveBindingFromIamPolicyWithCondition(policy, 619 member, 620 role, 621 condition, 622 all_conditions=False): 623 """Given an IAM policy, remove bindings as specified by the args. 624 625 An IAM binding is a pair of role and member with an optional condition. 626 Check if the arguments passed define both the role and member attribute, 627 search the policy for a binding that contains this role, member and condition, 628 and remove it from the policy. 629 630 Args: 631 policy: IAM policy from which we want to remove bindings. 632 member: The member to remove from the IAM policy. 633 role: The role of the member should be removed from. 634 condition: The condition of the binding to be removed. 635 all_conditions: If true, all bindings with the specified member and role 636 will be removed, regardless of the condition. 637 638 Raises: 639 IamPolicyBindingNotFound: If specified binding is not found. 640 IamPolicyBindingIncompleteError: when user removes a binding without 641 specifying --condition to a policy containing conditions in the 642 non-interactive mode. 643 """ 644 if not all_conditions and _PolicyContainsCondition( 645 policy) and not _ConditionIsSpecified(condition): 646 if not console_io.CanPrompt(): 647 message = ( 648 'Removing a binding without specifying a condition from a ' 649 'policy containing conditions is prohibited in non-interactive ' 650 'mode. Run the command again with `--condition=None` to remove a ' 651 'binding without condition or run command with `--all` to remove all ' 652 'bindings of the specified member and role.') 653 raise IamPolicyBindingIncompleteError(message) 654 condition = _PromptForConditionRemoveBindingFromIamPolicy( 655 policy, member, role) 656 657 if all_conditions or _IsAllConditions(condition): 658 _RemoveBindingFromIamPolicyAllConditions(policy, member, role) 659 else: 660 condition = None if _IsNoneCondition(condition) else condition 661 _RemoveBindingFromIamPolicyWithCondition(policy, member, role, condition) 662 663 664def _RemoveBindingFromIamPolicyAllConditions(policy, member, role): 665 """Remove all member/role bindings from policy regardless of condition.""" 666 conditions_removed = False 667 for binding in policy.bindings: 668 if role == binding.role and member in binding.members: 669 binding.members.remove(member) 670 conditions_removed = True 671 if not conditions_removed: 672 raise IamPolicyBindingNotFound('Policy bindings with the specified member ' 673 'and role not found!') 674 policy.bindings[:] = [b for b in policy.bindings if b.members] 675 676 677def _RemoveBindingFromIamPolicyWithCondition(policy, member, role, condition): 678 """Remove the member/role binding with the condition from policy.""" 679 for binding in policy.bindings: 680 if (role == binding.role and _EqualConditions( 681 binding_condition=binding.condition, input_condition=condition) and 682 member in binding.members): 683 binding.members.remove(member) 684 break 685 else: 686 raise IamPolicyBindingNotFound('Policy binding with the specified member, ' 687 'role, and condition not found!') 688 policy.bindings[:] = [b for b in policy.bindings if b.members] 689 690 691def _PolicyContainsCondition(policy): 692 """Investigate if policy has bindings with condition. 693 694 Given an IAM policy and return True if the policy contains any binding 695 which has a condition. Return False otherwise. 696 697 Args: 698 policy: IAM policy. 699 700 Returns: 701 True if policy has bindings with conditions, otherwise False. 702 """ 703 for binding in policy.bindings: 704 if binding.condition: 705 return True 706 return False 707 708 709def BindingInPolicy(policy, member, role): 710 """Returns True if policy contains the specified binding.""" 711 for binding in policy.bindings: 712 if binding.role == role and member in binding.members: 713 return True 714 return False 715 716 717def RemoveBindingFromIamPolicy(policy, member, role): 718 """Given an IAM policy, remove bindings as specified by the args. 719 720 An IAM binding is a pair of role and member. Check if the arguments passed 721 define both the role and member attribute, search the policy for a binding 722 that contains this role and member, and remove it from the policy. 723 724 Args: 725 policy: IAM policy from which we want to remove bindings. 726 member: The member to remove from the IAM policy. 727 role: The role the member should be removed from. 728 729 Raises: 730 IamPolicyBindingNotFound: If specified binding is not found. 731 """ 732 733 # First, remove the member from any binding that has the given role. 734 # A server policy can have duplicates. 735 for binding in policy.bindings: 736 if binding.role == role and member in binding.members: 737 binding.members.remove(member) 738 break 739 else: 740 message = 'Policy binding with the specified member and role not found!' 741 raise IamPolicyBindingNotFound(message) 742 743 # Second, remove any empty bindings. 744 policy.bindings[:] = [b for b in policy.bindings if b.members] 745 746 747def ConstructUpdateMaskFromPolicy(policy_file_path): 748 """Construct a FieldMask based on input policy. 749 750 Args: 751 policy_file_path: Path to the JSON or YAML IAM policy file. 752 Returns: 753 a FieldMask containing policy fields to be modified, based on which fields 754 are present in the input file. 755 """ 756 policy_file = files.ReadFileContents(policy_file_path) 757 # Since json is a subset of yaml, parse file as yaml. 758 policy = yaml.load(policy_file) 759 760 # The IAM update mask should only contain top level fields. Sort the fields 761 # for testing purposes. 762 return ','.join(sorted(policy.keys())) 763 764 765def ParsePolicyFile(policy_file_path, policy_message_type): 766 """Construct an IAM Policy protorpc.Message from a JSON/YAML formatted file. 767 768 Args: 769 policy_file_path: Path to the JSON or YAML IAM policy file. 770 policy_message_type: Policy message type to convert JSON or YAML to. 771 Returns: 772 a protorpc.Message of type policy_message_type filled in from the JSON or 773 YAML policy file. 774 Raises: 775 BadFileException if the JSON or YAML file is malformed. 776 """ 777 policy, unused_mask = ParseYamlOrJsonPolicyFile(policy_file_path, 778 policy_message_type) 779 780 if not policy.etag: 781 msg = ('The specified policy does not contain an "etag" field ' 782 'identifying a specific version to replace. Changing a ' 783 'policy without an "etag" can overwrite concurrent policy ' 784 'changes.') 785 console_io.PromptContinue( 786 message=msg, prompt_string='Replace existing policy', cancel_on_no=True) 787 return policy 788 789 790def ParsePolicyFileWithUpdateMask(policy_file_path, policy_message_type): 791 """Construct an IAM Policy protorpc.Message from a JSON/YAML formatted file. 792 793 Also contructs a FieldMask based on input policy. 794 Args: 795 policy_file_path: Path to the JSON or YAML IAM policy file. 796 policy_message_type: Policy message type to convert JSON or YAML to. 797 Returns: 798 a tuple of (policy, updateMask) where policy is a protorpc.Message of type 799 policy_message_type filled in from the JSON or YAML policy file and 800 updateMask is a FieldMask containing policy fields to be modified, based on 801 which fields are present in the input file. 802 Raises: 803 BadFileException if the JSON or YAML file is malformed. 804 IamEtagReadError if the etag is badly formatted. 805 """ 806 policy, update_mask = ParseYamlOrJsonPolicyFile(policy_file_path, 807 policy_message_type) 808 809 if not policy.etag: 810 msg = ('The specified policy does not contain an "etag" field ' 811 'identifying a specific version to replace. Changing a ' 812 'policy without an "etag" can overwrite concurrent policy ' 813 'changes.') 814 console_io.PromptContinue( 815 message=msg, prompt_string='Replace existing policy', cancel_on_no=True) 816 return (policy, update_mask) 817 818 819def ParseYamlOrJsonPolicyFile(policy_file_path, policy_message_type): 820 """Create an IAM Policy protorpc.Message from a YAML or JSON formatted file. 821 822 Returns the parsed policy object and FieldMask derived from input dict. 823 Args: 824 policy_file_path: Path to the YAML or JSON IAM policy file. 825 policy_message_type: Policy message type to convert YAML to. 826 Returns: 827 a tuple of (policy, updateMask) where policy is a protorpc.Message of type 828 policy_message_type filled in from the JSON or YAML policy file and 829 updateMask is a FieldMask containing policy fields to be modified, based on 830 which fields are present in the input file. 831 Raises: 832 BadFileException if the YAML or JSON file is malformed. 833 IamEtagReadError if the etag is badly formatted. 834 """ 835 policy_to_parse = yaml.load_path(policy_file_path) 836 try: 837 policy = encoding.PyValueToMessage(policy_message_type, policy_to_parse) 838 update_mask = ','.join(sorted(policy_to_parse.keys())) 839 except (AttributeError) as e: 840 # Raised when the input file is not properly formatted YAML policy file. 841 raise gcloud_exceptions.BadFileException( 842 'Policy file [{0}] is not a properly formatted YAML or JSON ' 843 'policy file. {1}' 844 .format(policy_file_path, six.text_type(e))) 845 except (apitools_messages.DecodeError, binascii.Error) as e: 846 # DecodeError is raised when etag is badly formatted (not proper Base64) 847 raise IamEtagReadError( 848 'The etag of policy file [{0}] is not properly formatted. {1}' 849 .format(policy_file_path, six.text_type(e))) 850 return (policy, update_mask) 851 852 853def ParseYamlOrJsonCondition( 854 condition_file_content, 855 file_format_exception=CONDITION_FILE_FORMAT_EXCEPTION): 856 """Create a condition of IAM policy binding from content of YAML or JSON file. 857 858 Args: 859 condition_file_content: string, the content of a YAML or JSON file 860 containing a condition. 861 file_format_exception: InvalidArgumentException, the exception to throw when 862 condition file is incorrectly formatted. 863 864 Returns: 865 a dictionary representation of the condition. 866 """ 867 868 condition = yaml.load(condition_file_content) 869 ValidateConditionArgument(condition, file_format_exception) 870 return condition 871 872 873def ParseYamlToRole(file_path, role_message_type): 874 """Construct an IAM Role protorpc.Message from a Yaml formatted file. 875 876 Args: 877 file_path: Path to the Yaml IAM Role file. 878 role_message_type: Role message type to convert Yaml to. 879 880 Returns: 881 a protorpc.Message of type role_message_type filled in from the Yaml 882 role file. 883 Raises: 884 BadFileException if the Yaml file is malformed or does not exist. 885 """ 886 role_to_parse = yaml.load_path(file_path) 887 if 'stage' in role_to_parse: 888 role_to_parse['stage'] = role_to_parse['stage'].upper() 889 try: 890 role = encoding.PyValueToMessage(role_message_type, role_to_parse) 891 except (AttributeError) as e: 892 # Raised when the YAML file is not properly formatted YAML role file. 893 raise gcloud_exceptions.BadFileException( 894 'Role file {0} is not a properly formatted YAML role file. {1}' 895 .format(file_path, six.text_type(e))) 896 except (apitools_messages.DecodeError, binascii.Error) as e: 897 # DecodeError is raised when etag is badly formatted (not proper Base64) 898 raise IamEtagReadError( 899 'The etag of role file {0} is not properly formatted. {1}' 900 .format(file_path, six.text_type(e))) 901 return role 902 903 904def GetDetailedHelpForSetIamPolicy(collection, example_id='', 905 example_see_more='', additional_flags='', 906 use_an=False): 907 """Returns a detailed_help for a set-iam-policy command. 908 909 Args: 910 collection: Name of the command collection (ex: "project", "dataset") 911 example_id: Collection identifier to display in a sample command 912 (ex: "my-project", '1234') 913 example_see_more: Optional "See ... for details" message. If not specified, 914 includes a default reference to IAM managing-policies documentation 915 additional_flags: str, additional flags to include in the example command 916 (after the command name and before the ID of the resource). 917 use_an: If True, uses "an" instead of "a" for the article preceding uses of 918 the collection. 919 Returns: 920 a dict with boilerplate help text for the set-iam-policy command 921 """ 922 if not example_id: 923 example_id = 'example-' + collection 924 925 if not example_see_more: 926 example_see_more = """ 927 See https://cloud.google.com/iam/docs/managing-policies for details 928 of the policy file format and contents.""" 929 930 additional_flags = additional_flags + ' ' if additional_flags else '' 931 a = 'an' if use_an else 'a' 932 return { 933 'brief': 934 'Set IAM policy for {0} {1}.'.format(a, collection), 935 'DESCRIPTION': 936 '{description}', 937 'EXAMPLES': 938 textwrap.dedent("""\ 939 The following command will read an IAM policy from 'policy.json' and 940 set it for {a} {collection} with '{id}' as the identifier: 941 942 $ {{command}} {flags}{id} policy.json 943 944 {see_more}""".format( 945 collection=collection, 946 id=example_id, 947 see_more=example_see_more, 948 flags=additional_flags, 949 a=a)) 950 } 951 952 953def GetDetailedHelpForAddIamPolicyBinding(collection, 954 example_id, 955 role='roles/editor', 956 use_an=False, 957 condition=False): 958 """Returns a detailed_help for an add-iam-policy-binding command. 959 960 Args: 961 collection: Name of the command collection (ex: "project", "dataset") 962 example_id: Collection identifier to display in a sample command 963 (ex: "my-project", '1234') 964 role: The sample role to use in the documentation. The default of 965 'roles/editor' is usually sufficient, but if your command group's 966 users would more likely use a different role, you can override it here. 967 use_an: If True, uses "an" instead of "a" for the article preceding uses of 968 the collection. 969 condition: If True, add help text for condition. 970 971 Returns: 972 a dict with boilerplate help text for the add-iam-policy-binding command 973 """ 974 a = 'an' if use_an else 'a' 975 note = ('See https://cloud.google.com/iam/docs/managing-policies for details ' 976 'of policy role and member types.') 977 detailed_help = { 978 'brief': 979 'Add IAM policy binding for {0} {1}.'.format(a, collection), 980 'DESCRIPTION': 981 '{description}', 982 'EXAMPLES': 983 """To add an IAM policy binding for the role of '{role}' for the user 984'test-user@gmail.com' on {a} {collection} with identifier 985'{example_id}', run: 986 987 $ {{command}} {example_id} --member='user:test-user@gmail.com' --role='{role}' 988 989To add an IAM policy binding for the role of '{role}' to the service 990account 'test-proj1@example.domain.com', run: 991 992 $ {{command}} {example_id} --member='serviceAccount:test-proj1@example.domain.com' --role='{role}' 993 994To add an IAM policy binding for the role of '{role}' for all 995authenticated users on {a} {collection} with identifier 996'{example_id}', run: 997 998 $ {{command}} {example_id} --member='allAuthenticatedUsers' --role='{role}' 999 """.format(collection=collection, example_id=example_id, role=role, a=a) 1000 } 1001 if condition: 1002 detailed_help['EXAMPLES'] = detailed_help['EXAMPLES'] + """\n 1003To add an IAM policy binding that expires at the end of the year 2018 for the 1004role of '{role}' and the user 'test-user@gmail.com' on {a} {collection} with 1005identifier '{example_id}', run: 1006 1007 $ {{command}} {example_id} --member='user:test-user@gmail.com' --role='{role}' --condition='expression=request.time < timestamp("2019-01-01T00:00:00Z"),title=expires_end_of_2018,description=Expires at midnight on 2018-12-31' 1008 """.format( 1009 collection=collection, example_id=example_id, role=role, a=a) 1010 detailed_help['EXAMPLES'] = '\n'.join([detailed_help['EXAMPLES'], note]) 1011 return detailed_help 1012 1013 1014def GetDetailedHelpForRemoveIamPolicyBinding(collection, 1015 example_id, 1016 role='roles/editor', 1017 use_an=False, 1018 condition=False): 1019 """Returns a detailed_help for a remove-iam-policy-binding command. 1020 1021 Args: 1022 collection: Name of the command collection (ex: "project", "dataset") 1023 example_id: Collection identifier to display in a sample command 1024 (ex: "my-project", '1234') 1025 role: The sample role to use in the documentation. The default of 1026 'roles/editor' is usually sufficient, but if your command group's 1027 users would more likely use a different role, you can override it here. 1028 use_an: If True, uses "an" instead of "a" for the article preceding uses of 1029 the collection. 1030 condition: If True, add help text for condition. 1031 Returns: 1032 a dict with boilerplate help text for the remove-iam-policy-binding command 1033 """ 1034 a = 'an' if use_an else 'a' 1035 note = ('See https://cloud.google.com/iam/docs/managing-policies for details' 1036 ' of policy role and member types.') 1037 detailed_help = { 1038 'brief': 1039 'Remove IAM policy binding for {0} {1}.'.format(a, collection), 1040 'DESCRIPTION': 1041 '{description}', 1042 'EXAMPLES': 1043 """\ 1044To remove an IAM policy binding for the role of '{role}' for the 1045user 'test-user@gmail.com' on {collection} with identifier 1046'{example_id}', run: 1047 1048 $ {{command}} {example_id} --member='user:test-user@gmail.com' --role='{role}' 1049 1050To remove an IAM policy binding for the role of '{role}' from all 1051authenticated users on {collection} '{example_id}', run: 1052 1053 $ {{command}} {example_id} --member='allAuthenticatedUsers' --role='{role}' 1054 """.format(collection=collection, example_id=example_id, role=role) 1055 } 1056 if condition: 1057 detailed_help['EXAMPLES'] = detailed_help['EXAMPLES'] + """\n 1058To remove an IAM policy binding with a condition of 1059expression='request.time < timestamp("2019-01-01T00:00:00Z")', 1060title='expires_end_of_2018', and description='Expires at midnight on 2018-12-31' 1061for the role of '{role}' for the user 'test-user@gmail.com' on {collection} 1062with identifier '{example_id}', run: 1063 1064 $ {{command}} {example_id} --member='user:test-user@gmail.com' --role='{role}' --condition='expression=request.time < timestamp("2019-01-01T00:00:00Z"),title=expires_end_of_2018,description=Expires at midnight on 2018-12-31' 1065 1066To remove all IAM policy bindings regardless of the condition for the role of 1067'{role}' and for the user 'test-user@gmail.com' on {collection} with 1068identifier '{example_id}', run: 1069 1070 $ {{command}} {example_id} --member='user:test-user@gmail.com' --role='{role}' --all 1071 """.format( 1072 collection=collection, example_id=example_id, role='roles/browser') 1073 detailed_help['EXAMPLES'] = '\n'.join([detailed_help['EXAMPLES'], note]) 1074 return detailed_help 1075 1076 1077def GetHintForServiceAccountResource(action='act on'): 1078 """Returns a hint message for commands treating service account as a resource. 1079 1080 Args: 1081 action: the action to take on the service account resource (with necessary 1082 prepositions), such as 'add iam policy bindings to'. 1083 """ 1084 1085 return ('When managing IAM roles, you can treat a service account either as ' 1086 'a resource or as an identity. This command is to {action} a ' 1087 'service account resource. There are other gcloud commands to ' 1088 'manage IAM policies for other types of resources. For example, to ' 1089 'manage IAM policies on a project, use the `$ gcloud projects` ' 1090 'commands.'.format(action=action)) 1091 1092 1093def ManagedByFromString(managed_by): 1094 """Parses a string into a MANAGED_BY enum. 1095 1096 MANAGED_BY is an enum of who manages a service account key resource. IAM 1097 will rotate any SYSTEM_MANAGED keys by default. 1098 1099 Args: 1100 managed_by: A string representation of a MANAGED_BY. Can be one of *user*, 1101 *system* or *any*. 1102 1103 Returns: 1104 A KeyTypeValueValuesEnum (MANAGED_BY) value. 1105 """ 1106 if managed_by == 'user': 1107 return [MANAGED_BY.USER_MANAGED] 1108 elif managed_by == 'system': 1109 return [MANAGED_BY.SYSTEM_MANAGED] 1110 elif managed_by == 'any': 1111 return [] 1112 else: 1113 return [MANAGED_BY.KEY_TYPE_UNSPECIFIED] 1114 1115 1116def KeyTypeFromString(key_str): 1117 """Parses a string into a KeyType enum. 1118 1119 Args: 1120 key_str: A string representation of a KeyType. Can be either *p12* or 1121 *json*. 1122 1123 Returns: 1124 A PrivateKeyTypeValueValuesEnum value. 1125 """ 1126 if key_str == 'p12': 1127 return KEY_TYPES.TYPE_PKCS12_FILE 1128 elif key_str == 'json': 1129 return KEY_TYPES.TYPE_GOOGLE_CREDENTIALS_FILE 1130 else: 1131 return KEY_TYPES.TYPE_UNSPECIFIED 1132 1133 1134def KeyTypeToString(key_type): 1135 """Get a string version of a KeyType enum. 1136 1137 Args: 1138 key_type: An enum of either KEY_TYPES or CREATE_KEY_TYPES. 1139 1140 Returns: 1141 The string representation of the key_type, such that 1142 parseKeyType(keyTypeToString(x)) is a no-op. 1143 """ 1144 if (key_type == KEY_TYPES.TYPE_PKCS12_FILE or 1145 key_type == CREATE_KEY_TYPES.TYPE_PKCS12_FILE): 1146 return 'p12' 1147 elif (key_type == KEY_TYPES.TYPE_GOOGLE_CREDENTIALS_FILE or 1148 key_type == CREATE_KEY_TYPES.TYPE_GOOGLE_CREDENTIALS_FILE): 1149 return 'json' 1150 else: 1151 return 'unspecified' 1152 1153 1154def KeyTypeToCreateKeyType(key_type): 1155 """Transforms between instances of KeyType enums. 1156 1157 Transforms KeyTypes into CreateKeyTypes. 1158 1159 Args: 1160 key_type: A ServiceAccountKey.PrivateKeyTypeValueValuesEnum value. 1161 1162 Returns: 1163 A IamProjectsServiceAccountKeysCreateRequest.PrivateKeyTypeValueValuesEnum 1164 value. 1165 """ 1166 # For some stupid reason, HTTP requests generates different enum types for 1167 # each instance of an enum in the proto buffer. What's worse is that they're 1168 # not equal to one another. 1169 if key_type == KEY_TYPES.TYPE_PKCS12_FILE: 1170 return CREATE_KEY_TYPES.TYPE_PKCS12_FILE 1171 elif key_type == KEY_TYPES.TYPE_GOOGLE_CREDENTIALS_FILE: 1172 return CREATE_KEY_TYPES.TYPE_GOOGLE_CREDENTIALS_FILE 1173 else: 1174 return CREATE_KEY_TYPES.TYPE_UNSPECIFIED 1175 1176 1177def KeyTypeFromCreateKeyType(key_type): 1178 """The inverse of *toCreateKeyType*.""" 1179 if key_type == CREATE_KEY_TYPES.TYPE_PKCS12_FILE: 1180 return KEY_TYPES.TYPE_PKCS12_FILE 1181 elif key_type == CREATE_KEY_TYPES.TYPE_GOOGLE_CREDENTIALS_FILE: 1182 return KEY_TYPES.TYPE_GOOGLE_CREDENTIALS_FILE 1183 else: 1184 return KEY_TYPES.TYPE_UNSPECIFIED 1185 1186 1187def AccountNameValidator(): 1188 # https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts/create 1189 return arg_parsers.RegexpValidator( 1190 r'[a-z][a-z0-9\-]{4,28}[a-z0-9]', 1191 'Service account name must be between 6 and 30 characters (inclusive), ' 1192 'must begin with a lowercase letter, and consist of lowercase ' 1193 'alphanumeric characters that can be separated by hyphens.') 1194 1195 1196def ProjectToProjectResourceName(project): 1197 """Turns a project id into a project resource name.""" 1198 return 'projects/{0}'.format(project) 1199 1200 1201def EmailToAccountResourceName(email): 1202 """Turns an email into a service account resource name.""" 1203 return 'projects/-/serviceAccounts/{0}'.format(email) 1204 1205 1206def EmailAndKeyToResourceName(email, key): 1207 """Turns an email and key id into a key resource name.""" 1208 return 'projects/-/serviceAccounts/{0}/keys/{1}'.format(email, key) 1209 1210 1211def EmailAndIdentityBindingToResourceName(email, identity_binding): 1212 """Turns an email and identity binding id into a key resource name.""" 1213 return 'projects/-/serviceAccounts/{0}/identityBindings/{1}'.format( 1214 email, identity_binding) 1215 1216 1217def GetKeyIdFromResourceName(name): 1218 """Gets the key id from a resource name. No validation is done.""" 1219 return name.split('/')[5] 1220 1221 1222def PublicKeyTypeFromString(key_str): 1223 """Parses a string into a PublicKeyType enum. 1224 1225 Args: 1226 key_str: A string representation of a PublicKeyType. Can be either *pem* or 1227 *raw*. 1228 1229 Returns: 1230 A PublicKeyTypeValueValuesEnum value. 1231 """ 1232 if key_str == 'pem': 1233 return PUBLIC_KEY_TYPES.TYPE_X509_PEM_FILE 1234 return PUBLIC_KEY_TYPES.TYPE_RAW_PUBLIC_KEY 1235 1236 1237def StageTypeFromString(stage_str): 1238 """Parses a string into a stage enum. 1239 1240 Args: 1241 stage_str: A string representation of a StageType. Can be *alpha* or *beta* 1242 or *ga* or *deprecated* or *disabled*. 1243 1244 Returns: 1245 A StageValueValuesEnum value. 1246 """ 1247 lower_stage_str = stage_str.lower() 1248 stage_dict = { 1249 'alpha': STAGE_TYPES.ALPHA, 1250 'beta': STAGE_TYPES.BETA, 1251 'ga': STAGE_TYPES.GA, 1252 'deprecated': STAGE_TYPES.DEPRECATED, 1253 'disabled': STAGE_TYPES.DISABLED 1254 } 1255 if lower_stage_str not in stage_dict: 1256 raise gcloud_exceptions.InvalidArgumentException( 1257 'stage', 1258 'The stage should be one of ' + ','.join(sorted(stage_dict)) + '.') 1259 return stage_dict[lower_stage_str] 1260 1261 1262def VerifyParent(organization, project, attribute='custom roles'): 1263 """Verify the parent name.""" 1264 if organization is None and project is None: 1265 raise gcloud_exceptions.RequiredArgumentException( 1266 '--organization or --project', 1267 'Should specify the project or organization name for {0}.' 1268 .format(attribute)) 1269 if organization and project: 1270 raise gcloud_exceptions.ConflictingArgumentsException( 1271 'organization', 'project') 1272 1273 1274def GetRoleName(organization, 1275 project, 1276 role, 1277 attribute='custom roles', 1278 parameter_name='ROLE_ID'): 1279 """Gets the Role name from organization Id and role Id.""" 1280 if role.startswith('roles/'): 1281 if project or organization: 1282 raise gcloud_exceptions.InvalidArgumentException( 1283 parameter_name, 1284 'The role id that starts with \'roles/\' only stands for curated ' 1285 'role. Should not specify the project or organization for curated ' 1286 'roles') 1287 return role 1288 1289 if role.startswith('projects/') or role.startswith('organizations/'): 1290 raise gcloud_exceptions.InvalidArgumentException( 1291 parameter_name, 'The role id should not include any \'projects/\' or ' 1292 '\'organizations/\' prefix.') 1293 if '/' in role: 1294 raise gcloud_exceptions.InvalidArgumentException( 1295 parameter_name, 'The role id should not include any \'/\' character.') 1296 VerifyParent(organization, project, attribute) 1297 if organization: 1298 return 'organizations/{0}/roles/{1}'.format(organization, role) 1299 return 'projects/{0}/roles/{1}'.format(project, role) 1300 1301 1302def GetParentName(organization, project, attribute='custom roles'): 1303 """Gets the Role parent name from organization name or project name.""" 1304 VerifyParent(organization, project, attribute) 1305 if organization: 1306 return 'organizations/{0}'.format(organization) 1307 return 'projects/{0}'.format(project) 1308 1309 1310def GetResourceName(resource_ref): 1311 """Convert a full resource URL to an atomic path.""" 1312 full_name = resource_ref.SelfLink() 1313 full_name = re.sub(r'\w+://', '//', full_name) # no protocol at the start 1314 full_name = re.sub(r'/v[0-9]+[0-9a-zA-Z]*/', '/', full_name) # no version 1315 if full_name.startswith('//www.'): 1316 # Convert '//www.googleapis.com/compute/' to '//compute.googleapis.com/' 1317 splitted_list = full_name.split('/') 1318 service = full_name.split('/')[3] 1319 splitted_list.pop(3) 1320 full_name = '/'.join(splitted_list) 1321 full_name = full_name.replace('//www.', '//{0}.'.format(service)) 1322 return full_name 1323 1324 1325def ServiceAccountsUriFunc(resource): 1326 """Transforms a service account resource into a URL string. 1327 1328 Args: 1329 resource: The ServiceAccount object 1330 1331 Returns: 1332 URL to the service account 1333 """ 1334 1335 ref = resources.REGISTRY.Parse(resource.uniqueId, 1336 {'projectsId': resource.projectId}, 1337 collection=SERVICE_ACCOUNTS_COLLECTION) 1338 return ref.SelfLink() 1339 1340 1341def AddServiceAccountNameArg(parser, action='to act on'): 1342 """Adds the IAM service account name argument that supports tab completion. 1343 1344 Args: 1345 parser: An argparse.ArgumentParser-like object to which we add the args. 1346 action: Action to display in the help message. Should be something like 1347 'to act on' or a relative phrase like 'whose policy to get'. 1348 1349 Raises: 1350 ArgumentError if one of the arguments is already defined in the parser. 1351 """ 1352 1353 parser.add_argument('service_account', 1354 metavar='SERVICE_ACCOUNT', 1355 type=GetIamAccountFormatValidator(), 1356 completer=completers.IamServiceAccountCompleter, 1357 help=('The service account {}. The account should be ' 1358 'formatted either as a numeric service account ID ' 1359 'or as an email, like this: ' 1360 '123456789876543212345 or ' 1361 'my-iam-account@somedomain.com.'.format(action))) 1362 1363 1364def LogSetIamPolicy(name, kind): 1365 log.status.Print('Updated IAM policy for {} [{}].'.format(kind, name)) 1366 1367 1368def GetIamAccountFormatValidator(): 1369 """Checks that provided iam account identifier is valid.""" 1370 return arg_parsers.RegexpValidator( 1371 # Overly broad on purpose but catches most common issues. 1372 r'^(.+@.+\..+|[0-9]+)$', 1373 'Not a valid service account identifier. It should be either a ' 1374 'numeric string representing the unique_id or an email of the form: ' 1375 'my-iam-account@somedomain.com or ' 1376 'my-iam-account@PROJECT_ID.iam.gserviceaccount.com') 1377 1378 1379def GetIamOutputFileValidator(): 1380 """Checks if the output file is writable.""" 1381 1382 def IsWritable(value): 1383 try: 1384 with files.FileWriter(value, private=True) as f: 1385 f.close() 1386 return value 1387 except files.Error as e: 1388 raise gcloud_exceptions.BadFileException(e) 1389 1390 return IsWritable 1391 1392 1393def SetRoleStageIfAlpha(role): 1394 """Set the role stage to Alpha if None. 1395 1396 Args: 1397 role: A protorpc.Message of type Role. 1398 """ 1399 if role.stage is None: 1400 role.stage = StageTypeFromString('alpha') 1401 1402 1403def GetResourceReference(project, organization): 1404 """Get the resource reference of a project or organization. 1405 1406 Args: 1407 project: A project name string. 1408 organization: An organization id string. 1409 1410 Returns: 1411 The resource reference of the given project or organization. 1412 """ 1413 if project: 1414 return resources.REGISTRY.Parse( 1415 project, collection='cloudresourcemanager.projects') 1416 else: 1417 return resources.REGISTRY.Parse( 1418 organization, collection='cloudresourcemanager.organizations') 1419 1420 1421def TestingPermissionsWarning(permissions): 1422 """Prompt a warning for TESTING permissions with a 'y/n' question. 1423 1424 Args: 1425 permissions: A list of permissions that need to be warned. 1426 """ 1427 if permissions: 1428 msg = ('Note: permissions [' + ', '.join(permissions) + 1429 '] are in \'TESTING\' stage which means ' 1430 'the functionality is not mature and they can go away in the ' 1431 'future. This can break your workflows, so do not use them in ' 1432 'production systems!') 1433 console_io.PromptContinue( 1434 message=msg, 1435 prompt_string='Are you sure you want to make this change?', 1436 cancel_on_no=True) 1437 1438 1439def ApiDisabledPermissionsWarning(permissions): 1440 """Prompt a warning for API diabled permissions. 1441 1442 Args: 1443 permissions: A list of permissions that need to be warned. 1444 """ 1445 if permissions: 1446 msg = ( 1447 'API is not enabled for permissions: [' + ', '.join(permissions) + 1448 ']. Please enable the corresponding APIs to use those permissions.\n') 1449 log.warning(msg) 1450