Skip to content

Commit aa7ede1

Browse files
lsiracelharo
andauthored
feat: downscoping with credential access boundaries (#702)
* feat: adds CAB rules classes (#687) * feat: adds CAB rules classes * fix: copyright * fix: revert pom * fix: review * fix: bad link * fix: more null and empty checks * fix: expand javadoc * fix: split null/empty checks * fix: use checkNotNull * feat: downscoping with credential access boundaries (#691) * feat: downscoping with credential access boundaries * fix: rename RefreshableOAuth2Credentials to OAuth2CredentialsWithRefresh * fix: review nits * test: adds integration tests for downscoping with credential access boundaries * fix: use source credential expiration when STS does not return expires_in * fix: require an expiration time to be passed in the AccessToken consumed by OAuth2CredentialsWithRefresh Co-authored-by: Elliotte Rusty Harold <elharo@users.noreply.github.com>
1 parent 5f923cd commit aa7ede1

13 files changed

+1797
-21
lines changed
Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
/*
2+
* Copyright 2021 Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
*
15+
* * Neither the name of Google LLC nor the names of its
16+
* contributors may be used to endorse or promote products derived from
17+
* this software without specific prior written permission.
18+
*
19+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+
*/
31+
32+
package com.google.auth.oauth2;
33+
34+
import static com.google.common.base.Preconditions.checkArgument;
35+
import static com.google.common.base.Preconditions.checkNotNull;
36+
37+
import com.google.api.client.json.GenericJson;
38+
import java.util.ArrayList;
39+
import java.util.List;
40+
import javax.annotation.Nullable;
41+
42+
/**
43+
* Defines an upper bound of permissions available for a GCP credential via {@link
44+
* AccessBoundaryRule}s.
45+
*
46+
* <p>See <a href='https://cloud.google.com/iam/docs/downscoping-short-lived-credentials'>for more
47+
* information.</a>
48+
*/
49+
public final class CredentialAccessBoundary {
50+
51+
private static final int RULES_SIZE_LIMIT = 10;
52+
53+
private final List<AccessBoundaryRule> accessBoundaryRules;
54+
55+
CredentialAccessBoundary(List<AccessBoundaryRule> accessBoundaryRules) {
56+
checkNotNull(accessBoundaryRules);
57+
checkArgument(
58+
!accessBoundaryRules.isEmpty(), "At least one access boundary rule must be provided.");
59+
checkArgument(
60+
accessBoundaryRules.size() < RULES_SIZE_LIMIT,
61+
String.format(
62+
"The provided list has more than %s access boundary rules.", RULES_SIZE_LIMIT));
63+
this.accessBoundaryRules = accessBoundaryRules;
64+
}
65+
66+
/**
67+
* Internal method that returns the JSON string representation of the credential access boundary.
68+
*/
69+
String toJson() {
70+
List<GenericJson> rules = new ArrayList<>();
71+
for (AccessBoundaryRule rule : accessBoundaryRules) {
72+
GenericJson ruleJson = new GenericJson();
73+
ruleJson.setFactory(OAuth2Utils.JSON_FACTORY);
74+
75+
ruleJson.put("availableResource", rule.getAvailableResource());
76+
ruleJson.put("availablePermissions", rule.getAvailablePermissions());
77+
78+
AccessBoundaryRule.AvailabilityCondition availabilityCondition =
79+
rule.getAvailabilityCondition();
80+
if (availabilityCondition != null) {
81+
GenericJson availabilityConditionJson = new GenericJson();
82+
availabilityConditionJson.setFactory(OAuth2Utils.JSON_FACTORY);
83+
84+
availabilityConditionJson.put("expression", availabilityCondition.getExpression());
85+
if (availabilityCondition.getTitle() != null) {
86+
availabilityConditionJson.put("title", availabilityCondition.getTitle());
87+
}
88+
if (availabilityCondition.getDescription() != null) {
89+
availabilityConditionJson.put("description", availabilityCondition.getDescription());
90+
}
91+
92+
ruleJson.put("availabilityCondition", availabilityConditionJson);
93+
}
94+
rules.add(ruleJson);
95+
}
96+
GenericJson accessBoundaryRulesJson = new GenericJson();
97+
accessBoundaryRulesJson.setFactory(OAuth2Utils.JSON_FACTORY);
98+
accessBoundaryRulesJson.put("accessBoundaryRules", rules);
99+
100+
GenericJson json = new GenericJson();
101+
json.setFactory(OAuth2Utils.JSON_FACTORY);
102+
json.put("accessBoundary", accessBoundaryRulesJson);
103+
return json.toString();
104+
}
105+
106+
public List<AccessBoundaryRule> getAccessBoundaryRules() {
107+
return accessBoundaryRules;
108+
}
109+
110+
public static Builder newBuilder() {
111+
return new Builder();
112+
}
113+
114+
public static class Builder {
115+
private List<AccessBoundaryRule> accessBoundaryRules;
116+
117+
private Builder() {}
118+
119+
/**
120+
* Sets the list of {@link AccessBoundaryRule}'s.
121+
*
122+
* <p>This list must not exceed 10 rules.
123+
*/
124+
public Builder setRules(List<AccessBoundaryRule> rule) {
125+
accessBoundaryRules = new ArrayList<>(checkNotNull(rule));
126+
return this;
127+
}
128+
129+
public CredentialAccessBoundary.Builder addRule(AccessBoundaryRule rule) {
130+
if (accessBoundaryRules == null) {
131+
accessBoundaryRules = new ArrayList<>();
132+
}
133+
accessBoundaryRules.add(checkNotNull(rule));
134+
return this;
135+
}
136+
137+
public CredentialAccessBoundary build() {
138+
return new CredentialAccessBoundary(accessBoundaryRules);
139+
}
140+
}
141+
142+
/**
143+
* Defines an upper bound of permissions on a particular resource.
144+
*
145+
* <p>The following snippet shows an AccessBoundaryRule that applies to the Cloud Storage bucket
146+
* bucket-one to set the upper bound of permissions to those defined by the
147+
* roles/storage.objectViewer role.
148+
*
149+
* <pre><code>
150+
* AccessBoundaryRule rule = AccessBoundaryRule.newBuilder()
151+
* .setAvailableResource("//storage.googleapis.com/projects/_/buckets/bucket-one")
152+
* .addAvailablePermission("inRole:roles/storage.objectViewer")
153+
* .build();
154+
* </code></pre>
155+
*/
156+
public static final class AccessBoundaryRule {
157+
158+
private final String availableResource;
159+
private final List<String> availablePermissions;
160+
161+
@Nullable private final AvailabilityCondition availabilityCondition;
162+
163+
AccessBoundaryRule(
164+
String availableResource,
165+
List<String> availablePermissions,
166+
@Nullable AvailabilityCondition availabilityCondition) {
167+
this.availableResource = checkNotNull(availableResource);
168+
this.availablePermissions = new ArrayList<>(checkNotNull(availablePermissions));
169+
this.availabilityCondition = availabilityCondition;
170+
171+
checkArgument(!availableResource.isEmpty(), "The provided availableResource is empty.");
172+
checkArgument(
173+
!availablePermissions.isEmpty(), "The list of provided availablePermissions is empty.");
174+
for (String permission : availablePermissions) {
175+
if (permission == null) {
176+
throw new IllegalArgumentException("One of the provided available permissions is null.");
177+
}
178+
if (permission.isEmpty()) {
179+
throw new IllegalArgumentException("One of the provided available permissions is empty.");
180+
}
181+
}
182+
}
183+
184+
public String getAvailableResource() {
185+
return availableResource;
186+
}
187+
188+
public List<String> getAvailablePermissions() {
189+
return availablePermissions;
190+
}
191+
192+
@Nullable
193+
public AvailabilityCondition getAvailabilityCondition() {
194+
return availabilityCondition;
195+
}
196+
197+
public static Builder newBuilder() {
198+
return new Builder();
199+
}
200+
201+
public static class Builder {
202+
private String availableResource;
203+
private List<String> availablePermissions;
204+
205+
@Nullable private AvailabilityCondition availabilityCondition;
206+
207+
private Builder() {}
208+
209+
/**
210+
* Sets the available resource, which is the full resource name of the GCP resource to allow
211+
* access to.
212+
*
213+
* <p>For example: "//storage.googleapis.com/projects/_/buckets/example".
214+
*/
215+
public Builder setAvailableResource(String availableResource) {
216+
this.availableResource = availableResource;
217+
return this;
218+
}
219+
220+
/**
221+
* Sets the list of permissions that can be used on the resource. This should be a list of IAM
222+
* roles prefixed by inRole.
223+
*
224+
* <p>For example: {"inRole:roles/storage.objectViewer"}.
225+
*/
226+
public Builder setAvailablePermissions(List<String> availablePermissions) {
227+
this.availablePermissions = new ArrayList<>(checkNotNull(availablePermissions));
228+
return this;
229+
}
230+
231+
/**
232+
* Adds a permission that can be used on the resource. This should be an IAM role prefixed by
233+
* inRole.
234+
*
235+
* <p>For example: "inRole:roles/storage.objectViewer".
236+
*/
237+
public Builder addAvailablePermission(String availablePermission) {
238+
if (availablePermissions == null) {
239+
availablePermissions = new ArrayList<>();
240+
}
241+
availablePermissions.add(availablePermission);
242+
return this;
243+
}
244+
245+
/**
246+
* Sets the availability condition which is an IAM condition that defines constraints to apply
247+
* to the token expressed in CEL format.
248+
*/
249+
public Builder setAvailabilityCondition(AvailabilityCondition availabilityCondition) {
250+
this.availabilityCondition = availabilityCondition;
251+
return this;
252+
}
253+
254+
public AccessBoundaryRule build() {
255+
return new AccessBoundaryRule(
256+
availableResource, availablePermissions, availabilityCondition);
257+
}
258+
}
259+
260+
/**
261+
* An optional condition that can be used as part of a {@link AccessBoundaryRule} to further
262+
* restrict permissions.
263+
*
264+
* <p>For example, you can define an AvailabilityCondition that applies to a set of Cloud
265+
* Storage objects whose names start with auth:
266+
*
267+
* <pre><code>
268+
* AvailabilityCondition availabilityCondition = AvailabilityCondition.newBuilder()
269+
* .setExpression("resource.name.startsWith('projects/_/buckets/bucket-123/objects/auth')")
270+
* .build();
271+
* </code></pre>
272+
*/
273+
public static final class AvailabilityCondition {
274+
private final String expression;
275+
276+
@Nullable private final String title;
277+
@Nullable private final String description;
278+
279+
AvailabilityCondition(
280+
String expression, @Nullable String title, @Nullable String description) {
281+
this.expression = checkNotNull(expression);
282+
this.title = title;
283+
this.description = description;
284+
285+
checkArgument(!expression.isEmpty(), "The provided expression is empty.");
286+
}
287+
288+
public String getExpression() {
289+
return expression;
290+
}
291+
292+
@Nullable
293+
public String getTitle() {
294+
return title;
295+
}
296+
297+
@Nullable
298+
public String getDescription() {
299+
return description;
300+
}
301+
302+
public static Builder newBuilder() {
303+
return new Builder();
304+
}
305+
306+
public static final class Builder {
307+
private String expression;
308+
309+
@Nullable private String title;
310+
@Nullable private String description;
311+
312+
private Builder() {}
313+
314+
/**
315+
* Sets the required expression which must be defined in Common Expression Language (CEL)
316+
* format.
317+
*
318+
* <p>This expression specifies the Cloud Storage object where permissions are available.
319+
* See <a href='https://cloud.google.com/iam/docs/conditions-overview#cel'>for more
320+
* information.</a>
321+
*/
322+
public Builder setExpression(String expression) {
323+
this.expression = expression;
324+
return this;
325+
}
326+
327+
/** Sets the optional title that identifies the purpose of the condition. */
328+
public Builder setTitle(String title) {
329+
this.title = title;
330+
return this;
331+
}
332+
333+
/** Sets the description that details the purpose of the condition. */
334+
public Builder setDescription(String description) {
335+
this.description = description;
336+
return this;
337+
}
338+
339+
public AvailabilityCondition build() {
340+
return new AvailabilityCondition(expression, title, description);
341+
}
342+
}
343+
}
344+
}
345+
}

0 commit comments

Comments
 (0)