diff --git a/ChangeLog.txt b/ChangeLog.txt index d2400ebf8a..f2b96a089a 100644 --- a/ChangeLog.txt +++ b/ChangeLog.txt @@ -25,8 +25,8 @@ Broker: corresponding PUBACK/PUBREC. - MOSQ_EVT_TICK is now passed to plugins when `per_listener_settings` is true. - The dynamic security plugin now supports `%c` and `%u` patterns for - substituting client id and username respectively, in publishClientSend and - publishClientReceive ACLs. + substituting client id and username respectively, in all ACLs except for + subscribeLiteral and unsubscribeLiteral. - Add `mosquitto_topic_matches_sub_with_pattern()`, which can match against subscriptions with `%c` and `%u` patterns for client id / username substitution. diff --git a/plugins/dynamic-security/acl.c b/plugins/dynamic-security/acl.c index 301bf0b2df..225bcea05b 100644 --- a/plugins/dynamic-security/acl.c +++ b/plugins/dynamic-security/acl.c @@ -103,9 +103,13 @@ static int acl_check_subscribe(struct mosquitto_evt_acl_check *ed, struct dynsec struct dynsec__acl *acl, *acl_tmp = NULL; size_t len; bool result; + const char *clientid, *username; len = strlen(ed->topic); + clientid = mosquitto_client_id(ed->client); + username = mosquitto_client_username(ed->client); + HASH_ITER(hh, base_rolelist, rolelist, rolelist_tmp){ HASH_FIND(hh, rolelist->role->acls.subscribe_literal, ed->topic, len, acl); if(acl){ @@ -116,7 +120,7 @@ static int acl_check_subscribe(struct mosquitto_evt_acl_check *ed, struct dynsec } } HASH_ITER(hh, rolelist->role->acls.subscribe_pattern, acl, acl_tmp){ - mosquitto_sub_matches_acl(acl->topic, ed->topic, &result); + mosquitto_sub_matches_acl_with_pattern(acl->topic, ed->topic, clientid, username, &result); if(result){ if(acl->allow){ return MOSQ_ERR_SUCCESS; @@ -142,9 +146,13 @@ static int acl_check_unsubscribe(struct mosquitto_evt_acl_check *ed, struct dyns struct dynsec__acl *acl, *acl_tmp = NULL; size_t len; bool result; + const char *clientid, *username; len = strlen(ed->topic); + clientid = mosquitto_client_id(ed->client); + username = mosquitto_client_username(ed->client); + HASH_ITER(hh, base_rolelist, rolelist, rolelist_tmp){ HASH_FIND(hh, rolelist->role->acls.unsubscribe_literal, ed->topic, len, acl); if(acl){ @@ -155,7 +163,7 @@ static int acl_check_unsubscribe(struct mosquitto_evt_acl_check *ed, struct dyns } } HASH_ITER(hh, rolelist->role->acls.unsubscribe_pattern, acl, acl_tmp){ - mosquitto_sub_matches_acl(acl->topic, ed->topic, &result); + mosquitto_sub_matches_acl_with_pattern(acl->topic, ed->topic, clientid, username, &result); if(result){ if(acl->allow){ return MOSQ_ERR_SUCCESS; diff --git a/test/broker/14-dynsec-acl.py b/test/broker/14-dynsec-acl.py index 7bb5500b17..07f4580e34 100755 --- a/test/broker/14-dynsec-acl.py +++ b/test/broker/14-dynsec-acl.py @@ -45,6 +45,10 @@ def command_check(sock, command_payload, expected_response): { "command": "addRoleACL", "rolename": "myrole", "acltype": "subscribePattern", "topic": "multilevel-wildcard/#", "allow": True }, { "command": "addRoleACL", "rolename": "myrole", "acltype": "unsubscribeLiteral", "topic": "simple/topic", "allow": False }, { "command": "addRoleACL", "rolename": "myrole", "acltype": "subscribePattern", "topic": "pattern/#", "allow": True }, + { "command": "addRoleACL", "rolename": "myrole", "acltype": "subscribePattern", "topic": "subscribe/pattern/c/%c/allowed", "allow": True }, + { "command": "addRoleACL", "rolename": "myrole", "acltype": "subscribePattern", "topic": "subscribe/pattern/c/%c/denied", "allow": False }, + { "command": "addRoleACL", "rolename": "myrole", "acltype": "subscribePattern", "topic": "subscribe/pattern/u/%u/allowed", "allow": True }, + { "command": "addRoleACL", "rolename": "myrole", "acltype": "subscribePattern", "topic": "subscribe/pattern/u/%u/denied", "allow": False }, { "command": "addGroupClient", "groupname": "mygroup", "username": "user_one" } ]} @@ -54,6 +58,8 @@ def command_check(sock, command_payload, expected_response): {'command': 'addGroupRole'}, {'command': 'addRoleACL'}, {'command': 'addRoleACL'}, {'command': 'addRoleACL'}, {'command': 'addRoleACL'}, + {'command': 'addRoleACL'}, {'command': 'addRoleACL'}, + {'command': 'addRoleACL'}, {'command': 'addRoleACL'}, {'command': 'addRoleACL'}, {'command': 'addGroupClient'} ]} @@ -63,16 +69,16 @@ def command_check(sock, command_payload, expected_response): { "command": "addRoleACL", "rolename": "myrole", "acltype": "publishClientSend", "topic": "single-wildcard/deny/deny", "priority":10, "allow": False }, { "command": "addRoleACL", "rolename": "myrole", "acltype": "publishClientSend", "topic": "single-wildcard/+/+", "allow": True }, { "command": "addRoleACL", "rolename": "myrole", "acltype": "publishClientSend", "topic": "multilevel-wildcard/topic/#", "allow": True }, - { "command": "addRoleACL", "rolename": "myrole", "acltype": "publishClientSend", "topic": "pattern/%u/topic/#", "allow": True }, - { "command": "addRoleACL", "rolename": "myrole", "acltype": "publishClientSend", "topic": "pattern/%u/denied", "allow": False }, - { "command": "addRoleACL", "rolename": "myrole", "acltype": "publishClientSend", "topic": "pattern/%c/topic/#", "allow": True }, - { "command": "addRoleACL", "rolename": "myrole", "acltype": "publishClientSend", "topic": "pattern/%c/denied", "allow": False }, + { "command": "addRoleACL", "rolename": "myrole", "acltype": "publishClientSend", "topic": "pattern/u/%u/topic/#", "allow": True }, + { "command": "addRoleACL", "rolename": "myrole", "acltype": "publishClientSend", "topic": "pattern/u/%u/denied", "allow": False }, + { "command": "addRoleACL", "rolename": "myrole", "acltype": "publishClientSend", "topic": "pattern/c/%c/topic/#", "allow": True }, + { "command": "addRoleACL", "rolename": "myrole", "acltype": "publishClientSend", "topic": "pattern/c/%c/denied", "allow": False }, { "command": "addRoleACL", "rolename": "myrole", "acltype": "publishClientReceive", "topic": "single-wildcard/bob/bob", "allow": False }, { "command": "addRoleACL", "rolename": "myrole", "acltype": "publishClientReceive", "topic": "multilevel-wildcard/topic/topic/denied", "allow": False }, - { "command": "addRoleACL", "rolename": "myrole", "acltype": "publishClientReceive", "topic": "pattern/%u/topic/#", "allow": True }, - { "command": "addRoleACL", "rolename": "myrole", "acltype": "publishClientReceive", "topic": "pattern/%u/denied", "allow": False }, - { "command": "addRoleACL", "rolename": "myrole", "acltype": "publishClientReceive", "topic": "pattern/%c/topic/#", "allow": True }, - { "command": "addRoleACL", "rolename": "myrole", "acltype": "publishClientReceive", "topic": "pattern/%c/denied", "allow": False }, + { "command": "addRoleACL", "rolename": "myrole", "acltype": "publishClientReceive", "topic": "pattern/u/%u/topic/#", "allow": True }, + { "command": "addRoleACL", "rolename": "myrole", "acltype": "publishClientReceive", "topic": "pattern/u/%u/denied", "allow": False }, + { "command": "addRoleACL", "rolename": "myrole", "acltype": "publishClientReceive", "topic": "pattern/c/%c/topic/#", "allow": True }, + { "command": "addRoleACL", "rolename": "myrole", "acltype": "publishClientReceive", "topic": "pattern/c/%c/denied", "allow": False }, ]} add_publish_acl_response = {'responses': [ @@ -167,23 +173,48 @@ def command_check(sock, command_payload, expected_response): disconnect_kick_packet = mosq_test.gen_disconnect(reason_code=mqtt5_rc.MQTT_RC_ADMINISTRATIVE_ACTION, proto_ver=5) mid = 15 -publish_u_pattern_packet = mosq_test.gen_publish(mid=mid, topic="pattern/user_one/topic", qos=1, proto_ver=5, payload="test") +publish_u_pattern_packet = mosq_test.gen_publish(mid=mid, topic="pattern/u/user_one/topic", qos=1, proto_ver=5, payload="test") puback_u_packet_success = mosq_test.gen_puback(mid=mid, proto_ver=5) -publish_u_pattern_packet_r = mosq_test.gen_publish(topic="pattern/user_one/topic", qos=0, proto_ver=5, payload="test") +publish_u_pattern_packet_r = mosq_test.gen_publish(topic="pattern/u/user_one/topic", qos=0, proto_ver=5, payload="test") mid = 16 -publish_u_pattern_packet_denied = mosq_test.gen_publish(mid=mid, topic="pattern/user_one/denied", qos=1, proto_ver=5, payload="test") +publish_u_pattern_packet_denied = mosq_test.gen_publish(mid=mid, topic="pattern/u/user_one/denied", qos=1, proto_ver=5, payload="test") puback_u_packet_denied = mosq_test.gen_puback(mid=mid, reason_code=mqtt5_rc.MQTT_RC_NOT_AUTHORIZED, proto_ver=5) mid = 17 -publish_c_pattern_packet = mosq_test.gen_publish(mid=mid, topic="pattern/user_one/topic", qos=1, proto_ver=5, payload="test") +publish_c_pattern_packet = mosq_test.gen_publish(mid=mid, topic="pattern/c/cid/topic", qos=1, proto_ver=5, payload="test") puback_c_packet_success = mosq_test.gen_puback(mid=mid, proto_ver=5) -publish_c_pattern_packet_r = mosq_test.gen_publish(topic="pattern/user_one/topic", qos=0, proto_ver=5, payload="test") +publish_c_pattern_packet_r = mosq_test.gen_publish(topic="pattern/c/cid/topic", qos=0, proto_ver=5, payload="test") mid = 18 -publish_c_pattern_packet_denied = mosq_test.gen_publish(mid=mid, topic="pattern/user_one/denied", qos=1, proto_ver=5, payload="test") +publish_c_pattern_packet_denied = mosq_test.gen_publish(mid=mid, topic="pattern/c/cid/denied", qos=1, proto_ver=5, payload="test") puback_c_packet_denied = mosq_test.gen_puback(mid=mid, reason_code=mqtt5_rc.MQTT_RC_NOT_AUTHORIZED, proto_ver=5) +mid = 19 +subscribe_u_pattern_packet_allowed_success = mosq_test.gen_subscribe(mid, "subscribe/pattern/u/user_one/allowed", qos=1, proto_ver=5) +suback_u_pattern_packet_allowed_success = mosq_test.gen_suback(mid, 1, proto_ver=5) + +mid = 20 +subscribe_u_pattern_packet_allowed_fail = mosq_test.gen_subscribe(mid, "subscribe/pattern/u/bad/allowed", qos=1, proto_ver=5) +suback_u_pattern_packet_allowed_fail = mosq_test.gen_suback(mid, mqtt5_rc.MQTT_RC_NOT_AUTHORIZED, proto_ver=5) + +mid = 21 +subscribe_u_pattern_packet_denied_success = mosq_test.gen_subscribe(mid, "subscribe/pattern/u/user_one/denied", qos=1, proto_ver=5) +suback_u_pattern_packet_denied_success = mosq_test.gen_suback(mid, mqtt5_rc.MQTT_RC_NOT_AUTHORIZED, proto_ver=5) + +mid = 22 +subscribe_c_pattern_packet_allowed_success = mosq_test.gen_subscribe(mid, "subscribe/pattern/c/cid/allowed", qos=1, proto_ver=5) +suback_c_pattern_packet_allowed_success = mosq_test.gen_suback(mid, 1, proto_ver=5) + +mid = 23 +subscribe_c_pattern_packet_allowed_fail = mosq_test.gen_subscribe(mid, "subscribe/pattern/c/bad/allowed", qos=1, proto_ver=5) +suback_c_pattern_packet_allowed_fail = mosq_test.gen_suback(mid, mqtt5_rc.MQTT_RC_NOT_AUTHORIZED, proto_ver=5) + +mid = 24 +subscribe_c_pattern_packet_denied_success = mosq_test.gen_subscribe(mid, "subscribe/pattern/c/cid/denied", qos=1, proto_ver=5) +suback_c_pattern_packet_denied_success = mosq_test.gen_suback(mid, mqtt5_rc.MQTT_RC_NOT_AUTHORIZED, proto_ver=5) + + try: os.mkdir(str(port)) shutil.copyfile("dynamic-security-init.json", "%d/dynamic-security.json" % (port)) @@ -297,20 +328,38 @@ def command_check(sock, command_payload, expected_response): # Multi unsubscribe should be allowed mosq_test.do_send_receive(csock, unsubscribe_multi_packet, unsuback_multi_packet_success, "unsuback multi 1") - # Publish to "pattern/user_one/topic" - this is allowed + # Publish to "pattern/u/user_one/topic" - this is allowed csock.send(publish_u_pattern_packet) mosq_test.receive_unordered(csock, publish_u_pattern_packet_r, puback_u_packet_success, "puback pattern 1 / publish r") - # Publish to "pattern/user_one/denied" - this is not allowed + # Publish to "pattern/u/user_one/denied" - this is not allowed mosq_test.do_send_receive(csock, publish_u_pattern_packet_denied, puback_u_packet_denied, "puback pattern 2") - # Publish to "pattern/cid/topic" - this is allowed + # Publish to "pattern/c/cid/topic" - this is allowed csock.send(publish_c_pattern_packet) mosq_test.receive_unordered(csock, publish_c_pattern_packet_r, puback_c_packet_success, "puback pattern 3 / publish r") - # Publish to "pattern/cid/denied" - this is not allowed + # Publish to "pattern/c/cid/denied" - this is not allowed mosq_test.do_send_receive(csock, publish_c_pattern_packet_denied, puback_c_packet_denied, "puback pattern 4") + # Subscribe to "subscribe/pattern/u/user_one/allowed" - this is allowed + mosq_test.do_send_receive(csock, subscribe_u_pattern_packet_allowed_success, suback_u_pattern_packet_allowed_success, "suback pattern 1") + + # Subscribe to "subscribe/pattern/u/bad/allowed" - this is not allowed + mosq_test.do_send_receive(csock, subscribe_u_pattern_packet_allowed_fail, suback_u_pattern_packet_allowed_fail, "suback pattern 2") + + # Subscribe to "subscribe/pattern/u/user_one/denied" - this is not allowed + mosq_test.do_send_receive(csock, subscribe_u_pattern_packet_denied_success, suback_u_pattern_packet_denied_success, "suback pattern 3") + + # Subscribe to "subscribe/pattern/c/cid/allowed" - this is allowed + mosq_test.do_send_receive(csock, subscribe_c_pattern_packet_allowed_success, suback_c_pattern_packet_allowed_success, "suback pattern 4") + + # Subscribe to "subscribe/pattern/c/bad/allowed" - this is not allowed + mosq_test.do_send_receive(csock, subscribe_c_pattern_packet_allowed_fail, suback_c_pattern_packet_allowed_fail, "suback pattern 5") + + # Subscribe to "subscribe/pattern/c/cid/denied" - this is not allowed + mosq_test.do_send_receive(csock, subscribe_c_pattern_packet_denied_success, suback_c_pattern_packet_denied_success, "suback pattern 6") + # Delete the role, client should be kicked command_check(sock, delete_role_command, delete_role_response) @@ -361,8 +410,3 @@ def command_check(sock, command_payload, expected_response): exit(rc) - -publishClientSend -publishClientReceive -subscribeLiteral -subscribePattern diff --git a/www/pages/documentation/dynamic-security.md b/www/pages/documentation/dynamic-security.md index 6ffb901536..01e79d69c1 100644 --- a/www/pages/documentation/dynamic-security.md +++ b/www/pages/documentation/dynamic-security.md @@ -192,11 +192,12 @@ to deny would prevent matching devices from subscribing to any topic at all. #### ACL pattern substitution -The `publishClientSend` and `publishClientReceive` ACL types can make use of -pattern substitution. This means that the strings `%c` and `%u` will be -replaced with the client id and username of the client being checked, -respectively. The pattern strings must be the only item in that level of -hierarchy, so the ACL `topic/%count` will not be considered as a pattern. +The `publishClientSend`, `publishClientReceive`, `subscribePattern`, and +`unsubscribePattern` ACL types can make use of pattern substitution. This means +that the strings `%c` and `%u` will be replaced with the client id and username +of the client being checked, respectively. The pattern strings must be the only +item in that level of hierarchy, so the ACL `topic/%count` will not be +considered as a pattern. For example, with an ACL of `room/%c/temperature`, a client connecting with client id `kitchen` would be allowed to use the topic