This is an automated email from the ASF dual-hosted git repository.

collado pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris.git


The following commit(s) were added to refs/heads/main by this push:
     new 5c1f9bb0 Update privilege model to support granting CatalogRole access 
to PrincipalRoles (#361)
5c1f9bb0 is described below

commit 5c1f9bb0e193221915ac0ee6e9bacf3ce2b7e701
Author: Michael Collado <[email protected]>
AuthorDate: Wed Oct 9 14:06:02 2024 -0700

    Update privilege model to support granting CatalogRole access to 
PrincipalRoles (#361)
    
    Co-authored-by: Michael Collado <[email protected]>
---
 .../core/auth/PolarisAuthorizableOperation.java    |  24 +-
 .../admin/PolarisServiceImplIntegrationTest.java   | 682 ++++++++++++++++++---
 2 files changed, 594 insertions(+), 112 deletions(-)

diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizableOperation.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizableOperation.java
index 97ff58b4..b98d6541 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizableOperation.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizableOperation.java
@@ -141,8 +141,7 @@ public enum PolarisAuthorizableOperation {
   DELETE_PRINCIPAL_ROLE(PRINCIPAL_ROLE_DROP),
   LIST_ASSIGNEE_PRINCIPALS_FOR_PRINCIPAL_ROLE(PRINCIPAL_ROLE_LIST_GRANTS),
   LIST_CATALOG_ROLES_FOR_PRINCIPAL_ROLE(PRINCIPAL_ROLE_LIST_GRANTS),
-  ASSIGN_CATALOG_ROLE_TO_PRINCIPAL_ROLE(
-      CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE, 
PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE),
+  
ASSIGN_CATALOG_ROLE_TO_PRINCIPAL_ROLE(CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE),
   REVOKE_CATALOG_ROLE_FROM_PRINCIPAL_ROLE(
       CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE, 
PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE),
   LIST_CATALOG_ROLES(CATALOG_ROLE_LIST),
@@ -156,38 +155,31 @@ public enum PolarisAuthorizableOperation {
   REVOKE_ROOT_GRANT_FROM_PRINCIPAL_ROLE(
       SERVICE_MANAGE_ACCESS, PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE),
   LIST_GRANTS_ON_ROOT(SERVICE_MANAGE_ACCESS),
-  ADD_PRINCIPAL_GRANT_TO_PRINCIPAL_ROLE(
-      PRINCIPAL_MANAGE_GRANTS_ON_SECURABLE, 
PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE),
+  ADD_PRINCIPAL_GRANT_TO_PRINCIPAL_ROLE(PRINCIPAL_MANAGE_GRANTS_ON_SECURABLE),
   REVOKE_PRINCIPAL_GRANT_FROM_PRINCIPAL_ROLE(
       PRINCIPAL_MANAGE_GRANTS_ON_SECURABLE, 
PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE),
   LIST_GRANTS_ON_PRINCIPAL(PRINCIPAL_LIST_GRANTS),
-  ADD_PRINCIPAL_ROLE_GRANT_TO_PRINCIPAL_ROLE(
-      PRINCIPAL_ROLE_MANAGE_GRANTS_ON_SECURABLE, 
PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE),
+  
ADD_PRINCIPAL_ROLE_GRANT_TO_PRINCIPAL_ROLE(PRINCIPAL_ROLE_MANAGE_GRANTS_ON_SECURABLE),
   REVOKE_PRINCIPAL_ROLE_GRANT_FROM_PRINCIPAL_ROLE(
       PRINCIPAL_ROLE_MANAGE_GRANTS_ON_SECURABLE, 
PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE),
   LIST_GRANTS_ON_PRINCIPAL_ROLE(PRINCIPAL_ROLE_LIST_GRANTS),
-  ADD_CATALOG_ROLE_GRANT_TO_CATALOG_ROLE(
-      CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE, 
CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE),
+  
ADD_CATALOG_ROLE_GRANT_TO_CATALOG_ROLE(CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE),
   REVOKE_CATALOG_ROLE_GRANT_FROM_CATALOG_ROLE(
       CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE, 
CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE),
   LIST_GRANTS_ON_CATALOG_ROLE(CATALOG_ROLE_LIST_GRANTS),
-  ADD_CATALOG_GRANT_TO_CATALOG_ROLE(
-      CATALOG_MANAGE_GRANTS_ON_SECURABLE, 
CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE),
+  ADD_CATALOG_GRANT_TO_CATALOG_ROLE(CATALOG_MANAGE_GRANTS_ON_SECURABLE),
   REVOKE_CATALOG_GRANT_FROM_CATALOG_ROLE(
       CATALOG_MANAGE_GRANTS_ON_SECURABLE, 
CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE),
   LIST_GRANTS_ON_CATALOG(CATALOG_LIST_GRANTS),
-  ADD_NAMESPACE_GRANT_TO_CATALOG_ROLE(
-      NAMESPACE_MANAGE_GRANTS_ON_SECURABLE, 
CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE),
+  ADD_NAMESPACE_GRANT_TO_CATALOG_ROLE(NAMESPACE_MANAGE_GRANTS_ON_SECURABLE),
   REVOKE_NAMESPACE_GRANT_FROM_CATALOG_ROLE(
       NAMESPACE_MANAGE_GRANTS_ON_SECURABLE, 
CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE),
   LIST_GRANTS_ON_NAMESPACE(NAMESPACE_LIST_GRANTS),
-  ADD_TABLE_GRANT_TO_CATALOG_ROLE(
-      TABLE_MANAGE_GRANTS_ON_SECURABLE, 
CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE),
+  ADD_TABLE_GRANT_TO_CATALOG_ROLE(TABLE_MANAGE_GRANTS_ON_SECURABLE),
   REVOKE_TABLE_GRANT_FROM_CATALOG_ROLE(
       TABLE_MANAGE_GRANTS_ON_SECURABLE, 
CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE),
   LIST_GRANTS_ON_TABLE(TABLE_LIST_GRANTS),
-  ADD_VIEW_GRANT_TO_CATALOG_ROLE(
-      VIEW_MANAGE_GRANTS_ON_SECURABLE, CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE),
+  ADD_VIEW_GRANT_TO_CATALOG_ROLE(VIEW_MANAGE_GRANTS_ON_SECURABLE),
   REVOKE_VIEW_GRANT_FROM_CATALOG_ROLE(
       VIEW_MANAGE_GRANTS_ON_SECURABLE, CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE),
   LIST_GRANTS_ON_VIEW(VIEW_LIST_GRANTS),
diff --git 
a/polaris-service/src/test/java/org/apache/polaris/service/admin/PolarisServiceImplIntegrationTest.java
 
b/polaris-service/src/test/java/org/apache/polaris/service/admin/PolarisServiceImplIntegrationTest.java
index 9001fbb3..ca5650f0 100644
--- 
a/polaris-service/src/test/java/org/apache/polaris/service/admin/PolarisServiceImplIntegrationTest.java
+++ 
b/polaris-service/src/test/java/org/apache/polaris/service/admin/PolarisServiceImplIntegrationTest.java
@@ -38,10 +38,17 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.iceberg.catalog.Namespace;
+import org.apache.iceberg.rest.RESTUtil;
+import org.apache.iceberg.rest.requests.CreateNamespaceRequest;
 import org.apache.iceberg.rest.responses.ErrorResponse;
+import org.apache.iceberg.rest.responses.ListNamespacesResponse;
+import org.apache.polaris.core.admin.model.AddGrantRequest;
 import org.apache.polaris.core.admin.model.AwsStorageConfigInfo;
 import org.apache.polaris.core.admin.model.AzureStorageConfigInfo;
 import org.apache.polaris.core.admin.model.Catalog;
+import org.apache.polaris.core.admin.model.CatalogGrant;
+import org.apache.polaris.core.admin.model.CatalogPrivilege;
 import org.apache.polaris.core.admin.model.CatalogProperties;
 import org.apache.polaris.core.admin.model.CatalogRole;
 import org.apache.polaris.core.admin.model.CatalogRoles;
@@ -53,6 +60,10 @@ import 
org.apache.polaris.core.admin.model.CreatePrincipalRoleRequest;
 import org.apache.polaris.core.admin.model.ExternalCatalog;
 import org.apache.polaris.core.admin.model.FileStorageConfigInfo;
 import org.apache.polaris.core.admin.model.GrantCatalogRoleRequest;
+import org.apache.polaris.core.admin.model.GrantPrincipalRoleRequest;
+import org.apache.polaris.core.admin.model.GrantResource;
+import org.apache.polaris.core.admin.model.NamespaceGrant;
+import org.apache.polaris.core.admin.model.NamespacePrivilege;
 import org.apache.polaris.core.admin.model.PolarisCatalog;
 import org.apache.polaris.core.admin.model.Principal;
 import org.apache.polaris.core.admin.model.PrincipalRole;
@@ -76,6 +87,7 @@ import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
+import org.slf4j.LoggerFactory;
 
 @ExtendWith({DropwizardExtensionsSupport.class, 
PolarisConnectionExtension.class})
 public class PolarisServiceImplIntegrationTest {
@@ -117,10 +129,79 @@ public class PolarisServiceImplIntegrationTest {
           .readEntity(Catalogs.class)
           .getCatalogs()
           .forEach(
-              catalog ->
-                  newRequest("http://localhost:%d/api/management/v1/catalogs/"; 
+ catalog.getName())
-                      .delete()
-                      .close());
+              catalog -> {
+                // clean up the catalog before we try to drop it
+
+                // delete all the namespaces
+                try (Response res =
+                    newRequest(
+                            "http://localhost:%d/api/catalog/v1/";
+                                + catalog.getName()
+                                + "/namespaces")
+                        .get()) {
+                  if (res.getStatus() != Response.Status.OK.getStatusCode()) {
+                    LoggerFactory.getLogger(getClass())
+                        .warn(
+                            "Unable to list namespaces in catalog {}: {}",
+                            catalog.getName(),
+                            res.readEntity(String.class));
+                  } else {
+                    res.readEntity(ListNamespacesResponse.class)
+                        .namespaces()
+                        .forEach(
+                            namespace -> {
+                              newRequest(
+                                      "http://localhost:%d/api/catalog/v1/";
+                                          + catalog.getName()
+                                          + "/namespaces/"
+                                          + 
RESTUtil.encodeNamespace(namespace))
+                                  .delete()
+                                  .close();
+                            });
+                  }
+                }
+
+                // delete all the catalog roles except catalog_admin
+                try (Response res =
+                    newRequest(
+                            "http://localhost:%d/api/management/v1/catalogs/";
+                                + catalog.getName()
+                                + "/catalog-roles")
+                        .get()) {
+                  if (res.getStatus() != Response.Status.OK.getStatusCode()) {
+                    LoggerFactory.getLogger(getClass())
+                        .warn(
+                            "Unable to list catalog roles for catalog {}: {}",
+                            catalog.getName(),
+                            res.readEntity(String.class));
+                    return;
+                  }
+                  res.readEntity(CatalogRoles.class).getRoles().stream()
+                      .filter(cr -> !cr.getName().equals("catalog_admin"))
+                      .forEach(
+                          cr ->
+                              newRequest(
+                                      
"http://localhost:%d/api/management/v1/catalogs/";
+                                          + catalog.getName()
+                                          + "/catalog-roles/"
+                                          + cr.getName())
+                                  .delete()
+                                  .close());
+                }
+
+                Response deleteResponse =
+                    newRequest(
+                            "http://localhost:%d/api/management/v1/catalogs/"; 
+ catalog.getName())
+                        .delete();
+                if (deleteResponse.getStatus() != 
Response.Status.NO_CONTENT.getStatusCode()) {
+                  LoggerFactory.getLogger(getClass())
+                      .warn(
+                          "Unable to delete catalog {}: {}",
+                          catalog.getName(),
+                          deleteResponse.readEntity(String.class));
+                }
+                deleteResponse.close();
+              });
     }
     try (Response response = 
newRequest("http://localhost:%d/api/management/v1/principals";).get()) {
       response.readEntity(Principals.class).getPrincipals().stream()
@@ -213,7 +294,7 @@ public class PolarisServiceImplIntegrationTest {
               realm);
     }
     try (Response response =
-        newRequest("http://localhost:%d/api/management/v1/catalogs";, "BEARER " 
+ newToken).get()) {
+        newRequest("http://localhost:%d/api/management/v1/catalogs";, 
newToken).get()) {
       assertThat(response).returns(403, Response::getStatus);
     }
   }
@@ -345,7 +426,7 @@ public class PolarisServiceImplIntegrationTest {
     requestNode.set("catalog", catalogNode);
 
     try (Response response =
-        newRequest("http://localhost:%d/api/management/v1/catalogs";, "Bearer " 
+ userToken)
+        newRequest("http://localhost:%d/api/management/v1/catalogs";, userToken)
             .post(Entity.json(requestNode))) {
       assertThat(response)
           .returns(Response.Status.BAD_REQUEST.getStatusCode(), 
Response::getStatus);
@@ -363,7 +444,7 @@ public class PolarisServiceImplIntegrationTest {
     String catalogString =
         "{\"catalog\": 
{\"type\":\"INTERNAL\",\"name\":\"my-catalog\",\"properties\":{\"default-base-location\":\"s3://my-bucket/path/to/data\"}}}";
     try (Response response =
-        newRequest("http://localhost:%d/api/management/v1/catalogs";, "Bearer " 
+ userToken)
+        newRequest("http://localhost:%d/api/management/v1/catalogs";, userToken)
             .post(Entity.json(catalogString))) {
       assertThat(response)
           .returns(Response.Status.BAD_REQUEST.getStatusCode(), 
Response::getStatus);
@@ -380,7 +461,7 @@ public class PolarisServiceImplIntegrationTest {
   public void testCreateCatalogWithUnparsableJson() throws 
JsonProcessingException {
     String catalogString = "{\"catalog\": {{\"bad data}";
     try (Response response =
-        newRequest("http://localhost:%d/api/management/v1/catalogs";, "Bearer " 
+ userToken)
+        newRequest("http://localhost:%d/api/management/v1/catalogs";, userToken)
             .post(Entity.json(catalogString))) {
       assertThat(response)
           .returns(Response.Status.BAD_REQUEST.getStatusCode(), 
Response::getStatus);
@@ -408,7 +489,7 @@ public class PolarisServiceImplIntegrationTest {
             .setStorageConfigInfo(fileStorage)
             .build();
     try (Response response =
-        newRequest("http://localhost:%d/api/management/v1/catalogs";, "Bearer " 
+ userToken)
+        newRequest("http://localhost:%d/api/management/v1/catalogs";, userToken)
             .post(Entity.json(new CreateCatalogRequest(catalog)))) {
       assertThat(response)
           .returns(Response.Status.BAD_REQUEST.getStatusCode(), 
Response::getStatus);
@@ -438,7 +519,7 @@ public class PolarisServiceImplIntegrationTest {
             .setStorageConfigInfo(awsConfigModel)
             .build();
     try (Response response =
-        newRequest("http://localhost:%d/api/management/v1/catalogs";, "Bearer " 
+ userToken)
+        newRequest("http://localhost:%d/api/management/v1/catalogs";, userToken)
             .post(Entity.json(new CreateCatalogRequest(catalog)))) {
       assertThat(response).returns(Response.Status.CREATED.getStatusCode(), 
Response::getStatus);
     }
@@ -446,9 +527,7 @@ public class PolarisServiceImplIntegrationTest {
     // 200 successful GET after creation
     Catalog fetchedCatalog = null;
     try (Response response =
-        newRequest(
-                "http://localhost:%d/api/management/v1/catalogs/"; + 
catalogName,
-                "Bearer " + userToken)
+        newRequest("http://localhost:%d/api/management/v1/catalogs/"; + 
catalogName, userToken)
             .get()) {
       assertThat(response).returns(200, Response::getStatus);
       fetchedCatalog = response.readEntity(Catalog.class);
@@ -471,9 +550,7 @@ public class PolarisServiceImplIntegrationTest {
 
     // failure to update
     try (Response response =
-        newRequest(
-                "http://localhost:%d/api/management/v1/catalogs/"; + 
catalogName,
-                "Bearer " + userToken)
+        newRequest("http://localhost:%d/api/management/v1/catalogs/"; + 
catalogName, userToken)
             .put(Entity.json(updateRequest))) {
       assertThat(response)
           .returns(Response.Status.BAD_REQUEST.getStatusCode(), 
Response::getStatus);
@@ -503,11 +580,7 @@ public class PolarisServiceImplIntegrationTest {
             .setProperties(new 
CatalogProperties("s3://my-bucket/path/to/data"))
             .setStorageConfigInfo(awsConfigModel)
             .build();
-    try (Response response =
-        newRequest("http://localhost:%d/api/management/v1/catalogs";)
-            .post(Entity.json(new CreateCatalogRequest(catalog)))) {
-      assertThat(response).returns(Response.Status.CREATED.getStatusCode(), 
Response::getStatus);
-    }
+    createCatalog(catalog);
 
     try (Response response =
         newRequest("http://localhost:%d/api/management/v1/catalogs/"; + 
catalogName).get()) {
@@ -584,11 +657,7 @@ public class PolarisServiceImplIntegrationTest {
             .build();
 
     // 200 Successful create
-    try (Response response =
-        newRequest("http://localhost:%d/api/management/v1/catalogs";)
-            .post(Entity.json(new CreateCatalogRequest(catalog)))) {
-      assertThat(response).returns(Response.Status.CREATED.getStatusCode(), 
Response::getStatus);
-    }
+    createCatalog(catalog);
 
     // 200 successful GET after creation
     Catalog fetchedCatalog = null;
@@ -662,11 +731,7 @@ public class PolarisServiceImplIntegrationTest {
             .setProperties(new CatalogProperties("s3://bucket1/"))
             .build();
 
-    try (Response response =
-        newRequest("http://localhost:%d/api/management/v1/catalogs";)
-            .post(Entity.json(new CreateCatalogRequest(catalog)))) {
-      assertThat(response).returns(Response.Status.CREATED.getStatusCode(), 
Response::getStatus);
-    }
+    createCatalog(catalog);
 
     // Second attempt to create the same entity should fail with CONFLICT.
     try (Response response =
@@ -778,12 +843,12 @@ public class PolarisServiceImplIntegrationTest {
     return EXT.client()
         .target(String.format(url, EXT.getLocalPort()))
         .request("application/json")
-        .header("Authorization", token)
+        .header("Authorization", "Bearer " + token)
         .header(REALM_PROPERTY_KEY, realm);
   }
 
   private static Invocation.Builder newRequest(String url) {
-    return newRequest(url, "Bearer " + userToken);
+    return newRequest(url, userToken);
   }
 
   @Test
@@ -831,12 +896,7 @@ public class PolarisServiceImplIntegrationTest {
                 new AwsStorageConfigInfo(
                     "arn:aws:iam::012345678901:role/jdoe", 
StorageConfigInfo.StorageTypeEnum.S3))
             .build();
-    try (Response response =
-        newRequest("http://localhost:%d/api/management/v1/catalogs";)
-            .post(Entity.json(new CreateCatalogRequest(catalog)))) {
-
-      assertThat(response).returns(Response.Status.CREATED.getStatusCode(), 
Response::getStatus);
-    }
+    createCatalog(catalog);
 
     String longInvalidName = RandomStringUtils.random(MAX_IDENTIFIER_LENGTH + 
1, true, true);
     List<String> invalidCatalogRoleNames =
@@ -881,8 +941,7 @@ public class PolarisServiceImplIntegrationTest {
               realm);
     }
     try (Response response =
-        newRequest("http://localhost:%d/api/management/v1/principals";, "Bearer 
" + newToken)
-            .get()) {
+        newRequest("http://localhost:%d/api/management/v1/principals";, 
newToken).get()) {
       assertThat(response).returns(403, Response::getStatus);
     }
   }
@@ -927,8 +986,7 @@ public class PolarisServiceImplIntegrationTest {
     // Any call should initially fail with error indicating that rotation is 
needed.
     try (Response response =
         newRequest(
-                "http://localhost:%d/api/management/v1/principals/myprincipal";,
-                "Bearer " + newPrincipalToken)
+                
"http://localhost:%d/api/management/v1/principals/myprincipal";, 
newPrincipalToken)
             .get()) {
       assertThat(response).returns(Response.Status.FORBIDDEN.getStatusCode(), 
Response::getStatus);
       ErrorResponse error = response.readEntity(ErrorResponse.class);
@@ -943,7 +1001,7 @@ public class PolarisServiceImplIntegrationTest {
     try (Response response =
         newRequest(
                 
"http://localhost:%d/api/management/v1/principals/myprincipal/rotate";,
-                "Bearer " + newPrincipalToken)
+                newPrincipalToken)
             .post(Entity.json(""))) {
       assertThat(response).returns(Response.Status.OK.getStatusCode(), 
Response::getStatus);
       PrincipalWithCredentials parsed = 
response.readEntity(PrincipalWithCredentials.class);
@@ -1119,11 +1177,7 @@ public class PolarisServiceImplIntegrationTest {
   public void testCreateListUpdateAndDeletePrincipalRole() {
     PrincipalRole principalRole =
         new PrincipalRole("myprincipalrole", Map.of("custom-tag", "foo"), 0L, 
0L, 1);
-    try (Response response =
-        newRequest("http://localhost:%d/api/management/v1/principal-roles";)
-            .post(Entity.json(new CreatePrincipalRoleRequest(principalRole)))) 
{
-      assertThat(response).returns(Response.Status.CREATED.getStatusCode(), 
Response::getStatus);
-    }
+    createPrincipalRole(principalRole);
 
     // Second attempt to create the same entity should fail with CONFLICT.
     try (Response response =
@@ -1214,11 +1268,7 @@ public class PolarisServiceImplIntegrationTest {
     String goodName = RandomStringUtils.random(MAX_IDENTIFIER_LENGTH, true, 
true);
     PrincipalRole principalRole =
         new PrincipalRole(goodName, Map.of("custom-tag", 
"good_principal_role"), 0L, 0L, 1);
-    try (Response response =
-        newRequest("http://localhost:%d/api/management/v1/principal-roles";)
-            .post(Entity.json(new CreatePrincipalRoleRequest(principalRole)))) 
{
-      assertThat(response).returns(Response.Status.CREATED.getStatusCode(), 
Response::getStatus);
-    }
+    createPrincipalRole(principalRole);
 
     String longInvalidName = RandomStringUtils.random(MAX_IDENTIFIER_LENGTH + 
1, true, true);
     List<String> invalidPrincipalRoleNames =
@@ -1284,12 +1334,7 @@ public class PolarisServiceImplIntegrationTest {
                 new AwsStorageConfigInfo(
                     "arn:aws:iam::012345678901:role/jdoe", 
StorageConfigInfo.StorageTypeEnum.S3))
             .build();
-    try (Response response =
-        newRequest("http://localhost:%d/api/management/v1/catalogs";)
-            .post(Entity.json(new CreateCatalogRequest(catalog)))) {
-
-      assertThat(response).returns(Response.Status.CREATED.getStatusCode(), 
Response::getStatus);
-    }
+    createCatalog(catalog);
 
     Catalog catalog2 =
         PolarisCatalog.builder()
@@ -1300,12 +1345,7 @@ public class PolarisServiceImplIntegrationTest {
                     "arn:aws:iam::012345678901:role/jdoe", 
StorageConfigInfo.StorageTypeEnum.S3))
             .setProperties(new 
CatalogProperties("s3://required/base/other_location"))
             .build();
-    try (Response response =
-        newRequest("http://localhost:%d/api/management/v1/catalogs";)
-            .post(Entity.json(new CreateCatalogRequest(catalog2)))) {
-
-      assertThat(response).returns(Response.Status.CREATED.getStatusCode(), 
Response::getStatus);
-    }
+    createCatalog(catalog2);
 
     CatalogRole catalogRole =
         new CatalogRole("mycatalogrole", Map.of("custom-tag", "foo"), 0L, 0L, 
1);
@@ -1463,12 +1503,7 @@ public class PolarisServiceImplIntegrationTest {
 
     // One PrincipalRole
     PrincipalRole principalRole = new PrincipalRole("myprincipalrole");
-    try (Response response =
-        newRequest("http://localhost:%d/api/management/v1/principal-roles";)
-            .post(Entity.json(new CreatePrincipalRoleRequest(principalRole)))) 
{
-
-      assertThat(response).returns(Response.Status.CREATED.getStatusCode(), 
Response::getStatus);
-    }
+    createPrincipalRole(principalRole);
 
     // Assign the role to myprincipal1
     try (Response response =
@@ -1576,20 +1611,10 @@ public class PolarisServiceImplIntegrationTest {
   public void testAssignListAndRevokeCatalogRoles() {
     // Create two PrincipalRoles
     PrincipalRole principalRole1 = new PrincipalRole("mypr1");
-    try (Response response =
-        newRequest("http://localhost:%d/api/management/v1/principal-roles";)
-            .post(Entity.json(new 
CreatePrincipalRoleRequest(principalRole1)))) {
-
-      assertThat(response).returns(Response.Status.CREATED.getStatusCode(), 
Response::getStatus);
-    }
+    createPrincipalRole(principalRole1);
 
     PrincipalRole principalRole2 = new PrincipalRole("mypr2");
-    try (Response response =
-        newRequest("http://localhost:%d/api/management/v1/principal-roles";)
-            .post(Entity.json(new 
CreatePrincipalRoleRequest(principalRole2)))) {
-
-      assertThat(response).returns(Response.Status.CREATED.getStatusCode(), 
Response::getStatus);
-    }
+    createPrincipalRole(principalRole2);
 
     // One CatalogRole
     Catalog catalog =
@@ -1601,12 +1626,7 @@ public class PolarisServiceImplIntegrationTest {
                     "arn:aws:iam::012345678901:role/jdoe", 
StorageConfigInfo.StorageTypeEnum.S3))
             .setProperties(new CatalogProperties("s3://bucket1/"))
             .build();
-    try (Response response =
-        newRequest("http://localhost:%d/api/management/v1/catalogs";)
-            .post(Entity.json(new CreateCatalogRequest(catalog)))) {
-
-      assertThat(response).returns(Response.Status.CREATED.getStatusCode(), 
Response::getStatus);
-    }
+    createCatalog(catalog);
 
     CatalogRole catalogRole = new CatalogRole("mycr");
     try (Response response =
@@ -1626,12 +1646,7 @@ public class PolarisServiceImplIntegrationTest {
                 new AwsStorageConfigInfo(
                     "arn:aws:iam::012345678901:role/jdoe", 
StorageConfigInfo.StorageTypeEnum.S3))
             .build();
-    try (Response response =
-        newRequest("http://localhost:%d/api/management/v1/catalogs";)
-            .post(Entity.json(new CreateCatalogRequest(otherCatalog)))) {
-
-      assertThat(response).returns(Response.Status.CREATED.getStatusCode(), 
Response::getStatus);
-    }
+    createCatalog(otherCatalog);
 
     CatalogRole otherCatalogRole = new CatalogRole("myothercr");
     try (Response response =
@@ -1775,4 +1790,479 @@ public class PolarisServiceImplIntegrationTest {
       assertThat(response).returns(204, Response::getStatus);
     }
   }
+
+  @Test
+  public void testCatalogAdminGrantAndRevokeCatalogRoles() {
+    // Create a PrincipalRole and a new catalog. Grant the catalog_admin role 
to the new principal
+    // role
+    String principalRoleName = "mypr33";
+    PrincipalRole principalRole1 = new PrincipalRole(principalRoleName);
+    createPrincipalRole(principalRole1);
+
+    String catalogName = "myuniquetestcatalog";
+    Catalog catalog =
+        PolarisCatalog.builder()
+            .setType(Catalog.TypeEnum.INTERNAL)
+            .setName(catalogName)
+            .setStorageConfigInfo(
+                new AwsStorageConfigInfo(
+                    "arn:aws:iam::012345678901:role/jdoe", 
StorageConfigInfo.StorageTypeEnum.S3))
+            .setProperties(new CatalogProperties("s3://bucket1/"))
+            .build();
+    createCatalog(catalog);
+
+    CatalogRole catalogAdminRole = readCatalogRole(catalogName, 
"catalog_admin");
+    grantCatalogRoleToPrincipalRole(principalRoleName, catalogName, 
catalogAdminRole, userToken);
+
+    PrincipalWithCredentials catalogAdminPrincipal = 
createPrincipal("principal1");
+
+    
grantPrincipalRoleToPrincipal(catalogAdminPrincipal.getPrincipal().getName(), 
principalRole1);
+
+    String catalogAdminToken =
+        TokenUtils.getTokenFromSecrets(
+            EXT.client(),
+            EXT.getLocalPort(),
+            catalogAdminPrincipal.getCredentials().getClientId(),
+            catalogAdminPrincipal.getCredentials().getClientSecret(),
+            realm);
+
+    // Create a second principal role. Use the catalog admin principal to list 
principal roles and
+    // grant a catalog role to the new principal role
+    String principalRoleName2 = "mypr2";
+    PrincipalRole principalRole2 = new PrincipalRole(principalRoleName2);
+    createPrincipalRole(principalRole2);
+
+    // create a catalog role and grant it manage_content privilege
+    String catalogRoleName = "mycr1";
+    createCatalogRole(catalogName, catalogRoleName, catalogAdminToken);
+
+    CatalogPrivilege privilege = CatalogPrivilege.CATALOG_MANAGE_CONTENT;
+    grantPrivilegeToCatalogRole(
+        catalogName,
+        catalogRoleName,
+        new CatalogGrant(privilege, GrantResource.TypeEnum.CATALOG),
+        catalogAdminToken,
+        Response.Status.CREATED);
+
+    // The catalog admin can grant the new catalog role to the mypr2 principal 
role
+    grantCatalogRoleToPrincipalRole(
+        principalRoleName2, catalogName, new CatalogRole(catalogRoleName), 
catalogAdminToken);
+
+    // But the catalog admin cannot revoke the role because it requires
+    // PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE
+    try (Response response =
+        newRequest(
+                "http://localhost:%d/api/management/v1/principal-roles/";
+                    + principalRoleName
+                    + "/catalog-roles/"
+                    + catalogName
+                    + "/"
+                    + catalogRoleName,
+                catalogAdminToken)
+            .delete()) {
+      assertThat(response).returns(Response.Status.FORBIDDEN.getStatusCode(), 
Response::getStatus);
+    }
+
+    // The service admin can revoke the role because it has the
+    // PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE privilege
+    try (Response response =
+        newRequest(
+                "http://localhost:%d/api/management/v1/principal-roles/";
+                    + principalRoleName
+                    + "/catalog-roles/"
+                    + catalogName
+                    + "/"
+                    + catalogRoleName,
+                userToken)
+            .delete()) {
+      assertThat(response).returns(Response.Status.NO_CONTENT.getStatusCode(), 
Response::getStatus);
+    }
+  }
+
+  @Test
+  public void testServiceAdminCanTransferCatalogAdmin() {
+    // Create a PrincipalRole and a new catalog. Grant the catalog_admin role 
to the new principal
+    // role
+    String principalRoleName = "mypr33";
+    PrincipalRole principalRole1 = new PrincipalRole(principalRoleName);
+    createPrincipalRole(principalRole1);
+
+    String catalogName = "myothertestcatalog";
+    Catalog catalog =
+        PolarisCatalog.builder()
+            .setType(Catalog.TypeEnum.INTERNAL)
+            .setName(catalogName)
+            .setStorageConfigInfo(
+                new AwsStorageConfigInfo(
+                    "arn:aws:iam::012345678901:role/jdoe", 
StorageConfigInfo.StorageTypeEnum.S3))
+            .setProperties(new CatalogProperties("s3://bucket1/"))
+            .build();
+    createCatalog(catalog);
+
+    CatalogRole catalogAdminRole = readCatalogRole(catalogName, 
"catalog_admin");
+    grantCatalogRoleToPrincipalRole(principalRoleName, catalogName, 
catalogAdminRole, userToken);
+
+    PrincipalWithCredentials catalogAdminPrincipal = 
createPrincipal("principal1");
+
+    
grantPrincipalRoleToPrincipal(catalogAdminPrincipal.getPrincipal().getName(), 
principalRole1);
+
+    String catalogAdminToken =
+        TokenUtils.getTokenFromSecrets(
+            EXT.client(),
+            EXT.getLocalPort(),
+            catalogAdminPrincipal.getCredentials().getClientId(),
+            catalogAdminPrincipal.getCredentials().getClientSecret(),
+            realm);
+
+    // service_admin revokes the catalog_admin privilege from its principal 
role
+    try {
+      try (Response response =
+          newRequest(
+                  
"http://localhost:%d/api/management/v1/principal-roles/service_admin/catalog-roles/";
+                      + catalogName
+                      + "/catalog_admin",
+                  userToken)
+              .delete()) {
+        assertThat(response)
+            .returns(Response.Status.NO_CONTENT.getStatusCode(), 
Response::getStatus);
+      }
+
+      // the service_admin can not revoke the catalog_admin privilege from the 
new principal role
+      try (Response response =
+          newRequest(
+                  "http://localhost:%d/api/management/v1/principal-roles/";
+                      + principalRoleName
+                      + "/catalog-roles/"
+                      + catalogName
+                      + "/catalog_admin",
+                  catalogAdminToken)
+              .delete()) {
+        assertThat(response)
+            .returns(Response.Status.FORBIDDEN.getStatusCode(), 
Response::getStatus);
+      }
+    } finally {
+      // grant the admin role back to service_admin so that cleanup can happen
+      grantCatalogRoleToPrincipalRole(
+          "service_admin", catalogName, catalogAdminRole, catalogAdminToken);
+    }
+  }
+
+  @Test
+  public void testCatalogAdminGrantAndRevokeCatalogRolesFromWrongCatalog() {
+    // Create a PrincipalRole and a new catalog. Grant the catalog_admin role 
to the new principal
+    // role
+    String principalRoleName = "mypr33";
+    PrincipalRole principalRole1 = new PrincipalRole(principalRoleName);
+    createPrincipalRole(principalRole1);
+
+    // create a catalog
+    String catalogName = "mytestcatalog";
+    Catalog catalog =
+        PolarisCatalog.builder()
+            .setType(Catalog.TypeEnum.INTERNAL)
+            .setName(catalogName)
+            .setStorageConfigInfo(
+                new AwsStorageConfigInfo(
+                    "arn:aws:iam::012345678901:role/jdoe", 
StorageConfigInfo.StorageTypeEnum.S3))
+            .setProperties(new CatalogProperties("s3://bucket1/"))
+            .build();
+    createCatalog(catalog);
+
+    // create a second catalog
+    String catalogName2 = "anothercatalog";
+    Catalog catalog2 =
+        PolarisCatalog.builder()
+            .setType(Catalog.TypeEnum.INTERNAL)
+            .setName(catalogName2)
+            .setStorageConfigInfo(
+                new AwsStorageConfigInfo(
+                    "arn:aws:iam::012345678901:role/jdoe", 
StorageConfigInfo.StorageTypeEnum.S3))
+            .setProperties(new CatalogProperties("s3://bucket1/"))
+            .build();
+    createCatalog(catalog2);
+
+    // create a catalog role *in the second catalog* and grant it 
manage_content privilege
+    String catalogRoleName = "mycr1";
+    createCatalogRole(catalogName2, catalogRoleName, userToken);
+
+    // Get the catalog admin role from the *first* catalog and grant that role 
to the principal role
+    CatalogRole catalogAdminRole = readCatalogRole(catalogName, 
"catalog_admin");
+    grantCatalogRoleToPrincipalRole(principalRoleName, catalogName, 
catalogAdminRole, userToken);
+
+    // Create a principal and grant the principal role to it
+    PrincipalWithCredentials catalogAdminPrincipal = 
createPrincipal("principal1");
+    
grantPrincipalRoleToPrincipal(catalogAdminPrincipal.getPrincipal().getName(), 
principalRole1);
+
+    String catalogAdminToken =
+        TokenUtils.getTokenFromSecrets(
+            EXT.client(),
+            EXT.getLocalPort(),
+            catalogAdminPrincipal.getCredentials().getClientId(),
+            catalogAdminPrincipal.getCredentials().getClientSecret(),
+            realm);
+
+    // Create a second principal role.
+    String principalRoleName2 = "mypr2";
+    PrincipalRole principalRole2 = new PrincipalRole(principalRoleName2);
+    createPrincipalRole(principalRole2);
+
+    // The catalog admin cannot grant the new catalog role to the mypr2 
principal role because the
+    // catalog role is in the wrong catalog
+    try (Response response =
+        newRequest(
+                "http://localhost:%d/api/management/v1/principal-roles/";
+                    + principalRoleName
+                    + "/catalog-roles/"
+                    + catalogName2,
+                catalogAdminToken)
+            .put(Entity.json(new GrantCatalogRoleRequest(new 
CatalogRole(catalogRoleName))))) {
+      assertThat(response).returns(Response.Status.FORBIDDEN.getStatusCode(), 
Response::getStatus);
+    }
+  }
+
+  @Test
+  public void testTableManageAccessCanGrantAndRevokeFromCatalogRoles() {
+    // Create a PrincipalRole and a new catalog.
+    String principalRoleName = "mypr33";
+    PrincipalRole principalRole1 = new PrincipalRole(principalRoleName);
+    createPrincipalRole(principalRole1);
+
+    // create a catalog
+    String catalogName = "mytablemanagecatalog";
+    Catalog catalog =
+        PolarisCatalog.builder()
+            .setType(Catalog.TypeEnum.INTERNAL)
+            .setName(catalogName)
+            .setStorageConfigInfo(
+                new AwsStorageConfigInfo(
+                    "arn:aws:iam::012345678901:role/jdoe", 
StorageConfigInfo.StorageTypeEnum.S3))
+            .setProperties(new CatalogProperties("s3://bucket1/"))
+            .build();
+    createCatalog(catalog);
+
+    // create a valid target CatalogRole in this catalog
+    createCatalogRole(catalogName, "target_catalog_role", userToken);
+
+    // create a second catalog
+    String catalogName2 = "anothertablemanagecatalog";
+    Catalog catalog2 =
+        PolarisCatalog.builder()
+            .setType(Catalog.TypeEnum.INTERNAL)
+            .setName(catalogName2)
+            .setStorageConfigInfo(
+                new AwsStorageConfigInfo(
+                    "arn:aws:iam::012345678901:role/jdoe", 
StorageConfigInfo.StorageTypeEnum.S3))
+            .setProperties(new CatalogProperties("s3://bucket1/"))
+            .build();
+    createCatalog(catalog2);
+
+    // create an *invalid* target CatalogRole in second catalog
+    createCatalogRole(catalogName2, "invalid_target_catalog_role", userToken);
+
+    // create the namespace "c" in *both* namespaces
+    String namespaceName = "c";
+    createNamespace(catalogName, namespaceName);
+    createNamespace(catalogName2, namespaceName);
+
+    // create a catalog role *in the first catalog* and grant it 
manage_content privilege at the
+    // namespace level
+    // grant that role to the PrincipalRole
+    String catalogRoleName = "ns_manage_access_role";
+    createCatalogRole(catalogName, catalogRoleName, userToken);
+    grantPrivilegeToCatalogRole(
+        catalogName,
+        catalogRoleName,
+        new NamespaceGrant(
+            List.of(namespaceName),
+            NamespacePrivilege.CATALOG_MANAGE_ACCESS,
+            GrantResource.TypeEnum.NAMESPACE),
+        userToken,
+        Response.Status.CREATED);
+
+    grantCatalogRoleToPrincipalRole(
+        principalRoleName, catalogName, new CatalogRole(catalogRoleName), 
userToken);
+
+    // Create a principal and grant the principal role to it
+    PrincipalWithCredentials catalogAdminPrincipal = 
createPrincipal("ns_manage_access_user");
+    
grantPrincipalRoleToPrincipal(catalogAdminPrincipal.getPrincipal().getName(), 
principalRole1);
+
+    String manageAccessUserToken =
+        TokenUtils.getTokenFromSecrets(
+            EXT.client(),
+            EXT.getLocalPort(),
+            catalogAdminPrincipal.getCredentials().getClientId(),
+            catalogAdminPrincipal.getCredentials().getClientSecret(),
+            realm);
+
+    // Use the ns_manage_access_user to grant TABLE_CREATE access to the 
target catalog role
+    // This works because the user has CATALOG_MANAGE_ACCESS within the 
namespace and the target
+    // catalog role is in
+    // the same catalog
+    grantPrivilegeToCatalogRole(
+        catalogName,
+        "target_catalog_role",
+        new NamespaceGrant(
+            List.of(namespaceName),
+            NamespacePrivilege.TABLE_CREATE,
+            GrantResource.TypeEnum.NAMESPACE),
+        manageAccessUserToken,
+        Response.Status.CREATED);
+
+    // Even though the ns_manage_access_role can grant privileges to the 
catalog role, it cannot
+    // grant the target
+    // catalog role to the mypr2 principal role because it doesn't have 
privilege to manage grants
+    // on the catalog role
+    // as a securable
+    try (Response response =
+        newRequest(
+                "http://localhost:%d/api/management/v1/principal-roles/";
+                    + principalRoleName
+                    + "/catalog-roles/"
+                    + catalogName,
+                manageAccessUserToken)
+            .put(
+                Entity.json(new GrantCatalogRoleRequest(new 
CatalogRole("target_catalog_role"))))) {
+      assertThat(response).returns(Response.Status.FORBIDDEN.getStatusCode(), 
Response::getStatus);
+    }
+
+    // The user cannot grant catalog-level privileges to the catalog role
+    grantPrivilegeToCatalogRole(
+        catalogName,
+        "target_catalog_role",
+        new CatalogGrant(CatalogPrivilege.TABLE_CREATE, 
GrantResource.TypeEnum.CATALOG),
+        manageAccessUserToken,
+        Response.Status.FORBIDDEN);
+
+    // even though the namespace "c" exists in both catalogs, the 
ns_manage_access_role can only
+    // grant privileges for
+    // the namespace in its own catalog
+    grantPrivilegeToCatalogRole(
+        catalogName2,
+        "invalid_target_catalog_role",
+        new NamespaceGrant(
+            List.of(namespaceName),
+            NamespacePrivilege.TABLE_CREATE,
+            GrantResource.TypeEnum.NAMESPACE),
+        manageAccessUserToken,
+        Response.Status.FORBIDDEN);
+
+    // nor can it grant privileges to the catalog role in the second catalog
+    grantPrivilegeToCatalogRole(
+        catalogName2,
+        "invalid_target_catalog_role",
+        new CatalogGrant(CatalogPrivilege.TABLE_CREATE, 
GrantResource.TypeEnum.CATALOG),
+        manageAccessUserToken,
+        Response.Status.FORBIDDEN);
+  }
+
+  private static void createNamespace(String catalogName, String 
namespaceName) {
+    try (Response response =
+        newRequest("http://localhost:%d/api/catalog/v1/"; + catalogName + 
"/namespaces", userToken)
+            .post(
+                Entity.json(
+                    CreateNamespaceRequest.builder()
+                        .withNamespace(Namespace.of(namespaceName))
+                        .build()))) {
+      assertThat(response).returns(Response.Status.OK.getStatusCode(), 
Response::getStatus);
+    }
+  }
+
+  private static void createCatalog(Catalog catalog) {
+    try (Response response =
+        newRequest("http://localhost:%d/api/management/v1/catalogs";)
+            .post(Entity.json(new CreateCatalogRequest(catalog)))) {
+
+      assertThat(response).returns(Response.Status.CREATED.getStatusCode(), 
Response::getStatus);
+    }
+  }
+
+  private static void grantPrivilegeToCatalogRole(
+      String catalogName,
+      String catalogRoleName,
+      GrantResource grant,
+      String catalogAdminToken,
+      Response.Status expectedStatus) {
+    try (Response response =
+        newRequest(
+                "http://localhost:%d/api/management/v1/catalogs/";
+                    + catalogName
+                    + "/catalog-roles/"
+                    + catalogRoleName
+                    + "/grants",
+                catalogAdminToken)
+            .put(Entity.json(new AddGrantRequest(grant)))) {
+      assertThat(response).returns(expectedStatus.getStatusCode(), 
Response::getStatus);
+    }
+  }
+
+  private static void createCatalogRole(
+      String catalogName, String catalogRoleName, String catalogAdminToken) {
+    try (Response response =
+        newRequest(
+                "http://localhost:%d/api/management/v1/catalogs/"; + 
catalogName + "/catalog-roles",
+                catalogAdminToken)
+            .post(Entity.json(new CreateCatalogRoleRequest(new 
CatalogRole(catalogRoleName))))) {
+      assertThat(response).returns(Response.Status.CREATED.getStatusCode(), 
Response::getStatus);
+    }
+  }
+
+  private static void grantPrincipalRoleToPrincipal(
+      String principalName, PrincipalRole principalRole) {
+    try (Response response =
+        newRequest(
+                "http://localhost:%d/api/management/v1/principals/";
+                    + principalName
+                    + "/principal-roles")
+            .put(Entity.json(new GrantPrincipalRoleRequest(principalRole)))) {
+      assertThat(response).returns(Response.Status.CREATED.getStatusCode(), 
Response::getStatus);
+    }
+  }
+
+  private static PrincipalWithCredentials createPrincipal(String 
principalName) {
+    PrincipalWithCredentials catalogAdminPrincipal;
+    try (Response response =
+        newRequest("http://localhost:%d/api/management/v1/principals";)
+            .post(Entity.json(new CreatePrincipalRequest(new 
Principal(principalName), false)))) {
+      assertThat(response).returns(Response.Status.CREATED.getStatusCode(), 
Response::getStatus);
+      catalogAdminPrincipal = 
response.readEntity(PrincipalWithCredentials.class);
+    }
+    return catalogAdminPrincipal;
+  }
+
+  private static void grantCatalogRoleToPrincipalRole(
+      String principalRoleName, String catalogName, CatalogRole catalogRole, 
String token) {
+    try (Response response =
+        newRequest(
+                "http://localhost:%d/api/management/v1/principal-roles/";
+                    + principalRoleName
+                    + "/catalog-roles/"
+                    + catalogName,
+                token)
+            .put(Entity.json(new GrantCatalogRoleRequest(catalogRole)))) {
+      assertThat(response).returns(Response.Status.CREATED.getStatusCode(), 
Response::getStatus);
+    }
+  }
+
+  private static CatalogRole readCatalogRole(String catalogName, String 
roleName) {
+    try (Response response =
+        newRequest(
+                "http://localhost:%d/api/management/v1/catalogs/";
+                    + catalogName
+                    + "/catalog-roles/"
+                    + roleName)
+            .get()) {
+
+      assertThat(response).returns(Response.Status.OK.getStatusCode(), 
Response::getStatus);
+      return response.readEntity(CatalogRole.class);
+    }
+  }
+
+  private static void createPrincipalRole(PrincipalRole principalRole1) {
+    try (Response response =
+        newRequest("http://localhost:%d/api/management/v1/principal-roles";)
+            .post(Entity.json(new 
CreatePrincipalRoleRequest(principalRole1)))) {
+
+      assertThat(response).returns(Response.Status.CREATED.getStatusCode(), 
Response::getStatus);
+    }
+  }
 }


Reply via email to