Skip to content

Fix: assign BasicMember role in members API so members can access draft project details#1140

Merged
mihow merged 4 commits intomainfrom
fix/members-api-draft-project-access
Apr 1, 2026
Merged

Fix: assign BasicMember role in members API so members can access draft project details#1140
mihow merged 4 commits intomainfrom
fix/members-api-draft-project-access

Conversation

@mohamedelabbas1996
Copy link
Copy Markdown
Contributor

@mohamedelabbas1996 mohamedelabbas1996 commented Feb 19, 2026

Summary

Members added via the Members API received 403 errors when viewing draft project details. Draft permission checks use BasicMember.has_role(), but the API only assigned the requested role (Identifier, Researcher, etc.) without also assigning BasicMember.

List of Changes

  • perform_create and perform_update in UserProjectMembershipViewSet now assign BasicMember alongside the requested role (skipped when the requested role is already BasicMember)
  • perform_update revokes roles from the old user when a membership's user is changed, preventing permission leaks
  • perform_destroy revokes all roles (including BasicMember) before deleting the membership
  • Added TestMembersApiDraftProjectAccess with 7 tests covering:
    • All 4 roles (BasicMember, Identifier, Researcher, ProjectManager) can access draft projects after creation
    • Role update retains draft access
    • Membership deletion revokes draft access
    • Non-members cannot access draft projects

How to Test

docker compose -f docker-compose.ci.yml run --rm django python manage.py test ami.users.tests.test_membership_management_api.TestMembersApiDraftProjectAccess --keepdb

Checklist

  • I have tested these changes appropriately.
  • I have added and/or modified relevant tests.
  • I updated relevant documentation or comments.
  • I have verified that this PR follows the project's coding standards.
  • Any dependent changes have already been merged to main.

Summary by CodeRabbit

  • New Features

    • Added two new user roles: Identifier and Researcher.
    • Members with assigned roles can access draft project details.
  • Bug Fixes

    • Membership role changes and removals now correctly update a user’s access to draft projects.
  • Tests

    • Added tests covering draft project access for role assignment, updates, and deletions.

…ft project details

Co-authored-by: Cursor <cursoragent@cursor.com>
@mohamedelabbas1996 mohamedelabbas1996 self-assigned this Feb 19, 2026
@netlify
Copy link
Copy Markdown

netlify Bot commented Feb 19, 2026

Deploy Preview for antenna-preview canceled.

Name Link
🔨 Latest commit c3c03b6
🔍 Latest deploy log https://app.netlify.com/projects/antenna-preview/deploys/69cc8645b9bcda00088e143b

@netlify
Copy link
Copy Markdown

netlify Bot commented Feb 19, 2026

Deploy Preview for antenna-ssec canceled.

Name Link
🔨 Latest commit c3c03b6
🔍 Latest deploy log https://app.netlify.com/projects/antenna-ssec/deploys/69cc8645b5c7870008b445ec

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 19, 2026

📝 Walkthrough

Walkthrough

Membership view logic now unassigns all role subclasses then assigns the chosen role and conditionally ensures BasicMember is also assigned; tests were added to validate draft-project access for various role types and membership lifecycle operations.

Changes

Cohort / File(s) Summary
API Role Assignment
ami/users/api/views.py
Imports BasicMember; perform_create/perform_update/perform_destroy now unassign all Role subclasses (including BasicMember), assign the selected role_cls, and conditionally add BasicMember when role_cls is not BasicMember. perform_update handles previous membership user revocation when user changes.
Draft Project Access Tests
ami/users/tests/test_membership_management_api.py
Added TestMembersApiDraftProjectAccess, helper _add_member_and_assert_can_access_draft, and tests that create/update/delete memberships with roles (BasicMember, Identifier, Researcher, ProjectManager) to assert draft-project access rules for members vs non-members.
Role utilities usage
ami/users/...roles*
Tests import additional public role symbols (Identifier, Researcher) and create_roles_for_project to programmatically generate project roles for draft-access scenarios.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant MembershipView as "MembershipView\n(api/views.py)"
    participant RoleManager as "RoleManager\n(unassign/assign helpers)"
    participant DB as "Database\n(memberships, roles)"

    Client->>MembershipView: POST /members/ (create) with role_id, user
    MembershipView->>RoleManager: unassign_all_role_subclasses(old_user?)
    RoleManager->>DB: remove role assignments
    MembershipView->>RoleManager: assign role_cls to user
    RoleManager->>DB: add role assignment
    alt role_cls != BasicMember
        MembershipView->>RoleManager: assign BasicMember to user
        RoleManager->>DB: add BasicMember assignment
    end
    RoleManager-->>MembershipView: success
    MembershipView-->>Client: 201 Created

    Note over MembershipView,RoleManager: update flow additionally revokes roles from prior membership.user when user changed

    Client->>MembershipView: DELETE /members/{id}
    MembershipView->>RoleManager: unassign_all_role_subclasses(target_user incl BasicMember)
    RoleManager->>DB: remove assignments
    MembershipView->>DB: delete membership
    MembershipView-->>Client: 204 No Content
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 I hopped through code with a carrot and pen,
I nudged the roles, then nudged them again,
BasicMember now joins every team,
Drafts open wide like a sunlit stream,
Hop, hop—tests pass, let's nibble some zen.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.77% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately summarizes the main fix: assigning BasicMember role in the Members API to enable draft project access.
Description check ✅ Passed The PR description covers the summary, list of changes, test instructions, and checklist. However, it lacks sections for related issues, detailed description with side effects/risks, deployment notes, and screenshots.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/members-api-draft-project-access

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.

Copy link
Copy Markdown
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: 1

🧹 Nitpick comments (2)
ami/users/api/views.py (1)

63-66: Redundant BasicMember.assign_user call when role_cls is BasicMember

When the requested role_id maps to BasicMember, lines 65–66 (and 90–91 in perform_update) both call BasicMember.assign_user(user, project) back-to-back — the second call is a no-op due to Django's idempotent M2M add, but it incurs an extra DB round-trip on every BasicMember membership creation or update.

♻️ Suggested fix (apply to both perform_create and perform_update)
-            role_cls.assign_user(user, project)
-            BasicMember.assign_user(user, project)
+            role_cls.assign_user(user, project)
+            if role_cls is not BasicMember:
+                BasicMember.assign_user(user, project)

Also applies to: 88-91

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ami/users/api/views.py` around lines 63 - 66, The code unconditionally calls
BasicMember.assign_user(user, project) after assigning role_cls, causing a
redundant DB round-trip when role_cls is BasicMember; update both perform_create
and perform_update to skip the extra call by checking if role_cls is not
BasicMember (e.g., if role_cls is not BasicMember: BasicMember.assign_user(user,
project)) or otherwise only call BasicMember.assign_user when role_cls differs,
leaving the Role.__subclasses__ unassignment and role_cls.assign_user(user,
project) logic unchanged.
ami/users/tests/test_membership_management_api.py (1)

270-297: Test coverage gaps: perform_update and perform_destroy paths not exercised for draft access

The four parameterised tests only exercise perform_create (POST → assert 200). Two additional paths are untested:

  1. Update path — after a role change via PATCH, the member should retain draft access (verifies perform_update re-assigns BasicMember).
  2. Delete path — after membership deletion, the user should lose access (verifies perform_destroy correctly strips all roles including BasicMember).
💡 Suggested additions
def test_member_role_update_retains_draft_access(self):
    """After a role change via API, member should still access draft project."""
    self._add_member_and_assert_can_access_draft(self.user_basic, BasicMember.__name__)

    self.client.force_authenticate(self.superuser)
    membership = UserProjectMembership.objects.get(project=self.project, user=self.user_basic)
    update_url = f"{self.members_url}{membership.pk}/"
    resp = self.client.patch(update_url, {"role_id": Researcher.__name__}, format="json")
    self.assertEqual(resp.status_code, 200)

    self.client.force_authenticate(self.user_basic)
    detail_resp = self.client.get(self.detail_url)
    self.assertEqual(detail_resp.status_code, 200, "Member should retain draft access after role update")


def test_deleted_member_cannot_access_draft_project(self):
    """After membership deletion, former member should lose draft access."""
    self._add_member_and_assert_can_access_draft(self.user_basic, BasicMember.__name__)

    self.client.force_authenticate(self.superuser)
    membership = UserProjectMembership.objects.get(project=self.project, user=self.user_basic)
    delete_url = f"{self.members_url}{membership.pk}/"
    self.client.delete(delete_url)

    self.client.force_authenticate(self.user_basic)
    detail_resp = self.client.get(self.detail_url)
    self.assertIn(detail_resp.status_code, (403, 404), "Removed member should not access draft project")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ami/users/tests/test_membership_management_api.py` around lines 270 - 297,
The tests only exercise perform_create; add two tests to cover perform_update
and perform_destroy: 1) implement test_member_role_update_retains_draft_access
that uses _add_member_and_assert_can_access_draft(self.user_basic,
BasicMember.__name__), then as self.superuser fetch the membership via
UserProjectMembership.objects.get(project=self.project, user=self.user_basic),
PATCH the membership at f"{self.members_url}{membership.pk}/" to change role_id
to Researcher.__name__ and assert 200, then authenticate as the user and assert
GET to self.detail_url returns 200 (verifying perform_update reassigns
BasicMember); 2) implement test_deleted_member_cannot_access_draft_project that
adds the member the same way, then as self.superuser delete the membership at
f"{self.members_url}{membership.pk}/", then authenticate as the user and assert
GET to self.detail_url returns 403 or 404 (verifying perform_destroy removes
draft access).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ami/users/tests/test_membership_management_api.py`:
- Line 260: Ruff flags the hardcoded password literal used when creating the
superuser (User.objects.create_superuser(password="x")), so replace the literal
with a single shared test password constant (e.g., TEST_PASSWORD = "x") defined
at the top of the test module and use that constant in
TestUserProjectMembershipAPI.setUp and here, or alternatively add a local noqa
comment to silence S106 for this line; update usages to reference TEST_PASSWORD
and ensure create_superuser(...) uses that symbol instead of the raw string.

---

Nitpick comments:
In `@ami/users/api/views.py`:
- Around line 63-66: The code unconditionally calls
BasicMember.assign_user(user, project) after assigning role_cls, causing a
redundant DB round-trip when role_cls is BasicMember; update both perform_create
and perform_update to skip the extra call by checking if role_cls is not
BasicMember (e.g., if role_cls is not BasicMember: BasicMember.assign_user(user,
project)) or otherwise only call BasicMember.assign_user when role_cls differs,
leaving the Role.__subclasses__ unassignment and role_cls.assign_user(user,
project) logic unchanged.

In `@ami/users/tests/test_membership_management_api.py`:
- Around line 270-297: The tests only exercise perform_create; add two tests to
cover perform_update and perform_destroy: 1) implement
test_member_role_update_retains_draft_access that uses
_add_member_and_assert_can_access_draft(self.user_basic, BasicMember.__name__),
then as self.superuser fetch the membership via
UserProjectMembership.objects.get(project=self.project, user=self.user_basic),
PATCH the membership at f"{self.members_url}{membership.pk}/" to change role_id
to Researcher.__name__ and assert 200, then authenticate as the user and assert
GET to self.detail_url returns 200 (verifying perform_update reassigns
BasicMember); 2) implement test_deleted_member_cannot_access_draft_project that
adds the member the same way, then as self.superuser delete the membership at
f"{self.members_url}{membership.pk}/", then authenticate as the user and assert
GET to self.detail_url returns 403 or 404 (verifying perform_destroy removes
draft access).

Comment thread ami/users/tests/test_membership_management_api.py
@mohamedelabbas1996 mohamedelabbas1996 marked this pull request as draft February 19, 2026 18:49
@mihow mihow added the next up! label Mar 26, 2026
@mihow mihow marked this pull request as ready for review April 1, 2026 02:14
Copilot AI review requested due to automatic review settings April 1, 2026 02:14
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes a permissions gap where members added/updated via the Members API could not view draft project details because they were missing the BasicMember role required by draft permission checks.

Changes:

  • Assign BasicMember in UserProjectMembershipViewSet.perform_create and perform_update after assigning the requested role.
  • Ensure perform_destroy revokes all role groups before deleting the membership.
  • Add regression tests asserting members created via the API (across multiple roles) can access draft project details.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
ami/users/api/views.py Ensures API-managed memberships also get BasicMember so draft-project retrieve permission checks succeed.
ami/users/tests/test_membership_management_api.py Adds regression coverage for draft project access for members added via the Members API across roles.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ami/users/api/views.py Outdated
@mihow
Copy link
Copy Markdown
Collaborator

mihow commented Apr 1, 2026

Claude says: Addressed the review feedback in the latest push:

  1. Redundant BasicMember.assign_user call (CodeRabbit nitpick) — Added if role_cls is not BasicMember guard in both perform_create and perform_update.

  2. Test coverage gaps for update/delete paths (CodeRabbit nitpick) — Added test_member_role_update_retains_draft_access and test_deleted_member_cannot_access_draft_project.

  3. Old user retains roles on user change (Copilot) — perform_update now captures old_user before reassignment and revokes all roles from the old user when the user changes.

All 7 tests pass.

- Revoke old user's roles in perform_update when membership user changes
- Skip redundant BasicMember.assign_user when role_cls is already BasicMember
- Add tests for role update retaining draft access and deletion revoking it

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Copy Markdown
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: 1

🧹 Nitpick comments (1)
ami/users/tests/test_membership_management_api.py (1)

287-337: Exercise the membership-reassignment branch too.

These tests cover create, delete, and same-user role changes, but they never hit the new perform_update() path where membership.user changes and the old user's roles are revoked. Please add a PATCH that reassigns the membership to a different user and asserts the previous user loses draft access while the replacement user keeps it. If this suite is meant to cover all supported roles, add MLDataManager here as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ami/users/tests/test_membership_management_api.py` around lines 287 - 337,
Add a new assertion branch that exercises the membership-reassignment path (the
perform_update that changes membership.user): after creating a membership via
_add_member_and_assert_can_access_draft (as in
test_member_role_update_retains_draft_access), authenticate as self.superuser,
fetch the UserProjectMembership for self.user_basic, then PATCH
f"{self.members_url}{membership.pk}/" with a payload that reassigns "user" to a
different user (e.g. self.user_identifier.pk) and optionally includes "role_id"
(to cover role-preserving reassignment), assert 200, then authenticate as the
original user and assert they now get 403/404 on self.detail_url, and
authenticate as the new user and assert they get 200; also ensure MLDataManager
is exercised either by adding a separate variant calling
_add_member_and_assert_can_access_draft(self.user_ml_data_manager,
MLDataManager.__name__) or by including MLDataManager in the tested roles so the
reassignment case covers that role too.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ami/users/api/views.py`:
- Around line 60-70: The view is temporarily disconnecting the global
m2m_changed receiver (manage_project_membership), which causes race conditions;
instead, implement a request-scoped skip flag (e.g., a thread-local or
contextvar) that manage_project_membership checks and returns early when set,
and set/clear that flag around the role update block (the section using
Role.__subclasses__(), role_cls.assign_user, BasicMember.assign_user and
touching Group.user_set.through) so you avoid m2m_changed.disconnect/reconnect
while keeping the signal registered globally.

---

Nitpick comments:
In `@ami/users/tests/test_membership_management_api.py`:
- Around line 287-337: Add a new assertion branch that exercises the
membership-reassignment path (the perform_update that changes membership.user):
after creating a membership via _add_member_and_assert_can_access_draft (as in
test_member_role_update_retains_draft_access), authenticate as self.superuser,
fetch the UserProjectMembership for self.user_basic, then PATCH
f"{self.members_url}{membership.pk}/" with a payload that reassigns "user" to a
different user (e.g. self.user_identifier.pk) and optionally includes "role_id"
(to cover role-preserving reassignment), assert 200, then authenticate as the
original user and assert they now get 403/404 on self.detail_url, and
authenticate as the new user and assert they get 200; also ensure MLDataManager
is exercised either by adding a separate variant calling
_add_member_and_assert_can_access_draft(self.user_ml_data_manager,
MLDataManager.__name__) or by including MLDataManager in the tested roles so the
reassignment case covers that role too.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: baad5f32-6111-4c63-bf04-668dc9f55e18

📥 Commits

Reviewing files that changed from the base of the PR and between 02154ad and c3c03b6.

📒 Files selected for processing (2)
  • ami/users/api/views.py
  • ami/users/tests/test_membership_management_api.py

Comment thread ami/users/api/views.py
@mihow mihow merged commit ed8f857 into main Apr 1, 2026
7 checks passed
@mihow mihow deleted the fix/members-api-draft-project-access branch April 1, 2026 03:02
@mihow
Copy link
Copy Markdown
Collaborator

mihow commented Apr 1, 2026

Thanks @mohamedelabbas1996 !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants