From bfd77c447d738c25b6fcfc79ff414a76ecfe9934 Mon Sep 17 00:00:00 2001 From: Madhan Neethiraj Date: Thu, 31 Oct 2024 09:55:30 -0700 Subject: [PATCH] Add Apache Ranger authorizer plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: lozbrown Co-authored-by: Grzegorz KokosiƄski --- .github/workflows/ci.yml | 1 + core/trino-server/src/main/provisio/trino.xml | 6 + docs/src/main/sphinx/security.md | 1 + .../security/apache-ranger-access-control.md | 164 +++ .../built-in-system-access-control.md | 3 + docs/src/main/sphinx/security/overview.md | 3 + plugin/trino-apache-ranger/pom.xml | 198 ++++ .../plugin/ranger/ApacheRangerPlugin.java | 29 + .../io/trino/plugin/ranger/RangerConfig.java | 75 ++ .../ranger/RangerSystemAccessControl.java | 1017 +++++++++++++++++ .../RangerSystemAccessControlFactory.java | 63 + .../ranger/RangerTrinoAccessRequest.java | 38 + .../plugin/ranger/RangerTrinoAccessType.java | 33 + .../ranger/RangerTrinoEventListener.java | 74 ++ .../plugin/ranger/RangerTrinoResource.java | 151 +++ .../plugin/ranger/RangerAdminClientImpl.java | 57 + .../ranger/TestRangerSystemAccessControl.java | 327 ++++++ .../test/resources/ranger-trino-security.xml | 52 + .../src/test/resources/trino-policies.json | 305 +++++ pom.xml | 14 + .../io/trino/tests/product/TestGroups.java | 1 + .../environment/EnvMultinodeApacheRanger.java | 89 ++ .../suite/suites/SuiteApacheRanger.java | 37 + .../access-control.properties | 4 + .../mariadb.properties | 4 + .../ranger-policymgr-ssl.xml | 44 + .../ranger-trino-audit.xml | 24 + .../ranger-trino-security.xml | 48 + .../trino-policies.json | 189 +++ .../product/ranger/TestApacheRanger.java | 63 + 30 files changed, 3114 insertions(+) create mode 100644 docs/src/main/sphinx/security/apache-ranger-access-control.md create mode 100644 plugin/trino-apache-ranger/pom.xml create mode 100644 plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/ApacheRangerPlugin.java create mode 100644 plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/RangerConfig.java create mode 100644 plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/RangerSystemAccessControl.java create mode 100644 plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/RangerSystemAccessControlFactory.java create mode 100644 plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/RangerTrinoAccessRequest.java create mode 100644 plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/RangerTrinoAccessType.java create mode 100644 plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/RangerTrinoEventListener.java create mode 100644 plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/RangerTrinoResource.java create mode 100644 plugin/trino-apache-ranger/src/test/java/io/trino/plugin/ranger/RangerAdminClientImpl.java create mode 100644 plugin/trino-apache-ranger/src/test/java/io/trino/plugin/ranger/TestRangerSystemAccessControl.java create mode 100644 plugin/trino-apache-ranger/src/test/resources/ranger-trino-security.xml create mode 100644 plugin/trino-apache-ranger/src/test/resources/trino-policies.json create mode 100644 testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/env/environment/EnvMultinodeApacheRanger.java create mode 100644 testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/suite/suites/SuiteApacheRanger.java create mode 100644 testing/trino-product-tests-launcher/src/main/resources/docker/trino-product-tests/conf/environment/multinode-apache-ranger/access-control.properties create mode 100644 testing/trino-product-tests-launcher/src/main/resources/docker/trino-product-tests/conf/environment/multinode-apache-ranger/mariadb.properties create mode 100644 testing/trino-product-tests-launcher/src/main/resources/docker/trino-product-tests/conf/environment/multinode-apache-ranger/ranger-policymgr-ssl.xml create mode 100644 testing/trino-product-tests-launcher/src/main/resources/docker/trino-product-tests/conf/environment/multinode-apache-ranger/ranger-trino-audit.xml create mode 100644 testing/trino-product-tests-launcher/src/main/resources/docker/trino-product-tests/conf/environment/multinode-apache-ranger/ranger-trino-security.xml create mode 100644 testing/trino-product-tests-launcher/src/main/resources/docker/trino-product-tests/conf/environment/multinode-apache-ranger/trino-policies.json create mode 100644 testing/trino-product-tests/src/main/java/io/trino/tests/product/ranger/TestApacheRanger.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20a840fcc289..cb6a029e0491 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -881,6 +881,7 @@ jobs: - suite-delta-lake-databricks143 - suite-delta-lake-databricks154 - suite-databricks-unity-http-hms + - suite-apache-ranger - suite-gcs - suite-clients - suite-functions diff --git a/core/trino-server/src/main/provisio/trino.xml b/core/trino-server/src/main/provisio/trino.xml index 9b96d3d97f2e..6d290e42b81b 100644 --- a/core/trino-server/src/main/provisio/trino.xml +++ b/core/trino-server/src/main/provisio/trino.xml @@ -33,6 +33,12 @@ + + + + + + diff --git a/docs/src/main/sphinx/security.md b/docs/src/main/sphinx/security.md index e80f32c5cc2b..b0c51440451b 100644 --- a/docs/src/main/sphinx/security.md +++ b/docs/src/main/sphinx/security.md @@ -51,6 +51,7 @@ security/group-file security/built-in-system-access-control security/file-system-access-control security/opa-access-control +security/apache-ranger-access-control ``` ## Security inside the cluster diff --git a/docs/src/main/sphinx/security/apache-ranger-access-control.md b/docs/src/main/sphinx/security/apache-ranger-access-control.md new file mode 100644 index 000000000000..129682f53dd2 --- /dev/null +++ b/docs/src/main/sphinx/security/apache-ranger-access-control.md @@ -0,0 +1,164 @@ +# Apache Ranger access control + +The Apache Ranger access control plugin supports use of Apache Ranger policies to authorize data access in Trino on catalogs, schemas, tables, and columns. The plugin also supports column-masking, row-filtering and audit logging. + +## Requirements + +* Access to a Apache Ranger deployment with the desired authorization policies. +* Access to an audit store using Solr, HDFS, Log4J, or S3 to save audit logs. +* Apache Ranger 2.5.0 and greater include the required Trino service definition. Earlier versions of Apache Ranger require an update of the service definition available in the version [here]( + https://github.com/apache/ranger/blob/ranger-2.5/agents-common/src/main/resources/service-defs/ranger-servicedef-trino.json). + +## Configuration + +To use only Ranger for access control, create the file `etc/access-control.properties` on the coordinator, +with the following configuration, and configurations listed in the table below: + +```properties +access-control.name=apache-ranger +``` + + +To combine Ranger access control with file-based or other access control systems, create the file +`etc/access-control.properties` on the coordinator, with the following configuration that lists +multiple access control configuration file paths: + +```properties +access-control.config-files=etc/trino/file-based.properties,etc/trino/apache-ranger.properties +``` + +Order the configuration files list in the desired order of the different systems +for overall access control. Configure each access-control system in the +specified files. + +The following table lists the configuration properties for the Ranger access control: + +:::{list-table} Apache Ranger access control configuration properties +:widths: 30, 70 +:header-rows: 1 + +* - Name + - Description +* - `apache-ranger.service.name` + - Name of the service having policies to be enforced by the plugin +* - `apache-ranger.plugin.config.resource` + - List of Ranger plugin configuration files, comma separated. Relative paths will be resolved dynamically by searching in the classpath. +* - `apache-ranger.hadoop.config.resource` + - List of Hadoop configuration files, comma separated. Relative paths will be resolved dynamically by searching in the classpath. +::: + +### ranger-trino-security.xml +``` + + + + ranger.plugin.trino.policy.rest.url + https://ranger-hostname:6182 + MANDATORY: a comma separated list of URLs to Apache Ranger instances in a deployment + + + + ranger.plugin.trino.access.cluster.name + + Name to identify the cluster running the Trino instance. This is recorded in audit logs generated by the plugin + + + + ranger.plugin.trino.use.rangerGroups + false + Boolean flag to specify whether user-to-groups mapping should be obtained from in Apache Ranger. Default: false + + + + ranger.plugin.trino.use.only.rangerGroups + false + Boolean flag. true: use only user-to-groups mapping from Apache Ranger; false: use user-to-groups mappings from Apache Ranger and Trino. Default: false + + + + ranger.plugin.trino.super.users + + Comma separated list of user names. Superusers will be authorized for all accesses, without requiring explicit policy grants. + + + + ranger.plugin.trino.super.groups + + Comma separated list of group names. Users in supergroups will be authorized for all accesses, without requiring explicit policy grants + + +``` + +### ranger-trino-audit.xml +``` + + + + xasecure.audit.is.enabled + true + Boolean flag to specify if the plugin should generate access audit logs. Default: true + + + + xasecure.audit.solr.is.enabled + false + Boolean flag to specify if audit logs should be stored in Solr. Default: false + + + + xasecure.audit.solr.solr_url + + URL to Solr deployment where the plugin should send access audits to + + +``` + +### ranger-policymgr-ssl.xml +``` + + + + + xasecure.policymgr.clientssl.keystore + + Path to keystore file. Only required for two-way SSL. This property should not be included for one-way SSL + + + + xasecure.policymgr.clientssl.keystore.type + jks + Type of keystore. Default: jks + + + + xasecure.policymgr.clientssl.keystore.credential.file + + Path to credential file for the keystore; the credential should be in alias sslKeyStore. Only required for two-way SSL. This property should not be included for one-way SSL + + + + xasecure.policymgr.clientssl.truststore + + Path to truststore file + + + + xasecure.policymgr.clientssl.truststore.type + jks + Type of truststore. Default: jks + + + + xasecure.policymgr.clientssl.truststore.credential.file + + Path to credential file for the truststore; the credential should be in alias sslTrustStore + + +``` + +## Required policies + +* Users will need permission to execute queries in Trino. Without a policy in Apache Ranger to grant this permission, users will not be able to execute any query. + * To allow this, create a policy in Apache Ranger for `queryId` resource having value `*`, with `execute` permission for user `{USER}`. +* Users will need permission to impersonate themselves in Trino. Without a policy in Apache Ranger to grant this permission, users will not be able to execute any query. + * To allow this, create a policy in Apache Ranger for `trinouser` resource having value `{USER}`, with `impersonate` permission for user `{USER}`. diff --git a/docs/src/main/sphinx/security/built-in-system-access-control.md b/docs/src/main/sphinx/security/built-in-system-access-control.md index 3ac4ca693fbd..f991e1e0538d 100644 --- a/docs/src/main/sphinx/security/built-in-system-access-control.md +++ b/docs/src/main/sphinx/security/built-in-system-access-control.md @@ -42,6 +42,9 @@ Trino offers the following built-in system access control implementations: * - `opa` - Use Open Policy Agent (OPA) for authorization. See [](/security/opa-access-control). +* - `ranger` + - Use Apache Ranger policies for authorization. See + [](/security/apache-ranger-access-control). ::: If you want to limit access on a system level in any other way than the ones diff --git a/docs/src/main/sphinx/security/overview.md b/docs/src/main/sphinx/security/overview.md index f44568e7732c..a2887201e84f 100644 --- a/docs/src/main/sphinx/security/overview.md +++ b/docs/src/main/sphinx/security/overview.md @@ -116,6 +116,9 @@ To implement access control, use: the catalog, schema, or table level. - [](opa-access-control), where you use Open Policy Agent to make access control decisions on a fined-grained level. +- [](apache-ranger-access-control), where you use Apache Ranger to make fine-grained + access control decisions, apply dynamic row-filters and column-masking at + query execution time, and generate audit logs. In addition, Trino {doc}`provides an API ` that allows you to create a custom access control method, or to extend an existing diff --git a/plugin/trino-apache-ranger/pom.xml b/plugin/trino-apache-ranger/pom.xml new file mode 100644 index 000000000000..82902045679c --- /dev/null +++ b/plugin/trino-apache-ranger/pom.xml @@ -0,0 +1,198 @@ + + + 4.0.0 + + + io.trino + trino-root + 466-SNAPSHOT + ../../pom.xml + + + trino-apache-ranger + + trino-plugin + Trino - Apache Ranger access control + Trino - Apache Ranger access control + + + 3.2.2 + 24.1.0 + 1.2.0 + 0.8.1 + 1.19.3 + 2.5.0 + + + + + com.google.guava + guava + + + com.google.inject + guice + + + io.airlift + bootstrap + + + io.airlift + configuration + + + io.airlift + log + + + io.trino.hadoop + hadoop-apache + + + org.apache.ranger + ranger-plugins-common + ${dep.ranger.version} + + + commons-logging + commons-logging + + + org.apache.hadoop + hadoop-common + + + + + com.fasterxml.jackson.core + jackson-annotations + provided + + + io.opentelemetry + opentelemetry-api + provided + + + io.opentelemetry + opentelemetry-context + provided + + + io.trino + trino-spi + provided + + + com.amazonaws + aws-java-sdk-logs + ${dep.aws-sdk.version} + runtime + + + com.carrotsearch + hppc + ${dep.hppc.version} + runtime + + + com.google.code.gson + gson + runtime + + + commons-collections + commons-collections + ${dep.commons.collections.version} + runtime + + + org.apache.hadoop.thirdparty + hadoop-shaded-guava + ${dep.hadoop-shaded-guava.version} + runtime + + + org.apache.ranger + ranger-plugins-audit + ${dep.ranger.version} + runtime + + + commons-logging + commons-logging + + + org.apache.hadoop + hadoop-common + + + + + org.apache.zookeeper + zookeeper + runtime + + + org.graalvm.js + js-scriptengine + ${dep.graalvm.version} + runtime + + + org.graalvm.polyglot + inspect-community + ${dep.graalvm.version} + pom + runtime + + + org.graalvm.polyglot + js-community + ${dep.graalvm.version} + pom + runtime + + + org.graalvm.polyglot + polyglot + ${dep.graalvm.version} + runtime + + + org.graalvm.polyglot + profiler-community + ${dep.graalvm.version} + pom + runtime + + + org.graalvm.sdk + graal-sdk + ${dep.graalvm.version} + runtime + + + org.graalvm.truffle + truffle-api + ${dep.graalvm.version} + runtime + + + io.airlift + junit-extensions + test + + + org.assertj + assertj-core + test + + + org.junit.jupiter + junit-jupiter-api + test + + + diff --git a/plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/ApacheRangerPlugin.java b/plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/ApacheRangerPlugin.java new file mode 100644 index 000000000000..0bd1800bcff4 --- /dev/null +++ b/plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/ApacheRangerPlugin.java @@ -0,0 +1,29 @@ +/* + * Licensed 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 io.trino.plugin.ranger; + +import io.trino.spi.Plugin; +import io.trino.spi.security.SystemAccessControlFactory; + +import static java.util.Collections.singletonList; + +public class ApacheRangerPlugin + implements Plugin +{ + @Override + public Iterable getSystemAccessControlFactories() + { + return singletonList(new RangerSystemAccessControlFactory()); + } +} diff --git a/plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/RangerConfig.java b/plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/RangerConfig.java new file mode 100644 index 000000000000..a35662d5dd83 --- /dev/null +++ b/plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/RangerConfig.java @@ -0,0 +1,75 @@ +/* + * Licensed 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 io.trino.plugin.ranger; + +import com.google.common.collect.ImmutableList; +import io.airlift.configuration.Config; +import io.airlift.configuration.ConfigDescription; +import io.airlift.configuration.validation.FileExists; + +import java.io.File; +import java.util.List; + +import static com.google.common.collect.ImmutableList.toImmutableList; + +public class RangerConfig +{ + private String serviceName; + private List pluginConfigResource = ImmutableList.of(); + private List hadoopConfigResource = ImmutableList.of(); + + public String getServiceName() + { + return serviceName; + } + + @Config("apache-ranger.service.name") + @ConfigDescription("Name of Ranger service containing policies to enforce") + public RangerConfig setServiceName(String serviceName) + { + this.serviceName = serviceName; + return this; + } + + public List<@FileExists File> getPluginConfigResource() + { + return pluginConfigResource; + } + + @Config("apache-ranger.plugin.config.resource") + @ConfigDescription("List of paths to Ranger plugin configuration files") + public RangerConfig setPluginConfigResource(List pluginConfigResource) + { + this.pluginConfigResource = pluginConfigResource.stream() + .map(File::new) + .collect(toImmutableList()); + return this; + } + + @Config("apache-ranger.hadoop.config.resource") + @ConfigDescription("List of paths to hadoop configuration files") + @SuppressWarnings("unused") + public RangerConfig setHadoopConfigResource(List hadoopConfigResource) + { + this.hadoopConfigResource = hadoopConfigResource.stream() + .map(File::new) + .collect(toImmutableList()); + return this; + } + + public List<@FileExists File> getHadoopConfigResource() + { + return hadoopConfigResource; + } +} diff --git a/plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/RangerSystemAccessControl.java b/plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/RangerSystemAccessControl.java new file mode 100644 index 000000000000..560ff24106d1 --- /dev/null +++ b/plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/RangerSystemAccessControl.java @@ -0,0 +1,1017 @@ +/* + * Licensed 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 io.trino.plugin.ranger; + +import com.google.common.collect.ImmutableList; +import com.google.inject.Inject; +import io.airlift.log.Logger; +import io.trino.spi.QueryId; +import io.trino.spi.connector.CatalogSchemaName; +import io.trino.spi.connector.CatalogSchemaRoutineName; +import io.trino.spi.connector.CatalogSchemaTableName; +import io.trino.spi.connector.EntityKindAndName; +import io.trino.spi.connector.EntityPrivilege; +import io.trino.spi.connector.SchemaTableName; +import io.trino.spi.eventlistener.EventListener; +import io.trino.spi.function.SchemaFunctionName; +import io.trino.spi.security.Identity; +import io.trino.spi.security.Privilege; +import io.trino.spi.security.SystemAccessControl; +import io.trino.spi.security.SystemSecurityContext; +import io.trino.spi.security.TrinoPrincipal; +import io.trino.spi.security.ViewExpression; +import io.trino.spi.type.Type; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.security.UserGroupInformation; +import org.apache.ranger.authorization.hadoop.config.RangerPluginConfig; +import org.apache.ranger.plugin.audit.RangerDefaultAuditHandler; +import org.apache.ranger.plugin.model.RangerPolicy; +import org.apache.ranger.plugin.model.RangerServiceDef; +import org.apache.ranger.plugin.policyengine.RangerAccessRequest; +import org.apache.ranger.plugin.policyengine.RangerAccessResult; +import org.apache.ranger.plugin.service.RangerBasePlugin; + +import java.io.File; +import java.net.URL; +import java.security.Principal; +import java.time.Instant; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Strings.isNullOrEmpty; +import static io.trino.plugin.ranger.RangerTrinoAccessType.ALTER; +import static io.trino.plugin.ranger.RangerTrinoAccessType.CREATE; +import static io.trino.plugin.ranger.RangerTrinoAccessType.DELETE; +import static io.trino.plugin.ranger.RangerTrinoAccessType.DROP; +import static io.trino.plugin.ranger.RangerTrinoAccessType.EXECUTE; +import static io.trino.plugin.ranger.RangerTrinoAccessType.IMPERSONATE; +import static io.trino.plugin.ranger.RangerTrinoAccessType.INSERT; +import static io.trino.plugin.ranger.RangerTrinoAccessType.READ_SYSINFO; +import static io.trino.plugin.ranger.RangerTrinoAccessType.SELECT; +import static io.trino.plugin.ranger.RangerTrinoAccessType.SHOW; +import static io.trino.plugin.ranger.RangerTrinoAccessType.WRITE_SYSINFO; +import static io.trino.plugin.ranger.RangerTrinoAccessType._ANY; +import static io.trino.spi.security.AccessDeniedException.denyAddColumn; +import static io.trino.spi.security.AccessDeniedException.denyAlterColumn; +import static io.trino.spi.security.AccessDeniedException.denyCommentColumn; +import static io.trino.spi.security.AccessDeniedException.denyCommentTable; +import static io.trino.spi.security.AccessDeniedException.denyCommentView; +import static io.trino.spi.security.AccessDeniedException.denyCreateCatalog; +import static io.trino.spi.security.AccessDeniedException.denyCreateFunction; +import static io.trino.spi.security.AccessDeniedException.denyCreateMaterializedView; +import static io.trino.spi.security.AccessDeniedException.denyCreateSchema; +import static io.trino.spi.security.AccessDeniedException.denyCreateTable; +import static io.trino.spi.security.AccessDeniedException.denyCreateView; +import static io.trino.spi.security.AccessDeniedException.denyCreateViewWithSelect; +import static io.trino.spi.security.AccessDeniedException.denyDeleteTable; +import static io.trino.spi.security.AccessDeniedException.denyDropCatalog; +import static io.trino.spi.security.AccessDeniedException.denyDropColumn; +import static io.trino.spi.security.AccessDeniedException.denyDropFunction; +import static io.trino.spi.security.AccessDeniedException.denyDropMaterializedView; +import static io.trino.spi.security.AccessDeniedException.denyDropSchema; +import static io.trino.spi.security.AccessDeniedException.denyDropTable; +import static io.trino.spi.security.AccessDeniedException.denyDropView; +import static io.trino.spi.security.AccessDeniedException.denyExecuteProcedure; +import static io.trino.spi.security.AccessDeniedException.denyExecuteQuery; +import static io.trino.spi.security.AccessDeniedException.denyExecuteTableProcedure; +import static io.trino.spi.security.AccessDeniedException.denyImpersonateUser; +import static io.trino.spi.security.AccessDeniedException.denyInsertTable; +import static io.trino.spi.security.AccessDeniedException.denyReadSystemInformationAccess; +import static io.trino.spi.security.AccessDeniedException.denyRefreshMaterializedView; +import static io.trino.spi.security.AccessDeniedException.denyRenameColumn; +import static io.trino.spi.security.AccessDeniedException.denyRenameMaterializedView; +import static io.trino.spi.security.AccessDeniedException.denyRenameSchema; +import static io.trino.spi.security.AccessDeniedException.denyRenameTable; +import static io.trino.spi.security.AccessDeniedException.denyRenameView; +import static io.trino.spi.security.AccessDeniedException.denySelectColumns; +import static io.trino.spi.security.AccessDeniedException.denySetCatalogSessionProperty; +import static io.trino.spi.security.AccessDeniedException.denySetMaterializedViewProperties; +import static io.trino.spi.security.AccessDeniedException.denySetSchemaAuthorization; +import static io.trino.spi.security.AccessDeniedException.denySetSystemSessionProperty; +import static io.trino.spi.security.AccessDeniedException.denySetTableAuthorization; +import static io.trino.spi.security.AccessDeniedException.denySetTableProperties; +import static io.trino.spi.security.AccessDeniedException.denySetUser; +import static io.trino.spi.security.AccessDeniedException.denySetViewAuthorization; +import static io.trino.spi.security.AccessDeniedException.denyShowColumns; +import static io.trino.spi.security.AccessDeniedException.denyShowCreateFunction; +import static io.trino.spi.security.AccessDeniedException.denyShowCreateSchema; +import static io.trino.spi.security.AccessDeniedException.denyShowCreateTable; +import static io.trino.spi.security.AccessDeniedException.denyShowFunctions; +import static io.trino.spi.security.AccessDeniedException.denyShowSchemas; +import static io.trino.spi.security.AccessDeniedException.denyShowTables; +import static io.trino.spi.security.AccessDeniedException.denyTruncateTable; +import static io.trino.spi.security.AccessDeniedException.denyUpdateTableColumns; +import static io.trino.spi.security.AccessDeniedException.denyWriteSystemInformationAccess; +import static java.util.Objects.requireNonNullElse; +import static java.util.function.Predicate.not; + +public class RangerSystemAccessControl + implements SystemAccessControl +{ + private static final Logger LOG = Logger.get(RangerSystemAccessControl.class); + + public static final String RANGER_TRINO_SERVICETYPE = "trino"; + public static final String RANGER_TRINO_APPID = "trino"; + + private final RangerBasePlugin rangerPlugin; + private final RangerTrinoEventListener eventListener = new RangerTrinoEventListener(); + + @Inject + public RangerSystemAccessControl(RangerConfig config) + throws Exception + { + checkArgument(!isNullOrEmpty(config.getServiceName()), "apache-ranger.service.name is not configured"); + + Configuration hadoopConf = new Configuration(); + + for (File configPath : config.getHadoopConfigResource()) { + URL url = configPath.toURI().toURL(); + LOG.info("Loading Hadoop config %s from url %s", configPath, url); + hadoopConf.addResource(url); + } + + UserGroupInformation.setConfiguration(hadoopConf); + RangerPluginConfig pluginConfig = new RangerPluginConfig(RANGER_TRINO_SERVICETYPE, config.getServiceName(), RANGER_TRINO_APPID, null, null, null); + + for (File configPath : config.getPluginConfigResource()) { + pluginConfig.addResourceIfReadable(configPath.getAbsolutePath()); + } + + rangerPlugin = new RangerBasePlugin(pluginConfig); + rangerPlugin.init(); + rangerPlugin.setResultProcessor(new RangerDefaultAuditHandler()); + } + + @Override + public void checkCanImpersonateUser(Identity identity, String userName) + { + if (!hasPermission(RangerTrinoResource.forUser(userName), identity, null, IMPERSONATE, "ImpersonateUser")) { + denyImpersonateUser(identity.getUser(), userName); + } + } + + @Deprecated + @Override + public void checkCanSetUser(Optional principal, String userName) + { + if (!hasPermission(RangerTrinoResource.forUser(userName), principal, null, IMPERSONATE, "SetUser")) { + denySetUser(principal, userName); + } + } + + @Override + public void checkCanExecuteQuery(Identity identity, QueryId queryId) + { + if (!hasPermission(RangerTrinoResource.forQueryId(queryId.getId()), identity, queryId, EXECUTE, "ExecuteQuery")) { + denyExecuteQuery(); + } + } + + @Override + public void checkCanViewQueryOwnedBy(Identity identity, Identity queryOwner) + { + if (!hasPermission(RangerTrinoResource.forUser(queryOwner.getUser()), identity, null, IMPERSONATE, "ViewQueryOwnedBy")) { + denyImpersonateUser(identity.getUser(), queryOwner.getUser()); + } + } + + @Override + public Collection filterViewQueryOwnedBy(Identity identity, Collection queryOwners) + { + Set toExclude = new HashSet<>(); + + for (Identity queryOwner : queryOwners) { + if (!hasPermissionForFilter(RangerTrinoResource.forUser(queryOwner.getUser()), identity, null, IMPERSONATE, "filterViewQueryOwnedBy")) { + toExclude.add(queryOwner); + } + } + + if (toExclude.isEmpty()) { + return queryOwners; + } + + return queryOwners.stream().filter(not(toExclude::contains)).collect(Collectors.toList()); + } + + @Override + public void checkCanKillQueryOwnedBy(Identity identity, Identity queryOwner) + { + if (!hasPermission(RangerTrinoResource.forUser(queryOwner.getUser()), identity, null, IMPERSONATE, "KillQueryOwnedBy")) { + denyImpersonateUser(identity.getUser(), queryOwner.getUser()); + } + } + + @Override + public void checkCanReadSystemInformation(Identity identity) + { + if (!hasPermission(RangerTrinoResource.forSystemInformation(), identity, null, READ_SYSINFO, "ReadSystemInformation")) { + denyReadSystemInformationAccess(); + } + } + + @Override + public void checkCanWriteSystemInformation(Identity identity) + { + if (!hasPermission(RangerTrinoResource.forSystemInformation(), identity, null, WRITE_SYSINFO, "WriteSystemInformation")) { + denyWriteSystemInformationAccess(); + } + } + + @Override + public void checkCanSetSystemSessionProperty(Identity identity, QueryId queryId, String propertyName) + { + if (!hasPermission(RangerTrinoResource.forSystemProperty(propertyName), identity, queryId, ALTER, "SetSystemSessionProperty")) { + denySetSystemSessionProperty(propertyName); + } + } + + @Override + public boolean canAccessCatalog(SystemSecurityContext context, String catalogName) + { + return hasPermission(RangerTrinoResource.forCatalog(catalogName), context, _ANY, "AccessCatalog"); + } + + @Override + public void checkCanCreateCatalog(SystemSecurityContext context, String catalogName) + { + if (!hasPermission(RangerTrinoResource.forCatalog(catalogName), context, CREATE, "CreateCatalog")) { + denyCreateCatalog(catalogName); + } + } + + @Override + public void checkCanDropCatalog(SystemSecurityContext context, String catalogName) + { + if (!hasPermission(RangerTrinoResource.forCatalog(catalogName), context, DROP, "DropCatalog")) { + denyDropCatalog(catalogName); + } + } + + @Override + public void checkCanSetCatalogSessionProperty(SystemSecurityContext context, String catalogName, String propertyName) + { + if (!hasPermission(RangerTrinoResource.forSessionProperty(catalogName, propertyName), context, ALTER, "SetCatalogSessionProperty")) { + denySetCatalogSessionProperty(catalogName, propertyName); + } + } + + @Override + public Set filterCatalogs(SystemSecurityContext context, Set catalogs) + { + Set toExclude = new HashSet<>(); + + for (String catalog : catalogs) { + if (!hasPermissionForFilter(RangerTrinoResource.forCatalog(catalog), context, _ANY, "filterCatalogs")) { + toExclude.add(catalog); + } + } + + if (toExclude.isEmpty()) { + return catalogs; + } + + return catalogs.stream().filter(not(toExclude::contains)).collect(Collectors.toSet()); + } + + @Override + public void checkCanCreateSchema(SystemSecurityContext context, CatalogSchemaName schema, Map properties) + { + if (!hasPermission(RangerTrinoResource.forSchema(schema.getCatalogName(), schema.getSchemaName()), context, CREATE, "CreateSchema")) { + denyCreateSchema(schema.getSchemaName()); + } + } + + @Override + public void checkCanDropSchema(SystemSecurityContext context, CatalogSchemaName schema) + { + if (!hasPermission(RangerTrinoResource.forSchema(schema.getCatalogName(), schema.getSchemaName()), context, DROP, "DropSchema")) { + denyDropSchema(schema.getSchemaName()); + } + } + + @Override + public void checkCanRenameSchema(SystemSecurityContext context, CatalogSchemaName schema, String newSchemaName) + { + boolean isAllowed = hasPermission(RangerTrinoResource.forSchema(schema.getCatalogName(), schema.getSchemaName()), context, ALTER, "RenameSchema:source") && + hasPermission(RangerTrinoResource.forSchema(schema.getCatalogName(), newSchemaName), context, ALTER, "RenameSchema:target"); + + if (!isAllowed) { + denyRenameSchema(schema.getSchemaName(), newSchemaName); + } + } + + @Override + public void checkCanSetSchemaAuthorization(SystemSecurityContext context, CatalogSchemaName schema, TrinoPrincipal principal) + { + if (!hasPermission(RangerTrinoResource.forSchema(schema.getCatalogName(), schema.getSchemaName()), context, ALTER, "SetSchemaAuthorization")) { + denySetSchemaAuthorization(schema.getSchemaName(), principal); + } + } + + @Override + public void checkCanShowSchemas(SystemSecurityContext context, String catalogName) + { + if (!hasPermission(RangerTrinoResource.forCatalog(catalogName), context, _ANY, "ShowSchemas")) { + denyShowSchemas(catalogName); + } + } + + @Override + public Set filterSchemas(SystemSecurityContext context, String catalogName, Set schemaNames) + { + Set toExclude = new HashSet<>(); + + for (String schemaName : schemaNames) { + if (!hasPermissionForFilter(RangerTrinoResource.forSchema(catalogName, schemaName), context, _ANY, "filterSchemas")) { + toExclude.add(schemaName); + } + } + + if (toExclude.isEmpty()) { + return schemaNames; + } + + return schemaNames.stream().filter(not(toExclude::contains)).collect(Collectors.toSet()); + } + + @Override + public void checkCanShowCreateSchema(SystemSecurityContext context, CatalogSchemaName schema) + { + if (!hasPermission(RangerTrinoResource.forSchema(schema.getCatalogName(), schema.getSchemaName()), context, SHOW, "ShowCreateSchema")) { + denyShowCreateSchema(schema.getSchemaName()); + } + } + + @Override + public void checkCanCreateTable(SystemSecurityContext context, CatalogSchemaTableName table, Map properties) + { + if (!hasPermission(createTableResource(table), context, CREATE, "CreateTable")) { + denyCreateTable(table.getSchemaTableName().getTableName()); + } + } + + @Override + public void checkCanDropTable(SystemSecurityContext context, CatalogSchemaTableName table) + { + if (!hasPermission(createTableResource(table), context, DROP, "DropTable")) { + denyDropTable(table.getSchemaTableName().getTableName()); + } + } + + @Override + public void checkCanRenameTable(SystemSecurityContext context, CatalogSchemaTableName table, CatalogSchemaTableName newTable) + { + boolean isAllowed = hasPermission(createTableResource(table), context, ALTER, "RenameTable:source") && + hasPermission(createTableResource(newTable), context, ALTER, "RenameTable:target"); + + if (!isAllowed) { + denyRenameTable(table.getSchemaTableName().getTableName(), newTable.getSchemaTableName().getTableName()); + } + } + + @Override + public void checkCanSetTableProperties(SystemSecurityContext context, CatalogSchemaTableName table, Map> properties) + { + if (!hasPermission(createTableResource(table), context, ALTER, "SetTableProperties")) { + denySetTableProperties(table.toString()); + } + } + + @Override + public void checkCanSetTableComment(SystemSecurityContext context, CatalogSchemaTableName table) + { + if (!hasPermission(createTableResource(table), context, ALTER, "SetTableComment")) { + denyCommentTable(table.toString()); + } + } + + @Override + public void checkCanSetTableAuthorization(SystemSecurityContext context, CatalogSchemaTableName table, TrinoPrincipal principal) + { + if (!hasPermission(createTableResource(table), context, ALTER, "SetTableAuthorization")) { + denySetTableAuthorization(table.toString(), principal); + } + } + + @Override + public void checkCanShowTables(SystemSecurityContext context, CatalogSchemaName schema) + { + if (!hasPermission(RangerTrinoResource.forSchema(schema.getCatalogName(), schema.getSchemaName()), context, _ANY, "ShowTables")) { + denyShowTables(schema.toString()); + } + } + + @Override + public void checkCanShowCreateTable(SystemSecurityContext context, CatalogSchemaTableName table) + { + if (!hasPermission(createTableResource(table), context, SHOW, "ShowCreateTable")) { + denyShowCreateTable(table.toString()); + } + } + + @Override + public void checkCanInsertIntoTable(SystemSecurityContext context, CatalogSchemaTableName table) + { + if (!hasPermission(createTableResource(table), context, INSERT, "InsertIntoTable")) { + denyInsertTable(table.getSchemaTableName().getTableName()); + } + } + + @Override + public void checkCanDeleteFromTable(SystemSecurityContext context, CatalogSchemaTableName table) + { + if (!hasPermission(createTableResource(table), context, DELETE, "DeleteFromTable")) { + denyDeleteTable(table.getSchemaTableName().getTableName()); + } + } + + @Override + public void checkCanTruncateTable(SystemSecurityContext context, CatalogSchemaTableName table) + { + if (!hasPermission(createTableResource(table), context, DELETE, "TruncateTable")) { + denyTruncateTable(table.getSchemaTableName().getTableName()); + } + } + + @Override + public Set filterTables(SystemSecurityContext context, String catalogName, Set tableNames) + { + Set toExclude = new HashSet<>(); + + for (SchemaTableName tableName : tableNames) { + RangerTrinoResource resource = RangerTrinoResource.forTable(catalogName, tableName.getSchemaName(), tableName.getTableName()); + + if (!hasPermissionForFilter(resource, context, _ANY, "filterTables")) { + toExclude.add(tableName); + } + } + + if (toExclude.isEmpty()) { + return tableNames; + } + + return tableNames.stream().filter(not(toExclude::contains)).collect(Collectors.toSet()); + } + + @Override + public void checkCanAddColumn(SystemSecurityContext context, CatalogSchemaTableName table) + { + if (!hasPermission(createTableResource(table), context, ALTER, "AddColumn")) { + denyAddColumn(table.getSchemaTableName().getTableName()); + } + } + + @Override + public void checkCanAlterColumn(SystemSecurityContext context, CatalogSchemaTableName table) + { + if (!hasPermission(createTableResource(table), context, ALTER, "AlterColumn")) { + denyAlterColumn(table.getSchemaTableName().getTableName()); + } + } + + @Override + public void checkCanDropColumn(SystemSecurityContext context, CatalogSchemaTableName table) + { + if (!hasPermission(createTableResource(table), context, ALTER, "DropColumn")) { + denyDropColumn(table.getSchemaTableName().getTableName()); + } + } + + @Override + public void checkCanRenameColumn(SystemSecurityContext context, CatalogSchemaTableName table) + { + if (!hasPermission(createTableResource(table), context, ALTER, "RenameColumn")) { + denyRenameColumn(table.getSchemaTableName().getTableName()); + } + } + + @Override + public void checkCanSetColumnComment(SystemSecurityContext context, CatalogSchemaTableName table) + { + if (!hasPermission(createTableResource(table), context, ALTER, "SetColumnComment")) { + denyCommentColumn(table.toString()); + } + } + + @Override + public void checkCanShowColumns(SystemSecurityContext context, CatalogSchemaTableName table) + { + if (!hasPermission(createTableResource(table), context, _ANY, "ShowColumns")) { + denyShowColumns(table.toString()); + } + } + + @Override + public void checkCanSelectFromColumns(SystemSecurityContext context, CatalogSchemaTableName table, Set columns) + { + for (RangerTrinoResource resource : RangerTrinoResource.forColumns(table.getCatalogName(), table.getSchemaTableName().getSchemaName(), table.getSchemaTableName().getTableName(), columns)) { + if (!hasPermission(resource, context, SELECT, "SelectFromColumns")) { + denySelectColumns(table.getSchemaTableName().getTableName(), columns); + } + } + } + + @Override + public void checkCanUpdateTableColumns(SystemSecurityContext context, CatalogSchemaTableName table, Set updatedColumnNames) + { + if (!hasPermission(createTableResource(table), context, INSERT, "UpdateTableColumns")) { + denyUpdateTableColumns(table.getSchemaTableName().getTableName(), updatedColumnNames); + } + } + + @Deprecated + @Override + public Set filterColumns(SystemSecurityContext context, CatalogSchemaTableName table, Set columns) + { + Set toExclude = new HashSet<>(); + String catalogName = table.getCatalogName(); + String schemaName = table.getSchemaTableName().getSchemaName(); + String tableName = table.getSchemaTableName().getTableName(); + + for (String column : columns) { + RangerTrinoResource resource = RangerTrinoResource.forColumn(catalogName, schemaName, tableName, column); + + if (!hasPermissionForFilter(resource, context, _ANY, "filterColumns")) { + toExclude.add(column); + } + } + + if (toExclude.isEmpty()) { + return columns; + } + + return columns.stream().filter(not(toExclude::contains)).collect(Collectors.toSet()); + } + + @Override + public void checkCanCreateView(SystemSecurityContext context, CatalogSchemaTableName view) + { + if (!hasPermission(createTableResource(view), context, CREATE, "CreateView")) { + denyCreateView(view.getSchemaTableName().getTableName()); + } + } + + @Override + public void checkCanDropView(SystemSecurityContext context, CatalogSchemaTableName view) + { + if (!hasPermission(createTableResource(view), context, DROP, "DropView")) { + denyDropView(view.getSchemaTableName().getTableName()); + } + } + + @Override + public void checkCanRenameView(SystemSecurityContext context, CatalogSchemaTableName view, CatalogSchemaTableName newView) + { + boolean isAllowed = hasPermission(createTableResource(view), context, ALTER, "RenameView:source") && + hasPermission(createTableResource(newView), context, ALTER, "RenameView:target"); + + if (!isAllowed) { + denyRenameView(view.toString(), newView.toString()); + } + } + + @Override + public void checkCanSetViewAuthorization(SystemSecurityContext context, CatalogSchemaTableName view, TrinoPrincipal principal) + { + if (!hasPermission(createTableResource(view), context, ALTER, "SetViewAuthorization")) { + denySetViewAuthorization(view.toString(), principal); + } + } + + @Override + public void checkCanCreateViewWithSelectFromColumns(SystemSecurityContext context, CatalogSchemaTableName table, Set columns) + { + for (RangerTrinoResource resource : RangerTrinoResource.forColumns(table.getCatalogName(), table.getSchemaTableName().getSchemaName(), table.getSchemaTableName().getTableName(), columns)) { + if (!hasPermission(resource, context, SELECT, "CreateViewWithSelectFromColumns")) { + denyCreateViewWithSelect(table.getSchemaTableName().getTableName(), context.getIdentity()); + } + } + } + + @Override + public void checkCanSetViewComment(SystemSecurityContext context, CatalogSchemaTableName view) + { + if (!hasPermission(createTableResource(view), context, ALTER, "SetViewComment")) { + denyCommentView(view.toString()); + } + } + + @Override + public void checkCanCreateMaterializedView(SystemSecurityContext context, CatalogSchemaTableName materializedView, Map properties) + { + if (!hasPermission(createTableResource(materializedView), context, CREATE, "CreateMaterializedView")) { + denyCreateMaterializedView(materializedView.getSchemaTableName().getTableName()); + } + } + + @Override + public void checkCanRefreshMaterializedView(SystemSecurityContext context, CatalogSchemaTableName materializedView) + { + if (!hasPermission(createTableResource(materializedView), context, ALTER, "RefreshMaterializedView")) { + denyRefreshMaterializedView(materializedView.toString()); + } + } + + @Override + public void checkCanSetMaterializedViewProperties(SystemSecurityContext context, CatalogSchemaTableName materializedView, Map> properties) + { + if (!hasPermission(createTableResource(materializedView), context, ALTER, "SetMaterializedViewProperties")) { + denySetMaterializedViewProperties(materializedView.toString()); + } + } + + @Override + public void checkCanDropMaterializedView(SystemSecurityContext context, CatalogSchemaTableName materializedView) + { + if (!hasPermission(createTableResource(materializedView), context, DROP, "DropMaterializedView")) { + denyDropMaterializedView(materializedView.getSchemaTableName().getTableName()); + } + } + + @Override + public void checkCanRenameMaterializedView(SystemSecurityContext context, CatalogSchemaTableName materializedView, CatalogSchemaTableName newView) + { + boolean isAllowed = hasPermission(createTableResource(materializedView), context, ALTER, "RenameMaterializedView:source") && + hasPermission(createTableResource(newView), context, ALTER, "RenameMaterializedView:target"); + + if (!isAllowed) { + denyRenameMaterializedView(materializedView.toString(), newView.toString()); + } + } + + @Override + public void checkCanGrantSchemaPrivilege(SystemSecurityContext context, Privilege privilege, CatalogSchemaName schema, TrinoPrincipal grantee, boolean grantOption) + { + } + + @Override + public void checkCanDenySchemaPrivilege(SystemSecurityContext context, Privilege privilege, CatalogSchemaName schema, TrinoPrincipal grantee) + { + } + + @Override + public void checkCanRevokeSchemaPrivilege(SystemSecurityContext context, Privilege privilege, CatalogSchemaName schema, TrinoPrincipal revokee, boolean grantOption) + { + } + + @Override + public void checkCanGrantTablePrivilege(SystemSecurityContext context, Privilege privilege, CatalogSchemaTableName table, TrinoPrincipal grantee, boolean withGrantOption) + { + } + + @Override + public void checkCanDenyTablePrivilege(SystemSecurityContext context, Privilege privilege, CatalogSchemaTableName table, TrinoPrincipal grantee) + { + } + + @Override + public void checkCanRevokeTablePrivilege(SystemSecurityContext context, Privilege privilege, CatalogSchemaTableName table, TrinoPrincipal revokee, boolean grantOptionFor) + { + } + + @Override + public void checkCanGrantEntityPrivilege(SystemSecurityContext context, EntityPrivilege privilege, EntityKindAndName entity, TrinoPrincipal grantee, boolean grantOption) + { + } + + @Override + public void checkCanDenyEntityPrivilege(SystemSecurityContext context, EntityPrivilege privilege, EntityKindAndName entity, TrinoPrincipal grantee) + { + } + + @Override + public void checkCanRevokeEntityPrivilege(SystemSecurityContext context, EntityPrivilege privilege, EntityKindAndName entity, TrinoPrincipal revokee, boolean grantOption) + { + } + + @Override + public void checkCanCreateRole(SystemSecurityContext context, String role, Optional grantor) + { + } + + @Override + public void checkCanDropRole(SystemSecurityContext context, String role) + { + } + + @Override + public void checkCanShowRoles(SystemSecurityContext context) + { + } + + @Override + public void checkCanGrantRoles(SystemSecurityContext context, Set roles, Set grantees, boolean adminOption, Optional grantor) + { + } + + @Override + public void checkCanRevokeRoles(SystemSecurityContext context, Set roles, Set grantees, boolean adminOption, Optional grantor) + { + } + + @Override + public void checkCanShowCurrentRoles(SystemSecurityContext context) + { + } + + @Override + public void checkCanShowRoleGrants(SystemSecurityContext context) + { + } + + @Override + public void checkCanExecuteProcedure(SystemSecurityContext context, CatalogSchemaRoutineName procedure) + { + if (!hasPermission(RangerTrinoResource.forSchemaProcedure(procedure.getCatalogName(), procedure.getSchemaRoutineName().getSchemaName(), procedure.getSchemaRoutineName().getRoutineName()), context, EXECUTE, "ExecuteProcedure")) { + denyExecuteProcedure(procedure.getSchemaRoutineName().getRoutineName()); + } + } + + @Override + public void checkCanExecuteTableProcedure(SystemSecurityContext context, CatalogSchemaTableName catalogSchemaTableName, String procedure) + { + if (!hasPermission(createTableResource(catalogSchemaTableName), context, ALTER, "ExecuteTableProcedure")) { + denyExecuteTableProcedure(catalogSchemaTableName.toString(), procedure); + } + } + + @Override + public void checkCanCreateFunction(SystemSecurityContext context, CatalogSchemaRoutineName functionName) + { + if (!hasPermission(RangerTrinoResource.forSchemaFunction(functionName.getCatalogName(), functionName.getSchemaRoutineName().getSchemaName(), functionName.getSchemaRoutineName().getRoutineName()), context, CREATE, "CreateFunction")) { + denyCreateFunction(functionName.toString()); + } + } + + @Override + public void checkCanDropFunction(SystemSecurityContext context, CatalogSchemaRoutineName functionName) + { + if (!hasPermission(RangerTrinoResource.forSchemaFunction(functionName.getCatalogName(), functionName.getSchemaRoutineName().getSchemaName(), functionName.getSchemaRoutineName().getRoutineName()), context, DROP, "DropFunction")) { + denyDropFunction(functionName.toString()); + } + } + + @Override + public void checkCanShowCreateFunction(SystemSecurityContext context, CatalogSchemaRoutineName functionName) + { + if (!hasPermission(RangerTrinoResource.forSchemaFunction(functionName.getCatalogName(), functionName.getSchemaRoutineName().getSchemaName(), functionName.getSchemaRoutineName().getRoutineName()), context, SHOW, "ShowCreateFunction")) { + denyShowCreateFunction(functionName.toString()); + } + } + + @Override + public void checkCanShowFunctions(SystemSecurityContext context, CatalogSchemaName schema) + { + if (!hasPermission(RangerTrinoResource.forSchema(schema.getCatalogName(), schema.getSchemaName()), context, _ANY, "ShowFunctions")) { + denyShowFunctions(schema.toString()); + } + } + + @Override + public boolean canExecuteFunction(SystemSecurityContext context, CatalogSchemaRoutineName functionName) + { + return hasPermission(RangerTrinoResource.forSchemaFunction(functionName.getCatalogName(), functionName.getSchemaRoutineName().getSchemaName(), functionName.getSchemaRoutineName().getRoutineName()), context, EXECUTE, "ExecuteFunction"); + } + + @Override + public boolean canCreateViewWithExecuteFunction(SystemSecurityContext context, CatalogSchemaRoutineName functionName) + { + return hasPermission(RangerTrinoResource.forSchemaFunction(functionName.getCatalogName(), functionName.getSchemaRoutineName().getSchemaName(), functionName.getSchemaRoutineName().getRoutineName()), context, EXECUTE, "CreateViewWithExecuteFunction"); + } + + @Override + public Set filterFunctions(SystemSecurityContext context, String catalogName, Set functionNames) + { + Set toExclude = new HashSet<>(); + + for (SchemaFunctionName functionName : functionNames) { + RangerTrinoResource resource = RangerTrinoResource.forSchemaFunction(catalogName, functionName.getSchemaName(), functionName.getFunctionName()); + + if (!hasPermissionForFilter(resource, context, _ANY, "filterFunctions")) { + toExclude.add(functionName); + } + } + + if (toExclude.isEmpty()) { + return functionNames; + } + else { + return functionNames.stream().filter(not(toExclude::contains)).collect(Collectors.toSet()); + } + } + + @Override + public List getRowFilters(SystemSecurityContext context, CatalogSchemaTableName tableName) + { + RangerAccessResult result = getRowFilterResult(createAccessRequest(createTableResource(tableName), context, SELECT, "getRowFilters")); + + if (!isRowFilterEnabled(result)) { + return Collections.emptyList(); + } + + String filter = result.getFilterExpr(); + ViewExpression viewExpression = ViewExpression.builder().identity(context.getIdentity().getUser()) + .catalog(tableName.getCatalogName()) + .schema(tableName.getSchemaTableName().getSchemaName()) + .expression(filter).build(); + + return ImmutableList.of(viewExpression); + } + + @Override + public Optional getColumnMask(SystemSecurityContext context, CatalogSchemaTableName tableName, String columnName, Type type) + { + RangerAccessResult result = getDataMaskResult(createAccessRequest(RangerTrinoResource.forColumn(tableName.getCatalogName(), tableName.getSchemaTableName().getSchemaName(), tableName.getSchemaTableName().getTableName(), columnName), context, SELECT, "getColumnMask")); + + if (!isDataMaskEnabled(result)) { + return Optional.empty(); + } + + String maskType = result.getMaskType(); + RangerServiceDef.RangerDataMaskTypeDef maskTypeDef = result.getMaskTypeDef(); + String transformer = null; + + if (maskTypeDef != null) { + transformer = maskTypeDef.getTransformer(); + } + + if (RangerPolicy.MASK_TYPE_NULL.equalsIgnoreCase(maskType)) { + transformer = "NULL"; + } + else if (RangerPolicy.MASK_TYPE_CUSTOM.equalsIgnoreCase(maskType)) { + String maskedValue = result.getMaskedValue(); + + transformer = requireNonNullElse(maskedValue, "NULL"); + } + + if (transformer == null) { + return Optional.empty(); + } + else { + transformer = transformer.replace("{col}", columnName).replace("{type}", type.getDisplayName()); + + return Optional.of(ViewExpression.builder().identity(context.getIdentity().getUser()).catalog(tableName.getCatalogName()).schema(tableName.getSchemaTableName().getSchemaName()).expression(transformer).build()); + } + } + + @Override + public Iterable getEventListeners() + { + return ImmutableList.of(eventListener); + } + + @Override + public void shutdown() + { + rangerPlugin.cleanup(); + } + + private RangerAccessResult getDataMaskResult(RangerTrinoAccessRequest request) + { + return rangerPlugin.evalDataMaskPolicies(request, rangerPlugin.getResultProcessor()); + } + + private RangerAccessResult getRowFilterResult(RangerTrinoAccessRequest request) + { + return rangerPlugin.evalRowFilterPolicies(request, rangerPlugin.getResultProcessor()); + } + + private boolean isDataMaskEnabled(RangerAccessResult result) + { + return result.isMaskEnabled(); + } + + private boolean isRowFilterEnabled(RangerAccessResult result) + { + return result.isRowFilterEnabled(); + } + + private RangerTrinoAccessRequest createAccessRequest(RangerTrinoResource resource, SystemSecurityContext context, RangerTrinoAccessType accessType, String action) + { + Set userGroups = context.getIdentity().getGroups(); + + return new RangerTrinoAccessRequest(resource, context.getIdentity().getUser(), userGroups, getQueryTime(context), getClientAddress(context), getClientType(context), getQueryText(context), accessType, action); + } + + private RangerTrinoAccessRequest createAccessRequest(RangerTrinoResource resource, Identity identity, QueryId queryId, RangerTrinoAccessType accessType, String action) + { + Set userGroups = identity.getGroups(); + + return new RangerTrinoAccessRequest(resource, identity.getUser(), userGroups, getQueryTime(queryId), getClientAddress(queryId), getClientType(queryId), getQueryText(queryId), accessType, action); + } + + private Optional getClientAddress(QueryId queryId) + { + return queryId != null ? eventListener.getClientAddress(queryId.getId()) : Optional.empty(); + } + + private Optional getClientType(QueryId queryId) + { + return queryId != null ? eventListener.getClientType(queryId.getId()) : Optional.empty(); + } + + private Optional getQueryText(QueryId queryId) + { + return queryId != null ? eventListener.getQueryText(queryId.getId()) : Optional.empty(); + } + + private Optional getQueryTime(QueryId queryId) + { + return queryId != null ? eventListener.getQueryTime(queryId.getId()) : Optional.empty(); + } + + private Optional getClientAddress(SystemSecurityContext context) + { + return context != null ? getClientAddress(context.getQueryId()) : Optional.empty(); + } + + private Optional getClientType(SystemSecurityContext context) + { + return context != null ? getClientType(context.getQueryId()) : Optional.empty(); + } + + private Optional getQueryText(SystemSecurityContext context) + { + return context != null ? getQueryText(context.getQueryId()) : Optional.empty(); + } + + private Optional getQueryTime(SystemSecurityContext context) + { + return context != null ? getQueryTime(context.getQueryId()) : Optional.empty(); + } + + private boolean hasPermission(RangerTrinoResource resource, SystemSecurityContext context, RangerTrinoAccessType accessType, String action) + { + RangerAccessResult result = rangerPlugin.isAccessAllowed(createAccessRequest(resource, context, accessType, action)); + + return result != null && result.getIsAllowed(); + } + + private boolean hasPermissionForFilter(RangerTrinoResource resource, SystemSecurityContext context, RangerTrinoAccessType accessType, String action) + { + RangerTrinoAccessRequest request = createAccessRequest(resource, context, accessType, action); + + request.setResourceMatchingScope(RangerAccessRequest.ResourceMatchingScope.SELF_OR_DESCENDANTS); + + RangerAccessResult result = rangerPlugin.isAccessAllowed(request, null); + + return result != null && result.getIsAllowed(); + } + + private boolean hasPermission(RangerTrinoResource resource, Identity identity, QueryId queryId, RangerTrinoAccessType accessType, String action) + { + RangerAccessResult result = rangerPlugin.isAccessAllowed(createAccessRequest(resource, identity, queryId, accessType, action)); + + return result != null && result.getIsAllowed(); + } + + private boolean hasPermissionForFilter(RangerTrinoResource resource, Identity identity, QueryId queryId, RangerTrinoAccessType accessType, String action) + { + RangerTrinoAccessRequest request = createAccessRequest(resource, identity, queryId, accessType, action); + + request.setResourceMatchingScope(RangerAccessRequest.ResourceMatchingScope.SELF_OR_DESCENDANTS); + + RangerAccessResult result = rangerPlugin.isAccessAllowed(request, null); + + return result != null && result.getIsAllowed(); + } + + private boolean hasPermission(RangerTrinoResource resource, Optional principal, QueryId queryId, RangerTrinoAccessType accessType, String action) + { + RangerAccessResult result = rangerPlugin.isAccessAllowed(createAccessRequest(resource, toIdentity(principal), queryId, accessType, action)); + + return result != null && result.getIsAllowed(); + } + + private static RangerTrinoResource createTableResource(CatalogSchemaTableName catalogSchemaTableName) + { + return RangerTrinoResource.forTable(catalogSchemaTableName.getCatalogName(), catalogSchemaTableName.getSchemaTableName().getSchemaName(), catalogSchemaTableName.getSchemaTableName().getTableName()); + } + + private static Identity toIdentity(Optional principal) + { + if (principal.isPresent()) { + return Identity.ofUser(principal.get().getName()); + } + + return Identity.ofUser(""); + } +} diff --git a/plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/RangerSystemAccessControlFactory.java b/plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/RangerSystemAccessControlFactory.java new file mode 100644 index 000000000000..390b733868d5 --- /dev/null +++ b/plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/RangerSystemAccessControlFactory.java @@ -0,0 +1,63 @@ +/* + * Licensed 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 io.trino.plugin.ranger; + +import com.google.inject.Injector; +import com.google.inject.Scopes; +import io.airlift.bootstrap.Bootstrap; +import io.trino.spi.security.SystemAccessControl; +import io.trino.spi.security.SystemAccessControlFactory; + +import java.util.Map; + +import static io.airlift.configuration.ConfigBinder.configBinder; +import static java.util.Objects.requireNonNull; + +public class RangerSystemAccessControlFactory + implements SystemAccessControlFactory +{ + private static final String NAME = "apache-ranger"; + + @Override + public String getName() + { + return NAME; + } + + @Deprecated + @Override + public SystemAccessControl create(Map config) + { + return null; + } + + @Override + public SystemAccessControl create(Map config, SystemAccessControlContext context) + { + requireNonNull(config, "config is null"); + + Bootstrap app = new Bootstrap(binder -> + { + configBinder(binder).bindConfig(RangerConfig.class); + binder.bind(RangerSystemAccessControl.class).in(Scopes.SINGLETON); + }); + + Injector injector = app + .doNotInitializeLogging() + .setRequiredConfigurationProperties(config) + .initialize(); + + return injector.getInstance(RangerSystemAccessControl.class); + } +} diff --git a/plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/RangerTrinoAccessRequest.java b/plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/RangerTrinoAccessRequest.java new file mode 100644 index 000000000000..c6ec5f6ca5ce --- /dev/null +++ b/plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/RangerTrinoAccessRequest.java @@ -0,0 +1,38 @@ +/* + * Licensed 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 io.trino.plugin.ranger; + +import org.apache.ranger.plugin.policyengine.RangerAccessRequestImpl; + +import java.time.Instant; +import java.util.Date; +import java.util.Optional; +import java.util.Set; + +import static java.util.Locale.ENGLISH; + +class RangerTrinoAccessRequest + extends RangerAccessRequestImpl +{ + public RangerTrinoAccessRequest(RangerTrinoResource resource, String user, Set userGroups, Optional queryTime, Optional clientAddress, Optional clientType, Optional queryText, RangerTrinoAccessType trinoAccessType, String action) + { + super(resource, trinoAccessType.name().toLowerCase(ENGLISH), user, userGroups, null); + + setAction(action); + setAccessTime(queryTime.map(instant -> new Date(instant.getEpochSecond() * 1000)).orElseGet(Date::new)); + setClientIPAddress(clientAddress.orElse(null)); + setClientType(clientType.orElse(null)); + setRequestData(queryText.orElse(null)); + } +} diff --git a/plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/RangerTrinoAccessType.java b/plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/RangerTrinoAccessType.java new file mode 100644 index 000000000000..e6945d15a746 --- /dev/null +++ b/plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/RangerTrinoAccessType.java @@ -0,0 +1,33 @@ +/* + * Licensed 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 io.trino.plugin.ranger; + +enum RangerTrinoAccessType { + CREATE, + DROP, + SELECT, + INSERT, + DELETE, + USE, + ALTER, + ALL, + GRANT, + REVOKE, + SHOW, + IMPERSONATE, + EXECUTE, + READ_SYSINFO, + WRITE_SYSINFO, + _ANY +} diff --git a/plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/RangerTrinoEventListener.java b/plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/RangerTrinoEventListener.java new file mode 100644 index 000000000000..a25016683955 --- /dev/null +++ b/plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/RangerTrinoEventListener.java @@ -0,0 +1,74 @@ +/* + * Licensed 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 io.trino.plugin.ranger; + +import io.trino.spi.eventlistener.EventListener; +import io.trino.spi.eventlistener.QueryCompletedEvent; +import io.trino.spi.eventlistener.QueryContext; +import io.trino.spi.eventlistener.QueryCreatedEvent; +import io.trino.spi.eventlistener.QueryMetadata; + +import java.time.Instant; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +class RangerTrinoEventListener + implements EventListener +{ + private final Map activeQueries = new ConcurrentHashMap<>(); + + public Optional getClientAddress(String queryId) + { + QueryCreatedEvent event = activeQueries.get(queryId); + QueryContext context = event != null ? event.getContext() : null; + + return context != null && context.getRemoteClientAddress().isPresent() ? Optional.of(context.getRemoteClientAddress().get()) : Optional.empty(); + } + + public Optional getQueryText(String queryId) + { + QueryCreatedEvent event = activeQueries.get(queryId); + QueryMetadata metadata = event != null ? event.getMetadata() : null; + + return metadata != null ? Optional.of(metadata.getQuery()) : Optional.empty(); + } + + public Optional getQueryTime(String queryId) + { + QueryCreatedEvent event = activeQueries.get(queryId); + + return event != null ? Optional.of(event.getCreateTime()) : Optional.empty(); + } + + public Optional getClientType(String queryId) + { + QueryCreatedEvent event = activeQueries.get(queryId); + QueryContext context = event != null ? event.getContext() : null; + + return context != null && context.getUserAgent().isPresent() ? Optional.of(context.getUserAgent().get()) : Optional.empty(); + } + + @Override + public void queryCreated(QueryCreatedEvent event) + { + activeQueries.put(event.getMetadata().getQueryId(), event); + } + + @Override + public void queryCompleted(QueryCompletedEvent event) + { + activeQueries.remove(event.getMetadata().getQueryId()); + } +} diff --git a/plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/RangerTrinoResource.java b/plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/RangerTrinoResource.java new file mode 100644 index 000000000000..6a3d98366636 --- /dev/null +++ b/plugin/trino-apache-ranger/src/main/java/io/trino/plugin/ranger/RangerTrinoResource.java @@ -0,0 +1,151 @@ +/* + * Licensed 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 io.trino.plugin.ranger; + +import com.google.common.collect.ImmutableList; +import org.apache.ranger.plugin.policyengine.RangerAccessResourceImpl; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +class RangerTrinoResource + extends RangerAccessResourceImpl +{ + public static final String KEY_CATALOG = "catalog"; + public static final String KEY_SCHEMA = "schema"; + public static final String KEY_TABLE = "table"; + public static final String KEY_COLUMN = "column"; + public static final String KEY_USER = "trinouser"; + public static final String KEY_PROCEDURE = "procedure"; + public static final String KEY_SYSTEM_PROPERTY = "systemproperty"; + public static final String KEY_SESSION_PROPERTY = "sessionproperty"; + public static final String KEY_SCHEMA_FUNCTION = "schemafunction"; + public static final String KEY_ROLE = "role"; + public static final String KEY_QUERY_ID = "queryid"; + public static final String KEY_SYSINFO = "sysinfo"; + + public static RangerTrinoResource forUser(String userName) + { + return new RangerTrinoResource(KEY_USER, userName); + } + + public static RangerTrinoResource forRole(String roleName) + { + return new RangerTrinoResource(KEY_ROLE, roleName); + } + + public static RangerTrinoResource forQueryId(String queryId) + { + return new RangerTrinoResource(KEY_QUERY_ID, queryId); + } + + public static RangerTrinoResource forSystemProperty(String propertyName) + { + return new RangerTrinoResource(KEY_SYSTEM_PROPERTY, propertyName); + } + + public static RangerTrinoResource forSystemInformation() + { + return new RangerTrinoResource(KEY_SYSINFO, "*"); + } + + public static RangerTrinoResource forCatalog(String catalogName) + { + return new RangerTrinoResource(KEY_CATALOG, catalogName); + } + + public static RangerTrinoResource forSchema(String catalogName, String schema) + { + RangerTrinoResource ret = new RangerTrinoResource(); + + ret.setValue(KEY_CATALOG, catalogName); + ret.setValue(KEY_SCHEMA, schema); + + return ret; + } + + public static RangerTrinoResource forTable(String catalogName, String schema, String tableName) + { + RangerTrinoResource ret = new RangerTrinoResource(); + + ret.setValue(KEY_CATALOG, catalogName); + ret.setValue(KEY_SCHEMA, schema); + ret.setValue(KEY_TABLE, tableName); + + return ret; + } + + public static RangerTrinoResource forColumn(String catalogName, String schema, String tableName, String columnName) + { + RangerTrinoResource ret = new RangerTrinoResource(); + + ret.setValue(KEY_CATALOG, catalogName); + ret.setValue(KEY_SCHEMA, schema); + ret.setValue(KEY_TABLE, tableName); + ret.setValue(KEY_COLUMN, columnName); + + return ret; + } + + public static List forColumns(String catalogName, String schema, String tableName, Set columns) + { + if (columns.isEmpty()) { + return ImmutableList.of(forTable(catalogName, schema, tableName)); + } + + return columns.stream().map(columnName -> forColumn(catalogName, schema, tableName, columnName)).collect(Collectors.toList()); + } + + public static RangerTrinoResource forSchemaProcedure(String catalogName, String schema, String procedure) + { + RangerTrinoResource ret = new RangerTrinoResource(); + + ret.setValue(KEY_CATALOG, catalogName); + ret.setValue(KEY_SCHEMA, schema); + ret.setValue(KEY_PROCEDURE, procedure); + + return ret; + } + + public static RangerTrinoResource forSchemaFunction(String catalogName, String schema, String functionName) + { + RangerTrinoResource ret = new RangerTrinoResource(); + + ret.setValue(KEY_CATALOG, catalogName); + ret.setValue(KEY_SCHEMA, schema); + ret.setValue(KEY_SCHEMA_FUNCTION, functionName); + + return ret; + } + + public static RangerTrinoResource forSessionProperty(String catalogName, String propertyName) + { + RangerTrinoResource ret = new RangerTrinoResource(); + + ret.setValue(KEY_CATALOG, catalogName); + ret.setValue(KEY_SESSION_PROPERTY, propertyName); + + return ret; + } + + private RangerTrinoResource() + { + } + + public RangerTrinoResource(String key, String value) + { + setValue(key, value); + } +} diff --git a/plugin/trino-apache-ranger/src/test/java/io/trino/plugin/ranger/RangerAdminClientImpl.java b/plugin/trino-apache-ranger/src/test/java/io/trino/plugin/ranger/RangerAdminClientImpl.java new file mode 100644 index 000000000000..8b54bd3f5cb3 --- /dev/null +++ b/plugin/trino-apache-ranger/src/test/java/io/trino/plugin/ranger/RangerAdminClientImpl.java @@ -0,0 +1,57 @@ +/* + * Licensed 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 io.trino.plugin.ranger; + +import org.apache.ranger.admin.client.AbstractRangerAdminClient; +import org.apache.ranger.plugin.model.RangerServiceDef; +import org.apache.ranger.plugin.store.EmbeddedServiceDefsUtil; +import org.apache.ranger.plugin.util.ServicePolicies; + +import java.io.File; +import java.nio.charset.Charset; +import java.nio.file.FileSystems; +import java.nio.file.Files; + +public class RangerAdminClientImpl + extends AbstractRangerAdminClient +{ + private static final String policiesFilepath = "/src/test/resources/trino-policies.json"; + + @Override + public ServicePolicies getServicePoliciesIfUpdated(long lastKnownVersion, long lastActivationTimeInMillis) + throws Exception + { + String basedir = System.getProperty("basedir"); + + if (basedir == null) { + basedir = new File(".").getCanonicalPath(); + } + + byte[] policiesBytes = Files.readAllBytes(FileSystems.getDefault().getPath(basedir, policiesFilepath)); + + ServicePolicies ret = gson.fromJson(new String(policiesBytes, Charset.defaultCharset()), ServicePolicies.class); + RangerServiceDef serviceDef = EmbeddedServiceDefsUtil.instance().getEmbeddedServiceDef("trino"); + RangerServiceDef tagServiceDef = EmbeddedServiceDefsUtil.instance().getEmbeddedServiceDef("tag"); + + ret.setServiceDef(serviceDef); + + if (ret.getTagPolicies() == null) { + ret.setTagPolicies(new ServicePolicies.TagPolicies()); + } + + ret.getTagPolicies().setServiceDef(tagServiceDef); + + return ret; + } +} diff --git a/plugin/trino-apache-ranger/src/test/java/io/trino/plugin/ranger/TestRangerSystemAccessControl.java b/plugin/trino-apache-ranger/src/test/java/io/trino/plugin/ranger/TestRangerSystemAccessControl.java new file mode 100644 index 000000000000..e784df23580e --- /dev/null +++ b/plugin/trino-apache-ranger/src/test/java/io/trino/plugin/ranger/TestRangerSystemAccessControl.java @@ -0,0 +1,327 @@ +/* + * Licensed 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 io.trino.plugin.ranger; + +import com.google.common.collect.ImmutableSet; +import io.trino.spi.QueryId; +import io.trino.spi.connector.CatalogSchemaName; +import io.trino.spi.connector.CatalogSchemaRoutineName; +import io.trino.spi.connector.CatalogSchemaTableName; +import io.trino.spi.connector.SchemaTableName; +import io.trino.spi.function.SchemaFunctionName; +import io.trino.spi.security.AccessDeniedException; +import io.trino.spi.security.BasicPrincipal; +import io.trino.spi.security.Identity; +import io.trino.spi.security.SystemSecurityContext; +import io.trino.spi.security.TrinoPrincipal; +import io.trino.spi.security.ViewExpression; +import io.trino.spi.type.VarcharType; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import javax.security.auth.kerberos.KerberosPrincipal; + +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static io.trino.spi.security.PrincipalType.USER; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class TestRangerSystemAccessControl +{ + private static RangerSystemAccessControl accessControlManager; + + private static final Identity ALICE = new Identity.Builder("alice").withPrincipal(new BasicPrincipal("alice")).build(); + private static final Identity ADMIN = new Identity.Builder("admin").withPrincipal(new BasicPrincipal("admin")).build(); + private static final Identity KERBEROS_INVALID_ALICE = Identity.from(ALICE).withPrincipal(new KerberosPrincipal("mallory/example.com@EXAMPLE.COM")).build(); + private static final Identity BOB = Identity.ofUser("bob"); + + private static final Set ALL_CATALOGS = ImmutableSet.of("open-to-all", "all-allowed", "alice-catalog", "user-catalog"); + private static final Set QUERY_OWNERS = ImmutableSet.of(Identity.ofUser("bob"), Identity.ofUser("alice"), Identity.ofUser("frank")); + private static final String CATALOG_ALICE = "alice-catalog"; + private static final String CATALOG_USER_HOME = "user-catalog"; + private static final CatalogSchemaName SCHEMA_ALICE_SCH1 = new CatalogSchemaName(CATALOG_ALICE, "sch1"); + private static final CatalogSchemaTableName TABLE_ALICE_SCH1_TBL1 = new CatalogSchemaTableName(CATALOG_ALICE, "sch1", "tbl1"); + private static final CatalogSchemaTableName VIEW_ALICE_SCH1_VW1 = new CatalogSchemaTableName(CATALOG_ALICE, "sch1", "vw1"); + private static final CatalogSchemaRoutineName PROC_ALICE_SCH1_PROC1 = new CatalogSchemaRoutineName(CATALOG_ALICE, "sch1", "proc1"); + private static final CatalogSchemaRoutineName FUNC_ALICE_SCH1_FUNC1 = new CatalogSchemaRoutineName(CATALOG_ALICE, "sch1", "func1"); + private static final CatalogSchemaName SCHEMA_USER_BOB = new CatalogSchemaName(CATALOG_USER_HOME, "bob_schema"); + private static final CatalogSchemaTableName TABLE_USER_BOB_TBL1 = new CatalogSchemaTableName(CATALOG_USER_HOME, SCHEMA_USER_BOB.getSchemaName(), "tbl1"); + + @BeforeAll + public static void setUpBeforeClass() + throws Exception + { + RangerConfig config = new RangerConfig(); + + config.setServiceName("dev_trino"); + + accessControlManager = new RangerSystemAccessControl(config); + } + + @Test + public void testCanSetUserOperations() + { + accessControlManager.checkCanSetUser(ADMIN.getPrincipal(), BOB.getUser()); + accessControlManager.checkCanImpersonateUser(ADMIN, BOB.getUser()); + accessControlManager.checkCanViewQueryOwnedBy(ADMIN, BOB); + accessControlManager.checkCanKillQueryOwnedBy(ADMIN, BOB); + assertThat(accessControlManager.filterViewQueryOwnedBy(ALICE, QUERY_OWNERS)).isEmpty(); + + assertThatThrownBy(() -> accessControlManager.checkCanSetUser(ALICE.getPrincipal(), BOB.getUser())).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanImpersonateUser(ALICE, BOB.getUser())).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanViewQueryOwnedBy(ALICE, BOB)).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanKillQueryOwnedBy(ALICE, BOB)).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanImpersonateUser(KERBEROS_INVALID_ALICE, BOB.getUser())).isInstanceOf(AccessDeniedException.class); + } + + @Test + public void testSystemInformationOperations() + { + accessControlManager.checkCanReadSystemInformation(ADMIN); + accessControlManager.checkCanWriteSystemInformation(ADMIN); + + assertThatThrownBy(() -> accessControlManager.checkCanReadSystemInformation(ALICE)).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanWriteSystemInformation(ALICE)).isInstanceOf(AccessDeniedException.class); + } + + @Test + public void testSystemSessionPropertyOperations() + { + accessControlManager.checkCanSetSystemSessionProperty(ADMIN, new QueryId("q1"), "test-property"); + + assertThatThrownBy(() -> accessControlManager.checkCanSetSystemSessionProperty(ALICE, new QueryId("q1"), "test-property")).isInstanceOf(AccessDeniedException.class); + } + + @Test + public void testQueryOperations() + { + accessControlManager.checkCanExecuteQuery(ADMIN, new QueryId("1")); + + assertThatThrownBy(() -> accessControlManager.checkCanExecuteQuery(ALICE, new QueryId("1"))).isInstanceOf(AccessDeniedException.class); + } + + @Test + public void testCatalogOperations() + { + accessControlManager.canAccessCatalog(context(ALICE), CATALOG_ALICE); + accessControlManager.checkCanCreateCatalog(context(ALICE), CATALOG_ALICE); + accessControlManager.checkCanDropCatalog(context(ALICE), CATALOG_ALICE); + accessControlManager.checkCanSetCatalogSessionProperty(context(ALICE), CATALOG_ALICE, "property"); + assertThat(accessControlManager.filterCatalogs(context(ALICE), ALL_CATALOGS)).isEqualTo(ALL_CATALOGS); + assertThat(accessControlManager.filterCatalogs(context(BOB), ALL_CATALOGS)).isEqualTo(ImmutableSet.of("open-to-all", "all-allowed", "user-catalog")); + + assertThat(accessControlManager.canAccessCatalog(context(BOB), CATALOG_ALICE)).isFalse(); + + assertThatThrownBy(() -> accessControlManager.checkCanCreateCatalog(context(BOB), CATALOG_ALICE)).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanDropCatalog(context(BOB), CATALOG_ALICE)).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanSetCatalogSessionProperty(context(BOB), CATALOG_ALICE, "property")).isInstanceOf(AccessDeniedException.class); + } + + @Test + public void testSchemaOperations() + { + accessControlManager.checkCanCreateSchema(context(ALICE), SCHEMA_ALICE_SCH1, null); + accessControlManager.checkCanDropSchema(context(ALICE), SCHEMA_ALICE_SCH1); + accessControlManager.checkCanRenameSchema(context(ALICE), SCHEMA_ALICE_SCH1, "new-schema"); + accessControlManager.checkCanShowSchemas(context(ALICE), CATALOG_ALICE); + accessControlManager.checkCanSetSchemaAuthorization(context(ALICE), SCHEMA_ALICE_SCH1, new TrinoPrincipal(USER, "principal")); + accessControlManager.checkCanShowCreateSchema(context(ALICE), SCHEMA_ALICE_SCH1); + + Set aliceSchemas = ImmutableSet.of("sch1"); + assertThat(accessControlManager.filterSchemas(context(ALICE), CATALOG_ALICE, aliceSchemas)).isEqualTo(aliceSchemas); + assertThat(accessControlManager.filterSchemas(context(BOB), "alice-catalog", aliceSchemas)).isEmpty(); + + assertThatThrownBy(() -> accessControlManager.checkCanCreateSchema(context(BOB), SCHEMA_ALICE_SCH1, null)).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanDropSchema(context(BOB), SCHEMA_ALICE_SCH1)).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanRenameSchema(context(BOB), SCHEMA_ALICE_SCH1, "new-schema")).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanShowSchemas(context(BOB), SCHEMA_ALICE_SCH1.getCatalogName())).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanSetSchemaAuthorization(context(BOB), SCHEMA_ALICE_SCH1, new TrinoPrincipal(USER, "principal"))).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanShowCreateSchema(context(BOB), SCHEMA_ALICE_SCH1)).isInstanceOf(AccessDeniedException.class); + } + + @Test + public void testTableOperations() + { + CatalogSchemaTableName newTableName = new CatalogSchemaTableName("alice-catalog", "sch1", "new-table"); + + accessControlManager.checkCanCreateTable(context(ALICE), TABLE_ALICE_SCH1_TBL1, Map.of()); + accessControlManager.checkCanDropTable(context(ALICE), TABLE_ALICE_SCH1_TBL1); + accessControlManager.checkCanRenameTable(context(ALICE), TABLE_ALICE_SCH1_TBL1, newTableName); + accessControlManager.checkCanSetTableProperties(context(ALICE), TABLE_ALICE_SCH1_TBL1, Collections.emptyMap()); + accessControlManager.checkCanSetTableComment(context(ALICE), TABLE_ALICE_SCH1_TBL1); + accessControlManager.checkCanSetTableAuthorization(context(ALICE), TABLE_ALICE_SCH1_TBL1, new TrinoPrincipal(USER, "principal")); + accessControlManager.checkCanShowTables(context(ALICE), SCHEMA_ALICE_SCH1); + accessControlManager.checkCanShowCreateTable(context(ALICE), TABLE_ALICE_SCH1_TBL1); + accessControlManager.checkCanInsertIntoTable(context(ALICE), TABLE_ALICE_SCH1_TBL1); + accessControlManager.checkCanDeleteFromTable(context(ALICE), TABLE_ALICE_SCH1_TBL1); + accessControlManager.checkCanTruncateTable(context(ALICE), TABLE_ALICE_SCH1_TBL1); + accessControlManager.checkCanCreateTable(context(BOB), TABLE_USER_BOB_TBL1, Map.of()); + accessControlManager.checkCanDropTable(context(BOB), TABLE_USER_BOB_TBL1); + + Set aliceTables = ImmutableSet.of(new SchemaTableName("sch1", "tbl1")); + assertThat(accessControlManager.filterTables(context(ALICE), CATALOG_ALICE, aliceTables)).isEqualTo(aliceTables); + assertThat(accessControlManager.filterTables(context(BOB), "alice-catalog", aliceTables)).isEmpty(); + + assertThatThrownBy(() -> accessControlManager.checkCanCreateTable(context(BOB), TABLE_ALICE_SCH1_TBL1, Map.of())).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanDropTable(context(BOB), TABLE_ALICE_SCH1_TBL1)).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanRenameTable(context(BOB), TABLE_ALICE_SCH1_TBL1, newTableName)).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanSetTableProperties(context(BOB), TABLE_ALICE_SCH1_TBL1, Collections.emptyMap())).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanSetTableComment(context(BOB), TABLE_ALICE_SCH1_TBL1)).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanSetTableAuthorization(context(BOB), TABLE_ALICE_SCH1_TBL1, new TrinoPrincipal(USER, "principal"))).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanShowTables(context(BOB), SCHEMA_ALICE_SCH1)).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanShowCreateTable(context(BOB), TABLE_ALICE_SCH1_TBL1)).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanInsertIntoTable(context(BOB), TABLE_ALICE_SCH1_TBL1)).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanDeleteFromTable(context(BOB), TABLE_ALICE_SCH1_TBL1)).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanTruncateTable(context(BOB), TABLE_ALICE_SCH1_TBL1)).isInstanceOf(AccessDeniedException.class); + } + + @Test + public void testColumnOperations() + { + accessControlManager.checkCanAddColumn(context(ALICE), TABLE_ALICE_SCH1_TBL1); + accessControlManager.checkCanAlterColumn(context(ALICE), TABLE_ALICE_SCH1_TBL1); + accessControlManager.checkCanDropColumn(context(ALICE), TABLE_ALICE_SCH1_TBL1); + accessControlManager.checkCanRenameColumn(context(ALICE), TABLE_ALICE_SCH1_TBL1); + accessControlManager.checkCanSetColumnComment(context(ALICE), TABLE_ALICE_SCH1_TBL1); + accessControlManager.checkCanShowColumns(context(ALICE), TABLE_ALICE_SCH1_TBL1); + accessControlManager.checkCanSelectFromColumns(context(ALICE), TABLE_ALICE_SCH1_TBL1, ImmutableSet.of()); + accessControlManager.checkCanUpdateTableColumns(context(ALICE), TABLE_ALICE_SCH1_TBL1, Collections.emptySet()); + accessControlManager.checkCanAddColumn(context(BOB), TABLE_USER_BOB_TBL1); + accessControlManager.checkCanSelectFromColumns(context(BOB), TABLE_USER_BOB_TBL1, ImmutableSet.of()); + accessControlManager.checkCanDropColumn(context(BOB), TABLE_USER_BOB_TBL1); + + Set columns = Collections.singleton("column-1"); + Map> tableColumns = Collections.singletonMap(TABLE_ALICE_SCH1_TBL1.getSchemaTableName(), columns); + + assertThat(accessControlManager.filterColumns(context(ALICE), TABLE_ALICE_SCH1_TBL1, columns)).isEqualTo(columns); + assertThat(accessControlManager.filterColumns(context(ALICE), TABLE_ALICE_SCH1_TBL1.getCatalogName(), tableColumns)).isEqualTo(tableColumns); + assertThat(accessControlManager.filterColumns(context(BOB), TABLE_ALICE_SCH1_TBL1, columns)).isEmpty(); + assertThat(accessControlManager.filterColumns(context(BOB), TABLE_ALICE_SCH1_TBL1.getCatalogName(), tableColumns)).isEqualTo(Collections.singletonMap(TABLE_ALICE_SCH1_TBL1.getSchemaTableName(), Collections.emptySet())); + + assertThatThrownBy(() -> accessControlManager.checkCanAddColumn(context(BOB), TABLE_ALICE_SCH1_TBL1)).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanAlterColumn(context(BOB), TABLE_ALICE_SCH1_TBL1)).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanDropColumn(context(BOB), TABLE_ALICE_SCH1_TBL1)).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanRenameColumn(context(BOB), TABLE_ALICE_SCH1_TBL1)).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanSetColumnComment(context(BOB), TABLE_ALICE_SCH1_TBL1)).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanShowColumns(context(BOB), TABLE_ALICE_SCH1_TBL1)).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanSelectFromColumns(context(BOB), TABLE_ALICE_SCH1_TBL1, ImmutableSet.of())).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanUpdateTableColumns(context(BOB), TABLE_ALICE_SCH1_TBL1, Collections.emptySet())).isInstanceOf(AccessDeniedException.class); + } + + @Test + public void testViewOperations() + { + CatalogSchemaTableName newViewName = new CatalogSchemaTableName(VIEW_ALICE_SCH1_VW1.getCatalogName(), VIEW_ALICE_SCH1_VW1.getSchemaTableName().getSchemaName(), "new-view"); + + accessControlManager.checkCanCreateView(context(ALICE), VIEW_ALICE_SCH1_VW1); + accessControlManager.checkCanDropView(context(ALICE), VIEW_ALICE_SCH1_VW1); + accessControlManager.checkCanRenameView(context(ALICE), VIEW_ALICE_SCH1_VW1, newViewName); + accessControlManager.checkCanSetViewAuthorization(context(ALICE), VIEW_ALICE_SCH1_VW1, new TrinoPrincipal(USER, "user")); + accessControlManager.checkCanCreateViewWithSelectFromColumns(context(ALICE), TABLE_ALICE_SCH1_TBL1, ImmutableSet.of()); + accessControlManager.checkCanSetViewAuthorization(context(ALICE), VIEW_ALICE_SCH1_VW1, new TrinoPrincipal(USER, "user")); + accessControlManager.checkCanSetViewComment(context(ALICE), VIEW_ALICE_SCH1_VW1); + + assertThatThrownBy(() -> accessControlManager.checkCanCreateView(context(BOB), VIEW_ALICE_SCH1_VW1)).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanDropView(context(BOB), VIEW_ALICE_SCH1_VW1)).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanRenameView(context(BOB), VIEW_ALICE_SCH1_VW1, newViewName)).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanSetViewAuthorization(context(BOB), VIEW_ALICE_SCH1_VW1, new TrinoPrincipal(USER, "user"))).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanCreateViewWithSelectFromColumns(context(BOB), TABLE_ALICE_SCH1_TBL1, ImmutableSet.of())).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanSetViewAuthorization(context(BOB), VIEW_ALICE_SCH1_VW1, new TrinoPrincipal(USER, "user"))).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanSetViewComment(context(BOB), VIEW_ALICE_SCH1_VW1)).isInstanceOf(AccessDeniedException.class); + } + + @Test + public void testMaterializedViewOperations() + { + CatalogSchemaTableName newViewName = new CatalogSchemaTableName(VIEW_ALICE_SCH1_VW1.getCatalogName(), VIEW_ALICE_SCH1_VW1.getSchemaTableName().getSchemaName(), "new-view"); + + accessControlManager.checkCanCreateMaterializedView(context(ALICE), VIEW_ALICE_SCH1_VW1, Collections.emptyMap()); + accessControlManager.checkCanRefreshMaterializedView(context(ALICE), VIEW_ALICE_SCH1_VW1); + accessControlManager.checkCanSetMaterializedViewProperties(context(ALICE), VIEW_ALICE_SCH1_VW1, Collections.emptyMap()); + accessControlManager.checkCanDropMaterializedView(context(ALICE), VIEW_ALICE_SCH1_VW1); + accessControlManager.checkCanRenameMaterializedView(context(ALICE), VIEW_ALICE_SCH1_VW1, newViewName); + + assertThatThrownBy(() -> accessControlManager.checkCanCreateMaterializedView(context(BOB), VIEW_ALICE_SCH1_VW1, Collections.emptyMap())).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanRefreshMaterializedView(context(BOB), VIEW_ALICE_SCH1_VW1)).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanSetMaterializedViewProperties(context(BOB), VIEW_ALICE_SCH1_VW1, Collections.emptyMap())).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanDropMaterializedView(context(BOB), VIEW_ALICE_SCH1_VW1)).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanRenameMaterializedView(context(BOB), VIEW_ALICE_SCH1_VW1, newViewName)).isInstanceOf(AccessDeniedException.class); + } + + @Test + public void testProcedureOperations() + { + accessControlManager.checkCanExecuteProcedure(context(ALICE), PROC_ALICE_SCH1_PROC1); + accessControlManager.checkCanExecuteTableProcedure(context(ALICE), TABLE_ALICE_SCH1_TBL1, PROC_ALICE_SCH1_PROC1.getRoutineName()); + + assertThatThrownBy(() -> accessControlManager.checkCanExecuteProcedure(context(BOB), PROC_ALICE_SCH1_PROC1)).isInstanceOf(AccessDeniedException.class); + assertThatThrownBy(() -> accessControlManager.checkCanExecuteTableProcedure(context(BOB), TABLE_ALICE_SCH1_TBL1, PROC_ALICE_SCH1_PROC1.getRoutineName())).isInstanceOf(AccessDeniedException.class); + } + + @Test + public void testFunctionOperations() + { + accessControlManager.checkCanCreateFunction(context(ALICE), FUNC_ALICE_SCH1_FUNC1); + accessControlManager.checkCanDropFunction(context(ALICE), FUNC_ALICE_SCH1_FUNC1); + accessControlManager.checkCanShowCreateFunction(context(ALICE), FUNC_ALICE_SCH1_FUNC1); + accessControlManager.checkCanShowFunctions(context(ALICE), SCHEMA_ALICE_SCH1); + accessControlManager.canCreateViewWithExecuteFunction(context(ALICE), FUNC_ALICE_SCH1_FUNC1); + + assertThat(accessControlManager.canExecuteFunction(context(ALICE), FUNC_ALICE_SCH1_FUNC1)).isTrue(); + assertThat(accessControlManager.canExecuteFunction(context(BOB), FUNC_ALICE_SCH1_FUNC1)).isFalse(); + + Set functionNames = Collections.singleton(new SchemaFunctionName(SCHEMA_ALICE_SCH1.getSchemaName(), FUNC_ALICE_SCH1_FUNC1.getRoutineName())); + + assertThat(accessControlManager.filterFunctions(context(ALICE), CATALOG_ALICE, functionNames)).isEqualTo(functionNames); + assertThat(accessControlManager.filterFunctions(context(BOB), CATALOG_ALICE, functionNames)).isEmpty(); + } + + @Test + public void testColumnMask() + { + final VarcharType varcharType = VarcharType.createVarcharType(20); + + // MASK_NONE + Optional ret = accessControlManager.getColumnMask(context(ALICE), TABLE_ALICE_SCH1_TBL1, "national_id", varcharType); + assertThat(ret.isPresent()).isFalse(); + + // MASK_SHOW_FIRST_4 + ret = accessControlManager.getColumnMask(context(BOB), TABLE_ALICE_SCH1_TBL1, "national_id", varcharType); + assertThat(ret.isPresent()).isTrue(); + assertThat(ret.get().getExpression()).isEqualTo("cast(regexp_replace(national_id, '(^.{4})(.*)', x -> x[1] || regexp_replace(x[2], '.', 'X')) as varchar(20))"); + } + + @Test + public void testRowFilters() + { + List retArray = accessControlManager.getRowFilters(context(ALICE), TABLE_ALICE_SCH1_TBL1); + assertThat(retArray).isEmpty(); + + retArray = accessControlManager.getRowFilters(context(BOB), TABLE_ALICE_SCH1_TBL1); + assertThat(retArray).isNotEmpty(); + assertThat(retArray.size()).isEqualTo(1); + assertThat(retArray.get(0).getExpression()).isEqualTo("status = 'active'"); + } + + private SystemSecurityContext context(Identity id) + { + return new SystemSecurityContext(id, new QueryId("id_1"), Instant.now()); + } +} diff --git a/plugin/trino-apache-ranger/src/test/resources/ranger-trino-security.xml b/plugin/trino-apache-ranger/src/test/resources/ranger-trino-security.xml new file mode 100644 index 000000000000..89c2a0c75221 --- /dev/null +++ b/plugin/trino-apache-ranger/src/test/resources/ranger-trino-security.xml @@ -0,0 +1,52 @@ + + + + + + ranger.plugin.trino.service.name + cl1_trino + + Name of the Ranger service containing policies for this SampleApp instance + + + + + ranger.plugin.trino.policy.source.impl + io.trino.plugin.ranger.RangerAdminClientImpl + + Policy source. + + + + + ranger.plugin.trino.policy.pollIntervalMs + 30000 + + How often to poll for changes in policies? + + + + + ranger.plugin.trino.policy.cache.dir + ${project.build.directory} + + Directory where Ranger policies are cached after successful retrieval from the source + + + + diff --git a/plugin/trino-apache-ranger/src/test/resources/trino-policies.json b/plugin/trino-apache-ranger/src/test/resources/trino-policies.json new file mode 100644 index 000000000000..351b933ebf2c --- /dev/null +++ b/plugin/trino-apache-ranger/src/test/resources/trino-policies.json @@ -0,0 +1,305 @@ +{ + "serviceName": "dev_trino", + "serviceId": 203, + "policyVersion": 5, + "policies": [ + { + "id": 10, + "service": "dev_trino", + "serviceType": "trino", + "name": "checkCanImpersonateUser", + "policyType": 0, + "policyPriority": 0, + "resources": { "trinouser": { "values": [ "bob" ] } }, + "policyItems": [ + { "accesses": [ { "type": "impersonate" } ], "users": [ "admin" ] } + ] + }, + { + "id": 11, + "service": "dev_trino", + "serviceType": "trino", + "name": "SystemInformation", + "policyType": 0, + "policyPriority": 0, + "resources": { "sysinfo": { "values": [ "*" ] } }, + "policyItems": [ + { "accesses": [ { "type": "read_sysinfo" }, { "type": "write_sysinfo" } ], "users": [ "admin" ] } + ] + }, + { + "id": 12, + "service": "dev_trino", + "serviceType": "trino", + "name": "SystemSessionProperty", + "policyType": 0, + "policyPriority": 0, + "resources": { "systemproperty": { "values": [ "test-property" ] } }, + "policyItems": [ + { "accesses": [ { "type": "alter" } ], "users": [ "admin" ] } + ] + }, + { + "id": 13, + "service": "dev_trino", + "serviceType": "trino", + "name": "checkCanExecuteQuery", + "policyType": 0, + "policyPriority": 0, + "resources": { "queryid": { "values": [ "*" ] } }, + "policyItems": [ + { "accesses": [ { "type": "execute" } ], "users": [ "admin" ] } + ] + }, + { + "id": 14, + "service": "dev_trino", + "serviceType": "trino", + "name": "checkFunction", + "policyType": 0, + "policyPriority": 0, + "resources": { "function": { "values": [ "func1" ] } }, + "policyItems": [ + { + "accesses": [ { "type": "execute" }, { "type": "grant" } ], + "users": [ "alice" ] + } + ] + }, + { + "id": 15, + "service": "dev_trino", + "serviceType": "trino", + "name": "alice-schema", + "policyType": 0, + "policyPriority": 0, + "resources": { + "catalog": { "values": [ "alice-catalog" ] }, + "schema": { "values": [ "sch1", "new-schema" ] } + }, + "policyItems": [ + { "accesses": [ { "type": "select" }, { "type": "insert" }, { "type": "create" }, { "type": "drop" }, { "type": "alter" }, { "type": "show" }, { "type": "grant" }, { "type": "revoke" } ], "users": [ "alice" ] } + ] + }, + { + "id": 16, + "service": "dev_trino", + "serviceType": "trino", + "name": "alice-catalog", + "policyType": 0, + "policyPriority": 0, + "resources": { + "catalog": { "values": [ "alice-catalog" ] } + }, + "policyItems": [ + { "accesses": [ { "type": "select" }, { "type": "insert" }, { "type": "create" }, { "type": "drop" }, { "type": "use" }, { "type": "alter" }, { "type": "show" } ], "users": [ "alice" ] } + ] + }, + { + "id": 17, + "service": "dev_trino", + "serviceType": "trino", + "name": "alice-table", + "policyType": 0, + "policyPriority": 0, + "resources": { + "catalog": { "values": [ "alice-catalog" ] }, + "schema": { "values": [ "sch1" ] }, + "table": { "values": [ "tbl1", "new-table" ] + } + }, + "policyItems": [ + { "accesses": [ { "type": "select" }, { "type": "insert" }, { "type": "create" }, { "type": "drop" }, { "type": "delete" }, { "type": "alter" }, { "type": "grant" }, { "type": "revoke" }, { "type": "show" } ], "users": [ "alice" ] } + ] + }, + { + "id": 18, + "service": "dev_trino", + "serviceType": "trino", + "name": "alice-table", + "policyType": 0, + "policyPriority": 0, + "resources": { + "catalog": { "values": [ "alice-catalog" ] }, + "schema": { "values": [ "sch1" ] }, + "table": { "values": [ "tbl1" ] }, + "column": { "values": [ "*" ] } + }, + "policyItems": [ + { "accesses": [ { "type": "select" }, { "type": "insert" }, { "type": "create" }, { "type": "drop" }, { "type": "delete" }, { "type": "alter" }, { "type": "grant" }, { "type": "revoke" }, { "type": "show" } ], "users": [ "alice" ] } + ] + }, + { + "id": 19, + "service": "dev_trino", + "serviceType": "trino", + "name": "alice-procedure", + "policyType": 0, + "policyPriority": 0, + "resources": { + "catalog": { "values": [ "alice-catalog" ] }, + "schema": { "values": [ "sch1" ] }, + "procedure": { "values": [ "proc1" ] } + }, + "policyItems": [ + { "accesses": [ { "type": "execute" } ], "users": [ "alice" ] } + ] + }, + { + "id": 20, + "service": "dev_trino", + "serviceType": "trino", + "name": "alice-view", + "policyType": 0, + "policyPriority": 0, + "resources": { + "catalog": { "values": [ "alice-catalog" ] }, + "schema": { "values": [ "sch1" ] }, + "table": { "values": [ "vw1", "new-view" ] } + }, + "policyItems": [ + { "accesses": [ { "type": "select" }, { "type": "create" }, { "type": "drop" }, { "type": "alter" } ], "users": [ "alice" ] } + ] + }, + { + "id": 21, + "service": "dev_trino", + "serviceType": "trino", + "name": "alice-session-property", + "policyType": 0, + "policyPriority": 0, + "resources": { + "catalog": { "values": [ "alice-catalog" ] }, + "sessionproperty": { "values": [ "property" ] } + }, + "policyItems": [ + { "accesses": [ { "type": "show" }, { "type": "alter" } ], "users": [ "alice" ] } + ] + }, + { + "id": 22, + "service": "dev_trino", + "serviceType": "trino", + "name": "open-to-all", + "policyType": 0, + "policyPriority": 0, + "resources": { + "catalog": { "values": [ "open-to-all", "all-allowed" ] } + }, + "policyItems": [ + { "accesses": [ { "type": "select" } ], "users": [ "{USER}" ] } + ] + }, + { + "id": 23, + "service": "dev_trino", + "serviceType": "trino", + "name": "role", + "policyType": 0, + "policyPriority": 0, + "resources": { "role": { "values": [ "*" ] } }, + "policyItems": [ + { "accesses": [ { "type": "create" }, { "type": "drop" }, { "type": "show" }, { "type": "grant" }, { "type": "revoke" } ], "users": [ "admin" ] } + ] + }, + { + "id": 24, + "service": "dev_trino", + "serviceType": "trino", + "name": "schema-function", + "policyType": 0, + "policyPriority": 0, + "resources": { + "catalog": { "values": [ "alice-catalog" ] }, + "schema": { "values": [ "sch1" ] }, + "schemafunction": { "values": [ "func1" ] } + }, + "policyItems": [ + { "accesses": [ { "type": "create" }, { "type": "drop" }, { "type": "show" }, { "type": "execute" } ], "users": [ "alice" ] } + ] + }, + { + "id": 25, + "service": "dev_trino", + "serviceType": "trino", + "name": "test-column-mask", + "policyType": 1, + "policyPriority": 0, + "resources": { + "catalog": { "values": [ "alice-catalog" ] }, + "schema": { "values": [ "sch1" ] }, + "table": { "values": [ "tbl1" ] }, + "column": { "values": [ "national_id" ] } + }, + "dataMaskPolicyItems": [ + { "accesses": [ { "type": "select" } ], "users": [ "alice" ], "dataMaskInfo": { "dataMaskType": "MASK_NONE" } }, + { "accesses": [ { "type": "select" } ], "users": [ "{USER}" ], "dataMaskInfo": { "dataMaskType": "MASK_SHOW_FIRST_4" } } + ] + }, + { + "id": 26, + "service": "dev_trino", + "serviceType": "trino", + "name": "test-row-filter", + "policyType": 2, + "policyPriority": 0, + "resources": { + "catalog": { "values": [ "alice-catalog" ] }, + "schema": { "values": [ "sch1" ] }, + "table": { "values": [ "tbl1" ] } + }, + "rowFilterPolicyItems": [ + { "rowFilterInfo": { "filterExpr": "" }, "accesses": [ { "type": "select" } ], "users": [ "alice" ] }, + { "rowFilterInfo": { "filterExpr": "status = 'active'" }, "accesses": [ { "type": "select" } ], "users": [ "{USER}" ] } + ] + }, + { + "id": 27, + "service": "dev_trino", + "serviceType": "trino", + "name": "user's home schema in alice-catalog", + "policyType": 0, + "policyPriority": 0, + "resources": { + "catalog": { "values": [ "user-catalog" ] }, + "schema": { "values": [ "${{USER._name}}_schema" ] } + }, + "policyItems": [ + { "accesses": [ { "type": "select" }, { "type": "insert" }, { "type": "create" }, { "type": "drop" }, { "type": "alter" }, { "type": "show" }, { "type": "grant" }, { "type": "revoke" } ], "users": [ "{USER}" ] } + ] + }, + { + "id": 28, + "service": "dev_trino", + "serviceType": "trino", + "name": "user's home schema in alice-catalog", + "policyType": 0, + "policyPriority": 0, + "resources": { + "catalog": { "values": [ "user-catalog" ] }, + "schema": { "values": [ "${{USER._name}}_schema" ] }, + "table": { "values": [ "*" ] } + }, + "policyItems": [ + { "accesses": [ { "type": "select" }, { "type": "insert" }, { "type": "create" }, { "type": "drop" }, { "type": "alter" }, { "type": "show" }, { "type": "grant" }, { "type": "revoke" } ], "users": [ "{USER}" ] } + ] + }, + { + "id": 29, + "service": "dev_trino", + "serviceType": "trino", + "name": "user's home schema in alice-catalog", + "policyType": 0, + "policyPriority": 0, + "resources": { + "catalog": { "values": [ "user-catalog" ] }, + "schema": { "values": [ "${{USER._name}}_schema" ] }, + "table": { "values": [ "*" ] }, + "column": { "values": [ "*" ] } + }, + "policyItems": [ + { "accesses": [ { "type": "select" }, { "type": "insert" }, { "type": "create" }, { "type": "drop" }, { "type": "alter" }, { "type": "show" }, { "type": "grant" }, { "type": "revoke" } ], "users": [ "{USER}" ] } + ] + } + ] +} diff --git a/pom.xml b/pom.xml index bd282730530f..b7a64373733b 100644 --- a/pom.xml +++ b/pom.xml @@ -59,6 +59,7 @@ lib/trino-parquet lib/trino-plugin-toolkit lib/trino-record-decoder + plugin/trino-apache-ranger plugin/trino-base-jdbc plugin/trino-bigquery plugin/trino-blackhole @@ -958,6 +959,19 @@ ${dep.swagger.version} + + io.trino + trino-apache-ranger + ${project.version} + + + + io.trino + trino-apache-ranger + ${project.version} + test-jar + + io.trino trino-array diff --git a/testing/trino-product-tests-groups/src/main/java/io/trino/tests/product/TestGroups.java b/testing/trino-product-tests-groups/src/main/java/io/trino/tests/product/TestGroups.java index 90939d93a921..ce6d7104bba2 100644 --- a/testing/trino-product-tests-groups/src/main/java/io/trino/tests/product/TestGroups.java +++ b/testing/trino-product-tests-groups/src/main/java/io/trino/tests/product/TestGroups.java @@ -88,6 +88,7 @@ public final class TestGroups public static final String CLICKHOUSE = "clickhouse"; public static final String KUDU = "kudu"; public static final String MARIADB = "mariadb"; + public static final String APACHE_RANGER = "apache-ranger"; public static final String SNOWFLAKE = "snowflake"; public static final String DELTA_LAKE_OSS = "delta-lake-oss"; public static final String DELTA_LAKE_HDFS = "delta-lake-hdfs"; diff --git a/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/env/environment/EnvMultinodeApacheRanger.java b/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/env/environment/EnvMultinodeApacheRanger.java new file mode 100644 index 000000000000..4bca1170cdd2 --- /dev/null +++ b/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/env/environment/EnvMultinodeApacheRanger.java @@ -0,0 +1,89 @@ +/* + * Licensed 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 io.trino.tests.product.launcher.env.environment; + +import com.google.inject.Inject; +import io.trino.tests.product.launcher.docker.DockerFiles; +import io.trino.tests.product.launcher.env.DockerContainer; +import io.trino.tests.product.launcher.env.Environment; +import io.trino.tests.product.launcher.env.EnvironmentProvider; +import io.trino.tests.product.launcher.env.common.StandardMultinode; +import io.trino.tests.product.launcher.env.common.TestsEnvironment; +import io.trino.tests.product.launcher.testcontainers.PortBinder; +import org.testcontainers.containers.startupcheck.IsRunningStartupCheckStrategy; + +import static io.trino.tests.product.launcher.docker.ContainerUtil.forSelectedPorts; +import static io.trino.tests.product.launcher.env.EnvironmentContainers.COORDINATOR; +import static io.trino.tests.product.launcher.env.EnvironmentContainers.WORKER; +import static io.trino.tests.product.launcher.env.common.Standard.CONTAINER_TRINO_ETC; +import static java.util.Objects.requireNonNull; +import static org.testcontainers.utility.MountableFile.forHostPath; + +/** + * Trino with Apache Ranger authorizer plugin + */ +@TestsEnvironment +public class EnvMultinodeApacheRanger + extends EnvironmentProvider +{ + public static final int MARIADB_PORT = 23306; + + private final DockerFiles dockerFiles; + private final PortBinder portBinder; + + @Inject + public EnvMultinodeApacheRanger(StandardMultinode standardMultinode, DockerFiles dockerFiles, PortBinder portBinder) + { + super(standardMultinode); + this.dockerFiles = requireNonNull(dockerFiles, "dockerFiles is null"); + this.portBinder = requireNonNull(portBinder, "portBinder is null"); + } + + @Override + public void extendEnvironment(Environment.Builder builder) + { + DockerFiles.ResourceProvider configDir = dockerFiles.getDockerFilesHostDirectory("conf/environment/multinode-apache-ranger/"); + + builder.addConnector("mariadb", forHostPath(configDir.getPath("mariadb.properties"))); + builder.addContainer(createMariaDb()); + + builder.configureContainer(COORDINATOR, container -> container.withCopyFileToContainer(forHostPath(configDir.getPath("access-control.properties")), CONTAINER_TRINO_ETC + "/access-control.properties")); + builder.configureContainer(COORDINATOR, container -> container.withCopyFileToContainer(forHostPath(configDir.getPath("ranger-trino-security.xml")), CONTAINER_TRINO_ETC + "/ranger-trino-security.xml")); + builder.configureContainer(COORDINATOR, container -> container.withCopyFileToContainer(forHostPath(configDir.getPath("ranger-trino-audit.xml")), CONTAINER_TRINO_ETC + "/ranger-trino-audit.xml")); + builder.configureContainer(COORDINATOR, container -> container.withCopyFileToContainer(forHostPath(configDir.getPath("ranger-policymgr-ssl.xml")), CONTAINER_TRINO_ETC + "/ranger-policymgr-ssl.xml")); + builder.configureContainer(COORDINATOR, container -> container.withCopyFileToContainer(forHostPath(configDir.getPath("trino-policies.json")), "/tmp/apache-ranger-policycache/trino_dev_trino.json")); + + builder.configureContainer(WORKER, container -> container.withCopyFileToContainer(forHostPath(configDir.getPath("access-control.properties")), CONTAINER_TRINO_ETC + "/access-control.properties")); + builder.configureContainer(WORKER, container -> container.withCopyFileToContainer(forHostPath(configDir.getPath("ranger-trino-security.xml")), CONTAINER_TRINO_ETC + "/ranger-trino-security.xml")); + builder.configureContainer(WORKER, container -> container.withCopyFileToContainer(forHostPath(configDir.getPath("ranger-trino-audit.xml")), CONTAINER_TRINO_ETC + "/ranger-trino-audit.xml")); + builder.configureContainer(WORKER, container -> container.withCopyFileToContainer(forHostPath(configDir.getPath("ranger-policymgr-ssl.xml")), CONTAINER_TRINO_ETC + "/ranger-policymgr-ssl.xml")); + builder.configureContainer(WORKER, container -> container.withCopyFileToContainer(forHostPath(configDir.getPath("trino-policies.json")), "/tmp/apache-ranger-policycache/trino_dev_trino.json")); + } + + private DockerContainer createMariaDb() + { + DockerContainer container = new DockerContainer("mariadb:10.7.1", "mariadb") + .withEnv("MYSQL_USER", "test") + .withEnv("MYSQL_PASSWORD", "test") + .withEnv("MYSQL_ROOT_PASSWORD", "test") + .withEnv("MYSQL_DATABASE", "test") + .withCommand("mysqld", "--port", Integer.toString(MARIADB_PORT), "--character-set-server", "utf8mb4") + .withStartupCheckStrategy(new IsRunningStartupCheckStrategy()) + .waitingFor(forSelectedPorts(MARIADB_PORT)); + + portBinder.exposePort(container, MARIADB_PORT); + + return container; + } +} diff --git a/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/suite/suites/SuiteApacheRanger.java b/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/suite/suites/SuiteApacheRanger.java new file mode 100644 index 000000000000..b97312004666 --- /dev/null +++ b/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/suite/suites/SuiteApacheRanger.java @@ -0,0 +1,37 @@ +/* + * Licensed 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 io.trino.tests.product.launcher.suite.suites; + +import com.google.common.collect.ImmutableList; +import io.trino.tests.product.launcher.env.EnvironmentConfig; +import io.trino.tests.product.launcher.env.environment.EnvMultinodeApacheRanger; +import io.trino.tests.product.launcher.suite.Suite; +import io.trino.tests.product.launcher.suite.SuiteTestRun; + +import java.util.List; + +import static io.trino.tests.product.launcher.suite.SuiteTestRun.testOnEnvironment; + +public class SuiteApacheRanger + extends Suite +{ + @Override + public List getTestRuns(EnvironmentConfig config) + { + return ImmutableList.of( + testOnEnvironment(EnvMultinodeApacheRanger.class) + .withGroups("configured_features", "apache-ranger") + .build()); + } +} diff --git a/testing/trino-product-tests-launcher/src/main/resources/docker/trino-product-tests/conf/environment/multinode-apache-ranger/access-control.properties b/testing/trino-product-tests-launcher/src/main/resources/docker/trino-product-tests/conf/environment/multinode-apache-ranger/access-control.properties new file mode 100644 index 000000000000..1d9c791555a4 --- /dev/null +++ b/testing/trino-product-tests-launcher/src/main/resources/docker/trino-product-tests/conf/environment/multinode-apache-ranger/access-control.properties @@ -0,0 +1,4 @@ +access-control.name=apache-ranger + +apache-ranger.service.name=dev_trino +apache-ranger.plugin.config.resource=/docker/trino-product-tests/conf/trino/etc/ranger-trino-security.xml,/docker/trino-product-tests/conf/trino/etc/ranger-trino-audit.xml,/docker/trino-product-tests/conf/trino/etc/ranger-policymgr-ssl.xml diff --git a/testing/trino-product-tests-launcher/src/main/resources/docker/trino-product-tests/conf/environment/multinode-apache-ranger/mariadb.properties b/testing/trino-product-tests-launcher/src/main/resources/docker/trino-product-tests/conf/environment/multinode-apache-ranger/mariadb.properties new file mode 100644 index 000000000000..78530164025d --- /dev/null +++ b/testing/trino-product-tests-launcher/src/main/resources/docker/trino-product-tests/conf/environment/multinode-apache-ranger/mariadb.properties @@ -0,0 +1,4 @@ +connector.name=mariadb +connection-url=jdbc:mariadb://mariadb:23306/ +connection-user=test +connection-password=test diff --git a/testing/trino-product-tests-launcher/src/main/resources/docker/trino-product-tests/conf/environment/multinode-apache-ranger/ranger-policymgr-ssl.xml b/testing/trino-product-tests-launcher/src/main/resources/docker/trino-product-tests/conf/environment/multinode-apache-ranger/ranger-policymgr-ssl.xml new file mode 100644 index 000000000000..03e30dd449b4 --- /dev/null +++ b/testing/trino-product-tests-launcher/src/main/resources/docker/trino-product-tests/conf/environment/multinode-apache-ranger/ranger-policymgr-ssl.xml @@ -0,0 +1,44 @@ + + + + + + + xasecure.policymgr.clientssl.keystore + trinoservice-clientcert.jks + Java Keystore files + + + + xasecure.policymgr.clientssl.truststore + cacerts-xasecure.jks + java truststore file + + + + xasecure.policymgr.clientssl.keystore.credential.file + jceks://file/etc/trino/keystore-trinoservice-ssl.jceks + java keystore credential file + + + + xasecure.policymgr.clientssl.truststore.credential.file + jceks://file/etc/trino/truststore-trinoservice-ssl.jceks + java truststore credential file + + diff --git a/testing/trino-product-tests-launcher/src/main/resources/docker/trino-product-tests/conf/environment/multinode-apache-ranger/ranger-trino-audit.xml b/testing/trino-product-tests-launcher/src/main/resources/docker/trino-product-tests/conf/environment/multinode-apache-ranger/ranger-trino-audit.xml new file mode 100644 index 000000000000..cd7ddb136e85 --- /dev/null +++ b/testing/trino-product-tests-launcher/src/main/resources/docker/trino-product-tests/conf/environment/multinode-apache-ranger/ranger-trino-audit.xml @@ -0,0 +1,24 @@ + + + + + + xasecure.audit.is.enabled + false + + diff --git a/testing/trino-product-tests-launcher/src/main/resources/docker/trino-product-tests/conf/environment/multinode-apache-ranger/ranger-trino-security.xml b/testing/trino-product-tests-launcher/src/main/resources/docker/trino-product-tests/conf/environment/multinode-apache-ranger/ranger-trino-security.xml new file mode 100644 index 000000000000..9a9b09d76baf --- /dev/null +++ b/testing/trino-product-tests-launcher/src/main/resources/docker/trino-product-tests/conf/environment/multinode-apache-ranger/ranger-trino-security.xml @@ -0,0 +1,48 @@ + + + + + + ranger.plugin.trino.service.name + dev_trino + + Name of the Ranger service containing policies for this SampleApp instance + + + + + ranger.plugin.trino.super.users + trino,hive + List of users with superuser privileges + + + + ranger.plugin.trino.policy.rest.url + http://host.docker.internal:6080 + URL to Ranger Admin + + + + ranger.plugin.trino.policy.cache.dir + /tmp/apache-ranger-policycache + + Directory where Ranger policies are cached after successful retrieval from the source + + + + diff --git a/testing/trino-product-tests-launcher/src/main/resources/docker/trino-product-tests/conf/environment/multinode-apache-ranger/trino-policies.json b/testing/trino-product-tests-launcher/src/main/resources/docker/trino-product-tests/conf/environment/multinode-apache-ranger/trino-policies.json new file mode 100644 index 000000000000..134e4af5b324 --- /dev/null +++ b/testing/trino-product-tests-launcher/src/main/resources/docker/trino-product-tests/conf/environment/multinode-apache-ranger/trino-policies.json @@ -0,0 +1,189 @@ +{ + "serviceName": "dev_trino", + "serviceId": 203, + "serviceDef": { + "id": 203, + "name": "trino", + "displayName": "trino", + "implClass": "org.apache.ranger.services.trino.RangerServiceTrino", + "label": "Trino", + "description": "Trino", + "resources": [ + { "itemId": 1, "name": "catalog", "type": "string", "level": 10, "parent": "", "mandatory": true, "lookupSupported": true, "recursiveSupported": false, "excludesSupported": true, "matcher": "org.apache.ranger.plugin.resourcematcher.RangerDefaultResourceMatcher", "matcherOptions": { "wildCard": true, "ignoreCase": true }, "label": "Trino Catalog", "description": "Trino Catalog", "isValidLeaf": true }, + { "itemId": 2, "name": "schema", "type": "string", "level": 20, "parent": "catalog", "mandatory": true, "lookupSupported": true, "recursiveSupported": false, "excludesSupported": true, "matcher": "org.apache.ranger.plugin.resourcematcher.RangerDefaultResourceMatcher", "matcherOptions": { "wildCard": true, "ignoreCase": true }, "label": "Trino Schema", "description": "Trino Schema", "isValidLeaf": true }, + { "itemId": 3, "name": "table", "type": "string", "level": 30, "parent": "schema", "mandatory": true, "lookupSupported": true, "recursiveSupported": false, "excludesSupported": true, "matcher": "org.apache.ranger.plugin.resourcematcher.RangerDefaultResourceMatcher", "matcherOptions": { "wildCard": true, "ignoreCase": true }, "label": "Trino Table", "description": "Trino Table", "isValidLeaf": true }, + { "itemId": 4, "name": "column", "type": "string", "level": 40, "parent": "table", "mandatory": true, "lookupSupported": true, "recursiveSupported": false, "excludesSupported": true, "matcher": "org.apache.ranger.plugin.resourcematcher.RangerDefaultResourceMatcher", "matcherOptions": { "wildCard": true, "ignoreCase": true }, "label": "Trino Column", "description": "Trino Column", "isValidLeaf": true }, + { "itemId": 5, "name": "trinouser", "type": "string", "level": 10, "parent": "", "mandatory": true, "lookupSupported": false, "recursiveSupported": false, "excludesSupported": false, "matcher": "org.apache.ranger.plugin.resourcematcher.RangerDefaultResourceMatcher", "matcherOptions": { "wildCard": true, "ignoreCase": true }, "label": "Trino User", "description": "Trino User", "accessTypeRestrictions": ["impersonate"] }, + { "itemId": 6, "name": "systemproperty", "type": "string", "level": 10, "parent": "", "mandatory": true, "lookupSupported": false, "recursiveSupported": false, "excludesSupported": false, "matcher": "org.apache.ranger.plugin.resourcematcher.RangerDefaultResourceMatcher", "matcherOptions": { "wildCard": true, "ignoreCase": true }, "label": "System Property", "description": "Trino System Property", "accessTypeRestrictions": ["alter"] }, + { "itemId": 7, "name": "sessionproperty", "type": "string", "level": 20, "parent": "catalog", "mandatory": true, "lookupSupported": false, "recursiveSupported": false, "excludesSupported": false, "matcher": "org.apache.ranger.plugin.resourcematcher.RangerDefaultResourceMatcher", "matcherOptions": { "wildCard": true, "ignoreCase": true }, "label": "Catalog Session Property", "description": "Trino Catalog Session Property", "accessTypeRestrictions": ["alter"] }, + { "itemId": 8, "name": "function", "type": "string", "level": 10, "parent": "", "mandatory": true, "lookupSupported": false, "recursiveSupported": false, "excludesSupported": false, "matcher": "org.apache.ranger.plugin.resourcematcher.RangerDefaultResourceMatcher", "matcherOptions": { "wildCard": true, "ignoreCase": true }, "label": "Trino Function", "description": "Trino Function", "accessTypeRestrictions": ["execute", "grant"] }, + { "itemId": 9, "name": "procedure", "type": "string", "level": 30, "parent": "schema", "mandatory": true, "lookupSupported": false, "recursiveSupported": false, "excludesSupported": false, "matcher": "org.apache.ranger.plugin.resourcematcher.RangerDefaultResourceMatcher", "matcherOptions": { "wildCard": true, "ignoreCase": true }, "label": "Schema Procedure", "description": "Schema Procedure", "accessTypeRestrictions": ["execute", "grant"] }, + { "itemId": 10, "name": "schemafunction", "type": "string", "level": 30, "parent": "schema", "mandatory": true, "lookupSupported": false, "recursiveSupported": false, "excludesSupported": false, "matcher": "org.apache.ranger.plugin.resourcematcher.RangerDefaultResourceMatcher", "matcherOptions": { "wildCard": true, "ignoreCase": true }, "label": "Schema Function", "description": "Schema Function", "accessTypeRestrictions": [ "create", "drop", "show" ] }, + { "itemId": 11, "name": "queryid", "type": "string", "level": 10, "parent": "", "mandatory": true, "lookupSupported": false, "recursiveSupported": false, "excludesSupported": false, "matcher": "org.apache.ranger.plugin.resourcematcher.RangerDefaultResourceMatcher", "matcherOptions": { "wildCard": true, "ignoreCase": true }, "label": "Query ID", "description": "Query ID", "accessTypeRestrictions": [ "execute" ] }, + { "itemId": 12, "name": "sysinfo", "type": "string", "level": 10, "parent": "", "mandatory": true, "lookupSupported": false, "recursiveSupported": false, "excludesSupported": false, "matcher": "org.apache.ranger.plugin.resourcematcher.RangerDefaultResourceMatcher", "matcherOptions": { "wildCard": true, "ignoreCase": true }, "label": "System Information", "description": "Trino System Information", "accessTypeRestrictions": [ "read_sysinfo", "write_sysinfo" ] }, + { "itemId": 13, "name": "role", "type": "string", "level": 10, "parent": "", "mandatory": true, "lookupSupported": false, "recursiveSupported": false, "excludesSupported": false, "matcher": "org.apache.ranger.plugin.resourcematcher.RangerDefaultResourceMatcher", "matcherOptions": { "wildCard": true, "ignoreCase": true }, "label": "Role", "description": "Trino Role", "accessTypeRestrictions": [ "create", "drop", "show", "grant", "revoke" ] } + ], + "accessTypes": [ + { "itemId": 1, "name": "select", "label": "Select", "category": "READ" }, + { "itemId": 2, "name": "insert", "label": "Insert", "category": "UPDATE" }, + { "itemId": 3, "name": "create", "label": "Create", "category": "CREATE" }, + { "itemId": 4, "name": "drop", "label": "Drop", "category": "DELETE" }, + { "itemId": 5, "name": "delete", "label": "Delete", "category": "DELETE" }, + { "itemId": 6, "name": "use", "label": "Use", "category": "READ" }, + { "itemId": 7, "name": "alter", "label": "Alter", "category": "CREATE" }, + { "itemId": 8, "name": "grant", "label": "Grant", "category": "MANAGE" }, + { "itemId": 9, "name": "revoke", "label": "Revoke", "category": "MANAGE" }, + { "itemId": 10, "name": "show", "label": "Show", "category": "READ" }, + { "itemId": 11, "name": "impersonate", "label": "Impersonate", "category": "READ" }, + { + "itemId": 12, + "name": "all", + "label": "All", + "impliedGrants": [ + "select", + "insert", + "create", + "delete", + "drop", + "use", + "alter", + "grant", + "revoke", + "show", + "impersonate", + "execute", + "read_sysinfo", + "write_sysinfo" + ] + }, + { "itemId": 13, "name": "execute", "label": "Execute", "category": "READ" }, + { "itemId": 14, "name": "read_sysinfo", "label": "Read System Information", "category": "MANAGE" }, + { "itemId": 15, "name": "write_sysinfo", "label": "Write System Information", "category": "MANAGE" } + ], + "configs": [ + { + "itemId": 1, + "name": "username", + "type": "string", + "mandatory": true, + "label": "Username" + }, + { + "itemId": 2, + "name": "password", + "type": "password", + "mandatory": false, + "label": "Password" + }, + { + "itemId": 3, + "name": "jdbc.driverClassName", + "type": "string", + "mandatory": true, + "defaultValue": "io.trino.jdbc.TrinoDriver" + }, + { + "itemId": 4, + "name": "jdbc.url", + "type": "string", + "mandatory": true + }, + { + "itemId": 5, + "name": "ranger.plugin.audit.filters", + "type": "string", + "defaultValue": "[{'accessResult':'DENIED','isAudited':true},{'isAudited':false,'resources':{'queryid':{'values':['*']}},'accessTypes':['execute']},{'isAudited':false,'resources':{'trinouser':{'values':['{USER}']}},'accessTypes':['impersonate']}]" + }, + { + "itemId": 6, + "name": "ranger.plugin.super.users", + "label": "Superusers", + "description": "Superusers will have full access to all resources in this Trino instance", + "type": "string", + "defaultValue": "trino" + }, + { + "itemId": 7, + "name": "ranger.plugin.super.groups", + "label": "Superuser groups", + "description": "Users in superuser groups will have full access to all resources in this Trino instance", + "type": "string", + "defaultValue": "trino" + }, + { + "itemId": 8, + "name": "service.admin.users", + "label": "Service admin users", + "description": "Service admin users can create policies for any resource in this Trino instance", + "type": "string", + "defaultValue": "trino" + }, + { + "itemId": 9, + "name": "service.admin.groups", + "label": "Service admin usergroups", + "description": "Users in service admin usergroups can create policies for any resource in this Trino instance", + "type": "string", + "defaultValue": "trino" + } + ], + "dataMaskDef": { + "accessTypes": [ + { "name": "select" } + ], + "resources": [ + { "name": "catalog", "matcherOptions": { "wildCard": "true" }, "lookupSupported": true, "uiHint":"{ \"singleValue\":true }" }, + { "name": "schema", "matcherOptions": { "wildCard": "true" }, "lookupSupported": true, "uiHint":"{ \"singleValue\":true }" }, + { "name": "table", "matcherOptions": { "wildCard": "true" }, "lookupSupported": true, "uiHint":"{ \"singleValue\":true }" }, + { "name": "column", "matcherOptions": { "wildCard": "true" }, "lookupSupported": true, "uiHint":"{ \"singleValue\":true }" } + ], + "maskTypes": [ + { "itemId": 1, "name": "MASK", "label": "Redact", "description": "Replace lowercase with 'x', uppercase with 'X', digits with '0'", "transformer": "cast(regexp_replace(regexp_replace(regexp_replace({col},'([A-Z])', 'X'),'([a-z])','x'),'([0-9])','0') as {type})", "dataMaskOptions": { } }, + { "itemId": 2, "name": "MASK_SHOW_LAST_4", "label": "Partial mask: show last 4", "description": "Show last 4 characters; replace rest with 'X'", "transformer": "cast(regexp_replace({col}, '(.*)(.{4}$)', x -> regexp_replace(x[1], '.', 'X') || x[2]) as {type})" }, + { "itemId": 3, "name": "MASK_SHOW_FIRST_4", "label": "Partial mask: show first 4", "description": "Show first 4 characters; replace rest with 'x'", "transformer": "cast(regexp_replace({col}, '(^.{4})(.*)', x -> x[1] || regexp_replace(x[2], '.', 'X')) as {type})" }, + { "itemId": 4, "name": "MASK_HASH", "label": "Hash", "description": "Hash the value of a varchar with sha256", "transformer": "cast(to_hex(sha256(to_utf8({col}))) as {type})" }, + { "itemId": 5, "name": "MASK_NULL", "label": "Nullify", "description": "Replace with NULL" }, + { "itemId": 6, "name": "MASK_NONE", "label": "Unmasked (retain original value)", "description": "No masking" }, + { "itemId": 12, "name": "MASK_DATE_SHOW_YEAR", "label": "Date: show only year", "description": "Date: show only year", "transformer": "date_trunc('year', {col})" }, + { "itemId": 13, "name": "CUSTOM", "label": "Custom", "description": "Custom" } + ] + }, + "rowFilterDef": { + "accessTypes": [ + { "name": "select" } + ], + "resources": [ + { "name": "catalog", "matcherOptions": { "wildCard": "true" }, "lookupSupported": true, "mandatory": true, "uiHint": "{ \"singleValue\":true }" }, + { "name": "schema", "matcherOptions": { "wildCard": "true" }, "lookupSupported": true, "mandatory": true, "uiHint": "{ \"singleValue\":true }" }, + { "name": "table", "matcherOptions": { "wildCard": "true" }, "lookupSupported": true, "mandatory": true, "uiHint": "{ \"singleValue\":true }" } + ] + } + }, + "policyVersion": 5, + "policies": [ + { + "id": 10, + "service": "dev_trino", + "serviceType": "trino", + "name": "checkCanImpersonateUser", + "policyType": 0, + "policyPriority": 0, + "resources": { "trinouser": { "values": [ "{USER}" ] } }, + "policyItems": [ + { "accesses": [ { "type": "impersonate" } ], "users": [ "{USER}" ] } + ] + }, + { + "id": 11, + "service": "dev_trino", + "serviceType": "trino", + "name": "checkCanExecuteQuery", + "policyType": 0, + "policyPriority": 0, + "resources": { "queryid": { "values": [ "*" ] } }, + "policyItems": [ + { "accesses": [ { "type": "execute" } ], "users": [ "{USER}" ] } + ] + } + ] +} diff --git a/testing/trino-product-tests/src/main/java/io/trino/tests/product/ranger/TestApacheRanger.java b/testing/trino-product-tests/src/main/java/io/trino/tests/product/ranger/TestApacheRanger.java new file mode 100644 index 000000000000..7bde7104d0a9 --- /dev/null +++ b/testing/trino-product-tests/src/main/java/io/trino/tests/product/ranger/TestApacheRanger.java @@ -0,0 +1,63 @@ +/* + * Licensed 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 io.trino.tests.product.ranger; + +import io.trino.tempto.ProductTest; +import io.trino.tempto.query.QueryExecutionException; +import io.trino.tempto.query.QueryExecutor; +import org.testng.annotations.Test; + +import static io.trino.tempto.assertions.QueryAssert.Row.row; +import static io.trino.testing.TestingNames.randomNameSuffix; +import static io.trino.tests.product.TestGroups.APACHE_RANGER; +import static io.trino.tests.product.TestGroups.PROFILE_SPECIFIC_TESTS; +import static io.trino.tests.product.utils.QueryExecutors.connectToTrino; +import static io.trino.tests.product.utils.QueryExecutors.onTrino; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class TestApacheRanger + extends ProductTest +{ + @Test(groups = {APACHE_RANGER, PROFILE_SPECIFIC_TESTS}) + public void testCreateTableAsSelect() + { + String tableName = "mariadb.test.nation_" + randomNameSuffix(); + + // onTrino() is mapped to user hive. Ranger plugin is configured with hive as a superuser, so all queries from hive should succeed. + try (QueryExecutor trino = onTrino()) { + try { + trino.executeQuery("DROP TABLE IF EXISTS " + tableName); + assertThat(trino.executeQuery("CREATE TABLE " + tableName + " AS SELECT * FROM tpch.tiny.nation")).updatedRowsCountIsEqualTo(25); + assertThat(trino.executeQuery("SELECT COUNT(*) FROM " + tableName)).containsOnly(row(25)); + assertThat(trino.executeQuery("TRUNCATE TABLE " + tableName)).updatedRowsCountIsEqualTo(0); + assertThat(trino.executeQuery("INSERT INTO " + tableName + " SELECT * FROM tpch.tiny.nation")).updatedRowsCountIsEqualTo(25); + assertThat(trino.executeQuery("UPDATE " + tableName + " SET comment = 'updated comment'")).updatedRowsCountIsEqualTo(25); + assertThat(trino.executeQuery("DELETE FROM " + tableName)).updatedRowsCountIsEqualTo(25); + + // config 'alice@trino' is mapped to user alice. This user doesn't have any permissions in Ranger, so all queries should fail. + try (QueryExecutor userAlice = connectToTrino("alice@trino")) { + assertThatThrownBy(() -> userAlice.executeQuery("SELECT COUNT(*) FROM " + tableName)).isInstanceOf(QueryExecutionException.class); + assertThatThrownBy(() -> userAlice.executeQuery("TRUNCATE TABLE " + tableName)).isInstanceOf(QueryExecutionException.class); + assertThatThrownBy(() -> userAlice.executeQuery("INSERT INTO " + tableName + " SELECT * FROM tpch.tiny.nation")).isInstanceOf(QueryExecutionException.class); + assertThatThrownBy(() -> userAlice.executeQuery("UPDATE " + tableName + " SET comment = 'updated comment'")).isInstanceOf(QueryExecutionException.class); + assertThatThrownBy(() -> userAlice.executeQuery("DELETE FROM " + tableName)).isInstanceOf(QueryExecutionException.class); + } + } + finally { + trino.executeQuery("DROP TABLE IF EXISTS " + tableName); + } + } + } +}