{ "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[![Package version](https://badge.fury.io/py/drf-access-policy.svg)](https://pypi.python.org/pypi/drf-access-policy)\n[![Python versions](https://img.shields.io/pypi/status/drf-access-policy.svg)](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\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Description\n Should match the user of the current request by identifying a group they belong to or their user ID.\n
Special Values\n \"*\" (any user)
\n \"authenticated\" (any authenticated user)
\n \"anonymous\" (any non-authenticated user)\n
Type Union[str, List[str]]
Format\n Match by group with \"group:{name}\"
\n Match by ID with \"id:{id}\" \n
Examples\n [\"group:admins\", \"id:9322\"]
\n [\"id:5352\"]
\n [\"anonymous\"]
\n \"*\"\n
\n\n\n### *action*\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Description\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
TypeUnion[str, List[str]]
Special Values\n \"*\" (any action)
\n \"<safe_methods>\" (a read-only HTTP request: HEAD, GET, OPTIONS)\n
Examples\n [\"list\", \"delete\", \"create]
\n [\"*\"]
\n [\"<safe_methods>\"]\n
\n\n\n### *effect*\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n
Description\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
Typestr
ValuesEither \"allow\" or \"deny\"
\n\n\n### *condition*\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n
Description\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
TypeUnion[str, List[str]]
Examples\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\n## Policy Evaluation Logic\n\nTo determine whether access to a request is granted, two steps are applied: (1) finding out which statements apply to the request (2) denying or allowing the request based on those statements. \n- *Filtering statements*: A statement is applicable to the current request if all of the following are true (a) the request user matches one of the statement's principals, (b) the name of the method/function matches one of its actions, and (c) all custom conditions evaluate to true.\n- *Allow or deny*: The request is allowed if any of the statements have an effect of \"allow\", and none have an effect of \"deny\". By default, all requests are denied. Requests are implicitly denied if no `Allow` statements are found, and they are explicitly denied if any `Deny` statements are found. `Deny` statements *trump* `Allow` statements.\n\n## Object-Level Permissions/Custom Conditions \n\nWhat object-level permissions? You can easily check object-level access in a custom condition that's evaluated to determine whether the statement takes effect. This condition is passed the `view` instance, so you can get the model instance with a call to `view.get_object()`. You can even reference multiple conditions, to keep your access methods focused and testable, as well as parametrize these conditions with arguments.\n\n```python\nclass AccountAccessPolicy(AccessPolicy):\n statements = [\n ## ... other statements ...\n {\n \"action\": [\"withdraw\"],\n \"principal\": [\"*\"],\n \"effect\": \"allow\",\n \"condition\": [\"balance_is_positive\", \"user_must_be:owner\"] \n },\n {\n \"action\": [\"upgrade_to_gold_status\"],\n \"principal\": [\"*\"],\n \"effect\": \"allow\",\n \"condition\": [\"user_must_be:account_advisor\"]\n }\n ## ... other statements ...\n ]\n\n def balance_is_positive(self, request, view, action) -> bool:\n account = view.get_object()\n return account.balance > 0\n\n def user_must_be(self, request, view, action, field: str) -> bool:\n account = view.get_object()\n return getattr(account, field) == request.user\n```\n\nNotice how we're re-using the `user_must_be` method by parameterizing it with the model field that should be equal fo the user of the request: the statement will only be effective if this condition passes.\n\n## Re-Usable Conditions/Permissions \n\nIf you'd like to re-use custom conditions across policies, you can define them globally in a module and point to it via the setttings.\n\n```python\n# in your project settings.py\n\nDRF_ACCESS_POLICY = {\"reusable_conditions\": \"myproject.global_access_conditions\"}\n```\n\n```python\n# in myproject.global_access_conditions.py\n\ndef is_the_weather_nice(request, view, action: str) -> bool:\n data = weather_api.load_today()\n return data[\"temperature\"] > 68\n\ndef user_must_be(self, request, view, action, field: str) -> bool:\n account = view.get_object()\n return getattr(account, field) == request.user\n```\n\nThe policy class will first check its own methods for what's been defined in the `condition` property. If nothing is found, it will check the module defined in the `reusable_conditions` setting.\n\n## Multitenancy Data / Restricting QuerySets\n\nYou can define a class method on your policy class that takes a QuerySet and the current request and returns a securely scoped QuerySet representing only the database rows that the current user should have access to. This is helpful for multitenant situations or more generally when users should not have full visibility to model instances. Of course you could do this elsewhere in your code, but putting this method on the policy class keeps all access logic in a single place.\n\n```python\n class PhotoAlbumAccessPolicy(AccessPolicy):\n # ... statements, etc ...\n\n # Users can only access albums they have created\n @classmethod\n def scope_queryset(cls, request, qs):\n return qs.filter(creator=request.user)\n\n\n class TodoListAccessPolicy(AccessPolicy):\n # ... statements, etc ...\n\n # Users can only access todo lists owned by their organization\n @classmethod\n def scope_queryset(cls, request, qs):\n user_orgs = request.user.organizations.all()\n return qs.filter(org__id__in=user_orgs)\n```\n\n## Attaching to ViewSets and Function-Based Views\n\nYou attach access policies the same way you do with regular DRF permissions.\n\nFor ViewSets, add it to `permissions` property:\n```python\nclass ArticleViewSet(ModelViewSet):\n permission_classes = (ArticleAccessPolicy, )\n```\n\nFor function-based views, add it to `permissions_classes` decorator:\n```python\n@api_view([\"GET\"])\n@permission_classes((ArticleAccessPolicy,))\ndef create_article(request):\n ## you logic here...\n pass\n```\n\n## Loading Statements from External Source\n\nIf you don't want your policy statements hardcoded into the classes, you can load them from an external data source: a great step to take because you can then change access rules without redeploying code. \n\nJust define a method on your policy class called `get_policy_statements`, which has the following signature:\n`get_policy_statements(self, request, view) -> List[dict]`\n\nExample:\n\n```python\nclass UserAccessPolicy(AccessPolicy):\n id = 'user-policy'\n\n def get_policy_statements(self, request, view) -> List[dict]:\n statements = data_api.load_json(self.id)\n return json.loads(statements)\n```\n\nYou probably want to only define this method once on your own custom subclass of `AccessPolicy`, from which all your other access policies inherit.\n\n## Customizing User Group/Role Values\n\nIf you aren't using Django's built-in auth app, you may need to define a custom way to retrieve the role/group names to which the user belongs. Just define a method called `get_user_group_values` on your policy class. It is passed a single argument: the user of the current request. In the example below, the user model has a to-many relationship with a \"roles\", which have their \"name\" value in a field called \"title\".\n\n```python\nclass UserAccessPolicy(AccessPolicy):\n # ... other properties and methods ...\n\n def get_user_group_values(self, user) -> List[str]:\n return list(user.roles.values_list(\"title\", flat=True))\n```\n## Customizing Principal Prefixes\n\nBy default, the prefixes to identify the type of principle (user or group) are \"id:\" and \"group:\", respectively. You can customize this by setting these properties on your policy class:\n\n```python\nclass FriendRequestPolicy(permissions.BasePermission):\n group_prefix = \"role:\"\n id_prefix = \"staff_id:\"\n\n # .. the rest of you policy definition ..\n```\n\n# Changelog \n\n## 0.5.0 (September 2019)\n* Add option to define re-usable custom conditions/permissions in a module that can be referenced by multiple policies.\n\n## 0.4.2 (June 2019)\n* Fixes readme format for Pypy display.\n\n## 0.4.0 (June 2019)\n* Allow passing arguments to condition methods, via condition values formatted as `{method_name}:{arg_value}`.\n\n## 0.3.0 (May 2019)\n* Adds special `` action key that matches when the current request is an HTTP read-only method: HEAD, GET, OPTIONS.\n\n## 0.2.0 (May 2019)\n* Adds special `authenticated` and `anonymous` principal keys to match any authenticated user and any non-authenticated user, respectively. Thanks @bogdandm for discussion/advice!\n\n## 0.1.0 (May 2019)\n* Initial release\n\n# Testing\n\nTests are found in a simplified Django project in the ```/tests``` folder. Install the project requirements and do ```./manage.py test``` to run them.\n\n# License\n\nSee [License](LICENSE.md).", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/rsinger86/drf-access-policy", "keywords": "django restframework drf access policy authorization declaritive", "license": "MIT", "maintainer": "", "maintainer_email": "", "name": "drf-access-policy", "package_url": "https://pypi.org/project/drf-access-policy/", "platform": "", "project_url": "https://pypi.org/project/drf-access-policy/", "project_urls": { "Homepage": "https://github.com/rsinger86/drf-access-policy" }, "release_url": "https://pypi.org/project/drf-access-policy/0.5.0/", "requires_dist": null, "requires_python": "", "summary": "Declarative access policies/permissions modeled after AWS' IAM policies.", "version": "0.5.0" }, "last_serial": 5800562, "releases": { "0.1.0": [ { "comment_text": "", "digests": { "md5": "506cf979889fb000dc2bdff66b63f2b3", "sha256": "d21747bd4b0080cbbfb2a881bf23a6d0e13d8d12806331e17f9f016acc99705d" }, "downloads": -1, "filename": "drf-access-policy-0.1.0.tar.gz", "has_sig": false, "md5_digest": "506cf979889fb000dc2bdff66b63f2b3", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 8518, "upload_time": "2019-05-04T15:37:14", "url": "https://files.pythonhosted.org/packages/fe/f8/9937bb4393490e9e64532206cb850c9c7c7260d00a0e3e70271abd9cd41b/drf-access-policy-0.1.0.tar.gz" } ], "0.2.0": [ { "comment_text": "", "digests": { "md5": "19291b71275065c35d52e0f3ad18d734", "sha256": "a6365fec585a4131f98981aa13aaae9f794c14e06d7c6252a2c1acfaeb66e9a1" }, "downloads": -1, "filename": "drf-access-policy-0.2.0.tar.gz", "has_sig": false, "md5_digest": "19291b71275065c35d52e0f3ad18d734", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 8969, "upload_time": "2019-05-08T03:05:42", "url": "https://files.pythonhosted.org/packages/e0/8c/b8cab3a1da6c46d7e1b71917c3fe0389745ef6c54adcaa3740838c0049bf/drf-access-policy-0.2.0.tar.gz" } ], "0.3.0": [ { "comment_text": "", "digests": { "md5": "6fe54250c8058dc799e1bbca830c3b75", "sha256": "5bfdfb2c8591187a823d3bb6799886846851ec11eddb891dd582949c61118b9a" }, "downloads": -1, "filename": "drf-access-policy-0.3.0.tar.gz", "has_sig": false, "md5_digest": "6fe54250c8058dc799e1bbca830c3b75", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 9682, "upload_time": "2019-05-10T02:08:07", "url": "https://files.pythonhosted.org/packages/73/e9/5ea7d03d9b5c3eb3c17f3f2c5d8e1a5a51b8fffe8c565d314dd5e52e272d/drf-access-policy-0.3.0.tar.gz" } ], "0.4.0": [ { "comment_text": "", "digests": { "md5": "2fdb9cc4b08552e17f9d73eb0e60e795", "sha256": "1e596b7845320f277bf9634ba5a8d65c2134a23ef32230222ce0f4d2fed5aa78" }, "downloads": -1, "filename": "drf-access-policy-0.4.0.tar.gz", "has_sig": false, "md5_digest": "2fdb9cc4b08552e17f9d73eb0e60e795", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 7971, "upload_time": "2019-06-01T18:31:04", "url": "https://files.pythonhosted.org/packages/23/c9/e4c78230ece922cf3a3380ca48b33398e24743d5381ae1ecd74fc73cca11/drf-access-policy-0.4.0.tar.gz" } ], "0.4.1": [ { "comment_text": "", "digests": { "md5": "09005d0b93ad387ae9f4e59d9f16adc4", "sha256": "1565e1aa6ff64dd3e54aba79bb8bdf47b903682b59fac27c06f4b2ca20617d75" }, "downloads": -1, "filename": "drf-access-policy-0.4.1.tar.gz", "has_sig": false, "md5_digest": "09005d0b93ad387ae9f4e59d9f16adc4", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 11449, "upload_time": "2019-06-01T18:32:02", "url": "https://files.pythonhosted.org/packages/c3/67/4bf432e0e0df9a8fc8479dd14df4969df8d9af0b11045484f4d6d9880749/drf-access-policy-0.4.1.tar.gz" } ], "0.4.2": [ { "comment_text": "", "digests": { "md5": "7e8af4a757baf0f966d945fe15a9b07c", "sha256": "b1556a958b56d27fe8db7fe1f09de29b94b345ac9a9ef59ccca4a59e74dfd0d4" }, "downloads": -1, "filename": "drf-access-policy-0.4.2.tar.gz", "has_sig": false, "md5_digest": "7e8af4a757baf0f966d945fe15a9b07c", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 10385, "upload_time": "2019-06-01T18:35:43", "url": "https://files.pythonhosted.org/packages/e1/9a/98c5e659c01c01f6425bde2e96d444e7d1c1964d2cc0cb85af00bb99a01b/drf-access-policy-0.4.2.tar.gz" } ], "0.5.0": [ { "comment_text": "", "digests": { "md5": "45728bb92956e880d9f024e58ddd59a5", "sha256": "8825952f1cc0ba5ede9c3f7119595f8d3d6498d046756550db3007265dc58497" }, "downloads": -1, "filename": "drf-access-policy-0.5.0.tar.gz", "has_sig": false, "md5_digest": "45728bb92956e880d9f024e58ddd59a5", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 11174, "upload_time": "2019-09-08T20:47:44", "url": "https://files.pythonhosted.org/packages/72/14/7e64c8e6e022b247a05ee3d09aaf5daecb336ef1fb98d9c3bdb28651d6df/drf-access-policy-0.5.0.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "45728bb92956e880d9f024e58ddd59a5", "sha256": "8825952f1cc0ba5ede9c3f7119595f8d3d6498d046756550db3007265dc58497" }, "downloads": -1, "filename": "drf-access-policy-0.5.0.tar.gz", "has_sig": false, "md5_digest": "45728bb92956e880d9f024e58ddd59a5", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 11174, "upload_time": "2019-09-08T20:47:44", "url": "https://files.pythonhosted.org/packages/72/14/7e64c8e6e022b247a05ee3d09aaf5daecb336ef1fb98d9c3bdb28651d6df/drf-access-policy-0.5.0.tar.gz" } ] }