From c178a6db23ab513326e6d88985a6b972cb071a18 Mon Sep 17 00:00:00 2001 From: Rory Date: Wed, 18 Dec 2024 15:52:16 +0800 Subject: [PATCH] [#5661] feat(auth): Add JDBC authorization plugin interface --- .../access-control-integration-test.yml | 3 + .../authorization-jdbc/build.gradle.kts | 97 ++++ .../jdbc/JdbcAuthorizationObject.java | 93 ++++ .../jdbc/JdbcAuthorizationSQL.java | 117 +++++ .../authorization/jdbc/JdbcPrivilege.java | 103 ++++ .../jdbc/JdbcSQLBasedAuthorizationPlugin.java | 456 ++++++++++++++++++ .../JdbcSecurableObjectMappingProvider.java | 218 +++++++++ .../authorization/jdbc/TemporaryGroup.java | 51 ++ .../authorization/jdbc/TemporaryUser.java | 51 ++ .../JdbcSQLBasedAuthorizationPluginTest.java | 331 +++++++++++++ settings.gradle.kts | 2 +- 11 files changed, 1521 insertions(+), 1 deletion(-) create mode 100644 authorizations/authorization-jdbc/build.gradle.kts create mode 100644 authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcAuthorizationObject.java create mode 100644 authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcAuthorizationSQL.java create mode 100644 authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcPrivilege.java create mode 100644 authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcSQLBasedAuthorizationPlugin.java create mode 100644 authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcSecurableObjectMappingProvider.java create mode 100644 authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/TemporaryGroup.java create mode 100644 authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/TemporaryUser.java create mode 100644 authorizations/authorization-jdbc/src/test/java/org/apache/gravitino/authorization/jdbc/JdbcSQLBasedAuthorizationPluginTest.java diff --git a/.github/workflows/access-control-integration-test.yml b/.github/workflows/access-control-integration-test.yml index 54ffde2ee82..b64fb011633 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 00000000000..2649370d8cf --- /dev/null +++ b/authorizations/authorization-jdbc/build.gradle.kts @@ -0,0 +1,97 @@ +/* + * 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 { + doFirst { + environment("HADOOP_USER_NAME", "gravitino") + } + 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/JdbcAuthorizationObject.java b/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcAuthorizationObject.java new file mode 100644 index 00000000000..37e64093225 --- /dev/null +++ b/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcAuthorizationObject.java @@ -0,0 +1,93 @@ +/* + * 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 com.google.common.collect.Lists; +import java.util.List; +import javax.annotation.Nullable; +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 JdbcAuthorizationObject implements AuthorizationSecurableObject { + + public static final String ALL = "*"; + private String database; + private String table; + + List privileges; + + JdbcAuthorizationObject(String database, String table, List privileges) { + Preconditions.checkNotNull(database, "Jdbc authorization object database can't null"); + this.database = database; + this.table = table; + this.privileges = privileges; + } + + @Nullable + @Override + public String parent() { + if (table != null) { + return database; + } + + return null; + } + + @Override + public String name() { + if (table != null) { + return table; + } + + return database; + } + + @Override + public List names() { + List names = Lists.newArrayList(); + names.add(database); + if (table != null) { + names.add(table); + } + return names; + } + + @Override + public Type type() { + if (table != null) { + return () -> MetadataObject.Type.TABLE; + } + return () -> MetadataObject.Type.SCHEMA; + } + + @Override + public void validateAuthorizationMetadataObject() throws IllegalArgumentException {} + + @Override + public List privileges() { + return privileges; + } +} 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 00000000000..d9783d35c4b --- /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 a SQL statement to create a user + */ + String getCreateUserSQL(String username); + + /** + * Get SQL statements for creating a group. + * + * @param username the username to drop + * @return a SQL statement to drop a user + */ + String getDropUserSQL(String username); + + /** + * Get SQL statements for creating a role. + * + * @param roleName the role name to create + * @return a SQL statement to create a role + */ + String getCreateRoleSQL(String roleName); + + /** + * Get SQL statements for dropping a role. + * + * @param roleName the role name to drop + * @return a SQL statement to drop a role + */ + 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 a sql statement to grant privilege + */ + 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 a sql statement to revoke privilege + */ + 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 a sql statement to grant role + */ + 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 a sql statement to revoke role + */ + 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 getSetOwnerSQL( + MetadataObject.Type type, String objectName, Owner preOwner, Owner newOwner); +} 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 00000000000..118d61f09de --- /dev/null +++ b/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcPrivilege.java @@ -0,0 +1,103 @@ +/* + * 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 class JdbcPrivilege implements AuthorizationPrivilege { + + private static final JdbcPrivilege SELECT_PRI = new JdbcPrivilege(Type.SELECT); + private static final JdbcPrivilege INSERT_PRI = new JdbcPrivilege(Type.INSERT); + private static final JdbcPrivilege UPDATE_PRI = new JdbcPrivilege(Type.UPDATE); + private static final JdbcPrivilege ALTER_PRI = new JdbcPrivilege(Type.ALTER); + private static final JdbcPrivilege DELETE_PRI = new JdbcPrivilege(Type.DELETE); + private static final JdbcPrivilege ALL_PRI = new JdbcPrivilege(Type.ALL); + private static final JdbcPrivilege CREATE_PRI = new JdbcPrivilege(Type.CREATE); + private static final JdbcPrivilege DROP_PRI = new JdbcPrivilege(Type.DROP); + private static final JdbcPrivilege USAGE_PRI = new JdbcPrivilege(Type.USAGE); + + private final Type type; + + private JdbcPrivilege(Type type) { + this.type = type; + } + + @Override + public String getName() { + return type.getName(); + } + + @Override + public Privilege.Condition condition() { + return Privilege.Condition.ALLOW; + } + + @Override + public boolean equalsTo(String value) { + return false; + } + + static JdbcPrivilege valueOf(Type type) { + switch (type) { + case CREATE: + return CREATE_PRI; + case DELETE: + return DELETE_PRI; + case ALL: + return ALL_PRI; + case DROP: + return DROP_PRI; + case ALTER: + return ALTER_PRI; + case INSERT: + return INSERT_PRI; + case UPDATE: + return UPDATE_PRI; + case SELECT: + return SELECT_PRI; + case USAGE: + return USAGE_PRI; + default: + throw new IllegalArgumentException(String.format("Unsupported parameter type %s", type)); + } + } + + public enum Type { + SELECT("SELECT"), + INSERT("INSERT"), + UPDATE("UPDATE"), + ALTER("ALTER"), + DELETE("DELETE"), + ALL("ALL PRIVILEGES"), + CREATE("CREATE"), + DROP("DROP"), + USAGE("USAGE"); + + private final String name; + + Type(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } +} diff --git a/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcSQLBasedAuthorizationPlugin.java b/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcSQLBasedAuthorizationPlugin.java new file mode 100644 index 00000000000..0aff510e8e2 --- /dev/null +++ b/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcSQLBasedAuthorizationPlugin.java @@ -0,0 +1,456 @@ +/* + * 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.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 JdbcSQLBasedAuthorizationPlugin + implements AuthorizationPlugin, JdbcAuthorizationSQL { + + private static final String CONFIG_PREFIX = "authorization.jdbc."; + public static final String JDBC_DRIVER = CONFIG_PREFIX + "driver"; + public static final String JDBC_URL = CONFIG_PREFIX + "url"; + public static final String JDBC_USERNAME = CONFIG_PREFIX + "username"; + public static final String JDBC_PASSWORD = CONFIG_PREFIX + "password"; + + private static final String GROUP_PREFIX = "GRAVITINO_GROUP_"; + private static final Logger LOG = LoggerFactory.getLogger(JdbcSQLBasedAuthorizationPlugin.class); + + protected BasicDataSource dataSource; + protected JdbcSecurableObjectMappingProvider mappingProvider; + + public JdbcSQLBasedAuthorizationPlugin(Map config) { + // Initialize the data source + dataSource = new BasicDataSource(); + String jdbcUrl = config.get(JDBC_URL); + dataSource.setUrl(jdbcUrl); + dataSource.setDriverClassName(config.get(JDBC_DRIVER)); + dataSource.setUsername(config.get(JDBC_USERNAME)); + dataSource.setPassword(config.get(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(); + } 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 { + beforeExecuteSQL(); + + String sql = getCreateRoleSQL(role.name()); + 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 { + beforeExecuteSQL(); + + String sql = getDropRoleSQL(role.name()); + executeUpdateSQL(sql); + return null; + } + + @Override + public Boolean onRoleUpdated(Role role, RoleChange... changes) + throws AuthorizationPluginException { + beforeExecuteSQL(); + + 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("Don't support RoleChange %s", change)); + } + } + return true; + } + + @Override + public Boolean onGrantedRolesToUser(List roles, User user) + throws AuthorizationPluginException { + beforeExecuteSQL(); + + onUserAdded(user); + + for (Role role : roles) { + onRoleCreated(role); + String sql = getGrantRoleSQL(role.name(), "USER", user.name()); + executeUpdateSQL(sql); + } + return true; + } + + @Override + public Boolean onRevokedRolesFromUser(List roles, User user) + throws AuthorizationPluginException { + beforeExecuteSQL(); + + onUserAdded(user); + + for (Role role : roles) { + onRoleCreated(role); + String sql = getRevokeRoleSQL(role.name(), "USER", user.name()); + executeUpdateSQL(sql); + } + return true; + } + + @Override + public Boolean onGrantedRolesToGroup(List roles, Group group) + throws AuthorizationPluginException { + beforeExecuteSQL(); + + onGroupAdded(group); + + for (Role role : roles) { + onRoleCreated(role); + String sql = + getGrantRoleSQL(role.name(), "USER", String.format("%s%s", GROUP_PREFIX, group.name())); + executeUpdateSQL(sql); + } + return true; + } + + @Override + public Boolean onRevokedRolesFromGroup(List roles, Group group) + throws AuthorizationPluginException { + beforeExecuteSQL(); + + onGroupAdded(group); + + for (Role role : roles) { + onRoleCreated(role); + String sql = + getRevokeRoleSQL(role.name(), "USER", String.format("%s%s", GROUP_PREFIX, group.name())); + executeUpdateSQL(sql); + } + return true; + } + + @Override + public Boolean onUserAdded(User user) throws AuthorizationPluginException { + beforeExecuteSQL(); + + String sql = getCreateUserSQL(user.name()); + executeUpdateSQL(sql); + return true; + } + + @Override + public Boolean onUserRemoved(User user) throws AuthorizationPluginException { + beforeExecuteSQL(); + + String sql = getDropUserSQL(user.name()); + 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 { + beforeExecuteSQL(); + + String name = String.format("%s%s", GROUP_PREFIX, group.name()); + String sql = getCreateUserSQL(name); + executeUpdateSQL(sql); + return true; + } + + @Override + public Boolean onGroupRemoved(Group group) throws AuthorizationPluginException { + beforeExecuteSQL(); + + String name = String.format("%s%s", GROUP_PREFIX, group.name()); + String sql = getDropUserSQL(name); + 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 { + beforeExecuteSQL(); + + if (newOwner.type() == Owner.Type.USER) { + onUserAdded(new TemporaryUser(newOwner.name())); + } else if (newOwner.type() == Owner.Type.GROUP) { + onGroupAdded(new TemporaryGroup(newOwner.name())); + } else { + throw new IllegalArgumentException( + String.format("Don't support owner type %s", newOwner.type())); + } + + List authObjects = mappingProvider.translateOwner(metadataObject); + for (AuthorizationSecurableObject authObject : authObjects) { + List sqls = + getSetOwnerSQL( + authObject.type().metadataObjectType(), authObject.fullName(), preOwner, newOwner); + for (String sql : sqls) { + executeUpdateSQL(sql); + } + } + return true; + } + + @Override + public String getCreateUserSQL(String username) { + return String.format("CREATE USER %s", username); + } + + @Override + public String getDropUserSQL(String username) { + return String.format("DROP USER %s", username); + } + + @Override + public String getCreateRoleSQL(String roleName) { + return String.format("CREATE ROLE %s", roleName); + } + + @Override + public String getDropRoleSQL(String roleName) { + return String.format("DROP ROLE %s", roleName); + } + + @Override + public String getGrantPrivilegeSQL( + String privilege, String objectType, String objectName, String roleName) { + return String.format( + "GRANT %s ON %s %s TO ROLE %s", privilege, objectType, objectName, roleName); + } + + @Override + public String getRevokePrivilegeSQL( + String privilege, String objectType, String objectName, String roleName) { + return String.format( + "REVOKE %s ON %s %s FROM ROLE %s", privilege, objectType, objectName, roleName); + } + + @Override + public String getGrantRoleSQL(String roleName, String grantorType, String grantorName) { + return String.format("GRANT ROLE %s TO %s %s", roleName, grantorType, grantorName); + } + + @Override + public String getRevokeRoleSQL(String roleName, String revokerType, String revokerName) { + return String.format("REVOKE ROLE %s FROM %s %s", roleName, revokerType, revokerName); + } + + protected void beforeExecuteSQL() { + // Do nothing by default. + } + + @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 + * @return + */ + protected List convertResourceAll( + AuthorizationSecurableObject object) { + List authObjects = Lists.newArrayList(); + authObjects.add(object); + return authObjects; + } + + protected List filterUnsupportedPrivileges( + List 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 authObjects = mappingProvider.translatePrivilege(object); + for (AuthorizationSecurableObject authObject : authObjects) { + List convertedObjects = Lists.newArrayList(); + if (authObject.name().equals(JdbcAuthorizationObject.ALL)) { + convertedObjects.addAll(convertResourceAll(authObject)); + } else { + convertedObjects.add(authObject); + } + + for (AuthorizationSecurableObject convertedObject : convertedObjects) { + List 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) { + String sql = + getGrantPrivilegeSQL( + privilege, + convertedObject.metadataObjectType().name(), + convertedObject.fullName(), + role.name()); + executeUpdateSQL(sql, "is already granted"); + } + } + } + } + + private void revokeObjectPrivileges(Role role, SecurableObject removeObject) { + List authObjects = + mappingProvider.translatePrivilege(removeObject); + for (AuthorizationSecurableObject authObject : authObjects) { + List convertedObjects = Lists.newArrayList(); + if (authObject.name().equals(JdbcAuthorizationObject.ALL)) { + convertedObjects.addAll(convertResourceAll(authObject)); + } else { + convertedObjects.add(authObject); + } + + for (AuthorizationSecurableObject convertedObject : convertedObjects) { + List 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. + String sql = + getRevokePrivilegeSQL( + privilege, + convertedObject.metadataObjectType().name(), + convertedObject.fullName(), + role.name()); + executeUpdateSQL(sql, "Cannot find privilege Privilege"); + } + } + } + } +} 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 00000000000..b998f9a3bf1 --- /dev/null +++ b/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/JdbcSecurableObjectMappingProvider.java @@ -0,0 +1,218 @@ +/* + * 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> privilegeMapping = + ImmutableMap.of( + Privilege.Name.CREATE_TABLE, + Sets.newHashSet(JdbcPrivilege.valueOf(JdbcPrivilege.Type.CREATE)), + Privilege.Name.CREATE_SCHEMA, + Sets.newHashSet(JdbcPrivilege.valueOf(JdbcPrivilege.Type.CREATE)), + Privilege.Name.SELECT_TABLE, + Sets.newHashSet(JdbcPrivilege.valueOf(JdbcPrivilege.Type.SELECT)), + Privilege.Name.MODIFY_TABLE, + Sets.newHashSet( + JdbcPrivilege.valueOf(JdbcPrivilege.Type.SELECT), + JdbcPrivilege.valueOf(JdbcPrivilege.Type.UPDATE), + JdbcPrivilege.valueOf(JdbcPrivilege.Type.DELETE), + JdbcPrivilege.valueOf(JdbcPrivilege.Type.INSERT), + JdbcPrivilege.valueOf(JdbcPrivilege.Type.ALTER)), + Privilege.Name.USE_SCHEMA, + Sets.newHashSet(JdbcPrivilege.valueOf(JdbcPrivilege.Type.USAGE))); + + private final Map privilegeScopeMapping = + ImmutableMap.of( + Privilege.Name.CREATE_TABLE, PrivilegeScope.TABLE, + Privilege.Name.CREATE_SCHEMA, PrivilegeScope.DATABASE, + Privilege.Name.SELECT_TABLE, PrivilegeScope.TABLE, + Privilege.Name.MODIFY_TABLE, PrivilegeScope.TABLE, + Privilege.Name.USE_SCHEMA, PrivilegeScope.DATABASE); + + private final Set ownerPrivileges = ImmutableSet.of(); + + private final Set allowObjectTypes = + ImmutableSet.of( + MetadataObject.Type.METALAKE, + MetadataObject.Type.CATALOG, + MetadataObject.Type.SCHEMA, + MetadataObject.Type.TABLE); + + @Override + public Map> privilegesMappingRule() { + return privilegeMapping; + } + + @Override + public Set ownerMappingRule() { + return ownerPrivileges; + } + + @Override + public Set allowPrivilegesRule() { + return privilegeMapping.keySet(); + } + + @Override + public Set allowMetadataObjectTypesRule() { + return allowObjectTypes; + } + + @Override + public List translatePrivilege(SecurableObject securableObject) { + List authObjects = Lists.newArrayList(); + List databasePrivileges = Lists.newArrayList(); + List tablePrivileges = Lists.newArrayList(); + JdbcAuthorizationObject databaseObject; + JdbcAuthorizationObject tableObject; + switch (securableObject.type()) { + case METALAKE: + case CATALOG: + convertPluginPrivileges(securableObject, databasePrivileges, tablePrivileges); + + if (!databasePrivileges.isEmpty()) { + databaseObject = + new JdbcAuthorizationObject(JdbcAuthorizationObject.ALL, null, databasePrivileges); + authObjects.add(databaseObject); + } + + if (!tablePrivileges.isEmpty()) { + tableObject = + new JdbcAuthorizationObject( + JdbcAuthorizationObject.ALL, JdbcAuthorizationObject.ALL, tablePrivileges); + authObjects.add(tableObject); + } + break; + + case SCHEMA: + convertPluginPrivileges(securableObject, databasePrivileges, tablePrivileges); + if (!databasePrivileges.isEmpty()) { + databaseObject = + new JdbcAuthorizationObject(securableObject.name(), null, databasePrivileges); + authObjects.add(databaseObject); + } + + if (!tablePrivileges.isEmpty()) { + tableObject = + new JdbcAuthorizationObject( + securableObject.name(), JdbcAuthorizationObject.ALL, tablePrivileges); + authObjects.add(tableObject); + } + break; + + case TABLE: + convertPluginPrivileges(securableObject, databasePrivileges, tablePrivileges); + if (!tablePrivileges.isEmpty()) { + MetadataObject metadataObject = + MetadataObjects.parse(securableObject.parent(), MetadataObject.Type.SCHEMA); + tableObject = + new JdbcAuthorizationObject( + 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 translateOwner(MetadataObject metadataObject) { + List objects = Lists.newArrayList(); + JdbcPrivilege allPri = JdbcPrivilege.valueOf(JdbcPrivilege.Type.ALL); + switch (metadataObject.type()) { + case METALAKE: + case CATALOG: + objects.add( + new JdbcAuthorizationObject( + JdbcAuthorizationObject.ALL, null, Lists.newArrayList(allPri))); + objects.add( + new JdbcAuthorizationObject( + JdbcAuthorizationObject.ALL, + JdbcAuthorizationObject.ALL, + Lists.newArrayList(allPri))); + break; + case SCHEMA: + objects.add( + new JdbcAuthorizationObject(metadataObject.name(), null, Lists.newArrayList(allPri))); + objects.add( + new JdbcAuthorizationObject( + metadataObject.name(), JdbcAuthorizationObject.ALL, Lists.newArrayList(allPri))); + break; + case TABLE: + MetadataObject schema = + MetadataObjects.parse(metadataObject.parent(), MetadataObject.Type.SCHEMA); + objects.add( + new JdbcAuthorizationObject( + schema.name(), metadataObject.name(), Lists.newArrayList(allPri))); + break; + default: + throw new IllegalArgumentException(""); + } + return objects; + } + + @Override + public AuthorizationMetadataObject translateMetadataObject(MetadataObject metadataObject) { + throw new UnsupportedOperationException("Not supported"); + } + + private void convertPluginPrivileges( + SecurableObject securableObject, + List databasePrivileges, + List tablePrivileges) { + for (Privilege privilege : securableObject.privileges()) { + if (privilegeScopeMapping.get(privilege.name()) == PrivilegeScope.DATABASE) { + databasePrivileges.addAll(privilegeMapping.get(privilege.name())); + } else if (privilegeScopeMapping.get(privilege.name()) == PrivilegeScope.TABLE) { + tablePrivileges.addAll(privilegeMapping.get(privilege.name())); + } + } + } + + enum PrivilegeScope { + DATABASE, + TABLE + } +} diff --git a/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/TemporaryGroup.java b/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/TemporaryGroup.java new file mode 100644 index 00000000000..98f72354fa2 --- /dev/null +++ b/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/TemporaryGroup.java @@ -0,0 +1,51 @@ +/* + * 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.ImmutableList; +import java.util.List; +import org.apache.gravitino.Audit; +import org.apache.gravitino.authorization.Group; +import org.apache.gravitino.meta.AuditInfo; + +/* + * TemporaryGroup is used for translating the owner to the SQL. + */ +class TemporaryGroup implements Group { + private final String name; + + TemporaryGroup(String name) { + this.name = name; + } + + @Override + public String name() { + return name; + } + + @Override + public List roles() { + return ImmutableList.of(); + } + + @Override + public Audit auditInfo() { + return AuditInfo.EMPTY; + } +} diff --git a/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/TemporaryUser.java b/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/TemporaryUser.java new file mode 100644 index 00000000000..aa47710bf75 --- /dev/null +++ b/authorizations/authorization-jdbc/src/main/java/org/apache/gravitino/authorization/jdbc/TemporaryUser.java @@ -0,0 +1,51 @@ +/* + * 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.ImmutableList; +import java.util.List; +import org.apache.gravitino.Audit; +import org.apache.gravitino.authorization.User; +import org.apache.gravitino.meta.AuditInfo; + +/* + * TemporaryUser is used for translating the owner to the SQL. + */ +class TemporaryUser implements User { + private final String name; + + TemporaryUser(String name) { + this.name = name; + } + + @Override + public String name() { + return name; + } + + @Override + public List roles() { + return ImmutableList.of(); + } + + @Override + public Audit auditInfo() { + return AuditInfo.EMPTY; + } +} diff --git a/authorizations/authorization-jdbc/src/test/java/org/apache/gravitino/authorization/jdbc/JdbcSQLBasedAuthorizationPluginTest.java b/authorizations/authorization-jdbc/src/test/java/org/apache/gravitino/authorization/jdbc/JdbcSQLBasedAuthorizationPluginTest.java new file mode 100644 index 00000000000..58e335284a0 --- /dev/null +++ b/authorizations/authorization-jdbc/src/test/java/org/apache/gravitino/authorization/jdbc/JdbcSQLBasedAuthorizationPluginTest.java @@ -0,0 +1,331 @@ +/* + * 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.Lists; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.apache.gravitino.Audit; +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.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class JdbcSQLBasedAuthorizationPluginTest { + private static List expectSQLs = Lists.newArrayList(); + private static List expectTypes = Lists.newArrayList(); + private static List expectObjectNames = Lists.newArrayList(); + private static List> expectPreOwners = Lists.newArrayList(); + private static List expectNewOwners = Lists.newArrayList(); + private static int currentSQLIndex = 0; + private static int currentIndex = 0; + + private static final JdbcSQLBasedAuthorizationPlugin plugin = + new JdbcSQLBasedAuthorizationPlugin(Collections.emptyMap()) { + + @Override + public List 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(new TemporaryUser("tmp")); + + Assertions.assertThrows( + UnsupportedOperationException.class, () -> plugin.onUserAcquired(new TemporaryUser("tmp"))); + + expectSQLs = Lists.newArrayList("DROP USER tmp"); + currentSQLIndex = 0; + plugin.onUserRemoved(new TemporaryUser("tmp")); + } + + @Test + public void testGroupManagement() { + expectSQLs = Lists.newArrayList("CREATE USER GRAVITINO_GROUP_tmp"); + currentSQLIndex = 0; + plugin.onGroupAdded(new TemporaryGroup("tmp")); + + Assertions.assertThrows( + UnsupportedOperationException.class, + () -> plugin.onGroupAcquired(new TemporaryGroup("tmp"))); + + expectSQLs = Lists.newArrayList("DROP USER GRAVITINO_GROUP_tmp"); + currentSQLIndex = 0; + plugin.onGroupRemoved(new TemporaryGroup("tmp")); + } + + @Test + public void testRoleManagement() { + expectSQLs = Lists.newArrayList("CREATE ROLE tmp"); + currentSQLIndex = 0; + Role role = new TemporaryRole("tmp"); + plugin.onRoleCreated(role); + + Assertions.assertThrows(UnsupportedOperationException.class, () -> plugin.onRoleAcquired(role)); + + currentSQLIndex = 0; + expectSQLs = Lists.newArrayList("DROP ROLE tmp"); + plugin.onRoleDeleted(role); + } + + @Test + public void testPermissionManagement() { + Role role = new TemporaryRole("tmp"); + Group group = new TemporaryGroup("tmp"); + User user = new TemporaryUser("tmp"); + + currentSQLIndex = 0; + expectSQLs = + Lists.newArrayList( + "CREATE USER GRAVITINO_GROUP_tmp", + "CREATE ROLE tmp", + "GRANT ROLE tmp TO USER GRAVITINO_GROUP_tmp"); + plugin.onGrantedRolesToGroup(Lists.newArrayList(role), group); + + currentSQLIndex = 0; + expectSQLs = + Lists.newArrayList("CREATE USER tmp", "CREATE ROLE tmp", "GRANT ROLE tmp TO USER tmp"); + plugin.onGrantedRolesToUser(Lists.newArrayList(role), user); + + currentSQLIndex = 0; + expectSQLs = + Lists.newArrayList( + "CREATE USER GRAVITINO_GROUP_tmp", + "CREATE ROLE tmp", + "REVOKE ROLE tmp FROM USER GRAVITINO_GROUP_tmp"); + plugin.onRevokedRolesFromGroup(Lists.newArrayList(role), group); + + currentSQLIndex = 0; + expectSQLs = + Lists.newArrayList("CREATE USER tmp", "CREATE ROLE tmp", "REVOKE ROLE tmp FROM USER tmp"); + plugin.onRevokedRolesFromUser(Lists.newArrayList(role), user); + + // Test metalake object and different role change + currentSQLIndex = 0; + 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); + + currentSQLIndex = 0; + expectSQLs = Lists.newArrayList("CREATE ROLE tmp", "REVOKE SELECT ON TABLE *.* FROM ROLE tmp"); + roleChange = RoleChange.removeSecurableObject("tmp", metalakeObject); + plugin.onRoleUpdated(role, roleChange); + + currentSQLIndex = 0; + 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 + currentSQLIndex = 0; + 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 + currentSQLIndex = 0; + 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 + currentSQLIndex = 0; + 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 + expectTypes.clear(); + expectObjectNames.clear(); + expectPreOwners.clear(); + expectNewOwners.clear(); + currentIndex = 0; + expectSQLs = Lists.newArrayList("CREATE USER tmp"); + currentSQLIndex = 0; + + // 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 + expectTypes.clear(); + expectObjectNames.clear(); + expectPreOwners.clear(); + expectNewOwners.clear(); + currentIndex = 0; + expectSQLs = Lists.newArrayList("CREATE USER tmp"); + currentSQLIndex = 0; + + // 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 + expectTypes.clear(); + expectObjectNames.clear(); + expectPreOwners.clear(); + expectNewOwners.clear(); + currentIndex = 0; + expectSQLs = Lists.newArrayList("CREATE USER tmp"); + currentSQLIndex = 0; + + // 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 class TemporaryRole implements Role { + private final String name; + + public TemporaryRole(String name) { + this.name = name; + } + + @Override + public Audit auditInfo() { + return AuditInfo.EMPTY; + } + + @Override + public String name() { + return name; + } + + @Override + public Map properties() { + return Collections.emptyMap(); + } + + @Override + public List securableObjects() { + return Collections.emptyList(); + } + } + + 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; + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index a36fde93cd3..53427e7cac2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -56,7 +56,7 @@ if (gradle.startParameter.projectProperties["enableFuse"]?.toBoolean() ?: false) } 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