From ae9ecd074bdce2a1542a4cb24c99077df60a1820 Mon Sep 17 00:00:00 2001 From: Andrew Mirsky Date: Wed, 6 Aug 2025 00:10:34 -0400 Subject: [PATCH] updating ldap custom schema to include three ACL attributes, retrieve the correct topic list and check if topic is allowed --- amqtt/contrib/ldap.py | 37 +++++++++++++++++++++----- amqtt/plugins/__init__.py | 4 +++ tests/contrib/test_ldap.py | 6 ++--- tests/fixtures/ldap/customuser.schema | 8 +++--- tests/fixtures/ldap/customusers.ldif | 12 ++++----- tests/fixtures/ldap/docker-compose.yml | 2 +- 6 files changed, 48 insertions(+), 21 deletions(-) diff --git a/amqtt/contrib/ldap.py b/amqtt/contrib/ldap.py index 4974653..89beaf4 100644 --- a/amqtt/contrib/ldap.py +++ b/amqtt/contrib/ldap.py @@ -6,6 +6,7 @@ import ldap from amqtt.broker import BrokerContext from amqtt.contexts import Action from amqtt.errors import PluginInitError +from amqtt.plugins import TopicMatcher from amqtt.plugins.base import BaseAuthPlugin, BaseTopicPlugin from amqtt.session import Session @@ -72,6 +73,12 @@ class LDAPAuthPlugin(BaseAuthPlugin): class LDAPTopicPlugin(BaseTopicPlugin): """Plugin to authenticate a user with an LDAP directory server.""" + _action_attr_map = { + Action.PUBLISH: 'publish_attribute', + Action.SUBSCRIBE: 'subscribe_attribute', + Action.RECEIVE: 'receive_attribute' + } + def __init__(self, context: BrokerContext) -> None: super().__init__(context) @@ -82,13 +89,20 @@ class LDAPTopicPlugin(BaseTopicPlugin): except ldap.INVALID_CREDENTIALS as e: # pylint: disable=E1101 raise PluginInitError(self.__class__) from e + self.topic_matcher = TopicMatcher() + async def topic_filtering( self, *, session: Session | None = None, topic: str | None = None, action: Action | None = None ) -> bool | None: # search_filter = f"({self.config.user_attribute}={session.username})" search_filter = "(uid=jdoe)" - attrs = ["cn", "userType", "roleName", "isActive"] + attrs = [ + "cn", + self.config.publish_attribute, + self.config.subscribe_attribute, + self.config.receive_attribute + ] results = self.conn.search_s(self.config.base_dn, ldap.SCOPE_SUBTREE, search_filter, attrs) @@ -96,13 +110,22 @@ class LDAPTopicPlugin(BaseTopicPlugin): logger.debug(f"user not found: {session.username}") return False - for dn, entry in results: - print("DN:", dn) - print("publishACL:", entry.get("userType", [])) - print("subscribeACL:", entry.get("roleName", [])) - print("receiveACL:", entry.get("isActive", [])) + if len(results) > 1: + found_users = [dn for dn, _ in results] + logger.debug(f"multiple users found: {', '.join(found_users)}") + return False + + dn, entry = results[0] + + ldap_attribute = getattr(self.config, self._action_attr_map[action]) + allowed_topics = [t.decode("utf-8") for t in entry.get(ldap_attribute, [])] + logger.debug(f"DN: {dn} - {ldap_attribute}={allowed_topics}") + + return self.topic_matcher.are_topics_allowed(topic, allowed_topics) + # print(f"{self.config.publish_attribute} : ", entry.get(self.config.publish_attribute, [])) + # print(f"{self.config.subscribe_attribute} : ", entry.get(self.config.subscribe_attribute, [])) + # print(f"{self.config.receive_attribute} : ", entry.get(self.config.receive_attribute, [])) - return None @dataclass class Config: diff --git a/amqtt/plugins/__init__.py b/amqtt/plugins/__init__.py index a0c0327..9b2955a 100644 --- a/amqtt/plugins/__init__.py +++ b/amqtt/plugins/__init__.py @@ -32,3 +32,7 @@ class TopicMatcher: .lstrip("?")) match_pattern = self._topic_filter_matchers[a_filter] return bool(match_pattern.fullmatch(topic)) + + def are_topics_allowed(self, topic: str, many_filters: list[str]) -> bool: + + return any([self.is_topic_allowed(topic, a_filter) for a_filter in many_filters]) diff --git a/tests/contrib/test_ldap.py b/tests/contrib/test_ldap.py index 6f3164a..a0e184f 100644 --- a/tests/contrib/test_ldap.py +++ b/tests/contrib/test_ldap.py @@ -121,9 +121,9 @@ async def test_topic_ldap_plugin(): ctx.config = LDAPTopicPlugin.Config( server="ldap://localhost:1389", # server=ldap_service, - base_dn="dc=example,dc=org", + base_dn="dc=amqtt,dc=io", user_attribute="uid", - bind_dn="cn=admin,dc=example,dc=org", + bind_dn="cn=admin,dc=amqtt,dc=io", bind_password="adminpassword", publish_attribute="publishACL", subscribe_attribute="subscribeACL", @@ -135,4 +135,4 @@ async def test_topic_ldap_plugin(): s.username = "testuser" s.password = "testpassword" - assert await ldap_plugin.topic_filtering(session=s, topic='my/topic', action=Action.PUBLISH), "access not granted" + assert await ldap_plugin.topic_filtering(session=s, topic='my/topic/one', action=Action.PUBLISH), "access not granted" diff --git a/tests/fixtures/ldap/customuser.schema b/tests/fixtures/ldap/customuser.schema index a2ba614..fb26bb5 100644 --- a/tests/fixtures/ldap/customuser.schema +++ b/tests/fixtures/ldap/customuser.schema @@ -1,7 +1,7 @@ -attributetype ( 1.3.6.1.4.1.4203.666.1.1 NAME 'userType' DESC 'Type of user' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE ) +attributetype ( 1.3.6.1.4.1.4203.666.1.1 NAME 'publishACL' DESC 'topics for publishing' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE ) -attributetype ( 1.3.6.1.4.1.4203.666.1.2 NAME 'roleName' DESC 'Role of the user' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE ) +attributetype ( 1.3.6.1.4.1.4203.666.1.2 NAME 'subscribeACL' DESC 'topics for subscribing' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE ) -attributetype ( 1.3.6.1.4.1.4203.666.1.3 NAME 'isActive' DESC 'Account status' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE ) +attributetype ( 1.3.6.1.4.1.4203.666.1.3 NAME 'receiveACL' DESC 'topics for receiving' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE ) -objectclass ( 1.3.6.1.4.1.4203.666.2.1 NAME 'customuserinfo' DESC 'User info with custom attributes' SUP inetOrgPerson STRUCTURAL MUST ( cn $ sn $ isActive ) MAY ( userType $ roleName ) ) +objectclass ( 1.3.6.1.4.1.4203.666.2.1 NAME 'customuserinfo' DESC 'User info with custom attributes' SUP inetOrgPerson STRUCTURAL MUST ( cn $ sn ) MAY ( publishACL $ subscribeACL $ receiveACL ) ) diff --git a/tests/fixtures/ldap/customusers.ldif b/tests/fixtures/ldap/customusers.ldif index 3616293..d892368 100644 --- a/tests/fixtures/ldap/customusers.ldif +++ b/tests/fixtures/ldap/customusers.ldif @@ -1,16 +1,16 @@ -dn: ou=users,dc=example,dc=org +dn: ou=users,dc=amqtt,dc=io objectClass: organizationalUnit ou: users description: Organizational Unit for storing user entries -dn: uid=jdoe,ou=users,dc=example,dc=org +dn: uid=jdoe,ou=users,dc=amqtt,dc=io objectClass: inetOrgPerson objectClass: customuserinfo cn: John Doe sn: Doe uid: jdoe -mail: jdoe@example.org +mail: jdoe@amqtt.io userPassword: {SSHA}w0RfYQaFGMQq7c5QnW7xvXb+iXG0P5gB -userType: Admin -roleName: Manager -isActive: TRUE +publishACL: my/topic/one +subscribeACL: my/topic/two +receiveACL: my/topic/three diff --git a/tests/fixtures/ldap/docker-compose.yml b/tests/fixtures/ldap/docker-compose.yml index fd680ad..df9413d 100644 --- a/tests/fixtures/ldap/docker-compose.yml +++ b/tests/fixtures/ldap/docker-compose.yml @@ -15,7 +15,7 @@ services: - "1636:636" environment: - LDAP_ADMIN_PASSWORD=adminpassword - - LDAP_DOMAIN=example.org + - LDAP_DOMAIN=amqtt.io volumes: ldap-data: