authzlock 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. authzlock-0.1.0/.gitignore +25 -0
  2. authzlock-0.1.0/.pre-commit-config.yaml +8 -0
  3. authzlock-0.1.0/.pre-commit-hooks.yaml +23 -0
  4. authzlock-0.1.0/CHANGELOG.md +42 -0
  5. authzlock-0.1.0/CONTRIBUTING.md +144 -0
  6. authzlock-0.1.0/LICENSE +21 -0
  7. authzlock-0.1.0/PKG-INFO +357 -0
  8. authzlock-0.1.0/README.md +310 -0
  9. authzlock-0.1.0/action.yml +131 -0
  10. authzlock-0.1.0/noxfile.py +66 -0
  11. authzlock-0.1.0/pyproject.toml +102 -0
  12. authzlock-0.1.0/src/authzlock/__init__.py +5 -0
  13. authzlock-0.1.0/src/authzlock/__main__.py +7 -0
  14. authzlock-0.1.0/src/authzlock/_dump.py +29 -0
  15. authzlock-0.1.0/src/authzlock/_github.py +201 -0
  16. authzlock-0.1.0/src/authzlock/classify.py +370 -0
  17. authzlock-0.1.0/src/authzlock/cli.py +245 -0
  18. authzlock-0.1.0/src/authzlock/diff.py +218 -0
  19. authzlock-0.1.0/src/authzlock/django_loader.py +66 -0
  20. authzlock-0.1.0/src/authzlock/errors.py +23 -0
  21. authzlock-0.1.0/src/authzlock/extract/__init__.py +44 -0
  22. authzlock-0.1.0/src/authzlock/extract/custom.py +80 -0
  23. authzlock-0.1.0/src/authzlock/extract/decorators.py +229 -0
  24. authzlock-0.1.0/src/authzlock/extract/drf.py +95 -0
  25. authzlock-0.1.0/src/authzlock/extract/methods.py +51 -0
  26. authzlock-0.1.0/src/authzlock/extract/scoping.py +69 -0
  27. authzlock-0.1.0/src/authzlock/extract/source.py +43 -0
  28. authzlock-0.1.0/src/authzlock/extract/urls.py +63 -0
  29. authzlock-0.1.0/src/authzlock/gitutil.py +85 -0
  30. authzlock-0.1.0/src/authzlock/lockfile.py +214 -0
  31. authzlock-0.1.0/src/authzlock/model.py +97 -0
  32. authzlock-0.1.0/src/authzlock/py.typed +0 -0
  33. authzlock-0.1.0/src/authzlock/render.py +297 -0
@@ -0,0 +1,25 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ build/
6
+ dist/
7
+ .eggs/
8
+
9
+ # Environments
10
+ .venv/
11
+ venv/
12
+ .nox/
13
+ .tox/
14
+
15
+ # Tooling caches
16
+ .pytest_cache/
17
+ .mypy_cache/
18
+ .ruff_cache/
19
+ .coverage
20
+ htmlcov/
21
+
22
+ # Editors and OS
23
+ .idea/
24
+ .vscode/
25
+ .DS_Store
@@ -0,0 +1,8 @@
1
+ # Hooks for developing authzlock itself. Install once with `pre-commit install`.
2
+ repos:
3
+ - repo: https://github.com/astral-sh/ruff-pre-commit
4
+ rev: v0.16.9
5
+ hooks:
6
+ - id: ruff-check
7
+ args: [--fix]
8
+ - id: ruff-format
@@ -0,0 +1,23 @@
1
+ # Hooks that other repositories can use to keep their authz.lock current.
2
+ # Usage and configuration: docs/pre-commit.md.
3
+ #
4
+ # pre-commit runs hooks from the repository root. The entry `python -m authzlock` gets the
5
+ # current directory on sys.path from the interpreter; the `authzlock` script would work as
6
+ # well, since authzlock puts the current directory first on sys.path itself. Either way the
7
+ # project's settings module can be imported without setting PYTHONPATH.
8
+ - id: authzlock-check
9
+ name: authzlock check
10
+ description: Fail when authz.lock does not match the Django project's access rules.
11
+ entry: python -m authzlock check
12
+ language: python
13
+ pass_filenames: false
14
+ always_run: true
15
+ require_serial: true
16
+ - id: authzlock-update
17
+ name: authzlock update
18
+ description: Rewrite authz.lock from the Django project; fails when the file changed.
19
+ entry: python -m authzlock update
20
+ language: python
21
+ pass_filenames: false
22
+ always_run: true
23
+ require_serial: true
@@ -0,0 +1,42 @@
1
+ # Changelog
2
+
3
+ All notable changes to authzlock are recorded here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-10-02
10
+
11
+ First release.
12
+
13
+ ### Added
14
+
15
+ - Package skeleton with the `authzlock` command and `authzlock --version`.
16
+ - Loading a Django project from `DJANGO_SETTINGS_MODULE`, with plain error messages when the
17
+ settings module is missing or fails to import.
18
+ - Support for Python 3.10 to 3.13, Django 4.2, 5.1, 5.2, 6.0 and 6.1, and Django REST
19
+ Framework 3.14 and newer, tested in CI on every pull request.
20
+ - Release workflow that publishes to PyPI from a version tag using trusted publishing.
21
+ - `authzlock update` writes the project's access rules to `authz.lock`, a sorted YAML file
22
+ with schema version 1, and `authzlock check` exits 1 with a readable report when the
23
+ project and the lockfile differ. See `docs/lockfile.md` and `docs/cli.md`.
24
+ - pre-commit hooks `authzlock-check` and `authzlock-update`. See `docs/pre-commit.md`.
25
+ - `authzlock diff --base <ref>`: compares the lockfile committed at a git ref with the
26
+ current project and labels each route `loosened`, `tightened`, `added`, `removed` or
27
+ `changed-unknown`, as text or as markdown for a pull request comment, with
28
+ `--fail-on loosened` to fail only on loosened routes.
29
+ - GitHub Action (`smhasan94/authzlock@v1`): runs `authzlock diff` on a pull request, keeps one
30
+ comment with the changes up to date across pushes, and fails the job when a route is
31
+ loosened (`fail-on-loosened`, default true). See `docs/github-action.md`.
32
+ - Reference documentation indexed in `docs/index.md`, including
33
+ `docs/heuristics-and-limits.md` on what the heuristics cannot tell you, and a contributor
34
+ guide, `CONTRIBUTING.md`.
35
+
36
+ ### Fixed
37
+
38
+ - `authzlock` now declares Django 4.2 or newer as a dependency, so `pip install authzlock`
39
+ followed by `authzlock --version` works in a fresh environment. DRF stays optional.
40
+ - The `authzlock` command now finds the project's settings module when run from the project
41
+ root without `PYTHONPATH`: the current directory is put first on the import path, as
42
+ `manage.py` and `python -m authzlock` do.
@@ -0,0 +1,144 @@
1
+ # Contributing to authzlock
2
+
3
+ This page covers the development setup, the checks a change must pass, the branch and commit
4
+ conventions, and how to add a fixture project. [DEVELOPMENT.md](DEVELOPMENT.md) holds the
5
+ project brief and the same commands; `tests/test_docs.py` fails when the shell commands on
6
+ the two pages differ, so change both together. [docs/index.md](docs/index.md) lists the
7
+ reference pages.
8
+
9
+ ## Setup
10
+
11
+ Python 3.10 or newer and git are needed. Set up a dev environment once:
12
+
13
+ ```sh
14
+ python -m venv .venv && source .venv/bin/activate
15
+ pip install -e ".[dev]"
16
+ pip install nox
17
+ ```
18
+
19
+ ## Checks
20
+
21
+ Lint and format check (what CI runs):
22
+
23
+ ```sh
24
+ nox -s lint
25
+ # equivalent to:
26
+ ruff check . && ruff format --check .
27
+ ```
28
+
29
+ Auto-fix formatting and lint:
30
+
31
+ ```sh
32
+ ruff format . && ruff check --fix .
33
+ ```
34
+
35
+ Type check (mypy strict on the package):
36
+
37
+ ```sh
38
+ nox -s typecheck
39
+ # equivalent to:
40
+ mypy src/authzlock
41
+ ```
42
+
43
+ Tests, full matrix (Python 3.10-3.13 x Django 4.2/5.x):
44
+
45
+ ```sh
46
+ nox -s tests
47
+ ```
48
+
49
+ Tests, one interpreter and Django version:
50
+
51
+ ```sh
52
+ nox -s "tests(python='3.12', django='5.2')"
53
+ # or directly in the active venv:
54
+ pytest
55
+ ```
56
+
57
+ Tests against the oldest supported DRF (3.14, on Python 3.12 and Django 4.2):
58
+
59
+ ```sh
60
+ nox -s tests_min_drf
61
+ ```
62
+
63
+ Run everything CI runs:
64
+
65
+ ```sh
66
+ nox
67
+ ```
68
+
69
+ Session names are defined in `noxfile.py`. CI runs the same sessions on every pull request:
70
+ `lint`, `typecheck`, eleven `tests (python=X, django=Y)` cells and the DRF 3.14 cell, plus
71
+ the GitHub Action end-to-end jobs described in
72
+ [docs/github-action.md](docs/github-action.md#testing-the-action).
73
+
74
+ ## Branches, commits and pull requests
75
+
76
+ - Work happens on short-lived branches off `main`. A branch is named after its Linear
77
+ ticket: the lowercase identifier, a hyphen and a short slug of the title, for example
78
+ `sha-12-url-resolver-walk`.
79
+ - Commits use conventional prefixes: `feat:`, `fix:`, `docs:`, `chore:`, `test:`, `ci:`.
80
+ - Tests come first. Each test carries the ticket's T-case id in its name, for example
81
+ `test_t3_missing_module_raises_project_load_error`, and is committed before the change
82
+ that makes it pass.
83
+ - Every code change goes through a pull request against `main`. It is merged once CI passes
84
+ and it has been reviewed. `docs/plans` and `docs/backlog.md` may be committed to `main`
85
+ directly.
86
+ - A change is done when every T-case of its ticket passes in CI, ruff and mypy pass and the
87
+ docs are updated.
88
+ - Add a line to the `## [Unreleased]` section of [CHANGELOG.md](CHANGELOG.md) for any change
89
+ a user would notice.
90
+
91
+ Keep these rules from [DEVELOPMENT.md](DEVELOPMENT.md#working-rules) in mind: classification
92
+ stays conservative (when unsure, `changed-unknown`), the package makes no network calls,
93
+ lockfile output stays deterministic, and docs and messages use plain language.
94
+
95
+ ## Adding a fixture project
96
+
97
+ Fixture projects under `tests/fixtures/` are small Django projects, one per view style.
98
+ [tests/fixtures/README.md](tests/fixtures/README.md) describes their layout and how tests
99
+ use them. To add one:
100
+
101
+ 1. Create `tests/fixtures/<style>/` with `settings.py` (`ROOT_URLCONF`, `INSTALLED_APPS`,
102
+ and `REST_FRAMEWORK` defaults where DRF is used), `urls.py` and one app package holding
103
+ only the views the tests assert on. The settings module is always `settings`.
104
+ 2. Write the tests first, using `run_extract("<style>")` from `tests/harness.py`. It runs
105
+ the extraction in a separate process, because Django can only be set up once per
106
+ process.
107
+ 3. Check the CLI against it from the repository root:
108
+
109
+ ```sh
110
+ DJANGO_SETTINGS_MODULE=settings PYTHONPATH=tests/fixtures/drf_viewsets \
111
+ authzlock check --lockfile tests/fixtures/drf_viewsets/authz.lock.expected
112
+ ```
113
+
114
+ (with your fixture's directory in place of `drf_viewsets`; for a new fixture, use
115
+ `authzlock update` with the same options to write its first lockfile).
116
+ 4. To have its lockfile compared byte for byte in every CI cell, add the fixture's name to
117
+ `DETERMINISM_FIXTURES` in `tests/test_determinism.py` and write its golden lockfile,
118
+ `authz.lock.expected`:
119
+
120
+ ```sh
121
+ pytest tests/test_determinism.py -k t5 --update-golden
122
+ ```
123
+
124
+ This rewrites the most specific golden file that exists for the installed Django and
125
+ DRF. If DRF 3.14 builds different routes for the fixture (the `tests_min_drf` session
126
+ fails), create `authz.lock.drf-3.14.expected` as a copy of `authz.lock.expected` and
127
+ rewrite it with `nox -s tests_min_drf -- tests/test_determinism.py -k t5 --update-golden`.
128
+ Read the generated files before committing them; they are the expected output from then
129
+ on.
130
+ 5. Describe the fixture under "Current fixtures" in `tests/fixtures/README.md`.
131
+
132
+ ## Generated docs
133
+
134
+ The output blocks in [docs/scenario.md](docs/scenario.md) and the `diff` output in
135
+ [README.md](README.md) are produced by the tests, which fail when they are stale. After a
136
+ change to the output, rewrite them and review the diff:
137
+
138
+ ```sh
139
+ pytest tests/test_scenario.py tests/test_readme.py --update-docs
140
+ ```
141
+
142
+ `tests/test_docs.py` also checks that [docs/cli.md](docs/cli.md) lists exactly the options
143
+ in `--help`, that relative links resolve and that no `authzlock` command in a code block uses
144
+ an option that does not exist. Update the docs in the same pull request as the change.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sharukh Hasan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,357 @@
1
+ Metadata-Version: 2.5
2
+ Name: authzlock
3
+ Version: 0.1.0
4
+ Summary: An authorization lockfile for Django and Django REST Framework.
5
+ Project-URL: Homepage, https://github.com/smhasan94/authzlock
6
+ Project-URL: Issues, https://github.com/smhasan94/authzlock/issues
7
+ Author: Sharukh Hasan
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: authorization,django,drf,lockfile,permissions,security
11
+ Classifier: Development Status :: 2 - Pre-Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Framework :: Django
14
+ Classifier: Framework :: Django :: 4.2
15
+ Classifier: Framework :: Django :: 5.1
16
+ Classifier: Framework :: Django :: 5.2
17
+ Classifier: Framework :: Django :: 6.0
18
+ Classifier: Framework :: Django :: 6.1
19
+ Classifier: Intended Audience :: Developers
20
+ Classifier: License :: OSI Approved :: MIT License
21
+ Classifier: Operating System :: OS Independent
22
+ Classifier: Programming Language :: Python :: 3
23
+ Classifier: Programming Language :: Python :: 3.10
24
+ Classifier: Programming Language :: Python :: 3.11
25
+ Classifier: Programming Language :: Python :: 3.12
26
+ Classifier: Programming Language :: Python :: 3.13
27
+ Classifier: Topic :: Security
28
+ Classifier: Topic :: Software Development :: Quality Assurance
29
+ Classifier: Typing :: Typed
30
+ Requires-Python: >=3.10
31
+ Requires-Dist: django>=4.2
32
+ Requires-Dist: pyyaml>=6
33
+ Requires-Dist: typer>=0.12
34
+ Provides-Extra: dev
35
+ Requires-Dist: build>=1.2; extra == 'dev'
36
+ Requires-Dist: django>=4.2; extra == 'dev'
37
+ Requires-Dist: djangorestframework>=3.14; extra == 'dev'
38
+ Requires-Dist: drf-nested-routers>=0.93; extra == 'dev'
39
+ Requires-Dist: hatchling>=1.25; extra == 'dev'
40
+ Requires-Dist: mypy>=1.11; extra == 'dev'
41
+ Requires-Dist: nox>=2024.4; extra == 'dev'
42
+ Requires-Dist: pre-commit>=3.8; extra == 'dev'
43
+ Requires-Dist: pytest>=8; extra == 'dev'
44
+ Requires-Dist: ruff==0.16.9; extra == 'dev'
45
+ Requires-Dist: types-pyyaml; extra == 'dev'
46
+ Description-Content-Type: text/markdown
47
+
48
+ # authzlock
49
+
50
+ [![CI](https://github.com/smhasan94/authzlock/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/smhasan94/authzlock/actions/workflows/ci.yml)
51
+ [![PyPI](https://img.shields.io/pypi/v/authzlock.svg)](https://pypi.org/project/authzlock/)
52
+
53
+ An authorization lockfile for Django and Django REST Framework.
54
+
55
+ In a Django project, the rules for who may call which endpoint are spread across
56
+ `permission_classes` on views, `DEFAULT_PERMISSION_CLASSES` in settings, `get_permissions()`
57
+ and `get_queryset()` overrides, and decorators such as `login_required`. A pull request can
58
+ make an endpoint reachable by more users with a one-line change, and the reviewer sees the
59
+ line, not the effect. authzlock reads those rules from the code, writes them for every route
60
+ to a committed file, `authz.lock`, and on each pull request reports which routes changed and
61
+ whether a change made access looser.
62
+
63
+ **Status: pre-release.** The first release, 0.1.0, is not published yet. Until it is,
64
+ `pip install authzlock`, the `smhasan94/authzlock@v1` action tag and the `v0.1.0` pre-commit
65
+ rev below do not resolve; install from GitHub instead, as shown under Install.
66
+
67
+ ## Install
68
+
69
+ ```sh
70
+ pip install authzlock
71
+ ```
72
+
73
+ Install it into the same environment as your project, because authzlock imports your
74
+ settings and URL configuration. Django 4.2 or newer is installed with it if missing; Django REST
75
+ Framework is optional and only needed if your project uses it. Before the first release, install from the repository:
76
+
77
+ ```sh
78
+ pip install git+https://github.com/smhasan94/authzlock
79
+ ```
80
+
81
+ ## Quickstart
82
+
83
+ Run these from your project root, the directory that holds `manage.py`, inside a git
84
+ repository. Replace `mysite.settings` with your settings module.
85
+
86
+ ```sh quickstart
87
+ export DJANGO_SETTINGS_MODULE=mysite.settings
88
+ authzlock update
89
+ git add authz.lock
90
+ git commit -m "Add authz.lock"
91
+ authzlock check
92
+ ```
93
+
94
+ `update` writes `authz.lock` and prints how many routes it recorded. `check` prints
95
+ `authz.lock: up to date` and exits 0.
96
+
97
+ Now change an access rule. In the example project used to test this README, an `@action`
98
+ that only admins may call is opened to every signed-in user:
99
+
100
+ ```diff
101
+ - @action(detail=True, methods=["post"], permission_classes=[IsAdminUser])
102
+ + @action(detail=True, methods=["post"], permission_classes=[IsAuthenticated])
103
+ def archive(self, request: Request, pk: Any = None) -> Response:
104
+ ```
105
+
106
+ ```sh quickstart
107
+ authzlock check # exits 1
108
+ authzlock diff --base HEAD # exits 1
109
+ ```
110
+
111
+ `check` exits 1 and lists the routes whose rules no longer match `authz.lock`. `diff` compares
112
+ the lockfile committed at `HEAD` with the code as it is now and labels each change. The
113
+ action is served by three routes, and all three are `loosened`, because `IsAuthenticated`
114
+ lets in more users than `IsAdminUser`:
115
+
116
+ <!-- readme-diff:start -->
117
+ ```text
118
+ loosened POST ^invoices/(?P<pk>[^/.]+)/archive/$ -> billing.views.InvoiceViewSet permission_classes: [rest_framework.permissions.IsAdminUser] -> [rest_framework.permissions.IsAuthenticated] (R1: strongest built-in rest_framework.permissions.IsAdminUser -> rest_framework.permissions.IsAuthenticated)
119
+ loosened POST ^invoices/(?P<pk>[^/.]+)/archive\.(?P<format>[a-z0-9]+)/?$ -> billing.views.InvoiceViewSet permission_classes: [rest_framework.permissions.IsAdminUser] -> [rest_framework.permissions.IsAuthenticated] (R1: strongest built-in rest_framework.permissions.IsAdminUser -> rest_framework.permissions.IsAuthenticated)
120
+ loosened POST v2/^invoices/(?P<pk>[^/.]+)/archive/$ -> billing.views.InvoiceViewSet permission_classes: [rest_framework.permissions.IsAdminUser] -> [rest_framework.permissions.IsAuthenticated] (R1: strongest built-in rest_framework.permissions.IsAdminUser -> rest_framework.permissions.IsAuthenticated)
121
+
122
+ 3 loosened, 0 tightened, 0 added, 0 removed, 0 changed-unknown
123
+ ```
124
+ <!-- readme-diff:end -->
125
+
126
+ `diff` exits 1 whenever something changed; with `--fail-on loosened` it exits 1 only when a
127
+ route is loosened. To accept the change, record it and commit it with the code:
128
+
129
+ ```sh quickstart
130
+ authzlock update
131
+ git commit -am "Let signed-in users archive invoices"
132
+ authzlock check
133
+ ```
134
+
135
+ `check` exits 0 again. In CI, run `check` to make sure `authz.lock` is current, and `diff`
136
+ against the target branch to see what a pull request changes; the GitHub Action below does
137
+ the second. [docs/scenario.md](docs/scenario.md) walks through a longer example.
138
+
139
+ Exit codes for every command: 0 success, 1 the lockfile and the code differ, 2 error (the
140
+ project could not be loaded, the lockfile could not be read, or git failed).
141
+
142
+ ## The lockfile
143
+
144
+ `authz.lock` is YAML, sorted and stable, so the same code always gives the same bytes and a
145
+ review diff shows only real changes. Do not edit it by hand; run `authzlock update`. Two of
146
+ the routes from the example project:
147
+
148
+ ```yaml
149
+ schema_version: 1
150
+ routes:
151
+ - path: ^customers/$
152
+ name: customer-list
153
+ view: billing.views.CustomerViewSet
154
+ methods:
155
+ - GET
156
+ actions:
157
+ GET: list
158
+ permission_classes:
159
+ - rest_framework.permissions.IsAuthenticated
160
+ permission_source: settings-default
161
+ authentication_classes:
162
+ - rest_framework.authentication.SessionAuthentication
163
+ authentication_source: settings-default
164
+ django_auth: null
165
+ object_scoping:
166
+ get_object:
167
+ overridden: false
168
+ references_request_user: null
169
+ get_queryset:
170
+ overridden: false
171
+ references_request_user: null
172
+ perform_create:
173
+ overridden: false
174
+ references_request_user: null
175
+ - path: ^invoices/(?P<pk>[^/.]+)/archive/$
176
+ name: invoice-archive
177
+ view: billing.views.InvoiceViewSet
178
+ methods:
179
+ - POST
180
+ actions:
181
+ POST: archive
182
+ permission_classes:
183
+ - rest_framework.permissions.IsAdminUser
184
+ permission_source: action
185
+ authentication_classes:
186
+ - rest_framework.authentication.SessionAuthentication
187
+ authentication_source: settings-default
188
+ django_auth: null
189
+ object_scoping:
190
+ get_object:
191
+ overridden: false
192
+ references_request_user: null
193
+ get_queryset:
194
+ overridden: false
195
+ references_request_user: null
196
+ perform_create:
197
+ overridden: false
198
+ references_request_user: null
199
+ custom_permissions: {}
200
+ ```
201
+
202
+ Each route records its path, URL name, view, HTTP methods, the effective DRF permission and
203
+ authentication classes and where they come from (`view`, `action` or `settings-default`),
204
+ Django decorators for plain views, and whether the view overrides the hooks that scope
205
+ objects to the caller. A `get_permissions()` override is recorded as `dynamic`. Custom
206
+ permission classes are listed under `custom_permissions` with their docstring and the
207
+ routes that use them. [docs/lockfile.md](docs/lockfile.md) describes every key.
208
+
209
+ ## How changes are classified
210
+
211
+ `authzlock diff` gives every route that differs one label: `added`, `removed`, `loosened`,
212
+ `tightened` or `changed-unknown`. For a route on both sides, the rules below run in order and
213
+ the first that applies decides. Only DRF's built-in classes are ranked (`AllowAny` <
214
+ `IsAuthenticatedOrReadOnly` < `IsAuthenticated` < `IsAdminUser`), and Django decorators
215
+ (none < `login_required` < `permission_required`). A false `loosened` alarm is worse than
216
+ `changed-unknown`, so authzlock never guesses what a custom, composed or `dynamic`
217
+ permission does.
218
+
219
+ | Rule | Applies when | Label | Example |
220
+ |------|--------------|-------|---------|
221
+ | R1 | Both lists hold only ranked built-ins and the strongest class differs | `loosened` if the strongest class is weaker, `tightened` if stronger | `[IsAuthenticated]` to `[AllowAny]` is `loosened` |
222
+ | R2 | An unranked class is removed, the new list holds no unranked class, and the new strongest built-in is at most `IsAuthenticated` or at most the old strongest built-in | `loosened` | `[shop.permissions.IsOwner]` to `[IsAuthenticated]` is `loosened` |
223
+ | R3 | An unranked class is replaced by another unranked class, or by `IsAdminUser` | `changed-unknown` | `[shop.permissions.IsOwner]` to `[IsAdminUser]` is `changed-unknown` |
224
+ | R4 | An unranked class is added and every old entry is kept | `tightened` | `[IsAuthenticated]` to `[IsAuthenticated, shop.permissions.IsOwner]` is `tightened` |
225
+ | R5 | Either side of a changed field is `dynamic` | `changed-unknown` | `dynamic` to `[IsAuthenticated]` is `changed-unknown` |
226
+ | R6 | Only `django_auth.login_required` or `django_auth.permission_required` changed | `tightened` when the Django auth rank rises or `permission_required` gains entries; `loosened` for the reverse | `login_required` true to false is `loosened` |
227
+ | R7 | Only `methods` (and `actions`) changed and `methods` gained entries | `changed-unknown` | `[DELETE, GET]` to `[DELETE, GET, PUT]` is `changed-unknown` |
228
+ | R8 | Anything else | `changed-unknown` | `[(IsAuthenticated \| shop.permissions.IsOwner)]` to `[IsAuthenticated]` is `changed-unknown` |
229
+
230
+ R1 to R4 apply only when `permission_classes` is the one access field that changed.
231
+ [docs/classification.md](docs/classification.md) has the full rules, examples and the
232
+ cases that are `changed-unknown` on purpose.
233
+
234
+ ## GitHub Action
235
+
236
+ The action runs `authzlock diff` on a pull request against its base branch, posts the result
237
+ as one comment that it updates on every push, and fails the job when a route is loosened.
238
+ Save this as `.github/workflows/authzlock.yml`:
239
+
240
+ ```yaml
241
+ name: authzlock
242
+
243
+ on:
244
+ pull_request:
245
+
246
+ permissions:
247
+ contents: read
248
+ pull-requests: write
249
+
250
+ jobs:
251
+ authzlock:
252
+ runs-on: ubuntu-latest
253
+ steps:
254
+ - uses: actions/checkout@v4
255
+ - uses: actions/setup-python@v5
256
+ with:
257
+ python-version: "3.12"
258
+ - run: pip install -r requirements.txt
259
+ - uses: smhasan94/authzlock@v1
260
+ with:
261
+ settings-module: mysite.settings
262
+ python-version: "3.12"
263
+ ```
264
+
265
+ Install your project's dependencies before the action, in the Python version you give it.
266
+ Inputs, outputs, fork pull requests and how to also run `check` are in
267
+ [docs/github-action.md](docs/github-action.md).
268
+
269
+ ## pre-commit
270
+
271
+ The `authzlock-check` hook fails a commit when `authz.lock` does not match the code. Add this
272
+ to `.pre-commit-config.yaml` and run `pre-commit install`:
273
+
274
+ ```yaml
275
+ repos:
276
+ - repo: https://github.com/smhasan94/authzlock
277
+ rev: v0.1.0
278
+ hooks:
279
+ - id: authzlock-check
280
+ args: ["--settings", "mysite.settings"]
281
+ additional_dependencies:
282
+ - "django==5.2.*"
283
+ - "djangorestframework==3.16.*"
284
+ ```
285
+
286
+ The hook runs in its own virtualenv, so list every package your settings and URL
287
+ configuration import under `additional_dependencies`, pinned like your project. The
288
+ `authzlock-update` hook and the details are in [docs/pre-commit.md](docs/pre-commit.md).
289
+
290
+ ## What authzlock does not do
291
+
292
+ - It does not support FastAPI or any framework other than Django and Django REST Framework.
293
+ - It does not prove that custom permission logic is correct. A custom class such as
294
+ `IsOwner` is recorded by name with its docstring; its code is never run or judged.
295
+ - It does not do black-box testing of a running app. It reads code and settings, sends no
296
+ HTTP requests and needs no users, roles file or seeded data.
297
+ - It does not generate tests from the lockfile.
298
+
299
+ Some of what it records comes from a heuristic, and the lockfile names it as such.
300
+ `dynamic` means the view overrides `get_permissions()` and the classes are only known at
301
+ request time. `object_scoping` records whether `get_object`, `get_queryset` and
302
+ `perform_create` are overridden and whether the override reads `request.user` directly; it
303
+ does not follow helper calls and does not show that the query is correct. Decorators are
304
+ detected by reading the view's source, and ones authzlock cannot classify are listed under
305
+ `unknown_decorators`. A green `check` means the lockfile matches the code, not that the
306
+ rules are right. authzlock runs offline and sends nothing anywhere.
307
+ [docs/heuristics-and-limits.md](docs/heuristics-and-limits.md) covers each heuristic and its
308
+ limits.
309
+
310
+ ## Compatibility
311
+
312
+ | Python | Django 4.2 | Django 5.1 | Django 5.2 | Django 6.0 | Django 6.1 |
313
+ |--------|------------|------------|------------|------------|------------|
314
+ | 3.10 | yes | yes | yes | no | no |
315
+ | 3.11 | yes | yes | yes | no | no |
316
+ | 3.12 | yes | yes | yes | yes | yes |
317
+ | 3.13 | no | yes | yes | yes | yes |
318
+
319
+ Django REST Framework 3.14 and newer. CI runs every cell above with DRF 3.16 or newer, and
320
+ DRF 3.14 on Django 4.2 with Python 3.12. Routers from `drf-nested-routers` are supported.
321
+ Django 4.2 does not support Python 3.13, and Django 6.0 and newer need Python 3.12.
322
+
323
+ ## Documentation
324
+
325
+ [docs/index.md](docs/index.md) lists every page. The main ones:
326
+
327
+ - [docs/cli.md](docs/cli.md): commands, options, output formats and exit codes.
328
+ - [docs/lockfile.md](docs/lockfile.md): the lockfile format and its stability guarantees.
329
+ - [docs/classification.md](docs/classification.md): the rules R1 to R8.
330
+ - [docs/heuristics-and-limits.md](docs/heuristics-and-limits.md): what the heuristics can
331
+ and cannot tell you, and what a green `check` does not prove.
332
+ - [docs/scenario.md](docs/scenario.md): a pull request that drops `IsOwner`, end to end.
333
+ - [docs/github-action.md](docs/github-action.md): the GitHub Action.
334
+ - [docs/pre-commit.md](docs/pre-commit.md): the pre-commit hooks.
335
+ - [CHANGELOG.md](CHANGELOG.md): changes by release.
336
+
337
+ ## Development
338
+
339
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the setup, conventions and how to add a test
340
+ project, [DEVELOPMENT.md](DEVELOPMENT.md) for the project brief and development commands,
341
+ [docs/requirements.md](docs/requirements.md) for the requirements and
342
+ [docs/releasing.md](docs/releasing.md) for the release steps. The backlog lives in Linear and
343
+ is mirrored in [docs/backlog.md](docs/backlog.md). `tests/test_readme.py` runs the quickstart
344
+ above against the `drf_viewsets` test project and checks the lockfile excerpt, the rule table
345
+ and the snippets against their sources; `pytest tests/test_readme.py --update-docs` rewrites
346
+ the `diff` output shown above.
347
+
348
+ Every pull request and every push to `main` runs `.github/workflows/ci.yml`: a `lint` job
349
+ (ruff), a `typecheck` job (mypy strict) and one `tests (python=X, django=Y)` job per supported
350
+ combination, eleven in all, plus `tests (python=3.12, django=4.2, drf=3.14)` against the
351
+ oldest supported DRF. The jobs are the same nox sessions that `nox` runs locally with no
352
+ arguments. Mark `lint`, `typecheck` and every `tests (...)` job as required status checks on
353
+ `main` in the repository settings so a red check blocks merging.
354
+
355
+ ## License
356
+
357
+ MIT. See [LICENSE](LICENSE).