Authorisation and permissions

Client-side authorisation in this application is advisory. It shapes what a user sees and can attempt. Every protected operation is enforced by the backend. A missing guard is a usability bug; a missing backend check is a security bug. See decision D6.

The permission vocabulary

src/app/shared/users/user-permissions.enum.ts defines 40 codes. They divide roughly into:

Kind Codes

Login / session

AUTH_TOKEN, SUP_LOGIN, INT_LOGIN

Broad role

INT_SUPER_USER, VIEW_ONLY, FINANCE, MARKETING, CUST_SERVICE, Sales, UNDERWRITING, PROD_MGR, US ('us'), DEV, dev_support, DELETED

Administration

INT_UPDATE, INT_PERMS_ADM, INT_GLOBAL_SETTINGS, INT_EMAIL_SETTINGS, INT_RULES_UPDATE, INT_REP_ADMIN, CRM_ADMIN

Money operations

INT_MP_PMTS, INT_RCV, INT_XACT_CNCL, INT_FEE_UPD, INT_PMT_ADMIN, INT_PMT_REVIEW, MANAGE_PMT, MANAGE_BAD_DEBT

Supplier data

INT_SUP_CRED, MANAGE_LINKING_PAYMENT_GATEWAYS

Marketplace sub-permissions

MP_LOANS_MODIFY, MP_LOANS_DELETE, MP_STATEMENTS_MODIFY, MP_STATEMENTS_DELETE

Deny filters

REST_UW_ACCESS, ADVANCE_AUDITORS

Note the inconsistent casing: most are SCREAMING_SNAKE, but Sales, us and dev_support are not. The enum values are what the backend sends; match them exactly.

Full catalogue with enforcement points: the permission catalogue.

The two deny filters

REST_UW_ACCESS (restricted underwriting access) and ADVANCE_AUDITORS are not grants — they are restrictions. Holding one removes access rather than adding it, and they are applied with except: clauses across the whole application.

data: { permissions: { except: [USER_PERMISSION.REST_UW_ACCESS,
                                USER_PERMISSION.ADVANCE_AUDITORS] } }

Their reach goes well beyond routing:

Mechanism Effect

Route clauses

Nearly every feature area and supplier tab.

Sidebar

Almost every menu item is wrapped in *ngxPermissionsExcept="[REST_UW_ACCESS, ADVANCE_AUDITORS]".

Body class

AppComponent adds restricted-access to document.body for REST_UW_ACCESS, letting SCSS hide UI wholesale.

Data loading

SupplierEffects.getSupplierDetails replaces the funding, balance and contract calls with of(null) for ADVANCE_AUDITORS — the data never reaches the browser.

Audit logging

MonitorService.send() only logs when the user holds one of these two permissions — access by restricted users is traced as a KYC guardrail event. See observability.

Three route-level variants are defined in suppliers/suppliers-routing.routes.ts and spread into supplier tabs:

Constant Denies

RESTRICTED_UW_ACCESS_CONFIG

Both — the common case.

RESTRICTED_AUDITORS_ACCESS_CONFIG

REST_UW_ACCESS only. Auditors are allowed (used on the general tab).

RESTRICTED_REST_UW_ACCESS_ONLY_CONFIG

ADVANCE_AUDITORS only. Restricted-UW users are allowed (used on the underwriting parent).

The underwriting parent allows REST_UW_ACCESS but most of its children deny it, so a restricted user can reach the tab and then be bounced from daily and ucc. That is by design, not a bug — but it makes the effective permission surface hard to reason about from either the route table or the sidebar alone.

Only two routes use an allow clause: /admin (only: DEV) and suppliers/:key/underwriting/background-reports (only: FINANCE). Because only is an allow-list, FINANCE blocks even super users who lack that specific code.

The four gating mechanisms

1. NgxPermissionsGuard on routes

Reads data.permissions with only / except and an optional redirectTo. Runs third, after RouteAccessResolver and FeatureFlagsGeneralGuard. See routing.

Permissions are loaded into ngx-permissions in two places: AuthEffects.handleFullLogin at login, and AppComponent’s `auth subscription on every state change — the latter is what restores them after a reload.

2. Template directives

*ngxPermissionsOnly and *ngxPermissionsExcept appear in ~44 template locations, concentrated in sidenav-menu.component.html and the supplier workspace.

The sidebar’s permission clauses are maintained separately from the route’s. A mismatch shows a user a link that immediately bounces them. When you change a route’s permissions, change the menu entry too.

3. Imperative checks in components

Two different services, with different semantics — this is the most common source of confusion.

Call Semantics

NgxPermissionsService.getPermission(code)

Synchronous. Returns the permission object or undefined. Used as !!getPermission(…​) or != null. Reads the in-memory permission set.

NgxPermissionsService.hasPermission(code)

Returns a Promise<boolean>. Used with fromPromise(…​) or .then(…​).

UsersService.checkPermission(code, user?)

Returns an Observable<boolean>. Hits the users service; may consult the API.

UsersService.checkPermissions(codes, user)

Observable, all codes.

UsersService.checkGeneralPermission(s)

Observable variants that check the generalPerm flag rather than a supplier-scoped grant.

All five are in live use. getPermission for cheap synchronous hiding, checkPermission where the answer may depend on the user record.

4. SubPermissionsGuard

For sub-permissions, which ngx-permissions does not model:

// route
canActivate: [SubPermissionsGuard],
data: { requireOneOfSubPermissions: [USER_PERMISSION.MP_LOANS_MODIFY] }

The guard maps each code through UsersService.checkPermission, combineLatest`es them and passes if any is true. On failure it toasts ’Access denied.' and navigates to /. If requireOneOfSubPermissions is missing or empty it denies — a safe default.

Used only on the Amazon-lending add-loan and edit-loan routes. See marketplaces.

Permission models

// A permission granted to a user
class UserPermModel {
  permCode: USER_PERMISSION | string;
  permDesc: string;
  supplierKey: string;        // ← non-null means the grant is scoped to one supplier
  loginPermKey: string;       // the grant's own id, needed to revoke it
}

// A permission/group definition
class PermissionBaseModel { generalPerm: boolean; permCode; permDesc; }

@inheritSerialization(PermissionBaseModel)
class PermissionModel extends PermissionBaseModel {
  readonly type = 'group';
  assignable: boolean;        // may be assigned through the UI
  specSupPerm: boolean;       // may be scoped to a specific supplier
  get id()   { return this.permCode; }
  get key()  { return this.permCode; }
  get name() { return this.permDesc; }
}

Three flags define the shape of the permission system:

Flag Meaning

generalPerm

The permission applies globally rather than per supplier. checkGeneralPermission tests this.

specSupPerm

The permission can be scoped to a specific supplier — granted with a supplierKey.

assignable

The permission may be granted through the Manage UI. Non-assignable permissions exist but are managed elsewhere.

supplierKey on UserPermModel means Internal Admin supports supplier-scoped permissions — a user can hold a permission for one supplier only. ngx-permissions knows nothing about this: loadPermissions(perms.map(p ⇒ p.permCode)) discards the scope. So route and template gating is scope-blind; only UsersService.checkPermission (and the backend) can honour a supplier-scoped grant.

If you need scope-aware gating, use UsersService, not the directives.

PermissionModel.type = 'group' and UserModel.type = 'user' let the two be mixed in one list — which UsersAbstractService.getCombinedList() does, sorting by name. That is what powers assignee pickers that offer both users and groups.

Sub-permissions

GET perms/:permCode/sub-perms returns the sub-permissions of a permission. UsersService caches the result in an instance field after the first call, for any permCode — so the cache is keyed by nothing and a second call for a different permission returns the first one’s sub-permissions.

public getPermissionSubPerms(permCode: string): Observable<any[]> {
  if (!isNullOrUndefined(this.subPermsArr)) { return of(this.subPermsArr); }   // ← no key
  return this.httpClient.get(`${this.permsApiUrl}/${permCode}/sub-perms`)…
}

The cache ignores permCode. In practice only one permCode is queried per session (the logged-in user’s, by EvaComponent), which is why it has not surfaced as a bug. Do not add a second caller without fixing the cache.

Sub-permissions are also what EvaComponent forwards to EVA, so EVA’s authorisation derives from this call. See iframe messaging.

Administering permissions

/manage/users and /manage/perm-groups, gated by INT_PERMS_ADM:

Operation Endpoint

List permissions/groups

GET perms

A user’s grants

GET logins/:loginKey/perms

Grant

addPermissionToLogin(loginKey, { permCode, supplierKey }) — note the optional supplier scope

Revoke

removePermissionFromLogin(loginKey, loginPermKey) — by grant id, not by code

See manage.

Beyond permissions

Two other gates that are easy to confuse with authorisation:

Gate Difference

Feature flags

Control whether a capability exists in this environment, not who may use it. Composable with permissions — a route can be both flagged and permission-gated. See feature flags.

VPN check

VpnTesterService, behind the CHECK_VPN flag, warns when the operator is off the corporate network. Advisory; the real network restriction is upstream.

Practical guidance

  • Adding a restricted route: add the permission clause and the matching sidebar clause.

  • Hiding a control: prefer *ngxPermissionsExcept / *ngxPermissionsOnly in the template over an imperative check.

  • Needing an async answer or supplier scope: use UsersService.checkPermission.

  • Never rely on a guard as a security control.

  • When in doubt about which of getPermission / hasPermission / checkPermission to use, match the file you are editing — all three appear, and mixing paradigms in one component is worse than picking the local convention.