Browse Source

Fixed /v2 null-payload check and renamed acl/userId metadata keys

The null-payload check in handleRuleEngineRequestV2 used `getPayload() == null`,
but Jackson deserializes JSON `null` into NullNode rather than Java null, so
a request body with `"payload": null` bypassed the check and reached the rule
engine, returning 200 instead of 400. Added an `isNull()` guard alongside the
Java-null one.

Renamed the two new metadata keys to drop the `tb_` prefix:
  TB_ACL_SNAPSHOT_KEY ("tb_aclSnapshot") -> ACL_KEY ("acl")
  TB_USER_ID_KEY ("tb_userId")           -> USER_ID_KEY ("userId")

`userId` already exists as a rule-engine metadata key set by EntityActionService
and BaseUserProcessor for the same semantic (caller user's UUID), so the rename
aligns the v2 enrichment with the existing convention; `acl` has no other
in-codebase consumer.
pull/15618/head
Oleksandra Matviienko 3 weeks ago
parent
commit
66aeb310c5
  1. 17
      application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java
  2. 20
      application/src/test/java/org/thingsboard/server/controller/RuleEngineControllerV2EnrichmentTest.java
  3. 4
      common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java

17
application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.FutureCallback;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
@ -86,9 +87,9 @@ public class RuleEngineController extends BaseController {
private static final String V2_DESCRIPTION = "Variant of the Rule Engine REST API that enriches the forwarded `TbMsg` with two " +
"server-authoritative metadata keys before pushing it to the rule engine:\n\n" +
" * **`tb_aclSnapshot`** — a JSON array of `{entityId, allowed[]}` computed for every entity the caller lists in `aclEntities`. " +
" * **`acl`** — a JSON array of `{entityId, allowed[]}` computed for every entity the caller lists in `aclEntities`. " +
"The `allowed` array contains the `Operation` values the caller has on the specific instance;\n" +
" * **`tb_userId`** — UUID of the calling user, intended for audit logging inside rule chains.\n\n" +
" * **`userId`** — UUID of the calling user, intended for audit logging inside rule chains.\n\n" +
"Caller-supplied values for either key are overwritten by the platform. This endpoint preserves the existing v1 behavior " +
"(timeout, queue routing, REST Call Reply) — the enrichment is additive.\n\n" +
"Note: `SYS_ADMIN` callers operate against the system tenant, so tenant-scoped entities (DEVICE, ASSET, ...) " +
@ -121,7 +122,9 @@ public class RuleEngineController extends BaseController {
public DeferredResult<ResponseEntity> handleRuleEngineRequestV2(
@Parameter(description = "Enriched request body containing `payload` and optional `aclEntities`.", required = true)
@RequestBody RuleEngineV2Request request) throws ThingsboardException {
if (request == null || request.getPayload() == null) {
JsonNode payload = request != null ? request.getPayload() : null;
// Jackson maps JSON `null` to NullNode (not Java null), so check isNull() too.
if (payload == null || payload.isNull()) {
throw new ThingsboardException("Request body with 'payload' is required",
ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
@ -132,7 +135,7 @@ public class RuleEngineController extends BaseController {
// Always non-null on the v2 path so the shared helper writes the server-authoritative
// tb_user_id / tb_acl_snapshot metadata keys. v1 wrappers pass null and skip those writes.
List<EntityId> aclEntities = request.getAclEntities() != null ? request.getAclEntities() : List.of();
return handleRuleEngineRequest(entityTypeStr, entityIdStr, timeout, JacksonUtil.toString(request.getPayload()), request.getQueueName(), aclEntities);
return handleRuleEngineRequest(entityTypeStr, entityIdStr, timeout, JacksonUtil.toString(payload), request.getQueueName(), aclEntities);
}
@ApiOperation(value = "Push user message to the rule engine (handleRuleEngineRequestForUser)",
@ -259,8 +262,8 @@ public class RuleEngineController extends BaseController {
metaData.put("expirationTime", Long.toString(expTime));
if (aclEntities != null) {
// v2 path: server-authoritative keys written last so any caller value is overwritten.
metaData.put(TbMsgMetaData.TB_USER_ID_KEY, currentUser.getId().getId().toString());
metaData.put(TbMsgMetaData.TB_ACL_SNAPSHOT_KEY, buildAclSnapshot(currentUser, aclEntities));
metaData.put(TbMsgMetaData.USER_ID_KEY, currentUser.getId().getId().toString());
metaData.put(TbMsgMetaData.ACL_KEY, buildAclSnapshot(currentUser, aclEntities));
}
TbMsg msg = TbMsg.newMsg()
@ -314,7 +317,7 @@ public class RuleEngineController extends BaseController {
* entities (DEVICE, ASSET, ...) won't be resolved by the tenant-filtered lookup and the
* entry resolves to {@code allowed=[]} ACL enrichment is effectively a no-op for SYS_ADMIN.
*
* @return serialized JSON array suitable for writing into {@link TbMsgMetaData#TB_ACL_SNAPSHOT_KEY}.
* @return serialized JSON array suitable for writing into {@link TbMsgMetaData#ACL_KEY}.
*/
private String buildAclSnapshot(SecurityUser user, List<EntityId> entities) {
if (entities == null || entities.isEmpty()) {

20
application/src/test/java/org/thingsboard/server/controller/RuleEngineControllerV2EnrichmentTest.java

@ -76,7 +76,7 @@ public class RuleEngineControllerV2EnrichmentTest extends AbstractControllerTest
TbMsg captured = doRequestAndCapture(request, tenantId);
assertThat(captured.getMetaData().getValue(TbMsgMetaData.TB_USER_ID_KEY))
assertThat(captured.getMetaData().getValue(TbMsgMetaData.USER_ID_KEY))
.isEqualTo(tenantAdminUserId.getId().toString());
List<EntityAclEntry> acl = parseAcl(captured);
assertThat(acl).hasSize(1);
@ -166,8 +166,8 @@ public class RuleEngineControllerV2EnrichmentTest extends AbstractControllerTest
// aclEntities left null
TbMsg captured = doRequestAndCapture(request, tenantId);
assertThat(captured.getMetaData().getValue(TbMsgMetaData.TB_ACL_SNAPSHOT_KEY)).isEqualTo("[]");
assertThat(captured.getMetaData().getValue(TbMsgMetaData.TB_USER_ID_KEY))
assertThat(captured.getMetaData().getValue(TbMsgMetaData.ACL_KEY)).isEqualTo("[]");
assertThat(captured.getMetaData().getValue(TbMsgMetaData.USER_ID_KEY))
.isEqualTo(tenantAdminUserId.getId().toString());
}
@ -179,8 +179,8 @@ public class RuleEngineControllerV2EnrichmentTest extends AbstractControllerTest
request.setAclEntities(List.of());
TbMsg captured = doRequestAndCapture(request, tenantId);
assertThat(captured.getMetaData().getValue(TbMsgMetaData.TB_ACL_SNAPSHOT_KEY)).isEqualTo("[]");
assertThat(captured.getMetaData().getValue(TbMsgMetaData.TB_USER_ID_KEY))
assertThat(captured.getMetaData().getValue(TbMsgMetaData.ACL_KEY)).isEqualTo("[]");
assertThat(captured.getMetaData().getValue(TbMsgMetaData.USER_ID_KEY))
.isEqualTo(tenantAdminUserId.getId().toString());
}
@ -260,15 +260,15 @@ public class RuleEngineControllerV2EnrichmentTest extends AbstractControllerTest
Device device = createDevice("dev-inj", "tok-inj");
RuleEngineV2Request request = baseRequest();
request.setPayload(JacksonUtil.toJsonNode("{\"" + TbMsgMetaData.TB_ACL_SNAPSHOT_KEY + "\":\"attack\",\"" +
TbMsgMetaData.TB_USER_ID_KEY + "\":\"intruder\"}"));
request.setPayload(JacksonUtil.toJsonNode("{\"" + TbMsgMetaData.ACL_KEY + "\":\"attack\",\"" +
TbMsgMetaData.USER_ID_KEY + "\":\"intruder\"}"));
request.setAclEntities(List.of(device.getId()));
TbMsg captured = doRequestAndCapture(request, tenantId);
// Server-computed values, not the attacker's.
assertThat(captured.getMetaData().getValue(TbMsgMetaData.TB_ACL_SNAPSHOT_KEY)).contains("\"entityType\":\"DEVICE\"");
assertThat(captured.getMetaData().getValue(TbMsgMetaData.TB_USER_ID_KEY))
assertThat(captured.getMetaData().getValue(TbMsgMetaData.ACL_KEY)).contains("\"entityType\":\"DEVICE\"");
assertThat(captured.getMetaData().getValue(TbMsgMetaData.USER_ID_KEY))
.isEqualTo(tenantAdminUserId.getId().toString());
}
@ -323,7 +323,7 @@ public class RuleEngineControllerV2EnrichmentTest extends AbstractControllerTest
}
private List<EntityAclEntry> parseAcl(TbMsg msg) {
String acl = msg.getMetaData().getValue(TbMsgMetaData.TB_ACL_SNAPSHOT_KEY);
String acl = msg.getMetaData().getValue(TbMsgMetaData.ACL_KEY);
return JacksonUtil.fromString(acl, new TypeReference<List<EntityAclEntry>>() {
});
}

4
common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java

@ -35,14 +35,14 @@ public final class TbMsgMetaData implements Serializable {
* treat this value as authoritative; the platform overwrites any caller-supplied
* value of this key before the message is forwarded to the Rule Engine.
*/
public static final String TB_ACL_SNAPSHOT_KEY = "tb_aclSnapshot";
public static final String ACL_KEY = "acl";
/**
* Reserved metadata key. Populated by the platform with the UUID (as string) of
* the user that initiated the request intended for audit logging inside rule
* chains. The platform overwrites any caller-supplied value of this key.
*/
public static final String TB_USER_ID_KEY = "tb_userId";
public static final String USER_ID_KEY = "userId";
private final Map<String, String> data;

Loading…
Cancel
Save