This is an automated email from the ASF dual-hosted git repository.
CalvinKirs pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 13c57dcad9e [fix](auth) Add missing privilege checks for several
Nereids commands (#66218)
13c57dcad9e is described below
commit 13c57dcad9e80528f50ef6475a60b1451d97a1b0
Author: Calvin Kirs <[email protected]>
AuthorDate: Fri Aug 7 14:49:43 2026 +0800
[fix](auth) Add missing privilege checks for several Nereids commands
(#66218)
### What problem does this PR solve?
Problem Summary:
The Nereids path in `StmtExecutor` dispatches straight to
`Command.run()`, so each command has to enforce its own privileges. A
number of commands never got that check, and can currently be executed
by any authenticated user regardless of their grants.
This PR adds the missing checks:
| Command | Required privilege | Follows |
| --- | --- | --- |
| `AdminSetEncryptionRootKeyCommand`, `AdminRotateTdeRootKeyCommand` |
global `ADMIN` | `AdminSetFrontendConfig`, `AdminSetTableStatus`,
`AdminSetReplicaStatus`, `AdminCleanTrash` |
| `DropCatalogRecycleBinCommand` | global `ADMIN` | `SHOW CATALOG
RECYCLE BIN` already requires global `ADMIN`. `RECOVER` uses
`ALTER_CREATE`, but it is name-scoped while erasing takes a raw object
id, so it cannot be authorized at db/table level |
| `CreateDictionaryCommand` | `CREATE` on the dictionary **and**
`SELECT` on the source table | `CREATE TABLE` / `CREATE MTMV`. The
`SELECT` check is needed because the load task runs internally, and
unlike an MTMV the source table is not bound by the planner |
| `DropDictionaryCommand` | `DROP` on the dictionary | `DROP TABLE` /
`DROP MTMV` |
| `AddConstraintCommand`, `DropConstraintCommand` | `ALTER` on the
table. For a foreign key, also on the referenced table; for dropping a
primary key, also on every referencing table | the rest of `ALTER TABLE`
|
| `WarmUpClusterCommand` | `USAGE` on the source and destination compute
groups, plus `SELECT` on each table named by `WITH TABLE`. `ON TABLES`
additionally requires global `ADMIN` | `UseCloudClusterCommand` |
| `CancelWarmUpJobCommand` | global `ADMIN` | `CloudWarmUpJob` records
no owner, so a job cannot be scoped to the user who created it |
| `DropStageCommand` | global `ADMIN` | `CreateStageCommand`, which
already checked it |
Note that `ADMIN_PRIV` satisfies every predicate used above, so admin
users are unaffected by any of this.
#### Where the check is placed, and why
- `AddConstraintCommand`: for a foreign key the referenced table is
checked as well, because adding the constraint registers a reverse
reference on it.
- `DropConstraintCommand`: the check sits after the two table-resolution
paths converge. Resolution can fall back to a name-only lookup, and
putting the check on the normal path only would let the fallback skip
it.
- `DropConstraintCommand`, primary keys: dropping one cascades into
`ConstraintManager.cascadeDropForeignKeys()`, which deletes the foreign
key constraint of every referencing table, so `ALTER` is required on
each of those too. The cascade is atomic, so all of them are checked
before `dropConstraint()`.
- `CreateDictionaryCommand`: the check has to run after
`validateAndSet()`, since that is what fills in the default catalog/db
names.
- `WarmUpClusterCommand`, `WITH TABLE`: the per-table `SELECT` check is
inside the existing resolution loop, before the db/table lookup. It
authorizes the *internal* fully qualified name, not the catalog written
in the SQL, because the lookup and the `(db, table, partition)` triple
stored for the job are internal-catalog only.
- `WarmUpClusterCommand`, `ON TABLES`: this mode matches tables by glob
over the whole internal catalog, and
`CacheHotspotManager.refreshAllTableFilters()` keeps re-matching in the
background with no identity available to re-authorize new matches. There
is no fixed table set to authorize, so it requires global `ADMIN` on top
of the compute group `USAGE`.
#### Incidental changes reviewers should look at
These are not privilege checks, but they fall out of adding them:
1. `AdminRotateTdeRootKeyCommand`, `DropCatalogRecycleBinCommand`,
`DropStageCommand` had no `validate()` method at all. One was added to
each and is called at the top of `run()`.
2. `CreateDictionaryCommand.run()` and `DropDictionaryCommand.run()` now
declare `throws Exception`.
3. `CreateDictionaryCommand.run()`: the single try block that wrapped
`validateAndSet()` + `createDictionary()` is split in two, with the
check in between. Consequence: an access-denied error is **not** wrapped
in the `"Failed to create dictionary: ..."` prefix, while the failure
messages from `validateAndSet()` and `createDictionary()` are unchanged.
4. `WarmUpClusterCommand.validate()`: order is now cloud-mode → compute
group `USAGE` (+ `ADMIN` for `ON TABLES`) → compute group
existence/virtual-group validation → table resolution. The non-cloud
error message is unchanged, but in cloud mode a user without `USAGE` now
gets an access-denied error where they previously got "compute group
doesn't exist".
5. `WarmUpClusterCommand`, `WITH TABLE`: the `SELECT` check runs before
the db/table lookup, so a user without `SELECT` gets an access-denied
error where they previously got "unknown database/table". This is
deliberate: the error should not tell a user who cannot read the table
whether it exists.
6. `DropConstraintCommand`: the two `ALTER` checks are factored into a
private `checkAlterPriv()`, same shape as the one in
`AddConstraintCommand`.
#### Open questions
- `WarmUpClusterCommand`: for `WITH TABLE`, global `ADMIN` felt too
coarse for an operation scoped to compute groups the user already has
`USAGE` on, so it uses `checkCloudPriv(..., ResourceTypeEnum.CLUSTER)`
plus per-table `SELECT`. Happy to switch it back to `ADMIN` if the cloud
maintainers prefer that. `ON TABLES` does require `ADMIN`, since a
pattern job cannot be authorized per table.
- `DropConstraintCommand`: requiring `ALTER` on the referencing tables
means a user who owns the primary key table can no longer drop it once
somebody else's table references it. Rejecting the cascade and asking
for the foreign keys to be dropped separately would be the other option;
happy to switch.
- `CancelWarmUpJobCommand` keeps `ADMIN` only because there is nothing
to scope it to. If `CloudWarmUpJob` recorded the submitting user,
letting that user cancel their own job would be better.
- Out of scope, but noted while looking around: `ShowWarmUpCommand`
(`SHOW WARM UP JOB`) and `ShowDictionariesCommand` (`SHOW DICTIONARIES`)
have no privilege check either. Happy to follow up in a separate PR.
### Release note
Fix missing privilege checks on `ADMIN SET ENCRYPTION ROOT KEY`, `ADMIN
ROTATE TDE ROOT KEY`, `DROP CATALOG RECYCLE BIN`, `CREATE/DROP
DICTIONARY`, `ALTER TABLE ADD/DROP CONSTRAINT`, `WARM UP CLUSTER`,
`CANCEL WARM UP JOB` and `DROP STAGE`. These statements previously ran
for any authenticated user.
### Check List (For Author)
- Test <!-- At least one of them must be included. -->
- [x] Regression test
- [ ] Unit Test
- [ ] Manual test (add detailed scripts or steps below)
- [ ] No need to test or manual test. Explain why:
- [ ] This is a refactor/code format and no logic has been changed.
- [ ] Previous test can cover this change.
- [ ] No code files have been changed.
- [ ] Other reason <!-- Add your reason? -->
New `auth_call` cases for constraint, dictionary and recycle bin, each
covering both the denied and the granted path, including the cross-table
primary key / foreign key cascade. The cloud-only commands (`WARM UP
CLUSTER`, `CANCEL WARM UP JOB`, `DROP STAGE`) are not covered by a
regression case.
- Behavior changed:
- [ ] No.
- [x] Yes. Users without the privileges listed above can no longer run
these statements; they now get an access-denied error. See "Incidental
changes reviewers should look at" above for the error-message and
ordering changes that come with it.
- Does this need documentation?
- [ ] No.
- [x] Yes. <!-- Add document PR link here. eg:
https://github.com/apache/doris-website/pull/1214 -->
The privilege documentation for these statements should list the
required privileges.
---
.../catalog/constraint/ConstraintManager.java | 18 +++
.../trees/plans/commands/AddConstraintCommand.java | 16 +++
.../commands/AdminRotateTdeRootKeyCommand.java | 15 +++
.../commands/AdminSetEncryptionRootKeyCommand.java | 9 ++
.../plans/commands/CancelWarmUpJobCommand.java | 9 ++
.../plans/commands/CreateDictionaryCommand.java | 38 +++++-
.../commands/DropCatalogRecycleBinCommand.java | 15 +++
.../plans/commands/DropConstraintCommand.java | 25 ++++
.../plans/commands/DropDictionaryCommand.java | 12 +-
.../trees/plans/commands/DropStageCommand.java | 15 +++
.../trees/plans/commands/WarmUpClusterCommand.java | 42 ++++++-
.../cloud/WarmUpClusterOnTablesParseTest.java | 6 +
.../auth_call/test_ddl_constraint_auth.groovy | 129 +++++++++++++++++++++
.../auth_call/test_ddl_dictionary_auth.groovy | 106 +++++++++++++++++
.../suites/auth_call/test_recycle_bin_auth.groovy | 82 +++++++++++++
15 files changed, 533 insertions(+), 4 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/catalog/constraint/ConstraintManager.java
b/fe/fe-core/src/main/java/org/apache/doris/catalog/constraint/ConstraintManager.java
index cf16af663b6..5044f35d921 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/catalog/constraint/ConstraintManager.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/catalog/constraint/ConstraintManager.java
@@ -126,6 +126,24 @@ public class ConstraintManager implements Writable,
GsonPostProcessable {
}
}
+ /**
+ * Snapshot the tables whose foreign keys would be cascade-dropped along
with the given primary
+ * key constraint. Taken under the read lock: {@link
PrimaryKeyConstraint#getForeignTableInfos()}
+ * is only a view over a list that {@link #addConstraint} mutates under
the write lock, so callers
+ * outside the lock must not iterate it directly. Returns an empty list
for other constraint types.
+ */
+ public List<TableNameInfo> getCascadeDropTables(Constraint constraint) {
+ if (!(constraint instanceof PrimaryKeyConstraint)) {
+ return ImmutableList.of();
+ }
+ readLock();
+ try {
+ return ImmutableList.copyOf(((PrimaryKeyConstraint)
constraint).getForeignTableInfos());
+ } finally {
+ readUnlock();
+ }
+ }
+
/**
* Drop a constraint from the specified table.
* For PK constraints, cascade-drops all referencing FKs.
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AddConstraintCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AddConstraintCommand.java
index 7f9d2c468f2..5eeb3f80f4e 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AddConstraintCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AddConstraintCommand.java
@@ -24,9 +24,12 @@ import
org.apache.doris.catalog.constraint.ForeignKeyConstraint;
import org.apache.doris.catalog.constraint.PrimaryKeyConstraint;
import org.apache.doris.catalog.constraint.UniqueConstraint;
import org.apache.doris.catalog.info.TableNameInfo;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.ErrorReport;
import org.apache.doris.common.Pair;
import org.apache.doris.info.TableNameInfoUtils;
import org.apache.doris.mtmv.MTMVUtil;
+import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.nereids.NereidsPlanner;
import org.apache.doris.nereids.exceptions.AnalysisException;
import org.apache.doris.nereids.properties.PhysicalProperties;
@@ -71,6 +74,7 @@ public class AddConstraintCommand extends Command implements
ForwardWithSync {
TableNameInfo tableNameInfo = TableNameInfoUtils.fromCatalogDb(
table.getDatabase().getCatalog(), table.getDatabase(), table);
ImmutableList<String> columns = columnsAndTable.first;
+ checkAlterPriv(ctx, tableNameInfo);
Pair<ImmutableList<String>, TableNameInfo> referencedColumnsAndTable =
null;
if (constraint.isForeignKey()) {
@@ -79,6 +83,8 @@ public class AddConstraintCommand extends Command implements
ForwardWithSync {
TableIf refTable = refColumnsAndTable.second;
TableNameInfo refTableInfo = TableNameInfoUtils.fromCatalogDb(
refTable.getDatabase().getCatalog(),
refTable.getDatabase(), refTable);
+ // a foreign key also registers a reverse reference on the
referenced table
+ checkAlterPriv(ctx, refTableInfo);
referencedColumnsAndTable = Pair.of(refColumnsAndTable.first,
refTableInfo);
}
if (constraint.isForeignKey()) {
@@ -97,6 +103,16 @@ public class AddConstraintCommand extends Command
implements ForwardWithSync {
}
}
+ private void checkAlterPriv(ConnectContext ctx, TableNameInfo
tableNameInfo)
+ throws org.apache.doris.common.AnalysisException {
+ if (!Env.getCurrentEnv().getAccessManager().checkTblPriv(ctx,
tableNameInfo.getCtl(),
+ tableNameInfo.getDb(), tableNameInfo.getTbl(),
PrivPredicate.ALTER)) {
+
ErrorReport.reportAnalysisException(ErrorCode.ERR_TABLEACCESS_DENIED_ERROR,
"ALTER",
+ ctx.getQualifiedUser(), ctx.getRemoteIP(),
+ tableNameInfo.getDb() + ": " + tableNameInfo.getTbl());
+ }
+ }
+
private void addConstraintAndInvalidate(
TableNameInfo tableNameInfo,
org.apache.doris.catalog.constraint.Constraint constraint)
throws Exception {
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminRotateTdeRootKeyCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminRotateTdeRootKeyCommand.java
index 4952c6c586a..469c871f3a9 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminRotateTdeRootKeyCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminRotateTdeRootKeyCommand.java
@@ -17,8 +17,12 @@
package org.apache.doris.nereids.trees.plans.commands;
+import org.apache.doris.catalog.Env;
import org.apache.doris.common.AnalysisException;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.ErrorReport;
import org.apache.doris.encryption.KeyManagerInterface;
+import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.nereids.trees.plans.PlanType;
import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
import org.apache.doris.qe.ConnectContext;
@@ -52,6 +56,7 @@ public class AdminRotateTdeRootKeyCommand extends Command
implements ForwardWith
@Override
public void run(ConnectContext ctx, StmtExecutor executor) throws
Exception {
+ validate();
KeyManagerInterface keyManager = ctx.getEnv().getKeyManager();
if (keyManager != null) {
keyManager.rotateRootKey(properties);
@@ -60,6 +65,16 @@ public class AdminRotateTdeRootKeyCommand extends Command
implements ForwardWith
}
}
+ /**
+ * validate
+ */
+ public void validate() throws AnalysisException {
+ // check auth
+ if
(!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ConnectContext.get(),
PrivPredicate.ADMIN)) {
+
ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR,
"ADMIN");
+ }
+ }
+
@Override
public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
return visitor.visitAdminRotateTdeRootKeyCommand(this, context);
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminSetEncryptionRootKeyCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminSetEncryptionRootKeyCommand.java
index 5f98cd83b4c..0e969799c2d 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminSetEncryptionRootKeyCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminSetEncryptionRootKeyCommand.java
@@ -17,9 +17,13 @@
package org.apache.doris.nereids.trees.plans.commands;
+import org.apache.doris.catalog.Env;
import org.apache.doris.common.AnalysisException;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.ErrorReport;
import org.apache.doris.encryption.EncryptionKey;
import org.apache.doris.encryption.RootKeyInfo;
+import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.nereids.trees.plans.PlanType;
import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
import org.apache.doris.qe.ConnectContext;
@@ -63,6 +67,11 @@ public class AdminSetEncryptionRootKeyCommand extends
Command implements Forward
* validate
*/
public void validate() throws AnalysisException {
+ // check auth
+ if
(!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ConnectContext.get(),
PrivPredicate.ADMIN)) {
+
ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR,
"ADMIN");
+ }
+
if (properties == null || properties.isEmpty()) {
throw new AnalysisException("The properties must not be empty");
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CancelWarmUpJobCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CancelWarmUpJobCommand.java
index cfb20910f11..05d27a792a2 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CancelWarmUpJobCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CancelWarmUpJobCommand.java
@@ -17,9 +17,13 @@
package org.apache.doris.nereids.trees.plans.commands;
+import org.apache.doris.catalog.Env;
import org.apache.doris.cloud.catalog.CloudEnv;
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.mysql.privilege.PrivPredicate;
import org.apache.doris.nereids.analyzer.UnboundSlot;
import org.apache.doris.nereids.trees.expressions.EqualTo;
import org.apache.doris.nereids.trees.expressions.Expression;
@@ -59,6 +63,11 @@ public class CancelWarmUpJobCommand extends Command
implements ForwardWithSync {
* @throws AnalysisException check whether this sql is legal
*/
public void validate(ConnectContext ctx) throws AnalysisException {
+ // check auth
+ if (!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ctx,
PrivPredicate.ADMIN)) {
+
ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR,
"ADMIN");
+ }
+
if (!Config.isCloudMode()) {
throw new AnalysisException("The sql is illegal in disk mode ");
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CreateDictionaryCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CreateDictionaryCommand.java
index 6b8287200e7..e09d3cf6da7 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CreateDictionaryCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CreateDictionaryCommand.java
@@ -18,7 +18,12 @@
package org.apache.doris.nereids.trees.plans.commands;
import org.apache.doris.analysis.StmtType;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.ErrorReport;
+import org.apache.doris.datasource.InternalCatalog;
import org.apache.doris.dictionary.LayoutType;
+import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.nereids.exceptions.AnalysisException;
import org.apache.doris.nereids.trees.plans.PlanType;
import org.apache.doris.nereids.trees.plans.commands.info.CreateDictionaryInfo;
@@ -60,12 +65,20 @@ public class CreateDictionaryCommand extends Command
implements ForwardWithSync
}
@Override
- public void run(ConnectContext ctx, StmtExecutor executor) {
+ public void run(ConnectContext ctx, StmtExecutor executor) throws
Exception {
try {
// 1. Validate the dictionary info. names and existence.
createDictionaryInfo.validateAndSet(ctx);
+ } catch (Exception e) {
+ LOG.warn("Failed to create dictionary: {}", e.getMessage());
+ throw new AnalysisException("Failed to create dictionary: " +
e.getMessage());
+ }
+
+ // 2. Check auth. Must run after validateAndSet(), which fills in the
default catalog/db names.
+ checkAuth(ctx);
- // 2. Create dictionary and save it in manager. it will schedule
data load.
+ try {
+ // 3. Create dictionary and save it in manager. it will schedule
data load.
ctx.getEnv().getDictionaryManager().createDictionary(ctx,
createDictionaryInfo);
LOG.info("Created dictionary {} in {} from {}",
createDictionaryInfo.getDictName(),
@@ -75,4 +88,25 @@ public class CreateDictionaryCommand extends Command
implements ForwardWithSync
throw new AnalysisException("Failed to create dictionary: " +
e.getMessage());
}
}
+
+ /**
+ * A dictionary is created in the internal catalog and its data is loaded
by an internal task,
+ * so require CREATE on the dictionary itself and SELECT on the source
table. Without the latter
+ * the dictionary would expose data the creator can not read.
+ */
+ private void checkAuth(ConnectContext ctx) throws
org.apache.doris.common.AnalysisException {
+ if (!Env.getCurrentEnv().getAccessManager().checkTblPriv(ctx,
InternalCatalog.INTERNAL_CATALOG_NAME,
+ createDictionaryInfo.getDbName(),
createDictionaryInfo.getDictName(), PrivPredicate.CREATE)) {
+
ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR,
"CREATE");
+ }
+
+ String srcCtl = createDictionaryInfo.getSourceCtlName();
+ String srcDb = createDictionaryInfo.getSourceDbName();
+ String srcTbl = createDictionaryInfo.getSourceTableName();
+ if (!Env.getCurrentEnv().getAccessManager().checkTblPriv(ctx, srcCtl,
srcDb, srcTbl,
+ PrivPredicate.SELECT)) {
+
ErrorReport.reportAnalysisException(ErrorCode.ERR_TABLEACCESS_DENIED_ERROR,
"SELECT",
+ ctx.getQualifiedUser(), ctx.getRemoteIP(), srcDb + ": " +
srcTbl);
+ }
+ }
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropCatalogRecycleBinCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropCatalogRecycleBinCommand.java
index eaad053343e..48edc6bc306 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropCatalogRecycleBinCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropCatalogRecycleBinCommand.java
@@ -18,6 +18,9 @@
package org.apache.doris.nereids.trees.plans.commands;
import org.apache.doris.catalog.Env;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.ErrorReport;
+import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.nereids.exceptions.AnalysisException;
import org.apache.doris.nereids.trees.plans.PlanType;
import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
@@ -71,9 +74,21 @@ public class DropCatalogRecycleBinCommand extends Command
implements ForwardWith
@Override
public void run(ConnectContext ctx, StmtExecutor executor) throws
Exception {
+ validate();
Env.getCurrentEnv().dropCatalogRecycleBin(idType, id);
}
+ /**
+ * validate
+ */
+ public void validate() throws org.apache.doris.common.AnalysisException {
+ // Erasing from the recycle bin is irreversible and takes a raw object
id, so it can not be
+ // authorized at db/table level. Restrict it to ADMIN, same as the
other catalog-wide admin ops.
+ if
(!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ConnectContext.get(),
PrivPredicate.ADMIN)) {
+
ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR,
"ADMIN");
+ }
+ }
+
@Override
public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
return visitor.visitDropCatalogRecycleBinCommand(this, context);
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropConstraintCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropConstraintCommand.java
index fb8d506ef97..15419eb20be 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropConstraintCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropConstraintCommand.java
@@ -22,8 +22,11 @@ import org.apache.doris.catalog.MTMV;
import org.apache.doris.catalog.TableIf;
import org.apache.doris.catalog.constraint.Constraint;
import org.apache.doris.catalog.info.TableNameInfo;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.ErrorReport;
import org.apache.doris.info.TableNameInfoUtils;
import org.apache.doris.mtmv.MTMVUtil;
+import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.nereids.NereidsPlanner;
import org.apache.doris.nereids.analyzer.UnboundRelation;
import org.apache.doris.nereids.exceptions.AnalysisException;
@@ -75,17 +78,39 @@ public class DropConstraintCommand extends Command
implements ForwardWithSync {
+ "falling back to name-based lookup: {}", name,
e.getMessage());
tableNameInfo = extractTableNameFromPlan(ctx);
}
+ // must be checked on both paths above: table resolution failing
(which includes an
+ // authorization failure) falls back to a name-only lookup that binds
nothing.
+ checkAlterPriv(ctx, tableNameInfo);
Constraint constraint =
Env.getCurrentEnv().getConstraintManager().getConstraint(tableNameInfo, name);
if (constraint == null) {
throw new AnalysisException(
String.format("Unknown constraint %s on table %s.", name,
tableNameInfo));
}
+ // dropping a primary key cascades into
ConstraintManager.cascadeDropForeignKeys(), which
+ // deletes the foreign key constraints of every referencing table, so
those tables have to be
+ // authorized too. Checked before dropConstraint() because the cascade
is atomic. The snapshot
+ // is taken under the manager lock; a foreign key added after it still
needs ALTER on its own
+ // table to be created, so it cannot be used to bypass this.
+ for (TableNameInfo fkTableInfo
+ :
Env.getCurrentEnv().getConstraintManager().getCascadeDropTables(constraint)) {
+ checkAlterPriv(ctx, fkTableInfo);
+ }
List<MTMV> dependentMtmvs =
MTMVUtil.getDependentMtmvsByConstraint(tableNameInfo, constraint);
Env.getCurrentEnv().getConstraintManager().dropConstraint(tableNameInfo, name,
false);
MTMVUtil.invalidateRewriteCachesBestEffort(dependentMtmvs,
String.format("after drop constraint %s on table %s",
constraint.getName(), tableNameInfo));
}
+ private void checkAlterPriv(ConnectContext ctx, TableNameInfo
tableNameInfo)
+ throws org.apache.doris.common.AnalysisException {
+ if (!Env.getCurrentEnv().getAccessManager().checkTblPriv(ctx,
tableNameInfo.getCtl(),
+ tableNameInfo.getDb(), tableNameInfo.getTbl(),
PrivPredicate.ALTER)) {
+
ErrorReport.reportAnalysisException(ErrorCode.ERR_TABLEACCESS_DENIED_ERROR,
"ALTER",
+ ctx.getQualifiedUser(), ctx.getRemoteIP(),
+ tableNameInfo.getDb() + ": " + tableNameInfo.getTbl());
+ }
+ }
+
private TableNameInfo extractTableNameFromPlan(ConnectContext ctx) {
if (!(plan instanceof UnboundRelation)) {
throw new AnalysisException(
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropDictionaryCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropDictionaryCommand.java
index 23f58cf9664..3e7a564e58b 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropDictionaryCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropDictionaryCommand.java
@@ -18,6 +18,11 @@
package org.apache.doris.nereids.trees.plans.commands;
import org.apache.doris.analysis.StmtType;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.ErrorReport;
+import org.apache.doris.datasource.InternalCatalog;
+import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.nereids.exceptions.AnalysisException;
import org.apache.doris.nereids.trees.plans.PlanType;
import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
@@ -57,10 +62,15 @@ public class DropDictionaryCommand extends Command
implements ForwardWithSync {
}
@Override
- public void run(ConnectContext ctx, StmtExecutor executor) {
+ public void run(ConnectContext ctx, StmtExecutor executor) throws
Exception {
if (dbName == null) { // use current database
dbName = ctx.getDatabase();
}
+ // check auth. dictionaries always live in the internal catalog.
+ if (!Env.getCurrentEnv().getAccessManager().checkTblPriv(ctx,
InternalCatalog.INTERNAL_CATALOG_NAME,
+ dbName, dictName, PrivPredicate.DROP)) {
+
ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR,
"DROP");
+ }
try {
ctx.getEnv().getDictionaryManager().dropDictionary(ctx, dbName,
dictName, ifExists);
} catch (Exception e) {
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropStageCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropStageCommand.java
index bd54e875456..e38b40ffadb 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropStageCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropStageCommand.java
@@ -20,6 +20,10 @@ package org.apache.doris.nereids.trees.plans.commands;
import org.apache.doris.analysis.StmtType;
import org.apache.doris.catalog.Env;
import org.apache.doris.cloud.catalog.CloudEnv;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.ErrorReport;
+import org.apache.doris.common.UserException;
+import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.nereids.trees.plans.PlanType;
import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
import org.apache.doris.qe.ConnectContext;
@@ -45,9 +49,20 @@ public class DropStageCommand extends Command implements
ForwardWithSync {
@Override
public void run(ConnectContext ctx, StmtExecutor executor) throws
Exception {
+ validate();
((CloudEnv) Env.getCurrentEnv()).dropStage(this);
}
+ /**
+ * validate
+ */
+ public void validate() throws UserException {
+ // check auth. keep it aligned with CreateStageCommand.
+ if
(!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ConnectContext.get(),
PrivPredicate.ADMIN)) {
+
ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR,
"ADMIN");
+ }
+ }
+
@Override
public StmtType stmtType() {
return StmtType.DROP;
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/WarmUpClusterCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/WarmUpClusterCommand.java
index fd77fb41779..e568a90e33d 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/WarmUpClusterCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/WarmUpClusterCommand.java
@@ -17,6 +17,7 @@
package org.apache.doris.nereids.trees.plans.commands;
+import org.apache.doris.analysis.ResourceTypeEnum;
import org.apache.doris.catalog.Column;
import org.apache.doris.catalog.Database;
import org.apache.doris.catalog.Env;
@@ -33,6 +34,8 @@ import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
import org.apache.doris.common.Triple;
import org.apache.doris.common.UserException;
+import org.apache.doris.datasource.InternalCatalog;
+import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.nereids.trees.plans.PlanType;
import org.apache.doris.nereids.trees.plans.commands.info.WarmUpItem;
import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
@@ -139,6 +142,14 @@ public class WarmUpClusterCommand extends Command
implements ForwardWithSync {
handleWarmUp(ctx, executor);
}
+ private void checkComputeGroupUsage(ConnectContext ctx, String
computeGroup) throws AnalysisException {
+ if
(!Env.getCurrentEnv().getAccessManager().checkCloudPriv(ctx.getCurrentUserIdentity(),
+ computeGroup, PrivPredicate.USAGE, ResourceTypeEnum.CLUSTER)) {
+ throw new AnalysisException("USAGE denied to user '" +
ctx.getQualifiedUser() + "'@'"
+ + ctx.getRemoteIP() + "' for compute group '" +
computeGroup + "'");
+ }
+ }
+
private void checkWarmupCgs(CloudSystemInfoService cloudSys) throws
AnalysisException {
if (!Strings.isNullOrEmpty(srcCluster)) {
CloudComputeGroupMeta srcCg =
cloudSys.getComputeGroupByName(srcCluster);
@@ -180,6 +191,24 @@ public class WarmUpClusterCommand extends Command
implements ForwardWithSync {
throw new UserException("The sql is just support in cloud mode");
}
+ boolean hasOnTablesRules = onTablesRules != null &&
!onTablesRules.isEmpty();
+
+ // check auth. warming up moves data between compute groups, so
require USAGE on both ends
+ // instead of global ADMIN. Keep it aligned with
UseCloudClusterCommand.
+ checkComputeGroupUsage(connectContext, dstCluster);
+ if (!Strings.isNullOrEmpty(srcCluster)) {
+ checkComputeGroupUsage(connectContext, srcCluster);
+ }
+ if (hasOnTablesRules) {
+ // An ON TABLES job selects tables by pattern over the whole
internal catalog and keeps
+ // re-matching in the background
(CacheHotspotManager.refreshAllTableFilters), so there is
+ // no fixed table set to authorize here and no identity to
re-authorize later matches with.
+ // Require global ADMIN until the job carries its submitter.
+ if
(!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(connectContext,
PrivPredicate.ADMIN)) {
+
ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR,
"ADMIN");
+ }
+ }
+
CloudSystemInfoService cloudSys = ((CloudSystemInfoService)
Env.getCurrentSystemInfo());
if (!cloudSys.containClusterName(dstCluster)) {
throw new AnalysisException("The dstClusterName " + dstCluster + "
doesn't exist");
@@ -204,7 +233,6 @@ public class WarmUpClusterCommand extends Command
implements ForwardWithSync {
+ " is same with srcClusterName: " + srcCluster);
}
- boolean hasOnTablesRules = onTablesRules != null &&
!onTablesRules.isEmpty();
if (hasOnTablesRules && isWarmUpWithTable) {
throw new AnalysisException("ON TABLES clause cannot be used with
WITH TABLE warmup");
}
@@ -218,6 +246,18 @@ public class WarmUpClusterCommand extends Command
implements ForwardWithSync {
if (Strings.isNullOrEmpty(dbName)) {
ErrorReport.reportAnalysisException(ErrorCode.ERR_NO_DB_ERROR, dbName);
}
+ // Warm up only ever resolves and warms the internal catalog
(see the lookup below and
+ // the (db, table, partition) triple stored for the job), so
authorize the internal
+ // name rather than the catalog written in the SQL. Otherwise
SELECT on
+ // 'ext_ctl.db.tbl' would authorize warming up
'internal.db.tbl'.
+ // Checked before the lookup so a denied user learns nothing
about what exists.
+ if
(!Env.getCurrentEnv().getAccessManager().checkTblPriv(connectContext,
+ InternalCatalog.INTERNAL_CATALOG_NAME, dbName,
tableNameInfo.getTbl(),
+ PrivPredicate.SELECT)) {
+
ErrorReport.reportAnalysisException(ErrorCode.ERR_TABLEACCESS_DENIED_ERROR,
"SELECT",
+ connectContext.getQualifiedUser(),
connectContext.getRemoteIP(),
+ dbName + ": " + tableNameInfo.getTbl());
+ }
Database db =
Env.getCurrentInternalCatalog().getDbNullable(dbName);
if (db == null) {
ErrorReport.reportAnalysisException(ErrorCode.ERR_NO_DB_ERROR, dbName);
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/cloud/WarmUpClusterOnTablesParseTest.java
b/fe/fe-core/src/test/java/org/apache/doris/cloud/WarmUpClusterOnTablesParseTest.java
index ceee9533386..8e678302d34 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/cloud/WarmUpClusterOnTablesParseTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/cloud/WarmUpClusterOnTablesParseTest.java
@@ -17,6 +17,7 @@
package org.apache.doris.cloud;
+import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.catalog.Env;
import org.apache.doris.cloud.OnTablesFilter.TableFilterRule;
import org.apache.doris.cloud.OnTablesFilter.TableFilterRule.RuleType;
@@ -66,6 +67,11 @@ public class WarmUpClusterOnTablesParseTest {
originalSystemInfo = getField(env, Env.class, "systemInfo");
connectContext = new ConnectContext();
connectContext.setEnv(env);
+ // this test covers ON TABLES parsing and validation, not
authorization, so give the
+ // context an identity and let it bypass the privilege checks in
validate()
+ connectContext.setCurrentUserIdentity(UserIdentity.ROOT);
+ connectContext.setNoAuth(true);
+ connectContext.setSkipAuth(true);
connectContext.setThreadLocalInfo();
}
diff --git a/regression-test/suites/auth_call/test_ddl_constraint_auth.groovy
b/regression-test/suites/auth_call/test_ddl_constraint_auth.groovy
new file mode 100644
index 00000000000..0d610c88d36
--- /dev/null
+++ b/regression-test/suites/auth_call/test_ddl_constraint_auth.groovy
@@ -0,0 +1,129 @@
+// 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.
+
+suite("test_ddl_constraint_auth", "p0,auth_call") {
+ String user = 'test_ddl_constraint_auth_user'
+ String pwd = 'C123_567p'
+ String dbName = 'test_ddl_constraint_auth_db'
+ String tableName = 'test_ddl_constraint_auth_tb'
+ String constraintName = 'test_ddl_constraint_auth_uk'
+
+ try_sql("DROP USER ${user}")
+ try_sql """drop database if exists ${dbName}"""
+ sql """CREATE USER '${user}' IDENTIFIED BY '${pwd}'"""
+ sql """grant select_priv on regression_test to ${user}"""
+ //cloud-mode
+ if (isCloudMode()) {
+ def clusters = sql " SHOW CLUSTERS; "
+ assertTrue(!clusters.isEmpty())
+ def validCluster = clusters[0][0]
+ sql """GRANT USAGE_PRIV ON CLUSTER `${validCluster}` TO ${user}""";
+ }
+
+ sql """create database ${dbName}"""
+ sql """
+ CREATE TABLE IF NOT EXISTS ${dbName}.${tableName} (
+ id BIGINT,
+ username VARCHAR(30)
+ )
+ DISTRIBUTED BY HASH(id) BUCKETS 2
+ PROPERTIES ("replication_num" = "1");
+ """
+
+ // SELECT alone must not be enough to change constraints
+ sql """grant SELECT_PRIV on ${dbName}.${tableName} to ${user}"""
+ connect(user, "${pwd}", context.config.jdbcUrl) {
+ sql """use ${dbName}"""
+ test {
+ sql """ALTER TABLE ${tableName} ADD CONSTRAINT ${constraintName}
UNIQUE (id)"""
+ exception "denied"
+ }
+ }
+
+ sql """use ${dbName}"""
+ sql """ALTER TABLE ${tableName} ADD CONSTRAINT ${constraintName} UNIQUE
(id)"""
+
+ // dropping a constraint is refused as well. Note this goes through the
normal resolution path:
+ // the name-based fallback in DropConstraintCommand only triggers when
resolution throws (e.g. an
+ // external table removed out of band), which is not reproducible from a
suite, so the check on
+ // that branch is covered by inspection only.
+ connect(user, "${pwd}", context.config.jdbcUrl) {
+ sql """use ${dbName}"""
+ test {
+ sql """ALTER TABLE ${tableName} DROP CONSTRAINT
${constraintName}"""
+ exception "denied"
+ }
+ }
+ def constraints = sql """SHOW CONSTRAINTS FROM ${dbName}.${tableName}"""
+ assertTrue(constraints.size() == 1)
+
+ sql """grant ALTER_PRIV on ${dbName}.${tableName} to ${user}"""
+ connect(user, "${pwd}", context.config.jdbcUrl) {
+ sql """use ${dbName}"""
+ sql """ALTER TABLE ${tableName} DROP CONSTRAINT ${constraintName}"""
+ sql """ALTER TABLE ${tableName} ADD CONSTRAINT ${constraintName}
UNIQUE (id)"""
+ }
+ constraints = sql """SHOW CONSTRAINTS FROM ${dbName}.${tableName}"""
+ assertTrue(constraints.size() == 1)
+
+ // dropping a primary key cascades into the foreign keys of every
referencing table, so ALTER on
+ // the referencing tables is required as well
+ String pkTable = 'test_ddl_constraint_auth_pk_tb'
+ String fkTable = 'test_ddl_constraint_auth_fk_tb'
+ String pkName = 'test_ddl_constraint_auth_pk'
+ String fkName = 'test_ddl_constraint_auth_fk'
+ sql """
+ CREATE TABLE IF NOT EXISTS ${dbName}.${pkTable} (
+ id BIGINT NOT NULL,
+ username VARCHAR(30)
+ )
+ DISTRIBUTED BY HASH(id) BUCKETS 2
+ PROPERTIES ("replication_num" = "1");
+ """
+ sql """
+ CREATE TABLE IF NOT EXISTS ${dbName}.${fkTable} (
+ id BIGINT NOT NULL,
+ pk_id BIGINT NOT NULL
+ )
+ DISTRIBUTED BY HASH(id) BUCKETS 2
+ PROPERTIES ("replication_num" = "1");
+ """
+ sql """ALTER TABLE ${dbName}.${pkTable} ADD CONSTRAINT ${pkName} PRIMARY
KEY (id)"""
+ sql """ALTER TABLE ${dbName}.${fkTable} ADD CONSTRAINT ${fkName} FOREIGN
KEY (pk_id) REFERENCES ${pkTable}(id)"""
+
+ sql """grant ALTER_PRIV on ${dbName}.${pkTable} to ${user}"""
+ connect(user, "${pwd}", context.config.jdbcUrl) {
+ sql """use ${dbName}"""
+ test {
+ sql """ALTER TABLE ${pkTable} DROP CONSTRAINT ${pkName}"""
+ exception "denied"
+ }
+ }
+ def fkConstraints = sql """SHOW CONSTRAINTS FROM ${dbName}.${fkTable}"""
+ assertTrue(fkConstraints.size() == 1)
+
+ sql """grant ALTER_PRIV on ${dbName}.${fkTable} to ${user}"""
+ connect(user, "${pwd}", context.config.jdbcUrl) {
+ sql """use ${dbName}"""
+ sql """ALTER TABLE ${pkTable} DROP CONSTRAINT ${pkName}"""
+ }
+ fkConstraints = sql """SHOW CONSTRAINTS FROM ${dbName}.${fkTable}"""
+ assertTrue(fkConstraints.isEmpty())
+
+ sql """drop database if exists ${dbName}"""
+ try_sql("DROP USER ${user}")
+}
diff --git a/regression-test/suites/auth_call/test_ddl_dictionary_auth.groovy
b/regression-test/suites/auth_call/test_ddl_dictionary_auth.groovy
new file mode 100644
index 00000000000..394969acf73
--- /dev/null
+++ b/regression-test/suites/auth_call/test_ddl_dictionary_auth.groovy
@@ -0,0 +1,106 @@
+// 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.
+
+suite("test_ddl_dictionary_auth", "p0,auth_call") {
+ String user = 'test_ddl_dictionary_auth_user'
+ String pwd = 'C123_567p'
+ String dbName = 'test_ddl_dictionary_auth_db'
+ String tableName = 'test_ddl_dictionary_auth_tb'
+ String dictName = 'test_ddl_dictionary_auth_dict'
+
+ try_sql("DROP USER ${user}")
+ try_sql """drop database if exists ${dbName}"""
+ sql """CREATE USER '${user}' IDENTIFIED BY '${pwd}'"""
+ sql """grant select_priv on regression_test to ${user}"""
+ //cloud-mode
+ if (isCloudMode()) {
+ def clusters = sql " SHOW CLUSTERS; "
+ assertTrue(!clusters.isEmpty())
+ def validCluster = clusters[0][0]
+ sql """GRANT USAGE_PRIV ON CLUSTER `${validCluster}` TO ${user}""";
+ }
+
+ sql """create database ${dbName}"""
+ sql """
+ CREATE TABLE IF NOT EXISTS ${dbName}.${tableName} (
+ id BIGINT,
+ username VARCHAR(30)
+ )
+ DISTRIBUTED BY HASH(id) BUCKETS 2
+ PROPERTIES ("replication_num" = "1");
+ """
+ sql """insert into ${dbName}.${tableName} values(1, 'doris')"""
+
+ def createDictSql = """
+ CREATE DICTIONARY ${dbName}.${dictName} USING ${dbName}.${tableName}
+ (
+ id KEY,
+ username VALUE
+ )
+ LAYOUT(HASH_MAP)
+ PROPERTIES('data_lifetime'='600');
+ """
+
+ // no privilege at all on the database
+ connect(user, "${pwd}", context.config.jdbcUrl) {
+ test {
+ sql createDictSql
+ exception "denied"
+ }
+ test {
+ sql """DROP DICTIONARY ${dbName}.${dictName};"""
+ exception "denied"
+ }
+ }
+
+ // CREATE on the database is not enough: the dictionary load task runs as
ADMIN, so the
+ // creator must also be able to read the source table itself.
+ sql """grant CREATE_PRIV on ${dbName}.* to ${user}"""
+ connect(user, "${pwd}", context.config.jdbcUrl) {
+ test {
+ sql createDictSql
+ exception "denied"
+ }
+ }
+
+ sql """grant SELECT_PRIV on ${dbName}.${tableName} to ${user}"""
+ connect(user, "${pwd}", context.config.jdbcUrl) {
+ sql createDictSql
+ }
+ sql """use ${dbName}"""
+ def dictRes = sql """SHOW DICTIONARIES"""
+ assertTrue(dictRes.size() == 1)
+
+ // dropping needs DROP on the database
+ connect(user, "${pwd}", context.config.jdbcUrl) {
+ test {
+ sql """DROP DICTIONARY ${dbName}.${dictName};"""
+ exception "denied"
+ }
+ }
+
+ sql """grant DROP_PRIV on ${dbName}.* to ${user}"""
+ connect(user, "${pwd}", context.config.jdbcUrl) {
+ sql """DROP DICTIONARY ${dbName}.${dictName};"""
+ }
+ sql """use ${dbName}"""
+ dictRes = sql """SHOW DICTIONARIES"""
+ assertTrue(dictRes.size() == 0)
+
+ sql """drop database if exists ${dbName}"""
+ try_sql("DROP USER ${user}")
+}
diff --git a/regression-test/suites/auth_call/test_recycle_bin_auth.groovy
b/regression-test/suites/auth_call/test_recycle_bin_auth.groovy
new file mode 100644
index 00000000000..ea99772dabf
--- /dev/null
+++ b/regression-test/suites/auth_call/test_recycle_bin_auth.groovy
@@ -0,0 +1,82 @@
+// 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.
+
+suite("test_recycle_bin_auth", "p0,auth_call") {
+ String user = 'test_recycle_bin_auth_user'
+ String pwd = 'C123_567p'
+ String dbName = 'test_recycle_bin_auth_db'
+ String tableName = 'test_recycle_bin_auth_tb'
+
+ try_sql("DROP USER ${user}")
+ try_sql """drop database if exists ${dbName}"""
+ sql """CREATE USER '${user}' IDENTIFIED BY '${pwd}'"""
+ sql """grant select_priv on regression_test to ${user}"""
+ //cloud-mode
+ if (isCloudMode()) {
+ def clusters = sql " SHOW CLUSTERS; "
+ assertTrue(!clusters.isEmpty())
+ def validCluster = clusters[0][0]
+ sql """GRANT USAGE_PRIV ON CLUSTER `${validCluster}` TO ${user}""";
+ }
+
+ sql """create database ${dbName}"""
+ sql """
+ CREATE TABLE IF NOT EXISTS ${dbName}.${tableName} (
+ id BIGINT,
+ username VARCHAR(30)
+ )
+ DISTRIBUTED BY HASH(id) BUCKETS 2
+ PROPERTIES ("replication_num" = "1");
+ """
+ // give the user full privileges on the database. erasing the recycle bin
must still be
+ // refused: it takes a raw object id and is irreversible, so it is ADMIN
only.
+ sql """grant ALL on ${dbName}.* to ${user}"""
+
+ sql """drop table ${dbName}.${tableName}"""
+ def binRes = sql """SHOW CATALOG RECYCLE BIN WHERE NAME = "${tableName}"
"""
+ assertTrue(binRes.size() > 0)
+ def tableId = binRes[0][3]
+ logger.info("recycled table id: " + tableId)
+
+ connect(user, "${pwd}", context.config.jdbcUrl) {
+ test {
+ sql """DROP CATALOG RECYCLE BIN WHERE 'TableId' = ${tableId};"""
+ exception "denied"
+ }
+ test {
+ sql """DROP CATALOG RECYCLE BIN WHERE 'DbId' = ${tableId};"""
+ exception "denied"
+ }
+ test {
+ sql """DROP CATALOG RECYCLE BIN WHERE 'PartitionId' =
${tableId};"""
+ exception "denied"
+ }
+ }
+ // still there after the denied attempts
+ def afterRes = sql """SHOW CATALOG RECYCLE BIN WHERE NAME = "${tableName}"
"""
+ assertTrue(afterRes.size() == binRes.size())
+
+ sql """grant admin_priv on *.*.* to ${user}"""
+ connect(user, "${pwd}", context.config.jdbcUrl) {
+ sql """DROP CATALOG RECYCLE BIN WHERE 'TableId' = ${tableId};"""
+ }
+ def erasedRes = sql """SHOW CATALOG RECYCLE BIN WHERE NAME =
"${tableName}" """
+ assertTrue(erasedRes.size() < binRes.size())
+
+ sql """drop database if exists ${dbName}"""
+ try_sql("DROP USER ${user}")
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]