Skip to content

Role Enforcement — Portal & Internal

This document describes the permission model and enforcement strategy for Console's three role systems: Internal Users, Customer Portal Users, and Tenant-Level Roles.


1. Role Definitions

1.1 Internal Users (internal_users collection)

RoleDescription
platform_adminFull access to all Console admin features
ops_engineerOperations — instances, migrations, etc.
finance_adminFinance-only — billing, plans, meters
compliance_adminCompliance/audit — read access to System Workers, Discrepancies, Audit Log, Email Logs. Receives discrepancy email alerts. No finance.
readerView-only across the portal (instances, customers, tenants, users, releases, migrations, system pages). Excludes finance.

The roles column on internal_users is TEXT[]; a user may hold multiple roles. Permission checks union all roles. The "primary" role for nav/label purposes follows a fixed priority: platform_admin > ops_engineer > finance_admin > compliance_admin > reader.

1.2 Customer Portal Users (customer_users collection — portalRole field)

RoleDescription
ownerFull control. One per customer. Can manage admins, transfer ownership, delete account.
adminManage users, tenants, settings, auth config, webhooks, service accounts. Cannot promote to admin/owner.
billingView dashboard/tenants/instances + manage billing & invoices.
viewerRead-only access to dashboard, tenants, instances.
memberNo portal access. Can only access /me/* routes (profile, MFA). Auto-assigned when a user is added to a tenant without an existing portal role.

1.3 Tenant-Level Roles (tenant_user_assignments collection — role field)

RoleDescription
tenant_adminAdmin within the tenant (enforced by exto-go instance)
tenant_userRegular user within the tenant

These roles are passed to the exto-go instance during user provisioning. The instance enforces what each role can do within the tenant.


2. Permission Matrix — Internal Users

R = read-only access. Y = read + write. - = no access.

Capabilityplatform_adminops_engineerfinance_admincompliance_adminreader
Dashboard, Tenant InsightsYYYRR
Instances, Customers, TenantsYY--R
Releases, Changelog, EnvironmentYY--R
Internal Users (list)YRRRR
Internal User Invite/EditY----
Migrations (list)YY--R
Migrations (start/rerun/rollback)YY---
System WorkersYY-RR
Discrepancies (inbox)YY-RR
Discrepancies (purge/discard)YY---
Trigger Discrepancy AuditYY---
Audit Log, Email LogsYYYRR
User Attributes (definitions)Y----
Resolve DB URI (vault secrets)Y----
Billing Settings, Invoices, PaymentsY-Y--
Plans, Meters, Usage AggregatesY-Y--
Tenant Billing Config, Cascade PreviewY-Y--
Custom Plans, Credit ManagementY-Y--

UI note: Each role gets a tailored sidebar:

  • platform_admin / ops_engineer: full internal navigation (Finance group shown for platform_admin / users that also hold finance_admin).
  • finance_admin: finance-only sidebar.
  • compliance_admin: audit-focused sidebar (Dashboard, Tenant Insights, System Workers, Discrepancies, Audit Log, Email Logs, Changelog, Account).
  • reader: full internal navigation minus the Finance group and minus admin-only items (User Attributes).

Backend enforcement:

  • Operational internal roles (platform_admin, ops_engineer, finance_admin) bypass RequirePortalRole checks entirely.
  • Read-only internal roles (compliance_admin, reader) bypass non-financial RequirePortalRole actions but are explicitly denied on view_billing and manage_billing.
  • Per-handler guards: requireOpsOrAdmin (writes on system / discrepancies / migrations), requireFinanceAdmin (all billing writes + many finance reads), requirePlatformAdmin (user attributes, internal-user invite/edit, move customer user, claim ownership).
  • Per-handler read guard: requireInternalRead (system workers list, tenant discrepancy list) admits ops/admin + compliance_admin + reader.

3. Permission Matrix — Customer Portal Users

3.1 Portal Actions

Actionowneradminbillingviewermember
Portal DashboardYYYY-
View Tenants & InstancesYYYY-
View UsersYY---
Manage Users (invite, suspend, reactivate)YY---
Manage Tenant Users (assign/remove)YY---
View Billing & InvoicesYYY--
Manage BillingYYY--
Customer SettingsYY---
Auth Config (SSO, IdPs)YY---
Service AccountsYY---
WebhooksYY---
Account Settings (own profile)YYYYY
ChangelogYYYY-

3.2 Role Management (who can assign which portalRole)

ActorCan assignCannot assign
owneradmin, billing, viewer, memberowner (use transfer)
adminbilling, viewer, memberowner, admin
  • Only owner can promote/demote admins.
  • Admins cannot modify other admins or the owner.
  • Owner transfer is a dedicated action (see section 5).

3.3 Member Role Behaviour

  • Blocked from all portal routes by CustomerScopedMiddleware (HTTP 403).
  • Can only access /me/* routes (profile, MFA).
  • Gets a restricted top-bar layout with no sidebar.
  • Redirected to /customers/:id/account after login.
  • Auto-assigned when a user is added to a tenant without an existing portal role (handled in AssignUserToTenant and InviteTenantUser).

4. Owner-Only Capabilities

These actions are reserved exclusively for the owner role:

CapabilityDetail
Transfer ownershipDesignate another portal user as the new owner. The current owner is demoted to admin. Atomic operation.
Promote to adminOnly the owner can set portalRole = "admin" on another user.
Demote adminOnly the owner can change an admin's role to billing/viewer/member.
Delete customer accountSoft-delete the customer organisation (future — requires confirmation flow).

Constraint: There must always be exactly one owner per customer. The system must prevent:

  • Removing the owner without transferring ownership first.
  • Self-demotion if you are the only owner.

5. Enforcement Architecture

5.1 Backend Layers

Request


JWKSMiddleware          → JWT signature validation


RequestContextMiddleware → Resolve user type + role from DB
  │                        (internal_users or customer_users)


CustomerScopedMiddleware → Scope check (customer can only access own data)
  │                        Block member role from portal routes
  │                        Internal users bypass


requirePortalRole(action) → Per-route permission check
  │                          Uses central permission map
  │                          Internal users bypass


Service Layer             → Role-change validation
                             (who can assign which portalRole)
                             Owner transfer logic

5.1.1 Permission Map (internal/app/permissions.go)

Central map[action][]allowedRoles defining all portal permissions. Single source of truth for both requirePortalRole middleware and service-layer validation.

Helper functions:

  • CanPerform(role string, action string) bool
  • AllowedRoleChanges(actorRole string) []string

5.1.2 Route Middleware (internal/auth/customer_scope.go)

requirePortalRole(actions ...string) — Gin middleware factory:

  1. Extract RequestClaims from context.
  2. Internal user → allow (bypass).
  3. Check CanPerform(claims.Role, action) for each action.
  4. Deny → HTTP 403 {"error": "insufficient permissions"}.

Applied per-route in cmd/api/main.go:

Route PatternRequired Action
GET /customers/:id/usersview_users
POST /customers/:id/users/invitemanage_users
POST /customers/:id/users/invite-bulkmanage_users
PUT /customers/:id/users/:uidmanage_users + service-layer validation
POST /customers/:id/users/:uid/suspendmanage_users
POST /customers/:id/users/:uid/reactivatemanage_users
GET /customers/:id/billing/*view_billing
PUT /customers/:id/settingsmanage_settings
* /customers/:id/auth-config/*manage_auth_config
* /customers/:id/service-accounts/*manage_service_accounts
* /customers/:id/webhooks/*manage_webhooks

5.1.3 Service-Layer Validation (internal/handler/users/service.go)

UpdateCustomerUser — before changing portalRole:

  1. Resolve actor role and target user's current role.
  2. Check AllowedRoleChanges(actorRole) includes the new role.
  3. Prevent self-demotion for the only owner.
  4. For owner transfer: atomically set new owner + demote old owner to admin.

5.2 Frontend Layers

useConsoleAuth (Zustand store)


permissions.ts          → canPerform(role, action) mirror of backend map
  │                        useCanPerform(action) hook


<RequirePortalRole>     → Route guard component
  │                        Renders "access denied" or redirects if denied


CustomerPortalLayout    → Sidebar filtering based on canPerform


Page Components         → Show/hide action buttons (invite, suspend, role
                           dropdowns) based on canPerform

5.2.1 Permission Module (web/src/auth/permissions.ts)

  • Mirrors backend permission map.
  • canPerform(role, action): boolean
  • useCanPerform(action): boolean — hook using useConsoleAuth.
  • allowedRoleChanges(actorRole): string[] — for role dropdown options.

5.2.2 Route Guard (web/src/auth/RequirePortalRole.tsx)

Wrapper component for portal routes:

tsx
<RequirePortalRole actions={["view_users"]}>
  <CustomerUsersPage />
</RequirePortalRole>

Internal users always pass. Denied users see an "insufficient permissions" page.

Applied in web/src/routes/index.tsx:

RouteRequired Action
/dashboardview_dashboard
/tenantsview_tenants
/usersview_users
/billingview_billing
/settingsmanage_settings
/account(no guard)

5.2.3 Sidebar Filtering (web/src/layout/CustomerPortalLayout.tsx)

Filter navigation items by canPerform:

RoleVisible Nav Items
ownerAll
adminAll
billingDashboard, Tenants, Instances, Billing, Changelog, Account
viewerDashboard, Tenants, Instances, Changelog, Account
memberAccount only (restricted layout, no sidebar)

5.2.4 Per-Page Action Visibility

  • Users page: Hide "Invite" button unless manage_users. Role dropdown only shows options from allowedRoleChanges(actorRole).
  • Tenant detail: Hide "Assign Users" / "Invite" unless manage_tenant_users.
  • Settings page: Disable form fields unless manage_settings.
  • Owner transfer: Visible only to current owner, with confirmation dialog.

6. Key Enforcement Points (Summary)

LayerFileWhat it does
JWT validationinternal/auth/middleware.goVerify token, resolve user type/role
Customer scopinginternal/auth/customer_scope.goScope isolation + block member role
Route permissionsinternal/auth/customer_scope.gorequirePortalRole per-route checks
Role-change logicinternal/handler/users/service.goWho can assign which portalRole
Internal admininternal/handler/billing/handler.gorequireFinanceAdmin()
Internal admininternal/handler/migrations/handler.gorequireOpsOrAdmin()
Internal admininternal/app/request.goIsPlatformAdmin()
Frontend permsweb/src/auth/permissions.tsClient-side permission map + hooks
Frontend guardsweb/src/auth/RequirePortalRole.tsxRoute-level access control
Frontend navweb/src/layout/CustomerPortalLayout.tsxSidebar filtering by role

The backend is the source of truth. Frontend checks are for UX only — the server returns 403 for any unauthorized request regardless of what the UI shows.


7. Implementation Order

StepScopeDescription
1Backendinternal/app/permissions.go — permission map + helpers
2Backendinternal/auth/customer_scope.gorequirePortalRole middleware
3Backendcmd/api/main.go — apply middleware to portal routes
4Backendinternal/handler/users/service.go — role-change validation + owner transfer
5Frontendweb/src/auth/permissions.ts — permission map + hooks
6Frontendweb/src/auth/RequirePortalRole.tsx — route guard component
7Frontendweb/src/routes/index.tsx — wrap portal routes with guards
8Frontendweb/src/layout/CustomerPortalLayout.tsx — sidebar filtering
9FrontendPortal pages — show/hide action buttons based on permissions
10BothOwnership transfer endpoint + UI

Steps 1-4 and 5-6 can be done in parallel. Steps 7-9 depend on 5-6.