Skip to content
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

feat(organizations): add endpoints to handle organization members TASK-963 #5235

Merged
merged 24 commits into from
Nov 22, 2024

Conversation

rajpatel24
Copy link
Contributor

@rajpatel24 rajpatel24 commented Nov 6, 2024

🗒️ Checklist

  1. run linter locally
  2. update all related docs (API, README, inline, etc.), if any
  3. draft PR with a title <type>(<scope>)<!>: <title> TASK-1234
  4. tag PR: at least frontend or backend unless it's global
  5. fill in the template below and delete template comments
  6. review thyself: read the diff and repro the preview as written
  7. open PR & confirm that CI passes
  8. request reviewers, if needed
  9. delete this section before merging

📣 Summary

  • Enhance the organization member management API to support role updates and member removal via PATCH and DELETE requests, excluding member creation.

📖 Description

  • This update introduces endpoints for managing organization members:

    • GET /api/v2/organizations/<org-id>/members/ to list all members in an organization.
    • PATCH /api/v2/organizations/<org-id>/members/<username>/ to update a member's role (e.g., promote to admin).
    • DELETE /api/v2/organizations/<org-id>/members/<username>/ to remove a member from the organization.

Note: Creating members is not supported via this endpoint. Roles are restricted to 'admin' or 'member'. Including the details of invited members (those who have not yet joined the organization) is not covered in this update.

👷 Description for instance maintainers

  • This update provides new functionality to manage organization members:

    • Role updates for members (promotion/demotion between 'admin' and 'member').
    • Member removal from an organization.
    • The endpoints are optimized to use database joins to fetch member roles efficiently without excessive database queries, ensuring minimal load.

👀 Preview steps

  1. ℹ️ Have an account and an organization with multiple members.
  2. Use the GET method to list the members of the organization.
  3. Use the PATCH method to update a member's role (e.g., promote a user to admin).
  4. Use the DELETE method to remove a member from the organization.
  5. 🟢 Notice the endpoints behave as expected, with valid role updates and member removal.

💭 Notes

  • The code uses database joins to optimize role determination for members, avoiding inefficient role lookups.
  • Role validation restricts role assignments to 'admin' or 'member' only.
  • Including invited members' details is not covered in this update. This will be implemented in a future update.

@rajpatel24 rajpatel24 force-pushed the task-963-create-endpoints-to-handle-org-members branch from dcaab9f to 7533999 Compare November 6, 2024 20:10
@rajpatel24 rajpatel24 removed the request for review from jnm November 6, 2024 20:11
@rajpatel24 rajpatel24 force-pushed the task-963-create-endpoints-to-handle-org-members branch from 7533999 to e17f6a7 Compare November 6, 2024 20:13
@rajpatel24 rajpatel24 changed the title feat(Organizations): Add endpoints to handle organization members TASK-963 feat(organizations): add endpoints to handle organization members TASK-963 Nov 8, 2024
@@ -142,3 +142,10 @@ class TinyPaginated(PageNumberPagination):
Same as Paginated with a small page size
"""
page_size = 50


class OrganizationPagination(PageNumberPagination):
Copy link
Contributor

Choose a reason for hiding this comment

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

I would rename the class just to match what it is paginating (i.e.: organization members)

"""
Pagination class for Organization
"""
page_size = 10
Copy link
Contributor

Choose a reason for hiding this comment

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

Is 10 a requirement?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

10 is not a requirement defined in the task. I need to check with you or the team to determine the appropriate value for it.

Copy link
Contributor

Choose a reason for hiding this comment

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

@rajpatel24 Can we change this to limit offset pagination? Our table components on the frontend currently assume limit offset pagination and @noliveleger said it shouldn't cause any problems to use that here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jamesrkiger I have removed the custom pagination class for the organization members API. It will now use LimitOffsetPagination by default.

]

def get_has_mfa_enabled(self, obj):
return config.MFA_ENABLED
Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, you need to get if the user has MFA enabled. (e.g. MfaMethod.objects.filter(user=obj, is_active=True).exists()). Ensure to optimize this for a list like you did for role ;-)

URL_NAMESPACE = URL_NAMESPACE

def setUp(self):
self.organization = baker.make(Organization, id='org_12345')
Copy link
Contributor

Choose a reason for hiding this comment

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

For future-proof validaiton, please add mmo_override=True
This can be simplied with

self.organization = baker.make(Organization, id='org_12345', mmo_override=True)
self.owner_user = baker.make(User, username='owner')
self.member_user = baker.make(User, username='member')
self.invited_user = baker.make(User, username='invited')

self.organization.add_user(self.owner_user)  # first user added to org, is always the owner and an admin
self.organization.add_user(self.member_user, is_admin=False) 

self.organization = baker.make(Organization, id='org_12345')
self.owner_user = baker.make(User, username='owner')
self.member_user = baker.make(User, username='member')
self.invited_user = baker.make(User, username='invited')
Copy link
Contributor

Choose a reason for hiding this comment

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

invited_user is not used at all for the moment, right? Let's remove until we implement invitations.

Comment on lines 46 to 75
def test_list_members(self):
response = self.client.get(self.list_url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn(
'owner',
[member['user__username'] for member in response.data.get('results')]
)
self.assertIn(
'member',
[member['user__username'] for member in response.data.get('results')]
)

def test_retrieve_member_details(self):
response = self.client.get(self.detail_url('member'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['user__username'], 'member')
self.assertEqual(response.data['role'], 'member')

def test_update_member_role(self):
data = {'role': 'admin'}
response = self.client.patch(self.detail_url('member'), data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['role'], 'admin')

def test_delete_member(self):
response = self.client.delete(self.detail_url('member'))
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
# Confirm deletion
response = self.client.get(self.detail_url('member'))
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
Copy link
Contributor

Choose a reason for hiding this comment

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

All your tests are done with self.client == self.owner , but we need to validate permissions for regular members, admins and anonymous. Let's those tests as soon as #5218 is merged. I will tag this PR blocked by

Copy link
Contributor Author

@rajpatel24 rajpatel24 Nov 14, 2024

Choose a reason for hiding this comment

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

Thanks for pointing that out! I've added the role-based validation tests for organization member permissions.

One thing I still need to figure out is that I have added a new permission class and tried to use obj to retrieve the user role using the optimized approach I wrote in get_queryset(). It works fine when I test the endpoints in Postman or using a curl. However, for the tests, I'm unable to fetch the actual role, and it defaults to the member role. Do you have any suggestions for this?

kobo/apps/organizations/serializers.py Outdated Show resolved Hide resolved
kobo/apps/organizations/serializers.py Outdated Show resolved Hide resolved
kobo/apps/organizations/serializers.py Show resolved Hide resolved
kobo/apps/organizations/views.py Outdated Show resolved Hide resolved
@noliveleger
Copy link
Contributor

This PR is blocked by #5218

@magicznyleszek
Copy link
Member

@noliveleger @rajpatel24 this is no longer blocked, right?

@rajpatel24 rajpatel24 force-pushed the task-963-create-endpoints-to-handle-org-members branch from 05e5c5f to c25d807 Compare November 14, 2024 15:46
@rajpatel24 rajpatel24 force-pushed the task-963-create-endpoints-to-handle-org-members branch from c25d807 to 3c174df Compare November 14, 2024 19:51
@rajpatel24 rajpatel24 force-pushed the task-963-create-endpoints-to-handle-org-members branch from 315941e to 7c580e7 Compare November 14, 2024 20:20
@rajpatel24 rajpatel24 force-pushed the task-963-create-endpoints-to-handle-org-members branch from 7c580e7 to d876fbc Compare November 14, 2024 20:27
kobo/apps/organizations/permissions.py Outdated Show resolved Hide resolved
kobo/apps/organizations/permissions.py Outdated Show resolved Hide resolved
kobo/apps/organizations/permissions.py Outdated Show resolved Hide resolved
kobo/apps/organizations/permissions.py Outdated Show resolved Hide resolved
kobo/apps/organizations/serializers.py Outdated Show resolved Hide resolved
Comment on lines 140 to 146
class OrganizationMembersPagination(PageNumberPagination):
"""
Pagination class for Organization Members
"""
page_size_query_param = 'page_size'


Copy link
Contributor

Choose a reason for hiding this comment

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

You can delete this class, not needed anymore

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. It will use LimitOffsetPagination by default

source='user.date_joined', format='%Y-%m-%dT%H:%M:%SZ'
)
user__username = serializers.ReadOnlyField(source='user.username')
user__name = serializers.ReadOnlyField(source='user.get_full_name')
Copy link
Contributor

Choose a reason for hiding this comment

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

I think that's the wrong field. You need to retrieve User.extra_details.data['name'] . BTW in that case, the field should be user__extra_details__name .

cc @jamesrkiger

@@ -79,10 +86,12 @@ class OrganizationViewSet(viewsets.ModelViewSet):
queryset = Organization.objects.all()
serializer_class = OrganizationSerializer
lookup_field = 'id'
permission_classes = (IsAuthenticated, IsOrgAdminOrReadOnly)
permission_classes = [HasOrgRolePermission]
pagination_class = AssetUsagePagination
Copy link
Contributor

Choose a reason for hiding this comment

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

Asked by the front-end team, let's use LimitOffsetPagination instead.

Copy link
Contributor Author

@rajpatel24 rajpatel24 Nov 21, 2024

Choose a reason for hiding this comment

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

Okay, I removed AssetUsagePagination. It will now use LimitOffsetPagination by default for /organizations and /organizations/{org_id}/members.

Comment on lines 458 to 466
def destroy(self, request, *args, **kwargs):
"""
Delete an organization member and their associated user account
"""
instance = self.get_object()
user = instance.user
instance.delete()
user.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
Copy link
Contributor

@noliveleger noliveleger Nov 20, 2024

Choose a reason for hiding this comment

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

You can remove this method entirely. No need to overload but right now, we want to delete the OrganizationUser only. Do not touch the User object.

magicznyleszek and others added 3 commits November 20, 2024 23:03
…delete requests TASK-964 (#5274)

### 📣 Summary
Restricted POST and DELETE methods for `organizations` endpoint to
prevent unintended changes.


### 📖 Description
Previously, the organizations endpoint allowed POST and DELETE requests,
which could lead to accidental changes or deletions. This update
restricts these methods, ensuring that organizations data remains intact
and secure.


### 👀 Preview steps
- Send a GET request to the organizations endpoint to retrieve a list of
organizations.
- Attempt to send a POST request to create a new organization (this
should fail).
- Attempt to send a DELETE request to delete an existing organization
(this should fail).
- Send a PATCH request to update an existing organization (this should
succeed).


### 💭 Notes
- This change is a security enhancement to prevent accidental or
malicious modifications to organizations data.
- Tests have been updated to reflect the new behaviour.
Copy link
Contributor

@noliveleger noliveleger left a comment

Choose a reason for hiding this comment

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

LGTM

For the record: The complex query took only few millisecond on a 500 members org.

@rajpatel24 rajpatel24 merged commit 79e33e8 into main Nov 22, 2024
7 checks passed
@rajpatel24 rajpatel24 deleted the task-963-create-endpoints-to-handle-org-members branch November 22, 2024 06:50
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.

4 participants