permissible 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Gaussian
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.2
2
+ Name: permissible
3
+ Version: 0.3.1
4
+ Summary: Extended, flexible and powerful object-level and rule-driven permissions for Django & Django REST framework
5
+ Project-URL: Homepage, https://github.com/gaussian/permissible
6
+ Project-URL: Issues, https://github.com/gaussian/permissible/issues
7
+ Requires-Python: >=3.12
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: Django<6,>=5
11
+ Requires-Dist: djangorestframework>=3
12
+ Requires-Dist: django-guardian
13
+ Requires-Dist: djangorestframework-guardian
14
+
15
+ `permissible` is a module to make it easier to configure object-level permissions,
16
+ and to help unify the different places performing permissions checks (including DRF
17
+ and Django admin) to create a full permissions check that can work without any
18
+ further architectural pondering.
19
+
20
+ It is built on top of django-guardian but can be easily configured for other
21
+ object-level libraries.
22
+
23
+
24
+ # Introduction
25
+
26
+ This module allows us to define permission requirements in our Models
27
+ (similarly to how django-rules does it in Model.Meta). Given that different
28
+ view engines (e.g. DRF vs Django's admin) have different implementations for
29
+ checking permissions, this allows us to centralize the permissions
30
+ configuration and keep the code clear and simple. This approach also allows
31
+ us to unify permissions checks across both Django admin and DRF (and indeed
32
+ any other place you use PermissibleMixin).
33
+
34
+ # Installation
35
+
36
+ Install with `pip install https://github.com/gaussian/permissible.git`.
37
+
38
+
39
+ # Features
40
+
41
+ ## Feature 1: Consistent permissions configuration
42
+
43
+ In its simplest form, `permissible` can be used just for its permissions
44
+ configuration. This has no impact on your database, and does not rely on any
45
+ particular object-level permissions library. (It does require one; we prefer
46
+ django-guardian.)
47
+
48
+ Here, we add the `PermissibleMixin` to each model we want to protect, and
49
+ define "permissions maps" that define what permissions are needed for each action
50
+ that is taken on an object in the model (e.g. a "retrieve" action on a "survey").
51
+ (We can also use classes like `PermissibleSelfOnlyMixin` to define good default
52
+ permission maps for our models.)
53
+
54
+ With the permissions configured, now we can force different views to use them:
55
+ - If you would like the permissions to work for API views (via
56
+ django-rest-framework): Add `PermissiblePerms` to the `permission_classes` for
57
+ the viewsets for our models
58
+ - If you would like the permissions to work in the Django admin: Add
59
+ `PermissibleAdminMixin` to the admin classes for our models
60
+
61
+ That's it. Actions are now protected by permissions checks. But there is no easy
62
+ way to create the permissions in the first place. That's where the next two
63
+ features come in.
64
+
65
+
66
+ ## Feature 2: Simple permissions assignment using "root" models
67
+
68
+ The `permissible` library can also help automatically assign permissions based on
69
+ certain "root" models. The root model is the model we should check permissions
70
+ against. For instance, the root model for a "project file" might be a "project",
71
+ in which case having certain permissions on the "project" would confer other
72
+ permissions for the "project files", even though no specific permission exists
73
+ for the "project file".
74
+ Of course, it's easy to link a "project" to a "project file" through a foreign key.
75
+ But `permissible` solves the problem of tying this to the Django `Group` model,
76
+ which is what we use for permissions.
77
+
78
+ To accomplish this, `permissible` provides two base model classes that you should use:
79
+ 1. **`PermRoot`**: Make the root model (e.g. `Team`) derive from `PermRoot`
80
+ 2. **`PermRootGroup`**: Create a new model that derives from `PermRootGroup`
81
+ and has a `ForeignKey` to the root model
82
+
83
+ You can then simply adjust your permissions maps in `PermissibleMixin` to
84
+ incorporate checking of the root model for permissions. See the documentation for
85
+ `PermDef` and `PermissibleMixin.has_object_permissions` for info and examples.
86
+
87
+ You can also use `PermRootAdminMixin` to help you manage the `PermRoot` records.
88
+
89
+
90
+ ## Feature 3: Assignment on record creation
91
+
92
+ `permissible` can automatically assign object permissions on object creation,
93
+ through use of 3 view-related mixins:
94
+ - `admin.PermissibleObjectAssignMixin` (for admin classes - give creating user all
95
+ permissions)
96
+ - `serializers.PermissibleObjectAssignMixin` (for serializers - give creating user
97
+ all permissions)
98
+ - `serializers.PermissibleRootObjectAssignMixin` (for serializers for root models
99
+ like "Team" or "Project - add creating user to all root model's Groups)
100
+
101
+ NOTE: this feature is dependent on django-guardian, as it uses the `assign_perm`
102
+ shortcut. Also, `admin.PermissibleObjectAssignMixin` extends the
103
+ `ObjectPermissionsAssignmentMixin` mixin from djangorestframework-guardian.
104
+
105
+
106
+ # Full instructions
107
+
108
+ 1.
109
+
110
+
111
+ # Example flow
112
+
113
+ - The application has the following models:
114
+ - `User` (inherits Django's base abstract user model)
115
+ - `Group` (Django's model)
116
+ - `Team` (inherits `PermRoot`)
117
+ - `TeamGroup` (inherits `PermRootGroup`)
118
+ - `TeamInfo` (contains a foreign key to `Team`)
119
+
120
+ ### Create a team
121
+ - A new team is created (via Django admin), which triggers the creation of appropriate
122
+ groups and assignment of permissions:
123
+ - `Team.save()` creates several `TeamGroup` records, one for each possible role
124
+ (e.g. member, owner)
125
+ - For each `TeamGroup`, the `save()` method triggers the creation of a new `Group`,
126
+ and assigns permissions to each of these groups, in accordance with
127
+ `PermRootGroup.role_definitions`:
128
+ - `TeamGroup` with "Member" role is given no permissions
129
+ - `TeamGroup` with "Viewer" role is given "view_team" permission
130
+ - `TeamGroup` with "Contributor" role is given "contribute_to_team" and "view_team"
131
+ permissions
132
+ - `TeamGroup` with "Admin" role is given "change_team", "contribute_to_team" and
133
+ "view_team" permissions
134
+ - `TeamGroup` with "Owner" role is given "delete", "change_team", "contribute_to_team"
135
+ and "view_team" permissions
136
+ - (NOTE: this behavior can be customized)
137
+ - Note that no one is given permission to create `Team` to begin with - it must have
138
+ been created by a superuser or someone who was manually given such permission in the admin
139
+
140
+ ### Create a user
141
+ - A new user is created (via Django admin), and added to the relevant groups (e.g. members, admins)
142
+
143
+ ### Edit a team-related record
144
+ - The user tries to edit a `TeamInfo` record, either via API (django-rest-framework) or Django
145
+ admin, triggering the following checks:
146
+ - View/viewset checks global permissions
147
+ - View/viewset checks object permissions:
148
+ - Checking object permission directly FAILS (as this user was not given any permission for
149
+ this object in particular)
150
+ - Checking permission for root object (i.e. team) SUCCEEDS if the user was added to the
151
+ correct groups
152
+
153
+ ### Create a team-related record
154
+ - The user tries to create a `TeamInfo` record, either via API (django-rest-framework) or Django
155
+ admin, triggering the following checks:
156
+ - View/viewset checks global permissions
157
+ - View/viewset checks creation permissions:
158
+ - Checking object permission directly FAILS as this object doesn't have an ID yet, so
159
+ can't have any permissions associated with it
160
+ - Checking permission for root object (i.e. team) SUCCEEDS if the user was added to the
161
+ correct groups
162
+ - View/viewset does not check object permission (this is out of our control, and makes sense
163
+ as there is no object)
164
+ - Upon create
@@ -0,0 +1,150 @@
1
+ `permissible` is a module to make it easier to configure object-level permissions,
2
+ and to help unify the different places performing permissions checks (including DRF
3
+ and Django admin) to create a full permissions check that can work without any
4
+ further architectural pondering.
5
+
6
+ It is built on top of django-guardian but can be easily configured for other
7
+ object-level libraries.
8
+
9
+
10
+ # Introduction
11
+
12
+ This module allows us to define permission requirements in our Models
13
+ (similarly to how django-rules does it in Model.Meta). Given that different
14
+ view engines (e.g. DRF vs Django's admin) have different implementations for
15
+ checking permissions, this allows us to centralize the permissions
16
+ configuration and keep the code clear and simple. This approach also allows
17
+ us to unify permissions checks across both Django admin and DRF (and indeed
18
+ any other place you use PermissibleMixin).
19
+
20
+ # Installation
21
+
22
+ Install with `pip install https://github.com/gaussian/permissible.git`.
23
+
24
+
25
+ # Features
26
+
27
+ ## Feature 1: Consistent permissions configuration
28
+
29
+ In its simplest form, `permissible` can be used just for its permissions
30
+ configuration. This has no impact on your database, and does not rely on any
31
+ particular object-level permissions library. (It does require one; we prefer
32
+ django-guardian.)
33
+
34
+ Here, we add the `PermissibleMixin` to each model we want to protect, and
35
+ define "permissions maps" that define what permissions are needed for each action
36
+ that is taken on an object in the model (e.g. a "retrieve" action on a "survey").
37
+ (We can also use classes like `PermissibleSelfOnlyMixin` to define good default
38
+ permission maps for our models.)
39
+
40
+ With the permissions configured, now we can force different views to use them:
41
+ - If you would like the permissions to work for API views (via
42
+ django-rest-framework): Add `PermissiblePerms` to the `permission_classes` for
43
+ the viewsets for our models
44
+ - If you would like the permissions to work in the Django admin: Add
45
+ `PermissibleAdminMixin` to the admin classes for our models
46
+
47
+ That's it. Actions are now protected by permissions checks. But there is no easy
48
+ way to create the permissions in the first place. That's where the next two
49
+ features come in.
50
+
51
+
52
+ ## Feature 2: Simple permissions assignment using "root" models
53
+
54
+ The `permissible` library can also help automatically assign permissions based on
55
+ certain "root" models. The root model is the model we should check permissions
56
+ against. For instance, the root model for a "project file" might be a "project",
57
+ in which case having certain permissions on the "project" would confer other
58
+ permissions for the "project files", even though no specific permission exists
59
+ for the "project file".
60
+ Of course, it's easy to link a "project" to a "project file" through a foreign key.
61
+ But `permissible` solves the problem of tying this to the Django `Group` model,
62
+ which is what we use for permissions.
63
+
64
+ To accomplish this, `permissible` provides two base model classes that you should use:
65
+ 1. **`PermRoot`**: Make the root model (e.g. `Team`) derive from `PermRoot`
66
+ 2. **`PermRootGroup`**: Create a new model that derives from `PermRootGroup`
67
+ and has a `ForeignKey` to the root model
68
+
69
+ You can then simply adjust your permissions maps in `PermissibleMixin` to
70
+ incorporate checking of the root model for permissions. See the documentation for
71
+ `PermDef` and `PermissibleMixin.has_object_permissions` for info and examples.
72
+
73
+ You can also use `PermRootAdminMixin` to help you manage the `PermRoot` records.
74
+
75
+
76
+ ## Feature 3: Assignment on record creation
77
+
78
+ `permissible` can automatically assign object permissions on object creation,
79
+ through use of 3 view-related mixins:
80
+ - `admin.PermissibleObjectAssignMixin` (for admin classes - give creating user all
81
+ permissions)
82
+ - `serializers.PermissibleObjectAssignMixin` (for serializers - give creating user
83
+ all permissions)
84
+ - `serializers.PermissibleRootObjectAssignMixin` (for serializers for root models
85
+ like "Team" or "Project - add creating user to all root model's Groups)
86
+
87
+ NOTE: this feature is dependent on django-guardian, as it uses the `assign_perm`
88
+ shortcut. Also, `admin.PermissibleObjectAssignMixin` extends the
89
+ `ObjectPermissionsAssignmentMixin` mixin from djangorestframework-guardian.
90
+
91
+
92
+ # Full instructions
93
+
94
+ 1.
95
+
96
+
97
+ # Example flow
98
+
99
+ - The application has the following models:
100
+ - `User` (inherits Django's base abstract user model)
101
+ - `Group` (Django's model)
102
+ - `Team` (inherits `PermRoot`)
103
+ - `TeamGroup` (inherits `PermRootGroup`)
104
+ - `TeamInfo` (contains a foreign key to `Team`)
105
+
106
+ ### Create a team
107
+ - A new team is created (via Django admin), which triggers the creation of appropriate
108
+ groups and assignment of permissions:
109
+ - `Team.save()` creates several `TeamGroup` records, one for each possible role
110
+ (e.g. member, owner)
111
+ - For each `TeamGroup`, the `save()` method triggers the creation of a new `Group`,
112
+ and assigns permissions to each of these groups, in accordance with
113
+ `PermRootGroup.role_definitions`:
114
+ - `TeamGroup` with "Member" role is given no permissions
115
+ - `TeamGroup` with "Viewer" role is given "view_team" permission
116
+ - `TeamGroup` with "Contributor" role is given "contribute_to_team" and "view_team"
117
+ permissions
118
+ - `TeamGroup` with "Admin" role is given "change_team", "contribute_to_team" and
119
+ "view_team" permissions
120
+ - `TeamGroup` with "Owner" role is given "delete", "change_team", "contribute_to_team"
121
+ and "view_team" permissions
122
+ - (NOTE: this behavior can be customized)
123
+ - Note that no one is given permission to create `Team` to begin with - it must have
124
+ been created by a superuser or someone who was manually given such permission in the admin
125
+
126
+ ### Create a user
127
+ - A new user is created (via Django admin), and added to the relevant groups (e.g. members, admins)
128
+
129
+ ### Edit a team-related record
130
+ - The user tries to edit a `TeamInfo` record, either via API (django-rest-framework) or Django
131
+ admin, triggering the following checks:
132
+ - View/viewset checks global permissions
133
+ - View/viewset checks object permissions:
134
+ - Checking object permission directly FAILS (as this user was not given any permission for
135
+ this object in particular)
136
+ - Checking permission for root object (i.e. team) SUCCEEDS if the user was added to the
137
+ correct groups
138
+
139
+ ### Create a team-related record
140
+ - The user tries to create a `TeamInfo` record, either via API (django-rest-framework) or Django
141
+ admin, triggering the following checks:
142
+ - View/viewset checks global permissions
143
+ - View/viewset checks creation permissions:
144
+ - Checking object permission directly FAILS as this object doesn't have an ID yet, so
145
+ can't have any permissions associated with it
146
+ - Checking permission for root object (i.e. team) SUCCEEDS if the user was added to the
147
+ correct groups
148
+ - View/viewset does not check object permission (this is out of our control, and makes sense
149
+ as there is no object)
150
+ - Upon create
@@ -0,0 +1 @@
1
+ __version__ = "0.3.1"
@@ -0,0 +1,160 @@
1
+ """
2
+ `permissible` (a `neutron` module by Gaussian)
3
+ Author: Kut Akdogan & Gaussian Holdings, LLC. (2016-)
4
+ """
5
+
6
+ from collections import OrderedDict
7
+ from itertools import chain
8
+
9
+ from django.contrib import admin
10
+ from django.contrib.admin.widgets import AutocompleteSelect
11
+ from django.contrib.auth import get_user_model
12
+ from django.core.exceptions import ValidationError
13
+ from django import forms
14
+ from django.http import Http404
15
+ from django.template.response import TemplateResponse
16
+ from django.urls import path, reverse
17
+ from django.utils.html import format_html
18
+
19
+ from .models import PermissibleMixin
20
+
21
+ User = get_user_model()
22
+
23
+
24
+ class PermissibleAdminMixin(object):
25
+ """
26
+ Restricts viewing, editing, changing, and deleting on an object to those
27
+ who have the necessary permissions for that object.
28
+
29
+ Models that are to be protected in this way should use `PermissibleMixin`,
30
+ and the necessary permissions should be configured using `global_action_perm_map`
31
+ and `obj_action_perm_map` from that mixin.
32
+
33
+ Requires use of an object-level permissions library/schema such as
34
+ django-guardian.
35
+ """
36
+
37
+ def _has_permission(self, action: str, request, obj: PermissibleMixin):
38
+ assert issubclass(self.model, PermissibleMixin), \
39
+ "Must use `PermissibleMixin` on the model class"
40
+
41
+ # Permission checks
42
+ perm_check_kwargs = {
43
+ "user": request.user,
44
+ "action": action,
45
+ "context": {"request": request}
46
+ }
47
+ if not obj:
48
+ if not self.model.has_global_permission(**perm_check_kwargs):
49
+ return False
50
+ if action != "create":
51
+ # Not sure how we"d reach here...
52
+ return False
53
+ # For "create" action, we must create a dummy object from request data
54
+ # and use it to check permissions against
55
+ obj = self.model.make_objs_from_data(request.data)[0]
56
+ return obj.has_object_permission(**perm_check_kwargs)
57
+
58
+ def has_add_permission(self, request, obj=None):
59
+ return self._has_permission("create", request=request, obj=obj)
60
+
61
+ def has_change_permission(self, request, obj=None):
62
+ return self._has_permission("update", request=request, obj=obj)
63
+
64
+ def has_delete_permission(self, request, obj=None):
65
+ return self._has_permission("destroy", request=request, obj=obj)
66
+
67
+ def has_view_permission(self, request, obj=None):
68
+ return self._has_permission("retrieve", request=request, obj=obj) or \
69
+ self._has_permission("update", request=request, obj=obj)
70
+
71
+
72
+ class PermissibleObjectAssignMixin(object):
73
+ pass
74
+
75
+
76
+ class PermRootForm(forms.Form):
77
+ add = forms.BooleanField(initial=True, required=False, label="Add groups (uncheck to remove)")
78
+
79
+ def __init__(self, perm_root_class, *args, **kwargs):
80
+ super().__init__(*args, **kwargs)
81
+
82
+ perm_root_group_class = perm_root_class.get_group_join_rel().related_model
83
+ role_choices = ((role_value, role_label)
84
+ for role_value, (role_label, _) in perm_root_group_class.ROLE_DEFINITIONS.items())
85
+
86
+ # Get related field, to make an autocomplete widget
87
+ users_field = perm_root_class._meta.get_field("users")
88
+
89
+ self.fields.update(dict(
90
+ user=forms.ModelChoiceField(queryset=User.objects.all(),
91
+ widget=AutocompleteSelect(users_field, admin.site)),
92
+ roles=forms.MultipleChoiceField(choices=role_choices)
93
+ ))
94
+
95
+
96
+ class PermRootAdminMixin(object):
97
+ def get_urls(self):
98
+ urls = super().get_urls()
99
+ custom_urls = [
100
+ path("<object_id>/permissible/", self.admin_site.admin_view(self.permissible_view), name=self.get_permissible_change_url_name())
101
+ ]
102
+ return custom_urls + urls
103
+
104
+ def permissible_view(self, request, object_id):
105
+ obj = self.model.objects.get(pk=object_id)
106
+
107
+ if not self.has_change_permission(request=request, obj=obj):
108
+ raise Http404("Lacking permission")
109
+
110
+ if request.method == "POST":
111
+ form = PermRootForm(self.model, request.POST)
112
+ if form.is_valid():
113
+ roles = form.cleaned_data["roles"] or []
114
+ if not request.user.is_superuser and any(r in roles for r in ("adm", "own")):
115
+ raise ValidationError(f"Bad roles, must be superuser: {roles}")
116
+ user = form.cleaned_data["user"]
117
+ if form.cleaned_data["add"]:
118
+ obj.add_user_to_groups(user=user, roles=roles)
119
+ else:
120
+ obj.remove_user_from_groups(user=user, roles=roles)
121
+ else:
122
+ form = PermRootForm(self.model)
123
+
124
+ unordered_role_to_users = {perm_root_group.role: [
125
+ str(u) for u in perm_root_group.group.user_set.values_list(User.USERNAME_FIELD, flat=True)
126
+ ] for perm_root_group in obj.get_group_joins().all()}
127
+
128
+ base_roles = ("own", "adm", "con", "view", "mem")
129
+ role_to_users = OrderedDict()
130
+ for role in base_roles:
131
+ role_to_users[role] = unordered_role_to_users.get(role, [])
132
+ for role in unordered_role_to_users.keys():
133
+ if role not in base_roles:
134
+ role_to_users[role] = unordered_role_to_users.get(role, [])
135
+
136
+ users = list(set(chain(*role_to_users.values())))
137
+
138
+ context = {
139
+ "title": f"Add users to permissible groups of {obj}",
140
+ "form": form,
141
+ "role_to_users": role_to_users,
142
+ "users": users,
143
+ "opts": self.model._meta,
144
+ # Include common variables for rendering the admin template.
145
+ **self.admin_site.each_context(request),
146
+ }
147
+ return TemplateResponse(request, "admin/permissible_changeform.html", context)
148
+
149
+ readonly_fields = (
150
+ "permissible_groups_link",
151
+ )
152
+
153
+ def get_permissible_change_url_name(self):
154
+ return "%s_%s_permissible_change" % (self.model._meta.app_label, self.model._meta.model_name)
155
+
156
+ def permissible_groups_link(self, obj):
157
+ url = reverse("admin:" + self.get_permissible_change_url_name(), args=(obj.pk,))
158
+ link_text = "Edit permissible groups"
159
+ html_format_string = "<a href=' {url}'>{link_text}</a>" # SPACE IS NEEDED!
160
+ return format_html(html_format_string, url=url, text=link_text)
File without changes
@@ -0,0 +1,3 @@
1
+ # TODO: make this more DRY
2
+ def before_scenario(context, scenario):
3
+ context.responses = []
@@ -0,0 +1,84 @@
1
+ """
2
+ `permissible` (a `neutron` module by Gaussian)
3
+ Author: Kut Akdogan & Gaussian Holdings, LLC. (2016-)
4
+ """
5
+
6
+ from django.conf import settings
7
+ from rest_framework import filters
8
+ from rest_framework.exceptions import PermissionDenied
9
+ from rest_framework_guardian.filters import ObjectPermissionsFilter
10
+
11
+
12
+ class PermissibleFilter(ObjectPermissionsFilter):
13
+ """
14
+ Same as django-rest-framework-guardian's `ObjectPermissionsFilter`,
15
+ but does not perform filtering for detail routes (i.e. routes that
16
+ retrieve a specific object).
17
+ """
18
+
19
+ def filter_queryset(self, request, queryset, view):
20
+ if view.detail:
21
+ return queryset
22
+ else:
23
+ return super().filter_queryset(request, queryset, view)
24
+
25
+
26
+ class PermissibleRootFilter(filters.BaseFilterBackend):
27
+ """
28
+ For a defined set of fields on the view (`view.filter_perm_fields`),
29
+ check permission on each field AND filter down to that field.
30
+
31
+ e.g. for listable "survey questions", we might want to return those
32
+ survey questions that are owned by "surveys" to which this user has
33
+ access
34
+
35
+ NOTE: as with `PermissibleFilter`, we do not perform filtering for
36
+ detail routes (i.e. routes that retrieve a specific object).
37
+ """
38
+
39
+ def filter_queryset(self, request, queryset, view):
40
+ if view.detail:
41
+ return queryset
42
+
43
+ assert hasattr(view, "filter_perm_fields"), \
44
+ "Badly configured view, need `filter_perm_fields`."
45
+
46
+ assert not any("__" in f for f in view.filter_perm_fields), \
47
+ f"Cannot yet accommodate joined fields in `PermissibleRootFilter`: {view.filter_perm_fields}"
48
+
49
+ # For each "permission" field, check permissions, then filter the queryset
50
+ for perm_filterset_fields, needed_short_perm_code in view.filter_perm_fields:
51
+
52
+ # Get related object (e.g. "Team" from "team_id"), nested if need be
53
+ model_class = queryset.model
54
+ related_obj = None
55
+ if not isinstance(perm_filterset_fields, (tuple, list)):
56
+ perm_filterset_fields = (perm_filterset_fields,)
57
+ field_for_filter, pk_for_filter = None, None
58
+ for i, perm_filterset_field in enumerate(perm_filterset_fields):
59
+ related_model = model_class._meta.get_field(perm_filterset_field).related_model
60
+ # First item - get the ID value from the query params object
61
+ if i == 0:
62
+ related_pk = request.query_params.get(perm_filterset_field)
63
+ field_for_filter = perm_filterset_field
64
+ pk_for_filter = related_pk
65
+ # Not the first item, i.e. this is nested - get ID value by traversing the nesting
66
+ else:
67
+ related_obj.refresh_from_db()
68
+ related_pk = getattr(related_obj, perm_filterset_field)
69
+ related_obj = related_model(pk=related_pk)
70
+ model_class = related_model
71
+
72
+ # Check permission for related object
73
+ perm = f"{related_model._meta.app_label}.{needed_short_perm_code}_{related_model._meta.model_name}"
74
+ if not request.user.has_perm(perm, related_obj):
75
+ message = "Permission denied in filter"
76
+ if settings.DEBUG or settings.IS_TEST:
77
+ message += f" - {perm}"
78
+ raise PermissionDenied(message)
79
+
80
+ # Filter, but only by the first perm_filterset_field (`field_for_filter`,
81
+ # the one that is not nested) as we followed the chain of ownership
82
+ queryset = queryset.filter(**{field_for_filter: pk_for_filter})
83
+
84
+ return queryset
@@ -0,0 +1,5 @@
1
+ from .perm_root import PermRootGroup, PermRoot, PermRootUser, build_role_field
2
+ from .base import BasePermRoot, PermRootModelMetaclass
3
+ from .permissible_mixin import PermissibleMixin, PermissibleSelfOnlyMixin, PermissibleRootOnlyMixin, \
4
+ PermissibleBasicRootOnlyMixin, PermissibleSelfOrRootMixin, PermissibleDenyDefaultMixin
5
+ # from .tests import TestPermissibleFromSelf, TestPermRoot, TestPermRootGroup, TestPermRootUser, TestPermissibleFromRoot
@@ -0,0 +1,28 @@
1
+ """
2
+ `permissible` (a `neutron` module by Gaussian)
3
+ Author: Kut Akdogan & Gaussian Holdings, LLC. (2016-)
4
+ """
5
+
6
+ from django.db import models
7
+
8
+ from .permissible_mixin import PermissibleMixin
9
+ from .metaclasses import AbstractModelMetaclass, ExtraPermModelMetaclass
10
+
11
+
12
+ class PermRootModelMetaclass(ExtraPermModelMetaclass, AbstractModelMetaclass):
13
+ permission_definitions = (
14
+ ("add_on_{}", "Can add related records onto {}"),
15
+ ("change_on_{}", "Can change related records on {}"),
16
+ ("change_permission_{}", "Can change permissions of {}"),
17
+ )
18
+
19
+
20
+ class BasePermRoot(PermissibleMixin, models.Model, metaclass=PermRootModelMetaclass):
21
+ """
22
+ A model that acts as the root for a permission hierarchy. This model is primarily
23
+ used as a base class for PermRoot (which has considerable functionality), but
24
+ can also be used directly as a way to add root object permissions without
25
+ the associated PermRootGroup and PermRootUser models and functionality.
26
+ """
27
+ class Meta:
28
+ abstract = True
@@ -0,0 +1,32 @@
1
+ """
2
+ `permissible` (a `neutron` module by Gaussian)
3
+ Author: Kut Akdogan & Gaussian Holdings, LLC. (2016-)
4
+ """
5
+
6
+ from abc import ABCMeta
7
+ from typing import Iterable, Tuple
8
+
9
+ from django.db import models
10
+
11
+
12
+ class AbstractModelMetaclass(ABCMeta, models.base.ModelBase):
13
+ pass
14
+
15
+
16
+ class ExtraPermModelMetaclass(models.base.ModelBase):
17
+ """
18
+ Metaclass to allow model to automatically create extra permissions.
19
+ """
20
+ permission_definitions = () # type: Iterable[Tuple[str, str]]
21
+
22
+ def __new__(mcs, name, bases, attrs):
23
+ new_class = super().__new__(mcs, name, bases, attrs)
24
+
25
+ new_class._meta.permissions = new_class._meta.permissions or tuple()
26
+ new_class._meta.permissions += tuple(
27
+ (codename.format(new_class._meta.model_name), description.format(new_class._meta.verbose_name))
28
+ for codename, description in mcs.permission_definitions
29
+ )
30
+ new_class._meta.original_attrs["permissions"] = new_class._meta.permissions
31
+
32
+ return new_class