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