diff --git a/core/trino-main/src/main/java/io/trino/connector/system/FunctionsAuthorization.java b/core/trino-main/src/main/java/io/trino/connector/system/FunctionsAuthorization.java new file mode 100644 index 000000000000..7d055013d854 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/connector/system/FunctionsAuthorization.java @@ -0,0 +1,159 @@ +/* + * 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.connector.system; + +import com.google.common.collect.ImmutableList; +import com.google.inject.Inject; +import io.trino.FullConnectorSession; +import io.trino.Session; +import io.trino.metadata.Metadata; +import io.trino.metadata.QualifiedObjectPrefix; +import io.trino.security.AccessControl; +import io.trino.spi.ErrorCodeSupplier; +import io.trino.spi.TrinoException; +import io.trino.spi.connector.CatalogSchemaName; +import io.trino.spi.connector.ConnectorSession; +import io.trino.spi.connector.ConnectorTableMetadata; +import io.trino.spi.connector.ConnectorTransactionHandle; +import io.trino.spi.connector.InMemoryRecordSet; +import io.trino.spi.connector.RecordCursor; +import io.trino.spi.connector.SchemaTableName; +import io.trino.spi.connector.SystemTable; +import io.trino.spi.function.SchemaFunctionName; +import io.trino.spi.predicate.Domain; +import io.trino.spi.predicate.TupleDomain; +import io.trino.spi.security.FunctionAuthorization; +import io.trino.spi.security.TrinoPrincipal; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static io.trino.connector.system.jdbc.FilterUtil.tryGetSingleVarcharValue; +import static io.trino.metadata.MetadataListing.listAllAvailableSchemas; +import static io.trino.metadata.MetadataUtil.TableMetadataBuilder.tableMetadataBuilder; +import static io.trino.spi.StandardErrorCode.GENERIC_INTERNAL_ERROR; +import static io.trino.spi.connector.SystemTable.Distribution.SINGLE_COORDINATOR; +import static io.trino.spi.type.VarcharType.VARCHAR; +import static io.trino.spi.type.VarcharType.createUnboundedVarcharType; +import static java.util.Objects.requireNonNull; + +public class FunctionsAuthorization + implements SystemTable +{ + public static final SchemaTableName FUNCTIONS_AUTHORIZATION_NAME = new SchemaTableName("metadata", "functions_authorization"); + + public static final ConnectorTableMetadata FUNCTIONS_AUTHORIZATION = tableMetadataBuilder(FUNCTIONS_AUTHORIZATION_NAME) + .column("catalog", createUnboundedVarcharType()) + .column("schema", createUnboundedVarcharType()) + .column("name", createUnboundedVarcharType()) + .column("authorization_type", createUnboundedVarcharType()) + .column("authorization", createUnboundedVarcharType()) + .build(); + + private final Metadata metadata; + private final AccessControl accessControl; + + @Inject + public FunctionsAuthorization(Metadata metadata, AccessControl accessControl) + { + this.metadata = requireNonNull(metadata, "metadata is null"); + this.accessControl = requireNonNull(accessControl, "accessControl is null"); + } + + @Override + public Distribution getDistribution() + { + return SINGLE_COORDINATOR; + } + + @Override + public ConnectorTableMetadata getTableMetadata() + { + return FUNCTIONS_AUTHORIZATION; + } + + @Override + public RecordCursor cursor(ConnectorTransactionHandle transactionHandle, ConnectorSession connectorSession, TupleDomain constraint) + { + Session session = ((FullConnectorSession) connectorSession).getSession(); + InMemoryRecordSet.Builder table = InMemoryRecordSet.builder(FUNCTIONS_AUTHORIZATION); + for (CatalogFunctionAuthorization functionAuthorization : getFunctionsAuthorization(session, constraint)) { + SchemaFunctionName schemaFunctionName = functionAuthorization.functionAuthorization().schemaFunctionName(); + TrinoPrincipal trinoPrincipal = functionAuthorization.functionAuthorization().trinoPrincipal(); + table.addRow( + functionAuthorization.catalog(), + schemaFunctionName.getSchemaName(), + schemaFunctionName.getFunctionName(), + trinoPrincipal.getType().toString(), + trinoPrincipal.getName()); + } + return table.build().cursor(); + } + + private List getFunctionsAuthorization(Session session, TupleDomain constraint) + { + try { + return doGetFunctionsAuthorization(session, constraint); + } + catch (RuntimeException exception) { + ErrorCodeSupplier result = GENERIC_INTERNAL_ERROR; + if (exception instanceof TrinoException trinoException) { + result = trinoException::getErrorCode; + } + throw new TrinoException( + result, + "Error access functions_authorization metadata table", + exception); + } + } + + private List doGetFunctionsAuthorization(Session session, TupleDomain constraint) + { + Domain catalogDomain = constraint.getDomain(0, VARCHAR); + Domain schemaDomain = constraint.getDomain(1, VARCHAR); + Set availableSchemas = listAllAvailableSchemas( + session, + metadata, + accessControl, + catalogDomain, + schemaDomain); + + Optional functionName = tryGetSingleVarcharValue(constraint.getDomain(2, VARCHAR)); + ImmutableList.Builder result = ImmutableList.builder(); + availableSchemas.forEach(catalogSchemaName -> { + Set allFunctionsAuthorization = metadata.getFunctionsAuthorizationInfo(session, new QualifiedObjectPrefix(catalogSchemaName.getCatalogName(), Optional.of(catalogSchemaName.getSchemaName()), functionName)); + Set filteredFunctions = accessControl.filterFunctions( + session.toSecurityContext(), + catalogSchemaName.getCatalogName(), + allFunctionsAuthorization.stream().map(FunctionAuthorization::schemaFunctionName).collect(toImmutableSet())); + + allFunctionsAuthorization.stream() + .filter(functionAuthorization -> filteredFunctions.contains(functionAuthorization.schemaFunctionName())) + .map(functionAuthorization -> new CatalogFunctionAuthorization(catalogSchemaName.getCatalogName(), functionAuthorization)) + .forEach(result::add); + }); + return result.build(); + } + + private record CatalogFunctionAuthorization(String catalog, FunctionAuthorization functionAuthorization) + { + public CatalogFunctionAuthorization + { + requireNonNull(catalog, "catalog is null"); + requireNonNull(functionAuthorization, "functionAuthorization is null"); + } + } +} diff --git a/core/trino-main/src/main/java/io/trino/connector/system/SchemasAuthorization.java b/core/trino-main/src/main/java/io/trino/connector/system/SchemasAuthorization.java new file mode 100644 index 000000000000..dd7e1404107c --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/connector/system/SchemasAuthorization.java @@ -0,0 +1,148 @@ +/* + * 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.connector.system; + +import com.google.common.collect.ImmutableList; +import com.google.inject.Inject; +import io.trino.FullConnectorSession; +import io.trino.Session; +import io.trino.metadata.Metadata; +import io.trino.metadata.QualifiedSchemaPrefix; +import io.trino.security.AccessControl; +import io.trino.spi.TrinoException; +import io.trino.spi.connector.CatalogSchemaName; +import io.trino.spi.connector.ConnectorSession; +import io.trino.spi.connector.ConnectorTableMetadata; +import io.trino.spi.connector.ConnectorTransactionHandle; +import io.trino.spi.connector.InMemoryRecordSet; +import io.trino.spi.connector.RecordCursor; +import io.trino.spi.connector.SchemaTableName; +import io.trino.spi.connector.SystemTable; +import io.trino.spi.predicate.Domain; +import io.trino.spi.predicate.TupleDomain; +import io.trino.spi.security.SchemaAuthorization; +import io.trino.spi.security.TrinoPrincipal; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import static io.trino.connector.system.jdbc.FilterUtil.tryGetSingleVarcharValue; +import static io.trino.metadata.MetadataListing.listAllAvailableSchemas; +import static io.trino.metadata.MetadataUtil.TableMetadataBuilder.tableMetadataBuilder; +import static io.trino.spi.StandardErrorCode.GENERIC_INTERNAL_ERROR; +import static io.trino.spi.connector.SystemTable.Distribution.SINGLE_COORDINATOR; +import static io.trino.spi.type.VarcharType.VARCHAR; +import static io.trino.spi.type.VarcharType.createUnboundedVarcharType; +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.groupingBy; + +public class SchemasAuthorization + implements SystemTable +{ + public static final SchemaTableName SCHEMAS_AUTHORIZATION_NAME = new SchemaTableName("metadata", "schemas_authorization"); + + public static final ConnectorTableMetadata SCHEMAS_AUTHORIZATION = tableMetadataBuilder(SCHEMAS_AUTHORIZATION_NAME) + .column("catalog", createUnboundedVarcharType()) + .column("schema", createUnboundedVarcharType()) + .column("authorization_type", createUnboundedVarcharType()) + .column("authorization", createUnboundedVarcharType()) + .build(); + + private final Metadata metadata; + private final AccessControl accessControl; + + @Inject + public SchemasAuthorization(Metadata metadata, AccessControl accessControl) + { + this.metadata = requireNonNull(metadata, "metadata is null"); + this.accessControl = requireNonNull(accessControl, "accessControl is null"); + } + + @Override + public Distribution getDistribution() + { + return SINGLE_COORDINATOR; + } + + @Override + public ConnectorTableMetadata getTableMetadata() + { + return SCHEMAS_AUTHORIZATION; + } + + @Override + public RecordCursor cursor(ConnectorTransactionHandle transactionHandle, ConnectorSession connectorSession, TupleDomain constraint) + { + Session session = ((FullConnectorSession) connectorSession).getSession(); + InMemoryRecordSet.Builder table = InMemoryRecordSet.builder(SCHEMAS_AUTHORIZATION); + for (CatalogSchemaAuthorization catalogSchemaAuthorization : getSchemasAuthorization(session, constraint)) { + TrinoPrincipal trinoPrincipal = catalogSchemaAuthorization.schemaAuthorization().trinoPrincipal(); + table.addRow( + catalogSchemaAuthorization.catalog(), + catalogSchemaAuthorization.schemaAuthorization().schemaName(), + trinoPrincipal.getType().toString(), + trinoPrincipal.getName()); + } + return table.build().cursor(); + } + + private List getSchemasAuthorization(Session session, TupleDomain constraint) + { + try { + return doGetSchemasAuthorization(session, constraint); + } + catch (RuntimeException exception) { + throw new TrinoException( + GENERIC_INTERNAL_ERROR, + "Error access schemas_authorizations metadata table", + exception); + } + } + + private List doGetSchemasAuthorization(Session session, TupleDomain constraint) + { + Domain catalogDomain = constraint.getDomain(0, VARCHAR); + Domain schemaDomain = constraint.getDomain(1, VARCHAR); + Set availableSchemas = listAllAvailableSchemas( + session, + metadata, + accessControl, + catalogDomain, + schemaDomain); + Optional schemaName = tryGetSingleVarcharValue(schemaDomain); + Map> groupedByCatalog = availableSchemas.stream() + .collect(groupingBy(CatalogSchemaName::getCatalogName, Collectors.toSet())); + ImmutableList.Builder result = ImmutableList.builder(); + for (String catalog : groupedByCatalog.keySet()) { + Set allSchemasAuthorization = metadata.getSchemasAuthorizationInfo(session, new QualifiedSchemaPrefix(catalog, schemaName)); + allSchemasAuthorization.stream() + .filter(schemaAuthorization -> groupedByCatalog.get(catalog).contains(new CatalogSchemaName(catalog, schemaAuthorization.schemaName()))) + .map(schemaAuthorization -> new CatalogSchemaAuthorization(catalog, schemaAuthorization)) + .forEach(result::add); + } + return result.build(); + } + + private record CatalogSchemaAuthorization(String catalog, SchemaAuthorization schemaAuthorization) + { + public CatalogSchemaAuthorization + { + requireNonNull(catalog, "catalog is null"); + requireNonNull(schemaAuthorization, "schemaAuthorization is null"); + } + } +} diff --git a/core/trino-main/src/main/java/io/trino/connector/system/SystemConnectorModule.java b/core/trino-main/src/main/java/io/trino/connector/system/SystemConnectorModule.java index 7d6bb634309b..1b3782c16c36 100644 --- a/core/trino-main/src/main/java/io/trino/connector/system/SystemConnectorModule.java +++ b/core/trino-main/src/main/java/io/trino/connector/system/SystemConnectorModule.java @@ -47,6 +47,9 @@ public void configure(Binder binder) globalTableBinder.addBinding().to(QuerySystemTable.class).in(Scopes.SINGLETON); globalTableBinder.addBinding().to(TaskSystemTable.class).in(Scopes.SINGLETON); globalTableBinder.addBinding().to(CatalogSystemTable.class).in(Scopes.SINGLETON); + globalTableBinder.addBinding().to(SchemasAuthorization.class).in(Scopes.SINGLETON); + globalTableBinder.addBinding().to(TablesAuthorization.class).in(Scopes.SINGLETON); + globalTableBinder.addBinding().to(FunctionsAuthorization.class).in(Scopes.SINGLETON); globalTableBinder.addBinding().to(TableCommentSystemTable.class).in(Scopes.SINGLETON); globalTableBinder.addBinding().to(SchemaPropertiesSystemTable.class).in(Scopes.SINGLETON); globalTableBinder.addBinding().to(TablePropertiesSystemTable.class).in(Scopes.SINGLETON); diff --git a/core/trino-main/src/main/java/io/trino/connector/system/TablesAuthorization.java b/core/trino-main/src/main/java/io/trino/connector/system/TablesAuthorization.java new file mode 100644 index 000000000000..5e77a422402e --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/connector/system/TablesAuthorization.java @@ -0,0 +1,147 @@ +/* + * 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.connector.system; + +import com.google.common.collect.ImmutableList; +import com.google.inject.Inject; +import io.trino.FullConnectorSession; +import io.trino.Session; +import io.trino.metadata.Metadata; +import io.trino.metadata.QualifiedTablePrefix; +import io.trino.security.AccessControl; +import io.trino.spi.TrinoException; +import io.trino.spi.connector.CatalogSchemaName; +import io.trino.spi.connector.ConnectorSession; +import io.trino.spi.connector.ConnectorTableMetadata; +import io.trino.spi.connector.ConnectorTransactionHandle; +import io.trino.spi.connector.InMemoryRecordSet; +import io.trino.spi.connector.RecordCursor; +import io.trino.spi.connector.SchemaTableName; +import io.trino.spi.connector.SystemTable; +import io.trino.spi.predicate.TupleDomain; +import io.trino.spi.security.TableAuthorization; +import io.trino.spi.security.TrinoPrincipal; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static io.trino.connector.system.jdbc.FilterUtil.tryGetSingleVarcharValue; +import static io.trino.metadata.MetadataListing.listAllAvailableSchemas; +import static io.trino.metadata.MetadataListing.listTables; +import static io.trino.metadata.MetadataUtil.TableMetadataBuilder.tableMetadataBuilder; +import static io.trino.spi.StandardErrorCode.GENERIC_INTERNAL_ERROR; +import static io.trino.spi.connector.SystemTable.Distribution.SINGLE_COORDINATOR; +import static io.trino.spi.type.VarcharType.VARCHAR; +import static io.trino.spi.type.VarcharType.createUnboundedVarcharType; +import static java.util.Objects.requireNonNull; + +public class TablesAuthorization + implements SystemTable +{ + public static final SchemaTableName TABLES_AUTHORIZATION_NAME = new SchemaTableName("metadata", "tables_authorization"); + + public static final ConnectorTableMetadata TABLES_AUTHORIZATION = tableMetadataBuilder(TABLES_AUTHORIZATION_NAME) + .column("catalog", createUnboundedVarcharType()) + .column("schema", createUnboundedVarcharType()) + .column("name", createUnboundedVarcharType()) + .column("authorization_type", createUnboundedVarcharType()) + .column("authorization", createUnboundedVarcharType()) + .build(); + + private final Metadata metadata; + private final AccessControl accessControl; + + @Inject + public TablesAuthorization(Metadata metadata, AccessControl accessControl) + { + this.metadata = requireNonNull(metadata, "metadata is null"); + this.accessControl = requireNonNull(accessControl, "accessControl is null"); + } + + @Override + public Distribution getDistribution() + { + return SINGLE_COORDINATOR; + } + + @Override + public ConnectorTableMetadata getTableMetadata() + { + return TABLES_AUTHORIZATION; + } + + @Override + public RecordCursor cursor(ConnectorTransactionHandle transactionHandle, ConnectorSession connectorSession, TupleDomain constraint) + { + Session session = ((FullConnectorSession) connectorSession).getSession(); + InMemoryRecordSet.Builder table = InMemoryRecordSet.builder(TABLES_AUTHORIZATION); + for (CatalogTableAuthorization catalogTableAuthorization : getTablesAuthorization(session, constraint)) { + SchemaTableName schemaTableName = catalogTableAuthorization.tableAuthorization().schemaTableName(); + TrinoPrincipal trinoPrincipal = catalogTableAuthorization.tableAuthorization.trinoPrincipal(); + table.addRow( + catalogTableAuthorization.catalog(), + schemaTableName.getSchemaName(), + schemaTableName.getTableName(), + trinoPrincipal.getType().toString(), + trinoPrincipal.getName()); + } + return table.build().cursor(); + } + + private List getTablesAuthorization(Session session, TupleDomain constraint) + { + try { + return doGetTablesAuthorization(session, constraint); + } + catch (RuntimeException exception) { + throw new TrinoException( + GENERIC_INTERNAL_ERROR, + "Error access tables_authorization metadata table", + exception); + } + } + + private List doGetTablesAuthorization( + Session session, + TupleDomain constraint) + { + Set availableSchemas = listAllAvailableSchemas(session, + metadata, + accessControl, + constraint.getDomain(0, VARCHAR), + constraint.getDomain(1, VARCHAR)); + Optional tableName = tryGetSingleVarcharValue(constraint.getDomain(2, VARCHAR)); + ImmutableList.Builder result = ImmutableList.builder(); + availableSchemas.forEach(catalogSchemaName -> { + QualifiedTablePrefix prefix = new QualifiedTablePrefix(catalogSchemaName.getCatalogName(), Optional.of(catalogSchemaName.getSchemaName()), tableName); + Set accessibleNames = listTables(session, metadata, accessControl, prefix); + Set allTablesAuthorization = metadata.getTablesAuthorizationInfo(session, prefix); + allTablesAuthorization.stream() + .filter(tableAuthorization -> accessibleNames.contains(tableAuthorization.schemaTableName())) + .map(tableAuthorization -> new CatalogTableAuthorization(catalogSchemaName.getCatalogName(), tableAuthorization)) + .forEach(result::add); + }); + return result.build(); + } + + private record CatalogTableAuthorization(String catalog, TableAuthorization tableAuthorization) + { + public CatalogTableAuthorization + { + requireNonNull(catalog, "catalog is null"); + requireNonNull(tableAuthorization, "tableAuthorization is null"); + } + } +} diff --git a/core/trino-main/src/main/java/io/trino/metadata/DisabledSystemSecurityMetadata.java b/core/trino-main/src/main/java/io/trino/metadata/DisabledSystemSecurityMetadata.java index 9977dcb744df..47b04e665dc3 100644 --- a/core/trino-main/src/main/java/io/trino/metadata/DisabledSystemSecurityMetadata.java +++ b/core/trino-main/src/main/java/io/trino/metadata/DisabledSystemSecurityMetadata.java @@ -21,10 +21,13 @@ import io.trino.spi.connector.EntityKindAndName; import io.trino.spi.connector.EntityPrivilege; import io.trino.spi.function.CatalogSchemaFunctionName; +import io.trino.spi.security.FunctionAuthorization; import io.trino.spi.security.GrantInfo; import io.trino.spi.security.Identity; import io.trino.spi.security.Privilege; import io.trino.spi.security.RoleGrant; +import io.trino.spi.security.SchemaAuthorization; +import io.trino.spi.security.TableAuthorization; import io.trino.spi.security.TrinoPrincipal; import java.util.List; @@ -222,6 +225,24 @@ public void setEntityOwner(Session session, EntityKindAndName entityKindAndName, throw notSupportedException(entityKindAndName.name().get(0)); } + @Override + public Set getSchemasAuthorizationInfo(Session session, QualifiedSchemaPrefix prefix) + { + return Set.of(); + } + + @Override + public Set getTablesAuthorizationInfo(Session session, QualifiedTablePrefix prefix) + { + return Set.of(); + } + + @Override + public Set getFunctionsAuthorizationInfo(Session session, QualifiedObjectPrefix prefix) + { + return Set.of(); + } + private static TrinoException notSupportedException(String catalogName) { return new TrinoException(NOT_SUPPORTED, "Catalog does not support permission management: " + catalogName); diff --git a/core/trino-main/src/main/java/io/trino/metadata/Metadata.java b/core/trino-main/src/main/java/io/trino/metadata/Metadata.java index f62f971add03..0d0662bb12a6 100644 --- a/core/trino-main/src/main/java/io/trino/metadata/Metadata.java +++ b/core/trino-main/src/main/java/io/trino/metadata/Metadata.java @@ -66,10 +66,13 @@ import io.trino.spi.function.LanguageFunction; import io.trino.spi.function.OperatorType; import io.trino.spi.predicate.TupleDomain; +import io.trino.spi.security.FunctionAuthorization; import io.trino.spi.security.GrantInfo; import io.trino.spi.security.Identity; import io.trino.spi.security.Privilege; import io.trino.spi.security.RoleGrant; +import io.trino.spi.security.SchemaAuthorization; +import io.trino.spi.security.TableAuthorization; import io.trino.spi.security.TrinoPrincipal; import io.trino.spi.statistics.ComputedStatistics; import io.trino.spi.statistics.TableStatistics; @@ -853,4 +856,19 @@ default boolean isMaterializedView(Session session, QualifiedObjectName viewName WriterScalingOptions getInsertWriterScalingOptions(Session session, TableHandle tableHandle); void setEntityAuthorization(Session session, EntityKindAndName entityKindAndName, TrinoPrincipal principal); + + /** + * Returns list of schemas authorization info + */ + Set getSchemasAuthorizationInfo(Session session, QualifiedSchemaPrefix prefix); + + /** + * Returns list of tables authorization info + */ + Set getTablesAuthorizationInfo(Session session, QualifiedTablePrefix prefix); + + /** + * Returns list of functions authorization info + */ + Set getFunctionsAuthorizationInfo(Session session, QualifiedObjectPrefix prefix); } diff --git a/core/trino-main/src/main/java/io/trino/metadata/MetadataListing.java b/core/trino-main/src/main/java/io/trino/metadata/MetadataListing.java index c0a93f356601..0041c839c2ec 100644 --- a/core/trino-main/src/main/java/io/trino/metadata/MetadataListing.java +++ b/core/trino-main/src/main/java/io/trino/metadata/MetadataListing.java @@ -21,6 +21,7 @@ import io.trino.spi.ErrorCodeSupplier; import io.trino.spi.TrinoException; import io.trino.spi.connector.CatalogHandle; +import io.trino.spi.connector.CatalogSchemaName; import io.trino.spi.connector.ColumnMetadata; import io.trino.spi.connector.RelationType; import io.trino.spi.connector.SchemaTableName; @@ -85,6 +86,22 @@ public static List listCatalogs(Session session, Metadata metadata, .collect(toImmutableList()); } + public static Set listAllAvailableSchemas( + Session session, + Metadata metadata, + AccessControl accessControl, + Domain catalogDomain, + Domain schemaDomain) + { + Set catalogNames = listCatalogNames(session, metadata, accessControl, catalogDomain); + Optional schemaName = tryGetSingleVarcharValue(schemaDomain); + return catalogNames.stream() + .flatMap(catalogName -> + listSchemas(session, metadata, accessControl, catalogName, schemaName).stream() + .map(name -> new CatalogSchemaName(catalogName, name))) + .collect(toImmutableSet()); + } + public static SortedSet listSchemas(Session session, Metadata metadata, AccessControl accessControl, String catalogName) { return listSchemas(session, metadata, accessControl, catalogName, Optional.empty()); @@ -347,7 +364,7 @@ private static Map> doListTableColumns(Ses return result.buildOrThrow(); } - private static TrinoException handleListingException(RuntimeException exception, String type, String catalogName) + public static TrinoException handleListingException(RuntimeException exception, String type, String catalogName) { ErrorCodeSupplier result = GENERIC_INTERNAL_ERROR; if (exception instanceof TrinoException trinoException) { diff --git a/core/trino-main/src/main/java/io/trino/metadata/MetadataManager.java b/core/trino-main/src/main/java/io/trino/metadata/MetadataManager.java index 124b54486e8c..e0df627ca5ed 100644 --- a/core/trino-main/src/main/java/io/trino/metadata/MetadataManager.java +++ b/core/trino-main/src/main/java/io/trino/metadata/MetadataManager.java @@ -103,10 +103,13 @@ import io.trino.spi.function.SchemaFunctionName; import io.trino.spi.function.Signature; import io.trino.spi.predicate.TupleDomain; +import io.trino.spi.security.FunctionAuthorization; import io.trino.spi.security.GrantInfo; import io.trino.spi.security.Identity; import io.trino.spi.security.Privilege; import io.trino.spi.security.RoleGrant; +import io.trino.spi.security.SchemaAuthorization; +import io.trino.spi.security.TableAuthorization; import io.trino.spi.security.TrinoPrincipal; import io.trino.spi.statistics.ComputedStatistics; import io.trino.spi.statistics.TableStatistics; @@ -2864,4 +2867,40 @@ private static boolean cannotExist(QualifiedObjectName name) { return name.catalogName().isEmpty() || name.schemaName().isEmpty() || name.objectName().isEmpty(); } + + @Override + public Set getSchemasAuthorizationInfo(Session session, QualifiedSchemaPrefix prefix) + { + requireNonNull(prefix, "prefix is null"); + + Optional catalog = getOptionalCatalogMetadata(session, prefix.catalogName()); + if (catalog.map(CatalogMetadata::getSecurityManagement).filter(SYSTEM::equals).isPresent()) { + return systemSecurityMetadata.getSchemasAuthorizationInfo(session, prefix); + } + return ImmutableSet.of(); + } + + @Override + public Set getTablesAuthorizationInfo(Session session, QualifiedTablePrefix prefix) + { + requireNonNull(prefix, "prefix is null"); + + Optional catalog = getOptionalCatalogMetadata(session, prefix.getCatalogName()); + if (catalog.map(CatalogMetadata::getSecurityManagement).filter(SYSTEM::equals).isPresent()) { + return systemSecurityMetadata.getTablesAuthorizationInfo(session, prefix); + } + return ImmutableSet.of(); + } + + @Override + public Set getFunctionsAuthorizationInfo(Session session, QualifiedObjectPrefix prefix) + { + requireNonNull(prefix, "prefix is null"); + + Optional catalog = getOptionalCatalogMetadata(session, prefix.catalogName()); + if (catalog.map(CatalogMetadata::getSecurityManagement).filter(SYSTEM::equals).isPresent()) { + return systemSecurityMetadata.getFunctionsAuthorizationInfo(session, prefix); + } + return ImmutableSet.of(); + } } diff --git a/core/trino-main/src/main/java/io/trino/metadata/QualifiedObjectPrefix.java b/core/trino-main/src/main/java/io/trino/metadata/QualifiedObjectPrefix.java new file mode 100644 index 000000000000..43a9572cb479 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/metadata/QualifiedObjectPrefix.java @@ -0,0 +1,46 @@ +/* + * 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.metadata; + +import java.util.Objects; +import java.util.Optional; + +import static io.trino.metadata.MetadataUtil.checkCatalogName; +import static java.util.Objects.requireNonNull; + +public record QualifiedObjectPrefix( + String catalogName, + Optional schemaName, + Optional objectName) +{ + public QualifiedObjectPrefix + { + checkCatalogName(catalogName); + requireNonNull(schemaName, "schemaName is null"); + requireNonNull(objectName, "objectName is null"); + } + + public boolean matches(QualifiedObjectName objectName) + { + return Objects.equals(catalogName, objectName.catalogName()) + && schemaName.map(schema -> Objects.equals(schema, objectName.schemaName())).orElse(true) + && this.objectName.map(table -> Objects.equals(table, objectName.objectName())).orElse(true); + } + + @Override + public String toString() + { + return catalogName + '.' + schemaName.orElse("*") + '.' + objectName.orElse("*"); + } +} diff --git a/core/trino-main/src/main/java/io/trino/metadata/QualifiedSchemaPrefix.java b/core/trino-main/src/main/java/io/trino/metadata/QualifiedSchemaPrefix.java new file mode 100644 index 000000000000..44429cc3c925 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/metadata/QualifiedSchemaPrefix.java @@ -0,0 +1,45 @@ +/* + * 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.metadata; + +import io.trino.spi.connector.CatalogSchemaName; + +import java.util.Objects; +import java.util.Optional; + +import static io.trino.metadata.MetadataUtil.checkCatalogName; +import static java.util.Objects.requireNonNull; + +public record QualifiedSchemaPrefix( + String catalogName, + Optional schemaName) +{ + public QualifiedSchemaPrefix + { + checkCatalogName(catalogName); + requireNonNull(schemaName, "schemaName is null"); + } + + public boolean matches(CatalogSchemaName catalogSchemaName) + { + return Objects.equals(catalogName, catalogSchemaName.getCatalogName()) + && schemaName.map(schema -> Objects.equals(schema, catalogSchemaName.getSchemaName())).orElse(true); + } + + @Override + public String toString() + { + return catalogName + '.' + schemaName.orElse("*"); + } +} diff --git a/core/trino-main/src/main/java/io/trino/metadata/SystemSecurityMetadata.java b/core/trino-main/src/main/java/io/trino/metadata/SystemSecurityMetadata.java index a5c54a43617c..d3f93a92fdfa 100644 --- a/core/trino-main/src/main/java/io/trino/metadata/SystemSecurityMetadata.java +++ b/core/trino-main/src/main/java/io/trino/metadata/SystemSecurityMetadata.java @@ -19,10 +19,13 @@ import io.trino.spi.connector.EntityKindAndName; import io.trino.spi.connector.EntityPrivilege; import io.trino.spi.function.CatalogSchemaFunctionName; +import io.trino.spi.security.FunctionAuthorization; import io.trino.spi.security.GrantInfo; import io.trino.spi.security.Identity; import io.trino.spi.security.Privilege; import io.trino.spi.security.RoleGrant; +import io.trino.spi.security.SchemaAuthorization; +import io.trino.spi.security.TableAuthorization; import io.trino.spi.security.TrinoPrincipal; import java.util.Optional; @@ -240,4 +243,10 @@ default void validateEntityKindAndPrivileges(Session session, String entityKind, * to be fully qualified, i.e., if the entity is a table, the name is of size three. */ void setEntityOwner(Session session, EntityKindAndName entityKindAndName, TrinoPrincipal principal); + + Set getSchemasAuthorizationInfo(Session session, QualifiedSchemaPrefix prefix); + + Set getTablesAuthorizationInfo(Session session, QualifiedTablePrefix prefix); + + Set getFunctionsAuthorizationInfo(Session session, QualifiedObjectPrefix prefix); } diff --git a/core/trino-main/src/main/java/io/trino/tracing/TracingMetadata.java b/core/trino-main/src/main/java/io/trino/tracing/TracingMetadata.java index 2049cc5d5d01..74fde7d63218 100644 --- a/core/trino-main/src/main/java/io/trino/tracing/TracingMetadata.java +++ b/core/trino-main/src/main/java/io/trino/tracing/TracingMetadata.java @@ -32,6 +32,8 @@ import io.trino.metadata.OperatorNotFoundException; import io.trino.metadata.OutputTableHandle; import io.trino.metadata.QualifiedObjectName; +import io.trino.metadata.QualifiedObjectPrefix; +import io.trino.metadata.QualifiedSchemaPrefix; import io.trino.metadata.QualifiedTablePrefix; import io.trino.metadata.RedirectionAwareTableHandle; import io.trino.metadata.ResolvedFunction; @@ -95,10 +97,13 @@ import io.trino.spi.function.LanguageFunction; import io.trino.spi.function.OperatorType; import io.trino.spi.predicate.TupleDomain; +import io.trino.spi.security.FunctionAuthorization; import io.trino.spi.security.GrantInfo; import io.trino.spi.security.Identity; import io.trino.spi.security.Privilege; import io.trino.spi.security.RoleGrant; +import io.trino.spi.security.SchemaAuthorization; +import io.trino.spi.security.TableAuthorization; import io.trino.spi.security.TrinoPrincipal; import io.trino.spi.statistics.ComputedStatistics; import io.trino.spi.statistics.TableStatistics; @@ -1533,6 +1538,33 @@ public void setEntityAuthorization(Session session, EntityKindAndName entityKind } } + @Override + public Set getSchemasAuthorizationInfo(Session session, QualifiedSchemaPrefix prefix) + { + Span span = startSpan("getSchemasAuthorizationInfo", prefix); + try (var ignored = scopedSpan(span)) { + return delegate.getSchemasAuthorizationInfo(session, prefix); + } + } + + @Override + public Set getTablesAuthorizationInfo(Session session, QualifiedTablePrefix prefix) + { + Span span = startSpan("getTablesAuthorizationInfo", prefix); + try (var ignored = scopedSpan(span)) { + return delegate.getTablesAuthorizationInfo(session, prefix); + } + } + + @Override + public Set getFunctionsAuthorizationInfo(Session session, QualifiedObjectPrefix prefix) + { + Span span = startSpan("getFunctionsAuthorizationInfo", prefix); + try (var ignored = scopedSpan(span)) { + return delegate.getFunctionsAuthorizationInfo(session, prefix); + } + } + private Span startSpan(String methodName) { return tracer.spanBuilder("Metadata." + methodName) @@ -1586,6 +1618,21 @@ private Span startSpan(String methodName, QualifiedTablePrefix prefix) .setAttribute(TrinoAttributes.TABLE, prefix.getTableName().orElse(null)); } + private Span startSpan(String methodName, QualifiedObjectPrefix prefix) + { + return startSpan(methodName) + .setAttribute(TrinoAttributes.CATALOG, prefix.catalogName()) + .setAttribute(TrinoAttributes.SCHEMA, prefix.schemaName().orElse(null)) + .setAttribute(TrinoAttributes.ENTITY, prefix.objectName().orElse(null)); + } + + private Span startSpan(String methodName, QualifiedSchemaPrefix prefix) + { + return startSpan(methodName) + .setAttribute(TrinoAttributes.CATALOG, prefix.catalogName()) + .setAttribute(TrinoAttributes.SCHEMA, prefix.schemaName().orElse(null)); + } + private Span startSpan(String methodName, String catalogName, ConnectorTableMetadata tableMetadata) { return startSpan(methodName) diff --git a/core/trino-main/src/test/java/io/trino/metadata/AbstractMockMetadata.java b/core/trino-main/src/test/java/io/trino/metadata/AbstractMockMetadata.java index f73b20013274..f1956d9c69e9 100644 --- a/core/trino-main/src/test/java/io/trino/metadata/AbstractMockMetadata.java +++ b/core/trino-main/src/test/java/io/trino/metadata/AbstractMockMetadata.java @@ -72,10 +72,13 @@ import io.trino.spi.function.OperatorType; import io.trino.spi.function.Signature; import io.trino.spi.predicate.TupleDomain; +import io.trino.spi.security.FunctionAuthorization; import io.trino.spi.security.GrantInfo; import io.trino.spi.security.Identity; import io.trino.spi.security.Privilege; import io.trino.spi.security.RoleGrant; +import io.trino.spi.security.SchemaAuthorization; +import io.trino.spi.security.TableAuthorization; import io.trino.spi.security.TrinoPrincipal; import io.trino.spi.statistics.ComputedStatistics; import io.trino.spi.statistics.TableStatistics; @@ -1027,4 +1030,22 @@ public void setEntityAuthorization(Session session, EntityKindAndName entityKind { throw new UnsupportedOperationException(); } + + @Override + public Set getSchemasAuthorizationInfo(Session session, QualifiedSchemaPrefix prefix) + { + throw new UnsupportedOperationException(); + } + + @Override + public Set getTablesAuthorizationInfo(Session session, QualifiedTablePrefix prefix) + { + throw new UnsupportedOperationException(); + } + + @Override + public Set getFunctionsAuthorizationInfo(Session session, QualifiedObjectPrefix prefix) + { + throw new UnsupportedOperationException(); + } } diff --git a/core/trino-spi/src/main/java/io/trino/spi/security/FunctionAuthorization.java b/core/trino-spi/src/main/java/io/trino/spi/security/FunctionAuthorization.java new file mode 100644 index 000000000000..d7f590086b0f --- /dev/null +++ b/core/trino-spi/src/main/java/io/trino/spi/security/FunctionAuthorization.java @@ -0,0 +1,27 @@ +/* + * 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.spi.security; + +import io.trino.spi.function.SchemaFunctionName; + +import static java.util.Objects.requireNonNull; + +public record FunctionAuthorization(SchemaFunctionName schemaFunctionName, TrinoPrincipal trinoPrincipal) +{ + public FunctionAuthorization + { + requireNonNull(schemaFunctionName, "schemaFunctionName is null"); + requireNonNull(trinoPrincipal, "trinoPrincipal is null"); + } +} diff --git a/core/trino-spi/src/main/java/io/trino/spi/security/SchemaAuthorization.java b/core/trino-spi/src/main/java/io/trino/spi/security/SchemaAuthorization.java new file mode 100644 index 000000000000..f428296d023d --- /dev/null +++ b/core/trino-spi/src/main/java/io/trino/spi/security/SchemaAuthorization.java @@ -0,0 +1,25 @@ +/* + * 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.spi.security; + +import static java.util.Objects.requireNonNull; + +public record SchemaAuthorization(String schemaName, TrinoPrincipal trinoPrincipal) +{ + public SchemaAuthorization + { + requireNonNull(schemaName, "schemaName is null"); + requireNonNull(trinoPrincipal, "trinoPrincipal is null"); + } +} diff --git a/core/trino-spi/src/main/java/io/trino/spi/security/TableAuthorization.java b/core/trino-spi/src/main/java/io/trino/spi/security/TableAuthorization.java new file mode 100644 index 000000000000..9bddaeea0732 --- /dev/null +++ b/core/trino-spi/src/main/java/io/trino/spi/security/TableAuthorization.java @@ -0,0 +1,27 @@ +/* + * 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.spi.security; + +import io.trino.spi.connector.SchemaTableName; + +import static java.util.Objects.requireNonNull; + +public record TableAuthorization(SchemaTableName schemaTableName, TrinoPrincipal trinoPrincipal) +{ + public TableAuthorization + { + requireNonNull(schemaTableName, "schemaTableName is null"); + requireNonNull(trinoPrincipal, "trinoPrincipal is null"); + } +} diff --git a/testing/trino-product-tests/src/main/resources/sql-tests/testcases/system/selectInformationSchemaColumns.result b/testing/trino-product-tests/src/main/resources/sql-tests/testcases/system/selectInformationSchemaColumns.result index 053085869c00..1a1abf3759aa 100644 --- a/testing/trino-product-tests/src/main/resources/sql-tests/testcases/system/selectInformationSchemaColumns.result +++ b/testing/trino-product-tests/src/main/resources/sql-tests/testcases/system/selectInformationSchemaColumns.result @@ -46,6 +46,11 @@ system| metadata| column_properties| property_name| varchar| YES| null| null| system| metadata| column_properties| default_value| varchar| YES| null| null| system| metadata| column_properties| type| varchar| YES| null| null| system| metadata| column_properties| description| varchar| YES| null| null| +system| metadata| functions_authorization| catalog| varchar| YES| null| null| +system| metadata| functions_authorization| schema| varchar| YES| null| null| +system| metadata| functions_authorization| name| varchar| YES| null| null| +system| metadata| functions_authorization| authorization_type| varchar| YES| null| null| +system| metadata| functions_authorization| authorization| varchar| YES| null| null| system| metadata| materialized_view_properties| catalog_name| varchar| YES| null| null| system| metadata| materialized_view_properties| property_name| varchar| YES| null| null| system| metadata| materialized_view_properties| default_value| varchar| YES| null| null| @@ -66,6 +71,10 @@ system| metadata| schema_properties| property_name| varchar| YES| null| null| system| metadata| schema_properties| default_value| varchar| YES| null| null| system| metadata| schema_properties| type| varchar| YES| null| null| system| metadata| schema_properties| description| varchar| YES| null| null| +system| metadata| schemas_authorization| catalog| varchar| YES| null| null| +system| metadata| schemas_authorization| schema| varchar| YES| null| null| +system| metadata| schemas_authorization| authorization_type| varchar| YES| null| null| +system| metadata| schemas_authorization| authorization| varchar| YES| null| null| system| metadata| table_comments| catalog_name| varchar| YES| null| null| system| metadata| table_comments| schema_name| varchar| YES| null| null| system| metadata| table_comments| table_name| varchar| YES| null| null| @@ -75,6 +84,11 @@ system| metadata| table_properties| property_name| varchar| YES| null| null| system| metadata| table_properties| default_value| varchar| YES| null| null| system| metadata| table_properties| type| varchar| YES| null| null| system| metadata| table_properties| description| varchar| YES| null| null| +system| metadata| tables_authorization| catalog| varchar| YES| null| null| +system| metadata| tables_authorization| schema| varchar| YES| null| null| +system| metadata| tables_authorization| name| varchar| YES| null| null| +system| metadata| tables_authorization| authorization_type| varchar| YES| null| null| +system| metadata| tables_authorization| authorization| varchar| YES| null| null| system| runtime| nodes| node_id| varchar| YES| null| null| system| runtime| nodes| http_uri| varchar| YES| null| null| system| runtime| nodes| node_version| varchar| YES| null| null| diff --git a/testing/trino-product-tests/src/main/resources/sql-tests/testcases/system/showTablesSystemMetadata.result b/testing/trino-product-tests/src/main/resources/sql-tests/testcases/system/showTablesSystemMetadata.result index 01723cb1db9e..4679793e471f 100644 --- a/testing/trino-product-tests/src/main/resources/sql-tests/testcases/system/showTablesSystemMetadata.result +++ b/testing/trino-product-tests/src/main/resources/sql-tests/testcases/system/showTablesSystemMetadata.result @@ -5,5 +5,8 @@ schema_properties| table_properties| materialized_view_properties| materialized_views| +schemas_authorization| +tables_authorization| +functions_authorization| column_properties| table_comments| diff --git a/testing/trino-tests/src/test/java/io/trino/security/TestAccessControl.java b/testing/trino-tests/src/test/java/io/trino/security/TestAccessControl.java index f965d9d6f55f..87d26bee3d9a 100644 --- a/testing/trino-tests/src/test/java/io/trino/security/TestAccessControl.java +++ b/testing/trino-tests/src/test/java/io/trino/security/TestAccessControl.java @@ -79,6 +79,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; +import static com.google.common.collect.ImmutableSet.toImmutableSet; import static com.google.inject.multibindings.OptionalBinder.newOptionalBinder; import static io.trino.SystemSessionProperties.QUERY_MAX_MEMORY; import static io.trino.spi.security.PrincipalType.USER; @@ -166,6 +167,7 @@ protected SystemAccessControl delegate() queryRunner.createCatalog("blackhole", "blackhole"); queryRunner.installPlugin(new MemoryPlugin()); queryRunner.createCatalog("memory", "memory", Map.of()); + queryRunner.createCatalog("memory_test", "memory", Map.of()); queryRunner.installPlugin(new TpchPlugin()); queryRunner.createCatalog("tpch", "tpch"); queryRunner.installPlugin(new MockConnectorPlugin(MockConnectorFactory.builder() @@ -507,6 +509,9 @@ public void testViewOwnersRoleGrants() systemSecurityMetadata.revokeRoles(getSession(), Set.of("view_owner_role_without_access"), Set.of(viewOwnerPrincipal), false, Optional.empty()); getQueryRunner().execute(viewOwnerSession, "SELECT * FROM " + viewName); + assertQuery( + "SELECT * FROM system.metadata.tables_authorization", + "VALUES('blackhole', 'default', '%s', 'USER', '%s')".formatted(viewName, viewOwner)); assertAccessAllowed(viewOwnerSession, "DROP VIEW " + viewName); } @@ -1479,6 +1484,191 @@ public void testAccessControlWithRolesAndRowFilter() assertAccessAllowed(session, "SELECT nationkey FROM nation"); } + + @Test + public void testSchemasAuthorization() + { + reset(); + systemAccessControl.set(new DenyEntitiesAccessSystemAccessControl()); + + String schema1 = "schema_" + randomNameSuffix(); + String schema2 = "schema_2_" + randomNameSuffix(); + String deniedSchema = "deny_schema_" + randomNameSuffix(); + String schemaOwnerName1 = "schema_owner_1_" + randomNameSuffix(); + String schemaOwnerName2 = "schema_owner_2_" + randomNameSuffix(); + + Session schemaOwner1 = TestingSession.testSessionBuilder() + .setIdentity(Identity.ofUser(schemaOwnerName1)) + .setCatalog(getSession().getCatalog()) + .setSchema(getSession().getSchema()) + .build(); + + Session schemaOwner2 = TestingSession.testSessionBuilder() + .setIdentity(Identity.ofUser(schemaOwnerName2)) + .setCatalog(getSession().getCatalog()) + .setSchema(getSession().getSchema()) + .build(); + + getQueryRunner().execute( + schemaOwner1, + "CREATE SCHEMA memory.%s".formatted(schema1)); + assertQuery( + "SELECT * FROM system.metadata.schemas_authorization", + "VALUES('memory', '%s', 'USER', '%s')".formatted(schema1, schemaOwnerName1)); + + getQueryRunner().execute(schemaOwner1, "ALTER SCHEMA memory.%s SET AUTHORIZATION %s".formatted(schema1, schemaOwnerName2)); + assertQuery( + "SELECT * FROM system.metadata.schemas_authorization", + "VALUES('memory', '%s', 'USER', '%s')".formatted(schema1, schemaOwnerName2)); + + getQueryRunner().execute( + schemaOwner1, + "CREATE SCHEMA memory_test.%s".formatted(schema2)); + assertQuery( + "SELECT * FROM system.metadata.schemas_authorization", + "VALUES('memory', '%s', 'USER', '%s'), ('memory_test', '%s', 'USER', '%s')".formatted(schema1, schemaOwnerName2, schema2, schemaOwnerName1)); + assertQuery( + "SELECT * FROM system.metadata.schemas_authorization WHERE catalog = 'memory'", + "VALUES('memory', '%s', 'USER', '%s')".formatted(schema1, schemaOwnerName2)); + assertQuery( + "SELECT * FROM system.metadata.schemas_authorization WHERE catalog = 'memory_test'", + "VALUES('memory_test', '%s', 'USER', '%s')".formatted(schema2, schemaOwnerName1)); + getQueryRunner().execute( + schemaOwner1, + "CREATE SCHEMA memory_test.%s".formatted(deniedSchema)); + assertQuery( + "SELECT * FROM system.metadata.schemas_authorization", + "VALUES('memory', '%s', 'USER', '%s'), ('memory_test', '%s', 'USER', '%s')".formatted(schema1, schemaOwnerName2, schema2, schemaOwnerName1)); + + getQueryRunner().execute(schemaOwner2, "DROP SCHEMA memory.%s".formatted(schema1)); + getQueryRunner().execute(schemaOwner1, "DROP SCHEMA memory_test.%s".formatted(schema2)); + getQueryRunner().execute(schemaOwner1, "DROP SCHEMA memory_test.%s".formatted(deniedSchema)); + assertQueryReturnsEmptyResult("SELECT * FROM system.metadata.schemas_authorization"); + } + + @Test + public void testTablesAuthorization() + { + reset(); + systemAccessControl.set(new DenyEntitiesAccessSystemAccessControl()); + + String table1 = "table_name_" + randomNameSuffix(); + String table2 = "table_name_2_" + randomNameSuffix(); + String deniedTable = "deny_table_name_" + randomNameSuffix(); + String view = "view_name_" + randomNameSuffix(); + String tableOwnerName1 = "table_owner_1_" + randomNameSuffix(); + String tableOwnerName2 = "table_owner_2_" + randomNameSuffix(); + + Session tableOwner1 = TestingSession.testSessionBuilder() + .setIdentity(Identity.ofUser(tableOwnerName1)) + .setCatalog(getSession().getCatalog()) + .setSchema(getSession().getSchema()) + .build(); + + Session tableOwner2 = TestingSession.testSessionBuilder() + .setIdentity(Identity.ofUser(tableOwnerName2)) + .setCatalog(getSession().getCatalog()) + .setSchema(getSession().getSchema()) + .build(); + + getQueryRunner().execute(tableOwner1, "CREATE TABLE memory.default.%s (id INT)".formatted(table1)); + assertQuery( + "SELECT * FROM system.metadata.tables_authorization", + "VALUES('memory', 'default', '%s', 'USER', '%s')".formatted(table1, tableOwnerName1)); + + getQueryRunner().execute(tableOwner1, "ALTER TABLE memory.default.%s SET AUTHORIZATION %s".formatted(table1, tableOwnerName2)); + assertQuery( + "SELECT * FROM system.metadata.tables_authorization", + "VALUES('memory', 'default', '%s', 'USER', '%s')".formatted(table1, tableOwnerName2)); + + getQueryRunner().execute(tableOwner1, "CREATE VIEW memory.default.%s AS SELECT * FROM memory.default.%s".formatted(view, table1)); + assertQuery( + "SELECT * FROM system.metadata.tables_authorization", + "VALUES('memory', 'default', '%s', 'USER', '%s'), ('memory', 'default', '%s', 'USER', '%s')".formatted(table1, tableOwnerName2, view, tableOwnerName1)); + + getQueryRunner().execute(tableOwner1, "CREATE TABLE memory_test.default.%s (id INT)".formatted(table2)); + assertQuery( + "SELECT * FROM system.metadata.tables_authorization WHERE catalog = 'memory'", + "VALUES('memory', 'default', '%s', 'USER', '%s'), ('memory', 'default', '%s', 'USER', '%s')".formatted(table1, tableOwnerName2, view, tableOwnerName1)); + assertQuery( + "SELECT * FROM system.metadata.tables_authorization", + "VALUES('memory', 'default', '%s', 'USER', '%s'), ('memory', 'default', '%s', 'USER', '%s'), ('memory_test', 'default', '%s', 'USER', '%s')".formatted(table1, tableOwnerName2, view, tableOwnerName1, table2, tableOwnerName1)); + assertQuery( + "SELECT * FROM system.metadata.tables_authorization WHERE catalog = 'memory_test'", + "VALUES('memory_test', 'default', '%s', 'USER', '%s')".formatted(table2, tableOwnerName1)); + getQueryRunner().execute(tableOwner1, "CREATE TABLE memory_test.default.%s (id INT)".formatted(deniedTable)); + assertQuery( + "SELECT * FROM system.metadata.tables_authorization", + "VALUES('memory', 'default', '%s', 'USER', '%s'), ('memory', 'default', '%s', 'USER', '%s'), ('memory_test', 'default', '%s', 'USER', '%s')".formatted(table1, tableOwnerName2, view, tableOwnerName1, table2, tableOwnerName1)); + + getQueryRunner().execute(tableOwner2, "DROP TABLE memory.default." + table1); + getQueryRunner().execute(tableOwner1, "DROP TABLE memory_test.default." + table2); + getQueryRunner().execute(tableOwner1, "DROP VIEW memory.default." + view); + getQueryRunner().execute(tableOwner1, "DROP TABLE memory_test.default." + deniedTable); + assertQueryReturnsEmptyResult("SELECT * FROM system.metadata.tables_authorization"); + } + + @Test + public void testFunctionsAuthorization() + { + reset(); + systemAccessControl.set(new DenyEntitiesAccessSystemAccessControl()); + + String function = "function_" + randomNameSuffix(); + String deniedFunction = "deny_function_" + randomNameSuffix(); + String functionOwnerName1 = "function_owner_1_" + randomNameSuffix(); + String functionOwnerName2 = "function_owner_2_" + randomNameSuffix(); + + Session functionOwner1 = TestingSession.testSessionBuilder() + .setIdentity(Identity.ofUser(functionOwnerName1)) + .setCatalog(getSession().getCatalog()) + .setSchema(getSession().getSchema()) + .build(); + + Session functionOwner2 = TestingSession.testSessionBuilder() + .setIdentity(Identity.ofUser(functionOwnerName2)) + .setCatalog(getSession().getCatalog()) + .setSchema(getSession().getSchema()) + .build(); + + getQueryRunner().execute( + functionOwner1, + "CREATE FUNCTION memory.default.%s (x integer) RETURNS bigint RETURN x + 42".formatted(function)); + assertQuery( + "SELECT * FROM system.metadata.functions_authorization", + "VALUES('memory', 'default', '%s', 'USER', '%s')".formatted(function, functionOwnerName1)); + + getQueryRunner().execute(functionOwner1, "ALTER FUNCTION memory.default.%s SET AUTHORIZATION %s".formatted(function, functionOwnerName2)); + assertQuery( + "SELECT * FROM system.metadata.functions_authorization", + "VALUES('memory', 'default', '%s', 'USER', '%s')".formatted(function, functionOwnerName2)); + + getQueryRunner().execute( + functionOwner1, + "CREATE FUNCTION memory_test.default.%s (x integer) RETURNS bigint RETURN x + 42".formatted(function)); + assertQuery( + "SELECT * FROM system.metadata.functions_authorization", + "VALUES('memory', 'default', '%s', 'USER', '%s'), ('memory_test', 'default', '%s', 'USER', '%s')".formatted(function, functionOwnerName2, function, functionOwnerName1)); + assertQuery( + "SELECT * FROM system.metadata.functions_authorization WHERE catalog = 'memory'", + "VALUES('memory', 'default', '%s', 'USER', '%s')".formatted(function, functionOwnerName2)); + assertQuery( + "SELECT * FROM system.metadata.functions_authorization WHERE catalog = 'memory_test'", + "VALUES('memory_test', 'default', '%s', 'USER', '%s')".formatted(function, functionOwnerName1)); + // this won't be visible as it is denied by the access control + getQueryRunner().execute( + functionOwner1, + "CREATE FUNCTION memory_test.default.%s (x integer) RETURNS bigint RETURN x + 42".formatted(deniedFunction)); + assertQuery( + "SELECT * FROM system.metadata.functions_authorization", + "VALUES('memory', 'default', '%s', 'USER', '%s'), ('memory_test', 'default', '%s', 'USER', '%s')".formatted(function, functionOwnerName2, function, functionOwnerName1)); + + getQueryRunner().execute(functionOwner2, "DROP FUNCTION memory.default.%s(integer)".formatted(function)); + getQueryRunner().execute(functionOwner1, "DROP FUNCTION memory_test.default.%s(integer)".formatted(function)); + getQueryRunner().execute(functionOwner1, "DROP FUNCTION memory_test.default.%s(integer)".formatted(deniedFunction)); + assertQueryReturnsEmptyResult("SELECT * FROM system.metadata.functions_authorization"); + } + private static final class DenySetPropertiesSystemAccessControl extends AllowAllSystemAccessControl { @@ -1523,4 +1713,32 @@ private static void checkProperties(Map properties) } } } + + private static final class DenyEntitiesAccessSystemAccessControl + extends AllowAllSystemAccessControl + { + @Override + public Set filterSchemas(SystemSecurityContext context, String catalogName, Set schemaNames) + { + return schemaNames.stream() + .filter(schemaName -> !schemaName.startsWith("deny_")) + .collect(toImmutableSet()); + } + + @Override + public Set filterTables(SystemSecurityContext context, String catalogName, Set tableNames) + { + return tableNames.stream() + .filter(tableName -> !tableName.getTableName().startsWith("deny_")) + .collect(toImmutableSet()); + } + + @Override + public Set filterFunctions(SystemSecurityContext context, String catalogName, Set functionNames) + { + return functionNames.stream() + .filter(functionName -> !functionName.getFunctionName().startsWith("deny_")) + .collect(toImmutableSet()); + } + } } diff --git a/testing/trino-tests/src/test/java/io/trino/security/TestingSystemSecurityMetadata.java b/testing/trino-tests/src/test/java/io/trino/security/TestingSystemSecurityMetadata.java index 179985e419fc..3314b6074500 100644 --- a/testing/trino-tests/src/test/java/io/trino/security/TestingSystemSecurityMetadata.java +++ b/testing/trino-tests/src/test/java/io/trino/security/TestingSystemSecurityMetadata.java @@ -16,16 +16,21 @@ import com.google.common.collect.ImmutableSet; import io.trino.Session; import io.trino.metadata.QualifiedObjectName; +import io.trino.metadata.QualifiedObjectPrefix; +import io.trino.metadata.QualifiedSchemaPrefix; import io.trino.metadata.QualifiedTablePrefix; import io.trino.metadata.SystemSecurityMetadata; import io.trino.spi.connector.CatalogSchemaName; import io.trino.spi.connector.CatalogSchemaTableName; import io.trino.spi.connector.EntityKindAndName; import io.trino.spi.function.CatalogSchemaFunctionName; +import io.trino.spi.security.FunctionAuthorization; import io.trino.spi.security.GrantInfo; import io.trino.spi.security.Identity; import io.trino.spi.security.Privilege; import io.trino.spi.security.RoleGrant; +import io.trino.spi.security.SchemaAuthorization; +import io.trino.spi.security.TableAuthorization; import io.trino.spi.security.TrinoPrincipal; import java.util.ArrayDeque; @@ -50,14 +55,18 @@ class TestingSystemSecurityMetadata private final Set roles = synchronizedSet(new HashSet<>()); private final Set roleGrants = synchronizedSet(new HashSet<>()); private final Map viewOwners = synchronizedMap(new HashMap<>()); + private final Map tableOwners = synchronizedMap(new HashMap<>()); private final Map functionOwners = synchronizedMap(new HashMap<>()); + private final Map schemaOwners = synchronizedMap(new HashMap<>()); public void reset() { roles.clear(); roleGrants.clear(); viewOwners.clear(); + tableOwners.clear(); functionOwners.clear(); + schemaOwners.clear(); } public String getFunctionOwner(CatalogSchemaFunctionName functionName) @@ -258,7 +267,10 @@ public void functionDropped(Session session, CatalogSchemaFunctionName function) } @Override - public void schemaCreated(Session session, CatalogSchemaName schema) {} + public void schemaCreated(Session session, CatalogSchemaName schema) + { + schemaOwners.put(schema, session.getIdentity()); + } @Override public void schemaRenamed(Session session, CatalogSchemaName sourceSchema, CatalogSchemaName targetSchema) {} @@ -267,7 +279,10 @@ public void schemaRenamed(Session session, CatalogSchemaName sourceSchema, Catal public void schemaDropped(Session session, CatalogSchemaName schema) {} @Override - public void tableCreated(Session session, CatalogSchemaTableName table) {} + public void tableCreated(Session session, CatalogSchemaTableName table) + { + tableOwners.put(table, session.getIdentity()); + } @Override public void tableRenamed(Session session, CatalogSchemaTableName sourceTable, CatalogSchemaTableName targetTable) {} @@ -298,8 +313,70 @@ public void setEntityOwner(Session session, EntityKindAndName entityKindAndName, checkArgument(principal.getType() == USER, "Only a user can be a view owner"); viewOwners.put(new CatalogSchemaTableName(name.get(0), name.get(1), name.get(2)), Identity.ofUser(principal.getName())); } + else if (entityKindAndName.entityKind().startsWith("TABLE")) { + checkArgument(principal.getType() == USER, "Only a user can be a table owner"); + tableOwners.put(new CatalogSchemaTableName(name.get(0), name.get(1), name.get(2)), Identity.ofUser(principal.getName())); + } + else if (entityKindAndName.entityKind().startsWith("FUNCTION")) { + checkArgument(principal.getType() == USER, "Only a user can be a function owner"); + functionOwners.put(new CatalogSchemaFunctionName(name.get(0), name.get(1), name.get(2)), Identity.ofUser(principal.getName())); + } + else if (entityKindAndName.entityKind().startsWith("SCHEMA")) { + checkArgument(principal.getType() == USER, "Only a user can be a schema owner"); + schemaOwners.put(new CatalogSchemaName(name.get(0), name.get(1)), Identity.ofUser(principal.getName())); + } else { throw new UnsupportedOperationException(); } } + + @Override + public Set getSchemasAuthorizationInfo(Session session, QualifiedSchemaPrefix prefix) + { + return schemaOwners.keySet().stream() + .filter(catalogSchemaName -> prefix.matches( + new CatalogSchemaName( + catalogSchemaName.getCatalogName(), + catalogSchemaName.getSchemaName()))) + .map(schemaName -> { + Identity owner = schemaOwners.get(schemaName); + return new SchemaAuthorization(schemaName.getSchemaName(), new TrinoPrincipal(USER, owner.getUser())); + }) + .collect(toImmutableSet()); + } + + @Override + public Set getTablesAuthorizationInfo(Session session, QualifiedTablePrefix prefix) + { + Map combined = new HashMap<>(); + combined.putAll(tableOwners); + combined.putAll(viewOwners); + return combined.keySet().stream() + .filter(catalogSchemaTableName -> prefix.matches( + new QualifiedObjectName( + catalogSchemaTableName.getCatalogName(), + catalogSchemaTableName.getSchemaTableName().getSchemaName(), + catalogSchemaTableName.getSchemaTableName().getTableName()))) + .map(viewName -> { + Identity owner = combined.get(viewName); + return new TableAuthorization(viewName.getSchemaTableName(), new TrinoPrincipal(USER, owner.getUser())); + }) + .collect(toImmutableSet()); + } + + @Override + public Set getFunctionsAuthorizationInfo(Session session, QualifiedObjectPrefix prefix) + { + return functionOwners.keySet().stream() + .filter(catalogSchemaFunctionName -> prefix.matches( + new QualifiedObjectName( + catalogSchemaFunctionName.getCatalogName(), + catalogSchemaFunctionName.getSchemaName(), + catalogSchemaFunctionName.getFunctionName()))) + .map(functionName -> { + Identity owner = functionOwners.get(functionName); + return new FunctionAuthorization(functionName.getSchemaFunctionName(), new TrinoPrincipal(USER, owner.getUser())); + }) + .collect(toImmutableSet()); + } }