django-denied


Namedjango-denied JSON
Version 1.3 PyPI version JSON
download
home_pagehttps://github.com/mblayman/django-denied
SummaryAn authorization system based exclusively on allow lists
upload_time2024-04-06 17:57:34
maintainerNone
docs_urlNone
authorMatt Layman
requires_pythonNone
licenseMIT
keywords django
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # django-denied

> None shall pass.
>
> The Black Knight

django-denied is an authorization system
for the Django web framework.
With django-denied,
every Django view *must be explicitly allowed*.
This design means
that developers have to make a choice
about authorization
for a view to work.

In other words,
django-denied makes authorization a requirement
for every view in a Django project.

## Who should use this?

This package is well suited for Django projects
that need to protect pages against unauthorized access normally.
If you are making a service
that requires user's to login
and restricts which data a user sees,
then django-denied may be a good fit for you.

If your web application is meant to be open
for a large audience,
especially with lots of anonymous users,
then this package may be overkill for your needs.
A blog or content management system may not be a good candidate.

## Install

Get the package.

```
pip install django-denied
```

django-denied uses Django's built-in `auth` and `admin` apps.
These apps also depend on the `contenttypes` app.
Ensure that these apps are in your `INSTALL_APPS`
in your Django settings file.

```python
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    ...,
]
```

Add the `DeniedMiddleware`.
This middleware does all the authorization checking.
The middleware depends on the `request.user`,
so be sure to include it *after* the `AuthenticationMiddleware`.

```python
MIDDLEWARE = [
    ...,
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "denied.middleware.DeniedMiddleware",
    ...,
]
```

Now you're ready to start.

## Usage

django-denied has two primary modes
for handling views.

1. `allow`
2. `authorize`

These decorators are the main interface
of the package
and are described in the sections below.

By default,
django-denied assumes that all users should be authenticated,
with the exception of allowed views or login pages.

The login pages are

* The page defined by `settings.LOGIN_URL` and
* The Django admin login defined at the `admin:login` route.

If you set `LOGIN_URL`,
django-denied expects the path form
of the setting
(e.g., `/accounts/login/`)
rather than the `url` name
(e.g., `accounts:login`).

### Allowing views

Every app is likely to have views
that should be made accessible to unauthenticated users.
A company's about page, terms of service, and privacy policy are all good examples.

The `allow` decorator is for marking a Django view as exempt
from the authorization checking done
by the `DeniedMiddleware`.

This is an example of how you might create a terms of service view.

```python
# application/views.py
from denied.decorators import allow
from django.shortcuts import render


@allow
def terms_of_service(request):
    return render(request, "tos.html", {})
```

The `allow` decorator has a secondary function.
Aside from allowing a single view,
the decorator can allow a set of views
that you would use with `django.urls.path`.

This is necessary to permit third party apps
that have other views,
but are unaware of the django-denied system.

This is an example of using `allow`
to permit the Django admin views
as well as the popular app,
[django-allauth](https://django-allauth.readthedocs.io/en/latest/).

```python
# project/urls.py
from denied.decorators import allow
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("accounts/", allow(include("allauth.urls"))),
    path("admin/", allow(admin.site.urls)),
]
```

Note:
Even if you include `allow` on a view or a set of views,
that does not mean that what you've allowed will suddenly
bypass any existing authentication or authorization checking.
***This is a feature, not a bug!***

`login_required`, `permission_required`,
and any other authentication or authorization checking
that pre-exist on views will remain.
*django-denied does not disable the security features
of other third party libraries.*

## Authorizing views

With django-denied,
a Django view is authorized with the `authorize` decorator
and an *authorizer* function.
An authorizer has a function signature of

```python
from django.http import HttpRequest


def example_authorizer(request: HttpRequest, **view_kwargs: dict) -> bool:
    ...
```

The authorizer evaluates the incoming request and view information
and should return `True` if the request is authorized
or `False` is the request is not authorized.
The `view_kwargs` include any data that was parsed out of the URL route.

The authorizer acts as a declarative way
of showing what is authorized
for the view.

```python
from denied.decorators import authorize

from .authorizers import example_authorizer


@authorize(example_authorizer)
def example_view(request):
    ...
```

To use `authorize` on a class-based view,
you must attach the decorator to the `dispatch` method.

```python
from denied.decorators import authorize
from django.utils.decorators import method_decorator
from django.views.generic import DetailView

from .authorizers import example_authorizer
from .models import Example


@method_decorator(authorize(example_authorizer), "dispatch")
class ExampleDetail(DetailView):
    queryset = Example.objects.all()
```
### Built-in authorizers

The library includes built-in authorizers
for common cases.

#### `denied.authorizers.any_authorized`

This authorizer always evaluates to `True` and is the logical equivalent
to `login_required` since django-denied always enforces authentication checking.

#### `denied.authorizers.staff_authorized`

This authorizer only permits access when `user.is_staff == True`.
`staff_authorized` is equivalent to `staff_member_required`
from the Django `admin` app.

#### Authorizer example

This section shows a more complete example
of an authorizer
to give you a sense
of how django-denied works in practice.

For our example,
we'll consider a project tracking application.
This is little more than a TODO list
that groups the tasks into projects.

Here are the models.

```python
# application/models.py
from django.contrib.auth.models import User
from django.db import models


class Project(models.Model):
    owner = models.ForeignKey(User, on_delete=models.CASCADE)


class Task(models.Model):
    project = models.ForeignKey(Project, on_delete=models.CASCADE)
    description = models.TextField()
    completed = models.BooleanField(default=False)
```

For this simple system,
only project owners can do anything
with a task.
Let's create the authorizer for that.

```python
# application/authorizers.py


def task_authorized(request, **view_kwargs):
    return Task.objects.filter(
        project__owner=request.user,
        pk=view_kwargs["pk"],
    ).exists()
```

These are the URLs we want to support
with this authorizer.

```python
# application/urls.py

from django.urls import path

from .views import task_detail, task_edit

urlpatterns = [
    path("tasks/<int:pk>/", task_detail, name="task_detail"),
    path("tasks/<int:pk>/edit/", task_detail, name="task_edit"),
]
```

Now we can set our views
and set their authorization.

```python
# application/views.py
from denied.decorators import authorize
from django.shortcuts import render

from .authorizers import task_authorized
from .models import Task


@authorize(task_authorized)
def task_detail(request, pk):
    task = Task.objects.get(pk=pk)
    return render(request, "task_detail.html", {"task": task})


@authorize(task_authorized)
def task_edit(request, pk):
    task = Task.objects.get(pk=pk)
    return render(request, "task_edit.html", {"task": task})
```

Since the authorizer handles the access control,
we can be confident that the task is safe to fetch
by its key alone.
Access control is pushed to the boundary of the view
so that the view's internal logic is about as simple
as you can make it.

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/mblayman/django-denied",
    "name": "django-denied",
    "maintainer": null,
    "docs_url": null,
    "requires_python": null,
    "maintainer_email": null,
    "keywords": "Django",
    "author": "Matt Layman",
    "author_email": "matthewlayman@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/4d/5f/e02af85f3369d7a569aca6f39956ed6273aabe9f80441232d561f6438b9e/django-denied-1.3.tar.gz",
    "platform": null,
    "description": "# django-denied\n\n> None shall pass.\n>\n> The Black Knight\n\ndjango-denied is an authorization system\nfor the Django web framework.\nWith django-denied,\nevery Django view *must be explicitly allowed*.\nThis design means\nthat developers have to make a choice\nabout authorization\nfor a view to work.\n\nIn other words,\ndjango-denied makes authorization a requirement\nfor every view in a Django project.\n\n## Who should use this?\n\nThis package is well suited for Django projects\nthat need to protect pages against unauthorized access normally.\nIf you are making a service\nthat requires user's to login\nand restricts which data a user sees,\nthen django-denied may be a good fit for you.\n\nIf your web application is meant to be open\nfor a large audience,\nespecially with lots of anonymous users,\nthen this package may be overkill for your needs.\nA blog or content management system may not be a good candidate.\n\n## Install\n\nGet the package.\n\n```\npip install django-denied\n```\n\ndjango-denied uses Django's built-in `auth` and `admin` apps.\nThese apps also depend on the `contenttypes` app.\nEnsure that these apps are in your `INSTALL_APPS`\nin your Django settings file.\n\n```python\nINSTALLED_APPS = [\n    \"django.contrib.admin\",\n    \"django.contrib.auth\",\n    \"django.contrib.contenttypes\",\n    ...,\n]\n```\n\nAdd the `DeniedMiddleware`.\nThis middleware does all the authorization checking.\nThe middleware depends on the `request.user`,\nso be sure to include it *after* the `AuthenticationMiddleware`.\n\n```python\nMIDDLEWARE = [\n    ...,\n    \"django.contrib.auth.middleware.AuthenticationMiddleware\",\n    \"denied.middleware.DeniedMiddleware\",\n    ...,\n]\n```\n\nNow you're ready to start.\n\n## Usage\n\ndjango-denied has two primary modes\nfor handling views.\n\n1. `allow`\n2. `authorize`\n\nThese decorators are the main interface\nof the package\nand are described in the sections below.\n\nBy default,\ndjango-denied assumes that all users should be authenticated,\nwith the exception of allowed views or login pages.\n\nThe login pages are\n\n* The page defined by `settings.LOGIN_URL` and\n* The Django admin login defined at the `admin:login` route.\n\nIf you set `LOGIN_URL`,\ndjango-denied expects the path form\nof the setting\n(e.g., `/accounts/login/`)\nrather than the `url` name\n(e.g., `accounts:login`).\n\n### Allowing views\n\nEvery app is likely to have views\nthat should be made accessible to unauthenticated users.\nA company's about page, terms of service, and privacy policy are all good examples.\n\nThe `allow` decorator is for marking a Django view as exempt\nfrom the authorization checking done\nby the `DeniedMiddleware`.\n\nThis is an example of how you might create a terms of service view.\n\n```python\n# application/views.py\nfrom denied.decorators import allow\nfrom django.shortcuts import render\n\n\n@allow\ndef terms_of_service(request):\n    return render(request, \"tos.html\", {})\n```\n\nThe `allow` decorator has a secondary function.\nAside from allowing a single view,\nthe decorator can allow a set of views\nthat you would use with `django.urls.path`.\n\nThis is necessary to permit third party apps\nthat have other views,\nbut are unaware of the django-denied system.\n\nThis is an example of using `allow`\nto permit the Django admin views\nas well as the popular app,\n[django-allauth](https://django-allauth.readthedocs.io/en/latest/).\n\n```python\n# project/urls.py\nfrom denied.decorators import allow\nfrom django.contrib import admin\nfrom django.urls import include, path\n\nurlpatterns = [\n    path(\"accounts/\", allow(include(\"allauth.urls\"))),\n    path(\"admin/\", allow(admin.site.urls)),\n]\n```\n\nNote:\nEven if you include `allow` on a view or a set of views,\nthat does not mean that what you've allowed will suddenly\nbypass any existing authentication or authorization checking.\n***This is a feature, not a bug!***\n\n`login_required`, `permission_required`,\nand any other authentication or authorization checking\nthat pre-exist on views will remain.\n*django-denied does not disable the security features\nof other third party libraries.*\n\n## Authorizing views\n\nWith django-denied,\na Django view is authorized with the `authorize` decorator\nand an *authorizer* function.\nAn authorizer has a function signature of\n\n```python\nfrom django.http import HttpRequest\n\n\ndef example_authorizer(request: HttpRequest, **view_kwargs: dict) -> bool:\n    ...\n```\n\nThe authorizer evaluates the incoming request and view information\nand should return `True` if the request is authorized\nor `False` is the request is not authorized.\nThe `view_kwargs` include any data that was parsed out of the URL route.\n\nThe authorizer acts as a declarative way\nof showing what is authorized\nfor the view.\n\n```python\nfrom denied.decorators import authorize\n\nfrom .authorizers import example_authorizer\n\n\n@authorize(example_authorizer)\ndef example_view(request):\n    ...\n```\n\nTo use `authorize` on a class-based view,\nyou must attach the decorator to the `dispatch` method.\n\n```python\nfrom denied.decorators import authorize\nfrom django.utils.decorators import method_decorator\nfrom django.views.generic import DetailView\n\nfrom .authorizers import example_authorizer\nfrom .models import Example\n\n\n@method_decorator(authorize(example_authorizer), \"dispatch\")\nclass ExampleDetail(DetailView):\n    queryset = Example.objects.all()\n```\n### Built-in authorizers\n\nThe library includes built-in authorizers\nfor common cases.\n\n#### `denied.authorizers.any_authorized`\n\nThis authorizer always evaluates to `True` and is the logical equivalent\nto `login_required` since django-denied always enforces authentication checking.\n\n#### `denied.authorizers.staff_authorized`\n\nThis authorizer only permits access when `user.is_staff == True`.\n`staff_authorized` is equivalent to `staff_member_required`\nfrom the Django `admin` app.\n\n#### Authorizer example\n\nThis section shows a more complete example\nof an authorizer\nto give you a sense\nof how django-denied works in practice.\n\nFor our example,\nwe'll consider a project tracking application.\nThis is little more than a TODO list\nthat groups the tasks into projects.\n\nHere are the models.\n\n```python\n# application/models.py\nfrom django.contrib.auth.models import User\nfrom django.db import models\n\n\nclass Project(models.Model):\n    owner = models.ForeignKey(User, on_delete=models.CASCADE)\n\n\nclass Task(models.Model):\n    project = models.ForeignKey(Project, on_delete=models.CASCADE)\n    description = models.TextField()\n    completed = models.BooleanField(default=False)\n```\n\nFor this simple system,\nonly project owners can do anything\nwith a task.\nLet's create the authorizer for that.\n\n```python\n# application/authorizers.py\n\n\ndef task_authorized(request, **view_kwargs):\n    return Task.objects.filter(\n        project__owner=request.user,\n        pk=view_kwargs[\"pk\"],\n    ).exists()\n```\n\nThese are the URLs we want to support\nwith this authorizer.\n\n```python\n# application/urls.py\n\nfrom django.urls import path\n\nfrom .views import task_detail, task_edit\n\nurlpatterns = [\n    path(\"tasks/<int:pk>/\", task_detail, name=\"task_detail\"),\n    path(\"tasks/<int:pk>/edit/\", task_detail, name=\"task_edit\"),\n]\n```\n\nNow we can set our views\nand set their authorization.\n\n```python\n# application/views.py\nfrom denied.decorators import authorize\nfrom django.shortcuts import render\n\nfrom .authorizers import task_authorized\nfrom .models import Task\n\n\n@authorize(task_authorized)\ndef task_detail(request, pk):\n    task = Task.objects.get(pk=pk)\n    return render(request, \"task_detail.html\", {\"task\": task})\n\n\n@authorize(task_authorized)\ndef task_edit(request, pk):\n    task = Task.objects.get(pk=pk)\n    return render(request, \"task_edit.html\", {\"task\": task})\n```\n\nSince the authorizer handles the access control,\nwe can be confident that the task is safe to fetch\nby its key alone.\nAccess control is pushed to the boundary of the view\nso that the view's internal logic is about as simple\nas you can make it.\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "An authorization system based exclusively on allow lists",
    "version": "1.3",
    "project_urls": {
        "Homepage": "https://github.com/mblayman/django-denied"
    },
    "split_keywords": [
        "django"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "ecbf0dbbbfdf63e999992331d6b0360c698be903040472ad51a445d72a8b0813",
                "md5": "bd6ad6a9a780530e7337362a694c0942",
                "sha256": "3fc870443097e6911cedca89d6d19c4aa8e4f10b7a02e2627e177ad60b880096"
            },
            "downloads": -1,
            "filename": "django_denied-1.3-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "bd6ad6a9a780530e7337362a694c0942",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": null,
            "size": 7590,
            "upload_time": "2024-04-06T17:57:33",
            "upload_time_iso_8601": "2024-04-06T17:57:33.154947Z",
            "url": "https://files.pythonhosted.org/packages/ec/bf/0dbbbfdf63e999992331d6b0360c698be903040472ad51a445d72a8b0813/django_denied-1.3-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "4d5fe02af85f3369d7a569aca6f39956ed6273aabe9f80441232d561f6438b9e",
                "md5": "3ee27510c2e9bf5c24a1e45e86e8ea7e",
                "sha256": "130da88b74984c36fd8deef44e5cec7b759828c514f1fffcb541a12b1c9f96fa"
            },
            "downloads": -1,
            "filename": "django-denied-1.3.tar.gz",
            "has_sig": false,
            "md5_digest": "3ee27510c2e9bf5c24a1e45e86e8ea7e",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": null,
            "size": 11806,
            "upload_time": "2024-04-06T17:57:34",
            "upload_time_iso_8601": "2024-04-06T17:57:34.724645Z",
            "url": "https://files.pythonhosted.org/packages/4d/5f/e02af85f3369d7a569aca6f39956ed6273aabe9f80441232d561f6438b9e/django-denied-1.3.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-04-06 17:57:34",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "mblayman",
    "github_project": "django-denied",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "tox": true,
    "lcname": "django-denied"
}
        
Elapsed time: 0.20199s