diff --git a/gcloud-java-resourcemanager/src/main/java/com/google/gcloud/resourcemanager/ResourceManager.java b/gcloud-java-resourcemanager/src/main/java/com/google/gcloud/resourcemanager/ResourceManager.java index f9ddf6872414..de5c430687ec 100644 --- a/gcloud-java-resourcemanager/src/main/java/com/google/gcloud/resourcemanager/ResourceManager.java +++ b/gcloud-java-resourcemanager/src/main/java/com/google/gcloud/resourcemanager/ResourceManager.java @@ -22,6 +22,7 @@ import com.google.gcloud.Service; import com.google.gcloud.spi.ResourceManagerRpc; +import java.util.List; import java.util.Set; /** @@ -168,7 +169,34 @@ public static ProjectListOption fields(ProjectField... fields) { } /** - * Creates a new project. + * The permissions associated with a Google Cloud project. These values can be used when calling + * {@link #testPermissions}. + */ + public enum Permission { + CREATE("create"), + DELETE("delete"), + GET("get"), + GET_POLICY("getIamPolicy"), + LIST("list"), + OWN("own"), + REPLACE("update"), + REPLACE_POLICY("setIamPolicy"), + SET_BILLING("setBillingAccount"), + UNDELETE("undelete"); + + private final String strValue; + + Permission(String suffix) { + this.strValue = "resourcemanager.projects." + suffix; + } + + String strValue() { + return strValue; + } + } + + /** + * Create a new project. * *

Initially, the project resource is owned by its creator exclusively. The creator can later * grant permission to others to read or update the project. Several APIs are activated @@ -263,4 +291,72 @@ public static ProjectListOption fields(ProjectField... fields) { * @throws ResourceManagerException upon failure */ void undelete(String projectId); + + /** + * Returns the IAM access control policy for the specified project. Returns null if the resource + * does not exist or if you do not have adequate permission to view the project. + * + * @see + * Resource Manager getIamPolicy + * @throws ResourceManagerException upon failure + */ + Policy getPolicy(String projectId); + + /** + * Sets the IAM access control policy for the specified project. Replaces any existing policy. The + * following constraints apply: + *

+ * Note: Removing service accounts from policies or changing their roles can render services + * completely inoperable. It is important to understand how the service account is being used + * before removing or updating its roles. + * + * It is recommended that you use the read-modify-write pattern. This pattern entails reading the + * project's current policy, updating it locally, and then sending the modified policy for + * writing. Cloud IAM solves the problem of conflicting processes simultaneously attempting to + * modify a policy by using the etag property. This property is used to verify whether the + * policy has changed since the last request. When you make a request to Cloud IAM with an etag + * value, Cloud IAM compares the etag value in the request with the existing etag value associated + * with the policy. It writes the policy only if the etag values match. If an etag is not + * provided, the policy is overwritten blindly. + * + * An example of using the read-write-modify pattern is as follows: + *
 {@code
+   * Policy currentPolicy = resourceManager.getPolicy("my-project-id");
+   * Policy modifiedPolicy =
+   *     current.toBuilder().removeIdentity(Role.VIEWER, Identity.user("user@gmail.com"));
+   * Policy newPolicy = resourceManager.setPolicy("my-project-id", modified);
+   * }
+   * 
+ * + * @see + * Resource Manager setIamPolicy + * @throw ResourceManagerException upon failure + */ + Policy replacePolicy(String projectId, Policy newPolicy); + + /** + * Returns the permissions that a caller has on the specified project. You typically don't call + * this method if you're using Google Cloud Platform directly to manage permissions. This method + * is intended for integration with your proprietary software, such as a customized graphical user + * interface. For example, the Cloud Platform Console tests IAM permissions internally to + * determine which UI should be available to the logged-in user. + * + * @see + * Resource Manager testIamPermissions + * @throw ResourceManagerException upon failure + */ + List testPermissions(String projectId, List permissions); } diff --git a/gcloud-java-resourcemanager/src/main/java/com/google/gcloud/resourcemanager/ResourceManagerImpl.java b/gcloud-java-resourcemanager/src/main/java/com/google/gcloud/resourcemanager/ResourceManagerImpl.java index 4176b4e610ba..09f8a9bb9229 100644 --- a/gcloud-java-resourcemanager/src/main/java/com/google/gcloud/resourcemanager/ResourceManagerImpl.java +++ b/gcloud-java-resourcemanager/src/main/java/com/google/gcloud/resourcemanager/ResourceManagerImpl.java @@ -23,6 +23,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.gcloud.BaseService; import com.google.gcloud.Page; @@ -32,6 +33,7 @@ import com.google.gcloud.spi.ResourceManagerRpc; import com.google.gcloud.spi.ResourceManagerRpc.Tuple; +import java.util.List; import java.util.Map; import java.util.concurrent.Callable; @@ -181,6 +183,61 @@ public Void call() { } } + @Override + public Policy getPolicy(final String projectId) { + try { + com.google.api.services.cloudresourcemanager.model.Policy answer = + runWithRetries( + new Callable() { + @Override + public com.google.api.services.cloudresourcemanager.model.Policy call() { + return resourceManagerRpc.getPolicy(projectId); + } + }, + options().retryParams(), + EXCEPTION_HANDLER); + return answer == null ? null : Policy.fromPb(answer); + } catch (RetryHelperException e) { + throw ResourceManagerException.translateAndThrow(e); + } + } + + @Override + public Policy replacePolicy(final String projectId, final Policy newPolicy) { + try { + return Policy.fromPb(runWithRetries( + new Callable() { + @Override + public com.google.api.services.cloudresourcemanager.model.Policy call() { + return resourceManagerRpc.replacePolicy(projectId, newPolicy.toPb()); + } + }, options().retryParams(), EXCEPTION_HANDLER)); + } catch (RetryHelperException e) { + throw ResourceManagerException.translateAndThrow(e); + } + } + + @Override + public List testPermissions(final String projectId, final List permissions) { + try { + return runWithRetries( + new Callable>() { + @Override + public List call() { + return resourceManagerRpc.testPermissions(projectId, + Lists.transform(permissions, new Function() { + @Override + public String apply(Permission p) { + return p.strValue(); + } + })); + } + }, options().retryParams(), EXCEPTION_HANDLER); + } catch (RetryHelperException e) { + throw ResourceManagerException.translateAndThrow(e); + } + } + private Map optionMap(Option... options) { Map temp = Maps.newEnumMap(ResourceManagerRpc.Option.class); for (Option option : options) { diff --git a/gcloud-java-resourcemanager/src/main/java/com/google/gcloud/resourcemanager/testing/LocalResourceManagerHelper.java b/gcloud-java-resourcemanager/src/main/java/com/google/gcloud/resourcemanager/testing/LocalResourceManagerHelper.java index a077eb6144a5..b8bfcb77b76c 100644 --- a/gcloud-java-resourcemanager/src/main/java/com/google/gcloud/resourcemanager/testing/LocalResourceManagerHelper.java +++ b/gcloud-java-resourcemanager/src/main/java/com/google/gcloud/resourcemanager/testing/LocalResourceManagerHelper.java @@ -5,7 +5,12 @@ import static java.net.HttpURLConnection.HTTP_OK; import com.google.api.client.json.JsonFactory; +import com.google.api.services.cloudresourcemanager.model.Binding; +import com.google.api.services.cloudresourcemanager.model.Policy; import com.google.api.services.cloudresourcemanager.model.Project; +import com.google.api.services.cloudresourcemanager.model.SetIamPolicyRequest; +import com.google.api.services.cloudresourcemanager.model.TestIamPermissionsRequest; +import com.google.api.services.cloudresourcemanager.model.TestIamPermissionsResponse; import com.google.common.base.Joiner; import com.google.common.base.Objects; import com.google.common.collect.ImmutableList; @@ -29,11 +34,14 @@ import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; +import java.util.UUID; import java.util.concurrent.ConcurrentSkipListMap; import java.util.logging.Level; import java.util.logging.Logger; @@ -45,7 +53,25 @@ * Utility to create a local Resource Manager mock for testing. * *

The mock runs in a separate thread, listening for HTTP requests on the local machine at an - * ephemeral port. + * ephemeral port. While this mock attempts to simulate the Cloud Resource Manager, there are some + * divergences in behavior. The following is a non-exhaustive list of some of those behavioral + * differences: + * + *

    + *
  • This mock assumes you have adequate permissions for any action. Related to this, + * testIamPermissions always indicates that the caller has all permissions listed in the + * request. + *
  • IAM policies are set to an empty policy with version 0 (only legacy roles supported) upon + * project creation. The actual service will not have an empty list of bindings and may also + * set your version to 1. + *
  • There is no input validation for the policy provided when replacing a policy. + *
  • In this mock, projects never move from the DELETE_REQUESTED lifecycle state to + * DELETE_IN_PROGRESS without an explicit call to the utility method + * {@link #changeLifecycleState}. Similarly, a project is never completely removed without an + * explicit call to the utility method {@link #removeProject}. + *
  • The messages in the error responses given by this mock do not necessarily match the messages + * given by the actual service. + *
*/ @SuppressWarnings("restriction") public class LocalResourceManagerHelper { @@ -61,6 +87,10 @@ public class LocalResourceManagerHelper { private static final Pattern LIST_FIELDS_PATTERN = Pattern.compile("(.*?)projects\\((.*?)\\)(.*?)"); private static final String[] NO_FIELDS = {}; + private static final String PERMISSION_PREFIX = "resourcemanager.projects."; + private static final Set PERMISSION_SUFFIXES = ImmutableSet.of("create", "delete", "get", + "getIamPolicy", "list", "own", "setIamPolicy", "setBillingAccount", "testIamPolicy", + "undelete", "update"); static { try { @@ -77,6 +107,7 @@ public class LocalResourceManagerHelper { private final HttpServer server; private final ConcurrentSkipListMap projects = new ConcurrentSkipListMap<>(); + private final Map policies = new HashMap<>(); private final int port; private static class Response { @@ -98,6 +129,7 @@ String body() { } private enum Error { + ABORTED(409, "global", "aborted", "ABORTED"), ALREADY_EXISTS(409, "global", "alreadyExists", "ALREADY_EXISTS"), PERMISSION_DENIED(403, "global", "forbidden", "PERMISSION_DENIED"), FAILED_PRECONDITION(400, "global", "failedPrecondition", "FAILED_PRECONDITION"), @@ -149,13 +181,7 @@ public void handle(HttpExchange exchange) { try { switch (requestMethod) { case "POST": - if (path.endsWith(":undelete")) { - response = undelete(projectIdFromUri(path)); - } else { - String requestBody = - decodeContent(exchange.getRequestHeaders(), exchange.getRequestBody()); - response = create(jsonFactory.fromString(requestBody, Project.class)); - } + response = handlePost(exchange, path); break; case "DELETE": response = delete(projectIdFromUri(path)); @@ -186,6 +212,30 @@ public void handle(HttpExchange exchange) { } } + private Response handlePost(HttpExchange exchange, String path) throws IOException { + String requestBody = decodeContent(exchange.getRequestHeaders(), exchange.getRequestBody()); + if (!path.contains(":")) { + return create(jsonFactory.fromString(requestBody, Project.class)); + } else { + switch (path.split(":", 2)[1]) { + case "undelete": + return undelete(projectIdFromUri(path)); + case "getIamPolicy": + return getPolicy(projectIdFromUri(path)); + case "setIamPolicy": + return replacePolicy(projectIdFromUri(path), + jsonFactory.fromString(requestBody, SetIamPolicyRequest.class).getPolicy()); + case "testIamPermissions": + return testPermissions(projectIdFromUri(path), + jsonFactory.fromString(requestBody, TestIamPermissionsRequest.class) + .getPermissions()); + default: + return Error.BAD_REQUEST.response( + "The server could not understand the following request URI: POST " + path); + } + } + } + private static void writeResponse(HttpExchange exchange, Response response) { exchange.getResponseHeaders().set("Content-type", "application/json; charset=UTF-8"); OutputStream outputStream = exchange.getResponseBody(); @@ -316,7 +366,7 @@ private static boolean isValidIdOrLabel(String value, int minLength, int maxLeng return value.length() >= minLength && value.length() <= maxLength; } - Response create(Project project) { + synchronized Response create(Project project) { String customErrorMessage = checkForProjectErrors(project); if (customErrorMessage != null) { return Error.INVALID_ARGUMENT.response(customErrorMessage); @@ -328,6 +378,11 @@ Response create(Project project) { return Error.ALREADY_EXISTS.response( "A project with the same project ID (" + project.getProjectId() + ") already exists."); } + Policy emptyPolicy = new Policy() + .setBindings(Collections.emptyList()) + .setEtag(UUID.randomUUID().toString()) + .setVersion(0); + policies.put(project.getProjectId(), emptyPolicy); try { String createdProjectStr = jsonFactory.toString(project); return new Response(HTTP_OK, createdProjectStr); @@ -539,6 +594,60 @@ synchronized Response undelete(String projectId) { return response; } + synchronized Response getPolicy(String projectId) { + Policy policy = policies.get(projectId); + if (policy == null) { + return Error.PERMISSION_DENIED.response("Project " + projectId + " not found."); + } + try { + return new Response(HTTP_OK, jsonFactory.toString(policy)); + } catch (IOException e) { + return Error.INTERNAL_ERROR.response( + "Error when serializing the IAM policy for " + projectId); + } + } + + synchronized Response replacePolicy(String projectId, Policy policy) { + Policy originalPolicy = policies.get(projectId); + if (originalPolicy == null) { + return Error.PERMISSION_DENIED.response("Error when replacing the policy for " + + projectId + " because the project was not found."); + } + String etag = policy.getEtag(); + if (etag != null && !originalPolicy.getEtag().equals(etag)) { + return Error.ABORTED.response("Policy etag mismatch when replacing the policy for project " + + projectId + ", please retry the read."); + } + policy.setEtag(UUID.randomUUID().toString()); + policy.setVersion(originalPolicy.getVersion()); + policies.put(projectId, policy); + try { + return new Response(HTTP_OK, jsonFactory.toString(policy)); + } catch (IOException e) { + return Error.INTERNAL_ERROR.response( + "Error when serializing the policy for project " + projectId); + } + } + + Response testPermissions(String projectId, List permissions) { + if (!projects.containsKey(projectId)) { + return Error.PERMISSION_DENIED.response("Project " + projectId + " not found."); + } + for (String p : permissions) { + if (!p.startsWith(PERMISSION_PREFIX) + || !PERMISSION_SUFFIXES.contains(p.substring(PERMISSION_PREFIX.length()))) { + return Error.INVALID_ARGUMENT.response("Invalid permission: " + p); + } + } + try { + return new Response(HTTP_OK, + jsonFactory.toString(new TestIamPermissionsResponse().setPermissions(permissions))); + } catch (IOException e) { + return Error.INTERNAL_ERROR.response( + "Error when serializing permissions " + Arrays.asList(permissions)); + } + } + private LocalResourceManagerHelper() { try { server = HttpServer.create(new InetSocketAddress(0), 0); @@ -606,6 +715,7 @@ public synchronized boolean changeLifecycleState(String projectId, String lifecy public synchronized boolean removeProject(String projectId) { // Because this method is synchronized, any code that relies on non-atomic read/write operations // should not fail if that code is also synchronized. - return projects.remove(checkNotNull(projectId)) != null; + policies.remove(checkNotNull(projectId)); + return projects.remove(projectId) != null; } } diff --git a/gcloud-java-resourcemanager/src/main/java/com/google/gcloud/spi/DefaultResourceManagerRpc.java b/gcloud-java-resourcemanager/src/main/java/com/google/gcloud/spi/DefaultResourceManagerRpc.java index d30cd2df3627..fab14061d433 100644 --- a/gcloud-java-resourcemanager/src/main/java/com/google/gcloud/spi/DefaultResourceManagerRpc.java +++ b/gcloud-java-resourcemanager/src/main/java/com/google/gcloud/spi/DefaultResourceManagerRpc.java @@ -11,13 +11,22 @@ import com.google.api.client.http.HttpTransport; import com.google.api.client.json.jackson.JacksonFactory; import com.google.api.services.cloudresourcemanager.Cloudresourcemanager; +import com.google.api.services.cloudresourcemanager.model.GetIamPolicyRequest; import com.google.api.services.cloudresourcemanager.model.ListProjectsResponse; +import com.google.api.services.cloudresourcemanager.model.Policy; import com.google.api.services.cloudresourcemanager.model.Project; +import com.google.api.services.cloudresourcemanager.model.SetIamPolicyRequest; +import com.google.api.services.cloudresourcemanager.model.TestIamPermissionsRequest; +import com.google.api.services.cloudresourcemanager.model.TestIamPermissionsResponse; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.gcloud.resourcemanager.ResourceManagerException; import com.google.gcloud.resourcemanager.ResourceManagerOptions; import java.io.IOException; +import java.util.List; import java.util.Map; +import java.util.Set; public class DefaultResourceManagerRpc implements ResourceManagerRpc { @@ -107,5 +116,50 @@ public Project replace(Project project) { throw translate(ex); } } -} + @Override + public Policy getPolicy(String projectId) throws ResourceManagerException { + try { + return resourceManager.projects() + .getIamPolicy(projectId, new GetIamPolicyRequest()) + .execute(); + } catch (IOException ex) { + ResourceManagerException translated = translate(ex); + if (translated.code() == HTTP_FORBIDDEN) { + // Service returns permission denied if policy doesn't exist. + return null; + } else { + throw translated; + } + } + } + + @Override + public Policy replacePolicy(String projectId, Policy newPolicy) throws ResourceManagerException { + try { + return resourceManager.projects() + .setIamPolicy(projectId, new SetIamPolicyRequest().setPolicy(newPolicy)).execute(); + } catch (IOException ex) { + throw translate(ex); + } + } + + @Override + public List testPermissions(String projectId, List permissions) + throws ResourceManagerException { + try { + TestIamPermissionsResponse response = resourceManager.projects() + .testIamPermissions( + projectId, new TestIamPermissionsRequest().setPermissions(permissions)) + .execute(); + Set permissionsOwned = ImmutableSet.copyOf(response.getPermissions()); + ImmutableList.Builder answer = ImmutableList.builder(); + for (String p : permissions) { + answer.add(permissionsOwned.contains(p)); + } + return answer.build(); + } catch (IOException ex) { + throw translate(ex); + } + } +} diff --git a/gcloud-java-resourcemanager/src/main/java/com/google/gcloud/spi/ResourceManagerRpc.java b/gcloud-java-resourcemanager/src/main/java/com/google/gcloud/spi/ResourceManagerRpc.java index e1d72a6a373e..dd009c52b05e 100644 --- a/gcloud-java-resourcemanager/src/main/java/com/google/gcloud/spi/ResourceManagerRpc.java +++ b/gcloud-java-resourcemanager/src/main/java/com/google/gcloud/spi/ResourceManagerRpc.java @@ -16,9 +16,11 @@ package com.google.gcloud.spi; +import com.google.api.services.cloudresourcemanager.model.Policy; import com.google.api.services.cloudresourcemanager.model.Project; import com.google.gcloud.resourcemanager.ResourceManagerException; +import java.util.List; import java.util.Map; public interface ResourceManagerRpc { @@ -121,5 +123,27 @@ public Y y() { */ Project replace(Project project); + /** + * Returns the IAM policy associated with a project. + * + * @throws ResourceManagerException upon failure + */ + Policy getPolicy(String projectId); + + /** + * Replaces the IAM policy associated with the given project ID. + * + * @throws ResourceManagerException upon failure + */ + Policy replacePolicy(String projectId, Policy newPolicy); + + /** + * Tests whether the caller has the given permissions. Returns a list of booleans corresponding to + * whether or not the user has the permission in the same position of input list. + * + * @throws ResourceManagerException upon failure + */ + List testPermissions(String projectId, List permissions); + // TODO(ajaykannan): implement "Organization" functionality when available (issue #319) } diff --git a/gcloud-java-resourcemanager/src/test/java/com/google/gcloud/resourcemanager/LocalResourceManagerHelperTest.java b/gcloud-java-resourcemanager/src/test/java/com/google/gcloud/resourcemanager/LocalResourceManagerHelperTest.java index a3418fff98ab..3b140ccdb828 100644 --- a/gcloud-java-resourcemanager/src/test/java/com/google/gcloud/resourcemanager/LocalResourceManagerHelperTest.java +++ b/gcloud-java-resourcemanager/src/test/java/com/google/gcloud/resourcemanager/LocalResourceManagerHelperTest.java @@ -2,11 +2,14 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import com.google.api.services.cloudresourcemanager.model.Binding; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.gcloud.resourcemanager.testing.LocalResourceManagerHelper; import com.google.gcloud.spi.DefaultResourceManagerRpc; @@ -18,8 +21,10 @@ import org.junit.BeforeClass; import org.junit.Test; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Map; public class LocalResourceManagerHelperTest { @@ -45,7 +50,12 @@ public class LocalResourceManagerHelperTest { .setLabels(ImmutableMap.of("k1", "v1", "k2", "v2")); private static final com.google.api.services.cloudresourcemanager.model.Project PROJECT_WITH_PARENT = - copyFrom(COMPLETE_PROJECT).setProjectId("project-with-parent-id").setParent(PARENT); + copyFrom(COMPLETE_PROJECT).setProjectId("project-with-parent-id").setParent(PARENT); + private static final List BINDINGS = ImmutableList.of( + new Binding().setRole("roles/owner").setMembers(ImmutableList.of("user:me@gmail.com")), + new Binding().setRole("roles/viewer").setMembers(ImmutableList.of("group:group@gmail.com"))); + private static final com.google.api.services.cloudresourcemanager.model.Policy POLICY = + new com.google.api.services.cloudresourcemanager.model.Policy().setBindings(BINDINGS); @BeforeClass public static void beforeClass() { @@ -92,6 +102,13 @@ public void testCreate() { assertNull(returnedProject.getParent()); assertNotNull(returnedProject.getProjectNumber()); assertNotNull(returnedProject.getCreateTime()); + com.google.api.services.cloudresourcemanager.model.Policy policy = + rpc.getPolicy(PARTIAL_PROJECT.getProjectId()); + assertEquals(Collections.EMPTY_LIST, policy.getBindings()); + assertNotNull(policy.getEtag()); + assertEquals(0, policy.getVersion().intValue()); + rpc.replacePolicy(PARTIAL_PROJECT.getProjectId(), POLICY); + assertEquals(POLICY.getBindings(), rpc.getPolicy(PARTIAL_PROJECT.getProjectId()).getBindings()); try { rpc.create(PARTIAL_PROJECT); fail("Should fail, project already exists."); @@ -99,6 +116,8 @@ public void testCreate() { assertEquals(409, e.code()); assertTrue(e.getMessage().startsWith("A project with the same project ID") && e.getMessage().endsWith("already exists.")); + assertEquals( + POLICY.getBindings(), rpc.getPolicy(PARTIAL_PROJECT.getProjectId()).getBindings()); } returnedProject = rpc.create(PROJECT_WITH_PARENT); compareReadWriteFields(PROJECT_WITH_PARENT, returnedProject); @@ -609,6 +628,64 @@ public void testUndeleteWhenDeleteInProgress() { } } + @Test + public void testGetPolicy() { + assertNull(rpc.getPolicy("nonexistent-project")); + rpc.create(PARTIAL_PROJECT); + com.google.api.services.cloudresourcemanager.model.Policy policy = + rpc.getPolicy(PARTIAL_PROJECT.getProjectId()); + assertEquals(Collections.EMPTY_LIST, policy.getBindings()); + assertNotNull(policy.getEtag()); + } + + @Test + public void testReplacePolicy() { + try { + rpc.replacePolicy("nonexistent-project", POLICY); + fail("Project doesn't exist."); + } catch (ResourceManagerException e) { + assertEquals(403, e.code()); + assertTrue(e.getMessage().contains("project was not found")); + } + rpc.create(PARTIAL_PROJECT); + com.google.api.services.cloudresourcemanager.model.Policy invalidPolicy = + new com.google.api.services.cloudresourcemanager.model.Policy().setEtag("wrong-etag"); + try { + rpc.replacePolicy(PARTIAL_PROJECT.getProjectId(), invalidPolicy); + fail("Invalid etag."); + } catch (ResourceManagerException e) { + assertEquals(409, e.code()); + assertTrue(e.getMessage().startsWith("Policy etag mismatch")); + } + String originalEtag = rpc.getPolicy(PARTIAL_PROJECT.getProjectId()).getEtag(); + com.google.api.services.cloudresourcemanager.model.Policy newPolicy = + rpc.replacePolicy(PARTIAL_PROJECT.getProjectId(), POLICY); + assertEquals(POLICY.getBindings(), newPolicy.getBindings()); + assertNotNull(newPolicy.getEtag()); + assertNotEquals(originalEtag, newPolicy.getEtag()); + } + + @Test + public void testTestPermissions() { + List permissions = ImmutableList.of("resourcemanager.projects.get"); + try { + rpc.testPermissions("nonexistent-project", permissions); + } catch (ResourceManagerException e) { + assertEquals(403, e.code()); + assertEquals("Project nonexistent-project not found.", e.getMessage()); + } + rpc.create(PARTIAL_PROJECT); + try { + rpc.testPermissions(PARTIAL_PROJECT.getProjectId(), ImmutableList.of("get")); + fail("Invalid permission."); + } catch (ResourceManagerException e) { + assertEquals(400, e.code()); + assertEquals("Invalid permission: get", e.getMessage()); + } + assertEquals(ImmutableList.of(true), + rpc.testPermissions(PARTIAL_PROJECT.getProjectId(), permissions)); + } + @Test public void testChangeLifecycleStatus() { assertFalse(RESOURCE_MANAGER_HELPER.changeLifecycleState( @@ -632,8 +709,10 @@ public void testChangeLifecycleStatus() { public void testRemoveProject() { assertFalse(RESOURCE_MANAGER_HELPER.removeProject(COMPLETE_PROJECT.getProjectId())); rpc.create(COMPLETE_PROJECT); + assertNotNull(rpc.getPolicy(COMPLETE_PROJECT.getProjectId())); assertTrue(RESOURCE_MANAGER_HELPER.removeProject(COMPLETE_PROJECT.getProjectId())); assertNull(rpc.get(COMPLETE_PROJECT.getProjectId(), EMPTY_RPC_OPTIONS)); + assertNull(rpc.getPolicy(COMPLETE_PROJECT.getProjectId())); } private void compareReadWriteFields( diff --git a/gcloud-java-resourcemanager/src/test/java/com/google/gcloud/resourcemanager/ResourceManagerImplTest.java b/gcloud-java-resourcemanager/src/test/java/com/google/gcloud/resourcemanager/ResourceManagerImplTest.java index 1bc233311a4d..68a70f65114a 100644 --- a/gcloud-java-resourcemanager/src/test/java/com/google/gcloud/resourcemanager/ResourceManagerImplTest.java +++ b/gcloud-java-resourcemanager/src/test/java/com/google/gcloud/resourcemanager/ResourceManagerImplTest.java @@ -18,15 +18,19 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.gcloud.Identity; import com.google.gcloud.Page; import com.google.gcloud.resourcemanager.ProjectInfo.ResourceId; +import com.google.gcloud.resourcemanager.ResourceManager.Permission; import com.google.gcloud.resourcemanager.ResourceManager.ProjectField; import com.google.gcloud.resourcemanager.ResourceManager.ProjectGetOption; import com.google.gcloud.resourcemanager.ResourceManager.ProjectListOption; @@ -43,6 +47,7 @@ import org.junit.rules.ExpectedException; import java.util.Iterator; +import java.util.List; import java.util.Map; public class ResourceManagerImplTest { @@ -65,6 +70,10 @@ public class ResourceManagerImplTest { .parent(PARENT) .build(); private static final Map EMPTY_RPC_OPTIONS = ImmutableMap.of(); + private static final Policy POLICY = Policy.builder() + .addBinding(Policy.Role.OWNER, Identity.user("me@gmail.com")) + .addBinding(Policy.Role.EDITOR, Identity.serviceAccount("serviceaccount@gmail.com")) + .build(); @Rule public ExpectedException thrown = ExpectedException.none(); @@ -320,6 +329,57 @@ public void testUndelete() { } } + @Test + public void testGetPolicy() { + assertNull(RESOURCE_MANAGER.getPolicy(COMPLETE_PROJECT.projectId())); + RESOURCE_MANAGER.create(COMPLETE_PROJECT); + RESOURCE_MANAGER.replacePolicy(COMPLETE_PROJECT.projectId(), POLICY); + Policy retrieved = RESOURCE_MANAGER.getPolicy(COMPLETE_PROJECT.projectId()); + assertEquals(POLICY.bindings(), retrieved.bindings()); + assertNotNull(retrieved.etag()); + assertEquals(0, retrieved.version().intValue()); + } + + @Test + public void testReplacePolicy() { + try { + RESOURCE_MANAGER.replacePolicy("nonexistent-project", POLICY); + fail("Project doesn't exist."); + } catch (ResourceManagerException e) { + assertEquals(403, e.code()); + assertTrue(e.getMessage().endsWith("project was not found.")); + } + RESOURCE_MANAGER.create(PARTIAL_PROJECT); + Policy oldPolicy = RESOURCE_MANAGER.getPolicy(PARTIAL_PROJECT.projectId()); + RESOURCE_MANAGER.replacePolicy(PARTIAL_PROJECT.projectId(), POLICY); + try { + RESOURCE_MANAGER.replacePolicy(PARTIAL_PROJECT.projectId(), oldPolicy); + fail("Policy with an invalid etag didn't cause error."); + } catch (ResourceManagerException e) { + assertEquals(409, e.code()); + assertTrue(e.getMessage().contains("Policy etag mismatch")); + } + String originalEtag = RESOURCE_MANAGER.getPolicy(PARTIAL_PROJECT.projectId()).etag(); + Policy newPolicy = RESOURCE_MANAGER.replacePolicy(PARTIAL_PROJECT.projectId(), POLICY); + assertEquals(POLICY.bindings(), newPolicy.bindings()); + assertNotNull(newPolicy.etag()); + assertNotEquals(originalEtag, newPolicy.etag()); + } + + @Test + public void testTestPermissions() { + List permissions = ImmutableList.of(Permission.GET); + try { + RESOURCE_MANAGER.testPermissions("nonexistent-project", permissions); + } catch (ResourceManagerException e) { + assertEquals(403, e.code()); + assertEquals("Project nonexistent-project not found.", e.getMessage()); + } + RESOURCE_MANAGER.create(PARTIAL_PROJECT); + assertEquals(ImmutableList.of(true), + RESOURCE_MANAGER.testPermissions(PARTIAL_PROJECT.projectId(), permissions)); + } + @Test public void testRetryableException() { ResourceManagerRpcFactory rpcFactoryMock = EasyMock.createMock(ResourceManagerRpcFactory.class);