blob: 61b1d187669136b65b52228186afa4366f15f83e [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
15import pytest
16
17from google.auth import downscoped
18
19
20EXPRESSION = (
21 "resource.name.startsWith('projects/_/buckets/example-bucket/objects/customer-a')"
22)
23TITLE = "customer-a-objects"
24DESCRIPTION = (
25 "Condition to make permissions available for objects starting with customer-a"
26)
27AVAILABLE_RESOURCE = "//storage.googleapis.com/projects/_/buckets/example-bucket"
28AVAILABLE_PERMISSIONS = ["inRole:roles/storage.objectViewer"]
29
30OTHER_EXPRESSION = (
31 "resource.name.startsWith('projects/_/buckets/example-bucket/objects/customer-b')"
32)
33OTHER_TITLE = "customer-b-objects"
34OTHER_DESCRIPTION = (
35 "Condition to make permissions available for objects starting with customer-b"
36)
37OTHER_AVAILABLE_RESOURCE = "//storage.googleapis.com/projects/_/buckets/other-bucket"
38OTHER_AVAILABLE_PERMISSIONS = ["inRole:roles/storage.objectCreator"]
39
40
41def make_availability_condition(expression, title=None, description=None):
42 return downscoped.AvailabilityCondition(expression, title, description)
43
44
45def make_access_boundary_rule(
46 available_resource, available_permissions, availability_condition=None
47):
48 return downscoped.AccessBoundaryRule(
49 available_resource, available_permissions, availability_condition
50 )
51
52
53def make_credential_access_boundary(rules):
54 return downscoped.CredentialAccessBoundary(rules)
55
56
57class TestAvailabilityCondition(object):
58 def test_constructor(self):
59 availability_condition = make_availability_condition(
60 EXPRESSION, TITLE, DESCRIPTION
61 )
62
63 assert availability_condition.expression == EXPRESSION
64 assert availability_condition.title == TITLE
65 assert availability_condition.description == DESCRIPTION
66
67 def test_constructor_required_params_only(self):
68 availability_condition = make_availability_condition(EXPRESSION)
69
70 assert availability_condition.expression == EXPRESSION
71 assert availability_condition.title is None
72 assert availability_condition.description is None
73
74 def test_setters(self):
75 availability_condition = make_availability_condition(
76 EXPRESSION, TITLE, DESCRIPTION
77 )
78 availability_condition.expression = OTHER_EXPRESSION
79 availability_condition.title = OTHER_TITLE
80 availability_condition.description = OTHER_DESCRIPTION
81
82 assert availability_condition.expression == OTHER_EXPRESSION
83 assert availability_condition.title == OTHER_TITLE
84 assert availability_condition.description == OTHER_DESCRIPTION
85
86 def test_invalid_expression_type(self):
87 with pytest.raises(TypeError) as excinfo:
88 make_availability_condition([EXPRESSION], TITLE, DESCRIPTION)
89
90 assert excinfo.match("The provided expression is not a string.")
91
92 def test_invalid_title_type(self):
93 with pytest.raises(TypeError) as excinfo:
94 make_availability_condition(EXPRESSION, False, DESCRIPTION)
95
96 assert excinfo.match("The provided title is not a string or None.")
97
98 def test_invalid_description_type(self):
99 with pytest.raises(TypeError) as excinfo:
100 make_availability_condition(EXPRESSION, TITLE, False)
101
102 assert excinfo.match("The provided description is not a string or None.")
103
104 def test_to_json_required_params_only(self):
105 availability_condition = make_availability_condition(EXPRESSION)
106
107 assert availability_condition.to_json() == {"expression": EXPRESSION}
108
109 def test_to_json_(self):
110 availability_condition = make_availability_condition(
111 EXPRESSION, TITLE, DESCRIPTION
112 )
113
114 assert availability_condition.to_json() == {
115 "expression": EXPRESSION,
116 "title": TITLE,
117 "description": DESCRIPTION,
118 }
119
120
121class TestAccessBoundaryRule(object):
122 def test_constructor(self):
123 availability_condition = make_availability_condition(
124 EXPRESSION, TITLE, DESCRIPTION
125 )
126 access_boundary_rule = make_access_boundary_rule(
127 AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
128 )
129
130 assert access_boundary_rule.available_resource == AVAILABLE_RESOURCE
131 assert access_boundary_rule.available_permissions == tuple(
132 AVAILABLE_PERMISSIONS
133 )
134 assert access_boundary_rule.availability_condition == availability_condition
135
136 def test_constructor_required_params_only(self):
137 access_boundary_rule = make_access_boundary_rule(
138 AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS
139 )
140
141 assert access_boundary_rule.available_resource == AVAILABLE_RESOURCE
142 assert access_boundary_rule.available_permissions == tuple(
143 AVAILABLE_PERMISSIONS
144 )
145 assert access_boundary_rule.availability_condition is None
146
147 def test_setters(self):
148 availability_condition = make_availability_condition(
149 EXPRESSION, TITLE, DESCRIPTION
150 )
151 other_availability_condition = make_availability_condition(
152 OTHER_EXPRESSION, OTHER_TITLE, OTHER_DESCRIPTION
153 )
154 access_boundary_rule = make_access_boundary_rule(
155 AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
156 )
157 access_boundary_rule.available_resource = OTHER_AVAILABLE_RESOURCE
158 access_boundary_rule.available_permissions = OTHER_AVAILABLE_PERMISSIONS
159 access_boundary_rule.availability_condition = other_availability_condition
160
161 assert access_boundary_rule.available_resource == OTHER_AVAILABLE_RESOURCE
162 assert access_boundary_rule.available_permissions == tuple(
163 OTHER_AVAILABLE_PERMISSIONS
164 )
165 assert (
166 access_boundary_rule.availability_condition == other_availability_condition
167 )
168
169 def test_invalid_available_resource_type(self):
170 availability_condition = make_availability_condition(
171 EXPRESSION, TITLE, DESCRIPTION
172 )
173 with pytest.raises(TypeError) as excinfo:
174 make_access_boundary_rule(
175 None, AVAILABLE_PERMISSIONS, availability_condition
176 )
177
178 assert excinfo.match("The provided available_resource is not a string.")
179
180 def test_invalid_available_permissions_type(self):
181 availability_condition = make_availability_condition(
182 EXPRESSION, TITLE, DESCRIPTION
183 )
184 with pytest.raises(TypeError) as excinfo:
185 make_access_boundary_rule(
186 AVAILABLE_RESOURCE, [0, 1, 2], availability_condition
187 )
188
189 assert excinfo.match(
190 "Provided available_permissions are not a list of strings."
191 )
192
193 def test_invalid_available_permissions_value(self):
194 availability_condition = make_availability_condition(
195 EXPRESSION, TITLE, DESCRIPTION
196 )
197 with pytest.raises(ValueError) as excinfo:
198 make_access_boundary_rule(
199 AVAILABLE_RESOURCE,
200 ["roles/storage.objectViewer"],
201 availability_condition,
202 )
203
204 assert excinfo.match("available_permissions must be prefixed with 'inRole:'.")
205
206 def test_invalid_availability_condition_type(self):
207 with pytest.raises(TypeError) as excinfo:
208 make_access_boundary_rule(
209 AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, {"foo": "bar"}
210 )
211
212 assert excinfo.match(
213 "The provided availability_condition is not a 'google.auth.downscoped.AvailabilityCondition' or None."
214 )
215
216 def test_to_json(self):
217 availability_condition = make_availability_condition(
218 EXPRESSION, TITLE, DESCRIPTION
219 )
220 access_boundary_rule = make_access_boundary_rule(
221 AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
222 )
223
224 assert access_boundary_rule.to_json() == {
225 "availablePermissions": AVAILABLE_PERMISSIONS,
226 "availableResource": AVAILABLE_RESOURCE,
227 "availabilityCondition": {
228 "expression": EXPRESSION,
229 "title": TITLE,
230 "description": DESCRIPTION,
231 },
232 }
233
234 def test_to_json_required_params_only(self):
235 access_boundary_rule = make_access_boundary_rule(
236 AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS
237 )
238
239 assert access_boundary_rule.to_json() == {
240 "availablePermissions": AVAILABLE_PERMISSIONS,
241 "availableResource": AVAILABLE_RESOURCE,
242 }
243
244
245class TestCredentialAccessBoundary(object):
246 def test_constructor(self):
247 availability_condition = make_availability_condition(
248 EXPRESSION, TITLE, DESCRIPTION
249 )
250 access_boundary_rule = make_access_boundary_rule(
251 AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
252 )
253 rules = [access_boundary_rule]
254 credential_access_boundary = make_credential_access_boundary(rules)
255
256 assert credential_access_boundary.rules == tuple(rules)
257
258 def test_setters(self):
259 availability_condition = make_availability_condition(
260 EXPRESSION, TITLE, DESCRIPTION
261 )
262 access_boundary_rule = make_access_boundary_rule(
263 AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
264 )
265 rules = [access_boundary_rule]
266 other_availability_condition = make_availability_condition(
267 OTHER_EXPRESSION, OTHER_TITLE, OTHER_DESCRIPTION
268 )
269 other_access_boundary_rule = make_access_boundary_rule(
270 OTHER_AVAILABLE_RESOURCE,
271 OTHER_AVAILABLE_PERMISSIONS,
272 other_availability_condition,
273 )
274 other_rules = [other_access_boundary_rule]
275 credential_access_boundary = make_credential_access_boundary(rules)
276 credential_access_boundary.rules = other_rules
277
278 assert credential_access_boundary.rules == tuple(other_rules)
279
280 def test_add_rule(self):
281 availability_condition = make_availability_condition(
282 EXPRESSION, TITLE, DESCRIPTION
283 )
284 access_boundary_rule = make_access_boundary_rule(
285 AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
286 )
287 rules = [access_boundary_rule] * 9
288 credential_access_boundary = make_credential_access_boundary(rules)
289
290 # Add one more rule. This should not raise an error.
291 additional_access_boundary_rule = make_access_boundary_rule(
292 OTHER_AVAILABLE_RESOURCE, OTHER_AVAILABLE_PERMISSIONS
293 )
294 credential_access_boundary.add_rule(additional_access_boundary_rule)
295
296 assert len(credential_access_boundary.rules) == 10
297 assert credential_access_boundary.rules[9] == additional_access_boundary_rule
298
299 def test_add_rule_invalid_value(self):
300 availability_condition = make_availability_condition(
301 EXPRESSION, TITLE, DESCRIPTION
302 )
303 access_boundary_rule = make_access_boundary_rule(
304 AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
305 )
306 rules = [access_boundary_rule] * 10
307 credential_access_boundary = make_credential_access_boundary(rules)
308
309 # Add one more rule to exceed maximum allowed rules.
310 with pytest.raises(ValueError) as excinfo:
311 credential_access_boundary.add_rule(access_boundary_rule)
312
313 assert excinfo.match(
314 "Credential access boundary rules can have a maximum of 10 rules."
315 )
316 assert len(credential_access_boundary.rules) == 10
317
318 def test_add_rule_invalid_type(self):
319 availability_condition = make_availability_condition(
320 EXPRESSION, TITLE, DESCRIPTION
321 )
322 access_boundary_rule = make_access_boundary_rule(
323 AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
324 )
325 rules = [access_boundary_rule]
326 credential_access_boundary = make_credential_access_boundary(rules)
327
328 # Add an invalid rule to exceed maximum allowed rules.
329 with pytest.raises(TypeError) as excinfo:
330 credential_access_boundary.add_rule("invalid")
331
332 assert excinfo.match(
333 "The provided rule does not contain a valid 'google.auth.downscoped.AccessBoundaryRule'."
334 )
335 assert len(credential_access_boundary.rules) == 1
336 assert credential_access_boundary.rules[0] == access_boundary_rule
337
338 def test_invalid_rules_type(self):
339 with pytest.raises(TypeError) as excinfo:
340 make_credential_access_boundary(["invalid"])
341
342 assert excinfo.match(
343 "List of rules provided do not contain a valid 'google.auth.downscoped.AccessBoundaryRule'."
344 )
345
346 def test_invalid_rules_value(self):
347 availability_condition = make_availability_condition(
348 EXPRESSION, TITLE, DESCRIPTION
349 )
350 access_boundary_rule = make_access_boundary_rule(
351 AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
352 )
353 too_many_rules = [access_boundary_rule] * 11
354 with pytest.raises(ValueError) as excinfo:
355 make_credential_access_boundary(too_many_rules)
356
357 assert excinfo.match(
358 "Credential access boundary rules can have a maximum of 10 rules."
359 )
360
361 def test_to_json(self):
362 availability_condition = make_availability_condition(
363 EXPRESSION, TITLE, DESCRIPTION
364 )
365 access_boundary_rule = make_access_boundary_rule(
366 AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
367 )
368 rules = [access_boundary_rule]
369 credential_access_boundary = make_credential_access_boundary(rules)
370
371 assert credential_access_boundary.to_json() == {
372 "accessBoundary": {
373 "accessBoundaryRules": [
374 {
375 "availablePermissions": AVAILABLE_PERMISSIONS,
376 "availableResource": AVAILABLE_RESOURCE,
377 "availabilityCondition": {
378 "expression": EXPRESSION,
379 "title": TITLE,
380 "description": DESCRIPTION,
381 },
382 }
383 ]
384 }
385 }