-
-
Notifications
You must be signed in to change notification settings - Fork 184
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
feat(organizations): add endpoints to handle organization members TASK-963 #5235
Conversation
dcaab9f
to
7533999
Compare
7533999
to
e17f6a7
Compare
…e-endpoints-to-handle-org-members
kpi/paginators.py
Outdated
@@ -142,3 +142,10 @@ class TinyPaginated(PageNumberPagination): | |||
Same as Paginated with a small page size | |||
""" | |||
page_size = 50 | |||
|
|||
|
|||
class OrganizationPagination(PageNumberPagination): |
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.
I would rename the class just to match what it is paginating (i.e.: organization members)
kpi/paginators.py
Outdated
""" | ||
Pagination class for Organization | ||
""" | ||
page_size = 10 |
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.
Is 10
a requirement?
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.
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.
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.
@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.
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.
@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 |
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.
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') |
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.
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') |
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.
invited_user
is not used at all for the moment, right? Let's remove until we implement invitations.
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) |
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.
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
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.
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?
This PR is blocked by #5218 |
@noliveleger @rajpatel24 this is no longer blocked, right? |
05e5c5f
to
c25d807
Compare
c25d807
to
3c174df
Compare
315941e
to
7c580e7
Compare
7c580e7
to
d876fbc
Compare
…ation user table and enhance test coverage
kpi/paginators.py
Outdated
class OrganizationMembersPagination(PageNumberPagination): | ||
""" | ||
Pagination class for Organization Members | ||
""" | ||
page_size_query_param = 'page_size' | ||
|
||
|
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.
You can delete this class, not needed anymore
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.
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') |
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.
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
kobo/apps/organizations/views.py
Outdated
@@ -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 |
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.
Asked by the front-end team, let's use LimitOffsetPagination
instead.
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.
Okay, I removed AssetUsagePagination
. It will now use LimitOffsetPagination
by default for /organizations
and /organizations/{org_id}/members
.
kobo/apps/organizations/views.py
Outdated
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) |
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.
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.
…le pagination with LimitOffsetPagination
…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.
… to improve performance
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.
LGTM
For the record: The complex query took only few millisecond on a 500 members org.
🗒️ Checklist
<type>(<scope>)<!>: <title> TASK-1234
frontend
orbackend
unless it's global📣 Summary
📖 Description
This update introduces endpoints for managing organization members:
/api/v2/organizations/<org-id>/members/
to list all members in an organization./api/v2/organizations/<org-id>/members/<username>/
to update a member's role (e.g., promote to admin)./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:
👀 Preview steps
💭 Notes