# Phase 6 implementation report

**Status:** Implemented against the approved `docs/PHASE6_PLAN.md` at `b1e1d6a46e2c0aa5588937f8966543c5218d7a13`, including **P6-D1–P6-D12**.  
**Checkpoint I:** `34ac7d9d1f73a4c7c3ee3f408ca8b85da23b4414`.  
**Final correction:** `146baa157c2cf861be103f3d2ae7aebea064529d` (IAM user-list timestamps displayed in Asia/Kolkata).  
**Final approved implementation HEAD:** `146baa157c2cf861be103f3d2ae7aebea064529d`.  
**Suite at that HEAD:** **313 tests / 10,394 assertions / 0 failures** (`vendor/bin/phpunit --no-coverage`, independent rerun at the final implementation HEAD; not reused from Checkpoint I).  
**Stack:** CodeIgniter 4, PHP 8.2+, MySQL 8 InnoDB, server-rendered Views, CI4 server-side sessions, Argon2id, CSRF, active-role-only permission evaluation.  
**Phase 7:** not started. No Phase 7 plan was created.

---

## 1. Files changed

### Added

- `app/Database/Migrations/2026-09-14-600001_AddIamPhase6Columns.php`
- `app/Controllers/{IamUserController,IamRoleController,PasswordChangeController}.php`
- `app/Filters/PasswordChangeGateFilter.php`
- `app/Modules/IAM/Domain/{IamCopy,IamEvents,PermissionGroups}.php`
- `app/Modules/IAM/Services/{AntiEscalation,TemporaryCredentialService,UserAdminService,RoleAdminService}.php`
- `app/Modules/Ui/Phase6Frames.php`
- `app/Views/pages/iam/users/{list,form}.php`
- `app/Views/pages/iam/roles/{list,form}.php`
- `app/Views/pages/auth/password.php`
- `app/Views/pages/audit/_detail.php`
- `docs/reference/RMC_Crusher_ERP_Phase1_Figma_Frame_Component_Manifest_v1.1.csv`
- `docs/reference/Approved-Frame-Name-Aliases.md`
- `docs/reference/SOURCE_CHECKSUMS.md`
- `docs/PHASE6_IMPLEMENTATION_REPORT.md` (this file)
- Tests: `Phase6SchemaTest`, `Phase6IamDomainTest`, `Phase6CredentialHttpTest`, `Phase6UserFrameHttpTest`, `Phase6UserMobileFrameHttpTest`, `Phase6RoleFrameHttpTest`, `Phase6RoleMobileFrameHttpTest`, `Phase6AuditCompletenessTest`, `Phase6SessionSecurityTest`, `Phase6FramesTest`, `Phase6CloseoutTest`, `PermissionGroupsTest`

### Changed

- `app/Config/Routes.php` — IAM users/roles, password-change, health `phase: 6`; no DELETE routes
- `app/Config/Filters.php` — `passwordGate`
- `app/Controllers/AuditController.php` — eight filters, scope 403, unauthorised export **403**
- `app/Modules/Audit/Services/AuditQuery.php` — eight filters/columns, changed-field decorate, export CSV
- `app/Modules/Auth/Services/AuthService.php` — D2 expiry, password change, no secrets in audit
- `app/Modules/Auth/Controllers/LoginController.php` — first-login redirect
- `app/Modules/Ui/FrameSimulation.php` — Phase 6 overlay; production still ignores `?frame=`
- `app/Views/pages/audit/history.php`, nav sidebar, `public/css/app.css`, `public/js/app.js`
- `README.md`

No `full_name`, `role_code`, JWT, Shield, SMTP, React, Company Admin, or new permission codes.

---

## 2. Source documents and checksums

Copied **without modification** on **2026-09-14** (UTC). See `docs/reference/SOURCE_CHECKSUMS.md`.

| Original filename | Repository filename | SHA-256 |
|---|---|---|
| `uploads/RMC_Crusher_ERP_Phase1_Figma_Frame_Component_Manifest_v1.1_4e22.csv` | `docs/reference/RMC_Crusher_ERP_Phase1_Figma_Frame_Component_Manifest_v1.1.csv` | `314b31b7625b5406515262c1189d9d7b8970ad36677df503a1ace32b714edde7` |
| `uploads/Approved-Frame-Name-Aliases_61ba.md` | `docs/reference/Approved-Frame-Name-Aliases.md` | `1ff992bd1a39f8b48a44d16e0e5e6e3b9780f306e0254e490d7427d02efc07c8` |

`cmp` confirmed byte-identical copies.

---

## 3. Migration up/down evidence

Migration `2026-09-14-600001_AddIamPhase6Columns`:

- `up()` adds only missing columns (verified via `information_schema`, not CI4 field-name cache).
- `down()` drops only Phase 6 columns/indexes/check constraints.
- Live `rmc_erp` after migrate: `email` VARCHAR(254) NULL UNIQUE, `employee_code` VARCHAR(20) NULL UNIQUE, `must_change_password` TINYINT(1) NOT NULL DEFAULT 0, `temporary_password_expires_at_utc` DATETIME(6) NULL, `app_user.row_version` INT UNSIGNED NOT NULL DEFAULT 1; `role.description` TEXT NULL, `is_active` TINYINT(1) NOT NULL DEFAULT 1, `role.row_version` INT UNSIGNED NOT NULL DEFAULT 1.
- No `full_name`, no `role_code`. Existing password hashes and `role_id` values unchanged. Seeded users keep NULL email.
- PHPUnit: `Phase6SchemaTest` runs `down()` then `up()` in-test and asserts user rows, hashes, and `role_id` values are preserved.

---

## 4. Final schema

Additive only. Phase 0–5 FKs and indexes remain.

**`app_user` Phase 6 columns:** `email` (nullable unique; multiple NULL valid), `employee_code` (nullable unique; multiple NULL valid), `must_change_password` (CHECK 0/1), `temporary_password_expires_at_utc`, `row_version`. Display name remains `display_name`.

**`role` Phase 6 columns:** `description`, `is_active` (CHECK 0/1), `row_version`. Identity remains immutable `role_id` plus unique `role_name`.

---

## 5. Route / controller / service / permission matrix

| Method | Path | Permission | Controller | Notes |
|---|---|---|---|---|
| GET | `/iam/users` | `IAM.VIEW` | `IamUserController` | Scope overlap |
| GET | `/iam/users/create` | `IAM.MANAGE_USERS` | | |
| POST | `/iam/users` | `IAM.MANAGE_USERS` | | D2 one-time password |
| GET | `/iam/users/{id}` | `IAM.VIEW` | | SYSTEM hidden |
| GET | `/iam/users/{id}/edit` | `IAM.MANAGE_USERS` | | S. Patil is Edit-Default |
| POST | `/iam/users/{id}` | `IAM.MANAGE_USERS` | | `row_version`; no silent replacements |
| POST | `/iam/users/{id}/deactivate` | `IAM.MANAGE_USERS` | | Not self; not last SA; `AUTH_USER_DISABLED` |
| POST | `/iam/users/{id}/reactivate` | `IAM.MANAGE_USERS` | | Grants not restored |
| POST | `/iam/users/{id}/revoke-sessions` | `IAM.MANAGE_USERS` | | `AUTH_SESSIONS_REVOKED` |
| POST | `/iam/users/{id}/temporary-password` | `IAM.MANAGE_USERS` | | New D2 credential |
| GET | `/iam/roles` | `IAM.VIEW` | `IamRoleController` | |
| GET | `/iam/roles/create` | `IAM.MANAGE_ROLES` | | |
| POST | `/iam/roles` | `IAM.MANAGE_ROLES` | | Hybrid C subset |
| GET | `/iam/roles/{id}` | `IAM.VIEW` | | |
| GET | `/iam/roles/{id}/edit` | `IAM.MANAGE_ROLES` | | Billing Clerk is Edit-Default |
| POST | `/iam/roles/{id}` | `IAM.MANAGE_ROLES` | | |
| POST | `/iam/roles/{id}/deactivate` | `IAM.MANAGE_ROLES` | | P6-D9 |
| GET/POST | `/account/password` | authenticated + D2 gate | `PasswordChangeController` | Not a CSV frame |
| GET | `/audit` | `AUDIT.VIEW` | `AuditController` | Eight filters; inaccessible scope **403** |
| POST | `/audit/export` | `AUDIT.EXPORT` | | Same filters; missing permission **403** |
| POST | `/session/switch-role` | held role | existing | `IAM_ROLE_SWITCH` + history |

Services: `UserAdminService`, `RoleAdminService`, `AntiEscalation`, `TemporaryCredentialService`, `AuditQuery`, `AuthService`.  
No new permission codes. No DELETE routes. Every POST is CSRF-protected and server-authorised.

---

## 6. Anti-escalation implementation (Hybrid C)

`AntiEscalation` evaluates **only the editor’s active role**. Inactive assigned roles do not union. Submitted checkboxes and multi-selects are recalculated server-side.

- Ungrantable permission reason: `Disabled — Permissions above your administrative level. A role cannot grant permissions it does not already hold.`
- Inaccessible scope reason: `Disabled — You cannot grant Company or Plant access outside your own administrative scope.`
- Super Admin assignable only when the editor’s **active** role is Super Admin. SYSTEM is omitted from pickers. Authority is not inferred from `role_id`, name, or `is_system`. No Company Admin. Unauthorised POST → **403**.

---

## 7. User credential flow (P6-D2)

Cryptographically secure plaintext is generated, Argon2id-hashed, and shown **once** via an encrypted consume-once cache token (`?issued=`), not session flash. `Cache-Control: no-store`. Refresh/back does not redisplay. List/view/edit never return the plaintext.

Until change: only `/account/password` and logout. Other module GETs redirect; other POSTs **403**. Expiry 24 hours; expired temporary password fails login with the generic error. Successful change clears `must_change_password` and `temporary_password_expires_at_utc` and increments `session_version`. Regeneration invalidates the prior hash and all sessions. No SMTP.

---

## 8. Session-invalidation matrix

Mechanism remains `app_user.session_version` (no JWT). Affected rows are locked `ORDER BY user_id ASC FOR UPDATE`. Stale browser request → login redirect; AJAX → **401**; ordinary permission/scope denial → **403**. `AUTH_SESSION_REJECTED` is written. Role-switch history is written only for actual active-role switches.

| Change | `session_version` |
|---|---|
| Deactivate / reactivate | Yes |
| Assigned role add/remove | Yes |
| Active role replacement (explicit) | Yes |
| Company grant add/remove | Yes |
| Plant grant add/remove | Yes |
| Permission add/remove on a role (enabled users with that `active_role_id`) | Yes |
| Password change / temp regenerate / revoke sessions / active-role switch | Yes |
| Profile text only (`display_name`, email, employee_code) | No |

---

## 9. Last Super Admin and SYSTEM protection

≥1 enabled interactive (`is_system=0`) user whose **active** role is Super Admin. SYSTEM does not count and cannot be listed, edited, assigned, or used for interactive login. Super Admin role cannot be deactivated. The invariant is rechecked under locks immediately before commit. Self-deactivation is rejected. Tests use extra fixture Super Admins; the seeded `n.joshi` row is not permanently destroyed.

---

## 10. IAM-SCR-001 behaviour

User list (default/filtered/empty/loading), create, view, edit, deactivate, reactivate, revoke sessions, temporary-password regeneration, optimistic concurrency, immutable audit-history link.

Fields: `display_name`, email, employee_code, Roles / Companies / Plants Multi-Select (exactly those three groups; no Super Admin Multi-Select), status, Created, Last modified.

S. Patil demonstrates Store Keeper + Billing Clerk with exactly one active role. Copy uses **Billing Clerk**, never Billing Manager. Active user needs ≥1 assigned role; `active_role_id` must belong to that set; no automatic replacement. Plant grants must sit under selected companies. No physical delete.

---

## 11. IAM-SCR-002 behaviour

Role list (default/empty/loading), create, view, edit, permission search, seven accordions, Expand all / Collapse all, deactivate where permitted, optimistic concurrency, audit-history link.

Groups exactly: Masters; Material; Challans; Invoices; Credit Notes and IRN; Reports; Administration. Collapsed summaries show selected counts. Ungrantable permissions remain visible and disabled. `role_id` immutable; no `role_code`; unique `role_name`; rename does not rewrite historical audit JSON. Cannot deactivate while it is `active_role_id` for an enabled user. Permission changes invalidate those users in ascending `user_id` order in the same transaction as history and audit.

---

## 12. AUD-SCR-001 completeness

Existing six AUD frames only. Canonical names unchanged. Approved aliases preserved as metadata only:

- `D-AUD-SCR-001-Default` → `D-AUD-SCR-001-View-Default`
- `D-AUD-SCR-001-DetailExpanded` → `D-AUD-SCR-001-View-Filtered`
- `D-AUD-SCR-001-ExportRestricted` → `D-AUD-SCR-001-View-PermissionRestricted`

Columns: Timestamp, User, Active Role, Action, Entity, Reference, Company, Plant.  
Filters: Date Range, User, Active Role, Action, Entity, Reference, Company, Plant.  
Newest first; immutable; no edit/delete; expansion shows changed fields only; stored UTC + Asia/Kolkata; Role Switch shows previous/new active role; inaccessible Company/Plant filter **403**; mobile event cards; empty copy **`No audit events match the selected filters.`**

---

## 13. AUDIT.EXPORT grant verification

Unchanged seed matrix:

| Role | AUDIT.VIEW | AUDIT.EXPORT |
|---|---|---|
| Super Admin | Yes | Yes |
| Accounts Manager | Yes | No |
| Plant Manager | No | No |
| Store Keeper | No | No |
| Billing Clerk | No | No |
| SYSTEM | No | No |

AUD-SCR-001 uses `AUDIT.EXPORT`. RPT-SCR-013 uses `REPORT.EXPORT`. Unauthorised `POST /audit/export` returns **403**. PermissionRestricted removes Export. Export uses the same eight filters and scope and writes `AUDIT_EXPORT_REQUESTED`. Grants were not widened.

---

## 14. Exact 32-frame inventory

FR-0569–FR-0600. **32/32** direct CSV matches. **26 desktop / 6 mobile / 0 tablet.** IAM aliases: none. Password-change and one-time-password views are operational only and are not counted as CSV frames.

---

## 15. Existing six AUD frames preserved

FR-0601–FR-0606 remain in `FrameCatalog::all()` (88 Phase 1 frames). They are not duplicated in `Phase6Frames`. Alias names are not additional catalog keys.

---

## 16. Desktop / mobile QA

Live browser QA (not HTTP-200-only) is recorded in §16 of the closeout commit notes and walkthrough artifacts: IAM user list default/filtered/empty; S. Patil Edit Default; User Edit PermissionRestricted and Error; User Create Default; Role list; Billing Clerk Edit Default; Role Edit Filtered / PermissionRestricted / Error; Audit Default / Filtered / Empty / PermissionRestricted; all six mobile IAM frames at 390×844 (header, hamburger ≥44×44, closed drawer, wrapping multi-selects, accordion counts, disabled reasons, sticky footer, no horizontal overflow).

---

## 17. Security / concurrency QA

Lower administrator cannot assign Super Admin. Ungrantable submitted permissions are rejected server-side. Direct unauthorised POST returns 403. Deactivated users cannot authenticate. Expired temporary passwords cannot authenticate normally. First-login users cannot open ERP modules. Last Super Admin cannot be removed concurrently. Destructive checks use extra fixtures, not sole deletion of the seeded Super Admin.

---

## 18. Test and assertion totals

`vendor/bin/phpunit --no-coverage` at Checkpoint I (`34ac7d9d1f73a4c7c3ee3f408ca8b85da23b4414`): **313 tests, 10,394 assertions, 0 failures.**

Independent closeout rerun at the final correction / approved implementation HEAD (`146baa157c2cf861be103f3d2ae7aebea064529d`): **313 tests, 10,394 assertions, 0 failures** (Time: 23:15.413). Baseline 263 / 9,002 is preserved and exceeded. The Phase 6 count was not predetermined.

---

## 19. Phase 0–5 regression

Frozen figures reasserted: ledger **116.000 MT**; allocation 18.000 + 12.000 = **30.000 MT**; LOT-2026-00230 remaining **13.000 MT**; disposition 3.000 + 7.000 + 2.000 = **12.000 MT**; WST-2026-000067 Approved; INV-2026-000119 **₹177,592.00**; CRN-2026-000031 **₹75,089.00**; `INWARD.REVERSE` only Store Keeper and Super Admin; no `INWARD.APPROVE`; no automatic IRN. Full suite includes the existing Phase 0–5 tests.

---

## 20. Commit hash for every checkpoint

| CP | Hash | Subject |
|---|---|---|
| Plan (approved) | `b1e1d6a46e2c0aa5588937f8966543c5218d7a13` | Corrected Phase 6 plan P6-D1–P6-D12 |
| 0 | `5ee43a92fda0e4e964881f905c169b28b5370b3f` | Source CSV, aliases, checksums |
| A | `54400673a07615594fd7ae799f88e190b1ecf1da` | Additive IAM columns |
| B | `0506b66573e9197c65f8ab88ca7d708ae786b272` | Domain, Hybrid C, D2 |
| C | `eeed26d9fd04e2c5fe507c0c9137caa058070caf` | IAM-SCR-001 desktop |
| D | `eed8b39f0a35bea814ea7148e02e770838aa9642` | IAM-SCR-001 mobile |
| E | `59a3208ac8ea12b5b98fdd51076369b9d61da168` | IAM-SCR-002 desktop |
| F | `ed8f6ca5711a9b0a95fb93dbe2b724cf4d69330d` | IAM-SCR-002 mobile |
| G | `dafbdfc2d926078136d54d091e5d780cf766005b` | AUD-SCR-001 completeness |
| H | `9e66296556d5ae07be8a8d3726e34f8680c0a572` | Session, security, concurrency |
| I | `34ac7d9d1f73a4c7c3ee3f408ca8b85da23b4414` | 32-frame inventory, QA, report, full suite |
| Final correction | `146baa157c2cf861be103f3d2ae7aebea064529d` | IAM user-list timestamps in Asia/Kolkata (display only) |

**Final approved implementation HEAD:** `146baa157c2cf861be103f3d2ae7aebea064529d`.

---

## 21. Deviations and remaining risks

- Existing seeded users keep **NULL email** until an administrator supplies one on save. New users require a unique lowercase email. Demo addresses were not fabricated in the migration.
- One-time passwords use a consume-once encrypted cache token rather than session flash, to prevent redisplay and server-side flash leakage.
- Unauthorised `POST /audit/export` now returns HTTP **403** (previously a redirect with flash). This matches P6-D10.
- Health JSON `phase` is **6**.
- Frame simulation remains development/testing-only and does not bypass permission, scope, persisted status, or validation.
- User list Created / Last modified columns display Asia/Kolkata (not raw UTC microseconds). Live browser QA confirmed this after the Checkpoint I follow-up. The correction formats existing `created_at_utc` / `updated_at_utc` for display only; it does not rewrite stored UTC values, audit ordering, audit filtering, exported timestamps, before/after audit JSON, or session behaviour.

---

## 22. Phase 7 was not started

No Phase 7 plan, routes, screens, migrations, or tests were created. Implementation stops after this Phase 6 closeout.

---

**Acceptance:** 32/32 IAM frames; 26 desktop; 6 mobile; 0 tablet; existing six AUD frames preserved; zero duplicate frame names; zero physical DELETE routes; zero permission union; zero anti-escalation bypass; zero detached security checks; zero audit mutation controls; Phase 0–5 tests remain in the green suite; Phase 6 tests green; live browser QA complete.
