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

Support setting role in add-member API requests #9190

Merged
merged 1 commit into from
Jan 7, 2025

Conversation

seanh
Copy link
Contributor

@seanh seanh commented Dec 6, 2024

Fixes #9140.

Add support for an optional JSON body in add-membership API requests that lets the user set the membership's role in the add-member API request itself (rather than having to add the membership and then call the edit-membership API to change the role).

For backwards compatibility if the add-membership API is called without a body it still adds a member with the plain "member" role, as currently.

No updated API docs in this request since the backwards-compat means this is a non-breaking change. I'll send a separate PR later to update the docs for several membership APIs that've had new features added recently.

Note that the add-member API can currently only be called with an authclient. It can't be called as a user. Auth clients have permission to add any user to any group (within the auth client's authority) and with any role.

Testing

  1. Log in as devdata_admin

  2. Go to http://localhost:5000/groups/new and create a new group

  3. Go to http://localhost:5000/admin/oauthclients/new and create an authclient with these properties:

    Authority: localhost
    Grant type: client_credentials
    Trusted: yes

  4. Use the authclient to authenticate an API request to add devdata_user to the group. With no JSON body it'll create a plain "member" role:

    $ httpx http://localhost:5000/api/groups/{pubid}/members/acct:devdata_user@localhost --method POST --auth {client_id} {client_secret}
    HTTP/1.1 200 OK
    
    {
        "authority": "localhost",
        "userid": "acct:devdata_user@localhost",
        "username": "devdata_user",
        "display_name": null,
        "roles": [
            "member"
        ],
        "actions": [],
        "created": "2024-12-06T12:54:27.790391+00:00",
        "updated": "2024-12-06T12:54:27.790394+00:00"
    }
    
  5. Create an API key and use it to delete the membership so we can recreate it again:

    $ httpx http://localhost:5000/api/groups/{pubid}/members/acct:devdata_user@localhost --method DELETE --headers Authorization 'Bearer {apikey}'
    
  6. Now make the POST request again with a JSON body and it'll add the membership with the role from the JSON body:

    $ httpx http://localhost:5000/api/groups/{pubid}/members/acct:devdata_user@localhost --method POST --auth {client_id} {client_secret} --json '{"roles": ["owner"]}'
    
  7. You can also trying posting an invalid role and you'll get an error message.

  8. You can repeatedly make the same request (with or without a JSON body) and it'll keep on responding 200 OK and just returning the existing membership. But if you make a request with different roles it'll respond 409 Conflict.

@seanh seanh requested a review from marcospri December 6, 2024 17:47
@seanh seanh force-pushed the add-membership-request-body branch 3 times, most recently from 4d6aa54 to d713620 Compare December 7, 2024 15:10
Comment on lines +119 to +129
"""
Add `userid` to `group` with `roles` and return the resulting membership.

If `roles=None` it will default to `[GroupMembershipRoles.MEMBER]`.

If a membership matching `group`, `userid` and `roles` already exists
in the DB it will just be returned.

:raise ConflictError: if a membership already exists with the given
group and userid but different roles
"""
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Previously if you called this method and a matching membership already existed it would just return the existing membership. Now that I've added the roles argument it introduces a new case:

It's now possible to call this method when a membership already exists with the matching group and userid but with different roles. In this case we can't create a new membership. Nor do we want to update the existing membership (for example the API view that calls this method doesnt' want it to update existing memberships: a POST API should never update an existing resource). So raise an exception in that case.

For backwards-compatibility it will still return a pre-existing membership if it has the same roles.

Comment on lines +106 to +109
else:
# This doesn't mean the membership will be created with no roles:
# default roles will be applied by the service.
roles = None
Copy link
Contributor Author

Choose a reason for hiding this comment

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

For backwards-compatibility add-group-membership requests with no body are still allowed and they add memberships with the default role.

Comment on lines +117 to +118
except ConflictError as err:
raise HTTPConflict(str(err)) from err
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Return a 409 if there's an existing membership with different roles.

For backwards-compatibility if there's an existing membership with the same roles it'll still return 200 OK.

@seanh seanh force-pushed the add-membership-request-body branch from d713620 to 19731f3 Compare December 7, 2024 15:17
user = self.user_fetcher(userid)

existing_membership = self.get_membership(group, user)
kwargs = {"roles": roles}
Copy link
Member

Choose a reason for hiding this comment

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

I don't think I get why we use this kwargs here vs a more explicit roles only check, are there any other membership attributes that might create a conflict coming soon?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah this was just worrying about the future: I'm trying to code in such a way that, if we ever do add support for another attribute besides roles, that attribute will be taken into account when deciding whether to raise ConflictError as well

@seanh seanh force-pushed the add-membership-response-body branch from 2ebfb06 to c649dc9 Compare December 12, 2024 17:57
@seanh seanh force-pushed the add-membership-request-body branch from 19731f3 to d9632c0 Compare December 12, 2024 18:00
@seanh seanh requested a review from Copilot December 12, 2024 18:04

Choose a reason for hiding this comment

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

Copilot reviewed 5 out of 5 changed files in this pull request and generated no suggestions.

Comments skipped due to low confidence (1)

h/services/group_members.py:140

  • [nitpick] The error message could be more specific. Consider changing it to: 'The user is already a member of the group with different roles.'
raise ConflictError("The user is already a member of the group, with conflicting membership attributes")
Base automatically changed from add-membership-response-body to main January 7, 2025 14:14
Add support for an optional JSON body in add-member API requests that
lets the user set the membership's role in the add-member API request.

The add-group-membership API will now return a 409 Conflict if a
conflicting membership (one with the same user and group but different
roles) already exists in the DB:

    POST /api/groups/{groupid}/members/{userid}
    {"roles": ["owner"]}
    -> 200 OK

    POST /api/groups/{groupid}/members/{userid}
    {"roles": ["admin"]}
    -> 409 Conflict

Previously this would have resulted in a 200 OK even though the
membership's role in the DB would *not* have been updated with the new
role from the second request.

For backwards-compatibility it will still respond 200 OK if the
subsequent request's role *does* match the existing role in the DB:

    POST /api/groups/{groupid}/members/{userid}
    {"roles": ["owner"]}
    -> 200 OK

    POST /api/groups/{groupid}/members/{userid}
    {"roles": ["owner"]}
    -> 200 OK

This also works if requests are made with no JSON body (which creates
memberships with the default role of "member"):

    POST /api/groups/{groupid}/members/{userid}
    (no body content)
    -> 200 OK

    POST /api/groups/{groupid}/members/{userid}
    (no body content)
    -> 200 OK
@seanh seanh force-pushed the add-membership-request-body branch from d9632c0 to 2b30f91 Compare January 7, 2025 14:46
@seanh seanh merged commit 49b9e3d into main Jan 7, 2025
10 checks passed
@seanh seanh deleted the add-membership-request-body branch January 7, 2025 14:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add roles to add-group-member API
2 participants