Commit a8721948 authored by Simon Charette's avatar Simon Charette
Browse files

Fixed #26470 -- Converted auth permission validation to system checks.

Thanks Tim for the review.
parent fc34be89
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
from django.apps import AppConfig
from django.contrib.auth.checks import check_user_model
from django.core import checks
from django.db.models.signals import post_migrate
from django.utils.translation import ugettext_lazy as _

from .checks import check_models_permissions, check_user_model
from .management import create_permissions


@@ -15,3 +15,4 @@ class AuthConfig(AppConfig):
        post_migrate.connect(create_permissions,
            dispatch_uid="django.contrib.auth.management.create_permissions")
        checks.register(check_user_model, checks.Tags.models)
        checks.register(check_models_permissions, checks.Tags.models)
+73 −0
Original line number Diff line number Diff line
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from itertools import chain

from django.apps import apps
from django.conf import settings
from django.core import checks

from .management import _get_builtin_permissions


def check_user_model(app_configs=None, **kwargs):
    if app_configs is None:
@@ -70,3 +74,72 @@ def check_user_model(app_configs=None, **kwargs):
            )

    return errors


def check_models_permissions(app_configs=None, **kwargs):
    if app_configs is None:
        models = apps.get_models()
    else:
        models = chain.from_iterable(app_config.get_models() for app_config in app_configs)

    Permission = apps.get_model('auth', 'Permission')
    permission_name_max_length = Permission._meta.get_field('name').max_length
    errors = []

    for model in models:
        opts = model._meta
        builtin_permissions = dict(_get_builtin_permissions(opts))
        # Check builtin permission name length.
        max_builtin_permission_name_length = max(len(name) for name in builtin_permissions.values())
        if max_builtin_permission_name_length > permission_name_max_length:
            verbose_name_max_length = (
                permission_name_max_length - (max_builtin_permission_name_length - len(opts.verbose_name_raw))
            )
            errors.append(
                checks.Error(
                    "The verbose_name of model '%s.%s' must be at most %d characters "
                    "for its builtin permission names to be at most %d characters." % (
                        opts.app_label, opts.object_name, verbose_name_max_length, permission_name_max_length
                    ),
                    obj=model,
                    id='auth.E007',
                )
            )
        codenames = set()
        for codename, name in opts.permissions:
            # Check custom permission name length.
            if len(name) > permission_name_max_length:
                errors.append(
                    checks.Error(
                        "The permission named '%s' of model '%s.%s' is longer than %d characters." % (
                            name, opts.app_label, opts.object_name, permission_name_max_length
                        ),
                        obj=model,
                        id='auth.E008',
                    )
                )
            # Check custom permissions codename clashing.
            if codename in builtin_permissions:
                errors.append(
                    checks.Error(
                        "The permission codenamed '%s' clashes with a builtin permission "
                        "for model '%s.%s'." % (
                            codename, opts.app_label, opts.object_name
                        ),
                        obj=model,
                        id='auth.E005',
                    )
                )
            elif codename in codenames:
                errors.append(
                    checks.Error(
                        "The permission codenamed '%s' is duplicated for model '%s.%s'." % (
                            codename, opts.app_label, opts.object_name
                        ),
                        obj=model,
                        id='auth.E006',
                    )
                )
            codenames.add(codename)

    return errors
+2 −48
Original line number Diff line number Diff line
@@ -9,19 +9,17 @@ import unicodedata
from django.apps import apps
from django.contrib.auth import get_permission_codename
from django.core import exceptions
from django.core.management.base import CommandError
from django.db import DEFAULT_DB_ALIAS, router
from django.utils import six
from django.utils.encoding import DEFAULT_LOCALE_ENCODING


def _get_all_permissions(opts, ctype):
def _get_all_permissions(opts):
    """
    Returns (codename, name) for all permissions in the given opts.
    """
    builtin = _get_builtin_permissions(opts)
    custom = list(opts.permissions)
    _check_permission_clashing(custom, builtin, ctype)
    return builtin + custom


@@ -37,26 +35,6 @@ def _get_builtin_permissions(opts):
    return perms


def _check_permission_clashing(custom, builtin, ctype):
    """
    Check that permissions for a model do not clash. Raises CommandError if
    there are duplicate permissions.
    """
    pool = set()
    builtin_codenames = set(p[0] for p in builtin)
    for codename, _name in custom:
        if codename in pool:
            raise CommandError(
                "The permission codename '%s' is duplicated for model '%s.%s'." %
                (codename, ctype.app_label, ctype.model_class().__name__))
        elif codename in builtin_codenames:
            raise CommandError(
                "The permission codename '%s' clashes with a builtin permission "
                "for model '%s.%s'." %
                (codename, ctype.app_label, ctype.model_class().__name__))
        pool.add(codename)


def create_permissions(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, **kwargs):
    if not app_config.models_module:
        return
@@ -71,9 +49,6 @@ def create_permissions(app_config, verbosity=2, interactive=True, using=DEFAULT_

    from django.contrib.contenttypes.models import ContentType

    permission_name_max_length = Permission._meta.get_field('name').max_length
    verbose_name_max_length = permission_name_max_length - 11  # len('Can change ') prefix

    # This will hold the permissions we're looking for as
    # (content_type, (codename, name))
    searched_perms = list()
@@ -84,17 +59,8 @@ def create_permissions(app_config, verbosity=2, interactive=True, using=DEFAULT_
        # before creating foreign keys to them.
        ctype = ContentType.objects.db_manager(using).get_for_model(klass)

        if len(klass._meta.verbose_name) > verbose_name_max_length:
            raise exceptions.ValidationError(
                "The verbose_name of %s.%s is longer than %s characters" % (
                    ctype.app_label,
                    ctype.model,
                    verbose_name_max_length,
                )
            )

        ctypes.add(ctype)
        for perm in _get_all_permissions(klass._meta, ctype):
        for perm in _get_all_permissions(klass._meta):
            searched_perms.append((ctype, perm))

    # Find all the Permissions that have a content_type for a model we're
@@ -111,18 +77,6 @@ def create_permissions(app_config, verbosity=2, interactive=True, using=DEFAULT_
        for ct, (codename, name) in searched_perms
        if (ct.pk, codename) not in all_perms
    ]
    # Validate the permissions before bulk_creation to avoid cryptic database
    # error when the name is longer than 255 characters
    for perm in perms:
        if len(perm.name) > permission_name_max_length:
            raise exceptions.ValidationError(
                "The permission name %s of %s.%s is longer than %s characters" % (
                    perm.name,
                    perm.content_type.app_label,
                    perm.content_type.model,
                    permission_name_max_length,
                )
            )
    Permission.objects.using(using).bulk_create(perms)
    if verbosity >= 2:
        for perm in perms:
+10 −0
Original line number Diff line number Diff line
@@ -432,6 +432,16 @@ Auth
  ``USERNAME_FIELD``.
* **auth.W004**: ``<field>`` is named as the ``USERNAME_FIELD``, but it is not
  unique.
* **auth.E005**: The permission codenamed ``<codename>`` clashes with a builtin
  permission for model ``<model>``.
* **auth.E006**: The permission codenamed ``<codename>`` is duplicated for model
  ``<model>``.
* **auth.E007**: The :attr:`verbose_name
  <django.db.models.Options.verbose_name>` of model ``<model>`` must be at most
  244 characters for its builtin permission names
  to be at most 255 characters.
* **auth.E008**: The permission named ``<name>`` of model ``<model>`` is longer
  than 255 characters.


Content Types
+83 −1
Original line number Diff line number Diff line
from __future__ import unicode_literals

from django.contrib.auth.checks import check_user_model
from django.contrib.auth.checks import (
    check_models_permissions, check_user_model,
)
from django.contrib.auth.models import AbstractBaseUser
from django.core import checks
from django.db import models
@@ -80,3 +82,83 @@ class UserModelChecksTests(SimpleTestCase):
                    id='auth.W004',
                ),
            ])


@isolate_apps('auth_tests', attr_name='apps')
@override_system_checks([check_models_permissions])
class ModelsPermissionsChecksTests(SimpleTestCase):
    def test_clashing_default_permissions(self):
        class Checked(models.Model):
            class Meta:
                permissions = [
                    ('change_checked', 'Can edit permission (duplicate)')
                ]
        errors = checks.run_checks(self.apps.get_app_configs())
        self.assertEqual(errors, [
            checks.Error(
                "The permission codenamed 'change_checked' clashes with a builtin "
                "permission for model 'auth_tests.Checked'.",
                obj=Checked,
                id='auth.E005',
            ),
        ])

    def test_non_clashing_custom_permissions(self):
        class Checked(models.Model):
            class Meta:
                permissions = [
                    ('my_custom_permission', 'Some permission'),
                    ('other_one', 'Some other permission'),
                ]
        errors = checks.run_checks(self.apps.get_app_configs())
        self.assertEqual(errors, [])

    def test_clashing_custom_permissions(self):
        class Checked(models.Model):
            class Meta:
                permissions = [
                    ('my_custom_permission', 'Some permission'),
                    ('other_one', 'Some other permission'),
                    ('my_custom_permission', 'Some permission with duplicate permission code'),
                ]
        errors = checks.run_checks(self.apps.get_app_configs())
        self.assertEqual(errors, [
            checks.Error(
                "The permission codenamed 'my_custom_permission' is duplicated for "
                "model 'auth_tests.Checked'.",
                obj=Checked,
                id='auth.E006',
            ),
        ])

    def test_verbose_name_max_length(self):
        class Checked(models.Model):
            class Meta:
                verbose_name = 'some ridiculously long verbose name that is out of control' * 5
        errors = checks.run_checks(self.apps.get_app_configs())
        self.assertEqual(errors, [
            checks.Error(
                "The verbose_name of model 'auth_tests.Checked' must be at most 244 "
                "characters for its builtin permission names to be at most 255 characters.",
                obj=Checked,
                id='auth.E007',
            ),
        ])

    def test_custom_permission_name_max_length(self):
        custom_permission_name = 'some ridiculously long verbose name that is out of control' * 5

        class Checked(models.Model):
            class Meta:
                permissions = [
                    ('my_custom_permission', custom_permission_name),
                ]
        errors = checks.run_checks(self.apps.get_app_configs())
        self.assertEqual(errors, [
            checks.Error(
                "The permission named '%s' of model 'auth_tests.Checked' is longer "
                "than 255 characters." % custom_permission_name,
                obj=Checked,
                id='auth.E008',
            ),
        ])
Loading