diff --git a/src/core/models/rooms.py b/src/core/models/rooms.py
index 4a336b213300456a8528dceb136d8c9ede3f4963..c1c137845ba9350d4fb3557fe1e1fc106f15e3ad 100644
--- a/src/core/models/rooms.py
+++ b/src/core/models/rooms.py
@@ -10,21 +10,20 @@ from django.contrib.postgres.fields import DateTimeRangeField
 from django.core.exceptions import ValidationError
 from django.core.validators import URLValidator
 from django.db import models
-from django.db.models import Q
+from django.db.models import Q, QuerySet
 from django.utils.text import slugify
 from django.utils.translation import gettext_lazy as _
 
+from core.fields import ConferenceReference
+from core.markdown import compile_translated_markdown_fields, store_relationships
+from core.models.assemblies import Assembly
+from core.models.base_managers import ConferenceManagerMixin
+from core.models.conference import Conference, ConferenceMember
+from core.models.shared import BackendMixin
 from core.models.tags import TagItem
+from core.utils import str2bool
 from core.validators import FileSizeValidator, ImageDimensionValidator
 
-from ..fields import ConferenceReference
-from ..markdown import compile_translated_markdown_fields, store_relationships
-from ..models.conference import Conference, ConferenceMember
-from ..models.users import PlatformUser
-from ..utils import str2bool
-from .assemblies import Assembly
-from .shared import BackendMixin
-
 
 class RoomType(models.TextChoices):
     LECTURE_HALL = 'lecturehall', _('Room__type-lecturehall')
@@ -39,39 +38,14 @@ class RoomType(models.TextChoices):
     OTHER = 'other', _('Room__type-other')
 
 
-class RoomManager(models.Manager):
-    def accessible_by_user(self, user: PlatformUser, conference: Conference):
-        assert user is not None
-        assert conference is not None
-
-        qs = self.get_queryset().filter(conference=conference)
-
-        # crude hack because Django makes it that hard to customize AnonymousUser
-        if user is None or not user.is_authenticated:
-            user = PlatformUser.get_anonymous_user()
+class RoomManager(ConferenceManagerMixin['Room']):
+    staff_permissions = ['assembly_team']
+    conference_filter = 'assembly__conference'
+    assembly_filter = 'assembly'
 
-        # for global staff apply no more limits
-        if user.is_staff or user.is_superuser:
-            return qs
-
-        # see if we can return all events (i.e. the user is marked as "staff")
-        if user.is_authenticated:
-            try:
-                member = ConferenceMember.objects.get(conference=conference, user=user)
-                if member.is_staff:
-                    return qs
-            except ConferenceMember.DoesNotExist:
-                pass
-
-        # for everybody else, only show public events
-        return qs.filter(blocked=False)
-
-    def conference_accessible(self, conference: Conference):
-        return (
-            self.get_queryset()
-            .filter(conference=conference, blocked=False)
-            .filter(Q(assembly__state_assembly__in=Assembly.PUBLIC_STATES) | Q(assembly__state_channel__in=Assembly.PUBLIC_STATES))
-            .exclude(room_type=RoomType.PROJECT)
+    def apply_public_filter(self, queryset: 'QuerySet[Room]', member: ConferenceMember | None = None) -> 'QuerySet[Room]':
+        return queryset.filter(blocked=False).filter(
+            Q(assembly__state_assembly__in=Assembly.PUBLIC_STATES) | Q(assembly__state_channel__in=Assembly.PUBLIC_STATES)
         )
 
     def assignable_in_timeframe(self, conference: Conference, start: datetime, end: datetime, assembly: Optional[Assembly] = None, is_sos: bool = False):
@@ -471,6 +445,15 @@ class RoomShare(models.Model):
             raise ValidationError(errors)
 
 
+class RoomLinkManager(ConferenceManagerMixin['RoomLink']):
+    staff_permissions = ['assembly_team']
+    conference_filter = 'room__assembly__conference'
+    assembly_filter = 'room__assembly'
+
+    def apply_manage_filter(self, queryset: 'QuerySet[RoomLink]', member: ConferenceMember) -> 'QuerySet[RoomLink]':
+        return queryset.filter(Q(assembly__members__member=member, assembly__members__can_manage_assembly=True))
+
+
 class RoomLink(models.Model):
     class LinkType(models.TextChoices):
         WEBSITE = 'website', _('RoomLink__type-website')
@@ -482,6 +465,8 @@ class RoomLink(models.Model):
         VIDEO = 'video', _('RoomLink__type-video')
         AUDIO = 'audio', _('RoomLink__type-audio')
 
+    objects = RoomLinkManager()
+
     URL_LINKTYPES = [LinkType.WEBSITE, LinkType.CHAT, LinkType.BBB, LinkType.JITSI, LinkType.PAD, LinkType.VIDEO, LinkType.AUDIO]
     """All LinkTypes which require the link to be a valid URL. The notable exception is media_ccc_de where only a global id is required."""