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

[ENG-2814] Allow Read-only and Read/Write contributors to view a project's draft registrations #10660

Conversation

uditijmehta
Copy link
Contributor

Purpose

Change permissions for DraftRegistrations so Read/Write contributors can view all listed DraftRegistrations for a node.

GET for reading, POST for DR creation and PATCH for editing

Changes

  • Add new permissions class NodeDraftRegistrationsListPermission to NodeDraftRegistrationsList
  • Break up test runner into separate classes
  • Split apart large single test functions into individual cases
  • add more to testing matrix test_<user_permission>_draft_not_node has all cases

#10468

QA Notes

Ticket

<(https://openscience.atlassian.net/browse/ENG-2814)>

@uditijmehta uditijmehta changed the title Draft reg permission [ENG-2814] Allow Read-only and Read/Write contributors to view a project's draft registrations Jul 9, 2024
Copy link
Collaborator

@jwalz jwalz left a comment

Choose a reason for hiding this comment

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

Sorry, there's some history to this ticket that isn't well documented (I did the dumb thing of just assuming that I'd own this whenever it came back up) >_<

data = res.json['data']
assert len(data) == 1

@pytest.mark.skip('Old OSF Groups code')
Copy link
Collaborator

Choose a reason for hiding this comment

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

Feel free to just delete all of the tests for osf groups

@@ -50,7 +50,7 @@ def check_resource_permissions(self, resource):

class DraftRegistrationList(NodeDraftRegistrationsList):
permission_classes = (
IsContributorOrAdminContributor,
DraftRegistrationPermission,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think the Permissions class actually does anything here. It only defines has_object_permission, which is only invoked by default on detail views. That makes sense here, considering the endpoint specifically surfaces the active drafts for the requesting user.

Comment on lines 75 to 86
if request.method in permissions.SAFE_METHODS:
if isinstance(obj, DraftRegistration):
node_permission = obj.branched_from.has_permission(auth.user, READ)
return obj.has_permission(auth.user, READ) or node_permission
elif request.method == 'POST': # Only Admin can create a draft registration
if isinstance(obj, DraftRegistration):
node_permission = obj.branched_from.has_permission(auth.user, ADMIN)
return obj.has_permission(auth.user, ADMIN) or node_permission
else:
if isinstance(obj, DraftRegistration):
node_permission = obj.branched_from.has_permission(auth.user, WRITE)
return obj.has_permission(auth.user, WRITE) or node_permission
Copy link
Collaborator

Choose a reason for hiding this comment

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

Making all permissions inheritable from the branched_from node can lead to some confusing situations down the line -- notably, when a Draft is Registered and suddenly a user who thought they had a certain permission level doesn't have those permissions anymore, but there are probably also other edge cases just because there are lots of skeletons in the registration workflow.

There is a plan to some day kill off DraftRegistrationContributors (which were only created due to a confusion in requirements when No Project Registratsions were implemented).

For now, let's constrain this PR to the specific issue identified in the ticket:
Users who view a Project's DraftRegistrations should see the Drafts on which they are contributors.

@@ -625,7 +625,7 @@ class NodeDraftRegistrationsList(JSONAPIBaseView, generics.ListCreateAPIView, No
Use DraftRegistrationsList endpoint instead.
"""
permission_classes = (
IsAdminContributor,
DraftRegistrationPermission,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I can't comment on the actually relevant line:
My instinct is that the get_queryset should change to support the ticket requirements. Instead of listing all of the Drafts, this should list the Drafts on which the user is a contributor.

i.e., the queryset should return user.draft_registractions_active.filter(branched_from=node)
Existing behavior for the anonymous user would be to 401 -- which would redirect them to the login page -- but

We should probably get some additional clarification from Product about a couple of things:
First: Should users who are contributors on the Draft but not the branched_from Project be able to see their Drafts on the list of the Project's Drafts (so long as the Project is public)?
Second: Should Project Admins be able to view all Drafts, even if they aren't contributors, or should that wait until the related ticket to give branched_from Project Admins implicit read permissions on Drafts even when they aren't contributors?
Third: What should the behavior be for Unauthenticated users? Should we 401, implicitly redirecting them to the login page? Or should we just return the Empty queryset?

I'm happy to own this communication if you would like.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This is unaddressed. Let me know if you want a quick Zoom to clarify.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I discussed the questions you raised with Mark.

  1. If a user is a contributor to a draft registration but not to the main project, they should still be able to see their drafts if the project is public.
  2. For now, we'll wait to implement a feature that gives project admins implicit read permissions on all related drafts.
  3. Users who are not logged in should be redirected to the login page if they try to view draft registrations. If they don't have an account, they should be sent to a create an account page.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for sorting this out!

All of that being true, there's a bit of work left to do. Mostly, this endpoint (NodeDraftRegistrationList) is currently returning the full list of DraftRegistrations to all Node contributors.

Two fixes to get the correct behavior:
First, you'll need to update this get_queryset function to return something like user.active_draft_registrations.filter(branched_from=node).

Second, you'll need to update your Permission class to use can_view instead of has_permission(READ) so that non-contributors on the Project can still see their associated Drafts.

Copy link
Collaborator

Choose a reason for hiding this comment

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

(I added some redundant-ish comments on tests to highlight where these requirements were being missed; we'll want/need to expand the test coverage, too)

Copy link
Contributor

Choose a reason for hiding this comment

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

There are some unaddressed comments here regarding the queryset.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I had missed this comment. I have updated the PR.

@@ -659,9 +659,9 @@ class NodeDraftRegistrationDetail(JSONAPIBaseView, generics.RetrieveUpdateDestro
Use DraftRegistrationDetail endpoint instead.
"""
permission_classes = (
DraftRegistrationPermission,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Given everything else I've said, I think the Detail views can go untouched for now.

Copy link
Contributor

Choose a reason for hiding this comment

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

Do we still need to update the permission of the detail view?

Comment on lines 72 to 76
return DraftRegistration.objects.filter(
Q(_contributors=user) &
Q(deleted__isnull=True) &
(Q(registered_node__isnull=True) | Q(registered_node__deleted__isnull=False)),
)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why the explicit query instead of reusing the user.draft_registrations_active?

@@ -625,7 +625,7 @@ class NodeDraftRegistrationsList(JSONAPIBaseView, generics.ListCreateAPIView, No
Use DraftRegistrationsList endpoint instead.
"""
permission_classes = (
IsAdminContributor,
DraftRegistrationPermission,
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is unaddressed. Let me know if you want a quick Zoom to clarify.

def test_admin_can_view_draft_list(
self, app, user, draft_registration, project_public,
schema, url_draft_registrations):
def test_draft_admin_can_view_draft_list(
Copy link
Collaborator

Choose a reason for hiding this comment

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

user here is also a Project admin, as they are the creator of project_public.

group = OSFGroupFactory(creator=group_mem)
project_public.add_osf_group(group, permissions.ADMIN)
res = app.get(url_draft_registrations, auth=group_mem.auth, expect_errors=True)
def test_node_admin_can_view_draft_list(
Copy link
Collaborator

Choose a reason for hiding this comment

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

This test is confirming that a Project Admin can view the draft list even if they are not a contributor on the draft -- and, in fact, confirms that they can see the Draft in the list, even if they are not a contributor on the Draft, which was identified as work for a different ticket.

url_draft_registrations,
auth=user_non_contrib.auth,
expect_errors=True)
def test_logged_in_non_contributor_cannot_view_draft_list(self, app, user_non_contrib, url_draft_registrations):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Non-contributor on the project who is a contributor on the Draft should still see the draft in the list. This shows that they will 403 instead (because we're checking has_permission(user, READ) instead of can_view(user))

user_read_contrib, user_non_contrib,
project_public, payload, group,
url_draft_registrations, group_mem):
def test_write_only_contributor_cannot_create_draft(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Thank you for breaking this into distinct tests!!

)

@pytest.fixture()
def metaschema_open_ended(self):
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit/honest question: why the separate schema?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think the separate metaschema_open_ended fixture is there to ensure draft registrations are tested with the right schema for consistency.

@@ -625,7 +625,7 @@ class NodeDraftRegistrationsList(JSONAPIBaseView, generics.ListCreateAPIView, No
Use DraftRegistrationsList endpoint instead.
"""
permission_classes = (
IsAdminContributor,
DraftRegistrationPermission,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for sorting this out!

All of that being true, there's a bit of work left to do. Mostly, this endpoint (NodeDraftRegistrationList) is currently returning the full list of DraftRegistrations to all Node contributors.

Two fixes to get the correct behavior:
First, you'll need to update this get_queryset function to return something like user.active_draft_registrations.filter(branched_from=node).

Second, you'll need to update your Permission class to use can_view instead of has_permission(READ) so that non-contributors on the Project can still see their associated Drafts.

@@ -625,7 +625,7 @@ class NodeDraftRegistrationsList(JSONAPIBaseView, generics.ListCreateAPIView, No
Use DraftRegistrationsList endpoint instead.
"""
permission_classes = (
IsAdminContributor,
DraftRegistrationPermission,
Copy link
Collaborator

Choose a reason for hiding this comment

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

(I added some redundant-ish comments on tests to highlight where these requirements were being missed; we'll want/need to expand the test coverage, too)

drf_permissions.IsAuthenticatedOrReadOnly,
base_permissions.TokenHasScope,
DraftRegistrationPermission,
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is still a noop, since DraftRegistrationPermission only defines has_object_permission -- and this endpoint doesn't need any extra permissions, since it's just surfacing the current user's drafts (https://media1.tenor.com/m/j5TI9U8hISQAAAAC/emperors-new-groove-why.gif)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Without it, a bunch of our tests are failing because it is letting unauthorized users create draft registrations.

The following tests failed when it was removed:

test_write_only_contributor_cannot_create_draft
test_read_only_contributor_cannot_create_draft
test_draft_registration_attributes_copied_from_node
test_logged_in_non_contributor_cannot_create_draft

Copy link
Contributor

@adlius adlius left a comment

Choose a reason for hiding this comment

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

Some nitpicks and unaddressed comments from @jwalz . Also there seems to be some merge conflicts.

if request.method in permissions.SAFE_METHODS:
if isinstance(obj, DraftRegistration):
return obj.is_contributor(auth.user) or obj.has_permission(auth.user, READ)
return obj.has_permission(auth.user, READ)
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd like this to be more specific. Maybe add an isinstance(obj, NodeModel) here, since we are checking the permission of the node?

elif request.method == 'POST': # Only Admin can create a draft registration
if isinstance(obj, DraftRegistration):
return obj.is_contributor(auth.user) and obj.has_permission(auth.user, ADMIN)
return obj.has_permission(auth.user, ADMIN)
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here

else:
if isinstance(obj, DraftRegistration):
return obj.is_contributor(auth.user) and obj.has_permission(auth.user, WRITE)
return obj.has_permission(auth.user, WRITE)
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here.

@@ -625,7 +625,7 @@ class NodeDraftRegistrationsList(JSONAPIBaseView, generics.ListCreateAPIView, No
Use DraftRegistrationsList endpoint instead.
"""
permission_classes = (
IsAdminContributor,
DraftRegistrationPermission,
Copy link
Contributor

Choose a reason for hiding this comment

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

There are some unaddressed comments here regarding the queryset.

@uditijmehta uditijmehta changed the base branch from develop to feature/b-and-i-24-14 July 25, 2024 12:21
Copy link
Contributor

@adlius adlius left a comment

Choose a reason for hiding this comment

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

LGTM. Just one small question.

@@ -659,9 +659,9 @@ class NodeDraftRegistrationDetail(JSONAPIBaseView, generics.RetrieveUpdateDestro
Use DraftRegistrationDetail endpoint instead.
"""
permission_classes = (
DraftRegistrationPermission,
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we still need to update the permission of the detail view?

Copy link
Collaborator

@jwalz jwalz left a comment

Choose a reason for hiding this comment

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

lgtm

@brianjgeiger brianjgeiger merged commit 0b6a3da into CenterForOpenScience:feature/b-and-i-24-14 Aug 1, 2024
6 checks passed
uditijmehta added a commit to uditijmehta/osf.io that referenced this pull request Aug 9, 2024
…ect's draft registrations (CenterForOpenScience#10660)

## Purpose

Change permissions for DraftRegistrations so Read/Write contributors can view all listed DraftRegistrations for a node.

GET for reading, POST for DR creation and PATCH for editing

## Changes

- Add new permissions class NodeDraftRegistrationsListPermission to NodeDraftRegistrationsList
- Break up test runner into separate classes
- Split apart large single test functions into individual cases
- add more to testing matrix test_<user_permission>_draft_not_node has all cases

CenterForOpenScience#10468

## QA Notes


## Ticket

https://openscience.atlassian.net/browse/ENG-2814
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.

None yet

4 participants