Metadata-Version: 2.4
Name: django-middleaudit
Version: 0.2.0
Summary: Audit log for Django projects: a middleware applies configurable rules to every request and stores the results.
Author-email: Aníbal Pacheco <apacheco.uy@gmail.com>, pyspring <hello@pyspring.com>
License-Expression: MIT
Keywords: django,audit,audit-log,middleware
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.0
Classifier: Framework :: Django :: 5.1
Classifier: Framework :: Django :: 5.2
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Django>=4.2
Provides-Extra: dev
Requires-Dist: black; extra == "dev"
Requires-Dist: isort; extra == "dev"
Requires-Dist: mypy; extra == "dev"
Requires-Dist: pylint; extra == "dev"
Requires-Dist: pylint-django; extra == "dev"
Dynamic: license-file

# django-middleaudit

A reusable Django app that adds an audit log to your project. A middleware runs
every request through a set of *audit rules*; whenever a rule returns a result
(anything other than `None`), an audit log entry is stored in the database.

> **Status:** under active development, working towards a first minimal release
> on PyPI.

## How it works

1. You add the `middleaudit` middleware to your project.
2. On each request, the middleware applies the configured audit rules.
3. Each rule receives the request and returns either `None` (nothing to audit)
   or a result describing what happened.
4. Every non-`None` result is persisted as an audit log entry.

Rules are plain classes with a well-known interface, so writing your own is
trivial: subclass the provided base class, implement one method, and add the
rule to your settings.

The app ships with a default rule set, which the project settings can override
or extend.

## Installation

```bash
pip install django-middleaudit
```

Add the app and the middleware to your settings:

```python
INSTALLED_APPS = [
    # ...
    "middleaudit",
]

MIDDLEWARE = [
    # ...
    "middleaudit.middleware.MiddleauditMiddleware",
]
```

The middleware must be placed after
`django.contrib.auth.middleware.AuthenticationMiddleware`, so rules can
inspect `request.user`.

Run the migrations:

```bash
python manage.py migrate middleaudit
```

Optionally, include the audit log view in your URLconf:

```python
urlpatterns = [
    # ...
    path("audit-log/", include("middleaudit.urls")),
]
```

## The audit log view

The app provides a view, restricted to staff users by default (see
`MIDDLEAUDIT_AUTH_ACCESS` below), that lists the audit log entries from most
recent to oldest, paginated (page size defaults to Django admin's default,
100 entries per page).

### Linking to the view

The `middleaudit_url` template tag returns the URL of the audit log view when
the current user passes the access check, and an empty string otherwise, so
navigation templates can show the link only to users who may follow it:

```html
{% load middleaudit_tags %}

{% middleaudit_url as audit_url %}
{% if audit_url %}<a href="{{ audit_url }}">Audit log</a>{% endif %}
```

## Rules

A rule is a class with a known attribute and method:

```python
from middleaudit.rules import AuditRule


class AccessRule(AuditRule):
    """The default rule: logs every authenticated request."""

    name = "access"

    def apply(self, request):
        if not request.user.is_authenticated:
            return None
        return (request.user, f"accessed URL {request.path}")
```

- `name` identifies the rule in the audit log entries it produces.
- `apply(request)` returns `None` when there is nothing to audit, or a result
  tuple that becomes an audit log entry.

### Including and excluding URLs

Every rule can restrict the URLs it applies to through two class attributes,
both lists of regex patterns matched against the request path (with
`re.search`, like Django's `IGNORABLE_404_URLS`):

- `include_urls`: paths the rule applies to. An empty list (the default)
  means *every* path.
- `exclude_urls`: paths the rule skips. An empty list (the default) means
  none. Excludes win over includes.

```python
class ApiAccessRule(AccessRule):
    include_urls = [r"^/api/"]
    exclude_urls = [r"^/api/health/"]
```

The URL filters are applied twice: the middleware never audits requests the
rule does not match, and the audit log view also hides *already stored*
entries whose path the current filters of their rule would not match, so
tightening a rule retroactively cleans the listing (see
`MIDDLEAUDIT_HIDE_UNMATCHED` below).

`AccessRule` excludes `/favicon.ico` and static and media files by default,
deriving the file patterns from your `STATIC_URL` and `MEDIA_URL` settings. Subclasses that set
`exclude_urls` *add* to those defaults rather than replace them. Note that in
production static and media files are usually served by the web server
directly, never reaching Django; this default mostly matters when running
under `runserver` in development.

Rules that need dynamic patterns can override `get_include_urls()` /
`get_exclude_urls()` instead of the class attributes (that is how
`AccessRule` reads `STATIC_URL` and `MEDIA_URL`).

### Configuring the rule set

The default rule set can be overridden from your project settings with a list
of dotted paths:

```python
MIDDLEAUDIT_RULES = [
    "middleaudit.rules.AccessRule",
    "myproject.audit.MyCustomRule",
]
```

## Settings

Every setting this app reads is prefixed with `MIDDLEAUDIT_`.

- `MIDDLEAUDIT_RULES`: list of dotted paths to the audit rule classes to
  apply. Defaults to the app's built-in rule set.
- `MIDDLEAUDIT_PAGE_SIZE`: page size of the audit log view. Defaults to
  Django admin's default page size (100).

  ```python
  MIDDLEAUDIT_PAGE_SIZE = 50
  ```

- `MIDDLEAUDIT_AUTH_ACCESS`: dotted path to a callable deciding who may
  access the audit log view. It receives the user and returns a boolean.
  Defaults to `"middleaudit.access.is_staff"` (active staff users); the app
  also ships `"middleaudit.access.is_admin"` (active superusers) ready to
  use, or point it at your own callable:

  ```python
  MIDDLEAUDIT_AUTH_ACCESS = "middleaudit.access.is_admin"
  ```

  The `middleaudit_url` template tag applies the same check, so the whole
  app follows this setting consistently.

- `MIDDLEAUDIT_HIDE_UNMATCHED`: whether the audit log view hides stored
  entries whose path the *current* URL filters of the rule that created them
  (matched by rule name) would not match — because it is excluded, or
  because the rule now has `include_urls` and the path falls outside them.
  Defaults to `True`, so tightening a rule retroactively cleans the listing
  without deleting anything; set it to `False` to always list every stored
  entry. Entries from rules no longer configured are always listed.

  ```python
  MIDDLEAUDIT_HIDE_UNMATCHED = False
  ```

  The patterns are evaluated by the database's regex operator, which for the
  simple prefix-style patterns used here behaves like Python's `re.search`.

- `MIDDLEAUDIT_BASE_TEMPLATE`: name of the base template the audit log view
  extends, so the listing integrates with your project's look and feel.
  Defaults to `"middleaudit/base.html"`, a minimal standalone base shipped
  with the app.

  ```python
  MIDDLEAUDIT_BASE_TEMPLATE = "myproject/base.html"
  ```

  The only requirement is that the template defines a `content` block, where
  the audit log table is rendered. A minimal valid base template looks like
  this:

  ```html
  <!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="utf-8">
    <title>My project</title>
  </head>
  <body>
    {% block content %}{% endblock %}
  </body>
  </html>
  ```

- `MIDDLEAUDIT_HEADER_TEMPLATE`: name of the template rendered above the
  audit log table. Defaults to `"middleaudit/header.html"`, a simple `<h1>`
  heading. Override it to
  integrate the heading area with your project (e.g. breadcrumbs plus a
  title) without having to override the whole listing template — which is
  the one most likely to change between versions of this app.

  ```python
  MIDDLEAUDIT_HEADER_TEMPLATE = "myproject/audit_log_header.html"
  ```

- `MIDDLEAUDIT_LIST_TEMPLATE`: name of the listing template itself, for
  customizations that go beyond the base and header templates. Defaults to
  `"middleaudit/auditlogentry_list.html"`. The named template can extend the
  default one and override any of its blocks:

  ```python
  MIDDLEAUDIT_LIST_TEMPLATE = "myproject/audit_log.html"
  ```

  ```html
  {% extends "middleaudit/auditlogentry_list.html" %}

  {% block content %}
  <div class="my-container">{{ block.super }}</div>
  {% endblock %}

  {% block middleaudit_table %}
  {# render the entries in page_obj your own way #}
  {% endblock %}
  ```

  The default listing template wraps each of its parts in a block, so you can
  replace them individually while keeping the rest:

  - `middleaudit_header`: the include of the header template.
  - `middleaudit_table`: the audit log table.
  - `middleaudit_pagination`: the previous/next pagination links.

  The table and the pagination paragraph carry the `middleaudit-table` and
  `middleaudit-pagination` CSS classes, so restyling them usually takes no
  template override at all.

  Since your template extends the app's, you can also override blocks defined
  by *your* base template (page title, container classes, extra CSS...) on the
  audit log page only.

The base and header template settings are only needed when you want to point at a template
with a *different* name. Since the defaults are looked up through Django's
normal template loaders, your project can also override them without touching
settings by shadowing them: a `middleaudit/header.html` (or
`middleaudit/base.html`) in one of your `TEMPLATES` `DIRS` takes precedence
over the copies shipped with the app.

## Roadmap

- Response-aware rules. Some actions can only be asserted once the response
  is known: "user X deleted something" is only true if the delete request
  succeeded (e.g. returned a 2xx status). Rules will optionally be able to
  inspect the response, with the middleware applying them after the view has
  run. The basic access rule does not need this — the access happens
  regardless of the outcome.
- View decorators to trigger a specific rule directly, for views whose intent
  is already known:

  ```python
  @middleaudit(rule_name="delete_something")
  def delete_view(request, pk):
      ...
  ```

## Development

Run the test suite:

```bash
django-admin test middleaudit --settings=test_settings --pythonpath=. --failfast
```

Format, type-check and lint the code:

```bash
./lint.sh        # check only
./lint.sh fix    # apply isort/black fixes, then run checkers
```

## License

MIT — see [LICENSE](LICENSE).
