# Permission and Display Audit Report

Audit date: 2026-07-14

Scope: Review-only audit of UI display, navigation, route guards, API authorization, role helpers, permission definitions, and current database permission templates. No runtime source code or database data was changed.

## 1. Executive Summary

Overall assessment: the system is partially aligned with the permission model. Content workflow modules such as announcements and online lessons use `effectivePermissions` and shared workflow helpers consistently for most row actions. However, several HRD operational modules still authorize by `businessRole`, `membership.role`, or broad role helper functions instead of the explicit permission keys that already exist.

Finding count by severity:

| Severity | Count |
| --- | ---: |
| Critical | 0 |
| High | 6 |
| Medium | 5 |
| Low | 3 |

Highest risk modules:

| Module | Risk |
| --- | --- |
| Employee import and master review | Explicit permissions exist but route/page/API guards use HRD role only. |
| Training records review and reports | Organization-wide access and review are derived from membership role instead of granular permissions. |
| Training matrix and courses | Some routes use role guards while navigation suggests permission-aware access. |
| Navigation | Sidebar uses role/businessRole for many items, while pages and APIs sometimes use different checks. |

Data exposure observed:

- Reports can expose organization-wide data when `membership.role` is `admin`, even if the active permission template only grants `reports:self_read`.
- Employee directory and master-data review areas are visible to HRD-role users even when the template does not include `employee_import:run` or `employee_master_review:approve`.

Backend authorization summary:

- Announcements, online lessons, training policies, training record import, and permission management have meaningful permission checks.
- Courses, employee directory, employee import, master review, training matrix, training record review, reports, audit logs, and users still contain role-based or membership-role authorization seams.

## 2. Roles and Permissions Discovered

Role sources:

| Source | Evidence | Behavior |
| --- | --- | --- |
| `systemRole` | `src/lib/auth/roles.ts` | `super_admin` is treated as IT-like and HRD-capable in some helpers. |
| `membership.role` | `src/db/schema.ts`, `src/lib/auth/roles.ts` | `owner`/`admin` imply HRD; `member` implies EMPLOYEE. |
| `businessRole` | `src/lib/auth/roles.ts` | Derived from system and membership role, not from permission template. |
| `effectivePermissions` | `src/auth.ts`, `src/lib/auth/authorization.server.ts` | Resolved from active permission template, overrides, or fallback membership/default permissions. |

Permission definition source:

- `src/lib/auth/permissions.ts` defines all permission keys and defaults.
- `src/features/permission-management/server/permission-management-catalog.ts` defines catalog metadata.
- `permission_templates`, `permission_template_permissions`, `user_permission_template_assignments`, and `user_permission_overrides` provide database-configured permissions.

Important discovered permission groups:

| Group | Examples |
| --- | --- |
| Employee directory/import | `employee_directory:read`, `employee_directory:write`, `employee_import:run`, `employee_master_review:approve` |
| Courses and policy | `courses:read`, `courses:write`, `training_policy:read`, `training_policy:write` |
| Training matrix | `training_matrix:read`, `training_matrix:write` |
| Training records | `training_record:self_read`, `training_record:read_all`, `training_record:write_all`, `training_record:review`, `training_record:approve`, `training_record:reject` |
| Announcements | `announcement:create`, `announcement:edit_draft`, `announcement:approve`, `announcement:revision`, `announcement:publish` |
| Online lessons | `online_lesson:create`, `online_lesson:edit_draft`, `online_lesson:approve`, `online_lesson:revision`, `online_lesson:publish` |
| Reports | `reports:self_read`, `reports:organization_read`, `reports:export`, `reports:export_excel`, `reports:export_pdf` |
| Permission management | `permission_template:*`, `user_permission:*` |

Database template observations:

| Template | Count | Notable permissions |
| --- | ---: | --- |
| `employee` | 8 | Self training record, public announcements, public online lessons, self reports, notifications. |
| `hrd-staff` id 2 | 42 | Includes `training_record:approve`, `training_record:review`, `reports:organization_read`, content publishing but excludes `employee_import:run` and `employee_master_review:approve`. |
| `hrd-staff` id 5/id 8 | 30 | Includes HRD write/read basics but excludes review/approve/report-org and employee import/master-review permissions. |
| `manager` | 56 | Includes training review, reports export, online lesson approve/revision/reject, announcement approve/revision/reject. |
| `IT` | 78 | Broad system, permission management, audit, import, master review, and all content workflow permissions. |

## 3. Permission Matrix

| Module | UI/Page Entry | Primary Page Guard | API Guard | Permission Alignment |
| --- | --- | --- | --- | --- |
| Overview | `requireOrg` nav | Employee dashboard access | Server report/overview scope | Medium: mixed role/permission usage. |
| Users | IT nav | User management access | user management helper | Medium: helper is IT/business-role based, not `users:*`. |
| Employee directory | HRD/IT nav | HRD dashboard access | `requireHRD()` | High mismatch: ignores `employee_directory:*`. |
| Employee import | hidden/direct route | HRD dashboard access | `requireHRD()` | High mismatch: ignores `employee_import:run`. |
| Master review | hidden/direct route | HRD dashboard access | `requireHRD()` | High mismatch: ignores `employee_master_review:approve`. |
| Courses | HRD/IT nav | HRD dashboard access | `requireHRD()` | High mismatch: ignores `courses:read/write`. |
| Training policy | HRD/IT nav | `training_policy:read` | `training_policy:read/write` | Good alignment. |
| Training matrix | HRD/IT nav | `training_matrix:read` | `membership.role` via `canManageTrainingMatrix()` | High mismatch between page guard and API. |
| Training records list | all org users | Employee dashboard access | membership role and data helper | Medium: permissions exist but access logic is role based. |
| Training records import | conditional button | `write_all + approve` | `write_all + approve` | Good alignment. |
| Pending review | HRD nav | HRD dashboard access | training record list query | Medium mismatch: page/nav role based, review permission not required. |
| Reports | HRD nav, direct employee page | Employee dashboard access | report scope by role | High mismatch: ignores `reports:organization_read/export`. |
| Announcements | HRD/IT nav | Employee dashboard access plus action permissions | content workflow permissions | Mostly aligned. |
| Online lessons | all org users | Employee dashboard access plus action permissions | content workflow permissions | Mostly aligned. |
| Audit logs | super admin nav | IT dashboard access | `requireIT()` | Medium mismatch: permission keys exist but not used. |
| Permission management | super admin nav | super admin dashboard access | assumed super admin route guards | Low: intentionally super admin-only, not permission-template based. |

## 4. Route and Navigation Audit

| ID | Route | Menu Permission | Route Guard | Direct URL Result | Status |
| --- | --- | --- | --- | --- | --- |
| NAV-001 | `/dashboard/pending-review` | `businessRole: HRD` | `requireHRDDashboardAccess()` | HRD-role users can enter without checking `training_record:review` | Mismatch |
| NAV-002 | `/dashboard/reports` | `businessRole: HRD` | `requireEmployeeDashboardAccess()` | Employees can enter directly; dataset scopes by membership role | Mismatch |
| NAV-003 | `/dashboard/employee-directory` | `businessRoles: HRD, IT` | `requireHRDDashboardAccess()` | HRD-role users can enter without `employee_directory:read` | Mismatch |
| NAV-004 | `/dashboard/courses` | `businessRoles: HRD, IT` | `requireHRDDashboardAccess()` | HRD-role users can enter without `courses:read` | Mismatch |
| NAV-005 | `/dashboard/training-policy` | `businessRoles: HRD, IT` | `requirePermissionDashboardAccess('training_policy:read')` | Users with nav role but missing permission would see menu but be redirected | UI/route mismatch possible |
| NAV-006 | `/dashboard/training-matrix` | `businessRoles: HRD, IT` | `requirePermissionDashboardAccess('training_matrix:read')` | Menu is role-based; route is permission-based | UI/route mismatch possible |
| NAV-007 | `/dashboard/import-employees` | No sidebar item found | `requireHRDDashboardAccess()` | HRD-role users can enter directly without `employee_import:run` | Hidden but accessible |
| NAV-008 | `/dashboard/master-review` | No sidebar item found | `requireHRDDashboardAccess()` | HRD-role users can enter directly without `employee_master_review:approve` | Hidden but accessible |
| NAV-009 | `/dashboard/audit-logs` | `systemRole: super_admin` | `requireITDashboardAccess()` | IT-like users may satisfy page guard depending business role | Mismatch |
| NAV-010 | `/dashboard/online-lessons` | `requireOrg` | Employee dashboard access plus action permissions | Direct URL aligns for read; actions are permission checked | Acceptable |
| NAV-011 | `/dashboard/announcements` | `businessRoles: HRD, IT` | Employee dashboard access plus action permissions | Read access can be broad; create/action buttons use permissions | Mostly aligned |

## 5. UI Display Audit

| ID | Module | Page / Component | Element | Expected | Actual | Severity | Evidence |
| --- | --- | --- | --- | --- | --- | --- | --- |
| UI-001 | Navigation | Sidebar | HRD module links | Show by explicit permission where permission exists | Many HRD links show by `businessRoles` | Medium | `src/config/nav-config.ts` |
| UI-002 | Training records | Listing page | Review queue button | Show only with review permission | Uses `isHRD()` | Medium | `src/app/dashboard/training-records/page.tsx` |
| UI-003 | Training records | Pending review page | Page visibility | Require `training_record:review` or approve/reject permissions | Requires HRD role only | Medium | `src/app/dashboard/pending-review/page.tsx` |
| UI-004 | Employee import | Direct page | Page visibility | Require `employee_import:run` | Requires HRD role only | High | `src/app/dashboard/import-employees/page.tsx` |
| UI-005 | Master review | Direct page | Page visibility | Require `employee_master_review:approve` | Requires HRD role only | High | `src/app/dashboard/master-review/page.tsx` |
| UI-006 | Courses | Listing and create link | Create/write access | Require `courses:write` for write UI | HRD page role gate; action visibility not permission-scoped at page level | Medium | `src/app/dashboard/courses/page.tsx` |
| UI-007 | Reports | Reports page | Organization filters and organization-wide data | Require `reports:organization_read` | Uses membership role for org-wide access | High | `src/features/reports/server/report-data.ts` |
| UI-008 | Training policy | Page and actions | Read/write | Uses permission guard and write permission in component | Aligned | Low | `src/app/dashboard/training-policy/page.tsx`, `src/features/training-policy/components/training-policy-page.tsx` |
| UI-009 | Online lessons | Action menu | edit/submit/approve/return/reject/publish | Use content workflow permissions and status | Aligned | Low | `src/features/online-lessons/components/online-lesson-action-menu.tsx` |
| UI-010 | Announcements | Action menu | edit/submit/approve/return/reject/publish | Use content workflow permissions and status | Aligned | Low | `src/features/announcements/components/announcement-action-menu.tsx` |

## 6. Backend Authorization Audit

| ID | Endpoint / Action | Authentication | Permission | Ownership | Status Guard | Result |
| --- | --- | --- | --- | --- | --- | --- |
| API-001 | `POST /api/import-employees` | `requireHRD()` | None for `employee_import:run` | Organization scoped | File validation only | High mismatch |
| API-002 | `GET /api/import-employees/template` | `requireHRD()` | None for `employee_import:run` | N/A | N/A | High mismatch |
| API-003 | `GET/PATCH /api/master-review` | `requireHRD()` | None for `employee_master_review:approve` | Organization scoped | Entity action check | High mismatch |
| API-004 | `GET/POST/PATCH/DELETE /api/courses` | `requireHRD()` or `isHRD()` | None for `courses:read/write` | Organization scoped | N/A | High mismatch |
| API-005 | `/api/employee-directory` and targets | `requireHRD()` | None for `employee_directory:read/write` | Organization scoped | N/A | High mismatch |
| API-006 | `/api/training-matrix*` | `requireOrganizationAccess()` | Uses `membership.role`, not `training_matrix:*` | Organization scoped | N/A | High mismatch |
| API-007 | `/api/training-records/[id]/review` | `requireOrganizationAccess()` | Uses `canReviewTrainingRecords(membership.role)` | Organization scoped | Review status guard in data layer | High mismatch |
| API-008 | `/api/training-records/import` | `requireAllPermissions(['training_record:write_all','training_record:approve'])` | Explicit | Organization scoped | Upload validation | Aligned |
| API-009 | `/api/training-policies*` | `requirePermission()` | Explicit read/write | Organization scoped | Active/clone/deactivate guards | Aligned |
| API-010 | `/api/announcements*` | Organization access plus workflow helpers | Explicit content permissions | Owner and status checked | Workflow status checked | Aligned |
| API-011 | `/api/online-lessons*` | Organization access plus workflow helpers | Explicit content permissions | Owner and status checked | Workflow status checked | Aligned |
| API-012 | `/api/reports/export` | Not fully reviewed in detail | Permission keys exist | Requires confirmation | Requires confirmation | Needs follow-up |
| API-013 | `/api/audit-logs*` | `requireIT()` | Does not use `audit_logs:*` | Organization scoped | N/A | Medium mismatch |
| API-014 | `/api/users*` | User management helper | Helper is IT/businessRole based | Organization scoped | N/A | Medium mismatch |

## 7. Findings

### PERM-001: HRD role grants employee import without `employee_import:run`

- Severity: High
- Module: Employee import
- Affected roles: HRD users without `employee_import:run`, including some `hrd-staff` templates.
- Expected behavior: page and API should require `employee_import:run`.
- Actual behavior: page uses `requireHRDDashboardAccess()` and API uses `requireHRD()`.
- Security or business impact: HRD-role users can import employee/training data by direct URL/API even if the configured template does not grant import.
- Root cause: role guard is used instead of permission guard.
- Evidence: `employee_import:run` exists in `src/lib/auth/permissions.ts`; guards are in `src/app/dashboard/import-employees/page.tsx` and `src/app/api/import-employees/route.ts`.
- File path: `src/app/dashboard/import-employees/page.tsx`, `src/app/api/import-employees/route.ts`, `src/app/api/import-employees/template/route.ts`
- Line or code reference: page line 6, route line 454, template route line 12.
- Reproduction steps: sign in as HRD-role user with template lacking `employee_import:run`, open `/dashboard/import-employees`, submit Excel to `/api/import-employees`.
- Recommended remediation: replace page/API guards with `requirePermissionDashboardAccess('employee_import:run')` and `requirePermission('employee_import:run')`.
- Related findings: PERM-002, PERM-003.

### PERM-002: Master review does not require `employee_master_review:approve`

- Severity: High
- Module: Master review
- Affected roles: HRD users without `employee_master_review:approve`.
- Expected behavior: viewing and approving/rejecting imported master data should require `employee_master_review:approve`.
- Actual behavior: page and API use HRD role only.
- Security or business impact: users can approve/reject company, department, and position changes beyond configured template permissions.
- Root cause: permission key exists but is not enforced.
- Evidence: permission key in `src/lib/auth/permissions.ts`; route guards in `src/app/dashboard/master-review/page.tsx` and `src/app/api/master-review/route.ts`.
- File path: `src/app/dashboard/master-review/page.tsx`, `src/app/api/master-review/route.ts`
- Line or code reference: page line 9, API lines 42 and 137.
- Reproduction steps: HRD user without `employee_master_review:approve` opens `/dashboard/master-review`, calls PATCH `/api/master-review`.
- Recommended remediation: require `employee_master_review:approve` on page and API.
- Related findings: PERM-001.

### PERM-003: Employee directory read/write ignores `employee_directory:*`

- Severity: High
- Module: Employee directory
- Affected roles: HRD users whose effective template excludes directory read/write.
- Expected behavior: list/detail requires `employee_directory:read`; create/edit/targets require `employee_directory:write`.
- Actual behavior: dashboard pages and API use HRD role only.
- Security or business impact: broad employee profile and target management access can exceed template configuration.
- Root cause: legacy HRD guard remains after permission keys were introduced.
- Evidence: `employee_directory:read/write` exist in `src/lib/auth/permissions.ts`; route uses `requireHRD()` in `src/app/api/employee-directory/route.ts` and target route.
- File path: `src/app/dashboard/employee-directory/page.tsx`, `src/app/api/employee-directory/route.ts`, `src/app/api/employee-directory/[id]/target/route.ts`
- Line or code reference: dashboard line 17, API line 7, target API lines 27 and 62.
- Reproduction steps: HRD user without employee directory permissions opens `/dashboard/employee-directory` or calls `/api/employee-directory`.
- Recommended remediation: map read/write operations to `employee_directory:read` and `employee_directory:write`.
- Related findings: PERM-001.

### PERM-004: Courses use HRD role instead of `courses:read/write`

- Severity: High
- Module: Courses
- Affected roles: HRD users without course permissions; users with `courses:*` but not HRD.
- Expected behavior: list/detail require `courses:read`; create/update/delete require `courses:write`.
- Actual behavior: dashboard and API use HRD role checks.
- Security or business impact: permission templates cannot precisely control course access.
- Root cause: role-based feature guard predates explicit permission keys.
- Evidence: `courses:read/write` in `src/lib/auth/permissions.ts`; page uses `requireHRDDashboardAccess()`; API uses `requireHRD()` and `isHRD()`.
- File path: `src/app/dashboard/courses/page.tsx`, `src/app/api/courses/route.ts`, `src/app/api/courses/[id]/route.ts`
- Line or code reference: page line 20, API route lines 22 and 64.
- Reproduction steps: assign user template with/without `courses:*` and compare UI/API with HRD membership role.
- Recommended remediation: use `requirePermissionDashboardAccess('courses:read')` for read pages and `requirePermission('courses:write')` for mutations.
- Related findings: PERM-006.

### PERM-005: Training matrix page guard uses permissions but API uses membership role

- Severity: High
- Module: Training matrix
- Affected roles: users with `training_matrix:read/write` but non-admin membership, and admin users without template permission.
- Expected behavior: API should enforce `training_matrix:read` or `training_matrix:write`.
- Actual behavior: API calls `canManageTrainingMatrix(membership.role)`.
- Security or business impact: permission template changes may not affect API access; UI and backend can disagree.
- Root cause: split guard model between page and API.
- Evidence: page uses `requirePermissionDashboardAccess('training_matrix:read')`; API uses `canManageTrainingMatrix(membership.role)`.
- File path: `src/app/dashboard/training-matrix/page.tsx`, `src/features/training-matrix/server/training-matrix-data.ts`, `src/app/api/training-matrix/route.ts`
- Line or code reference: page line 20, helper line 62, API lines 17 and 54.
- Reproduction steps: grant `training_matrix:*` to a member-role user or deny it to an admin-role user, then call API directly.
- Recommended remediation: replace role helper at API boundary with `requirePermission()` checks.
- Related findings: PERM-004.

### PERM-006: Training record review uses membership role instead of review permissions

- Severity: High
- Module: Training records
- Affected roles: admin-role users without `training_record:review/approve/reject`, and non-admin users with explicit review permissions.
- Expected behavior: review queue and review API should require explicit review/approve/reject permissions.
- Actual behavior: review eligibility uses `isHRD()` or `canReviewTrainingRecords(membership.role)`.
- Security or business impact: review authority is tied to membership role and can exceed template configuration.
- Root cause: old membership-role model remains in canonical training-record data helpers.
- Evidence: `canReviewTrainingRecords(role)` delegates to owner/admin role; pending-review page uses HRD guard.
- File path: `src/features/training-records/server/training-record-data.ts`, `src/app/dashboard/pending-review/page.tsx`, `src/app/api/training-records/[id]/review/route.ts`
- Line or code reference: helper line 132, pending page line 20, review API line 34.
- Reproduction steps: HRD/admin user lacking `training_record:review` opens pending review and calls review API.
- Recommended remediation: introduce permission-aware review checks using `training_record:review`, `training_record:approve`, and `training_record:reject`.
- Related findings: PERM-007.

### PERM-007: Reports organization scope ignores `reports:organization_read`

- Severity: High
- Module: Reports
- Affected roles: admin-role users with only `reports:self_read`, and non-admin users with `reports:organization_read`.
- Expected behavior: organization-wide reports require `reports:organization_read`; export requires export permissions.
- Actual behavior: organization scope is based on membership role through `canViewAllReports(role)`.
- Security or business impact: admin-role users can see organization-wide reports even when the template grants only self reports.
- Root cause: reports scope uses membership role instead of permission keys.
- Evidence: `reports:organization_read` exists in permissions; `canViewAllReports(role)` uses owner/admin.
- File path: `src/features/reports/server/report-data.ts`, `src/app/dashboard/reports/page.tsx`
- Line or code reference: report helper line 61, dashboard page line 20.
- Reproduction steps: HRD staff template id 5/id 8 has only `reports:self_read` but membership role admin can still be scoped as all by role.
- Recommended remediation: calculate report scope from `effectivePermissions`.
- Related findings: PERM-006.

### PERM-008: Sidebar uses business roles where explicit permissions exist

- Severity: Medium
- Module: Navigation
- Affected roles: users whose templates diverge from membership roles.
- Expected behavior: sidebar should hide/show entries based on the same permission used by page/API where possible.
- Actual behavior: many items use `businessRole` or `businessRoles`.
- Security or business impact: users may see menu items they cannot use, or not see items they can use by direct URL.
- Root cause: nav configuration predates fine-grained permission templates.
- Evidence: `src/config/nav-config.ts` uses `businessRole: "HRD"` and `businessRoles: ["HRD","IT"]`.
- File path: `src/config/nav-config.ts`, `src/hooks/use-nav.ts`
- Line or code reference: nav lines 23, 64, 72, 80, 88, 96, 99, 159.
- Reproduction steps: assign a template with read permission but non-HRD membership role, or remove a permission from HRD template.
- Recommended remediation: prefer `access.permission` for permission-backed modules; keep role access only for role-only concepts.
- Related findings: PERM-004, PERM-005.

### PERM-009: `use-nav` reads `session.user.permissions` instead of explicitly using `effectivePermissions`

- Severity: Medium
- Module: Navigation
- Affected roles: users with allow/deny overrides if `permissions` ever diverges from `effectivePermissions`.
- Expected behavior: permission-based nav should use effective permissions.
- Actual behavior: nav access context uses `session.user.permissions`.
- Security or business impact: low current risk because Auth.js currently assigns both fields to effective permissions, but maintainability risk if they diverge later.
- Root cause: naming ambiguity between permissions and effectivePermissions.
- Evidence: `src/auth.ts` currently assigns both; `src/hooks/use-nav.ts` uses `session.user.permissions`.
- File path: `src/auth.ts`, `src/hooks/use-nav.ts`
- Line or code reference: auth lines 265-298, hook line 106.
- Reproduction steps: future change separates raw permissions from effective permissions.
- Recommended remediation: use `session.user.effectivePermissions` in navigation.
- Related findings: PERM-008.

### PERM-010: Audit logs use IT role instead of `audit_logs:*`

- Severity: Medium
- Module: Audit logs
- Affected roles: users with audit permissions but not IT role; IT users without explicit audit permissions.
- Expected behavior: read/export require `audit_logs:read` and `audit_logs:export`.
- Actual behavior: page and APIs use IT role helpers.
- Security or business impact: permission templates cannot independently control audit access.
- Root cause: audit module is role-gated.
- Evidence: `audit_logs:*` keys exist; audit page/API use IT guard.
- File path: `src/app/dashboard/audit-logs/page.tsx`, `src/app/api/audit-logs/route.ts`, `src/app/api/audit-logs/export/route.ts`
- Line or code reference: dashboard line 16, API line 8, export API line 16.
- Reproduction steps: grant `audit_logs:read` to non-IT or remove audit permissions from IT template.
- Recommended remediation: use permission guards while preserving super admin fallback if required.
- Related findings: PERM-008.

### PERM-011: User management helper is role-based despite `users:*` permissions

- Severity: Medium
- Module: Users
- Affected roles: users with `users:*` but not IT role, and IT-role users without `users:*`.
- Expected behavior: users list and mutations should enforce `users:read/create/update/delete/manage`.
- Actual behavior: `requireUserManagementAccess()` delegates to `canManageUsers()`, which checks IT role.
- Security or business impact: permission templates cannot precisely delegate user-management tasks.
- Root cause: business-role helper is used as the source of truth.
- Evidence: `users:*` keys exist; `canManageUsers()` returns `isIT()`.
- File path: `src/lib/auth/session.ts`, `src/lib/auth/roles.ts`, `src/app/dashboard/users/page.tsx`
- Line or code reference: session line 199, roles line 94, users page line 18.
- Reproduction steps: assign a non-IT user `users:read`; user still cannot access. Remove `users:*` from IT template; IT role may still pass helper.
- Recommended remediation: introduce permission-specific user management guards.
- Related findings: PERM-010.

### PERM-012: Permission templates with same code differ across organizations

- Severity: Low
- Module: Permission management / operations
- Affected roles: users assigned `hrd-staff` in different organizations.
- Expected behavior: same template code may vary by organization, but reports should make this explicit.
- Actual behavior: DB has `hrd-staff` with 42 permissions in one org and 30 in others.
- Security or business impact: operators may assume "HRD Staff" means the same capability everywhere.
- Root cause: organization-scoped templates share code/name but differ in permission sets.
- Evidence: database query returned `hrd-staff` id 2 with 42 permissions, id 5/id 8 with 30 permissions.
- File path: database tables `permission_templates`, `permission_template_permissions`.
- Line or code reference: schema tables in `src/db/schema.ts`.
- Reproduction steps: compare permission templates by `code = 'hrd-staff'` across organizations.
- Recommended remediation: document organization-scoped template differences and add UI comparison/audit affordance.
- Related findings: PERM-008.

### PERM-013: Direct hidden routes exist for HRD-only workflows

- Severity: Medium
- Module: Navigation / routes
- Affected roles: HRD users without explicit hidden-route permissions.
- Expected behavior: hidden routes should still enforce exact permissions.
- Actual behavior: hidden import/master-review routes are not sidebar-visible but are accessible by direct URL to HRD role.
- Security or business impact: hiding the menu does not prevent access.
- Root cause: route guards are less strict than intended permission keys.
- Evidence: `/dashboard/import-employees` and `/dashboard/master-review` are not in sidebar but have HRD-only guards.
- File path: `src/config/nav-config.ts`, `src/app/dashboard/import-employees/page.tsx`, `src/app/dashboard/master-review/page.tsx`
- Line or code reference: nav does not include these routes; page lines 6 and 9.
- Reproduction steps: manually type direct route URL as HRD staff.
- Recommended remediation: direct routes must require explicit permission, regardless of sidebar visibility.
- Related findings: PERM-001, PERM-002.

### PERM-014: Content workflow modules are comparatively well aligned

- Severity: Low
- Module: Announcements and online lessons
- Affected roles: content creators, reviewers, publishers.
- Expected behavior: status transitions and row actions use explicit permissions.
- Actual behavior: shared workflow helpers map actions to module-specific permissions and are used in UI/API.
- Security or business impact: low; this is a positive control area and should be used as a reference.
- Root cause: centralized content workflow helper.
- Evidence: `canEditContent`, `canDeleteContent`, and `canTransitionContent` are used by action menus and APIs.
- File path: `src/features/content-approval/lib/workflow.ts`, online lesson/announcement components and APIs.
- Line or code reference: workflow lines 109-168.
- Reproduction steps: compare action menu and API guards for status transitions.
- Recommended remediation: reuse this pattern for other modules.
- Related findings: PERM-004 to PERM-011.

## 8. Role-based Test Scenarios

| Test ID | Role / Template | Page | Action | Expected Result |
| --- | --- | --- | --- | --- |
| T-001 | Employee template | `/dashboard/online-lessons` | View public lesson | Allowed if published/public. |
| T-002 | Employee template | `/dashboard/online-lessons/manage/new` | Create lesson | Denied without `online_lesson:create`. |
| T-003 | HRD Staff id 5/id 8 | `/dashboard/import-employees` | Direct URL | Should be denied because template lacks `employee_import:run`. Current likely allowed by HRD role. |
| T-004 | HRD Staff id 5/id 8 | `/dashboard/master-review` | Approve/reject master data | Should be denied because template lacks `employee_master_review:approve`. Current likely allowed by HRD role. |
| T-005 | HRD Staff id 5/id 8 | `/dashboard/reports` | Organization-wide report | Should be denied because template lacks `reports:organization_read`. Current may be allowed by admin membership role. |
| T-006 | HRD Staff id 5/id 8 | `/dashboard/pending-review` | Open review queue | Should require `training_record:review`; current uses HRD role. |
| T-007 | HRD Staff id 2 | `/dashboard/training-records/import` | Import records | Allowed because template includes `training_record:write_all` and `training_record:approve`. |
| T-008 | HRD Manager | Online lesson pending item | Return for revision | Allowed with `online_lesson:revision`. |
| T-009 | HRD Staff | Returned own online lesson | Edit and resubmit | Allowed with `online_lesson:edit_draft` and `online_lesson:submit`. |
| T-010 | Non-owner HRD Staff | Other user's returned online lesson | Edit | Denied unless a future manage permission is introduced. |
| T-011 | Member with `training_matrix:read` override | `/dashboard/training-matrix` | Open page and call API | Page should allow; current API may deny by membership role. |
| T-012 | IT template | `/dashboard/audit-logs` | Read/export audit logs | Allowed currently by IT role; should also be verified against `audit_logs:*`. |
| T-013 | Super admin | Permission management | Manage templates/users | Allowed by super admin. |

## 9. Prioritized Remediation Plan

### Priority 0 - Critical Security

No confirmed Critical findings in this audit. The most serious confirmed issues are High permission mismatches where broad HRD/admin roles override finer-grained permission templates.

### Priority 1 - Permission Mismatch

1. Replace employee import page/API guards with `employee_import:run`.
2. Replace master review page/API guards with `employee_master_review:approve`.
3. Replace courses page/API guards with `courses:read` and `courses:write`.
4. Replace employee directory read/write guards with `employee_directory:read` and `employee_directory:write`.
5. Replace training matrix API role helper with `training_matrix:read/write`.
6. Replace training record review role helper with `training_record:review/approve/reject`.
7. Replace report organization-wide scope with `reports:organization_read` and export checks with export permissions.

### Priority 2 - Data Exposure

1. Review reports data scope first because it can expose organization-wide data through membership role.
2. Review employee-directory detail and target routes for employee profile/target exposure.
3. Review hidden HRD routes and enforce exact permissions.

### Priority 3 - Maintainability

1. Update sidebar config to use `access.permission` where explicit permissions exist.
2. Use `effectivePermissions` consistently in navigation.
3. Extract module-level permission helpers for courses, employee directory, reports, training matrix, and training records.
4. Use content workflow helper pattern as a reference for other workflow modules.

## 10. Files Reviewed

| File | Reason |
| --- | --- |
| `plans/system-ui-permission-review-plan.md` | Audit requirements and report structure. |
| `.agents/skills/kiranism-shadcn-dashboard/SKILL.md` | Project dashboard/RBAC guidance. |
| `docs/AI_DEVELOPMENT_GUIDE.md` | Project implementation conventions. |
| `docs/PROJECT_ARCHITECTURE.md` | Architecture and auth/RBAC context. |
| `src/lib/auth/permissions.ts` | Permission definitions and defaults. |
| `src/lib/auth/roles.ts` | Business role and membership role mapping. |
| `src/lib/auth/session.ts` | Server auth guard behavior. |
| `src/lib/auth/authorization.server.ts` | Effective permission resolution. |
| `src/auth.ts` | Auth.js session enrichment and effective permission assignment. |
| `src/config/nav-config.ts` | Sidebar visibility. |
| `src/hooks/use-nav.ts` | Client navigation filtering. |
| `src/features/content-approval/lib/workflow.ts` | Content workflow permission helper. |
| `src/features/online-lessons/components/online-lesson-action-menu.tsx` | Online lesson UI action checks. |
| `src/features/announcements/components/announcement-action-menu.tsx` | Announcement UI action checks. |
| `src/features/training-records/server/training-record-data.ts` | Training record access/review logic. |
| `src/features/training-matrix/server/training-matrix-data.ts` | Training matrix role helper. |
| `src/features/reports/server/report-data.ts` | Report scope logic. |
| `src/app/dashboard/**/page.tsx` | Route/page guards. |
| `src/app/api/**/route.ts` | API authorization patterns. |
| Database permission templates | Current configured role/template reality. |

## 11. Files Not Reviewed or Limitations

- Manual browser verification was not performed in this pass.
- API calls were not executed as each role; findings are based on static code inspection and database configuration.
- Report export route was not fully expanded; it should be reviewed in a follow-up because export permission keys exist.
- Legacy/template modules such as products, forms, kanban, chat, and billing were not deeply audited because they are outside the current training-system permission core.
- Some Thai text appears mojibake in terminal output because of encoding, but file paths and authorization logic were readable.

## 12. Final Assessment

The system is not yet fully permission-first. It is safe in many content workflow areas because online lessons and announcements consistently use `effectivePermissions`, ownership, and status transition helpers. The main readiness gap is older HRD operational modules that still trust `businessRole` or `membership.role`.

Before production hardening, fix the High findings around employee import, master review, employee directory, courses, training matrix API, training record review, and reports scope. The most urgent business risk is that users with HRD/admin membership can access or act on data beyond what their assigned permission template appears to grant.

Confirmed deliverables:

- Found 14 findings: 0 Critical, 6 High, 5 Medium, 3 Low.
- Highest risk modules: employee import, master review, reports, training records review, training matrix API, employee directory.
- Data exposure risk exists for reports and employee directory/master-data review.
- No API route was found with no authentication at all in the reviewed core modules, but several APIs enforce broad role guards instead of explicit permissions.
- This report was created at `permission-display-audit.md`.
- No source code behavior or database data was changed during the audit.
