37 KiB
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.roleisadmin, even if the active permission template only grantsreports:self_read. - Employee directory and master-data review areas are visible to HRD-role users even when the template does not include
employee_import:runoremployee_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.tsdefines all permission keys and defaults.src/features/permission-management/server/permission-management-catalog.tsdefines catalog metadata.permission_templates,permission_template_permissions,user_permission_template_assignments, anduser_permission_overridesprovide 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 somehrd-stafftemplates. - Expected behavior: page and API should require
employee_import:run. - Actual behavior: page uses
requireHRDDashboardAccess()and API usesrequireHRD(). - 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:runexists insrc/lib/auth/permissions.ts; guards are insrc/app/dashboard/import-employees/page.tsxandsrc/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')andrequirePermission('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 insrc/app/dashboard/master-review/page.tsxandsrc/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:approveopens/dashboard/master-review, calls PATCH/api/master-review. - Recommended remediation: require
employee_master_review:approveon 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 requireemployee_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/writeexist insrc/lib/auth/permissions.ts; route usesrequireHRD()insrc/app/api/employee-directory/route.tsand 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-directoryor calls/api/employee-directory. - Recommended remediation: map read/write operations to
employee_directory:readandemployee_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 requirecourses: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/writeinsrc/lib/auth/permissions.ts; page usesrequireHRDDashboardAccess(); API usesrequireHRD()andisHRD(). - 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 andrequirePermission('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/writebut non-admin membership, and admin users without template permission. - Expected behavior: API should enforce
training_matrix:readortraining_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 usescanManageTrainingMatrix(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()orcanReviewTrainingRecords(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:reviewopens pending review and calls review API. - Recommended remediation: introduce permission-aware review checks using
training_record:review,training_record:approve, andtraining_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 withreports: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_readexists 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_readbut 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
businessRoleorbusinessRoles. - 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.tsusesbusinessRole: "HRD"andbusinessRoles: ["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.permissionfor 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
permissionsever diverges fromeffectivePermissions. - 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.tscurrently assigns both;src/hooks/use-nav.tsusessession.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.effectivePermissionsin 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:readandaudit_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:readto 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 withoutusers:*. - Expected behavior: users list and mutations should enforce
users:read/create/update/delete/manage. - Actual behavior:
requireUserManagementAccess()delegates tocanManageUsers(), 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()returnsisIT(). - 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. Removeusers:*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-staffin different organizations. - Expected behavior: same template code may vary by organization, but reports should make this explicit.
- Actual behavior: DB has
hrd-staffwith 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-staffid 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-employeesand/dashboard/master-revieware 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, andcanTransitionContentare 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
- Replace employee import page/API guards with
employee_import:run. - Replace master review page/API guards with
employee_master_review:approve. - Replace courses page/API guards with
courses:readandcourses:write. - Replace employee directory read/write guards with
employee_directory:readandemployee_directory:write. - Replace training matrix API role helper with
training_matrix:read/write. - Replace training record review role helper with
training_record:review/approve/reject. - Replace report organization-wide scope with
reports:organization_readand export checks with export permissions.
Priority 2 - Data Exposure
- Review reports data scope first because it can expose organization-wide data through membership role.
- Review employee-directory detail and target routes for employee profile/target exposure.
- Review hidden HRD routes and enforce exact permissions.
Priority 3 - Maintainability
- Update sidebar config to use
access.permissionwhere explicit permissions exist. - Use
effectivePermissionsconsistently in navigation. - Extract module-level permission helpers for courses, employee directory, reports, training matrix, and training records.
- 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.