User Permissions & Module Gate
Overview
Access control in cms-api-go is made up of four layers that behave quite differently. This is one of the easiest parts of the system to misread, so it is worth separating clearly.
- CASL policy (
@CheckPolicies) — metadata is present on every route, but enforcement is turned off:CheckPoliciesis a no-op that lets every request through, matching the original NestJSPoliciesGuard.canActivate, which always returnedtrue. As a result, the permissions insystem_moduleandsystem_role_moduleserve only to show or hide menu items in cms-web, viaGET /api/user/:id/permission. - SuperAdminGuard — actually enforced:
roleIdmust equal 1. - ModuleGate — actually enforced. When a platform admin disables a module for an organization, a customer calling the API directly gets a 403.
- AppEnabledGuard — actually enforced. Apps such as appointment, loyalty, and bulletin must be enabled for the organization before they can be used.
Business Flow
Reading your own permissions (for menu rendering)
- cms-web calls
GET /api/user/:id/permission, where:idmust be the caller's own id. Anything else returns 400 with the messageCannot access other userId. - The service loads the user together with
SystemRoleModule(the role baseline) and the full list of module names fromsystem_module. - If the user has
type = customerand their organization has rows inorganization_module, that organization's override replaces the baseline. Platform admins control these overrides. - If the user has
type = onemoby— which includes role 1, the platform operator — the role baseline always applies and is never overridden. - The response is a list shaped as
{subject: module name, action: [...]}, which cms-web uses to decide which menu items to display.
ModuleGate (server-side enforcement)
- The middleware reads the caller's
user.typefrom CLS viaselfId. - If the caller is
onemoby, or cannot be identified, the request passes straight through (governors bypass). - If the caller is
customer, the gate asksorgmodulesetting.IsModuleEnabledForOrg(orgId, moduleName)using the same baseline-versus-override logic described in step 3 above. - A disabled module returns 403 with
ModuleDisabled. However, an error during the lookup fails OPEN — the request is allowed through and the error is logged — so that a hiccuping query cannot take the system down or lock an organization out of its modules. PlatformOnlydoes the opposite and fails CLOSED, because the risk there is privilege escalation. It protectsPUT /api/apps/:appId, which enables or disables an app for an organization and must only ever be driven from the platform admin console.
Key Files & Functions
| File | Responsibility |
|---|---|
internal/modules/auth/policy.go | The PolicyAction and PolicyModule enums, plus Can() and the no-op CheckPolicies() |
internal/modules/auth/guards.go | SuperAdmin() and InternalApiKey() |
internal/modules/modulegate/modulegate.go | ModuleGate(d, moduleName), PlatformOnly(d), gate(), and dbCallerType() |
internal/modules/apps/guard.go | AppEnabledGuard(d) — matches the regex /apps/(\w+) and then checks IsAppEnabled |
internal/modules/user/service.go | FindPermissionByUserID (around line 405) |
internal/modules/orgmodulesetting/service.go | IsModuleEnabledForOrg — the resolver shared by both the gate and the permission endpoint |
Related endpoint:
| Method | Route | Handler | Note |
|---|---|---|---|
| GET | /api/user/:id/permission | ct.findPermissionByUserId | No @CheckPolicies; the service restricts reads to the caller's own record |
Modules wrapped by ModuleGate (named as they appear in system_module):
api-key, attribute-master, audiences, auto-response, campaign, customer-database,
form-builder, friend-track, import-mapping, report-all-friends, rich-menu, rich-message,
template-message, trigger-rule, and workflow.
The PolicyModule enum values present in the code:
line-oa, campaign, rich-message, rich-menu, auto-response, organization, dashboard,
user, system_role, system_module, line-user, audiences, report-all-friends,
template-message, api-key, form-builder, customer-database, import-mapping,
tracking-log, attribute-master, trigger-rule, workflow, friend-track,
system-attribute, quick-reply, and otp-config.
Connections to Other Services
- Tables —
system_module,system_role,system_role_module,organization_module,user, andline_oa_app. - CLS — uses
selfId,organizationId,roleId, andisSuperAdmin, all populated by the JWT guard. - Who sets the overrides — platform admins, through the Organization Module Setting module.
- App-level gating — tied directly to the Apps Platform module.
- Frontend — cms-web consumes these permissions to render its menus.