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.
- entirius_django_access-1.0.0/.gitattributes +2 -0
- entirius_django_access-1.0.0/.github/CODEOWNERS +1 -0
- entirius_django_access-1.0.0/.github/workflows/ci.yml +51 -0
- entirius_django_access-1.0.0/.github/workflows/publish.yml +19 -0
- entirius_django_access-1.0.0/.gitignore +36 -0
- entirius_django_access-1.0.0/.license-header.txt +3 -0
- entirius_django_access-1.0.0/.pre-commit-config.yaml +39 -0
- entirius_django_access-1.0.0/AGENTS.md +73 -0
- entirius_django_access-1.0.0/CHANGELOG.md +181 -0
- entirius_django_access-1.0.0/CLAUDE.md +1 -0
- entirius_django_access-1.0.0/LICENSE +373 -0
- entirius_django_access-1.0.0/Makefile +16 -0
- entirius_django_access-1.0.0/PKG-INFO +158 -0
- entirius_django_access-1.0.0/README.md +116 -0
- entirius_django_access-1.0.0/docs/api.md +120 -0
- entirius_django_access-1.0.0/docs/concept.md +143 -0
- entirius_django_access-1.0.0/docs/erd-config.yaml +18 -0
- entirius_django_access-1.0.0/docs/gotchas.md +70 -0
- entirius_django_access-1.0.0/docs/install.md +124 -0
- entirius_django_access-1.0.0/docs/module-authors.md +118 -0
- entirius_django_access-1.0.0/docs/openapi.yaml +2278 -0
- entirius_django_access-1.0.0/docs/operations.md +114 -0
- entirius_django_access-1.0.0/docs/testing.md +68 -0
- entirius_django_access-1.0.0/docs/upgrade.md +45 -0
- entirius_django_access-1.0.0/pyproject.toml +84 -0
- entirius_django_access-1.0.0/src/django_access/__init__.py +6 -0
- entirius_django_access-1.0.0/src/django_access/admin.py +72 -0
- entirius_django_access-1.0.0/src/django_access/api/__init__.py +0 -0
- entirius_django_access-1.0.0/src/django_access/api/admin/__init__.py +0 -0
- entirius_django_access-1.0.0/src/django_access/api/admin/urls.py +30 -0
- entirius_django_access-1.0.0/src/django_access/api/admin/views/__init__.py +0 -0
- entirius_django_access-1.0.0/src/django_access/api/admin/views/_base.py +129 -0
- entirius_django_access-1.0.0/src/django_access/api/admin/views/applications.py +74 -0
- entirius_django_access-1.0.0/src/django_access/api/admin/views/audit.py +49 -0
- entirius_django_access-1.0.0/src/django_access/api/admin/views/catalogue.py +23 -0
- entirius_django_access-1.0.0/src/django_access/api/admin/views/grants.py +75 -0
- entirius_django_access-1.0.0/src/django_access/api/admin/views/roles.py +97 -0
- entirius_django_access-1.0.0/src/django_access/api/admin/views/staff.py +119 -0
- entirius_django_access-1.0.0/src/django_access/api/admin/views/tokens.py +129 -0
- entirius_django_access-1.0.0/src/django_access/api/me.py +33 -0
- entirius_django_access-1.0.0/src/django_access/api/permissions.py +26 -0
- entirius_django_access-1.0.0/src/django_access/apps.py +22 -0
- entirius_django_access-1.0.0/src/django_access/catalogue/__init__.py +0 -0
- entirius_django_access-1.0.0/src/django_access/catalogue/areas.py +148 -0
- entirius_django_access-1.0.0/src/django_access/catalogue/defaults.py +279 -0
- entirius_django_access-1.0.0/src/django_access/catalogue/registry.py +122 -0
- entirius_django_access-1.0.0/src/django_access/catalogue/scopes.py +111 -0
- entirius_django_access-1.0.0/src/django_access/checks.py +220 -0
- entirius_django_access-1.0.0/src/django_access/exceptions.py +33 -0
- entirius_django_access-1.0.0/src/django_access/management/__init__.py +0 -0
- entirius_django_access-1.0.0/src/django_access/management/commands/__init__.py +0 -0
- entirius_django_access-1.0.0/src/django_access/management/commands/access_import_legacy_keys.py +75 -0
- entirius_django_access-1.0.0/src/django_access/management/commands/access_legacy_report.py +55 -0
- entirius_django_access-1.0.0/src/django_access/management/commands/access_purge_legacy_keys.py +60 -0
- entirius_django_access-1.0.0/src/django_access/management/commands/access_routes.py +28 -0
- entirius_django_access-1.0.0/src/django_access/management/commands/access_token.py +131 -0
- entirius_django_access-1.0.0/src/django_access/middleware.py +54 -0
- entirius_django_access-1.0.0/src/django_access/migrations/0001_initial.py +141 -0
- entirius_django_access-1.0.0/src/django_access/migrations/0002_builtin_roles_and_staff_managers.py +60 -0
- entirius_django_access-1.0.0/src/django_access/migrations/0003_applications_and_tokens.py +91 -0
- entirius_django_access-1.0.0/src/django_access/migrations/0004_legacy_tokens_without_expiry.py +27 -0
- entirius_django_access-1.0.0/src/django_access/migrations/0005_token_hash_per_channel.py +24 -0
- entirius_django_access-1.0.0/src/django_access/migrations/__init__.py +0 -0
- entirius_django_access-1.0.0/src/django_access/models/__init__.py +10 -0
- entirius_django_access-1.0.0/src/django_access/models/application.py +23 -0
- entirius_django_access-1.0.0/src/django_access/models/audit.py +49 -0
- entirius_django_access-1.0.0/src/django_access/models/grant.py +37 -0
- entirius_django_access-1.0.0/src/django_access/models/role.py +35 -0
- entirius_django_access-1.0.0/src/django_access/models/token.py +49 -0
- entirius_django_access-1.0.0/src/django_access/openapi.py +44 -0
- entirius_django_access-1.0.0/src/django_access/schemas/__init__.py +0 -0
- entirius_django_access-1.0.0/src/django_access/schemas/requests.py +184 -0
- entirius_django_access-1.0.0/src/django_access/schemas/responses.py +274 -0
- entirius_django_access-1.0.0/src/django_access/services/__init__.py +0 -0
- entirius_django_access-1.0.0/src/django_access/services/access_service.py +289 -0
- entirius_django_access-1.0.0/src/django_access/services/directory.py +173 -0
- entirius_django_access-1.0.0/src/django_access/services/gate.py +235 -0
- entirius_django_access-1.0.0/src/django_access/services/legacy.py +453 -0
- entirius_django_access-1.0.0/src/django_access/services/login_guard.py +136 -0
- entirius_django_access-1.0.0/src/django_access/services/permissions.py +136 -0
- entirius_django_access-1.0.0/src/django_access/services/route_map.py +434 -0
- entirius_django_access-1.0.0/src/django_access/services/tokens.py +286 -0
- entirius_django_access-1.0.0/src/django_access/signals.py +52 -0
- entirius_django_access-1.0.0/src/django_access/testing.py +56 -0
- entirius_django_access-1.0.0/src/django_access/urls.py +13 -0
- entirius_django_access-1.0.0/tests/__init__.py +3 -0
- entirius_django_access-1.0.0/tests/admin_login_urls.py +14 -0
- entirius_django_access-1.0.0/tests/admin_urls.py +21 -0
- entirius_django_access-1.0.0/tests/conftest.py +160 -0
- entirius_django_access-1.0.0/tests/faq_stub/__init__.py +3 -0
- entirius_django_access-1.0.0/tests/faq_stub/apps.py +13 -0
- entirius_django_access-1.0.0/tests/foreign_urls.py +23 -0
- entirius_django_access-1.0.0/tests/gate_urls.py +77 -0
- entirius_django_access-1.0.0/tests/helpers.py +43 -0
- entirius_django_access-1.0.0/tests/key_urls.py +43 -0
- entirius_django_access-1.0.0/tests/legacy_apps/__init__.py +4 -0
- entirius_django_access-1.0.0/tests/legacy_apps/accounts/__init__.py +3 -0
- entirius_django_access-1.0.0/tests/legacy_apps/accounts/apps.py +9 -0
- entirius_django_access-1.0.0/tests/legacy_apps/accounts/models.py +13 -0
- entirius_django_access-1.0.0/tests/legacy_apps/checkout/__init__.py +3 -0
- entirius_django_access-1.0.0/tests/legacy_apps/checkout/apps.py +9 -0
- entirius_django_access-1.0.0/tests/legacy_apps/checkout/models.py +18 -0
- entirius_django_access-1.0.0/tests/legacy_apps/contact_forms/__init__.py +3 -0
- entirius_django_access-1.0.0/tests/legacy_apps/contact_forms/apps.py +9 -0
- entirius_django_access-1.0.0/tests/legacy_apps/contact_forms/models.py +14 -0
- entirius_django_access-1.0.0/tests/legacy_apps/returns/__init__.py +3 -0
- entirius_django_access-1.0.0/tests/legacy_apps/returns/apps.py +9 -0
- entirius_django_access-1.0.0/tests/legacy_apps/returns/models.py +8 -0
- entirius_django_access-1.0.0/tests/legacy_apps/reviews/__init__.py +3 -0
- entirius_django_access-1.0.0/tests/legacy_apps/reviews/apps.py +9 -0
- entirius_django_access-1.0.0/tests/legacy_apps/reviews/models.py +8 -0
- entirius_django_access-1.0.0/tests/legacy_apps/vault/__init__.py +3 -0
- entirius_django_access-1.0.0/tests/legacy_apps/vault/apps.py +9 -0
- entirius_django_access-1.0.0/tests/legacy_apps/vault/models.py +8 -0
- entirius_django_access-1.0.0/tests/ownership_urls.py +19 -0
- entirius_django_access-1.0.0/tests/product_delete_urls.py +44 -0
- entirius_django_access-1.0.0/tests/route_map_urls.py +102 -0
- entirius_django_access-1.0.0/tests/security/__init__.py +0 -0
- entirius_django_access-1.0.0/tests/security/conftest.py +86 -0
- entirius_django_access-1.0.0/tests/security/contract.py +177 -0
- entirius_django_access-1.0.0/tests/security/test_admin_api_audit.py +126 -0
- entirius_django_access-1.0.0/tests/security/test_admin_api_escalation.py +154 -0
- entirius_django_access-1.0.0/tests/security/test_admin_api_idor.py +66 -0
- entirius_django_access-1.0.0/tests/security/test_admin_api_leakage.py +38 -0
- entirius_django_access-1.0.0/tests/security/test_admin_api_lockout_race.py +103 -0
- entirius_django_access-1.0.0/tests/security/test_auth_paths.py +54 -0
- entirius_django_access-1.0.0/tests/security/test_bypass_audit.py +81 -0
- entirius_django_access-1.0.0/tests/security/test_cost.py +63 -0
- entirius_django_access-1.0.0/tests/security/test_failures.py +100 -0
- entirius_django_access-1.0.0/tests/security/test_legacy.py +278 -0
- entirius_django_access-1.0.0/tests/security/test_legacy_policy.py +207 -0
- entirius_django_access-1.0.0/tests/security/test_log_hygiene.py +87 -0
- entirius_django_access-1.0.0/tests/security/test_matrix.py +181 -0
- entirius_django_access-1.0.0/tests/security/test_modes.py +52 -0
- entirius_django_access-1.0.0/tests/security/test_paths.py +117 -0
- entirius_django_access-1.0.0/tests/security/test_token_api.py +202 -0
- entirius_django_access-1.0.0/tests/security/test_tokens_hygiene.py +85 -0
- entirius_django_access-1.0.0/tests/security/test_tokens_lifecycle.py +123 -0
- entirius_django_access-1.0.0/tests/security/test_tokens_lookup.py +114 -0
- entirius_django_access-1.0.0/tests/security/urls.py +124 -0
- entirius_django_access-1.0.0/tests/settings.py +65 -0
- entirius_django_access-1.0.0/tests/test_access_service.py +169 -0
- entirius_django_access-1.0.0/tests/test_access_token_command.py +101 -0
- entirius_django_access-1.0.0/tests/test_admin_api.py +259 -0
- entirius_django_access-1.0.0/tests/test_audit_folding.py +153 -0
- entirius_django_access-1.0.0/tests/test_catalogue.py +212 -0
- entirius_django_access-1.0.0/tests/test_checks.py +56 -0
- entirius_django_access-1.0.0/tests/test_gate.py +238 -0
- entirius_django_access-1.0.0/tests/test_legacy.py +267 -0
- entirius_django_access-1.0.0/tests/test_login_guard.py +210 -0
- entirius_django_access-1.0.0/tests/test_me_api.py +60 -0
- entirius_django_access-1.0.0/tests/test_migration_0002.py +66 -0
- entirius_django_access-1.0.0/tests/test_models.py +41 -0
- entirius_django_access-1.0.0/tests/test_openapi.py +107 -0
- entirius_django_access-1.0.0/tests/test_ownership.py +99 -0
- entirius_django_access-1.0.0/tests/test_permissions.py +177 -0
- entirius_django_access-1.0.0/tests/test_preflight.py +80 -0
- entirius_django_access-1.0.0/tests/test_product_delete.py +162 -0
- entirius_django_access-1.0.0/tests/test_registry.py +146 -0
- entirius_django_access-1.0.0/tests/test_route_map.py +233 -0
- entirius_django_access-1.0.0/tests/test_route_map_scoping.py +222 -0
- entirius_django_access-1.0.0/tests/test_smoke.py +8 -0
- entirius_django_access-1.0.0/tests/test_staff_create.py +151 -0
- entirius_django_access-1.0.0/tests/test_token_api.py +233 -0
- entirius_django_access-1.0.0/tests/test_token_rotation.py +90 -0
- entirius_django_access-1.0.0/tests/test_tokens.py +279 -0
- entirius_django_access-1.0.0/tests/test_view_areas.py +145 -0
- entirius_django_access-1.0.0/tests/urls.py +6 -0
- entirius_django_access-1.0.0/tests/view_area_urls.py +51 -0
- entirius_django_access-1.0.0/tests/widgets_stub/__init__.py +0 -0
- entirius_django_access-1.0.0/tests/widgets_stub/apps.py +11 -0
- entirius_django_access-1.0.0/uv.lock +1274 -0
|
@@ -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,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
|