diff --git a/src/backoffice/forms.py b/src/backoffice/forms/__init__.py similarity index 100% rename from src/backoffice/forms.py rename to src/backoffice/forms/__init__.py diff --git a/src/backoffice/forms/mqtt.py b/src/backoffice/forms/mqtt.py new file mode 100644 index 0000000000000000000000000000000000000000..5f13f312d74519090e8bdc61dbef7a22592de0f9 --- /dev/null +++ b/src/backoffice/forms/mqtt.py @@ -0,0 +1,35 @@ +from django.forms import ModelForm +from core.models import MqttTopic, MqttCredential, MqttCredentialAccess, MqttUserAccess + + +class MqttTopicCreateForm(ModelForm): + class Meta: + model = MqttTopic + fields = [ + 'name', + 'access_mode', + 'subtree_wildcard', + 'description', + ] + + +class MqttTopicEditForm(ModelForm): + class Meta: + model = MqttTopic + fields = [ + 'access_mode', + 'subtree_wildcard', + 'description', + ] + + +class MqttCredentialAccessForm(ModelForm): + class Meta: + model = MqttCredentialAccess + fields = [ + 'conference_member', + 'access_mode', + ] + + def __init__(self): + self.fields['conference_member'].disabled = True diff --git a/src/core/integrations/mqtt.py b/src/core/integrations/mqtt.py new file mode 100644 index 0000000000000000000000000000000000000000..914e927cebfa8c44a05acfdd2172d2ef171c2bd6 --- /dev/null +++ b/src/core/integrations/mqtt.py @@ -0,0 +1,31 @@ +class MqttIntegration: + def __init__(self): + # probably needs some info how to reach the Mqtt broker + credentials + pass + + def initialize(self): + """ called at startup """ + pass + + def create_or_update_user(self, username: str, password: str, admin: bool): + """ + Creates user or updates password / admin access. + If User was created, also create a private topic where only the user can publish/subscribe. + """ + pass + + def remove_user(self, username): + """ removes user with username `username`. """ + pass + + def create_or_update_topic(self, topic_path: str, public_subscribe: bool, public_publish: bool): + """ creates topic at path `topic_path` or updates global access """ + pass + + def remove_topic(self, topic_path: str): + """ removes topic at `topic_path` """ + pass + + def update_user_permission(self, topic_path: str, username: str, subscribe: bool, publish: bool): + """ updates permissions for user `username` at topic `topic_path` """ + pass diff --git a/src/core/models/__init__.py b/src/core/models/__init__.py index 7d128951df1495c15d919e98cff4e901f4b14810..f917e8462aef5d6228ee06f3fd2217b338aac07f 100644 --- a/src/core/models/__init__.py +++ b/src/core/models/__init__.py @@ -6,6 +6,7 @@ from .board import BulletinBoardEntry from .events import Event, EventAttachment, EventLikeCount, EventParticipant from .pages import StaticPage, StaticPageRevision from .messages import DirectMessage +from .mqtt import MqttTopic, MqttCredential, MqttCredentialAccess, MqttUserAccess from .rooms import Room, RoomLink from .schedules import ScheduleSource, ScheduleSourceImport, ScheduleSourceMapping from .shared import BackendMixin @@ -21,6 +22,7 @@ __all__ = [ 'BackendMixin', 'Badge', 'BadgeToken', 'BulletinBoardEntry', 'Conference', 'ConferenceMember', 'ConferenceMemberTicket', 'ConferenceTag', 'ConferenceTrack', 'DereferrerStats', 'DirectMessage', + 'MqttTopic', 'MqttCredential', 'MqttCredentialAccess', 'MqttUserAccess', 'Event', 'EventAttachment', 'EventLikeCount', 'EventParticipant', 'PlatformUser', 'Room', 'RoomLink', diff --git a/src/core/models/conference.py b/src/core/models/conference.py index 10f93cbaba92bacee4f9811875ebf0d49315ca0e..bdb74f38146f10ab0285ba3ed10b5ae00036e5e8 100644 --- a/src/core/models/conference.py +++ b/src/core/models/conference.py @@ -29,6 +29,8 @@ class ConferenceMember(models.Model): permission_groups = models.ManyToManyField(Group, blank=True, related_name='+') static_page_groups = pg_fields.ArrayField(models.CharField(max_length=50), blank=True, null=True) + mqtt_defaultuser = models.ManyToManyField('MqttCredential', related_name='conference_member') + class Meta: permissions = [ ('assembly_team', _('ConferenceMember__permission-assembly_team')), diff --git a/src/core/models/mqtt.py b/src/core/models/mqtt.py new file mode 100644 index 0000000000000000000000000000000000000000..798b045d91e8dafae3d9563d348311fee7890b9a --- /dev/null +++ b/src/core/models/mqtt.py @@ -0,0 +1,101 @@ +from django.db import models +from django.db.models import F +from django.utils.translation import gettext_lazy as _ + +from .rooms import Room + + +class MqttTopic(models.Model): + + ACCESS_MODE__NONE = '' + ACCESS_MODE__READ = 'r' + ACCESS_MODE__WRITE = 'w' + ACCESS_MODE__READWRITE = 'rw' + ACCESS_MODES = ( + (ACCESS_MODE__NONE, _("MqttTopic--AccessMode--none")), + (ACCESS_MODE__READ, _("MqttTopic--AccessMode--read")), + (ACCESS_MODE__WRITE, _("MqttTopic--AccessMode--write")), + (ACCESS_MODE__READWRITE, _("MqttTopic--AccessMode--readwrite")), + ) + + name = models.CharField( + max_length=200, + help_text=_('MqttTopic--name--help'), + verbose_name=_('MqttTopic--name') + ) + description = models.TextField( + blank=True, null=True, + help_text=_('MqttTopic--description--help'), + verbose_name=_('MqttTopic--description') + ) + access_mode = models.CharField( + choices=ACCESS_MODES, max_length=max([len(k) for (k, v) in ACCESS_MODES]), + help_text=_('MqttTopic--access_mode--help'), + verbose_name=_('MqttTopic--access_mode') + ) + subtree_wildcard = models.BooleanField( + help_text=_('MqttTopic--subtree_wildcard--help'), + verbose_name=_('MqttTopic--subtree_wildcard') + ) + room = models.ForeignKey(Room, on_delete=models.CASCADE, related_name='mqtt_topics') + + +class MqttCredential(models.Model): + class Meta: + constraints = [ + models.UniqueConstraint('username', name='mqttcredential__username'), + ] + + username = models.CharField( + max_length=200, + help_text=_('MqttUser--username--help'), + verbose_name=_('MqttUser--username') + ) + password = models.CharField( + max_length=200, + help_text=_('MqttUser--password--help'), + verbose_name=_('MqttUser--password') + ) + blocked = models.BooleanField( + help_text=_('MqttUser--blocked--help'), + verbose_name=_('MqttUser--blocked') + ) + description = models.CharField( + help_text=_('MqttTopic--description--help'), + verbose_name=_('MqttTopic--description') + ) + + # ForeignKey am PlatformUser - für den anonymen Clientuser auf den man per SSO zugreifen kann + # Many2Many am ConferenceMember für weitere Clientuser die auch Permissions requesten können + + +class MqttUserAccess(models.Model): + class Meta: + constraints = [ + models.UniqueConstraint('topic', 'credential', condition=F(granted=False), name='mqttuseraccess_unique_requests'), + models.UniqueConstraint('topic', 'credential', condition=F(granted=True), name='mqttuseraccess_unique_grants'), + ] + + topic = models.ForeignKey(MqttTopic, on_delete=models.CASCADE, related_name='access') + conference_member = models.ForeignKey('ConferenceMember', on_delete=models.CASCADE, related_name='mqtt_access') + granted = models.BooleanField(default=False) + access_mode = models.CharField( + choices=MqttTopic.ACCESS_MODES, max_length=max([len(k) for (k, v) in MqttTopic.ACCESS_MODES]), + help_text=_('MqttUserAccess--access_mode--help'), + verbose_name=_('MqttUserAccess--access_mode') + ) + + +class MqttCredentialAccess(models.Model): + class Meta: + constraints = [ + models.UniqueConstraint('topic', 'credential', name='mqttcredaccess_unique'), + ] + + topic = models.ForeignKey(MqttTopic, on_delete=models.CASCADE, related_name='access') + conference_member = models.ForeignKey(MqttCredential, on_delete=models.CASCADE, related_name='access') + access_mode = models.CharField( + choices=MqttTopic.ACCESS_MODES, max_length=max([len(k) for (k, v) in MqttTopic.ACCESS_MODES]), + help_text=_('MqttUserAccess--access_mode--help'), + verbose_name=_('MqttUserAccess--access_mode') + ) diff --git a/src/core/models/users.py b/src/core/models/users.py index 2bb966b13d823544968f9c271f3a7f7b7bdf76e3..471fcc9c98d3c509b69449a37e4a9fff8e89127c 100644 --- a/src/core/models/users.py +++ b/src/core/models/users.py @@ -181,6 +181,8 @@ class PlatformUser(AbstractUser): verbose_name=_("PlatformUser__wa_force_website_trigger"), ) + mqtt_defaultuser = models.ForeignKey('MqttCredential', on_delete=models.SET_NULL, related_name='user') + @classmethod def get_user_flags(cls, user): return {