From 422c3ab04a85918c2c485d1b5d7dd739c318fcfc Mon Sep 17 00:00:00 2001 From: dingshuangxi888 Date: Thu, 13 Aug 2026 19:09:05 +0800 Subject: [PATCH] Improve ACL 2.0 authorization context handling --- .../AuthorizationCompatibility.java | 116 ++ .../authorization/AuthorizationEvaluator.java | 34 +- .../DefaultAuthorizationContextBuilder.java | 449 +++++-- .../AuthorizationEvaluatorTest.java | 277 ++++- ...efaultAuthorizationContextBuilderTest.java | 1092 +++++++++++++++-- .../authorization/model/ResourceTest.java | 31 +- .../auth/pipeline/AuthorizationPipeline.java | 2 +- .../processor/QueryMessageProcessor.java | 52 + .../pipeline/AuthorizationPipelineTest.java | 72 ++ .../processor/ClientManageProcessorTest.java | 19 + .../EndTransactionProcessorTest.java | 20 +- .../processor/QueryMessageProcessorTest.java | 138 ++- .../grpc/pipeline/AuthorizationPipeline.java | 2 +- .../remoting/RemotingProtocolServer.java | 6 + .../pipeline/AuthorizationPipeline.java | 2 +- .../pipeline/AuthorizationPipelineTest.java | 71 ++ .../remoting/RemotingProtocolServerTest.java | 47 + .../pipeline/AuthorizationPipelineTest.java | 74 ++ .../header/CreateTopicListRequestHeader.java | 10 +- .../GetAllSubscriptionGroupRequestHeader.java | 11 +- ...GetAllSubscriptionGroupResponseHeader.java | 5 - .../GetAllTopicConfigRequestHeader.java | 11 +- .../GetAllTopicConfigResponseHeader.java | 5 - .../GetLiteClientInfoRequestHeader.java | 12 + .../header/GetLiteGroupInfoRequestHeader.java | 4 + .../header/GetLiteTopicInfoRequestHeader.java | 9 + .../header/GetMaxOffsetRequestHeader.java | 2 +- .../GetParentTopicInfoRequestHeader.java | 4 + .../header/PopLiteMessageRequestHeader.java | 4 + .../header/SearchOffsetRequestHeader.java | 2 +- .../TriggerLiteDispatchRequestHeader.java | 4 + 31 files changed, 2393 insertions(+), 194 deletions(-) create mode 100644 auth/src/main/java/org/apache/rocketmq/auth/authorization/AuthorizationCompatibility.java create mode 100644 broker/src/test/java/org/apache/rocketmq/broker/auth/pipeline/AuthorizationPipelineTest.java create mode 100644 proxy/src/test/java/org/apache/rocketmq/proxy/grpc/pipeline/AuthorizationPipelineTest.java create mode 100644 proxy/src/test/java/org/apache/rocketmq/proxy/remoting/RemotingProtocolServerTest.java create mode 100644 proxy/src/test/java/org/apache/rocketmq/proxy/remoting/pipeline/AuthorizationPipelineTest.java diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authorization/AuthorizationCompatibility.java b/auth/src/main/java/org/apache/rocketmq/auth/authorization/AuthorizationCompatibility.java new file mode 100644 index 00000000000..75fe11bf741 --- /dev/null +++ b/auth/src/main/java/org/apache/rocketmq/auth/authorization/AuthorizationCompatibility.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.auth.authorization; + +import apache.rocketmq.v2.ClientType; +import apache.rocketmq.v2.HeartbeatRequest; +import apache.rocketmq.v2.NotifyClientTerminationRequest; +import apache.rocketmq.v2.TelemetryCommand; +import com.google.protobuf.GeneratedMessageV3; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.heartbeat.HeartbeatData; +import org.apache.rocketmq.remoting.protocol.heartbeat.ProducerData; + +final class AuthorizationCompatibility { + + private AuthorizationCompatibility() { + } + + static boolean matches(RemotingCommand request) { + if (request == null) { + return false; + } + try { + switch (request.getCode()) { + case RequestCode.HEART_BEAT: + return isProducerHeartbeat(request); + case RequestCode.UNREGISTER_CLIENT: + return isProducerUnregister(request); + case RequestCode.END_TRANSACTION: + case RequestCode.VIEW_MESSAGE_BY_ID: + return isHistoricalTopicAbsent(request); + default: + return false; + } + } catch (Throwable ignored) { + return false; + } + } + + static boolean matches(GeneratedMessageV3 request) { + if (request instanceof HeartbeatRequest) { + HeartbeatRequest heartbeat = (HeartbeatRequest) request; + return StringUtils.isBlank(heartbeat.getGroup().getName()) + && (heartbeat.getClientType() == ClientType.PRODUCER + || heartbeat.getClientType() == ClientType.CLIENT_TYPE_UNSPECIFIED); + } + if (request instanceof NotifyClientTerminationRequest) { + NotifyClientTerminationRequest termination = (NotifyClientTerminationRequest) request; + return StringUtils.isBlank(termination.getGroup().getName()); + } + if (request instanceof TelemetryCommand) { + TelemetryCommand telemetry = (TelemetryCommand) request; + switch (telemetry.getCommandCase()) { + case SETTINGS: + return telemetry.getSettings().hasPublishing() + && telemetry.getSettings().getPublishing().getTopicsCount() == 0; + case THREAD_STACK_TRACE: + case VERIFY_MESSAGE_RESULT: + return true; + default: + return false; + } + } + return false; + } + + private static boolean isProducerHeartbeat(RemotingCommand request) { + if (request.getBody() == null) { + return false; + } + HeartbeatData heartbeat = HeartbeatData.decode(request.getBody(), HeartbeatData.class); + if (heartbeat == null || CollectionUtils.isNotEmpty(heartbeat.getConsumerDataSet()) + || CollectionUtils.isEmpty(heartbeat.getProducerDataSet())) { + return false; + } + for (ProducerData producer : heartbeat.getProducerDataSet()) { + if (producer == null || producer.getGroupName() == null) { + return false; + } + } + return true; + } + + private static boolean isProducerUnregister(RemotingCommand request) { + return StringUtils.isNotBlank(getExtField(request, "producerGroup")) + && StringUtils.isBlank(getExtField(request, "consumerGroup")); + } + + /** + * Historical END_TRANSACTION and VIEW_MESSAGE_BY_ID requests carry no topic field. + */ + private static boolean isHistoricalTopicAbsent(RemotingCommand request) { + return request.getExtFields() != null && StringUtils.isBlank(getExtField(request, "topic")); + } + + private static String getExtField(RemotingCommand request, String name) { + return request.getExtFields() == null ? null : request.getExtFields().get(name); + } +} diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authorization/AuthorizationEvaluator.java b/auth/src/main/java/org/apache/rocketmq/auth/authorization/AuthorizationEvaluator.java index f043810cc98..11d7a5ccdfe 100644 --- a/auth/src/main/java/org/apache/rocketmq/auth/authorization/AuthorizationEvaluator.java +++ b/auth/src/main/java/org/apache/rocketmq/auth/authorization/AuthorizationEvaluator.java @@ -16,13 +16,16 @@ */ package org.apache.rocketmq.auth.authorization; +import com.google.protobuf.GeneratedMessageV3; import java.util.List; import java.util.function.Supplier; import org.apache.commons.collections.CollectionUtils; import org.apache.rocketmq.auth.authorization.context.AuthorizationContext; +import org.apache.rocketmq.auth.authorization.exception.AuthorizationException; import org.apache.rocketmq.auth.authorization.factory.AuthorizationFactory; import org.apache.rocketmq.auth.authorization.strategy.AuthorizationStrategy; import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; public class AuthorizationEvaluator { @@ -36,10 +39,37 @@ public AuthorizationEvaluator(AuthConfig authConfig, Supplier metadataService this.authorizationStrategy = AuthorizationFactory.getStrategy(authConfig, metadataService); } - public void evaluate(List contexts) { + /** + * Visible for testing: allows injecting a stub strategy. + */ + AuthorizationEvaluator(AuthorizationStrategy authorizationStrategy) { + this.authorizationStrategy = authorizationStrategy; + } + + public void evaluate(List contexts) { if (CollectionUtils.isEmpty(contexts)) { - return; + throw new AuthorizationException("authorization context is empty."); } contexts.forEach(this.authorizationStrategy::evaluate); } + + public void evaluate(RemotingCommand request, List contexts) { + if (CollectionUtils.isNotEmpty(contexts)) { + contexts.forEach(this.authorizationStrategy::evaluate); + return; + } + if (!AuthorizationCompatibility.matches(request)) { + throw new AuthorizationException("authorization context is empty."); + } + } + + public void evaluate(GeneratedMessageV3 request, List contexts) { + if (CollectionUtils.isNotEmpty(contexts)) { + contexts.forEach(this.authorizationStrategy::evaluate); + return; + } + if (!AuthorizationCompatibility.matches(request)) { + throw new AuthorizationException("authorization context is empty."); + } + } } diff --git a/auth/src/main/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilder.java b/auth/src/main/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilder.java index 7f4fca51174..61aac9af284 100644 --- a/auth/src/main/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilder.java +++ b/auth/src/main/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilder.java @@ -36,14 +36,16 @@ import io.grpc.Metadata; import io.netty.channel.ChannelHandlerContext; import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.EnumSet; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Properties; +import java.util.Set; import org.apache.commons.collections.CollectionUtils; -import org.apache.commons.collections.MapUtils; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.apache.rocketmq.acl.common.AclException; @@ -54,24 +56,38 @@ import org.apache.rocketmq.auth.authorization.exception.AuthorizationException; import org.apache.rocketmq.auth.authorization.model.Resource; import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.TopicConfig; import org.apache.rocketmq.common.action.Action; import org.apache.rocketmq.common.action.RocketMQAction; import org.apache.rocketmq.common.constant.CommonConstants; import org.apache.rocketmq.common.constant.GrpcConstants; +import org.apache.rocketmq.common.lite.LiteSubscriptionDTO; import org.apache.rocketmq.common.message.MessageQueue; import org.apache.rocketmq.common.resource.ResourcePattern; import org.apache.rocketmq.common.resource.ResourceType; import org.apache.rocketmq.common.resource.RocketMQResource; import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.common.RemotingHelper; import org.apache.rocketmq.remoting.protocol.NamespaceUtil; import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; import org.apache.rocketmq.remoting.protocol.RequestCode; import org.apache.rocketmq.remoting.protocol.RequestHeaderRegistry; +import org.apache.rocketmq.remoting.protocol.body.BatchAck; +import org.apache.rocketmq.remoting.protocol.body.BatchAckMessageRequestBody; +import org.apache.rocketmq.remoting.protocol.body.CheckClientRequestBody; +import org.apache.rocketmq.remoting.protocol.body.CreateTopicListRequestBody; import org.apache.rocketmq.remoting.protocol.body.DeleteSubscriptionGroupListRequestBody; import org.apache.rocketmq.remoting.protocol.body.DeleteTopicListRequestBody; import org.apache.rocketmq.remoting.protocol.body.LockBatchRequestBody; +import org.apache.rocketmq.remoting.protocol.body.LiteSubscriptionCtlRequestBody; +import org.apache.rocketmq.remoting.protocol.body.QueryAssignmentRequestBody; +import org.apache.rocketmq.remoting.protocol.body.SetMessageRequestModeRequestBody; +import org.apache.rocketmq.remoting.protocol.body.SubscriptionGroupList; import org.apache.rocketmq.remoting.protocol.body.UnlockBatchRequestBody; +import org.apache.rocketmq.remoting.protocol.header.CreateTopicRequestHeader; import org.apache.rocketmq.remoting.protocol.header.GetConsumerListByGroupRequestHeader; import org.apache.rocketmq.remoting.protocol.header.QueryConsumerOffsetRequestHeader; import org.apache.rocketmq.remoting.protocol.header.UnregisterClientRequestHeader; @@ -79,18 +95,16 @@ import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumerData; import org.apache.rocketmq.remoting.protocol.heartbeat.HeartbeatData; import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingDetail; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; public class DefaultAuthorizationContextBuilder implements AuthorizationContextBuilder { private static final String TOPIC = "topic"; private static final String GROUP = "group"; - private static final String A = "a"; private static final String B = "b"; private static final String CONSUMER_GROUP = "consumerGroup"; private final AuthConfig authConfig; - private static final EnumSet CONSUMER_CLIENT_TYPES = - EnumSet.of(ClientType.PUSH_CONSUMER, ClientType.SIMPLE_CONSUMER, ClientType.PULL_CONSUMER); - private final RequestHeaderRegistry requestHeaderRegistry; public DefaultAuthorizationContextBuilder(AuthConfig authConfig) { @@ -118,10 +132,12 @@ public List build(Metadata metadata, GeneratedMessa } if (message instanceof HeartbeatRequest) { HeartbeatRequest request = (HeartbeatRequest) message; - if (!isConsumerClientType(request.getClientType())) { - return null; + if (StringUtils.isNotBlank(request.getGroup().getName())) { + if (request.getClientType() == ClientType.PRODUCER) { + throw new AuthorizationException("group is not allowed for producer heartbeat."); + } + result = newGroupSubContexts(metadata, request.getGroup()); } - result = newGroupSubContexts(metadata, request.getGroup()); } if (message instanceof ReceiveMessageRequest) { ReceiveMessageRequest request = (ReceiveMessageRequest) message; @@ -132,9 +148,6 @@ public List build(Metadata metadata, GeneratedMessa } if (message instanceof SyncLiteSubscriptionRequest) { SyncLiteSubscriptionRequest request = (SyncLiteSubscriptionRequest) message; - if (request.getLiteTopicSetCount() <= 0) { - return null; - } result = newSubContexts(metadata, request.getGroup(), request.getTopic()); } if (message instanceof AckMessageRequest) { @@ -153,7 +166,7 @@ public List build(Metadata metadata, GeneratedMessa } if (message instanceof ChangeInvisibleDurationRequest) { ChangeInvisibleDurationRequest request = (ChangeInvisibleDurationRequest) message; - result = newGroupSubContexts(metadata, request.getGroup()); + result = newSubContexts(metadata, request.getGroup(), request.getTopic()); } if (message instanceof QueryRouteRequest) { QueryRouteRequest request = (QueryRouteRequest) message; @@ -181,8 +194,8 @@ public List build(ChannelHandlerContext context, Re List result = new ArrayList<>(); try { HashMap fields = command.getExtFields(); - if (MapUtils.isEmpty(fields)) { - return result; + if (fields == null) { + fields = new HashMap<>(); } Subject subject = null; if (fields.containsKey(SessionCredentials.ACCESS_KEY)) { @@ -195,35 +208,46 @@ public List build(ChannelHandlerContext context, Re Resource group; switch (command.getCode()) { case RequestCode.GET_ROUTEINFO_BY_TOPIC: - if (NamespaceUtil.isRetryTopic(fields.get(TOPIC))) { - group = Resource.ofGroup(fields.get(TOPIC)); + String routeTopic = requireResource(fields.get(TOPIC), "topic"); + if (NamespaceUtil.isRetryTopic(routeTopic)) { + group = Resource.ofGroup(routeTopic); result.add(DefaultAuthorizationContext.of(subject, group, Arrays.asList(Action.SUB, Action.GET), sourceIp)); } else { - topic = Resource.ofTopic(fields.get(TOPIC)); + topic = Resource.ofTopic(routeTopic); result.add(DefaultAuthorizationContext.of(subject, topic, Arrays.asList(Action.PUB, Action.SUB, Action.GET), sourceIp)); } break; case RequestCode.SEND_MESSAGE: - if (NamespaceUtil.isRetryTopic(fields.get(TOPIC))) { - group = Resource.ofGroup(fields.get(TOPIC)); + String sendTopic = requireResource(fields.get(TOPIC), "topic"); + if (NamespaceUtil.isRetryTopic(sendTopic)) { + group = Resource.ofGroup(sendTopic); result.add(DefaultAuthorizationContext.of(subject, group, Action.SUB, sourceIp)); } else { - topic = Resource.ofTopic(fields.get(TOPIC)); + topic = Resource.ofTopic(sendTopic); result.add(DefaultAuthorizationContext.of(subject, topic, Action.PUB, sourceIp)); } break; case RequestCode.SEND_MESSAGE_V2: case RequestCode.SEND_BATCH_MESSAGE: - if (NamespaceUtil.isRetryTopic(fields.get(B))) { - group = Resource.ofGroup(fields.get(B)); + String compactSendTopic = requireResource(fields.get(B), "topic"); + if (NamespaceUtil.isRetryTopic(compactSendTopic)) { + group = Resource.ofGroup(compactSendTopic); result.add(DefaultAuthorizationContext.of(subject, group, Action.SUB, sourceIp)); } else { - topic = Resource.ofTopic(fields.get(B)); + topic = Resource.ofTopic(compactSendTopic); result.add(DefaultAuthorizationContext.of(subject, topic, Action.PUB, sourceIp)); } break; + case RequestCode.SEND_REPLY_MESSAGE: + topic = Resource.ofTopic(requireResource(fields.get(TOPIC), "topic")); + result.add(DefaultAuthorizationContext.of(subject, topic, Action.PUB, sourceIp)); + break; + case RequestCode.SEND_REPLY_MESSAGE_V2: + topic = Resource.ofTopic(requireResource(fields.get(B), "topic")); + result.add(DefaultAuthorizationContext.of(subject, topic, Action.PUB, sourceIp)); + break; case RequestCode.RECALL_MESSAGE: - topic = Resource.ofTopic(fields.get(TOPIC)); + topic = Resource.ofTopic(requireResource(fields.get(TOPIC), "topic")); result.add(DefaultAuthorizationContext.of(subject, topic, Action.PUB, sourceIp)); break; case RequestCode.END_TRANSACTION: @@ -232,32 +256,106 @@ public List build(ChannelHandlerContext context, Re result.add(DefaultAuthorizationContext.of(subject, topic, Action.PUB, sourceIp)); } break; + case RequestCode.VIEW_MESSAGE_BY_ID: + if (StringUtils.isNotBlank(fields.get(TOPIC))) { + topic = Resource.ofTopic(fields.get(TOPIC)); + result.add(DefaultAuthorizationContext.of(subject, topic, Action.GET, sourceIp)); + } + break; case RequestCode.CONSUMER_SEND_MSG_BACK: - group = Resource.ofGroup(fields.get(GROUP)); + group = Resource.ofGroup(requireResource(fields.get(GROUP), "consumer group")); result.add(DefaultAuthorizationContext.of(subject, group, Action.SUB, sourceIp)); break; case RequestCode.PULL_MESSAGE: - if (!NamespaceUtil.isRetryTopic(fields.get(TOPIC))) { - topic = Resource.ofTopic(fields.get(TOPIC)); + case RequestCode.LITE_PULL_MESSAGE: + String pullTopic = requireResource(fields.get(TOPIC), "topic"); + String pullGroup = requireResource(fields.get(CONSUMER_GROUP), "consumer group"); + if (NamespaceUtil.isRetryTopic(pullTopic)) { + if (!StringUtils.equals(pullTopic, MixAll.getRetryTopic(pullGroup))) { + throw new AuthorizationException("retry topic does not match consumer group."); + } + } else { + topic = Resource.ofTopic(pullTopic); result.add(DefaultAuthorizationContext.of(subject, topic, Action.SUB, sourceIp)); } - group = Resource.ofGroup(fields.get(CONSUMER_GROUP)); + group = Resource.ofGroup(pullGroup); result.add(DefaultAuthorizationContext.of(subject, group, Action.SUB, sourceIp)); break; + case RequestCode.BATCH_ACK_MESSAGE: + BatchAckMessageRequestBody batchAckBody = decodeRequiredBody( + command, BatchAckMessageRequestBody.class, "batch ack"); + if (CollectionUtils.isEmpty(batchAckBody.getAcks())) { + throw new AuthorizationException("batch ack is empty."); + } + Set ackResources = new LinkedHashSet<>(); + for (BatchAck ack : batchAckBody.getAcks()) { + if (ack == null) { + throw new AuthorizationException("batch ack entry is null."); + } + addUniqueContext(result, ackResources, subject, + Resource.ofTopic(requireResource(ack.getTopic(), "topic")), + Action.SUB, sourceIp); + addUniqueContext(result, ackResources, subject, + Resource.ofGroup(requireResource(ack.getConsumerGroup(), "consumer group")), + Action.SUB, sourceIp); + } + break; + case RequestCode.QUERY_ASSIGNMENT: + QueryAssignmentRequestBody assignmentBody = decodeRequiredBody( + command, QueryAssignmentRequestBody.class, "query assignment"); + result.add(DefaultAuthorizationContext.of(subject, + Resource.ofTopic(requireResource(assignmentBody.getTopic(), "topic")), + Action.SUB, sourceIp)); + result.add(DefaultAuthorizationContext.of(subject, + Resource.ofGroup(requireResource(assignmentBody.getConsumerGroup(), "consumer group")), + Action.SUB, sourceIp)); + break; + case RequestCode.SET_MESSAGE_REQUEST_MODE: + SetMessageRequestModeRequestBody modeBody = decodeRequiredBody( + command, SetMessageRequestModeRequestBody.class, "message request mode"); + result.add(DefaultAuthorizationContext.of(subject, + Resource.ofTopic(requireResource(modeBody.getTopic(), "topic")), + Action.SUB, sourceIp)); + result.add(DefaultAuthorizationContext.of(subject, + Resource.ofGroup(requireResource(modeBody.getConsumerGroup(), "consumer group")), + Action.UPDATE, sourceIp)); + break; + case RequestCode.CHECK_CLIENT_CONFIG: + CheckClientRequestBody checkClientBody = decodeRequiredBody( + command, CheckClientRequestBody.class, "client config"); + if (checkClientBody.getSubscriptionData() == null) { + throw new AuthorizationException("subscription is null."); + } + result.add(DefaultAuthorizationContext.of(subject, + Resource.ofTopic(requireResource( + checkClientBody.getSubscriptionData().getTopic(), "topic")), + Action.SUB, sourceIp)); + result.add(DefaultAuthorizationContext.of(subject, + Resource.ofGroup(requireResource(checkClientBody.getGroup(), "consumer group")), + Action.SUB, sourceIp)); + break; case RequestCode.QUERY_MESSAGE: - topic = Resource.ofTopic(fields.get(TOPIC)); + topic = Resource.ofTopic(requireResource(fields.get(TOPIC), "topic")); result.add(DefaultAuthorizationContext.of(subject, topic, Arrays.asList(Action.SUB, Action.GET), sourceIp)); break; case RequestCode.HEART_BEAT: - HeartbeatData heartbeatData = HeartbeatData.decode(command.getBody(), HeartbeatData.class); + HeartbeatData heartbeatData = decodeRequiredBody(command, HeartbeatData.class, "heartbeat"); for (ConsumerData data : heartbeatData.getConsumerDataSet()) { - group = Resource.ofGroup(data.getGroupName()); + if (data == null) { + throw new AuthorizationException("consumer data is null."); + } + group = Resource.ofGroup(requireResource(data.getGroupName(), "consumer group")); result.add(DefaultAuthorizationContext.of(subject, group, Action.SUB, sourceIp)); for (SubscriptionData subscriptionData : data.getSubscriptionDataSet()) { - if (NamespaceUtil.isRetryTopic(subscriptionData.getTopic())) { + if (subscriptionData == null) { + throw new AuthorizationException("subscription is null."); + } + String subscriptionTopic = + requireResource(subscriptionData.getTopic(), "topic"); + if (NamespaceUtil.isRetryTopic(subscriptionTopic)) { continue; } - topic = Resource.ofTopic(subscriptionData.getTopic()); + topic = Resource.ofTopic(subscriptionTopic); result.add(DefaultAuthorizationContext.of(subject, topic, Action.SUB, sourceIp)); } } @@ -273,88 +371,232 @@ public List build(ChannelHandlerContext context, Re case RequestCode.GET_CONSUMER_LIST_BY_GROUP: final GetConsumerListByGroupRequestHeader getConsumerListByGroupRequestHeader = command.decodeCommandCustomHeader(GetConsumerListByGroupRequestHeader.class); - group = Resource.ofGroup(getConsumerListByGroupRequestHeader.getConsumerGroup()); + group = Resource.ofGroup(requireResource( + getConsumerListByGroupRequestHeader.getConsumerGroup(), "consumer group")); result.add(DefaultAuthorizationContext.of(subject, group, Arrays.asList(Action.SUB, Action.GET), sourceIp)); break; case RequestCode.QUERY_CONSUMER_OFFSET: final QueryConsumerOffsetRequestHeader queryConsumerOffsetRequestHeader = command.decodeCommandCustomHeader(QueryConsumerOffsetRequestHeader.class); - if (!NamespaceUtil.isRetryTopic(queryConsumerOffsetRequestHeader.getTopic())) { - topic = Resource.ofTopic(queryConsumerOffsetRequestHeader.getTopic()); + String queryOffsetTopic = requireResource( + queryConsumerOffsetRequestHeader.getTopic(), "topic"); + String queryOffsetGroup = requireResource( + queryConsumerOffsetRequestHeader.getConsumerGroup(), "consumer group"); + if (!NamespaceUtil.isRetryTopic(queryOffsetTopic)) { + topic = Resource.ofTopic(queryOffsetTopic); result.add(DefaultAuthorizationContext.of(subject, topic, Arrays.asList(Action.SUB, Action.GET), sourceIp)); } - group = Resource.ofGroup(queryConsumerOffsetRequestHeader.getConsumerGroup()); + group = Resource.ofGroup(queryOffsetGroup); result.add(DefaultAuthorizationContext.of(subject, group, Arrays.asList(Action.SUB, Action.GET), sourceIp)); break; case RequestCode.UPDATE_CONSUMER_OFFSET: final UpdateConsumerOffsetRequestHeader updateConsumerOffsetRequestHeader = command.decodeCommandCustomHeader(UpdateConsumerOffsetRequestHeader.class); - if (!NamespaceUtil.isRetryTopic(updateConsumerOffsetRequestHeader.getTopic())) { - topic = Resource.ofTopic(updateConsumerOffsetRequestHeader.getTopic()); + String updateOffsetTopic = requireResource( + updateConsumerOffsetRequestHeader.getTopic(), "topic"); + String updateOffsetGroup = requireResource( + updateConsumerOffsetRequestHeader.getConsumerGroup(), "consumer group"); + if (!NamespaceUtil.isRetryTopic(updateOffsetTopic)) { + topic = Resource.ofTopic(updateOffsetTopic); result.add(DefaultAuthorizationContext.of(subject, topic, Arrays.asList(Action.SUB, Action.UPDATE), sourceIp)); } - group = Resource.ofGroup(updateConsumerOffsetRequestHeader.getConsumerGroup()); + group = Resource.ofGroup(updateOffsetGroup); result.add(DefaultAuthorizationContext.of(subject, group, Arrays.asList(Action.SUB, Action.UPDATE), sourceIp)); break; case RequestCode.LOCK_BATCH_MQ: LockBatchRequestBody lockBatchRequestBody = LockBatchRequestBody.decode(command.getBody(), LockBatchRequestBody.class); - group = Resource.ofGroup(lockBatchRequestBody.getConsumerGroup()); + group = Resource.ofGroup(requireResource( + lockBatchRequestBody.getConsumerGroup(), "consumer group")); result.add(DefaultAuthorizationContext.of(subject, group, Action.SUB, sourceIp)); if (CollectionUtils.isNotEmpty(lockBatchRequestBody.getMqSet())) { for (MessageQueue messageQueue : lockBatchRequestBody.getMqSet()) { - if (NamespaceUtil.isRetryTopic(messageQueue.getTopic())) { + String lockTopic = requireResource(messageQueue.getTopic(), "topic"); + if (NamespaceUtil.isRetryTopic(lockTopic)) { continue; } - topic = Resource.ofTopic(messageQueue.getTopic()); + topic = Resource.ofTopic(lockTopic); result.add(DefaultAuthorizationContext.of(subject, topic, Action.SUB, sourceIp)); } } break; case RequestCode.UNLOCK_BATCH_MQ: - UnlockBatchRequestBody unlockBatchRequestBody = UnlockBatchRequestBody.decode(command.getBody(), UnlockBatchRequestBody.class); - group = Resource.ofGroup(unlockBatchRequestBody.getConsumerGroup()); + UnlockBatchRequestBody unlockBatchRequestBody = UnlockBatchRequestBody.decode( + command.getBody(), UnlockBatchRequestBody.class); + group = Resource.ofGroup(requireResource( + unlockBatchRequestBody.getConsumerGroup(), "consumer group")); result.add(DefaultAuthorizationContext.of(subject, group, Action.SUB, sourceIp)); if (CollectionUtils.isNotEmpty(unlockBatchRequestBody.getMqSet())) { for (MessageQueue messageQueue : unlockBatchRequestBody.getMqSet()) { - if (NamespaceUtil.isRetryTopic(messageQueue.getTopic())) { + String unlockTopic = requireResource(messageQueue.getTopic(), "topic"); + if (NamespaceUtil.isRetryTopic(unlockTopic)) { continue; } - topic = Resource.ofTopic(messageQueue.getTopic()); + topic = Resource.ofTopic(unlockTopic); result.add(DefaultAuthorizationContext.of(subject, topic, Action.SUB, sourceIp)); } } break; - case RequestCode.DELETE_TOPIC_IN_BROKER_LIST: - // Batch APIs carry their target list in the request body, not in an annotated - // CommandCustomHeader, so the annotation-based path in - // RequestHeaderRegistry would otherwise produce an empty context list and let - // the request through without a DELETE permission check. Decode the body and - // emit one DELETE context per topic instead. - DeleteTopicListRequestBody deleteTopicListRequestBody = - DeleteTopicListRequestBody.decode(command.getBody(), DeleteTopicListRequestBody.class); - if (CollectionUtils.isNotEmpty(deleteTopicListRequestBody.getTopicList())) { - for (String topicName : deleteTopicListRequestBody.getTopicList()) { - if (StringUtils.isBlank(topicName)) { - continue; - } - topic = Resource.ofTopic(topicName); - result.add(DefaultAuthorizationContext.of(subject, topic, Action.DELETE, sourceIp)); + case RequestCode.LITE_SUBSCRIPTION_CTL: + LiteSubscriptionCtlRequestBody liteSubscriptionBody = decodeRequiredBody( + command, LiteSubscriptionCtlRequestBody.class, "lite subscription"); + if (CollectionUtils.isEmpty(liteSubscriptionBody.getSubscriptionSet())) { + throw new AuthorizationException("lite subscription is empty."); + } + Set liteSubscriptionResources = new LinkedHashSet<>(); + for (LiteSubscriptionDTO subscription : liteSubscriptionBody.getSubscriptionSet()) { + if (subscription == null) { + throw new AuthorizationException("lite subscription is null."); } + addUniqueContext(result, liteSubscriptionResources, subject, + Resource.ofGroup(requireResource(subscription.getGroup(), "consumer group")), + Action.SUB, sourceIp); + addUniqueContext(result, liteSubscriptionResources, subject, + Resource.ofTopic(requireResource(subscription.getTopic(), "topic")), + Action.SUB, sourceIp); } break; - case RequestCode.DELETE_SUBSCRIPTION_GROUP_LIST: - // See DELETE_TOPIC_IN_BROKER_LIST: emit one DELETE context per group from the - // request body so authorization can enforce per-group DELETE permission. - DeleteSubscriptionGroupListRequestBody deleteGroupListRequestBody = - DeleteSubscriptionGroupListRequestBody.decode(command.getBody(), DeleteSubscriptionGroupListRequestBody.class); - if (CollectionUtils.isNotEmpty(deleteGroupListRequestBody.getGroupNameList())) { - for (String groupName : deleteGroupListRequestBody.getGroupNameList()) { - if (StringUtils.isBlank(groupName)) { - continue; - } - group = Resource.ofGroup(groupName); - result.add(DefaultAuthorizationContext.of(subject, group, Action.DELETE, sourceIp)); + case RequestCode.UPDATE_BROKER_CONFIG: + result.add(DefaultAuthorizationContext.of(subject, + Resource.ofCluster(authConfig.getClusterName()), Action.UPDATE, sourceIp)); + break; + case RequestCode.UPDATE_AND_CREATE_TOPIC_LIST: + CreateTopicListRequestBody topicListBody = decodeRequiredBody( + command, CreateTopicListRequestBody.class, "topic list"); + if (CollectionUtils.isEmpty(topicListBody.getTopicConfigList())) { + throw new AuthorizationException("topic list is empty."); + } + Set topicListResources = new LinkedHashSet<>(); + for (TopicConfig topicConfig : topicListBody.getTopicConfigList()) { + if (topicConfig == null) { + throw new AuthorizationException("topic config is null."); } + String topicName = requireResource(topicConfig.getTopicName(), "topic"); + Resource resource = NamespaceUtil.isRetryTopic(topicName) + ? Resource.ofGroup(topicName) : Resource.ofTopic(topicName); + addUniqueContext(result, topicListResources, subject, resource, Action.CREATE, sourceIp); + } + break; + case RequestCode.UPDATE_COLD_DATA_FLOW_CTR_CONFIG: + Properties properties = MixAll.string2Properties( + decodeRequiredText(command, "cold data flow config")); + if (properties == null || properties.isEmpty()) { + throw new AuthorizationException("cold data flow config is empty."); + } + Set coldDataResources = new LinkedHashSet<>(); + for (String consumerGroup : properties.stringPropertyNames()) { + addUniqueContext(result, coldDataResources, subject, + Resource.ofGroup(requireResource(consumerGroup, "consumer group")), + Action.UPDATE, sourceIp); + } + break; + case RequestCode.REMOVE_COLD_DATA_FLOW_CTR_CONFIG: + group = Resource.ofGroup(requireResource( + decodeRequiredText(command, "consumer group"), "consumer group")); + result.add(DefaultAuthorizationContext.of(subject, group, Action.UPDATE, sourceIp)); + break; + case RequestCode.UPDATE_AND_CREATE_SUBSCRIPTIONGROUP: + SubscriptionGroupConfig subscriptionGroupConfig = + RemotingSerializable.decode(command.getBody(), SubscriptionGroupConfig.class); + if (subscriptionGroupConfig == null + || StringUtils.isBlank(subscriptionGroupConfig.getGroupName())) { + throw new AuthorizationException("subscription group is null."); + } + result.add(DefaultAuthorizationContext.of(subject, + Resource.ofGroup(subscriptionGroupConfig.getGroupName()), Action.CREATE, sourceIp)); + break; + case RequestCode.UPDATE_AND_CREATE_SUBSCRIPTIONGROUP_LIST: + SubscriptionGroupList subscriptionGroupList = decodeRequiredBody( + command, SubscriptionGroupList.class, "subscription group list"); + if (CollectionUtils.isEmpty(subscriptionGroupList.getGroupConfigList())) { + throw new AuthorizationException("subscription group list is empty."); + } + Set subscriptionGroupResources = new LinkedHashSet<>(); + for (SubscriptionGroupConfig groupConfig : subscriptionGroupList.getGroupConfigList()) { + if (groupConfig == null) { + throw new AuthorizationException("subscription group config is null."); + } + addUniqueContext(result, subscriptionGroupResources, subject, + Resource.ofGroup(requireResource(groupConfig.getGroupName(), "consumer group")), + Action.CREATE, sourceIp); + } + break; + case RequestCode.UPDATE_AND_CREATE_STATIC_TOPIC: + CreateTopicRequestHeader createTopicRequestHeader = + command.decodeCommandCustomHeader(CreateTopicRequestHeader.class); + if (createTopicRequestHeader == null) { + throw new AuthorizationException("topic header is null."); + } + String staticTopic = requireResource(createTopicRequestHeader.getTopic(), "topic"); + TopicQueueMappingDetail mappingDetail = decodeRequiredBody( + command, TopicQueueMappingDetail.class, "topic queue mapping"); + if (!StringUtils.equals( + staticTopic, requireResource(mappingDetail.getTopic(), "mapping topic"))) { + throw new AuthorizationException("mapping topic does not match topic header."); + } + topic = Resource.ofTopic(staticTopic); + result.add(DefaultAuthorizationContext.of(subject, topic, Action.CREATE, sourceIp)); + break; + case RequestCode.GET_BROKER_CONFIG: + case RequestCode.GET_BROKER_RUNTIME_INFO: + case RequestCode.GET_ALL_CONSUMER_OFFSET: + case RequestCode.GET_TIMER_CHECK_POINT: + case RequestCode.GET_ALL_DELAY_OFFSET: + case RequestCode.GET_BROKER_HA_STATUS: + case RequestCode.GET_BROKER_EPOCH_CACHE: + case RequestCode.GET_BROKER_LITE_INFO: + result.add(DefaultAuthorizationContext.of(subject, + Resource.ofCluster(authConfig.getClusterName()), Action.GET, sourceIp)); + break; + case RequestCode.GET_ALL_TOPIC_CONFIG: + case RequestCode.GET_TIMER_METRICS: + case RequestCode.GET_SYSTEM_TOPIC_LIST_FROM_BROKER: + result.add(DefaultAuthorizationContext.of(subject, + Resource.of(ResourceType.TOPIC, null, ResourcePattern.ANY), Action.LIST, sourceIp)); + break; + case RequestCode.GET_COLD_DATA_FLOW_CTR_INFO: + case RequestCode.GET_ALL_SUBSCRIPTIONGROUP_CONFIG: + result.add(DefaultAuthorizationContext.of(subject, + Resource.of(ResourceType.GROUP, null, ResourcePattern.ANY), Action.LIST, sourceIp)); + break; + case RequestCode.GET_ALL_MESSAGE_REQUEST_MODE: + result.add(DefaultAuthorizationContext.of(subject, + Resource.of(ResourceType.TOPIC, null, ResourcePattern.ANY), Action.LIST, sourceIp)); + result.add(DefaultAuthorizationContext.of(subject, + Resource.of(ResourceType.GROUP, null, ResourcePattern.ANY), Action.LIST, sourceIp)); + break; + case RequestCode.SET_COMMITLOG_READ_MODE: + case RequestCode.CLEAN_EXPIRED_CONSUMEQUEUE: + case RequestCode.DELETE_EXPIRED_COMMITLOG: + case RequestCode.CLEAN_UNUSED_TOPIC: + case RequestCode.POP_ROLLBACK: + case RequestCode.SWITCH_TIMER_ENGINE: + result.add(DefaultAuthorizationContext.of(subject, + Resource.ofCluster(authConfig.getClusterName()), Action.UPDATE, sourceIp)); + break; + case RequestCode.DELETE_TOPIC_IN_BROKER_LIST: + DeleteTopicListRequestBody deleteTopicListRequestBody = decodeRequiredBody( + command, DeleteTopicListRequestBody.class, "topic list"); + if (CollectionUtils.isEmpty(deleteTopicListRequestBody.getTopicList())) { + throw new AuthorizationException("topic list is empty."); + } + Set deleteTopicResources = new LinkedHashSet<>(); + for (String topicName : deleteTopicListRequestBody.getTopicList()) { + String requiredTopic = requireResource(topicName, "topic"); + Resource resource = NamespaceUtil.isRetryTopic(requiredTopic) + ? Resource.ofGroup(requiredTopic) : Resource.ofTopic(requiredTopic); + addUniqueContext(result, deleteTopicResources, subject, resource, Action.DELETE, sourceIp); + } + break; + case RequestCode.DELETE_SUBSCRIPTION_GROUP_LIST: + DeleteSubscriptionGroupListRequestBody deleteGroupListRequestBody = decodeRequiredBody( + command, DeleteSubscriptionGroupListRequestBody.class, "subscription group list"); + if (CollectionUtils.isEmpty(deleteGroupListRequestBody.getGroupNameList())) { + throw new AuthorizationException("subscription group list is empty."); + } + Set deleteGroupResources = new LinkedHashSet<>(); + for (String groupName : deleteGroupListRequestBody.getGroupNameList()) { + group = Resource.ofGroup(requireResource(groupName, "consumer group")); + addUniqueContext(result, deleteGroupResources, subject, group, Action.DELETE, sourceIp); } break; default: @@ -375,6 +617,38 @@ public List build(ChannelHandlerContext context, Re return result; } + private static T decodeRequiredBody(RemotingCommand command, Class bodyClass, String bodyName) { + if (command.getBody() == null || command.getBody().length == 0) { + throw new AuthorizationException(bodyName + " is null."); + } + T body = RemotingSerializable.decode(command.getBody(), bodyClass); + if (body == null) { + throw new AuthorizationException(bodyName + " is null."); + } + return body; + } + + private static String decodeRequiredText(RemotingCommand command, String bodyName) { + if (command.getBody() == null || command.getBody().length == 0) { + throw new AuthorizationException(bodyName + " is null."); + } + return new String(command.getBody(), StandardCharsets.UTF_8); + } + + private static String requireResource(String resource, String resourceName) { + if (StringUtils.isBlank(resource)) { + throw new AuthorizationException(resourceName + " is null."); + } + return resource; + } + + private static void addUniqueContext(List contexts, + Set resources, Subject subject, Resource resource, Action action, String sourceIp) { + if (resources.add(resource.getResourceKey())) { + contexts.add(DefaultAuthorizationContext.of(subject, resource, action, sourceIp)); + } + } + private List buildContextByAnnotation(Subject subject, RemotingCommand request, String sourceIp) throws Exception { List result = new ArrayList<>(); @@ -406,15 +680,32 @@ private List buildContextByAnnotation(Subject subje String splitter = rocketMQResource.splitter(); Object value = field.get(header); if (value == null) { + if (field.getAnnotation(CFNotNull.class) != null) { + throw new AuthorizationException(field.getName() + " is null."); + } + continue; + } + boolean resourceRequired = field.getAnnotation(CFNotNull.class) != null; + String fieldValue = value.toString(); + if (StringUtils.isBlank(fieldValue)) { + if (resourceRequired) { + requireResource(fieldValue, field.getName()); + } continue; } String[] resourceValues; if (StringUtils.isNotBlank(splitter)) { - resourceValues = StringUtils.split(value.toString(), splitter); + resourceValues = StringUtils.split(fieldValue, splitter); } else { - resourceValues = new String[] {value.toString()}; + resourceValues = new String[] {fieldValue}; } for (String resourceValue : resourceValues) { + if (StringUtils.isBlank(resourceValue)) { + if (resourceRequired) { + requireResource(resourceValue, field.getName()); + } + continue; + } if (resourceType == ResourceType.TOPIC && NamespaceUtil.isRetryTopic(resourceValue)) { resource = Resource.ofGroup(resourceValue); result.add(DefaultAuthorizationContext.of(subject, resource, Arrays.asList(actions), sourceIp)); @@ -475,10 +766,6 @@ private static List newContext(Metadata metadata, T return result; } - private boolean isConsumerClientType(ClientType clientType) { - return CONSUMER_CLIENT_TYPES.contains(clientType); - } - private static List newPubContext(Metadata metadata, apache.rocketmq.v2.Resource topic) { if (topic == null || StringUtils.isBlank(topic.getName())) { throw new AuthorizationException("topic is null."); diff --git a/auth/src/test/java/org/apache/rocketmq/auth/authorization/AuthorizationEvaluatorTest.java b/auth/src/test/java/org/apache/rocketmq/auth/authorization/AuthorizationEvaluatorTest.java index c888d8c0056..8d49e965cb8 100644 --- a/auth/src/test/java/org/apache/rocketmq/auth/authorization/AuthorizationEvaluatorTest.java +++ b/auth/src/test/java/org/apache/rocketmq/auth/authorization/AuthorizationEvaluatorTest.java @@ -16,7 +16,17 @@ */ package org.apache.rocketmq.auth.authorization; +import apache.rocketmq.v2.ClientType; +import apache.rocketmq.v2.HeartbeatRequest; +import apache.rocketmq.v2.NotifyClientTerminationRequest; +import apache.rocketmq.v2.Publishing; +import apache.rocketmq.v2.QueryRouteRequest; +import apache.rocketmq.v2.Settings; +import apache.rocketmq.v2.TelemetryCommand; +import apache.rocketmq.v2.ThreadStackTrace; +import apache.rocketmq.v2.VerifyMessageResult; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import org.apache.commons.collections.CollectionUtils; @@ -33,15 +43,31 @@ import org.apache.rocketmq.auth.authorization.manager.AuthorizationMetadataManager; import org.apache.rocketmq.auth.authorization.model.Acl; import org.apache.rocketmq.auth.authorization.model.Resource; +import org.apache.rocketmq.auth.authorization.strategy.AuthorizationStrategy; import org.apache.rocketmq.auth.config.AuthConfig; import org.apache.rocketmq.auth.helper.AuthTestHelper; import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.resource.ResourcePattern; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.header.EndTransactionRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.UnregisterClientRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ViewMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumerData; +import org.apache.rocketmq.remoting.protocol.heartbeat.HeartbeatData; +import org.apache.rocketmq.remoting.protocol.heartbeat.ProducerData; import org.junit.After; import org.junit.Assert; +import org.junit.Assume; import org.junit.Before; import org.junit.Test; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + public class AuthorizationEvaluatorTest { private AuthConfig authConfig; @@ -384,6 +410,255 @@ public void evaluate9() { }); } + @Test + public void evaluateTypedAnyListResources() { + Assume.assumeFalse(MixAll.isMac()); + User listUser = User.of("list-user", "test"); + User getUser = User.of("get-user", "test"); + User literalUser = User.of("literal-user", "test"); + User topicUser = User.of("topic-user", "test"); + this.authenticationMetadataManager.createUser(listUser).join(); + this.authenticationMetadataManager.createUser(getUser).join(); + this.authenticationMetadataManager.createUser(literalUser).join(); + this.authenticationMetadataManager.createUser(topicUser).join(); + + this.authorizationMetadataManager.createAcl(AuthTestHelper.buildAcl( + "User:list-user", "Topic:*,Group:*", "List", null, Decision.ALLOW)).join(); + this.authorizationMetadataManager.createAcl(AuthTestHelper.buildAcl( + "User:get-user", "Topic:*,Group:*", "Get", null, Decision.ALLOW)).join(); + this.authorizationMetadataManager.createAcl(AuthTestHelper.buildAcl( + "User:literal-user", "Topic:orders,Group:consumers", "List", null, Decision.ALLOW)).join(); + this.authorizationMetadataManager.createAcl(AuthTestHelper.buildAcl( + "User:topic-user", "Topic:*", "List", null, Decision.ALLOW)).join(); + + this.evaluator.evaluate(Arrays.asList( + typedAnyListContext("list-user", ResourceType.TOPIC), + typedAnyListContext("list-user", ResourceType.GROUP))); + + Assert.assertThrows(AuthorizationException.class, () -> this.evaluator.evaluate( + Collections.singletonList(typedAnyListContext("get-user", ResourceType.TOPIC)))); + Assert.assertThrows(AuthorizationException.class, () -> this.evaluator.evaluate( + Collections.singletonList(typedAnyListContext("literal-user", ResourceType.TOPIC)))); + + this.evaluator.evaluate( + Collections.singletonList(typedAnyListContext("topic-user", ResourceType.TOPIC))); + Assert.assertThrows(AuthorizationException.class, () -> this.evaluator.evaluate( + Collections.singletonList(typedAnyListContext("topic-user", ResourceType.GROUP)))); + } + + @Test + public void rejectsEmptyContextWithoutRequest() { + AuthorizationEvaluator requestEvaluator = new AuthorizationEvaluator(mock(AuthorizationStrategy.class)); + + Assert.assertThrows(AuthorizationException.class, () -> requestEvaluator.evaluate(null)); + Assert.assertThrows(AuthorizationException.class, + () -> requestEvaluator.evaluate(Collections.emptyList())); + } + + @Test + public void evaluatesNonEmptyContextBeforeCompatibilityMatching() { + AuthorizationStrategy strategy = mock(AuthorizationStrategy.class); + AuthorizationEvaluator requestEvaluator = new AuthorizationEvaluator(strategy); + DefaultAuthorizationContext context = DefaultAuthorizationContext.of( + Subject.of("User:test"), Resource.ofTopic("topic"), Action.PUB, "127.0.0.1"); + + requestEvaluator.evaluate(RemotingCommand.createRequestCommand(-1, null), + Collections.singletonList(context)); + + verify(strategy).evaluate(context); + } + + @Test + public void acceptsOnlyResourceLessRemotingCompatibilityShapes() { + AuthorizationEvaluator requestEvaluator = new AuthorizationEvaluator(mock(AuthorizationStrategy.class)); + + requestEvaluator.evaluate(producerHeartbeat("producerGroup"), Collections.emptyList()); + requestEvaluator.evaluate(producerHeartbeat(" "), Collections.emptyList()); + Assert.assertThrows(AuthorizationException.class, + () -> requestEvaluator.evaluate(producerHeartbeat(null), Collections.emptyList())); + Assert.assertThrows(AuthorizationException.class, + () -> requestEvaluator.evaluate(remotingRequest( + RequestCode.HEART_BEAT, new HeartbeatData().encode()), Collections.emptyList())); + HeartbeatData invalidHeartbeat = new HeartbeatData(); + invalidHeartbeat.setProducerDataSet(Collections.singleton(null)); + Assert.assertThrows(AuthorizationException.class, + () -> requestEvaluator.evaluate(remotingRequest( + RequestCode.HEART_BEAT, invalidHeartbeat.encode()), Collections.emptyList())); + + HeartbeatData mixedHeartbeat = new HeartbeatData(); + ProducerData producerData = new ProducerData(); + producerData.setGroupName("producerGroup"); + mixedHeartbeat.setProducerDataSet(Collections.singleton(producerData)); + ConsumerData consumerData = new ConsumerData(); + consumerData.setGroupName("consumerGroup"); + mixedHeartbeat.setConsumerDataSet(Collections.singleton(consumerData)); + Assert.assertThrows(AuthorizationException.class, + () -> requestEvaluator.evaluate(remotingRequest(RequestCode.HEART_BEAT, mixedHeartbeat.encode()), + Collections.emptyList())); + + requestEvaluator.evaluate(unregister("producerGroup", null), Collections.emptyList()); + requestEvaluator.evaluate(unregister(null, "producerGroup", null), Collections.emptyList()); + Assert.assertThrows(AuthorizationException.class, + () -> requestEvaluator.evaluate(unregister(null, null), Collections.emptyList())); + Assert.assertThrows(AuthorizationException.class, + () -> requestEvaluator.evaluate(unregister("producerGroup", "consumerGroup"), + Collections.emptyList())); + + requestEvaluator.evaluate(endTransaction(true), Collections.emptyList()); + requestEvaluator.evaluate(endTransaction(false), Collections.emptyList()); + Assert.assertThrows(AuthorizationException.class, + () -> requestEvaluator.evaluate( + RemotingCommand.createRequestCommand(RequestCode.END_TRANSACTION, null), + Collections.emptyList())); + Assert.assertThrows(AuthorizationException.class, + () -> requestEvaluator.evaluate(endTransaction( + "topic", 1L, 2L, MessageSysFlag.TRANSACTION_COMMIT_TYPE, "messageId"), + Collections.emptyList())); + requestEvaluator.evaluate(endTransaction( + null, -1L, 2L, MessageSysFlag.TRANSACTION_COMMIT_TYPE, "messageId"), Collections.emptyList()); + requestEvaluator.evaluate(endTransaction( + null, 1L, -1L, MessageSysFlag.TRANSACTION_COMMIT_TYPE, "messageId"), Collections.emptyList()); + requestEvaluator.evaluate(endTransaction( + null, 1L, 2L, 99, "messageId"), Collections.emptyList()); + requestEvaluator.evaluate(endTransaction( + null, 1L, 2L, null, "messageId"), Collections.emptyList()); + + requestEvaluator.evaluate(viewMessage(0L), Collections.emptyList()); + requestEvaluator.evaluate(viewMessage(-1L), Collections.emptyList()); + requestEvaluator.evaluate(viewMessage(null), Collections.emptyList()); + Assert.assertThrows(AuthorizationException.class, + () -> requestEvaluator.evaluate( + RemotingCommand.createRequestCommand(RequestCode.VIEW_MESSAGE_BY_ID, null), + Collections.emptyList())); + Assert.assertThrows(AuthorizationException.class, + () -> requestEvaluator.evaluate(viewMessage("topic", 0L), Collections.emptyList())); + Assert.assertThrows(AuthorizationException.class, + () -> requestEvaluator.evaluate(RemotingCommand.createRequestCommand(-1, null), + Collections.emptyList())); + } + + @Test + public void acceptsOnlyResourceLessGrpcCompatibilityShapes() { + AuthorizationEvaluator requestEvaluator = new AuthorizationEvaluator(mock(AuthorizationStrategy.class)); + + requestEvaluator.evaluate(HeartbeatRequest.newBuilder() + .setClientType(ClientType.PRODUCER) + .build(), Collections.emptyList()); + requestEvaluator.evaluate(HeartbeatRequest.newBuilder() + .setClientType(ClientType.CLIENT_TYPE_UNSPECIFIED) + .build(), Collections.emptyList()); + Assert.assertThrows(AuthorizationException.class, + () -> requestEvaluator.evaluate(HeartbeatRequest.newBuilder() + .setClientType(ClientType.PRODUCER) + .setGroup(apache.rocketmq.v2.Resource.newBuilder().setName("consumerGroup")) + .build(), Collections.emptyList())); + Assert.assertThrows(AuthorizationException.class, + () -> requestEvaluator.evaluate(HeartbeatRequest.newBuilder() + .setClientType(ClientType.PUSH_CONSUMER) + .build(), Collections.emptyList())); + + requestEvaluator.evaluate(NotifyClientTerminationRequest.getDefaultInstance(), Collections.emptyList()); + Assert.assertThrows(AuthorizationException.class, + () -> requestEvaluator.evaluate(NotifyClientTerminationRequest.newBuilder() + .setGroup(apache.rocketmq.v2.Resource.newBuilder().setName("consumerGroup")) + .build(), Collections.emptyList())); + + requestEvaluator.evaluate(TelemetryCommand.newBuilder() + .setThreadStackTrace(ThreadStackTrace.newBuilder().setNonce("nonce")) + .build(), Collections.emptyList()); + requestEvaluator.evaluate(TelemetryCommand.newBuilder() + .setVerifyMessageResult(VerifyMessageResult.newBuilder().setNonce("nonce")) + .build(), Collections.emptyList()); + requestEvaluator.evaluate(TelemetryCommand.newBuilder() + .setThreadStackTrace(ThreadStackTrace.getDefaultInstance()) + .build(), Collections.emptyList()); + requestEvaluator.evaluate(TelemetryCommand.newBuilder() + .setVerifyMessageResult(VerifyMessageResult.getDefaultInstance()) + .build(), Collections.emptyList()); + requestEvaluator.evaluate(TelemetryCommand.newBuilder() + .setSettings(Settings.newBuilder().setPublishing(Publishing.getDefaultInstance())) + .build(), Collections.emptyList()); + Assert.assertThrows(AuthorizationException.class, + () -> requestEvaluator.evaluate(TelemetryCommand.newBuilder() + .setSettings(Settings.newBuilder().setPublishing(Publishing.newBuilder() + .addTopics(apache.rocketmq.v2.Resource.newBuilder().setName("topic")))) + .build(), Collections.emptyList())); + Assert.assertThrows(AuthorizationException.class, + () -> requestEvaluator.evaluate(TelemetryCommand.getDefaultInstance(), Collections.emptyList())); + Assert.assertThrows(AuthorizationException.class, + () -> requestEvaluator.evaluate(QueryRouteRequest.getDefaultInstance(), Collections.emptyList())); + } + + private RemotingCommand producerHeartbeat(String producerGroup) { + HeartbeatData heartbeatData = new HeartbeatData(); + ProducerData producerData = new ProducerData(); + producerData.setGroupName(producerGroup); + heartbeatData.setProducerDataSet(Collections.singleton(producerData)); + return remotingRequest(RequestCode.HEART_BEAT, heartbeatData.encode()); + } + + private RemotingCommand unregister(String producerGroup, String consumerGroup) { + return unregister("clientId", producerGroup, consumerGroup); + } + + private RemotingCommand unregister(String clientId, String producerGroup, String consumerGroup) { + UnregisterClientRequestHeader header = new UnregisterClientRequestHeader(); + header.setClientID(clientId); + header.setProducerGroup(producerGroup); + header.setConsumerGroup(consumerGroup); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UNREGISTER_CLIENT, header); + request.makeCustomHeaderToNet(); + return request; + } + + private RemotingCommand endTransaction(boolean complete) { + return endTransaction(null, 1L, 2L, MessageSysFlag.TRANSACTION_COMMIT_TYPE, + complete ? "messageId" : null); + } + + private RemotingCommand endTransaction(String topic, Long transactionOffset, + Long commitLogOffset, Integer state, String messageId) { + EndTransactionRequestHeader header = new EndTransactionRequestHeader(); + header.setTopic(topic); + header.setProducerGroup("producerGroup"); + header.setTranStateTableOffset(transactionOffset); + header.setCommitLogOffset(commitLogOffset); + header.setCommitOrRollback(state); + header.setMsgId(messageId); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.END_TRANSACTION, header); + request.makeCustomHeaderToNet(); + return request; + } + + private RemotingCommand viewMessage(Long offset) { + return viewMessage(null, offset); + } + + private RemotingCommand viewMessage(String topic, Long offset) { + ViewMessageRequestHeader header = new ViewMessageRequestHeader(); + header.setTopic(topic); + header.setOffset(offset); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.VIEW_MESSAGE_BY_ID, header); + request.makeCustomHeaderToNet(); + return request; + } + + private RemotingCommand remotingRequest(int requestCode, byte[] body) { + RemotingCommand request = RemotingCommand.createRequestCommand(requestCode, null); + request.setBody(body); + return request; + } + + private DefaultAuthorizationContext typedAnyListContext(String username, ResourceType resourceType) { + DefaultAuthorizationContext context = DefaultAuthorizationContext.of( + Subject.of("User:" + username), + Resource.of(resourceType, null, ResourcePattern.ANY), + Action.LIST, + "192.168.0.1"); + context.setRpcCode(String.valueOf(RequestCode.GET_ALL_TOPIC_CONFIG)); + return context; + } + private void clearAllUsers() { List users = this.authenticationMetadataManager.listUser(null).join(); if (CollectionUtils.isEmpty(users)) { @@ -399,4 +674,4 @@ private void clearAllAcls() { } acls.forEach(acl -> this.authorizationMetadataManager.deleteAcl(acl.getSubject(), null, null).join()); } -} \ No newline at end of file +} diff --git a/auth/src/test/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilderTest.java b/auth/src/test/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilderTest.java index a9de3473245..dbad9e9d01e 100644 --- a/auth/src/test/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilderTest.java +++ b/auth/src/test/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilderTest.java @@ -35,6 +35,7 @@ import apache.rocketmq.v2.Settings; import apache.rocketmq.v2.Subscription; import apache.rocketmq.v2.SubscriptionEntry; +import apache.rocketmq.v2.SyncLiteSubscriptionRequest; import apache.rocketmq.v2.TelemetryCommand; import com.alibaba.fastjson2.JSON; import com.google.common.collect.Sets; @@ -45,39 +46,71 @@ import io.netty.channel.ChannelId; import io.netty.util.Attribute; import io.netty.util.AttributeKey; +import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.BitSet; +import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import org.apache.rocketmq.auth.authorization.context.DefaultAuthorizationContext; +import org.apache.rocketmq.auth.authorization.exception.AuthorizationException; import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.common.TopicConfig; import org.apache.rocketmq.common.TopicFilterType; import org.apache.rocketmq.common.action.Action; import org.apache.rocketmq.common.constant.GrpcConstants; +import org.apache.rocketmq.common.lite.LiteSubscriptionAction; +import org.apache.rocketmq.common.lite.LiteSubscriptionDTO; +import org.apache.rocketmq.common.resource.ResourcePattern; import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.netty.AttributeKeys; import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; import org.apache.rocketmq.remoting.protocol.RequestCode; import org.apache.rocketmq.remoting.protocol.RequestHeaderRegistry; +import org.apache.rocketmq.remoting.protocol.body.BatchAck; +import org.apache.rocketmq.remoting.protocol.body.BatchAckMessageRequestBody; +import org.apache.rocketmq.remoting.protocol.body.CheckClientRequestBody; +import org.apache.rocketmq.remoting.protocol.body.CreateTopicListRequestBody; import org.apache.rocketmq.remoting.protocol.body.DeleteSubscriptionGroupListRequestBody; import org.apache.rocketmq.remoting.protocol.body.DeleteTopicListRequestBody; import org.apache.rocketmq.remoting.protocol.body.LockBatchRequestBody; +import org.apache.rocketmq.remoting.protocol.body.LiteSubscriptionCtlRequestBody; +import org.apache.rocketmq.remoting.protocol.body.QueryAssignmentRequestBody; +import org.apache.rocketmq.remoting.protocol.body.SetMessageRequestModeRequestBody; +import org.apache.rocketmq.remoting.protocol.body.SubscriptionGroupList; import org.apache.rocketmq.remoting.protocol.body.UnlockBatchRequestBody; import org.apache.rocketmq.remoting.protocol.header.ConsumerSendMsgBackRequestHeader; import org.apache.rocketmq.remoting.protocol.header.CreateTopicRequestHeader; import org.apache.rocketmq.remoting.protocol.header.CreateUserRequestHeader; import org.apache.rocketmq.remoting.protocol.header.EndTransactionRequestHeader; import org.apache.rocketmq.remoting.protocol.header.GetConsumerListByGroupRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetLiteClientInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetLiteGroupInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetLiteTopicInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMaxOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetParentTopicInfoRequestHeader; import org.apache.rocketmq.remoting.protocol.header.HeartbeatRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PopMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PopLiteMessageRequestHeader; import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; import org.apache.rocketmq.remoting.protocol.header.QueryConsumerOffsetRequestHeader; import org.apache.rocketmq.remoting.protocol.header.QueryMessageRequestHeader; import org.apache.rocketmq.remoting.protocol.header.RecallMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SearchOffsetRequestHeader; import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeaderV2; +import org.apache.rocketmq.remoting.protocol.header.TriggerLiteDispatchRequestHeader; import org.apache.rocketmq.remoting.protocol.header.UnregisterClientRequestHeader; import org.apache.rocketmq.remoting.protocol.header.UpdateConsumerOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ViewMessageRequestHeader; import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumerData; import org.apache.rocketmq.remoting.protocol.heartbeat.HeartbeatData; import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingDetail; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -212,12 +245,17 @@ public void buildGrpc() { request = ChangeInvisibleDurationRequest.newBuilder() .setGroup(Resource.newBuilder().setName("group").build()) + .setTopic(Resource.newBuilder().setName("topic").build()) + .setLiteTopic("liteTopic") .build(); result = builder.build(metadata, request); - Assert.assertEquals(1, result.size()); - Assert.assertEquals(result.get(0).getSubject().getSubjectKey(), "User:rocketmq"); - Assert.assertEquals(result.get(0).getResource().getResourceKey(), "Group:group"); - Assert.assertTrue(result.get(0).getActions().containsAll(Arrays.asList(Action.SUB))); + Assert.assertEquals(2, result.size()); + Assert.assertEquals(getContext(result, ResourceType.GROUP).getSubject().getSubjectKey(), "User:rocketmq"); + Assert.assertEquals(getContext(result, ResourceType.GROUP).getResource().getResourceKey(), "Group:group"); + Assert.assertTrue(getContext(result, ResourceType.GROUP).getActions().containsAll(Arrays.asList(Action.SUB))); + Assert.assertEquals(getContext(result, ResourceType.TOPIC).getSubject().getSubjectKey(), "User:rocketmq"); + Assert.assertEquals(getContext(result, ResourceType.TOPIC).getResource().getResourceKey(), "Topic:topic"); + Assert.assertTrue(getContext(result, ResourceType.TOPIC).getActions().containsAll(Arrays.asList(Action.SUB))); request = QueryRouteRequest.newBuilder() .setTopic(Resource.newBuilder().setName("topic").build()) @@ -299,6 +337,7 @@ public void buildRemoting() { Assert.assertEquals(RequestCode.SEND_MESSAGE + "", result.get(0).getRpcCode()); sendMessageRequestHeader = new SendMessageRequestHeader(); + sendMessageRequestHeader.setProducerGroup("unrelatedProducer"); sendMessageRequestHeader.setTopic("%RETRY%group"); request = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, sendMessageRequestHeader); request.setVersion(441); @@ -323,6 +362,7 @@ public void buildRemoting() { Assert.assertTrue(result.get(0).getActions().containsAll(Arrays.asList(Action.PUB))); sendMessageRequestHeaderV2 = new SendMessageRequestHeaderV2(); + sendMessageRequestHeaderV2.setA("unrelatedProducer"); sendMessageRequestHeaderV2.setTopic("%RETRY%group"); request = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE_V2, sendMessageRequestHeaderV2); request.setVersion(441); @@ -367,8 +407,8 @@ public void buildRemoting() { request.setVersion(441); request.addExtField("AccessKey", "rocketmq"); request.makeCustomHeaderToNet(); - result = builder.build(channelHandlerContext, request); - Assert.assertEquals(0, result.size()); + RemotingCommand endTransactionWithoutTopic = request; + Assert.assertTrue(builder.build(channelHandlerContext, endTransactionWithoutTopic).isEmpty()); ConsumerSendMsgBackRequestHeader consumerSendMsgBackRequestHeader = new ConsumerSendMsgBackRequestHeader(); consumerSendMsgBackRequestHeader.setGroup("group"); @@ -513,108 +553,1003 @@ public void buildRemoting() { Assert.assertEquals("Cluster:DefaultCluster", result.get(0).getResource().getResourceKey()); Assert.assertTrue(result.get(0).getActions().containsAll(Arrays.asList(Action.UPDATE))); - LockBatchRequestBody lockBatchRequestBody = new LockBatchRequestBody(); - lockBatchRequestBody.setConsumerGroup("group"); - java.util.Set lockMqSet = new java.util.HashSet<>(); - - lockMqSet.add(new org.apache.rocketmq.common.message.MessageQueue("topic", "broker-a", 0)); - // retry topic, should be skipped - lockMqSet.add(new org.apache.rocketmq.common.message.MessageQueue("%RETRY%group", "broker-a", 1)); - lockBatchRequestBody.setMqSet(lockMqSet); - - request = RemotingCommand.createRequestCommand(RequestCode.LOCK_BATCH_MQ, null); - request.setBody(JSON.toJSONBytes(lockBatchRequestBody)); + request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_BROKER_CONFIG, null); request.setVersion(441); request.addExtField("AccessKey", "rocketmq"); request.makeCustomHeaderToNet(); + result = builder.build(channelHandlerContext, request); + Assert.assertEquals(1, result.size()); + Assert.assertEquals("User:rocketmq", result.get(0).getSubject().getSubjectKey()); + Assert.assertEquals("Cluster:DefaultCluster", result.get(0).getResource().getResourceKey()); + Assert.assertTrue(result.get(0).getActions().containsAll(Arrays.asList(Action.UPDATE))); + Assert.assertEquals(RequestCode.UPDATE_BROKER_CONFIG + "", result.get(0).getRpcCode()); + request = RemotingCommand.createRequestCommand(RequestCode.GET_BROKER_CONFIG, null); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); result = builder.build(channelHandlerContext, request); - Assert.assertEquals(2, result.size()); + Assert.assertEquals(1, result.size()); + Assert.assertEquals("User:rocketmq", result.get(0).getSubject().getSubjectKey()); + Assert.assertEquals("Cluster:DefaultCluster", result.get(0).getResource().getResourceKey()); + Assert.assertTrue(result.get(0).getActions().containsAll(Arrays.asList(Action.GET))); + Assert.assertEquals(RequestCode.GET_BROKER_CONFIG + "", result.get(0).getRpcCode()); + } - Assert.assertEquals("User:rocketmq", getContext(result, ResourceType.GROUP).getSubject().getSubjectKey()); - Assert.assertEquals("Group:group", getContext(result, ResourceType.GROUP).getResource().getResourceKey()); - Assert.assertTrue(getContext(result, ResourceType.GROUP).getActions().containsAll(Arrays.asList(Action.SUB))); + @Test + public void rejectBlankResourcesForExistingRemotingRequests() { + mockRemotingChannel(); + + int[] topicCodes = { + RequestCode.GET_ROUTEINFO_BY_TOPIC, + RequestCode.SEND_MESSAGE, + RequestCode.RECALL_MESSAGE, + RequestCode.QUERY_MESSAGE + }; + for (int requestCode : topicCodes) { + RemotingCommand request = remotingRequest(requestCode, null, null); + request.addExtField("topic", " "); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, request)); + } - Assert.assertEquals("User:rocketmq", getContext(result, ResourceType.TOPIC).getSubject().getSubjectKey()); - Assert.assertEquals("Topic:topic", getContext(result, ResourceType.TOPIC).getResource().getResourceKey()); - Assert.assertTrue(getContext(result, ResourceType.TOPIC).getActions().containsAll(Arrays.asList(Action.SUB))); + int[] compactTopicCodes = { + RequestCode.SEND_MESSAGE_V2, + RequestCode.SEND_BATCH_MESSAGE + }; + for (int requestCode : compactTopicCodes) { + RemotingCommand request = remotingRequest(requestCode, null, null); + request.addExtField("b", " "); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, request)); + } - Assert.assertEquals("192.168.0.1", getContext(result, ResourceType.TOPIC).getSourceIp()); - Assert.assertEquals("channel-id", getContext(result, ResourceType.TOPIC).getChannelId()); - Assert.assertEquals(String.valueOf(RequestCode.LOCK_BATCH_MQ), getContext(result, ResourceType.TOPIC).getRpcCode()); - - UnlockBatchRequestBody unlockBatchRequestBody = new UnlockBatchRequestBody(); - unlockBatchRequestBody.setConsumerGroup("group"); - java.util.Set unlockMqSet = new java.util.HashSet<>(); - unlockMqSet.add(new org.apache.rocketmq.common.message.MessageQueue("topic", "broker-a", 0)); - // retry topic, should be skipped - unlockMqSet.add(new org.apache.rocketmq.common.message.MessageQueue("%RETRY%group", "broker-a", 1)); - unlockBatchRequestBody.setMqSet(unlockMqSet); - - request = RemotingCommand.createRequestCommand(RequestCode.UNLOCK_BATCH_MQ, null); - request.setBody(JSON.toJSONBytes(unlockBatchRequestBody)); + RemotingCommand sendBackRequest = remotingRequest(RequestCode.CONSUMER_SEND_MSG_BACK, null, null); + sendBackRequest.addExtField("group", " "); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, sendBackRequest)); + + GetConsumerListByGroupRequestHeader listHeader = new GetConsumerListByGroupRequestHeader(); + listHeader.setConsumerGroup(" "); + Assert.assertThrows(AuthorizationException.class, () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.GET_CONSUMER_LIST_BY_GROUP, listHeader, null))); + + QueryConsumerOffsetRequestHeader queryOffsetHeader = new QueryConsumerOffsetRequestHeader(); + queryOffsetHeader.setTopic(" "); + queryOffsetHeader.setConsumerGroup("group"); + queryOffsetHeader.setQueueId(0); + Assert.assertThrows(AuthorizationException.class, () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.QUERY_CONSUMER_OFFSET, queryOffsetHeader, null))); + queryOffsetHeader.setTopic("topic"); + queryOffsetHeader.setConsumerGroup(" "); + Assert.assertThrows(AuthorizationException.class, () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.QUERY_CONSUMER_OFFSET, queryOffsetHeader, null))); + + UpdateConsumerOffsetRequestHeader updateOffsetHeader = new UpdateConsumerOffsetRequestHeader(); + updateOffsetHeader.setTopic(" "); + updateOffsetHeader.setConsumerGroup("group"); + Assert.assertThrows(AuthorizationException.class, () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.UPDATE_CONSUMER_OFFSET, updateOffsetHeader, null))); + updateOffsetHeader.setTopic("topic"); + updateOffsetHeader.setConsumerGroup(" "); + Assert.assertThrows(AuthorizationException.class, () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.UPDATE_CONSUMER_OFFSET, updateOffsetHeader, null))); + + LockBatchRequestBody lockBody = new LockBatchRequestBody(); + lockBody.setConsumerGroup(" "); + Assert.assertThrows(AuthorizationException.class, () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.LOCK_BATCH_MQ, null, lockBody.encode()))); + lockBody.setConsumerGroup("group"); + lockBody.setMqSet(Collections.singleton( + new org.apache.rocketmq.common.message.MessageQueue(" ", "broker-a", 0))); + Assert.assertThrows(AuthorizationException.class, () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.LOCK_BATCH_MQ, null, lockBody.encode()))); + + UnlockBatchRequestBody unlockBody = new UnlockBatchRequestBody(); + unlockBody.setConsumerGroup(" "); + Assert.assertThrows(AuthorizationException.class, () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.UNLOCK_BATCH_MQ, null, unlockBody.encode()))); + unlockBody.setConsumerGroup("group"); + unlockBody.setMqSet(Collections.singleton( + new org.apache.rocketmq.common.message.MessageQueue(" ", "broker-a", 0))); + Assert.assertThrows(AuthorizationException.class, () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.UNLOCK_BATCH_MQ, null, unlockBody.encode()))); + + PopMessageRequestHeader popHeader = new PopMessageRequestHeader(); + popHeader.setConsumerGroup("group"); + popHeader.setTopic(" "); + Assert.assertThrows(AuthorizationException.class, () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.POP_MESSAGE, popHeader, null))); + popHeader.setConsumerGroup(" "); + popHeader.setTopic("topic"); + Assert.assertThrows(AuthorizationException.class, () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.POP_MESSAGE, popHeader, null))); + } + + /** + * Verifies that annotation-based remoting headers produce the expected authorization contexts + * after RequestHeaderRegistry initialization. + */ + @Test + public void buildRemotingByAnnotation() { + when(channel.id()).thenReturn(mockChannelId("channel-id")); + when(channel.hasAttr(eq(AttributeKeys.PROXY_PROTOCOL_ADDR))).thenReturn(true); + when(channel.attr(eq(AttributeKeys.PROXY_PROTOCOL_ADDR))).thenReturn(mockAttribute("192.168.0.1")); + when(channel.hasAttr(eq(AttributeKeys.PROXY_PROTOCOL_PORT))).thenReturn(true); + when(channel.attr(eq(AttributeKeys.PROXY_PROTOCOL_PORT))).thenReturn(mockAttribute("1234")); + when(channelHandlerContext.channel()).thenReturn(channel); + + PopMessageRequestHeader popMessageRequestHeader = new PopMessageRequestHeader(); + popMessageRequestHeader.setConsumerGroup("group"); + popMessageRequestHeader.setTopic("topic"); + popMessageRequestHeader.setQueueId(0); + popMessageRequestHeader.setMaxMsgNums(32); + popMessageRequestHeader.setInvisibleTime(60000L); + popMessageRequestHeader.setPollTime(15000L); + popMessageRequestHeader.setBornTime(System.currentTimeMillis()); + popMessageRequestHeader.setInitMode(0); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.POP_MESSAGE, popMessageRequestHeader); request.setVersion(441); request.addExtField("AccessKey", "rocketmq"); request.makeCustomHeaderToNet(); + List result = builder.build(channelHandlerContext, request); - result = builder.build(channelHandlerContext, request); Assert.assertEquals(2, result.size()); - Assert.assertEquals("User:rocketmq", getContext(result, ResourceType.GROUP).getSubject().getSubjectKey()); Assert.assertEquals("Group:group", getContext(result, ResourceType.GROUP).getResource().getResourceKey()); Assert.assertTrue(getContext(result, ResourceType.GROUP).getActions().containsAll(Arrays.asList(Action.SUB))); - Assert.assertEquals("User:rocketmq", getContext(result, ResourceType.TOPIC).getSubject().getSubjectKey()); Assert.assertEquals("Topic:topic", getContext(result, ResourceType.TOPIC).getResource().getResourceKey()); Assert.assertTrue(getContext(result, ResourceType.TOPIC).getActions().containsAll(Arrays.asList(Action.SUB))); - Assert.assertEquals("192.168.0.1", getContext(result, ResourceType.TOPIC).getSourceIp()); - Assert.assertEquals("channel-id", getContext(result, ResourceType.TOPIC).getChannelId()); - Assert.assertEquals(String.valueOf(RequestCode.UNLOCK_BATCH_MQ), getContext(result, ResourceType.TOPIC).getRpcCode()); + Assert.assertEquals(RequestCode.POP_MESSAGE + "", getContext(result, ResourceType.TOPIC).getRpcCode()); + } - // DELETE_TOPIC_IN_BROKER_LIST: body-driven, must yield one DELETE context per topic. - DeleteTopicListRequestBody deleteTopicListBody = new DeleteTopicListRequestBody(); - deleteTopicListBody.setTopicList(Arrays.asList("topicA", "topicB", "", " ")); + @Test + public void manuallyResolvedRemotingRequestsAreNotRegisteredForAnnotationFallback() { + Assert.assertEquals(PopMessageRequestHeader.class, RequestHeaderRegistry.getInstance() + .getRequestHeader(RequestCode.POP_MESSAGE)); + Assert.assertNull(RequestHeaderRegistry.getInstance() + .getRequestHeader(RequestCode.UPDATE_AND_CREATE_TOPIC_LIST)); + Assert.assertNull(RequestHeaderRegistry.getInstance() + .getRequestHeader(RequestCode.GET_ALL_TOPIC_CONFIG)); + Assert.assertNull(RequestHeaderRegistry.getInstance() + .getRequestHeader(RequestCode.GET_ALL_SUBSCRIPTIONGROUP_CONFIG)); + } + + @Test + public void buildRemotingExternalAdminRequests() { + when(channel.id()).thenReturn(mockChannelId("channel-id")); + when(channel.hasAttr(eq(AttributeKeys.PROXY_PROTOCOL_ADDR))).thenReturn(true); + when(channel.attr(eq(AttributeKeys.PROXY_PROTOCOL_ADDR))).thenReturn(mockAttribute("192.168.0.1")); + when(channel.hasAttr(eq(AttributeKeys.PROXY_PROTOCOL_PORT))).thenReturn(true); + when(channel.attr(eq(AttributeKeys.PROXY_PROTOCOL_PORT))).thenReturn(mockAttribute("1234")); + when(channelHandlerContext.channel()).thenReturn(channel); + + int[] readCodes = new int[] { + RequestCode.GET_BROKER_RUNTIME_INFO, + RequestCode.GET_ALL_CONSUMER_OFFSET + }; + for (int requestCode : readCodes) { + RemotingCommand request = RemotingCommand.createRequestCommand(requestCode, null); + request.setVersion(441); + request.addExtField("AccessKey", "rocketmq"); + request.makeCustomHeaderToNet(); + + List result = builder.build(channelHandlerContext, request); + + Assert.assertEquals(1, result.size()); + Assert.assertEquals("Cluster:DefaultCluster", result.get(0).getResource().getResourceKey()); + Assert.assertTrue(result.get(0).getActions().contains(Action.GET)); + Assert.assertEquals(String.valueOf(requestCode), result.get(0).getRpcCode()); + } + } + + @Test + public void buildRemotingUpdateAndCreateSubscriptionGroupRequiresGroupCreate() { + when(channel.id()).thenReturn(mockChannelId("channel-id")); + when(channel.hasAttr(eq(AttributeKeys.PROXY_PROTOCOL_ADDR))).thenReturn(true); + when(channel.attr(eq(AttributeKeys.PROXY_PROTOCOL_ADDR))).thenReturn(mockAttribute("192.168.0.1")); + when(channel.hasAttr(eq(AttributeKeys.PROXY_PROTOCOL_PORT))).thenReturn(true); + when(channel.attr(eq(AttributeKeys.PROXY_PROTOCOL_PORT))).thenReturn(mockAttribute("1234")); + when(channelHandlerContext.channel()).thenReturn(channel); - request = RemotingCommand.createRequestCommand(RequestCode.DELETE_TOPIC_IN_BROKER_LIST, null); - request.setBody(JSON.toJSONBytes(deleteTopicListBody)); + SubscriptionGroupConfig config = new SubscriptionGroupConfig(); + config.setGroupName("groupA"); + RemotingCommand request = RemotingCommand.createRequestCommand( + RequestCode.UPDATE_AND_CREATE_SUBSCRIPTIONGROUP, null); request.setVersion(441); request.addExtField("AccessKey", "rocketmq"); + request.setBody(RemotingSerializable.encode(config)); request.makeCustomHeaderToNet(); - result = builder.build(channelHandlerContext, request); - // Blank entries are filtered, so 2 valid topics produce 2 contexts. - Assert.assertEquals(2, result.size()); - for (DefaultAuthorizationContext ctx : result) { - Assert.assertEquals(ResourceType.TOPIC, ctx.getResource().getResourceType()); - Assert.assertEquals("User:rocketmq", ctx.getSubject().getSubjectKey()); - Assert.assertTrue(ctx.getActions().contains(Action.DELETE)); - Assert.assertEquals(String.valueOf(RequestCode.DELETE_TOPIC_IN_BROKER_LIST), ctx.getRpcCode()); - } - Assert.assertTrue(result.stream().anyMatch(ctx -> "Topic:topicA".equals(ctx.getResource().getResourceKey()))); - Assert.assertTrue(result.stream().anyMatch(ctx -> "Topic:topicB".equals(ctx.getResource().getResourceKey()))); + List result = builder.build(channelHandlerContext, request); - // DELETE_SUBSCRIPTION_GROUP_LIST: body-driven, must yield one DELETE context per group. - DeleteSubscriptionGroupListRequestBody deleteGroupListBody = new DeleteSubscriptionGroupListRequestBody(); - deleteGroupListBody.setGroupNameList(Arrays.asList("groupX", "groupY")); + Assert.assertEquals(1, result.size()); + Assert.assertEquals("Group:groupA", result.get(0).getResource().getResourceKey()); + Assert.assertTrue(result.get(0).getActions().contains(Action.CREATE)); + Assert.assertEquals(String.valueOf(RequestCode.UPDATE_AND_CREATE_SUBSCRIPTIONGROUP), + result.get(0).getRpcCode()); + } - request = RemotingCommand.createRequestCommand(RequestCode.DELETE_SUBSCRIPTION_GROUP_LIST, null); - request.setBody(JSON.toJSONBytes(deleteGroupListBody)); + @Test + public void buildRemotingUpdateAndCreateSubscriptionGroupRejectsMissingGroup() { + when(channel.id()).thenReturn(mockChannelId("channel-id")); + when(channel.hasAttr(eq(AttributeKeys.PROXY_PROTOCOL_ADDR))).thenReturn(true); + when(channel.attr(eq(AttributeKeys.PROXY_PROTOCOL_ADDR))).thenReturn(mockAttribute("192.168.0.1")); + when(channel.hasAttr(eq(AttributeKeys.PROXY_PROTOCOL_PORT))).thenReturn(true); + when(channel.attr(eq(AttributeKeys.PROXY_PROTOCOL_PORT))).thenReturn(mockAttribute("1234")); + when(channelHandlerContext.channel()).thenReturn(channel); + + RemotingCommand request = RemotingCommand.createRequestCommand( + RequestCode.UPDATE_AND_CREATE_SUBSCRIPTIONGROUP, null); request.setVersion(441); request.addExtField("AccessKey", "rocketmq"); request.makeCustomHeaderToNet(); - result = builder.build(channelHandlerContext, request); - Assert.assertEquals(2, result.size()); - for (DefaultAuthorizationContext ctx : result) { - Assert.assertEquals(ResourceType.GROUP, ctx.getResource().getResourceType()); - Assert.assertEquals("User:rocketmq", ctx.getSubject().getSubjectKey()); - Assert.assertTrue(ctx.getActions().contains(Action.DELETE)); - Assert.assertEquals(String.valueOf(RequestCode.DELETE_SUBSCRIPTION_GROUP_LIST), ctx.getRpcCode()); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, request)); + } + + @Test + public void buildAdditionalRemotingDataRequests() { + mockRemotingChannel(); + + PullMessageRequestHeader litePullHeader = new PullMessageRequestHeader(); + litePullHeader.setTopic("liteTopic"); + litePullHeader.setConsumerGroup("liteGroup"); + List result = builder.build(channelHandlerContext, + remotingRequest(RequestCode.LITE_PULL_MESSAGE, litePullHeader, null)); + assertResourceSet(result, "Topic:liteTopic", "Group:liteGroup"); + assertActions(result, "Topic:liteTopic", Action.SUB); + assertActions(result, "Group:liteGroup", Action.SUB); + + litePullHeader.setTopic("%RETRY%retryGroup"); + litePullHeader.setConsumerGroup("retryGroup"); + result = builder.build(channelHandlerContext, + remotingRequest(RequestCode.LITE_PULL_MESSAGE, litePullHeader, null)); + assertResourceOrder(result, "Group:retryGroup"); + assertActions(result, "Group:retryGroup", Action.SUB); + + ViewMessageRequestHeader viewMessageHeader = new ViewMessageRequestHeader(); + viewMessageHeader.setTopic("viewTopic"); + viewMessageHeader.setOffset(0L); + result = builder.build(channelHandlerContext, + remotingRequest(RequestCode.VIEW_MESSAGE_BY_ID, viewMessageHeader, null)); + assertResourceOrder(result, "Topic:viewTopic"); + assertActions(result, "Topic:viewTopic", Action.GET); + + BatchAck firstAck = batchAck("topicA", "groupA", "0"); + BatchAck duplicateAck = batchAck("topicA", "groupA", "0"); + BatchAck secondAck = batchAck("topicB", "groupB", "0"); + BatchAck retryV1Ack = batchAck("topicA", "groupA", "1"); + BatchAck retryV2Ack = batchAck("topicA", "groupA", "2"); + BatchAckMessageRequestBody batchAckBody = new BatchAckMessageRequestBody(); + batchAckBody.setAcks(Arrays.asList( + firstAck, duplicateAck, secondAck, retryV1Ack, retryV2Ack)); + result = builder.build(channelHandlerContext, + remotingRequest(RequestCode.BATCH_ACK_MESSAGE, null, batchAckBody.encode())); + assertResourceOrder(result, + "Topic:topicA", + "Group:groupA", + "Topic:topicB", + "Group:groupB"); + for (DefaultAuthorizationContext context : result) { + Assert.assertEquals(Collections.singletonList(Action.SUB), context.getActions()); } - Assert.assertTrue(result.stream().anyMatch(ctx -> "Group:groupX".equals(ctx.getResource().getResourceKey()))); - Assert.assertTrue(result.stream().anyMatch(ctx -> "Group:groupY".equals(ctx.getResource().getResourceKey()))); + SendMessageRequestHeader replyHeader = new SendMessageRequestHeader(); + replyHeader.setProducerGroup("producerOnly"); + replyHeader.setTopic("replyTopic"); + result = builder.build(channelHandlerContext, + remotingRequest(RequestCode.SEND_REPLY_MESSAGE, replyHeader, null)); + assertResourceOrder(result, "Topic:replyTopic"); + assertActions(result, "Topic:replyTopic", Action.PUB); + + SendMessageRequestHeaderV2 replyHeaderV2 = new SendMessageRequestHeaderV2(); + replyHeaderV2.setA("producerOnly"); + replyHeaderV2.setB("replyTopicV2"); + result = builder.build(channelHandlerContext, + remotingRequest(RequestCode.SEND_REPLY_MESSAGE_V2, replyHeaderV2, null)); + assertResourceOrder(result, "Topic:replyTopicV2"); + assertActions(result, "Topic:replyTopicV2", Action.PUB); + + QueryAssignmentRequestBody assignmentBody = new QueryAssignmentRequestBody(); + assignmentBody.setTopic("assignmentTopic"); + assignmentBody.setConsumerGroup("assignmentGroup"); + result = builder.build(channelHandlerContext, + remotingRequest(RequestCode.QUERY_ASSIGNMENT, null, assignmentBody.encode())); + assertResourceSet(result, "Topic:assignmentTopic", "Group:assignmentGroup"); + assertActions(result, "Topic:assignmentTopic", Action.SUB); + assertActions(result, "Group:assignmentGroup", Action.SUB); + + SetMessageRequestModeRequestBody modeBody = new SetMessageRequestModeRequestBody(); + modeBody.setTopic("modeTopic"); + modeBody.setConsumerGroup("modeGroup"); + result = builder.build(channelHandlerContext, + remotingRequest(RequestCode.SET_MESSAGE_REQUEST_MODE, null, modeBody.encode())); + assertResourceSet(result, "Topic:modeTopic", "Group:modeGroup"); + assertActions(result, "Topic:modeTopic", Action.SUB); + assertActions(result, "Group:modeGroup", Action.UPDATE); + + CheckClientRequestBody checkClientBody = new CheckClientRequestBody(); + checkClientBody.setGroup("checkGroup"); + SubscriptionData subscriptionData = new SubscriptionData(); + subscriptionData.setTopic("checkTopic"); + checkClientBody.setSubscriptionData(subscriptionData); + result = builder.build(channelHandlerContext, + remotingRequest(RequestCode.CHECK_CLIENT_CONFIG, null, checkClientBody.encode())); + assertResourceSet(result, "Topic:checkTopic", "Group:checkGroup"); + assertActions(result, "Topic:checkTopic", Action.SUB); + assertActions(result, "Group:checkGroup", Action.SUB); + } + + @Test + public void buildConsumerStartOffsetRequestsForTopicReadAndSubscription() { + mockRemotingChannel(); + + GetMaxOffsetRequestHeader header = new GetMaxOffsetRequestHeader(); + header.setTopic("topic"); + header.setQueueId(0); + + List result = builder.build(channelHandlerContext, + remotingRequest(RequestCode.GET_MAX_OFFSET, header, null)); + + assertResourceOrder(result, "Topic:topic"); + assertActions(result, "Topic:topic", Action.SUB, Action.GET); + + header.setTopic("%RETRY%group"); + result = builder.build(channelHandlerContext, + remotingRequest(RequestCode.GET_MAX_OFFSET, header, null)); + + assertResourceOrder(result, "Group:group"); + assertActions(result, "Group:group", Action.SUB, Action.GET); + + SearchOffsetRequestHeader searchHeader = new SearchOffsetRequestHeader(); + searchHeader.setTopic("topic"); + searchHeader.setQueueId(0); + searchHeader.setTimestamp(0L); + result = builder.build(channelHandlerContext, + remotingRequest(RequestCode.SEARCH_OFFSET_BY_TIMESTAMP, searchHeader, null)); + + assertResourceOrder(result, "Topic:topic"); + assertActions(result, "Topic:topic", Action.SUB, Action.GET); + + searchHeader.setTopic("%RETRY%group"); + result = builder.build(channelHandlerContext, + remotingRequest(RequestCode.SEARCH_OFFSET_BY_TIMESTAMP, searchHeader, null)); + + assertResourceOrder(result, "Group:group"); + assertActions(result, "Group:group", Action.SUB, Action.GET); + } + + @Test + public void rejectMalformedAdditionalRemotingDataRequests() { + mockRemotingChannel(); + + PullMessageRequestHeader litePullHeader = new PullMessageRequestHeader(); + litePullHeader.setConsumerGroup("liteGroup"); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.LITE_PULL_MESSAGE, litePullHeader, null))); + litePullHeader.setTopic("liteTopic"); + litePullHeader.setConsumerGroup(" "); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.LITE_PULL_MESSAGE, litePullHeader, null))); + + PullMessageRequestHeader mismatchedRetryHeader = new PullMessageRequestHeader(); + mismatchedRetryHeader.setTopic("%RETRY%ownerGroup"); + mismatchedRetryHeader.setConsumerGroup("otherGroup"); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.PULL_MESSAGE, mismatchedRetryHeader, null))); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.LITE_PULL_MESSAGE, mismatchedRetryHeader, null))); + + SendMessageRequestHeader replyHeader = new SendMessageRequestHeader(); + replyHeader.setProducerGroup("producerOnly"); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.SEND_REPLY_MESSAGE, replyHeader, null))); + SendMessageRequestHeaderV2 replyHeaderV2 = new SendMessageRequestHeaderV2(); + replyHeaderV2.setA("producerOnly"); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.SEND_REPLY_MESSAGE_V2, replyHeaderV2, null))); + + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.BATCH_ACK_MESSAGE, null, null))); + BatchAckMessageRequestBody emptyBatch = new BatchAckMessageRequestBody(); + emptyBatch.setAcks(Collections.emptyList()); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.BATCH_ACK_MESSAGE, null, emptyBatch.encode()))); + BatchAckMessageRequestBody nullEntryBatch = new BatchAckMessageRequestBody(); + nullEntryBatch.setAcks(Collections.singletonList(null)); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.BATCH_ACK_MESSAGE, null, nullEntryBatch.encode()))); + BatchAck blankTopicAck = batchAck("topic", "group", "0"); + blankTopicAck.setTopic(" "); + BatchAckMessageRequestBody blankTopicBatch = new BatchAckMessageRequestBody(); + blankTopicBatch.setAcks(Collections.singletonList(blankTopicAck)); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.BATCH_ACK_MESSAGE, null, blankTopicBatch.encode()))); + BatchAck blankGroupAck = batchAck("topic", "group", "0"); + blankGroupAck.setConsumerGroup(" "); + BatchAckMessageRequestBody blankGroupBatch = new BatchAckMessageRequestBody(); + blankGroupBatch.setAcks(Collections.singletonList(blankGroupAck)); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.BATCH_ACK_MESSAGE, null, blankGroupBatch.encode()))); + + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.QUERY_ASSIGNMENT, null, null))); + QueryAssignmentRequestBody assignmentBody = new QueryAssignmentRequestBody(); + assignmentBody.setTopic(" "); + assignmentBody.setConsumerGroup("group"); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.QUERY_ASSIGNMENT, null, assignmentBody.encode()))); + assignmentBody.setTopic("topic"); + assignmentBody.setConsumerGroup(" "); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.QUERY_ASSIGNMENT, null, assignmentBody.encode()))); + + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.SET_MESSAGE_REQUEST_MODE, null, null))); + SetMessageRequestModeRequestBody modeBody = new SetMessageRequestModeRequestBody(); + modeBody.setTopic(" "); + modeBody.setConsumerGroup("group"); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.SET_MESSAGE_REQUEST_MODE, null, modeBody.encode()))); + modeBody.setTopic("topic"); + modeBody.setConsumerGroup(" "); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.SET_MESSAGE_REQUEST_MODE, null, modeBody.encode()))); + + CheckClientRequestBody checkClientBody = new CheckClientRequestBody(); + checkClientBody.setGroup("checkGroup"); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.CHECK_CLIENT_CONFIG, null, checkClientBody.encode()))); + SubscriptionData blankSubscription = new SubscriptionData(); + blankSubscription.setTopic(" "); + checkClientBody.setSubscriptionData(blankSubscription); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.CHECK_CLIENT_CONFIG, null, checkClientBody.encode()))); + blankSubscription.setTopic("checkTopic"); + checkClientBody.setGroup(" "); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.CHECK_CLIENT_CONFIG, null, checkClientBody.encode()))); + + EndTransactionRequestHeader endTransactionHeader = new EndTransactionRequestHeader(); + endTransactionHeader.setProducerGroup("producerOnly"); + RemotingCommand request = remotingRequest(RequestCode.END_TRANSACTION, endTransactionHeader, null); + Assert.assertTrue(builder.build(channelHandlerContext, request).isEmpty()); + endTransactionHeader.setTopic(" "); + Assert.assertTrue(builder.build(channelHandlerContext, + remotingRequest(RequestCode.END_TRANSACTION, endTransactionHeader, null)).isEmpty()); + + ViewMessageRequestHeader viewMessageHeader = new ViewMessageRequestHeader(); + viewMessageHeader.setOffset(0L); + Assert.assertTrue(builder.build(channelHandlerContext, + remotingRequest(RequestCode.VIEW_MESSAGE_BY_ID, viewMessageHeader, null)).isEmpty()); + viewMessageHeader.setTopic(" "); + Assert.assertTrue(builder.build(channelHandlerContext, + remotingRequest(RequestCode.VIEW_MESSAGE_BY_ID, viewMessageHeader, null)).isEmpty()); + } + + @Test + public void buildAdditionalRemotingDataPathBoundaries() { + mockRemotingChannel(); + + SendMessageRequestHeaderV2 batchHeader = new SendMessageRequestHeaderV2(); + batchHeader.setB("%RETRY%batchGroup"); + List result = builder.build(channelHandlerContext, + remotingRequest(RequestCode.SEND_BATCH_MESSAGE, batchHeader, null)); + assertResourceOrder(result, "Group:batchGroup"); + assertActions(result, "Group:batchGroup", Action.SUB); + + PullMessageRequestHeader retryPullHeader = new PullMessageRequestHeader(); + retryPullHeader.setTopic("%RETRY%pullGroup"); + retryPullHeader.setConsumerGroup("pullGroup"); + result = builder.build(channelHandlerContext, + remotingRequest(RequestCode.PULL_MESSAGE, retryPullHeader, null)); + assertResourceOrder(result, "Group:pullGroup"); + assertActions(result, "Group:pullGroup", Action.SUB); + + PullMessageRequestHeader missingPullGroup = new PullMessageRequestHeader(); + missingPullGroup.setTopic("topic"); + Assert.assertThrows(AuthorizationException.class, () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.PULL_MESSAGE, missingPullGroup, null))); + + Assert.assertThrows(AuthorizationException.class, () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.HEART_BEAT, new HeartbeatRequestHeader(), null))); + + HeartbeatData heartbeatData = new HeartbeatData(); + ConsumerData consumerData = new ConsumerData(); + consumerData.setGroupName("group"); + SubscriptionData subscriptionData = new SubscriptionData(); + subscriptionData.setTopic(" "); + consumerData.setSubscriptionDataSet(Collections.singleton(subscriptionData)); + heartbeatData.setConsumerDataSet(Collections.singleton(consumerData)); + Assert.assertThrows(AuthorizationException.class, () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.HEART_BEAT, new HeartbeatRequestHeader(), + JSON.toJSONBytes(heartbeatData)))); + + RemotingCommand listRequest = + RemotingCommand.createRequestCommand(RequestCode.GET_ALL_TOPIC_CONFIG, null); + listRequest.setExtFields(null); + result = builder.build(channelHandlerContext, listRequest); + assertAnyResource(result, ResourceType.TOPIC); + assertActions(result, "Topic:*", Action.LIST); + } + + @Test + public void buildRemainingResourceAdminRequests() { + mockRemotingChannel(); + + CreateTopicListRequestBody topicListBody = new CreateTopicListRequestBody(Arrays.asList( + new TopicConfig("topicA"), new TopicConfig("%RETRY%groupA"), new TopicConfig("topicA"))); + List result = builder.build(channelHandlerContext, + remotingRequest(RequestCode.UPDATE_AND_CREATE_TOPIC_LIST, null, topicListBody.encode())); + assertResourceOrder(result, "Topic:topicA", "Group:groupA"); + assertActions(result, "Topic:topicA", Action.CREATE); + assertActions(result, "Group:groupA", Action.CREATE); + + result = builder.build(channelHandlerContext, + remotingRequest(RequestCode.UPDATE_COLD_DATA_FLOW_CTR_CONFIG, null, + "groupA=1\ngroupB=2\n".getBytes(StandardCharsets.UTF_8))); + assertResourceSet(result, "Group:groupA", "Group:groupB"); + assertActions(result, "Group:groupA", Action.UPDATE); + assertActions(result, "Group:groupB", Action.UPDATE); + + result = builder.build(channelHandlerContext, + remotingRequest(RequestCode.REMOVE_COLD_DATA_FLOW_CTR_CONFIG, null, + "groupA".getBytes(StandardCharsets.UTF_8))); + assertResourceOrder(result, "Group:groupA"); + assertActions(result, "Group:groupA", Action.UPDATE); + + SubscriptionGroupConfig groupA = new SubscriptionGroupConfig(); + groupA.setGroupName("groupA"); + SubscriptionGroupConfig groupB = new SubscriptionGroupConfig(); + groupB.setGroupName("groupB"); + SubscriptionGroupConfig duplicateGroup = new SubscriptionGroupConfig(); + duplicateGroup.setGroupName("groupA"); + SubscriptionGroupList groupList = new SubscriptionGroupList(Arrays.asList(groupA, groupB, duplicateGroup)); + result = builder.build(channelHandlerContext, + remotingRequest(RequestCode.UPDATE_AND_CREATE_SUBSCRIPTIONGROUP_LIST, null, groupList.encode())); + assertResourceOrder(result, "Group:groupA", "Group:groupB"); + assertActions(result, "Group:groupA", Action.CREATE); + assertActions(result, "Group:groupB", Action.CREATE); + + TopicQueueMappingDetail mappingDetail = + new TopicQueueMappingDetail("staticTopic", 1, "broker-a", 1L); + result = builder.build(channelHandlerContext, + remotingRequest(RequestCode.UPDATE_AND_CREATE_STATIC_TOPIC, + createTopicHeader("staticTopic"), mappingDetail.encode())); + assertResourceOrder(result, "Topic:staticTopic"); + assertActions(result, "Topic:staticTopic", Action.CREATE); + } + + @Test + public void rejectMalformedResourceAdminRequests() { + mockRemotingChannel(); + + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.UPDATE_AND_CREATE_TOPIC_LIST, null, null))); + CreateTopicListRequestBody emptyTopicList = + new CreateTopicListRequestBody(Collections.emptyList()); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.UPDATE_AND_CREATE_TOPIC_LIST, null, emptyTopicList.encode()))); + CreateTopicListRequestBody nullTopicEntry = + new CreateTopicListRequestBody(Collections.singletonList(null)); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.UPDATE_AND_CREATE_TOPIC_LIST, null, nullTopicEntry.encode()))); + + CreateTopicListRequestBody invalidTopicList = + new CreateTopicListRequestBody(Arrays.asList(new TopicConfig("topicA"), new TopicConfig(" "))); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.UPDATE_AND_CREATE_TOPIC_LIST, null, invalidTopicList.encode()))); + + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.UPDATE_COLD_DATA_FLOW_CTR_CONFIG, null, new byte[0]))); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.REMOVE_COLD_DATA_FLOW_CTR_CONFIG, null, new byte[0]))); + + SubscriptionGroupConfig invalidGroup = new SubscriptionGroupConfig(); + invalidGroup.setGroupName(" "); + SubscriptionGroupList emptyGroupList = new SubscriptionGroupList(Collections.emptyList()); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.UPDATE_AND_CREATE_SUBSCRIPTIONGROUP_LIST, null, + emptyGroupList.encode()))); + SubscriptionGroupList nullGroupEntry = + new SubscriptionGroupList(Collections.singletonList(null)); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.UPDATE_AND_CREATE_SUBSCRIPTIONGROUP_LIST, null, + nullGroupEntry.encode()))); + SubscriptionGroupList invalidGroupList = + new SubscriptionGroupList(Arrays.asList(new SubscriptionGroupConfig(), invalidGroup)); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.UPDATE_AND_CREATE_SUBSCRIPTIONGROUP_LIST, null, + invalidGroupList.encode()))); + + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.UPDATE_AND_CREATE_STATIC_TOPIC, createTopicHeader(" "), null))); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.UPDATE_AND_CREATE_STATIC_TOPIC, + createTopicHeader("staticTopic"), null))); + TopicQueueMappingDetail blankMapping = + new TopicQueueMappingDetail(" ", 1, "broker-a", 1L); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.UPDATE_AND_CREATE_STATIC_TOPIC, + createTopicHeader("staticTopic"), blankMapping.encode()))); + TopicQueueMappingDetail mismatchedMapping = + new TopicQueueMappingDetail("otherTopic", 1, "broker-a", 1L); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.UPDATE_AND_CREATE_STATIC_TOPIC, + createTopicHeader("staticTopic"), mismatchedMapping.encode()))); + } + + @Test + public void buildRemainingAdminReadRequests() { + mockRemotingChannel(); + + int[] topicListCodes = new int[] { + RequestCode.GET_ALL_TOPIC_CONFIG, + RequestCode.GET_TIMER_METRICS, + RequestCode.GET_SYSTEM_TOPIC_LIST_FROM_BROKER + }; + for (int requestCode : topicListCodes) { + List result = builder.build(channelHandlerContext, + remotingRequest(requestCode, null, null)); + assertResourceOrder(result, "Topic:*"); + assertAnyResource(result, ResourceType.TOPIC); + assertActions(result, "Topic:*", Action.LIST); + } + + int[] groupListCodes = new int[] { + RequestCode.GET_COLD_DATA_FLOW_CTR_INFO, + RequestCode.GET_ALL_SUBSCRIPTIONGROUP_CONFIG + }; + for (int requestCode : groupListCodes) { + List result = builder.build(channelHandlerContext, + remotingRequest(requestCode, null, null)); + assertResourceOrder(result, "Group:*"); + assertAnyResource(result, ResourceType.GROUP); + assertActions(result, "Group:*", Action.LIST); + } + + List requestModeResult = builder.build(channelHandlerContext, + remotingRequest(RequestCode.GET_ALL_MESSAGE_REQUEST_MODE, null, null)); + assertResourceOrder(requestModeResult, "Topic:*", "Group:*"); + assertAnyResource(requestModeResult, ResourceType.TOPIC); + assertAnyResource(requestModeResult, ResourceType.GROUP); + assertActions(requestModeResult, "Topic:*", Action.LIST); + assertActions(requestModeResult, "Group:*", Action.LIST); + + int[] clusterGetCodes = new int[] { + RequestCode.GET_TIMER_CHECK_POINT, + RequestCode.GET_ALL_DELAY_OFFSET, + RequestCode.GET_BROKER_HA_STATUS, + RequestCode.GET_BROKER_EPOCH_CACHE, + RequestCode.GET_BROKER_LITE_INFO + }; + for (int requestCode : clusterGetCodes) { + List result = builder.build(channelHandlerContext, + remotingRequest(requestCode, null, null)); + assertResourceOrder(result, "Cluster:DefaultCluster"); + assertActions(result, "Cluster:DefaultCluster", Action.GET); + } + + int[] updateCodes = new int[] { + RequestCode.SET_COMMITLOG_READ_MODE, + RequestCode.CLEAN_EXPIRED_CONSUMEQUEUE, + RequestCode.DELETE_EXPIRED_COMMITLOG, + RequestCode.CLEAN_UNUSED_TOPIC, + RequestCode.POP_ROLLBACK, + RequestCode.SWITCH_TIMER_ENGINE + }; + for (int requestCode : updateCodes) { + List result = builder.build(channelHandlerContext, + remotingRequest(requestCode, null, null)); + assertResourceOrder(result, "Cluster:DefaultCluster"); + assertActions(result, "Cluster:DefaultCluster", Action.UPDATE); + } + } + + @Test + public void buildLiteHeaderRequests() { + mockRemotingChannel(); + + PopLiteMessageRequestHeader popHeader = new PopLiteMessageRequestHeader(); + popHeader.setClientId("clientA"); + popHeader.setConsumerGroup("groupA"); + popHeader.setTopic("topicA"); + popHeader.setMaxMsgNum(16); + popHeader.setInvisibleTime(3000); + popHeader.setPollTime(1000); + popHeader.setBornTime(System.currentTimeMillis()); + List result = builder.build(channelHandlerContext, + remotingRequest(RequestCode.POP_LITE_MESSAGE, popHeader, null)); + assertResourceOrder(result, "Group:groupA", "Topic:topicA"); + assertActions(result, "Group:groupA", Action.SUB); + assertActions(result, "Topic:topicA", Action.SUB); + + GetParentTopicInfoRequestHeader parentTopicHeader = new GetParentTopicInfoRequestHeader(); + parentTopicHeader.setTopic("topicA"); + result = builder.build(channelHandlerContext, + remotingRequest(RequestCode.GET_PARENT_TOPIC_INFO, parentTopicHeader, null)); + assertResourceOrder(result, "Topic:topicA"); + assertActions(result, "Topic:topicA", Action.GET); + + GetLiteTopicInfoRequestHeader liteTopicHeader = new GetLiteTopicInfoRequestHeader(); + liteTopicHeader.setParentTopic("topicA"); + liteTopicHeader.setLiteTopic("liteTopicA"); + result = builder.build(channelHandlerContext, + remotingRequest(RequestCode.GET_LITE_TOPIC_INFO, liteTopicHeader, null)); + assertResourceOrder(result, "Topic:topicA"); + assertActions(result, "Topic:topicA", Action.GET); + + GetLiteClientInfoRequestHeader clientInfoHeader = new GetLiteClientInfoRequestHeader(); + clientInfoHeader.setParentTopic("topicA"); + clientInfoHeader.setGroup("groupA"); + clientInfoHeader.setClientId("clientA"); + result = builder.build(channelHandlerContext, + remotingRequest(RequestCode.GET_LITE_CLIENT_INFO, clientInfoHeader, null)); + assertResourceOrder(result, "Topic:topicA", "Group:groupA"); + assertActions(result, "Topic:topicA", Action.GET); + assertActions(result, "Group:groupA", Action.GET); + + GetLiteGroupInfoRequestHeader groupInfoHeader = new GetLiteGroupInfoRequestHeader(); + groupInfoHeader.setGroup("groupA"); + result = builder.build(channelHandlerContext, + remotingRequest(RequestCode.GET_LITE_GROUP_INFO, groupInfoHeader, null)); + assertResourceOrder(result, "Group:groupA"); + assertActions(result, "Group:groupA", Action.GET); + + TriggerLiteDispatchRequestHeader dispatchHeader = new TriggerLiteDispatchRequestHeader(); + dispatchHeader.setGroup("groupA"); + result = builder.build(channelHandlerContext, + remotingRequest(RequestCode.TRIGGER_LITE_DISPATCH, dispatchHeader, null)); + assertResourceOrder(result, "Group:groupA"); + assertActions(result, "Group:groupA", Action.UPDATE); + } + + @Test + public void buildLiteSubscriptionControlFromBody() { + mockRemotingChannel(); + + LiteSubscriptionDTO subscriptionA = new LiteSubscriptionDTO() + .setAction(LiteSubscriptionAction.PARTIAL_ADD) + .setClientId("clientA") + .setGroup("groupA") + .setTopic("topicA"); + LiteSubscriptionDTO subscriptionB = new LiteSubscriptionDTO() + .setAction(LiteSubscriptionAction.COMPLETE_ADD) + .setClientId("clientB") + .setGroup("groupB") + .setTopic("topicA"); + LiteSubscriptionCtlRequestBody requestBody = new LiteSubscriptionCtlRequestBody(); + requestBody.setSubscriptionSet(new LinkedHashSet<>(Arrays.asList(subscriptionA, subscriptionB))); + + List result = builder.build(channelHandlerContext, + remotingRequest(RequestCode.LITE_SUBSCRIPTION_CTL, null, requestBody.encode())); + assertResourceSet(result, "Group:groupA", "Topic:topicA", "Group:groupB"); + assertActions(result, "Group:groupA", Action.SUB); + assertActions(result, "Topic:topicA", Action.SUB); + assertActions(result, "Group:groupB", Action.SUB); + } + + @Test + public void buildBatchDeleteRequests() { + mockRemotingChannel(); + + DeleteTopicListRequestBody topicListBody = new DeleteTopicListRequestBody(); + topicListBody.setTopicList(Arrays.asList("topicA", "%RETRY%groupA", "topicA")); + List result = builder.build(channelHandlerContext, + remotingRequest(RequestCode.DELETE_TOPIC_IN_BROKER_LIST, null, topicListBody.encode())); + assertResourceOrder(result, "Topic:topicA", "Group:groupA"); + assertActions(result, "Topic:topicA", Action.DELETE); + assertActions(result, "Group:groupA", Action.DELETE); + for (DefaultAuthorizationContext context : result) { + Assert.assertEquals(String.valueOf(RequestCode.DELETE_TOPIC_IN_BROKER_LIST), context.getRpcCode()); + } + + DeleteSubscriptionGroupListRequestBody groupListBody = new DeleteSubscriptionGroupListRequestBody(); + groupListBody.setGroupNameList(Arrays.asList("groupX", "groupY")); + result = builder.build(channelHandlerContext, + remotingRequest(RequestCode.DELETE_SUBSCRIPTION_GROUP_LIST, null, groupListBody.encode())); + assertResourceOrder(result, "Group:groupX", "Group:groupY"); + assertActions(result, "Group:groupX", Action.DELETE); + assertActions(result, "Group:groupY", Action.DELETE); + for (DefaultAuthorizationContext context : result) { + Assert.assertEquals(String.valueOf(RequestCode.DELETE_SUBSCRIPTION_GROUP_LIST), context.getRpcCode()); + } + } + + @Test + public void rejectMalformedBodyDrivenRequests() { + mockRemotingChannel(); + + Assert.assertThrows(AuthorizationException.class, () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.LITE_SUBSCRIPTION_CTL, null, null))); + + LiteSubscriptionCtlRequestBody emptySubscriptionBody = new LiteSubscriptionCtlRequestBody(); + emptySubscriptionBody.setSubscriptionSet(Collections.emptySet()); + Assert.assertThrows(AuthorizationException.class, () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.LITE_SUBSCRIPTION_CTL, null, emptySubscriptionBody.encode()))); + + LiteSubscriptionDTO invalidSubscription = new LiteSubscriptionDTO() + .setAction(LiteSubscriptionAction.PARTIAL_ADD) + .setClientId("clientA") + .setGroup(" ") + .setTopic("topicA"); + LiteSubscriptionCtlRequestBody invalidSubscriptionBody = new LiteSubscriptionCtlRequestBody(); + invalidSubscriptionBody.setSubscriptionSet(Collections.singleton(invalidSubscription)); + Assert.assertThrows(AuthorizationException.class, () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.LITE_SUBSCRIPTION_CTL, null, invalidSubscriptionBody.encode()))); + + DeleteTopicListRequestBody emptyTopicList = new DeleteTopicListRequestBody(Collections.emptyList()); + Assert.assertThrows(AuthorizationException.class, () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.DELETE_TOPIC_IN_BROKER_LIST, null, emptyTopicList.encode()))); + + DeleteTopicListRequestBody invalidTopicList = new DeleteTopicListRequestBody( + Arrays.asList("topicA", " ")); + Assert.assertThrows(AuthorizationException.class, () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.DELETE_TOPIC_IN_BROKER_LIST, null, invalidTopicList.encode()))); + + DeleteSubscriptionGroupListRequestBody invalidGroupList = + new DeleteSubscriptionGroupListRequestBody(Arrays.asList("groupA", " ")); + Assert.assertThrows(AuthorizationException.class, () -> builder.build(channelHandlerContext, + remotingRequest(RequestCode.DELETE_SUBSCRIPTION_GROUP_LIST, null, invalidGroupList.encode()))); + } + + @Test + public void buildGrpcHeartbeatByClientShape() { + Metadata metadata = new Metadata(); + metadata.put(GrpcConstants.AUTHORIZATION_AK, "rocketmq"); + metadata.put(GrpcConstants.REMOTE_ADDRESS, "192.168.0.1"); + metadata.put(GrpcConstants.CHANNEL_ID, "channel-id"); + + List result = builder.build(metadata, HeartbeatRequest.newBuilder() + .setClientType(ClientType.CLIENT_TYPE_UNSPECIFIED) + .setGroup(Resource.newBuilder().setName("historicalConsumer")) + .build()); + assertResourceOrder(result, "Group:historicalConsumer"); + assertActions(result, "Group:historicalConsumer", Action.SUB); + + Assert.assertNull(builder.build(metadata, HeartbeatRequest.newBuilder() + .setClientType(ClientType.PRODUCER) + .build())); + Assert.assertNull(builder.build(metadata, HeartbeatRequest.newBuilder() + .setClientType(ClientType.CLIENT_TYPE_UNSPECIFIED) + .build())); + Assert.assertThrows(AuthorizationException.class, + () -> builder.build(metadata, HeartbeatRequest.newBuilder() + .setClientType(ClientType.PRODUCER) + .setGroup(Resource.newBuilder().setName("mustNotBecomeGroup")) + .build())); + } + + @Test + public void buildGrpcLiteSubscriptionIgnoresLiteTopics() { + Metadata metadata = new Metadata(); + metadata.put(GrpcConstants.AUTHORIZATION_AK, "rocketmq"); + metadata.put(GrpcConstants.REMOTE_ADDRESS, "192.168.0.1"); + metadata.put(GrpcConstants.CHANNEL_ID, "channel-id"); + + SyncLiteSubscriptionRequest emptyLiteTopics = SyncLiteSubscriptionRequest.newBuilder() + .setAction(apache.rocketmq.v2.LiteSubscriptionAction.COMPLETE_REMOVE) + .setTopic(Resource.newBuilder().setName("parentTopic")) + .setGroup(Resource.newBuilder().setName("group")) + .build(); + List result = builder.build(metadata, emptyLiteTopics); + assertResourceOrder(result, "Group:group", "Topic:parentTopic"); + assertActions(result, "Group:group", Action.SUB); + assertActions(result, "Topic:parentTopic", Action.SUB); + + SyncLiteSubscriptionRequest withLiteTopics = SyncLiteSubscriptionRequest.newBuilder() + .setAction(apache.rocketmq.v2.LiteSubscriptionAction.PARTIAL_ADD) + .setTopic(Resource.newBuilder().setName("parentTopic")) + .setGroup(Resource.newBuilder().setName("group")) + .addLiteTopicSet("liteTopic") + .build(); + result = builder.build(metadata, withLiteTopics); + assertResourceOrder(result, "Group:group", "Topic:parentTopic"); + assertActions(result, "Group:group", Action.SUB); + assertActions(result, "Topic:parentTopic", Action.SUB); + } + + private BatchAck batchAck(String topic, String group, String retry) { + BatchAck ack = new BatchAck(); + ack.setTopic(topic); + ack.setConsumerGroup(group); + ack.setRetry(retry); + BitSet bitSet = new BitSet(); + bitSet.set(0); + ack.setBitSet(bitSet); + return ack; + } + + private RemotingCommand remotingRequest(int requestCode, CommandCustomHeader header, byte[] body) { + RemotingCommand request = RemotingCommand.createRequestCommand(requestCode, header); + request.addExtField("AccessKey", "rocketmq"); + request.setBody(body); + request.makeCustomHeaderToNet(); + return request; + } + + private CreateTopicRequestHeader createTopicHeader(String topic) { + CreateTopicRequestHeader header = new CreateTopicRequestHeader(); + header.setTopic(topic); + header.setDefaultTopic("defaultTopic"); + header.setReadQueueNums(8); + header.setWriteQueueNums(8); + header.setPerm(6); + header.setTopicFilterType(TopicFilterType.SINGLE_TAG.name()); + header.setOrder(false); + return header; + } + + private void mockRemotingChannel() { + when(channel.id()).thenReturn(mockChannelId("channel-id")); + when(channel.hasAttr(eq(AttributeKeys.PROXY_PROTOCOL_ADDR))).thenReturn(true); + when(channel.attr(eq(AttributeKeys.PROXY_PROTOCOL_ADDR))).thenReturn(mockAttribute("192.168.0.1")); + when(channel.hasAttr(eq(AttributeKeys.PROXY_PROTOCOL_PORT))).thenReturn(true); + when(channel.attr(eq(AttributeKeys.PROXY_PROTOCOL_PORT))).thenReturn(mockAttribute("1234")); + when(channelHandlerContext.channel()).thenReturn(channel); + } + + private void assertResourceOrder(List contexts, String... resourceKeys) { + Assert.assertEquals(resourceKeys.length, contexts.size()); + for (int i = 0; i < resourceKeys.length; i++) { + Assert.assertEquals(resourceKeys[i], contexts.get(i).getResource().getResourceKey()); + } + } + + private void assertResourceSet(List contexts, String... resourceKeys) { + Set actual = new LinkedHashSet<>(); + for (DefaultAuthorizationContext context : contexts) { + actual.add(context.getResource().getResourceKey()); + } + Assert.assertEquals(new LinkedHashSet<>(Arrays.asList(resourceKeys)), actual); + Assert.assertEquals(resourceKeys.length, contexts.size()); + } + + private void assertActions(List contexts, String resourceKey, Action... actions) { + DefaultAuthorizationContext context = contexts.stream() + .filter(item -> resourceKey.equals(item.getResource().getResourceKey())) + .findFirst() + .orElse(null); + Assert.assertNotNull(context); + Assert.assertEquals(new LinkedHashSet<>(Arrays.asList(actions)), + new LinkedHashSet<>(context.getActions())); } private DefaultAuthorizationContext getContext(List contexts, @@ -623,6 +1558,13 @@ private DefaultAuthorizationContext getContext(List .findFirst().orElse(null); } + private void assertAnyResource(List contexts, ResourceType resourceType) { + DefaultAuthorizationContext context = getContext(contexts, resourceType); + Assert.assertNotNull(context); + Assert.assertEquals(ResourcePattern.ANY, context.getResource().getResourcePattern()); + Assert.assertNull(context.getResource().getResourceName()); + } + private ChannelId mockChannelId(String channelId) { return new ChannelId() { @Override @@ -684,4 +1626,4 @@ public void remove() { } }; } -} \ No newline at end of file +} diff --git a/auth/src/test/java/org/apache/rocketmq/auth/authorization/model/ResourceTest.java b/auth/src/test/java/org/apache/rocketmq/auth/authorization/model/ResourceTest.java index a17a4ab6bec..40c01215ef0 100644 --- a/auth/src/test/java/org/apache/rocketmq/auth/authorization/model/ResourceTest.java +++ b/auth/src/test/java/org/apache/rocketmq/auth/authorization/model/ResourceTest.java @@ -16,6 +16,9 @@ */ package org.apache.rocketmq.auth.authorization.model; +import java.util.Collections; +import org.apache.rocketmq.auth.authorization.enums.Decision; +import org.apache.rocketmq.common.action.Action; import org.apache.rocketmq.common.resource.ResourcePattern; import org.apache.rocketmq.common.resource.ResourceType; import org.junit.Assert; @@ -48,6 +51,32 @@ public void parseResource() { @Test public void isMatch() { + Resource topicAny = Resource.of("Topic:*"); + Resource groupAny = Resource.of("Group:*"); + Resource topicListResource = Resource.of(ResourceType.TOPIC, null, ResourcePattern.ANY); + Resource groupListResource = Resource.of(ResourceType.GROUP, null, ResourcePattern.ANY); + Assert.assertTrue(topicAny.isMatch(topicListResource)); + Assert.assertTrue(groupAny.isMatch(groupListResource)); + Assert.assertFalse(topicAny.isMatch(groupListResource)); + Assert.assertFalse(Resource.ofTopic("orders").isMatch(topicListResource)); + Assert.assertFalse(Resource.ofCluster("DefaultCluster").isMatch(topicListResource)); } -} \ No newline at end of file + + @Test + public void typedAnyListPolicyMatch() { + Resource topicListResource = Resource.of(ResourceType.TOPIC, null, ResourcePattern.ANY); + Resource groupListResource = Resource.of(ResourceType.GROUP, null, ResourcePattern.ANY); + PolicyEntry topicListPolicy = PolicyEntry.of( + Resource.of("Topic:*"), Collections.singletonList(Action.LIST), null, Decision.ALLOW); + PolicyEntry literalTopicPolicy = PolicyEntry.of( + Resource.ofTopic("orders"), Collections.singletonList(Action.LIST), null, Decision.ALLOW); + + Assert.assertTrue(topicListPolicy.isMatchResource(topicListResource)); + Assert.assertTrue(topicListPolicy.isMatchAction(Collections.singletonList(Action.LIST))); + Assert.assertTrue(topicListPolicy.isMatchResource(Resource.ofTopic("orders"))); + Assert.assertFalse(topicListPolicy.isMatchResource(groupListResource)); + Assert.assertFalse(literalTopicPolicy.isMatchResource(topicListResource)); + Assert.assertFalse(topicListPolicy.isMatchAction(Collections.singletonList(Action.GET))); + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/auth/pipeline/AuthorizationPipeline.java b/broker/src/main/java/org/apache/rocketmq/broker/auth/pipeline/AuthorizationPipeline.java index c588dae4e83..e6901ad0e70 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/auth/pipeline/AuthorizationPipeline.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/auth/pipeline/AuthorizationPipeline.java @@ -50,7 +50,7 @@ public void execute(ChannelHandlerContext ctx, RemotingCommand request) throws E } try { List contexts = newContexts(ctx, request); - evaluator.evaluate(contexts); + evaluator.evaluate(request, contexts); } catch (AuthorizationException | AuthenticationException ex) { throw new AbortProcessException(ResponseCode.NO_PERMISSION, ex.getMessage()); } catch (Throwable ex) { diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/QueryMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/QueryMessageProcessor.java index 18197ed22cc..cb1c4287ce3 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/QueryMessageProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/QueryMessageProcessor.java @@ -20,6 +20,10 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.FileRegion; import io.opentelemetry.api.common.Attributes; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; import java.util.concurrent.TimeUnit; import org.apache.commons.lang3.StringUtils; import org.apache.rocketmq.broker.BrokerController; @@ -28,6 +32,9 @@ import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.constant.LoggerName; import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.topic.TopicValidator; import org.apache.rocketmq.logging.org.slf4j.Logger; import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.remoting.common.RemotingHelper; @@ -42,6 +49,7 @@ import org.apache.rocketmq.remoting.protocol.header.ViewMessageRequestHeader; import org.apache.rocketmq.store.QueryMessageResult; import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.apache.rocketmq.store.timer.TimerMessageStore; import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.LABEL_REQUEST_CODE; import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.LABEL_RESPONSE_CODE; @@ -49,6 +57,11 @@ public class QueryMessageProcessor implements NettyRequestProcessor { private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + private static final Set REAL_TOPIC_STORAGE_TOPICS = new HashSet<>(Arrays.asList( + TopicValidator.RMQ_SYS_SCHEDULE_TOPIC, + TimerMessageStore.TIMER_TOPIC, + TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC, + TopicValidator.RMQ_SYS_TRANS_CHECK_MAX_TIME_TOPIC)); private final BrokerController brokerController; public QueryMessageProcessor(final BrokerController brokerController) { @@ -146,6 +159,22 @@ public RemotingCommand viewMessageById(ChannelHandlerContext ctx, RemotingComman final SelectMappedBufferResult selectMappedBufferResult = this.brokerController.getMessageStore().selectOneMessageByOffset(requestHeader.getOffset()); if (selectMappedBufferResult != null) { + if (StringUtils.isNotBlank(requestHeader.getTopic())) { + MessageExt message = MessageDecoder.decode( + selectMappedBufferResult.getByteBuffer().duplicate(), false, false); + if (message == null) { + selectMappedBufferResult.release(); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("decode message by the offset failed"); + return response; + } + if (!matchesRequestTopic(requestHeader.getTopic(), message)) { + selectMappedBufferResult.release(); + response.setCode(ResponseCode.NO_PERMISSION); + response.setRemark("The topic does not match the message"); + return response; + } + } response.setCode(ResponseCode.SUCCESS); response.setRemark(null); @@ -181,4 +210,27 @@ public RemotingCommand viewMessageById(ChannelHandlerContext ctx, RemotingComman return response; } + + private boolean matchesRequestTopic(String requestTopic, MessageExt message) { + String logicalTopic = message.getTopic(); + if (REAL_TOPIC_STORAGE_TOPICS.contains(logicalTopic)) { + logicalTopic = message.getProperty(MessageConst.PROPERTY_REAL_TOPIC); + } + if (Objects.equals(requestTopic, logicalTopic)) { + return true; + } + if (MixAll.isLmq(requestTopic) + && StringUtils.isNotBlank(logicalTopic) + && MixAll.topicAllowsLMQ(logicalTopic)) { + String multiDispatch = message.getProperty(MessageConst.PROPERTY_INNER_MULTI_DISPATCH); + if (StringUtils.isNotBlank(multiDispatch)) { + for (String dispatchTopic : StringUtils.split(multiDispatch, MixAll.LMQ_DISPATCH_SEPARATOR)) { + if (Objects.equals(requestTopic, dispatchTopic)) { + return true; + } + } + } + } + return false; + } } diff --git a/broker/src/test/java/org/apache/rocketmq/broker/auth/pipeline/AuthorizationPipelineTest.java b/broker/src/test/java/org/apache/rocketmq/broker/auth/pipeline/AuthorizationPipelineTest.java new file mode 100644 index 00000000000..0fa2ceb49e6 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/auth/pipeline/AuthorizationPipelineTest.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.auth.pipeline; + +import io.netty.channel.ChannelHandlerContext; +import java.util.Collections; +import java.util.List; +import org.apache.rocketmq.auth.authorization.context.AuthorizationContext; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.common.AbortProcessException; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.heartbeat.HeartbeatData; +import org.apache.rocketmq.remoting.protocol.heartbeat.ProducerData; +import org.junit.Assert; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThatCode; + +public class AuthorizationPipelineTest { + + @Test + public void allowsCompatibleRequestWithEmptyContexts() throws Exception { + AuthorizationPipeline pipeline = createPipeline(); + ProducerData producerData = new ProducerData(); + producerData.setGroupName("producerGroup"); + HeartbeatData heartbeatData = new HeartbeatData(); + heartbeatData.getProducerDataSet().add(producerData); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.HEART_BEAT, null); + request.setBody(heartbeatData.encode()); + + assertThatCode(() -> pipeline.execute(null, request)).doesNotThrowAnyException(); + } + + @Test + public void rejectsUnsupportedRequestWithEmptyContexts() { + AuthorizationPipeline pipeline = createPipeline(); + RemotingCommand request = RemotingCommand.createRequestCommand(-1, null); + + AbortProcessException exception = Assert.assertThrows(AbortProcessException.class, + () -> pipeline.execute(null, request)); + Assert.assertEquals(ResponseCode.NO_PERMISSION, exception.getResponseCode()); + } + + private AuthorizationPipeline createPipeline() { + AuthConfig authConfig = new AuthConfig(); + authConfig.setConfigName("broker-authorization-pipeline-test"); + authConfig.setAuthorizationEnabled(true); + return new AuthorizationPipeline(authConfig) { + @Override + protected List newContexts(ChannelHandlerContext ctx, + RemotingCommand request) { + return Collections.emptyList(); + } + }; + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/processor/ClientManageProcessorTest.java b/broker/src/test/java/org/apache/rocketmq/broker/processor/ClientManageProcessorTest.java index ec7164247ec..d22862b49cc 100644 --- a/broker/src/test/java/org/apache/rocketmq/broker/processor/ClientManageProcessorTest.java +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/ClientManageProcessorTest.java @@ -42,6 +42,7 @@ import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumerData; import org.apache.rocketmq.remoting.protocol.heartbeat.HeartbeatData; import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.ProducerData; import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; import org.apache.rocketmq.store.config.MessageStoreConfig; import org.junit.Before; @@ -146,6 +147,24 @@ public void processRequest_heartbeat() throws RemotingCommandException { assertThat(consumerGroupInfoSimple).isEqualTo(consumerGroupInfo); } + @Test + public void processRequest_producerHeartbeat() throws RemotingCommandException { + ProducerData producerData = new ProducerData(); + producerData.setGroupName(group); + HeartbeatData heartbeatData = new HeartbeatData(); + heartbeatData.setClientID(clientId); + heartbeatData.getProducerDataSet().add(producerData); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.HEART_BEAT, null); + request.setLanguage(LanguageCode.JAVA); + request.setVersion(100); + request.setBody(heartbeatData.encode()); + + RemotingCommand response = clientManageProcessor.processRequest(handlerContext, request); + + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + assertThat(brokerController.getProducerManager().getGroupChannelTable().get(group)).containsKey(channel); + } + @Test public void test_heartbeat_costTime() { String topic = "TOPIC_TEST"; diff --git a/broker/src/test/java/org/apache/rocketmq/broker/processor/EndTransactionProcessorTest.java b/broker/src/test/java/org/apache/rocketmq/broker/processor/EndTransactionProcessorTest.java index 1751ad96fdb..42af2dbe47e 100644 --- a/broker/src/test/java/org/apache/rocketmq/broker/processor/EndTransactionProcessorTest.java +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/EndTransactionProcessorTest.java @@ -154,6 +154,24 @@ public void testProcessRequest_RejectRollBackMessage() throws RemotingCommandExc assertThat(response.getCode()).isEqualTo(ResponseCode.ILLEGAL_OPERATION); } + @Test + public void testProcessRequestAllowsMissingTopicForCompatibility() throws RemotingCommandException { + when(transactionMsgService.commitMessage(any(EndTransactionRequestHeader.class))) + .thenReturn(createResponse(ResponseCode.SUCCESS)); + when(messageStore.putMessage(any(MessageExtBrokerInner.class))) + .thenReturn(new PutMessageResult(PutMessageStatus.PUT_OK, + createAppendMessageResult(AppendMessageStatus.PUT_OK))); + EndTransactionRequestHeader header = createEndTransactionRequestHeader( + MessageSysFlag.TRANSACTION_COMMIT_TYPE, false); + header.setTopic(null); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.END_TRANSACTION, header); + request.makeCustomHeaderToNet(); + + RemotingCommand response = endTransactionProcessor.processRequest(handlerContext, request); + + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + private MessageExt createDefaultMessageExt() { MessageExt messageExt = new MessageExt(); messageExt.setMsgId("12345678"); @@ -169,7 +187,7 @@ private MessageExt createDefaultMessageExt() { private EndTransactionRequestHeader createEndTransactionRequestHeader(int status, boolean isCheckMsg) { EndTransactionRequestHeader header = new EndTransactionRequestHeader(); - header.setTopic("topic"); + header.setTopic(TOPIC); header.setCommitLogOffset(123456789L); header.setFromTransactionCheck(isCheckMsg); header.setCommitOrRollback(status); diff --git a/broker/src/test/java/org/apache/rocketmq/broker/processor/QueryMessageProcessorTest.java b/broker/src/test/java/org/apache/rocketmq/broker/processor/QueryMessageProcessorTest.java index 3656c5be2bd..f842e8da6b2 100644 --- a/broker/src/test/java/org/apache/rocketmq/broker/processor/QueryMessageProcessorTest.java +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/QueryMessageProcessorTest.java @@ -19,9 +19,19 @@ import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashMap; import org.apache.rocketmq.broker.BrokerController; import org.apache.rocketmq.common.BrokerConfig; import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.topic.TopicValidator; import org.apache.rocketmq.remoting.exception.RemotingCommandException; import org.apache.rocketmq.remoting.netty.NettyClientConfig; import org.apache.rocketmq.remoting.netty.NettyServerConfig; @@ -34,6 +44,7 @@ import org.apache.rocketmq.store.QueryMessageResult; import org.apache.rocketmq.store.SelectMappedBufferResult; import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.timer.TimerMessageStore; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -42,12 +53,14 @@ import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; -import java.util.HashMap; - import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) @@ -95,7 +108,7 @@ public void testQueryMessage() throws RemotingCommandException { } @Test - public void testViewMessageById() throws RemotingCommandException { + public void testViewMessageById() throws Exception { ViewMessageRequestHeader viewMessageRequestHeader = new ViewMessageRequestHeader(); viewMessageRequestHeader.setTopic("topic"); viewMessageRequestHeader.setOffset(0L); @@ -107,11 +120,128 @@ public void testViewMessageById() throws RemotingCommandException { RemotingCommand response = queryMessageProcessor.processRequest(handlerContext, request); Assert.assertEquals(response.getCode(), ResponseCode.SYSTEM_ERROR); - when(messageStore.selectOneMessageByOffset(anyLong())).thenReturn(new SelectMappedBufferResult(0, null, 0, null)); + when(messageStore.selectOneMessageByOffset(anyLong())).thenReturn(messageResult("topic", null)); response = queryMessageProcessor.processRequest(handlerContext, request); Assert.assertNull(response); } + @Test + public void testViewMessageByIdAllowsMissingTopicForCompatibility() throws Exception { + ViewMessageRequestHeader header = new ViewMessageRequestHeader(); + header.setOffset(0L); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.VIEW_MESSAGE_BY_ID, header); + request.makeCustomHeaderToNet(); + when(messageStore.selectOneMessageByOffset(0L)).thenReturn( + new SelectMappedBufferResult(0L, ByteBuffer.allocate(Integer.BYTES), Integer.BYTES, null)); + + RemotingCommand response = queryMessageProcessor.processRequest(handlerContext, request); + + Assert.assertNull(response); + verify(messageStore).selectOneMessageByOffset(0L); + verify(channel).writeAndFlush(any()); + } + + @Test + public void testViewMessageByIdRejectsMismatchedTopic() throws Exception { + RemotingCommand request = createViewMessageRequest("allowedTopic"); + SelectMappedBufferResult result = spy(messageResult("actualTopic", null)); + when(messageStore.selectOneMessageByOffset(0L)).thenReturn(result); + + RemotingCommand response = queryMessageProcessor.processRequest(handlerContext, request); + + Assert.assertEquals(ResponseCode.NO_PERMISSION, response.getCode()); + verify(result).release(); + verify(channel, never()).writeAndFlush(any()); + } + + @Test + public void testViewMessageByIdRejectsForgedRealTopicOnNormalMessage() throws Exception { + RemotingCommand request = createViewMessageRequest("allowedTopic"); + SelectMappedBufferResult result = spy(messageResult("actualTopic", "allowedTopic")); + when(messageStore.selectOneMessageByOffset(0L)).thenReturn(result); + + RemotingCommand response = queryMessageProcessor.processRequest(handlerContext, request); + + Assert.assertEquals(ResponseCode.NO_PERMISSION, response.getCode()); + verify(result).release(); + verify(channel, never()).writeAndFlush(any()); + } + + @Test + public void testViewMessageByIdRejectsUndecodableMessage() throws Exception { + RemotingCommand request = createViewMessageRequest("topic"); + SelectMappedBufferResult result = spy( + new SelectMappedBufferResult(0L, ByteBuffer.allocate(Integer.BYTES), Integer.BYTES, null)); + when(messageStore.selectOneMessageByOffset(0L)).thenReturn(result); + + RemotingCommand response = queryMessageProcessor.processRequest(handlerContext, request); + + Assert.assertEquals(ResponseCode.SYSTEM_ERROR, response.getCode()); + verify(result).release(); + verify(channel, never()).writeAndFlush(any()); + } + + @Test + public void testViewMessageByIdMatchesLogicalTopicOfInternalMessages() throws Exception { + RemotingCommand request = createViewMessageRequest("actualTopic"); + + for (String storedTopic : Arrays.asList( + TopicValidator.RMQ_SYS_SCHEDULE_TOPIC, + TimerMessageStore.TIMER_TOPIC, + TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC, + TopicValidator.RMQ_SYS_TRANS_CHECK_MAX_TIME_TOPIC)) { + when(messageStore.selectOneMessageByOffset(0L)) + .thenReturn(messageResult(storedTopic, "actualTopic")); + Assert.assertNull(queryMessageProcessor.processRequest(handlerContext, request)); + } + + verify(channel, times(4)).writeAndFlush(any()); + } + + @Test + public void testViewMessageByIdMatchesLmqDispatchTopic() throws Exception { + RemotingCommand request = createViewMessageRequest("%LMQ%target"); + when(messageStore.selectOneMessageByOffset(0L)) + .thenReturn(messageResult("parentTopic", null, "%LMQ%other,%LMQ%target")); + + Assert.assertNull(queryMessageProcessor.processRequest(handlerContext, request)); + + verify(channel).writeAndFlush(any()); + } + + private RemotingCommand createViewMessageRequest(String topic) { + ViewMessageRequestHeader header = new ViewMessageRequestHeader(); + header.setTopic(topic); + header.setOffset(0L); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.VIEW_MESSAGE_BY_ID, header); + request.makeCustomHeaderToNet(); + return request; + } + + private SelectMappedBufferResult messageResult(String topic, String realTopic) throws Exception { + return messageResult(topic, realTopic, null); + } + + private SelectMappedBufferResult messageResult(String topic, String realTopic, String multiDispatch) + throws Exception { + MessageExt message = new MessageExt(); + message.setBody("body".getBytes(StandardCharsets.UTF_8)); + message.setTopic(topic); + message.setQueueId(0); + message.setQueueOffset(0L); + message.setCommitLogOffset(0L); + message.setBornHost(new InetSocketAddress("127.0.0.1", 10911)); + message.setStoreHost(new InetSocketAddress("127.0.0.1", 10911)); + if (realTopic != null) { + MessageAccessor.putProperty(message, MessageConst.PROPERTY_REAL_TOPIC, realTopic); + } + if (multiDispatch != null) { + MessageAccessor.putProperty(message, MessageConst.PROPERTY_INNER_MULTI_DISPATCH, multiDispatch); + } + ByteBuffer buffer = ByteBuffer.wrap(MessageDecoder.encode(message, false)); + return new SelectMappedBufferResult(0L, buffer, buffer.remaining(), null); + } + private RemotingCommand createQueryMessageRequest(String topic, String key, int maxNum, long beginTimestamp, long endTimestamp,String flag) { QueryMessageRequestHeader requestHeader = new QueryMessageRequestHeader(); requestHeader.setTopic(topic); diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/pipeline/AuthorizationPipeline.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/pipeline/AuthorizationPipeline.java index c0b33426da5..69a528d2f6c 100644 --- a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/pipeline/AuthorizationPipeline.java +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/pipeline/AuthorizationPipeline.java @@ -48,7 +48,7 @@ public void execute(ProxyContext context, Metadata headers, GeneratedMessageV3 r } try { List contexts = newContexts(context, headers, request); - authorizationEvaluator.evaluate(contexts); + authorizationEvaluator.evaluate(request, contexts); } catch (AuthorizationException | AuthenticationException ex) { throw ex; } catch (Throwable ex) { diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/RemotingProtocolServer.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/RemotingProtocolServer.java index c26f6bc2ef4..4f16a530c76 100644 --- a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/RemotingProtocolServer.java +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/RemotingProtocolServer.java @@ -62,6 +62,7 @@ import org.apache.rocketmq.remoting.netty.TlsSystemConfig; import org.apache.rocketmq.remoting.protocol.RemotingCommand; import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.RequestHeaderRegistry; import org.apache.rocketmq.remoting.protocol.ResponseCode; public class RemotingProtocolServer implements StartAndShutdown, RemotingProxyOutClient { @@ -239,6 +240,11 @@ protected void registerRemotingServer(RemotingServer remotingServer) { remotingServer.registerProcessor(RequestCode.UNLOCK_BATCH_MQ, consumerManagerActivity, this.defaultExecutor); remotingServer.registerProcessor(RequestCode.GET_ROUTEINFO_BY_TOPIC, getTopicRouteActivity, this.topicRouteExecutor); + + /* + * Initialize the mapping of request codes to request headers. + */ + RequestHeaderRegistry.getInstance().initialize(); } @Override diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/pipeline/AuthorizationPipeline.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/pipeline/AuthorizationPipeline.java index 49eb647ea53..8f770a13f8a 100644 --- a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/pipeline/AuthorizationPipeline.java +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/pipeline/AuthorizationPipeline.java @@ -49,7 +49,7 @@ public void execute(ChannelHandlerContext ctx, RemotingCommand request, ProxyCon } try { List contexts = newContexts(request, ctx, context); - authorizationEvaluator.evaluate(contexts); + authorizationEvaluator.evaluate(request, contexts); } catch (AuthorizationException | AuthenticationException ex) { throw ex; } catch (Throwable ex) { diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/pipeline/AuthorizationPipelineTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/pipeline/AuthorizationPipelineTest.java new file mode 100644 index 00000000000..15dddd36b3c --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/pipeline/AuthorizationPipelineTest.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.grpc.pipeline; + +import apache.rocketmq.v2.ClientType; +import apache.rocketmq.v2.HeartbeatRequest; +import apache.rocketmq.v2.TelemetryCommand; +import com.google.protobuf.GeneratedMessageV3; +import io.grpc.Metadata; +import java.util.Collections; +import java.util.List; +import org.apache.rocketmq.auth.authorization.context.AuthorizationContext; +import org.apache.rocketmq.auth.authorization.exception.AuthorizationException; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.junit.Assert; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.mock; + +public class AuthorizationPipelineTest { + + @Test + public void allowsCompatibleRequestWithEmptyContexts() { + AuthorizationPipeline pipeline = createPipeline(); + HeartbeatRequest request = HeartbeatRequest.newBuilder() + .setClientType(ClientType.PRODUCER) + .build(); + + assertThatCode(() -> pipeline.execute(ProxyContext.create(), new Metadata(), request)) + .doesNotThrowAnyException(); + } + + @Test + public void rejectsUnsupportedRequestWithEmptyContexts() { + AuthorizationPipeline pipeline = createPipeline(); + + Assert.assertThrows(AuthorizationException.class, + () -> pipeline.execute(ProxyContext.create(), new Metadata(), + TelemetryCommand.getDefaultInstance())); + } + + private AuthorizationPipeline createPipeline() { + AuthConfig authConfig = new AuthConfig(); + authConfig.setConfigName("grpc-authorization-pipeline-test"); + authConfig.setAuthorizationEnabled(true); + return new AuthorizationPipeline(authConfig, mock(MessagingProcessor.class)) { + @Override + protected List newContexts(ProxyContext context, Metadata headers, + GeneratedMessageV3 request) { + return Collections.emptyList(); + } + }; + } +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/RemotingProtocolServerTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/RemotingProtocolServerTest.java new file mode 100644 index 00000000000..acd9c1c2d44 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/RemotingProtocolServerTest.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting; + +import org.apache.rocketmq.remoting.RemotingServer; +import org.apache.rocketmq.remoting.protocol.RequestHeaderRegistry; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; + +public class RemotingProtocolServerTest { + + @Test + public void shouldInitializeRequestHeaderRegistryWhenRegisteringProcessors() { + RemotingProtocolServer protocolServer = + mock(RemotingProtocolServer.class, Mockito.CALLS_REAL_METHODS); + RemotingServer remotingServer = mock(RemotingServer.class); + RequestHeaderRegistry requestHeaderRegistry = mock(RequestHeaderRegistry.class); + + try (MockedStatic registry = mockStatic(RequestHeaderRegistry.class)) { + registry.when(RequestHeaderRegistry::getInstance).thenReturn(requestHeaderRegistry); + + protocolServer.registerRemotingServer(remotingServer); + + verify(requestHeaderRegistry).initialize(); + } + } +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/pipeline/AuthorizationPipelineTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/pipeline/AuthorizationPipelineTest.java new file mode 100644 index 00000000000..cc621630d1d --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/pipeline/AuthorizationPipelineTest.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.remoting.pipeline; + +import io.netty.channel.ChannelHandlerContext; +import java.util.Collections; +import java.util.List; +import org.apache.rocketmq.auth.authorization.context.AuthorizationContext; +import org.apache.rocketmq.auth.authorization.exception.AuthorizationException; +import org.apache.rocketmq.auth.config.AuthConfig; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.heartbeat.HeartbeatData; +import org.apache.rocketmq.remoting.protocol.heartbeat.ProducerData; +import org.junit.Assert; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.mock; + +public class AuthorizationPipelineTest { + + @Test + public void allowsCompatibleRequestWithEmptyContexts() throws Exception { + AuthorizationPipeline pipeline = createPipeline(); + ProducerData producerData = new ProducerData(); + producerData.setGroupName("producerGroup"); + HeartbeatData heartbeatData = new HeartbeatData(); + heartbeatData.getProducerDataSet().add(producerData); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.HEART_BEAT, null); + request.setBody(heartbeatData.encode()); + + assertThatCode(() -> pipeline.execute(null, request, ProxyContext.create())) + .doesNotThrowAnyException(); + } + + @Test + public void rejectsUnsupportedRequestWithEmptyContexts() { + AuthorizationPipeline pipeline = createPipeline(); + RemotingCommand request = RemotingCommand.createRequestCommand(-1, null); + + Assert.assertThrows(AuthorizationException.class, + () -> pipeline.execute(null, request, ProxyContext.create())); + } + + private AuthorizationPipeline createPipeline() { + AuthConfig authConfig = new AuthConfig(); + authConfig.setConfigName("remoting-authorization-pipeline-test"); + authConfig.setAuthorizationEnabled(true); + return new AuthorizationPipeline(authConfig, mock(MessagingProcessor.class)) { + @Override + protected List newContexts(RemotingCommand request, + ChannelHandlerContext ctx, ProxyContext context) { + return Collections.emptyList(); + } + }; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CreateTopicListRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CreateTopicListRequestHeader.java index 615de750c48..8a55077df7f 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CreateTopicListRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CreateTopicListRequestHeader.java @@ -16,13 +16,15 @@ */ package org.apache.rocketmq.remoting.protocol.header; -import org.apache.rocketmq.common.action.Action; -import org.apache.rocketmq.common.action.RocketMQAction; import org.apache.rocketmq.remoting.exception.RemotingCommandException; -import org.apache.rocketmq.remoting.protocol.RequestCode; import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; -@RocketMQAction(value = RequestCode.UPDATE_AND_CREATE_TOPIC_LIST, action = Action.CREATE) +/** + * Header metadata for a batch topic-create request. Topic names are carried in the request body. + * + *

This header must not declare {@code @RocketMQAction}: it has no concrete topic resource. + * Authorization resources are resolved from the request body by {@code DefaultAuthorizationContextBuilder}. + */ public class CreateTopicListRequestHeader extends RpcRequestHeader { @Override public void checkFields() throws RemotingCommandException { diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetAllSubscriptionGroupRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetAllSubscriptionGroupRequestHeader.java index 6d67afdb9c0..c49537ba88d 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetAllSubscriptionGroupRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetAllSubscriptionGroupRequestHeader.java @@ -17,15 +17,16 @@ package org.apache.rocketmq.remoting.protocol.header; -import org.apache.rocketmq.common.action.Action; -import org.apache.rocketmq.common.action.RocketMQAction; -import org.apache.rocketmq.common.resource.ResourceType; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.exception.RemotingCommandException; -import org.apache.rocketmq.remoting.protocol.RequestCode; -@RocketMQAction(value = RequestCode.GET_ALL_SUBSCRIPTIONGROUP_CONFIG, resource = ResourceType.GROUP, action = Action.GET) +/** + * This request targets the complete subscription-group configuration set and does not identify a single group. + * + *

This header must not declare {@code @RocketMQAction}: it carries no resource fields, and the + * typed {@code Group:ANY + LIST} resource is constructed by {@code DefaultAuthorizationContextBuilder}. + */ public class GetAllSubscriptionGroupRequestHeader implements CommandCustomHeader { @Override public void checkFields() throws RemotingCommandException { diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetAllSubscriptionGroupResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetAllSubscriptionGroupResponseHeader.java index 8f42a1b2a8b..ad079197b31 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetAllSubscriptionGroupResponseHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetAllSubscriptionGroupResponseHeader.java @@ -17,15 +17,10 @@ package org.apache.rocketmq.remoting.protocol.header; -import org.apache.rocketmq.common.action.Action; -import org.apache.rocketmq.common.action.RocketMQAction; -import org.apache.rocketmq.common.resource.ResourceType; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.exception.RemotingCommandException; -import org.apache.rocketmq.remoting.protocol.RequestCode; -@RocketMQAction(value = RequestCode.GET_ALL_SUBSCRIPTIONGROUP_CONFIG, resource = ResourceType.GROUP, action = Action.LIST) public class GetAllSubscriptionGroupResponseHeader implements CommandCustomHeader { @Override public void checkFields() throws RemotingCommandException { diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetAllTopicConfigRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetAllTopicConfigRequestHeader.java index 769a814d34f..c1113ae0c81 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetAllTopicConfigRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetAllTopicConfigRequestHeader.java @@ -17,15 +17,16 @@ package org.apache.rocketmq.remoting.protocol.header; -import org.apache.rocketmq.common.action.Action; -import org.apache.rocketmq.common.action.RocketMQAction; -import org.apache.rocketmq.common.resource.ResourceType; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.exception.RemotingCommandException; -import org.apache.rocketmq.remoting.protocol.RequestCode; -@RocketMQAction(value = RequestCode.GET_ALL_TOPIC_CONFIG, resource = ResourceType.TOPIC, action = Action.GET) +/** + * This request targets the complete topic configuration set and does not identify a single topic. + * + *

This header must not declare {@code @RocketMQAction}: it carries no resource fields, and the + * typed {@code Topic:ANY + LIST} resource is constructed by {@code DefaultAuthorizationContextBuilder}. + */ public class GetAllTopicConfigRequestHeader implements CommandCustomHeader { @Override public void checkFields() throws RemotingCommandException { diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetAllTopicConfigResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetAllTopicConfigResponseHeader.java index 9f514070c8a..e116b344c63 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetAllTopicConfigResponseHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetAllTopicConfigResponseHeader.java @@ -20,14 +20,9 @@ */ package org.apache.rocketmq.remoting.protocol.header; -import org.apache.rocketmq.common.action.Action; -import org.apache.rocketmq.common.action.RocketMQAction; -import org.apache.rocketmq.common.resource.ResourceType; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.exception.RemotingCommandException; -import org.apache.rocketmq.remoting.protocol.RequestCode; -@RocketMQAction(value = RequestCode.GET_ALL_TOPIC_CONFIG, resource = ResourceType.TOPIC, action = Action.LIST) public class GetAllTopicConfigResponseHeader implements CommandCustomHeader { @Override diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetLiteClientInfoRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetLiteClientInfoRequestHeader.java index 9b0f99c67f8..f3b97c4550e 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetLiteClientInfoRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetLiteClientInfoRequestHeader.java @@ -17,12 +17,24 @@ package org.apache.rocketmq.remoting.protocol.header; +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +@RocketMQAction(value = RequestCode.GET_LITE_CLIENT_INFO, action = Action.GET) public class GetLiteClientInfoRequestHeader implements CommandCustomHeader { + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) private String parentTopic; + + @CFNotNull + @RocketMQResource(ResourceType.GROUP) private String group; private String clientId; private int maxCount = 1000; diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetLiteGroupInfoRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetLiteGroupInfoRequestHeader.java index db1dbd2a4c3..a770eb43591 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetLiteGroupInfoRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetLiteGroupInfoRequestHeader.java @@ -16,12 +16,16 @@ */ package org.apache.rocketmq.remoting.protocol.header; +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; import org.apache.rocketmq.common.resource.ResourceType; import org.apache.rocketmq.common.resource.RocketMQResource; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +@RocketMQAction(value = RequestCode.GET_LITE_GROUP_INFO, action = Action.GET) public class GetLiteGroupInfoRequestHeader implements CommandCustomHeader { @CFNotNull diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetLiteTopicInfoRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetLiteTopicInfoRequestHeader.java index bdb9c8408f3..00a582b958d 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetLiteTopicInfoRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetLiteTopicInfoRequestHeader.java @@ -17,11 +17,20 @@ package org.apache.rocketmq.remoting.protocol.header; +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; +import org.apache.rocketmq.common.resource.ResourceType; +import org.apache.rocketmq.common.resource.RocketMQResource; import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +@RocketMQAction(value = RequestCode.GET_LITE_TOPIC_INFO, action = Action.GET) public class GetLiteTopicInfoRequestHeader implements CommandCustomHeader { + @CFNotNull + @RocketMQResource(ResourceType.TOPIC) private String parentTopic; private String liteTopic; diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetMaxOffsetRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetMaxOffsetRequestHeader.java index 68b36a24056..f5b1fa7c598 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetMaxOffsetRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetMaxOffsetRequestHeader.java @@ -31,7 +31,7 @@ import org.apache.rocketmq.remoting.protocol.RequestCode; import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; -@RocketMQAction(value = RequestCode.GET_MAX_OFFSET, action = Action.GET) +@RocketMQAction(value = RequestCode.GET_MAX_OFFSET, action = {Action.SUB, Action.GET}) public class GetMaxOffsetRequestHeader extends TopicQueueRequestHeader { @CFNotNull @RocketMQResource(ResourceType.TOPIC) diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetParentTopicInfoRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetParentTopicInfoRequestHeader.java index 885e9cb66b7..0a8391089e2 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetParentTopicInfoRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetParentTopicInfoRequestHeader.java @@ -17,12 +17,16 @@ package org.apache.rocketmq.remoting.protocol.header; +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; import org.apache.rocketmq.common.resource.ResourceType; import org.apache.rocketmq.common.resource.RocketMQResource; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +@RocketMQAction(value = RequestCode.GET_PARENT_TOPIC_INFO, action = Action.GET) public class GetParentTopicInfoRequestHeader implements CommandCustomHeader { @CFNotNull diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/PopLiteMessageRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/PopLiteMessageRequestHeader.java index e42606f3148..a16723a312e 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/PopLiteMessageRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/PopLiteMessageRequestHeader.java @@ -18,12 +18,16 @@ package org.apache.rocketmq.remoting.protocol.header; import com.google.common.base.MoreObjects; +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; import org.apache.rocketmq.common.resource.ResourceType; import org.apache.rocketmq.common.resource.RocketMQResource; import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; +@RocketMQAction(value = RequestCode.POP_LITE_MESSAGE, action = Action.SUB) public class PopLiteMessageRequestHeader extends RpcRequestHeader { @CFNotNull diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/SearchOffsetRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/SearchOffsetRequestHeader.java index c66098159eb..bb1340e35b6 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/SearchOffsetRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/SearchOffsetRequestHeader.java @@ -31,7 +31,7 @@ import org.apache.rocketmq.remoting.protocol.RequestCode; import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; -@RocketMQAction(value = RequestCode.SEARCH_OFFSET_BY_TIMESTAMP, action = Action.GET) +@RocketMQAction(value = RequestCode.SEARCH_OFFSET_BY_TIMESTAMP, action = {Action.SUB, Action.GET}) public class SearchOffsetRequestHeader extends TopicQueueRequestHeader { @CFNotNull @RocketMQResource(ResourceType.TOPIC) diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/TriggerLiteDispatchRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/TriggerLiteDispatchRequestHeader.java index e39725e2074..4772f2300d3 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/TriggerLiteDispatchRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/TriggerLiteDispatchRequestHeader.java @@ -16,12 +16,16 @@ */ package org.apache.rocketmq.remoting.protocol.header; +import org.apache.rocketmq.common.action.Action; +import org.apache.rocketmq.common.action.RocketMQAction; import org.apache.rocketmq.common.resource.ResourceType; import org.apache.rocketmq.common.resource.RocketMQResource; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RequestCode; +@RocketMQAction(value = RequestCode.TRIGGER_LITE_DISPATCH, action = Action.UPDATE) public class TriggerLiteDispatchRequestHeader implements CommandCustomHeader { @CFNotNull