[Refactor](auth)(step-2) Add AccessController to support customized authorization (#16802)

Support specifying AccessControllerFactory when creating catalog

create catalog hive properties(
...
"access_controller.class" = "org.apache.doris.mysql.privilege.RangerAccessControllerFactory",
"access_controller.properties.prop1" = "xxx",
"access_controller.properties.prop2" = "yyy",
...
)
So that user can specified their own access controller, such as RangerAccessController

Add interface to check column level privilege

A new method of CatalogAccessController: checkColsPriv(),
for checking column level privileges.

TODO:
Support grant column level privileges statements in Doris

Add TestExternalCatalog/Database/Table/ScanNode

These classes are used for FE unit test. In unit test you can

create catalog test1 properties(
    "type" = "test"
    "catalog_provider.class" = "org.apache.doris.datasource.ColumnPrivTest$MockedCatalogProvider"
    "access_controller.class" = "org.apache.doris.mysql.privilege.TestAccessControllerFactory",
    "access_controller.properties.key1" = "val1",
    "access_controller.properties.key2" = "val2"
);
To create a test catalog, and specify catalog_provider to mock database/table/schema metadata

Set roles in current user identity in connection context

The roles can be used for authorization in access controller.
This commit is contained in:
Mingyu Chen
2023-02-20 10:32:48 +08:00
committed by GitHub
parent 5291f14aff
commit 97230a54fb
44 changed files with 1217 additions and 55 deletions

View File

@ -59,7 +59,7 @@ public class DropCatalogStmt extends DdlStmt {
if (!Env.getCurrentEnv().getAccessManager().checkCtlPriv(
ConnectContext.get(), catalogName, PrivPredicate.DROP)) {
ErrorReport.reportAnalysisException(ErrorCode.ERR_CATALOG_ACCESS_DENIED,
analyzer.getQualifiedUser(), catalogName);
ConnectContext.get().getQualifiedUser(), catalogName);
}
}

View File

@ -381,7 +381,7 @@ public class SelectStmt extends QueryStmt {
.checkTblPriv(ConnectContext.get(), tblRef.getName(), PrivPredicate.SELECT)) {
ErrorReport.reportAnalysisException(ErrorCode.ERR_TABLEACCESS_DENIED_ERROR, "SELECT",
ConnectContext.get().getQualifiedUser(), ConnectContext.get().getRemoteIP(),
dbName + ": " + tableName);
dbName + "." + tableName);
}
tableMap.put(table.getId(), table);
}

View File

@ -67,8 +67,6 @@ public class SlotDescriptor {
private ColumnStats stats; // only set if 'column' isn't set
private boolean isAgg;
private boolean isMultiRef;
// used for load to get more information of varchar and decimal
private Type originType;
// If set to false, then such slots will be ignored during
// materialize them.Used to optmize to read less data and less memory usage
private boolean needMaterialize = true;
@ -162,7 +160,6 @@ public class SlotDescriptor {
public void setColumn(Column column) {
this.column = column;
this.type = column.getType();
this.originType = column.getOriginType();
}
public void setSrcColumn(Column column) {
@ -254,10 +251,6 @@ public class SlotDescriptor {
this.label = label;
}
public void setSourceExprs(List<Expr> exprs) {
sourceExprs = exprs;
}
public void setSourceExpr(Expr expr) {
sourceExprs = Collections.singletonList(expr);
}
@ -316,11 +309,9 @@ public class SlotDescriptor {
return true;
}
// TODO
public TSlotDescriptor toThrift() {
TSlotDescriptor tSlotDescriptor = new TSlotDescriptor(id.asInt(), parent.getId().asInt(),
(originType != null ? originType.toThrift() : type.toThrift()), -1, byteOffset, nullIndicatorByte,
type.toThrift(), -1, byteOffset, nullIndicatorByte,
nullIndicatorBit, ((column != null) ? column.getName() : ""), slotIdx, isMaterialized);
tSlotDescriptor.setNeedMaterialize(needMaterialize);
if (column != null) {

View File

@ -162,6 +162,11 @@ public class TableName implements Writable {
return false;
}
@Override
public int hashCode() {
return Objects.hash(ctl, tbl, db);
}
public String toSql() {
StringBuilder stringBuilder = new StringBuilder();
if (ctl != null && !ctl.equals(InternalCatalog.INTERNAL_CATALOG_NAME)) {

View File

@ -40,6 +40,7 @@ import org.apache.logging.log4j.Logger;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.Set;
// https://dev.mysql.com/doc/refman/8.0/en/account-names.html
// user name must be literally matched.
@ -52,12 +53,14 @@ public class UserIdentity implements Writable, GsonPostProcessable {
@SerializedName(value = "user")
private String user;
@SerializedName(value = "host")
private String host;
@SerializedName(value = "isDomain")
private boolean isDomain;
// The roles which this user belongs to.
// Used for authorization in Access Controller
// This field is only set when getting current user from auth and not need to persist
private Set<String> roles;
private boolean isAnalyzed = false;
@ -125,6 +128,14 @@ public class UserIdentity implements Writable, GsonPostProcessable {
this.isAnalyzed = true;
}
public void setRoles(Set<String> roles) {
this.roles = roles;
}
public Set<String> getRoles() {
return roles;
}
public void analyze(String clusterName) throws AnalysisException {
if (isAnalyzed) {
return;

View File

@ -270,6 +270,10 @@ public abstract class Table extends MetaObject implements Writable, TableIf {
this.qualifiedDbName = qualifiedDbName;
}
public String getQualifiedDbName() {
return qualifiedDbName;
}
public String getQualifiedName() {
if (StringUtils.isEmpty(qualifiedDbName)) {
return name;

View File

@ -135,7 +135,7 @@ public interface TableIf {
enum TableType {
MYSQL, ODBC, OLAP, SCHEMA, INLINE_VIEW, VIEW, BROKER, ELASTICSEARCH, HIVE, ICEBERG, HUDI, JDBC,
TABLE_VALUED_FUNCTION, HMS_EXTERNAL_TABLE, ES_EXTERNAL_TABLE, MATERIALIZED_VIEW, JDBC_EXTERNAL_TABLE,
ICEBERG_EXTERNAL_TABLE;
ICEBERG_EXTERNAL_TABLE, TEST_EXTERNAL_TABLE;
public String toEngineName() {
switch (this) {

View File

@ -47,6 +47,7 @@ public class JdbcExternalTable extends ExternalTable {
super(id, name, catalog, dbName, TableType.JDBC_EXTERNAL_TABLE);
}
@Override
protected synchronized void makeSureInitialized() {
if (!objectCreated) {
jdbcTable = toJdbcTable();

View File

@ -0,0 +1,150 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.catalog.external;
import org.apache.doris.catalog.Env;
import org.apache.doris.datasource.ExternalCatalog;
import org.apache.doris.datasource.InitDatabaseLog;
import org.apache.doris.datasource.test.TestExternalCatalog;
import org.apache.doris.persist.gson.GsonPostProcessable;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.gson.annotations.SerializedName;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class TestExternalDatabase extends ExternalDatabase<TestExternalTable> implements GsonPostProcessable {
private static final Logger LOG = LogManager.getLogger(TestExternalDatabase.class);
// Cache of table name to table id.
private Map<String, Long> tableNameToId = Maps.newConcurrentMap();
@SerializedName(value = "idToTbl")
private Map<Long, TestExternalTable> idToTbl = Maps.newConcurrentMap();
public TestExternalDatabase(ExternalCatalog extCatalog, long id, String name) {
super(extCatalog, id, name);
}
@Override
protected void init() {
InitDatabaseLog initDatabaseLog = new InitDatabaseLog();
initDatabaseLog.setType(InitDatabaseLog.Type.TEST);
initDatabaseLog.setCatalogId(extCatalog.getId());
initDatabaseLog.setDbId(id);
List<String> tableNames = extCatalog.listTableNames(null, name);
if (tableNames != null) {
Map<String, Long> tmpTableNameToId = Maps.newConcurrentMap();
Map<Long, TestExternalTable> tmpIdToTbl = Maps.newHashMap();
for (String tableName : tableNames) {
long tblId;
if (tableNameToId != null && tableNameToId.containsKey(tableName)) {
tblId = tableNameToId.get(tableName);
tmpTableNameToId.put(tableName, tblId);
TestExternalTable table = idToTbl.get(tblId);
tmpIdToTbl.put(tblId, table);
initDatabaseLog.addRefreshTable(tblId);
} else {
tblId = Env.getCurrentEnv().getNextId();
tmpTableNameToId.put(tableName, tblId);
TestExternalTable table = new TestExternalTable(tblId, tableName, name,
(TestExternalCatalog) extCatalog);
tmpIdToTbl.put(tblId, table);
initDatabaseLog.addCreateTable(tblId, tableName);
}
}
tableNameToId = tmpTableNameToId;
idToTbl = tmpIdToTbl;
}
initialized = true;
Env.getCurrentEnv().getEditLog().logInitExternalDb(initDatabaseLog);
}
public void setTableExtCatalog(ExternalCatalog extCatalog) {
for (TestExternalTable table : idToTbl.values()) {
table.setCatalog(extCatalog);
}
}
public void replayInitDb(InitDatabaseLog log, ExternalCatalog catalog) {
Map<String, Long> tmpTableNameToId = Maps.newConcurrentMap();
Map<Long, TestExternalTable> tmpIdToTbl = Maps.newConcurrentMap();
for (int i = 0; i < log.getRefreshCount(); i++) {
TestExternalTable table = getTableForReplay(log.getRefreshTableIds().get(i));
tmpTableNameToId.put(table.getName(), table.getId());
tmpIdToTbl.put(table.getId(), table);
}
for (int i = 0; i < log.getCreateCount(); i++) {
TestExternalTable table = new TestExternalTable(log.getCreateTableIds().get(i),
log.getCreateTableNames().get(i), name, (TestExternalCatalog) catalog);
tmpTableNameToId.put(table.getName(), table.getId());
tmpIdToTbl.put(table.getId(), table);
}
tableNameToId = tmpTableNameToId;
idToTbl = tmpIdToTbl;
initialized = true;
}
// TODO(ftw): drew
@Override
public Set<String> getTableNamesWithLock() {
makeSureInitialized();
return Sets.newHashSet(tableNameToId.keySet());
}
@Override
public List<TestExternalTable> getTables() {
makeSureInitialized();
return Lists.newArrayList(idToTbl.values());
}
@Override
public TestExternalTable getTableNullable(String tableName) {
makeSureInitialized();
if (!tableNameToId.containsKey(tableName)) {
return null;
}
return idToTbl.get(tableNameToId.get(tableName));
}
@Override
public TestExternalTable getTableNullable(long tableId) {
makeSureInitialized();
return idToTbl.get(tableId);
}
public TestExternalTable getTableForReplay(long tableId) {
return idToTbl.get(tableId);
}
@Override
public void gsonPostProcess() throws IOException {
tableNameToId = Maps.newConcurrentMap();
for (TestExternalTable tbl : idToTbl.values()) {
tableNameToId.put(tbl.getName(), tbl.getId());
}
rwLock = new ReentrantReadWriteLock(true);
}
}

View File

@ -0,0 +1,63 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.catalog.external;
import org.apache.doris.catalog.Column;
import org.apache.doris.datasource.test.TestExternalCatalog;
import org.apache.doris.thrift.TTableDescriptor;
import org.apache.doris.thrift.TTableType;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.List;
/**
* TestExternalTable is a table for unit test.
*/
public class TestExternalTable extends ExternalTable {
private static final Logger LOG = LogManager.getLogger(TestExternalTable.class);
public TestExternalTable(long id, String name, String dbName, TestExternalCatalog catalog) {
super(id, name, catalog, dbName, TableType.TEST_EXTERNAL_TABLE);
}
@Override
protected synchronized void makeSureInitialized() {
}
@Override
public String getMysqlType() {
return type.name();
}
@Override
public TTableDescriptor toThrift() {
makeSureInitialized();
TTableDescriptor tTableDescriptor = new TTableDescriptor(getId(), TTableType.TEST_EXTERNAL_TABLE,
getFullSchema().size(),
0, getName(), "");
return tTableDescriptor;
}
@Override
public List<Column> initSchema() {
return ((TestExternalCatalog) catalog).mockedSchema(dbName, name);
}
}

View File

@ -0,0 +1,45 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.common;
/**
* Thrown for authorization errors encountered when accessing Catalog objects.
*/
public class AuthorizationException extends UserException {
public ErrorCode errorCode = ErrorCode.ERR_COMMON_ERROR;
public Object[] msgs;
public AuthorizationException(String msg, Throwable cause) {
super(msg, cause);
}
public AuthorizationException(String msg) {
super(msg);
}
public AuthorizationException(ErrorCode code, Object... msgs) {
super(code.formatErrorMsg(msgs));
this.errorCode = code;
this.msgs = msgs;
}
public String formatErrMsg() {
return errorCode.formatErrorMsg(msgs);
}
}

View File

@ -31,6 +31,7 @@ import org.apache.doris.catalog.ScalarType;
import org.apache.doris.catalog.Type;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.Config;
import org.apache.doris.datasource.CatalogMgr;
import org.apache.doris.policy.Policy;
import org.apache.doris.policy.StoragePolicy;
import org.apache.doris.resource.Tag;
@ -814,5 +815,22 @@ public class PropertyAnalyzer {
throw new AnalysisException(e.getMessage());
}
}
// validate access controller properties
// eg:
// (
// "access_controller.class" = "org.apache.doris.mysql.privilege.RangerAccessControllerFactory",
// "access_controller.properties.prop1" = "xxx",
// "access_controller.properties.prop2" = "yyy",
// )
// 1. get access controller class
String acClass = properties.getOrDefault(CatalogMgr.ACCESS_CONTROLLER_CLASS_PROP, "");
if (!Strings.isNullOrEmpty(acClass)) {
// 2. check if class exists
try {
Class.forName(acClass);
} catch (ClassNotFoundException e) {
throw new AnalysisException("failed to find class " + acClass, e);
}
}
}
}

View File

@ -26,7 +26,9 @@ import org.apache.doris.analysis.StatementBase;
import org.apache.doris.catalog.Env;
import org.apache.doris.catalog.Resource;
import org.apache.doris.common.DdlException;
import org.apache.doris.common.FeConstants;
import org.apache.doris.datasource.iceberg.IcebergExternalCatalogFactory;
import org.apache.doris.datasource.test.TestExternalCatalog;
import org.apache.parquet.Strings;
@ -37,7 +39,6 @@ import java.util.Optional;
* A factory to create catalog instance of log or covert catalog into log.
*/
public class CatalogFactory {
/**
* Convert the sql statement into catalog log.
*/
@ -80,11 +81,11 @@ public class CatalogFactory {
Resource catalogResource = Optional.ofNullable(Env.getCurrentEnv().getResourceMgr().getResource(resource))
.orElseThrow(() -> new DdlException("Resource doesn't exist: " + resource));
catalogType = catalogResource.getType().name().toLowerCase();
if (props.containsKey("type")) {
if (props.containsKey(CatalogMgr.CATALOG_TYPE_PROP)) {
throw new DdlException("Can not set 'type' when creating catalog with resource");
}
} else {
String type = props.get("type");
String type = props.get(CatalogMgr.CATALOG_TYPE_PROP);
if (Strings.isNullOrEmpty(type)) {
throw new DdlException("Missing property 'type' in properties");
}
@ -106,6 +107,12 @@ public class CatalogFactory {
case "iceberg":
catalog = IcebergExternalCatalogFactory.createCatalog(catalogId, name, resource, props);
break;
case "test":
if (!FeConstants.runningUnitTest) {
throw new DdlException("test catalog is only for FE unit test");
}
catalog = new TestExternalCatalog(catalogId, name, resource, props);
break;
default:
throw new DdlException("Unknown catalog type: " + catalogType);
}

View File

@ -136,7 +136,7 @@ public interface CatalogIf<T extends DatabaseIf> {
s -> new AnalysisException(ErrorCode.ERR_BAD_DB_ERROR.formatErrorMsg(s), ErrorCode.ERR_BAD_DB_ERROR));
}
// Called when catalog is dropped
default void onClose() {
}
}

View File

@ -76,6 +76,10 @@ import java.util.stream.Collectors;
public class CatalogMgr implements Writable, GsonPostProcessable {
private static final Logger LOG = LogManager.getLogger(CatalogMgr.class);
public static final String ACCESS_CONTROLLER_CLASS_PROP = "access_controller.class";
public static final String ACCESS_CONTROLLER_PROPERTY_PREFIX_PROP = "access_controller.properties.";
public static final String CATALOG_TYPE_PROP = "type";
private static final String YES = "yes";
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);

View File

@ -38,8 +38,10 @@ import com.google.common.collect.Maps;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import org.apache.commons.lang.NotImplementedException;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.parquet.Strings;
import org.jetbrains.annotations.Nullable;
import java.io.DataInput;
@ -139,6 +141,7 @@ public abstract class ExternalCatalog implements CatalogIf<ExternalDatabase>, Wr
protected final void initLocalObjects() {
if (!objectCreated) {
initLocalObjectsImpl();
initAccessController();
objectCreated = true;
}
}
@ -147,6 +150,38 @@ public abstract class ExternalCatalog implements CatalogIf<ExternalDatabase>, Wr
// hms client, read properties from hive-site.xml, es client
protected abstract void initLocalObjectsImpl();
/**
* eg:
* (
* ""access_controller.class" = "org.apache.doris.mysql.privilege.RangerAccessControllerFactory",
* "access_controller.properties.prop1" = "xxx",
* "access_controller.properties.prop2" = "yyy",
* )
*/
private void initAccessController() {
Map<String, String> properties = getCatalogProperty().getProperties();
// 1. get access controller class name
String className = properties.getOrDefault(CatalogMgr.ACCESS_CONTROLLER_CLASS_PROP, "");
if (Strings.isNullOrEmpty(className)) {
// not set access controller, use internal access controller
return;
}
// 2. get access controller properties
Map<String, String> acProperties = Maps.newHashMap();
for (Map.Entry<String, String> entry : properties.entrySet()) {
if (entry.getKey().startsWith(CatalogMgr.ACCESS_CONTROLLER_PROPERTY_PREFIX_PROP)) {
acProperties.put(
StringUtils.removeStart(entry.getKey(), CatalogMgr.ACCESS_CONTROLLER_PROPERTY_PREFIX_PROP),
entry.getValue());
}
}
// 3. create access controller
Env.getCurrentEnv().getAccessManager().createAccessController(name, className, acProperties);
}
// init schema related objects
protected abstract void init();
@ -261,6 +296,16 @@ public abstract class ExternalCatalog implements CatalogIf<ExternalDatabase>, Wr
Text.writeString(out, GsonUtils.GSON.toJson(this));
}
@Override
public void onClose() {
removeAccessController();
CatalogIf.super.onClose();
}
private void removeAccessController() {
Env.getCurrentEnv().getAccessManager().removeAccessController(name);
}
public void replayInitCatalog(InitCatalogLog log) {
Map<String, Long> tmpDbNameToId = Maps.newConcurrentMap();
Map<Long, ExternalDatabase> tmpIdToDb = Maps.newConcurrentMap();

View File

@ -37,6 +37,7 @@ public class InitCatalogLog implements Writable {
ES,
JDBC,
ICEBERG,
TEST,
UNKNOWN;
}

View File

@ -36,6 +36,7 @@ public class InitDatabaseLog implements Writable {
HMS,
ES,
JDBC,
TEST,
UNKNOWN;
}

View File

@ -52,6 +52,7 @@ public class JdbcExternalCatalog extends ExternalCatalog {
@Override
public void onClose() {
super.onClose();
if (jdbcClient != null) {
jdbcClient.closeClient();
}

View File

@ -0,0 +1,155 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.datasource.test;
import org.apache.doris.catalog.Column;
import org.apache.doris.catalog.Env;
import org.apache.doris.catalog.external.ExternalDatabase;
import org.apache.doris.catalog.external.TestExternalDatabase;
import org.apache.doris.datasource.CatalogProperty;
import org.apache.doris.datasource.ExternalCatalog;
import org.apache.doris.datasource.InitCatalogLog;
import org.apache.doris.datasource.SessionContext;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.List;
import java.util.Map;
/**
* This catalog is for unit test.
* You can provide an implementation of TestCatalogProvider, which can mock metadata such as database/table/schema
* You can refer to ColumnPrivTest for example.
*/
public class TestExternalCatalog extends ExternalCatalog {
private static final Logger LOG = LogManager.getLogger(TestExternalCatalog.class);
private TestCatalogProvider catalogProvider;
public TestExternalCatalog(long catalogId, String name, String resource, Map<String, String> props) {
super(catalogId, name);
this.type = "test";
this.catalogProperty = new CatalogProperty(resource, props);
Class<?> providerClazz = null;
try {
providerClazz = Class.forName(props.get("catalog_provider.class"));
this.catalogProvider = (TestCatalogProvider) providerClazz.newInstance();
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
protected void initLocalObjectsImpl() {
}
@Override
protected void init() {
Map<String, Long> tmpDbNameToId = Maps.newConcurrentMap();
Map<Long, ExternalDatabase> tmpIdToDb = Maps.newConcurrentMap();
InitCatalogLog initCatalogLog = new InitCatalogLog();
initCatalogLog.setCatalogId(id);
initCatalogLog.setType(InitCatalogLog.Type.TEST);
List<String> allDatabaseNames = mockedDatabaseNames();
for (String dbName : allDatabaseNames) {
long dbId;
if (dbNameToId != null && dbNameToId.containsKey(dbName)) {
dbId = dbNameToId.get(dbName);
tmpDbNameToId.put(dbName, dbId);
ExternalDatabase db = idToDb.get(dbId);
db.setUnInitialized(invalidCacheInInit);
tmpIdToDb.put(dbId, db);
initCatalogLog.addRefreshDb(dbId);
} else {
dbId = Env.getCurrentEnv().getNextId();
tmpDbNameToId.put(dbName, dbId);
TestExternalDatabase db = new TestExternalDatabase(this, dbId, dbName);
tmpIdToDb.put(dbId, db);
initCatalogLog.addCreateDb(dbId, dbName);
}
}
dbNameToId = tmpDbNameToId;
idToDb = tmpIdToDb;
Env.getCurrentEnv().getEditLog().logInitCatalog(initCatalogLog);
}
private List<String> mockedDatabaseNames() {
return Lists.newArrayList(catalogProvider.getMetadata().keySet());
}
private List<String> mockedTableNames(String dbName) {
if (!catalogProvider.getMetadata().containsKey(dbName)) {
throw new RuntimeException("unknown database: " + dbName);
}
return Lists.newArrayList(catalogProvider.getMetadata().get(dbName).keySet());
}
public List<Column> mockedSchema(String dbName, String tblName) {
if (!catalogProvider.getMetadata().containsKey(dbName)) {
throw new RuntimeException("unknown db: " + dbName);
}
if (!catalogProvider.getMetadata().get(dbName).containsKey(tblName)) {
throw new RuntimeException("unknown tbl: " + tblName);
}
return catalogProvider.getMetadata().get(dbName).get(tblName);
}
@Override
public List<String> listDatabaseNames(SessionContext ctx) {
makeSureInitialized();
return Lists.newArrayList(dbNameToId.keySet());
}
@Override
public List<String> listTableNames(SessionContext ctx, String dbName) {
makeSureInitialized();
TestExternalDatabase db = (TestExternalDatabase) idToDb.get(dbNameToId.get(dbName));
if (db != null && db.isInitialized()) {
List<String> names = Lists.newArrayList();
db.getTables().stream().forEach(table -> names.add(table.getName()));
return names;
} else {
return mockedTableNames(dbName);
}
}
@Override
public boolean tableExist(SessionContext ctx, String dbName, String tblName) {
makeSureInitialized();
if (!catalogProvider.getMetadata().containsKey(dbName)) {
return false;
}
if (!catalogProvider.getMetadata().get(dbName).containsKey(tblName)) {
return false;
}
return true;
}
public interface TestCatalogProvider {
// db name -> (tbl name -> schema)
Map<String, Map<String, List<Column>>> getMetadata();
}
}

View File

@ -163,7 +163,7 @@ public class LoadingTaskPlanner {
// Generate plan trees
// 1. Broker scan node
ScanNode scanNode;
scanNode = new ExternalFileScanNode(new PlanNodeId(nextNodeId++), scanTupleDesc);
scanNode = new ExternalFileScanNode(new PlanNodeId(nextNodeId++), scanTupleDesc, false);
((ExternalFileScanNode) scanNode).setLoadInfo(loadJobId, txnId, table, brokerDesc, fileGroups,
fileStatusesList, filesAdded, strictMode, loadParallelism, userInfo);
scanNode.init(analyzer);

View File

@ -0,0 +1,25 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.mysql.privilege;
import java.util.Map;
public interface AccessControllerFactory {
CatalogAccessController createAccessController(Map<String, String> prop);
}

View File

@ -20,12 +20,16 @@ package org.apache.doris.mysql.privilege;
import org.apache.doris.analysis.TableName;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.catalog.AuthorizationInfo;
import org.apache.doris.common.UserException;
import org.apache.doris.datasource.InternalCatalog;
import org.apache.doris.mysql.privilege.Auth.PrivLevel;
import org.apache.doris.qe.ConnectContext;
import com.google.common.base.Preconditions;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Maps;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.Map;
@ -37,6 +41,8 @@ import java.util.Map;
* And using InternalCatalogAccessController as default.
*/
public class AccessControllerManager {
private static final Logger LOG = LogManager.getLogger(AccessControllerManager.class);
private SystemAccessController sysAccessController;
private CatalogAccessController internalAccessController;
private Map<String, CatalogAccessController> ctlToCtlAccessController = Maps.newConcurrentMap();
@ -51,8 +57,30 @@ public class AccessControllerManager {
return ctlToCtlAccessController.getOrDefault(ctl, internalAccessController);
}
public void addCatalogAccessControl(String ctl, CatalogAccessController controller) {
ctlToCtlAccessController.put(ctl, controller);
public boolean checkIfAccessControllerExist(String ctl) {
return ctlToCtlAccessController.containsKey(ctl);
}
public void createAccessController(String ctl, String acFactoryClassName, Map<String, String> prop) {
Class<?> factoryClazz = null;
try {
factoryClazz = Class.forName(acFactoryClassName);
AccessControllerFactory factory = (AccessControllerFactory) factoryClazz.newInstance();
CatalogAccessController accessController = factory.createAccessController(prop);
ctlToCtlAccessController.put(ctl, accessController);
LOG.info("create access controller {} for catalog {}", ctl, acFactoryClassName);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public void removeAccessController(String ctl) {
ctlToCtlAccessController.remove(ctl);
LOG.info("remove access controller for catalog {}", ctl);
}
public Auth getAuth() {
@ -73,10 +101,9 @@ public class AccessControllerManager {
return checkCtlPriv(ctx.getCurrentUserIdentity(), ctl, wanted);
}
public boolean checkCtlPriv(UserIdentity currentUser, String ctl, PrivPredicate wanted) {
private boolean checkCtlPriv(UserIdentity currentUser, String ctl, PrivPredicate wanted) {
boolean hasGlobal = sysAccessController.checkGlobalPriv(currentUser, wanted);
boolean hasCtl = getAccessControllerOrDefault(ctl).checkCtlPriv(hasGlobal, currentUser, ctl, wanted);
return hasGlobal || hasCtl;
return getAccessControllerOrDefault(ctl).checkCtlPriv(hasGlobal, currentUser, ctl, wanted);
}
// ==== Database ====
@ -94,16 +121,10 @@ public class AccessControllerManager {
public boolean checkDbPriv(UserIdentity currentUser, String ctl, String db, PrivPredicate wanted) {
boolean hasGlobal = sysAccessController.checkGlobalPriv(currentUser, wanted);
boolean hasDb = getAccessControllerOrDefault(ctl).checkDbPriv(hasGlobal, currentUser, ctl, db, wanted);
return hasGlobal || hasDb;
return getAccessControllerOrDefault(ctl).checkDbPriv(hasGlobal, currentUser, ctl, db, wanted);
}
// ==== Table ====
public boolean checkTblPriv(ConnectContext ctx, String qualifiedCtl,
String qualifiedDb, String tbl, PrivPredicate wanted) {
return checkTblPriv(ctx.getCurrentUserIdentity(), qualifiedCtl, qualifiedDb, tbl, wanted);
}
public boolean checkTblPriv(ConnectContext ctx, String qualifiedDb, String tbl, PrivPredicate wanted) {
return checkTblPriv(ctx, Auth.DEFAULT_CATALOG, qualifiedDb, tbl, wanted);
}
@ -113,14 +134,29 @@ public class AccessControllerManager {
return checkTblPriv(ctx, tableName.getCtl(), tableName.getDb(), tableName.getTbl(), wanted);
}
public boolean checkTblPriv(ConnectContext ctx, String qualifiedCtl,
String qualifiedDb, String tbl, PrivPredicate wanted) {
return checkTblPriv(ctx.getCurrentUserIdentity(), qualifiedCtl, qualifiedDb, tbl, wanted);
}
public boolean checkTblPriv(UserIdentity currentUser, String db, String tbl, PrivPredicate wanted) {
return checkTblPriv(currentUser, Auth.DEFAULT_CATALOG, db, tbl, wanted);
}
public boolean checkTblPriv(UserIdentity currentUser, String ctl, String db, String tbl, PrivPredicate wanted) {
boolean hasGlobal = sysAccessController.checkGlobalPriv(currentUser, wanted);
boolean hasTbl = getAccessControllerOrDefault(ctl).checkTblPriv(hasGlobal, currentUser, ctl, db, tbl, wanted);
return hasGlobal || hasTbl;
return getAccessControllerOrDefault(ctl).checkTblPriv(hasGlobal, currentUser, ctl, db, tbl, wanted);
}
// ==== Column ====
public void checkColumnsPriv(UserIdentity currentUser, String ctl, HashMultimap<TableName, String> tableToColsMap,
PrivPredicate wanted) throws UserException {
boolean hasGlobal = sysAccessController.checkGlobalPriv(currentUser, wanted);
CatalogAccessController accessController = getAccessControllerOrDefault(ctl);
for (TableName tableName : tableToColsMap.keys()) {
accessController.checkColsPriv(hasGlobal, currentUser, ctl, tableName.getDb(),
tableName.getTbl(), tableToColsMap.get(tableName), wanted);
}
}
// ==== Resource ====

View File

@ -37,6 +37,7 @@ import org.apache.doris.catalog.InfoSchemaDb;
import org.apache.doris.cluster.ClusterNamespace;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.AuthenticationException;
import org.apache.doris.common.AuthorizationException;
import org.apache.doris.common.DdlException;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
@ -182,6 +183,8 @@ public class Auth implements Writable {
readLock();
try {
userManager.checkPassword(remoteUser, remoteHost, remotePasswd, randomString, currentUser);
Set<String> roles = userRoleManager.getRolesByUser(currentUser.get(0));
currentUser.get(0).setRoles(roles);
} finally {
readUnlock();
}
@ -206,13 +209,17 @@ public class Auth implements Writable {
throw new AuthenticationException(ErrorCode.ERR_ACCESS_DENIED_ERROR, remoteUser + "@" + remoteHost,
Strings.isNullOrEmpty(remotePasswd) ? "NO" : "YES");
}
return;
} else {
readLock();
try {
userManager.checkPlainPassword(remoteUser, remoteHost, remotePasswd, currentUser);
} finally {
readUnlock();
}
}
readLock();
try {
userManager.checkPlainPassword(remoteUser, remoteHost, remotePasswd, currentUser);
} finally {
readUnlock();
if (currentUser != null) {
Set<String> roles = userRoleManager.getRolesByUser(currentUser.get(0));
currentUser.get(0).setRoles(roles);
}
}
@ -306,6 +313,13 @@ public class Auth implements Writable {
}
}
// ==== Column ====
public void checkColsPriv(UserIdentity currentUser, String ctl, String db, String tbl, Set<String> cols,
PrivPredicate wanted) throws AuthorizationException {
// TODO: Support column priv
}
// ==== Resource ====
public boolean checkResourcePriv(UserIdentity currentUser, String resourceName, PrivPredicate wanted) {
if (isLdapAuthEnabled() && LdapPrivsChecker.hasResourcePrivFromLdap(currentUser, resourceName, wanted)) {

View File

@ -18,6 +18,9 @@
package org.apache.doris.mysql.privilege;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.common.AuthorizationException;
import java.util.Set;
public interface CatalogAccessController {
// ==== Catalog ====
@ -45,4 +48,19 @@ public interface CatalogAccessController {
}
boolean checkTblPriv(UserIdentity currentUser, String ctl, String db, String tbl, PrivPredicate wanted);
// ==== Column ====
default void checkColsPriv(boolean hasGlobal, UserIdentity currentUser, String ctl, String db, String tbl,
Set<String> cols, PrivPredicate wanted) throws AuthorizationException {
try {
checkColsPriv(currentUser, ctl, db, tbl, cols, wanted);
} catch (AuthorizationException e) {
if (!hasGlobal) {
throw e;
}
}
}
void checkColsPriv(UserIdentity currentUser, String ctl, String db, String tbl,
Set<String> cols, PrivPredicate wanted) throws AuthorizationException;
}

View File

@ -18,6 +18,9 @@
package org.apache.doris.mysql.privilege;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.common.AuthorizationException;
import java.util.Set;
public class InternalCatalogAccessController implements CatalogAccessController {
private Auth auth;
@ -40,4 +43,10 @@ public class InternalCatalogAccessController implements CatalogAccessController
public boolean checkTblPriv(UserIdentity currentUser, String ctl, String db, String tbl, PrivPredicate wanted) {
return auth.checkTblPriv(currentUser, ctl, db, tbl, wanted);
}
@Override
public void checkColsPriv(UserIdentity currentUser, String ctl, String db, String tbl, Set<String> cols,
PrivPredicate wanted) throws AuthorizationException {
auth.checkColsPriv(currentUser, ctl, db, tbl, cols, wanted);
}
}

View File

@ -18,8 +18,17 @@
package org.apache.doris.mysql.privilege;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.common.AuthorizationException;
import java.util.Map;
import java.util.Set;
public class RangerAccessController implements CatalogAccessController {
public RangerAccessController(Map<String, String> properties) {
}
@Override
public boolean checkCtlPriv(UserIdentity currentUser, String ctl, PrivPredicate wanted) {
// TODO
@ -38,4 +47,11 @@ public class RangerAccessController implements CatalogAccessController {
// TODO
return false;
}
@Override
public void checkColsPriv(UserIdentity currentUser, String ctl, String db, String tbl, Set<String> cols,
PrivPredicate wanted) throws AuthorizationException {
// TODO
throw new AuthorizationException("not implemented");
}
}

View File

@ -0,0 +1,27 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.mysql.privilege;
import java.util.Map;
public class RangerAccessControllerFactory implements AccessControllerFactory {
@Override
public CatalogAccessController createAccessController(Map<String, String> prop) {
return new RangerAccessController(prop);
}
}

View File

@ -547,7 +547,8 @@ public class PhysicalPlanTranslator extends DefaultPlanVisitor<PlanFragment, Pla
ExternalTable table = fileScan.getTable();
TupleDescriptor tupleDescriptor = generateTupleDesc(slotList, table, context);
tupleDescriptor.setTable(table);
ExternalFileScanNode fileScanNode = new ExternalFileScanNode(context.nextPlanNodeId(), tupleDescriptor);
// TODO(cmy): determine the needCheckColumnPriv param
ExternalFileScanNode fileScanNode = new ExternalFileScanNode(context.nextPlanNodeId(), tupleDescriptor, false);
TableName tableName = new TableName(null, "", "");
TableRef ref = new TableRef(tableName, null, null);
BaseTableRef tableRef = new BaseTableRef(ref, table, tableName);

View File

@ -118,4 +118,9 @@ public class DataGenScanNode extends ScanNode {
throw new NereidsException("Can not compute shard locations for DataGenScanNode: " + e.getMessage(), e);
}
}
@Override
public boolean needToCheckColumnPriv() {
return false;
}
}

View File

@ -31,20 +31,32 @@ import org.apache.doris.analysis.SlotId;
import org.apache.doris.analysis.SlotRef;
import org.apache.doris.analysis.StatementBase;
import org.apache.doris.analysis.StorageBackend;
import org.apache.doris.analysis.TableName;
import org.apache.doris.analysis.TupleDescriptor;
import org.apache.doris.catalog.Column;
import org.apache.doris.catalog.Env;
import org.apache.doris.catalog.PrimitiveType;
import org.apache.doris.catalog.ScalarType;
import org.apache.doris.catalog.Table;
import org.apache.doris.catalog.TableIf;
import org.apache.doris.catalog.Type;
import org.apache.doris.catalog.external.ExternalTable;
import org.apache.doris.cluster.ClusterNamespace;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.UserException;
import org.apache.doris.common.util.VectorizedUtil;
import org.apache.doris.datasource.InternalCatalog;
import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.qe.ConnectContext;
import org.apache.doris.rewrite.mvrewrite.MVSelectFailedException;
import org.apache.doris.thrift.TQueryOptions;
import org.apache.doris.thrift.TRuntimeFilterMode;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@ -164,6 +176,8 @@ public class OriginalPlanner extends Planner {
insertStmt.prepareExpressions();
}
checkColumnPrivileges(singleNodePlan);
// TODO chenhao16 , no used materialization work
// compute referenced slots before calling computeMemLayout()
//analyzer.markRefdSlots(analyzer, singleNodePlan, resultExprs, null);
@ -298,6 +312,66 @@ public class OriginalPlanner extends Planner {
}
}
private void checkColumnPrivileges(PlanNode singleNodePlan) throws UserException {
if (ConnectContext.get() == null) {
return;
}
// 1. collect all columns from all scan nodes
List<ScanNode> scanNodes = Lists.newArrayList();
singleNodePlan.collect((PlanNode planNode) -> planNode instanceof ScanNode, scanNodes);
// catalog : <db.table : column>
Map<String, HashMultimap<TableName, String>> ctlToTableColumnMap = Maps.newHashMap();
for (ScanNode scanNode : scanNodes) {
if (!scanNode.needToCheckColumnPriv()) {
continue;
}
TupleDescriptor tupleDesc = scanNode.getTupleDesc();
TableIf table = tupleDesc.getTable();
if (table == null) {
continue;
}
TableName tableName = getFullQualifiedTableNameFromTable(table);
for (SlotDescriptor slotDesc : tupleDesc.getSlots()) {
if (!slotDesc.isMaterialized()) {
continue;
}
Column column = slotDesc.getColumn();
if (column == null) {
continue;
}
HashMultimap<TableName, String> tableColumnMap = ctlToTableColumnMap.get(tableName.getCtl());
if (tableColumnMap == null) {
tableColumnMap = HashMultimap.create();
ctlToTableColumnMap.put(tableName.getCtl(), tableColumnMap);
}
tableColumnMap.put(tableName, column.getName());
LOG.debug("collect column {} in {}", column.getName(), tableName);
}
}
// 2. check privs
// TODO: only support SELECT_PRIV now
PrivPredicate wanted = PrivPredicate.SELECT;
for (Map.Entry<String, HashMultimap<TableName, String>> entry : ctlToTableColumnMap.entrySet()) {
Env.getCurrentEnv().getAccessManager().checkColumnsPriv(ConnectContext.get().getCurrentUserIdentity(),
entry.getKey(), entry.getValue(), wanted);
}
}
private TableName getFullQualifiedTableNameFromTable(TableIf table) throws AnalysisException {
if (table instanceof Table) {
String dbName = ClusterNamespace.getNameFromFullName(((Table) table).getQualifiedDbName());
if (Strings.isNullOrEmpty(dbName)) {
throw new AnalysisException("failed to get db name from table " + table.getName());
}
return new TableName(InternalCatalog.INTERNAL_CATALOG_NAME, dbName, table.getName());
} else if (table instanceof ExternalTable) {
ExternalTable extTable = (ExternalTable) table;
return new TableName(extTable.getCatalog().getName(), extTable.getDbName(), extTable.getName());
} else {
throw new AnalysisException("table " + table.getName() + " is not internal or external table instance");
}
}
/**
* If there are unassigned conjuncts, returns a SelectNode on top of root that evaluate those conjuncts; otherwise
* returns root unchanged.

View File

@ -485,4 +485,11 @@ public abstract class ScanNode extends PlanNode {
desc.getTable().getName()).add("keyRanges", "").addValue(
super.debugString()).toString();
}
// Some of scan node(eg, DataGenScanNode) does not need to check column priv
// (because the it has no corresponding catalog/db/table info)
// Subclass may override this method.
public boolean needToCheckColumnPriv() {
return true;
}
}

View File

@ -1958,7 +1958,7 @@ public class SingleNodePlanner {
case HIVE:
throw new RuntimeException("Hive external table is not supported, try to use hive catalog please");
case ICEBERG:
scanNode = new ExternalFileScanNode(ctx.getNextNodeId(), tblRef.getDesc());
scanNode = new ExternalFileScanNode(ctx.getNextNodeId(), tblRef.getDesc(), true);
break;
case HUDI:
throw new UserException(
@ -1971,7 +1971,7 @@ public class SingleNodePlanner {
break;
case HMS_EXTERNAL_TABLE:
case ICEBERG_EXTERNAL_TABLE:
scanNode = new ExternalFileScanNode(ctx.getNextNodeId(), tblRef.getDesc());
scanNode = new ExternalFileScanNode(ctx.getNextNodeId(), tblRef.getDesc(), true);
break;
case ES_EXTERNAL_TABLE:
scanNode = new EsScanNode(ctx.getNextNodeId(), tblRef.getDesc(), "EsScanNode", true);
@ -1979,6 +1979,9 @@ public class SingleNodePlanner {
case JDBC_EXTERNAL_TABLE:
scanNode = new JdbcScanNode(ctx.getNextNodeId(), tblRef.getDesc(), true);
break;
case TEST_EXTERNAL_TABLE:
scanNode = new TestExternalTableScanNode(ctx.getNextNodeId(), tblRef.getDesc());
break;
default:
break;
}

View File

@ -189,7 +189,7 @@ public class StreamLoadPlanner {
}
// create scan node
ExternalFileScanNode fileScanNode = new ExternalFileScanNode(new PlanNodeId(0), scanTupleDesc);
ExternalFileScanNode fileScanNode = new ExternalFileScanNode(new PlanNodeId(0), scanTupleDesc, false);
// 1. create file group
DataDescription dataDescription = new DataDescription(destTable.getName(), taskInfo);
dataDescription.analyzeWithoutCheckPriv(db.getFullName());

View File

@ -0,0 +1,95 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.planner;
import org.apache.doris.analysis.Analyzer;
import org.apache.doris.analysis.TupleDescriptor;
import org.apache.doris.common.UserException;
import org.apache.doris.statistics.StatisticalType;
import org.apache.doris.statistics.StatsRecursiveDerive;
import org.apache.doris.thrift.TExplainLevel;
import org.apache.doris.thrift.TPlanNode;
import org.apache.doris.thrift.TPlanNodeType;
import org.apache.doris.thrift.TScanRangeLocations;
import org.apache.doris.thrift.TTestExternalScanNode;
import com.google.common.base.MoreObjects;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.List;
public class TestExternalTableScanNode extends ScanNode {
private static final Logger LOG = LogManager.getLogger(TestExternalTableScanNode.class);
private String tableName;
public TestExternalTableScanNode(PlanNodeId id, TupleDescriptor desc) {
super(id, desc, "TestExternalTableScanNode", StatisticalType.TEST_EXTERNAL_TABLE);
tableName = desc.getTable().getName();
}
@Override
public void init(Analyzer analyzer) throws UserException {
super.init(analyzer);
computeStats(analyzer);
}
@Override
public List<TScanRangeLocations> getScanRangeLocations(long maxScanRangeLength) {
return null;
}
@Override
public String getNodeExplainString(String prefix, TExplainLevel detailLevel) {
StringBuilder output = new StringBuilder();
output.append(prefix).append("TABLE: ").append(tableName).append("\n");
return output.toString();
}
@Override
public void finalize(Analyzer analyzer) throws UserException {
}
@Override
public void computeStats(Analyzer analyzer) throws UserException {
super.computeStats(analyzer);
// even if current node scan has no data,at least on backend will be assigned when the fragment actually execute
numNodes = numNodes <= 0 ? 1 : numNodes;
StatsRecursiveDerive.getStatsRecursiveDerive().statsRecursiveDerive(this);
cardinality = (long) statsDeriveResult.getRowCount();
}
@Override
protected void toThrift(TPlanNode msg) {
msg.node_type = TPlanNodeType.TEST_EXTERNAL_SCAN_NODE;
msg.test_external_scan_node = new TTestExternalScanNode();
msg.test_external_scan_node.setTupleId(desc.getId().asInt());
msg.test_external_scan_node.setTableName(tableName);
}
@Override
protected String debugString() {
MoreObjects.ToStringHelper helper = MoreObjects.toStringHelper(this);
return helper.addValue(super.debugString()).toString();
}
@Override
public int getNumInstances() {
return 1;
}
}

View File

@ -135,9 +135,12 @@ public class ExternalFileScanNode extends ExternalScanNode {
* External file scan node for:
* 1. Query hms table
* 2. Load from file
* needCheckColumnPriv: Some of ExternalFileScanNode do not need to check column priv
* eg: s3 tvf, load scan node.
* These scan nodes do not have corresponding catalog/database/table info, so no need to do priv check
*/
public ExternalFileScanNode(PlanNodeId id, TupleDescriptor desc) {
super(id, desc, "EXTERNAL_FILE_SCAN_NODE", StatisticalType.FILE_SCAN_NODE);
public ExternalFileScanNode(PlanNodeId id, TupleDescriptor desc, boolean needCheckColumnPriv) {
super(id, desc, "EXTERNAL_FILE_SCAN_NODE", StatisticalType.FILE_SCAN_NODE, needCheckColumnPriv);
}
// Only for broker load job.

View File

@ -35,8 +35,13 @@ import java.util.List;
*/
public class ExternalScanNode extends ScanNode {
public ExternalScanNode(PlanNodeId id, TupleDescriptor desc, String planNodeName, StatisticalType statisticalType) {
// set to false means this scan node does not need to check column priv.
private boolean needCheckColumnPriv;
public ExternalScanNode(PlanNodeId id, TupleDescriptor desc, String planNodeName, StatisticalType statisticalType,
boolean needCheckColumnPriv) {
super(id, desc, planNodeName, statisticalType);
this.needCheckColumnPriv = needCheckColumnPriv;
}
@Override
@ -48,4 +53,9 @@ public class ExternalScanNode extends ScanNode {
protected void toThrift(TPlanNode msg) {
}
@Override
public boolean needToCheckColumnPriv() {
return this.needCheckColumnPriv;
}
}

View File

@ -81,6 +81,11 @@ public class MetadataScanNode extends ScanNode {
buildScanRanges();
}
@Override
public boolean needToCheckColumnPriv() {
return super.needToCheckColumnPriv();
}
private void buildScanRanges() {
if (tvf.getMetaType() == MetadataTableValuedFunction.MetaType.ICEBERG) {
IcebergTableValuedFunction icebergTvf = (IcebergTableValuedFunction) tvf;

View File

@ -48,4 +48,5 @@ public enum StatisticalType {
FILE_SCAN_NODE,
METADATA_SCAN_NODE,
JDBC_SCAN_NODE,
TEST_EXTERNAL_TABLE,
}

View File

@ -207,7 +207,7 @@ public abstract class ExternalFileTableValuedFunction extends TableValuedFunctio
@Override
public ScanNode getScanNode(PlanNodeId id, TupleDescriptor desc) {
return new ExternalFileScanNode(id, desc);
return new ExternalFileScanNode(id, desc, false);
}
@Override

View File

@ -0,0 +1,299 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.datasource;
import org.apache.doris.analysis.CreateCatalogStmt;
import org.apache.doris.analysis.CreateDbStmt;
import org.apache.doris.analysis.CreateRoleStmt;
import org.apache.doris.analysis.CreateTableStmt;
import org.apache.doris.analysis.CreateUserStmt;
import org.apache.doris.analysis.CreateViewStmt;
import org.apache.doris.analysis.DropCatalogStmt;
import org.apache.doris.analysis.GrantStmt;
import org.apache.doris.analysis.ShowCatalogStmt;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.catalog.Column;
import org.apache.doris.catalog.Env;
import org.apache.doris.catalog.PrimitiveType;
import org.apache.doris.cluster.ClusterNamespace;
import org.apache.doris.common.AuthorizationException;
import org.apache.doris.common.FeConstants;
import org.apache.doris.datasource.test.TestExternalCatalog.TestCatalogProvider;
import org.apache.doris.mysql.privilege.AccessControllerFactory;
import org.apache.doris.mysql.privilege.Auth;
import org.apache.doris.mysql.privilege.CatalogAccessController;
import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.qe.ConnectContext;
import org.apache.doris.qe.ShowResultSet;
import org.apache.doris.utframe.TestWithFeService;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.junit.Assert;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class ColumnPrivTest extends TestWithFeService {
private static Auth auth;
private static Env env;
private CatalogMgr mgr;
private ConnectContext rootCtx;
@Override
protected void runBeforeAll() throws Exception {
FeConstants.runningUnitTest = true;
mgr = Env.getCurrentEnv().getCatalogMgr();
rootCtx = createDefaultCtx();
env = Env.getCurrentEnv();
auth = env.getAuth();
// 1. create test catalog
CreateCatalogStmt testCatalog = (CreateCatalogStmt) parseAndAnalyzeStmt(
"create catalog test1 properties(\n"
+ " \"type\" = \"test\",\n"
+ " \"catalog_provider.class\" "
+ "= \"org.apache.doris.datasource.ColumnPrivTest$MockedCatalogProvider\",\n"
+ " \"access_controller.class\" "
+ "= \"org.apache.doris.datasource.ColumnPrivTest$TestAccessControllerFactory\",\n"
+ " \"access_controller.properties.key1\" = \"val1\",\n"
+ " \"access_controller.properties.key2\" = \"val2\"\n"
+ ");",
rootCtx);
env.getCatalogMgr().createCatalog(testCatalog);
// 2. create internal db and tbl
CreateDbStmt createDbStmt = (CreateDbStmt) parseAndAnalyzeStmt("create database innerdb1");
env.createDb(createDbStmt);
createDbStmt = (CreateDbStmt) parseAndAnalyzeStmt("create database innerdb2");
env.createDb(createDbStmt);
CreateTableStmt createTableStmt = (CreateTableStmt) parseAndAnalyzeStmt(
"create table innerdb1.innertbl11\n"
+ "(\n"
+ " col1 int, \n"
+ " col2 string\n"
+ ")\n"
+ "distributed by hash(col1) buckets 1\n"
+ "properties(\"replication_num\" = \"1\");", rootCtx);
env.createTable(createTableStmt);
createTableStmt = (CreateTableStmt) parseAndAnalyzeStmt(
"create table innerdb1.innertbl12\n"
+ "(\n"
+ " col3 int, \n"
+ " col4 string\n"
+ ")\n"
+ "distributed by hash(col3) buckets 1\n"
+ "properties(\"replication_num\" = \"1\");", rootCtx);
env.createTable(createTableStmt);
createTableStmt = (CreateTableStmt) parseAndAnalyzeStmt(
"create table innerdb2.innertbl21\n"
+ "(\n"
+ " col5 int, \n"
+ " col6 string\n"
+ ")\n"
+ "distributed by hash(col5) buckets 1\n"
+ "properties(\"replication_num\" = \"1\");", rootCtx);
env.createTable(createTableStmt);
}
@Override
protected void runAfterAll() throws Exception {
super.runAfterAll();
rootCtx.setThreadLocalInfo();
Assert.assertTrue(env.getAccessManager().checkIfAccessControllerExist("test1"));
DropCatalogStmt stmt = (DropCatalogStmt) parseAndAnalyzeStmt("drop catalog test1");
env.getCatalogMgr().dropCatalog(stmt);
Assert.assertFalse(env.getAccessManager().checkIfAccessControllerExist("test1"));
}
@Test
public void testColumnPrivs() throws Exception {
String showCatalogSql = "SHOW CATALOGS";
ShowCatalogStmt showStmt = (ShowCatalogStmt) parseAndAnalyzeStmt(showCatalogSql);
ShowResultSet showResultSet = mgr.showCatalogs(showStmt);
Assertions.assertEquals(2, showResultSet.getResultRows().size());
CreateRoleStmt createRole1 = (CreateRoleStmt) parseAndAnalyzeStmt("create role role1;", rootCtx);
auth.createRole(createRole1);
GrantStmt grantRole = (GrantStmt) parseAndAnalyzeStmt("grant select_priv on test1.*.* to role 'role1';",
rootCtx);
auth.grant(grantRole);
grantRole = (GrantStmt) parseAndAnalyzeStmt(
"grant select_priv on internal.innerdb1.innertbl11 to role 'role1';", rootCtx);
auth.grant(grantRole);
grantRole = (GrantStmt) parseAndAnalyzeStmt(
"grant select_priv on internal.innerdb1.v1 to role 'role1';", rootCtx);
auth.grant(grantRole);
auth.createUser((CreateUserStmt) parseAndAnalyzeStmt(
"create user 'user1'@'%' identified by 'pwd1' default role 'role1';", rootCtx));
// create a view
CreateViewStmt viewStmt = (CreateViewStmt) parseAndAnalyzeStmt(
"create view innerdb1.v1 as select * from test1.db1.tbl11", rootCtx);
env.createView(viewStmt);
// Now we have
// internal.innerdb1
// innertbl11: col1(int), col2(string)
// innertbl12: col2(int), col4(string)
// internal.innerdb2
// innertbl21: col5(int), col6(string)
// test1.db1
// tbl11: a11(bigint), a12(string), a13(float)
// tbl12: b21(bigint), b22(string), b23(float)
// test1.db2
// tbl21: c11(bigint), c12(string), c13(float)
UserIdentity user1 = UserIdentity.createAnalyzedUserIdentWithIp("default_cluster:user1", "%");
Set<String> roles = Sets.newHashSet();
roles.add("role1");
user1.setRoles(roles);
ConnectContext user1Ctx = createCtx(user1, "127.0.0.1");
// 1. query inner table
testSql(user1Ctx, "select * from innerdb1.innertbl11", "0:VOlapScanNode");
// 2. query external table, without a11 column priv
testSql(user1Ctx, "select * from test1.db1.tbl11", "Access deny to column a11");
// 3. query external table, not query column a12
testSql(user1Ctx, "select a12 from test1.db1.tbl11", "TABLE: tbl11");
// change to test1.db1
user1Ctx.changeDefaultCatalog("test1");
user1Ctx.setDatabase("default_cluster:db1");
testSql(user1Ctx, "select a12 from tbl11 where a11 > 0;", "Access deny to column a11");
testSql(user1Ctx, "select sum(a13) from db1.tbl11 group by a11;", "Access deny to column a11");
testSql(user1Ctx, "select sum(a13) x from test1.db1.tbl11 group by a11 having x > 0;",
"Access deny to column a11");
testSql(user1Ctx, "select a12 from tbl11 where abs(a11) > 0;", "Access deny to column a11");
// TODO: how to handle count(*) when setting column privilege?
// testSql(user1Ctx, "select count(*) from tbl11;", "Access deny to column a11");
// change to internal.innerdb1
user1Ctx.changeDefaultCatalog("internal");
user1Ctx.setDatabase("default_cluster:innerdb1");
testSql(user1Ctx, "select sum(a13) x from test1.db1.tbl11 group by a11 having x > 0;",
"Access deny to column a11");
testSql(user1Ctx, "with cte1 as (select a11 from test1.db1.tbl11) select * from cte1;",
"Access deny to column a11");
testSql(user1Ctx, "select a12 from (select * from test1.db1.tbl11) x", "TABLE: tbl11");
testSql(user1Ctx, "select * from v1", "Access deny to column a11");
testSql(user1Ctx, "select * from numbers(\"number\" = \"1\");", "0:VDataGenScanNode");
}
private void testSql(ConnectContext ctx, String sql, String expectedMsg) throws Exception {
String res = getSQLPlanOrErrorMsg(ctx, "explain " + sql, false);
System.out.println(res);
Assert.assertTrue(res.contains(expectedMsg));
}
public static class TestAccessControllerFactory implements AccessControllerFactory {
@Override
public CatalogAccessController createAccessController(Map<String, String> prop) {
return new TestAccessController(prop);
}
public static class TestAccessController implements CatalogAccessController {
private Map<String, String> prop;
public TestAccessController(Map<String, String> prop) {
this.prop = prop;
}
@Override
public boolean checkCtlPriv(UserIdentity currentUser, String ctl, PrivPredicate wanted) {
return false;
}
@Override
public boolean checkDbPriv(UserIdentity currentUser, String ctl, String db, PrivPredicate wanted) {
return false;
}
@Override
public boolean checkTblPriv(UserIdentity currentUser, String ctl, String db, String tbl,
PrivPredicate wanted) {
if (ClusterNamespace.getNameFromFullName(currentUser.getQualifiedUser()).equals("user1")) {
if (ctl.equals("test1")) {
if (ClusterNamespace.getNameFromFullName(db).equals("db1")) {
if (tbl.equals("tbl11")) {
return true;
}
}
}
}
return false;
}
@Override
public void checkColsPriv(UserIdentity currentUser, String ctl, String db, String tbl, Set<String> cols,
PrivPredicate wanted) throws AuthorizationException {
if (currentUser.getQualifiedUser().contains("user1")) {
if (ctl.equals("test1")) {
if (db.equals("db1")) {
if (tbl.equals("tbl11")) {
if (cols.contains("a11")) {
throw new AuthorizationException("Access deny to column a11");
}
}
}
}
}
}
}
}
public static class MockedCatalogProvider implements TestCatalogProvider {
public static final Map<String, Map<String, List<Column>>> MOCKED_META;
static {
MOCKED_META = Maps.newHashMap();
Map<String, List<Column>> tblSchemaMap1 = Maps.newHashMap();
// db1
tblSchemaMap1.put("tbl11", Lists.newArrayList(
new Column("a11", PrimitiveType.BIGINT),
new Column("a12", PrimitiveType.STRING),
new Column("a13", PrimitiveType.FLOAT)
));
tblSchemaMap1.put("tbl12", Lists.newArrayList(
new Column("b21", PrimitiveType.BIGINT),
new Column("b22", PrimitiveType.STRING),
new Column("b23", PrimitiveType.FLOAT)
));
MOCKED_META.put("db1", tblSchemaMap1);
// db2
Map<String, List<Column>> tblSchemaMap2 = Maps.newHashMap();
tblSchemaMap2.put("tbl21", Lists.newArrayList(
new Column("c11", PrimitiveType.BIGINT),
new Column("c12", PrimitiveType.STRING),
new Column("c13", PrimitiveType.FLOAT)
));
MOCKED_META.put("db2", tblSchemaMap2);
}
@Override
public Map<String, Map<String, List<Column>>> getMetadata() {
return MOCKED_META;
}
}
}

View File

@ -441,16 +441,21 @@ public abstract class TestWithFeService {
}
public String getSQLPlanOrErrorMsg(String sql, boolean isVerbose) throws Exception {
connectContext.getState().reset();
StmtExecutor stmtExecutor = new StmtExecutor(connectContext, sql);
connectContext.setExecutor(stmtExecutor);
return getSQLPlanOrErrorMsg(connectContext, sql, isVerbose);
}
public String getSQLPlanOrErrorMsg(ConnectContext ctx, String sql, boolean isVerbose) throws Exception {
ctx.setThreadLocalInfo();
ctx.getState().reset();
StmtExecutor stmtExecutor = new StmtExecutor(ctx, sql);
ctx.setExecutor(stmtExecutor);
ConnectContext.get().setExecutor(stmtExecutor);
stmtExecutor.execute();
if (connectContext.getState().getStateType() != QueryState.MysqlStateType.ERR) {
if (ctx.getState().getStateType() != QueryState.MysqlStateType.ERR) {
Planner planner = stmtExecutor.planner();
return planner.getExplainString(new ExplainOptions(isVerbose, false));
} else {
return connectContext.getState().getErrorMessage();
return ctx.getState().getErrorMessage();
}
}

View File

@ -56,6 +56,7 @@ enum TPlanNodeType {
DATA_GEN_SCAN_NODE,
FILE_SCAN_NODE,
JDBC_SCAN_NODE,
TEST_EXTERNAL_SCAN_NODE,
}
// phases of an execution node
@ -430,7 +431,6 @@ struct TJdbcScanNode {
4: optional Types.TOdbcTableType table_type
}
struct TBrokerScanNode {
1: required Types.TTupleId tuple_id
@ -537,6 +537,11 @@ struct TMetaScanNode {
4: optional string table
}
struct TTestExternalScanNode {
1: optional Types.TTupleId tuple_id
2: optional string table_name
}
struct TSortInfo {
1: required list<Exprs.TExpr> ordering_exprs
2: required list<bool> is_asc_order
@ -1061,6 +1066,7 @@ struct TPlanNode {
44: optional TFileScanNode file_scan_node
45: optional TJdbcScanNode jdbc_scan_node
46: optional TNestedLoopJoinNode nested_loop_join_node
47: optional TTestExternalScanNode test_external_scan_node
101: optional list<Exprs.TExpr> projections
102: optional Types.TTupleId output_tuple_id

View File

@ -567,7 +567,8 @@ enum TTableType {
HIVE_TABLE,
ICEBERG_TABLE,
HUDI_TABLE,
JDBC_TABLE
JDBC_TABLE,
TEST_EXTERNAL_TABLE,
}
enum TOdbcTableType {