Skip to content

Conversation

@EdoStorm96
Copy link
Contributor

fixes #78

So here is my suggestion for how we handle permissions. I suggest:

  • Every ObjectType (eg. StudyType) receives specific methods for receiving a queryset which a user can access if they have specific permission types (eg. edit or view). These are viewable_queryset() and editable_queryset(). These are called via the object type's get_queryset() method. This method also receives, alongside the usual stuff, a permission type from MRPermissionTypes.

  • Every model receives (eg. Study) has specific methods for checking if a user has access to an instance of a model aka an object. These are can_be_viewed_by() and can_be_edited_by(). These are called via the model's can_be_accessed_by() method. This method receives a user and a permission type from MRPermissionTypes.

  • For queries of multiple objects, the ObjectType's get_queryset() is used and for doing queries or mutations on single models, the can_be_accessed_by() method gets used. These two methods might need to get placed in a mixin, since I foresee duplicating these for every ObjectType and Model will get redundant, but I am not a 100% sure if that will always work, so for now I've just left these where they are.

To get a proof of concept to work I've implemented a really basic Study model. I've also made queries and mutations for studies. For now, this can be tested using the GraphQLView (localhost:5000/backend/api/graphql). It all works fine in my experience.

I have also added 2 groups, which we'll definitely need according to our stakeholders:

  • Privacy officer
  • fetc member
    I've added a migration which automatically loads these groups. Be aware of that :)

I have not yet added the Study model or users which are members of these groups to create_dev_data, although that would be better ...

How I would test this is to create a couple of superusers, so you can login via the admin interface, make one of them a member of the privacy officer group via the admin interface and start making studies through mutations and then running some queries to see if they act as they should.

I think this design is pretty nice, but I think we need to carefully consider the naming as this will probably be re-used quite a bit.

I'm looking forward to hearing your feedback!

fixes #77
I also added a markdown table with the overview of permissions for the MVP and possible future permissions once we absorb the FETC.

@EdoStorm96
Copy link
Contributor Author

Oh and there is a problem with black? It can't parse create_dev_data, which I haven't toouched in this branch ...

Copy link
Contributor

@tijmenbaarda tijmenbaarda left a comment

Choose a reason for hiding this comment

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

I love the design! I still have to check whether it works properly by testing it with the some GraphQL queries, but here are some comments on the code.


@classmethod
def viewable_queryset(
cls, user: User, queryset: QuerySet[Study]
Copy link
Contributor

@RoordinkFrank RoordinkFrank Oct 17, 2025

Choose a reason for hiding this comment

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

edited

This overlaps with can_be_viewed_by. I assume GraphQL is the reason that is has to be this way? Would it be possible to get these two if statements in a separate method?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will work on this. Xander is also asking me.

Copy link
Contributor

@RoordinkFrank RoordinkFrank left a comment

Choose a reason for hiding this comment

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

I do really like the naming of everything you have made so far, looks great. Commnents are in the code.

Copy link
Contributor

@XanderVertegaal XanderVertegaal left a comment

Choose a reason for hiding this comment

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

Thanks for putting all the work in; it already works quite well and I love the human-readable syntax! <3

Most of my comments are nitpicks, but the one about reusing shared logic is one I would insist on. Feel free to fight me on anything else.

StudyType,
)

def resolve_study(root, info: ResolveInfo, id) -> Optional[Study]:
Copy link
Contributor

Choose a reason for hiding this comment

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

Decorate these resolvers with @staticmethod. That gets rid of the yellow squiggly (if you have one): "Instance methods should take a "self" parameter."

)
return queryset.all()

def resolve_my_study(root, info: ResolveInfo, id) -> Optional[Study]:
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of using 4 resolvers, it is shorter 2 and add an optional parameter editable to only see studies that you are allowed to edit.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, you may want to type id as id: str (or id: str | None if it is an optional param).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But id is an int right?

Copy link
Contributor

Choose a reason for hiding this comment

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

Django object IDs are int, but GraphQL IDs are treated as str, I believe. At least in DIAPP, where I tested this, type(id) yields str.

)
return cls(errors=[error])

# If permission check gets passed, save() and return the study
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this comment is needed.


user = info.context.user
# Initialize the study, but do not save()
study = Study(title=title, created_by=user)
Copy link
Contributor

Choose a reason for hiding this comment

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

The GraphQL endpoint is not authenticated, so user may be AnonymousUser. When I try to create a user without logging in, I see: "Cannot assign \"<SimpleLazyObject: <django.contrib.auth.models.AnonymousUser object at 0x00000255E7457BF0>>\": \"Study.created_by\" must be a \"User\" instance."

Perhaps it's good to do an early return if the user is None or AnonymousUser.

Comment on lines +8 to +9
PRIVACY_OFFICER = "Privacy officer"
FETC_MEMBER = "FETC member"
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you put these constants in a TextChoices enum?

Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting! I had no idea you could create dumps-as-migrations in this way. I'm not sure whether I am a fan of this. I think I'd rather keep fixtures/data separate from DB migrations. What do you think @miggol?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm a fan of using data migrations over fixtures wherever possible! Will elaborate in the meeting

@EdoStorm96
Copy link
Contributor Author

Alright, thanks for all your valuable feedback. I've made a major overhaul of the design, which does retain the nice syntax, but keeps permission logic in a centralized place. To achieve this, I've chosen to always reason from querysets.

I've made a custom manager, which has some new methods. The most important one being accessible_objects(). This method can be called like so for example: Study.objects.accessible_objects(user, MRPermission.EDIT) and will only return objects that the user has access to based on the permission type. I've made a base version of this manager in main.utils.permission_utils, which can be inherited from for specific objects, see StudyManager for an example of this.

I've also retained the User.has_access_to() method trough a little workaround, but it all uses the same logic. This method is not used currently, but can come in handy.

Talking to @XanderVertegaal and @miggol, we realized this PR does not really cover create permissions. I have made a placeholder for this (Study.can_be_created_by: it just checks if the user is authenticated.), but this should be implemented more extensively later.

From my testing it is all still working well, but the code has really fallen into place much nicer! I'm curious to hear your further thoughts!

Copy link
Contributor

@miggol miggol left a comment

Choose a reason for hiding this comment

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

Just comments for now! Let's discuss this further in the meeting

| 1.2 Create | Access to own | No access | Access to own | Access to own |
| 1.3 Update | Access to own | No access | Access to own | Access to own |
| 1.4 Delete | Access to own | No access | Access to own | Access to own |
| 1.5 View PDF | Access to own | No access | Access to own | Access to own |
Copy link
Contributor

Choose a reason for hiding this comment

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

You've explained to me that these two tables represent the pre and post submission situations. That could be made clearer in this documentation.

LOCAL_APPS = [
"main",
"form",
"study",
Copy link
Contributor

Choose a reason for hiding this comment

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

Now we're adding models I think it's worth having a talk about project structure, and how many distinct apps we want to end up with

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm a fan of using data migrations over fixtures wherever possible! Will elaborate in the meeting

Comment on lines 19 to 24
def has_access_to(self, object, mrpermission):
"""
Utility function to check if a user has access to a specific object,
with a specific permission.
"""
return object in object.__class__.objects.accessible_objects(self, mrpermission)
Copy link
Contributor

Choose a reason for hiding this comment

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

This could be (complemented by) can_view(self, object,) and can_edit(self, object,)

Personally I'd like to omit the opaque mrpermission argument wherever possible.

Comment on lines +27 to +35
class MRPermission(models.TextChoices):
"""
PermissionTypes are used in ObjectType's get_queryset method, to decide
which queryset gets returned or whether a user has access to an object,
based on the user's permissions.
"""

VIEW = "View"
EDIT = "Edit"
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't like mister permission. Passing around this extra argument, that can only have one of two values, to determine what you actually want to do? I would rather use two separate methods for viewing and editing wherever possible and forget about the argument. This makes for easier debugging as well.

For example, we could agree that StudyType.get_queryset just always means "view" by convention, and have a separate method for when we specifically want to access editable Studies.

Or we could annotate the get_queryset() results with an attribute stating if each Study is editable by the requesting user, provided both methods assuming editable studies will always be a subset of visible studies.

I don't know if that makes Graphene sense though, perhaps mrpermission is the right way of doing things over in GraphQL land. But even then I would prefer integrating that into the info object, perhaps @XanderVertegaal knows if this is possible.

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.

Think of a good system to implement permissions Create an overview of the required groups/permissions

6 participants