diff --git a/ChangeLog.txt b/ChangeLog.txt index 35ce3dd5..a49ccf47 100644 --- a/ChangeLog.txt +++ b/ChangeLog.txt @@ -24,6 +24,9 @@ Broker: using QoS 1 or 2 will receive the quota-exceeded reason code in the 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. Client library: - Add MOSQ_OPT_DISABLE_SOCKETPAIR to allow the disabling of the socketpair diff --git a/plugins/dynamic-security/acl.c b/plugins/dynamic-security/acl.c index 8a57f7a9..206cb4aa 100644 --- a/plugins/dynamic-security/acl.c +++ b/plugins/dynamic-security/acl.c @@ -38,10 +38,14 @@ static int acl_check_publish_c_recv(struct mosquitto_evt_acl_check *ed, struct d struct dynsec__rolelist *rolelist, *rolelist_tmp = NULL; struct dynsec__acl *acl, *acl_tmp = NULL; bool result; + const char *clientid, *username; + + clientid = mosquitto_client_id(ed->client); + username = mosquitto_client_username(ed->client); HASH_ITER(hh, base_rolelist, rolelist, rolelist_tmp){ HASH_ITER(hh, rolelist->role->acls.publish_c_recv, acl, acl_tmp){ - mosquitto_topic_matches_sub(acl->topic, ed->topic, &result); + mosquitto_topic_matches_sub_with_pattern(acl->topic, ed->topic, clientid, username, &result); if(result){ if(acl->allow){ return MOSQ_ERR_SUCCESS; @@ -66,10 +70,14 @@ static int acl_check_publish_c_send(struct mosquitto_evt_acl_check *ed, struct d struct dynsec__rolelist *rolelist, *rolelist_tmp = NULL; struct dynsec__acl *acl, *acl_tmp = NULL; bool result; + const char *clientid, *username; + + clientid = mosquitto_client_id(ed->client); + username = mosquitto_client_username(ed->client); HASH_ITER(hh, base_rolelist, rolelist, rolelist_tmp){ HASH_ITER(hh, rolelist->role->acls.publish_c_send, acl, acl_tmp){ - mosquitto_topic_matches_sub(acl->topic, ed->topic, &result); + mosquitto_topic_matches_sub_with_pattern(acl->topic, ed->topic, clientid, username, &result); if(result){ if(acl->allow){ return MOSQ_ERR_SUCCESS; diff --git a/src/linker-macosx.syms b/src/linker-macosx.syms index dddda52c..2b206fc2 100644 --- a/src/linker-macosx.syms +++ b/src/linker-macosx.syms @@ -32,4 +32,5 @@ _mosquitto_set_username _mosquitto_strdup _mosquitto_sub_topic_check _mosquitto_topic_matches_sub +_mosquitto_topic_matches_sub_with_pattern _mosquitto_validate_utf8 diff --git a/src/linker.syms b/src/linker.syms index 5792bc06..5eec8bb4 100644 --- a/src/linker.syms +++ b/src/linker.syms @@ -33,5 +33,6 @@ mosquitto_strdup; mosquitto_sub_topic_check; mosquitto_topic_matches_sub; + mosquitto_topic_matches_sub_with_pattern; mosquitto_validate_utf8; }; diff --git a/test/broker/14-dynsec-acl.py b/test/broker/14-dynsec-acl.py index 670be99a..7bb5500b 100755 --- a/test/broker/14-dynsec-acl.py +++ b/test/broker/14-dynsec-acl.py @@ -44,6 +44,7 @@ add_client_group_role_command = {"commands":[ { "command": "addRoleACL", "rolename": "myrole", "acltype": "subscribePattern", "topic": "single-wildcard/+/topic", "allow": True }, { "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": "addGroupClient", "groupname": "mygroup", "username": "user_one" } ]} @@ -53,6 +54,7 @@ add_client_group_role_response = {'responses': [ {'command': 'addGroupRole'}, {'command': 'addRoleACL'}, {'command': 'addRoleACL'}, {'command': 'addRoleACL'}, {'command': 'addRoleACL'}, + {'command': 'addRoleACL'}, {'command': 'addGroupClient'} ]} @@ -61,11 +63,23 @@ add_publish_acl_command = {"commands":[ { "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": "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 }, ]} add_publish_acl_response = {'responses': [ + {'command': 'addRoleACL'}, {'command': 'addRoleACL'}, + {'command': 'addRoleACL'}, {'command': 'addRoleACL'}, + {'command': 'addRoleACL'}, {'command': 'addRoleACL'}, + {'command': 'addRoleACL'}, {'command': 'addRoleACL'}, {'command': 'addRoleACL'}, {'command': 'addRoleACL'}, {'command': 'addRoleACL'}, {'command': 'addRoleACL'}, {'command': 'addRoleACL'}, {'command': 'addRoleACL'} @@ -146,8 +160,30 @@ mid = 13 unsubscribe_multi_packet = mosq_test.gen_unsubscribe(mid, "multilevel-wildcard/topic/topic/#", proto_ver=5) unsuback_multi_packet_success = mosq_test.gen_unsuback(mid, 0, proto_ver=5) +mid = 14 +subscribe_pattern_packet = mosq_test.gen_subscribe(mid, "pattern/#", 0, proto_ver=5) +suback_pattern_packet = mosq_test.gen_suback(mid, 0, proto_ver=5) + 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") +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") + +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") +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") +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") + +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") +puback_c_packet_denied = mosq_test.gen_puback(mid=mid, reason_code=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)) @@ -230,6 +266,9 @@ try: # Subscribe to "multilevel-wildcard/topic/topic/allowed" - this is now allowed mosq_test.do_send_receive(csock, subscribe_multi_packet, suback_multi_packet_success, "suback multi 3") + # Subscribe to "pattern/#" - allowed + mosq_test.do_send_receive(csock, subscribe_pattern_packet, suback_pattern_packet, "suback") + # Publish to "simple/topic" - this is now allowed csock.send(publish_simple_packet) mosq_test.receive_unordered(csock, publish_simple_packet_r, puback_simple_packet_success, "puback simple 3 / publish r") @@ -258,6 +297,21 @@ try: # 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 + 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 + 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 + 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 + mosq_test.do_send_receive(csock, publish_c_pattern_packet_denied, puback_c_packet_denied, "puback pattern 4") + + # Delete the role, client should be kicked command_check(sock, delete_role_command, delete_role_response) diff --git a/test/broker/14-dynsec-modify-client.py b/test/broker/14-dynsec-modify-client.py index 10c1466f..d67d559c 100755 --- a/test/broker/14-dynsec-modify-client.py +++ b/test/broker/14-dynsec-modify-client.py @@ -144,7 +144,7 @@ connect_packet = mosq_test.gen_connect("ctrl-test", keepalive=keepalive, usernam connack_packet = mosq_test.gen_connack(rc=0) mid = 2 -subscribe_packet = mosq_test.gen_subscribe(mid, "$CONTROL/#", 1) +subscribe_packet = mosq_test.gen_subscribe(mid, "$CONTROL/dynamic-security/#", 1) suback_packet = mosq_test.gen_suback(mid, 1) try: diff --git a/test/broker/14-dynsec-modify-group.py b/test/broker/14-dynsec-modify-group.py index 3c0025d1..a8a4931d 100755 --- a/test/broker/14-dynsec-modify-group.py +++ b/test/broker/14-dynsec-modify-group.py @@ -130,7 +130,7 @@ connect_packet = mosq_test.gen_connect("ctrl-test", keepalive=keepalive, usernam connack_packet = mosq_test.gen_connack(rc=0) mid = 2 -subscribe_packet = mosq_test.gen_subscribe(mid, "$CONTROL/#", 1) +subscribe_packet = mosq_test.gen_subscribe(mid, "$CONTROL/dynamic-security/#", 1) suback_packet = mosq_test.gen_suback(mid, 1) try: diff --git a/test/broker/14-dynsec-modify-role.py b/test/broker/14-dynsec-modify-role.py index b18bef56..91b59bca 100755 --- a/test/broker/14-dynsec-modify-role.py +++ b/test/broker/14-dynsec-modify-role.py @@ -111,7 +111,7 @@ connect_packet = mosq_test.gen_connect("ctrl-test", keepalive=keepalive, usernam connack_packet = mosq_test.gen_connack(rc=0) mid = 2 -subscribe_packet = mosq_test.gen_subscribe(mid, "$CONTROL/#", 1) +subscribe_packet = mosq_test.gen_subscribe(mid, "$CONTROL/dynamic-security/#", 1) suback_packet = mosq_test.gen_suback(mid, 1) try: diff --git a/test/broker/14-dynsec-role.py b/test/broker/14-dynsec-role.py index 030667a2..d4e42b56 100755 --- a/test/broker/14-dynsec-role.py +++ b/test/broker/14-dynsec-role.py @@ -176,7 +176,7 @@ connect_packet = mosq_test.gen_connect("ctrl-test", keepalive=keepalive, usernam connack_packet = mosq_test.gen_connack(rc=0) mid = 2 -subscribe_packet = mosq_test.gen_subscribe(mid, "$CONTROL/#", 1) +subscribe_packet = mosq_test.gen_subscribe(mid, "$CONTROL/dynamic-security/#", 1) suback_packet = mosq_test.gen_suback(mid, 1) try: diff --git a/www/pages/documentation/dynamic-security.md b/www/pages/documentation/dynamic-security.md index 89884cb0..6ffb9015 100644 --- a/www/pages/documentation/dynamic-security.md +++ b/www/pages/documentation/dynamic-security.md @@ -190,6 +190,21 @@ the ACL topic filter and the topic provided in the SUBSCRIBE or UNSUBSCRIBE message. This means that setting a `subscribePattern` ACL with topic filter `#` 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. + +For example, with an ACL of `room/%c/temperature`, a client connecting with +client id `kitchen` would be allowed to use the topic +`room/kitchen/temperature` only. + +If a client does not have a username, a pattern that includes `%u` will always +fail to match against that client. + #### Text name This is an optional text field to give a human friendly name to this role.