# Phase 1B.7: Organization, RBAC and authorization security audit

Audit date: 2026-09-26. Reviewed baseline: `4fa8239` (228 tests).
Scope: accounts, organization, access, admin, configuration, migrations,
dependencies, documentation, and repository hygiene. No business features,
authentication backend replacement, schema changes, or locking redesign.

## Executive result

No demonstrated runtime defect requiring a code fix was found in the reviewed
supported operations. Added 14 attack/regression tests and corrected an outdated
architecture statement that incorrectly listed RBAC as unimplemented.
Final full-suite verification: 242 total, 242 passed, 0 failed, 0 skipped.
All original 228 tests remain unchanged and passed; 14 audit tests were added.

This is a bounded source and automated-test audit, not a penetration test, formal
proof, production deployment certification, or comprehensive dependency CVE scan.

## Findings by severity

No CRITICAL or HIGH findings were identified. No MEDIUM findings were identified.

| Severity / status | Issue and affected component | Impact | Remediation / disposition | Verification |
| --- | --- | --- | --- | --- |
| LOW / documented | Production HSTS omits subdomains and preload (`security.W005`, `security.W021`). | Browser HSTS coverage does not extend automatically to subdomains or first-visit preload. | Retain the existing host-only policy until actual domain ownership and HTTPS coverage are established; review before deployment. No warnings suppressed. | Production deploy check exits 0 with exactly these two warnings. |
| INFO / fixed | `docs/ARCHITECTURE.md` said RBAC was unimplemented. | Misleading guidance for future development. | Corrected the statement to reflect the implemented foundation. | Documentation diff reviewed. |
| INFO / limitation | Django records migration application names/times, not source checksums. | Absolute proof of migration bytes at application time is unavailable. | Retain immutable migrations and Git history; no reset or squash. | Git history shows each project migration only being added, with no subsequent modifications; loader history and graph checks pass. |
| INFO / deployment constraint | `DB_PASSWORD` is read exclusively from a local dotenv file, without interpolation. | An environment-only secret injection deployment is not currently supported. | Preserve the documented bootstrap contract; provision a separate protected deployment `.env`, never the development file. Other database fields and Django secret support process environment overrides. | Inspected base settings, README and development documentation; no credential values reported. |
| INFO / limitation | Direct dependencies are pinned; transitive dependencies are not fully locked. | Exact transitive resolution may differ between installations. | Preserve the current dependency approach in this phase; no broad upgrade performed. | Requirement files inspected; `pip check` passes. |

## Architecture and authorization

Accounts owns only the UUID `AbstractUser` extension and native `UserAdmin`.
Organization owns hierarchy, assignment validation and lifecycle services. Access
owns global roles, Django Permission memberships, scoped role assignments, and
business authorization. There is no custom auth backend or role field on User.

Organization does not import access. Its two synchronous lifecycle signals are
registered by `AccessConfig.ready()` using dispatch UIDs. Receiver failures propagate
inside the originating transaction. Assignment model registration from models.py
uses string foreign keys and a shared model base; admin registration has an explicit
module import. These are understandable Django registration boundaries, not a
hidden organization/access import cycle. The access app must remain installed so
its lifecycle receivers are registered.

WHO is the persisted active user; WHAT is a permission on an active role; WHERE is
the canonical organizational assignment attached to that same active role assignment.
Correlated SQL `Exists` predicates couple these on one path. The separate permission
inventory is not an authorization decision. Staff, Groups and direct permissions
retain native Django semantics and supply no business scope. Primary status grants
no business privilege. Active superusers bypass role/scope requirements for saved,
supported targets, including inactive records; malformed, missing and ambiguous
permission identifiers still deny. Inactive superusers deny.

## Integrity and attack coverage

Existing tests are retained, including deliberate raw writes used to attack database
constraints and defense-in-depth authorization predicates.

| Area | Evidence and result |
| --- | --- |
| Organization ownership | Two-tree tests reject company ownership changes for regions, centers and departments, foreign-region moves and mixed-company assignment dimensions. |
| Active hierarchy | Tests reject active children under inactive parents, active scopes under inactive units, inactive primaries, and new/reactivated role assignments with inactive dependencies. |
| Nullable scope duplicates | All six supported nullable scope shapes are exercised through model validation and database uniqueness attacks; distinct scopes remain possible. |
| Primary invariant | Sequential collisions, atomic primary switching, rollback, deactivation clearing, reactivation without promotion, and concurrent database collisions are covered. |
| Role duplicates | Active user/role/scope duplicates fail at model and database levels; inactive historical duplicates remain allowed. |
| Same-path coupling | Forward/reverse center, region, department, combined-scope and cross-company mixing attacks deny; inactive roles and either assignment layer cannot donate permissions to another active scope. |
| Querysets | All five supported target models are covered, including assignment targets belonging to another user. Tests cover multiple roles/companies, user states, native permissions, caller filters and ordering, point/filter parity and no duplicate targets. |
| Unsupported inputs | Sliced and union/intersection/difference querysets return no rows, including for superusers. Invalid permission and unsupported target tests remain in the baseline. |
| Admin bypass | Forms reject hierarchy deactivation requiring cascade, invalid scopes and role assignments; ownership is readonly; primary changes use the service action. Permission saving delegates to its service and rolls back atomically on failure. |
| Reactivation | Service tests and the added top-down admin test confirm parents/units/assignments do not restore ended role paths. Parent-state and duplicate validation still apply. |

## Lifecycle and history

Company deactivation ends its active regions, centers, departments, organizational
assignments and role assignments, clearing primary flags. Region and leaf cascades
are scoped to affected descendants/assignments. Role and assignment deactivation
end their attached role paths. Existing tests verify unrelated scopes/trees remain
active and injected failures roll back cascades and permission replacement.

Normal lifecycle operations retain rows. Foreign keys use PROTECT to preserve
referenced parents and assignment history; admin disables assignment deletion.
Unreferenced organization/role records can still be deleted, and ORM/raw deletion
is not globally disabled. Scope corrections remain editable: these records are
not an immutable audit log. User deactivation intentionally preserves assignment
flags/history; authentication and authorization independently deny that user.

## Transactions, bulk writes and locking

Reviewed atomic services, model-save validation and PostgreSQL lock helpers:

- Organization writes take the company UPDATE lock. Cascades lock affected
  organizational assignments in UUID order before dispatching access lifecycle events.
- Organizational assignment writes take the stable user UPDATE lock, then sorted
  company SHARE locks. Primary services include the current-primary company before
  demotion/promotion. Service operations reload state after waiting.
- Role assignment writes take the user UPDATE lock, sorted organization-assignment
  SHARE locks, then sorted Role SHARE locks before validation/persistence.
- Role edits, permission replacement and lifecycle use a Role UPDATE lock.
  Ordinary role assignment creation does not take a company lock.

Production bulk operations found by source search are limited to organization
cascades, primary demotion, the synchronous role cascade receiver and role
lifecycle services. They are scoped and timestamped, inside the owning transaction
and lock protocol. No production `bulk_create`, `bulk_update`, admin bulk update,
or unrelated lifecycle-bypassing update was found. Test raw writes are intentional
constraint/corruption attacks. No production debug prints, custom credential logs,
or CSRF exemptions were found.

The baseline covers 19 distinct PostgreSQL concurrency cases: hierarchy lifecycle/movement,
primary and nullable uniqueness races, company/department deactivation versus scope
creation, role/scope deactivation versus role creation in both orders, duplicate
role assignments, and compatible narrower writes. Two added tests cover Region
deactivation versus organizational assignment creation in both orders. The tests
use separate database connections and inspect `pg_blocking_pids()` for expected
blocking. PostgreSQL reports server version 18.6.

No concrete lock-order defect requiring modification was demonstrated. This does
not establish formal deadlock freedom or validate arbitrary caller-composed
transactions, additional pre-acquired locks, or every possible interleaving.

## Authentication, sessions and HTTP

Native authentication remains configured. Audit HTTP tests verify active login,
wrong-password/inactive denial, staff-versus-nonstaff admin login, absence of model
access for staff without model permissions, UUID stability, hashing and safe user
representation. Native admin password changes hash passwords and invalidate another
session; admin forms do not display plaintext passwords. User administration retains
Django's powerful native permissions and must be limited to trusted administrators.
No custom request-to-model mass assignment or custom login endpoint exists.

CSRF, session and authentication middleware are present. Tests use
`Client(enforce_csrf_checks=True)` to reject unprotected login, role creation and
logout POSTs; a valid CSRF token permits an authorized role creation. Logout uses
POST and flushes the session; GET does not log out. Stored user deactivation blocks
subsequent admin requests. Admin actions remain gated by native model change
permissions, not the business scope filter.

## Production configuration and dependencies

Production forces DEBUG off, validates a strong secret and explicit non-wildcard
hosts, configures trusted CSRF origins from environment, redirects HTTP to HTTPS,
sets secure CSRF/session cookies, sets nosniff and DENY framing, and enables one year
of host-only HSTS. The default session cookie is HttpOnly. Django traceback pages
are disabled with production DEBUG=False. Missing configuration errors name fields
without including secret values. Static root and URL are configured separately
from source assets; real static/media serving and application server deployment
remain operational tasks. Proxy headers are not trusted by default.

Deploy checks used a randomly generated temporary process-only secret and
`audit.invalid` host/origin placeholders. No placeholder was stored or treated as
an actual production hostname. The existing local DB configuration was not printed.
Actual TLS, proxy, hostnames, filesystem permissions and production credentials
must be validated at deployment; no code-attributable deployment blocker was found
for the documented configuration contract.

Runtime dependencies are Django 5.2.17, psycopg[binary] 3.3.6 and python-dotenv 1.2.3.
Development/production requirement files extend base without extra tooling.
PostgreSQL is the only configured backend; no unnecessary service dependencies were
added. No package upgrade or claim of a current comprehensive advisory clearance.

## Secret and Git hygiene

`.env` and `.venv` are ignored and untracked; `.env.example` is tracked with secret
placeholders only. Reviewed tracked files/settings/docs/test credentials, scanned
for local credential matches and common private-key/token/credential-URL patterns.
Substring coincidences in framework labels and ordinary documentation are not
credential assignments. No committed credential or generated artifact was identified
in the reviewed current tree. Test passwords are explicit test-only fixtures.
This is not a forensic guarantee about deleted files or all external Git history.
No secret values are included in this report. No commit, staging, reset or push.

## Migrations

Four project migrations were inspected: accounts.0001, organization.0001,
organization.0002, access.0001. All contain real operations and correct auth/user/
organization dependencies. The loader reports no conflicts and consistent applied
history. All framework and project migrations are applied; no pending model change
or new migration. Git migration history contains additions only. Application-time
byte identity cannot be independently proven because Django stores no checksums.

## Known limitations and future writes

The implementation targets one default PostgreSQL database and Read Committed.
Cross-table invariants depend on supported validated model/service writes; arbitrary
raw/bulk writes bypass them. Admin is a trusted global administration plane using
native model permissions, not tenant-scoped business administration. Batch actions
commit each selected operation independently. No immutable audit trail is provided.

Authorization is a statement snapshot. An already evaluated queryset can be stale;
a successful boolean check does not lock authorization for a later mutation.

> Security-sensitive writes must establish authorization and relevant business
> invariants within an appropriate transactional operation.

Future domains must choose and test their transactional authorization protocol;
no speculative infrastructure is introduced here. New target models require
explicit reviewed adapters and appropriate intersection semantics.

## Verification commands and results

Commands used the existing `.venv/Scripts/python.exe` from the project root.

| Command / check | Result |
| --- | --- |
| `python manage.py check` | No issues. |
| `python manage.py check --deploy --settings=config.settings.production` | Exit 0; W005 and W021 only, no silencing. Temporary process environment described above. |
| `python manage.py makemigrations --check` | No changes detected. |
| `python manage.py showmigrations` | All migrations checked as applied. |
| `python manage.py migrate` | No migrations to apply. |
| MigrationLoader history/conflicts/empty-operation inspection | Consistent; no conflicts; no empty project migrations. |
| `python -m pip check` | No broken requirements found. |
| `python manage.py test apps.access.test_security_audit --noinput` | 14 passed, 0 failed, 0 skipped. |
| `python manage.py test --noinput` | 242 total, 242 passed, 0 failed, 0 skipped; 72.554 seconds. |
| `git diff --check` | Clean. |
| `git status`, `git diff`, `git diff --stat`, `git log --oneline -7` | Reviewed; intended tests/docs only, no staged changes. |
| `git log --oneline --name-status -- apps/*/migrations/*.py` | Project migration additions only; no later modifications in available history. |
| `git ls-files`, `git check-ignore`, credential-pattern checks | No tracked environment, credential or generated artifacts identified. |

## Freeze gate

No unresolved CRITICAL/HIGH finding; full regression, migrations, isolation,
same-path coupling, lifecycle, concurrency, admin and secret-hygiene gates passed.
The LOW deployment warnings and INFO limitations above are explicitly retained.
No code-attributable deployment blocker was found for the documented deployment
configuration. Scope is the Phase 1B foundation only; no next phase was started.

PHASE 1B VERIFIED - READY TO FREEZE
