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 40798a58603296955b7066583e224790da11c980 Author: yangyang zhong <[email protected]> AuthorDate: Mon May 26 14:21:39 2025 +0800 [#6786] feat(authz): Introduce JcasbinAuthorizer & GravitinoAdapter (#7197) ### What changes were proposed in this pull request? Introduce JcasbinAuthorizer and GravitinoAdapter for metadata authorization. ### Why are the changes needed? Fix: #6786 ### Does this PR introduce _any_ user-facing change? None ### How was this patch tested? org.apache.gravitino.server.authorization.jcasbin.TestJcasbinAuthorizer --- .../relational/service/RoleMetaService.java | 2 +- .../server/authorization/GravitinoAuthorizer.java | 8 + .../authorization/GravitinoAuthorizerProvider.java | 7 +- .../authorization/PassThroughAuthorizer.java | 3 + .../authorization/jcasbin/GravitinoAdapter.java | 57 +++++ .../authorization/jcasbin/JcasbinAuthorizer.java | 189 +++++++++++++++ .../TestAuthorizationExpressionEvaluator.java | 3 + .../jcasbin/TestJcasbinAuthorizer.java | 257 +++++++++++++++++++++ 8 files changed, 521 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/org/apache/gravitino/storage/relational/service/RoleMetaService.java b/core/src/main/java/org/apache/gravitino/storage/relational/service/RoleMetaService.java index 58e4a6e7fc..0522a3cb95 100644 --- a/core/src/main/java/org/apache/gravitino/storage/relational/service/RoleMetaService.java +++ b/core/src/main/java/org/apache/gravitino/storage/relational/service/RoleMetaService.java @@ -293,7 +293,7 @@ public class RoleMetaService { return true; } - private static List<SecurableObjectPO> listSecurableObjectsByRoleId(Long roleId) { + public static List<SecurableObjectPO> listSecurableObjectsByRoleId(Long roleId) { return SessionUtils.getWithoutCommit( SecurableObjectMapper.class, mapper -> mapper.listSecurableObjectsByRoleId(roleId)); } diff --git a/server-common/src/main/java/org/apache/gravitino/server/authorization/GravitinoAuthorizer.java b/server-common/src/main/java/org/apache/gravitino/server/authorization/GravitinoAuthorizer.java index 710fe93601..bf53ca7b5a 100644 --- a/server-common/src/main/java/org/apache/gravitino/server/authorization/GravitinoAuthorizer.java +++ b/server-common/src/main/java/org/apache/gravitino/server/authorization/GravitinoAuthorizer.java @@ -55,4 +55,12 @@ public interface GravitinoAuthorizer extends Closeable { * @return authorization result. */ boolean isOwner(Principal principal, String metalake, MetadataObject metadataObject); + + /** + * When the permissions of a role change, it is necessary to notify the GravitinoAuthorizer in + * order to clear the cache. + * + * @param roleId The role id; + */ + void handleRolePrivilegeChange(Long roleId); } 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 49390cf27f..3d837ba955 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 @@ -19,6 +19,7 @@ package org.apache.gravitino.server.authorization; 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 @@ -46,13 +47,11 @@ public class GravitinoAuthorizerProvider { if (gravitinoAuthorizer == null) { boolean enableAuthorization = serverConfig.get(Configs.ENABLE_AUTHORIZATION); if (enableAuthorization) { - // TODO + gravitinoAuthorizer = new JcasbinAuthorizer(); } else { gravitinoAuthorizer = new PassThroughAuthorizer(); } - if (gravitinoAuthorizer != null) { - gravitinoAuthorizer.initialize(); - } + gravitinoAuthorizer.initialize(); } } } diff --git a/server-common/src/main/java/org/apache/gravitino/server/authorization/PassThroughAuthorizer.java b/server-common/src/main/java/org/apache/gravitino/server/authorization/PassThroughAuthorizer.java index fa655617dc..72e3b810a3 100644 --- a/server-common/src/main/java/org/apache/gravitino/server/authorization/PassThroughAuthorizer.java +++ b/server-common/src/main/java/org/apache/gravitino/server/authorization/PassThroughAuthorizer.java @@ -45,6 +45,9 @@ public class PassThroughAuthorizer implements GravitinoAuthorizer { return true; } + @Override + public void handleRolePrivilegeChange(Long roleId) {} + @Override public void close() throws IOException {} } diff --git a/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/GravitinoAdapter.java b/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/GravitinoAdapter.java new file mode 100644 index 0000000000..e354ece115 --- /dev/null +++ b/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/GravitinoAdapter.java @@ -0,0 +1,57 @@ +/* + * 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.server.authorization.jcasbin; + +import java.util.List; +import org.casbin.jcasbin.model.Model; +import org.casbin.jcasbin.persist.Adapter; + +/** + * The {@link Adapter} in Jcasbin is used to load the policy from the Adpater when initializing the + * {@link org.casbin.jcasbin.main.Enforcer} , and to persist the policy when the method of executing + * the Enforcer changes the policy + * + * <p>GravitinoAdapter will not perform any actions because there is no need to persist Privilege. + * All Privileges will be temporarily loaded into the Jcasbin cache when a user requests them. + */ +public class GravitinoAdapter implements Adapter { + + /** Gravitino does not require an initialization strategy when an Enforcer is instantiated */ + @Override + public void loadPolicy(Model model) {} + + /** Gravitino does not need persistent Policy when modifying the permission policy */ + @Override + public void savePolicy(Model model) {} + + /** Gravitino does not need persistent Policy when modifying the permission policy */ + @Override + public void addPolicy(String jcasbinModelSection, String policyType, List<String> rule) {} + + /** Gravitino does not need persistent Policy when modifying the permission policy */ + @Override + public void removePolicy(String jcasbinModelSection, String policyType, List<String> rule) {} + + /** Gravitino does not need persistent Policy when modifying the permission policy */ + @Override + public void removeFilteredPolicy( + String jcasbinModelSection, + String policyType, + int policyFieldIndex, + String... policyFieldValues) {} +} diff --git a/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinAuthorizer.java b/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinAuthorizer.java new file mode 100644 index 0000000000..0534412be3 --- /dev/null +++ b/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinAuthorizer.java @@ -0,0 +1,189 @@ +/* + * 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.server.authorization.jcasbin; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Preconditions; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.Principal; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.commons.io.IOUtils; +import org.apache.gravitino.Entity; +import org.apache.gravitino.EntityStore; +import org.apache.gravitino.GravitinoEnv; +import org.apache.gravitino.MetadataObject; +import org.apache.gravitino.SupportsRelationOperations; +import org.apache.gravitino.auth.AuthConstants; +import org.apache.gravitino.authorization.Privilege; +import org.apache.gravitino.meta.BaseMetalake; +import org.apache.gravitino.meta.RoleEntity; +import org.apache.gravitino.server.authorization.GravitinoAuthorizer; +import org.apache.gravitino.server.authorization.MetadataIdConverter; +import org.apache.gravitino.server.web.ObjectMapperProvider; +import org.apache.gravitino.storage.relational.po.SecurableObjectPO; +import org.apache.gravitino.storage.relational.service.RoleMetaService; +import org.apache.gravitino.storage.relational.service.UserMetaService; +import org.apache.gravitino.utils.NameIdentifierUtil; +import org.casbin.jcasbin.main.Enforcer; +import org.casbin.jcasbin.model.Model; + +/** The Jcasbin implementation of GravitinoAuthorizer. */ +public class JcasbinAuthorizer implements GravitinoAuthorizer { + + /** Jcasbin enforcer is used for metadata authorization. */ + private Enforcer enforcer; + + /** + * loadedRoles is used to cache roles that have loaded permissions. When the permissions of a role + * are updated, they should be removed from it. + */ + private Set<Long> loadedRoles = ConcurrentHashMap.newKeySet(); + + @Override + public void initialize() { + try (InputStream modelStream = + JcasbinAuthorizer.class.getResourceAsStream("/jcasbin_model.conf")) { + Preconditions.checkNotNull(modelStream, "Jcasbin model file can not found."); + String modelData = IOUtils.toString(modelStream, StandardCharsets.UTF_8); + Model model = new Model(); + model.loadModelFromText(modelData); + enforcer = new Enforcer(model, new GravitinoAdapter()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean authorize( + Principal principal, + String metalake, + MetadataObject metadataObject, + Privilege.Name privilege) { + return authorizeInternal(principal, metalake, metadataObject, privilege.name()); + } + + @Override + public boolean isOwner(Principal principal, String metalake, MetadataObject metadataObject) { + return authorizeInternal(principal, metalake, metadataObject, AuthConstants.OWNER); + } + + @Override + public void handleRolePrivilegeChange(Long roleId) { + loadedRoles.remove(roleId); + enforcer.deleteRole(String.valueOf(roleId)); + } + + @Override + public void close() throws IOException {} + + private boolean authorizeInternal( + Principal principal, String metalake, MetadataObject metadataObject, String privilege) { + String username = principal.getName(); + return loadPrivilegeAndAuthorize(username, metalake, metadataObject, privilege); + } + + private static Long getMetalakeId(String metalake) { + try { + EntityStore entityStore = GravitinoEnv.getInstance().entityStore(); + BaseMetalake metalakeEntity = + entityStore.get( + NameIdentifierUtil.ofMetalake(metalake), + Entity.EntityType.METALAKE, + BaseMetalake.class); + Long metalakeId = metalakeEntity.id(); + return metalakeId; + } catch (Exception e) { + throw new RuntimeException("Can not get metalake id by entity store", e); + } + } + + private boolean authorizeByJcasbin(Long userId, MetadataObject metadataObject, String privilege) { + Long metadataId = MetadataIdConverter.doConvert(metadataObject); + return enforcer.enforce( + String.valueOf(userId), + String.valueOf(metadataObject.type()), + String.valueOf(metadataId), + privilege); + } + + private boolean loadPrivilegeAndAuthorize( + String username, String metalake, MetadataObject metadataObject, String privilege) { + Long metalakeId = getMetalakeId(metalake); + Long userId = UserMetaService.getInstance().getUserIdByMetalakeIdAndName(metalakeId, username); + loadPrivilege(metalake, username, userId); + return authorizeByJcasbin(userId, metadataObject, privilege); + } + + private void loadPrivilege(String metalake, String username, Long userId) { + try { + EntityStore entityStore = GravitinoEnv.getInstance().entityStore(); + List<RoleEntity> entities = + entityStore + .relationOperations() + .listEntitiesByRelation( + SupportsRelationOperations.Type.ROLE_USER_REL, + NameIdentifierUtil.ofUser(metalake, username), + Entity.EntityType.ROLE); + + for (RoleEntity role : entities) { + Long roleId = role.id(); + if (loadedRoles.contains(roleId)) { + continue; + } + enforcer.addRoleForUser(String.valueOf(userId), String.valueOf(roleId)); + loadPolicyByRoleId(roleId); + loadedRoles.add(roleId); + } + // TODO load owner relationship + } catch (Exception e) { + throw new RuntimeException("Can not load privilege", e); + } + } + + private void loadPolicyByRoleId(Long roleId) { + try { + List<SecurableObjectPO> securableObjects = + RoleMetaService.listSecurableObjectsByRoleId(roleId); + for (SecurableObjectPO securableObject : securableObjects) { + String privilegeNamesString = securableObject.getPrivilegeNames(); + String privilegeConditionsString = securableObject.getPrivilegeConditions(); + ObjectMapper objectMapper = ObjectMapperProvider.objectMapper(); + List<String> privileges = + objectMapper.readValue(privilegeNamesString, new TypeReference<List<String>>() {}); + List<String> privilegeConditions = + objectMapper.readValue(privilegeConditionsString, new TypeReference<List<String>>() {}); + for (int i = 0; i < privileges.size(); i++) { + enforcer.addPolicy( + String.valueOf(securableObject.getRoleId()), + securableObject.getType(), + String.valueOf(securableObject.getMetadataObjectId()), + privileges.get(i).toUpperCase(), + privilegeConditions.get(i).toLowerCase()); + } + } + } catch (JsonProcessingException e) { + throw new RuntimeException("Can not parse privilege names", e); + } + } +} diff --git a/server-common/src/test/java/org/apache/gravitino/server/authorization/expression/TestAuthorizationExpressionEvaluator.java b/server-common/src/test/java/org/apache/gravitino/server/authorization/expression/TestAuthorizationExpressionEvaluator.java index 2d2386ac76..4b25da563d 100644 --- a/server-common/src/test/java/org/apache/gravitino/server/authorization/expression/TestAuthorizationExpressionEvaluator.java +++ b/server-common/src/test/java/org/apache/gravitino/server/authorization/expression/TestAuthorizationExpressionEvaluator.java @@ -143,6 +143,9 @@ public class TestAuthorizationExpressionEvaluator { && Objects.equals("metalakeWithOwner", metadataObject.name()); } + @Override + public void handleRolePrivilegeChange(Long roleId) {} + @Override public void close() throws IOException {} } diff --git a/server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinAuthorizer.java b/server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinAuthorizer.java new file mode 100644 index 0000000000..d1a44cedc3 --- /dev/null +++ b/server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinAuthorizer.java @@ -0,0 +1,257 @@ +/* + * 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.server.authorization.jcasbin; + +import static org.apache.gravitino.authorization.Privilege.Name.USE_CATALOG; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.security.Principal; +import org.apache.gravitino.Entity; +import org.apache.gravitino.EntityStore; +import org.apache.gravitino.GravitinoEnv; +import org.apache.gravitino.MetadataObject; +import org.apache.gravitino.MetadataObjects; +import org.apache.gravitino.NameIdentifier; +import org.apache.gravitino.SupportsRelationOperations; +import org.apache.gravitino.UserPrincipal; +import org.apache.gravitino.authorization.Privilege; +import org.apache.gravitino.meta.AuditInfo; +import org.apache.gravitino.meta.BaseMetalake; +import org.apache.gravitino.meta.RoleEntity; +import org.apache.gravitino.meta.SchemaVersion; +import org.apache.gravitino.server.authorization.MetadataIdConverter; +import org.apache.gravitino.server.web.ObjectMapperProvider; +import org.apache.gravitino.storage.relational.po.SecurableObjectPO; +import org.apache.gravitino.storage.relational.service.MetalakeMetaService; +import org.apache.gravitino.storage.relational.service.RoleMetaService; +import org.apache.gravitino.storage.relational.service.UserMetaService; +import org.apache.gravitino.utils.NameIdentifierUtil; +import org.apache.gravitino.utils.PrincipalUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +/** Test of {@link JcasbinAuthorizer} */ +public class TestJcasbinAuthorizer { + + private static final Long USER_METALAKE_ID = 1L; + + private static final Long USER_ID = 2L; + + private static final Long ALLOW_ROLE_ID = 3L; + + private static final Long DENY_ROLE_ID = 5L; + + private static final Long CATALOG_ID = 4L; + + private static final String USERNAME = "tester"; + + private static final String METALAKE = "testMetalake"; + + private static UserMetaService mockUserMetaService = mock(UserMetaService.class); + + private static RoleMetaService roleMetaService = mock(RoleMetaService.class); + + private static EntityStore entityStore = mock(EntityStore.class); + + private static GravitinoEnv gravitinoEnv = mock(GravitinoEnv.class); + + private static SupportsRelationOperations supportsRelationOperations = + mock(SupportsRelationOperations.class); + + private static MockedStatic<PrincipalUtils> principalUtilsMockedStatic; + + private static MockedStatic<UserMetaService> userMetaServiceMockedStatic; + + private static MockedStatic<GravitinoEnv> gravitinoEnvMockedStatic; + + private static MockedStatic<MetalakeMetaService> metalakeMetaServiceMockedStatic; + + private static MockedStatic<MetadataIdConverter> metadataIdConverterMockedStatic; + + private static MockedStatic<RoleMetaService> roleMetaServiceMockedStatic; + + private static JcasbinAuthorizer jcasbinAuthorizer; + + @BeforeAll + public static void setup() throws IOException { + jcasbinAuthorizer = new JcasbinAuthorizer(); + jcasbinAuthorizer.initialize(); + when(mockUserMetaService.getUserIdByMetalakeIdAndName(USER_METALAKE_ID, USERNAME)) + .thenReturn(USER_ID); + principalUtilsMockedStatic = mockStatic(PrincipalUtils.class); + userMetaServiceMockedStatic = mockStatic(UserMetaService.class); + metalakeMetaServiceMockedStatic = mockStatic(MetalakeMetaService.class); + roleMetaServiceMockedStatic = mockStatic(RoleMetaService.class); + metadataIdConverterMockedStatic = mockStatic(MetadataIdConverter.class); + gravitinoEnvMockedStatic = mockStatic(GravitinoEnv.class); + gravitinoEnvMockedStatic.when(GravitinoEnv::getInstance).thenReturn(gravitinoEnv); + roleMetaServiceMockedStatic.when(RoleMetaService::getInstance).thenReturn(roleMetaService); + userMetaServiceMockedStatic.when(UserMetaService::getInstance).thenReturn(mockUserMetaService); + principalUtilsMockedStatic + .when(PrincipalUtils::getCurrentPrincipal) + .thenReturn(new UserPrincipal(USERNAME)); + metadataIdConverterMockedStatic + .when(() -> MetadataIdConverter.doConvert(any())) + .thenReturn(CATALOG_ID); + roleMetaServiceMockedStatic + .when(() -> RoleMetaService.listSecurableObjectsByRoleId(eq(ALLOW_ROLE_ID))) + .thenReturn(ImmutableList.of(getAllowSecurableObjectPO())); + roleMetaServiceMockedStatic + .when(() -> RoleMetaService.listSecurableObjectsByRoleId(eq(DENY_ROLE_ID))) + .thenReturn(ImmutableList.of(getDenySecurableObjectPO())); + when(gravitinoEnv.entityStore()).thenReturn(entityStore); + when(entityStore.relationOperations()).thenReturn(supportsRelationOperations); + BaseMetalake baseMetalake = + BaseMetalake.builder() + .withId(USER_METALAKE_ID) + .withVersion(SchemaVersion.V_0_1) + .withAuditInfo(AuditInfo.EMPTY) + .withName(METALAKE) + .build(); + when(entityStore.get( + eq(NameIdentifierUtil.ofMetalake(METALAKE)), + eq(Entity.EntityType.METALAKE), + eq(BaseMetalake.class))) + .thenReturn(baseMetalake); + } + + @AfterAll + public static void stop() { + if (principalUtilsMockedStatic != null) { + principalUtilsMockedStatic.close(); + } + if (userMetaServiceMockedStatic != null) { + userMetaServiceMockedStatic.close(); + } + if (metalakeMetaServiceMockedStatic != null) { + metalakeMetaServiceMockedStatic.close(); + } + } + + @Test + public void testAuthorize() throws IOException { + Principal currentPrincipal = PrincipalUtils.getCurrentPrincipal(); + assertFalse(doAuthorize(currentPrincipal)); + RoleEntity allowRole = getRoleEntity(ALLOW_ROLE_ID); + NameIdentifier userNameIdentifier = NameIdentifierUtil.ofUser(METALAKE, USERNAME); + // Mock adds roles to users. + when(supportsRelationOperations.listEntitiesByRelation( + eq(SupportsRelationOperations.Type.ROLE_USER_REL), + eq(userNameIdentifier), + eq(Entity.EntityType.ROLE))) + .thenReturn(ImmutableList.of(allowRole)); + assertTrue(doAuthorize(currentPrincipal)); + // Test role cache. + // When permissions are changed but handleRolePrivilegeChange is not executed, the system will + // use the cached permissions in JCasbin, so authorize can succeed. + Long newRoleId = -1L; + RoleEntity tempNewRole = getRoleEntity(newRoleId); + when(supportsRelationOperations.listEntitiesByRelation( + eq(SupportsRelationOperations.Type.ROLE_USER_REL), + eq(userNameIdentifier), + eq(Entity.EntityType.ROLE))) + .thenReturn(ImmutableList.of(tempNewRole)); + assertTrue(doAuthorize(currentPrincipal)); + // After clearing the cache, authorize will fail + jcasbinAuthorizer.handleRolePrivilegeChange(ALLOW_ROLE_ID); + assertFalse(doAuthorize(currentPrincipal)); + // When the user is re-assigned the correct role, the authorization will succeed. + when(supportsRelationOperations.listEntitiesByRelation( + eq(SupportsRelationOperations.Type.ROLE_USER_REL), + eq(userNameIdentifier), + eq(Entity.EntityType.ROLE))) + .thenReturn(ImmutableList.of(allowRole)); + assertTrue(doAuthorize(currentPrincipal)); + // Test deny + RoleEntity denyRole = getRoleEntity(DENY_ROLE_ID); + when(supportsRelationOperations.listEntitiesByRelation( + eq(SupportsRelationOperations.Type.ROLE_USER_REL), + eq(userNameIdentifier), + eq(Entity.EntityType.ROLE))) + .thenReturn(ImmutableList.of(allowRole, denyRole)); + assertFalse(doAuthorize(currentPrincipal)); + } + + private boolean doAuthorize(Principal currentPrincipal) { + return jcasbinAuthorizer.authorize( + currentPrincipal, + "testMetalake", + MetadataObjects.of(null, "testCatalog", MetadataObject.Type.CATALOG), + USE_CATALOG); + } + + private static RoleEntity getRoleEntity(Long roleId) { + return RoleEntity.builder() + .withId(roleId) + .withName("roleName") + .withAuditInfo(AuditInfo.EMPTY) + .build(); + } + + private static SecurableObjectPO getAllowSecurableObjectPO() { + ImmutableList<Privilege.Name> privileges = ImmutableList.of(USE_CATALOG); + ImmutableList<String> conditions = ImmutableList.of("ALLOW"); + ObjectMapper objectMapper = ObjectMapperProvider.objectMapper(); + try { + return SecurableObjectPO.builder() + .withType(String.valueOf(MetadataObject.Type.CATALOG)) + .withMetadataObjectId(CATALOG_ID) + .withRoleId(ALLOW_ROLE_ID) + .withPrivilegeNames(objectMapper.writeValueAsString(privileges)) + .withPrivilegeConditions(objectMapper.writeValueAsString(conditions)) + .withDeletedAt(0L) + .withCurrentVersion(1L) + .withLastVersion(1L) + .build(); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + private static SecurableObjectPO getDenySecurableObjectPO() { + ImmutableList<Privilege.Name> privileges = ImmutableList.of(USE_CATALOG); + ImmutableList<String> conditions = ImmutableList.of("DENY"); + ObjectMapper objectMapper = ObjectMapperProvider.objectMapper(); + try { + return SecurableObjectPO.builder() + .withType(String.valueOf(MetadataObject.Type.CATALOG)) + .withMetadataObjectId(CATALOG_ID) + .withRoleId(DENY_ROLE_ID) + .withPrivilegeNames(objectMapper.writeValueAsString(privileges)) + .withPrivilegeConditions(objectMapper.writeValueAsString(conditions)) + .withDeletedAt(0L) + .withCurrentVersion(1L) + .withLastVersion(1L) + .build(); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } +}
