RocketMQ privilege control

Posted by kinadian on Tue, 06 Aug 2019 05:21:12 +0200

RocketMQ, as an excellent middleware, has a wide range of applications. There are large-scale applications in different fields such as finance, e-commerce, telecommunications, medical, social science, security and so on. There is no doubt that the security is very questionable, because there is no security-related business module in RocketMQ, and the sending and consumption of messages can not be well controlled. It requires the business side to encapsulate the security module itself, which increases the cost of use. ACL privilege control has been added to the upgrade of RocketMQ 4.4.0. The improvement of this function directly promotes the promotion and use of RocketMQ in various fields, especially in areas with high security requirements such as finance, e-commerce and security.

1. Simple Use

1.1. What is ACL?

ACL is short for access control list, commonly known as access control list. Access control basically involves the concepts of users, resources, privileges, roles and so on. What objects do the above-mentioned objects correspond to in RocketMQ?

User: User is the basic element of access control, RocketMQ ACL will inevitably introduce the concept of user, that is, support user name and password. Resources: The objects to be protected, Topic involved in message sending and consumption groups involved in message consumption should be protected, so they can be abstracted into resources. Privileges: Operations that can be performed for resources. Roles: In RocketMQ, only two roles are defined: whether they are administrators or not.

1.2. Configuring ACL in RocketMQ

Acl default configuration file name: plain_acl.yml, which needs to be placed in the ${ROCKETMQ_HOME}/store/config directory

To use acl, you must turn on this function on the server side, configure it in Broker's configuration file, and aclEnable = true turns on this function

Configure plain_acl.yml file

globalWhiteRemoteAddresses:
- 10.10.15.*
- 192.168.0.*

accounts:
- accessKey: RocketMQ
  secretKey: 12345678
  whiteRemoteAddress:
  admin: false
  defaultTopicPerm: DENY
  defaultGroupPerm: SUB
  topicPerms:
  - topicA=DENY
  - topicB=PUB|SUB
  - topicC=SUB
  groupPerms:
  # the group should convert to retry topic
  - groupA=DENY
  - groupB=PUB|SUB
  - groupC=SUB

- accessKey: rocketmq2
  secretKey: 12345678
  whiteRemoteAddress: 192.168.1.*
  # if it is admin, it could access all resources
  admin: true

Let's introduce the meaning and use of the parameters in plain_acl.yml file.

field Value Meaning
globalWhiteRemoteAddresses *;192.168.*.*;192.168.0.1 Global IP Whitelist
accessKey Character string Access Key User Name
secretKey Character string Secret Key password
whiteRemoteAddress *;192.168.*.*;192.168.0.1 User IP Whitelist
admin true;false Administrator Account
defaultTopicPerm DENY;PUB;SUB;PUB|SUB Default Topic permissions
defaultGroupPerm DENY;PUB;SUB;PUB|SUB Default ConstumerGroup permissions
topicPerms topic = permission Permissions of each Topic
groupPerms group = permission Permissions of individual Consumer Groups

The Meaning of Permission Identifier

Jurisdiction Meaning
DENY refuse
ANY PUB or SUB permissions
PUB Send permission
SUB Subscription rights

Processing flow

Special requests such as UPDATE_AND_CREATE_TOPIC can only be operated by admin accounts.

For a resource, if there are explicit configuration privileges, the configuration privileges are used; if there are no explicit configuration privileges, the default privileges are used.

The default implementation of RocketMQ's permission control storage is based on the yml configuration file. Users can dynamically modify the properties defined by permission control without restarting Broker service nodes

If ACL is enabled at the same time as High Availability Deployment (Master/Slave Architecture), then global whitelist information needs to be set in the ${ROCKETMQ_HOME}/store/conf/plain_acl.yml configuration file of Broker Master node, that is, to set the ip address of Slave node to the global whitelist of Master node plain_acl.yml configuration file.

1.3. Code examples

1.3.1, Producer Code

public class AclProducer {
    public static void main(String[] args) throws MQClientException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name", getAclRPCHook());
        producer.setNamesrvAddr("10.10.15.246:9876;10.10.15.247:9876");
        producer.start();
        for (int i = 0; i < 10; i++) {
            try {
                Message msg = new Message("topicA" ,"TagA" , ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
                SendResult sendResult = producer.send(msg);
                System.out.printf("%s%n", sendResult);
            } catch (Exception e) {
                e.printStackTrace();
                Thread.sleep(1000);
            }
        }
        producer.shutdown();
    }

    static RPCHook getAclRPCHook() {
        return new AclClientRPCHook(new SessionCredentials("RocketMQ","12345678"));
    }
}

View the results

Error report indicates that topicA does not have permission. What we configure in plain_acl.yml file is RocketMQ user rejection, production and consumption topicA topic information. If we change the theme to topicB, we find that the message is sent successfully. topicB=PUB|SUB sets permission is production and consumption.

View the results

1.3.2, Consumer Code

public class AclConsumer {

    public static void main(String[] args) throws InterruptedException, MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("groupA", getAclRPCHook(),new AllocateMessageQueueAveragely());
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        consumer.subscribe("topicB", "*");
        consumer.setNamesrvAddr("10.10.15.246:9876;10.10.15.247:9876");
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                ConsumeConcurrentlyContext context) {
                System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }

    static RPCHook getAclRPCHook() {
        return new AclClientRPCHook(new SessionCredentials("RocketMQ","12345678"));
    }
}

View results: We found that no message was consumed and no error message was reported. For RocketMQ users topicB settings, it can be produced and consumed, but we found that its groupA=DENY is rejected, indicating that the consumer group is groupA and refuses to consume any message. We changed to groupB or groupC to view the results.

2. Source code analysis

Broker ACL schematic

2.1. ACL-related operations during Broker initialization

Initial acl () is called when the Broker service is created at startup and initialize.

private void initialAcl() {
	//Whether ACL function is turned on in broker configuration file and turned off by default
    if (!this.brokerConfig.isAclEnable()) {
        log.info("The broker dose not enable acl");
        return;
    }
    //Get a list of access validators and point to the loaded META-INF/service/org.apache.rocketmq.acl.AccessValidator file
    //org.apache.rocketmq.acl.plain.PlainAccessValidator, default only one
    List<AccessValidator> accessValidators = ServiceProvider.load(ServiceProvider.ACL_VALIDATOR_ID, AccessValidator.class);
    if (accessValidators == null || accessValidators.isEmpty()) {
        log.info("The broker dose not load the AccessValidator");
        return;
    }
    for (AccessValidator accessValidator: accessValidators) {
        final AccessValidator validator = accessValidator;
        //Register the "hook" object on the server and verify the privileges
        this.registerServerRPCHook(new RPCHook() {
            @Override
            public void doBeforeRequest(String remoteAddr, RemotingCommand request) {
                //Do not catch the exception
                validator.validate(validator.parse(request, remoteAddr));
            }
            @Override
            public void doAfterResponse(String remoteAddr, RemotingCommand request, RemotingCommand response) {
            }
        });
    }
}

There are annotations in the source code. Let's take a look at the registerServer RPCHook method

public void registerServerRPCHook(RPCHook rpcHook) {
	//Service-side Netty Remoting Server Service Registration "hook" function
    getRemotingServer().registerRPCHook(rpcHook);
    this.fastRemotingServer.registerRPCHook(rpcHook);
}

With regard to the use of Netty Remoting Server and Netty Remoting Client services, RocketMQ Remoting will focus on the following chapters

2.2. Plain Access Validator Privilege Verifier

PlainAccessValidator.parse(), which requires different inspection resources depending on the client's request for Code

switch (request.getCode()) {
	//Sending a message requires verifying that the top of the current account has PUB privileges
    case RequestCode.SEND_MESSAGE:
        accessResource.addResourceAndPerm(request.getExtFields().get("topic"), Permission.PUB);
        break;
    case RequestCode.SEND_MESSAGE_V2:
        accessResource.addResourceAndPerm(request.getExtFields().get("b"), Permission.PUB);
        break;
    case RequestCode.CONSUMER_SEND_MSG_BACK:
        accessResource.addResourceAndPerm(request.getExtFields().get("originTopic"), Permission.PUB);
        accessResource.addResourceAndPerm(getRetryTopic(request.getExtFields().get("group")), Permission.SUB);
        break;
    //When pulling out interest, you need to know whether the top pulled down by the consumer account has SUB privileges, and whether the subscription group consumerGroup has subprivileges.
    case RequestCode.PULL_MESSAGE:
        accessResource.addResourceAndPerm(request.getExtFields().get("topic"), Permission.SUB);
        accessResource.addResourceAndPerm(getRetryTopic(request.getExtFields().get("consumerGroup")), Permission.SUB);
        break;
    case RequestCode.QUERY_MESSAGE:
        accessResource.addResourceAndPerm(request.getExtFields().get("topic"), Permission.SUB);
        break;
    case RequestCode.HEART_BEAT:
        HeartbeatData heartbeatData = HeartbeatData.decode(request.getBody(), HeartbeatData.class);
        for (ConsumerData data : heartbeatData.getConsumerDataSet()) {
            accessResource.addResourceAndPerm(getRetryTopic(data.getGroupName()), Permission.SUB);
            for (SubscriptionData subscriptionData : data.getSubscriptionDataSet()) {
                accessResource.addResourceAndPerm(subscriptionData.getTopic(), Permission.SUB);
            }
        }
        break;
    case RequestCode.UNREGISTER_CLIENT:
        final UnregisterClientRequestHeader unregisterClientRequestHeader =
            (UnregisterClientRequestHeader) request
                .decodeCommandCustomHeader(UnregisterClientRequestHeader.class);
        accessResource.addResourceAndPerm(getRetryTopic(unregisterClientRequestHeader.getConsumerGroup()), Permission.SUB);
        break;
    case RequestCode.GET_CONSUMER_LIST_BY_GROUP:
        final GetConsumerListByGroupRequestHeader getConsumerListByGroupRequestHeader =
            (GetConsumerListByGroupRequestHeader) request
                .decodeCommandCustomHeader(GetConsumerListByGroupRequestHeader.class);
        accessResource.addResourceAndPerm(getRetryTopic(getConsumerListByGroupRequestHeader.getConsumerGroup()), Permission.SUB);
        break;
    case RequestCode.UPDATE_CONSUMER_OFFSET:
        final UpdateConsumerOffsetRequestHeader updateConsumerOffsetRequestHeader =
            (UpdateConsumerOffsetRequestHeader) request
                .decodeCommandCustomHeader(UpdateConsumerOffsetRequestHeader.class);
        accessResource.addResourceAndPerm(getRetryTopic(updateConsumerOffsetRequestHeader.getConsumerGroup()), Permission.SUB);
        accessResource.addResourceAndPerm(updateConsumerOffsetRequestHeader.getTopic(), Permission.SUB);
        break;
    default:
        break;

}

Get the set of privilege identifiers required by the current operation according to request.getCode() for later verification of the privilege identifiers in the system's privilege configuration file plain_acl.yml

2.3. Plain Permission Loader Resource Loader

Broker created Plain Access Validator when initializing related services, and we found that its permission resource loader Plain Permission Loader was invoked in its default constructor.

public PlainAccessValidator() {
    aclPlugEngine = new PlainPermissionLoader();
}

Create PlainPermissionLoader objects

public PlainPermissionLoader() {
	//Load the server-side permission file plain_acl.yml
    load();
    //Open threads check permission files every 500 ms to see if they have changed. If they have changed, load () is executed to load new permission files.
    watch();
}

View the load method flow

public void load() {
    Map<String, PlainAccessResource> plainAccessResourceMap = new HashMap<>();
    List<RemoteAddressStrategy> globalWhiteRemoteAddressStrategy = new ArrayList<>();

    JSONObject plainAclConfData = AclUtils.getYamlDataObject(fileHome + File.separator + fileName,
        JSONObject.class);

    if (plainAclConfData == null || plainAclConfData.isEmpty()) {
        throw new AclException(String.format("%s file  is not data", fileHome + File.separator + fileName));
    }
    log.info("Broker plain acl conf data is : ", plainAclConfData.toString());
    //Get the global whitelist IP set
    JSONArray globalWhiteRemoteAddressesList = plainAclConfData.getJSONArray("globalWhiteRemoteAddresses");
    if (globalWhiteRemoteAddressesList != null && !globalWhiteRemoteAddressesList.isEmpty()) {
        for (int i = 0; i < globalWhiteRemoteAddressesList.size(); i++) {
            globalWhiteRemoteAddressStrategy.add(remoteAddressStrategyFactory.
                    getRemoteAddressStrategy(globalWhiteRemoteAddressesList.getString(i)));
        }
    }
    //Get the set of account permissions
    JSONArray accounts = plainAclConfData.getJSONArray("accounts");
    if (accounts != null && !accounts.isEmpty()) {
        List<PlainAccessConfig> plainAccessConfigList = accounts.toJavaList(PlainAccessConfig.class);
        for (PlainAccessConfig plainAccessConfig : plainAccessConfigList) {
        	//Building permission resources for each account
            PlainAccessResource plainAccessResource = buildPlainAccessResource(plainAccessConfig);
            //Put AccessKey in Map as key and the permission resource of the account as value
            plainAccessResourceMap.put(plainAccessResource.getAccessKey(),plainAccessResource);
        }
    }
    this.globalWhiteRemoteAddressStrategy = globalWhiteRemoteAddressStrategy;
    this.plainAccessResourceMap = plainAccessResourceMap;
}

Load the resource file, parse the privilege identity, and wait for the privilege verifier PlainAccessValidator to call its validate() to verify the privilege

2.4. Privilege Checking Process

The core verification method Plain PermissionLoader. validate ()

public void validate(PlainAccessResource plainAccessResource) {

    //Global White List IP Check
    for (RemoteAddressStrategy remoteAddressStrategy : globalWhiteRemoteAddressStrategy) {
        //The matching success statement is a global whitelist IP with all privileges and returns directly.
    	if (remoteAddressStrategy.match(plainAccessResource)) {
            return;
        }
    }
    //To determine whether the username is empty, null throws an AclException exception
    if (plainAccessResource.getAccessKey() == null) {
        throw new AclException(String.format("No accessKey is configured"));
    }
    //Check whether the account exists in plain_acl.yml in the server-side permission resource file and throw an exception if not
    if (!plainAccessResourceMap.containsKey(plainAccessResource.getAccessKey())) {
        throw new AclException(String.format("No acl config for %s", plainAccessResource.getAccessKey()));
    }
    PlainAccessResource ownedAccess = plainAccessResourceMap.get(plainAccessResource.getAccessKey());
    //Check whether the whitelist IP of this account matches the upper client IP. The matching is successful with all permissions. Administrator permissions are required except for special permissions such as UPDATE_AND_CREATE_TOPIC.
    if (ownedAccess.getRemoteAddressStrategy().match(plainAccessResource)) {
        return;
    }
    //Check signature
    String signature = AclUtils.calSignature(plainAccessResource.getContent(), ownedAccess.getSecretKey());
    if (!signature.equals(plainAccessResource.getSignature())) {
        throw new AclException(String.format("Check signature failed for accessKey=%s", plainAccessResource.getAccessKey()));
    }
    //Check resource permissions in accounts
    checkPerm(plainAccessResource, ownedAccess);
}

View its resource validation within the current account

void checkPerm(PlainAccessResource needCheckedAccess, PlainAccessResource ownedAccess) {
	//Determine whether the Code of the requested command requires administrator privileges and whether the user is an administrator
    if (Permission.needAdminPerm(needCheckedAccess.getRequestCode()) && !ownedAccess.isAdmin()) {
        throw new AclException(String.format("Need admin permission for request code=%d, but accessKey=%s is not", needCheckedAccess.getRequestCode(), ownedAccess.getAccessKey()));
    }
    Map<String, Byte> needCheckedPermMap = needCheckedAccess.getResourcePermMap();
    Map<String, Byte> ownedPermMap = ownedAccess.getResourcePermMap();

    if (needCheckedPermMap == null) {
        // If the needCheckedPermMap is null,then return
        return;
    }
    for (Map.Entry<String, Byte> needCheckedEntry : needCheckedPermMap.entrySet()) {
        String resource = needCheckedEntry.getKey();
        Byte neededPerm = needCheckedEntry.getValue();
        //To determine whether it is a group, when building resourcePermMap, the key of the group = RETRY_GROUP_TOPIC_PREFIX + consumerGroup
        boolean isGroup = PlainAccessResource.isRetryTopic(resource);
        //The configuration item package in the permission profile of the system does not contain the permissions required for the client command request
        if (!ownedPermMap.containsKey(resource)) {
            //Determine whether it is a top or group privilege identifier, and what is the global privilege of this type?
            byte ownedPerm = isGroup ? needCheckedAccess.getDefaultGroupPerm() :
                needCheckedAccess.getDefaultTopicPerm();
            //Check permissions
            if (!Permission.checkPermission(neededPerm, ownedPerm)) {
                throw new AclException(String.format("No default permission for %s", PlainAccessResource.printStr(resource, isGroup)));
            }
            continue;
        }
        //If the configuration item in the permission profile of the system contains the permissions required by the client's command request, the permissions will be judged directly.
        if (!Permission.checkPermission(neededPerm, ownedPermMap.get(resource))) {
            throw new AclException(String.format("No default permission for %s", PlainAccessResource.printStr(resource, isGroup)));
        }
    }
}

All inspection processes throw AclException exceptions if one is not satisfied

2.5. Client sends request

The above figure only analyses the process flow of Broker server, and how the client calls us for specific analysis. Let's take sending a message as an example.

The core method of message sending that we have analyzed before is DefaultMQ Producer Impl. sendKernelImpl () method.

//Are "hooks" registered?
if (this.hasSendMessageHook()) {
    context = new SendMessageContext();
    context.setProducer(this);
    context.setProducerGroup(this.defaultMQProducer.getProducerGroup());
    context.setCommunicationMode(communicationMode);
    context.setBornHost(this.defaultMQProducer.getClientIP());
    context.setBrokerAddr(brokerAddr);
    context.setMessage(msg);
    context.setMq(mq);
    String isTrans = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED);
    if (isTrans != null && isTrans.equals("true")) {
        context.setMsgType(MessageType.Trans_Msg_Half);
    }

    if (msg.getProperty("__STARTDELIVERTIME") != null || msg.getProperty(MessageConst.PROPERTY_DELAY_TIME_LEVEL) != null) {
        context.setMsgType(MessageType.Delay_Msg);
    }
    //Encapsulate the parameter information of its ACL request
    this.executeSendMessageHookBefore(context);
}

hasSendMessageHook(), which we created when we built the Producer, is added to the sendMessageHookList attribute of DefaultMQ Producer Impl.

Let's look at the data preparation before calling AclClientRPCHook.doBeforeRequest() in its sending message NettyRemotingClient class

public void doBeforeRequest(String remoteAddr, RemotingCommand request) {
    byte[] total = AclUtils.combineRequestContent(request,
        parseRequestContent(request, sessionCredentials.getAccessKey(), sessionCredentials.getSecurityToken()));
    String signature = AclUtils.calSignature(total, sessionCredentials.getSecretKey());
    request.addExtField(SIGNATURE, signature);
    request.addExtField(ACCESS_KEY, sessionCredentials.getAccessKey());
    
    // The SecurityToken value is unneccessary,user can choose this one.
    if (sessionCredentials.getSecurityToken() != null) {
        request.addExtField(SECURITY_TOKEN, sessionCredentials.getSecurityToken());
    }
}

Just build signature s and Token s, ready to change data for Broker-side authentication.

Topics: Programming Netty Apache Attribute