2020-02-09 16:35:15 +00:00
|
|
|
|
import functools
|
2019-09-18 12:26:42 +00:00
|
|
|
|
import json
|
2020-02-09 16:35:15 +00:00
|
|
|
|
import operator
|
2019-09-18 12:26:42 +00:00
|
|
|
|
|
|
|
|
|
from django.contrib.contenttypes.models import ContentType
|
|
|
|
|
from django.core.exceptions import ValidationError
|
|
|
|
|
from django.db import models
|
2020-02-13 14:59:19 +00:00
|
|
|
|
from django.db.models import F, Q
|
2019-09-18 12:26:42 +00:00
|
|
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InstancedPermission:
|
|
|
|
|
|
2020-02-09 16:35:15 +00:00
|
|
|
|
def __init__(self, model, query, type, field):
|
2019-09-18 12:26:42 +00:00
|
|
|
|
self.model = model
|
2020-02-09 16:35:15 +00:00
|
|
|
|
self.query = query
|
2019-09-18 12:26:42 +00:00
|
|
|
|
self.type = type
|
|
|
|
|
self.field = field
|
|
|
|
|
|
|
|
|
|
def applies(self, obj, permission_type, field_name=None):
|
2020-02-09 16:35:15 +00:00
|
|
|
|
"""
|
|
|
|
|
Returns True if the permission applies to
|
|
|
|
|
the field `field_name` object `obj`
|
|
|
|
|
"""
|
2020-03-07 08:30:22 +00:00
|
|
|
|
if self.type == 'add':
|
|
|
|
|
if permission_type == self.type:
|
|
|
|
|
return self.query(obj)
|
2019-09-18 12:26:42 +00:00
|
|
|
|
if ContentType.objects.get_for_model(obj) != self.model:
|
2020-02-09 16:35:15 +00:00
|
|
|
|
# The permission does not apply to the model
|
2019-09-18 12:26:42 +00:00
|
|
|
|
return False
|
|
|
|
|
if self.permission is None:
|
|
|
|
|
if permission_type == self.type:
|
|
|
|
|
if field_name is not None:
|
|
|
|
|
return field_name == self.field
|
|
|
|
|
else:
|
|
|
|
|
return True
|
|
|
|
|
else:
|
|
|
|
|
return False
|
2020-02-09 16:35:15 +00:00
|
|
|
|
elif obj in self.model.objects.get(self.query):
|
|
|
|
|
return True
|
|
|
|
|
else:
|
|
|
|
|
return False
|
2019-09-18 12:26:42 +00:00
|
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
|
if self.field:
|
|
|
|
|
return _("Can {type} {model}.{field} in {permission}").format(type=self.type, model=self.model, field=self.field, permission=self.permission)
|
|
|
|
|
else:
|
|
|
|
|
return _("Can {type} {model} in {permission}").format(type=self.type, model=self.model, permission=self.permission)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Permission(models.Model):
|
|
|
|
|
|
|
|
|
|
PERMISSION_TYPES = [
|
2019-09-18 14:39:37 +00:00
|
|
|
|
('add', 'add'),
|
|
|
|
|
('view', 'view'),
|
|
|
|
|
('change', 'change'),
|
|
|
|
|
('delete', 'delete')
|
2019-09-18 12:26:42 +00:00
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
model = models.ForeignKey(ContentType, on_delete=models.CASCADE, related_name='+')
|
|
|
|
|
|
2020-02-09 16:35:15 +00:00
|
|
|
|
# A json encoded Q object with the following grammar
|
2020-02-09 17:14:36 +00:00
|
|
|
|
# query -> [] | {} (the empty query representing all objects)
|
2020-02-13 14:59:19 +00:00
|
|
|
|
# query -> ['AND', query, …] AND multiple queries
|
|
|
|
|
# | ['OR', query, …] OR multiple queries
|
|
|
|
|
# | ['NOT', query] Opposite of query
|
|
|
|
|
# query -> {key: value, …} A list of fields and values of a Q object
|
|
|
|
|
# key -> string A field name
|
|
|
|
|
# value -> int | string | bool | null Literal values
|
|
|
|
|
# | [parameter] A parameter
|
|
|
|
|
# | {'F': oper} An F object
|
|
|
|
|
# oper -> [string] A parameter
|
|
|
|
|
# | ['ADD', oper, …] Sum multiple F objects or literal
|
|
|
|
|
# | ['SUB', oper, oper] Substract two F objects or literal
|
|
|
|
|
# | ['MUL', oper, …] Multiply F objects or literals
|
|
|
|
|
# | int | string | bool | null Literal values
|
|
|
|
|
# | ['F', string] A field
|
2020-02-09 16:35:15 +00:00
|
|
|
|
#
|
|
|
|
|
# Examples:
|
|
|
|
|
# Q(is_admin=True) := {'is_admin': ['TYPE', 'bool', 'True']}
|
|
|
|
|
# ~Q(is_admin=True) := ['NOT', {'is_admin': ['TYPE', 'bool', 'True']}]
|
2020-02-09 17:14:36 +00:00
|
|
|
|
query = models.TextField()
|
2019-09-18 12:26:42 +00:00
|
|
|
|
|
2020-02-09 17:14:36 +00:00
|
|
|
|
type = models.CharField(max_length=15, choices=PERMISSION_TYPES)
|
2019-09-18 12:26:42 +00:00
|
|
|
|
|
2020-02-09 17:14:36 +00:00
|
|
|
|
field = models.CharField(max_length=255, blank=True)
|
|
|
|
|
|
|
|
|
|
description = models.CharField(max_length=255, blank=True)
|
2019-09-18 12:26:42 +00:00
|
|
|
|
|
|
|
|
|
class Meta:
|
2020-02-09 17:14:36 +00:00
|
|
|
|
unique_together = ('model', 'query', 'type', 'field')
|
2019-09-18 12:26:42 +00:00
|
|
|
|
|
|
|
|
|
def clean(self):
|
2019-09-18 14:39:37 +00:00
|
|
|
|
if self.field and self.type not in {'view', 'change'}:
|
2019-09-18 12:26:42 +00:00
|
|
|
|
raise ValidationError(_("Specifying field applies only to view and change permission types."))
|
|
|
|
|
|
|
|
|
|
def save(self):
|
|
|
|
|
self.full_clean()
|
|
|
|
|
super().save()
|
|
|
|
|
|
2020-02-13 14:59:19 +00:00
|
|
|
|
@staticmethod
|
|
|
|
|
def compute_f(_oper, **kwargs):
|
|
|
|
|
oper = _oper
|
|
|
|
|
if isinstance(oper, list):
|
|
|
|
|
if len(oper) == 1:
|
|
|
|
|
return kwargs[oper[0]].pk
|
|
|
|
|
elif len(oper) >= 2:
|
|
|
|
|
if oper[0] == 'ADD':
|
|
|
|
|
return functools.reduce(operator.add, [compute_f(oper, **kwargs) for oper in oper[1:]])
|
|
|
|
|
elif oper[0] == 'SUB':
|
|
|
|
|
return compute_f(oper[1], **kwargs) - compute_f(oper[2], **kwargs)
|
|
|
|
|
elif oper[0] == 'MUL':
|
|
|
|
|
return functools.reduce(operator.mul, [compute_f(oper, **kwargs) for oper in oper[1:]])
|
|
|
|
|
elif oper[0] == 'F':
|
|
|
|
|
return F(oper[1])
|
|
|
|
|
else:
|
|
|
|
|
return oper
|
|
|
|
|
# TODO: find a better way to crash here
|
|
|
|
|
raise Exception("F is wrong")
|
|
|
|
|
|
2020-02-09 17:14:36 +00:00
|
|
|
|
def _about(_self, _query, **kwargs):
|
2020-02-09 16:35:15 +00:00
|
|
|
|
self = _self
|
2020-02-09 17:14:36 +00:00
|
|
|
|
query = _query
|
2020-03-07 09:48:38 +00:00
|
|
|
|
if self.type == 'add':
|
2020-03-07 08:30:22 +00:00
|
|
|
|
# Handle add permission differently
|
|
|
|
|
return self._about_add(query, **kwargs)
|
2020-02-09 17:14:36 +00:00
|
|
|
|
if len(query) == 0:
|
|
|
|
|
# The query is either [] or {} and
|
2020-02-09 16:35:15 +00:00
|
|
|
|
# applies to all objects of the model
|
|
|
|
|
# to represent this we return None
|
2019-09-18 12:26:42 +00:00
|
|
|
|
return None
|
2020-02-09 17:14:36 +00:00
|
|
|
|
if isinstance(query, list):
|
|
|
|
|
if query[0] == 'AND':
|
|
|
|
|
return functools.reduce(operator.and_, [self._about(query, **kwargs) for query in query[1:]])
|
|
|
|
|
elif query[0] == 'OR':
|
|
|
|
|
return functools.reduce(operator.or_, [self._about(query, **kwargs) for query in query[1:]])
|
|
|
|
|
elif query[0] == 'NOT':
|
|
|
|
|
return ~self._about(query[1], **kwargs)
|
|
|
|
|
elif isinstance(query, dict):
|
2020-02-09 16:35:15 +00:00
|
|
|
|
q_kwargs = {}
|
2020-02-09 17:14:36 +00:00
|
|
|
|
for key in query:
|
|
|
|
|
value = query[key]
|
2020-02-09 16:35:15 +00:00
|
|
|
|
if isinstance(value, list):
|
|
|
|
|
# It is a parameter we query its primary key
|
|
|
|
|
q_kwargs[key] = kwargs[value[0]].pk
|
2020-02-13 14:59:19 +00:00
|
|
|
|
elif isinstance(value, dict):
|
|
|
|
|
# It is an F object
|
|
|
|
|
q_kwargs[key] = compute_f(query['F'], **kwargs)
|
2020-02-09 16:35:15 +00:00
|
|
|
|
else:
|
|
|
|
|
q_kwargs[key] = value
|
|
|
|
|
return Q(**q_kwargs)
|
2019-09-18 12:26:42 +00:00
|
|
|
|
else:
|
2020-02-09 16:35:15 +00:00
|
|
|
|
# TODO: find a better way to crash here
|
2020-02-09 17:14:36 +00:00
|
|
|
|
raise Exception("query {} is wrong".format(self.query))
|
2019-09-18 12:26:42 +00:00
|
|
|
|
|
2020-03-07 08:30:22 +00:00
|
|
|
|
def _about_add(_self, _query, **kwargs):
|
|
|
|
|
self = _self
|
|
|
|
|
query = _query
|
|
|
|
|
if len(query) == 0:
|
|
|
|
|
return lambda _: True
|
|
|
|
|
if isinstance(query, list):
|
|
|
|
|
if query[0] == 'AND':
|
|
|
|
|
return lambda obj: functools.reduce(operator.and_, [self._about_add(query, **kwargs)(obj) for query in query[1:]])
|
|
|
|
|
elif query[0] == 'OR':
|
|
|
|
|
return lambda obj: functools.reduce(operator.or_, [self._about_add(query, **kwargs)(obj) for query in query[1:]])
|
|
|
|
|
elif query[0] == 'NOT':
|
|
|
|
|
return lambda obj: not self._about_add(query[1], **kwargs)(obj)
|
|
|
|
|
elif isinstance(query, dict):
|
|
|
|
|
q_kwargs = {}
|
|
|
|
|
for key in query:
|
|
|
|
|
value = query[key]
|
|
|
|
|
if isinstance(value, list):
|
|
|
|
|
# It is a parameter we query its primary key
|
|
|
|
|
q_kwargs[key] = kwargs[value[0]].pk
|
|
|
|
|
elif isinstance(value, dict):
|
|
|
|
|
# It is an F object
|
|
|
|
|
q_kwargs[key] = compute_f(query['F'], **kwargs)
|
|
|
|
|
else:
|
|
|
|
|
q_kwargs[key] = value
|
|
|
|
|
def func(obj):
|
|
|
|
|
nonlocal q_kwargs
|
|
|
|
|
for arg in q_kwargs:
|
|
|
|
|
if getattr(obj, arg) != q_kwargs(arg):
|
|
|
|
|
return False
|
|
|
|
|
return True
|
|
|
|
|
return func
|
|
|
|
|
|
2019-09-18 12:26:42 +00:00
|
|
|
|
def about(self, **kwargs):
|
2020-02-09 17:14:36 +00:00
|
|
|
|
"""
|
|
|
|
|
Return an InstancedPermission with the parameters
|
|
|
|
|
replaced by their values and the query interpreted
|
|
|
|
|
"""
|
|
|
|
|
query = json.loads(self.query)
|
|
|
|
|
query = self._about(query, **kwargs)
|
2020-02-09 16:35:15 +00:00
|
|
|
|
return InstancedPermission(self.model, query, self.type, self.field)
|
2019-09-18 12:26:42 +00:00
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
if self.field:
|
2020-02-09 17:14:36 +00:00
|
|
|
|
return _("Can {type} {model}.{field} in {query}").format(type=self.type, model=self.model, field=self.field, query=self.query)
|
2019-09-18 12:26:42 +00:00
|
|
|
|
else:
|
2020-02-09 17:14:36 +00:00
|
|
|
|
return _("Can {type} {model} in {query}").format(type=self.type, model=self.model, query=self.query)
|
2019-09-18 12:26:42 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UserPermission(models.Model):
|
|
|
|
|
|
|
|
|
|
user = models.ForeignKey('auth.User', on_delete=models.CASCADE)
|
|
|
|
|
|
|
|
|
|
permission = models.ForeignKey(Permission, on_delete=models.CASCADE)
|
|
|
|
|
|