rsinger86 / drf-access-policy Goto Github PK
View Code? Open in Web Editor NEWDeclarative access policies/permissions modeled after AWS' IAM policies.
Home Page: https://rsinger86.github.io/drf-access-policy/
License: MIT License
Declarative access policies/permissions modeled after AWS' IAM policies.
Home Page: https://rsinger86.github.io/drf-access-policy/
License: MIT License
I had this error in production a couple of times.
The statement that caused it is the following:
{
"action": ["retrieve", "update", "partial_update"],
"principal": ["authenticated"],
"effect": "deny",
"condition_expression": "not has_teacher_privileges:assess_participations and not is_slot_in_scope",
}
def has_teacher_privileges(self, request, view, action, privilege):
from courses.models import Course
from courses.views import CourseViewSet
course_pk = (
view.kwargs.get("pk")
if isinstance(view, CourseViewSet)
else view.kwargs.get("course_pk") # nested view
)
try:
course = Course.objects.get(pk=course_pk)
except ValueError:
return False
return check_privilege(request.user, course, privilege)
def is_slot_in_scope(self, request, view, action):
slot = view.get_object()
return slot.is_in_scope()
This condition expression works most times, but sometimes it errors like I described. I haven't yet been able to identify what causes this, but I don't believe it to be something that has to do with my code. It looks like a bug in drf-access-policy.
Full stack trace:
Traceback (most recent call last):
File "/app/.heroku/python/lib/python3.8/site-packages/asgiref/sync.py", line 451, in thread_handler
raise exc_info[1]
File "/app/.heroku/python/lib/python3.8/site-packages/django/core/handlers/exception.py", line 42, in inner
response = await get_response(request)
File "/app/.heroku/python/lib/python3.8/site-packages/django/core/handlers/base.py", line 253, in _get_response_async
response = await wrapped_callback(
File "/app/.heroku/python/lib/python3.8/site-packages/asgiref/sync.py", line 414, in __call__
ret = await asyncio.wait_for(future, timeout=None)
File "/app/.heroku/python/lib/python3.8/asyncio/tasks.py", line 455, in wait_for
return await fut
File "/app/.heroku/python/lib/python3.8/site-packages/asgiref/current_thread_executor.py", line 22, in run
result = self.fn(*self.args, **self.kwargs)
File "/app/.heroku/python/lib/python3.8/site-packages/asgiref/sync.py", line 455, in thread_handler
return func(*args, **kwargs)
File "/app/.heroku/python/lib/python3.8/site-packages/sentry_sdk/integrations/django/views.py", line 67, in sentry_wrapped_callback
return callback(request, *args, **kwargs)
File "/app/.heroku/python/lib/python3.8/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
return view_func(*args, **kwargs)
File "/app/.heroku/python/lib/python3.8/site-packages/rest_framework/viewsets.py", line 125, in view
return self.dispatch(request, *args, **kwargs)
File "/app/.heroku/python/lib/python3.8/site-packages/rest_framework/views.py", line 509, in dispatch
response = self.handle_exception(exc)
File "/app/.heroku/python/lib/python3.8/site-packages/rest_framework/views.py", line 469, in handle_exception
self.raise_uncaught_exception(exc)
File "/app/.heroku/python/lib/python3.8/site-packages/rest_framework/views.py", line 480, in raise_uncaught_exception
raise exc
File "/app/.heroku/python/lib/python3.8/site-packages/rest_framework/views.py", line 497, in dispatch
self.initial(request, *args, **kwargs)
File "/app/.heroku/python/lib/python3.8/site-packages/sentry_sdk/integrations/django/__init__.py", line 271, in sentry_patched_drf_initial
return old_drf_initial(self, request, *args, **kwargs)
File "/app/.heroku/python/lib/python3.8/site-packages/rest_framework/views.py", line 415, in initial
self.check_permissions(request)
File "/app/.heroku/python/lib/python3.8/site-packages/rest_framework/views.py", line 332, in check_permissions
if not permission.has_permission(request, self):
File "/app/.heroku/python/lib/python3.8/site-packages/rest_access_policy/access_policy.py", line 52, in has_permission
allowed = self._evaluate_statements(statements, request, view, action)
File "/app/.heroku/python/lib/python3.8/site-packages/rest_access_policy/access_policy.py", line 94, in _evaluate_statements
matched = self._get_statements_matching_conditions(
File "/app/.heroku/python/lib/python3.8/site-packages/rest_access_policy/access_policy.py", line 214, in _get_statements_matching_conditions
passed = bool(boolExpr.parseString(condition)[0])
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 1124, in parse_string
loc, tokens = self._parse(instring, 0)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 5203, in parseImpl
return super().parseImpl(instring, loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 4352, in parseImpl
return self.expr._parse(instring, loc, doActions, callPreParse=False)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 5203, in parseImpl
return super().parseImpl(instring, loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 4352, in parseImpl
return self.expr._parse(instring, loc, doActions, callPreParse=False)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 4091, in parseImpl
return e._parse(
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 5203, in parseImpl
return super().parseImpl(instring, loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 4352, in parseImpl
return self.expr._parse(instring, loc, doActions, callPreParse=False)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 4091, in parseImpl
return e._parse(
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 3863, in parseImpl
loc, exprtokens = e._parse(instring, loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 4352, in parseImpl
return self.expr._parse(instring, loc, doActions, callPreParse=False)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 3841, in parseImpl
loc, resultlist = self.exprs[0]._parse(
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 5203, in parseImpl
return super().parseImpl(instring, loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 4352, in parseImpl
return self.expr._parse(instring, loc, doActions, callPreParse=False)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 4091, in parseImpl
return e._parse(
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 3863, in parseImpl
loc, exprtokens = e._parse(instring, loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 4352, in parseImpl
return self.expr._parse(instring, loc, doActions, callPreParse=False)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 3863, in parseImpl
loc, exprtokens = e._parse(instring, loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 5203, in parseImpl
return super().parseImpl(instring, loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 4352, in parseImpl
return self.expr._parse(instring, loc, doActions, callPreParse=False)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 4091, in parseImpl
return e._parse(
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 849, in _parseNoCache
tokens = fn(instring, tokens_start, ret_tokens)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 285, in wrapper
ret = func(*args[limit:])
TypeError: <lambda>() missing 1 required positional argument: 'token'
Internal Server Error: /courses/7/events/83P423W/participations/415/slots/3984/
Traceback (most recent call last):
File "/app/.heroku/python/lib/python3.8/site-packages/asgiref/sync.py", line 451, in thread_handler
raise exc_info[1]
File "/app/.heroku/python/lib/python3.8/site-packages/django/core/handlers/exception.py", line 42, in inner
response = await get_response(request)
File "/app/.heroku/python/lib/python3.8/site-packages/django/core/handlers/base.py", line 253, in _get_response_async
response = await wrapped_callback(
File "/app/.heroku/python/lib/python3.8/site-packages/asgiref/sync.py", line 414, in __call__
ret = await asyncio.wait_for(future, timeout=None)
File "/app/.heroku/python/lib/python3.8/asyncio/tasks.py", line 455, in wait_for
return await fut
File "/app/.heroku/python/lib/python3.8/site-packages/asgiref/current_thread_executor.py", line 22, in run
result = self.fn(*self.args, **self.kwargs)
File "/app/.heroku/python/lib/python3.8/site-packages/asgiref/sync.py", line 455, in thread_handler
return func(*args, **kwargs)
File "/app/.heroku/python/lib/python3.8/site-packages/sentry_sdk/integrations/django/views.py", line 67, in sentry_wrapped_callback
return callback(request, *args, **kwargs)
File "/app/.heroku/python/lib/python3.8/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
return view_func(*args, **kwargs)
File "/app/.heroku/python/lib/python3.8/site-packages/rest_framework/viewsets.py", line 125, in view
return self.dispatch(request, *args, **kwargs)
File "/app/.heroku/python/lib/python3.8/site-packages/rest_framework/views.py", line 509, in dispatch
response = self.handle_exception(exc)
File "/app/.heroku/python/lib/python3.8/site-packages/rest_framework/views.py", line 469, in handle_exception
self.raise_uncaught_exception(exc)
File "/app/.heroku/python/lib/python3.8/site-packages/rest_framework/views.py", line 480, in raise_uncaught_exception
raise exc
File "/app/.heroku/python/lib/python3.8/site-packages/rest_framework/views.py", line 497, in dispatch
self.initial(request, *args, **kwargs)
File "/app/.heroku/python/lib/python3.8/site-packages/sentry_sdk/integrations/django/__init__.py", line 271, in sentry_patched_drf_initial
return old_drf_initial(self, request, *args, **kwargs)
File "/app/.heroku/python/lib/python3.8/site-packages/rest_framework/views.py", line 415, in initial
self.check_permissions(request)
File "/app/.heroku/python/lib/python3.8/site-packages/rest_framework/views.py", line 332, in check_permissions
if not permission.has_permission(request, self):
File "/app/.heroku/python/lib/python3.8/site-packages/rest_access_policy/access_policy.py", line 52, in has_permission
allowed = self._evaluate_statements(statements, request, view, action)
File "/app/.heroku/python/lib/python3.8/site-packages/rest_access_policy/access_policy.py", line 94, in _evaluate_statements
matched = self._get_statements_matching_conditions(
File "/app/.heroku/python/lib/python3.8/site-packages/rest_access_policy/access_policy.py", line 214, in _get_statements_matching_conditions
passed = bool(boolExpr.parseString(condition)[0])
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 1124, in parse_string
loc, tokens = self._parse(instring, 0)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 5203, in parseImpl
return super().parseImpl(instring, loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 4352, in parseImpl
return self.expr._parse(instring, loc, doActions, callPreParse=False)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 5203, in parseImpl
return super().parseImpl(instring, loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 4352, in parseImpl
return self.expr._parse(instring, loc, doActions, callPreParse=False)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 4091, in parseImpl
return e._parse(
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 5203, in parseImpl
return super().parseImpl(instring, loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 4352, in parseImpl
return self.expr._parse(instring, loc, doActions, callPreParse=False)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 4091, in parseImpl
return e._parse(
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 3863, in parseImpl
loc, exprtokens = e._parse(instring, loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 4352, in parseImpl
return self.expr._parse(instring, loc, doActions, callPreParse=False)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 3863, in parseImpl
loc, exprtokens = e._parse(instring, loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 4767, in parseImpl
loc, tokens = self_expr_parse(instring, loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 3863, in parseImpl
loc, exprtokens = e._parse(instring, loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 5203, in parseImpl
return super().parseImpl(instring, loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 4352, in parseImpl
return self.expr._parse(instring, loc, doActions, callPreParse=False)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 4091, in parseImpl
return e._parse(
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 3863, in parseImpl
loc, exprtokens = e._parse(instring, loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 4352, in parseImpl
return self.expr._parse(instring, loc, doActions, callPreParse=False)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 3863, in parseImpl
loc, exprtokens = e._parse(instring, loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 5203, in parseImpl
return super().parseImpl(instring, loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 4352, in parseImpl
return self.expr._parse(instring, loc, doActions, callPreParse=False)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 810, in _parseNoCache
loc, tokens = self.parseImpl(instring, pre_loc, doActions)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 4091, in parseImpl
return e._parse(
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 849, in _parseNoCache
tokens = fn(instring, tokens_start, ret_tokens)
File "/app/.heroku/python/lib/python3.8/site-packages/pyparsing/core.py", line 285, in wrapper
ret = func(*args[limit:])
TypeError: <lambda>() missing 1 required positional argument: 'token'
What caused this? Is there any issue with the condition(s) I wrote?
While a request has undefined (method, action) combination , this module will raise 403, I think response 405 is much more appropriate.
For example, I defined an extra action in viewset:
# And make this public in AP's statements
@action(detail=False, methods=['post'])
def foo(self, request):
...
If any one try GET /foo
, he will get 403 rather 405.
After study source code, I found this happened because AccessPolicy will return False while can't find a match statement. But django-restframework already handled such case.
Maybe you should let it go, and let DRF did the rest?
Amazing package! ⭐
Now, when you have such a fine grained backend permissioning system, it becomes super helpful to be able to translate that into the frontend code (Javascript) so that you can control which actions to show/allow based on the expected backend permission. For instance, if you know that certain role doesn't have access to perform a "create" in a given viewset, it would be useful to know that in the frontend so as to block/hide the respective create button when the user has that role.
Any ideas on how something like this could be achievable?
Again, fantastic package!
I have ModelViewSet with permission_class and override method scope_querset inside, but it isn't called (if not override, parent's method not called too). what could be?
In the documentation url there are two function examples, the first one is correct, but the second one (user_must_be) has a "self" parameter which should not have.
Reusable functions are called as is, not as a class methods.
Url: https://rsinger86.github.io/drf-access-policy/reusable_conditions/
I would like to apply the class-based views, not to ViewSets or function-based views.
For example I would like to use the access policies to Views derived from rest_framework.generics, such as RetrieveAPIView.
I managed to apply the access policy when using "action": ["*"] for the RetrieveAPIView view, but if I use "action": ["list", "retrieve", "get"] it does not work, so I don't know how to use finely-tuned access policies in this context.
Thanks in advance for your help :)
This is more a question than an issue.
I have the following custom action on my ModelViewSet
:
@action(methods=["post", "delete"], detail=True, url_path="assign/(?P<userid>[^/.]+)")
def assign(self, request, pk=None, userid=None):
And on the AccessPolicy
I have a method:
def self_or_manager(self, request, view, action) -> bool:
I want to access the userid
in my condition method. Is this possible?
Thanks,
Tobias
Looks like pyparsing is not being installed with drf-access-policy:
$ pip install django djangorestframework drf-access-policy
Defaulting to user installation because normal site-packages is not writeable
Collecting django
Using cached Django-3.1.5-py3-none-any.whl (7.8 MB)
Collecting djangorestframework
Using cached djangorestframework-3.12.2-py3-none-any.whl (957 kB)
Processing ./.cache/pip/wheels/55/b4/74/357bd96251493a1be8fec4cdee10e54399b7f8bcbe864d20ce/drf_access_policy-0.8.5-py3-none-any.whl
Requirement already satisfied: sqlparse>=0.2.2 in ./.local/lib/python3.9/site-packages (from django) (0.4.1)
Requirement already satisfied: pytz in /usr/lib/python3.9/site-packages (from django) (2020.4)
Requirement already satisfied: asgiref<4,>=3.2.10 in ./.local/lib/python3.9/site-packages (from django) (3.3.1)
Installing collected packages: django, djangorestframework, drf-access-policy
Successfully installed django-3.1.5 djangorestframework-3.12.2 drf-access-policy-0.8.5
$ python3
Python 3.9.0 (default, Oct 6 2020, 00:00:00)
[GCC 10.2.1 20200826 (Red Hat 10.2.1-3)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from rest_access_policy import AccessPolicy
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/daviddavis/.local/lib/python3.9/site-packages/rest_access_policy/__init__.py", line 2, in <module>
from .access_policy import AccessPolicy
File "/home/daviddavis/.local/lib/python3.9/site-packages/rest_access_policy/access_policy.py", line 6, in <module>
from pyparsing import infixNotation, opAssoc
ModuleNotFoundError: No module named 'pyparsing'
Great package 🥇 Much clearer adn more flexible than the DRF build in permission system.
I appreciate the feature Field Level Permissions but can't get it to work fully.
From the docs:
Often, depending on the user, not all fields should be visible or only a subset should be writable.
The docs example shows how to remove a field at all, but I can't figure out how to achieve making one or more fields read_only. Even lurking at the source does not help me figure it out.
Would it be possible to explain that a little more or giving a hint how to achieve "only a subset should be writable"?
Thanks.
https://www.django-rest-framework.org/api-guide/permissions/#object-level-permissions
def get_object(self):
obj = get_object_or_404(self.get_queryset(), pk=self.kwargs["pk"])
self.check_object_permissions(self.request, obj)
return obj
So get_object
already has check_permissions call. So it would be nice to impelement has_object_permission
(https://github.com/encode/django-rest-framework/blob/master/rest_framework/permissions.py#L112).
It will have the same logic as has_permission
but provide additional attribute object
into conditions functions.
Signature of conditions would be f(self, request, view, action, object=None) -> bool
. And object-level conditions should have this check:
if object is None:
return True
P.S. If you agree with this I could impelement it and create PR.
First things first, this is an awesome package and I love it. Thanks!
Is there a way to give some information about which permission check failed in the resulting 403 response?
I'd like to add a message in the permission denied response depending on what statement condition returns False in the permission checks. For example, given the following statements:
{
"action": ["list"],
"principal": ["authenticated"],
"effect": "allow",
"condition_expression": "has_teacher_privileges:manage_events or requested_own_participations",
},
{
"action": ["create"],
"principal": ["authenticated"],
"effect": "allow",
"condition_expression": "can_participate",
}
If the method can_participate
returns False, I'd like the response body to maybe look something like:
{"detail": "You cannot participate"}
as opposed to the default for Django;
{"detail": "You do not have permission to perform this action."}
Is this in any way possible?
Suppose i have following serializer with FieldAccessMixin
:
class OrganizationSerializerV1(CustomFieldAccessMixin, serializers.ModelSerializer):
pass
And i have this serializer in another serializer with read_only=True
attribute:
class WorkOrganizationSerializerV1(serializers.ModelSerializer):
organizations_ids = serializers.PrimaryKeyRelatedField(
write_only=True,
many=True,
queryset=OrganizationV1.objects.all(),
source="organizations",
)
organizations = OrganizationSerializerV1(read_only=True, many=True)
So i always got an exception that i need a context with request in it but serializer is read only.
Is there any way that i can handle it?
Maybe we need something like that in source code?
class FieldAccessMixin(object):
def __init__(self, *args, **kwargs):
self.serializer_context = kwargs.get("context", {})
super().__init__(*args, **kwargs)
if self.read_only is False:
self._apply_fields_access()
self._apply_deprecated_field_permissions()
Errors occur when overriding PK on a field with a different name. For example, "uuid".
The reason for this is elif self.id_prefix + str(user.id) in principals:
in the file "access_policy.py". Please fix it to str(user.pk)
.
Thank you for earlier.
So,
I have a Membership model, with 3 roles - viewer, member, manager
I created a group for all users that aren't django staff or admins to filter out some processing
My problem is I have to repeat the same calls to get the membership instance to check for the permission.
(A member can be a part of multiple schedules)
class ShiftAccessPolicy(BaseAccessPolicy):
statements = [
{
"action": "*",
"principal": ["admin", "staff"],
"effect": "allow",
},
{
"action": ["list", "retrieve"],
"principal": "group:auth_user",
"effect": "allow",
"condition": "is_schedule_member"
},
{
"action": "create",
"principal": "group:auth_user",
"effect": "allow",
"condition_expression": "is_not_viewer_only"
},
{
"action": [ "archive"],
"principal": "group:auth_user",
"effect": "allow",
"condition_expression": "is_schedule_manager"
}
]
def is_schedule_manager(self, request, view, action):
membership = request.user.member.membership_set.filter(schedule__id=view.kwargs.get('schedule_pk'))
if membership.exists():
membership = membership[0]
return Roles.SCHEDULE_MANAGER == membership.role
else:
return False
def is_schedule_member(self, request, view, action):
membership = request.user.member.membership_set.filter(schedule__id=view.kwargs.get('schedule_pk'))
return membership.exists()
def is_viewer_only(self, request, view, action):
membership = request.user.member.membership_set.filter(schedule__id=view.kwargs.get('schedule_pk'))
if membership.exists():
membership = membership[0]
return Roles.VIEWER != membership.role
else:
return False
As can be seen - each time I have to get the membership...
3 questions came into mind:
Hello, there is a problem with the BoolOperand.__new__
method. The returned operand would not properly work in such a case:
check_something
.check_something:{parent}.attribute_one.attribute_two
inside a statement's condition
and it works.condition_expression
in a form not check_something:{parent}.attribute_one.attribute_two
and it will silently fail, because characters like {
and }
are not recognized properly.It should rather be:
class BoolOperand(object):
def __new__(cls):
return TRUE | FALSE | Combine(Word(alphanums + "_", max=256) + ":" + Word(printables, max=256))
Umm... I just wanted to tell you that you have a typo in the first line of
Line 1 in 56ad1e6
It says Dango REST :)
Should be Django, obviously.
hi , my question is simple , can i use it on generic api views ? like list api view or create api view ?
Although the actions I wrote in ViewSet are added to the action in the class we wrote for Access Policy, there is no triggering process.
Can I write all policies for different resources in one json file? if yes how to use it?
below is my policy.
from rest_access_policy import AccessPolicy
class SearchAccessPolicy(AccessPolicy):
statements = [
{
"action": ["person"],
"principal": ["authenticated"],
"effect": "allow",
}
]
and i also have a function in a ViewSet-based class.
@action(methods=['POST'], detail=False, name='person')
def person(self, request):
...
...
..
Even though i try to make a request as logged in user. it gives 403 error. How can i solve this problem?
First of all, thanks for a nice project, it's neat to be able to dynamically load json of access policy statements to apply highly configurable permissions with custom condition functions. We use this in a real-world application with real customers with real security implications.
We recently upgraded to 0.8.7 and discovered that a load of permission rules were now being ignored (so people who aren't supposed to be allowed to do things can now do them). Reason is this change: 199d53a It's trying to be clever and parsing the condition string, which means the wrong value is passed to a condition function. For example we had this condition string working fine in 0.7.0
"can_edit_event_attribute:start_date,end_date:EDIT_EVENT_DATES"
can_edit_event_attribute is a custom function on the access policy which takes the string argument start_date,end_date:EDIT_EVENT_DATES splits by colon to get attributes list which is comma separated, and then permission name. So logically this is saying "If you want to change the start_date or end_date fields in a json payload to this endpoint, you need the permission object EDIT_EVENT_DATES. This worked very nicely because when customers said "Hey, I also want to restrict the duration field with that same permission", we could just update the string in policy json to start_date,end_date,duration:EDIT_EVENT_DATES and bingo it works with config not code change. We have adopted this pattern of condition strings being a colon separated bunch of clauses which can themselves be comma separated as a way of passing arguments into the custom condition functions and it's powerful and convenient.
But now that the condition string is being parsed, the comma is making that string get truncated and all that is passed to the can_edit_event_attribute condition function is incorrectly 'start_date' rather than 'start_date,end_date:EDIT_EVENT_DATES'.
I appreciate being able to do logical expressions in the condition strings is useful, but it is not backwards compatible and could be breaking lots of other clients too (who might not notice that users can now subvert the permission rules).
I see the most recent change added '*' character to what counts as a word for the tokenizer. So the easy fix here would be adding comma too and that would fix my use case. But other clients could have condition strings containing spaces or the string OR if they'd built their own parser of condition strings so that's not really a good general fix. The crux of the issue is that it's invalid to apply a parser/tokenizer to what used to be a simple string and clients were relying on receiving verbatim. Maybe the solution is to introduce a new property in the json 'condition_expression' that supports the parsing, and 'condition' does no parsing and passes the string verbatim to avoid breaking clients relying on that prior behaviour.
I just installed this library and faced this issue
I am really new in this my apologies if it's a lame issue
Result: Access Policy doesn't work
class ArticleViewSet(ModelViewSet, AccessViewSetMixin):
access_policy = ArticleAccessPolicy
...
Result: Access Policy works!
class ArticleViewSet(AccessViewSetMixin, ModelViewSet):
access_policy = ArticleAccessPolicy
...
Result: Access Policy works!
class ArticleViewSet(ModelViewSet):
permission_classes = [ArticleAccessPolicy, ]
...
https://github.com/rsinger86/drf-access-policy/blob/master/rest_access_policy/access_policy.py#L79
This line will raise an error if user is not authorized.
It can be fixed by lazy function with is_authorized check (you do implicit is_authorized
check only if user should be in some group) or could move user_roles = self.get_user_group_values(user)
into for
loop under condition and cache it value.
Thanks for writing this library ❤️
This looks interesting. I have been searching for access related library for DRF that handles even object level and multi tenancy.
Hi, dear partners.
Today I visit the documentation webpage and currently doesn't work. I verified with other devices and still down.
Pardon me if I got a bit excited about this package. This issue is basically a philosophical discussion that goes outside the scope of this project.
I know that DRF's custom permissions are rather limited in that you can only return a boolean and DRF will take care of the response. Thinking in how you're combining static checks with more dynamic checks, it would be great if the error response could be made more explicit depending on the type of failure. Let me explain myself.
I consider a static check to be something that is unlikely to change over time. User or role based permissions are a good example of those, as permissions attached to users/roles do not depend on the state of the system. A dynamic check would be something that depends on the state of the system, that is, something that would fit in a "condition" of the statement.
To me, it's not philosophically correct to treat a failure on each situation in the same way, that is, it's not fair to just give a plain 403 on each and all situations. If a user doesn't have permission to perform certain action based on the user itself or its roles, that's a typical 403. However, if the failure comes from a condition that tested positive in the current request, but is likely to test negative when the system changes (e.g. is_happy_hour
), then, a different type of error would be expected, in order to tell the user that the 403 is not permanent. If not a different response status, at least a different level of details in the error response body.
Are you making such distinctions in your own uses of this package? Do you know of any DRF hook that would make something like this possible, other than possibly annotating the request and capturing the exception on the way out via middleware?
Hi there.
I'm using view.get_object()
as shown in the documentation but in certain cases there is an assertion failing (lookup_url_kwarg in self.kwargs
) inside of get_object()
. In the example I share here, it's on the list action, where the url follows the pattern: /quiz/?course=<pk>
. In another case, however, it's when I use the create action on a different model.
Expected view QuizViewSet to be called with a URL keyword argument named "pk". Fix your URL conf, or set the .lookup_field attribute on the view correctly.
Here is my policy:
statements = [
{
'action': ['retrieve'],
'principal': 'authenticated',
'effect': 'allow',
'condition_expression': 'is_in_course and accessible_if_student'
}
]
The condition methods:
def is_in_course(self, request, view, action):
user = request.user
quiz = view.get_object() # raises AssertionError on list action
return user in course.admins.all() or user in course.instructors.all() or user in course.graders.all() \
or user in course.students.all() or user in course.guests.all()
def accessible_if_student(self, request, view, action):
user = request.user
quiz = view.get_object()
course = quiz.course
if user in course.students.all():
return True
return quiz.published and user in quiz.students.all()
So clearly I'm missing the pk
but I'm not exactly sure how I'm supposed to provide that. Any ideas?
Hey @rsinger86,
After reading the code a bit, I stumbled across the code for matching principal on statements.
I found out the code at access_policy.py line 142 that the "authenticated" value for principal checks that the user is not anonymous instead of actually whether it is authenticated or not.
Out of curiosity, why is that implemented like so and not using built-in "is_authenticated"?
Is there a difference?
Thank you!
There is few default classes like IsAuthorized
but there is no reason to create custom method:
def is_authorized(self, *args, **kwargs):
return IsAuthorized.has_permission(*args, **kwargs)
Istead just add check of condition value (isclass(cond) and issubclass(cond, BasePermission) or isinstance(cond, BasePermission)
) so it can be pass directly:
"condition": permissions.IsAuthorized
I use my conditions with this behaviour when I want to allow any action based in some logics requireds in my application:
class MyPolicy(AccessPolicy):
statements = [
{
'action': '*',
'principal': 'authenticated',
'effect': 'allow',
'condition': ['check_permissions:*'] # this could be `check_permissions:can_add,can_read,can_this,can_that` but to prevent write all, I use *
}
]
def check_permissions(self, request, view, action, permissions)
if permissions == '*':
return True
else:
# Checking others conditions based on permissions param like 'can_add,can_read'
check_permissions:*
, this is a condition to allow any action based on values in Database and others logics.
But with 0.8.5 its breaks my implementation because it is passing None
to permissions
param in check_permissions
I was forced to downgrade to 0.8.1 to make my app works again and I we can't refactoring entire application because of the time and cost we will have.
So I'm justin reporting the issue that we faced with this version
I love this package. It is exactly what I needed. Simple and Clear
I have maybe one suggestion. Currently, the condition statement is basically a AND operation, all conditions in the list need to be True. It would good also to be able to have an OR operation.
e.g. "condition" : ["balance_is_positive", "account_is_blocked | is_manager"]
e.g. "condition" : "balance_is_positive & (is_manager | account_is_blocked)"
def get_user_group_values(self, user) -> List[str]:
return list(user.groups.values_list("name", flat=True))
AccessPolicy heavily rely on compute current user's groups, but this implementation seems bad... It will hit database every time(reported by django-debug-toolbar), rather than using cache. For example, User.objects.prefetch_related('groups').all()
take no credits here.
Simply change a bit will make a big progress:
# prefetch related somewhere
# user = User.objects.prefetch_related('groups').get(...)
...
def get_user_group_values(self, user) -> List[str]:
return [g.name for g in user.groups.all()]
From now on, no matter how many calls of get_user_group_values
, database only get two queries(prefetch take one extra query). If User
model haven't prefetch with Group
, things still keep the same, it won't get worse.
And there is a better solution, using cache_property, but this need to define a method in model, which can't not handled by assess policy.
Support store list statements in database
to make it easier to create and edit policies without changing the source code 🙂
I think that "effect" could have a default value of "allow" because
I believe I have found a bug in drf-access-policy
. During some requests I receive the following 500 error:
Traceback (most recent call last):
File "/usr/local/lib/python3.8/site-packages/django/core/handlers/exception.py", line 47, in inner
response = get_response(request)
File "/usr/local/lib/python3.8/site-packages/django/core/handlers/base.py", line 181, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/usr/local/lib/python3.8/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
return view_func(*args, **kwargs)
File "/usr/local/lib/python3.8/site-packages/rest_framework/viewsets.py", line 125, in view
return self.dispatch(request, *args, **kwargs)
File "/usr/local/lib/python3.8/site-packages/rest_framework/views.py", line 509, in dispatch
response = self.handle_exception(exc)
File "/usr/local/lib/python3.8/site-packages/rest_framework/views.py", line 469, in handle_exception
self.raise_uncaught_exception(exc)
File "/usr/local/lib/python3.8/site-packages/rest_framework/views.py", line 480, in raise_uncaught_exception
raise exc
File "/usr/local/lib/python3.8/site-packages/rest_framework/views.py", line 497, in dispatch
self.initial(request, *args, **kwargs)
File "/usr/local/lib/python3.8/site-packages/rest_framework/views.py", line 415, in initial
self.check_permissions(request)
File "/usr/local/lib/python3.8/site-packages/rest_framework/views.py", line 332, in check_permissions
if not permission.has_permission(request, self):
File "/usr/local/lib/python3.8/site-packages/rest_access_policy/access_policy.py", line 26, in has_permission
return self._evaluate_statements(statements, request, view, action)
File "/usr/local/lib/python3.8/site-packages/rest_access_policy/access_policy.py", line 62, in _evaluate_statements
matched = self._get_statements_matching_context_conditions(
File "/usr/local/lib/python3.8/site-packages/rest_access_policy/access_policy.py", line 181, in _get_statements_matching_context_conditions
passed = bool(boolExpr.parseString(condition)[0])
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 1943, in parseString
loc, tokens = self._parse(instring, 0)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 1683, in _parseNoCache
loc, tokens = self.parseImpl(instring, preloc, doActions)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 4462, in parseImpl
return self.expr._parse(instring, loc, doActions, callPreParse=False)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 1683, in _parseNoCache
loc, tokens = self.parseImpl(instring, preloc, doActions)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 4462, in parseImpl
return self.expr._parse(instring, loc, doActions, callPreParse=False)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 1683, in _parseNoCache
loc, tokens = self.parseImpl(instring, preloc, doActions)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 4254, in parseImpl
ret = e._parse(instring, loc, doActions)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 1683, in _parseNoCache
loc, tokens = self.parseImpl(instring, preloc, doActions)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 4462, in parseImpl
return self.expr._parse(instring, loc, doActions, callPreParse=False)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 1683, in _parseNoCache
loc, tokens = self.parseImpl(instring, preloc, doActions)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 4254, in parseImpl
ret = e._parse(instring, loc, doActions)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 1683, in _parseNoCache
loc, tokens = self.parseImpl(instring, preloc, doActions)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 4462, in parseImpl
return self.expr._parse(instring, loc, doActions, callPreParse=False)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 1683, in _parseNoCache
loc, tokens = self.parseImpl(instring, preloc, doActions)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 4254, in parseImpl
ret = e._parse(instring, loc, doActions)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 1683, in _parseNoCache
loc, tokens = self.parseImpl(instring, preloc, doActions)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 4069, in parseImpl
loc, exprtokens = e._parse(instring, loc, doActions)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 1683, in _parseNoCache
loc, tokens = self.parseImpl(instring, preloc, doActions)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 4462, in parseImpl
return self.expr._parse(instring, loc, doActions, callPreParse=False)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 1683, in _parseNoCache
loc, tokens = self.parseImpl(instring, preloc, doActions)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 4462, in parseImpl
return self.expr._parse(instring, loc, doActions, callPreParse=False)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 1683, in _parseNoCache
loc, tokens = self.parseImpl(instring, preloc, doActions)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 4254, in parseImpl
ret = e._parse(instring, loc, doActions)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 1683, in _parseNoCache
loc, tokens = self.parseImpl(instring, preloc, doActions)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 4069, in parseImpl
loc, exprtokens = e._parse(instring, loc, doActions)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 1683, in _parseNoCache
loc, tokens = self.parseImpl(instring, preloc, doActions)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 4462, in parseImpl
return self.expr._parse(instring, loc, doActions, callPreParse=False)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 1683, in _parseNoCache
loc, tokens = self.parseImpl(instring, preloc, doActions)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 4052, in parseImpl
loc, resultlist = self.exprs[0]._parse(instring, loc, doActions, callPreParse=False)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 1683, in _parseNoCache
loc, tokens = self.parseImpl(instring, preloc, doActions)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 4462, in parseImpl
return self.expr._parse(instring, loc, doActions, callPreParse=False)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 1683, in _parseNoCache
loc, tokens = self.parseImpl(instring, preloc, doActions)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 4254, in parseImpl
ret = e._parse(instring, loc, doActions)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 1683, in _parseNoCache
loc, tokens = self.parseImpl(instring, preloc, doActions)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 4462, in parseImpl
return self.expr._parse(instring, loc, doActions, callPreParse=False)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 1683, in _parseNoCache
loc, tokens = self.parseImpl(instring, preloc, doActions)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 4254, in parseImpl
ret = e._parse(instring, loc, doActions)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 1716, in _parseNoCache
tokens = fn(instring, tokensStart, retTokens)
File "/usr/local/lib/python3.8/site-packages/pyparsing.py", line 1316, in wrapper
ret = func(*args[limit[0]:])
TypeError: __init__() missing 1 required positional argument: 't'
The error seems to stem from File "/usr/local/lib/python3.8/site-packages/rest_access_policy/access_policy.py", line 181, in _get_statements_matching_context_conditions
I have two ModelViewSet's/ModelSerializer's, each with their access policies set. To create an object in one viewset you must give the PK of an object in the other viewset. At the moment it does not enforce the other viewset's access policy for that PK. Is does not deny access to a resource that you would otherwise not have access to through the other viewset. Is there a way to enforce the policy there?
Rather than having to always write a custom method when a statement needs to examine the contextual details of the request/user/object, some built-in conditions could be provided. I think the syntax would be to use a dictionary for these built-in checks, and have string values continue to reference a custom method on the policy.
{
"action": "*",
"principal": "*",
"condition": { "<condition_type>": "<value_option>" }
}
There could be three to start with:
{
"action": "*",
"principal": "*",
"condition": { "is_read": false } # whether the request method is HEAD, GET or OPTIONS
}
{
"action": "*",
"principal": "*",
"condition": { "client_ip": "203.0.113.0/24" } # whether the requester's IP matches
}
{
"action": "*",
"principal": "*",
"condition": { "user_is": "owner" } # Whether a field on the object instance (from view.get_object()) is equal to the request user
}
{
"action": "*",
"principal": "*",
"condition": { "is_authenticated": True } # whether the user is authenticated
}
AWS IAM provides a much more feature-rich version of this: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html
However, since we have the flexibility to write custom methods, I think it's best to only cover most common and simple cases with built-in conditions.
I need to change the response message and code dynamically to tell the front-end why it happened.
The message and the code can be defined by defining the message
and code
attribute, but they are static and it seems that there is no implementation to change them dynamically.
On the page https://rsinger86.github.io/drf-access-policy/ the 'Read On' link on the bottom of that page href is:
https://rsinger86.github.io/usage/view_set_usage
whereas it ought to be:
https://rsinger86.github.io/drf-access-policy/usage/view_set_usage/
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.