20 KiB
Phase 3 - Permission & Authorization Audit
Executive Summary
The authorization model is partially migrated to a permission-first design, but the live implementation is still mixed across effectivePermissions, membership.role, businessRole, and systemRole. That split creates real consistency risk: several sensitive pages and API routes still authorize by role-derived compatibility helpers even though the project now has permission templates, per-user overrides, and effectivePermissions.
The most important gap is that permission templates are not yet the single source of truth for access decisions. In practice, revoking a capability from an HRD/admin-style user through templates or overrides does not guarantee the capability is removed everywhere, because multiple paths still trust role-based helpers such as requireHRD(), requireIT(), canManageTrainingMatrix(membership.role), and canViewAllReports(role).
Authorization Architecture Overview
Current authorization layers:
- Authentication boundary
src/proxy.ts- Protects
/dashboard/**and a subset of/api/**routes from unauthenticated access.
- Session and organization access
src/lib/auth/session.tsrequireSession()->requireOrganizationAccess()-> specialized guards.
- Compatibility role helpers
src/lib/auth/roles.ts- Maps
super_admin, membership roles, and legacy business roles intoIT,HRD, andEMPLOYEE.
- Permission helpers
src/lib/auth/authorization.tshasPermission,hasAnyPermission,hasAllPermissions.
- Effective permission resolution
src/lib/auth/authorization.server.ts- Merges template assignments and user overrides into
effectivePermissions.
- Navigation filtering
src/config/nav-config.tssrc/hooks/use-nav.ts- Mostly UX-only visibility filtering.
Observed architecture state:
- Permission-first exists and is used correctly in some modules such as
training-policy. - Compatibility-mode role checks still control many sensitive routes and data scopes.
- Navigation, page guards, API guards, and data-scope rules are not fully aligned yet.
Permission Inventory
Defined permission families from src/lib/auth/permissions.ts:
- Organization:
organization:manage,organization:switch - Permission templates:
permission_template:* - User permission overrides:
user_permission:* - Users:
users:*,users:manage - Employee directory:
employee_directory:*,employee_import:run,employee_master_review:approve - Courses and matrix:
courses:*,training_policy:*,training_matrix:* - Training records: self/all read-write plus review workflow permissions
- Announcements: full publish workflow permissions
- Online lessons: full publish workflow permissions
- Reports:
reports:self_read,reports:organization_read,reports:export,reports:export_excel,reports:export_pdf - Audit logs:
audit_logs:read,audit_logs:export - Notifications:
notifications:read,notifications:manage
Inventory assessment:
- Coverage is broad enough for a permission-first model.
- The problem is not missing permission keys; the problem is inconsistent enforcement of those keys.
Route Permission Matrix
Representative page-level checks:
| Route | Nav Rule | Page Guard | Effective Result |
|---|---|---|---|
/dashboard/training-policy |
permission-based in nav | requirePermissionDashboardAccess('training_policy:read') |
Consistent permission-first |
/dashboard/users |
businessRoles: ['IT'] |
requireUserManagementDashboardAccess() |
Role-based compatibility check |
/dashboard/employee-directory |
businessRoles: ['HRD', 'IT'] |
requireHRDDashboardAccess() |
Page is narrower than nav and ignores permission keys |
/dashboard/training-matrix |
businessRoles: ['HRD', 'IT'] |
no page guard | Direct URL can render page shell without authorization gate |
/dashboard/reports |
businessRole: 'HRD' |
requireEmployeeDashboardAccess() |
Page allows broader access than nav; server content later scopes by role |
/dashboard/audit-logs |
systemRole: 'super_admin' |
requireITDashboardAccess() |
Nav and page criteria differ |
/dashboard/organizers |
systemRole: 'super_admin' |
inline isHRD(...) check |
Page allows HRD-compatible access even though nav is super-admin only |
/dashboard/permissions/* |
systemRole: 'super_admin' |
requireSuperAdminDashboardAccess() |
Consistent, super-admin only |
API Permission Matrix
Representative API-level checks:
| API Route | Guard Style | Assessment |
|---|---|---|
/api/training-policies |
requirePermission('training_policy:read/write') |
Good permission-first enforcement |
/api/permission-templates |
requireSuperAdminOrganizationAccess() |
Strong but role-based, not template-based |
/api/users |
requireUserManagementAccess() |
Compatibility-mode IT check, not users:* permissions |
/api/training-matrix |
requireOrganizationAccess() + canManageTrainingMatrix(membership.role) |
Role-based, bypasses training_matrix:* permissions |
/api/reports/export |
getReportsAccess() + canViewAllReports(role) |
Scope based on membership role, not report permissions |
/api/training-records/[id]/review |
requireOrganizationAccess() + canReviewTrainingRecords(membership.role) |
Role-based workflow approval |
/api/master-review |
requireHRD() |
Role-based, bypasses employee master-review permission keys |
/api/employees |
requireOrganizationAccess() only |
Any org member can list/create employees by direct API call |
/api/products |
requireOrganizationAccess() only |
Any org member can list/create products by direct API call |
/api/notifications |
requireOrganizationAccess() + self-scope |
Acceptable because data is always self-scoped |
Action-Level Permission Matrix
Representative action enforcement:
| Action | UI Check | API Check | Assessment |
|---|---|---|---|
| View training policy | permission-based | permission-based | Consistent |
| Create/update training policy | permission-based | permission-based | Consistent |
| Review training record | mixed role/permission UI | role-based API | Template revokes may not be honored everywhere |
| Manage training matrix | nav role-based | role-based API | training_matrix:write is not the real gate |
| Export reports | page accessible broadly | export scope constrained by membership role | reports:export* keys do not drive the actual outcome |
| Manage employee directory | page role-based | employees API open to all org members | UI/API mismatch |
| Manage users | page/API IT compatibility |
no users:* permission-first gate |
Fine-grained permission model not fully active |
| Read audit logs | nav super-admin, page/API IT | inconsistent source of truth |
Role / Template Mapping
Observed effective role mapping from src/lib/auth/roles.ts:
super_admin-> treated asIT- membership
owner/admin-> treated asHRD - membership
member-> treated asEMPLOYEE
Template behavior from src/lib/auth/authorization.server.ts:
- If user has assigned permission templates, template permissions replace fallback role defaults.
- User overrides then allow or deny individual permissions.
Audit conclusion:
- The permission engine is capable of precise access control.
- The application layer still frequently ignores that engine and falls back to role compatibility rules.
- Role names and template names therefore do not reliably predict real access without tracing the exact route.
Data Scope Assessment
Positive observations:
- Notifications are self-scoped by
organizationId + userId. - Training records use organization scoping and record-level lookup helpers.
- Reports distinguish self vs organization views in data-loading logic.
Scope weaknesses:
- Reports determine organization-wide visibility with
canViewAllReports(role)insrc/features/reports/server/report-data.ts, not withreports:organization_read. - Department summary, annual summary, and training matrix reports are therefore governed by membership role instead of permission keys.
- Training matrix management also trusts
membership.role, nottraining_matrix:read/write. - Employees and products APIs are scoped to organization, but not to action-level authority inside that organization.
Direct URL Assessment
Direct URL behavior is mixed:
/dashboard/**is authentication-protected bysrc/proxy.ts, but authorization still depends on page-level guards./dashboard/training-matrixhas no page guard, so an authenticated user can load the page shell directly even though its data API later blocks non-admin membership roles.- Several APIs are missing proxy-level authentication coverage entirely, including:
/api/online-lessons/**/api/permission-templates/**/api/user-permissions/**/api/permissions/catalog/api/permission-audit-logs
- Those routes appear to rely on route-handler guards instead of proxy enforcement. That is acceptable for security if every handler is guarded, but it is inconsistent with the rest of the API surface and increases review burden.
UI vs API Authorization Consistency
Main inconsistencies:
- Employee directory UI is HRD-only at page level, but
/api/employeesonly requires organization membership. - Training matrix nav suggests HRD/IT management access, but the page has no server guard and the API uses membership-role checks only.
- Reports nav is HRD-only, but the page allows all employee-area users and delegates scope reduction to server data logic.
- Audit logs nav is super-admin only, while page and API enforce
ITaccess. - Organizers nav is super-admin only, while the page uses
isHRD(...).
Conclusion:
- Navigation is not a reliable indicator of backend authorization.
- Several pages are stricter or looser than their APIs.
- Permission template rollout is incomplete because UI and API do not consistently consume the same access model.
Compatibility Mode Findings
Compatibility-mode authorization is still widespread:
requireHRD()requireIT()requireUserManagementAccess()canManageTrainingMatrix(membership.role)canViewAllReports(role)canReviewTrainingRecords(membership.role)- inline
isHRD(...)page checks
Risk:
- Any path that authorizes from compatibility roles can bypass explicit permission denies from templates or user overrides.
- This is especially important for review, reporting, user management, training matrix, and master-review flows.
Missing Permission Descriptions
Permission metadata review in src/features/permission-management/server/permission-management-catalog.ts:
- Most permission keys have catalog metadata and Thai descriptions.
descriptionEnis not populated for most entries.- The file content shows text encoding corruption in the stored description strings, which will degrade readability in the permission-management UI and audit output.
Findings by Severity
[High] Permission templates and user overrides are not the single source of truth for authorization
Module: Cross-cutting authorization
Location Found:
src/app/api/training-records/[id]/review/route.tssrc/app/api/training-matrix/route.tssrc/app/api/master-review/route.tssrc/features/reports/server/report-data.tssrc/app/dashboard/users/page.tsxsrc/app/dashboard/employee-directory/page.tsx
Details:
The codebase already resolves effectivePermissions, but many sensitive routes still authorize from membership or business-role compatibility logic. That means a permission template deny may not remove access from all paths for the same user.
Impact:
- Fine-grained RBAC is unreliable.
- Permission template rollout can produce false confidence.
- Privileged workflows may remain available after explicit deny overrides.
Evidence from Code:
- Training matrix uses
canManageTrainingMatrix(membership.role). - Reports use
canViewAllReports(role). - Master review uses
requireHRD(). - User management uses
requireUserManagementAccess()which maps toIT.
Example Scenario:
An admin-style member receives a template that removes report-wide access or training-matrix management. The user can still pass role-based guards because the route trusts membership role instead of effectivePermissions.
Recommendation:
Standardize all sensitive server-side authorization on requirePermission, requireAnyPermission, or requireAllPermissions, and reserve compatibility helpers only for temporary transitional wrappers.
Must Fix Before Production: Yes
[High] Employee and product management APIs allow any organization member to perform management actions by direct API access
Module: Employee Management, Products
Location Found:
src/app/api/employees/route.tssrc/app/api/products/route.ts
Details:
Both APIs only require requireOrganizationAccess(). There is no action-level permission check for list or create operations.
Impact:
- Any authenticated member in the same organization can query or create records through direct API calls even if the dashboard UI hides those actions.
- This is a true UI-vs-API authorization gap.
Evidence from Code:
GET /api/employeesandPOST /api/employeeshave noemployee_directory:*or import/review permission gate.GET /api/productsandPOST /api/productshave no product-specific privilege gate.
Example Scenario:
A standard employee cannot reach the employee directory through nav, but can still call /api/employees directly once authenticated in the same organization.
Recommendation:
Add server-side action gates for every management API and align them with the intended permission model.
Must Fix Before Production: Yes
[High] Reports authorization and scope are driven by membership role instead of report permissions
Module: Reports
Location Found:
src/features/reports/server/report-data.tssrc/app/api/reports/export/route.tssrc/app/dashboard/reports/page.tsx
Details:
canViewAllReports(role) grants organization-wide reporting only to membership owner or admin. The actual permission keys reports:self_read, reports:organization_read, and reports:export* do not control the real scope boundary.
Impact:
- Report permissions are partially decorative.
- Templates cannot precisely grant or revoke report scope independent of membership role.
- Export rules depend on role-derived access rather than permission-derived access.
Evidence from Code:
getReportsAccess()derivescanViewAllfromcanViewAllReports(scope.role).- Export route blocks non-organization-wide users from all reports except
employeeTranscriptusingcanViewAll, notreports:organization_read.
Example Scenario:
A user granted reports:organization_read through a template but holding membership member still cannot access org-wide reports, while an admin membership can.
Recommendation:
Replace report scope decisions with explicit report permissions and keep role defaults only as template seed values.
Must Fix Before Production: Yes
[Medium] Navigation, page guards, and API guards are inconsistent across key modules
Module: Navigation and dashboard access
Location Found:
src/config/nav-config.tssrc/hooks/use-nav.tssrc/app/dashboard/audit-logs/page.tsxsrc/app/dashboard/organizers/page.tsxsrc/app/dashboard/reports/page.tsxsrc/app/dashboard/training-matrix/page.tsx
Details:
The same feature is often gated differently in nav, page render, and API access.
Impact:
- Users get confusing UX.
- Direct URL behavior is harder to reason about.
- Authorization reviews become more error-prone.
Recommendation:
Define one canonical access contract per module and apply it consistently to nav visibility, page guard, and API handlers.
Must Fix Before Production: Consider
[Medium] Super-admin organization access mutates membership data as a side effect of authorization
Module: Session and organization access
Location Found:
src/lib/auth/session.ts
Details:
requireOrganizationAccess() automatically inserts an admin membership when a super_admin selects an organization without an existing membership.
Impact:
- Authorization has write-side effects.
- Auditability and least-privilege reasoning become harder.
- Accidental access expansion is possible through normal navigation.
Recommendation:
Separate "temporary super-admin impersonation/access" from persistent membership creation, or make membership creation an explicit administrative action.
Must Fix Before Production: Consider
[Low] Permission catalog descriptions have metadata quality issues
Module: Permission Management
Location Found:
src/features/permission-management/server/permission-management-catalog.ts
Details:
Descriptions exist, but descriptionEn is mostly absent and Thai strings appear encoding-corrupted in the file.
Impact:
- Permission management UX and audit readability are reduced.
- Not a direct security failure.
Recommendation:
Normalize encoding and complete optional description metadata where needed.
Must Fix Before Production: No
Privilege Escalation Risks
Primary privilege-escalation paths observed:
- Direct API access to
/api/employeesand/api/productsby ordinary org members. - Role-based fallback that ignores explicit permission denies in templates/overrides.
- Super-admin membership auto-creation inside
requireOrganizationAccess().
Recommended Remediation Order
- Lock down management APIs that currently require only organization membership.
- Convert sensitive server routes from compatibility-role checks to permission-first checks.
- Redefine reports scope and export authorization around
reports:*permissions. - Add missing page guards, especially for
training-matrix, and align page/API/nav contracts. - Remove authorization side effects from super-admin organization access.
- Clean up permission catalog metadata and encoding.
Files Reviewed
plans/tms-system-full-audit-prompts.mdsrc/config/nav-config.tssrc/hooks/use-nav.tssrc/components/layout/app-sidebar.tsxsrc/proxy.tssrc/lib/auth/session.tssrc/lib/auth/page-guards.tssrc/lib/auth/roles.tssrc/lib/auth/authorization.tssrc/lib/auth/authorization.server.tssrc/lib/auth/permissions.tssrc/app/dashboard/users/page.tsxsrc/app/dashboard/employee-directory/page.tsxsrc/app/dashboard/training-matrix/page.tsxsrc/app/dashboard/reports/page.tsxsrc/app/dashboard/audit-logs/page.tsxsrc/app/dashboard/organizers/page.tsxsrc/app/dashboard/training-policy/page.tsxsrc/app/dashboard/permissions/templates/page.tsxsrc/app/dashboard/permissions/users/page.tsxsrc/app/dashboard/permissions/audit-logs/page.tsxsrc/app/api/users/route.tssrc/app/api/employees/route.tssrc/app/api/products/route.tssrc/app/api/training-matrix/route.tssrc/app/api/training-records/[id]/review/route.tssrc/app/api/reports/export/route.tssrc/app/api/audit-logs/route.tssrc/app/api/audit-logs/export/route.tssrc/app/api/training-policies/route.tssrc/app/api/master-review/route.tssrc/app/api/notifications/route.tssrc/app/api/permission-templates/route.tssrc/app/api/permissions/catalog/route.tssrc/features/reports/server/report-data.tssrc/features/training-matrix/server/training-matrix-data.tssrc/features/permission-management/server/permission-management-catalog.ts
Areas Not Verified
- Runtime browser behavior for redirects, hidden actions, and toast/error handling
- Full API execution against seeded users for every role/template combination
- Exact UI rendering impact of permission catalog encoding issues
- Online lessons, announcements, and user-permission routes at full action-by-action depth
- Whether all non-proxy-covered API routes always apply equivalent route-handler guards in every nested handler