1818 */
1919package org .apache .iotdb .db .it .cq ;
2020
21+ import org .apache .iotdb .common .rpc .thrift .TEndPoint ;
22+ import org .apache .iotdb .common .rpc .thrift .TSStatus ;
23+ import org .apache .iotdb .commons .client .ClientPoolFactory ;
24+ import org .apache .iotdb .commons .client .IClientManager ;
25+ import org .apache .iotdb .commons .client .sync .SyncDataNodeInternalServiceClient ;
2126import org .apache .iotdb .it .env .EnvFactory ;
27+ import org .apache .iotdb .it .env .cluster .node .DataNodeWrapper ;
2228import org .apache .iotdb .it .framework .IoTDBTestRunner ;
2329import org .apache .iotdb .itbase .category .ClusterIT ;
2430import org .apache .iotdb .itbase .category .LocalStandaloneIT ;
31+ import org .apache .iotdb .mpp .rpc .thrift .TExecuteCQ ;
32+ import org .apache .iotdb .rpc .TSStatusCode ;
2533
2634import org .junit .AfterClass ;
2735import org .junit .BeforeClass ;
3139
3240import java .sql .Connection ;
3341import java .sql .ResultSet ;
42+ import java .sql .SQLException ;
3443import java .sql .Statement ;
44+ import java .util .ArrayDeque ;
45+ import java .util .Deque ;
3546import java .util .concurrent .TimeUnit ;
3647
48+ import static org .apache .iotdb .db .it .utils .TestUtils .resultSetEqualTest ;
3749import static org .apache .iotdb .itbase .constant .TestConstant .TIMESTAMP_STR ;
3850import static org .junit .Assert .assertEquals ;
51+ import static org .junit .Assert .assertTrue ;
3952import static org .junit .Assert .fail ;
4053
4154@ RunWith (IoTDBTestRunner .class )
@@ -52,6 +65,205 @@ public static void tearDown() throws Exception {
5265 EnvFactory .getEnv ().cleanClusterEnvironment ();
5366 }
5467
68+ @ Test
69+ public void testCQExecutionWithReadDataPermission () throws Exception {
70+ testCQExecutionWithReadDataPermission (false , false );
71+ }
72+
73+ @ Test
74+ public void testCQExecutionWithReadDataPermissionAlignByDevice () throws Exception {
75+ testCQExecutionWithReadDataPermission (true , false );
76+ }
77+
78+ @ Test
79+ public void testCQExecutionWithExplicitTargetReadDataPermission () throws Exception {
80+ testCQExecutionWithReadDataPermission (false , true );
81+ }
82+
83+ @ Test
84+ public void testCQExecutionWithExplicitTargetReadDataPermissionAlignByDevice () throws Exception {
85+ testCQExecutionWithReadDataPermission (true , true );
86+ }
87+
88+ private void testCQExecutionWithReadDataPermission (boolean alignByDevice , boolean explicitTargets )
89+ throws Exception {
90+ String cqId =
91+ (explicitTargets ? "cq_exp_auth" : "cq_read_auth" ) + (alignByDevice ? "_aligned" : "" );
92+ String database = "root." + cqId ;
93+ String username = cqId + "_user" ;
94+ String password = "CQRead!Test2026" ;
95+ String firstTarget = explicitTargets ? "m1" : "s1" ;
96+ String secondTarget = explicitTargets ? "m2" : "s2" ;
97+ String queryBody =
98+ "SELECT "
99+ + (explicitTargets ? "s1, s2" : "*" )
100+ + " INTO "
101+ + database
102+ + ".target("
103+ + (explicitTargets ? "m1, m2" : "::" )
104+ + ") FROM "
105+ + database
106+ + ".source"
107+ + (alignByDevice ? " ALIGN BY DEVICE" : "" );
108+ String resultQuery =
109+ "SELECT " + firstTarget + ", " + secondTarget + " FROM " + database + ".target" ;
110+ String resultHeader =
111+ "Time,"
112+ + database
113+ + ".target."
114+ + firstTarget
115+ + ","
116+ + database
117+ + ".target."
118+ + secondTarget
119+ + "," ;
120+
121+ IClientManager <TEndPoint , SyncDataNodeInternalServiceClient > clients =
122+ new IClientManager .Factory <TEndPoint , SyncDataNodeInternalServiceClient >()
123+ .createClientManager (
124+ new ClientPoolFactory .SyncDataNodeInternalServiceClientPoolFactory ());
125+ try (Connection connection = EnvFactory .getEnv ().getConnection ();
126+ Statement statement = connection .createStatement ();
127+ CQTestCleanup cleanup = new CQTestCleanup (statement )) {
128+ statement .execute ("CREATE USER " + username + " '" + password + "'" );
129+ cleanup .add ("DROP USER " + username );
130+ statement .execute ("CREATE DATABASE " + database );
131+ cleanup .add ("DELETE DATABASE " + database );
132+ statement .execute ("CREATE TIMESERIES " + database + ".source.s1 WITH DATATYPE=INT64" );
133+ statement .execute ("CREATE TIMESERIES " + database + ".source.s2 WITH DATATYPE=INT64" );
134+ statement .execute (
135+ "CREATE TIMESERIES " + database + ".target." + firstTarget + " WITH DATATYPE=INT64" );
136+ statement .execute (
137+ "CREATE TIMESERIES " + database + ".target." + secondTarget + " WITH DATATYPE=INT64" );
138+ statement .execute (
139+ "INSERT INTO "
140+ + database
141+ + ".source(time, s1, s2) VALUES (1, 11, 21), (1001, 12, 22), (2001, 13, 23), (3001, 14, 24)" );
142+ statement .execute ("GRANT SYSTEM ON root.** TO USER " + username );
143+ statement .execute ("GRANT WRITE_DATA ON " + database + ".target.** TO USER " + username );
144+ statement .execute ("GRANT READ_DATA ON " + database + ".source.s1 TO USER " + username );
145+
146+ try (Connection userConnection = EnvFactory .getEnv ().getConnection (username , password );
147+ Statement userStatement = userConnection .createStatement ()) {
148+ // Keep the scheduler idle and execute deterministic windows through its DataNode RPC.
149+ userStatement .execute (
150+ "CREATE CQ "
151+ + cqId
152+ + " RESAMPLE EVERY 1s BOUNDARY "
153+ + (System .currentTimeMillis () + TimeUnit .DAYS .toMillis (1 ))
154+ + " BEGIN "
155+ + queryBody
156+ + " END" );
157+ cleanup .add ("DROP CQ " + cqId );
158+ }
159+
160+ TExecuteCQ request = new TExecuteCQ (queryBody , 0 , 1000 , 10000 , "UTC" , cqId , username );
161+ String [] expectedRows ;
162+ if (explicitTargets ) {
163+ String columnMismatch =
164+ "the number of source columns and the number of target paths should be the same" ;
165+ executeCQOnAllDataNodes (clients , request , TSStatusCode .SEMANTIC_ERROR , columnMismatch );
166+ resultSetEqualTest (resultQuery , resultHeader , new String [0 ]);
167+
168+ // Explicit targets retain SELECT INTO's positional mapping. Restore access and retry the
169+ // same window, as the CQ scheduler does after an execution failure.
170+ statement .execute ("GRANT READ_DATA ON " + database + ".source.s2 TO USER " + username );
171+ executeCQOnAllDataNodes (clients , request , TSStatusCode .SUCCESS_STATUS , null );
172+ resultSetEqualTest (resultQuery , resultHeader , new String [] {"1,11,21," });
173+
174+ statement .execute ("REVOKE READ_DATA ON " + database + ".source.s2 FROM USER " + username );
175+ request .setStartTime (1000 ).setEndTime (2000 );
176+ executeCQOnAllDataNodes (clients , request , TSStatusCode .SEMANTIC_ERROR , columnMismatch );
177+ resultSetEqualTest (resultQuery , resultHeader , new String [] {"1,11,21," });
178+
179+ statement .execute ("GRANT READ_DATA ON " + database + ".source.s2 TO USER " + username );
180+ executeCQOnAllDataNodes (clients , request , TSStatusCode .SUCCESS_STATUS , null );
181+ expectedRows = new String [] {"1,11,21," , "1001,12,22," };
182+ } else {
183+ executeCQOnAllDataNodes (clients , request , TSStatusCode .SUCCESS_STATUS , null );
184+ // The wildcard must not copy s2, for which the CQ owner has no READ_DATA privilege.
185+ resultSetEqualTest (resultQuery , resultHeader , new String [] {"1,11,null," });
186+
187+ statement .execute ("REVOKE READ_DATA ON " + database + ".source.s1 FROM USER " + username );
188+ request .setStartTime (1000 ).setEndTime (2000 );
189+ executeCQOnAllDataNodes (clients , request , TSStatusCode .SUCCESS_STATUS , null );
190+ resultSetEqualTest (resultQuery , resultHeader , new String [] {"1,11,null," });
191+
192+ statement .execute ("GRANT READ_DATA ON " + database + ".source.** TO USER " + username );
193+ request .setStartTime (2000 ).setEndTime (3000 );
194+ executeCQOnAllDataNodes (clients , request , TSStatusCode .SUCCESS_STATUS , null );
195+ expectedRows = new String [] {"1,11,null," , "2001,13,23," };
196+ }
197+ resultSetEqualTest (resultQuery , resultHeader , expectedRows );
198+
199+ statement .execute ("DROP USER " + username );
200+ cleanup .remove ("DROP USER " + username );
201+ request .setStartTime (3000 ).setEndTime (4000 );
202+ executeCQOnAllDataNodes (clients , request , TSStatusCode .EXECUTE_STATEMENT_ERROR , username );
203+ resultSetEqualTest (resultQuery , resultHeader , expectedRows );
204+ } finally {
205+ clients .close ();
206+ }
207+ }
208+
209+ private void executeCQOnAllDataNodes (
210+ IClientManager <TEndPoint , SyncDataNodeInternalServiceClient > clients ,
211+ TExecuteCQ request ,
212+ TSStatusCode expectedStatus ,
213+ String expectedMessage )
214+ throws Exception {
215+ for (DataNodeWrapper dataNode : EnvFactory .getEnv ().getDataNodeWrapperList ()) {
216+ try (SyncDataNodeInternalServiceClient client =
217+ clients .borrowClient (
218+ new TEndPoint (dataNode .getInternalAddress (), dataNode .getInternalPort ()))) {
219+ TSStatus status = client .executeCQ (request );
220+ assertEquals (status .toString (), expectedStatus .getStatusCode (), status .getCode ());
221+ if (expectedMessage != null ) {
222+ assertTrue (status .toString (), status .isSetMessage ());
223+ assertTrue (status .getMessage (), status .getMessage ().contains (expectedMessage ));
224+ }
225+ }
226+ }
227+ }
228+
229+ private static class CQTestCleanup implements AutoCloseable {
230+ private final Statement statement ;
231+ private final Deque <String > statements = new ArrayDeque <>();
232+
233+ private CQTestCleanup (Statement statement ) {
234+ this .statement = statement ;
235+ }
236+
237+ private void add (String sql ) {
238+ statements .push (sql );
239+ }
240+
241+ private void remove (String sql ) {
242+ statements .remove (sql );
243+ }
244+
245+ @ Override
246+ public void close () throws SQLException {
247+ // Attempt every cleanup. Try-with-resources preserves the original test failure and adds
248+ // cleanup failures as suppressed exceptions.
249+ SQLException failure = null ;
250+ for (String sql : statements ) {
251+ try {
252+ statement .execute (sql );
253+ } catch (SQLException e ) {
254+ if (failure == null ) {
255+ failure = e ;
256+ } else {
257+ failure .addSuppressed (e );
258+ }
259+ }
260+ }
261+ if (failure != null ) {
262+ throw failure ;
263+ }
264+ }
265+ }
266+
55267 @ Test
56268 public void testCQExecution1 () {
57269 String insertTemplate =
0 commit comments