entirius-django-access 1.0.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 (172) hide show
  1. entirius_django_access-1.0.0/.gitattributes +2 -0
  2. entirius_django_access-1.0.0/.github/CODEOWNERS +1 -0
  3. entirius_django_access-1.0.0/.github/workflows/ci.yml +51 -0
  4. entirius_django_access-1.0.0/.github/workflows/publish.yml +19 -0
  5. entirius_django_access-1.0.0/.gitignore +36 -0
  6. entirius_django_access-1.0.0/.license-header.txt +3 -0
  7. entirius_django_access-1.0.0/.pre-commit-config.yaml +39 -0
  8. entirius_django_access-1.0.0/AGENTS.md +73 -0
  9. entirius_django_access-1.0.0/CHANGELOG.md +181 -0
  10. entirius_django_access-1.0.0/CLAUDE.md +1 -0
  11. entirius_django_access-1.0.0/LICENSE +373 -0
  12. entirius_django_access-1.0.0/Makefile +16 -0
  13. entirius_django_access-1.0.0/PKG-INFO +158 -0
  14. entirius_django_access-1.0.0/README.md +116 -0
  15. entirius_django_access-1.0.0/docs/api.md +120 -0
  16. entirius_django_access-1.0.0/docs/concept.md +143 -0
  17. entirius_django_access-1.0.0/docs/erd-config.yaml +18 -0
  18. entirius_django_access-1.0.0/docs/gotchas.md +70 -0
  19. entirius_django_access-1.0.0/docs/install.md +124 -0
  20. entirius_django_access-1.0.0/docs/module-authors.md +118 -0
  21. entirius_django_access-1.0.0/docs/openapi.yaml +2278 -0
  22. entirius_django_access-1.0.0/docs/operations.md +114 -0
  23. entirius_django_access-1.0.0/docs/testing.md +68 -0
  24. entirius_django_access-1.0.0/docs/upgrade.md +45 -0
  25. entirius_django_access-1.0.0/pyproject.toml +84 -0
  26. entirius_django_access-1.0.0/src/django_access/__init__.py +6 -0
  27. entirius_django_access-1.0.0/src/django_access/admin.py +72 -0
  28. entirius_django_access-1.0.0/src/django_access/api/__init__.py +0 -0
  29. entirius_django_access-1.0.0/src/django_access/api/admin/__init__.py +0 -0
  30. entirius_django_access-1.0.0/src/django_access/api/admin/urls.py +30 -0
  31. entirius_django_access-1.0.0/src/django_access/api/admin/views/__init__.py +0 -0
  32. entirius_django_access-1.0.0/src/django_access/api/admin/views/_base.py +129 -0
  33. entirius_django_access-1.0.0/src/django_access/api/admin/views/applications.py +74 -0
  34. entirius_django_access-1.0.0/src/django_access/api/admin/views/audit.py +49 -0
  35. entirius_django_access-1.0.0/src/django_access/api/admin/views/catalogue.py +23 -0
  36. entirius_django_access-1.0.0/src/django_access/api/admin/views/grants.py +75 -0
  37. entirius_django_access-1.0.0/src/django_access/api/admin/views/roles.py +97 -0
  38. entirius_django_access-1.0.0/src/django_access/api/admin/views/staff.py +119 -0
  39. entirius_django_access-1.0.0/src/django_access/api/admin/views/tokens.py +129 -0
  40. entirius_django_access-1.0.0/src/django_access/api/me.py +33 -0
  41. entirius_django_access-1.0.0/src/django_access/api/permissions.py +26 -0
  42. entirius_django_access-1.0.0/src/django_access/apps.py +22 -0
  43. entirius_django_access-1.0.0/src/django_access/catalogue/__init__.py +0 -0
  44. entirius_django_access-1.0.0/src/django_access/catalogue/areas.py +148 -0
  45. entirius_django_access-1.0.0/src/django_access/catalogue/defaults.py +279 -0
  46. entirius_django_access-1.0.0/src/django_access/catalogue/registry.py +122 -0
  47. entirius_django_access-1.0.0/src/django_access/catalogue/scopes.py +111 -0
  48. entirius_django_access-1.0.0/src/django_access/checks.py +220 -0
  49. entirius_django_access-1.0.0/src/django_access/exceptions.py +33 -0
  50. entirius_django_access-1.0.0/src/django_access/management/__init__.py +0 -0
  51. entirius_django_access-1.0.0/src/django_access/management/commands/__init__.py +0 -0
  52. entirius_django_access-1.0.0/src/django_access/management/commands/access_import_legacy_keys.py +75 -0
  53. entirius_django_access-1.0.0/src/django_access/management/commands/access_legacy_report.py +55 -0
  54. entirius_django_access-1.0.0/src/django_access/management/commands/access_purge_legacy_keys.py +60 -0
  55. entirius_django_access-1.0.0/src/django_access/management/commands/access_routes.py +28 -0
  56. entirius_django_access-1.0.0/src/django_access/management/commands/access_token.py +131 -0
  57. entirius_django_access-1.0.0/src/django_access/middleware.py +54 -0
  58. entirius_django_access-1.0.0/src/django_access/migrations/0001_initial.py +141 -0
  59. entirius_django_access-1.0.0/src/django_access/migrations/0002_builtin_roles_and_staff_managers.py +60 -0
  60. entirius_django_access-1.0.0/src/django_access/migrations/0003_applications_and_tokens.py +91 -0
  61. entirius_django_access-1.0.0/src/django_access/migrations/0004_legacy_tokens_without_expiry.py +27 -0
  62. entirius_django_access-1.0.0/src/django_access/migrations/0005_token_hash_per_channel.py +24 -0
  63. entirius_django_access-1.0.0/src/django_access/migrations/__init__.py +0 -0
  64. entirius_django_access-1.0.0/src/django_access/models/__init__.py +10 -0
  65. entirius_django_access-1.0.0/src/django_access/models/application.py +23 -0
  66. entirius_django_access-1.0.0/src/django_access/models/audit.py +49 -0
  67. entirius_django_access-1.0.0/src/django_access/models/grant.py +37 -0
  68. entirius_django_access-1.0.0/src/django_access/models/role.py +35 -0
  69. entirius_django_access-1.0.0/src/django_access/models/token.py +49 -0
  70. entirius_django_access-1.0.0/src/django_access/openapi.py +44 -0
  71. entirius_django_access-1.0.0/src/django_access/schemas/__init__.py +0 -0
  72. entirius_django_access-1.0.0/src/django_access/schemas/requests.py +184 -0
  73. entirius_django_access-1.0.0/src/django_access/schemas/responses.py +274 -0
  74. entirius_django_access-1.0.0/src/django_access/services/__init__.py +0 -0
  75. entirius_django_access-1.0.0/src/django_access/services/access_service.py +289 -0
  76. entirius_django_access-1.0.0/src/django_access/services/directory.py +173 -0
  77. entirius_django_access-1.0.0/src/django_access/services/gate.py +235 -0
  78. entirius_django_access-1.0.0/src/django_access/services/legacy.py +453 -0
  79. entirius_django_access-1.0.0/src/django_access/services/login_guard.py +136 -0
  80. entirius_django_access-1.0.0/src/django_access/services/permissions.py +136 -0
  81. entirius_django_access-1.0.0/src/django_access/services/route_map.py +434 -0
  82. entirius_django_access-1.0.0/src/django_access/services/tokens.py +286 -0
  83. entirius_django_access-1.0.0/src/django_access/signals.py +52 -0
  84. entirius_django_access-1.0.0/src/django_access/testing.py +56 -0
  85. entirius_django_access-1.0.0/src/django_access/urls.py +13 -0
  86. entirius_django_access-1.0.0/tests/__init__.py +3 -0
  87. entirius_django_access-1.0.0/tests/admin_login_urls.py +14 -0
  88. entirius_django_access-1.0.0/tests/admin_urls.py +21 -0
  89. entirius_django_access-1.0.0/tests/conftest.py +160 -0
  90. entirius_django_access-1.0.0/tests/faq_stub/__init__.py +3 -0
  91. entirius_django_access-1.0.0/tests/faq_stub/apps.py +13 -0
  92. entirius_django_access-1.0.0/tests/foreign_urls.py +23 -0
  93. entirius_django_access-1.0.0/tests/gate_urls.py +77 -0
  94. entirius_django_access-1.0.0/tests/helpers.py +43 -0
  95. entirius_django_access-1.0.0/tests/key_urls.py +43 -0
  96. entirius_django_access-1.0.0/tests/legacy_apps/__init__.py +4 -0
  97. entirius_django_access-1.0.0/tests/legacy_apps/accounts/__init__.py +3 -0
  98. entirius_django_access-1.0.0/tests/legacy_apps/accounts/apps.py +9 -0
  99. entirius_django_access-1.0.0/tests/legacy_apps/accounts/models.py +13 -0
  100. entirius_django_access-1.0.0/tests/legacy_apps/checkout/__init__.py +3 -0
  101. entirius_django_access-1.0.0/tests/legacy_apps/checkout/apps.py +9 -0
  102. entirius_django_access-1.0.0/tests/legacy_apps/checkout/models.py +18 -0
  103. entirius_django_access-1.0.0/tests/legacy_apps/contact_forms/__init__.py +3 -0
  104. entirius_django_access-1.0.0/tests/legacy_apps/contact_forms/apps.py +9 -0
  105. entirius_django_access-1.0.0/tests/legacy_apps/contact_forms/models.py +14 -0
  106. entirius_django_access-1.0.0/tests/legacy_apps/returns/__init__.py +3 -0
  107. entirius_django_access-1.0.0/tests/legacy_apps/returns/apps.py +9 -0
  108. entirius_django_access-1.0.0/tests/legacy_apps/returns/models.py +8 -0
  109. entirius_django_access-1.0.0/tests/legacy_apps/reviews/__init__.py +3 -0
  110. entirius_django_access-1.0.0/tests/legacy_apps/reviews/apps.py +9 -0
  111. entirius_django_access-1.0.0/tests/legacy_apps/reviews/models.py +8 -0
  112. entirius_django_access-1.0.0/tests/legacy_apps/vault/__init__.py +3 -0
  113. entirius_django_access-1.0.0/tests/legacy_apps/vault/apps.py +9 -0
  114. entirius_django_access-1.0.0/tests/legacy_apps/vault/models.py +8 -0
  115. entirius_django_access-1.0.0/tests/ownership_urls.py +19 -0
  116. entirius_django_access-1.0.0/tests/product_delete_urls.py +44 -0
  117. entirius_django_access-1.0.0/tests/route_map_urls.py +102 -0
  118. entirius_django_access-1.0.0/tests/security/__init__.py +0 -0
  119. entirius_django_access-1.0.0/tests/security/conftest.py +86 -0
  120. entirius_django_access-1.0.0/tests/security/contract.py +177 -0
  121. entirius_django_access-1.0.0/tests/security/test_admin_api_audit.py +126 -0
  122. entirius_django_access-1.0.0/tests/security/test_admin_api_escalation.py +154 -0
  123. entirius_django_access-1.0.0/tests/security/test_admin_api_idor.py +66 -0
  124. entirius_django_access-1.0.0/tests/security/test_admin_api_leakage.py +38 -0
  125. entirius_django_access-1.0.0/tests/security/test_admin_api_lockout_race.py +103 -0
  126. entirius_django_access-1.0.0/tests/security/test_auth_paths.py +54 -0
  127. entirius_django_access-1.0.0/tests/security/test_bypass_audit.py +81 -0
  128. entirius_django_access-1.0.0/tests/security/test_cost.py +63 -0
  129. entirius_django_access-1.0.0/tests/security/test_failures.py +100 -0
  130. entirius_django_access-1.0.0/tests/security/test_legacy.py +278 -0
  131. entirius_django_access-1.0.0/tests/security/test_legacy_policy.py +207 -0
  132. entirius_django_access-1.0.0/tests/security/test_log_hygiene.py +87 -0
  133. entirius_django_access-1.0.0/tests/security/test_matrix.py +181 -0
  134. entirius_django_access-1.0.0/tests/security/test_modes.py +52 -0
  135. entirius_django_access-1.0.0/tests/security/test_paths.py +117 -0
  136. entirius_django_access-1.0.0/tests/security/test_token_api.py +202 -0
  137. entirius_django_access-1.0.0/tests/security/test_tokens_hygiene.py +85 -0
  138. entirius_django_access-1.0.0/tests/security/test_tokens_lifecycle.py +123 -0
  139. entirius_django_access-1.0.0/tests/security/test_tokens_lookup.py +114 -0
  140. entirius_django_access-1.0.0/tests/security/urls.py +124 -0
  141. entirius_django_access-1.0.0/tests/settings.py +65 -0
  142. entirius_django_access-1.0.0/tests/test_access_service.py +169 -0
  143. entirius_django_access-1.0.0/tests/test_access_token_command.py +101 -0
  144. entirius_django_access-1.0.0/tests/test_admin_api.py +259 -0
  145. entirius_django_access-1.0.0/tests/test_audit_folding.py +153 -0
  146. entirius_django_access-1.0.0/tests/test_catalogue.py +212 -0
  147. entirius_django_access-1.0.0/tests/test_checks.py +56 -0
  148. entirius_django_access-1.0.0/tests/test_gate.py +238 -0
  149. entirius_django_access-1.0.0/tests/test_legacy.py +267 -0
  150. entirius_django_access-1.0.0/tests/test_login_guard.py +210 -0
  151. entirius_django_access-1.0.0/tests/test_me_api.py +60 -0
  152. entirius_django_access-1.0.0/tests/test_migration_0002.py +66 -0
  153. entirius_django_access-1.0.0/tests/test_models.py +41 -0
  154. entirius_django_access-1.0.0/tests/test_openapi.py +107 -0
  155. entirius_django_access-1.0.0/tests/test_ownership.py +99 -0
  156. entirius_django_access-1.0.0/tests/test_permissions.py +177 -0
  157. entirius_django_access-1.0.0/tests/test_preflight.py +80 -0
  158. entirius_django_access-1.0.0/tests/test_product_delete.py +162 -0
  159. entirius_django_access-1.0.0/tests/test_registry.py +146 -0
  160. entirius_django_access-1.0.0/tests/test_route_map.py +233 -0
  161. entirius_django_access-1.0.0/tests/test_route_map_scoping.py +222 -0
  162. entirius_django_access-1.0.0/tests/test_smoke.py +8 -0
  163. entirius_django_access-1.0.0/tests/test_staff_create.py +151 -0
  164. entirius_django_access-1.0.0/tests/test_token_api.py +233 -0
  165. entirius_django_access-1.0.0/tests/test_token_rotation.py +90 -0
  166. entirius_django_access-1.0.0/tests/test_tokens.py +279 -0
  167. entirius_django_access-1.0.0/tests/test_view_areas.py +145 -0
  168. entirius_django_access-1.0.0/tests/urls.py +6 -0
  169. entirius_django_access-1.0.0/tests/view_area_urls.py +51 -0
  170. entirius_django_access-1.0.0/tests/widgets_stub/__init__.py +0 -0
  171. entirius_django_access-1.0.0/tests/widgets_stub/apps.py +11 -0
  172. entirius_django_access-1.0.0/uv.lock +1274 -0
@@ -0,0 +1,2 @@
1
+ # uv.lock is generated — collapse it in diffs and exclude from language stats.
2
+ uv.lock linguist-generated=true
@@ -0,0 +1 @@
1
+ * @entirius/maintainers-backend
@@ -0,0 +1,51 @@
1
+ # Lint (ruff, licence headers), tests on Postgres and a secret scan for every push to develop and every PR.
2
+ name: CI
3
+
4
+ on:
5
+ workflow_dispatch:
6
+ push:
7
+ branches: [develop]
8
+ pull_request:
9
+ branches: [master, develop]
10
+
11
+ jobs:
12
+ lint:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: astral-sh/setup-uv@v4
17
+ - run: uv sync --frozen --all-extras
18
+ - run: uv run ruff check .
19
+ - run: uv run ruff format --check .
20
+ - run: uv run pre-commit run insert-license --all-files
21
+
22
+ test:
23
+ runs-on: ubuntu-latest
24
+ needs: lint
25
+ services:
26
+ postgres:
27
+ image: postgres:16-alpine
28
+ env:
29
+ POSTGRES_PASSWORD: postgres
30
+ ports: ['5432:5432']
31
+ options: >-
32
+ --health-cmd pg_isready --health-interval 5s --health-timeout 5s --health-retries 5
33
+ env:
34
+ DATABASE_URL: postgresql://postgres:postgres@localhost/test
35
+ steps:
36
+ - uses: actions/checkout@v4
37
+ - uses: astral-sh/setup-uv@v4
38
+ - run: uv sync --frozen --all-extras
39
+ - run: uv run pytest -x -q
40
+
41
+ secret-scan:
42
+ runs-on: ubuntu-latest
43
+ steps:
44
+ - uses: actions/checkout@v4
45
+ with:
46
+ fetch-depth: 0
47
+ - name: gitleaks
48
+ run: |
49
+ V=v8.30.0 # keep equal to the gitleaks rev in .pre-commit-config.yaml
50
+ curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/${V}/gitleaks_${V#v}_linux_x64.tar.gz" | sudo tar -xz -C /usr/local/bin gitleaks
51
+ gitleaks git --redact -v
@@ -0,0 +1,19 @@
1
+ name: Publish
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ release:
6
+ types: [published]
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ environment: pypi # GH environment bound to the PyPI trusted publisher
12
+ permissions:
13
+ id-token: write # OIDC - short-lived PyPI token, no stored secrets
14
+ contents: read
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: astral-sh/setup-uv@v4
18
+ - run: uv build
19
+ - run: uv publish --trusted-publishing always
@@ -0,0 +1,36 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ build/
6
+ dist/
7
+
8
+ # Virtualenv / uv
9
+ .venv/
10
+ .python-version
11
+
12
+ # Django (test project artifacts)
13
+ db.sqlite3
14
+ db.sqlite3-journal
15
+ *.log
16
+
17
+ # Test / coverage
18
+ .pytest_cache/
19
+ .coverage
20
+ htmlcov/
21
+ coverage.xml
22
+
23
+ # Tooling caches
24
+ .ruff_cache/
25
+ .mypy_cache/
26
+
27
+ # IDE / OS
28
+ .idea/
29
+ .vscode/
30
+ .DS_Store
31
+
32
+ # Secret scanning (forbidden-names blocklist - local symlink, never committed)
33
+ .gitleaks.toml
34
+
35
+ # Local gitleaks symlink (canonical config lives outside the repo)
36
+ .gitleaks.toml
@@ -0,0 +1,3 @@
1
+ This Source Code Form is subject to the terms of the Mozilla Public
2
+ License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
@@ -0,0 +1,39 @@
1
+ repos:
2
+ - repo: https://github.com/Lucas-C/pre-commit-hooks
3
+ rev: v1.5.6
4
+ hooks:
5
+ - id: insert-license
6
+ files: \.py$
7
+ exclude: (^|/)__init__\.py$
8
+ args:
9
+ - --license-filepath
10
+ - .license-header.txt
11
+ - --comment-style
12
+ - "#"
13
+
14
+ - repo: https://github.com/astral-sh/ruff-pre-commit
15
+ rev: v0.15.20
16
+ hooks:
17
+ - id: ruff-check
18
+ args: [--fix]
19
+ - id: ruff-format
20
+
21
+ - repo: local
22
+ hooks:
23
+ - id: require-canonical-gitleaks-config
24
+ name: require canonical .gitleaks.toml (forbidden-names)
25
+ entry: bash -c 'grep -q "forbidden-names" .gitleaks.toml 2>/dev/null || { echo "Missing or non-canonical .gitleaks.toml - symlink the config per the internal secret-scanning standard"; exit 1; }'
26
+ language: system
27
+ always_run: true
28
+ pass_filenames: false
29
+ - id: gitleaks-config-untracked
30
+ name: .gitleaks.toml must stay gitignored
31
+ entry: bash -c '! git ls-files --error-unmatch .gitleaks.toml 2>/dev/null || { echo ".gitleaks.toml is tracked - add it to .gitignore"; exit 1; }'
32
+ language: system
33
+ always_run: true
34
+ pass_filenames: false
35
+
36
+ - repo: https://github.com/gitleaks/gitleaks
37
+ rev: v8.30.0
38
+ hooks:
39
+ - id: gitleaks
@@ -0,0 +1,73 @@
1
+ # AGENTS.md
2
+
3
+ `entirius-django-access` (Django app `django_access`) — staff roles and permissions per module area, an admin API
4
+ gate, and hashed application tokens for the Entirius backend service (Volkanos).
5
+
6
+ ## Commands
7
+
8
+ | Command | Meaning |
9
+ |---|---|
10
+ | `make install` | sync dependencies (uv, incl. extras) |
11
+ | `make check` | lint + format-check (ruff) |
12
+ | `make fix` | auto-fix lint + format |
13
+ | `make test` | test suite (pytest + pytest-django) |
14
+
15
+ ## Conventions
16
+
17
+ - English only: code, docs, commits, branches, PRs.
18
+ - MPL-2.0: every non-trivial source file carries the license header (pre-commit inserts it).
19
+ - Toolchain: uv + ruff + hatchling + pytest; all config in `pyproject.toml`; `uv.lock` committed.
20
+ - Git flow: `master` (production) + `develop` (integration); changes land via PR; semver tag on `master`.
21
+ - Never rename the package / Django app_label / DB table prefix `django_access` — it is a schema contract.
22
+ - Migrations are part of the public contract — never edit an already released migration.
23
+ - Default: do not commit — git is the user's call.
24
+
25
+ ### Commit Message Format
26
+
27
+ **NEVER add `Co-Authored-By: Claude ...` (or any other Claude/Anthropic attribution) to commit messages.**
28
+
29
+ This overrides the default Claude Code behavior of appending a `Co-Authored-By` trailer. Commit messages MUST contain only the user's authored content — no robot footer, no "Generated with Claude Code" line, no co-author trailer.
30
+
31
+ Same rule applies to PR descriptions: no `Generated with [Claude Code]` footer.
32
+
33
+ ## Architecture
34
+
35
+ Read first: `docs/install.md` (host) · `docs/upgrade.md` (before the first deploy) · `docs/api.md` (caller) ·
36
+ `docs/concept.md` (why) · `docs/operations.md` (day 2) · `docs/gotchas.md` (before editing) · `docs/testing.md` ·
37
+ `docs/module-authors.md` (other modules). This section is the map; it explains nothing twice.
38
+
39
+ ```
40
+ src/django_access/
41
+ ├── apps.py (checks, cache signals, post_migrate legacy import) middleware.py (AccessGateMiddleware)
42
+ │ openapi.py (ApiKeyAuth hook) checks.py (E001–E008, E010–E012, W002, W003, W010, I001) exceptions.py urls.py
43
+ │ signals.py (cache invalidation, staff_user_created)
44
+ │ testing.py (assert_routes_covered, for module test suites)
45
+ ├── catalogue/ areas scopes (token scopes) defaults (route rules, method and area overrides) registry
46
+ ├── models/ role (Role, RolePermission) grant audit (AuditEntry, AuditAction) application token (ApiToken)
47
+ ├── services/ access_service (every role/grant mutation, staff create + audit + lockout guard) permissions (cached)
48
+ │ route_map (classify, audit_routes) gate (decide) tokens (issue, rotate, revoke, verify_api_key)
49
+ │ legacy (import, legacy_report, purge_legacy_sources) directory (read queries of the API)
50
+ │ login_guard (failed-login counter of the service's password logins, admin login form wrapper)
51
+ ├── schemas/ requests.py responses.py (Pydantic, extra="forbid")
52
+ ├── api/ me.py permissions.py (IsStaffUser, HasAreaPermission) admin/ (urls, thin views)
53
+ └── management/commands/ access_routes access_token access_import_legacy_keys access_legacy_report
54
+ access_purge_legacy_keys
55
+ ```
56
+
57
+ Flow: request → resolve → gate (admin set only: principal → role permissions → allow / 401 / 403) → view. Key routes:
58
+ the module's own auth → `verify_api_key(request, scope, channel_idx)` → one lookup by hash (the row of the channel,
59
+ else the unpinned one). `migrate` →
60
+ `post_migrate` → legacy import (no expiry; teams set one per token) → `access_import_legacy_keys --check` in the deploy
61
+ → `access_legacy_report` shows who still uses them → `access_purge_legacy_keys --yes` on demand. Route areas: the
62
+ view's `access_area` → the module's `AppConfig` rules → the access defaults.
63
+
64
+ | Topic | Where |
65
+ |---|---|
66
+ | Areas, roles, gate decision table, token rules, legacy mapping | `docs/concept.md` |
67
+ | Settings, middleware, URLs, OpenAPI hook, deploy order, rollback, hardening | `docs/install.md` |
68
+ | Endpoint, body, response, errors; `verify_api_key` contract | `docs/api.md`; `docs/openapi.yaml` |
69
+ | Upgrade preflight: `access_routes --unmapped`, `check --deploy` (`E011`), observe first | `docs/upgrade.md` |
70
+ | Legacy import report, `--check`, legacy report, expiry, purge, rotation, revocation, lockout | `docs/operations.md` |
71
+ | Which test covers what; fake legacy modules, the security suite, PostgreSQL-only tests, the harness run | `docs/testing.md` |
72
+ | `access_area` on views, a module's own areas/rules, `assert_routes_covered`, the dependency | `docs/module-authors.md` |
73
+ | ERD groupings | `docs/erd-config.yaml` |
@@ -0,0 +1,181 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0 — 2026-10-12
4
+
5
+ First release.
6
+
7
+ - Permission catalogue as code: 48 areas (read/write per module area), 9 application-token scopes, default route
8
+ rules for the 25 modules that own admin routes (full `ResolverMatch.route` regex, first match wins, a catch-all per
9
+ admin root), staff-baseline pseudo-area.
10
+ - `AppConfig.access_areas` / `access_route_rules` / `access_token_scopes`: a module's declaration replaces the
11
+ defaults of that kind for its own label.
12
+ - `catalogue.registry`: `areas()`, `scopes()`, `route_rules()`, `area()`, `perm_key()`, `parse_perm()`, `implies()`,
13
+ cached, `reset()` for tests.
14
+ - System checks (tag `entirius_config`, `django_access.E001`–`E006`): duplicate area or scope keys, invalid areas,
15
+ rules naming an unknown area or carrying an invalid regex, unreadable declarations.
16
+ - Models `Role`, `RolePermission`, `Grant` (one user or one `auth.Group`), `AuditEntry`; read-only Django admin.
17
+ - Built-in roles `administrator`, `manager`, `editor`, `viewer`, computed from the catalogue at run time; custom roles
18
+ hold validated permission keys.
19
+ - `services.permissions`: `effective_permissions(user)`, `has_permission(user, key)`, `granted_roles(user)`,
20
+ `manages_access(user)`; cached per user under a version bumped by every access change, group membership and
21
+ `is_staff` / `is_superuser` / `is_active` changes.
22
+ - `services.access_service`: `create_role`, `update_role`, `delete_role`, `grant_role`, `revoke_grant` — each in one
23
+ transaction with its audit row; a change that removes the last access manager (active staff superuser or
24
+ Administrator holder) raises `AccessLockout`. `access.manage` is built-in only: a custom role asking for
25
+ `access.manage:read` or `:write` raises `ReservedPermission` (`ACCESS_MANAGE_RESERVED`); `registry.custom_role_areas()`
26
+ lists the assignable areas. Grants go only to active staff users (or to a group).
27
+ - Migration `0002_builtin_roles_and_staff_managers` (renamed from `…_staff_administrators`, which it `replaces`): the
28
+ built-in roles, and Manager for every active non-superuser staff user on first adoption
29
+ (`grant.migrate` audit rows); access management stays with superusers until someone is granted Administrator.
30
+ - `services.route_map`: `walk()` yields every route as `ResolverMatch.route`, `classify()` gives owner, admin flag,
31
+ area and method overrides (no models imported), `required_permission()` the `<area>:<level>` a method needs,
32
+ `audit_routes()` the per-module route audit (JSON report on request). Admin set = `admin/` segment or `api-admin/`
33
+ path ∪ an `IsAdminUser`/`IsSuperUser` permission class (DRF `&`/`|` composites walked) ∪ the munin health, returns
34
+ download and pim viewer exceptions; the X-API-ADMIN-KEY erase routes are not admin. Route rules are owner-scoped
35
+ (a rule applies only to its own module's routes); framework routes match `FRAMEWORK_RULES` only — the Django admin
36
+ site is `superuser.only` (login, logout, jsi18n and password change on the staff baseline), the contentdb router root
37
+ `content.pages`, the OpenAPI views the staff baseline. `RouteInfo.self_auth` / `auth`: whether the view answers an
38
+ anonymous caller itself (a DRF view on exactly SimpleJWT `JWTAuthentication` / DRF `SessionAuthentication` with a
39
+ permission requiring a user, no `get_authenticators`/`get_permissions`/`check_permissions` override; or a view the
40
+ Django admin site marks itself); classes from `as_view` initkwargs first. The JSON report
41
+ adds `foreign_rule_matches` (the audit fails on any), `admin_not_self_auth` and `non_admin` with an audience.
42
+ - `catalogue.defaults.METHOD_OVERRIDES`: 9 POST-reads → read (communicator test-generate is a paid AI call: write), leads GDPR export and 6 GET PII exports/downloads →
43
+ write, contentdb GET `…/published/` → `content.publish:write`. `returns.attachments` is write-only.
44
+ - `manage.py access_routes [--check] [--json PATH]`.
45
+ - System check `django_access.W002`: a `LocMemCache`/`DummyCache` default cache with `DEBUG=False` (permission
46
+ changes reach other processes only after the cache timeout).
47
+ - Application tokens: models `Application` and `ApiToken` (migration `0003`). Raw value `ent_api_` +
48
+ `secrets.token_urlsafe(32)`, shown once; stored as SHA-256 `key_hash` (unique index), `prefix` (12) and `last_four`.
49
+ - `services.tokens`: `issue_token`, `rotate_token` (successor + overlap window), `revoke_token`, `create_application`
50
+ (each audited `application.create` / `token.create|rotate|revoke`, never with the raw value or `key_hash`), and
51
+ `verify_api_key(request, scope, channel_idx=None)`: `X-API-KEY`, else `X-API-ADMIN-KEY`; one uncached lookup by
52
+ hash; `None` for every failure (unknown, expired, revoked, inactive application, wrong or empty scopes, channel
53
+ mismatch); values over 256 characters run no query; `last_used_at` by a conditional update at most once per
54
+ `ACCESS_TOKEN_LAST_USED_INTERVAL_S` (300).
55
+ - Scope rules: at least one catalogue scope; publishable and secret scopes never share a token.
56
+ - Token lifetime (D31, replaces the D21 cap of earlier pre-releases — the secret-token maximum, its setting and its
57
+ two error codes are gone): an expiry is optional for every scope and has no maximum; a past one raises
58
+ `TokenExpiryError` (`EXPIRY_IN_PAST`); a rotated successor gets the given expiry or none. `ACCESS_TOKEN_ROTATION_DAYS`
59
+ (365; 0 = off), `tokens.token_age_days()` / `rotation_due()`: token rows (API), `access_token list` and
60
+ `access_legacy_report` show `age_days` and `rotation_due`, the catalogue `token_rotation_days`. Nothing is refused
61
+ for age.
62
+ - `manage.py access_token create|rotate|revoke|list`; read-only Django admin for applications and tokens, no add,
63
+ no `key_hash`.
64
+ - `django_access.middleware.AccessGateMiddleware` (append after the authentication middleware) +
65
+ `services.gate.decide()`: acts only on the admin set (outside it one memoized `classify()`, no authentication,
66
+ cache or query); the principal is what the view's own JWT/session authenticators would see (a session never counts
67
+ on a JWT-only view; API-key headers are never read); anonymous callers reach only self-authenticating views, else
68
+ 401 `NOT_AUTHENTICATED` + `WWW-Authenticate: Bearer realm="api"`; superuser passes, with one `gate.bypass` audit
69
+ row per write (GET PII exports included, any unsafe method on a route without an area with `needed: null`) written
70
+ after the response with its status; non-staff → 403 `STAFF_ONLY`, admin route without an area → 403
71
+ `UNMAPPED_ROUTE`, missing permission or a write on a read-only area → 403 `ACCESS_DENIED` (v2 envelope); an
72
+ exception in the decision → the v2 500 envelope, view not run.
73
+ - `ACCESS_GATE_MODE` = `enforce` (default) | `observe` (log refusals on `django_access.gate`, let through) | `off`;
74
+ any other value is enforced. System checks `django_access.E010` (invalid mode) and `W010` (not `enforce` with
75
+ `DEBUG=False`).
76
+ - The gate decides HEAD like GET (Django serves HEAD with the GET handler): a HEAD of a GET PII export needs the
77
+ export's write permission and leaves a superuser bypass row.
78
+ - Admin API v2 under `api/access/v2/admin/` (`django_access.urls`): `catalogue/` (staff baseline: areas by module with
79
+ `assignable`, built-in roles with computed permissions), `roles/` + `roles/<id>/` (custom roles only change; built-in
80
+ or lockout → 409, `access.manage` → 400 `ACCESS_MANAGE_RESERVED`), `grants/` + `grants/<id>/`, `staff/` +
81
+ `staff/<user_id>/` (active staff only; anything else → 404), `groups/`, `audit/` (filters `action`, `actor`, `from`,
82
+ `to`). JWT only, `IsStaffUser` + `HasAreaPermission` (`access.manage:read`/`:write`) on every admin view, Pydantic
83
+ schemas with `extra="forbid"`; `GET api/access/v2/me/` for any authenticated user. Audit rows carry the client
84
+ address by DRF's throttle rule (`NUM_PROXIES` set → the `X-Forwarded-For` entry `min(NUM_PROXIES, entries)` from the
85
+ right; unset → `REMOTE_ADDR`). The staff baseline (`IsStaffUser`) is active staff only. A racing duplicate role or grant is a conflict,
86
+ not an `IntegrityError`.
87
+ - Token API v2 under `api/access/v2/admin/`: `applications/` + `applications/<id>/` (list, create, PATCH `name`,
88
+ `description`, `is_active`; no DELETE; a name in use → 409), `applications/<id>/tokens/` (list, issue),
89
+ `tokens/<id>/rotate/` (`overlap_hours` 0–8760, the service limit, default 24; revoked → 409) and `tokens/<id>/revoke/` (idempotent, one
90
+ audit row). The raw value is only in the issue and rotate responses (`Cache-Control: no-store`, `Pragma: no-cache`);
91
+ no response carries `key_hash`; token rows show the effective `state` `active` | `expired` | `revoked` | `application inactive`, `age_days` and
92
+ `rotation_due`. `services.tokens.update_application` (field whitelist,
93
+ audited `application.update`) and `lifecycle_state`. The catalogue lists the token scopes.
94
+ - `django_access.openapi.add_api_key_security`: drf-spectacular postprocessing hook adding the `ApiKeyAuth` scheme
95
+ (`apiKey`, header `X-API-KEY`) and requiring it on every operation matching a token scope's routes, next to the
96
+ operation's existing requirements.
97
+ - Legacy keys (`services.legacy`): `import_legacy_keys(dry_run=, now=)` imports the seven module key tables
98
+ (accounts/checkout `APIAdminKey`, checkout and contact-forms `APIKey` pinned to their channel, returns, reviews and
99
+ vault `APIKey`) and `AGREEMENTS_API_KEY` as tokens with the same secret — one `Legacy keys: <app_label>`
100
+ application per module, `legacy=True`, `legacy_source` `<app>.<Model>#<pk>`, no expiry (D28: legacy keys never
101
+ expire by themselves; the setting `ACCESS_LEGACY_KEY_TTL_DAYS` of earlier 0.1.0 builds is gone — 0.1.0 is
102
+ unreleased). Idempotent by `key_hash`: an existing token is never changed (no revival). A secret on several channels → one pinned token per channel; a row added later that the existing token does not serve →
103
+ `stale`; a secret in a publishable and a secret
104
+ source → not imported (`mixed`); a secret under 32 characters → `prefix "legacy"`, empty `last_four`; a row
105
+ without a channel → skipped. Runs on `post_migrate` (errors logged by class, `migrate` never fails); one
106
+ `legacy.import` audit row per run that imported something.
107
+ - `manage.py access_import_legacy_keys [--dry-run] [--report] [--check]`: counts and `legacy_source` ids only, never
108
+ a value or a hash; `--check` exits 1 while a key is not imported, a secret is `mixed` or `stale`, or a source fails.
109
+ - `purge_legacy_sources(PurgeOptions)` + `manage.py access_purge_legacy_keys [--yes] [--force] [--recent-days N]`:
110
+ deletes the plaintext rows of imported legacy keys on demand, no time gate; without `--yes` it only lists
111
+ (`--dry-run` kept as an alias); a key used within `N` (30) days is refused (exit 1, listed with its last use) unless
112
+ `--force`, which also deletes never-imported rows; idempotent; one `legacy.purge` audit row per run that deleted
113
+ something; names `AGREEMENTS_API_KEY` for removal from the settings.
114
+ - Migration `0004_legacy_tokens_without_expiry`: clears the automatic import-time expiry of every legacy token no
115
+ `token.expiry` audit row names (reverse: no-op).
116
+ - `tokens.set_token_expiry(token, expires_at=, actor=)`: set or clear one token's expiry, audited `token.expiry`
117
+ (token id, `legacy`, from, to). Every token takes any future date or none; a past date → `EXPIRY_IN_PAST`; a
118
+ revoked token →
119
+ `AccessConflict`. API `POST admin/tokens/<id>/expiry/` (200 token row); CLI `access_token expire <id> --at
120
+ YYYY-MM-DD | --clear`.
121
+ - `manage.py access_legacy_report [--json PATH] [--module LABEL]`: one row per legacy token (source, scopes, channel,
122
+ created, last used, expiry, state, age in days, rotation due), never a value or a hash.
123
+ - Module docs: `docs/concept.md`, `install.md` (wiring, settings, deploy order, rollback, production hardening),
124
+ `api.md`, `operations.md`, `testing.md`, `gotchas.md`, `erd-config.yaml`, `openapi.yaml`.
125
+ - Requires Django 5.1+ and DRF 3.15.2+.
126
+ - Django admin superusers only (D32, replaces D17's `access.manage`): pseudo-area `superuser.only` (never grantable,
127
+ never in `me`, not a catalogue area; `E003`/`E004`/`E007` treat it like `staff.baseline`) for every `admin/` route
128
+ but the login pages; the gate answers every non-superuser staff user 403 `ACCESS_DENIED` (`superuser only`),
129
+ Administrators included; a superuser's writes keep their `gate.bypass` row.
130
+ - Area `pim.product_delete` (write only, flag `destructive`, 49 areas): `catalogue.defaults.AREA_OVERRIDES` make the
131
+ PIM product `DELETE` (both roots), the feature-set `DELETE` (it cascades to the set's products) and the
132
+ atlas/suppliers `realproducts/merge-by-ean/` need it instead of `pim.products` / `pim.schema` / `*.products`;
133
+ `RouteInfo.method_areas`, `method_areas` and `method_levels` in the route audit JSON. Administrator and
134
+ Manager hold it, Editor does not. `E003` also fires on an area override naming an unknown area.
135
+ - Module ownership: `django_access.testing.assert_routes_covered(app_label, urlconf=None, require_own=False)` for a
136
+ module's own test suite (fails on `unmapped`, `foreign_rule`, `unknown_area`, and `defaults` with `require_own`:
137
+ no `access_areas` declared or an owned admin route still mapped by the access defaults); check `django_access.I001`
138
+ (Info) lists installed apps with an admin route mapped by the defaults; `route_map.unique_entries()` /
139
+ `foreign_rule_matches()` public; guide `docs/module-authors.md`.
140
+ - Areas on views (D30): a view class or function may carry `access_area` and `access_levels` (method → `read` /
141
+ `write`); precedence view → the owner's `access_route_rules` → the access defaults; `access_levels` win per method
142
+ over `METHOD_OVERRIDES`, `AREA_OVERRIDES` still apply on top. `RouteInfo.area_source` (`view` | `app` | `default` |
143
+ `framework` | None), in the route audit JSON and output. Checks `E007` (unknown view area), `E008` (bad
144
+ `access_levels`), `W003` (view area outside the admin set).
145
+ - Upgrade preflight (D29): `manage.py access_routes --unmapped` (route, owner, methods; exit 1 when any) and the
146
+ deploy check `django_access.E011` (`check --deploy`: the gate enforces over unmapped admin routes); guide
147
+ `docs/upgrade.md`.
148
+ - `services.login_guard`: the failed-login counter of the service's password logins, moved from volkanos
149
+ (`api/token/`) — `login_username(request)`, `refuse_when_blocked(request, username)` (DRF `Throttled`),
150
+ `record_failure`, `clear`; per username + address and per address, hashed cache keys, settings
151
+ `AUTH_TOKEN_FAILURE_WINDOW_S` / `AUTH_TOKEN_MAX_FAILURES_PER_USER_IP` / `AUTH_TOKEN_MAX_FAILURES_PER_IP` (900 / 10 /
152
+ 100).
153
+ - `verify_api_key` is fail-closed on pins: a pinned token is refused where the caller passes `channel_idx=None`.
154
+ - OpenAPI tags in Title Case (`Access Grants`, …); `GET me` has a description.
155
+ - `POST api/access/v2/admin/staff/` (`access_staff_create`, `access.manage:write`): an active staff account (never a
156
+ superuser) with one role through `access_service.create_staff_user` — `staff.create` audit row, the generated
157
+ password returned once (`no-store`), a given one never echoed; signal `staff_user_created` inside the transaction.
158
+ - Legacy import: one secret on several channels → one pinned token per channel, its channel-less rows one unpinned
159
+ token with their scopes; report `unpinned` renamed `per_channel`; an unpinned token of an older import serving
160
+ channel rows → `stale`, nothing created. Purge judges each row by its own channel's token.
161
+ - `ApiToken.key_hash` no longer unique alone: `UniqueConstraint(key_hash, channel_idx, nulls_distinct=False)`
162
+ (migration `0005`, PostgreSQL 15+); `verify_api_key` takes the row of the route's channel, else the unpinned one.
163
+ - `login_guard`: a per-username counter from any address, `AUTH_TOKEN_MAX_FAILURES_PER_USER` (50) per
164
+ `AUTH_TOKEN_USER_FAILURE_WINDOW_S` (3600); `Throttled.wait` is the longest window that blocks.
165
+ - `GET admin/audit/?fold_bypass=true`: `gate.bypass` entries folded in the database into one row per actor and
166
+ calendar day (service time zone) before paging, so `count` and the pages are those of the folded rows; rows carry
167
+ `folded`, `first_at`, `last_at`. Without the parameter the response is unchanged.
168
+
169
+ ### Security
170
+
171
+ - `login_guard.guarded_admin_login(admin.site.login)`: the Django admin login form counts on the failed-login
172
+ counters of `api/token/` (same keys, limits and windows) and answers 429 with `Retry-After` when one blocks. The
173
+ form accepted unlimited guesses, also while `api/token/` was blocked. The service routes it at `admin/login/`
174
+ before the admin site (`docs/install.md`).
175
+ - `set_token_expiry` refuses a token that is already expired, the old token of a rotation after its cut-off included
176
+ (409 on `POST admin/tokens/<id>/expiry/`): a new date or `null` used to make it valid again.
177
+ - A per-process default cache (LocMem, Dummy) is the Error `django_access.E012` under `check --deploy`, in any
178
+ `DEBUG`; the plain check keeps the Warning `W002`. With such a cache a revoked grant keeps working in the other
179
+ workers and the login limits multiply by the worker count.
180
+ - Staff create answers a taken username and a taken e-mail address with one 409 message that names neither the field
181
+ nor the account that holds the value (it said "The e-mail address is taken" for any user's address).
@@ -0,0 +1 @@
1
+ @AGENTS.md