Gestionar derechos a nivel de instalación

Tarde o temprano, un desarrollador de Django se enfrenta a un problema: cómo hacerlo para que los usuarios no puedan modificar o eliminar, o incluso no ver, diferentes objetos del mismo tipo.


Digamos que su proyecto se trata de almacenar información del proyecto. Diferentes usuarios ingresan a diferentes proyectos y no deben ver información sobre otro proyecto. El mismo usuario puede ingresar a varios proyectos y tener un estado diferente en diferentes proyectos, en algún lugar solo puede ver información y en otros, editar datos. En algunos proyectos, el usuario está registrado como personal del proyecto, y en otros, solo como consumidor de sus servicios. El nivel de acceso, respectivamente, debe ser completamente diferente.


Hay varios paquetes involucrados en estos problemas, consideraremos uno de ellos: Django-Access . Todos los interesados ​​están invitados al gato.


Ejemplo


Considere el ejemplo brevemente descrito en el preámbulo. Suponemos que tenemos un cierto sistema de gestión de datos simple para proyectos de una determinada dirección, por ejemplo, el transporte. En términos de acceso a diversos datos, los usuarios del sistema se dividen en varias categorías:


  • Administrador: puede manipular datos sobre la actitud del personal hacia el proyecto, que administra, conecta y desconecta a los usuarios del proyecto, cambia el estado de acceso en el proyecto
  • Personal: puede administrar los datos de todo el proyecto: crear, eliminar, cambiar, todos los datos, excepto los datos sobre la actitud del personal hacia el proyecto
  • Auditor: puede ver datos del proyecto, pero no cambiarlos
  • Cliente: solo puede ver los registros relacionados con la interacción con él personalmente, así como cambiar los datos personales en su perfil

Cómo interactuar con el sistema de derechos de Django


, Django, django.contrib.auth. , Permission Group , , .



Django django-admin startproject sample.


Django, sample sample options .


- trans python manage.py startapp trans .



Project.


class Project(models.Model):
    name = models.CharField(
        max_length=256,
        db_index=True,
        verbose_name=_('Name'),
        help_text=_('Name of the project')
    )
    def __str__(self):
        return self.name
    class Meta:
        verbose_name = _('Project')
        verbose_name_plural = _('Projects')

. ProjectMember, .


class ProjectMember(models.Model):
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.CASCADE,
        verbose_name=_('User'), help_text=_('User who belongs to this project'),
        related_name='projects',
    )
    project = models.ForeignKey(
        Project, on_delete=models.CASCADE,
        verbose_name=_('Project'), help_text=_('Project to which this user belongs'),
        related_name='users',
    )
    allow_change = models.BooleanField(
        default=True,
        verbose_name=_('Allow Change'),
        help_text=_('Is the member allowed to change the project data?')
    )
    allow_manage = models.BooleanField(
        default=False,
        verbose_name=_('Allow Manage'),
        help_text=_('Is the member allowed to manage users for the project?')
    )

    def __str__(self):
        return _('%s -> %s') % (self.user, self.project)

    class Meta:
        verbose_name = _('Project Member')
        verbose_name_plural = _('Project Members')
        unique_together = (
            ('user', 'project'),
        )

, , . , , . , , - , .


— , — :


  • Vehicle
  • Driver
  • Order

, , , -.


class Vehicle(models.Model):
    project = models.ForeignKey(
        Project, on_delete=models.CASCADE,
        verbose_name=_('Project'), help_text=_('Project to which this vehicle belongs'),
        related_name='vehicles',
    )
    code = models.CharField(
        max_length=256,
        db_index=True,
        verbose_name=_('Code'),
        help_text=_('Registration code')
    )
    def __str__(self):
        return self.code
    class Meta:
        verbose_name = _('Vehicle')
        verbose_name_plural = _('Vehicles')
        unique_together = (
            ('code', 'project'),
        )

class Driver(models.Model):
    project = models.ForeignKey(
        Project, on_delete=models.CASCADE,
        verbose_name=_('Project'), help_text=_('Project to which this driver belongs'),
        related_name='drivers',
    )
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.CASCADE,
        verbose_name=_('User'), help_text=_('User who is a driver'),
        related_name='driving',
    )
    def __str__(self):
        return _('%s [%s]') % (self.user, self.project)

    class Meta:
        verbose_name = _('Driver')
        verbose_name_plural = _('Drivers')
        unique_together = (
            ('user', 'project'),
        )

class Order(models.Model):
    project = models.ForeignKey(
        Project, on_delete=models.CASCADE,
        verbose_name=_('Project'), help_text=_('Project to which this order belongs'),
        related_name='orders',
    )
    client = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.CASCADE,
        verbose_name=_('Client'), help_text=_('Client getting an order'),
        related_name='orders',
    )
    vehicle = models.ForeignKey(
        Vehicle, on_delete=models.CASCADE,
        verbose_name=_('Vehicle'), help_text=_('Vehicle assigned to this order'),
        related_name='orders',
    )
    driver = models.ForeignKey(
        Driver, on_delete=models.CASCADE,
        verbose_name=_('Driver'), help_text=_('Driver assigned to this order'),
        related_name='orders',
    )
    start_time = models.DateTimeField(
        db_index=True,
        verbose_name=_("Start Time"),
        help_text=_("Time when the order is started")
    )
    stop_time = models.DateTimeField(
        db_index=True,
        verbose_name=_("Stop Time"),
        help_text=_("Time when the order is stopped")
    )
    def __str__(self):
        return _('%(start_time)s-%(stop_time)s %(client)s %(driver)s %(vehicle)s') % {
            'start_time': self.start_time,
            'stop_time': self.stop_time,
            'client': self.client,
            'driver': self.driver,
            'vehicle': self.vehicle,
        }
    class Meta:
        verbose_name = _('Order')
        verbose_name_plural = _('Orders')


access trans settings.py, python makemigrations trans python migrate.


, , admin.py trans. Django-Access.


?

. -, Django-Access , , , Django, , , .


-, , , Django-Access, , Django, .


@admin.register(Project)
class ProjectAdmin(AccessModelAdmin):
    fields = ['name']
    list_display = ['name']
    search_fields = ['name']

@admin.register(ProjectMember)
class ProjectMemberAdmin(AccessModelAdmin):
    fields = ['user', 'project', 'allow_manage', 'allow_change']
    list_display = ['user', 'project', 'allow_manage', 'allow_change']
    search_fields = ['user__username', 'user__first_name', 'user__last_name', 'project__name']
    list_filters = ['project', 'allow_manage', 'allow_change']
    autocomplete_fields = ['user', 'project']

    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == "project":
            projects = AccessManager(Project).changeable(request)
            kwargs["queryset"] = projects
        return super(ProjectMemberAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)

@admin.register(Vehicle)
class VehicleAdmin(AccessModelAdmin):
    fields = ['project', 'code']
    list_display = ['code', 'project']
    list_filters = ['project']
    search_fields = ['code']

    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == "project":
            projects = Project.objects.filter(users__user=request.user, users__allow_change=True)
            kwargs["queryset"] = projects
        return super(VehicleAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)

@admin.register(Driver)
class DriverAdmin(AccessModelAdmin):
    fields = ['project', 'user']
    list_display = ['user', 'project']
    list_filters = ['project']
    search_fields = ['user__username', 'user__first_name', 'user__last_name', 'project__name']
    autocomplete_fields = ['user', 'project']

    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == "project":
            projects = Project.objects.filter(users__user=request.user, users__allow_change=True)
            kwargs["queryset"] = projects
        return super(DriverAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)

@admin.register(Order)
class OrderAdmin(AccessModelAdmin):
    fields = ['project', 'client', 'vehicle', 'driver', 'start_time', 'stop_time']
    list_display = ['start_time', 'stop_time', 'client', 'driver', 'vehicle', 'project']
    list_filters = ['project', 'vehicle', 'driver']
    search_fields = ['client__username', 'client__first_name', 'client__last_name', 'project__name',]
    autocomplete_fields = ['client', 'vehicle', 'driver', 'project']
    date_hierarchy = 'start_time'

    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == "project":
            projects = Project.objects.filter(users__user=request.user, users__allow_change=True)
            kwargs["queryset"] = projects
        return super(OrderAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)

, .


, .


, , autocomplete:


admin.site.unregister(User)
admin.site.unregister(Group)

@admin.register(User)
class AccessUserAdmin(AccessControlMixin, UserAdmin):
    def get_readonly_fields(self, request, obj=None):
        readonly_fields = super(AccessUserAdmin, self).get_readonly_fields(request, obj) or []
        if request.user.is_superuser:
            return readonly_fields
        if not obj:
            return readonly_fields
        restrict = ['is_superuser', 'last_login', 'date_joined']
        if obj.pk != request.user.pk:
            restrict = ['is_superuser', 'last_login', 'date_joined', 'password', 'email']
        return [f for f in readonly_fields if f not in restrict] + restrict

    def get_list_display(self, request):
        fields = super(AccessUserAdmin, self).get_list_display(request) or []
        if request.user.is_superuser:
            return fields
        restrict = ['password', 'email']
        return [f for f in fields if not f in restrict]

    def _fieldsets_exclude(self, fieldsets, exclude):
        ret = []
        for nm, params in fieldsets:
            if 'fields' not in params:
                ret.append((nm, params))
                continue
            fields = []
            for f in params['fields']:
                if f not in exclude:
                    fields.append(f)
            pars = {}
            pars.update(params)
            pars['fields'] = fields
            ret.append((nm, pars))
        return ret

    def _fieldsets_only(self, fieldsets, only):
        ret = []
        for nm, params in fieldsets:
            if 'fields' not in params:
                ret.append((nm, params))
                continue
            fields = []
            for f in params['fields']:
                if f in only:
                    fields.append(f)
            pars = {}
            pars.update(params)
            pars['fields'] = fields
            ret.append((nm, pars))
        return ret

    def get_fieldsets(self, request, obj=None):
        fieldsets = list(super(AccessUserAdmin, self).get_fieldsets(request, obj)) or []
        fields = self.get_fields(request, obj=obj)
        return self._fieldsets_only(fieldsets, fields)

    def get_fields(self, request, obj=None):
        fields = list(super(AccessUserAdmin, self).get_fields(request, obj)) or []
        exclude = ['is_staff', 'groups', 'user_permissions']
        if not request.user.is_superuser:
            exclude = ['is_staff', 'is_superuser', 'groups', 'user_permissions']
            if obj and obj.pk != request.user.pk:
                exclude = ['is_staff', 'password', 'email', 'groups', 'user_permissions']
        return [f for f in fields if not f in exclude]

    def save_model(self, request, obj, form, change):
        obj.is_staff = True
        return super(AccessUserAdmin, self).save_model(request, obj, form, change)

, , , ( is_staff is_superuser), , , .



, authorize. , , .


?

, . authorize INSTALLED_APPS settings.py, .


INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'access',
    'trans',

    # SHOULD BE ALWAYS LAST!
    'authorize',
]

python manage.py startapp authorize models.py. , .


?

, models.py, , , , INSTALLED_APPS. , , .


, , ( ) , . , , , .


, , , . , , , , . , , .


...

, . , , , — . , . .


AccessManager.register_plugins({
    User: CompoundPlugin(
        CheckAblePlugin(
            appendable=(lambda model, request: 
                {} if request.user.id and request.user.projects.filter(allow_manage=True) else
                False
            ),
        ),
        ApplyAblePlugin(
            visible=(
                lambda queryset, request:
                    queryset if request.user.projects.filter(
                        allow_manage=True
                    ) else queryset.filter(
                        projects__project__users__user=request.user.id
                    ).distinct() or queryset.filter(
                        id=request.user.id
                    ).distinct()
            ),
            changeable=(lambda queryset, request: queryset.filter(id=request.user.id).distinct()),
            deleteable=(lambda queryset, request: queryset.filter(id=request.user.id).distinct())
        )
    ),
})

.


-, , , , , . , , , , .


-, , , . , , .


AccessManager.register_plugins({
    Project: CompoundPlugin(
        CheckAblePlugin(
            appendable=(lambda model, request: False),
        ),
        ApplyAblePlugin(
            visible=(lambda queryset, request: queryset.filter(
                users__user=request.user.id
            ).distinct()),
            changeable=(lambda queryset, request: queryset.filter(
                users__user=request.user.id,
                users__allow_manage=True
            ).distinct()),
            deleteable=(lambda queryset, request: queryset.none()),
        )
    ),
    ProjectMember: CompoundPlugin(
        CheckAblePlugin(
            appendable=(lambda model, request: {} if request.user.id and request.user.projects.filter(allow_manage=True) else False),
        ),
        ApplyAblePlugin(
            visible=(lambda queryset, request: queryset.filter(
                project__users__user=request.user.id
            ).distinct()),
            changeable=(lambda queryset, request: queryset.filter(
                project__users__user=request.user.id,
                project__users__allow_manage=True
            ).distinct()),
            deleteable=(lambda queryset, request: queryset.filter(
                project__users__user=request.user.id,
                project__users__allow_manage=True
            ).distinct()),
        )
    ),
})

, , . , , allow_change.


, — , , () .


, , :


  • Driver — ,
  • Order — , , , , .

.


AccessManager.register_plugins({
    Vehicle: CompoundPlugin(
        ApplyAblePlugin(
            visible=(lambda queryset, request: queryset.filter(
                project__users__user=request.user.id
            ).distinct()),
            changeable=(lambda queryset, request: queryset.filter(
                project__users__user=request.user.id,
                project__users__allow_change=True
            ).distinct()),
            deleteable=(lambda queryset, request: queryset.filter(
                project__users__user=request.user.id,
                project__users__allow_change=True
            ).distinct()),
        )
    ),
    Driver: CompoundPlugin(
        ApplyAblePlugin(
            visible=(lambda queryset, request: queryset.filter(
                Q(project__users__user=request.user.id) | Q(user=request.user)
            ).distinct()),
            changeable=(lambda queryset, request: queryset.filter(
                project__users__user=request.user.id,
                project__users__allow_change=True
            ).distinct()),
            deleteable=(lambda queryset, request: queryset.filter(
                project__users__user=request.user.id,
                project__users__allow_change=True
            ).distinct()),
        )
    ),
    Order: CompoundPlugin(
        ApplyAblePlugin(
            visible=(lambda queryset, request: queryset.filter(
                Q(
                    project__users__user=request.user.id
                ) | Q(
                    client=request.user
                ) | Q(
                    driver__user=request.user
                )
            ).distinct()),
            changeable=(lambda queryset, request: queryset.filter(
                project__users__user=request.user.id,
                project__users__allow_change=True
            ).distinct()),
            deleteable=(lambda queryset, request: queryset.filter(
                project__users__user=request.user.id,
                project__users__allow_change=True
            ).distinct()),
        )
    ),
})

?

, .


, Django-Access , , .


- , . , . project, , .


ProjectMember:


    ...
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == "project":
            projects = AccessManager(Project).changeable(request)
            kwargs["queryset"] = projects
        return super(ProjectMemberAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)

:


    ...
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == "project":
            projects = Project.objects.filter(users__user=request.user, users__allow_change=True)
            kwargs["queryset"] = projects
        return super(..., self).formfield_for_foreignkey(db_field, request, **kwargs)

( … ).


. , Django, Django-Access , , Django. access.plugins.DjangoAccessPlugin, , .


settings.py. , , , , .


class SuperOnly(AccessPluginBase):
    def check_visible(self, model, request):
        return request.user.is_superuser and {}
    def check_changeable(self, model, request):
        return request.user.is_superuser and {}
    def check_appendable(self, model, request):
        return request.user.is_superuser and {}
    def check_deleteable(self, model, request):
        return request.user.is_superuser and {}

settings.py


ACCESS_DEFAULT_PLUGIN = "authorize.models.SuperOnly"


, , , .


Usando el paquete Django-Access , formulamos casi todas las reglas necesarias, y los detalles restantes de la restricción de acceso fueron implementados por pequeños cambios en el panel de administración.


Formulamos todas las reglas de restricción de acceso en forma de consultas al DBMS.


Puede ver el código fuente completo de la aplicación en ejecución en el repositorio de GitHub


All Articles