1# -*- coding: utf-8 -*- #
2# Copyright 2015 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"""Common utility functions for all projects commands."""
17
18from __future__ import absolute_import
19from __future__ import division
20from __future__ import unicode_literals
21
22import datetime
23import re
24
25from apitools.base.py.exceptions import HttpForbiddenError
26
27from googlecloudsdk.api_lib.cloudresourcemanager import organizations
28from googlecloudsdk.api_lib.cloudresourcemanager import projects_api
29from googlecloudsdk.api_lib.cloudresourcemanager import projects_util
30from googlecloudsdk.api_lib.resource_manager import folders
31from googlecloudsdk.command_lib.projects import exceptions
32from googlecloudsdk.core import resources
33
34import six
35
36PROJECTS_COLLECTION = 'cloudresourcemanager.projects'
37PROJECTS_API_VERSION = projects_util.DEFAULT_API_VERSION
38_CLOUD_CONSOLE_LAUNCH_DATE = datetime.datetime(2012, 10, 11)
39LIST_FORMAT = """
40    table(
41      projectId:sort=1,
42      name,
43      projectNumber
44    )
45"""
46
47
48_VALID_PROJECT_REGEX = re.compile(
49    r'^'
50    # An optional domain-like component, ending with a colon, e.g.,
51    # google.com:
52    r'(?:(?:[-a-z0-9]{1,63}\.)*(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?):)?'
53    # Followed by a required identifier-like component, for example:
54    #   waffle-house    match
55    #   -foozle        no match
56    #   Foozle         no match
57    # We specifically disallow project number, even though some GCP backends
58    # could accept them.
59    # We also allow a leading digit as some legacy project ids can have
60    # a leading digit.
61    r'(?:(?:[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?))'
62    r'$'
63)
64
65
66def ValidateProjectIdentifier(project):
67  """Checks to see if the project string is valid project name or number."""
68  if not isinstance(project, six.string_types):
69    return False
70
71  if project.isdigit() or _VALID_PROJECT_REGEX.match(project):
72    return True
73
74  return False
75
76
77def GetProjectNumber(project_id):
78  return projects_api.Get(ParseProject(project_id)).projectNumber
79
80
81def ParseProject(project_id, api_version=PROJECTS_API_VERSION):
82  # Override the default API map version so we can increment API versions on a
83  # API interface basis.
84  registry = resources.REGISTRY.Clone()
85  registry.RegisterApiByName('cloudresourcemanager', api_version)
86  return registry.Parse(
87      None, params={'projectId': project_id}, collection=PROJECTS_COLLECTION)
88
89
90def ProjectsUriFunc(resource, api_version=PROJECTS_API_VERSION):
91  project_id = (resource.get('projectId', None) if isinstance(resource, dict)
92                else resource.projectId)
93  ref = ParseProject(project_id, api_version)
94  return ref.SelfLink()
95
96
97def IdFromName(project_name):
98  """Returns a candidate id for a new project with the given name.
99
100  Args:
101    project_name: Human-readable name of the project.
102
103  Returns:
104    A candidate project id, or 'None' if no reasonable candidate is found.
105  """
106
107  def SimplifyName(name):
108    name = name.lower()
109    name = re.sub(r'[^a-z0-9\s/._-]', '', name, flags=re.U)
110    name = re.sub(r'[\s/._-]+', '-', name, flags=re.U)
111    name = name.lstrip('-0123456789').rstrip('-')
112    return name
113
114  def CloudConsoleNowString():
115    now = datetime.datetime.utcnow()
116    return '{}{:02}'.format((now - _CLOUD_CONSOLE_LAUNCH_DATE).days, now.hour)
117
118  def GenIds(name):
119    base = SimplifyName(name)
120    # Cloud Console generates the two following candidates in the opposite
121    # order, but they are validating uniqueness and we're not, so we put the
122    # "more unique" suggestion first.
123    yield base + '-' + CloudConsoleNowString()
124    yield base
125    # Cloud Console has an four-tier "allocate an unused id" architecture for
126    # coining ids *not* based on the project name. This might be sensible for
127    # an interface where ids are expected to be auto-generated, but seems like
128    # major overkill (and a shift in paradigm from "assistant" to "wizard") for
129    # gcloud. -shearer@ 2016-11
130
131  def IsValidId(i):
132    # TODO(b/32950431) could check availability of id
133    return 6 <= len(i) <= 30
134
135  for i in GenIds(project_name):
136    if IsValidId(i):
137      return i
138  return None
139
140
141def SetIamPolicyFromFileHook(ref, args, request):
142  """Hook to perserve SetIAMPolicy behavior for declarative surface."""
143  del ref
144  del args
145  update_mask = request.setIamPolicyRequest.updateMask
146  if update_mask:
147    # To preserve the existing set-iam-policy behavior of always overwriting
148    # bindings and etag, add bindings and etag to update_mask.
149    mask_fields = update_mask.split(',')
150    if 'bindings' not in mask_fields:
151      mask_fields.append('bindings')
152
153    if 'etag' not in update_mask:
154      mask_fields.append('etag')
155    request.setIamPolicyRequest.updateMask = ','.join(mask_fields)
156  return request
157
158
159def GetIamPolicyWithAncestors(project_id):
160  """Get IAM policy for given project and its ancestors.
161
162  Args:
163    project_id: project id
164
165  Returns:
166    IAM policy for given project and its ancestors
167  """
168  iam_policies = []
169  ancestry = projects_api.GetAncestry(project_id)
170
171  try:
172    for resource in ancestry.ancestor:
173      resource_type = resource.resourceId.type
174      resource_id = resource.resourceId.id
175      # this is the given project
176      if resource_type == 'project':
177        project_ref = ParseProject(project_id)
178        iam_policies.append({
179            'type': 'project',
180            'id': project_id,
181            'policy': projects_api.GetIamPolicy(project_ref)
182        })
183      if resource_type == 'folder':
184        iam_policies.append({
185            'type': resource_type,
186            'id': resource_id,
187            'policy': folders.GetIamPolicy(resource_id)
188        })
189      if resource_type == 'organization':
190        iam_policies.append({
191            'type': resource_type,
192            'id': resource_id,
193            'policy': organizations.Client().GetIamPolicy(resource_id),
194        })
195    return iam_policies
196  except HttpForbiddenError:
197    raise exceptions.AncestorsIamPolicyAccessDeniedError(
198        'User is not permitted to access IAM policy for one or more of the'
199        ' ancestors')
200