Using Rules for attribute level access control?
vincentwhales opened this issue · comments
In my django application, I have a model, Client, which holds information about the clients of my business. I have two groups of employees that uses my application - Technician and Finance.
Members of Technician can view/update attributes of Client such as 'password_hash', 'login_ip_address'. Members of Finance can view/update attributes such as 'total_revenue' or 'bank_account_number'.
I see in the documentation that Predicates can be created from any callable that accepts anything from zero to two positional arguments
.
- Is it possible to extend this to three positional arguments? I am interested to do the following:
@rules.predicate
def can_view_attribute(user, model, attribute):
- If the above is not possible, I plan to generate a rule for the
Client
class for every of its attribute. Rule generation will happen at the initialization phase of my application.- Are there any drawbacks to this approach?
- Has anyone attempted this approach before?
@rules.predicate
def can_view_client_attribute(user, attribute):
I am in a similar situation. I have a case where an object
can be subscribed to multiple groups
and I would like to check if a user can subscribe
or unsubscribe
object
from the groups
. The logic is something like
def can_subscribe(user, object, group):
return is_owner_or_maintainer(user, object) and is_member(user, group) \
and not is_subscribed(object, group)
is_subscribed
is a relationship between object
and group
and is not a predicate
for now. The problem here is that is_owner_or_maintainer
does not know which group the object is intended to subscribe, and is_member
does not know which object
the user
is trying to subscribe
. So in the end can_subscribe
cannot be implemented as a predicate
and has to be a function that uses two predicates
.
Although django's has_perm
mechanism is restricted to has_perm(user, object)
, perhaps three-way predicator
could be added to the general rule sets?
Another possibility is to extend context
to allow user-specified context
so that the predicate would know which group
the predicate
is applied. Something like
def can_subscribe(user, object):
group = can_subscribe.context['group']
# or group = request.some_way_to_determine.group
return is_owner_or_maintainer(user, object) and is_member(user, group) \
and not is_subscribed(object, group)
Not quite sure if it would work but maybe the following could solve my problem:
def can_subscribe(user, objects):
return is_owner_or_maintainer(user, objects[0]) and is_member(user, objects[1]) \
and not is_subscribed(objects[0], objects[1])
and be used as
user.has_perm('can_subscribe', [object, group])
I have similar needs as @vincentwhales tho I can currently live without this feature for now.
Another related topic is read-access to the property of the Django model. Attributes of a Django model are almost always actual data fields inside the database.
Properties are not always the case. They are kinda like virtual fields. While they cannot be modified (at least I think so), they can be read. Therefore certain read permissions might occur depending on the user's confidentiality and authorization level.
@vincentwhales I'm just newly reading the docs for django-rules, so I may be missing something, but can you not just combine two simpler predicates to achieve what you want?
I don't know the structure of your models, but assuming you have a one-to-one of either Technician
or Finance
instance pointing to each User
instance, wouldn't the following achieve your goal?
@rules.predicate
def user_is_technician(user):
return hasattr(user, "technician")
@rules.predicate
def technician_has_attribute(user, attribute):
return hasattr(user.technician, attribute)
user_is_technician_and_has_attribute = user_is_technician & technician_has_attribute
rules.add_perm('myapp.has_technician_and_attribute', user_is_technician_and_has_attribute)
@rules.predicate
def user_is_finance(user):
return hasattr(user, "finance")
@rules.predicate
def finance_has_attribute(user, attribute):
return hasattr(user.finance, attribute)
user_is_finance_and_has_attribute = user_is_finance & finance_has_attribute
rules.add_perm('myapp.has_finance_and_attribute', user_is_finance_and_has_attribute)
Or a more general case that doesn't care is user
is Technician
or Finance
in the predicate function definition, because we can get that info from the user
instance. We check if user
has an associated technician
or finance
model instance, and then check that for the attribute
. Returning False if user
does not have an associated technician
or finance
instance, or if the related instance does not have attribute
. This does assume that the attributes do not have overlap between the two client types.
@rules.predicate
def has_attribute(user, attribute):
if hasattr(user, "technician"):
return hasattr(user.technician, attribute)
if hasattr(user, "finance"):
return hasattr(user.finance, attribute)
return False
rules.add_perm('myapp.has_attribute', has_attribute)
jack = User.objects.get(username="jack")
jack.has_attribute("login_ip_address")
There's probably a more optimal solution, and I've only looked at the docs for 20-some minutes, but I would think this would work based on what I've read so far.
(sorry to revive such an old thread. Just surprised nobody has commented with this suggestion before. Or maybe I'm totally misunderstanding something)
I came here to request the same feature. Django inplace edit (https://github.com/ptav/django-inlineedit) has a nice feature of passing the (user, object, field) to a callable that checks permission to edit. It would be nice if this could be passed off to Django-rules predicates. At the moment I can't handle field level edit permissions