Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 1 addition & 15 deletions core/trino-main/src/main/java/io/trino/FeaturesConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"experimental.spill-order-by",
"spill-window-operator",
"experimental.spill-window-operator",
"legacy.allow-set-view-authorization",
})
public class FeaturesConfig
{
Expand Down Expand Up @@ -100,7 +101,6 @@ public class FeaturesConfig

private boolean legacyCatalogRoles;
private boolean incrementalHashArrayLoadFactorEnabled = true;
private boolean allowSetViewAuthorization;

private boolean legacyMaterializedViewGracePeriod;
private boolean hideInaccessibleColumns;
Expand Down Expand Up @@ -474,20 +474,6 @@ public FeaturesConfig setHideInaccessibleColumns(boolean hideInaccessibleColumns
return this;
}

public boolean isAllowSetViewAuthorization()
{
return allowSetViewAuthorization;
}

@Config("legacy.allow-set-view-authorization")
@ConfigDescription("For security reasons ALTER VIEW SET AUTHORIZATION is disabled for SECURITY DEFINER; " +
"setting this option to true will re-enable this functionality")
public FeaturesConfig setAllowSetViewAuthorization(boolean allowSetViewAuthorization)
{
this.allowSetViewAuthorization = allowSetViewAuthorization;
return this;
}

public boolean isForceSpillingJoin()
{
return forceSpillingJoin;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,11 @@

import com.google.common.util.concurrent.ListenableFuture;
import com.google.inject.Inject;
import io.trino.FeaturesConfig;
import io.trino.Session;
import io.trino.execution.warnings.WarningCollector;
import io.trino.metadata.Metadata;
import io.trino.metadata.QualifiedObjectName;
import io.trino.metadata.ViewDefinition;
import io.trino.security.AccessControl;
import io.trino.spi.TrinoException;
import io.trino.spi.security.TrinoPrincipal;
import io.trino.sql.tree.Expression;
import io.trino.sql.tree.SetViewAuthorization;
Expand All @@ -35,25 +32,21 @@
import static io.trino.metadata.MetadataUtil.createPrincipal;
import static io.trino.metadata.MetadataUtil.createQualifiedObjectName;
import static io.trino.metadata.MetadataUtil.getRequiredCatalogHandle;
import static io.trino.spi.StandardErrorCode.NOT_SUPPORTED;
import static io.trino.spi.StandardErrorCode.TABLE_NOT_FOUND;
import static io.trino.sql.analyzer.SemanticExceptions.semanticException;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;

public class SetViewAuthorizationTask
implements DataDefinitionTask<SetViewAuthorization>
{
private final Metadata metadata;
private final AccessControl accessControl;
private final boolean isAllowSetViewAuthorization;

@Inject
public SetViewAuthorizationTask(Metadata metadata, AccessControl accessControl, FeaturesConfig featuresConfig)
public SetViewAuthorizationTask(Metadata metadata, AccessControl accessControl)
{
this.metadata = requireNonNull(metadata, "metadata is null");
this.accessControl = requireNonNull(accessControl, "accessControl is null");
this.isAllowSetViewAuthorization = featuresConfig.isAllowSetViewAuthorization();
}

@Override
Expand All @@ -72,19 +65,13 @@ public ListenableFuture<Void> execute(
Session session = stateMachine.getSession();
QualifiedObjectName viewName = createQualifiedObjectName(session, statement, statement.getSource());
getRequiredCatalogHandle(metadata, session, statement, viewName.getCatalogName());
ViewDefinition view = metadata.getView(session, viewName)
.orElseThrow(() -> semanticException(TABLE_NOT_FOUND, statement, "View '%s' does not exist", viewName));
if (metadata.getView(session, viewName).isEmpty()) {
throw semanticException(TABLE_NOT_FOUND, statement, "View '%s' does not exist", viewName);
}

TrinoPrincipal principal = createPrincipal(statement.getPrincipal());
checkRoleExists(session, statement, metadata, principal, Optional.of(viewName.getCatalogName()).filter(catalog -> metadata.isCatalogManagedSecurity(session, catalog)));

if (!view.isRunAsInvoker() && !isAllowSetViewAuthorization) {
Comment thread
dain marked this conversation as resolved.
Outdated
throw new TrinoException(
NOT_SUPPORTED,
format(
"Cannot set authorization for view %s to %s: this feature is disabled",
viewName.getCatalogName() + '.' + viewName.getSchemaName() + '.' + viewName.getObjectName(), principal));
}
accessControl.checkCanSetViewAuthorization(session.toSecurityContext(), viewName, principal);

metadata.setViewAuthorization(session, viewName.asCatalogSchemaTableName(), principal);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ public void testDefaults()
.setIncrementalHashArrayLoadFactorEnabled(true)
.setLegacyMaterializedViewGracePeriod(false)
.setHideInaccessibleColumns(false)
.setAllowSetViewAuthorization(false)
.setForceSpillingJoin(false)
.setFaultTolerantExecutionExchangeEncryptionEnabled(true));
}
Expand Down Expand Up @@ -100,7 +99,6 @@ public void testExplicitPropertyMappings()
.put("incremental-hash-array-load-factor.enabled", "false")
.put("legacy.materialized-view-grace-period", "true")
.put("hide-inaccessible-columns", "true")
.put("legacy.allow-set-view-authorization", "true")
.put("force-spilling-join-operator", "true")
.put("fault-tolerant-execution.exchange-encryption-enabled", "false")
.buildOrThrow();
Expand Down Expand Up @@ -133,7 +131,6 @@ public void testExplicitPropertyMappings()
.setIncrementalHashArrayLoadFactorEnabled(false)
.setLegacyMaterializedViewGracePeriod(true)
.setHideInaccessibleColumns(true)
.setAllowSetViewAuthorization(true)
.setForceSpillingJoin(true)
.setFaultTolerantExecutionExchangeEncryptionEnabled(false);
assertFullMapping(properties, expected);
Expand Down
26 changes: 26 additions & 0 deletions docs/src/main/sphinx/security/authorization.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"authorization": [
{
"original_role": "admin",
"new_user": "bob",
"allow": false
},
{
"original_role": "admin",
"new_user": ".*",
"new_role": ".*"
}
],
"schemas": [
{
"role": "admin",
"owner": true
}
],
"tables": [
{
"role": "admin",
"privileges": ["OWNERSHIP"]
}
]
}
44 changes: 44 additions & 0 deletions docs/src/main/sphinx/security/file-system-access-control.rst
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,50 @@ The fixed management user only applies to HTTP by default. To enable the fixed
user over HTTPS, set the ``management.user.https-enabled`` configuration
property.

.. _system-file-auth-authorization:

Authorization rules
-------------------

These rules control the ability of how owner of schema, table or view can
be altered. These rules are applicable to commands like:

ALTER SCHEMA name SET AUTHORIZATION ( user | USER user | ROLE role )
ALTER TABLE name SET AUTHORIZATION ( user | USER user | ROLE role )
ALTER VIEW name SET AUTHORIZATION ( user | USER user | ROLE role )

When these rules are present, the authorization is based on the first matching
rule, processed from top to bottom. If no rules match, the authorization is
denied.

Notice that in order to execute ``ALTER`` command on schema, table or view user requires ``OWNERSHIP``
privilege.

Each authorization rule is composed of the following fields:

* ``original_user`` (optional): regex to match against the user requesting the
authorization. Defaults to ``.*``.
* ``original_group`` (optional): regex to match against group names of the
requesting authorization. Defaults to ``.*``.
* ``original_role`` (optional): regex to match against role names of the
requesting authorization. Defaults to ``.*``.
* ``new_user`` (optional): regex to match against the new owner user of the schema, table or view.
By default it does not match.
* ``new_role`` (optional): regex to match against the new owner role of the schema, table or view.
By default it does not match.
* ``allow`` (optional): boolean indicating if the authentication should be
allowed. Defaults to ``true``.

Notice that ``new_user`` and ``new_role`` are optional, however it is required to provide at least one of them.

The following example allows the ``admin`` role, to change owner of any schema, table or view
to any user, except to``bob``.

.. literalinclude:: authorization.json
:language: json

.. _system-file-auth-system_information:

.. _catalog-file-based-access-control:

Catalog-level access control files
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,21 @@ public class AccessControlRules
private final List<TableAccessControlRule> tableRules;
private final List<SessionPropertyAccessControlRule> sessionPropertyRules;
private final List<FunctionAccessControlRule> functionRules;
private final List<AuthorizationRule> authorizationRules;

@JsonCreator
public AccessControlRules(
@JsonProperty("schemas") Optional<List<SchemaAccessControlRule>> schemaRules,
@JsonProperty("tables") Optional<List<TableAccessControlRule>> tableRules,
@JsonProperty("session_properties") @JsonAlias("sessionProperties") Optional<List<SessionPropertyAccessControlRule>> sessionPropertyRules,
@JsonProperty("functions") Optional<List<FunctionAccessControlRule>> functionRules)
@JsonProperty("functions") Optional<List<FunctionAccessControlRule>> functionRules,
@JsonProperty("authorization") Optional<List<AuthorizationRule>> authorizationRules)
{
this.schemaRules = schemaRules.orElse(ImmutableList.of(SchemaAccessControlRule.ALLOW_ALL));
this.tableRules = tableRules.orElse(ImmutableList.of(TableAccessControlRule.ALLOW_ALL));
this.sessionPropertyRules = sessionPropertyRules.orElse(ImmutableList.of(SessionPropertyAccessControlRule.ALLOW_ALL));
this.functionRules = functionRules.orElse(ImmutableList.of(FunctionAccessControlRule.ALLOW_ALL));
this.authorizationRules = authorizationRules.orElse(ImmutableList.of());
}

public List<SchemaAccessControlRule> getSchemaRules()
Expand All @@ -61,11 +64,18 @@ public List<FunctionAccessControlRule> getFunctionRules()
return functionRules;
}

public List<AuthorizationRule> getAuthorizationRules()
{
return authorizationRules;
}

public boolean hasRoleRules()
{
return schemaRules.stream().anyMatch(rule -> rule.getRoleRegex().isPresent()) ||
tableRules.stream().anyMatch(rule -> rule.getRoleRegex().isPresent()) ||
sessionPropertyRules.stream().anyMatch(rule -> rule.getRoleRegex().isPresent()) ||
functionRules.stream().anyMatch(rule -> rule.getRoleRegex().isPresent());
functionRules.stream().anyMatch(rule -> rule.getRoleRegex().isPresent()) ||
authorizationRules.stream().anyMatch(rule -> rule.getOriginalRolePattern().isPresent()) ||
authorizationRules.stream().anyMatch(rule -> rule.getNewRolePattern().isPresent());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* 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.base.security;

import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.trino.spi.security.TrinoPrincipal;

import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;

import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkArgument;
import static java.lang.Boolean.TRUE;
import static java.util.Objects.requireNonNull;

public class AuthorizationRule
{
private final Optional<Pattern> originalUserPattern;
private final Optional<Pattern> originalGroupPattern;
private final Optional<Pattern> originalRolePattern;
private final Optional<Pattern> newUserPattern;
private final Optional<Pattern> newRolePattern;
private final boolean allow;

@JsonCreator
public AuthorizationRule(
@JsonProperty("original_user") @JsonAlias("originalUser") Optional<Pattern> originalUserPattern,
@JsonProperty("original_group") @JsonAlias("originalGroup") Optional<Pattern> originalGroupPattern,
@JsonProperty("original_role") @JsonAlias("originalRole") Optional<Pattern> originalRolePattern,
@JsonProperty("new_user") @JsonAlias("newUser") Optional<Pattern> newUserPattern,
@JsonProperty("new_role") @JsonAlias("newRole") Optional<Pattern> newRolePattern,
@JsonProperty("allow") Boolean allow)
{
checkArgument(newUserPattern.isPresent() || newRolePattern.isPresent(), "At least one of new_use or new_role is required, none were provided");
this.originalUserPattern = requireNonNull(originalUserPattern, "originalUserPattern is null");
this.originalGroupPattern = requireNonNull(originalGroupPattern, "originalGroupPattern is null");
this.originalRolePattern = requireNonNull(originalRolePattern, "originalRolePattern is null");
this.newUserPattern = requireNonNull(newUserPattern, "newUserPattern is null");
this.newRolePattern = requireNonNull(newRolePattern, "newRolePattern is null");
this.allow = firstNonNull(allow, TRUE);
}

public Optional<Boolean> match(String user, Set<String> groups, Set<String> roles, TrinoPrincipal newPrincipal)
{
if (originalUserPattern.map(regex -> regex.matcher(user).matches()).orElse(true) &&
(originalGroupPattern.isEmpty() || groups.stream().anyMatch(group -> originalGroupPattern.get().matcher(group).matches())) &&
(originalRolePattern.isEmpty() || roles.stream().anyMatch(role -> originalRolePattern.get().matcher(role).matches())) &&
matches(newPrincipal)) {
return Optional.of(allow);
}
return Optional.empty();
}

private boolean matches(TrinoPrincipal newPrincipal)
{
return switch (newPrincipal.getType()) {
case USER -> newUserPattern.map(regex -> regex.matcher(newPrincipal.getName()).matches()).orElse(false);
case ROLE -> newRolePattern.map(regex -> regex.matcher(newPrincipal.getName()).matches()).orElse(false);
};
}

public Optional<Pattern> getOriginalRolePattern()
{
return originalRolePattern;
}

public Optional<Pattern> getNewRolePattern()
{
return newRolePattern;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ public class FileBasedAccessControl
private final List<TableAccessControlRule> tableRules;
private final List<SessionPropertyAccessControlRule> sessionPropertyRules;
private final List<FunctionAccessControlRule> functionRules;
private final List<AuthorizationRule> authorizationRules;
Comment thread
dain marked this conversation as resolved.
Outdated
private final Set<AnySchemaPermissionsRule> anySchemaPermissionsRules;

public FileBasedAccessControl(CatalogName catalogName, AccessControlRules rules)
Expand All @@ -120,6 +121,7 @@ public FileBasedAccessControl(CatalogName catalogName, AccessControlRules rules)
this.tableRules = rules.getTableRules();
this.sessionPropertyRules = rules.getSessionPropertyRules();
this.functionRules = rules.getFunctionRules();
this.authorizationRules = rules.getAuthorizationRules();
ImmutableSet.Builder<AnySchemaPermissionsRule> anySchemaPermissionsRules = ImmutableSet.builder();
schemaRules.stream()
.map(SchemaAccessControlRule::toAnySchemaPermissionsRule)
Expand Down Expand Up @@ -169,6 +171,9 @@ public void checkCanSetSchemaAuthorization(ConnectorSecurityContext context, Str
if (!isSchemaOwner(context, schemaName)) {
denySetSchemaAuthorization(schemaName, principal);
}
if (!checkCanSetAuthorization(context, principal)) {
denySetSchemaAuthorization(schemaName, principal);
}
}

@Override
Expand Down Expand Up @@ -347,6 +352,9 @@ public void checkCanSetTableAuthorization(ConnectorSecurityContext context, Sche
if (!checkTablePermission(context, tableName, OWNERSHIP)) {
denySetTableAuthorization(tableName.toString(), principal);
}
if (!checkCanSetAuthorization(context, principal)) {
denySetTableAuthorization(tableName.toString(), principal);
}
}

@Override
Expand Down Expand Up @@ -423,6 +431,9 @@ public void checkCanSetViewAuthorization(ConnectorSecurityContext context, Schem
if (!checkTablePermission(context, viewName, OWNERSHIP)) {
denySetViewAuthorization(viewName.toString(), principal);
}
if (!checkCanSetAuthorization(context, principal)) {
denySetViewAuthorization(viewName.toString(), principal);
}
}

@Override
Expand Down Expand Up @@ -749,4 +760,16 @@ private boolean checkFunctionPermission(ConnectorSecurityContext context, Functi
.filter(executePredicate)
.isPresent();
}

private boolean checkCanSetAuthorization(ConnectorSecurityContext context, TrinoPrincipal principal)
{
ConnectorIdentity identity = context.getIdentity();
Set<String> roles = identity.getConnectorRole().stream()
.flatMap(role -> role.getRole().stream())
.collect(toImmutableSet());
return authorizationRules.stream()
.flatMap(rule -> rule.match(identity.getUser(), identity.getGroups(), roles, principal).stream())
.findFirst()
.orElse(false);
}
}
Loading