{ "info": { "author": "Robert Singer", "author_email": "robertgsinger@gmail.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 4 - Beta", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6" ], "description": "# Django REST - Access Policy\n\n[](https://pypi.python.org/pypi/drf-access-policy)\n[](https://img.shields.io/pypi/status/drf-access-policy.svg/)\n\nThis project brings a declaritive, organized approach to managing access control in Django REST Framework projects. Each ViewSet or function-based view can be assigned an explicit policy for the exposed resource(s). No more digging through views or seralizers to understand access logic -- it's all in one place in a format that less technical stakeholders can understand. If you're familiar with other declaritive access models, such as AWS' IAM, the syntax will be familiar. \n\nIn short, you can start expressing your access rules like this:\n\n```python\nclass ArticleAccessPolicy(AccessPolicy):\n statements = [\n {\n \"action\": [\"list\", \"retrieve\"],\n \"principal\": \"*\",\n \"effect\": \"allow\"\n },\n {\n \"action\": [\"publish\", \"unpublish\"],\n \"principal\": [\"group:editor\"],\n \"effect\": \"allow\" \n }\n ]\n```\n\nThis project has complete test coverage and the base `AccessPolicy` class is only ~150 lines of code: there's no magic here.\n\n# Table of Contents:\n\n- [Installation](#installation)\n- [Example #1: Policy for ViewSet](#example-1-policy-for-viewset)\n- [Example #2: Policy for Function-Based View](#example-2-policy-for-function-based-view)\n- [Documentation](#documentation)\n * [Statement Elements](#statement-elements)\n * [principal](#principal)\n * [action](#action)\n * [effect](#effect)\n * [condition](#condition)\n * [Policy Evaluation Logic](#policy-evaluation-logic)\n * [Object-Level Permissions/Conditions](#object-level-perm)\n * [Re-Usable Conditions/Permissions](#reusable-conditions)\n * [Multitenancy Data/Restricting QuerySets](#multitenancy-data--restricting-querysets)\n * [Attaching to ViewSets and Function-Based Views](#attaching-to-viewsets-and-function-based-views)\n * [Loading Statements from External Source](#loading-statements-from-external-source)\n * [Customizing User Group/Role Values](#customizing-user-grouprole-values)\n * [Customizing Principal Prefixes](#customizing-principal-prefixes)\n- [Changelog](#changelog)\n- [Testing](#testing)\n- [License](#license)\n\n# Setup\n\n```\npip install drf-access-policy\n```\n\nTo define a policy, import `AccessPolicy` and subclass it:\n\n```python\nfrom rest_access_policy import AccessPolicy\n\n\nclass ShoppingCartAccessPolicy(AccessPolicy):\n statements = [] # Now read on...\n```\n\n# Example #1: Policy for ViewSet\n\nIn a nutshell, a policy is comprised of \"statements\" that declare what \"actions\" a \"principal\" can or cannot perform on the resource, with optional custom checks that can examine any detail of the current request.\n\nHere are two more key points to remember going forward:\n* all access is implicitly denied by default\n* any statement with the \"deny\" effect overrides any and all \"allow\" statement\n\nNow let's look at the policy below an articles endpoint, provided through a view set.\n\n```python\nclass ArticleAccessPolicy(AccessPolicy):\n statements = [\n {\n \"action\": [\"list\", \"retrieve\"],\n \"principal\": \"*\",\n \"effect\": \"allow\"\n },\n {\n \"action\": [\"publish\", \"unpublish\"],\n \"principal\": [\"group:editor\"],\n \"effect\": \"allow\" \n },\n {\n \"action\": [\"delete\"],\n \"principal\": [\"*\"],\n \"effect\": \"allow\",\n \"condition\": \"is_author\" \n },\n {\n \"action\": [\"*\"],\n \"principal\": [\"*\"],\n \"effect\": \"deny\",\n \"condition\": \"is_happy_hour\"\n }\n ]\n\n def is_author(self, request, view, action) -> bool:\n article = view.get_object()\n return request.user == article.author \n\n def is_happy_hour(self, request, view, action) -> bool:\n now = datetime.datetime.now()\n return now.hour >= 17 and now.hour <= 18:\n\n @classmethod\n def scope_queryset(cls, request, queryset):\n if request.user.groups.filter(name='editor').exists():\n return queryset\n\n return queryset.filter(status='published')\n\n\nclass ArticleViewSet(ModelViewSet):\n # Just stick the policy here, as you would do with\n # regular DRF \"permissions\"\n permission_classes = (ArticleAccessPolicy, )\n\n # Helper property here to make get_queryset logic\n # more explicit\n @property\n def access_policy(self):\n return self.permission_classes[0]\n\n # Ensure that current user can only see the models \n # they are allowed to see\n def get_queryset(self):\n return self.access_policy.scope_queryset(\n self.request, Articles.objects.all()\n )\n \n @action(method=\"POST\")\n def publish(self, request, *args, **kwargs):\n pass\n\n @action(method=\"POST\")\n def unpublish(self, request, *args, **kwargs):\n pass\n\n # the rest of you view set definition...\n```\n\nThe actions correspond to the names of methods on the ViewSet. \n\nIn the example above, the following rules are put in place:\n- anyone is allowed to list and retrieve articles\n- users in the editor group are allowed to publish and unpublish articles\n- in order to delete an article, the user must be the author of the article. Notice how the condition method `is_author` calls `get_object()` on the view to get the current article.\n- if the condition `is_happy_hour`, evaluates to `True`, then no one is allowed to do anything.\n\nAdditionally, we have some logic in the `scope_queryset` method for filtering which models are visible to the current user. Here, we want users to only see published articles, unless they are an editor, in which case they case see articles with any status. You have to remember to call this method from the view, so I'd suggest reviewing this as part of a security audit checklist.\n\n# Example #2: Policy for Function-Based View\n\nYou can also you policies with function-based views. The action to reference in your policy statements is the name of the function. You can also bundle multiple functions into the same policy as the example below shows.\n\n```python\nclass AuditLogsAccessPolicy(AccessPolicy):\n statements = [\n {\n \"action\": [\"search_logs\"],\n \"principal\": \"group:it_staff\",\n \"effect\": \"allow\"\n },\n {\n \"action\": [\"download_logs\"],\n \"principal\": [\"group:it_admin\"],\n \"effect\": \"allow\" \n }\n ]\n\n\n@api_view([\"GET\"])\n@permission_classes((AuditLogsAccessPolicy,))\ndef search_logs(request):\n ## you logic here...\n pass\n\n\n@api_view([\"GET\"])\n@permission_classes((AuditLogsAccessPolicy,))\ndef download_logs(request):\n ## you logic here...\n pass\n```\n\n# Documentation \n\n## Statement Elements\n\n### *principal*\n\n
| Description | \n\n Should match the user of the current request by identifying a group they belong to or their user ID.\n | \n
| Special Values | \n\n \"*\" (any user) \n \"authenticated\" (any authenticated user) \n \"anonymous\" (any non-authenticated user)\n | \n
| Type | \n Union[str, List[str]] | \n
| Format | \n\n Match by group with \"group:{name}\" \n Match by ID with \"id:{id}\" \n | \n
| Examples | \n\n [\"group:admins\", \"id:9322\"] \n [\"id:5352\"] \n [\"anonymous\"] \n \"*\"\n | \n
| Description | \n\n The action or actions that the statement applies to. The value should match the name of a view set method or the name of the view function.\n | \n
| Type | \nUnion[str, List[str]] | \n
| Special Values | \n\n \"*\" (any action) \n \"<safe_methods>\" (a read-only HTTP request: HEAD, GET, OPTIONS)\n | \n
| Examples | \n\n [\"list\", \"delete\", \"create] \n [\"*\"] \n [\"<safe_methods>\"]\n | \n
| Description | \n\n Whether the statement, if it is in effect, should allow or deny access. All access is denied by default, so use deny when you'd like to override an allow statement that will also be in effect.\n | \n
| Type | \nstr | \n
| Values | \nEither \"allow\" or \"deny\" | \n
| Description | \n\n The name of a method on the policy that returns a boolean. The method signature is condition(request, view, action: str, custom_arg: str=None). If you want to pass a custom argument to the condition's method, format the value as {method_name}:{value}, e.g. user_must_be:owner will call a method named user_must_be, passing it the string \"owner\" as the final argument. If true, the policy will be in effect. Useful for enforcing object-level permissions. If list of conditions is given, all conditions must evaluate to True.\n | \n
| Type | \nUnion[str, List[str]] | \n
| Examples | \n\n \"is_manager_of_account\" \n \"is_author_of_post\" \n [\"balance_is_positive\", \"account_is_not_frozen\"]`\n \n \"user_must_be:account_manager\"\n | \n