Skip to content

wip: feat(core): restrict module access globally or per group#277

Draft
pchopinet wants to merge 16 commits into
mainfrom
feat/module-access-control
Draft

wip: feat(core): restrict module access globally or per group#277
pchopinet wants to merge 16 commits into
mainfrom
feat/module-access-control

Conversation

@pchopinet

@pchopinet pchopinet commented Jun 12, 2026

Copy link
Copy Markdown
Member

Summary

Adds per-module access control so an administrator can disable or limit access to workspace modules (notes, chat, calendar, mail) either globally or per auth.Group, configured entirely from the Django admin. The files and ai modules are intentionally never restrictable (files underpins notes/WebDAV; ai is surfaced inside other modules).

A single mechanism covers both intents:

  • Denylist: a module is on by default; a rule turns it off globally or for a group.
  • Allowlist: a module is off globally; a per-group rule turns it back on for that group.

What changed

  • ModuleAccessRule(module_slug, group, is_enabled) model in the core app (first model there), managed via the Django admin with a slug dropdown limited to restrictable modules. Two constraints guarantee one global rule per module plus one rule per (module, group).
  • Resolution service with precedence: superuser > any group grant > all group rules deny > global rule > default open. The per-user result is cached and invalidated on rule writes and on group-membership changes.
  • Restrictability is declared per module via a restrictable = True flag on each AppConfig, aggregated by a single helper (no hardcoded module list).
  • Two-layer enforcement:
    • UI: disabled modules are hidden from the nav, command palette and search; direct page hits return a 403 page (ajax fragments get a minimal body).
    • API: enforced in the DRF authentication layer so it covers session, Knox token and HTTP basic auth uniformly, regardless of a view's permission_classes.
  • Leak fixes found during review: the global SSE stream and the activity/dashboard aggregators now filter providers by the viewer's module access.

Notes

  • Restrictable modules: notes, chat, calendar, mail. Not restrictable: files, ai, and all infrastructure modules (core, dashboard, users, notifications).
  • WebDAV (/dav) is intentionally left unrestricted; since files is never restrictable this is moot.
  • No behavior change until an admin creates a rule: with no rules, every module stays open for everyone.

Test plan

  • Create a global rule disabling mail in the admin; confirm it disappears from nav/commands/search and that /mail and /api/v1/mail/* return 403 for a normal user (browser, token, and basic auth).
  • Confirm a superuser still has full access with the rule in place.
  • Grant mail back to a specific group and confirm only members regain access.
  • Confirm the SSE stream and dashboard/profile activity no longer surface the disabled module.
  • Automated: core suite plus mail/chat/calendar/ai/dashboard/users suites green (only the pre-existing WebDAV Windows flake fails locally).

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This pull request implements a comprehensive per-module access control system. Five workspace modules are marked as restrictable, a new ModuleAccessRule model stores per-module and per-group access rules, and access is enforced at authentication, middleware, and feature levels using a centralized service layer.

Changes

Module Access Control

Layer / File(s) Summary
Restrictable module flags
workspace/ai/apps.py, workspace/calendar/apps.py, workspace/chat/apps.py, workspace/mail/apps.py, workspace/notes/apps.py
Five modules (ai, calendar, chat, mail, notes) are flagged with restrictable = True in their AppConfig classes to enable module-level access control.
ModuleAccessRule model and storage
workspace/core/apps.py, workspace/core/migrations/0001_initial.py, workspace/core/models.py
New CoreConfig wires user-group-change signals for cache invalidation. ModuleAccessRule model stores global and group-scoped rules with uniqueness constraints and a clean() validator. Initial migration creates the table with UUID primary key, module slug, optional group FK, enabled flag, and timestamps.
Module access service layer
workspace/core/services/module_access.py, workspace/core/signals.py
module_access.py computes restrictable slugs from AppConfig.restrictable flags, resolves per-user enabled module sets via group rules with global fallback, and caches results. Exports can_access_module, filter_visible, and cache invalidation. Signal handler (on_user_groups_changed) invalidates cache when user groups change.
Authentication-layer enforcement and admin
workspace/core/authentication.py, workspace/core/admin.py
enforce_module_access function checks module access after URL routing and raises PermissionDenied for inaccessible restrictable modules. Three authentication class subclasses (ModuleAccessSessionAuthentication, ModuleAccessTokenAuthentication, ModuleAccessBasicAuthentication) wrap parent authenticate calls to invoke enforcement. Admin interface provides ModuleAccessRuleForm with dynamic module-slug choices and ModuleAccessRuleAdmin for rule management.
Request-level middleware and settings
workspace/core/middleware.py, workspace/settings.py
ModuleAccessMiddleware blocks requests to restrictable modules when user lacks access, returning 403 JSON for /api/, plain text for AJAX-like requests, or HTML 403 page otherwise. Middleware and custom authentication classes are wired into Django settings.
View and feature-level filtering
workspace/core/context_processors.py, workspace/core/views.py, workspace/core/views_sse.py, workspace/core/activity_registry.py
Context processors filter workspace_active_modules and workspace_commands by module visibility. UnifiedSearchView filters search results and commands. SSE provider initialization filters providers by module access. Activity registry filters providers per-viewer and short-circuits disabled-module queries.
Comprehensive test suite
workspace/core/tests/test_restrictable_modules.py, workspace/core/tests/test_module_access_admin.py, workspace/core/tests/test_module_access_model.py, workspace/core/tests/test_module_access_permission.py, workspace/core/tests/test_module_access_service.py, workspace/core/tests/test_module_access_middleware.py, workspace/core/tests/test_module_access_hiding.py, workspace/core/tests/test_activity.py, workspace/core/tests/test_views_sse.py
Tests validate model constraints, service logic (default access, non-restrictable slugs, global/group rules, superuser bypass, cache invalidation), middleware blocking for disabled modules, authentication enforcement, context filtering, activity registry filtering, and SSE provider filtering.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description clearly outlines the changeset: a module access control system allowing administrators to disable/limit access to workspace modules via Django admin, with detailed explanations of the new model, service, enforcement mechanisms, and test plan.
Title check ✅ Passed The title accurately describes the main change: adding per-module access control that can be restricted globally or per group, which is the core feature added across the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
workspace/core/views.py (1)

57-58: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Invalid Python 3 exception handling syntax.

except TypeError, ValueError: is Python 2 syntax and will raise a SyntaxError in Python 3. The correct syntax uses parentheses to catch multiple exception types.

🐛 Proposed fix
         try:
             limit = int(request.query_params.get("limit", 10))
-        except TypeError, ValueError:
+        except (TypeError, ValueError):
             limit = 10
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@workspace/core/views.py` around lines 57 - 58, The except clause uses Python
2 syntax "except TypeError, ValueError:" which is invalid in Python 3; update
the exception handler to use the tuple form "except (TypeError, ValueError):" so
both exceptions are correctly caught and the fallback assignment to limit = 10
still runs (locate the except block in workspace/core/views.py that sets limit).
🧹 Nitpick comments (7)
workspace/core/activity_registry.py (1)

93-111: 💤 Low value

Consider returning early if viewer user is not found.

When viewer_id is provided but the user doesn't exist (deleted account), the current logic falls through and yields all providers. While this is unlikely in normal operation (viewer_id comes from authenticated requests), explicitly returning an empty iterator or raising would be more defensively correct.

♻️ Optional defensive fix
     def _accessible_providers(self, viewer_id):
         """Yield provider infos the viewer may access.

         When viewer_id is None (system/unfiltered context) no filtering is
         applied. Otherwise providers for modules the viewer cannot access are
         skipped.
         """
         if viewer_id is None:
             yield from self._providers.values()
             return
         from django.contrib.auth import get_user_model

         from workspace.core.services.module_access import can_access_module

         viewer = get_user_model().objects.filter(pk=viewer_id).first()
+        if viewer is None:
+            return  # Unknown viewer: yield nothing rather than everything
         for info in self._providers.values():
-            if viewer is not None and not can_access_module(viewer, info.slug):
+            if not can_access_module(viewer, info.slug):
                 continue
             yield info
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@workspace/core/activity_registry.py` around lines 93 - 111, The
_accessible_providers method currently treats a non-existent viewer_id as an
unfiltered context because viewer becomes None and the access check is skipped;
change it to return early (yield nothing) when viewer_id is not None but the
user lookup returns no user: after viewer = ... .first() add a guard that if
viewer_id is not None and viewer is None then return (or raise), so subsequent
iteration uses can_access_module(viewer, info.slug) safely and does not
accidentally expose all providers.
workspace/core/tests/test_module_access_hiding.py (1)

12-17: ⚡ Quick win

Several cache-sensitive test classes only clear Django cache in tearDown().

workspace/core/tests/test_module_access_hiding.py, workspace/core/tests/test_module_access_middleware.py, workspace/core/tests/test_module_access_permission.py, workspace/core/tests/test_module_access_service.py, and workspace/core/tests/test_views_sse.py all exercise cached module-access state, but they clear cache only after each test. Because Django cache survives across TestCase classes, the first test in any of these classes can inherit stale entries from an earlier class. Clear the cache in setUp() as well, or register self.addCleanup(cache.clear) before warming any access decisions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@workspace/core/tests/test_module_access_hiding.py` around lines 12 - 17, The
tests clear the Django cache only in tearDown(), which allows stale cache
entries to leak into the first test of a different TestCase; in the setUp()
method (the one that creates self.factory and self.user) either call
cache.clear() at the start or register self.addCleanup(cache.clear) before any
access-warming occurs so each test starts with a clean cache; update the setUp()
in the TestCase class that defines setUp/tearDown (refer to the setUp and
tearDown methods and the cache.clear call) to include cache.clear() or
self.addCleanup(cache.clear).
workspace/core/tests/test_module_access_middleware.py (1)

31-48: ⚡ Quick win

The allow-path tests are too weak in workspace/core/tests/test_module_access_middleware.py and workspace/core/tests/test_module_access_permission.py.

These cases only assert status_code != 403, which still passes on unrelated 404/500/401 responses. That means they do not actually prove middleware/auth let the request through. Assert the expected downstream outcome for each endpoint instead so these tests verify successful access, not just the absence of one specific denial code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@workspace/core/tests/test_module_access_middleware.py` around lines 31 - 48,
The tests (test_enabled_module_not_blocked,
test_non_restrictable_module_never_blocked, test_superuser_bypasses_block)
currently only assert resp.status_code != 403 which can hide 404/500/401
failures; update each to assert the actual successful downstream outcome (e.g.
assert resp.status_code == 200 or use assertContains/assertIn on expected
response body) for the specific endpoints used (/api/v1/mail/accounts and
/api/v1/modules) so the tests verify real access rather than just absence of
403.
workspace/core/tests/test_activity.py (1)

470-493: 💤 Low value

Consider using explicit module paths for patch targets.

The tests use patch.object(info.provider_cls, "get_recent_events", ...) where info.provider_cls is retrieved from the registry at runtime. While this works correctly, using explicit module paths like @patch('workspace.chat.activity.ChatActivityProvider.get_recent_events') would make dependencies clearer and improve maintainability.

The current approach has a trade-off: it tests exactly what the registry uses (good for integration testing) but couples the test to the registry's runtime state (less explicit). For unit tests of filtering behavior, explicit paths are typically preferred.

📝 Example refactor for test_recent_events_excludes_disabled_source
-        info = activity_registry.get_all()["chat"]
-        with patch.object(
-            info.provider_cls,
-            "get_recent_events",
-            return_value=list(fake_events),
-        ):
+        with patch(
+            "workspace.chat.activity.ChatActivityProvider.get_recent_events",
+            return_value=list(fake_events),
+        ):
             events = activity_registry.get_recent_events(
                 self.viewer.id, viewer_id=self.viewer.id, source="chat"
             )

Apply the same pattern to test_recent_events_returns_enabled_source.

Also applies to: 495-515

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@workspace/core/tests/test_activity.py` around lines 470 - 493, The test uses
patch.object(info.provider_cls, "get_recent_events") which ties the test to
runtime registry state; change the patch target to an explicit import path for
the provider class (e.g. patch
'workspace.chat.activity.ChatActivityProvider.get_recent_events') in
test_recent_events_excludes_disabled_source (and the sibling
test_recent_events_returns_enabled_source) so the mocked method is clear and
stable; update test imports as needed and use patch as a decorator or context
manager with the string target instead of patch.object(info.provider_cls, ...).

Source: Coding guidelines

workspace/core/services/module_access.py (2)

21-27: 💤 Low value

Consider caching restrictable_module_slugs() result.

This function is called multiple times (lines 48, 68, 81) but iterates all app configs each time. Since the set of restrictable modules is static after app initialization, consider caching the result to avoid repeated iteration.

💡 Example implementation with module-level cache
+_restrictable_cache = None
+
 def restrictable_module_slugs() -> set[str]:
     """Return the slugs of modules whose AppConfig opts into restriction."""
+    global _restrictable_cache
+    if _restrictable_cache is not None:
+        return _restrictable_cache
-    return {
+    _restrictable_cache = {
         config.label
         for config in apps.get_app_configs()
         if getattr(config, "restrictable", False)
     }
+    return _restrictable_cache
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@workspace/core/services/module_access.py` around lines 21 - 27, The function
restrictable_module_slugs() currently iterates apps.get_app_configs() on every
call which is wasteful; cache its result (since app configs are static after
initialization) by memoizing it — either add a module-level variable (e.g.,
_RESTRICTABLE_MODULE_SLUGS) that is computed once and returned thereafter or
decorate restrictable_module_slugs with functools.lru_cache(maxsize=1); update
the function body to return the cached set and ensure any tests/import-time
initialization still triggers the initial computation.

85-88: ⚡ Quick win

Avoid calling slug_getter twice per item.

The condition calls slug_getter(item) twice for each item. Store the result in a variable to improve efficiency.

♻️ Proposed refactor
     enabled = enabled_module_slugs(user)
     return [
         item
         for item in items
-        if slug_getter(item) not in restrictable or slug_getter(item) in enabled
+        if (slug := slug_getter(item)) not in restrictable or slug in enabled
     ]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@workspace/core/services/module_access.py` around lines 85 - 88, The list
comprehension calls slug_getter(item) twice per item; compute the slug once and
reuse it. Replace the comprehension with one that stores slug =
slug_getter(item) (e.g., use the walrus operator "(slug := slug_getter(item))"
in the comprehension or switch to an explicit for-loop) and then test "slug not
in restrictable or slug in enabled"; update references to the symbols
slug_getter, items, restrictable, and enabled accordingly.
workspace/core/migrations/0001_initial.py (1)

28-28: 💤 Low value

Consider adding help_text to document the field.

The module_slug field lacks a help_text parameter. Adding documentation helps administrators understand the field's purpose in the Django admin interface.

📝 Suggested addition
-                ("module_slug", models.CharField(max_length=64)),
+                ("module_slug", models.CharField(max_length=64, help_text="Slug of the workspace module to control access for")),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@workspace/core/migrations/0001_initial.py` at line 28, The module_slug
CharField currently has no help_text; add descriptive help_text to the field
definition (the models.CharField(max_length=64) for module_slug) in the model
class that defines module_slug (not by hand-editing historical migration) — e.g.
module_slug = models.CharField(max_length=64, help_text="Short identifier used
for..."), then run makemigrations to produce an updated migration that records
the help_text change so Django admin shows the documentation.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@workspace/core/services/module_access.py`:
- Line 53: Replace the absolute import in module_access.py with a relative
import: change the current "from workspace.core.models import ModuleAccessRule"
to use the parent-package relative form so that ModuleAccessRule is imported via
"from ..models import ModuleAccessRule"; update any other similar absolute
imports in this file to follow the same relative pattern to match service-module
import guidelines.

---

Outside diff comments:
In `@workspace/core/views.py`:
- Around line 57-58: The except clause uses Python 2 syntax "except TypeError,
ValueError:" which is invalid in Python 3; update the exception handler to use
the tuple form "except (TypeError, ValueError):" so both exceptions are
correctly caught and the fallback assignment to limit = 10 still runs (locate
the except block in workspace/core/views.py that sets limit).

---

Nitpick comments:
In `@workspace/core/activity_registry.py`:
- Around line 93-111: The _accessible_providers method currently treats a
non-existent viewer_id as an unfiltered context because viewer becomes None and
the access check is skipped; change it to return early (yield nothing) when
viewer_id is not None but the user lookup returns no user: after viewer = ...
.first() add a guard that if viewer_id is not None and viewer is None then
return (or raise), so subsequent iteration uses can_access_module(viewer,
info.slug) safely and does not accidentally expose all providers.

In `@workspace/core/migrations/0001_initial.py`:
- Line 28: The module_slug CharField currently has no help_text; add descriptive
help_text to the field definition (the models.CharField(max_length=64) for
module_slug) in the model class that defines module_slug (not by hand-editing
historical migration) — e.g. module_slug = models.CharField(max_length=64,
help_text="Short identifier used for..."), then run makemigrations to produce an
updated migration that records the help_text change so Django admin shows the
documentation.

In `@workspace/core/services/module_access.py`:
- Around line 21-27: The function restrictable_module_slugs() currently iterates
apps.get_app_configs() on every call which is wasteful; cache its result (since
app configs are static after initialization) by memoizing it — either add a
module-level variable (e.g., _RESTRICTABLE_MODULE_SLUGS) that is computed once
and returned thereafter or decorate restrictable_module_slugs with
functools.lru_cache(maxsize=1); update the function body to return the cached
set and ensure any tests/import-time initialization still triggers the initial
computation.
- Around line 85-88: The list comprehension calls slug_getter(item) twice per
item; compute the slug once and reuse it. Replace the comprehension with one
that stores slug = slug_getter(item) (e.g., use the walrus operator "(slug :=
slug_getter(item))" in the comprehension or switch to an explicit for-loop) and
then test "slug not in restrictable or slug in enabled"; update references to
the symbols slug_getter, items, restrictable, and enabled accordingly.

In `@workspace/core/tests/test_activity.py`:
- Around line 470-493: The test uses patch.object(info.provider_cls,
"get_recent_events") which ties the test to runtime registry state; change the
patch target to an explicit import path for the provider class (e.g. patch
'workspace.chat.activity.ChatActivityProvider.get_recent_events') in
test_recent_events_excludes_disabled_source (and the sibling
test_recent_events_returns_enabled_source) so the mocked method is clear and
stable; update test imports as needed and use patch as a decorator or context
manager with the string target instead of patch.object(info.provider_cls, ...).

In `@workspace/core/tests/test_module_access_hiding.py`:
- Around line 12-17: The tests clear the Django cache only in tearDown(), which
allows stale cache entries to leak into the first test of a different TestCase;
in the setUp() method (the one that creates self.factory and self.user) either
call cache.clear() at the start or register self.addCleanup(cache.clear) before
any access-warming occurs so each test starts with a clean cache; update the
setUp() in the TestCase class that defines setUp/tearDown (refer to the setUp
and tearDown methods and the cache.clear call) to include cache.clear() or
self.addCleanup(cache.clear).

In `@workspace/core/tests/test_module_access_middleware.py`:
- Around line 31-48: The tests (test_enabled_module_not_blocked,
test_non_restrictable_module_never_blocked, test_superuser_bypasses_block)
currently only assert resp.status_code != 403 which can hide 404/500/401
failures; update each to assert the actual successful downstream outcome (e.g.
assert resp.status_code == 200 or use assertContains/assertIn on expected
response body) for the specific endpoints used (/api/v1/mail/accounts and
/api/v1/modules) so the tests verify real access rather than just absence of
403.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a2777e1a-1e96-4850-a7ba-67da9925b434

📥 Commits

Reviewing files that changed from the base of the PR and between 0ed07a1 and 0c59f67.

📒 Files selected for processing (28)
  • workspace/ai/apps.py
  • workspace/calendar/apps.py
  • workspace/chat/apps.py
  • workspace/core/activity_registry.py
  • workspace/core/admin.py
  • workspace/core/apps.py
  • workspace/core/authentication.py
  • workspace/core/context_processors.py
  • workspace/core/middleware.py
  • workspace/core/migrations/0001_initial.py
  • workspace/core/migrations/__init__.py
  • workspace/core/models.py
  • workspace/core/services/module_access.py
  • workspace/core/signals.py
  • workspace/core/tests/test_activity.py
  • workspace/core/tests/test_module_access_admin.py
  • workspace/core/tests/test_module_access_hiding.py
  • workspace/core/tests/test_module_access_middleware.py
  • workspace/core/tests/test_module_access_model.py
  • workspace/core/tests/test_module_access_permission.py
  • workspace/core/tests/test_module_access_service.py
  • workspace/core/tests/test_restrictable_modules.py
  • workspace/core/tests/test_views_sse.py
  • workspace/core/views.py
  • workspace/core/views_sse.py
  • workspace/mail/apps.py
  • workspace/notes/apps.py
  • workspace/settings.py

return set(restrictable)

# Imported here to avoid a circular import (the model imports this module).
from workspace.core.models import ModuleAccessRule

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use relative import as per coding guidelines.

Service files under workspace/*/services/*.py should use relative imports to import from the parent module. As per coding guidelines, this import should be from ..models import ModuleAccessRule.

♻️ Proposed fix
-    from workspace.core.models import ModuleAccessRule
+    from ..models import ModuleAccessRule
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
from workspace.core.models import ModuleAccessRule
from ..models import ModuleAccessRule
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@workspace/core/services/module_access.py` at line 53, Replace the absolute
import in module_access.py with a relative import: change the current "from
workspace.core.models import ModuleAccessRule" to use the parent-package
relative form so that ModuleAccessRule is imported via "from ..models import
ModuleAccessRule"; update any other similar absolute imports in this file to
follow the same relative pattern to match service-module import guidelines.

Source: Coding guidelines

@pchopinet pchopinet changed the title feat(core): restrict module access globally or per group wip: feat(core): restrict module access globally or per group Jun 12, 2026
@pchopinet pchopinet marked this pull request as draft June 12, 2026 11:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant