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'),
)
, , . , , . , , - , .
— , — :
, , , -.
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',
'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