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