codekingpro/portable-devtools
114k
1# Copyright 2021 Google LLC2#3# Licensed under the Apache License, Version 2.0 (the "License");4# you may not use this file except in compliance with the License.5# You may obtain a copy of the License at6#7# http://www.apache.org/licenses/LICENSE-2.08#9# Unless required by applicable law or agreed to in writing, software10# distributed under the License is distributed on an "AS IS" BASIS,11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12# See the License for the specific language governing permissions and13# limitations under the License.14 15"""Downscoping with Credential Access Boundaries16 17This module provides the ability to downscope credentials using18`Downscoping with Credential Access Boundaries`_. This is useful to restrict the19Identity and Access Management (IAM) permissions that a short-lived credential20can use.21 22To downscope permissions of a source credential, a Credential Access Boundary23that specifies which resources the new credential can access, as well as24an upper bound on the permissions that are available on each resource, has to25be defined. A downscoped credential can then be instantiated using the source26credential and the Credential Access Boundary.27 28The common pattern of usage is to have a token broker with elevated access29generate these downscoped credentials from higher access source credentials and30pass the downscoped short-lived access tokens to a token consumer via some31secure authenticated channel for limited access to Google Cloud Storage32resources.33 34For example, a token broker can be set up on a server in a private network.35Various workloads (token consumers) in the same network will send authenticated36requests to that broker for downscoped tokens to access or modify specific google37cloud storage buckets.38 39The broker will instantiate downscoped credentials instances that can be used to40generate short lived downscoped access tokens that can be passed to the token41consumer. These downscoped access tokens can be injected by the consumer into42google.oauth2.Credentials and used to initialize a storage client instance to43access Google Cloud Storage resources with restricted access.44 45Note: Only Cloud Storage supports Credential Access Boundaries. Other Google46Cloud services do not support this feature.47 48.. _Downscoping with Credential Access Boundaries: https://cloud.google.com/iam/docs/downscoping-short-lived-credentials49"""50 51import datetime52 53from google.auth import _helpers54from google.auth import credentials55from google.auth import exceptions56from google.oauth2 import sts57 58# The maximum number of access boundary rules a Credential Access Boundary can59# contain.60_MAX_ACCESS_BOUNDARY_RULES_COUNT = 1061# The token exchange grant_type used for exchanging credentials.62_STS_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange"63# The token exchange requested_token_type. This is always an access_token.64_STS_REQUESTED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token"65# The STS token URL used to exchanged a short lived access token for a downscoped one.66_STS_TOKEN_URL_PATTERN = "https://sts.{}/v1/token"67# The subject token type to use when exchanging a short lived access token for a68# downscoped token.69_STS_SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token"70 71 72class CredentialAccessBoundary(object):73 """Defines a Credential Access Boundary which contains a list of access boundary74 rules. Each rule contains information on the resource that the rule applies to,75 the upper bound of the permissions that are available on that resource and an76 optional condition to further restrict permissions.77 """78 79 def __init__(self, rules=[]):80 """Instantiates a Credential Access Boundary. A Credential Access Boundary81 can contain up to 10 access boundary rules.82 83 Args:84 rules (Sequence[google.auth.downscoped.AccessBoundaryRule]): The list of85 access boundary rules limiting the access that a downscoped credential86 will have.87 Raises:88 InvalidType: If any of the rules are not a valid type.89 InvalidValue: If the provided rules exceed the maximum allowed.90 """91 self.rules = rules92 93 @property94 def rules(self):95 """Returns the list of access boundary rules defined on the Credential96 Access Boundary.97 98 Returns:99 Tuple[google.auth.downscoped.AccessBoundaryRule, ...]: The list of access100 boundary rules defined on the Credential Access Boundary. These are returned101 as an immutable tuple to prevent modification.102 """103 return tuple(self._rules)104 105 @rules.setter106 def rules(self, value):107 """Updates the current rules on the Credential Access Boundary. This will overwrite108 the existing set of rules.109 110 Args:111 value (Sequence[google.auth.downscoped.AccessBoundaryRule]): The list of112 access boundary rules limiting the access that a downscoped credential113 will have.114 Raises:115 InvalidType: If any of the rules are not a valid type.116 InvalidValue: If the provided rules exceed the maximum allowed.117 """118 if len(value) > _MAX_ACCESS_BOUNDARY_RULES_COUNT:119 raise exceptions.InvalidValue(120 "Credential access boundary rules can have a maximum of {} rules.".format(121 _MAX_ACCESS_BOUNDARY_RULES_COUNT122 )123 )124 for access_boundary_rule in value:125 if not isinstance(access_boundary_rule, AccessBoundaryRule):126 raise exceptions.InvalidType(127 "List of rules provided do not contain a valid 'google.auth.downscoped.AccessBoundaryRule'."128 )129 # Make a copy of the original list.130 self._rules = list(value)131 132 def add_rule(self, rule):133 """Adds a single access boundary rule to the existing rules.134 135 Args:136 rule (google.auth.downscoped.AccessBoundaryRule): The access boundary rule,137 limiting the access that a downscoped credential will have, to be added to138 the existing rules.139 Raises:140 InvalidType: If any of the rules are not a valid type.141 InvalidValue: If the provided rules exceed the maximum allowed.142 """143 if len(self.rules) == _MAX_ACCESS_BOUNDARY_RULES_COUNT:144 raise exceptions.InvalidValue(145 "Credential access boundary rules can have a maximum of {} rules.".format(146 _MAX_ACCESS_BOUNDARY_RULES_COUNT147 )148 )149 if not isinstance(rule, AccessBoundaryRule):150 raise exceptions.InvalidType(151 "The provided rule does not contain a valid 'google.auth.downscoped.AccessBoundaryRule'."152 )153 self._rules.append(rule)154 155 def to_json(self):156 """Generates the dictionary representation of the Credential Access Boundary.157 This uses the format expected by the Security Token Service API as documented in158 `Defining a Credential Access Boundary`_.159 160 .. _Defining a Credential Access Boundary:161 https://cloud.google.com/iam/docs/downscoping-short-lived-credentials#define-boundary162 163 Returns:164 Mapping: Credential Access Boundary Rule represented in a dictionary object.165 """166 rules = []167 for access_boundary_rule in self.rules:168 rules.append(access_boundary_rule.to_json())169 170 return {"accessBoundary": {"accessBoundaryRules": rules}}171 172 173class AccessBoundaryRule(object):174 """Defines an access boundary rule which contains information on the resource that175 the rule applies to, the upper bound of the permissions that are available on that176 resource and an optional condition to further restrict permissions.177 """178 179 def __init__(180 self, available_resource, available_permissions, availability_condition=None181 ):182 """Instantiates a single access boundary rule.183 184 Args:185 available_resource (str): The full resource name of the Cloud Storage bucket186 that the rule applies to. Use the format187 "//storage.googleapis.com/projects/_/buckets/bucket-name".188 available_permissions (Sequence[str]): A list defining the upper bound that189 the downscoped token will have on the available permissions for the190 resource. Each value is the identifier for an IAM predefined role or191 custom role, with the prefix "inRole:". For example:192 "inRole:roles/storage.objectViewer".193 Only the permissions in these roles will be available.194 availability_condition (Optional[google.auth.downscoped.AvailabilityCondition]):195 Optional condition that restricts the availability of permissions to196 specific Cloud Storage objects.197 198 Raises:199 InvalidType: If any of the parameters are not of the expected types.200 InvalidValue: If any of the parameters are not of the expected values.201 """202 self.available_resource = available_resource203 self.available_permissions = available_permissions204 self.availability_condition = availability_condition205 206 @property207 def available_resource(self):208 """Returns the current available resource.209 210 Returns:211 str: The current available resource.212 """213 return self._available_resource214 215 @available_resource.setter216 def available_resource(self, value):217 """Updates the current available resource.218 219 Args:220 value (str): The updated value of the available resource.221 222 Raises:223 google.auth.exceptions.InvalidType: If the value is not a string.224 """225 if not isinstance(value, str):226 raise exceptions.InvalidType(227 "The provided available_resource is not a string."228 )229 self._available_resource = value230 231 @property232 def available_permissions(self):233 """Returns the current available permissions.234 235 Returns:236 Tuple[str, ...]: The current available permissions. These are returned237 as an immutable tuple to prevent modification.238 """239 return tuple(self._available_permissions)240 241 @available_permissions.setter242 def available_permissions(self, value):243 """Updates the current available permissions.244 245 Args:246 value (Sequence[str]): The updated value of the available permissions.247 248 Raises:249 InvalidType: If the value is not a list of strings.250 InvalidValue: If the value is not valid.251 """252 for available_permission in value:253 if not isinstance(available_permission, str):254 raise exceptions.InvalidType(255 "Provided available_permissions are not a list of strings."256 )257 if available_permission.find("inRole:") != 0:258 raise exceptions.InvalidValue(259 "available_permissions must be prefixed with 'inRole:'."260 )261 # Make a copy of the original list.262 self._available_permissions = list(value)263 264 @property265 def availability_condition(self):266 """Returns the current availability condition.267 268 Returns:269 Optional[google.auth.downscoped.AvailabilityCondition]: The current270 availability condition.271 """272 return self._availability_condition273 274 @availability_condition.setter275 def availability_condition(self, value):276 """Updates the current availability condition.277 278 Args:279 value (Optional[google.auth.downscoped.AvailabilityCondition]): The updated280 value of the availability condition.281 282 Raises:283 google.auth.exceptions.InvalidType: If the value is not of type google.auth.downscoped.AvailabilityCondition284 or None.285 """286 if not isinstance(value, AvailabilityCondition) and value is not None:287 raise exceptions.InvalidType(288 "The provided availability_condition is not a 'google.auth.downscoped.AvailabilityCondition' or None."289 )290 self._availability_condition = value291 292 def to_json(self):293 """Generates the dictionary representation of the access boundary rule.294 This uses the format expected by the Security Token Service API as documented in295 `Defining a Credential Access Boundary`_.296 297 .. _Defining a Credential Access Boundary:298 https://cloud.google.com/iam/docs/downscoping-short-lived-credentials#define-boundary299 300 Returns:301 Mapping: The access boundary rule represented in a dictionary object.302 """303 json = {304 "availablePermissions": list(self.available_permissions),305 "availableResource": self.available_resource,306 }307 if self.availability_condition:308 json["availabilityCondition"] = self.availability_condition.to_json()309 return json310 311 312class AvailabilityCondition(object):313 """An optional condition that can be used as part of a Credential Access Boundary314 to further restrict permissions."""315 316 def __init__(self, expression, title=None, description=None):317 """Instantiates an availability condition using the provided expression and318 optional title or description.319 320 Args:321 expression (str): A condition expression that specifies the Cloud Storage322 objects where permissions are available. For example, this expression323 makes permissions available for objects whose name starts with "customer-a":324 "resource.name.startsWith('projects/_/buckets/example-bucket/objects/customer-a')"325 title (Optional[str]): An optional short string that identifies the purpose of326 the condition.327 description (Optional[str]): Optional details about the purpose of the condition.328 329 Raises:330 InvalidType: If any of the parameters are not of the expected types.331 InvalidValue: If any of the parameters are not of the expected values.332 """333 self.expression = expression334 self.title = title335 self.description = description336 337 @property338 def expression(self):339 """Returns the current condition expression.340 341 Returns:342 str: The current conditon expression.343 """344 return self._expression345 346 @expression.setter347 def expression(self, value):348 """Updates the current condition expression.349 350 Args:351 value (str): The updated value of the condition expression.352 353 Raises:354 google.auth.exceptions.InvalidType: If the value is not of type string.355 """356 if not isinstance(value, str):357 raise exceptions.InvalidType("The provided expression is not a string.")358 self._expression = value359 360 @property361 def title(self):362 """Returns the current title.363 364 Returns:365 Optional[str]: The current title.366 """367 return self._title368 369 @title.setter370 def title(self, value):371 """Updates the current title.372 373 Args:374 value (Optional[str]): The updated value of the title.375 376 Raises:377 google.auth.exceptions.InvalidType: If the value is not of type string or None.378 """379 if not isinstance(value, str) and value is not None:380 raise exceptions.InvalidType("The provided title is not a string or None.")381 self._title = value382 383 @property384 def description(self):385 """Returns the current description.386 387 Returns:388 Optional[str]: The current description.389 """390 return self._description391 392 @description.setter393 def description(self, value):394 """Updates the current description.395 396 Args:397 value (Optional[str]): The updated value of the description.398 399 Raises:400 google.auth.exceptions.InvalidType: If the value is not of type string or None.401 """402 if not isinstance(value, str) and value is not None:403 raise exceptions.InvalidType(404 "The provided description is not a string or None."405 )406 self._description = value407 408 def to_json(self):409 """Generates the dictionary representation of the availability condition.410 This uses the format expected by the Security Token Service API as documented in411 `Defining a Credential Access Boundary`_.412 413 .. _Defining a Credential Access Boundary:414 https://cloud.google.com/iam/docs/downscoping-short-lived-credentials#define-boundary415 416 Returns:417 Mapping[str, str]: The availability condition represented in a dictionary418 object.419 """420 json = {"expression": self.expression}421 if self.title:422 json["title"] = self.title423 if self.description:424 json["description"] = self.description425 return json426 427 428class Credentials(credentials.CredentialsWithQuotaProject):429 """Defines a set of Google credentials that are downscoped from an existing set430 of Google OAuth2 credentials. This is useful to restrict the Identity and Access431 Management (IAM) permissions that a short-lived credential can use.432 The common pattern of usage is to have a token broker with elevated access433 generate these downscoped credentials from higher access source credentials and434 pass the downscoped short-lived access tokens to a token consumer via some435 secure authenticated channel for limited access to Google Cloud Storage436 resources.437 """438 439 def __init__(440 self,441 source_credentials,442 credential_access_boundary,443 quota_project_id=None,444 universe_domain=credentials.DEFAULT_UNIVERSE_DOMAIN,445 ):446 """Instantiates a downscoped credentials object using the provided source447 credentials and credential access boundary rules.448 To downscope permissions of a source credential, a Credential Access Boundary449 that specifies which resources the new credential can access, as well as an450 upper bound on the permissions that are available on each resource, has to be451 defined. A downscoped credential can then be instantiated using the source452 credential and the Credential Access Boundary.453 454 Args:455 source_credentials (google.auth.credentials.Credentials): The source credentials456 to be downscoped based on the provided Credential Access Boundary rules.457 credential_access_boundary (google.auth.downscoped.CredentialAccessBoundary):458 The Credential Access Boundary which contains a list of access boundary459 rules. Each rule contains information on the resource that the rule applies to,460 the upper bound of the permissions that are available on that resource and an461 optional condition to further restrict permissions.462 quota_project_id (Optional[str]): The optional quota project ID.463 universe_domain (Optional[str]): The universe domain value, default is googleapis.com464 Raises:465 google.auth.exceptions.RefreshError: If the source credentials466 return an error on token refresh.467 google.auth.exceptions.OAuthError: If the STS token exchange468 endpoint returned an error during downscoped token generation.469 """470 471 super(Credentials, self).__init__()472 self._source_credentials = source_credentials473 self._credential_access_boundary = credential_access_boundary474 self._quota_project_id = quota_project_id475 self._universe_domain = universe_domain or credentials.DEFAULT_UNIVERSE_DOMAIN476 self._sts_client = sts.Client(477 _STS_TOKEN_URL_PATTERN.format(self.universe_domain)478 )479 480 @_helpers.copy_docstring(credentials.Credentials)481 def refresh(self, request):482 # Generate an access token from the source credentials.483 self._source_credentials.refresh(request)484 now = _helpers.utcnow()485 # Exchange the access token for a downscoped access token.486 response_data = self._sts_client.exchange_token(487 request=request,488 grant_type=_STS_GRANT_TYPE,489 subject_token=self._source_credentials.token,490 subject_token_type=_STS_SUBJECT_TOKEN_TYPE,491 requested_token_type=_STS_REQUESTED_TOKEN_TYPE,492 additional_options=self._credential_access_boundary.to_json(),493 )494 self.token = response_data.get("access_token")495 # For downscoping CAB flow, the STS endpoint may not return the expiration496 # field for some flows. The generated downscoped token should always have497 # the same expiration time as the source credentials. When no expires_in498 # field is returned in the response, we can just get the expiration time499 # from the source credentials.500 if response_data.get("expires_in"):501 lifetime = datetime.timedelta(seconds=response_data.get("expires_in"))502 self.expiry = now + lifetime503 else:504 self.expiry = self._source_credentials.expiry505 506 @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)507 def with_quota_project(self, quota_project_id):508 return self.__class__(509 self._source_credentials,510 self._credential_access_boundary,511 quota_project_id=quota_project_id,512 )513 