-
Notifications
You must be signed in to change notification settings - Fork 432
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
Pass the right context
objects to edit_member()
and add_member()
#9201
Conversation
context
objects to edit_member()
and add_member()
class AddGroupMembershipContext: | ||
group: Group | ||
user: User | ||
new_roles: list[GroupMembershipRoles] | None |
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.
Adding a new AddGroupMembershipContext
class for the add_member()
view to use, alongside the existing GroupMembershipContext
and EditGroupMembershipContext
classes already used by other views.
It's unfortunate to have to type-annotate this as | None
here, but AddGroupMembershipContext
's are initially created with new_roles=None
and then new_roles
is set later so 🤷♂️
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.
Copilot reviewed 5 out of 7 changed files in this pull request and generated no suggestions.
Files not reviewed (2)
- h/traversal/init.py: Evaluated as low risk
- h/traversal/group_membership.py: Evaluated as low risk
5879fb8
to
b171774
Compare
@@ -63,6 +63,7 @@ | |||
from h.traversal.annotation import AnnotationContext, AnnotationRoot | |||
from h.traversal.group import GroupContext, GroupRequiredRoot, GroupRoot | |||
from h.traversal.group_membership import ( | |||
AddGroupMembershipContext, |
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.
We tend to import names from h/*/*.py
files into h/*/__init__.py
to make imports easier. It means you can do this:
from h.traversal import Foo, Bar, Gar
Instead of having to do this:
from h.traversal.foo import Foo
from h.traversal.bar import Bar
from h.traversal.gar import Gar
It also means the importer doesn't know about the internal structure of h/traversal/
so we can reorganise the internal files without breaking any importers.
@@ -82,6 +83,7 @@ | |||
"UserByIDRoot", | |||
"UserRoot", | |||
"GroupContext", | |||
"AddGroupMembershipContext", |
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.
We should get rid of this __all__
, it's unnecessary
assert ( | ||
context.new_roles is not None | ||
), "new_roles must be set before checking permissions" |
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.
The view always has to set context.new_roles
before calling request.has_permission(some_permission, context)
(which ends up calling this group_member_edit()
predicate). Adding this assert
here to make sure that the security predicate won't run if new_roles
isn't set, as that could potentially lead to permissions being granted when they shouldn't be.
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.
DD: Group Owners, Admins and Moderators contains a section explaining how security permissions work in Pyramid and h.
b171774
to
fc5dbd6
Compare
Tested it locally seems to be working just fine. |
19731f3
to
d9632c0
Compare
fc5dbd6
to
a3a946a
Compare
d9632c0
to
2b30f91
Compare
Move stuff out into helper functions, making way for future changes to the function's logic.
Problem ------- `group_membership_api_factory()` is the context factory for all views that use the `"api.group_member"` route: * `remove_member()` (`request_method="DELETE"`) * `add_member()` (`request_method="POST"`) * `edit_member()` (`request_method="PATCH"`) * Add in #9197 also `get_member()` (`request_method="GET"`) `group_membership_api_factory()` returns a `GroupMembershipContext` object but: * `GroupMembershipContext` isn't the right context for `edit_member()`: it lacks the `context.new_roles` attribute for the new roles that the request wants to change `context.membership.roles` to. * `GroupMembershipContext` isn't the right context for `add_member()`: it has an inappropriate `context.membership` attribute (when adding a new membership there shouldn't be an existing membership in the context) and it lacks the `context.new_roles` attribute for the roles that the request wants to create a membership with. The context for the `edit_member()` view should be an `EditGroupMembershipContext` object, and for `add_member()` it should be an `AddGroupMembershipContext`. As a result the `edit_member()` view has to create its own context object to pass to `request.has_permission()`: def edit_member(context: GroupMembershipContext, request): appstruct = EditGroupMembershipAPISchema().validate(json_payload(request)) new_roles = appstruct["roles"] if not request.has_permission( Permission.Group.MEMBER_EDIT, EditGroupMembershipContext( context.group, context.user, context.membership, new_roles ), ): raise HTTPNotFound() When a future PR enables users (not just auth clients) to call the add-membership API the `add_member()` view will have to do something similar: constructing its own `AddGroupMembershipContext` object and passing it to `request.has_permission()`. This means there are two different context objects in play for the `edit_member()` and `add_member()` views: the `context` that is passed to the view is a `GroupMembershipContext`, but the `context` that is passed to `request.has_permission()` is an `EditGroupMembershipContext` or `AddGroupMembershipContext` constructed by the view itself. Solution ------- This commit changes `group_membership_api_factory()` to return a `GroupMembershipContext` for `GET` and `DELETE` requests but return an `EditGroupMembershipContext` for `PATCH` requests and an `AddGroupMembershipContext` for `POST`s. It's not possible for `group_membership_api_factory()` to set the context's `new_roles` attribute: the value for `new_roles` isn't available until later in the request processing cycle after the view has parsed and validated the request's JSON body. So the factory returns `context` objects with `context.new_roles=None` and the `edit_member()` view has to set `new_roles` before calling `has_permission()`: appstruct = EditGroupMembershipAPISchema().validate(json_payload(request)) context.new_roles = appstruct["roles"] if not request.has_permission(Permission.Group.MEMBER_EDIT, context): raise HTTPNotFound() In future the `add_member()` view will have to do the same. So this is still a little weird, but I think it's better than having two different context objects for a single request.
a3a946a
to
544af1f
Compare
Background
Pyramid has the handy concept of a "context" object for a request, named
context
. The way the context object works is:routes.py
defines the "factory" for the"api.group_member"
route:The
factory="h.traversal.group_membership_api_factory"
means that whenever a request for/api/groups/{pubid}/members/{userid}
comes in Pyramid will callgroup_membership_api_factory()
to get the context object.group_membership_api_factory()
returns aGroupMembershipContext
object for thecontext
:Pyramid then passes that object as the
context
argument to various layers in its request processing cycle. For example:security/predicates.py
(which make security decisions about whether to permit or deny a request) receivecontext
as an argument.context
as an argument. There are three views for the"api.group_member"
route (remove_member()
,edit_member()
andadd_member()
, Add GET membership API #9197 will also addget_member()
) that all receiveGroupMembershipContext
objects as thecontext
argument.context
argument or available via therequest
object asrequest.context
.Problem
group_membership_api_factory()
is the context factory for all views that use the"api.group_member"
route:remove_member()
(request_method="DELETE"
)add_member()
(request_method="POST"
)edit_member()
(request_method="PATCH"
)get_member()
(request_method="GET"
)group_membership_api_factory()
returns aGroupMembershipContext
object but:GroupMembershipContext
isn't the right context foredit_member()
: it lacks thecontext.new_roles
attribute for the new roles that the request wants to changecontext.membership.roles
to.GroupMembershipContext
isn't the right context foradd_member()
: it has an inappropriatecontext.membership
attribute that has to be set toNone
(when adding a new membership there shouldn't be an existing membership in the context) and it lacks thecontext.new_roles
attribute for the roles that the request wants to create a new membership with.The context for the
edit_member()
view should be anEditGroupMembershipContext
object, and foradd_member()
it should be anAddGroupMembershipContext
.As a result the
edit_member()
view has to create its own context object to pass torequest.has_permission()
:h/h/views/api/group_members.py
Lines 116 to 126 in cfe86f0
When a future PR (#9200) enables users (not just auth clients) to call the add-membership API the
add_member()
view will have to do somethingsimilar: constructing its own
AddGroupMembershipContext
object and passing it torequest.has_permission()
.This means there are two different context objects in play for the
edit_member()
andadd_member()
views: thecontext
that is passed to the view is aGroupMembershipContext
, but thecontext
that is passed torequest.has_permission()
is anEditGroupMembershipContext
orAddGroupMembershipContext
constructed by the view itself.Solution
This commit changes
group_membership_api_factory()
to return aGroupMembershipContext
forGET
andDELETE
requests but return anEditGroupMembershipContext
forPATCH
requests and anAddGroupMembershipContext
forPOST
s.It's not possible for
group_membership_api_factory()
to set the context'snew_roles
attribute: the value fornew_roles
isn't available until later in the request processing cycle after the view has parsed and validated the request's JSON body. So the factory returnscontext
objects withcontext.new_roles=None
and theedit_member()
view has to setnew_roles
before callinghas_permission()
:In #9200 the
add_member()
view will have to do the same. So this is still a little weird, but I think it's better than having two different context objects for a single request.Testing
For now the add-member API can only be called by authclients, not users, so you'll have to create an authclient to test it:
Log in as
devdata_admin
(password:pass
).Go to http://localhost:5000/admin/oauthclients/new and create a new authclient with these properties:
Authority: localhost
Grant type: client_credentials
Trusted: yes
Go to http://localhost:5000/groups/new and create a new group.
Use the authclient to authenticate a request to the add-member API:
The edit-member and remove-member APIs can only be called by users, not authclients, so you'll have to go to http://localhost:5000/account/developer and create an API key to test them.
Now use the API key to authenticate a request to change
devdata_user
's role:Finally, use the API key to authenticate a request to delete
devdata_user
's group membership: