This is an automated email from the ASF dual-hosted git repository. liuxun pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push: new a4c863091 [#5661] feat(auth): Add JDBC authorization plugin interface (#5904) a4c863091 is described below commit a4c86309142d241ed54d38dcf7aa916a23601f17 Author: roryqi <ror...@apache.org> AuthorDate: Tue Dec 24 16:22:25 2024 +0800 [#5661] feat(auth): Add JDBC authorization plugin interface (#5904) ### What changes were proposed in this pull request? Add JDBC authorization plugin interface ### Why are the changes needed? Fix: #5661 ### Does this PR introduce _any_ user-facing change? No. ### How was this patch tested? Add a UT --- .../workflows/access-control-integration-test.yml | 3 + authorizations/authorization-jdbc/build.gradle.kts | 94 +++++ .../jdbc/JdbcAuthorizationPlugin.java | 461 +++++++++++++++++++++ .../jdbc/JdbcAuthorizationProperties.java | 44 ++ .../authorization/jdbc/JdbcAuthorizationSQL.java | 117 ++++++ .../authorization/jdbc/JdbcMetadataObject.java | 106 +++++ .../authorization/jdbc/JdbcPrivilege.java | 55 +++ .../authorization/jdbc/JdbcSecurableObject.java | 65 +++ .../jdbc/JdbcSecurableObjectMappingProvider.java | 212 ++++++++++ .../jdbc/JdbcAuthorizationPluginTest.java | 317 ++++++++++++++ settings.gradle.kts | 2 +- 11 files changed, 1475 insertions(+), 1 deletion(-) diff --git a/.github/workflows/access-control-integration-test.yml b/.github/workflows/access-control-integration-test.yml index 54ffde2ee..6997eaf9a 100644 --- a/.github/workflows/access-control-integration-test.yml +++ b/.github/workflows/access-control-integration-test.yml @@ -90,6 +90,9 @@ jobs: ./gradlew -PtestMode=embedded -PjdbcBackend=h2 -PjdkVersion=${{ matrix.java-version }} -PskipDockerTests=false :authorizations:authorization-ranger:test ./gradlew -PtestMode=deploy -PjdbcBackend=mysql -PjdkVersion=${{ matrix.java-version }} -PskipDockerTests=false :authorizations:authorization-ranger:test ./gradlew -PtestMode=deploy -PjdbcBackend=postgresql -PjdkVersion=${{ matrix.java-version }} -PskipDockerTests=false :authorizations:authorization-ranger:test + ./gradlew -PtestMode=embedded -PjdbcBackend=h2 -PjdkVersion=${{ matrix.java-version }} -PskipDockerTests=false :authorizations:authorization-jdbc:test + ./gradlew -PtestMode=deploy -PjdbcBackend=mysql -PjdkVersion=${{ matrix.java-version }} -PskipDockerTests=false :authorizations:authorization-jdbc:test + ./gradlew -PtestMode=deploy -PjdbcBackend=postgresql -PjdkVersion=${{ matrix.java-version }} -PskipDockerTests=false :authorizations:authorization-jdbc:test - name: Upload integrate tests reports uses: actions/upload-artifact@v3 diff --git a/authorizations/authorization-jdbc/build.gradle.kts b/authorizations/authorization-jdbc/build.gradle.kts new file mode 100644 index 000000000..8b105908c --- /dev/null +++ b/authorizations/authorization-jdbc/build.gradle.kts @@ -0,0 +1,94 @@ +/* + * 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. + */ +description = "authorization-jdbc" + +plugins { + `maven-publish` + id("java") + id("idea") +} + +dependencies { + implementation(project(":api")) { + exclude(group = "*") + } + implementation(project(":core")) { + exclude(group = "*") + } + + implementation(libs.bundles.log4j) + implementation(libs.commons.lang3) + implementation(libs.guava) + implementation(libs.javax.jaxb.api) { + exclude("*") + } + implementation(libs.javax.ws.rs.api) + implementation(libs.jettison) + compileOnly(libs.lombok) + implementation(libs.mail) + implementation(libs.rome) + implementation(libs.commons.dbcp2) + + testImplementation(project(":common")) + testImplementation(project(":clients:client-java")) + testImplementation(project(":server")) + testImplementation(project(":catalogs:catalog-common")) + testImplementation(project(":integration-test-common", "testArtifacts")) + testImplementation(libs.junit.jupiter.api) + testImplementation(libs.mockito.core) + testImplementation(libs.testcontainers) + testRuntimeOnly(libs.junit.jupiter.engine) +} + +tasks { + val runtimeJars by registering(Copy::class) { + from(configurations.runtimeClasspath) + into("build/libs") + } + + val copyAuthorizationLibs by registering(Copy::class) { + dependsOn("jar", runtimeJars) + from("build/libs") { + exclude("guava-*.jar") + exclude("log4j-*.jar") + exclude("slf4j-*.jar") + } + into("$rootDir/distribution/package/authorizations/ranger/libs") + } + + register("copyLibAndConfig", Copy::class) { + dependsOn(copyAuthorizationLibs) + } + + jar { + dependsOn(runtimeJars) + } +} + +tasks.test { + dependsOn(":catalogs:catalog-hive:jar", ":catalogs:catalog-hive:runtimeJars") + + val skipITs = project.hasProperty("skipITs") + if (skipITs) { + // Exclude integration tests + exclude("**/integration/test/**") + } else { + dependsOn(tasks.jar) + } +} diff --git a/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcAuthorizationPlugin.java b/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcAuthorizationPlugin.java new file mode 100644 index 000000000..f889cee22 --- /dev/null +++ b/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcAuthorizationPlugin.java @@ -0,0 +1,461 @@ +/* + * 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.authorization.jdbc; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Lists; +import java.io.IOException; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.apache.commons.dbcp2.BasicDataSource; +import org.apache.commons.pool2.impl.BaseObjectPoolConfig; +import org.apache.gravitino.MetadataObject; +import org.apache.gravitino.annotation.Unstable; +import org.apache.gravitino.authorization.AuthorizationPrivilege; +import org.apache.gravitino.authorization.AuthorizationSecurableObject; +import org.apache.gravitino.authorization.Group; +import org.apache.gravitino.authorization.MetadataObjectChange; +import org.apache.gravitino.authorization.Owner; +import org.apache.gravitino.authorization.Role; +import org.apache.gravitino.authorization.RoleChange; +import org.apache.gravitino.authorization.SecurableObject; +import org.apache.gravitino.authorization.User; +import org.apache.gravitino.connector.authorization.AuthorizationPlugin; +import org.apache.gravitino.exceptions.AuthorizationPluginException; +import org.apache.gravitino.meta.AuditInfo; +import org.apache.gravitino.meta.GroupEntity; +import org.apache.gravitino.meta.UserEntity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * JdbcSQLBasedAuthorizationPlugin is the base class for all JDBC-based authorization plugins. For + * example, JdbcHiveAuthorizationPlugin is the JDBC-based authorization plugin for Hive. Different + * JDBC-based authorization plugins can inherit this class and implement their own SQL statements. + */ +@Unstable +abstract class JdbcAuthorizationPlugin implements AuthorizationPlugin, JdbcAuthorizationSQL { + + private static final String GROUP_PREFIX = "GRAVITINO_GROUP_"; + private static final Logger LOG = LoggerFactory.getLogger(JdbcAuthorizationPlugin.class); + + protected BasicDataSource dataSource; + protected JdbcSecurableObjectMappingProvider mappingProvider; + + public JdbcAuthorizationPlugin(Map<String, String> config) { + // Initialize the data source + dataSource = new BasicDataSource(); + JdbcAuthorizationProperties.validate(config); + + String jdbcUrl = config.get(JdbcAuthorizationProperties.JDBC_URL); + dataSource.setUrl(jdbcUrl); + dataSource.setDriverClassName(config.get(JdbcAuthorizationProperties.JDBC_DRIVER)); + dataSource.setUsername(config.get(JdbcAuthorizationProperties.JDBC_USERNAME)); + dataSource.setPassword(config.get(JdbcAuthorizationProperties.JDBC_PASSWORD)); + dataSource.setDefaultAutoCommit(true); + dataSource.setMaxTotal(20); + dataSource.setMaxIdle(5); + dataSource.setMinIdle(0); + dataSource.setLogAbandoned(true); + dataSource.setRemoveAbandonedOnBorrow(true); + dataSource.setTestOnBorrow(BaseObjectPoolConfig.DEFAULT_TEST_ON_BORROW); + dataSource.setTestWhileIdle(BaseObjectPoolConfig.DEFAULT_TEST_WHILE_IDLE); + dataSource.setNumTestsPerEvictionRun(BaseObjectPoolConfig.DEFAULT_NUM_TESTS_PER_EVICTION_RUN); + dataSource.setTestOnReturn(BaseObjectPoolConfig.DEFAULT_TEST_ON_RETURN); + dataSource.setLifo(BaseObjectPoolConfig.DEFAULT_LIFO); + mappingProvider = new JdbcSecurableObjectMappingProvider(); + } + + @Override + public void close() throws IOException { + if (dataSource != null) { + try { + dataSource.close(); + dataSource = null; + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + } + + @Override + public Boolean onMetadataUpdated(MetadataObjectChange... changes) throws RuntimeException { + // This interface mainly handles the metadata object rename change and delete change. + // The privilege for JdbcSQLBasedAuthorizationPlugin will be renamed or deleted automatically. + // We don't need to do any other things. + return true; + } + + @Override + public Boolean onRoleCreated(Role role) throws AuthorizationPluginException { + List<String> sqls = getCreateRoleSQL(role.name()); + for (String sql : sqls) { + executeUpdateSQL(sql, "already exists"); + } + + if (role.securableObjects() != null) { + for (SecurableObject object : role.securableObjects()) { + onRoleUpdated(role, RoleChange.addSecurableObject(role.name(), object)); + } + } + + return true; + } + + @Override + public Boolean onRoleAcquired(Role role) throws AuthorizationPluginException { + throw new UnsupportedOperationException("Doesn't support to acquired a role"); + } + + @Override + public Boolean onRoleDeleted(Role role) throws AuthorizationPluginException { + List<String> sqls = getDropRoleSQL(role.name()); + for (String sql : sqls) { + executeUpdateSQL(sql); + } + return null; + } + + @Override + public Boolean onRoleUpdated(Role role, RoleChange... changes) + throws AuthorizationPluginException { + onRoleCreated(role); + for (RoleChange change : changes) { + if (change instanceof RoleChange.AddSecurableObject) { + SecurableObject object = ((RoleChange.AddSecurableObject) change).getSecurableObject(); + grantObjectPrivileges(role, object); + } else if (change instanceof RoleChange.RemoveSecurableObject) { + SecurableObject object = ((RoleChange.RemoveSecurableObject) change).getSecurableObject(); + revokeObjectPrivileges(role, object); + } else if (change instanceof RoleChange.UpdateSecurableObject) { + RoleChange.UpdateSecurableObject updateChange = (RoleChange.UpdateSecurableObject) change; + SecurableObject addObject = updateChange.getNewSecurableObject(); + SecurableObject removeObject = updateChange.getSecurableObject(); + revokeObjectPrivileges(role, removeObject); + grantObjectPrivileges(role, addObject); + } else { + throw new IllegalArgumentException( + String.format("RoleChange is not supported - %s", change)); + } + } + return true; + } + + @Override + public Boolean onGrantedRolesToUser(List<Role> roles, User user) + throws AuthorizationPluginException { + + for (Role role : roles) { + onRoleCreated(role); + List<String> sqls = getGrantRoleSQL(role.name(), "USER", user.name()); + for (String sql : sqls) { + executeUpdateSQL(sql); + } + } + return true; + } + + @Override + public Boolean onRevokedRolesFromUser(List<Role> roles, User user) + throws AuthorizationPluginException { + + for (Role role : roles) { + onRoleCreated(role); + List<String> sqls = getRevokeRoleSQL(role.name(), "USER", user.name()); + for (String sql : sqls) { + executeUpdateSQL(sql); + } + } + return true; + } + + @Override + public Boolean onGrantedRolesToGroup(List<Role> roles, Group group) + throws AuthorizationPluginException { + + for (Role role : roles) { + onRoleCreated(role); + List<String> sqls = + getGrantRoleSQL(role.name(), "USER", String.format("%s%s", GROUP_PREFIX, group.name())); + for (String sql : sqls) { + executeUpdateSQL(sql); + } + } + return true; + } + + @Override + public Boolean onRevokedRolesFromGroup(List<Role> roles, Group group) + throws AuthorizationPluginException { + + for (Role role : roles) { + onRoleCreated(role); + List<String> sqls = + getRevokeRoleSQL(role.name(), "USER", String.format("%s%s", GROUP_PREFIX, group.name())); + for (String sql : sqls) { + executeUpdateSQL(sql); + } + } + return true; + } + + @Override + public Boolean onUserAdded(User user) throws AuthorizationPluginException { + List<String> sqls = getCreateUserSQL(user.name()); + for (String sql : sqls) { + executeUpdateSQL(sql); + } + return true; + } + + @Override + public Boolean onUserRemoved(User user) throws AuthorizationPluginException { + List<String> sqls = getDropUserSQL(user.name()); + for (String sql : sqls) { + executeUpdateSQL(sql); + } + return true; + } + + @Override + public Boolean onUserAcquired(User user) throws AuthorizationPluginException { + throw new UnsupportedOperationException("Doesn't support to acquired a user"); + } + + @Override + public Boolean onGroupAdded(Group group) throws AuthorizationPluginException { + String name = String.format("%s%s", GROUP_PREFIX, group.name()); + List<String> sqls = getCreateUserSQL(name); + for (String sql : sqls) { + executeUpdateSQL(sql); + } + return true; + } + + @Override + public Boolean onGroupRemoved(Group group) throws AuthorizationPluginException { + String name = String.format("%s%s", GROUP_PREFIX, group.name()); + List<String> sqls = getDropUserSQL(name); + for (String sql : sqls) { + executeUpdateSQL(sql); + } + return true; + } + + @Override + public Boolean onGroupAcquired(Group group) throws AuthorizationPluginException { + throw new UnsupportedOperationException("Doesn't support to acquired a group"); + } + + @Override + public Boolean onOwnerSet(MetadataObject metadataObject, Owner preOwner, Owner newOwner) + throws AuthorizationPluginException { + if (newOwner.type() == Owner.Type.USER) { + onUserAdded( + UserEntity.builder() + .withName(newOwner.name()) + .withId(0L) + .withAuditInfo(AuditInfo.EMPTY) + .build()); + } else if (newOwner.type() == Owner.Type.GROUP) { + onGroupAdded( + GroupEntity.builder() + .withName(newOwner.name()) + .withId(0L) + .withAuditInfo(AuditInfo.EMPTY) + .build()); + } else { + throw new IllegalArgumentException( + String.format("Don't support owner type %s", newOwner.type())); + } + + List<AuthorizationSecurableObject> authObjects = mappingProvider.translateOwner(metadataObject); + for (AuthorizationSecurableObject authObject : authObjects) { + List<String> sqls = + getSetOwnerSQL( + authObject.type().metadataObjectType(), authObject.fullName(), preOwner, newOwner); + for (String sql : sqls) { + executeUpdateSQL(sql); + } + } + return true; + } + + @Override + public List<String> getCreateUserSQL(String username) { + return Lists.newArrayList(String.format("CREATE USER %s", username)); + } + + @Override + public List<String> getDropUserSQL(String username) { + return Lists.newArrayList(String.format("DROP USER %s", username)); + } + + @Override + public List<String> getCreateRoleSQL(String roleName) { + return Lists.newArrayList(String.format("CREATE ROLE %s", roleName)); + } + + @Override + public List<String> getDropRoleSQL(String roleName) { + return Lists.newArrayList(String.format("DROP ROLE %s", roleName)); + } + + @Override + public List<String> getGrantPrivilegeSQL( + String privilege, String objectType, String objectName, String roleName) { + return Lists.newArrayList( + String.format("GRANT %s ON %s %s TO ROLE %s", privilege, objectType, objectName, roleName)); + } + + @Override + public List<String> getRevokePrivilegeSQL( + String privilege, String objectType, String objectName, String roleName) { + return Lists.newArrayList( + String.format( + "REVOKE %s ON %s %s FROM ROLE %s", privilege, objectType, objectName, roleName)); + } + + @Override + public List<String> getGrantRoleSQL(String roleName, String grantorType, String grantorName) { + return Lists.newArrayList( + String.format("GRANT ROLE %s TO %s %s", roleName, grantorType, grantorName)); + } + + @Override + public List<String> getRevokeRoleSQL(String roleName, String revokerType, String revokerName) { + return Lists.newArrayList( + String.format("REVOKE ROLE %s FROM %s %s", roleName, revokerType, revokerName)); + } + + @VisibleForTesting + Connection getConnection() throws SQLException { + return dataSource.getConnection(); + } + + protected void executeUpdateSQL(String sql) { + executeUpdateSQL(sql, null); + } + + /** + * Convert the object name contains `*` to a list of AuthorizationSecurableObject. + * + * @param object The object contains the name with `*` to be converted + * @return The list of AuthorizationSecurableObject + */ + protected List<AuthorizationSecurableObject> convertResourceAll( + AuthorizationSecurableObject object) { + List<AuthorizationSecurableObject> authObjects = Lists.newArrayList(); + authObjects.add(object); + return authObjects; + } + + protected List<AuthorizationPrivilege> filterUnsupportedPrivileges( + List<AuthorizationPrivilege> privileges) { + return privileges; + } + + protected AuthorizationPluginException toAuthorizationPluginException(SQLException se) { + return new AuthorizationPluginException( + "JDBC authorization plugin fail to execute SQL, error code: %d", se.getErrorCode()); + } + + void executeUpdateSQL(String sql, String ignoreErrorMsg) { + try (final Connection connection = getConnection()) { + try (final Statement statement = connection.createStatement()) { + statement.executeUpdate(sql); + } + } catch (SQLException se) { + if (ignoreErrorMsg != null && se.getMessage().contains(ignoreErrorMsg)) { + return; + } + LOG.error("JDBC authorization plugin exception: ", se); + throw toAuthorizationPluginException(se); + } + } + + private void grantObjectPrivileges(Role role, SecurableObject object) { + List<AuthorizationSecurableObject> authObjects = mappingProvider.translatePrivilege(object); + for (AuthorizationSecurableObject authObject : authObjects) { + List<AuthorizationSecurableObject> convertedObjects = Lists.newArrayList(); + if (authObject.name().equals(JdbcSecurableObject.ALL)) { + convertedObjects.addAll(convertResourceAll(authObject)); + } else { + convertedObjects.add(authObject); + } + + for (AuthorizationSecurableObject convertedObject : convertedObjects) { + List<String> privileges = + filterUnsupportedPrivileges(authObject.privileges()).stream() + .map(AuthorizationPrivilege::getName) + .collect(Collectors.toList()); + // We don't grant the privileges in one SQL, because some privilege has been granted, it + // will cause the failure of the SQL. So we grant the privileges one by one. + for (String privilege : privileges) { + List<String> sqls = + getGrantPrivilegeSQL( + privilege, + convertedObject.metadataObjectType().name(), + convertedObject.fullName(), + role.name()); + for (String sql : sqls) { + executeUpdateSQL(sql, "is already granted"); + } + } + } + } + } + + private void revokeObjectPrivileges(Role role, SecurableObject removeObject) { + List<AuthorizationSecurableObject> authObjects = + mappingProvider.translatePrivilege(removeObject); + for (AuthorizationSecurableObject authObject : authObjects) { + List<AuthorizationSecurableObject> convertedObjects = Lists.newArrayList(); + if (authObject.name().equals(JdbcSecurableObject.ALL)) { + convertedObjects.addAll(convertResourceAll(authObject)); + } else { + convertedObjects.add(authObject); + } + + for (AuthorizationSecurableObject convertedObject : convertedObjects) { + List<String> privileges = + filterUnsupportedPrivileges(authObject.privileges()).stream() + .map(AuthorizationPrivilege::getName) + .collect(Collectors.toList()); + for (String privilege : privileges) { + // We don't revoke the privileges in one SQL, because some privilege has been revoked, it + // will cause the failure of the SQL. So we revoke the privileges one by one. + List<String> sqls = + getRevokePrivilegeSQL( + privilege, + convertedObject.metadataObjectType().name(), + convertedObject.fullName(), + role.name()); + for (String sql : sqls) { + executeUpdateSQL(sql, "Cannot find privilege Privilege"); + } + } + } + } + } +} diff --git a/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcAuthorizationProperties.java b/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcAuthorizationProperties.java new file mode 100644 index 000000000..b13504fd2 --- /dev/null +++ b/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcAuthorizationProperties.java @@ -0,0 +1,44 @@ +/* + * 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.authorization.jdbc; + +import java.util.Map; + +/** The properties for JDBC authorization plugin. */ +public class JdbcAuthorizationProperties { + private static final String CONFIG_PREFIX = "authorization.jdbc."; + public static final String JDBC_PASSWORD = CONFIG_PREFIX + "password"; + public static final String JDBC_USERNAME = CONFIG_PREFIX + "username"; + public static final String JDBC_URL = CONFIG_PREFIX + "url"; + public static final String JDBC_DRIVER = CONFIG_PREFIX + "driver"; + + public static void validate(Map<String, String> properties) { + String errorMsg = "%s is required"; + check(properties, JDBC_URL, errorMsg); + check(properties, JDBC_USERNAME, errorMsg); + check(properties, JDBC_PASSWORD, errorMsg); + check(properties, JDBC_DRIVER, errorMsg); + } + + private static void check(Map<String, String> properties, String key, String errorMsg) { + if (!properties.containsKey(key) && properties.get(key) != null) { + throw new IllegalArgumentException(String.format(errorMsg, key)); + } + } +} diff --git a/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcAuthorizationSQL.java b/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcAuthorizationSQL.java new file mode 100644 index 000000000..f7171ff35 --- /dev/null +++ b/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcAuthorizationSQL.java @@ -0,0 +1,117 @@ +/* + * 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.authorization.jdbc; + +import java.util.List; +import org.apache.gravitino.MetadataObject; +import org.apache.gravitino.annotation.Unstable; +import org.apache.gravitino.authorization.Owner; + +/** Interface for SQL operations of the underlying access control system. */ +@Unstable +interface JdbcAuthorizationSQL { + + /** + * Get SQL statements for creating a user. + * + * @param username the username to create + * @return the SQL statement list to create a user + */ + List<String> getCreateUserSQL(String username); + + /** + * Get SQL statements for creating a group. + * + * @param username the username to drop + * @return the SQL statement list to drop a user + */ + List<String> getDropUserSQL(String username); + + /** + * Get SQL statements for creating a role. + * + * @param roleName the role name to create + * @return the SQL statement list to create a role + */ + List<String> getCreateRoleSQL(String roleName); + + /** + * Get SQL statements for dropping a role. + * + * @param roleName the role name to drop + * @return the SQL statement list to drop a role + */ + List<String> getDropRoleSQL(String roleName); + + /** + * Get SQL statements for granting privileges. + * + * @param privilege the privilege to grant + * @param objectType the object type in the database system + * @param objectName the object name in the database system + * @param roleName the role name to grant + * @return the sql statement list to grant privilege + */ + List<String> getGrantPrivilegeSQL( + String privilege, String objectType, String objectName, String roleName); + + /** + * Get SQL statements for revoking privileges. + * + * @param privilege the privilege to revoke + * @param objectType the object type in the database system + * @param objectName the object name in the database system + * @param roleName the role name to revoke + * @return the sql statement list to revoke privilege + */ + List<String> getRevokePrivilegeSQL( + String privilege, String objectType, String objectName, String roleName); + + /** + * Get SQL statements for granting role. + * + * @param roleName the role name to grant + * @param grantorType the grantor type, usually USER or ROLE + * @param grantorName the grantor name + * @return the sql statement list to grant role + */ + List<String> getGrantRoleSQL(String roleName, String grantorType, String grantorName); + + /** + * Get SQL statements for revoking roles. + * + * @param roleName the role name to revoke + * @param revokerType the revoker type, usually USER or ROLE + * @param revokerName the revoker name + * @return the sql statement list to revoke role + */ + List<String> getRevokeRoleSQL(String roleName, String revokerType, String revokerName); + + /** + * Get SQL statements for setting owner. + * + * @param type The metadata object type + * @param objectName the object name in the database system + * @param preOwner the previous owner of the object + * @param newOwner the new owner of the object + * @return the sql statement list to set owner + */ + List<String> getSetOwnerSQL( + MetadataObject.Type type, String objectName, Owner preOwner, Owner newOwner); +} diff --git a/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcMetadataObject.java b/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcMetadataObject.java new file mode 100644 index 000000000..c74c7ae60 --- /dev/null +++ b/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcMetadataObject.java @@ -0,0 +1,106 @@ +/* + * 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.authorization.jdbc; + +import com.google.common.base.Preconditions; +import java.util.List; +import javax.annotation.Nullable; +import org.apache.gravitino.MetadataObject; +import org.apache.gravitino.authorization.AuthorizationMetadataObject; + +public class JdbcMetadataObject implements AuthorizationMetadataObject { + + private final String parent; + private final String name; + private final Type type; + + public JdbcMetadataObject(String parent, String name, Type type) { + this.parent = parent; + this.name = name; + this.type = type; + } + + @Nullable + @Override + public String parent() { + return parent; + } + + @Override + public String name() { + return name; + } + + @Override + public List<String> names() { + return DOT_SPLITTER.splitToList(fullName()); + } + + @Override + public Type type() { + return type; + } + + @Override + public void validateAuthorizationMetadataObject() throws IllegalArgumentException { + List<String> names = names(); + Preconditions.checkArgument( + names != null && !names.isEmpty(), "The name of the object is empty."); + Preconditions.checkArgument( + names.size() <= 2, "The name of the object is not in the format of 'database.table'."); + Preconditions.checkArgument(type != null, "The type of the object is null."); + if (names.size() == 1) { + Preconditions.checkArgument( + type.metadataObjectType() == MetadataObject.Type.SCHEMA, + "The type of the object is not SCHEMA."); + } else { + Preconditions.checkArgument( + type.metadataObjectType() == MetadataObject.Type.TABLE, + "The type of the object is not TABLE."); + } + + for (String name : names) { + Preconditions.checkArgument(name != null, "Cannot create a metadata object with null name"); + } + } + + public enum Type implements AuthorizationMetadataObject.Type { + SCHEMA(MetadataObject.Type.SCHEMA), + TABLE(MetadataObject.Type.TABLE); + + private final MetadataObject.Type metadataType; + + Type(MetadataObject.Type type) { + this.metadataType = type; + } + + public MetadataObject.Type metadataObjectType() { + return metadataType; + } + + public static Type fromMetadataType(MetadataObject.Type metadataType) { + for (Type type : Type.values()) { + if (type.metadataObjectType() == metadataType) { + return type; + } + } + throw new IllegalArgumentException("No matching JdbcMetadataObject.Type for " + metadataType); + } + } +} diff --git a/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcPrivilege.java b/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcPrivilege.java new file mode 100644 index 000000000..845b31a5b --- /dev/null +++ b/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcPrivilege.java @@ -0,0 +1,55 @@ +/* + * 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.authorization.jdbc; + +import org.apache.gravitino.authorization.AuthorizationPrivilege; +import org.apache.gravitino.authorization.Privilege; + +public enum JdbcPrivilege implements AuthorizationPrivilege { + SELECT("SELECT"), + INSERT("INSERT"), + UPDATE("UPDATE"), + ALTER("ALTER"), + DELETE("DELETE"), + ALL("ALL PRIVILEGES"), + CREATE("CREATE"), + DROP("DROP"), + USAGE("USAGE"); + + private final String name; + + JdbcPrivilege(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public Privilege.Condition condition() { + return Privilege.Condition.ALLOW; + } + + @Override + public boolean equalsTo(String value) { + return name.equals(value); + } +} diff --git a/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcSecurableObject.java b/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcSecurableObject.java new file mode 100644 index 000000000..78b82e2a8 --- /dev/null +++ b/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcSecurableObject.java @@ -0,0 +1,65 @@ +/* + * 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.authorization.jdbc; + +import java.util.List; +import org.apache.gravitino.MetadataObject; +import org.apache.gravitino.authorization.AuthorizationPrivilege; +import org.apache.gravitino.authorization.AuthorizationSecurableObject; + +/** + * JdbcAuthorizationObject is used for translating securable object to authorization securable + * object. JdbcAuthorizationObject has the database and table name. When table name is null, the + * object represents a database. The database can't be null. + */ +public class JdbcSecurableObject extends JdbcMetadataObject + implements AuthorizationSecurableObject { + + public static final String ALL = "*"; + + List<AuthorizationPrivilege> privileges; + + private JdbcSecurableObject( + String parent, + String name, + JdbcMetadataObject.Type type, + List<AuthorizationPrivilege> privileges) { + super(parent, name, type); + this.privileges = privileges; + } + + static JdbcSecurableObject create( + String schema, String table, List<AuthorizationPrivilege> privileges) { + String parent = table == null ? null : schema; + String name = table == null ? schema : table; + JdbcMetadataObject.Type type = + table == null + ? JdbcMetadataObject.Type.fromMetadataType(MetadataObject.Type.SCHEMA) + : JdbcMetadataObject.Type.fromMetadataType(MetadataObject.Type.TABLE); + + JdbcSecurableObject object = new JdbcSecurableObject(parent, name, type, privileges); + object.validateAuthorizationMetadataObject(); + return object; + } + + @Override + public List<AuthorizationPrivilege> privileges() { + return privileges; + } +} diff --git a/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcSecurableObjectMappingProvider.java b/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcSecurableObjectMappingProvider.java new file mode 100644 index 000000000..70b2d10e3 --- /dev/null +++ b/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcSecurableObjectMappingProvider.java @@ -0,0 +1,212 @@ +/* + * 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.authorization.jdbc; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.gravitino.MetadataObject; +import org.apache.gravitino.MetadataObjects; +import org.apache.gravitino.authorization.AuthorizationMetadataObject; +import org.apache.gravitino.authorization.AuthorizationPrivilege; +import org.apache.gravitino.authorization.AuthorizationPrivilegesMappingProvider; +import org.apache.gravitino.authorization.AuthorizationSecurableObject; +import org.apache.gravitino.authorization.Privilege; +import org.apache.gravitino.authorization.SecurableObject; + +/** + * JdbcSecurableObjectMappingProvider is used for translating securable object to authorization + * securable object. + */ +public class JdbcSecurableObjectMappingProvider implements AuthorizationPrivilegesMappingProvider { + + private final Map<Privilege.Name, Set<AuthorizationPrivilege>> privilegeMapping = + ImmutableMap.of( + Privilege.Name.CREATE_TABLE, Sets.newHashSet(JdbcPrivilege.CREATE), + Privilege.Name.CREATE_SCHEMA, Sets.newHashSet(JdbcPrivilege.CREATE), + Privilege.Name.SELECT_TABLE, Sets.newHashSet(JdbcPrivilege.SELECT), + Privilege.Name.MODIFY_TABLE, + Sets.newHashSet( + JdbcPrivilege.SELECT, + JdbcPrivilege.UPDATE, + JdbcPrivilege.DELETE, + JdbcPrivilege.INSERT, + JdbcPrivilege.ALTER), + Privilege.Name.USE_SCHEMA, Sets.newHashSet(JdbcPrivilege.USAGE)); + + private final Map<Privilege.Name, MetadataObject.Type> privilegeScopeMapping = + ImmutableMap.of( + Privilege.Name.CREATE_TABLE, MetadataObject.Type.TABLE, + Privilege.Name.CREATE_SCHEMA, MetadataObject.Type.SCHEMA, + Privilege.Name.SELECT_TABLE, MetadataObject.Type.TABLE, + Privilege.Name.MODIFY_TABLE, MetadataObject.Type.TABLE, + Privilege.Name.USE_SCHEMA, MetadataObject.Type.SCHEMA); + + private final Set<AuthorizationPrivilege> ownerPrivileges = ImmutableSet.of(); + + private final Set<MetadataObject.Type> allowObjectTypes = + ImmutableSet.of( + MetadataObject.Type.METALAKE, + MetadataObject.Type.CATALOG, + MetadataObject.Type.SCHEMA, + MetadataObject.Type.TABLE); + + @Override + public Map<Privilege.Name, Set<AuthorizationPrivilege>> privilegesMappingRule() { + return privilegeMapping; + } + + @Override + public Set<AuthorizationPrivilege> ownerMappingRule() { + return ownerPrivileges; + } + + @Override + public Set<Privilege.Name> allowPrivilegesRule() { + return privilegeMapping.keySet(); + } + + @Override + public Set<MetadataObject.Type> allowMetadataObjectTypesRule() { + return allowObjectTypes; + } + + @Override + public List<AuthorizationSecurableObject> translatePrivilege(SecurableObject securableObject) { + List<AuthorizationSecurableObject> authObjects = Lists.newArrayList(); + List<AuthorizationPrivilege> databasePrivileges = Lists.newArrayList(); + List<AuthorizationPrivilege> tablePrivileges = Lists.newArrayList(); + JdbcSecurableObject databaseObject; + JdbcSecurableObject tableObject; + switch (securableObject.type()) { + case METALAKE: + case CATALOG: + convertJdbcPrivileges(securableObject, databasePrivileges, tablePrivileges); + + if (!databasePrivileges.isEmpty()) { + databaseObject = + JdbcSecurableObject.create(JdbcSecurableObject.ALL, null, databasePrivileges); + authObjects.add(databaseObject); + } + + if (!tablePrivileges.isEmpty()) { + tableObject = + JdbcSecurableObject.create( + JdbcSecurableObject.ALL, JdbcSecurableObject.ALL, tablePrivileges); + authObjects.add(tableObject); + } + break; + + case SCHEMA: + convertJdbcPrivileges(securableObject, databasePrivileges, tablePrivileges); + if (!databasePrivileges.isEmpty()) { + databaseObject = + JdbcSecurableObject.create(securableObject.name(), null, databasePrivileges); + authObjects.add(databaseObject); + } + + if (!tablePrivileges.isEmpty()) { + tableObject = + JdbcSecurableObject.create( + securableObject.name(), JdbcSecurableObject.ALL, tablePrivileges); + authObjects.add(tableObject); + } + break; + + case TABLE: + convertJdbcPrivileges(securableObject, databasePrivileges, tablePrivileges); + if (!tablePrivileges.isEmpty()) { + MetadataObject metadataObject = + MetadataObjects.parse(securableObject.parent(), MetadataObject.Type.SCHEMA); + tableObject = + JdbcSecurableObject.create( + metadataObject.name(), securableObject.name(), tablePrivileges); + authObjects.add(tableObject); + } + break; + + default: + throw new IllegalArgumentException( + String.format("Don't support metadata object type %s", securableObject.type())); + } + + return authObjects; + } + + @Override + public List<AuthorizationSecurableObject> translateOwner(MetadataObject metadataObject) { + List<AuthorizationSecurableObject> objects = Lists.newArrayList(); + switch (metadataObject.type()) { + case METALAKE: + case CATALOG: + objects.add( + JdbcSecurableObject.create( + JdbcSecurableObject.ALL, null, Lists.newArrayList(JdbcPrivilege.ALL))); + objects.add( + JdbcSecurableObject.create( + JdbcSecurableObject.ALL, + JdbcSecurableObject.ALL, + Lists.newArrayList(JdbcPrivilege.ALL))); + break; + case SCHEMA: + objects.add( + JdbcSecurableObject.create( + metadataObject.name(), null, Lists.newArrayList(JdbcPrivilege.ALL))); + objects.add( + JdbcSecurableObject.create( + metadataObject.name(), + JdbcSecurableObject.ALL, + Lists.newArrayList(JdbcPrivilege.ALL))); + break; + case TABLE: + MetadataObject schema = + MetadataObjects.parse(metadataObject.parent(), MetadataObject.Type.SCHEMA); + objects.add( + JdbcSecurableObject.create( + schema.name(), metadataObject.name(), Lists.newArrayList(JdbcPrivilege.ALL))); + break; + default: + throw new IllegalArgumentException( + "Don't support metadata object type " + metadataObject.type()); + } + return objects; + } + + @Override + public AuthorizationMetadataObject translateMetadataObject(MetadataObject metadataObject) { + throw new UnsupportedOperationException("Not supported"); + } + + private void convertJdbcPrivileges( + SecurableObject securableObject, + List<AuthorizationPrivilege> databasePrivileges, + List<AuthorizationPrivilege> tablePrivileges) { + for (Privilege privilege : securableObject.privileges()) { + if (privilegeScopeMapping.get(privilege.name()) == MetadataObject.Type.SCHEMA) { + databasePrivileges.addAll(privilegeMapping.get(privilege.name())); + } else if (privilegeScopeMapping.get(privilege.name()) == MetadataObject.Type.TABLE) { + tablePrivileges.addAll(privilegeMapping.get(privilege.name())); + } + } + } +} diff --git a/authorizations/authorization-jdbc/src/test/java/org/apache/gravitino/authorization/jdbc/JdbcAuthorizationPluginTest.java b/authorizations/authorization-jdbc/src/test/java/org/apache/gravitino/authorization/jdbc/JdbcAuthorizationPluginTest.java new file mode 100644 index 000000000..b72392a6c --- /dev/null +++ b/authorizations/authorization-jdbc/src/test/java/org/apache/gravitino/authorization/jdbc/JdbcAuthorizationPluginTest.java @@ -0,0 +1,317 @@ +/* + * 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.authorization.jdbc; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.apache.gravitino.MetadataObject; +import org.apache.gravitino.MetadataObjects; +import org.apache.gravitino.authorization.Group; +import org.apache.gravitino.authorization.Owner; +import org.apache.gravitino.authorization.Privileges; +import org.apache.gravitino.authorization.Role; +import org.apache.gravitino.authorization.RoleChange; +import org.apache.gravitino.authorization.SecurableObject; +import org.apache.gravitino.authorization.SecurableObjects; +import org.apache.gravitino.authorization.User; +import org.apache.gravitino.meta.AuditInfo; +import org.apache.gravitino.meta.GroupEntity; +import org.apache.gravitino.meta.RoleEntity; +import org.apache.gravitino.meta.UserEntity; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class JdbcAuthorizationPluginTest { + private static List<String> expectSQLs = Lists.newArrayList(); + private static List<MetadataObject.Type> expectTypes = Lists.newArrayList(); + private static List<String> expectObjectNames = Lists.newArrayList(); + private static List<Optional<Owner>> expectPreOwners = Lists.newArrayList(); + private static List<Owner> expectNewOwners = Lists.newArrayList(); + private static int currentSQLIndex = 0; + private static int currentIndex = 0; + private static final Map<String, String> properties = + ImmutableMap.of( + JdbcAuthorizationProperties.JDBC_URL, + "xx", + JdbcAuthorizationProperties.JDBC_USERNAME, + "xx", + JdbcAuthorizationProperties.JDBC_PASSWORD, + "xx", + JdbcAuthorizationProperties.JDBC_DRIVER, + "xx"); + + private static final JdbcAuthorizationPlugin plugin = + new JdbcAuthorizationPlugin(properties) { + + @Override + public List<String> getSetOwnerSQL( + MetadataObject.Type type, String objectName, Owner preOwner, Owner newOwner) { + Assertions.assertEquals(expectTypes.get(currentIndex), type); + Assertions.assertEquals(expectObjectNames.get(currentIndex), objectName); + Assertions.assertEquals(expectPreOwners.get(currentIndex), Optional.ofNullable(preOwner)); + Assertions.assertEquals(expectNewOwners.get(currentIndex), newOwner); + currentIndex++; + return Collections.emptyList(); + } + + void executeUpdateSQL(String sql, String ignoreErrorMsg) { + Assertions.assertEquals(expectSQLs.get(currentSQLIndex), sql); + currentSQLIndex++; + } + }; + + @Test + public void testUserManagement() { + expectSQLs = Lists.newArrayList("CREATE USER tmp"); + currentSQLIndex = 0; + plugin.onUserAdded(createUser("tmp")); + + Assertions.assertThrows( + UnsupportedOperationException.class, () -> plugin.onUserAcquired(createUser("tmp"))); + + expectSQLs = Lists.newArrayList("DROP USER tmp"); + currentSQLIndex = 0; + plugin.onUserRemoved(createUser("tmp")); + } + + @Test + public void testGroupManagement() { + expectSQLs = Lists.newArrayList("CREATE USER GRAVITINO_GROUP_tmp"); + resetSQLIndex(); + plugin.onGroupAdded(createGroup("tmp")); + + Assertions.assertThrows( + UnsupportedOperationException.class, () -> plugin.onGroupAcquired(createGroup("tmp"))); + + expectSQLs = Lists.newArrayList("DROP USER GRAVITINO_GROUP_tmp"); + resetSQLIndex(); + plugin.onGroupRemoved(createGroup("tmp")); + } + + @Test + public void testRoleManagement() { + expectSQLs = Lists.newArrayList("CREATE ROLE tmp"); + resetSQLIndex(); + Role role = createRole("tmp"); + plugin.onRoleCreated(role); + + Assertions.assertThrows(UnsupportedOperationException.class, () -> plugin.onRoleAcquired(role)); + + resetSQLIndex(); + expectSQLs = Lists.newArrayList("DROP ROLE tmp"); + plugin.onRoleDeleted(role); + } + + @Test + public void testPermissionManagement() { + Role role = createRole("tmp"); + Group group = createGroup("tmp"); + User user = createUser("tmp"); + + resetSQLIndex(); + expectSQLs = + Lists.newArrayList("CREATE ROLE tmp", "GRANT ROLE tmp TO USER GRAVITINO_GROUP_tmp"); + plugin.onGrantedRolesToGroup(Lists.newArrayList(role), group); + + resetSQLIndex(); + expectSQLs = Lists.newArrayList("CREATE ROLE tmp", "GRANT ROLE tmp TO USER tmp"); + plugin.onGrantedRolesToUser(Lists.newArrayList(role), user); + + resetSQLIndex(); + expectSQLs = + Lists.newArrayList("CREATE ROLE tmp", "REVOKE ROLE tmp FROM USER GRAVITINO_GROUP_tmp"); + plugin.onRevokedRolesFromGroup(Lists.newArrayList(role), group); + + resetSQLIndex(); + expectSQLs = Lists.newArrayList("CREATE ROLE tmp", "REVOKE ROLE tmp FROM USER tmp"); + plugin.onRevokedRolesFromUser(Lists.newArrayList(role), user); + + // Test metalake object and different role change + resetSQLIndex(); + expectSQLs = Lists.newArrayList("CREATE ROLE tmp", "GRANT SELECT ON TABLE *.* TO ROLE tmp"); + SecurableObject metalakeObject = + SecurableObjects.ofMetalake("metalake", Lists.newArrayList(Privileges.SelectTable.allow())); + RoleChange roleChange = RoleChange.addSecurableObject("tmp", metalakeObject); + plugin.onRoleUpdated(role, roleChange); + + resetSQLIndex(); + expectSQLs = Lists.newArrayList("CREATE ROLE tmp", "REVOKE SELECT ON TABLE *.* FROM ROLE tmp"); + roleChange = RoleChange.removeSecurableObject("tmp", metalakeObject); + plugin.onRoleUpdated(role, roleChange); + + resetSQLIndex(); + expectSQLs = + Lists.newArrayList( + "CREATE ROLE tmp", + "REVOKE SELECT ON TABLE *.* FROM ROLE tmp", + "GRANT CREATE ON TABLE *.* TO ROLE tmp"); + SecurableObject newMetalakeObject = + SecurableObjects.ofMetalake("metalake", Lists.newArrayList(Privileges.CreateTable.allow())); + roleChange = RoleChange.updateSecurableObject("tmp", metalakeObject, newMetalakeObject); + plugin.onRoleUpdated(role, roleChange); + + // Test catalog object + resetSQLIndex(); + SecurableObject catalogObject = + SecurableObjects.ofCatalog("catalog", Lists.newArrayList(Privileges.SelectTable.allow())); + roleChange = RoleChange.addSecurableObject("tmp", catalogObject); + expectSQLs = Lists.newArrayList("CREATE ROLE tmp", "GRANT SELECT ON TABLE *.* TO ROLE tmp"); + plugin.onRoleUpdated(role, roleChange); + + // Test schema object + resetSQLIndex(); + SecurableObject schemaObject = + SecurableObjects.ofSchema( + catalogObject, "schema", Lists.newArrayList(Privileges.SelectTable.allow())); + roleChange = RoleChange.addSecurableObject("tmp", schemaObject); + expectSQLs = + Lists.newArrayList("CREATE ROLE tmp", "GRANT SELECT ON TABLE schema.* TO ROLE tmp"); + plugin.onRoleUpdated(role, roleChange); + + // Test table object + resetSQLIndex(); + SecurableObject tableObject = + SecurableObjects.ofTable( + schemaObject, "table", Lists.newArrayList(Privileges.SelectTable.allow())); + roleChange = RoleChange.addSecurableObject("tmp", tableObject); + expectSQLs = + Lists.newArrayList("CREATE ROLE tmp", "GRANT SELECT ON TABLE schema.table TO ROLE tmp"); + plugin.onRoleUpdated(role, roleChange); + } + + @Test + public void testOwnerManagement() { + + // Test metalake object + Owner owner = new TemporaryOwner("tmp", Owner.Type.USER); + MetadataObject metalakeObject = + MetadataObjects.of(null, "metalake", MetadataObject.Type.METALAKE); + expectSQLs = Lists.newArrayList("CREATE USER tmp"); + currentSQLIndex = 0; + expectTypes.add(MetadataObject.Type.SCHEMA); + expectObjectNames.add("*"); + expectPreOwners.add(Optional.empty()); + expectNewOwners.add(owner); + + expectTypes.add(MetadataObject.Type.TABLE); + expectObjectNames.add("*.*"); + expectPreOwners.add(Optional.empty()); + expectNewOwners.add(owner); + plugin.onOwnerSet(metalakeObject, null, owner); + + // clean up + cleanup(); + expectSQLs = Lists.newArrayList("CREATE USER tmp"); + + // Test catalog object + MetadataObject catalogObject = MetadataObjects.of(null, "catalog", MetadataObject.Type.CATALOG); + expectTypes.add(MetadataObject.Type.SCHEMA); + expectObjectNames.add("*"); + expectPreOwners.add(Optional.empty()); + expectNewOwners.add(owner); + + expectTypes.add(MetadataObject.Type.TABLE); + expectObjectNames.add("*.*"); + expectPreOwners.add(Optional.empty()); + expectNewOwners.add(owner); + plugin.onOwnerSet(catalogObject, null, owner); + + // clean up + cleanup(); + expectSQLs = Lists.newArrayList("CREATE USER tmp"); + + // Test schema object + MetadataObject schemaObject = + MetadataObjects.of("catalog", "schema", MetadataObject.Type.SCHEMA); + expectTypes.add(MetadataObject.Type.SCHEMA); + expectObjectNames.add("schema"); + expectPreOwners.add(Optional.empty()); + expectNewOwners.add(owner); + + expectTypes.add(MetadataObject.Type.TABLE); + expectObjectNames.add("schema.*"); + expectPreOwners.add(Optional.empty()); + expectNewOwners.add(owner); + plugin.onOwnerSet(schemaObject, null, owner); + + // clean up + cleanup(); + expectSQLs = Lists.newArrayList("CREATE USER tmp"); + + // Test table object + MetadataObject tableObject = + MetadataObjects.of( + Lists.newArrayList("catalog", "schema", "table"), MetadataObject.Type.TABLE); + + expectTypes.add(MetadataObject.Type.TABLE); + expectObjectNames.add("schema.table"); + expectPreOwners.add(Optional.empty()); + expectNewOwners.add(owner); + plugin.onOwnerSet(tableObject, null, owner); + } + + private static void resetSQLIndex() { + currentSQLIndex = 0; + } + + private static void cleanup() { + expectTypes.clear(); + expectObjectNames.clear(); + expectPreOwners.clear(); + expectNewOwners.clear(); + currentIndex = 0; + currentSQLIndex = 0; + } + + private static class TemporaryOwner implements Owner { + private final String name; + private final Type type; + + public TemporaryOwner(String name, Type type) { + this.name = name; + this.type = type; + } + + @Override + public String name() { + return name; + } + + @Override + public Type type() { + return type; + } + } + + private static Role createRole(String name) { + return RoleEntity.builder().withId(0L).withName(name).withAuditInfo(AuditInfo.EMPTY).build(); + } + + private static Group createGroup(String name) { + return GroupEntity.builder().withId(0L).withName(name).withAuditInfo(AuditInfo.EMPTY).build(); + } + + private static User createUser(String name) { + return UserEntity.builder().withId(0L).withName(name).withAuditInfo(AuditInfo.EMPTY).build(); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 150acdb00..b3eb56578 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -57,7 +57,7 @@ if (gradle.startParameter.projectProperties["enableFuse"]?.toBoolean() == true) } include("iceberg:iceberg-common") include("iceberg:iceberg-rest-server") -include("authorizations:authorization-ranger") +include("authorizations:authorization-ranger", "authorizations:authorization-jdbc") include("trino-connector:trino-connector", "trino-connector:integration-test") include("spark-connector:spark-common") // kyuubi hive connector doesn't support 2.13 for Spark3.3