Skip to content

Commit 3a0dcab

Browse files
authored
Merge pull request #188 from nyaruka/perms
Refactor permissions code
2 parents 45df7fb + 78aa651 commit 3a0dcab

File tree

5 files changed

+195
-210
lines changed

5 files changed

+195
-210
lines changed

smartmin/__init__.py

-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
1-
from __future__ import unicode_literals
2-
31
__version__ = "5.0.2"

smartmin/management/__init__.py

-168
Original file line numberDiff line numberDiff line change
@@ -1,168 +0,0 @@
1-
import sys
2-
3-
from django.apps import apps
4-
from django.conf import settings
5-
from django.contrib.auth.models import Group, Permission
6-
from django.contrib.contenttypes.models import ContentType
7-
from django.core.exceptions import ObjectDoesNotExist
8-
from django.db.models.signals import post_migrate
9-
10-
from smartmin.perms import assign_perm, remove_perm
11-
12-
permissions_app_name = None
13-
14-
15-
def get_permissions_app_name():
16-
"""
17-
Gets the app after which smartmin permissions should be installed. This can be specified by PERMISSIONS_APP in the
18-
Django settings or defaults to the last app with models
19-
"""
20-
global permissions_app_name
21-
22-
if not permissions_app_name:
23-
permissions_app_name = getattr(settings, "PERMISSIONS_APP", None)
24-
25-
if not permissions_app_name:
26-
app_names_with_models = [a.name for a in apps.get_app_configs() if a.models_module is not None]
27-
if app_names_with_models:
28-
permissions_app_name = app_names_with_models[-1]
29-
30-
return permissions_app_name
31-
32-
33-
def is_permissions_app(app_config):
34-
"""
35-
Returns whether this is the app after which permissions should be installed.
36-
"""
37-
return app_config.name == get_permissions_app_name()
38-
39-
40-
def check_role_permissions(role, permissions, current_permissions):
41-
"""
42-
Checks the the passed in role (can be user, group or AnonymousUser) has all the passed
43-
in permissions, granting them if necessary.
44-
"""
45-
role_permissions = []
46-
47-
# get all the current permissions, we'll remove these as we verify they should still be granted
48-
for permission in permissions:
49-
splits = permission.split(".")
50-
if len(splits) != 2 and len(splits) != 3:
51-
sys.stderr.write(" invalid permission %s, ignoring\n" % permission)
52-
continue
53-
54-
app = splits[0]
55-
codenames = []
56-
57-
if len(splits) == 2:
58-
codenames.append(splits[1])
59-
else:
60-
(object, action) = splits[1:]
61-
62-
# if this is a wildcard, then query our database for all the permissions that exist on this object
63-
if action == "*":
64-
for perm in Permission.objects.filter(codename__startswith="%s_" % object, content_type__app_label=app):
65-
codenames.append(perm.codename)
66-
# otherwise, this is an error, continue
67-
else:
68-
sys.stderr.write(" invalid permission %s, ignoring\n" % permission)
69-
continue
70-
71-
if len(codenames) == 0:
72-
continue
73-
74-
for codename in codenames:
75-
# the full codename for this permission
76-
full_codename = "%s.%s" % (app, codename)
77-
78-
# this marks all the permissions which should remain
79-
role_permissions.append(full_codename)
80-
81-
try:
82-
assign_perm(full_codename, role)
83-
except ObjectDoesNotExist:
84-
pass
85-
# sys.stderr.write(" unknown permission %s, ignoring\n" % permission)
86-
87-
# remove any that are extra
88-
for permission in current_permissions:
89-
if isinstance(permission, str):
90-
key = permission
91-
else:
92-
key = "%s.%s" % (permission.content_type.app_label, permission.codename)
93-
94-
if key not in role_permissions:
95-
remove_perm(key, role)
96-
97-
98-
def check_all_group_permissions(sender, **kwargs):
99-
"""
100-
Checks that all the permissions specified in our settings.py are set for our groups.
101-
"""
102-
if not is_permissions_app(sender):
103-
return
104-
105-
config = getattr(settings, "GROUP_PERMISSIONS", dict())
106-
107-
# for each of our items
108-
for name, permissions in config.items():
109-
# get or create the group
110-
(group, created) = Group.objects.get_or_create(name=name)
111-
if created:
112-
pass
113-
114-
check_role_permissions(group, permissions, group.permissions.all())
115-
116-
117-
def add_permission(content_type, permission):
118-
"""
119-
Adds the passed in permission to that content type. Note that the permission passed
120-
in should be a single word, or verb. The proper 'codename' will be generated from that.
121-
"""
122-
# build our permission slug
123-
codename = "%s_%s" % (content_type.model, permission)
124-
125-
# sys.stderr.write("Checking %s permission for %s\n" % (permission, content_type.name))
126-
127-
# does it already exist
128-
if not Permission.objects.filter(content_type=content_type, codename=codename):
129-
Permission.objects.create(
130-
content_type=content_type, codename=codename, name="Can %s %s" % (permission, content_type.name)
131-
)
132-
# sys.stderr.write("Added %s permission for %s\n" % (permission, content_type.name))
133-
134-
135-
def check_all_permissions(sender, **kwargs):
136-
"""
137-
This syncdb checks our PERMISSIONS setting in settings.py and makes sure all those permissions
138-
actually exit.
139-
"""
140-
if not is_permissions_app(sender):
141-
return
142-
143-
config = getattr(settings, "PERMISSIONS", dict())
144-
145-
# for each of our items
146-
for natural_key, permissions in config.items():
147-
# if the natural key '*' then that means add to all objects
148-
if natural_key == "*":
149-
# for each of our content types
150-
for content_type in ContentType.objects.all():
151-
for permission in permissions:
152-
add_permission(content_type, permission)
153-
154-
# otherwise, this is on a specific content type, add for each of those
155-
else:
156-
app, model = natural_key.split(".")
157-
try:
158-
content_type = ContentType.objects.get_by_natural_key(app, model)
159-
except ContentType.DoesNotExist:
160-
continue
161-
162-
# add each permission
163-
for permission in permissions:
164-
add_permission(content_type, permission)
165-
166-
167-
post_migrate.connect(check_all_permissions)
168-
post_migrate.connect(check_all_group_permissions)

smartmin/models.py

+5
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,13 @@
88

99
from django.conf import settings
1010
from django.db import models
11+
from django.db.models.signals import post_migrate
1112
from django.utils import timezone
1213

14+
from .perms import sync_permissions
15+
16+
post_migrate.connect(sync_permissions)
17+
1318

1419
class SmartImportRowError(Exception):
1520
def __init__(self, message):

smartmin/perms.py

+126-25
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,136 @@
1-
from django.contrib.auth.models import Permission
1+
import re
22

3+
from django.apps import apps
4+
from django.conf import settings
5+
from django.contrib.auth.models import Group, Permission
6+
from django.contrib.contenttypes.models import ContentType
37

4-
def assign_perm(perm, group):
8+
permissions_app_name = None
9+
perm_desc_regex = re.compile(r"(?P<app>\w+)\.(?P<codename>\w+)(?P<wild>\.\*)?")
10+
11+
12+
def get_permissions_app_name():
13+
"""
14+
Gets the app after which smartmin permissions should be installed. This can be specified by PERMISSIONS_APP in the
15+
Django settings or defaults to the last app with models
16+
"""
17+
global permissions_app_name
18+
19+
if not permissions_app_name:
20+
permissions_app_name = getattr(settings, "PERMISSIONS_APP", None)
21+
22+
if not permissions_app_name:
23+
app_names_with_models = [a.name for a in apps.get_app_configs() if a.models_module is not None]
24+
if app_names_with_models:
25+
permissions_app_name = app_names_with_models[-1]
26+
27+
return permissions_app_name
28+
29+
30+
def is_permissions_app(app_config):
31+
"""
32+
Returns whether this is the app after which permissions should be installed.
33+
"""
34+
return app_config.name == get_permissions_app_name()
35+
36+
37+
def update_group_permissions(group, permissions: list):
38+
"""
39+
Checks the the passed in role (can be user, group or AnonymousUser) has all the passed
40+
in permissions, granting them if necessary.
41+
"""
42+
43+
new_permissions = []
44+
45+
for perm_desc in permissions:
46+
app_label, codename, wild = _parse_perm_desc(perm_desc)
47+
48+
if wild:
49+
codenames = Permission.objects.filter(
50+
content_type__app_label=app_label, codename__startswith=f"{codename}_"
51+
).values_list("codename", flat=True)
52+
else:
53+
codenames = [codename]
54+
55+
perms = []
56+
for codename in codenames:
57+
try:
58+
perms.append(Permission.objects.get(content_type__app_label=app_label, codename=codename))
59+
except Permission.DoesNotExist:
60+
raise ValueError(f"Cannot grant permission {app_label}.{codename} as it does not exist.")
61+
62+
new_permissions.append((app_label, codename))
63+
64+
group.permissions.add(*perms)
65+
66+
# remove any that are extra
67+
for perm in group.permissions.select_related("content_type").all():
68+
if (perm.content_type.app_label, perm.codename) not in new_permissions:
69+
group.permissions.remove(perm)
70+
71+
72+
def sync_permissions(sender, **kwargs):
73+
"""
74+
1. Ensures all permissions decribed by the PERMISSIONS setting exist in the database.
75+
2. Ensures all permissions granted by the GROUP_PERMISSIONS setting are granted to the appropriate groups.
76+
"""
77+
78+
if not is_permissions_app(sender):
79+
return
80+
81+
# for each of our items
82+
for natural_key, permissions in getattr(settings, "PERMISSIONS", {}).items():
83+
# if the natural key '*' then that means add to all objects
84+
if natural_key == "*":
85+
# for each of our content types
86+
for content_type in ContentType.objects.all():
87+
for permission in permissions:
88+
_ensure_permission_exists(content_type, permission)
89+
90+
# otherwise, this is on a specific content type, add for each of those
91+
else:
92+
app, model = natural_key.split(".")
93+
try:
94+
content_type = ContentType.objects.get_by_natural_key(app, model)
95+
except ContentType.DoesNotExist:
96+
continue
97+
98+
# add each permission
99+
for permission in permissions:
100+
_ensure_permission_exists(content_type, permission)
101+
102+
# for each of our items
103+
for name, permissions in getattr(settings, "GROUP_PERMISSIONS", {}).items():
104+
# get or create the group
105+
(group, created) = Group.objects.get_or_create(name=name)
106+
if created:
107+
pass
108+
109+
update_group_permissions(group, permissions)
110+
111+
112+
def _parse_perm_desc(desc: str) -> tuple:
5113
"""
6-
Assigns a permission to a group
114+
Parses a permission descriptor into its app_label, model and permission parts, e.g.
115+
app.model.* => app, model, True
116+
app.model_perm => app, model_perm, False
7117
"""
8-
if not isinstance(perm, Permission):
9-
try:
10-
app_label, codename = perm.split(".", 1)
11-
except ValueError:
12-
raise ValueError(
13-
"For global permissions, first argument must be in" " format: 'app_label.codename' (is %r)" % perm
14-
)
15-
perm = Permission.objects.get(content_type__app_label=app_label, codename=codename)
16118

17-
group.permissions.add(perm)
18-
return perm
119+
match = perm_desc_regex.match(desc)
120+
if not match:
121+
raise ValueError(f"Invalid permission descriptor: {desc}")
19122

123+
return match.group("app"), match.group("codename"), bool(match.group("wild"))
20124

21-
def remove_perm(perm, group):
125+
126+
def _ensure_permission_exists(content_type: str, permission: str):
22127
"""
23-
Removes a permission from a group
128+
Adds the passed in permission to that content type. Note that the permission passed
129+
in should be a single word, or verb. The proper 'codename' will be generated from that.
24130
"""
25-
if not isinstance(perm, Permission):
26-
try:
27-
app_label, codename = perm.split(".", 1)
28-
except ValueError:
29-
raise ValueError(
30-
"For global permissions, first argument must be in" " format: 'app_label.codename' (is %r)" % perm
31-
)
32-
perm = Permission.objects.get(content_type__app_label=app_label, codename=codename)
33131

34-
group.permissions.remove(perm)
35-
return
132+
codename = f"{content_type.model}_{permission}" # build our permission slug
133+
134+
Permission.objects.get_or_create(
135+
content_type=content_type, codename=codename, defaults={"name": f"Can {permission} {content_type.name}"}
136+
)

0 commit comments

Comments
 (0)