Skip to content

Commit

Permalink
[Enhancement] Support column access privilege for Ranger (#47702)
Browse files Browse the repository at this point in the history
Signed-off-by: HangyuanLiu <460660596@qq.com>
(cherry picked from commit ba567ce)

# Conflicts:
#	fe/fe-core/src/main/java/com/starrocks/sql/analyzer/AuthorizerStmtVisitor.java
  • Loading branch information
HangyuanLiu committed Jul 3, 2024
1 parent 8f5113f commit de86f27
Show file tree
Hide file tree
Showing 10 changed files with 301 additions and 295 deletions.
2 changes: 0 additions & 2 deletions conf/ranger/ranger-servicedef-starrocks.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,6 @@
"label": "StarRocks View",
"description": "StarRocks View",
"accessTypeRestrictions": [
"select",
"drop",
"alter"
]
Expand All @@ -168,7 +167,6 @@
"label": "StarRocks Materialized View",
"description": "StarRocks Materialized View",
"accessTypeRestrictions": [
"select",
"refresh",
"drop",
"alter"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ default void checkAnyActionOnAnyTable(UserIdentity currentUser, Set<Long> roleId
}

default void checkColumnsAction(UserIdentity currentUser, Set<Long> roleIds, TableName tableName,
Set<String> columns, PrivilegeType privilegeType) throws AccessDeniedException {
String column, PrivilegeType privilegeType) throws AccessDeniedException {
throw new AccessDeniedException();
}

Expand Down
246 changes: 246 additions & 0 deletions fe/fe-core/src/main/java/com/starrocks/privilege/ColumnPrivilege.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
// Copyright 2021-present StarRocks, Inc. All rights reserved.
//
// 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
//
// https://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 com.starrocks.privilege;

import com.google.common.collect.Maps;
import com.starrocks.analysis.ParseNode;
import com.starrocks.analysis.TableName;
import com.starrocks.catalog.Column;
import com.starrocks.catalog.InternalCatalog;
import com.starrocks.catalog.Table;
import com.starrocks.catalog.View;
import com.starrocks.qe.ConnectContext;
import com.starrocks.sql.StatementPlanner;
import com.starrocks.sql.analyzer.Authorizer;
import com.starrocks.sql.ast.AstTraverser;
import com.starrocks.sql.ast.DeleteStmt;
import com.starrocks.sql.ast.InsertStmt;
import com.starrocks.sql.ast.QueryStatement;
import com.starrocks.sql.ast.TableRelation;
import com.starrocks.sql.ast.UpdateStmt;
import com.starrocks.sql.ast.ViewRelation;
import com.starrocks.sql.optimizer.OptExpression;
import com.starrocks.sql.optimizer.OptExpressionVisitor;
import com.starrocks.sql.optimizer.Optimizer;
import com.starrocks.sql.optimizer.OptimizerConfig;
import com.starrocks.sql.optimizer.base.ColumnRefFactory;
import com.starrocks.sql.optimizer.base.ColumnRefSet;
import com.starrocks.sql.optimizer.base.PhysicalPropertySet;
import com.starrocks.sql.optimizer.operator.Operator;
import com.starrocks.sql.optimizer.operator.logical.LogicalScanOperator;
import com.starrocks.sql.optimizer.operator.scalar.ColumnRefOperator;
import com.starrocks.sql.optimizer.rule.RuleSetType;
import com.starrocks.sql.optimizer.transformer.LogicalPlan;
import com.starrocks.sql.optimizer.transformer.RelationTransformer;
import com.starrocks.sql.optimizer.transformer.TransformerContext;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public class ColumnPrivilege {
public static void check(ConnectContext context, QueryStatement stmt) {
Map<TableName, Table> tableNameTableObj = Maps.newHashMap();
Map<Table, TableName> tableObjectToTableName = Maps.newHashMap();
new TableNameCollector(tableNameTableObj, tableObjectToTableName).visit(stmt);

Set<TableName> tableUsedExternalAccessController = new HashSet<>();
for (TableName tableName : tableNameTableObj.keySet()) {
String catalog = tableName.getCatalog() == null ?
InternalCatalog.DEFAULT_INTERNAL_CATALOG_NAME : tableName.getCatalog();
if (Authorizer.getInstance().getAccessControlOrDefault(catalog) instanceof ExternalAccessController) {
tableUsedExternalAccessController.add(tableName);
}
}

Map<TableName, Set<String>> scanColumns = new HashMap<>();
OptExpression optimizedPlan;
if (!tableUsedExternalAccessController.isEmpty()) {
/*
* The column access privilege of the query need to use the pruned column list.
* Because the unused columns will not check the column access privilege.
* For example, the table contains two columns v1 and v2, and user u1 only has
* the access privilege to v1, the select v1 from (select * from tbl) t can be checked because v2 has been pruned.
*/
ColumnRefFactory columnRefFactory = new ColumnRefFactory();
LogicalPlan logicalPlan;
Map<Operator, ParseNode> optToAstMap = StatementPlanner.makeOptToAstMap(context.getSessionVariable());

TransformerContext transformerContext = new TransformerContext(columnRefFactory, context, optToAstMap);
logicalPlan = new RelationTransformer(transformerContext).transformWithSelectLimit(stmt.getQueryRelation());

OptimizerConfig optimizerConfig = new OptimizerConfig(OptimizerConfig.OptimizerAlgorithm.RULE_BASED);
optimizerConfig.disableRuleSet(RuleSetType.SINGLE_TABLE_MV_REWRITE);
optimizerConfig.disableRuleSet(RuleSetType.MULTI_TABLE_MV_REWRITE);
optimizerConfig.disableRuleSet(RuleSetType.PRUNE_EMPTY_OPERATOR);
Optimizer optimizer = new Optimizer(optimizerConfig);
optimizedPlan = optimizer.optimize(context, logicalPlan.getRoot(),
new PhysicalPropertySet(), new ColumnRefSet(logicalPlan.getOutputColumn()), columnRefFactory);

optimizedPlan.getOp().accept(new ScanColumnCollector(tableObjectToTableName, scanColumns), optimizedPlan, null);
}

for (TableName tableName : tableNameTableObj.keySet()) {
if (tableUsedExternalAccessController.contains(tableName)) {
Set<String> columns = scanColumns.getOrDefault(tableName, new HashSet<>());
for (String column : columns) {
try {
Authorizer.checkColumnsAction(context.getCurrentUserIdentity(), context.getCurrentRoleIds(),
tableName, column, PrivilegeType.SELECT);
} catch (AccessDeniedException e) {
AccessDeniedException.reportAccessDenied(
tableName.getCatalog(),
context.getCurrentUserIdentity(), context.getCurrentRoleIds(),
PrivilegeType.SELECT.name(), ObjectType.COLUMN.name(), column);
}
}
} else {
Table table = tableNameTableObj.get(tableName);

if (table instanceof View) {
try {
// for privilege checking, treat hive view as table
if (table.getType() == Table.TableType.HIVE_VIEW) {
Authorizer.checkTableAction(context.getCurrentUserIdentity(), context.getCurrentRoleIds(),
tableName, PrivilegeType.SELECT);
} else {
Authorizer.checkViewAction(context.getCurrentUserIdentity(), context.getCurrentRoleIds(),
tableName, PrivilegeType.SELECT);
}
} catch (AccessDeniedException e) {
AccessDeniedException.reportAccessDenied(
InternalCatalog.DEFAULT_INTERNAL_CATALOG_NAME,
context.getCurrentUserIdentity(), context.getCurrentRoleIds(),
PrivilegeType.SELECT.name(), ObjectType.VIEW.name(), tableName.getTbl());
}
} else if (table.isMaterializedView()) {
try {
Authorizer.checkMaterializedViewAction(context.getCurrentUserIdentity(), context.getCurrentRoleIds(),
tableName, PrivilegeType.SELECT);
} catch (AccessDeniedException e) {
AccessDeniedException.reportAccessDenied(
InternalCatalog.DEFAULT_INTERNAL_CATALOG_NAME,
context.getCurrentUserIdentity(), context.getCurrentRoleIds(),
PrivilegeType.SELECT.name(), ObjectType.MATERIALIZED_VIEW.name(), tableName.getTbl());
}
} else {
try {
Authorizer.checkTableAction(context.getCurrentUserIdentity(), context.getCurrentRoleIds(),
tableName.getCatalog(), tableName.getDb(), table.getName(), PrivilegeType.SELECT);
} catch (AccessDeniedException e) {
AccessDeniedException.reportAccessDenied(
tableName.getCatalog(),
context.getCurrentUserIdentity(), context.getCurrentRoleIds(),
PrivilegeType.SELECT.name(), ObjectType.TABLE.name(), tableName.getTbl());
}
}
}
}
}

public static class ScanColumnCollector extends OptExpressionVisitor<Void, Void> {
private final Map<TableName, Set<String>> scanColumns;
private final Map<Table, TableName> tableObjToTableName;

public ScanColumnCollector(
Map<Table, TableName> tableObjToTableName,
Map<TableName, Set<String>> scanColumns) {
this.scanColumns = scanColumns;
this.tableObjToTableName = tableObjToTableName;
}

@Override
public Void visit(OptExpression optExpression, Void context) {
for (OptExpression input : optExpression.getInputs()) {
input.getOp().accept(this, input, null);
}
return null;
}

@Override
public Void visitLogicalTableScan(OptExpression node, Void context) {
LogicalScanOperator operator = (LogicalScanOperator) node.getOp();
Table table = operator.getTable();
TableName tableName = tableObjToTableName.get(table);

if (!scanColumns.containsKey(tableName)) {
scanColumns.put(tableName, new HashSet<>());
}

Set<String> tableColumns = scanColumns.get(tableName);
for (Map.Entry<ColumnRefOperator, Column> c : operator.getColRefToColumnMetaMap().entrySet()) {
String columName = c.getValue().getName();
tableColumns.add(columName);
}
return null;
}
}

private static class TableNameCollector extends AstTraverser<Void, Void> {
private final Map<TableName, Table> tableNameToTableObj;
private final Map<Table, TableName> tableTableNameMap;

public TableNameCollector(Map<TableName, Table> tableNameToTableObj, Map<Table, TableName> tableTableNameMap) {
this.tableNameToTableObj = tableNameToTableObj;
this.tableTableNameMap = tableTableNameMap;
}

@Override
public Void visitQueryStatement(QueryStatement statement, Void context) {
return visit(statement.getQueryRelation());
}

@Override
public Void visitInsertStatement(InsertStmt node, Void context) {
Table table = node.getTargetTable();
tableNameToTableObj.put(node.getTableName(), table);
tableTableNameMap.put(table, node.getTableName());
return super.visitInsertStatement(node, context);
}

@Override
public Void visitUpdateStatement(UpdateStmt node, Void context) {
Table table = node.getTable();
tableNameToTableObj.put(node.getTableName(), table);
tableTableNameMap.put(table, node.getTableName());
return super.visitUpdateStatement(node, context);
}

@Override
public Void visitDeleteStatement(DeleteStmt node, Void context) {
Table table = node.getTable();
tableNameToTableObj.put(node.getTableName(), table);
tableTableNameMap.put(table, node.getTableName());
return super.visitDeleteStatement(node, context);
}

@Override
public Void visitTable(TableRelation node, Void context) {
Table table = node.getTable();
tableNameToTableObj.put(node.getName(), table);
tableTableNameMap.put(table, node.getName());
return null;
}

@Override
public Void visitView(ViewRelation node, Void context) {
Table table = node.getView();
tableNameToTableObj.put(node.getName(), table);
tableTableNameMap.put(table, node.getName());
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,6 @@ public void checkAnyActionOnAnyTable(UserIdentity currentUser, Set<Long> roleIds
checkAnyActionOnTable(currentUser, roleIds, new TableName(catalog, db, "*"));
}

@Override
public void checkColumnsAction(UserIdentity currentUser, Set<Long> roleIds, TableName tableName,
Set<String> columns, PrivilegeType privilegeType) throws AccessDeniedException {
throw new AccessDeniedException("Column-level access control not implemented");
}

@Override
public void checkViewAction(UserIdentity currentUser, Set<Long> roleIds, TableName tableName, PrivilegeType privilegeType)
throws AccessDeniedException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,17 @@ public void checkAnyActionOnTable(UserIdentity currentUser, Set<Long> roleIds, T
PrivilegeType.ANY);
}

@Override
public void checkColumnsAction(UserIdentity currentUser, Set<Long> roleIds, TableName tableName,
String column, PrivilegeType privilegeType) throws AccessDeniedException {
hasPermission(RangerHiveResource.builder()
.setDatabase(tableName.getDb())
.setTable(tableName.getTbl())
.setColumn(column)
.build(),
currentUser, privilegeType);
}

@Override
public Map<String, Expr> getColumnMaskingPolicy(ConnectContext context, TableName tableName, List<Column> columns) {
Map<String, Expr> maskingExprMap = Maps.newHashMap();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,18 @@ public void checkAnyActionOnAnyTable(UserIdentity currentUser, Set<Long> roleIds
throw new AccessDeniedException();
}

@Override
public void checkColumnsAction(UserIdentity currentUser, Set<Long> roleIds, TableName tableName,
String column, PrivilegeType privilegeType) throws AccessDeniedException {
hasPermission(RangerStarRocksResource.builder()
.setCatalog(tableName.getCatalog())
.setDatabase(tableName.getDb())
.setTable(tableName.getTbl())
.setColumn(column)
.build(),
currentUser, privilegeType);
}

@Override
public void checkViewAction(UserIdentity currentUser, Set<Long> roleIds, TableName tableName, PrivilegeType privilegeType)
throws AccessDeniedException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ public static ExecPlan plan(StatementBase stmt, ConnectContext session,
QueryStatement queryStmt = (QueryStatement) stmt;
resultSinkType = queryStmt.hasOutFileClause() ? TResultSinkType.FILE : resultSinkType;
boolean isOnlyOlapTableQueries = AnalyzerUtils.isOnlyHasOlapTables(queryStmt);
needWholePhaseLock = isLockFree(isOnlyOlapTableQueries, session) ? false : true;
needWholePhaseLock = isLockFree(isOnlyOlapTableQueries, session) ? false : true;
ExecPlan plan;
if (needWholePhaseLock) {
plan = createQueryPlan(queryStmt, session, resultSinkType);
Expand Down Expand Up @@ -186,7 +186,7 @@ private static boolean isLockFree(boolean isOnlyOlapTable, ConnectContext sessio
/**
* Create a map from opt expression to parse node for the optimizer to use which only used in text match rewrite for mv.
*/
private static Map<Operator, ParseNode> makeOptToAstMap(SessionVariable sessionVariable) {
public static Map<Operator, ParseNode> makeOptToAstMap(SessionVariable sessionVariable) {
if (sessionVariable.isEnableMaterializedViewTextMatchRewrite()) {
return Maps.newHashMap();
}
Expand Down Expand Up @@ -225,6 +225,7 @@ private static ExecPlan createQueryPlan(StatementBase stmt,
new ColumnRefSet(logicalPlan.getOutputColumn()),
columnRefFactory);
}

try (Timer ignored = Tracers.watchScope("ExecPlanBuild")) {
// 3. Build fragment exec plan
/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,10 @@ public static void checkAnyActionOnTable(UserIdentity currentUser, Set<Long> rol
}

public static void checkColumnsAction(UserIdentity currentUser, Set<Long> roleIds,
TableName tableName, Set<String> columns,
TableName tableName, String column,
PrivilegeType privilegeType) throws AccessDeniedException {
getInstance().getAccessControlOrDefault(tableName.getCatalog()).checkColumnsAction(currentUser, roleIds,
tableName, columns, privilegeType);
tableName, column, privilegeType);
}

public static void checkViewAction(UserIdentity currentUser, Set<Long> roleIds, TableName tableName,
Expand Down
Loading

0 comments on commit de86f27

Please sign in to comment.