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 com.google.errorprone.annotations.CanIgnoreReturnValue; 39 import java.util.ArrayList; 40 import java.util.List; 41 import javax.annotation.Nullable; 42 43 /** 44 * Defines an upper bound of permissions available for a GCP credential via {@link 45 * AccessBoundaryRule}s. 46 * 47 * <p>See <a href='https://cloud.google.com/iam/docs/downscoping-short-lived-credentials'>for more 48 * information.</a> 49 */ 50 public final class CredentialAccessBoundary { 51 52 private static final int RULES_SIZE_LIMIT = 10; 53 54 private final List<AccessBoundaryRule> accessBoundaryRules; 55 CredentialAccessBoundary(List<AccessBoundaryRule> accessBoundaryRules)56 CredentialAccessBoundary(List<AccessBoundaryRule> accessBoundaryRules) { 57 checkNotNull(accessBoundaryRules); 58 checkArgument( 59 !accessBoundaryRules.isEmpty(), "At least one access boundary rule must be provided."); 60 checkArgument( 61 accessBoundaryRules.size() < RULES_SIZE_LIMIT, 62 String.format( 63 "The provided list has more than %s access boundary rules.", RULES_SIZE_LIMIT)); 64 this.accessBoundaryRules = accessBoundaryRules; 65 } 66 67 /** 68 * Internal method that returns the JSON string representation of the credential access boundary. 69 */ 70 String toJson() { 71 List<GenericJson> rules = new ArrayList<>(); 72 for (AccessBoundaryRule rule : accessBoundaryRules) { 73 GenericJson ruleJson = new GenericJson(); 74 ruleJson.setFactory(OAuth2Utils.JSON_FACTORY); 75 76 ruleJson.put("availableResource", rule.getAvailableResource()); 77 ruleJson.put("availablePermissions", rule.getAvailablePermissions()); 78 79 AccessBoundaryRule.AvailabilityCondition availabilityCondition = 80 rule.getAvailabilityCondition(); 81 if (availabilityCondition != null) { 82 GenericJson availabilityConditionJson = new GenericJson(); 83 availabilityConditionJson.setFactory(OAuth2Utils.JSON_FACTORY); 84 85 availabilityConditionJson.put("expression", availabilityCondition.getExpression()); 86 if (availabilityCondition.getTitle() != null) { 87 availabilityConditionJson.put("title", availabilityCondition.getTitle()); 88 } 89 if (availabilityCondition.getDescription() != null) { 90 availabilityConditionJson.put("description", availabilityCondition.getDescription()); 91 } 92 93 ruleJson.put("availabilityCondition", availabilityConditionJson); 94 } 95 rules.add(ruleJson); 96 } 97 GenericJson accessBoundaryRulesJson = new GenericJson(); 98 accessBoundaryRulesJson.setFactory(OAuth2Utils.JSON_FACTORY); 99 accessBoundaryRulesJson.put("accessBoundaryRules", rules); 100 101 GenericJson json = new GenericJson(); 102 json.setFactory(OAuth2Utils.JSON_FACTORY); 103 json.put("accessBoundary", accessBoundaryRulesJson); 104 return json.toString(); 105 } 106 107 public List<AccessBoundaryRule> getAccessBoundaryRules() { 108 return accessBoundaryRules; 109 } 110 111 public static Builder newBuilder() { 112 return new Builder(); 113 } 114 115 public static class Builder { 116 private List<AccessBoundaryRule> accessBoundaryRules; 117 118 private Builder() {} 119 120 /** 121 * Sets the list of {@link AccessBoundaryRule}'s. 122 * 123 * <p>This list must not exceed 10 rules. 124 * 125 * @param rule the collection of rules to be set, should not be null 126 * @return this {@code Builder} object 127 */ 128 @CanIgnoreReturnValue 129 public Builder setRules(List<AccessBoundaryRule> rule) { 130 accessBoundaryRules = new ArrayList<>(checkNotNull(rule)); 131 return this; 132 } 133 134 @CanIgnoreReturnValue 135 public CredentialAccessBoundary.Builder addRule(AccessBoundaryRule rule) { 136 if (accessBoundaryRules == null) { 137 accessBoundaryRules = new ArrayList<>(); 138 } 139 accessBoundaryRules.add(checkNotNull(rule)); 140 return this; 141 } 142 143 public CredentialAccessBoundary build() { 144 return new CredentialAccessBoundary(accessBoundaryRules); 145 } 146 } 147 148 /** 149 * Defines an upper bound of permissions on a particular resource. 150 * 151 * <p>The following snippet shows an AccessBoundaryRule that applies to the Cloud Storage bucket 152 * bucket-one to set the upper bound of permissions to those defined by the 153 * roles/storage.objectViewer role. 154 * 155 * <pre><code> 156 * AccessBoundaryRule rule = AccessBoundaryRule.newBuilder() 157 * .setAvailableResource("//storage.googleapis.com/projects/_/buckets/bucket-one") 158 * .addAvailablePermission("inRole:roles/storage.objectViewer") 159 * .build(); 160 * </code></pre> 161 */ 162 public static final class AccessBoundaryRule { 163 164 private final String availableResource; 165 private final List<String> availablePermissions; 166 167 @Nullable private final AvailabilityCondition availabilityCondition; 168 169 AccessBoundaryRule( 170 String availableResource, 171 List<String> availablePermissions, 172 @Nullable AvailabilityCondition availabilityCondition) { 173 this.availableResource = checkNotNull(availableResource); 174 this.availablePermissions = new ArrayList<>(checkNotNull(availablePermissions)); 175 this.availabilityCondition = availabilityCondition; 176 177 checkArgument(!availableResource.isEmpty(), "The provided availableResource is empty."); 178 checkArgument( 179 !availablePermissions.isEmpty(), "The list of provided availablePermissions is empty."); 180 for (String permission : availablePermissions) { 181 if (permission == null) { 182 throw new IllegalArgumentException("One of the provided available permissions is null."); 183 } 184 if (permission.isEmpty()) { 185 throw new IllegalArgumentException("One of the provided available permissions is empty."); 186 } 187 } 188 } 189 190 public String getAvailableResource() { 191 return availableResource; 192 } 193 194 public List<String> getAvailablePermissions() { 195 return availablePermissions; 196 } 197 198 @Nullable 199 public AvailabilityCondition getAvailabilityCondition() { 200 return availabilityCondition; 201 } 202 203 public static Builder newBuilder() { 204 return new Builder(); 205 } 206 207 public static class Builder { 208 private String availableResource; 209 private List<String> availablePermissions; 210 211 @Nullable private AvailabilityCondition availabilityCondition; 212 213 private Builder() {} 214 215 /** 216 * Sets the available resource, which is the full resource name of the GCP resource to allow 217 * access to. 218 * 219 * <p>For example: "//storage.googleapis.com/projects/_/buckets/example". 220 * 221 * @param availableResource the resource name to set 222 * @return this {@code Builder} object 223 */ 224 @CanIgnoreReturnValue 225 public Builder setAvailableResource(String availableResource) { 226 this.availableResource = availableResource; 227 return this; 228 } 229 230 /** 231 * Sets the list of permissions that can be used on the resource. This should be a list of IAM 232 * roles prefixed by inRole. 233 * 234 * <p>For example: {"inRole:roles/storage.objectViewer"}. 235 * 236 * @param availablePermissions the collection of permissions to set, should not be null 237 * @return this {@code Builder} object 238 */ 239 @CanIgnoreReturnValue 240 public Builder setAvailablePermissions(List<String> availablePermissions) { 241 this.availablePermissions = new ArrayList<>(checkNotNull(availablePermissions)); 242 return this; 243 } 244 245 /** 246 * Adds a permission that can be used on the resource. This should be an IAM role prefixed by 247 * inRole. 248 * 249 * <p>For example: "inRole:roles/storage.objectViewer". 250 * 251 * @param availablePermission a permission to add, should not be null 252 * @return this {@code Builder} object 253 */ 254 public Builder addAvailablePermission(String availablePermission) { 255 if (availablePermissions == null) { 256 availablePermissions = new ArrayList<>(); 257 } 258 availablePermissions.add(availablePermission); 259 return this; 260 } 261 262 /** 263 * Sets the availability condition which is an IAM condition that defines constraints to apply 264 * to the token expressed in CEL format. 265 * 266 * @param availabilityCondition the {@code AvailabilityCondition} to set 267 * @return this {@code Builder} object 268 */ 269 @CanIgnoreReturnValue 270 public Builder setAvailabilityCondition(AvailabilityCondition availabilityCondition) { 271 this.availabilityCondition = availabilityCondition; 272 return this; 273 } 274 275 public AccessBoundaryRule build() { 276 return new AccessBoundaryRule( 277 availableResource, availablePermissions, availabilityCondition); 278 } 279 } 280 281 /** 282 * An optional condition that can be used as part of a {@link AccessBoundaryRule} to further 283 * restrict permissions. 284 * 285 * <p>For example, you can define an AvailabilityCondition that applies to a set of Cloud 286 * Storage objects whose names start with auth: 287 * 288 * <pre><code> 289 * AvailabilityCondition availabilityCondition = AvailabilityCondition.newBuilder() 290 * .setExpression("resource.name.startsWith('projects/_/buckets/bucket-123/objects/auth')") 291 * .build(); 292 * </code></pre> 293 */ 294 public static final class AvailabilityCondition { 295 private final String expression; 296 297 @Nullable private final String title; 298 @Nullable private final String description; 299 300 AvailabilityCondition( 301 String expression, @Nullable String title, @Nullable String description) { 302 this.expression = checkNotNull(expression); 303 this.title = title; 304 this.description = description; 305 306 checkArgument(!expression.isEmpty(), "The provided expression is empty."); 307 } 308 309 public String getExpression() { 310 return expression; 311 } 312 313 @Nullable 314 public String getTitle() { 315 return title; 316 } 317 318 @Nullable 319 public String getDescription() { 320 return description; 321 } 322 323 public static Builder newBuilder() { 324 return new Builder(); 325 } 326 327 public static final class Builder { 328 private String expression; 329 330 @Nullable private String title; 331 @Nullable private String description; 332 333 private Builder() {} 334 335 /** 336 * Sets the required expression which must be defined in Common Expression Language (CEL) 337 * format. 338 * 339 * <p>This expression specifies the Cloud Storage object where permissions are available. 340 * See <a href='https://cloud.google.com/iam/docs/conditions-overview#cel'>for more 341 * information.</a> 342 * 343 * @param expression the expression to set 344 * @return this {@code Builder} object 345 */ 346 @CanIgnoreReturnValue 347 public Builder setExpression(String expression) { 348 this.expression = expression; 349 return this; 350 } 351 352 /** 353 * Sets the optional title that identifies the purpose of the condition. 354 * 355 * @param title the title to set 356 * @return this {@code Builder} object 357 */ 358 @CanIgnoreReturnValue 359 public Builder setTitle(String title) { 360 this.title = title; 361 return this; 362 } 363 364 /** 365 * Sets the description that details the purpose of the condition. 366 * 367 * @param description the description to set 368 * @return this {@code Builder} object 369 */ 370 @CanIgnoreReturnValue 371 public Builder setDescription(String description) { 372 this.description = description; 373 return this; 374 } 375 376 public AvailabilityCondition build() { 377 return new AvailabilityCondition(expression, title, description); 378 } 379 } 380 } 381 } 382 } 383