[feature-wip](multi-catalog) support to switch catalog (#10381)

Add `switch catalog` stmt with privilege check
This commit is contained in:
Ashin Gau
2022-06-24 10:42:13 +08:00
committed by GitHub
parent f15d84335c
commit 516f5b1789
22 changed files with 452 additions and 65 deletions

View File

@ -278,7 +278,8 @@ terminal String KW_ADD, KW_ADMIN, KW_AFTER, KW_AGGREGATE, KW_ALIAS, KW_ALL, KW_A
KW_WARNINGS, KW_WEEK, KW_WHEN, KW_WHITELIST, KW_WHERE, KW_WITH, KW_WORK, KW_WRITE,
KW_YEAR,
KW_NOT_NULL,
KW_CATALOG, KW_CATALOGS;
KW_CATALOG, KW_CATALOGS,
KW_SWITCH;
terminal COMMA, COLON, DOT, DOTDOTDOT, AT, STAR, LPAREN, RPAREN, SEMICOLON, LBRACKET, RBRACKET, DIVIDE, MOD, ADD, SUBTRACT;
terminal BITAND, BITOR, BITXOR, BITNOT;
@ -301,7 +302,7 @@ nonterminal StatementBase stmt, show_stmt, show_param, help_stmt, load_stmt,
show_routine_load_stmt, show_routine_load_task_stmt, show_create_routine_load_stmt,
describe_stmt, alter_stmt,
use_stmt, kill_stmt, drop_stmt, recover_stmt, grant_stmt, revoke_stmt, create_stmt, set_stmt, sync_stmt, cancel_stmt, cancel_param, delete_stmt,
link_stmt, migrate_stmt, enter_stmt, transaction_stmt, unsupported_stmt, export_stmt, admin_stmt, truncate_stmt,
link_stmt, migrate_stmt, switch_stmt, enter_stmt, transaction_stmt, unsupported_stmt, export_stmt, admin_stmt, truncate_stmt,
import_columns_stmt, import_delete_on_stmt, import_sequence_stmt, import_where_stmt, install_plugin_stmt, uninstall_plugin_stmt,
import_preceding_filter_stmt, unlock_tables_stmt, lock_tables_stmt, refresh_stmt;
@ -670,6 +671,8 @@ stmt ::=
{: RESULT = query; :}
| migrate_stmt:query
{: RESULT = query; :}
| switch_stmt:stmt
{: RESULT = stmt; :}
| enter_stmt:enter
{: RESULT = enter; :}
| query_stmt:query
@ -3422,6 +3425,14 @@ opt_set_qualifier ::=
{: RESULT = Qualifier.ALL; :}
;
// Change catalog
switch_stmt ::=
KW_SWITCH ident:catalog
{:
RESULT = new SwitchStmt(catalog);
:}
;
// Change cluster
enter_stmt ::=
KW_ENTER ident:cluster

View File

@ -17,18 +17,14 @@
package org.apache.doris.analysis;
import org.apache.doris.analysis.CompoundPredicate.Operator;
import org.apache.doris.catalog.Catalog;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.Config;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
import org.apache.doris.common.FeNameFormat;
import org.apache.doris.common.UserException;
import org.apache.doris.common.util.Util;
import org.apache.doris.datasource.InternalDataSource;
import org.apache.doris.mysql.privilege.PaloPrivilege;
import org.apache.doris.mysql.privilege.PrivBitSet;
import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.qe.ConnectContext;
@ -57,21 +53,15 @@ public class AlterCatalogNameStmt extends DdlStmt {
@Override
public void analyze(Analyzer analyzer) throws UserException {
super.analyze(analyzer);
if (!Config.enable_multi_catalog) {
throw new AnalysisException("The multi-catalog feature is still in experiment, and you can enable it "
+ "manually by set fe configuration named `enable_multi_catalog` to be ture.");
}
if (Strings.isNullOrEmpty(catalogName)) {
throw new AnalysisException("Datasource name is not set");
}
Util.checkCatalogAllRules(catalogName);
if (catalogName.equals(InternalDataSource.INTERNAL_DS_NAME)) {
throw new AnalysisException("Internal catalog can't be alter.");
}
if (!Catalog.getCurrentCatalog().getAuth().checkGlobalPriv(ConnectContext.get(),
PrivPredicate.of(PrivBitSet.of(PaloPrivilege.ADMIN_PRIV, PaloPrivilege.ALTER_PRIV), Operator.OR))) {
ErrorReport.reportAnalysisException(ErrorCode.ERR_DBACCESS_DENIED_ERROR,
if (!Catalog.getCurrentCatalog().getAuth().checkCtlPriv(
ConnectContext.get(), catalogName, PrivPredicate.ALTER)) {
ErrorReport.reportAnalysisException(ErrorCode.ERR_CATALOG_ACCESS_DENIED,
analyzer.getQualifiedUser(), catalogName);
}

View File

@ -17,14 +17,17 @@
package org.apache.doris.analysis;
import org.apache.doris.catalog.Catalog;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.Config;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
import org.apache.doris.common.FeNameFormat;
import org.apache.doris.common.UserException;
import org.apache.doris.common.util.PrintableMap;
import org.apache.doris.common.util.Util;
import org.apache.doris.datasource.InternalDataSource;
import com.google.common.base.Strings;
import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.qe.ConnectContext;
import java.util.Map;
@ -51,12 +54,11 @@ public class AlterCatalogPropertyStmt extends DdlStmt {
@Override
public void analyze(Analyzer analyzer) throws UserException {
super.analyze(analyzer);
if (!Config.enable_multi_catalog) {
throw new AnalysisException("The multi-catalog feature is still in experiment, and you can enable it "
+ "manually by set fe configuration named `enable_multi_catalog` to be ture.");
}
if (Strings.isNullOrEmpty(catalogName)) {
throw new AnalysisException("Datasource name is not set");
Util.checkCatalogAllRules(catalogName);
if (!Catalog.getCurrentCatalog().getAuth().checkCtlPriv(
ConnectContext.get(), catalogName, PrivPredicate.ALTER)) {
ErrorReport.reportAnalysisException(ErrorCode.ERR_CATALOG_ACCESS_DENIED,
analyzer.getQualifiedUser(), catalogName);
}
if (catalogName.equals(InternalDataSource.INTERNAL_DS_NAME)) {

View File

@ -19,12 +19,12 @@ package org.apache.doris.analysis;
import org.apache.doris.catalog.Catalog;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.Config;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
import org.apache.doris.common.FeNameFormat;
import org.apache.doris.common.UserException;
import org.apache.doris.common.util.PrintableMap;
import org.apache.doris.common.util.Util;
import org.apache.doris.datasource.InternalDataSource;
import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.qe.ConnectContext;
@ -64,17 +64,14 @@ public class CreateCatalogStmt extends DdlStmt {
@Override
public void analyze(Analyzer analyzer) throws UserException {
super.analyze(analyzer);
if (!Config.enable_multi_catalog) {
throw new AnalysisException("The multi-catalog feature is still in experiment, and you can enable it "
+ "manually by set fe configuration named `enable_multi_catalog` to be ture.");
}
Util.checkCatalogAllRules(catalogName);
if (catalogName.equals(InternalDataSource.INTERNAL_DS_NAME)) {
throw new AnalysisException("Internal catalog name can't be create.");
}
FeNameFormat.checkCommonName("catalog", catalogName);
if (!Catalog.getCurrentCatalog().getAuth().checkGlobalPriv(ConnectContext.get(), PrivPredicate.CREATE)) {
ErrorReport.reportAnalysisException(ErrorCode.ERR_DBACCESS_DENIED_ERROR,
if (!Catalog.getCurrentCatalog().getAuth().checkCtlPriv(
ConnectContext.get(), catalogName, PrivPredicate.CREATE)) {
ErrorReport.reportAnalysisException(ErrorCode.ERR_CATALOG_ACCESS_DENIED,
analyzer.getQualifiedUser(), catalogName);
}
FeNameFormat.checkCatalogProperties(properties);

View File

@ -19,16 +19,14 @@ package org.apache.doris.analysis;
import org.apache.doris.catalog.Catalog;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.Config;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
import org.apache.doris.common.UserException;
import org.apache.doris.common.util.Util;
import org.apache.doris.datasource.InternalDataSource;
import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.qe.ConnectContext;
import com.google.common.base.Strings;
/**
* Statement for drop a catalog.
*/
@ -52,21 +50,16 @@ public class DropCatalogStmt extends DdlStmt {
@Override
public void analyze(Analyzer analyzer) throws UserException {
super.analyze(analyzer);
if (!Config.enable_multi_catalog) {
throw new AnalysisException("The multi-catalog feature is still in experiment, and you can enable it "
+ "manually by set fe configuration named `enable_multi_catalog` to be ture.");
}
if (Strings.isNullOrEmpty(catalogName)) {
throw new AnalysisException("Datasource name is not set");
}
Util.checkCatalogAllRules(catalogName);
if (catalogName.equals(InternalDataSource.INTERNAL_DS_NAME)) {
throw new AnalysisException("Internal catalog can't be drop.");
}
if (!Catalog.getCurrentCatalog().getAuth().checkGlobalPriv(ConnectContext.get(), PrivPredicate.DROP)) {
ErrorReport.reportAnalysisException(ErrorCode.ERR_DBACCESS_DENIED_ERROR,
ConnectContext.get().getQualifiedUser(), catalogName);
if (!Catalog.getCurrentCatalog().getAuth().checkCtlPriv(
ConnectContext.get(), catalogName, PrivPredicate.DROP)) {
ErrorReport.reportAnalysisException(ErrorCode.ERR_CATALOG_ACCESS_DENIED,
analyzer.getQualifiedUser(), catalogName);
}
}

View File

@ -168,15 +168,21 @@ public class GrantStmt extends DdlStmt {
if (!Catalog.getCurrentCatalog().getAuth().checkGlobalPriv(ConnectContext.get(), PrivPredicate.GRANT)) {
ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR, "GRANT");
}
} else if (tblPattern.getPrivLevel() == PrivLevel.CATALOG) {
if (!Catalog.getCurrentCatalog().getAuth().checkCtlPriv(ConnectContext.get(),
tblPattern.getQualifiedCtl(), PrivPredicate.GRANT)) {
ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR, "GRANT");
}
} else if (tblPattern.getPrivLevel() == PrivLevel.DATABASE) {
if (!Catalog.getCurrentCatalog().getAuth().checkDbPriv(ConnectContext.get(),
tblPattern.getQualifiedDb(), PrivPredicate.GRANT)) {
tblPattern.getQualifiedCtl(), tblPattern.getQualifiedDb(), PrivPredicate.GRANT)) {
ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR, "GRANT");
}
} else {
// table level
if (!Catalog.getCurrentCatalog().getAuth().checkTblPriv(ConnectContext.get(),
tblPattern.getQualifiedDb(), tblPattern.getTbl(), PrivPredicate.GRANT)) {
tblPattern.getQualifiedCtl(), tblPattern.getQualifiedDb(),
tblPattern.getTbl(), PrivPredicate.GRANT)) {
ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR, "GRANT");
}
}

View File

@ -0,0 +1,65 @@
// 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.analysis;
import org.apache.doris.catalog.Catalog;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
import org.apache.doris.common.UserException;
import org.apache.doris.common.util.Util;
import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.qe.ConnectContext;
public class SwitchStmt extends StatementBase {
private final String catalogName;
public SwitchStmt(String catalogName) {
this.catalogName = catalogName;
}
public String getCatalogName() {
return catalogName;
}
@Override
public String toSql() {
return "SWITCH `" + catalogName + "`";
}
@Override
public String toString() {
return toSql();
}
public void analyze(Analyzer analyzer) throws UserException {
super.analyze(analyzer);
Util.checkCatalogAllRules(catalogName);
if (!Catalog.getCurrentCatalog().getAuth().checkCtlPriv(
ConnectContext.get(), catalogName, PrivPredicate.SHOW)) {
ErrorReport.reportAnalysisException(
ErrorCode.ERR_CATALOG_ACCESS_DENIED, analyzer.getQualifiedUser(), catalogName);
}
}
@Override
public RedirectStatus getRedirectStatus() {
return RedirectStatus.NO_FORWARD;
}
}

View File

@ -4218,6 +4218,15 @@ public class Catalog {
this.alter.getClusterHandler().cancel(stmt);
}
// Switch catalog of this sesseion.
public void changeCatalog(ConnectContext ctx, String catalogName) throws DdlException {
if (dataSourceMgr.getCatalogNullable(catalogName) == null) {
throw new DdlException(ErrorCode.ERR_UNKNOWN_CATALOG.formatErrorMsg(
catalogName), ErrorCode.ERR_UNKNOWN_CATALOG);
}
ctx.changeDefaultCatalog(catalogName);
}
// Change current database of this session.
public void changeDb(ConnectContext ctx, String qualifiedDb) throws DdlException {
if (!auth.checkDbPriv(ctx, qualifiedDb, PrivPredicate.SHOW)) {

View File

@ -1686,7 +1686,10 @@ public enum ErrorCode {
+ "Use `SHOW PARTITIONS FROM %s` to see the currently partitions of this table. "),
ERROR_SQL_AND_LIMITATIONS_SET_IN_ONE_RULE(5084, new byte[]{'4', '2', '0', '0', '0'},
"sql/sqlHash and partition_num/tablet_num/cardinality cannot be set in one rule."),
ERR_WRONG_CATALOG_NAME(5085, new byte[]{'4', '2', '0', '0', '0'}, "Incorrect catalog name '%s'");
ERR_WRONG_CATALOG_NAME(5085, new byte[]{'4', '2', '0', '0', '0'}, "Incorrect catalog name '%s'"),
ERR_UNKNOWN_CATALOG(5086, new byte[]{'4', '2', '0', '0', '0'}, "Unknown catalog '%s'"),
ERR_CATALOG_ACCESS_DENIED(5087, new byte[]{'4', '2', '0', '0', '0'},
"Access denied for user '%s' to catalog '%s'");
// This is error code
private final int code;

View File

@ -20,6 +20,9 @@ package org.apache.doris.common.util;
import org.apache.doris.catalog.Column;
import org.apache.doris.catalog.PrimitiveType;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.Config;
import org.apache.doris.common.FeNameFormat;
import org.apache.doris.datasource.InternalDataSource;
import org.apache.doris.qe.ConnectContext;
import com.google.common.base.Preconditions;
@ -457,4 +460,29 @@ public class Util {
}
return s;
}
/**
* Multi-catalog feature is in experiment, and should be enabled by user manually.
*/
public static void checkCatalogEnabled() throws AnalysisException {
if (!Config.enable_multi_catalog) {
throw new AnalysisException("The multi-catalog feature is still in experiment, and you can enable it "
+ "manually by set fe configuration named `enable_multi_catalog` to be ture.");
}
}
/**
* Check all rules of catalog.
*/
public static void checkCatalogAllRules(String catalog) throws AnalysisException {
checkCatalogEnabled();
if (Strings.isNullOrEmpty(catalog)) {
throw new AnalysisException("Catalog name is empty.");
}
if (!catalog.equals(InternalDataSource.INTERNAL_DS_NAME)) {
FeNameFormat.checkCommonName("catalog", catalog);
}
}
}

View File

@ -25,11 +25,15 @@ import org.apache.doris.analysis.ShowCatalogStmt;
import org.apache.doris.catalog.Catalog;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.DdlException;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
import org.apache.doris.common.UserException;
import org.apache.doris.common.io.Text;
import org.apache.doris.common.io.Writable;
import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.persist.OperationType;
import org.apache.doris.persist.gson.GsonUtils;
import org.apache.doris.qe.ConnectContext;
import org.apache.doris.qe.ShowResultSet;
import com.google.common.collect.Lists;
@ -150,6 +154,13 @@ public class DataSourceMgr implements Writable {
Catalog.getCurrentCatalog().getEditLog().logDatasourceLog(OperationType.OP_ALTER_DS_PROPS, log);
}
/**
* Get catalog, or null if not exists.
*/
public DataSourceIf getCatalogNullable(String catalogName) {
return nameToCatalogs.get(catalogName);
}
/**
* List all catalog or get the special catalog with a name.
*/
@ -159,16 +170,24 @@ public class DataSourceMgr implements Writable {
try {
if (showStmt.getCatalogName() == null) {
for (DataSourceIf ds : nameToCatalogs.values()) {
List<String> row = Lists.newArrayList();
row.add(ds.getName());
row.add(ds.getType());
rows.add(row);
if (Catalog.getCurrentCatalog().getAuth().checkCtlPriv(
ConnectContext.get(), ds.getName(), PrivPredicate.SHOW)) {
List<String> row = Lists.newArrayList();
row.add(ds.getName());
row.add(ds.getType());
rows.add(row);
}
}
} else {
if (!nameToCatalogs.containsKey(showStmt.getCatalogName())) {
throw new AnalysisException("No catalog found with name: " + showStmt.getCatalogName());
}
DataSourceIf ds = nameToCatalogs.get(showStmt.getCatalogName());
if (!Catalog.getCurrentCatalog().getAuth().checkCtlPriv(
ConnectContext.get(), ds.getName(), PrivPredicate.SHOW)) {
ErrorReport.reportAnalysisException(ErrorCode.ERR_CATALOG_ACCESS_DENIED,
ConnectContext.get().getQualifiedUser(), ds.getName());
}
for (Map.Entry<String, String> elem : ds.getProperties().entrySet()) {
List<String> row = Lists.newArrayList();
row.add(elem.getKey());

View File

@ -21,6 +21,7 @@ import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.common.io.Text;
import org.apache.doris.qe.ConnectContext;
import com.google.common.base.Preconditions;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@ -66,6 +67,23 @@ public class DbPrivTable extends PrivTable {
savedPrivs.or(matchedEntry.getPrivSet());
}
public boolean hasPrivsOfCatalog(UserIdentity currentUser, String ctl) {
for (PrivEntry entry : entries) {
DbPrivEntry dbPrivEntry = (DbPrivEntry) entry;
if (!dbPrivEntry.match(currentUser, true)) {
continue;
}
// check catalog
Preconditions.checkState(!dbPrivEntry.isAnyCtl());
if (dbPrivEntry.getCtlPattern().match(ctl)) {
return true;
}
}
return false;
}
public boolean hasClusterPriv(ConnectContext ctx, String clusterName) {
for (PrivEntry entry : entries) {
DbPrivEntry dbPrivEntry = (DbPrivEntry) entry;

View File

@ -353,6 +353,35 @@ public class PaloAuth implements Writable {
return false;
}
public boolean checkCtlPriv(ConnectContext ctx, String ctl, PrivPredicate wanted) {
return checkCtlPriv(ctx.getCurrentUserIdentity(), ctl, wanted);
}
public boolean checkCtlPriv(UserIdentity currentUser, String ctl, PrivPredicate wanted) {
if (!Config.enable_auth_check) {
return true;
}
if (wanted.getPrivs().containsNodePriv()) {
LOG.debug("should not check NODE priv in catalog level. user: {}, catalog: {}",
currentUser, ctl);
return false;
}
PrivBitSet savedPrivs = PrivBitSet.of();
if (checkGlobalInternal(currentUser, wanted, savedPrivs)
|| checkCatalogInternal(currentUser, ctl, wanted, savedPrivs)) {
return true;
}
// if user has any privs of databases or tables in this catalog, and the wanted priv is SHOW, return true
if (ctl != null && wanted == PrivPredicate.SHOW && checkAnyPrivWithinCatalog(currentUser, ctl)) {
return true;
}
LOG.debug("failed to get wanted privs: {}, granted: {}", wanted, savedPrivs);
return false;
}
public boolean checkDbPriv(ConnectContext ctx, String qualifiedDb, PrivPredicate wanted) {
return checkDbPriv(ctx.getCurrentUserIdentity(), qualifiedDb, wanted);
}
@ -361,6 +390,10 @@ public class PaloAuth implements Writable {
return checkDbPriv(currentUser, DEFAULT_CATALOG, db, wanted);
}
public boolean checkDbPriv(ConnectContext ctx, String ctl, String db, PrivPredicate wanted) {
return checkDbPriv(ctx.getCurrentUserIdentity(), ctl, db, wanted);
}
/*
* Check if 'user'@'host' on 'db' has 'wanted' priv.
* If the given db is null, which means it will no check if database name is matched.
@ -383,7 +416,7 @@ public class PaloAuth implements Writable {
}
// if user has any privs of table in this db, and the wanted priv is SHOW, return true
if (ctl != null && db != null && wanted == PrivPredicate.SHOW && checkTblWithDb(currentUser, ctl, db)) {
if (ctl != null && db != null && wanted == PrivPredicate.SHOW && checkAnyPrivWithinDb(currentUser, ctl, db)) {
return true;
}
@ -391,12 +424,27 @@ public class PaloAuth implements Writable {
return false;
}
/*
* User may not have privs on a catalog, but have privs of databases or tables in this catalog.
* So we have to check if user has any privs of databases or tables in this catalog.
* if so, the catalog should be visible to this user.
*/
private boolean checkAnyPrivWithinCatalog(UserIdentity currentUser, String ctl) {
readLock();
try {
return dbPrivTable.hasPrivsOfCatalog(currentUser, ctl)
|| tablePrivTable.hasPrivsOfCatalog(currentUser, ctl);
} finally {
readUnlock();
}
}
/*
* User may not have privs on a database, but have privs of tables in this database.
* So we have to check if user has any privs of tables in this database.
* if so, the database should be visible to this user.
*/
private boolean checkTblWithDb(UserIdentity currentUser, String ctl, String db) {
private boolean checkAnyPrivWithinDb(UserIdentity currentUser, String ctl, String db) {
readLock();
try {
return (isLdapAuthEnabled() && LdapPrivsChecker.hasPrivsOfDb(currentUser, db))

View File

@ -69,6 +69,23 @@ public class TablePrivTable extends PrivTable {
savedPrivs.or(matchedEntry.getPrivSet());
}
public boolean hasPrivsOfCatalog(UserIdentity currentUser, String ctl) {
for (PrivEntry entry : entries) {
TablePrivEntry tblPrivEntry = (TablePrivEntry) entry;
if (!tblPrivEntry.match(currentUser, true)) {
continue;
}
// check catalog
Preconditions.checkState(!tblPrivEntry.isAnyCtl());
if (tblPrivEntry.getCtlPattern().match(ctl)) {
return true;
}
}
return false;
}
public boolean hasPrivsOfDb(UserIdentity currentUser, String ctl, String db) {
for (PrivEntry entry : entries) {
TablePrivEntry tblPrivEntry = (TablePrivEntry) entry;

View File

@ -44,6 +44,7 @@ import org.apache.doris.analysis.SqlScanner;
import org.apache.doris.analysis.StatementBase;
import org.apache.doris.analysis.StmtRewriter;
import org.apache.doris.analysis.StringLiteral;
import org.apache.doris.analysis.SwitchStmt;
import org.apache.doris.analysis.TransactionBeginStmt;
import org.apache.doris.analysis.TransactionCommitStmt;
import org.apache.doris.analysis.TransactionRollbackStmt;
@ -414,6 +415,8 @@ public class StmtExecutor implements ProfileWriter {
handleSetStmt();
} else if (parsedStmt instanceof EnterStmt) {
handleEnterStmt();
} else if (parsedStmt instanceof SwitchStmt) {
handleSwitchStmt();
} else if (parsedStmt instanceof UseStmt) {
handleUseStmt();
} else if (parsedStmt instanceof TransactionStmt) {
@ -1410,6 +1413,18 @@ public class StmtExecutor implements ProfileWriter {
context.getState().setOk();
}
// Process switch catalog
private void handleSwitchStmt() throws AnalysisException {
SwitchStmt switchStmt = (SwitchStmt) parsedStmt;
try {
context.getCatalog().changeCatalog(context, switchStmt.getCatalogName());
} catch (DdlException e) {
context.getState().setError(e.getMysqlErrorCode(), e.getMessage());
return;
}
context.getState().setOk();
}
// Process use statement.
private void handleUseStmt() throws AnalysisException {
UseStmt useStmt = (UseStmt) parsedStmt;

View File

@ -435,6 +435,7 @@ import org.apache.doris.qe.SqlModeHelper;
keywordMap.put("not_null", new Integer(SqlParserSymbols.KW_NOT_NULL));
keywordMap.put("catalog", new Integer(SqlParserSymbols.KW_CATALOG));
keywordMap.put("catalogs", new Integer(SqlParserSymbols.KW_CATALOGS));
keywordMap.put("switch", new Integer(SqlParserSymbols.KW_SWITCH));
}
// map from token id to token description

View File

@ -43,7 +43,7 @@ public class AlterCatalogNameStmtTest {
Config.enable_multi_catalog = true;
analyzer = AccessTestUtil.fetchAdminAnalyzer(false);
MockedAuth.mockedAuth(auth);
MockedAuth.mockedConnectContext(ctx, "root", "127.0.0.1");
MockedAuth.mockedConnectContext(ctx, "root", "%");
}
@Test

View File

@ -46,7 +46,7 @@ public class AlterCatalogPropsStmtTest {
Config.enable_multi_catalog = true;
analyzer = AccessTestUtil.fetchAdminAnalyzer(false);
MockedAuth.mockedAuth(auth);
MockedAuth.mockedConnectContext(ctx, "root", "127.0.0.1");
MockedAuth.mockedConnectContext(ctx, "root", "%");
}
@Test

View File

@ -47,7 +47,7 @@ public class CreateCatalogStmtTest {
Config.enable_multi_catalog = true;
analyzer = AccessTestUtil.fetchAdminAnalyzer(true);
MockedAuth.mockedAuth(auth);
MockedAuth.mockedConnectContext(ctx, "root", "127.0.0.1");
MockedAuth.mockedConnectContext(ctx, "root", "%");
}
@Test

View File

@ -44,7 +44,7 @@ public class DropCatalogStmtTest {
Config.enable_multi_catalog = true;
analyzer = AccessTestUtil.fetchAdminAnalyzer(true);
MockedAuth.mockedAuth(auth);
MockedAuth.mockedConnectContext(ctx, "root", "127.0.0.1");
MockedAuth.mockedConnectContext(ctx, "root", "%");
}
@Test

View File

@ -0,0 +1,161 @@
// 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.analysis;
import org.apache.doris.catalog.Catalog;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.Config;
import org.apache.doris.datasource.InternalDataSource;
import org.apache.doris.mysql.privilege.PaloAuth;
import org.apache.doris.qe.ConnectContext;
import org.apache.doris.utframe.DorisAssert;
import org.apache.doris.utframe.UtFrameUtils;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import java.util.UUID;
public class SwitchStmtTest {
private static String runningDir = "fe/mocked/DemoTest/" + UUID.randomUUID().toString() + "/";
private static DorisAssert dorisAssert;
private static String clusterName = "default_cluster";
private static PaloAuth auth;
private static Catalog catalog;
private static UserIdentity user1;
private static UserIdentity user2;
@AfterClass
public static void tearDown() throws Exception {
UtFrameUtils.cleanDorisFeDir(runningDir);
}
@BeforeClass
public static void setUp() throws Exception {
Config.enable_multi_catalog = true;
UtFrameUtils.createDorisCluster(runningDir);
// use root to initialize.
ConnectContext rootCtx = UtFrameUtils.createDefaultCtx();
catalog = Catalog.getCurrentCatalog();
auth = catalog.getAuth();
// grant with no catalog is switched, internal catalog works.
CreateRoleStmt createRole1 = (CreateRoleStmt) UtFrameUtils.parseAndAnalyzeStmt("create role role1;", rootCtx);
auth.createRole(createRole1);
GrantStmt grantRole1 = (GrantStmt) UtFrameUtils.parseAndAnalyzeStmt(
"grant grant_priv on tpch.* to role 'role1';", rootCtx);
auth.grant(grantRole1);
// user1 can't switch to hive
auth.createUser((CreateUserStmt) UtFrameUtils.parseAndAnalyzeStmt(
"create user 'user1'@'%' identified by 'pwd1' default role 'role1';", rootCtx));
user1 = new UserIdentity("user1", "%");
user1.analyze(clusterName);
// create catalog
CreateCatalogStmt hiveCatalog = (CreateCatalogStmt) UtFrameUtils.parseAndAnalyzeStmt(
"create catalog hive properties('type' = 'hms', 'hive.metastore.uris' = 'thrift://192.168.0.1:9083');",
rootCtx);
catalog.getDataSourceMgr().createCatalog(hiveCatalog);
CreateCatalogStmt iceBergCatalog = (CreateCatalogStmt) UtFrameUtils.parseAndAnalyzeStmt(
"create catalog iceberg properties('type' = 'hms', 'iceberg.hive.metastore.uris' = 'thrift://192.168.0.1:9083');",
rootCtx);
catalog.getDataSourceMgr().createCatalog(iceBergCatalog);
// switch to hive.
SwitchStmt switchHive = (SwitchStmt) UtFrameUtils.parseAndAnalyzeStmt("switch hive;", rootCtx);
catalog.changeCatalog(rootCtx, switchHive.getCatalogName());
CreateRoleStmt createRole2 = (CreateRoleStmt) UtFrameUtils.parseAndAnalyzeStmt("create role role2;", rootCtx);
auth.createRole(createRole2);
GrantStmt grantRole2 = (GrantStmt) UtFrameUtils.parseAndAnalyzeStmt(
"grant grant_priv on tpch.customer to role 'role2';", rootCtx);
auth.grant(grantRole2);
auth.createUser((CreateUserStmt) UtFrameUtils.parseAndAnalyzeStmt(
"create user 'user2'@'%' identified by 'pwd2' default role 'role2';", rootCtx));
user2 = new UserIdentity("user2", "%");
user2.analyze(clusterName);
}
@Test
public void testSwitchCommand() throws Exception {
// mock the login of user1
ConnectContext user1Ctx = UtFrameUtils.createDefaultCtx(user1, "127.0.0.1");
// user1 can switch to internal catalog
UtFrameUtils.parseAndAnalyzeStmt("switch " + InternalDataSource.INTERNAL_DS_NAME + ";", user1Ctx);
Assert.assertEquals(InternalDataSource.INTERNAL_DS_NAME, user1Ctx.getDefaultCatalog());
// user1 can't switch to hive
try {
UtFrameUtils.parseAndAnalyzeStmt("switch hive;", user1Ctx);
Assert.fail("user1 switch to hive with no privilege.");
} catch (AnalysisException e) {
Assert.assertEquals(e.getMessage(),
"errCode = 2, detailMessage = Access denied for user 'default_cluster:user1' to catalog 'hive'");
}
Assert.assertEquals(InternalDataSource.INTERNAL_DS_NAME, user1Ctx.getDefaultCatalog());
// mock the login of user2
ConnectContext user2Ctx = UtFrameUtils.createDefaultCtx(user2, "127.0.0.1");
// user2 can switch to internal catalog
UtFrameUtils.parseAndAnalyzeStmt("switch " + InternalDataSource.INTERNAL_DS_NAME + ";", user2Ctx);
Assert.assertEquals(InternalDataSource.INTERNAL_DS_NAME, user2Ctx.getDefaultCatalog());
// user2 can switch to hive
SwitchStmt switchHive = (SwitchStmt) UtFrameUtils.parseAndAnalyzeStmt("switch hive;", user2Ctx);
catalog.changeCatalog(user2Ctx, switchHive.getCatalogName());
Assert.assertEquals(user2Ctx.getDefaultCatalog(), "hive");
// user2 can grant select_priv to tpch.customer
GrantStmt user2GrantHiveTable = (GrantStmt) UtFrameUtils.parseAndAnalyzeStmt(
"grant select_priv on tpch.customer to 'user2'@'%';", user2Ctx);
auth.grant(user2GrantHiveTable);
}
@Test
public void testShowCatalogStmtWithPrivileges() throws Exception {
// mock the login of user1
ConnectContext user1Ctx = UtFrameUtils.createDefaultCtx(user1, "127.0.0.1");
ShowCatalogStmt user1Show = (ShowCatalogStmt) UtFrameUtils.parseAndAnalyzeStmt("show catalogs;", user1Ctx);
List<List<String>> user1ShowResult = catalog.getDataSourceMgr().showCatalogs(user1Show).getResultRows();
Assert.assertEquals(user1ShowResult.size(), 1);
Assert.assertEquals(user1ShowResult.get(0).get(0), InternalDataSource.INTERNAL_DS_NAME);
// mock the login of user1
ConnectContext user2Ctx = UtFrameUtils.createDefaultCtx(user2, "127.0.0.1");
ShowCatalogStmt user2Show = (ShowCatalogStmt) UtFrameUtils.parseAndAnalyzeStmt("show catalogs;", user2Ctx);
List<List<String>> user2ShowResult = catalog.getDataSourceMgr().showCatalogs(user2Show).getResultRows();
Assert.assertEquals(user2ShowResult.size(), 2);
Assert.assertTrue(user2ShowResult.stream().map(l -> l.get(0)).anyMatch(c -> c.equals("hive")));
// access denied
ShowCatalogStmt user2ShowHive = (ShowCatalogStmt) UtFrameUtils.parseAndAnalyzeStmt("show catalog hive;",
user2Ctx);
List<List<String>> user2ShowHiveResult = catalog.getDataSourceMgr().showCatalogs(user2ShowHive).getResultRows();
Assert.assertTrue(
user2ShowHiveResult.stream().map(l -> l.get(0)).anyMatch(c -> c.equals("hive.metastore.uris")));
try {
catalog.getDataSourceMgr().showCatalogs(
(ShowCatalogStmt) UtFrameUtils.parseAndAnalyzeStmt("show catalog iceberg;", user2Ctx));
Assert.fail("");
} catch (AnalysisException e) {
Assert.assertEquals(e.getMessage(),
"errCode = 2, detailMessage = Access denied for user 'default_cluster:user2' to catalog 'iceberg'");
}
}
}

View File

@ -31,7 +31,6 @@ import org.apache.doris.common.Config;
import org.apache.doris.common.DdlException;
import org.apache.doris.common.FeConstants;
import org.apache.doris.common.util.SqlParserUtils;
import org.apache.doris.mysql.privilege.PaloAuth;
import org.apache.doris.planner.Planner;
import org.apache.doris.qe.ConnectContext;
import org.apache.doris.qe.OriginStatement;
@ -74,18 +73,23 @@ import java.util.UUID;
public class UtFrameUtils {
// Help to create a mocked ConnectContext.
public static ConnectContext createDefaultCtx() throws IOException {
public static ConnectContext createDefaultCtx(UserIdentity userIdentity, String remoteIp) throws IOException {
SocketChannel channel = SocketChannel.open();
ConnectContext ctx = new ConnectContext(channel);
ctx.setCluster(SystemInfoService.DEFAULT_CLUSTER);
ctx.setCurrentUserIdentity(UserIdentity.ROOT);
ctx.setQualifiedUser(PaloAuth.ROOT_USER);
ctx.setRemoteIP("127.0.0.1");
ctx.setCurrentUserIdentity(userIdentity);
ctx.setQualifiedUser(userIdentity.getQualifiedUser());
ctx.setRemoteIP(remoteIp);
ctx.setCatalog(Catalog.getCurrentCatalog());
ctx.setThreadLocalInfo();
return ctx;
}
// Help to create a mocked ConnectContext for root.
public static ConnectContext createDefaultCtx() throws IOException {
return createDefaultCtx(UserIdentity.ROOT, "127.0.0.1");
}
// Parse an origin stmt and analyze it. Return a StatementBase instance.
public static StatementBase parseAndAnalyzeStmt(String originStmt, ConnectContext ctx)
throws Exception {