-
Notifications
You must be signed in to change notification settings - Fork 5.4k
fix(membership): block self-rejoin after admin removes a member #9663
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: preview
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -89,10 +89,13 @@ def create(self, request, slug, project_id): | |
| ): | ||
| project_member.role = member_roles[str(project_member.member_id)] | ||
| project_member.is_active = True | ||
| project_member.access_revoked = False | ||
| bulk_project_members.append(project_member) | ||
|
|
||
| # Update the roles of the existing members | ||
| ProjectMember.objects.bulk_update(bulk_project_members, ["is_active", "role"], batch_size=100) | ||
| ProjectMember.objects.bulk_update( | ||
| bulk_project_members, ["is_active", "role", "access_revoked"], batch_size=100 | ||
| ) | ||
|
|
||
| # Get the minimum sort_order for each member in the workspace | ||
| member_sort_orders = ( | ||
|
|
@@ -280,10 +283,17 @@ def partial_update(self, request, slug, project_id, pk): | |
| status=status.HTTP_403_FORBIDDEN, | ||
| ) | ||
|
|
||
| was_active = project_member.is_active | ||
| serializer = ProjectMemberSerializer(project_member, data=request.data, partial=True) | ||
|
|
||
| if serializer.is_valid(): | ||
| serializer.save() | ||
| if was_active and serializer.instance.is_active is False: | ||
| serializer.instance.access_revoked = True | ||
| serializer.instance.save(update_fields=["access_revoked", "updated_at"]) | ||
| elif (not was_active) and serializer.instance.is_active is True: | ||
| serializer.instance.access_revoked = False | ||
| serializer.instance.save(update_fields=["access_revoked", "updated_at"]) | ||
|
Comment on lines
+286
to
+296
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Make the reactivation path reachable. The query at Line 210 requires 🤖 Prompt for AI Agents🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Persist the status transition atomically.
🤖 Prompt for AI Agents |
||
| return Response(serializer.data, status=status.HTTP_200_OK) | ||
| return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) | ||
|
|
||
|
|
@@ -317,6 +327,7 @@ def destroy(self, request, slug, project_id, pk): | |
| ) | ||
|
|
||
| project_member.is_active = False | ||
| project_member.access_revoked = True | ||
| project_member.save() | ||
| return Response(status=status.HTTP_204_NO_CONTENT) | ||
|
|
||
|
|
@@ -343,8 +354,9 @@ def leave(self, request, slug, project_id): | |
| }, | ||
| status=status.HTTP_400_BAD_REQUEST, | ||
| ) | ||
| # Deactivate the user | ||
| # Deactivate the user (voluntary leave — public self-join still allowed) | ||
| project_member.is_active = False | ||
| project_member.access_revoked = False | ||
| project_member.save() | ||
| return Response(status=status.HTTP_204_NO_CONTENT) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -143,9 +143,10 @@ def destroy(self, request, slug, pk): | |||||||||
| # Deactivate the users from the projects where the user is part of | ||||||||||
| _ = ProjectMember.objects.filter( | ||||||||||
| workspace__slug=slug, member_id=workspace_member.member_id, is_active=True | ||||||||||
| ).update(is_active=False, updated_at=timezone.now()) | ||||||||||
| ).update(is_active=False, access_revoked=True, updated_at=timezone.now()) | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Revoke inactive project memberships during workspace removal. The Update all project memberships for the member in this workspace, not only active memberships. Proposed fix _ = ProjectMember.objects.filter(
- workspace__slug=slug, member_id=workspace_member.member_id, is_active=True
+ workspace__slug=slug, member_id=workspace_member.member_id
).update(is_active=False, access_revoked=True, updated_at=timezone.now())📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
|
|
||||||||||
| workspace_member.is_active = False | ||||||||||
| workspace_member.access_revoked = True | ||||||||||
| workspace_member.save() | ||||||||||
| return Response(status=status.HTTP_204_NO_CONTENT) | ||||||||||
|
|
||||||||||
|
|
@@ -199,8 +200,9 @@ def leave(self, request, slug): | |||||||||
| workspace__slug=slug, member_id=workspace_member.member_id, is_active=True | ||||||||||
| ).update(is_active=False, updated_at=timezone.now()) | ||||||||||
|
|
||||||||||
| # # Deactivate the user | ||||||||||
| # # Deactivate the user (voluntary leave) | ||||||||||
| workspace_member.is_active = False | ||||||||||
| workspace_member.access_revoked = False | ||||||||||
| workspace_member.save() | ||||||||||
| return Response(status=status.HTTP_204_NO_CONTENT) | ||||||||||
|
|
||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| # Copyright (c) 2023-present Plane Software, Inc. and contributors | ||
| # SPDX-License-Identifier: AGPL-3.0-only | ||
| # See the LICENSE file for details. | ||
|
|
||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ("db", "0122_alter_draftissue_assignees_alter_issue_assignees_and_more"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name="projectmember", | ||
| name="access_revoked", | ||
| field=models.BooleanField(default=False), | ||
| ), | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| # Copyright (c) 2023-present Plane Software, Inc. and contributors | ||
| # SPDX-License-Identifier: AGPL-3.0-only | ||
| # See the LICENSE file for details. | ||
|
|
||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ("db", "0123_projectmember_access_revoked"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name="workspacemember", | ||
| name="access_revoked", | ||
| field=models.BooleanField(default=False), | ||
| ), | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| # Copyright (c) 2023-present Plane Software, Inc. and contributors | ||
| # SPDX-License-Identifier: AGPL-3.0-only | ||
| # See the LICENSE file for details. | ||
|
|
||
| from django.db import migrations | ||
|
|
||
|
|
||
| def mark_inactive_members_revoked(apps, schema_editor): | ||
| WorkspaceMember = apps.get_model("db", "WorkspaceMember") | ||
| ProjectMember = apps.get_model("db", "ProjectMember") | ||
| WorkspaceMember.objects.filter(is_active=False).update(access_revoked=True) | ||
| ProjectMember.objects.filter(is_active=False).update(access_revoked=True) | ||
|
|
||
|
|
||
| def noop_reverse(apps, schema_editor): | ||
| pass | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ("db", "0124_workspacemember_access_revoked"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.RunPython(mark_inactive_members_revoked, noop_reverse), | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| # Copyright (c) 2023-present Plane Software, Inc. and contributors | ||
| # SPDX-License-Identifier: AGPL-3.0-only | ||
| # See the LICENSE file for details. | ||
|
|
||
| import pytest | ||
|
|
||
| from plane.db.models import ProjectMember | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_project_member_has_access_revoked_field(): | ||
| field = ProjectMember._meta.get_field("access_revoked") | ||
| assert field.default is False |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Clear the workspace revocation flag during project invitation acceptance.
When an existing inactive
WorkspaceMemberis reactivated at Lines 271-273, the code does not setworkspace_member.access_revoked = False. The project membership is restored, but the workspace membership remains marked revoked. Clear both flags in the same save.Proposed fix
else: # Else make him active workspace_member.is_active = True + workspace_member.access_revoked = False workspace_member.save()🤖 Prompt for AI Agents