djust docs
Browse documentation
intermediate

Declarative Permissions Document

Validate every view's auth config against a committed YAML document, so the permission model is an auditable artifact.

On this page

The djust_audit --permissions permissions.yaml flag validates every LiveView in your project against a committed, human-readable YAML document describing the expected auth configuration for each view. Run with --strict and CI fails on any deviation, making the permission model an auditable artifact — security reviewers can sign it off once, and no new view can merge without an explicit declaration. (Without --strict the command prints findings and exits 0.)

LiveComponent subclasses are audited too. In strict mode they must also be declared, usually as public: true, since a component inherits its parent view's auth.

This closes a structural gap in djust_audit: the tool can tell "no auth" from "some auth", but it cannot tell that login_required=True should have been permission_required=['claims.view_supervisor_dashboard']. The document IS the ground truth for what each view SHOULD require.

Why

In a recent penetration test of a djust-based application, every view in the claims/, settlement/, investigation/, and documents/ namespaces had login_required=True set. djust_audit reported them all as "protected." In reality, the lowest-privilege authenticated user could access the examiner dashboard, the supervisor dashboard, and every claim detail page in the database by ID walk.

The gap was structural: the framework's check could see that auth existed, but it had no way to know that "some auth" should have been "supervisor role only" for specific views. That's application knowledge the framework doesn't have — unless the app tells the framework explicitly.

Creating permissions.yaml

Bootstrap a starter document from your existing code:

python manage.py djust_audit --dump-permissions > permissions.yaml

Review the generated file carefully. Views with existing permission_required attributes will be pre-populated; views with login_required only will get a TODO note asking you to confirm the intended role model; views with no auth will be marked public: true with a TODO asking you to confirm that's intentional.

Then commit the file.

Schema

# permissions.yaml
# Expected permissions per LiveView. djust_audit --permissions permissions.yaml
# fails if the actual code deviates from this document.

version: 1
strict: true  # require every LiveView to be declared

views:
  # ---------- intentionally public ----------
  apps.public.views.Home:
    public: true
    notes: "Landing page — confirmed public after security review"

  apps.intake.views.IntakeWizardView:
    public: true
    notes: "Public intake wizard for anonymous users (RFP FR-I-a)"

  # ---------- authenticated, role-scoped ----------
  apps.claims.views.ExaminerDashboardView:
    login_required: true
    permissions: ["claims.view_examiner_dashboard"]
    roles: ["Examiner", "Supervisor", "Director"]

  apps.claims.views.ClaimDetailView:
    login_required: true
    permissions: ["claims.view_claim"]
    roles: ["Claimant", "Examiner", "Supervisor"]
    # Document the intended object-level scoping. djust can't verify role
    # membership or object ownership at static-analysis time, but listing
    # the fields makes the intent reviewable.
    object_scoping:
      fields: ["claimant.email", "assigned_examiner"]

Per-view keys

KeyTypeDescription
publicboolView is intentionally accessible without auth. Mutually exclusive with login_required / permissions.
login_requiredboolDeclares that the view requires auth. Satisfied by any auth in code: login_required, permission_required, a custom check_permissions(), or an auth dispatch mixin.
permissions[str]Matches cls.permission_required (list or single string, normalized to list).
roles[str]Documentation only — djust cannot verify Django group membership via static analysis.
object_scoping.fields[str]Documents which object-level fields the view checks for ownership. Currently informational; may be promoted to AST-verified in a later release.
notesstrFree-form documentation shown in error/diff output.

Top-level keys

KeyTypeDefaultDescription
versionint1Document schema version. Only 1 is supported.
strictbooltrueWhen true, views found in code but not declared in the document are reported (P002). Whether a finding fails the command is controlled by the --strict CLI flag.
viewsmapping(required)Mapping of dotted view path → per-view declaration.

Findings

Each deviation between the document and the code produces a finding with a stable error code:

CodeSeverityMeaning
djust.P001errorView declared in permissions.yaml but not found in code (stale entry).
djust.P002errorView found in code but not declared in permissions.yaml (strict mode only).
djust.P003errorDocument says public: true but code has auth configured.
djust.P004errorDocument says auth required but code has none (no login_required, permission_required, custom check_permissions() or auth mixin).
djust.P005errorPermission list in permissions.yaml does not match cls.permission_required.
djust.P006warningReserved: not emitted yet. object_scoping is stored but not checked.
djust.P007inforoles declaration — djust cannot verify at static-analysis time, treated as documentation.

CLI

# Human-friendly terminal output
python manage.py djust_audit --permissions permissions.yaml

# Machine-readable, fails CI on any error/warning
python manage.py djust_audit --permissions permissions.yaml --strict --json > audit.json

# Bootstrap a starter permissions.yaml from current code
python manage.py djust_audit --dump-permissions > permissions.yaml

CI integration

# .github/workflows/ci.yml
- name: Audit permissions document
  run: |
    python manage.py djust_audit \
      --permissions permissions.yaml \
      --strict \
      --json > /tmp/audit.json

Any new view added without a corresponding permissions.yaml entry fails the build. A developer can't "forget RBAC" — they can't even merge the code.

What this catches

  1. "Forgot the decorator" — new view added without updating permissions.yaml fails CI.
  2. "Decorator drift" — permission_required = ['claims.view_claim'] changed to something else fails CI unless the document is also updated.
  3. Stale declarations — a view removed from the codebase but still in the document is flagged.
  4. Mismatched public/auth intent — a view declared public but actually has auth, or vice versa.
  5. Auditable artifact — a security reviewer can read permissions.yaml in five minutes and sign off on the entire app's permission model.

What this does NOT catch

  • Role membership — roles: ["Examiner"] is documentation. djust cannot verify Django group membership at static-analysis time; that still requires runtime enforcement in the view.
  • Object-level access control — object_scoping.fields is currently informational. Claim.objects.get(pk=...) in get_object() without a filter(assigned_examiner=request.user) clause is not automatically detected. Use AST-based anti-pattern detection (see #660) for that class of check.
  • Business-logic authorization — whether a user should be allowed to perform a specific action on a specific record remains application code. The document describes framework-level auth config, not app-level policies.

References

  • #657 — original feature issue
  • OWASP A01:2021 Broken Access Control
  • CWE-285 Improper Authorization
  • NIST SP 800-53 AC-3 Access Enforcement