Skip to content

Commit cca1dbb

Browse files
authored
[CQ] Check READ_DATA permissions during continuous query execution
1 parent bddc548 commit cca1dbb

6 files changed

Lines changed: 406 additions & 9 deletions

File tree

‎integration-test/src/test/java/org/apache/iotdb/db/it/cq/IoTDBCQExecIT.java‎

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,18 @@
1818
*/
1919
package 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;
2126
import org.apache.iotdb.it.env.EnvFactory;
27+
import org.apache.iotdb.it.env.cluster.node.DataNodeWrapper;
2228
import org.apache.iotdb.it.framework.IoTDBTestRunner;
2329
import org.apache.iotdb.itbase.category.ClusterIT;
2430
import org.apache.iotdb.itbase.category.LocalStandaloneIT;
31+
import org.apache.iotdb.mpp.rpc.thrift.TExecuteCQ;
32+
import org.apache.iotdb.rpc.TSStatusCode;
2533

2634
import org.junit.AfterClass;
2735
import org.junit.BeforeClass;
@@ -31,11 +39,16 @@
3139

3240
import java.sql.Connection;
3341
import java.sql.ResultSet;
42+
import java.sql.SQLException;
3443
import java.sql.Statement;
44+
import java.util.ArrayDeque;
45+
import java.util.Deque;
3546
import java.util.concurrent.TimeUnit;
3647

48+
import static org.apache.iotdb.db.it.utils.TestUtils.resultSetEqualTest;
3749
import static org.apache.iotdb.itbase.constant.TestConstant.TIMESTAMP_STR;
3850
import static org.junit.Assert.assertEquals;
51+
import static org.junit.Assert.assertTrue;
3952
import 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 =

‎iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121

2222
/** Compile-time i18n constants for DataNode misc subsystems (English). */
2323
public final class DataNodeMiscMessages {
24+
public static final String LOG_FAILED_TO_AUTHORIZE_CQ_ARG_FOR_USER_ARG_ARG_B436A81F =
25+
"Failed to authorize CQ {} for user {}: {}";
26+
2427
public static final String MESSAGE_QUERY_DOES_NOT_BELONG_TO_CURRENT_SESSION_A1198237 =
2528
"The requested query does not belong to the current session.";
2629

‎iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121

2222
/** 编译时国际化常量 - DataNode 杂项子系统(中文)。 */
2323
public final class DataNodeMiscMessages {
24+
public static final String LOG_FAILED_TO_AUTHORIZE_CQ_ARG_FOR_USER_ARG_ARG_B436A81F =
25+
"CQ {} 的用户 {} 鉴权失败:{}";
26+
2427
public static final String MESSAGE_QUERY_DOES_NOT_BELONG_TO_CURRENT_SESSION_A1198237 =
2528
"请求的查询不属于当前会话。";
2629

‎iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/thrift/impl/DataNodeInternalRPCServiceImpl.java‎

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@
187187
import org.apache.iotdb.db.queryengine.plan.relational.planner.node.schema.RollbackTableDevicesBlackListNode;
188188
import org.apache.iotdb.db.queryengine.plan.relational.planner.node.schema.TableAttributeColumnDropNode;
189189
import org.apache.iotdb.db.queryengine.plan.relational.planner.node.schema.TableSchemaQueryWriteVisitor;
190+
import org.apache.iotdb.db.queryengine.plan.relational.security.TreeAccessCheckContext;
190191
import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.DeleteDevice;
191192
import org.apache.iotdb.db.queryengine.plan.statement.component.WhereCondition;
192193
import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowStatement;
@@ -1875,17 +1876,16 @@ public TSStatus executeCQ(TExecuteCQ req) {
18751876

18761877
SESSION_MANAGER.registerSession(session);
18771878

1878-
SESSION_MANAGER.supplySession(
1879-
session,
1880-
// TODO: User the real userId
1881-
-1,
1882-
req.getUsername(),
1883-
ZoneId.of(req.getZoneId()),
1884-
ClientVersion.V_1_0);
1885-
18861879
String executedSQL = req.queryBody;
18871880

18881881
try {
1882+
SESSION_MANAGER.supplySession(
1883+
session,
1884+
AuthorityChecker.getUserId(req.getUsername()).orElse(-1L),
1885+
req.getUsername(),
1886+
ZoneId.of(req.getZoneId()),
1887+
ClientVersion.V_1_0);
1888+
18891889
QueryStatement s =
18901890
(QueryStatement) StatementGenerator.createStatement(req.queryBody, session.getZoneId());
18911891
if (s == null) {
@@ -1918,6 +1918,22 @@ public TSStatus executeCQ(TExecuteCQ req) {
19181918
}
19191919
executedSQL = String.join(" ", s.constructFormattedSQL().split("\n")).replaceAll(" +", " ");
19201920

1921+
// Refresh the owner's READ_DATA scope for each CQ execution.
1922+
TSStatus status =
1923+
AuthorityChecker.checkAuthority(
1924+
s,
1925+
new TreeAccessCheckContext(
1926+
session.getUserId(), session.getUsername(), session.getClientAddress())
1927+
.setSqlString(executedSQL));
1928+
if (status.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
1929+
LOGGER.warn(
1930+
DataNodeMiscMessages.LOG_FAILED_TO_AUTHORIZE_CQ_ARG_FOR_USER_ARG_ARG_B436A81F,
1931+
req.getCqId(),
1932+
req.getUsername(),
1933+
status);
1934+
return status;
1935+
}
1936+
19211937
long queryId =
19221938
SESSION_MANAGER.requestQueryId(session, SESSION_MANAGER.requestStatementId(session));
19231939
// Create and cache dataset

‎iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/security/TreeAccessCheckVisitor.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1395,7 +1395,7 @@ public TSStatus visitQuery(QueryStatement statement, TreeAccessCheckContext cont
13951395
AUDIT_LOGGER.recordObjectAuthenticationAuditLog(
13961396
context.setResult(false),
13971397
() -> statement.getPaths().stream().distinct().collect(Collectors.toList()).toString());
1398-
return new TSStatus(e.getCode().getStatusCode());
1398+
return new TSStatus(e.getCode().getStatusCode()).setMessage(e.getMessage());
13991399
}
14001400
AUDIT_LOGGER.recordObjectAuthenticationAuditLog(
14011401
context.setResult(true),

0 commit comments

Comments
 (0)