blob: beea50ec7d659bf92f104b969e18d84e60f2a266 [file] [log] [blame]
bojeil-googled8839212021-07-08 10:56:22 -07001# Copyright 2021 Google LLC
2#
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 at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# 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 and
13# limitations under the License.
14
15"""Downscoping with Credential Access Boundaries
16
17This module provides the ability to downscope credentials using
18`Downscoping with Credential Access Boundaries`_. This is useful to restrict the
19Identity and Access Management (IAM) permissions that a short-lived credential
20can use.
21
22To downscope permissions of a source credential, a Credential Access Boundary
23that specifies which resources the new credential can access, as well as
24an upper bound on the permissions that are available on each resource, has to
25be defined. A downscoped credential can then be instantiated using the source
26credential and the Credential Access Boundary.
27
28The common pattern of usage is to have a token broker with elevated access
29generate these downscoped credentials from higher access source credentials and
30pass the downscoped short-lived access tokens to a token consumer via some
31secure authenticated channel for limited access to Google Cloud Storage
32resources.
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 authenticated
36requests to that broker for downscoped tokens to access or modify specific google
37cloud storage buckets.
38
39The broker will instantiate downscoped credentials instances that can be used to
40generate short lived downscoped access tokens that can be passed to the token
41consumer. These downscoped access tokens can be injected by the consumer into
42google.oauth2.Credentials and used to initialize a storage client instance to
43access Google Cloud Storage resources with restricted access.
44
45Note: Only Cloud Storage supports Credential Access Boundaries. Other Google
46Cloud services do not support this feature.
47
48.. _Downscoping with Credential Access Boundaries: https://cloud.google.com/iam/docs/downscoping-short-lived-credentials
49"""
50
51# The maximum number of access boundary rules a Credential Access Boundary can
52# contain.
53_MAX_ACCESS_BOUNDARY_RULES_COUNT = 10
54
55
56class CredentialAccessBoundary(object):
57 """Defines a Credential Access Boundary which contains a list of access boundary
58 rules. Each rule contains information on the resource that the rule applies to,
59 the upper bound of the permissions that are available on that resource and an
60 optional condition to further restrict permissions.
61 """
62
63 def __init__(self, rules=[]):
64 """Instantiates a Credential Access Boundary. A Credential Access Boundary
65 can contain up to 10 access boundary rules.
66
67 Args:
68 rules (Sequence[google.auth.downscoped.AccessBoundaryRule]): The list of
69 access boundary rules limiting the access that a downscoped credential
70 will have.
71 Raises:
72 TypeError: If any of the rules are not a valid type.
73 ValueError: If the provided rules exceed the maximum allowed.
74 """
75 self.rules = rules
76
77 @property
78 def rules(self):
79 """Returns the list of access boundary rules defined on the Credential
80 Access Boundary.
81
82 Returns:
83 Tuple[google.auth.downscoped.AccessBoundaryRule, ...]: The list of access
84 boundary rules defined on the Credential Access Boundary. These are returned
85 as an immutable tuple to prevent modification.
86 """
87 return tuple(self._rules)
88
89 @rules.setter
90 def rules(self, value):
91 """Updates the current rules on the Credential Access Boundary. This will overwrite
92 the existing set of rules.
93
94 Args:
95 value (Sequence[google.auth.downscoped.AccessBoundaryRule]): The list of
96 access boundary rules limiting the access that a downscoped credential
97 will have.
98 Raises:
99 TypeError: If any of the rules are not a valid type.
100 ValueError: If the provided rules exceed the maximum allowed.
101 """
102 if len(value) > _MAX_ACCESS_BOUNDARY_RULES_COUNT:
103 raise ValueError(
104 "Credential access boundary rules can have a maximum of {} rules.".format(
105 _MAX_ACCESS_BOUNDARY_RULES_COUNT
106 )
107 )
108 for access_boundary_rule in value:
109 if not isinstance(access_boundary_rule, AccessBoundaryRule):
110 raise TypeError(
111 "List of rules provided do not contain a valid 'google.auth.downscoped.AccessBoundaryRule'."
112 )
113 # Make a copy of the original list.
114 self._rules = list(value)
115
116 def add_rule(self, rule):
117 """Adds a single access boundary rule to the existing rules.
118
119 Args:
120 rule (google.auth.downscoped.AccessBoundaryRule): The access boundary rule,
121 limiting the access that a downscoped credential will have, to be added to
122 the existing rules.
123 Raises:
124 TypeError: If any of the rules are not a valid type.
125 ValueError: If the provided rules exceed the maximum allowed.
126 """
127 if len(self.rules) == _MAX_ACCESS_BOUNDARY_RULES_COUNT:
128 raise ValueError(
129 "Credential access boundary rules can have a maximum of {} rules.".format(
130 _MAX_ACCESS_BOUNDARY_RULES_COUNT
131 )
132 )
133 if not isinstance(rule, AccessBoundaryRule):
134 raise TypeError(
135 "The provided rule does not contain a valid 'google.auth.downscoped.AccessBoundaryRule'."
136 )
137 self._rules.append(rule)
138
139 def to_json(self):
140 """Generates the dictionary representation of the Credential Access Boundary.
141 This uses the format expected by the Security Token Service API as documented in
142 `Defining a Credential Access Boundary`_.
143
144 .. _Defining a Credential Access Boundary:
145 https://cloud.google.com/iam/docs/downscoping-short-lived-credentials#define-boundary
146
147 Returns:
148 Mapping: Credential Access Boundary Rule represented in a dictionary object.
149 """
150 rules = []
151 for access_boundary_rule in self.rules:
152 rules.append(access_boundary_rule.to_json())
153
154 return {"accessBoundary": {"accessBoundaryRules": rules}}
155
156
157class AccessBoundaryRule(object):
158 """Defines an access boundary rule which contains information on the resource that
159 the rule applies to, the upper bound of the permissions that are available on that
160 resource and an optional condition to further restrict permissions.
161 """
162
163 def __init__(
164 self, available_resource, available_permissions, availability_condition=None
165 ):
166 """Instantiates a single access boundary rule.
167
168 Args:
169 available_resource (str): The full resource name of the Cloud Storage bucket
170 that the rule applies to. Use the format
171 "//storage.googleapis.com/projects/_/buckets/bucket-name".
172 available_permissions (Sequence[str]): A list defining the upper bound that
173 the downscoped token will have on the available permissions for the
174 resource. Each value is the identifier for an IAM predefined role or
175 custom role, with the prefix "inRole:". For example:
176 "inRole:roles/storage.objectViewer".
177 Only the permissions in these roles will be available.
178 availability_condition (Optional[google.auth.downscoped.AvailabilityCondition]):
179 Optional condition that restricts the availability of permissions to
180 specific Cloud Storage objects.
181
182 Raises:
183 TypeError: If any of the parameters are not of the expected types.
184 ValueError: If any of the parameters are not of the expected values.
185 """
186 self.available_resource = available_resource
187 self.available_permissions = available_permissions
188 self.availability_condition = availability_condition
189
190 @property
191 def available_resource(self):
192 """Returns the current available resource.
193
194 Returns:
195 str: The current available resource.
196 """
197 return self._available_resource
198
199 @available_resource.setter
200 def available_resource(self, value):
201 """Updates the current available resource.
202
203 Args:
204 value (str): The updated value of the available resource.
205
206 Raises:
207 TypeError: If the value is not a string.
208 """
209 if not isinstance(value, str):
210 raise TypeError("The provided available_resource is not a string.")
211 self._available_resource = value
212
213 @property
214 def available_permissions(self):
215 """Returns the current available permissions.
216
217 Returns:
218 Tuple[str, ...]: The current available permissions. These are returned
219 as an immutable tuple to prevent modification.
220 """
221 return tuple(self._available_permissions)
222
223 @available_permissions.setter
224 def available_permissions(self, value):
225 """Updates the current available permissions.
226
227 Args:
228 value (Sequence[str]): The updated value of the available permissions.
229
230 Raises:
231 TypeError: If the value is not a list of strings.
232 ValueError: If the value is not valid.
233 """
234 for available_permission in value:
235 if not isinstance(available_permission, str):
236 raise TypeError(
237 "Provided available_permissions are not a list of strings."
238 )
239 if available_permission.find("inRole:") != 0:
240 raise ValueError(
241 "available_permissions must be prefixed with 'inRole:'."
242 )
243 # Make a copy of the original list.
244 self._available_permissions = list(value)
245
246 @property
247 def availability_condition(self):
248 """Returns the current availability condition.
249
250 Returns:
251 Optional[google.auth.downscoped.AvailabilityCondition]: The current
252 availability condition.
253 """
254 return self._availability_condition
255
256 @availability_condition.setter
257 def availability_condition(self, value):
258 """Updates the current availability condition.
259
260 Args:
261 value (Optional[google.auth.downscoped.AvailabilityCondition]): The updated
262 value of the availability condition.
263
264 Raises:
265 TypeError: If the value is not of type google.auth.downscoped.AvailabilityCondition
266 or None.
267 """
268 if not isinstance(value, AvailabilityCondition) and value is not None:
269 raise TypeError(
270 "The provided availability_condition is not a 'google.auth.downscoped.AvailabilityCondition' or None."
271 )
272 self._availability_condition = value
273
274 def to_json(self):
275 """Generates the dictionary representation of the access boundary rule.
276 This uses the format expected by the Security Token Service API as documented in
277 `Defining a Credential Access Boundary`_.
278
279 .. _Defining a Credential Access Boundary:
280 https://cloud.google.com/iam/docs/downscoping-short-lived-credentials#define-boundary
281
282 Returns:
283 Mapping: The access boundary rule represented in a dictionary object.
284 """
285 json = {
286 "availablePermissions": list(self.available_permissions),
287 "availableResource": self.available_resource,
288 }
289 if self.availability_condition:
290 json["availabilityCondition"] = self.availability_condition.to_json()
291 return json
292
293
294class AvailabilityCondition(object):
295 """An optional condition that can be used as part of a Credential Access Boundary
296 to further restrict permissions."""
297
298 def __init__(self, expression, title=None, description=None):
299 """Instantiates an availability condition using the provided expression and
300 optional title or description.
301
302 Args:
303 expression (str): A condition expression that specifies the Cloud Storage
304 objects where permissions are available. For example, this expression
305 makes permissions available for objects whose name starts with "customer-a":
306 "resource.name.startsWith('projects/_/buckets/example-bucket/objects/customer-a')"
307 title (Optional[str]): An optional short string that identifies the purpose of
308 the condition.
309 description (Optional[str]): Optional details about the purpose of the condition.
310
311 Raises:
312 TypeError: If any of the parameters are not of the expected types.
313 ValueError: If any of the parameters are not of the expected values.
314 """
315 self.expression = expression
316 self.title = title
317 self.description = description
318
319 @property
320 def expression(self):
321 """Returns the current condition expression.
322
323 Returns:
324 str: The current conditon expression.
325 """
326 return self._expression
327
328 @expression.setter
329 def expression(self, value):
330 """Updates the current condition expression.
331
332 Args:
333 value (str): The updated value of the condition expression.
334
335 Raises:
336 TypeError: If the value is not of type string.
337 """
338 if not isinstance(value, str):
339 raise TypeError("The provided expression is not a string.")
340 self._expression = value
341
342 @property
343 def title(self):
344 """Returns the current title.
345
346 Returns:
347 Optional[str]: The current title.
348 """
349 return self._title
350
351 @title.setter
352 def title(self, value):
353 """Updates the current title.
354
355 Args:
356 value (Optional[str]): The updated value of the title.
357
358 Raises:
359 TypeError: If the value is not of type string or None.
360 """
361 if not isinstance(value, str) and value is not None:
362 raise TypeError("The provided title is not a string or None.")
363 self._title = value
364
365 @property
366 def description(self):
367 """Returns the current description.
368
369 Returns:
370 Optional[str]: The current description.
371 """
372 return self._description
373
374 @description.setter
375 def description(self, value):
376 """Updates the current description.
377
378 Args:
379 value (Optional[str]): The updated value of the description.
380
381 Raises:
382 TypeError: If the value is not of type string or None.
383 """
384 if not isinstance(value, str) and value is not None:
385 raise TypeError("The provided description is not a string or None.")
386 self._description = value
387
388 def to_json(self):
389 """Generates the dictionary representation of the availability condition.
390 This uses the format expected by the Security Token Service API as documented in
391 `Defining a Credential Access Boundary`_.
392
393 .. _Defining a Credential Access Boundary:
394 https://cloud.google.com/iam/docs/downscoping-short-lived-credentials#define-boundary
395
396 Returns:
397 Mapping[str, str]: The availability condition represented in a dictionary
398 object.
399 """
400 json = {"expression": self.expression}
401 if self.title:
402 json["title"] = self.title
403 if self.description:
404 json["description"] = self.description
405 return json