[feature-wip](multi-catalog) support to switch catalog (#10381)
Add `switch catalog` stmt with privilege check
This commit is contained in:
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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)) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user