From 67773511669a2a0d7e3f2f9032ef2f33c8907a8c Mon Sep 17 00:00:00 2001 From: Rowan Seymour Date: Tue, 11 Jun 2024 11:21:18 -0500 Subject: [PATCH] Reorganize permissions code --- smartmin/__init__.py | 2 - smartmin/management/__init__.py | 168 ---------------------------- smartmin/perms.py | 35 ------ smartmin/users/models.py | 6 + smartmin/users/perms.py | 189 ++++++++++++++++++++++++++++++++ test_runner/blog/tests.py | 4 +- 6 files changed, 196 insertions(+), 208 deletions(-) delete mode 100644 smartmin/perms.py create mode 100644 smartmin/users/perms.py diff --git a/smartmin/__init__.py b/smartmin/__init__.py index 3e0ce7e..3a223dd 100644 --- a/smartmin/__init__.py +++ b/smartmin/__init__.py @@ -1,3 +1 @@ -from __future__ import unicode_literals - __version__ = "5.0.2" diff --git a/smartmin/management/__init__.py b/smartmin/management/__init__.py index 6f55757..e69de29 100644 --- a/smartmin/management/__init__.py +++ b/smartmin/management/__init__.py @@ -1,168 +0,0 @@ -import sys - -from django.apps import apps -from django.conf import settings -from django.contrib.auth.models import Group, Permission -from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ObjectDoesNotExist -from django.db.models.signals import post_migrate - -from smartmin.perms import assign_perm, remove_perm - -permissions_app_name = None - - -def get_permissions_app_name(): - """ - Gets the app after which smartmin permissions should be installed. This can be specified by PERMISSIONS_APP in the - Django settings or defaults to the last app with models - """ - global permissions_app_name - - if not permissions_app_name: - permissions_app_name = getattr(settings, "PERMISSIONS_APP", None) - - if not permissions_app_name: - app_names_with_models = [a.name for a in apps.get_app_configs() if a.models_module is not None] - if app_names_with_models: - permissions_app_name = app_names_with_models[-1] - - return permissions_app_name - - -def is_permissions_app(app_config): - """ - Returns whether this is the app after which permissions should be installed. - """ - return app_config.name == get_permissions_app_name() - - -def check_role_permissions(role, permissions, current_permissions): - """ - Checks the the passed in role (can be user, group or AnonymousUser) has all the passed - in permissions, granting them if necessary. - """ - role_permissions = [] - - # get all the current permissions, we'll remove these as we verify they should still be granted - for permission in permissions: - splits = permission.split(".") - if len(splits) != 2 and len(splits) != 3: - sys.stderr.write(" invalid permission %s, ignoring\n" % permission) - continue - - app = splits[0] - codenames = [] - - if len(splits) == 2: - codenames.append(splits[1]) - else: - (object, action) = splits[1:] - - # if this is a wildcard, then query our database for all the permissions that exist on this object - if action == "*": - for perm in Permission.objects.filter(codename__startswith="%s_" % object, content_type__app_label=app): - codenames.append(perm.codename) - # otherwise, this is an error, continue - else: - sys.stderr.write(" invalid permission %s, ignoring\n" % permission) - continue - - if len(codenames) == 0: - continue - - for codename in codenames: - # the full codename for this permission - full_codename = "%s.%s" % (app, codename) - - # this marks all the permissions which should remain - role_permissions.append(full_codename) - - try: - assign_perm(full_codename, role) - except ObjectDoesNotExist: - pass - # sys.stderr.write(" unknown permission %s, ignoring\n" % permission) - - # remove any that are extra - for permission in current_permissions: - if isinstance(permission, str): - key = permission - else: - key = "%s.%s" % (permission.content_type.app_label, permission.codename) - - if key not in role_permissions: - remove_perm(key, role) - - -def check_all_group_permissions(sender, **kwargs): - """ - Checks that all the permissions specified in our settings.py are set for our groups. - """ - if not is_permissions_app(sender): - return - - config = getattr(settings, "GROUP_PERMISSIONS", dict()) - - # for each of our items - for name, permissions in config.items(): - # get or create the group - (group, created) = Group.objects.get_or_create(name=name) - if created: - pass - - check_role_permissions(group, permissions, group.permissions.all()) - - -def add_permission(content_type, permission): - """ - Adds the passed in permission to that content type. Note that the permission passed - in should be a single word, or verb. The proper 'codename' will be generated from that. - """ - # build our permission slug - codename = "%s_%s" % (content_type.model, permission) - - # sys.stderr.write("Checking %s permission for %s\n" % (permission, content_type.name)) - - # does it already exist - if not Permission.objects.filter(content_type=content_type, codename=codename): - Permission.objects.create( - content_type=content_type, codename=codename, name="Can %s %s" % (permission, content_type.name) - ) - # sys.stderr.write("Added %s permission for %s\n" % (permission, content_type.name)) - - -def check_all_permissions(sender, **kwargs): - """ - This syncdb checks our PERMISSIONS setting in settings.py and makes sure all those permissions - actually exit. - """ - if not is_permissions_app(sender): - return - - config = getattr(settings, "PERMISSIONS", dict()) - - # for each of our items - for natural_key, permissions in config.items(): - # if the natural key '*' then that means add to all objects - if natural_key == "*": - # for each of our content types - for content_type in ContentType.objects.all(): - for permission in permissions: - add_permission(content_type, permission) - - # otherwise, this is on a specific content type, add for each of those - else: - app, model = natural_key.split(".") - try: - content_type = ContentType.objects.get_by_natural_key(app, model) - except ContentType.DoesNotExist: - continue - - # add each permission - for permission in permissions: - add_permission(content_type, permission) - - -post_migrate.connect(check_all_permissions) -post_migrate.connect(check_all_group_permissions) diff --git a/smartmin/perms.py b/smartmin/perms.py deleted file mode 100644 index 29a5f57..0000000 --- a/smartmin/perms.py +++ /dev/null @@ -1,35 +0,0 @@ -from django.contrib.auth.models import Permission - - -def assign_perm(perm, group): - """ - Assigns a permission to a group - """ - if not isinstance(perm, Permission): - try: - app_label, codename = perm.split(".", 1) - except ValueError: - raise ValueError( - "For global permissions, first argument must be in" " format: 'app_label.codename' (is %r)" % perm - ) - perm = Permission.objects.get(content_type__app_label=app_label, codename=codename) - - group.permissions.add(perm) - return perm - - -def remove_perm(perm, group): - """ - Removes a permission from a group - """ - if not isinstance(perm, Permission): - try: - app_label, codename = perm.split(".", 1) - except ValueError: - raise ValueError( - "For global permissions, first argument must be in" " format: 'app_label.codename' (is %r)" % perm - ) - perm = Permission.objects.get(content_type__app_label=app_label, codename=codename) - - group.permissions.remove(perm) - return diff --git a/smartmin/users/models.py b/smartmin/users/models.py index d534e7d..7b492a3 100644 --- a/smartmin/users/models.py +++ b/smartmin/users/models.py @@ -4,8 +4,14 @@ from django.conf import settings from django.contrib.auth.hashers import check_password from django.db import models +from django.db.models.signals import post_migrate from django.utils import timezone +from .perms import sync_group_permissions, sync_user_permissions + +post_migrate.connect(sync_user_permissions) +post_migrate.connect(sync_group_permissions) + def is_password_complex(password): has_caps = re.search("[A-Z]+", password) diff --git a/smartmin/users/perms.py b/smartmin/users/perms.py new file mode 100644 index 0000000..a74bfeb --- /dev/null +++ b/smartmin/users/perms.py @@ -0,0 +1,189 @@ +import sys + +from django.apps import apps +from django.conf import settings +from django.contrib.auth.models import Group, Permission +from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ObjectDoesNotExist + +permissions_app_name = None + + +def assign_perm(perm, group): + """ + Assigns a permission to a group + """ + if not isinstance(perm, Permission): + try: + app_label, codename = perm.split(".", 1) + except ValueError: + raise ValueError( + "For global permissions, first argument must be in" " format: 'app_label.codename' (is %r)" % perm + ) + perm = Permission.objects.get(content_type__app_label=app_label, codename=codename) + + group.permissions.add(perm) + return perm + + +def remove_perm(perm, group): + """ + Removes a permission from a group + """ + if not isinstance(perm, Permission): + try: + app_label, codename = perm.split(".", 1) + except ValueError: + raise ValueError( + "For global permissions, first argument must be in" " format: 'app_label.codename' (is %r)" % perm + ) + perm = Permission.objects.get(content_type__app_label=app_label, codename=codename) + + group.permissions.remove(perm) + return + + +def get_permissions_app_name(): + """ + Gets the app after which smartmin permissions should be installed. This can be specified by PERMISSIONS_APP in the + Django settings or defaults to the last app with models + """ + global permissions_app_name + + if not permissions_app_name: + permissions_app_name = getattr(settings, "PERMISSIONS_APP", None) + + if not permissions_app_name: + app_names_with_models = [a.name for a in apps.get_app_configs() if a.models_module is not None] + if app_names_with_models: + permissions_app_name = app_names_with_models[-1] + + return permissions_app_name + + +def is_permissions_app(app_config): + """ + Returns whether this is the app after which permissions should be installed. + """ + return app_config.name == get_permissions_app_name() + + +def check_role_permissions(role, permissions, current_permissions): + """ + Checks the the passed in role (can be user, group or AnonymousUser) has all the passed + in permissions, granting them if necessary. + """ + role_permissions = [] + + # get all the current permissions, we'll remove these as we verify they should still be granted + for permission in permissions: + splits = permission.split(".") + if len(splits) != 2 and len(splits) != 3: + sys.stderr.write(" invalid permission %s, ignoring\n" % permission) + continue + + app = splits[0] + codenames = [] + + if len(splits) == 2: + codenames.append(splits[1]) + else: + (object, action) = splits[1:] + + # if this is a wildcard, then query our database for all the permissions that exist on this object + if action == "*": + for perm in Permission.objects.filter(codename__startswith="%s_" % object, content_type__app_label=app): + codenames.append(perm.codename) + # otherwise, this is an error, continue + else: + sys.stderr.write(" invalid permission %s, ignoring\n" % permission) + continue + + if len(codenames) == 0: + continue + + for codename in codenames: + # the full codename for this permission + full_codename = "%s.%s" % (app, codename) + + # this marks all the permissions which should remain + role_permissions.append(full_codename) + + try: + assign_perm(full_codename, role) + except ObjectDoesNotExist: + pass + # sys.stderr.write(" unknown permission %s, ignoring\n" % permission) + + # remove any that are extra + for permission in current_permissions: + if isinstance(permission, str): + key = permission + else: + key = "%s.%s" % (permission.content_type.app_label, permission.codename) + + if key not in role_permissions: + remove_perm(key, role) + + +def _ensure_permission_exists(content_type, permission): + """ + Adds the passed in permission to that content type. Note that the permission passed + in should be a single word, or verb. The proper 'codename' will be generated from that. + """ + + codename = "%s_%s" % (content_type.model, permission) # build our permission slug + + Permission.objects.get_or_create( + content_type=content_type, codename=codename, defaults={"name": f"Can {permission} {content_type.name}"} + ) + + +def sync_user_permissions(sender, **kwargs): + """ + Syncs user permissions in the database withe the PERMISSIONS setting. + """ + if not is_permissions_app(sender): + return + + config = getattr(settings, "PERMISSIONS", {}) + + # for each of our items + for natural_key, permissions in config.items(): + # if the natural key '*' then that means add to all objects + if natural_key == "*": + # for each of our content types + for content_type in ContentType.objects.all(): + for permission in permissions: + _ensure_permission_exists(content_type, permission) + + # otherwise, this is on a specific content type, add for each of those + else: + app, model = natural_key.split(".") + try: + content_type = ContentType.objects.get_by_natural_key(app, model) + except ContentType.DoesNotExist: + continue + + # add each permission + for permission in permissions: + _ensure_permission_exists(content_type, permission) + + +def sync_group_permissions(sender, **kwargs): + """ + Syncs group permissions in the database withe the PERMISSIONS setting. + """ + if not is_permissions_app(sender): + return + + config = getattr(settings, "GROUP_PERMISSIONS", {}) + + # for each of our items + for name, permissions in config.items(): + # get or create the group + (group, created) = Group.objects.get_or_create(name=name) + if created: + pass + + check_role_permissions(group, permissions, group.permissions.all()) diff --git a/test_runner/blog/tests.py b/test_runner/blog/tests.py index d36c8da..132e0e5 100644 --- a/test_runner/blog/tests.py +++ b/test_runner/blog/tests.py @@ -1,7 +1,6 @@ import json from datetime import datetime, timedelta, timezone as tzone from unittest.mock import patch - from zoneinfo import ZoneInfo from django.conf import settings @@ -10,17 +9,16 @@ from django.core.files.uploadedfile import SimpleUploadedFile from django.test import TestCase, override_settings from django.test.client import Client -from django.test.utils import override_settings from django.urls import reverse from django.utils import timezone import smartmin from smartmin.csv_imports.models import ImportTask -from smartmin.management import check_role_permissions from smartmin.models import SmartImportRowError from smartmin.templatetags.smartmin import get, get_value_from_view, user_as_string, view_as_json from smartmin.tests import SmartminTest from smartmin.users.models import FailedLogin, PasswordHistory, RecoveryToken, is_password_complex +from smartmin.users.perms import check_role_permissions from smartmin.views import smart_url from smartmin.widgets import DatePickerWidget, ImageThumbnailWidget from test_runner.blog.models import Category, Post