This is an automated email from the ASF dual-hosted git repository.
liuxun pushed a commit to branch branch-metadata-authz
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/branch-metadata-authz by this
push:
new fe5a6a4371 [#6786] feat(authz): Introduce JcasbinAuthorizer &
GravitinoAdapter (#7197)
fe5a6a4371 is described below
commit fe5a6a4371a5398d1c2b21126713b421231407ad
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 +-
...ughAuthorizer.java => MetadataIdConverter.java} | 31 +--
.../authorization/PassThroughAuthorizer.java | 3 +
.../authorization/jcasbin/GravitinoAdapter.java | 57 +++++
.../authorization/jcasbin/JcasbinAuthorizer.java | 189 +++++++++++++++
.../TestAuthorizationExpressionEvaluator.java | 3 +
.../jcasbin/TestJcasbinAuthorizer.java | 257 +++++++++++++++++++++
9 files changed, 527 insertions(+), 30 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/MetadataIdConverter.java
similarity index 55%
copy from
server-common/src/main/java/org/apache/gravitino/server/authorization/PassThroughAuthorizer.java
copy to
server-common/src/main/java/org/apache/gravitino/server/authorization/MetadataIdConverter.java
index fa655617dc..f0327d456f 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/MetadataIdConverter.java
@@ -17,34 +17,15 @@
package org.apache.gravitino.server.authorization;
-import java.io.IOException;
-import java.security.Principal;
import org.apache.gravitino.MetadataObject;
-import org.apache.gravitino.authorization.Privilege;
-/**
- * The default implementation of GravitinoAuthorizer, indicating that metadata
permission control is
- * not enabled.
- */
-public class PassThroughAuthorizer implements GravitinoAuthorizer {
-
- @Override
- public void initialize() {}
+/** It is used to convert MetadataObject to MetadataId */
+public class MetadataIdConverter {
- @Override
- public boolean authorize(
- Principal principal,
- String metalake,
- MetadataObject metadataObject,
- Privilege.Name privilege) {
- return true;
- }
+ private MetadataIdConverter() {}
- @Override
- public boolean isOwner(Principal principal, String metalake, MetadataObject
metadataObject) {
- return true;
+ public static Long doConvert(MetadataObject metadataObject) {
+ // TODO depend on MetaCache
+ return 0L;
}
-
- @Override
- public void close() throws IOException {}
}
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);
+ }
+ }
+}