# RBAC foundation (Phase 1B.4)

## WHAT and WHERE

apps.access owns global Role definitions and UserRoleAssignment records. A Role is
a named collection of native Django Permission objects: WHAT capabilities might be
available. A role assignment references the existing organization-owned
UserOrganizationAssignment: WHERE the user operates. It stores no duplicate company,
region, center or department columns. Accounts/User has no role field or schema change.

Access depends on organization and Django auth. Organization does not import access.
Two explicit synchronous Django signals in organization.lifecycle_events allow access
to validate assignment deactivation and participate in supported lifecycle transactions.
AccessConfig.ready() registers receivers with dispatch UIDs. This is a small integration
boundary, not a background queue, generic policy engine, or automatic save cascade.

## Models and database rules

Role has UUID v4 id, required code/name, optional description, active flag (True by
default), permissions M2M to auth.Permission and timestamps. Codes are trimmed and
uppercased before validation, maximum 64 characters, ASCII letters/digits/hyphens/
underscores, beginning with a letter or digit. access_role_code_format rejects
noncanonical database writes and access_role_code_uniq enforces global uniqueness.
Codes are business identifiers, not primary keys or application branches. Validated
corrections remain possible. Names/descriptions are not normalized by model code.
No company-specific role definitions, role-name logic or seed roles are introduced.

UserRoleAssignment has UUID v4 id, User, Role, UserOrganizationAssignment, is_active
(default True), and timestamps. All FKs use PROTECT to retain history. Many roles
per user/scope and one role across different scopes are supported. User must match
the organizational assignment's user and cannot be transferred after creation.
Role/scope corrections remain possible through validated writes; use deactivation
and a new record when preserving the previous posting matters. This is not an audit log.

access_active_role_assignment_uniq is a partial unique index on user, role and
organization_assignment where is_active is True. Inactive historical duplicates
are allowed. Reactivation checks active dependencies and duplicates again. There
is no useful additional same-row active-state CHECK: parent states and user equality
are cross-table rules and cannot be ordinary PostgreSQL CHECK constraints.

The existing small organization TimeStampedModel is reused; no changes to existing
master tables or migrations are needed. UUID primary keys and FK/unique indexes
support the current lookups without speculative indexes.

## Validation and lifecycle

Every ordinary role/role-assignment save runs full_clean(). Partial saves also
validate the effective persisted combination, preventing unsaved fields from hiding
an invalid partial write. Forms perform the same checks. Active role assignments
require a currently active User, Role and organizational assignment. Inactive records
may reference inactive parents but must still match the user. Validation reads fresh
database state rather than trusting cached parents.

```python
from apps.access.services import (
    create_role_assignment, deactivate_role_assignment, reactivate_role_assignment,
    deactivate_role, reactivate_role, set_role_permissions,
)

assignment = create_role_assignment(user=user, role=role,
                                    organization_assignment=organization_assignment)
role = set_role_permissions(role=role, permissions=[permission])
assignment = deactivate_role_assignment(assignment=assignment)
role = deactivate_role(role=role)
```

Services accept persisted instances, reload current state and return fresh records.
They do not persist pending unsaved edits on their input objects.

- deactivate_role atomically ends its active role assignments and deactivates the role.
  Unrelated roles/scopes remain untouched. Direct Role.save() deactivation with active
  assignments is rejected with an instruction to use the lifecycle service/action.
- deactivate_role_assignment changes only that role assignment. Reactivation is
  explicit and validates all dependencies again.
- Explicit organizational-assignment deactivation ends attached active role assignments
  before ending the organizational assignment and clearing its primary flag.
- Company/region/center/department lifecycle cascades reach those same role assignments
  through the organization integration hook, inside the original transaction.
- Direct organizational-assignment deactivation with active role assignments is
  rejected; it does not silently run a cascade from save().
- Reactivating a role, organizational unit or organizational assignment NEVER restores
  role assignments automatically. Records and inactive history remain in place.

Organization services lock affected organizational-assignment rows before dispatching
assignments_deactivating. The access receiver ends active attached role assignments
and updates timestamps. Receiver errors propagate and roll back the entire operation;
there is no best-effort or asynchronous delivery. assignment_validating performs only
validation, with no cross-record side effects.

User deactivation has no hidden cascade: existing role assignment flags/history remain,
no organizational units are deactivated, and no new active role assignment may be
created for that user. The permission collection API always checks stored User.is_active.
The Phase 1B.5 [authorization engine](AUTHORIZATION.md) also requires active users
at the decision boundary.

## Permission management and query foundation

Role.permissions uses Django's Permission table and its existing content types; no
parallel permission registry, future permissions or custom authentication backend exists.
set_role_permissions accepts saved, existing Django Permission objects, replaces the
membership atomically under the role lock, and updates the timestamp. The native M2M
FK protects references; an invalid replacement cannot discard prior memberships.
Admin provides Django's searchable horizontal permission selector. Newly installed
apps can supply their normal Django permissions later.

```python
from apps.access.queries import business_permissions_for_user
permissions = business_permissions_for_user(user)  # QuerySet of Permission objects
```

This is an UNSCOPED inventory of permissions across active role assignments and roles
attached to active organizational assignments. It queries current database user state,
returns nothing for inactive/anonymous users, excludes mismatched user links even if
introduced by unsupported raw writes, and deduplicates permissions across roles/scopes.
It does not cache permissions on User or inspect role names.

Do not use this union as an authorization check for an object, company or center.
It does not expand organizational scopes, resolve access to business records or check
arbitrary parent-unit corruption. The separate Phase 1B.5 [authorization engine](AUTHORIZATION.md) provides point
decisions and database scope filtering. This inventory API itself performs no such
authorization.

Django user.has_perm(), Groups, direct user_permissions, is_staff and is_superuser are
unchanged. Adding a business role does not itself grant Django Admin access or populate
Django groups/direct permissions. Native superusers remain system administrators and
need no organizational assignment. The business-permission inventory gives an unassigned
superuser no special union/bypass; the separate authorization engine defines its
explicit superuser bypass in AUTHORIZATION.md. The inventory excludes
native group/direct permissions by design.

## Transactions and locking

The protocol targets the existing single default PostgreSQL database with Read Committed:

1. Ordinary role-assignment writes lock the User row FOR UPDATE, then protect old/new
   organization-assignment rows FOR SHARE in UUID order, then old/new Role rows FOR SHARE
   in UUID order. Validation and persistence run in one atomic transaction.
2. Role lifecycle, role edits and set_role_permissions lock only the target Role row
   FOR UPDATE. Role deactivation updates attached assignments before changing the role.
3. Direct organization-assignment lifecycle follows its existing user/company protocol,
   then locks its assignment row FOR UPDATE before the access notification. Hierarchy
   cascades retain the company lock and lock affected assignment rows in UUID order.
4. Access lifecycle receivers perform scoped role-assignment updates without acquiring
   user/role locks, while the originating operation holds assignment-row locks.

Shared parent locks conflict with deactivation's exclusive locks but allow distinct
users' role writes to share a role concurrently. Ordinary RBAC creation does NOT lock
Company. Per-user writes serialize; the database partial unique index remains the final
protection for duplicate writers that bypass model/service validation. All dependent
locks are acquired before ordinary role-assignment writes. Caller-created multi-step
transactions with additional/pre-acquired locks require their own lock-order review;
this is not a claim of universal deadlock freedom.

Nine new TransactionTestCase concurrency tests use separate PostgreSQL connections and
observe pg_blocking_pids() for expected waits: duplicate raw insert collision; role
and organization-assignment deactivation versus creation in both orders; company
cascade versus creation in both orders; compatible distinct-user writes; and creation
while another connection holds a company lock. The last two demonstrate narrower
coordination rather than assuming it from timing alone. Rollback tests cover role,
organization, assignment and permission-replacement operations.

## Admin, history and limitations

Role and UserRoleAssignment have list displays, filters, search and normal Django
permission checks. Roles expose the native permission selector; assignments use
autocomplete for user/role/scope. Existing assignment users are read-only. Explicit
admin lifecycle actions call services; role-assignment deletion is disabled. Role
and parent deletion are protected while history references them. Normal lifecycle
is deactivation, without a generic soft-delete framework. ORM/raw deletion is not
globally prohibited. Batch admin actions commit each selected service operation
independently rather than promising all-or-nothing behavior across the selection.

QuerySet.update(), bulk_create(), bulk_update(), fixture loading and raw SQL bypass
cross-table validation and locking. The database still enforces canonical role codes,
uniqueness and foreign keys, but not User/assignment equality or parent active states.
Only the reviewed service bulk updates are supported for lifecycle operations. The
organization hooks must stay registered when access is installed. No seeded roles,
future business permissions or additional business models are introduced here.
Phase 1B.5 authorization is documented separately in AUTHORIZATION.md.

## Migration and verification

access.0001_initial creates Role, its Permission M2M table, UserRoleAssignment, indexes
and constraints. Dependencies are organization.0002, Django auth and AUTH_USER_MODEL.
No previous migration is modified and no business data is seeded. Django generates
its normal model permissions for the new models during migrate.

```powershell
python manage.py check
python manage.py makemigrations --check
python manage.py migrate
python manage.py test --noinput
python manage.py showmigrations access
python manage.py sqlmigrate access 0001
```

The nine access concurrency tests run automatically with the full suite, or separately
with `python manage.py test apps.access.test_concurrency --noinput`. They use Django's
isolated PostgreSQL test database and existing test privileges, with no extra configuration.
