Repository navigation
Releases: entirius/entirius-django-access
Releases · entirius/entirius-django-access
Release list
v1.0.0
First release.
- Permission catalogue as code: 48 areas (read/write per module area), 9 application-token scopes, default route
rules for the 25 modules that own admin routes (fullResolverMatch.routeregex, first match wins, a catch-all per
admin root), staff-baseline pseudo-area. AppConfig.access_areas/access_route_rules/access_token_scopes: a module's declaration replaces the
defaults of that kind for its own label.catalogue.registry:areas(),scopes(),route_rules(),area(),perm_key(),parse_perm(),implies(),
cached,reset()for tests.- System checks (tag
entirius_config,django_access.E001–E006): duplicate area or scope keys, invalid areas,
rules naming an unknown area or carrying an invalid regex, unreadable declarations. - Models
Role,RolePermission,Grant(one user or oneauth.Group),AuditEntry; read-only Django admin. - Built-in roles
administrator,manager,editor,viewer, computed from the catalogue at run time; custom roles
hold validated permission keys. services.permissions:effective_permissions(user),has_permission(user, key),granted_roles(user),
manages_access(user); cached per user under a version bumped by every access change, group membership and
is_staff/is_superuser/is_activechanges.services.access_service:create_role,update_role,delete_role,grant_role,revoke_grant— each in one
transaction with its audit row; a change that removes the last access manager (active staff superuser or
Administrator holder) raisesAccessLockout.access.manageis built-in only: a custom role asking for
access.manage:reador:writeraisesReservedPermission(ACCESS_MANAGE_RESERVED);registry.custom_role_areas()
lists the assignable areas. Grants go only to active staff users (or to a group).- Migration
0002_builtin_roles_and_staff_managers(renamed from…_staff_administrators, which itreplaces): the
built-in roles, and Manager for every active non-superuser staff user on first adoption
(grant.migrateaudit rows); access management stays with superusers until someone is granted Administrator. services.route_map:walk()yields every route asResolverMatch.route,classify()gives owner, admin flag,
area and method overrides (no models imported),required_permission()the<area>:<level>a method needs,
audit_routes()the per-module route audit (JSON report on request). Admin set =admin/segment orapi-admin/
path ∪ anIsAdminUser/IsSuperUserpermission class (DRF&/|composites walked) ∪ the munin health, returns
download and pim viewer exceptions; the X-API-ADMIN-KEY erase routes are not admin. Route rules are owner-scoped
(a rule applies only to its own module's routes); framework routes matchFRAMEWORK_RULESonly — the Django admin
site issuperuser.only(login, logout, jsi18n and password change on the staff baseline), the contentdb router root
content.pages, the OpenAPI views the staff baseline.RouteInfo.self_auth/auth: whether the view answers an
anonymous caller itself (a DRF view on exactly SimpleJWTJWTAuthentication/ DRFSessionAuthenticationwith a
permission requiring a user, noget_authenticators/get_permissions/check_permissionsoverride; or a view the
Django admin site marks itself); classes fromas_viewinitkwargs first. The JSON report
addsforeign_rule_matches(the audit fails on any),admin_not_self_authandnon_adminwith an audience.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 →
write, contentdb GET…/published/→content.publish:write.returns.attachmentsis write-only.manage.py access_routes [--check] [--json PATH].- System check
django_access.W002: aLocMemCache/DummyCachedefault cache withDEBUG=False(permission
changes reach other processes only after the cache timeout). - Application tokens: models
ApplicationandApiToken(migration0003). Raw valueent_api_+
secrets.token_urlsafe(32), shown once; stored as SHA-256key_hash(unique index),prefix(12) andlast_four. services.tokens:issue_token,rotate_token(successor + overlap window),revoke_token,create_application
(each auditedapplication.create/token.create|rotate|revoke, never with the raw value orkey_hash), and
verify_api_key(request, scope, channel_idx=None):X-API-KEY, elseX-API-ADMIN-KEY; one uncached lookup by
hash;Nonefor every failure (unknown, expired, revoked, inactive application, wrong or empty scopes, channel
mismatch); values over 256 characters run no query;last_used_atby a conditional update at most once per
ACCESS_TOKEN_LAST_USED_INTERVAL_S(300).- Scope rules: at least one catalogue scope; publishable and secret scopes never share a token.
- Token lifetime (D31, replaces the D21 cap of earlier pre-releases — the secret-token maximum, its setting and its
two error codes are gone): an expiry is optional for every scope and has no maximum; a past one raises
TokenExpiryError(EXPIRY_IN_PAST); a rotated successor gets the given expiry or none.ACCESS_TOKEN_ROTATION_DAYS
(365; 0 = off),tokens.token_age_days()/rotation_due(): token rows (API),access_token listand
access_legacy_reportshowage_daysandrotation_due, the cataloguetoken_rotation_days. Nothing is refused
for age. manage.py access_token create|rotate|revoke|list; read-only Django admin for applications and tokens, no add,
nokey_hash.django_access.middleware.AccessGateMiddleware(append after the authentication middleware) +
services.gate.decide(): acts only on the admin set (outside it one memoizedclassify(), no authentication,
cache or query); the principal is what the view's own JWT/session authenticators would see (a session never counts
on a JWT-only view; API-key headers are never read); anonymous callers reach only self-authenticating views, else
401NOT_AUTHENTICATED+WWW-Authenticate: Bearer realm="api"; superuser passes, with onegate.bypassaudit
row per write (GET PII exports included, any unsafe method on a route without an area withneeded: null) written
after the response with its status; non-staff → 403STAFF_ONLY, admin route without an area → 403
UNMAPPED_ROUTE, missing permission or a write on a read-only area → 403ACCESS_DENIED(v2 envelope); an
exception in the decision → the v2 500 envelope, view not run.ACCESS_GATE_MODE=enforce(default) |observe(log refusals ondjango_access.gate, let through) |off;
any other value is enforced. System checksdjango_access.E010(invalid mode) andW010(notenforcewith
DEBUG=False).- The gate decides HEAD like GET (Django serves HEAD with the GET handler): a HEAD of a GET PII export needs the
export's write permission and leaves a superuser bypass row. - Admin API v2 under
api/access/v2/admin/(django_access.urls):catalogue/(staff baseline: areas by module with
assignable, built-in roles with computed permissions),roles/+roles/<id>/(custom roles only change; built-in
or lockout → 409,access.manage→ 400ACCESS_MANAGE_RESERVED),grants/+grants/<id>/,staff/+
staff/<user_id>/(active staff only; anything else → 404),groups/,audit/(filtersaction,actor,from,
to). JWT only,IsStaffUser+HasAreaPermission(access.manage:read/:write) on every admin view, Pydantic
schemas withextra="forbid";GET api/access/v2/me/for any authenticated user. Audit rows carry the client
address by DRF's throttle rule (NUM_PROXIESset → theX-Forwarded-Forentrymin(NUM_PROXIES, entries)from the
right; unset →REMOTE_ADDR). The staff baseline (IsStaffUser) is active staff only. A racing duplicate role or grant is a conflict,
not anIntegrityError. - Token API v2 under
api/access/v2/admin/:applications/+applications/<id>/(list, create, PATCHname,
description,is_active; no DELETE; a name in use → 409),applications/<id>/tokens/(list, issue),
tokens/<id>/rotate/(overlap_hours0–8760, the service limit, default 24; revoked → 409) andtokens/<id>/revoke/(idempotent, one
audit row). The raw value is only in the issue and rotate responses (Cache-Control: no-store,Pragma: no-cache);
no response carrieskey_hash; token rows show the effectivestateactive|expired|revoked|application inactive,age_daysand
rotation_due.services.tokens.update_application(field whitelist,
auditedapplication.update) andlifecycle_state. The catalogue lists the token scopes. django_access.openapi.add_api_key_security: drf-spectacular postprocessing hook adding theApiKeyAuthscheme
(apiKey, headerX-API-KEY) and requiring it on every operation matching a token scope's routes, next to the
operation's existing requirements.- Legacy keys (
services.legacy):import_legacy_keys(dry_run=, now=)imports the seven module key tables
(accounts/checkoutAPIAdminKey, checkout and contact-formsAPIKeypinned to their channel, returns, reviews and
vaultAPIKey) andAGREEMENTS_API_KEYas tokens with the same secret — oneLegacy keys: <app_label>
application per module,legacy=True,legacy_source<app>.<Model>#<pk>, no expiry (D28: legacy keys never
expire by themselves; the settingACCESS_LEGACY_KEY_TTL_DAYSof earlier 0.1.0 builds is gone — 0.1.0 is
unreleased). Idempotent bykey_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 →
stale; a secret in a publishable and a secret
source → not imported (mixed); a secret under 32 characters →prefix "legacy",...