1 /* 2 * Copyright 2016 Google LLC 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.cloud; 18 19 import static com.google.common.base.Preconditions.checkArgument; 20 import static com.google.common.base.Preconditions.checkNotNull; 21 22 import com.google.api.core.ApiFunction; 23 import com.google.api.core.InternalApi; 24 import com.google.common.base.MoreObjects; 25 import com.google.common.collect.ImmutableList; 26 import com.google.common.collect.ImmutableMap; 27 import com.google.common.collect.ImmutableSet; 28 import com.google.common.io.BaseEncoding; 29 import com.google.protobuf.ByteString; 30 import com.google.type.Expr; 31 import java.io.Serializable; 32 import java.util.ArrayList; 33 import java.util.Iterator; 34 import java.util.LinkedList; 35 import java.util.List; 36 import java.util.Map; 37 import java.util.Objects; 38 import java.util.Set; 39 40 /** 41 * Class for Identity and Access Management (IAM) policies. IAM policies are used to specify access 42 * settings for Cloud Platform resources. A policy is a list of bindings. A binding assigns a set of 43 * identities to a role, where the identities can be user accounts, Google groups, Google domains, 44 * and service accounts. A role is a named list of permissions defined by IAM. 45 * 46 * @see <a href="https://cloud.google.com/iam/docs/reference/rest/v1/Policy">Policy</a> 47 */ 48 public final class Policy implements Serializable { 49 50 private static final long serialVersionUID = -3348914530232544290L; 51 private final ImmutableList<Binding> bindingsList; 52 private final String etag; 53 private final int version; 54 55 /* 56 * Return true if Policy is version 3 OR bindings has a conditional binding. 57 * Return false if Policy is version 1 AND bindings does not have a conditional binding. 58 */ isConditional(int version, List<Binding> bindingsList)59 private static boolean isConditional(int version, List<Binding> bindingsList) { 60 for (Binding binding : bindingsList) { 61 if (binding.getCondition() != null) { 62 return true; 63 } 64 } 65 if (version == 3) { 66 return true; 67 } 68 return false; 69 } 70 71 public abstract static class Marshaller<T> { 72 73 @InternalApi("This class should only be extended within google-cloud-java") Marshaller()74 protected Marshaller() {} 75 76 protected static final ApiFunction<String, Identity> IDENTITY_VALUE_OF_FUNCTION = 77 new ApiFunction<String, Identity>() { 78 @Override 79 public Identity apply(String identityPb) { 80 return Identity.valueOf(identityPb); 81 } 82 }; 83 protected static final ApiFunction<Identity, String> IDENTITY_STR_VALUE_FUNCTION = 84 new ApiFunction<Identity, String>() { 85 @Override 86 public String apply(Identity identity) { 87 return identity.strValue(); 88 } 89 }; 90 fromPb(T policyPb)91 protected abstract Policy fromPb(T policyPb); 92 toPb(Policy policy)93 protected abstract T toPb(Policy policy); 94 } 95 96 public static class DefaultMarshaller extends Marshaller<com.google.iam.v1.Policy> { 97 98 @Override fromPb(com.google.iam.v1.Policy policyPb)99 protected Policy fromPb(com.google.iam.v1.Policy policyPb) { 100 ImmutableList.Builder<Binding> bindingsListBuilder = ImmutableList.builder(); 101 for (com.google.iam.v1.Binding bindingPb : policyPb.getBindingsList()) { 102 Binding.Builder convertedBinding = 103 Binding.newBuilder() 104 .setRole(bindingPb.getRole()) 105 .setMembers(bindingPb.getMembersList()); 106 if (bindingPb.hasCondition()) { 107 Expr expr = bindingPb.getCondition(); 108 convertedBinding.setCondition( 109 Condition.newBuilder() 110 .setTitle(expr.getTitle()) 111 .setDescription(expr.getDescription()) 112 .setExpression(expr.getExpression()) 113 .build()); 114 } 115 bindingsListBuilder.add(convertedBinding.build()); 116 } 117 return newBuilder() 118 .setBindings(bindingsListBuilder.build()) 119 .setEtag( 120 policyPb.getEtag().isEmpty() 121 ? null 122 : BaseEncoding.base64().encode(policyPb.getEtag().toByteArray())) 123 .setVersion(policyPb.getVersion()) 124 .build(); 125 } 126 127 @Override toPb(Policy policy)128 protected com.google.iam.v1.Policy toPb(Policy policy) { 129 com.google.iam.v1.Policy.Builder policyBuilder = com.google.iam.v1.Policy.newBuilder(); 130 List<com.google.iam.v1.Binding> bindingPbList = new LinkedList<>(); 131 for (Binding binding : policy.getBindingsList()) { 132 com.google.iam.v1.Binding.Builder bindingBuilder = com.google.iam.v1.Binding.newBuilder(); 133 bindingBuilder.setRole(binding.getRole()); 134 bindingBuilder.addAllMembers(binding.getMembers()); 135 if (binding.getCondition() != null) { 136 Condition condition = binding.getCondition(); 137 bindingBuilder.setCondition( 138 Expr.newBuilder() 139 .setTitle(condition.getTitle()) 140 .setDescription(condition.getDescription()) 141 .setExpression(condition.getExpression()) 142 .build()); 143 } 144 bindingPbList.add(bindingBuilder.build()); 145 } 146 policyBuilder.addAllBindings(bindingPbList); 147 if (policy.etag != null) { 148 policyBuilder.setEtag(ByteString.copyFrom(BaseEncoding.base64().decode(policy.etag))); 149 } 150 policyBuilder.setVersion(policy.version); 151 return policyBuilder.build(); 152 } 153 } 154 155 /** A builder for {@code Policy} objects. */ 156 public static class Builder { 157 private final List<Binding> bindingsList = new ArrayList<Binding>(); 158 private String etag; 159 private int version; 160 161 @InternalApi("This class should only be extended within google-cloud-java") Builder()162 protected Builder() {} 163 164 @InternalApi("This class should only be extended within google-cloud-java") Builder(Policy policy)165 protected Builder(Policy policy) { 166 bindingsList.addAll(policy.bindingsList); 167 setEtag(policy.etag); 168 setVersion(policy.version); 169 } 170 171 /** 172 * Replaces the builder's map of bindings with the given map of bindings. 173 * 174 * @throws NullPointerException if the given map is null or contains any null keys or values 175 * @throws IllegalArgumentException if any identities in the given map are null or if policy 176 * version is equal to 3 or has conditional bindings because conditional policies are not 177 * supported 178 */ setBindings(Map<Role, Set<Identity>> bindings)179 public final Builder setBindings(Map<Role, Set<Identity>> bindings) { 180 checkNotNull(bindings, "The provided map of bindings cannot be null."); 181 checkArgument( 182 !isConditional(this.version, this.bindingsList), 183 "setBindings() is only supported with version 1 policies and non-conditional policies"); 184 for (Map.Entry<Role, Set<Identity>> binding : bindings.entrySet()) { 185 checkNotNull(binding.getKey(), "The role cannot be null."); 186 Set<Identity> identities = binding.getValue(); 187 checkNotNull(identities, "A role cannot be assigned to a null set of identities."); 188 checkArgument(!identities.contains(null), "Null identities are not permitted."); 189 } 190 // convert into List format 191 this.bindingsList.clear(); 192 for (Map.Entry<Role, Set<Identity>> binding : bindings.entrySet()) { 193 Binding.Builder bindingBuilder = Binding.newBuilder(); 194 bindingBuilder.setRole(binding.getKey().getValue()); 195 ImmutableList.Builder<String> membersBuilder = ImmutableList.builder(); 196 for (Identity identity : binding.getValue()) { 197 membersBuilder.add(identity.strValue()); 198 } 199 bindingBuilder.setMembers(membersBuilder.build()); 200 this.bindingsList.add(bindingBuilder.build()); 201 } 202 return this; 203 } 204 205 /** 206 * Replaces the builder's List of bindings with the given List of Bindings. 207 * 208 * @throws NullPointerException if the given list is null, role is null, or contains any null 209 * members in bindings 210 */ setBindings(List<Binding> bindings)211 public final Builder setBindings(List<Binding> bindings) { 212 this.bindingsList.clear(); 213 for (Binding binding : bindings) { 214 Binding.Builder bindingBuilder = Binding.newBuilder(); 215 bindingBuilder.setMembers(ImmutableList.copyOf(binding.getMembers())); 216 bindingBuilder.setRole(binding.getRole()); 217 bindingBuilder.setCondition(binding.getCondition()); 218 this.bindingsList.add(bindingBuilder.build()); 219 } 220 return this; 221 } 222 223 /** 224 * Removes the role (and all identities associated with that role) from the policy. 225 * 226 * @throws IllegalArgumentException if policy version is equal to 3 or has conditional bindings 227 * because conditional policies are not supported 228 */ removeRole(Role role)229 public final Builder removeRole(Role role) { 230 checkArgument( 231 !isConditional(this.version, this.bindingsList), 232 "removeRole() is only supported with version 1 policies and non-conditional policies"); 233 Iterator iterator = bindingsList.iterator(); 234 235 while (iterator.hasNext()) { 236 Binding binding = (Binding) iterator.next(); 237 if (binding.getRole().equals(role.getValue())) { 238 iterator.remove(); 239 return this; 240 } 241 } 242 return this; 243 } 244 245 /** 246 * Adds one or more identities to the policy under the role specified. 247 * 248 * @throws NullPointerException if the role or any of the identities is null. 249 * @throws IllegalArgumentException if policy version is equal to 3 or has conditional bindings. 250 */ addIdentity(Role role, Identity first, Identity... others)251 public final Builder addIdentity(Role role, Identity first, Identity... others) { 252 checkArgument( 253 !isConditional(this.version, this.bindingsList), 254 "addIdentity() is only supported with version 1 policies and non-conditional policies"); 255 String nullIdentityMessage = "Null identities are not permitted."; 256 checkNotNull(first, nullIdentityMessage); 257 checkNotNull(others, nullIdentityMessage); 258 checkNotNull(role, "The role cannot be null."); 259 for (int i = 0; i < bindingsList.size(); i++) { 260 Binding binding = bindingsList.get(i); 261 if (binding.getRole().equals(role.getValue())) { 262 Binding.Builder bindingBuilder = binding.toBuilder(); 263 ImmutableSet.Builder<String> membersBuilder = ImmutableSet.builder(); 264 membersBuilder.addAll(binding.getMembers()); 265 membersBuilder.add(first.strValue()); 266 for (Identity identity : others) { 267 membersBuilder.add(identity.strValue()); 268 } 269 bindingBuilder.setMembers(membersBuilder.build()); 270 bindingsList.set(i, bindingBuilder.build()); 271 return this; 272 } 273 } 274 // Binding does not yet exist. 275 Binding.Builder bindingBuilder = Binding.newBuilder().setRole(role.getValue()); 276 ImmutableSet.Builder<String> membersBuilder = ImmutableSet.builder(); 277 membersBuilder.add(first.strValue()); 278 for (Identity identity : others) { 279 membersBuilder.add(identity.strValue()); 280 } 281 bindingBuilder.setMembers(membersBuilder.build()); 282 bindingsList.add(bindingBuilder.build()); 283 return this; 284 } 285 286 /** 287 * Removes one or more identities from an existing binding. Does nothing if the binding 288 * associated with the provided role doesn't exist. 289 * 290 * @throws IllegalArgumentException if policy version is equal to 3 or has conditional bindings 291 */ removeIdentity(Role role, Identity first, Identity... others)292 public final Builder removeIdentity(Role role, Identity first, Identity... others) { 293 checkArgument( 294 !isConditional(this.version, this.bindingsList), 295 "removeIdentity() is only supported with version 1 policies and non-conditional policies"); 296 String nullIdentityMessage = "Null identities are not permitted."; 297 checkNotNull(first, nullIdentityMessage); 298 checkNotNull(others, nullIdentityMessage); 299 checkNotNull(role, "The role cannot be null."); 300 for (int i = 0; i < bindingsList.size(); i++) { 301 Binding binding = bindingsList.get(i); 302 if (binding.getRole().equals(role.getValue())) { 303 Binding.Builder bindingBuilder = binding.toBuilder().removeMembers(first.strValue()); 304 for (Identity identity : others) { 305 bindingBuilder.removeMembers(identity.strValue()); 306 } 307 Binding updatedBindings = bindingBuilder.build(); 308 bindingsList.set(i, updatedBindings); 309 break; 310 } 311 } 312 313 Iterator iterator = bindingsList.iterator(); 314 while (iterator.hasNext()) { 315 Binding binding = (Binding) iterator.next(); 316 if (binding.getRole().equals(role.getValue()) && binding.getMembers().size() == 0) { 317 iterator.remove(); 318 break; 319 } 320 } 321 322 return this; 323 } 324 325 /** 326 * Sets the policy's etag. 327 * 328 * <p>Etags are used for optimistic concurrency control as a way to help prevent simultaneous 329 * updates of a policy from overwriting each other. It is strongly suggested that systems make 330 * use of the etag in the read-modify-write cycle to perform policy updates in order to avoid 331 * race conditions. An etag is returned in the response to getIamPolicy, and systems are 332 * expected to put that etag in the request to setIamPolicy to ensure that their change will be 333 * applied to the same version of the policy. If no etag is provided in the call to 334 * setIamPolicy, then the existing policy is overwritten blindly. 335 */ setEtag(String etag)336 public final Builder setEtag(String etag) { 337 this.etag = etag; 338 return this; 339 } 340 341 /** Sets the version of the policy. */ setVersion(int version)342 public final Builder setVersion(int version) { 343 this.version = version; 344 return this; 345 } 346 347 /** Creates a {@code Policy} object. */ build()348 public final Policy build() { 349 return new Policy(this); 350 } 351 } 352 Policy(Builder builder)353 private Policy(Builder builder) { 354 this.bindingsList = ImmutableList.copyOf(builder.bindingsList); 355 this.etag = builder.etag; 356 this.version = builder.version; 357 } 358 359 /** Returns a builder containing the properties of this IAM Policy. */ toBuilder()360 public Builder toBuilder() { 361 return new Builder(this); 362 } 363 364 /** 365 * Returns the map of bindings that comprises the policy. 366 * 367 * @throws IllegalArgumentException if policy version is equal to 3 or has conditional bindings 368 */ getBindings()369 public Map<Role, Set<Identity>> getBindings() { 370 checkArgument( 371 !isConditional(this.version, this.bindingsList), 372 "getBindings() is only supported with version 1 policies and non-conditional policies"); 373 ImmutableMap.Builder<Role, Set<Identity>> bindingsV1Builder = ImmutableMap.builder(); 374 for (Binding binding : bindingsList) { 375 ImmutableSet.Builder<Identity> identities = ImmutableSet.builder(); 376 for (String member : binding.getMembers()) { 377 identities.add(Identity.valueOf(member)); 378 } 379 bindingsV1Builder.put(Role.of(binding.getRole()), identities.build()); 380 } 381 return bindingsV1Builder.build(); 382 } 383 384 /** Returns the list of bindings that comprises the policy for version 3. */ getBindingsList()385 public ImmutableList<Binding> getBindingsList() { 386 return bindingsList; 387 } 388 389 /** 390 * Returns the policy's etag. 391 * 392 * <p>Etags are used for optimistic concurrency control as a way to help prevent simultaneous 393 * updates of a policy from overwriting each other. It is strongly suggested that systems make use 394 * of the etag in the read-modify-write cycle to perform policy updates in order to avoid race 395 * conditions. An etag is returned in the response to getIamPolicy, and systems are expected to 396 * put that etag in the request to setIamPolicy to ensure that their change will be applied to the 397 * same version of the policy. If no etag is provided in the call to setIamPolicy, then the 398 * existing policy is overwritten blindly. 399 */ getEtag()400 public String getEtag() { 401 return etag; 402 } 403 404 /** 405 * Returns the version of the policy. The default version is 0, meaning only the "owner", 406 * "editor", and "viewer" roles are permitted. If the version is 1, you may also use other roles. 407 */ getVersion()408 public int getVersion() { 409 return version; 410 } 411 412 @Override toString()413 public String toString() { 414 return MoreObjects.toStringHelper(this) 415 .add("bindings", bindingsList) 416 .add("etag", etag) 417 .add("version", version) 418 .toString(); 419 } 420 421 @Override hashCode()422 public int hashCode() { 423 return Objects.hash(getClass(), bindingsList, etag, version); 424 } 425 426 @Override equals(Object obj)427 public boolean equals(Object obj) { 428 if (obj == this) { 429 return true; 430 } 431 if (!(obj instanceof Policy)) { 432 return false; 433 } 434 Policy other = (Policy) obj; 435 if (bindingsList == null && other.getBindingsList() == null) { 436 return true; 437 } 438 if ((bindingsList == null && other.getBindingsList() != null) 439 || bindingsList != null && other.getBindingsList() == null 440 || bindingsList.size() != other.getBindingsList().size()) { 441 return false; 442 } 443 for (Binding binding : bindingsList) { 444 if (!other.getBindingsList().contains(binding)) { 445 return false; 446 } 447 } 448 return Objects.equals(etag, other.getEtag()) && version == other.getVersion(); 449 } 450 451 /** Returns a builder for {@code Policy} objects. */ newBuilder()452 public static Builder newBuilder() { 453 return new Builder(); 454 } 455 } 456