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.
- permissible-0.3.1/LICENSE +21 -0
- permissible-0.3.1/PKG-INFO +164 -0
- permissible-0.3.1/README.md +150 -0
- permissible-0.3.1/permissible/__init__.py +1 -0
- permissible-0.3.1/permissible/admin.py +160 -0
- permissible-0.3.1/permissible/features/__init__.py +0 -0
- permissible-0.3.1/permissible/features/environment.py +3 -0
- permissible-0.3.1/permissible/features/steps/__init__.py +0 -0
- permissible-0.3.1/permissible/filters.py +84 -0
- permissible-0.3.1/permissible/models/__init__.py +5 -0
- permissible-0.3.1/permissible/models/base.py +28 -0
- permissible-0.3.1/permissible/models/metaclasses.py +32 -0
- permissible-0.3.1/permissible/models/perm_root.py +368 -0
- permissible-0.3.1/permissible/models/permissible_mixin.py +452 -0
- permissible-0.3.1/permissible/models/tests.py +46 -0
- permissible-0.3.1/permissible/perm_def.py +133 -0
- permissible-0.3.1/permissible/permissions.py +123 -0
- permissible-0.3.1/permissible/serializers.py +55 -0
- permissible-0.3.1/permissible/signals.py +81 -0
- permissible-0.3.1/permissible/utils/signals.py +42 -0
- permissible-0.3.1/permissible/views.py +17 -0
- permissible-0.3.1/permissible.egg-info/PKG-INFO +164 -0
- permissible-0.3.1/permissible.egg-info/SOURCES.txt +26 -0
- permissible-0.3.1/permissible.egg-info/dependency_links.txt +1 -0
- permissible-0.3.1/permissible.egg-info/requires.txt +4 -0
- permissible-0.3.1/permissible.egg-info/top_level.txt +1 -0
- permissible-0.3.1/pyproject.toml +23 -0
- permissible-0.3.1/setup.cfg +4 -0
|
@@ -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
|
|
File without changes
|
|
@@ -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
|