This is an automated email from the ASF dual-hosted git repository. jshao pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/gravitino.git
commit f2aaa9fb9bb5f9ff7c9663058f68e8ff6e1e0fcc Author: yangyang zhong <[email protected]> AuthorDate: Tue Jul 1 12:10:12 2025 +0800 [#7449]feat(authz): Support Catalog Authorization (#7450) ### What changes were proposed in this pull request? Support Catalog Authorization ### Why are the changes needed? Fix: #7449 ### Does this PR introduce _any_ user-facing change? None ### How was this patch tested? org.apache.gravitino.client.integration.test.authorization.CatalogAuthorizationIT --- .../authorization/BaseRestApiAuthorizationIT.java | 81 ++++++++++++++ .../test/authorization/CatalogAuthorizationIT.java | 118 +++++++++++++++++++++ .../main/java/org/apache/gravitino/Configs.java | 7 ++ .../authentication/AuthenticationFilter.java | 12 ++- .../authorization/GravitinoAuthorizerProvider.java | 22 +++- .../annotations/AuthorizationExpression.java | 8 ++ .../authorization/annotations/TestAnnotations.java | 4 +- .../web/filter/GravitinoInterceptionService.java | 40 ++++--- .../server/web/rest/CatalogOperations.java | 75 +++++++++++-- .../filter/TestGravitinoInterceptionService.java | 7 +- .../server/web/rest/BaseOperationsTest.java | 39 ++++--- .../server/web/rest/TestCatalogOperations.java | 3 +- 12 files changed, 362 insertions(+), 54 deletions(-) diff --git a/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/BaseRestApiAuthorizationIT.java b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/BaseRestApiAuthorizationIT.java new file mode 100644 index 0000000000..bcf2066cae --- /dev/null +++ b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/BaseRestApiAuthorizationIT.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.gravitino.client.integration.test.authorization; + +import java.io.IOException; +import java.util.HashMap; +import org.apache.gravitino.Configs; +import org.apache.gravitino.client.GravitinoAdminClient; +import org.apache.gravitino.integration.test.util.BaseIT; +import org.apache.gravitino.server.authorization.jcasbin.JcasbinAuthorizer; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.shaded.com.google.common.collect.ImmutableMap; + +public class BaseRestApiAuthorizationIT extends BaseIT { + + protected static final String METALAKE = "testMetalake"; + + protected static final String USER = "tester"; + + protected static final String USER_WITH_AUTHORIZATION = "tester2"; + + /** Mock a user without permissions. */ + protected static GravitinoAdminClient clientWithNoAuthorization; + + private static final Logger LOG = LoggerFactory.getLogger(BaseRestApiAuthorizationIT.class); + + @BeforeAll + @Override + public void startIntegrationTest() throws Exception { + // Enable authorization + customConfigs.putAll( + ImmutableMap.of( + "SimpleAuthUserName", + USER, + Configs.ENABLE_AUTHORIZATION.getKey(), + "true", + Configs.AUTHORIZATION_IMPL.getKey(), + JcasbinAuthorizer.class.getCanonicalName())); + super.startIntegrationTest(); + client.createMetalake(METALAKE, "", new HashMap<>()); + client.loadMetalake(METALAKE).addUser(USER_WITH_AUTHORIZATION); + clientWithNoAuthorization = + GravitinoAdminClient.builder(serverUri).withSimpleAuth(USER_WITH_AUTHORIZATION).build(); + } + + @AfterAll + @Override + public void stopIntegrationTest() throws IOException, InterruptedException { + client.dropMetalake(METALAKE, true); + + if (clientWithNoAuthorization != null) { + clientWithNoAuthorization.close(); + clientWithNoAuthorization = null; + } + + try { + closer.close(); + } catch (Exception e) { + LOG.error("Exception in closing CloseableGroup", e); + } + super.stopIntegrationTest(); + } +} diff --git a/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/CatalogAuthorizationIT.java b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/CatalogAuthorizationIT.java new file mode 100644 index 0000000000..105f7cef66 --- /dev/null +++ b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/CatalogAuthorizationIT.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.gravitino.client.integration.test.authorization; + +import static org.junit.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.common.collect.Maps; +import java.util.Map; +import org.apache.gravitino.Catalog; +import org.apache.gravitino.integration.test.container.ContainerSuite; +import org.apache.gravitino.integration.test.container.HiveContainer; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +@Tag("gravitino-docker-test") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class CatalogAuthorizationIT extends BaseRestApiAuthorizationIT { + + private String catalog1 = "catalog1"; + + private String catalog2 = "catalog2"; + + private static final ContainerSuite containerSuite = ContainerSuite.getInstance(); + + private static String hmsUri; + + @BeforeAll + public void startIntegrationTest() throws Exception { + containerSuite.startHiveContainer(); + super.startIntegrationTest(); + hmsUri = + String.format( + "thrift://%s:%d", + containerSuite.getHiveContainer().getContainerIpAddress(), + HiveContainer.HIVE_METASTORE_PORT); + } + + @Test + @Order(1) + public void testCreateCatalog() { + Map<String, String> properties = Maps.newHashMap(); + properties.put("metastore.uris", hmsUri); + assertThrows( + "Can not access metadata {" + catalog1 + "}.", + RuntimeException.class, + () -> { + clientWithNoAuthorization + .loadMetalake(METALAKE) + .createCatalog(catalog1, Catalog.Type.RELATIONAL, "hive", "comment", properties); + }); + client + .loadMetalake(METALAKE) + .createCatalog(catalog1, Catalog.Type.RELATIONAL, "hive", "comment", properties); + client + .loadMetalake(METALAKE) + .createCatalog(catalog2, Catalog.Type.RELATIONAL, "hive", "comment", properties); + assertThrows( + "Can not access metadata {" + catalog1 + "}.", + RuntimeException.class, + () -> { + clientWithNoAuthorization + .loadMetalake(METALAKE) + .createCatalog(catalog1, Catalog.Type.RELATIONAL, "hive", "comment", properties); + }); + } + + @Test + @Order(2) + public void testListCatalog() { + String[] catalogs = clientWithNoAuthorization.loadMetalake(METALAKE).listCatalogs(); + assertEquals(0, catalogs.length); + catalogs = client.loadMetalake(METALAKE).listCatalogs(); + assertEquals(2, catalogs.length); + assertArrayEquals(new String[] {catalog1, catalog2}, catalogs); + } + + @Test + @Order(3) + public void testDeleteCatalog() { + String[] catalogs = client.loadMetalake(METALAKE).listCatalogs(); + assertEquals(2, catalogs.length); + assertArrayEquals(new String[] {catalog1, catalog2}, catalogs); + assertThrows( + "Can not access metadata {" + catalog1 + "}.", + RuntimeException.class, + () -> { + clientWithNoAuthorization.loadMetalake(METALAKE).dropCatalog(catalog1, true); + }); + client.loadMetalake(METALAKE).dropCatalog(catalog1, true); + catalogs = client.loadMetalake(METALAKE).listCatalogs(); + assertEquals(1, catalogs.length); + assertArrayEquals(new String[] {catalog2}, catalogs); + client.loadMetalake(METALAKE).dropCatalog(catalog2, true); + catalogs = client.loadMetalake(METALAKE).listCatalogs(); + assertEquals(0, catalogs.length); + } +} diff --git a/core/src/main/java/org/apache/gravitino/Configs.java b/core/src/main/java/org/apache/gravitino/Configs.java index dd06a0496c..24d445ccb7 100644 --- a/core/src/main/java/org/apache/gravitino/Configs.java +++ b/core/src/main/java/org/apache/gravitino/Configs.java @@ -285,6 +285,13 @@ public class Configs { .booleanConf() .createWithDefault(false); + public static final ConfigEntry<String> AUTHORIZATION_IMPL = + new ConfigBuilder("gravitino.authorization.impl") + .doc("Metadata authorization implementation") + .version(ConfigConstants.VERSION_1_0_0) + .stringConf() + .createWithDefault("org.apache.gravitino.server.authorization.PassThroughAuthorizer"); + public static final ConfigEntry<List<String>> SERVICE_ADMINS = new ConfigBuilder("gravitino.authorization.serviceAdmins") .doc("The admins of Gravitino service") diff --git a/server-common/src/main/java/org/apache/gravitino/server/authentication/AuthenticationFilter.java b/server-common/src/main/java/org/apache/gravitino/server/authentication/AuthenticationFilter.java index 8e376738f7..7df4e68d6a 100644 --- a/server-common/src/main/java/org/apache/gravitino/server/authentication/AuthenticationFilter.java +++ b/server-common/src/main/java/org/apache/gravitino/server/authentication/AuthenticationFilter.java @@ -34,6 +34,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.gravitino.auth.AuthConstants; import org.apache.gravitino.exceptions.UnauthorizedException; +import org.apache.gravitino.utils.PrincipalUtils; public class AuthenticationFilter implements Filter { @@ -82,8 +83,12 @@ public class AuthenticationFilter implements Filter { if (principal == null) { throw new UnauthorizedException("The provided credentials did not support"); } - - chain.doFilter(request, response); + PrincipalUtils.doAs( + principal, + () -> { + chain.doFilter(request, response); + return null; + }); } catch (UnauthorizedException ue) { HttpServletResponse resp = (HttpServletResponse) response; if (!ue.getChallenges().isEmpty()) { @@ -95,6 +100,9 @@ public class AuthenticationFilter implements Filter { } } resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, ue.getMessage()); + } catch (Exception e) { + HttpServletResponse resp = (HttpServletResponse) response; + resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage()); } } diff --git a/server-common/src/main/java/org/apache/gravitino/server/authorization/GravitinoAuthorizerProvider.java b/server-common/src/main/java/org/apache/gravitino/server/authorization/GravitinoAuthorizerProvider.java index 3d837ba955..e909ae30a5 100644 --- a/server-common/src/main/java/org/apache/gravitino/server/authorization/GravitinoAuthorizerProvider.java +++ b/server-common/src/main/java/org/apache/gravitino/server/authorization/GravitinoAuthorizerProvider.java @@ -17,9 +17,10 @@ package org.apache.gravitino.server.authorization; +import java.io.Closeable; +import java.io.IOException; import org.apache.gravitino.Configs; import org.apache.gravitino.server.ServerConfig; -import org.apache.gravitino.server.authorization.jcasbin.JcasbinAuthorizer; /** * Used to initialize and store {@link GravitinoAuthorizer}. When Gravitino Server starts up, it @@ -27,7 +28,7 @@ import org.apache.gravitino.server.authorization.jcasbin.JcasbinAuthorizer; * in the GravitinoAuthorizerProvider. The GravitinoAuthorizer instance can then be retrieved using * the getGravitinoAuthorizer method. */ -public class GravitinoAuthorizerProvider { +public class GravitinoAuthorizerProvider implements Closeable { private static final GravitinoAuthorizerProvider INSTANCE = new GravitinoAuthorizerProvider(); @@ -47,7 +48,14 @@ public class GravitinoAuthorizerProvider { if (gravitinoAuthorizer == null) { boolean enableAuthorization = serverConfig.get(Configs.ENABLE_AUTHORIZATION); if (enableAuthorization) { - gravitinoAuthorizer = new JcasbinAuthorizer(); + String authorizationImpl = serverConfig.get(Configs.AUTHORIZATION_IMPL); + try { + gravitinoAuthorizer = + (GravitinoAuthorizer) + Class.forName(authorizationImpl).getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new IllegalArgumentException("Can not initialize GravitinoAuthorizer", e); + } } else { gravitinoAuthorizer = new PassThroughAuthorizer(); } @@ -69,4 +77,12 @@ public class GravitinoAuthorizerProvider { public GravitinoAuthorizer getGravitinoAuthorizer() { return gravitinoAuthorizer; } + + @Override + public void close() throws IOException { + if (gravitinoAuthorizer != null) { + gravitinoAuthorizer.close(); + } + gravitinoAuthorizer = null; + } } diff --git a/server-common/src/main/java/org/apache/gravitino/server/authorization/annotations/AuthorizationExpression.java b/server-common/src/main/java/org/apache/gravitino/server/authorization/annotations/AuthorizationExpression.java index 6173f61158..58be4fb206 100644 --- a/server-common/src/main/java/org/apache/gravitino/server/authorization/annotations/AuthorizationExpression.java +++ b/server-common/src/main/java/org/apache/gravitino/server/authorization/annotations/AuthorizationExpression.java @@ -22,6 +22,7 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.apache.gravitino.MetadataObject; /** * This annotation is used to implement unified authentication in AOP. Use Expressions to define the @@ -36,4 +37,11 @@ public @interface AuthorizationExpression { * @return the expression to evaluate for authorization. */ String expression() default ""; + + /** + * Used to identify the type of metadata that needs to be accessed + * + * @return accessMetadataType + */ + MetadataObject.Type accessMetadataType(); } diff --git a/server-common/src/test/java/org/apache/gravitino/server/authorization/annotations/TestAnnotations.java b/server-common/src/test/java/org/apache/gravitino/server/authorization/annotations/TestAnnotations.java index 75a041eabb..556a0ce368 100644 --- a/server-common/src/test/java/org/apache/gravitino/server/authorization/annotations/TestAnnotations.java +++ b/server-common/src/test/java/org/apache/gravitino/server/authorization/annotations/TestAnnotations.java @@ -52,7 +52,9 @@ public class TestAnnotations { metadataType = MetadataObject.Type.CATALOG) public void testAuthedMethodUseResourceType() {} - @AuthorizationExpression(expression = "CATALOG::CREATE_TABLE || TABLE::CREATE_TABLE") + @AuthorizationExpression( + expression = "CATALOG::CREATE_TABLE || TABLE::CREATE_TABLE", + accessMetadataType = MetadataObject.Type.METALAKE) public void testAuthedMethodUseExpression() {} } diff --git a/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java b/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java index 098490d432..412b78f4f6 100644 --- a/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java +++ b/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java @@ -34,6 +34,7 @@ import org.aopalliance.intercept.MethodInvocation; import org.apache.gravitino.Entity; import org.apache.gravitino.MetadataObject; import org.apache.gravitino.NameIdentifier; +import org.apache.gravitino.exceptions.ForbiddenException; import org.apache.gravitino.server.authorization.annotations.AuthorizationExpression; import org.apache.gravitino.server.authorization.annotations.AuthorizationMetadata; import org.apache.gravitino.server.authorization.expression.AuthorizationExpressionEvaluator; @@ -82,23 +83,32 @@ public class GravitinoInterceptionService implements InterceptionService { */ @Override public Object invoke(MethodInvocation methodInvocation) throws Throwable { - Method method = methodInvocation.getMethod(); - Parameter[] parameters = method.getParameters(); - AuthorizationExpression expressionAnnotation = - method.getAnnotation(AuthorizationExpression.class); - if (expressionAnnotation != null) { - String expression = expressionAnnotation.expression(); - Object[] args = methodInvocation.getArguments(); - Map<Entity.EntityType, NameIdentifier> metadataContext = - extractNameIdentifierFromParameters(parameters, args); - AuthorizationExpressionEvaluator authorizationExpressionEvaluator = - new AuthorizationExpressionEvaluator(expression); - boolean authorizeResult = authorizationExpressionEvaluator.evaluate(metadataContext); - if (!authorizeResult) { - return Utils.internalError("Can not access metadata."); + try { + Method method = methodInvocation.getMethod(); + Parameter[] parameters = method.getParameters(); + AuthorizationExpression expressionAnnotation = + method.getAnnotation(AuthorizationExpression.class); + if (expressionAnnotation != null) { + String expression = expressionAnnotation.expression(); + Object[] args = methodInvocation.getArguments(); + Map<Entity.EntityType, NameIdentifier> metadataContext = + extractNameIdentifierFromParameters(parameters, args); + AuthorizationExpressionEvaluator authorizationExpressionEvaluator = + new AuthorizationExpressionEvaluator(expression); + boolean authorizeResult = authorizationExpressionEvaluator.evaluate(metadataContext); + if (!authorizeResult) { + MetadataObject.Type type = expressionAnnotation.accessMetadataType(); + NameIdentifier accessMetadataName = + metadataContext.get(Entity.EntityType.valueOf(type.name())); + return Utils.forbidden( + String.format("Can not access metadata {%s}.", accessMetadataName.name()), + new ForbiddenException("Can not access metadata {%s}.", accessMetadataName)); + } } + return methodInvocation.proceed(); + } catch (Exception ex) { + return Utils.forbidden("Can not access metadata. Cause by: " + ex.getMessage(), ex); } - return methodInvocation.proceed(); } private Map<Entity.EntityType, NameIdentifier> extractNameIdentifierFromParameters( diff --git a/server/src/main/java/org/apache/gravitino/server/web/rest/CatalogOperations.java b/server/src/main/java/org/apache/gravitino/server/web/rest/CatalogOperations.java index 9acd635f0c..367d3bfa36 100644 --- a/server/src/main/java/org/apache/gravitino/server/web/rest/CatalogOperations.java +++ b/server/src/main/java/org/apache/gravitino/server/web/rest/CatalogOperations.java @@ -20,6 +20,8 @@ package org.apache.gravitino.server.web.rest; import com.codahale.metrics.annotation.ResponseMetered; import com.codahale.metrics.annotation.Timed; +import java.util.Arrays; +import java.util.stream.Collectors; import javax.inject.Inject; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Consumes; @@ -38,6 +40,7 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import org.apache.gravitino.Catalog; import org.apache.gravitino.CatalogChange; +import org.apache.gravitino.Entity; import org.apache.gravitino.MetadataObject; import org.apache.gravitino.NameIdentifier; import org.apache.gravitino.Namespace; @@ -53,6 +56,8 @@ import org.apache.gravitino.dto.responses.DropResponse; import org.apache.gravitino.dto.responses.EntityListResponse; import org.apache.gravitino.dto.util.DTOConverters; import org.apache.gravitino.metrics.MetricNames; +import org.apache.gravitino.server.authorization.MetadataFilterHelper; +import org.apache.gravitino.server.authorization.annotations.AuthorizationExpression; import org.apache.gravitino.server.authorization.annotations.AuthorizationMetadata; import org.apache.gravitino.server.web.Utils; import org.apache.gravitino.utils.NameIdentifierUtil; @@ -95,11 +100,36 @@ public class CatalogOperations { // Lock the root and the metalake with WRITE lock to ensure the consistency of the list. if (verbose) { Catalog[] catalogs = catalogDispatcher.listCatalogsInfo(catalogNS); + Arrays.stream(catalogs) + .filter( + catalog -> { + NameIdentifier[] nameIdentifiers = + new NameIdentifier[] { + NameIdentifierUtil.ofCatalog(metalake, catalog.name()) + }; + return MetadataFilterHelper.filterByExpression( + metalake, + "METALAKE::USE_CATALOG || CATALOG::USE_CATALOG " + + "|| METALAKE::OWNER || CATALOG::OWNER", + Entity.EntityType.CATALOG, + nameIdentifiers) + .length + > 0; + }) + .collect(Collectors.toList()) + .toArray(new Catalog[0]); Response response = Utils.ok(new CatalogListResponse(DTOConverters.toDTOs(catalogs))); LOG.info("List {} catalogs info under metalake: {}", catalogs.length, metalake); return response; } else { NameIdentifier[] idents = catalogDispatcher.listCatalogs(catalogNS); + idents = + MetadataFilterHelper.filterByExpression( + metalake, + "METALAKE::USE_CATALOG || CATALOG::USE_CATALOG " + + "|| METALAKE::OWNER || CATALOG::OWNER", + Entity.EntityType.CATALOG, + idents); Response response = Utils.ok(new EntityListResponse(idents)); LOG.info("List {} catalogs under metalake: {}", idents.length, metalake); return response; @@ -114,6 +144,9 @@ public class CatalogOperations { @Produces("application/vnd.gravitino.v1+json") @Timed(name = "create-catalog." + MetricNames.HTTP_PROCESS_DURATION, absolute = true) @ResponseMetered(name = "create-catalog", absolute = true) + @AuthorizationExpression( + expression = "METALAKE::CREATE_CATALOG || METALAKE::OWNER", + accessMetadataType = MetadataObject.Type.CATALOG) public Response createCatalog( @PathParam("metalake") @AuthorizationMetadata(type = MetadataObject.Type.METALAKE) String metalake, @@ -149,7 +182,9 @@ public class CatalogOperations { @Timed(name = "test-connection." + MetricNames.HTTP_PROCESS_DURATION, absolute = true) @ResponseMetered(name = "test-connection", absolute = true) public Response testConnection( - @PathParam("metalake") String metalake, CatalogCreateRequest request) { + @PathParam("metalake") @AuthorizationMetadata(type = MetadataObject.Type.METALAKE) + String metalake, + CatalogCreateRequest request) { LOG.info("Received test connection request for catalog: {}.{}", metalake, request.getName()); try { return Utils.doAs( @@ -180,9 +215,15 @@ public class CatalogOperations { @Produces("application/vnd.gravitino.v1+json") @Timed(name = "set-catalog." + MetricNames.HTTP_PROCESS_DURATION, absolute = true) @ResponseMetered(name = "set-catalog", absolute = true) + @AuthorizationExpression( + expression = + "METALAKE::USE_CATALOG || CATALOG::USE_CATALOG || METALAKE::OWNER || CATALOG::OWNER", + accessMetadataType = MetadataObject.Type.CATALOG) public Response setCatalog( - @PathParam("metalake") String metalake, - @PathParam("catalog") String catalogName, + @PathParam("metalake") @AuthorizationMetadata(type = MetadataObject.Type.METALAKE) + String metalake, + @PathParam("catalog") @AuthorizationMetadata(type = MetadataObject.Type.CATALOG) + String catalogName, CatalogSetRequest request) { LOG.info("Received set request for catalog: {}.{}", metalake, catalogName); try { @@ -221,8 +262,15 @@ public class CatalogOperations { @Produces("application/vnd.gravitino.v1+json") @Timed(name = "load-catalog." + MetricNames.HTTP_PROCESS_DURATION, absolute = true) @ResponseMetered(name = "load-catalog", absolute = true) + @AuthorizationExpression( + expression = + "METALAKE::USE_CATALOG || CATALOG::USE_CATALOG || METALAKE::OWNER || CATALOG::OWNER", + accessMetadataType = MetadataObject.Type.CATALOG) public Response loadCatalog( - @PathParam("metalake") String metalakeName, @PathParam("catalog") String catalogName) { + @PathParam("metalake") @AuthorizationMetadata(type = MetadataObject.Type.METALAKE) + String metalakeName, + @PathParam("catalog") @AuthorizationMetadata(type = MetadataObject.Type.CATALOG) + String catalogName) { LOG.info("Received load catalog request for catalog: {}.{}", metalakeName, catalogName); try { NameIdentifier ident = NameIdentifierUtil.ofCatalog(metalakeName, catalogName); @@ -242,9 +290,14 @@ public class CatalogOperations { @Produces("application/vnd.gravitino.v1+json") @Timed(name = "alter-catalog." + MetricNames.HTTP_PROCESS_DURATION, absolute = true) @ResponseMetered(name = "alter-catalog", absolute = true) + @AuthorizationExpression( + expression = "METALAKE::OWNER || CATALOG::OWNER", + accessMetadataType = MetadataObject.Type.CATALOG) public Response alterCatalog( - @PathParam("metalake") String metalakeName, - @PathParam("catalog") String catalogName, + @PathParam("metalake") @AuthorizationMetadata(type = MetadataObject.Type.METALAKE) + String metalakeName, + @PathParam("catalog") @AuthorizationMetadata(type = MetadataObject.Type.CATALOG) + String catalogName, CatalogUpdatesRequest request) { LOG.info("Received alter catalog request for catalog: {}.{}", metalakeName, catalogName); try { @@ -274,9 +327,14 @@ public class CatalogOperations { @Produces("application/vnd.gravitino.v1+json") @Timed(name = "drop-catalog." + MetricNames.HTTP_PROCESS_DURATION, absolute = true) @ResponseMetered(name = "drop-catalog", absolute = true) + @AuthorizationExpression( + expression = "METALAKE::OWNER || CATALOG::OWNER", + accessMetadataType = MetadataObject.Type.CATALOG) public Response dropCatalog( - @PathParam("metalake") String metalakeName, - @PathParam("catalog") String catalogName, + @PathParam("metalake") @AuthorizationMetadata(type = MetadataObject.Type.METALAKE) + String metalakeName, + @PathParam("catalog") @AuthorizationMetadata(type = MetadataObject.Type.CATALOG) + String catalogName, @DefaultValue("false") @QueryParam("force") boolean force) { LOG.info("Received drop catalog request for catalog: {}.{}", metalakeName, catalogName); try { @@ -288,7 +346,6 @@ public class CatalogOperations { if (!dropped) { LOG.warn("Failed to drop catalog {} under metalake {}", catalogName, metalakeName); } - Response response = Utils.ok(new DropResponse(dropped)); LOG.info("Catalog dropped: {}.{}", metalakeName, catalogName); return response; diff --git a/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java b/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java index 591935c8eb..41a8c8a1d4 100644 --- a/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java +++ b/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java @@ -75,13 +75,16 @@ public class TestGravitinoInterceptionService { when(methodInvocation.getArguments()).thenReturn(new Object[] {"testMetalake2"}); Response response2 = (Response) methodInterceptor.invoke(methodInvocation); assertEquals( - "Can not access metadata.", ((ErrorResponse) response2.getEntity()).getMessage()); + "Can not access metadata {testMetalake2}.", + ((ErrorResponse) response2.getEntity()).getMessage()); } } public static class TestOperations { - @AuthorizationExpression(expression = "METALAKE::USE_CATALOG || METALAKE::OWNER") + @AuthorizationExpression( + expression = "METALAKE::USE_CATALOG || METALAKE::OWNER", + accessMetadataType = MetadataObject.Type.METALAKE) public Response testMethod( @AuthorizationMetadata(type = MetadataObject.Type.METALAKE) String metalake) { return Utils.ok("ok"); diff --git a/server-common/src/main/java/org/apache/gravitino/server/authorization/annotations/AuthorizationExpression.java b/server/src/test/java/org/apache/gravitino/server/web/rest/BaseOperationsTest.java similarity index 54% copy from server-common/src/main/java/org/apache/gravitino/server/authorization/annotations/AuthorizationExpression.java copy to server/src/test/java/org/apache/gravitino/server/web/rest/BaseOperationsTest.java index 6173f61158..97933126d6 100644 --- a/server-common/src/main/java/org/apache/gravitino/server/authorization/annotations/AuthorizationExpression.java +++ b/server/src/test/java/org/apache/gravitino/server/web/rest/BaseOperationsTest.java @@ -6,9 +6,7 @@ * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at - * * http://www.apache.org/licenses/LICENSE-2.0 - * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @@ -16,24 +14,25 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.gravitino.server.authorization.annotations; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; +package org.apache.gravitino.server.web.rest; -/** - * This annotation is used to implement unified authentication in AOP. Use Expressions to define the - * required privileges for an API. - */ -@Target({ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) -public @interface AuthorizationExpression { - /** - * The expression to evaluate for authorization, which represents multiple privileges. - * - * @return the expression to evaluate for authorization. - */ - String expression() default ""; +import java.io.IOException; +import org.apache.gravitino.server.ServerConfig; +import org.apache.gravitino.server.authorization.GravitinoAuthorizerProvider; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; + +public abstract class BaseOperationsTest extends JerseyTest { + + @BeforeAll + public static void start() { + GravitinoAuthorizerProvider.getInstance().initialize(new ServerConfig()); + } + + @AfterAll + public static void stop() throws IOException { + GravitinoAuthorizerProvider.getInstance().close(); + } } diff --git a/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalogOperations.java b/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalogOperations.java index 9b8c860933..b902d7c68e 100644 --- a/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalogOperations.java +++ b/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalogOperations.java @@ -70,14 +70,13 @@ import org.apache.gravitino.rest.RESTUtils; import org.glassfish.hk2.utilities.binding.AbstractBinder; import org.glassfish.jersey.client.HttpUrlConnectorProvider; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.test.JerseyTest; import org.glassfish.jersey.test.TestProperties; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.mockito.Mockito; -public class TestCatalogOperations extends JerseyTest { +public class TestCatalogOperations extends BaseOperationsTest { private static class MockServletRequestFactory extends ServletRequestFactoryBase { @Override
