Skip to content

Stop creating duplicated roles when description is changed - #8391

Merged
melton-jason merged 8 commits into
mainfrom
fix_duplicated_roles
Aug 4, 2026
Merged

Stop creating duplicated roles when description is changed#8391
melton-jason merged 8 commits into
mainfrom
fix_duplicated_roles

Conversation

@melton-jason

@melton-jason melton-jason commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Fixes #8390

A more ideal solution would probably be to have a uniqueness rule defined for: SpRole must have unique Name in Collection, and/or ever better a constraint at the database level to enforce uniqueness.

Checklist

  • Self-review the PR after opening it to make sure the changes look good and
    self-explanatory (or properly documented)
  • Add relevant issue to release milestone
  • Add pr to documentation list
  • Add automated tests
  • Add a reverse migration if a migration is present in the PR
  • Add migration function to
    def fix_schema_config(stdout: WriteToStdOut | None = None):

Testing instructions

  • In Security and Accounts for a specific Collection, modify the Description of the Read Only - Legacy, Full Access - Legacy, or Collection Admin Roles
  • Restart the Specify instance or manually run the run_key_migration_functions command
  • For each Role where the description was changed, ensure that only one Role with the same name exists in the Collection and the modified description is preserved

Summary by CodeRabbit

  • Bug Fixes

    • Improved case-insensitive matching when initializing library roles.
    • Ensured missing library and collection roles are created with their configured policies.
    • Improved assignment of user types to existing roles and prevented unnecessary duplicate roles.
  • Refactor

    • Streamlined role and policy initialization for more consistent permission setup.
    • Centralized role definitions and legacy user-type mappings for more reliable permission management.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

Warning

One or more dependencies are approaching or past End-of-Life.
Please plan upgrades accordingly.

STATUS=WARNING
NODE_VERSION=20
NODE_CYCLE=20
EOL_DATE=2026-04-30
DAYS_REMAINING=-96

--- Node.js ---
Version: 20
EOL: 2026-04-30
Status: WARNING

STATUS=OK
PYTHON_VERSION=3.12
PYTHON_CYCLE=3.12
EOL_DATE=2028-10-31
DAYS_REMAINING=819

--- Python ---
Version: 3.12
EOL: 2028-10-31
Status: OK

STATUS=WARNING
DJANGO_VERSION=4.2
DJANGO_CYCLE=4.2
EOL_DATE=2026-04-07
DAYS_REMAINING=-119

--- Django ---
Version: 4.2
EOL: 2026-04-07
Status: WARNING


@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@melton-jason, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 56 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 92e94387-c61b-4f0e-98fc-02f4243fb2af

📥 Commits

Reviewing files that changed from the base of the PR and between 30edfc0 and ec9550c.

📒 Files selected for processing (1)
  • specifyweb/backend/permissions/initialize.py
📝 Walkthrough

Walkthrough

Role initialization now uses declarative library-role definitions, shared user-type mappings, case-insensitive role lookup, bulk policy creation, and shared handling for library and collection roles.

Changes

Role initialization

Layer / File(s) Summary
User-type role assignment
specifyweb/backend/permissions/initialize.py
User-role assignment uses the shared user-type mapping, reuses the earliest matching collection role, and creates a role only when no match exists.
Declarative role definitions and policy creation
specifyweb/backend/permissions/initialize.py
Declarative definitions describe role policies. A shared helper resolves or creates roles and bulk-creates policies for new roles.
Library and collection role initialization
specifyweb/backend/permissions/initialize.py
Library roles use case-insensitive matching. create_roles initializes library roles, derives available user types, creates legacy collection roles when required, and creates collection-admin roles through the shared helper.

Sequence Diagram(s)

sequenceDiagram
  participant create_roles
  participant create_missing_library_roles
  participant _create_role_and_policies
  participant Role
  participant RolePolicy
  create_roles->>create_missing_library_roles: initialize configured library roles
  create_missing_library_roles->>Role: case-insensitive role lookup
  create_missing_library_roles->>_create_role_and_policies: create missing role
  _create_role_and_policies->>Role: create or reuse role
  _create_role_and_policies->>RolePolicy: bulk-create policies
  create_roles->>_create_role_and_policies: create collection roles
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Automatic Tests ⚠️ Warning The PR changes role initialization but adds no test files or assertions; existing permission tests do not cover duplicate-role handling or description preservation. Add automated tests that run initialization with modified legacy-role descriptions and assert one role per collection, earliest-role reuse, and preserved descriptions.
Testing Instructions ❓ Inconclusive Custom check execution failed before a final verdict was produced. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the main objective: preventing duplicate roles when descriptions change during migration.
Linked Issues check ✅ Passed The changes implement the core requirement to prevent duplicate default roles when descriptions are modified and run_key_migration_functions executes.
Out of Scope Changes check ✅ Passed All changes focus on role initialization and duplicate prevention; no unrelated modifications are present.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix_duplicated_roles

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (4)
specifyweb/backend/permissions/initialize.py (4)

391-391: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace the mutable default argument.

Ruff flags role_filters: dict = dict() (B006). The dictionary is not mutated today, but a default of None removes the hazard. Please also add a return type annotation.

♻️ Proposed fix
-def _create_role_and_policies(role_model, role_policy_model, role_name: str, role_filters: dict = dict()):
+def _create_role_and_policies(role_model, role_policy_model, role_name: str, role_filters: Optional[dict] = None):
+    role_filters = role_filters or {}
🤖 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 `@specifyweb/backend/permissions/initialize.py` at line 391, Update
_create_role_and_policies to use None instead of the mutable dict() default for
role_filters, normalize it to an empty dictionary inside the function before
use, and add an appropriate return type annotation.

Source: Linters/SAST tools


450-476: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop the redundant .get() fallbacks.

_USERTYPES_TO_ROLE_NAMES defines Guest, FullAccess, and Manager at Lines 83-88, so the fallback values never apply. They also hide a real failure: if a mapping key is ever renamed, the fallback name is looked up in LIBRARY_ROLES and raises KeyError inside _create_role_and_policies. Use direct subscripts for a clearer failure point.

🤖 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 `@specifyweb/backend/permissions/initialize.py` around lines 450 - 476, The
role creation loop should use direct subscripting for the Guest, FullAccess, and
Manager entries in _USERTYPES_TO_ROLE_NAMES instead of .get() fallbacks. Update
each _create_role_and_policies call while preserving the existing conditional
behavior and role creation flow.

445-448: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Materialize user_types once.

user_types is a lazy queryset. Each in test on Lines 447-448 runs a separate SELECT DISTINCT. Wrap the query in a set to run it once.

♻️ Proposed fix
-    user_types = Specifyuser.objects.all().values_list("usertype", flat=True).distinct()
+    user_types = set(Specifyuser.objects.values_list("usertype", flat=True).distinct())
🤖 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 `@specifyweb/backend/permissions/initialize.py` around lines 445 - 448,
Materialize the lazy queryset assigned to user_types by wrapping the distinct
values_list result in a set, so the subsequent has_guest and has_full_access
membership checks reuse one fetched collection and issue only one query.

133-156: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider sourcing the descriptions from LIBRARY_ROLES.

The descriptions in ROLE_DESCRIPTIONS (Lines 97-102) duplicate the descriptions in LIBRARY_ROLES (Lines 346-388). If the two copies drift, a newly created collection role gets a different description than the same role created by create_roles. Resolve the description through _USERTYPES_TO_ROLE_NAMES and LIBRARY_ROLES instead.

Also, the fallback f"{user_type} - {collection_name}" on Line 136 is unreachable because Line 133 already rejects unmapped user types. A plain subscript is clearer.

♻️ Proposed simplification
-        role_name = _USERTYPES_TO_ROLE_NAMES.get(user_type, f"{user_type} - {collection_name}")
-        role_description = ROLE_DESCRIPTIONS.get(user_type, "No description available.")
+        role_name = _USERTYPES_TO_ROLE_NAMES[user_type]
+        role_description = LIBRARY_ROLES[role_name]["description"]
🤖 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 `@specifyweb/backend/permissions/initialize.py` around lines 133 - 156, Update
the role initialization block to use a direct subscript on
_USERTYPES_TO_ROLE_NAMES for role_name, removing the unreachable fallback.
Resolve role_description from the corresponding role entry in LIBRARY_ROLES
using the mapped role name, and stop using the duplicated ROLE_DESCRIPTIONS
mapping so descriptions remain consistent with create_roles.
🤖 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 `@specifyweb/backend/permissions/initialize.py`:
- Around line 421-427: Update the role lookup before missing_roles so the __in
operand is a materialized list of lowercased all_roles values, and normalize the
returned existing_role_names to lowercase before computing the set difference.
Ensure missing_roles compares normalized names case-insensitively while
preserving the existing role creation flow.
- Around line 403-414: Update the role policy creation comprehension in the
role-initialization function to iterate directly over the resource/action
mapping in resolved_role["policies"], producing each resource and action without
calling items() on individual resource strings. Also return the created role
after bulk_create, matching the existing reuse path so every execution returns
role.
- Around line 393-401: Update role creation in create_roles to avoid
get_or_create, which fails when duplicate matching roles exist. Query matching
roles ordered by the earliest record, reuse the first role when present, and
create a new role with the existing defaults only when none match, preserving
the current role_filters and return behavior.

---

Nitpick comments:
In `@specifyweb/backend/permissions/initialize.py`:
- Line 391: Update _create_role_and_policies to use None instead of the mutable
dict() default for role_filters, normalize it to an empty dictionary inside the
function before use, and add an appropriate return type annotation.
- Around line 450-476: The role creation loop should use direct subscripting for
the Guest, FullAccess, and Manager entries in _USERTYPES_TO_ROLE_NAMES instead
of .get() fallbacks. Update each _create_role_and_policies call while preserving
the existing conditional behavior and role creation flow.
- Around line 445-448: Materialize the lazy queryset assigned to user_types by
wrapping the distinct values_list result in a set, so the subsequent has_guest
and has_full_access membership checks reuse one fetched collection and issue
only one query.
- Around line 133-156: Update the role initialization block to use a direct
subscript on _USERTYPES_TO_ROLE_NAMES for role_name, removing the unreachable
fallback. Resolve role_description from the corresponding role entry in
LIBRARY_ROLES using the mapped role name, and stop using the duplicated
ROLE_DESCRIPTIONS mapping so descriptions remain consistent with create_roles.
🪄 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: 8f57a289-3f49-4bc6-ae0b-3c6b360ae4a1

📥 Commits

Reviewing files that changed from the base of the PR and between f5911c1 and 0107ce7.

📒 Files selected for processing (1)
  • specifyweb/backend/permissions/initialize.py

Comment thread specifyweb/backend/permissions/initialize.py Outdated
Comment thread specifyweb/backend/permissions/initialize.py
Comment thread specifyweb/backend/permissions/initialize.py Outdated
@github-project-automation github-project-automation Bot moved this from 📋Back Log to Dev Attention Needed in General Tester Board Aug 3, 2026
Comment thread specifyweb/backend/permissions/initialize.py Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
specifyweb/backend/permissions/initialize.py (2)

396-406: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

get_or_create still fails with MultipleObjectsReturned on pre-existing duplicates.

This diff only adds the DefaultRole type annotation at line 397. The get_or_create call at lines 398-404 is unchanged. get_or_create calls get() first, so a collection that already holds two rows matching role_name and role_filters raises MultipleObjectsReturned. Since this PR's objective is to fix duplicate default roles including cases where duplicates already exist, this call will crash run_key_migration_functions on any database that already has the duplication this PR is meant to resolve. assign_users_to_roles (Lines 147-150) already uses the safer pattern of filtering, ordering by pk, and taking the first match.

🐛 Proposed fix
     resolved_role: DefaultRole = LIBRARY_ROLES[role_name]
-    role, is_new = role_model.objects.get_or_create(
-        name=role_name,
-        **role_filters,
-        defaults={
-            "description": resolved_role["description"]
-        }
-    )
-    if not is_new:
-        return role
+    role = role_model.objects.filter(
+        name=role_name,
+        **role_filters
+    ).order_by("pk").first()
+    if role is not None:
+        return role
+
+    role = role_model.objects.create(
+        name=role_name,
+        description=resolved_role["description"],
+        **role_filters
+    )
🤖 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 `@specifyweb/backend/permissions/initialize.py` around lines 396 - 406, Update
_create_role_and_policies to avoid get_or_create when matching duplicate roles
may already exist: filter by role_name and role_filters, order by pk, and reuse
the first matching role. Only create a new role with the resolved description
when no match exists, preserving the existing return behavior and aligning with
assign_users_to_roles.

408-418: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Policy iteration is fixed, but the "newly created role" branch still returns None implicitly.

Line 415 now correctly iterates resolved_role["policies"].items(), fixing the AttributeError flagged in the previous review. However, after bulk_create runs, the function has no explicit return role for the branch where a role is newly created. The function returns role only via the early return at line 406 when reusing an existing role; the "created" path falls through and returns None. CodeQL also flags this at line 396 ("Explicit returns mixed with implicit (fall through) returns"). Any caller in create_roles that expects a Role object back from this helper after creating a new role receives None instead.

🐛 Proposed fix
     role_policy_model.objects.bulk_create(
         [
             role_policy_model(
                 role=role,
                 resource=resource,
                 action=action
             )
             for resource, actions in resolved_role["policies"].items()
             for action in actions
         ]
     )
+    return role
🤖 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 `@specifyweb/backend/permissions/initialize.py` around lines 408 - 418, Update
the newly created role path in the role-creation helper to explicitly return
role after bulk_create completes. Preserve the existing early return for reused
roles and ensure every branch returns the Role object expected by create_roles,
eliminating the implicit fall-through.
🤖 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.

Duplicate comments:
In `@specifyweb/backend/permissions/initialize.py`:
- Around line 396-406: Update _create_role_and_policies to avoid get_or_create
when matching duplicate roles may already exist: filter by role_name and
role_filters, order by pk, and reuse the first matching role. Only create a new
role with the resolved description when no match exists, preserving the existing
return behavior and aligning with assign_users_to_roles.
- Around line 408-418: Update the newly created role path in the role-creation
helper to explicitly return role after bulk_create completes. Preserve the
existing early return for reused roles and ensure every branch returns the Role
object expected by create_roles, eliminating the implicit fall-through.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9d571de9-3250-407a-9fc0-ce22aba16809

📥 Commits

Reviewing files that changed from the base of the PR and between 0107ce7 and 1bd8a77.

📒 Files selected for processing (1)
  • specifyweb/backend/permissions/initialize.py

@melton-jason
melton-jason requested a review from a team August 4, 2026 15:07
@melton-jason
melton-jason merged commit 752d6fe into main Aug 4, 2026
24 checks passed
@melton-jason
melton-jason deleted the fix_duplicated_roles branch August 4, 2026 18:01
@github-project-automation github-project-automation Bot moved this from Dev Attention Needed to ✅Done in General Tester Board Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: ✅Done

Development

Successfully merging this pull request may close these issues.

Duplicate Default Roles being created by run_key_migration_functions

3 participants