Skip to content

[fix](auth) Add missing privilege checks for several Nereids commands - #66218

Merged
CalvinKirs merged 5 commits into
apache:masterfrom
CalvinKirs:tde_auth
Aug 7, 2026
Merged

[fix](auth) Add missing privilege checks for several Nereids commands#66218
CalvinKirs merged 5 commits into
apache:masterfrom
CalvinKirs:tde_auth

Conversation

@CalvinKirs

@CalvinKirs CalvinKirs commented Jul 29, 2026

Copy link
Copy Markdown
Member

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
    • 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

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.
    • 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.
    • Yes.

The privilege documentation for these statements should list the required privileges.

Several Nereids commands do not check privileges before executing.
The Nereids path in StmtExecutor dispatches straight to Command.run(),
so every command has to enforce its own privileges, and these ones were
missed:

- AdminSetEncryptionRootKeyCommand / AdminRotateTdeRootKeyCommand: now
  require ADMIN, like the other ADMIN SET statements.
- DropCatalogRecycleBinCommand: now requires ADMIN. It takes a raw
  object id and erases instantly, so it cannot be authorized at
  db/table level.
- CreateDictionaryCommand: requires CREATE on the target database plus
  SELECT on the source table, since the load task runs internally.
  DropDictionaryCommand requires DROP on the database.
- AddConstraintCommand / DropConstraintCommand: require ALTER on the
  table. For a foreign key the referenced table is checked as well,
  because it gets a reverse reference registered on it. In
  DropConstraintCommand the check is placed after the name-based
  fallback so neither resolution path can skip it.
- WarmUpClusterCommand / CancelWarmUpJobCommand / DropStageCommand:
  require ADMIN, matching CreateStageCommand which already did.

Add auth_call regression cases for constraint, dictionary and recycle
bin covering both the denied and the granted path.
@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@CalvinKirs

Copy link
Copy Markdown
Member Author

run buildall

@CalvinKirs

Copy link
Copy Markdown
Member Author

/review

…ions

Follow-up on the privilege checks added in the previous commit.

Dictionary: use checkTblPriv on the dictionary itself instead of
checkDbPriv on its database, matching CREATE/DROP TABLE and
CREATE/DROP MTMV. checkDbPriv would reject a user who was granted the
privilege on the dictionary name directly.

Warm up: WARM UP CLUSTER now requires USAGE on the source and
destination compute groups instead of global ADMIN, matching
UseCloudClusterCommand, plus SELECT on each table named by WITH TABLE.
CANCEL WARM UP JOB keeps ADMIN because CloudWarmUpJob records no
owner, so a job cannot be scoped to the user who created it.
…intCommand

WarmUpClusterOnTablesParseTest builds a bare ConnectContext with no user
identity, so the privilege check added to WarmUpClusterCommand.validate()
hit a NullPointerException in getRolesByUserWithLdap(). Neither
checkGlobalPriv(ctx, ...) nor checkCloudPriv(ctx, ...) tolerates a null
identity, which is fine for the real execution path since StmtExecutor
always runs with an authenticated context. Give the test an identity and
let it bypass the checks, since it covers ON TABLES parsing rather than
authorization.

Also make AddConstraintCommand use the ConnectContext passed to run()
instead of the static ConnectContext.get(). DropConstraintCommand already
did, and the static one is null on any path that has no thread local set.
@CalvinKirs
CalvinKirs requested a review from gavinchou as a code owner July 29, 2026 09:31
@CalvinKirs

Copy link
Copy Markdown
Member Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 29006 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 7efece3b359dce9fb468a0637e3c272119fc4aed, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17713	4075	4003	4003
q2	2033	321	198	198
q3	10298	1463	828	828
q4	4683	479	338	338
q5	7511	830	579	579
q6	186	171	136	136
q7	762	814	601	601
q8	9325	1384	1334	1334
q9	5443	4302	4296	4296
q10	6746	1773	1479	1479
q11	508	363	329	329
q12	728	576	452	452
q13	18097	3281	2761	2761
q14	265	262	235	235
q15	q16	789	770	707	707
q17	973	957	1044	957
q18	6978	5783	5508	5508
q19	1325	1271	1097	1097
q20	802	674	553	553
q21	5920	2588	2320	2320
q22	421	358	295	295
Total cold run time: 101506 ms
Total hot run time: 29006 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	4343	4276	4276	4276
q2	285	318	210	210
q3	4576	5007	4370	4370
q4	2014	2141	1349	1349
q5	4342	4249	4267	4249
q6	229	173	129	129
q7	1713	1623	1702	1623
q8	2695	2202	2154	2154
q9	8010	8214	7624	7624
q10	4674	4755	4256	4256
q11	560	416	396	396
q12	788	771	560	560
q13	3406	3984	2960	2960
q14	308	301	319	301
q15	q16	743	751	670	670
q17	1369	1332	1365	1332
q18	8409	7659	7435	7435
q19	1221	1134	1065	1065
q20	2210	2181	1948	1948
q21	5193	4501	4380	4380
q22	515	443	401	401
Total cold run time: 57603 ms
Total hot run time: 51688 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 177551 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 7efece3b359dce9fb468a0637e3c272119fc4aed, data reload: false

query5	4326	628	488	488
query6	461	226	219	219
query7	4994	627	329	329
query8	328	187	170	170
query9	8758	4053	4110	4053
query10	465	348	301	301
query11	5940	2344	2158	2158
query12	157	103	105	103
query13	1269	603	430	430
query14	6275	5186	4906	4906
query14_1	4261	4233	4220	4220
query15	219	205	186	186
query16	1045	484	498	484
query17	1155	707	581	581
query18	2540	483	350	350
query19	218	198	157	157
query20	120	111	108	108
query21	236	163	138	138
query22	13624	13582	13415	13415
query23	17457	16545	16115	16115
query23_1	16158	16237	16182	16182
query24	7481	1790	1295	1295
query24_1	1306	1285	1270	1270
query25	579	462	383	383
query26	1352	392	213	213
query27	2494	582	387	387
query28	4409	2004	2000	2000
query29	1084	629	507	507
query30	343	265	229	229
query31	1123	1100	972	972
query32	109	67	62	62
query33	526	336	258	258
query34	1197	1110	672	672
query35	770	782	679	679
query36	1035	1033	863	863
query37	154	107	94	94
query38	1880	1694	1698	1694
query39	891	873	849	849
query39_1	827	844	848	844
query40	246	164	144	144
query41	67	62	68	62
query42	92	93	89	89
query43	313	327	277	277
query44	1424	763	753	753
query45	191	181	177	177
query46	1047	1191	689	689
query47	2124	2111	2015	2015
query48	407	398	302	302
query49	578	417	300	300
query50	1055	435	373	373
query51	10563	10673	10655	10655
query52	88	86	73	73
query53	254	277	202	202
query54	272	233	226	226
query55	74	75	67	67
query56	323	290	277	277
query57	1327	1276	1220	1220
query58	283	251	281	251
query59	1606	1690	1438	1438
query60	335	273	275	273
query61	153	153	151	151
query62	545	494	437	437
query63	247	196	196	196
query64	2794	1033	829	829
query65	4737	4665	4627	4627
query66	1837	500	433	433
query67	29227	29156	29171	29156
query68	3197	1538	928	928
query69	429	300	269	269
query70	908	828	806	806
query71	370	341	300	300
query72	3041	2743	2413	2413
query73	832	837	435	435
query74	5088	4939	4734	4734
query75	2559	2486	2127	2127
query76	2332	1179	804	804
query77	342	367	292	292
query78	11971	11888	11307	11307
query79	1397	1142	765	765
query80	1331	538	462	462
query81	559	331	285	285
query82	635	153	118	118
query83	376	333	288	288
query84	285	162	129	129
query85	1001	618	561	561
query86	419	247	222	222
query87	1834	1825	1744	1744
query88	3722	2798	2781	2781
query89	453	370	330	330
query90	1929	209	189	189
query91	204	196	164	164
query92	61	60	54	54
query93	1668	1506	988	988
query94	718	366	316	316
query95	801	571	497	497
query96	1031	834	366	366
query97	2623	2616	2494	2494
query98	217	209	200	200
query99	1105	1133	968	968
Total cold run time: 263495 ms
Total hot run time: 177551 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 25.03 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 7efece3b359dce9fb468a0637e3c272119fc4aed, data reload: false

query1	0.00	0.00	0.00
query2	0.09	0.05	0.05
query3	0.26	0.14	0.14
query4	1.60	0.15	0.14
query5	0.24	0.22	0.22
query6	1.25	1.10	1.03
query7	0.04	0.00	0.00
query8	0.05	0.04	0.04
query9	0.39	0.30	0.30
query10	0.53	0.61	0.53
query11	0.20	0.13	0.13
query12	0.19	0.14	0.14
query13	0.47	0.47	0.47
query14	1.03	1.02	1.02
query15	0.61	0.60	0.60
query16	0.33	0.31	0.34
query17	1.08	1.08	1.13
query18	0.22	0.21	0.21
query19	2.14	2.00	1.99
query20	0.02	0.01	0.01
query21	15.47	0.22	0.14
query22	4.81	0.05	0.05
query23	16.16	0.30	0.12
query24	2.94	0.39	0.33
query25	0.11	0.05	0.05
query26	0.74	0.20	0.16
query27	0.04	0.03	0.04
query28	3.56	0.94	0.54
query29	12.48	4.10	3.27
query30	0.27	0.14	0.15
query31	2.78	0.60	0.31
query32	3.22	0.59	0.50
query33	3.12	3.17	3.33
query34	15.65	4.18	3.52
query35	3.56	3.49	3.53
query36	0.55	0.46	0.42
query37	0.09	0.06	0.06
query38	0.06	0.04	0.04
query39	0.04	0.03	0.03
query40	0.18	0.17	0.16
query41	0.08	0.03	0.03
query42	0.04	0.02	0.02
query43	0.04	0.04	0.04
Total cold run time: 96.73 s
Total hot run time: 25.03 s

@github-actions

Copy link
Copy Markdown
Contributor

Codex automated review failed and did not complete.

Error: Review context preparation failed before Codex ran; inspect the 'Prepare authoritative PR context and required AGENTS guides' step.
Workflow run: https://github.com/apache/doris/actions/runs/30430336300

Please inspect the workflow logs and rerun the review after the underlying issue is resolved.

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 29290 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 7efece3b359dce9fb468a0637e3c272119fc4aed, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17649	4100	4073	4073
q2	2042	328	203	203
q3	10314	1406	819	819
q4	4677	477	342	342
q5	7524	847	571	571
q6	187	175	138	138
q7	789	805	618	618
q8	9344	1533	1532	1532
q9	5581	4336	4323	4323
q10	6782	1740	1479	1479
q11	508	349	325	325
q12	756	574	460	460
q13	18098	3424	2739	2739
q14	269	274	245	245
q15	q16	784	779	709	709
q17	965	1064	950	950
q18	6897	5719	5484	5484
q19	1297	1278	1125	1125
q20	820	684	576	576
q21	5839	2654	2274	2274
q22	423	357	305	305
Total cold run time: 101545 ms
Total hot run time: 29290 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	4390	4288	4299	4288
q2	289	311	233	233
q3	4553	5000	4457	4457
q4	2076	2163	1368	1368
q5	4345	4206	4262	4206
q6	226	175	131	131
q7	1733	1648	2071	1648
q8	2659	2206	2172	2172
q9	7987	8173	7821	7821
q10	4628	4660	4219	4219
q11	577	433	382	382
q12	781	746	559	559
q13	3232	3599	2949	2949
q14	300	316	295	295
q15	q16	714	746	669	669
q17	1344	1333	1329	1329
q18	7976	7429	7436	7429
q19	1158	1140	1090	1090
q20	2208	2199	1944	1944
q21	5217	4508	4417	4417
q22	541	468	418	418
Total cold run time: 56934 ms
Total hot run time: 52024 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 177166 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 7efece3b359dce9fb468a0637e3c272119fc4aed, data reload: false

query5	4334	627	490	490
query6	478	228	209	209
query7	4923	594	328	328
query8	360	187	175	175
query9	8777	3995	4036	3995
query10	489	365	308	308
query11	5957	2344	2122	2122
query12	162	102	101	101
query13	1255	562	419	419
query14	6253	5195	4873	4873
query14_1	4216	4218	4222	4218
query15	209	212	182	182
query16	995	471	468	468
query17	939	708	599	599
query18	2456	476	364	364
query19	214	195	158	158
query20	108	109	107	107
query21	231	163	135	135
query22	13706	13499	13277	13277
query23	17353	16424	16157	16157
query23_1	16257	16289	16246	16246
query24	7502	1771	1261	1261
query24_1	1325	1288	1290	1288
query25	578	457	387	387
query26	1361	366	223	223
query27	2567	600	390	390
query28	4453	2046	2046	2046
query29	1078	623	490	490
query30	335	263	227	227
query31	1113	1092	976	976
query32	125	62	62	62
query33	549	325	264	264
query34	1195	1131	643	643
query35	755	793	663	663
query36	1017	1021	906	906
query37	158	110	103	103
query38	1914	1694	1661	1661
query39	888	860	870	860
query39_1	849	822	826	822
query40	255	165	142	142
query41	66	65	64	64
query42	96	92	91	91
query43	317	325	282	282
query44	1413	764	766	764
query45	197	190	173	173
query46	1060	1197	730	730
query47	2118	2135	1997	1997
query48	412	375	297	297
query49	579	408	301	301
query50	1045	429	339	339
query51	10798	10593	10653	10593
query52	85	85	74	74
query53	274	271	204	204
query54	286	234	219	219
query55	75	69	66	66
query56	308	304	293	293
query57	1323	1311	1221	1221
query58	271	263	249	249
query59	1535	1624	1393	1393
query60	317	278	261	261
query61	153	153	150	150
query62	545	494	435	435
query63	242	200	200	200
query64	2816	1093	875	875
query65	4741	4622	4630	4622
query66	1851	544	373	373
query67	29246	28537	28983	28537
query68	3432	1519	1070	1070
query69	421	304	271	271
query70	880	808	819	808
query71	398	347	321	321
query72	3025	2705	2558	2558
query73	873	819	415	415
query74	5080	4897	4742	4742
query75	2548	2503	2159	2159
query76	2285	1188	800	800
query77	348	407	285	285
query78	11899	11844	11431	11431
query79	1377	1166	742	742
query80	1176	555	472	472
query81	542	336	287	287
query82	876	155	125	125
query83	382	332	307	307
query84	324	161	131	131
query85	999	615	525	525
query86	393	238	239	238
query87	1840	1828	1771	1771
query88	3680	2813	2805	2805
query89	444	384	322	322
query90	1938	194	190	190
query91	208	192	165	165
query92	66	60	54	54
query93	1540	1668	987	987
query94	658	360	302	302
query95	810	597	467	467
query96	1128	794	361	361
query97	2618	2608	2495	2495
query98	215	203	196	196
query99	1092	1111	972	972
Total cold run time: 263777 ms
Total hot run time: 177166 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.94 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 7efece3b359dce9fb468a0637e3c272119fc4aed, data reload: false

query1	0.01	0.00	0.01
query2	0.11	0.05	0.05
query3	0.26	0.14	0.13
query4	1.61	0.14	0.14
query5	0.23	0.23	0.22
query6	1.25	1.05	1.09
query7	0.04	0.01	0.00
query8	0.06	0.03	0.03
query9	0.37	0.31	0.30
query10	0.55	0.59	0.58
query11	0.18	0.13	0.14
query12	0.18	0.14	0.14
query13	0.47	0.47	0.46
query14	1.02	1.02	1.01
query15	0.61	0.58	0.60
query16	0.32	0.32	0.33
query17	1.11	1.14	1.08
query18	0.23	0.21	0.21
query19	2.03	1.91	2.00
query20	0.01	0.01	0.01
query21	15.41	0.20	0.14
query22	4.97	0.05	0.05
query23	16.15	0.31	0.13
query24	2.95	0.39	0.34
query25	0.10	0.05	0.05
query26	0.74	0.20	0.16
query27	0.05	0.04	0.04
query28	3.60	0.89	0.53
query29	12.46	4.10	3.26
query30	0.28	0.18	0.15
query31	2.77	0.58	0.32
query32	3.21	0.58	0.48
query33	3.14	3.18	3.22
query34	15.55	4.29	3.50
query35	3.53	3.52	3.50
query36	0.55	0.44	0.40
query37	0.10	0.07	0.07
query38	0.05	0.04	0.03
query39	0.04	0.03	0.03
query40	0.19	0.16	0.15
query41	0.09	0.03	0.03
query42	0.04	0.02	0.03
query43	0.05	0.04	0.03
Total cold run time: 96.67 s
Total hot run time: 24.94 s

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 38.46% (30/78) 🎉
Increment coverage report
Complete coverage report

@CalvinKirs

Copy link
Copy Markdown
Member Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking authorization gaps remain in the constraint, dictionary, and cloud warm-up changes; the six inline comments contain the concrete reproductions and fixes.

Checkpoint conclusions:

  • Goal and effective privileges: incomplete. The intended predicates are ALTER on constraint endpoints, CREATE/DROP for dictionaries plus source SELECT, global ADMIN for the administrative commands, and compute-group USAGE plus table SELECT where applicable. The accepted findings show cross-table mutation, pre-authorization metadata disclosure, row/data-policy bypass, and incomplete warm-up table authorization.
  • Scope and parallel paths: the diff is small, but the review traced constraint cascades/replay, dictionary creation/load/refresh/read, all warm-up syntax modes and job lifecycle, forwarding, privilege inheritance, and the changed tests. No extra review_focus.txt guidance was supplied, so the whole PR was reviewed.
  • Correctness, concurrency, lifecycle, and persistence: constraint metadata changes remain manager-locked and replay/MTMV invalidation are equivalent; dictionary loads and warm-up refreshes run asynchronously, and their missing caller identity is captured by the accepted policy/recheck findings. Warm-up journaling and forwarded context installation are otherwise consistent, with no separate lock, deadlock, atomicity, crash, or failover defect found.
  • Configuration and compatibility: no config, FE-BE variable/contract, function-symbol, storage-format, or rolling-upgrade change is introduced.
  • Performance and observability: the added privilege lookups are not a material hot-path cost. Existing logs identify the affected operations; no separate metrics gap was found, while unauthorized matched-table visibility is part of the accepted ON TABLES issue.
  • Tests: the added suites cover only UNIQUE constraints, basic dictionary grants, and recycle-bin ADMIN, while the warm-up unit test explicitly bypasses auth. They do not cover FK cascades, metadata-order denial, row/mask policies, warm-up catalog mismatch, dynamic matches, or forwarding. Per the review contract, no local build or tests were run.

TableNameInfo tableNameInfo = TableNameInfoUtils.fromCatalogDb(
table.getDatabase().getCatalog(), table.getDatabase(), table);
ImmutableList<String> columns = columnsAndTable.first;
checkAlterPriv(ctx, tableNameInfo);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The relation and columns have already been bound by extractColumnsAndTable() before this ALTER gate; ANALYZED_PLAN returns before the rewrite-stage privilege rule. Consequently a caller without ALTER can distinguish missing tables/columns from valid protected ones, and the FK path similarly probes the referenced side before its second check. Normalize both table names and authorize them first, then bind columns; add no-grant existing/missing-object tests plus an ALTER-without-SELECT success case.

throw new AnalysisException("Failed to create dictionary: " + e.getMessage());
}

// 2. Check auth. Must run after validateAndSet(), which fills in the default catalog/db names.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validateAndSet() has already resolved the source catalog/DB/table, read its full schema, and validated column names/types before either new check runs. A user without SELECT can therefore distinguish missing objects/columns and invalid types from the eventual access denial. Split default-name normalization from metadata binding, run the target/source checks on the normalized names, and only then resolve and validate the source.

String srcCtl = createDictionaryInfo.getSourceCtlName();
String srcDb = createDictionaryInfo.getSourceDbName();
String srcTbl = createDictionaryInfo.getSourceTableName();
if (!Env.getCurrentEnv().getAccessManager().checkTblPriv(ctx, srcCtl, srcDb, srcTbl,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A plain table-level SELECT check does not establish that the creator may read the raw source rows. createDictionary() schedules dataLoad(null, ...), which builds an ADMIN context; LogicalCheckPolicy skips row filters and data masks for ADMIN, so a policy-restricted user can create a dictionary containing rows/unmasked values their own SELECT would hide and read them with dict_get. Preserve the effective principal/policies for the load (and refresh), or otherwise prevent policy-bound sources from being materialized this way.

}
// 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.
if (!Env.getCurrentEnv().getAccessManager().checkTblPriv(ctx, tableNameInfo.getCtl(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This authorizes only the named table, but dropping a primary key calls ConstraintManager.cascadeDropForeignKeys() and removes the actual FK constraints from every table in foreignTableInfos. A user with ALTER on the parent but none on a child can therefore delete the child's constraint. Require ALTER on every referencing table before the atomic mutation (or reject the cascade and require separately authorized FK drops), and cover the cross-table PK/FK case rather than only UNIQUE.


// 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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This USAGE gate admits non-admin ON TABLES jobs, but that mode then scans every internal DB/table and stores matching IDs/names without table authorization; refreshAllTableFilters() adds future matches without an effective identity to reauthorize them. A user with USAGE on both compute groups can therefore warm protected tables. Either keep pattern jobs ADMIN-only, or require SELECT for every initial match and persist the principal so future matches are reauthorized.

if (table == null) {
ErrorReport.reportAnalysisException(ErrorCode.ERR_BAD_TABLE_ERROR, tableNameInfo.getTbl());
}
if (!Env.getCurrentEnv().getAccessManager().checkTblPriv(connectContext, tableNameInfo.getCtl(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The object resolved and later warmed is always internal.db.table, but this check uses the catalog supplied in SQL and the downstream triple drops the catalog. Thus SELECT on ext.db.t can authorize warming the same-named internal.db.t. The internal lookup and OlapTable cast also happen before this check, leaking object/type existence to denied users. Reject non-internal catalogs and authorize the internal fully qualified name before lookup/cast, or carry one resolved catalog/object consistently end to end.

…ternal warm up target

Three follow-ups on the privilege checks:

- DROP CONSTRAINT on a primary key cascades into
  ConstraintManager.cascadeDropForeignKeys(), which deletes the foreign key
  constraints of every referencing table. Require ALTER on those tables too.
- WARM UP ... ON TABLES selects tables by pattern over the whole internal
  catalog and keeps re-matching in the background with no identity, so compute
  group USAGE is not enough to bound it. Require global ADMIN for that mode.
- WARM UP ... WITH TABLE always resolves and warms the internal catalog, but
  the SELECT check used the catalog written in the SQL, so SELECT on
  ext_ctl.db.tbl authorized warming internal.db.tbl. Authorize the internal
  name, and do it before the db/table lookup.

Also extend the constraint regression case with the cross-table PK/FK drop.
@CalvinKirs

Copy link
Copy Markdown
Member Author

run buildall

@CalvinKirs

Copy link
Copy Markdown
Member Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review status: changes requested for head 5ac9cfcd467b3a9759157677358b50aeed58eb50.

I reviewed all 14 changed files plus command dispatch, forwarding, privilege resolution, dictionary load/consume lifecycle, warm-up job lifecycle, constraint locking/journaling, and the added tests. Three new inline issues remain: the PK/FK authorization TOCTOU, dictionary consumers with no query-time authorization, and the regression test that never reaches its claimed fallback path.

Existing-thread reconciliation:

  • Still unresolved: AddConstraint binds before ALTER (r3679441033), CreateDictionary binds before authorization (r3679441037), and ADMIN dictionary loads/refreshes bypass creator policies (r3679441039).
  • Addressed in the current head: ON TABLES is ADMIN-only (r3679441044) and explicit warm-up checks internal-catalog SELECT before lookup (r3679441046). The current child-table checks address the static cascade scope from r3679441042 but remain non-atomic as described inline.

Critical checkpoints:

  • Goal and scope: this is a focused privilege-hardening change, but it is incomplete while the above RBAC gaps remain.
  • Concurrency and atomicity: ConstraintManager serializes mutation, but authorization is over a live child set outside the lock; this is blocking. No other changed command introduces concurrent shared-state mutation.
  • Lifecycle: dictionary initial/automatic loads, manual refresh, and consumers were traced; the missing durable policy principal is already covered by an existing thread, and query-time consumers add a distinct bypass. Warm-up rematching and cancellation are correctly scoped after the current fixes.
  • Forwarding and parallel paths: the original caller identity is preserved to the master; no alternate public TDE, recycle-bin, stage, or warm-up mutation bypass was found.
  • Compatibility, configuration, and protocol: no configuration, FE-BE protocol, storage-format, or rolling-upgrade change.
  • Persistence and data writes: constraint logs replay deterministically, but replay cannot repair an unauthorized raced cascade; no other transaction or persistence defect was found.
  • Tests: the new auth regressions cover basic denied/granted paths, but the fallback claim is unexercised and there is no two-user dictionary-consumer or latch-based constraint-race coverage. Per the runner contract, I performed static review only and did not execute tests.
  • Performance and observability: the new checks are lightweight; no additional metric/logging requirement or material performance issue was found.

User focus: no additional focus was provided; the full PR was reviewed.

// 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.
for (TableNameInfo fkTableInfo : ((PrimaryKeyConstraint) constraint).getForeignTableInfos()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The child checks and cascade are separated by an unlocked TOCTOU window. getConstraint() releases the manager read lock and returns the live PrimaryKeyConstraint; getForeignTableInfos() is only an unmodifiable view of its mutable ArrayList. Another request can add an FK after this loop but before dropConstraint() takes the write lock, so the cascade removes that new child's FK even though this caller was never checked for ALTER there (and mutation during iteration can also throw ConcurrentModificationException). Snapshot the target set, authorize outside the lock, then make the write-locked drop conditional on the same constraint identity/version and exact child set; retry on mismatch. Please add a latch-based ADD-FK/drop-PK test.

String srcCtl = createDictionaryInfo.getSourceCtlName();
String srcDb = createDictionaryInfo.getSourceDbName();
String srcTbl = createDictionaryInfo.getSourceTableName();
if (!Env.getCurrentEnv().getAccessManager().checkTblPriv(ctx, srcCtl, srcDb, srcTbl,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This checks the source only for the creator, but dictionary consumers are not checked. DictGet.customSignatureDict() and DictGetMany.customSignatureDict() resolve the materialized descriptor directly from DictionaryManager without consulting AccessControllerManager/ConnectContext, and a query such as SELECT dict_get('db.secret_dict', 'value', 1) references no source relation that normal SELECT analysis could protect. User B can therefore read a dictionary created by user A despite having no grant on its source. Enforce a query-time rule for both functions (at minimum SELECT on the persisted source, aligned with the effective-policy model from the existing load thread) and add a two-user regression test.

sql """use ${dbName}"""
sql """ALTER TABLE ${tableName} ADD CONSTRAINT ${constraintName} UNIQUE (id)"""

// dropping a constraint is refused as well, including when the user can not even resolve

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case does not exercise the fallback it claims to cover. The user still has SELECT_PRIV, the internal table exists, and extractTable() uses ANALYZED_PLAN, which returns before the rewrite-stage CheckPrivileges rule; the later no-SELECT PK/FK cases therefore do not force the catch branch either. A regression that moves checkAlterPriv() into only the successful-resolution branch, or otherwise skips it after fallback, can leave this suite green. Add a deterministic case where table resolution fails while the unresolved name still maps to stored constraint metadata, then verify both the denied and ALTER-authorized outcomes.

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 29303 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 5ac9cfcd467b3a9759157677358b50aeed58eb50, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17707	4165	4043	4043
q2	2005	321	197	197
q3	10286	1440	822	822
q4	4684	473	335	335
q5	7494	873	561	561
q6	186	167	135	135
q7	758	828	613	613
q8	9322	1535	1626	1535
q9	5526	4318	4295	4295
q10	6756	1738	1464	1464
q11	492	383	336	336
q12	724	581	460	460
q13	18106	3327	2767	2767
q14	265	257	252	252
q15	q16	786	777	713	713
q17	1052	1058	908	908
q18	7031	5691	5506	5506
q19	1171	1222	1114	1114
q20	812	689	611	611
q21	5672	2588	2338	2338
q22	427	355	298	298
Total cold run time: 101262 ms
Total hot run time: 29303 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	4404	4336	4326	4326
q2	292	318	205	205
q3	4603	4979	4366	4366
q4	2050	2125	1362	1362
q5	4428	4224	4270	4224
q6	227	171	125	125
q7	1709	1631	1810	1631
q8	2654	2204	2169	2169
q9	8085	8178	7801	7801
q10	4673	4662	4269	4269
q11	555	414	388	388
q12	773	800	571	571
q13	3237	3655	3013	3013
q14	296	294	278	278
q15	q16	730	739	650	650
q17	1351	1308	1325	1308
q18	8021	7354	7281	7281
q19	1184	1162	1097	1097
q20	2206	2193	1928	1928
q21	5206	4545	4347	4347
q22	525	468	434	434
Total cold run time: 57209 ms
Total hot run time: 51773 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 177311 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 5ac9cfcd467b3a9759157677358b50aeed58eb50, data reload: false

query5	4326	631	479	479
query6	471	219	204	204
query7	4889	599	354	354
query8	364	184	166	166
query9	8779	4006	4029	4006
query10	482	368	305	305
query11	5915	2319	2129	2129
query12	152	101	95	95
query13	1261	617	423	423
query14	6225	5208	4869	4869
query14_1	4202	4194	4226	4194
query15	213	209	180	180
query16	1009	485	417	417
query17	910	695	557	557
query18	2423	467	337	337
query19	208	184	143	143
query20	110	109	108	108
query21	237	153	129	129
query22	13627	13611	13345	13345
query23	17370	16501	16096	16096
query23_1	16267	16286	16310	16286
query24	7492	1736	1275	1275
query24_1	1320	1311	1319	1311
query25	612	435	366	366
query26	1322	343	217	217
query27	2601	610	377	377
query28	4488	2014	2007	2007
query29	1049	597	472	472
query30	340	257	235	235
query31	1104	1085	988	988
query32	105	59	60	59
query33	515	314	244	244
query34	1208	1149	672	672
query35	754	765	678	678
query36	1035	1075	885	885
query37	156	107	90	90
query38	1886	1846	1668	1668
query39	884	884	849	849
query39_1	852	834	843	834
query40	252	166	159	159
query41	77	78	66	66
query42	94	93	88	88
query43	318	325	277	277
query44	1440	804	798	798
query45	203	182	185	182
query46	1092	1245	756	756
query47	2131	2131	1990	1990
query48	403	409	309	309
query49	586	421	322	322
query50	1057	457	351	351
query51	10745	10870	10400	10400
query52	88	86	77	77
query53	268	283	201	201
query54	276	232	232	232
query55	75	72	69	69
query56	307	332	300	300
query57	1313	1305	1191	1191
query58	291	276	265	265
query59	1562	1660	1472	1472
query60	334	284	269	269
query61	179	173	169	169
query62	545	502	442	442
query63	252	201	209	201
query64	2941	1138	961	961
query65	4773	4670	4646	4646
query66	1882	515	406	406
query67	29405	28734	29160	28734
query68	3233	1598	1021	1021
query69	409	322	284	284
query70	916	839	863	839
query71	362	332	397	332
query72	3031	2701	2439	2439
query73	823	805	445	445
query74	5076	4967	4727	4727
query75	2532	2472	2143	2143
query76	2315	1162	755	755
query77	344	386	286	286
query78	11948	12077	11360	11360
query79	1360	1168	731	731
query80	1273	550	477	477
query81	518	328	294	294
query82	584	158	121	121
query83	370	329	294	294
query84	320	166	134	134
query85	974	629	541	541
query86	412	249	233	233
query87	1823	1821	1763	1763
query88	3771	2840	2836	2836
query89	431	384	330	330
query90	1940	207	188	188
query91	202	192	159	159
query92	64	61	55	55
query93	1739	1465	1043	1043
query94	697	355	327	327
query95	785	617	464	464
query96	1165	763	358	358
query97	2642	2667	2485	2485
query98	214	206	204	204
query99	1081	1127	969	969
Total cold run time: 263781 ms
Total hot run time: 177311 ms

@CalvinKirs

Copy link
Copy Markdown
Member Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes.

New findings in this review:

  • Constraint cascade correctness and authorization still rely on a lossy per-child reverse index. With two distinct FKs from one child to the same PK, dropping either FK removes the sole reverse entry; a later PK or parent-table drop misses the surviving FK. See the new inline comment.
  • The new two-table ADD FOREIGN KEY, warm-up/cancel, TDE root-key, and DROP STAGE authorization boundaries have no denied/granted execution coverage. See the four coverage comments.

Existing-thread status after deduplication:

  • Still unresolved at this head: ADD CONSTRAINT and CREATE DICTIONARY bind protected metadata before their new gates; dictionary loads run as ADMIN without the creator's row/data-mask policy and consumers remain unchecked; the PK-drop authorization snapshot is not identity/exact-set fenced against concurrent FK or same-name replacement changes; and the DROP CONSTRAINT fallback remains unproved by a deterministic test. These are already covered by the live threads and were not repeated inline.
  • The current head does address the two earlier warm-up table-identity threads: dynamic ON TABLES jobs are ADMIN-only, and fixed-table authorization now uses the internal object before lookup.

Critical checkpoints:

  • Goal and tests: the PR adds direct checks, but the outstanding production paths and missing authorization matrices mean the stated RBAC fix is not yet proved end to end.
  • Scope and compatibility: the code is generally focused; no configuration, FE/BE protocol, storage-format, or rolling-upgrade change was introduced.
  • Concurrency, lifecycle, and persistence: constraint authorization is still separated from the write-locked mutation, and the new deterministic reverse-index undercount persists in manager state; dictionary policy/consumer lifecycle remains covered by existing blockers. No additional warm-up persistence defect was substantiated beyond the covered threads.
  • Performance and observability: the added predicate checks and snapshots introduce no material hot-path cost, and no new logging/metrics blocker was found.
  • Testing performed: static review only, as required by the review runner; no builds or tests were run. The changed regressions cover constraint, dictionary, and recycle-bin basics, but not the accepted authorization matrices or two-FK invariant.
  • User focus: review_focus.txt contains no additional focus, so the whole PR was reviewed.

}
readLock();
try {
return ImmutableList.copyOf(((PrimaryKeyConstraint) constraint).getForeignTableInfos());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This snapshot assumes foreignTableInfos enumerates every live FK target, but it is only a per-table reverse index. A child can have fk1(a) -> parent(id) and fk2(b) -> parent(id): the second registration is deduplicated by table, then dropping either FK unconditionally removes the child's sole reverse entry while the other FK remains. This method subsequently returns an empty target set, and cascadeDropForeignKeys() uses the same list, so dropping the PK neither authorizes that child nor removes its surviving FK; checkNoReferencingForeignKeys() can also allow the parent table to be dropped. Track exact FK references/refcounts (or verify remaining FKs before removing the child entry) and add a two-FKs-one-child regression.

// context an identity and let it bypass the privilege checks in validate()
connectContext.setCurrentUserIdentity(UserIdentity.ROOT);
connectContext.setNoAuth(true);
connectContext.setSkipAuth(true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fixture now makes every validate() call run as ROOT with both auth bypass flags, so it cannot catch regressions in any of the new security behavior: source/destination USAGE, fixed-table SELECT, ON TABLES ADMIN, or CANCEL WARM UP JOB ADMIN. The existing cloud warm-up suites also invoke these statements as the suite administrator. Please add denied and granted cases with non-root identities for each gate (including independently missing source vs destination USAGE); otherwise these checks can be removed or wired to the wrong object while all tests remain green.


@Override
public void run(ConnectContext ctx, StmtExecutor executor) throws Exception {
validate();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither TDE root-key command has an execution-level authorization test for these new gates. Please exercise both AdminRotateTdeRootKeyCommand and AdminSetEncryptionRootKeyCommand with non-admin and ADMIN contexts (mocking the key manager), and assert that denial happens before rotateRootKey/setRootKey while ADMIN reaches the call. At present either check can be removed or wired to the wrong context without a test failing.


@Override
public void run(ConnectContext ctx, StmtExecutor executor) throws Exception {
validate();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new ADMIN boundary is not exercised by any non-admin test; existing stage tests run as the suite administrator. Add a denied/granted command or cloud regression test and assert that a denied caller never reaches CloudEnv.dropStage(). Otherwise this validate() call can regress without any coverage signal.

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)"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This FK is created as the suite administrator, so it never exercises the new ALTER check on the referenced table; the restricted user only adds a single-table UNIQUE constraint and later tests PK-drop cascade. Add child-only ALTER (deny), parent-only ALTER (deny), and both-granted (succeed) FK-creation cases, and verify neither denial leaves an FK behind. Otherwise the second gate can be removed or checked against the wrong table while this suite remains green.

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 36.11% (26/72) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 43.48% (40/92) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 28858 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 40fe0f09a3df7a454054a8b5566c105a9150bffd, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17594	3912	3927	3912
q2	2013	324	197	197
q3	10274	1404	838	838
q4	4675	465	342	342
q5	7517	824	553	553
q6	183	174	137	137
q7	732	783	658	658
q8	9400	1571	1581	1571
q9	5277	4045	4003	4003
q10	6746	1602	1343	1343
q11	518	348	338	338
q12	724	580	460	460
q13	18089	3268	2715	2715
q14	262	265	248	248
q15	q16	750	734	653	653
q17	1028	991	1046	991
q18	6520	5588	5565	5565
q19	1331	1205	1032	1032
q20	815	685	583	583
q21	5835	2613	2412	2412
q22	429	355	307	307
Total cold run time: 100712 ms
Total hot run time: 28858 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	4260	4184	4176	4176
q2	273	333	220	220
q3	4528	5001	4422	4422
q4	2167	2194	1404	1404
q5	4224	4099	4117	4099
q6	227	171	125	125
q7	1683	1576	1441	1441
q8	2477	2285	2142	2142
q9	7435	7443	7536	7443
q10	4304	4350	3906	3906
q11	565	411	370	370
q12	724	707	516	516
q13	3237	3501	2935	2935
q14	287	296	286	286
q15	q16	686	715	651	651
q17	1290	1274	1249	1249
q18	12218	11090	11805	11090
q19	1143	1131	1184	1131
q20	2262	2244	1916	1916
q21	5575	4812	4889	4812
q22	542	490	423	423
Total cold run time: 60107 ms
Total hot run time: 54757 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 166561 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 40fe0f09a3df7a454054a8b5566c105a9150bffd, data reload: false

query5	4323	598	455	455
query6	478	240	200	200
query7	4849	572	357	357
query8	317	160	143	143
query9	8770	4026	4045	4026
query10	482	348	293	293
query11	5868	2215	2010	2010
query12	144	97	94	94
query13	1257	624	428	428
query14	6112	4251	3961	3961
query14_1	3770	3763	3776	3763
query15	202	197	177	177
query16	976	454	413	413
query17	886	666	563	563
query18	2465	468	345	345
query19	209	186	154	154
query20	108	102	101	101
query21	233	156	134	134
query22	13031	13021	12760	12760
query23	15738	14990	14574	14574
query23_1	15463	15233	15155	15155
query24	7822	1678	1269	1269
query24_1	1258	1257	1247	1247
query25	580	475	374	374
query26	1322	351	214	214
query27	2606	588	383	383
query28	4557	2035	2040	2035
query29	1066	600	500	500
query30	345	261	223	223
query31	1179	1132	1058	1058
query32	110	60	59	59
query33	533	310	251	251
query34	1206	1141	666	666
query35	747	742	632	632
query36	771	766	687	687
query37	153	103	93	93
query38	1836	1752	1696	1696
query39	864	831	787	787
query39_1	790	766	778	766
query40	242	172	147	147
query41	67	65	66	65
query42	94	93	92	92
query43	315	325	274	274
query44	1437	783	766	766
query45	182	175	164	164
query46	1066	1183	718	718
query47	1529	1498	1474	1474
query48	403	441	297	297
query49	568	393	307	307
query50	1088	407	344	344
query51	10628	10688	10508	10508
query52	85	86	73	73
query53	267	283	206	206
query54	277	225	213	213
query55	73	70	66	66
query56	305	312	293	293
query57	1032	995	887	887
query58	282	262	266	262
query59	1511	1610	1422	1422
query60	297	264	251	251
query61	156	149	147	147
query62	400	326	267	267
query63	237	195	200	195
query64	2851	1059	856	856
query65	3880	3801	3827	3801
query66	1821	462	368	368
query67	28219	27473	27263	27263
query68	3136	1485	962	962
query69	403	303	263	263
query70	851	784	767	767
query71	369	332	306	306
query72	2962	2614	2343	2343
query73	836	795	413	413
query74	4629	4494	4296	4296
query75	2342	2335	2004	2004
query76	2337	1168	767	767
query77	347	362	275	275
query78	11251	11196	10759	10759
query79	1386	1196	745	745
query80	1199	525	461	461
query81	500	329	277	277
query82	610	171	136	136
query83	367	319	289	289
query84	313	155	133	133
query85	968	632	537	537
query86	360	231	224	224
query87	2002	1963	1830	1830
query88	3687	2815	2823	2815
query89	383	320	285	285
query90	1883	204	195	195
query91	195	194	163	163
query92	67	58	55	55
query93	1636	1517	1023	1023
query94	658	372	313	313
query95	793	492	564	492
query96	1096	815	353	353
query97	2458	2472	2340	2340
query98	194	189	181	181
query99	734	729	604	604
Total cold run time: 253927 ms
Total hot run time: 166561 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 23.96 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 40fe0f09a3df7a454054a8b5566c105a9150bffd, data reload: false

query1	0.01	0.01	0.01
query2	0.10	0.05	0.05
query3	0.26	0.14	0.14
query4	1.60	0.13	0.13
query5	0.23	0.22	0.22
query6	1.16	0.85	0.79
query7	0.04	0.01	0.01
query8	0.07	0.04	0.04
query9	0.37	0.31	0.32
query10	0.57	0.57	0.58
query11	0.19	0.13	0.14
query12	0.19	0.14	0.13
query13	0.48	0.46	0.46
query14	0.99	1.01	0.99
query15	0.61	0.61	0.58
query16	0.32	0.31	0.31
query17	1.13	1.09	1.10
query18	0.21	0.20	0.19
query19	2.03	2.03	1.94
query20	0.01	0.02	0.01
query21	15.42	0.20	0.15
query22	4.88	0.05	0.05
query23	16.13	0.34	0.12
query24	2.94	0.43	0.32
query25	0.12	0.05	0.05
query26	0.75	0.20	0.15
query27	0.03	0.05	0.03
query28	3.55	0.78	0.35
query29	12.49	3.95	3.15
query30	0.28	0.15	0.15
query31	2.77	0.57	0.32
query32	3.22	0.59	0.48
query33	3.10	3.22	3.22
query34	15.63	3.91	3.30
query35	3.26	3.22	3.24
query36	0.56	0.44	0.43
query37	0.09	0.07	0.06
query38	0.05	0.04	0.04
query39	0.04	0.03	0.03
query40	0.19	0.15	0.16
query41	0.08	0.03	0.03
query42	0.04	0.03	0.03
query43	0.04	0.04	0.03
Total cold run time: 96.23 s
Total hot run time: 23.96 s

@924060929 924060929 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the full diff against the privilege-check conventions (CreateStageCommand, UseCloudClusterCommand, CheckPrivileges, Auth/Role). Overall: the direction is right and I did not find a security hole — every check sits on the real execution path (run()/validate()), forwarding re-executes with the reconstructed user identity, the internal VCG warm-up path (CloudInstanceStatusChecker) never calls validate(), global ADMIN satisfies every predicate via checkGlobalInternal, and PatternMatcher.match(null) returns false so null db/cluster never bypasses. The ConstraintManager snapshot under the read lock and the cascade TOCTOU reasoning are sound. Four minor comments inline.

*/
public void validate() throws AnalysisException {
// check auth
if (!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ConnectContext.get(), PrivPredicate.ADMIN)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: use the ctx passed to run() instead of the static ConnectContext.get() here. Commit 3 of this PR already switched AddConstraintCommand to the passed context because the static one is null on any path with no thread-local set, but the same pattern remains in four new validate() methods (this file, AdminSetEncryptionRootKeyCommand, DropCatalogRecycleBinCommand, DropStageCommand). Equivalent on the real execution path, but inconsistent within the PR — suggest threading ctx through all four.

*/
public void validate(ConnectContext ctx) throws AnalysisException {
// check auth
if (!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ctx, PrivPredicate.ADMIN)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the ADMIN check now runs before the disk-mode check, so in disk mode a non-admin user gets a misleading "Access denied...ADMIN" error instead of "The sql is illegal in disk mode". WarmUpClusterCommand keeps the cloud-mode check first (checks are placed after it) — consider the same ordering here for consistency.

dbName = ctx.getDatabase();
}
// check auth. dictionaries always live in the internal catalog.
if (!Env.getCurrentEnv().getAccessManager().checkTblPriv(ctx, InternalCatalog.INTERNAL_CATALOG_NAME,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: when no default database is selected, dbName stays null and checkTblPriv(db=null) always returns false (PatternMatcher.match(null) is false, so no wildcard bypass either), so the user gets an access-denied error instead of the previous "no database selected" style error from the dictionary manager. Pure UX regression — consider reporting the missing-db case explicitly before the check.

*/
public void validate() throws AnalysisException {
// check auth
if (!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ConnectContext.get(), PrivPredicate.ADMIN)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test coverage note: the two TDE commands (this one and AdminRotateTdeRootKeyCommand) have no regression case even though the denied path is pure FE and testable in disk mode. The cloud-only commands (WARM UP CLUSTER / CANCEL WARM UP JOB / DROP STAGE) are acknowledged as uncovered — acceptable. Also: the constraint suite's comment honestly states the name-based fallback branch is covered by inspection only.

@github-actions github-actions Bot added the approved Indicates a PR has been approved by one committer. label Aug 6, 2026
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

PR approved by at least one committer and no changes requested.

@CalvinKirs
CalvinKirs merged commit 13c57dc into apache:master Aug 7, 2026
33 checks passed
@CalvinKirs
CalvinKirs deleted the tde_auth branch August 7, 2026 06:50
linrrzqqq pushed a commit to linrrzqqq/doris that referenced this pull request Aug 10, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#66218

Problem Summary: Dictionary refresh rebuilt its internal INSERT SELECT statement by concatenating raw target and source object names. Identifier text containing SQL syntax could therefore change the parsed query structure. Quote and escape every identifier component with the existing Nereids helper, and add a parser-level unit test that verifies hostile-looking names remain one source relation.

### Release note

Fix dictionary refresh for quoted object names.

### Check List (For Author)

- Test: Unit test added; not run locally because the required thirdparty protoc binary is unavailable in the worktree
- Behavior changed: Yes; dictionary load SQL now treats all metadata names strictly as identifiers
- Does this need documentation: No
linrrzqqq pushed a commit to linrrzqqq/doris that referenced this pull request Aug 10, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#66218

Problem Summary: Dictionary refresh rebuilt its internal INSERT SELECT statement by concatenating raw target and source object names. Identifier text containing SQL syntax could therefore change the parsed query structure. Quote and escape every identifier component with the existing Nereids helper, and add a parser-level unit test that verifies hostile-looking names remain one source relation.

### Release note

Fix dictionary refresh for quoted object names.

### Check List (For Author)

- Test: Unit test added; not run locally because the required thirdparty protoc binary is unavailable in the worktree
- Behavior changed: Yes; dictionary load SQL now treats all metadata names strictly as identifiers
- Does this need documentation: No
wyxxxcat pushed a commit to wyxxxcat/doris that referenced this pull request Aug 17, 2026
…apache#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:
apache/doris-website#1214 -->

The privilege documentation for these statements should list the
required privileges.
mrhhsg added a commit to mrhhsg/doris that referenced this pull request Aug 31, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#66218

Problem Summary:

`SHOW DICTIONARIES` and `EXPLAIN DICTIONARY` did not check any privilege. Any
user who can `USE` a database (which only needs a privilege on some table of
that database) could list every dictionary of the database together with its
source table name, status and BE data distribution, and describe its columns.
`REFRESH DICTIONARY` only failed inside the internal `INSERT INTO`, after the
dictionary had been looked up and switched to `LOADING`.

This is inconsistent with `SHOW TABLES`, which hides tables the user cannot
show, and with `CREATE/DROP DICTIONARY`, which already require privileges on the
dictionary name (apache#66218).

Dictionaries are authorized like tables of the internal catalog, so:

- `SHOW DICTIONARIES` now skips dictionaries the user has no `SHOW` privilege
  on, the same way `SHOW TABLES` filters tables.
- `EXPLAIN DICTIONARY` requires `SHOW` on the dictionary, like `DESCRIBE` on a
  table.
- `REFRESH DICTIONARY` checks `LOAD` on the dictionary and `SELECT` on the
  source table up front. These are the privileges the internal `INSERT INTO ...
  SELECT` already required, so nobody loses the ability to refresh; the checks
  now happen before the dictionary is resolved and before its status is flipped
  to `LOADING`, so an unauthorized request can no longer block concurrent
  refreshes while its INSERT is being planned.

The checks run before the dictionary is looked up, so a denied user cannot
probe whether a dictionary exists either.

### Release note

None

### Check List (For Author)

- Test
    - [x] Regression test: `auth_call/test_ddl_dictionary_auth` now covers a
      user with a privilege on another table of the database (must not see,
      describe or refresh the dictionary), `SHOW_VIEW` on the database (sees
      the dictionary and its source table, may describe it, still cannot
      refresh), `LOAD` on the database without `SELECT` on the source table
      (rejected before the dictionary starts loading), and both (may refresh).
    - [ ] Unit Test
    - [ ] Manual test (add detailed scripts or steps below)
    - [ ] No need to test or manual test. Explain why:

- Behavior changed:
    - [ ] No.
    - [x] Yes. Users without `SHOW` on a dictionary no longer see it in
      `SHOW DICTIONARIES` and cannot `EXPLAIN DICTIONARY` it. `REFRESH
      DICTIONARY` still needs `LOAD` on the dictionary and `SELECT` on the
      source table, but is now rejected before the dictionary is touched.

- Does this need documentation?
    - [ ] No.
    - [x] Yes. The privilege requirements of the three statements should be
      documented.

Claude-Session: https://claude.ai/code/session_01X9KukfTLYxHmP6iYyEnQtW
mrhhsg added a commit to mrhhsg/doris that referenced this pull request Aug 31, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#66218

Problem Summary:

`SHOW DICTIONARIES` and `EXPLAIN DICTIONARY` did not check any privilege. Any
user who can `USE` a database (which only needs a privilege on some table of
that database) could list every dictionary of the database together with its
source table name, status and BE data distribution, and describe its columns.
`REFRESH DICTIONARY` only failed inside the internal `INSERT INTO`, after the
dictionary had been looked up and switched to `LOADING`. Reading values through
`dict_get()` / `dict_get_many()` never checked any privilege at all.

This is inconsistent with `SHOW TABLES`, which hides tables the user cannot
show, and with `CREATE/DROP DICTIONARY`, which already require privileges on the
dictionary name (apache#66218).

Dictionaries are authorized like tables of the internal catalog, so:

- `SHOW DICTIONARIES` now skips dictionaries the user has no `SHOW` privilege
  on, the same way `SHOW TABLES` filters tables.
- `EXPLAIN DICTIONARY` requires `SHOW` on the dictionary, like `DESCRIBE` on a
  table.
- `REFRESH DICTIONARY` checks `LOAD` on the dictionary and column-aware
  `SELECT` on the dictionary's source columns up front (`checkColumnsPriv`,
  the same contract the generated `INSERT` enforces through `CheckPrivileges`,
  so column-level grants keep working). These are the privileges the reload
  already required; the checks now happen before the dictionary is resolved and
  before its status is flipped to `LOADING`, so an unauthorized request can no
  longer block concurrent refreshes while its INSERT is being planned.
- `dict_get()` / `dict_get_many()` require `SELECT` on the dictionary before
  resolving it, so a user who cannot see a dictionary cannot read its values or
  probe whether it exists.
- `SHOW DICTIONARIES` returns early when the visible set is empty: an empty id
  list means "all dictionaries" to the BE status RPC (`get_dictionary_status`),
  so the previous code fanned status RPCs to every alive BE (also reachable
  before this change via an empty database or a non-matching `LIKE`).

The checks run before the dictionary is looked up, so a denied user cannot
probe whether a dictionary exists either.

### Release note

None

### Check List (For Author)

- Test
    - [x] Regression test: `auth_call/test_ddl_dictionary_auth` now covers a
      user with a privilege on another table of the database (must not see,
      describe or refresh the dictionary), `SHOW_VIEW` on the database (sees
      the dictionary and its source table, may describe it, still cannot
      refresh), `LOAD` on the database without `SELECT` on the source table
      (rejected before the dictionary starts loading), column-level `SELECT`
      on the source columns (may refresh), and `dict_get()` denied until
      `SELECT` on the dictionary is granted.
    - [ ] Unit Test
    - [ ] Manual test (add detailed scripts or steps below)
    - [ ] No need to test or manual test. Explain why:

- Behavior changed:
    - [ ] No.
    - [x] Yes. Users without `SHOW` on a dictionary no longer see it in
      `SHOW DICTIONARIES` and cannot `EXPLAIN DICTIONARY` it. `REFRESH
      DICTIONARY` still needs `LOAD` on the dictionary and (column-aware)
      `SELECT` on the source columns, but is now rejected before the dictionary
      is touched. `dict_get()` / `dict_get_many()` now require `SELECT` on the
      dictionary; internal paths without a user context are unaffected.

- Does this need documentation?
    - [ ] No.
    - [x] Yes. The privilege requirements of the three statements should be
      documented.

Claude-Session: https://claude.ai/code/session_01X9KukfTLYxHmP6iYyEnQtW
mrhhsg added a commit to mrhhsg/doris that referenced this pull request Sep 1, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#66218

Problem Summary:

`SHOW DICTIONARIES` and `EXPLAIN DICTIONARY` did not check any privilege. Any
user who can `USE` a database (which only needs a privilege on some table of
that database) could list every dictionary of the database together with its
source table name, status and BE data distribution, and describe its columns.
`REFRESH DICTIONARY` only failed inside the internal `INSERT INTO`, after the
dictionary had been looked up and switched to `LOADING`. Reading values through
`dict_get()` / `dict_get_many()` never checked any privilege at all.

This is inconsistent with `SHOW TABLES`, which hides tables the user cannot
show, and with `CREATE/DROP DICTIONARY`, which already require privileges on the
dictionary name (apache#66218).

Dictionaries are authorized like tables of the internal catalog, so:

- `SHOW DICTIONARIES` now skips dictionaries the user has no `SHOW` privilege
  on, the same way `SHOW TABLES` filters tables.
- `EXPLAIN DICTIONARY` requires `SHOW` on the dictionary, like `DESCRIBE` on a
  table.
- `REFRESH DICTIONARY` checks `LOAD` on the dictionary and column-aware
  `SELECT` on the dictionary's source columns up front (`checkColumnsPriv` on
  the source columns' own names, the same contract the generated `INSERT`
  enforces through `CheckPrivileges`, so column-level grants keep working even
  when the dictionary definition spells the columns differently). These are the
  privileges the reload already required; the checks now happen before the
  dictionary is resolved and before its status is flipped to `LOADING`, so an
  unauthorized request can no longer block concurrent refreshes while its
  INSERT is being planned.
- `dict_get()` / `dict_get_many()` require `SELECT` on the dictionary before
  resolving it, so a user who cannot see a dictionary cannot read its values or
  probe whether it exists.
- Statements that read a dictionary are kept out of the sql cache and are not
  short-circuited as reusable point-query plans of a server prepared statement:
  neither mechanism records the dictionary's privilege or version, so a cached
  result / reused plan could survive a revoke or a refresh. Dictionaries are a
  cache already, so the lost sql-cache hit is not a real cost.
- `SHOW DICTIONARIES` returns early when the visible set is empty: an empty id
  list means "all dictionaries" to the BE status RPC (`get_dictionary_status`),
  so the previous code fanned status RPCs to every alive BE (also reachable
  before this change via an empty database or a non-matching `LIKE`).

The checks run before the dictionary is looked up, so a denied user cannot
probe whether a dictionary exists either.

### Release note

None

### Check List (For Author)

- Test
    - [x] Regression test: `auth_call/test_ddl_dictionary_auth` now covers a
      user with a privilege on another table of the database (must not see,
      describe or refresh the dictionary), `SHOW_VIEW` on the database (sees
      the dictionary and its source table, may describe it, still cannot
      refresh), `LOAD` on the database without `SELECT` on the source table
      (rejected before the dictionary starts loading), column-level `SELECT`
      on the source columns (may refresh), and `dict_get()` denied until
      `SELECT` on the dictionary is granted. New
      `auth_call/test_dictionary_read_auth_cache` (nonConcurrent) covers a
      dictionary spelling its columns differently from the source table, a
      primed sql-cache statement that is denied right after the revoke, and a
      server prepared point query whose second EXECUTE is denied after the
      revoke.
    - [ ] Unit Test
    - [ ] Manual test (add detailed scripts or steps below)
    - [ ] No need to test or manual test. Explain why:

- Behavior changed:
    - [ ] No.
    - [x] Yes. Users without `SHOW` on a dictionary no longer see it in
      `SHOW DICTIONARIES` and cannot `EXPLAIN DICTIONARY` it. `REFRESH
      DICTIONARY` still needs `LOAD` on the dictionary and (column-aware)
      `SELECT` on the source columns, but is now rejected before the dictionary
      is touched. `dict_get()` / `dict_get_many()` now require `SELECT` on the
      dictionary; internal paths without a user context are unaffected.
      Statements reading a dictionary no longer use the sql cache or a reused
      short-circuit point-query plan.

- Does this need documentation?
    - [ ] No.
    - [x] Yes. The privilege requirements of the three statements should be
      documented.

Claude-Session: https://claude.ai/code/session_01X9KukfTLYxHmP6iYyEnQtW
mrhhsg added a commit to mrhhsg/doris that referenced this pull request Sep 1, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#66218

Problem Summary:

`SHOW DICTIONARIES` and `EXPLAIN DICTIONARY` did not check any privilege. Any
user who can `USE` a database (which only needs a privilege on some table of
that database) could list every dictionary of the database together with its
source table name, status and BE data distribution, and describe its columns.
`REFRESH DICTIONARY` only failed inside the internal `INSERT INTO`, after the
dictionary had been looked up and switched to `LOADING`. Reading values through
`dict_get()` / `dict_get_many()` never checked any privilege at all.

This is inconsistent with `SHOW TABLES`, which hides tables the user cannot
show, and with `CREATE/DROP DICTIONARY`, which already require privileges on the
dictionary name (apache#66218).

Dictionaries are authorized like tables of the internal catalog, so:

- `SHOW DICTIONARIES` now skips dictionaries the user has no `SHOW` privilege
  on, the same way `SHOW TABLES` filters tables.
- `EXPLAIN DICTIONARY` requires `SHOW` on the dictionary, like `DESCRIBE` on a
  table.
- `REFRESH DICTIONARY` checks `LOAD` on the dictionary and column-aware
  `SELECT` on the dictionary's source columns up front (`checkColumnsPriv` on
  the source columns' own names, the same contract the generated `INSERT`
  enforces through `CheckPrivileges`, so column-level grants keep working even
  when the dictionary definition spells the columns differently). These are the
  privileges the reload already required; the checks now happen before the
  dictionary is resolved and before its status is flipped to `LOADING`, so an
  unauthorized request can no longer block concurrent refreshes while its
  INSERT is being planned.
- `dict_get()` / `dict_get_many()` require `SELECT` on the dictionary. The
  check runs once at bind time in `ExpressionAnalyzer`, before the dictionary
  is resolved (no existence probing) and only outside persisted view bodies:
  a view is authorized as a whole by `CheckPrivileges.visitLogicalView`, so a
  user holding `SELECT` on the view alone keeps reading through it, while
  direct calls, alias function bodies and `CREATE/ALTER VIEW` bodies are
  checked against the caller.
- Statements that read a dictionary are kept out of the sql cache and are not
  short-circuited as reusable point-query plans of a server prepared statement:
  neither mechanism records the dictionary's privilege or version, so a cached
  result / reused plan could survive a revoke or a refresh. Both marks are set
  on the `StatementContext` at bind time, so a read that constant folding later
  turns into a literal still counts. Dictionaries are a cache already, so the
  lost sql-cache hit is not a real cost.
- `REFRESH DICTIONARY` authorizes the source columns against the resolved
  source table (catalog, database and table names as the load scan reports
  them), not the spelling persisted by `CREATE DICTIONARY`, which can differ
  under `lower_case_table_names`.
- `SHOW DICTIONARIES` returns early when the visible set is empty: an empty id
  list means "all dictionaries" to the BE status RPC (`get_dictionary_status`),
  so the previous code fanned status RPCs to every alive BE (also reachable
  before this change via an empty database or a non-matching `LIKE`).

The checks run before the dictionary is looked up, so a denied user cannot
probe whether a dictionary exists either.

### Release note

None

### Check List (For Author)

- Test
    - [x] Regression test: `auth_call/test_ddl_dictionary_auth` now covers a
      user with a privilege on another table of the database (must not see,
      describe or refresh the dictionary), `SHOW_VIEW` on the database (sees
      the dictionary and its source table, may describe it, still cannot
      refresh), `LOAD` on the database without `SELECT` on the source table
      (rejected before the dictionary starts loading), column-level `SELECT`
      on the source columns (may refresh), and `dict_get()` denied until
      `SELECT` on the dictionary is granted. New
      `auth_call/test_dictionary_read_auth_cache` (nonConcurrent) covers a
      dictionary spelling its columns differently from the source table, a
      primed sql-cache statement that is denied right after the revoke, a
      server prepared point query whose second EXECUTE is denied after the
      revoke (with a non-constant and, under `enable_fold_constant_by_be`, a
      literal-only dictionary read), view-only reads through views built on
      `dict_get()` / `dict_get_many()` with direct calls as negative controls,
      and a global alias function wrapping `dict_get()`.
    - [ ] Unit Test
    - [ ] Manual test (add detailed scripts or steps below)
    - [ ] No need to test or manual test. Explain why:

- Behavior changed:
    - [ ] No.
    - [x] Yes. Users without `SHOW` on a dictionary no longer see it in
      `SHOW DICTIONARIES` and cannot `EXPLAIN DICTIONARY` it. `REFRESH
      DICTIONARY` still needs `LOAD` on the dictionary and (column-aware)
      `SELECT` on the source columns, but is now rejected before the dictionary
      is touched. `dict_get()` / `dict_get_many()` now require `SELECT` on the
      dictionary; internal paths without a user context are unaffected.
      Statements reading a dictionary no longer use the sql cache or a reused
      short-circuit point-query plan. Expressions bound in the executing user's
      statement that call `dict_get()` — row policies, generated columns, sync
      materialized view definitions, alias function bodies — now require that
      user to hold `SELECT` on the dictionary, like any other read of it.

- Does this need documentation?
    - [ ] No.
    - [x] Yes. The privilege requirements of the three statements should be
      documented.

Claude-Session: https://claude.ai/code/session_01X9KukfTLYxHmP6iYyEnQtW
mrhhsg added a commit to mrhhsg/doris that referenced this pull request Sep 1, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#66218

Problem Summary:

`SHOW DICTIONARIES` and `EXPLAIN DICTIONARY` did not check any privilege. Any
user who can `USE` a database (which only needs a privilege on some table of
that database) could list every dictionary of the database together with its
source table name, status and BE data distribution, and describe its columns.
This is inconsistent with `SHOW TABLES`, which hides tables the user cannot
show, and with `CREATE/DROP DICTIONARY`, which already require privileges on the
dictionary name (apache#66218).

Dictionaries are authorized like tables of the internal catalog, so:

- `SHOW DICTIONARIES` skips dictionaries the user has no `SHOW` privilege on,
  the same way `SHOW TABLES` filters tables. When nothing is visible it returns
  before collecting BE status: an empty id list means "all dictionaries" to the
  `get_dictionary_status` RPC, so the previous code fanned status RPCs to every
  alive BE for that case (also reachable before this change through an empty
  database or a non-matching `LIKE`).
- `EXPLAIN DICTIONARY` requires `SHOW` on the dictionary, like `DESCRIBE` on a
  table.
- `REFRESH DICTIONARY` checks `LOAD` on the dictionary up front, like `DROP
  DICTIONARY` checks `DROP`. The internal `INSERT INTO` already required it, so
  nobody loses the ability to refresh; the check now happens before the
  dictionary is resolved and before its status is switched to `LOADING`.

The checks run before the dictionary is looked up, so a denied user cannot
probe whether a dictionary exists either.

Reading dictionary values through `dict_get()` / `dict_get_many()` has never
been authorized; that is a separate change and is tracked in apache#67345.

### Release note

None

### Check List (For Author)

- Test
    - [x] Regression test: `auth_call/test_ddl_dictionary_auth` now covers a
      user with a privilege on another table of the database (must not see,
      describe or refresh the dictionary), `SHOW_VIEW` on the database (sees
      the dictionary and its source table, may describe it, still cannot
      refresh, and the denied refresh leaves the dictionary untouched), and
      `LOAD` on the database (may refresh).
    - [ ] Unit Test
    - [ ] Manual test (add detailed scripts or steps below)
    - [ ] No need to test or manual test. Explain why:

- Behavior changed:
    - [ ] No.
    - [x] Yes. Users without `SHOW` on a dictionary no longer see it in
      `SHOW DICTIONARIES` and cannot `EXPLAIN DICTIONARY` it. `REFRESH
      DICTIONARY` still needs `LOAD` on the dictionary, but is now rejected
      before the dictionary is touched.

- Does this need documentation?
    - [ ] No.
    - [x] Yes. The privilege requirements of the three statements should be
      documented.

Claude-Session: https://claude.ai/code/session_01X9KukfTLYxHmP6iYyEnQtW
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by one committer. dev/4.0.x dev/4.1.x dev/4.1.x-conflict

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants