Skip to content

Commit

Permalink
Merge pull request #5415 from hypothesis/group-upsert-root
Browse files Browse the repository at this point in the history
Add `GroupUpsertContext` and `GroupUpsertRoot`
  • Loading branch information
lyzadanger committed Nov 9, 2018
2 parents 41a7ed1 + a32b754 commit 76f2699
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 1 deletion.
5 changes: 4 additions & 1 deletion h/traversal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
from h.traversal.roots import OrganizationLogoRoot
from h.traversal.roots import ProfileRoot
from h.traversal.roots import GroupRoot
from h.traversal.roots import GroupUpsertRoot
from h.traversal.roots import UserRoot
from h.traversal.contexts import AnnotationContext
from h.traversal.contexts import OrganizationContext
from h.traversal.contexts import GroupContext

from h.traversal.contexts import GroupUpsertContext

__all__ = (
"Root",
Expand All @@ -21,9 +22,11 @@
"OrganizationRoot",
"OrganizationLogoRoot",
"GroupRoot",
"GroupUpsertRoot",
"ProfileRoot",
"UserRoot",
"AnnotationContext",
"OrganizationContext",
"GroupContext",
"GroupUpsertContext",
)
36 changes: 36 additions & 0 deletions h/traversal/contexts.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from pyramid.security import Allow
from pyramid.security import principals_allowed_by_permission

from h.auth import role
from h.models.organization import ORGANIZATION_DEFAULT_PUBID


Expand Down Expand Up @@ -143,3 +144,38 @@ def organization(self):
if self.group.organization is not None:
return OrganizationContext(self.group.organization, self.request)
return None


class GroupUpsertContext(object):
"""Context for group UPSERT"""

def __init__(self, group, request):
self._request = request
self.group = group

def __acl__(self):
"""
Get the ACL from the group model or set "upsert" for all users in absence of model
If there is a group model, get the ACL from there. Otherwise, return an
ACL that sets the "upsert" permission for authenticated requests that have
a real user associated with them via :attr:`h.auth.role.User`.
The "upsert" permission is an unusual hybrid. It has a different meaning
depending on the upsert situation.
If there is no group associated with the context, the "upsert" permission
should be given to all real users such that they may use the UPSERT endpoint
to create a new group. However, if there is a group associated with the
context, the "upsert" permission is managed by the model. The model only
applies "upsert" for the group's creator. This will allow the endpoint to
support updating a specific group (model), but only if the request's
user should be able to update the group.
"""

# TODO: This and ``GroupContext`` can likely be merged once ``GroupContext``
# is used more resource-appropriately and returned by :class:`h.traversal.roots.GroupRoot`
# during traversal
if self.group is not None:
return self.group.__acl__()
return [(Allow, role.User, 'upsert')]
26 changes: 26 additions & 0 deletions h/traversal/roots.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,32 @@ def __getitem__(self, pubid_or_groupid):
return group


class GroupUpsertRoot(object):
"""
Root factory for group "UPSERT" API
This Root can support a route in which the traversal's ``__getitem__``
will attempt a lookup but will not raise if that fails.
This is to allow a single route that can accept and update an existing group
OR create a new one.
"""

__acl__ = GroupRoot.__acl__

def __init__(self, request):
self._request = request
self._group_root = GroupRoot(request)

def __getitem__(self, pubid_or_groupid):
try:
group = self._group_root[pubid_or_groupid]
except KeyError:
group = None

return contexts.GroupUpsertContext(group=group, request=self._request)


class ProfileRoot(object):
"""
Simple Root for API profile endpoints
Expand Down
50 changes: 50 additions & 0 deletions tests/h/traversal/contexts_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
from pyramid import security
from pyramid.authorization import ACLAuthorizationPolicy

from h.auth import role
from h.models import Organization
from h.services.group_links import GroupLinksService
from h.traversal.contexts import AnnotationContext
from h.traversal.contexts import GroupContext
from h.traversal.contexts import GroupUpsertContext
from h.traversal.contexts import OrganizationContext


Expand Down Expand Up @@ -342,6 +344,54 @@ def test_default_property_if_default_organization(self, factories, pyramid_reque
assert organization_context.default is True


@pytest.mark.usefixtures('links_svc')
class TestGroupUpsertContext(object):

def test_acl_applies_root_upsert_to_user_role_when_no_group(self, pyramid_config, pyramid_request):
policy = ACLAuthorizationPolicy()
pyramid_config.testing_securitypolicy('acct:adminuser@foo',
groupids=[security.Authenticated, role.User])
pyramid_config.set_authorization_policy(policy)

context = GroupUpsertContext(group=None, request=pyramid_request)

assert pyramid_request.has_permission('upsert', context)

def test_acl_denies_root_upsert_if_no_user_role_and_no_group(self, pyramid_config, pyramid_request):
policy = ACLAuthorizationPolicy()
pyramid_config.testing_securitypolicy('acct:adminuser@foo',
groupids=[security.Authenticated])
pyramid_config.set_authorization_policy(policy)

context = GroupUpsertContext(group=None, request=pyramid_request)

assert not pyramid_request.has_permission('upsert', context)

def test_acl_applies_group_model_acl_if_group_is_not_None(self, pyramid_config, pyramid_request, factories):
group = factories.Group()
policy = ACLAuthorizationPolicy()
pyramid_config.testing_securitypolicy('acct:adminuser@foo',
groupids=[security.Authenticated])
pyramid_config.set_authorization_policy(policy)

context = GroupUpsertContext(group=group, request=pyramid_request)

assert context.__acl__() == group.__acl__()

def test_acl_does_not_apply_root_upsert_permission_if_group_is_not_None(self, pyramid_config, pyramid_request, factories):
group = factories.Group()
policy = ACLAuthorizationPolicy()
pyramid_config.testing_securitypolicy('acct:adminuser@foo',
groupids=[security.Authenticated, role.User])
pyramid_config.set_authorization_policy(policy)

context = GroupUpsertContext(group=group, request=pyramid_request)

# an `upsert` permission could be present in the ACL via the model IF the current
# user were the creator, but they're not
assert not pyramid_request.has_permission('upsert', context)


class FakeGroup(object):
# NB: Tests that use this do not validate that the principals are correct
# for the indicated group. They validate that those principals are being
Expand Down
34 changes: 34 additions & 0 deletions tests/h/traversal/roots_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from h.traversal.roots import OrganizationRoot
from h.traversal.roots import OrganizationLogoRoot
from h.traversal.roots import GroupRoot
from h.traversal.roots import GroupUpsertRoot
from h.traversal.roots import ProfileRoot
from h.traversal.roots import UserRoot
from h.traversal.contexts import AnnotationContext
Expand Down Expand Up @@ -342,6 +343,39 @@ def group_factory(self, pyramid_request):
return GroupRoot(pyramid_request)


@pytest.mark.usefixtures("GroupRoot", "GroupUpsertContext")
class TestGroupUpsertRoot(object):

def test_getitem_returns_empty_upsert_context_if_missing_group(self, pyramid_request, GroupRoot, GroupUpsertContext):
root = GroupUpsertRoot(pyramid_request)
GroupRoot.return_value.__getitem__.side_effect = KeyError('bang')

context = root['whatever']

GroupRoot.return_value.__getitem__.assert_called_once_with('whatever')
assert context == GroupUpsertContext.return_value
GroupUpsertContext.assert_called_once_with(group=None, request=pyramid_request)

def test_getitem_returns_populated_upsert_context_if_group_found(self, pyramid_request, GroupRoot, GroupUpsertContext, factories):
group = factories.Group()
root = GroupUpsertRoot(pyramid_request)
GroupRoot.return_value.__getitem__.return_value = group

context = root['agroup']

GroupRoot.return_value.__getitem__.assert_called_once_with('agroup')
assert context == GroupUpsertContext.return_value
GroupUpsertContext.assert_called_once_with(group=group, request=pyramid_request)

@pytest.fixture
def GroupRoot(self, patch):
return patch('h.traversal.roots.GroupRoot')

@pytest.fixture
def GroupUpsertContext(self, patch):
return patch('h.traversal.roots.contexts.GroupUpsertContext')


@pytest.mark.usefixtures('user_service',
'client_authority')
class TestUserRoot(object):
Expand Down

0 comments on commit 76f2699

Please sign in to comment.