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 |
|
Broad role |
|
Administration |
|
Money operations |
|
Supplier data |
|
Marketplace sub-permissions |
|
Deny filters |
|
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
|
Body class |
|
Data loading |
|
Audit logging |
|
Three route-level variants are defined in
suppliers/suppliers-routing.routes.ts and spread into supplier tabs:
| Constant | Denies |
|---|---|
|
Both — the common case. |
|
|
|
|
|
The |
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 |
|---|---|
|
Synchronous. Returns the permission object or |
|
Returns a |
|
Returns an |
|
Observable, all codes. |
|
Observable variants that check the |
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 |
|---|---|
|
The permission applies globally rather than per supplier. |
|
The permission can be scoped to a specific supplier — granted with a
|
|
The permission may be granted through the Manage UI. Non-assignable permissions exist but are managed elsewhere. |
|
If you need scope-aware gating, use |
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.
The cache ignores |
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 |
|
A user’s grants |
|
Grant |
|
Revoke |
|
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 |
|
Practical guidance
-
Adding a restricted route: add the permission clause and the matching sidebar clause.
-
Hiding a control: prefer
*ngxPermissionsExcept/*ngxPermissionsOnlyin 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/checkPermissionto use, match the file you are editing — all three appear, and mixing paradigms in one component is worse than picking the local convention.