updating ldap custom schema to include three ACL attributes, retrieve the correct topic list and check if topic is allowed

pull/287/head
Andrew Mirsky 2025-08-06 00:10:34 -04:00
rodzic 8c6b6da9ad
commit ae9ecd074b
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: A98E67635CDF2C39
6 zmienionych plików z 48 dodań i 21 usunięć

Wyświetl plik

@ -6,6 +6,7 @@ import ldap
from amqtt.broker import BrokerContext from amqtt.broker import BrokerContext
from amqtt.contexts import Action from amqtt.contexts import Action
from amqtt.errors import PluginInitError from amqtt.errors import PluginInitError
from amqtt.plugins import TopicMatcher
from amqtt.plugins.base import BaseAuthPlugin, BaseTopicPlugin from amqtt.plugins.base import BaseAuthPlugin, BaseTopicPlugin
from amqtt.session import Session from amqtt.session import Session
@ -72,6 +73,12 @@ class LDAPAuthPlugin(BaseAuthPlugin):
class LDAPTopicPlugin(BaseTopicPlugin): class LDAPTopicPlugin(BaseTopicPlugin):
"""Plugin to authenticate a user with an LDAP directory server.""" """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: def __init__(self, context: BrokerContext) -> None:
super().__init__(context) super().__init__(context)
@ -82,13 +89,20 @@ class LDAPTopicPlugin(BaseTopicPlugin):
except ldap.INVALID_CREDENTIALS as e: # pylint: disable=E1101 except ldap.INVALID_CREDENTIALS as e: # pylint: disable=E1101
raise PluginInitError(self.__class__) from e raise PluginInitError(self.__class__) from e
self.topic_matcher = TopicMatcher()
async def topic_filtering( async def topic_filtering(
self, *, session: Session | None = None, topic: str | None = None, action: Action | None = None self, *, session: Session | None = None, topic: str | None = None, action: Action | None = None
) -> bool | None: ) -> bool | None:
# search_filter = f"({self.config.user_attribute}={session.username})" # search_filter = f"({self.config.user_attribute}={session.username})"
search_filter = "(uid=jdoe)" 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) 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}") logger.debug(f"user not found: {session.username}")
return False return False
for dn, entry in results: if len(results) > 1:
print("DN:", dn) found_users = [dn for dn, _ in results]
print("publishACL:", entry.get("userType", [])) logger.debug(f"multiple users found: {', '.join(found_users)}")
print("subscribeACL:", entry.get("roleName", [])) return False
print("receiveACL:", entry.get("isActive", []))
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 @dataclass
class Config: class Config:

Wyświetl plik

@ -32,3 +32,7 @@ class TopicMatcher:
.lstrip("?")) .lstrip("?"))
match_pattern = self._topic_filter_matchers[a_filter] match_pattern = self._topic_filter_matchers[a_filter]
return bool(match_pattern.fullmatch(topic)) 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])

Wyświetl plik

@ -121,9 +121,9 @@ async def test_topic_ldap_plugin():
ctx.config = LDAPTopicPlugin.Config( ctx.config = LDAPTopicPlugin.Config(
server="ldap://localhost:1389", server="ldap://localhost:1389",
# server=ldap_service, # server=ldap_service,
base_dn="dc=example,dc=org", base_dn="dc=amqtt,dc=io",
user_attribute="uid", user_attribute="uid",
bind_dn="cn=admin,dc=example,dc=org", bind_dn="cn=admin,dc=amqtt,dc=io",
bind_password="adminpassword", bind_password="adminpassword",
publish_attribute="publishACL", publish_attribute="publishACL",
subscribe_attribute="subscribeACL", subscribe_attribute="subscribeACL",
@ -135,4 +135,4 @@ async def test_topic_ldap_plugin():
s.username = "testuser" s.username = "testuser"
s.password = "testpassword" 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"

Wyświetl plik

@ -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 ) )

Wyświetl plik

@ -1,16 +1,16 @@
dn: ou=users,dc=example,dc=org dn: ou=users,dc=amqtt,dc=io
objectClass: organizationalUnit objectClass: organizationalUnit
ou: users ou: users
description: Organizational Unit for storing user entries 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: inetOrgPerson
objectClass: customuserinfo objectClass: customuserinfo
cn: John Doe cn: John Doe
sn: Doe sn: Doe
uid: jdoe uid: jdoe
mail: jdoe@example.org mail: jdoe@amqtt.io
userPassword: {SSHA}w0RfYQaFGMQq7c5QnW7xvXb+iXG0P5gB userPassword: {SSHA}w0RfYQaFGMQq7c5QnW7xvXb+iXG0P5gB
userType: Admin publishACL: my/topic/one
roleName: Manager subscribeACL: my/topic/two
isActive: TRUE receiveACL: my/topic/three

Wyświetl plik

@ -15,7 +15,7 @@ services:
- "1636:636" - "1636:636"
environment: environment:
- LDAP_ADMIN_PASSWORD=adminpassword - LDAP_ADMIN_PASSWORD=adminpassword
- LDAP_DOMAIN=example.org - LDAP_DOMAIN=amqtt.io
volumes: volumes:
ldap-data: ldap-data: