Skip to content

feat: Add model-level permissions framework (rebase test - no not merge)#1110

Open
mihow wants to merge 34 commits intomainfrom
feat/model-level-permissions-rebased
Open

feat: Add model-level permissions framework (rebase test - no not merge)#1110
mihow wants to merge 34 commits intomainfrom
feat/model-level-permissions-rebased

Conversation

@mihow
Copy link
Collaborator

@mihow mihow commented Jan 31, 2026

Summary

This update extends the Antenna permissions framework to support both object-level and model-level permissions.

  • Model-level permission checks: The framework now detects whether an object is not tied to a specific project (or is linked to multiple projects) and applies model-level permissions
  • Frontend permissions exposure: User permissions are now exposed through the user_permissions field in API responses at both collection and object levels
  • AuthorizedUser global role: A new global role grants model-level permissions to all signed-in users (create projects, processing services, taxa, etc.)
  • Auto-assignment signal: New users are automatically assigned the AuthorizedUser role on registration

Key Changes

Backend

  • Added GlobalRole base class for model-level permissions
  • Refactored permission checks to support hybrid object/model-level approach
  • Added check_permission, check_model_level_permission, and check_object_level_permission methods
  • Added model-level permissions for ProcessingService and Taxon models
  • New migrations for permissions and auto-assignment of AuthorizedUser role

Permissions

  • create_project - Create new projects globally
  • create_processingservice, update_processingservice, delete_processingservice - Manage processing services
  • register_pipelines_processingservice - Register pipelines for processing services
  • create_taxon, update_taxon, delete_taxon - Manage taxa globally
  • assign_tags_taxon - Assign tags to taxa

Test plan

  • All 225 Django tests pass
  • Model-level permission tests for ProcessingService and Taxon
  • AuthorizedUser global role assignment tests
  • Project permission tests (create, update, delete)
  • Role-based permission tests

Related Issues

Closes #1006

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Implemented granular permission controls enabling authorized users to create projects
    • Enhanced permission enforcement for processing services, jobs, and taxons
  • Refactor

    • Restructured internal permission system for improved consistency and maintainability across object-level and model-level permission handling

✏️ Tip: You can customize this high-level summary in your review settings.

…ns for creating processing services, registering pipelines, creating taxa, and assigning tags
mohamedelabbas1996 and others added 4 commits January 31, 2026 01:21
…rriding existing

permission names and instead rely on model-level permission names defined
in the project model's Meta.permissions.
Co-Authored-By: Claude <noreply@anthropic.com>
@netlify
Copy link

netlify bot commented Jan 31, 2026

Deploy Preview for antenna-preview canceled.

Name Link
🔨 Latest commit cd5c17c
🔍 Latest deploy log https://app.netlify.com/projects/antenna-preview/deploys/697dd1c126abaf0008acdb4d

@netlify
Copy link

netlify bot commented Jan 31, 2026

Deploy Preview for antenna-ssec canceled.

Name Link
🔨 Latest commit cd5c17c
🔍 Latest deploy log https://app.netlify.com/projects/antenna-ssec/deploys/697dd1c13322bb000871f7c5

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 31, 2026

📝 Walkthrough

Walkthrough

This pull request refactors the permission system to support model-level and object-level permissions separately, introducing a PermissionsMixin to centralize permission logic and enabling all authorized users to create projects through global roles and permissions groups.

Changes

Cohort / File(s) Summary
Permission Mixin & Serializers
ami/base/models.py, ami/base/permissions.py, ami/base/serializers.py
Extracted permission logic from BaseModel into new PermissionsMixin with methods for object-level, model-level, and custom permission checks. Added get_instance_for_permission_check to DefaultSerializer for permission validation during creation.
Job & ML Service Views
ami/jobs/models.py, ami/jobs/views.py, ami/ml/views.py
Renamed permission methods to reflect object-level scope (check_custom_permission → check_custom_object_level_permission). Added ObjectPermission checks to ProcessingServiceViewSet; updated views to use new method names and permission check patterns.
Project & Taxon Models
ami/main/models.py, ami/main/api/views.py
Added check_permission, get_permissions, and get_collection_level_permissions to Project and SourceImage. Expanded model-level permissions for processing services and taxons. Updated Project creation to explicitly set owner before saving; added ObjectPermission to TaxonViewSet.
Global & Project Roles
ami/users/roles.py, ami/users/signals.py, ami/main/apps.py
Introduced GlobalRole base class and AuthorizedUser subclass for model-level permissions. Restructured existing roles to use object_level_permissions. Renamed create_roles to create_project_based_roles; added create_global_roles and assign_authorized_user_group signal handlers.
Migrations & Configuration
ami/main/migrations/...
Added migration to assign AuthorizedUser group to all existing users; updated Project model options with expanded permission set and ordering; added merge migration 0082.
Tests
ami/main/tests.py, ami/ml/tests.py
Introduced BasePermissionTestCase with permission helpers; added comprehensive test suites for model-level permissions, role-based permissions, and AuthorizedUser group assignment. Changed test_regular_user_cannot_create_project to test_regular_user_can_create_project (expects 201).

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant API as API View
    participant Model as Model Instance
    participant Mixin as PermissionsMixin
    participant Guardian as Guardian/Django Perms
    participant Signal as Signal Handler

    User->>API: POST /projects/
    API->>Model: create instance + check_permission(user, 'create')
    Model->>Mixin: check_permission(user, 'create')
    Mixin->>Mixin: action=='create' → check_model_level_permission
    Mixin->>Guardian: has_perm(user, 'main.create_project')
    Guardian-->>Mixin: permission result
    Mixin-->>Model: boolean result
    Model-->>API: permission granted/denied
    
    alt Permission Granted
        API->>Model: save(owner=user)
        Note over Model: Instance saved to DB
        Model->>Signal: post_save signal triggered
        Signal->>Signal: assign_authorized_user_group(user)
        Note over Signal: User added to AuthorizedUser group
    else Permission Denied
        API-->>User: HTTP 403 PermissionDenied
    end
    
    API-->>User: HTTP 201 Created
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

backend


🐰 Permissions now hop in layers,
Model-wide and object-wise capers,
Global roles grant creation's cheer,
All users now can build here—
A system fresh, permission deep! 🎯

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.77% 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
Title check ✅ Passed The title 'feat: Add model-level permissions framework' directly summarizes the main change—introducing a model-level permissions framework to extend the existing permissions system.
Description check ✅ Passed The description covers summary, key changes, test plan, and related issues. It aligns with the template structure and provides sufficient context about objectives and implementation.
Linked Issues check ✅ Passed Changes directly address #1006 by enabling model-level 'create_project' permission, exposing permissions in API responses, and auto-assigning AuthorizedUser role for all signed-in users.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing model-level permissions framework. Migrations, role definitions, permission checks, API exposure, and test additions align with PR objectives.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/model-level-permissions-rebased

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.

@mihow mihow changed the title feat: Add model-level permissions framework feat: Add model-level permissions framework (rebase test - no not merge) Jan 31, 2026
@mihow
Copy link
Collaborator Author

mihow commented Jan 31, 2026

Code review

Found 1 issue:

  1. Inconsistent string splitting in get_custom_object_level_permissions causes incorrect permission names to be returned

In ami/base/permissions.py, the method uses split("_", 1)[0] for filtering but rsplit("_", 1)[0] for extraction. For a permission like "register_pipelines_processingservice":

  • The filter condition perm.split("_", 1)[0] gets "register" (correct)
  • The extraction perm.rsplit("_", 1)[0] gets "register_pipelines" (incorrect - should be "register")

This will return wrong permission names for any multi-word custom permission.

Retrieve custom (non-CRUD) permissions for this instance.
"""
object_perms = self._get_object_perms(user)
custom_perms = {
perm.rsplit("_", 1)[0]
for perm in object_perms
if perm.split("_", 1)[0] not in ["view", "create", "update", "delete"]
}
return list(custom_perms)

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (2)
ami/main/models.py (2)

333-364: ⚠️ Potential issue | 🟠 Major

Project custom actions are no longer routed through the new mixin hook.
PermissionsMixin now calls check_custom_object_level_permission, but Project still defines check_custom_permission, so the charts action falls back to the default codename check and will likely be denied for non‑superusers. Rename the override to keep the custom logic active.

🛠️ Suggested fix
-    def check_custom_permission(self, user, action: str) -> bool:
+    def check_custom_object_level_permission(self, user, action: str) -> bool:
         """
         Check custom permissions for actions like 'charts'.
         Charts is treated as a read-only operation, so it follows the same
         permission logic as 'retrieve'.
         """
         from ami.users.roles import BasicMember

         if action == "charts":
             # Same permission logic as retrieve action
             if self.draft:
                 # Allow view permission for members and owners of draft projects
                 return BasicMember.has_role(user, self) or user == self.owner or user.is_superuser
             return True

         # Fall back to default permission checking for other actions
-        return super().check_custom_permission(user, action)
+        return super().check_custom_object_level_permission(user, action)

1984-2006: ⚠️ Potential issue | 🟠 Major

Fall back to base custom permission checks for non‑star actions.
The override returns None for other actions, which denies any other custom permission even if the user has it.

🛠️ Suggested fix
     def check_custom_object_level_permission(self, user, action: str) -> bool:
         project = self.get_project() if hasattr(self, "get_project") else None
         if action in ["star", "unstar"]:
             return user.has_perm(Project.Permissions.STAR_SOURCE_IMAGE, project)
+        return super().check_custom_object_level_permission(user, action)
🤖 Fix all issues with AI agents
In `@ami/base/permissions.py`:
- Around line 72-76: Guard against user being None and ensure
response_data["user_permissions"] is treated as a set before updating: if user
is None, replace it with an AnonymousUser instance (import from
django.contrib.auth.models.AnonymousUser) before calling
model.get_collection_level_permissions(user=..., project=...), and normalize
response_data.get("user_permissions") by coercing any existing list (or other
iterable) into a set before calling .update(); after merging
collection_level_perms into that set, store the result back as a list in
response_data["user_permissions"]. Use the surrounding identifiers
(response_data, model.get_collection_level_permissions, collection_level_perms,
user) to locate where to apply this change.

In `@ami/main/migrations/0079_assign_authorized_user_group.py`:
- Around line 57-65: In the backwards function remove the dead fetches: delete
the unused Permission = apps.get_model("auth", "Permission") line and stop
assigning project_ct from ContentType.objects.get; instead perform an existence
check (e.g. ContentType.objects.filter(app_label="main",
model="project").exists()) or call ContentType.objects.get without assigning to
a variable if you only need to test presence, and remove the
logger.info("Fetched ContentType for Project model.") line; keep the ContentType
lookup/exception handling logic but avoid storing unused variables (refer to
Permission and project_ct and the ContentType.objects.get call to locate edits).

In `@ami/ml/views.py`:
- Line 13: The status action currently fetches the instance via
ProcessingService.objects.get(pk=pk) and bypasses object-level checks; change it
to use self.get_object() inside the status view method so ObjectPermission (and
the model's check_permission()) runs just like register_pipelines does; locate
the status method in the view, replace the direct
ProcessingService.objects.get(...) usage with self.get_object() and ensure any
subsequent logic uses that returned instance.
🧹 Nitpick comments (3)
ami/main/models.py (1)

366-375: Silence the unused project parameter.
Rename to _project (or remove if the interface allows) to avoid AR003 without changing behavior.

♻️ Suggested tweak
-    def get_collection_level_permissions(
-        cls, user: AbstractUser | AnonymousUser, project: "Project | None" = None
-    ) -> list[str]:
+    def get_collection_level_permissions(
+        cls, user: AbstractUser | AnonymousUser, _project: "Project | None" = None
+    ) -> list[str]:
ami/users/signals.py (1)

44-44: Minor: Prefix unused variable with underscore.

The created variable from get_or_create is never used.

Proposed fix
-            group, created = Group.objects.get_or_create(name=role_cls.get_group_name())
+            group, _created = Group.objects.get_or_create(name=role_cls.get_group_name())
ami/main/migrations/0079_assign_authorized_user_group.py (1)

44-48: Consider using bulk operations for better performance.

The current implementation iterates over all users and calls user.groups.add(group) individually, which creates N database queries. For large user bases, using bulk operations would be more efficient.

Proposed improvement using through model
-    for idx, user in enumerate(User.objects.all().iterator(), start=1):
-        user.groups.add(group)
-        if idx % 100 == 0:
-            logger.info(f"Processed {idx}/{total_users} users...")
+    # Use bulk_create on the through model for better performance
+    UserGroups = User.groups.through
+    existing_memberships = set(
+        UserGroups.objects.filter(group=group).values_list('user_id', flat=True)
+    )
+    
+    new_memberships = [
+        UserGroups(user_id=user_id, group_id=group.id)
+        for user_id in User.objects.values_list('id', flat=True).iterator()
+        if user_id not in existing_memberships
+    ]
+    
+    UserGroups.objects.bulk_create(new_memberships, ignore_conflicts=True)
+    logger.info(f"Assigned 'AuthorizedUser' group to {len(new_memberships)} users.")

Comment on lines +72 to 76
logger.info(f"add_collection_level_permissions model {model.__name__}, {type(model)} ")
permissions = response_data.get("user_permissions", set())
if user and user.is_superuser:
permissions.add("create")
if user and project and f"create_{model.__name__.lower()}" in get_perms(user, project):
permissions.add("create")
collection_level_perms = model.get_collection_level_permissions(user=user, project=project)
permissions.update(collection_level_perms)
response_data["user_permissions"] = list(permissions)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's understand the function and its context
cd / && find . -name "permissions.py" -path "*/ami/base/*" 2>/dev/null | head -5

Repository: RolnickLab/antenna

Length of output: 105


🏁 Script executed:

#!/bin/bash
# Check the function signature and context around lines 72-76
cat -n ami/base/permissions.py | sed -n '50,100p'

Repository: RolnickLab/antenna

Length of output: 2715


🏁 Script executed:

#!/bin/bash
# Search for call sites of this function
rg -n "add_collection_level_permissions" --type py -C 3

Repository: RolnickLab/antenna

Length of output: 2021


🏁 Script executed:

#!/bin/bash
# Search for get_collection_level_permissions to understand its implementation
rg -n "def get_collection_level_permissions" --type py -A 5

Repository: RolnickLab/antenna

Length of output: 906


🏁 Script executed:

#!/bin/bash
# Check how user_permissions is set and used elsewhere
rg -n "user_permissions" ami/base/permissions.py -B 2 -A 2

Repository: RolnickLab/antenna

Length of output: 1057


🏁 Script executed:

#!/bin/bash
# Find all calls to add_collection_level_permissions
rg -n "add_collection_level_permissions\(" --type py -B 2 -A 2

Repository: RolnickLab/antenna

Length of output: 802


🏁 Script executed:

#!/bin/bash
# Check if user can be None in those call contexts
rg -n "self\.request\.user" ami/base/pagination.py -B 5 -A 5

Repository: RolnickLab/antenna

Length of output: 549


🏁 Script executed:

#!/bin/bash
# Check the get_collection_level_permissions implementation to see what happens with None
cat -n ami/base/permissions.py | sed -n '291,310p'

Repository: RolnickLab/antenna

Length of output: 1134


🏁 Script executed:

#!/bin/bash
# Check if there are any explicit user=None calls anywhere
rg -n "user=None" --type py | grep -i "collection\|permission"

Repository: RolnickLab/antenna

Length of output: 164


🏁 Script executed:

#!/bin/bash
# Check if add_collection_level_permissions could be called multiple times on the same response_data
rg -n "add_collection_level_permissions\|add_object_level_permissions" --type py -B 5 -A 5

Repository: RolnickLab/antenna

Length of output: 44


🏁 Script executed:

#!/bin/bash
# Check what super().get_paginated_response() returns to understand response_data initial state
rg -n "get_paginated_response" ami/base/pagination.py -A 15

Repository: RolnickLab/antenna

Length of output: 869


🏁 Script executed:

#!/bin/bash
# Verify if both add_object_level_permissions and add_collection_level_permissions are called on the same response_data
rg -n "add_object_level_permissions\|add_collection_level_permissions" --type py -C 5 | head -50

Repository: RolnickLab/antenna

Length of output: 44


Guard against user=None and normalize user_permissions to a set.

The function signature allows user: User | None, but model.get_collection_level_permissions() expects user: AbstractUser | AnonymousUser and will raise AttributeError if None is passed. Additionally, line 73 assumes response_data.get("user_permissions", set()) returns a set, but since line 76 stores it as a list, a subsequent call on the same response_data will fail when calling .update() on a list.

🛠️ Suggested fix
-    permissions = response_data.get("user_permissions", set())
-    collection_level_perms = model.get_collection_level_permissions(user=user, project=project)
+    permissions = set(response_data.get("user_permissions", []))
+    if user is None:
+        user = AnonymousUser()
+    collection_level_perms = model.get_collection_level_permissions(user=user, project=project)
     permissions.update(collection_level_perms)
🤖 Prompt for AI Agents
In `@ami/base/permissions.py` around lines 72 - 76, Guard against user being None
and ensure response_data["user_permissions"] is treated as a set before
updating: if user is None, replace it with an AnonymousUser instance (import
from django.contrib.auth.models.AnonymousUser) before calling
model.get_collection_level_permissions(user=..., project=...), and normalize
response_data.get("user_permissions") by coercing any existing list (or other
iterable) into a set before calling .update(); after merging
collection_level_perms into that set, store the result back as a list in
response_data["user_permissions"]. Use the surrounding identifiers
(response_data, model.get_collection_level_permissions, collection_level_perms,
user) to locate where to apply this change.

Comment on lines +57 to +65
Permission = apps.get_model("auth", "Permission")
ContentType = apps.get_model("contenttypes", "ContentType")

try:
project_ct = ContentType.objects.get(app_label="main", model="project")
logger.info("Fetched ContentType for Project model.")
except ContentType.DoesNotExist:
logger.warning("ContentType for Project model not found. Nothing to reverse.")
return
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove unused variables in backwards function.

Permission and project_ct are fetched but never used. This is dead code that should be cleaned up.

Proposed fix
 def backwards(apps, schema_editor):
     logger.info("Reversing migration: Removing 'AuthorizedUser' role from users...")

     User = apps.get_model("users", "User")
     Group = apps.get_model("auth", "Group")
-    Permission = apps.get_model("auth", "Permission")
-    ContentType = apps.get_model("contenttypes", "ContentType")
-
-    try:
-        project_ct = ContentType.objects.get(app_label="main", model="project")
-        logger.info("Fetched ContentType for Project model.")
-    except ContentType.DoesNotExist:
-        logger.warning("ContentType for Project model not found. Nothing to reverse.")
-        return

     try:
         group = Group.objects.get(name="AuthorizedUser")
📝 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
Permission = apps.get_model("auth", "Permission")
ContentType = apps.get_model("contenttypes", "ContentType")
try:
project_ct = ContentType.objects.get(app_label="main", model="project")
logger.info("Fetched ContentType for Project model.")
except ContentType.DoesNotExist:
logger.warning("ContentType for Project model not found. Nothing to reverse.")
return
User = apps.get_model("users", "User")
Group = apps.get_model("auth", "Group")
try:
group = Group.objects.get(name="AuthorizedUser")
🧰 Tools
🪛 Ruff (0.14.14)

[error] 57-57: Local variable Permission is assigned to but never used

Remove assignment to unused variable Permission

(F841)


[error] 61-61: Local variable project_ct is assigned to but never used

Remove assignment to unused variable project_ct

(F841)

🤖 Prompt for AI Agents
In `@ami/main/migrations/0079_assign_authorized_user_group.py` around lines 57 -
65, In the backwards function remove the dead fetches: delete the unused
Permission = apps.get_model("auth", "Permission") line and stop assigning
project_ct from ContentType.objects.get; instead perform an existence check
(e.g. ContentType.objects.filter(app_label="main", model="project").exists()) or
call ContentType.objects.get without assigning to a variable if you only need to
test presence, and remove the logger.info("Fetched ContentType for Project
model.") line; keep the ContentType lookup/exception handling logic but avoid
storing unused variables (refer to Permission and project_ct and the
ContentType.objects.get call to locate edits).

from rest_framework.request import Request
from rest_framework.response import Response

from ami.base.permissions import ObjectPermission
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find and inspect the ObjectPermission class
echo "=== ObjectPermission class ==="
rg -n "class ObjectPermission" -S -A 10

echo ""
echo "=== ProcessingServiceViewSet class and status action ==="
rg -n "class ProcessingServiceViewSet" -S -A 50 | head -80

echo ""
echo "=== status action in ami/ml/views.py ==="
rg -n "def status" -A 15 ami/ml/views.py

echo ""
echo "=== Permission class assignment in viewset ==="
rg -n "permission_classes" ami/ml/views.py

echo ""
echo "=== ProcessingService model ==="
rg -n "class ProcessingService" -S -A 10

Repository: RolnickLab/antenna

Length of output: 13078


Replace direct object fetch with self.get_object() in the status action to enforce object-level permissions.

The status action at line 185 bypasses permission checks by directly querying ProcessingService.objects.get(pk=pk), while the register_pipelines action at line 191 correctly uses self.get_object() to trigger ObjectPermission.has_object_permission(). Since ObjectPermission delegates authorization to the model's check_permission() method, the status action must follow the same pattern.

Suggested fix
-        processing_service = ProcessingService.objects.get(pk=pk)
+        processing_service = self.get_object()
🤖 Prompt for AI Agents
In `@ami/ml/views.py` at line 13, The status action currently fetches the instance
via ProcessingService.objects.get(pk=pk) and bypasses object-level checks;
change it to use self.get_object() inside the status view method so
ObjectPermission (and the model's check_permission()) runs just like
register_pipelines does; locate the status method in the view, replace the
direct ProcessingService.objects.get(...) usage with self.get_object() and
ensure any subsequent logic uses that returned instance.

@mihow
Copy link
Collaborator Author

mihow commented Jan 31, 2026

High-Level Architecture Review

The hybrid permission approach (model-level vs object-level based on get_project_accessor()) is reasonable and fits well with the existing architecture. It reuses Django Groups/Permissions, django-guardian, and maintains backward compatibility.

Observations to consider:

  1. Hardcoded app_label = "main" in check_model_level_permission() (ami/base/permissions.py:110) - Assumes all model-level permissions live in the main app. Could cause issues if permissions are needed for models in other apps.

  2. The split/rsplit inconsistency - Flagged in previous comment, needs fixing before merge.

  3. Multiple migrations for similar changes - Migrations 0079, 0080, 0081 all alter project options with permission changes. Could be consolidated if this hasn't shipped yet.

  4. Group name format difference - GlobalRole uses class name directly (e.g., "AuthorizedUser") while Role uses {project_pk}_{project_name}_{RoleName}. This is fine but could benefit from a comment documenting the convention.

  5. Permission sync on deploy - sync_group_permissions() needs to be called during migrations/deploys to keep permissions in sync. Worth verifying this works correctly in staging before production.

Overall the approach is sound - it doesn't require major refactoring, uses familiar Django patterns, and has good test coverage.

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.

Enable all users to create projects

2 participants