Sooner or later, a Django developer is faced with a problem: how to make it so that users cannot modify or delete, or even not see, different objects of the same type.
Let's say your project is about storing project information. Different users enter different projects and should not see information about another project. The same user can enter several projects and have different statuses in different projects - somewhere he can only view information, and in others - edit data. In some projects, the user is registered as project personnel, and in another, only as a consumer of his services. The access level, respectively, should be completely different.
Several packages are involved in these issues, we will consider one of them - Django-Access . Everyone who is interested is invited to cat.
Example
Consider the example briefly described in the preamble. We assume that we have a certain simple data management system for projects of a certain direction, for example, transport. In terms of access to various data, system users are divided into several categories:
- Administrator - can manipulate data on the attitude of personnel to the project, which manages, connect and disconnect users from the project, change the access status in the project
- Personnel - can manage the data of the entire project: create, delete, change - all data, except data on the attitude of personnel to the project
- Auditor - can view project data, but not change them
- Client - can only view records relating to interaction with him personally, as well as change personal data in his profile
How to interact with the Django rights system
, 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"
, , , .
With the help of the Django-Access package , we formulated almost all the necessary rules, and the remaining details of access restriction were implemented by small changes in the admin panel.
We formulated all access restriction rules in the form of queries to the DBMS.
You can see the full source code of the running application on the GitHub repository