@bluealba/platform-cli 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +278 -15
- package/docs/404.mdx +5 -0
- package/docs/architecture/api-explorer.mdx +478 -0
- package/docs/architecture/architecture-diagrams.mdx +12 -0
- package/docs/architecture/authentication-system.mdx +903 -0
- package/docs/architecture/authorization-system.mdx +886 -0
- package/docs/architecture/bootstrap.mdx +1442 -0
- package/docs/architecture/gateway-architecture.mdx +845 -0
- package/docs/architecture/multi-tenancy.mdx +1150 -0
- package/docs/architecture/overview.mdx +776 -0
- package/docs/architecture/scheduler.mdx +818 -0
- package/docs/architecture/shell.mdx +885 -0
- package/docs/architecture/ui-extension-points.mdx +781 -0
- package/docs/architecture/user-states.mdx +794 -0
- package/docs/development/overview.mdx +21 -0
- package/docs/development/workflow.mdx +914 -0
- package/docs/getting-started/core-concepts.mdx +892 -0
- package/docs/getting-started/installation.mdx +780 -0
- package/docs/getting-started/overview.mdx +83 -0
- package/docs/getting-started/quick-start.mdx +940 -0
- package/docs/guides/adding-documentation-sites.mdx +1367 -0
- package/docs/guides/creating-services.mdx +1736 -0
- package/docs/guides/creating-ui-modules.mdx +1860 -0
- package/docs/guides/identity-providers.mdx +1007 -0
- package/docs/guides/mermaid-diagrams.mdx +212 -0
- package/docs/guides/using-feature-flags.mdx +1059 -0
- package/docs/guides/working-with-rooms.mdx +566 -0
- package/docs/index.mdx +57 -0
- package/docs/platform-cli/commands.mdx +604 -0
- package/docs/platform-cli/overview.mdx +195 -0
- package/package.json +5 -2
- package/skills/ba-platform/platform-cli.skill.md +26 -0
- package/skills/ba-platform/platform.skill.md +35 -0
- package/templates/application-monorepo-template/gitignore +95 -0
- package/templates/bootstrap-service-template/Dockerfile.development +1 -1
- package/templates/bootstrap-service-template/gitignore +57 -0
- package/templates/bootstrap-service-template/package.json +1 -1
- package/templates/bootstrap-service-template/src/main.ts +6 -16
- package/templates/customization-ui-module-template/Dockerfile.development +1 -1
- package/templates/customization-ui-module-template/gitignore +73 -0
- package/templates/nestjs-service-module-template/Dockerfile.development +1 -1
- package/templates/nestjs-service-module-template/gitignore +56 -0
- package/templates/platform-init-template/{{platformName}}-core/gitignore +97 -0
- package/templates/platform-init-template/{{platformName}}-core/local/.env.example +1 -1
- package/templates/platform-init-template/{{platformName}}-core/local/platform-docker-compose.yml +1 -1
- package/templates/platform-init-template/{{platformName}}-core/local/{{platformName}}-core-docker-compose.yml +0 -1
- package/templates/react-ui-module-template/Dockerfile +1 -1
- package/templates/react-ui-module-template/Dockerfile.development +1 -3
- package/templates/react-ui-module-template/caddy/Caddyfile +1 -1
- package/templates/react-ui-module-template/gitignore +72 -0
- package/templates/react-ui-module-template/Dockerfile_nginx +0 -11
- package/templates/react-ui-module-template/nginx/default.conf +0 -23
|
@@ -0,0 +1,886 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Authorization System
|
|
3
|
+
description: Deep dive into the Blue Alba Platform authorization architecture - RBAC model, operations, roles, rules, and permission checking
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
import { Card, CardGrid, Aside, Tabs, TabItem } from '@astrojs/starlight/components';
|
|
7
|
+
|
|
8
|
+
The Blue Alba Platform implements a comprehensive **Role-Based Access Control (RBAC)** system with fine-grained permissions, conditional rules, and resource-level authorization.
|
|
9
|
+
|
|
10
|
+
## Authorization Model Overview
|
|
11
|
+
|
|
12
|
+
The authorization system is built on four core concepts that form a hierarchy:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
Applications
|
|
16
|
+
↓
|
|
17
|
+
Operations (permissions)
|
|
18
|
+
↓
|
|
19
|
+
Roles (collections of operations)
|
|
20
|
+
↓
|
|
21
|
+
Rules (conditional allow/deny logic)
|
|
22
|
+
↓
|
|
23
|
+
Users & Groups (assignment targets)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
<CardGrid stagger>
|
|
27
|
+
<Card title="Applications" icon="seti:folder">
|
|
28
|
+
Top-level boundary for authorization. Operations, roles, and rules are scoped to applications.
|
|
29
|
+
</Card>
|
|
30
|
+
|
|
31
|
+
<Card title="Operations" icon="approve-check">
|
|
32
|
+
Granular permissions representing specific actions (e.g., `users::read`, `orders::create`).
|
|
33
|
+
</Card>
|
|
34
|
+
|
|
35
|
+
<Card title="Roles" icon="seti:config">
|
|
36
|
+
Named collections of operations assigned to users and groups.
|
|
37
|
+
</Card>
|
|
38
|
+
|
|
39
|
+
<Card title="Rules" icon="seti:license">
|
|
40
|
+
Conditional logic that allows or denies access based on context.
|
|
41
|
+
</Card>
|
|
42
|
+
</CardGrid>
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Applications
|
|
47
|
+
|
|
48
|
+
Applications are the highest level of authorization scoping.
|
|
49
|
+
|
|
50
|
+
### Application Entity
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
interface Application {
|
|
54
|
+
id: number;
|
|
55
|
+
name: string; // Unique identifier (e.g., "customer-portal")
|
|
56
|
+
displayName: string; // Human-readable name
|
|
57
|
+
description?: string;
|
|
58
|
+
allowedByDefault: boolean; // If true, users get access by default
|
|
59
|
+
createdAt: Date;
|
|
60
|
+
createdBy: string;
|
|
61
|
+
updatedAt: Date;
|
|
62
|
+
updatedBy: string;
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Operations
|
|
69
|
+
|
|
70
|
+
**Operations** represent the most granular level of permission in the system.
|
|
71
|
+
|
|
72
|
+
### Operation Entity
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
interface Operation {
|
|
76
|
+
id: number;
|
|
77
|
+
name: string; // e.g., "users::read"
|
|
78
|
+
description?: string; // Human-readable description
|
|
79
|
+
applicationId: number; // Scoped to application
|
|
80
|
+
createdAt: Date;
|
|
81
|
+
createdBy: string;
|
|
82
|
+
updatedAt: Date;
|
|
83
|
+
updatedBy: string;
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Operation Naming Conventions
|
|
88
|
+
|
|
89
|
+
Operations follow a hierarchical naming pattern for consistency:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
{domain}::{resource}::{action}[.{modifier}]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Structure**
|
|
96
|
+
|
|
97
|
+
- **domain**: Functional area of the system (e.g., authorization, authentication, documentation)
|
|
98
|
+
- **resource**: Specific entity within the domain (e.g., role, auth-methods, impersonation)
|
|
99
|
+
- **action**: Operation to perform (e.g., delete, manage, read, config)
|
|
100
|
+
- **modifier (optional)**: Additional qualifier to specify scope or context
|
|
101
|
+
|
|
102
|
+
**Examples**:
|
|
103
|
+
|
|
104
|
+
<Tabs>
|
|
105
|
+
<TabItem label="Basic Operations">
|
|
106
|
+
```
|
|
107
|
+
users::read → View users
|
|
108
|
+
users::write → Create/update users
|
|
109
|
+
users::delete → Delete users
|
|
110
|
+
orders::read → View orders
|
|
111
|
+
orders::create → Create new orders
|
|
112
|
+
orders::update → Modify existing orders
|
|
113
|
+
orders::delete → Delete orders
|
|
114
|
+
reports::read → View reports
|
|
115
|
+
reports::export → Export reports to file
|
|
116
|
+
```
|
|
117
|
+
</TabItem>
|
|
118
|
+
|
|
119
|
+
<TabItem label="Complex Operations">
|
|
120
|
+
```
|
|
121
|
+
authorization::role::delete → Delete roles from the authorization system
|
|
122
|
+
authorization::role::manage-operations → Manage operations assigned to specific roles
|
|
123
|
+
```
|
|
124
|
+
</TabItem>
|
|
125
|
+
</Tabs>
|
|
126
|
+
|
|
127
|
+
### Using Operations in Code
|
|
128
|
+
|
|
129
|
+
<Tabs>
|
|
130
|
+
<TabItem label="Frontend (React)">
|
|
131
|
+
```typescript
|
|
132
|
+
import { Authorized, useAuth } from '@bluealba/pae-ui-react-core';
|
|
133
|
+
|
|
134
|
+
function OrderManagement() {
|
|
135
|
+
const { hasAccess } = useAuth();
|
|
136
|
+
|
|
137
|
+
// Check single operation
|
|
138
|
+
const canRead = hasAccess('orders::read');
|
|
139
|
+
|
|
140
|
+
// Check if user has ALL of the operations
|
|
141
|
+
const isAdmin = hasAccess(['orders::read', 'orders::write', 'orders::delete']);
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<div>
|
|
145
|
+
{/* Declarative authorization */}
|
|
146
|
+
<Authorized operations=["orders::read"]>
|
|
147
|
+
<OrderList />
|
|
148
|
+
</Authorized>
|
|
149
|
+
|
|
150
|
+
{/* Programmatic authorization */}
|
|
151
|
+
{canRead && <ViewOrderButton />}
|
|
152
|
+
{isAdmin && <AdminPanel />}
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
</TabItem>
|
|
158
|
+
</Tabs>
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Roles
|
|
163
|
+
|
|
164
|
+
**Roles** are named collections of operations assigned to users and groups.
|
|
165
|
+
|
|
166
|
+
### Role Entity
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
interface Role {
|
|
170
|
+
id: number;
|
|
171
|
+
name: string; // e.g., "admin", "viewer", "editor"
|
|
172
|
+
description?: string;
|
|
173
|
+
applicationId: number; // Scoped to application
|
|
174
|
+
operations: Operation[]; // Associated operations
|
|
175
|
+
createdAt: Date;
|
|
176
|
+
createdBy: string;
|
|
177
|
+
updatedAt: Date;
|
|
178
|
+
updatedBy: string;
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Standard Role Patterns
|
|
183
|
+
|
|
184
|
+
<CardGrid>
|
|
185
|
+
<Card title="Admin Role" icon="star">
|
|
186
|
+
**Full application access**
|
|
187
|
+
|
|
188
|
+
Operations:
|
|
189
|
+
- `{app}.*` (all operations for the application)
|
|
190
|
+
|
|
191
|
+
Use Case: System administrators
|
|
192
|
+
</Card>
|
|
193
|
+
|
|
194
|
+
<Card title="Viewer Role" icon="open-book">
|
|
195
|
+
**Read-only access**
|
|
196
|
+
|
|
197
|
+
Operations:
|
|
198
|
+
- `users::read`
|
|
199
|
+
- `orders::read`
|
|
200
|
+
- `reports::read`
|
|
201
|
+
|
|
202
|
+
Use Case: Users who need visibility without modification rights
|
|
203
|
+
</Card>
|
|
204
|
+
|
|
205
|
+
<Card title="Editor Role" icon="pencil">
|
|
206
|
+
**Read and write access**
|
|
207
|
+
|
|
208
|
+
Operations:
|
|
209
|
+
- `users::read`
|
|
210
|
+
- `users::write`
|
|
211
|
+
- `orders::read`
|
|
212
|
+
- `orders::write`
|
|
213
|
+
|
|
214
|
+
Use Case: Users who can create and update data
|
|
215
|
+
</Card>
|
|
216
|
+
|
|
217
|
+
<Card title="Approver Role" icon="approve-check">
|
|
218
|
+
**Specialized workflow role**
|
|
219
|
+
|
|
220
|
+
Operations:
|
|
221
|
+
- `orders::read`
|
|
222
|
+
- `orders::approve`
|
|
223
|
+
- `orders::reject`
|
|
224
|
+
|
|
225
|
+
Use Case: Users with approval authority
|
|
226
|
+
</Card>
|
|
227
|
+
</CardGrid>
|
|
228
|
+
|
|
229
|
+
### Role Assignment
|
|
230
|
+
|
|
231
|
+
Roles can be assigned in two ways:
|
|
232
|
+
|
|
233
|
+
<Tabs>
|
|
234
|
+
<TabItem label="Direct Assignment">
|
|
235
|
+
```typescript
|
|
236
|
+
// Assign role directly to a user
|
|
237
|
+
POST /applications/:appName/rules
|
|
238
|
+
{
|
|
239
|
+
"subjectType": "user",
|
|
240
|
+
"subject": "john@acme.com",
|
|
241
|
+
"resourceType": "role",
|
|
242
|
+
"resource": "editor",
|
|
243
|
+
"denied": false,
|
|
244
|
+
"tenantId": 1
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Direct assignment creates a rule granting the role to the specific user within the given tenant.
|
|
249
|
+
</TabItem>
|
|
250
|
+
|
|
251
|
+
<TabItem label="Group Assignment">
|
|
252
|
+
```typescript
|
|
253
|
+
// Assign role to a group
|
|
254
|
+
POST /applications/:appName/rules
|
|
255
|
+
{
|
|
256
|
+
"subjectType": "group",
|
|
257
|
+
"subject": "editors-group",
|
|
258
|
+
"resourceType": "role",
|
|
259
|
+
"resource": "editor",
|
|
260
|
+
"denied": false,
|
|
261
|
+
"tenantId": 1
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Add user to group
|
|
265
|
+
POST /_/groups/:groupId/members/:username
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
All users in the group automatically receive the role's operations.
|
|
269
|
+
</TabItem>
|
|
270
|
+
</Tabs>
|
|
271
|
+
|
|
272
|
+
<Aside type="tip">
|
|
273
|
+
**Best Practice**: Use group-based role assignment for easier management. Instead of assigning roles to 100 users individually, assign the role to a group and manage group membership.
|
|
274
|
+
</Aside>
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Rules
|
|
279
|
+
|
|
280
|
+
**Rules** add conditional logic to authorization, enabling fine-grained access control.
|
|
281
|
+
|
|
282
|
+
### Rule Entity
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
interface Rule {
|
|
286
|
+
id: number;
|
|
287
|
+
subjectType: 'user' | 'group'; // Who the rule applies to
|
|
288
|
+
subject: string; // Username or group name
|
|
289
|
+
resourceType: 'operation' | 'role' | 'application';
|
|
290
|
+
resourceId: number; // ID of the resource
|
|
291
|
+
resourceName: string; // Name of the resource
|
|
292
|
+
denied: boolean; // Allow (false) or Deny (true)
|
|
293
|
+
applicationId?: number; // Optional application scope
|
|
294
|
+
tenantId?: number; // Tenant scope (null = global, applies to all tenants)
|
|
295
|
+
createdAt: Date;
|
|
296
|
+
createdBy: string;
|
|
297
|
+
updatedAt: Date;
|
|
298
|
+
updatedBy: string;
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Tenant-Scoped Rules
|
|
303
|
+
|
|
304
|
+
Rules can be scoped to a specific tenant or apply globally across all tenants:
|
|
305
|
+
|
|
306
|
+
- **Tenant-scoped rules** (`tenantId` set): Apply only within the specified tenant. All rules created from the Admin UI are automatically scoped to the current tenant.
|
|
307
|
+
- **Global rules** (`tenantId` is `null`): Apply across all tenants. These are typically legacy rules created before tenant scoping was introduced.
|
|
308
|
+
|
|
309
|
+
<Aside type="note">
|
|
310
|
+
In the Admin UI, global rules are highlighted with a **"Global"** badge. When deleting a global rule, the UI displays a confirmation warning that the deletion will affect all tenants.
|
|
311
|
+
</Aside>
|
|
312
|
+
|
|
313
|
+
### Rule Types
|
|
314
|
+
|
|
315
|
+
<Tabs>
|
|
316
|
+
<TabItem label="Allow Rules">
|
|
317
|
+
Grant access to a resource.
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
{
|
|
321
|
+
subjectType: 'user',
|
|
322
|
+
subject: 'john@acme.com',
|
|
323
|
+
resourceType: 'operation',
|
|
324
|
+
resourceName: 'orders::read',
|
|
325
|
+
denied: false // ALLOW
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Result: User `john@acme.com` can perform `orders.read` operation.
|
|
330
|
+
</TabItem>
|
|
331
|
+
|
|
332
|
+
<TabItem label="Deny Rules">
|
|
333
|
+
Revoke access to a resource (takes precedence over allow).
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
{
|
|
337
|
+
subjectType: 'user',
|
|
338
|
+
subject: 'john@acme.com',
|
|
339
|
+
resourceType: 'operation',
|
|
340
|
+
resourceName: 'orders::delete',
|
|
341
|
+
denied: true // DENY
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Result: User `john@acme.com` cannot perform `orders.delete`, even if a role grants it.
|
|
346
|
+
</TabItem>
|
|
347
|
+
|
|
348
|
+
<TabItem label="Group Rules">
|
|
349
|
+
Apply rules to all members of a group.
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
{
|
|
353
|
+
subjectType: 'group',
|
|
354
|
+
subject: 'admins',
|
|
355
|
+
resourceType: 'role',
|
|
356
|
+
resourceName: 'admin',
|
|
357
|
+
denied: false // ALLOW
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Result: All users in the `admins` group receive the `admin` role.
|
|
362
|
+
</TabItem>
|
|
363
|
+
|
|
364
|
+
<TabItem label="Application Rules">
|
|
365
|
+
Grant access to entire applications.
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
{
|
|
369
|
+
subjectType: 'user',
|
|
370
|
+
subject: 'john@acme.com',
|
|
371
|
+
resourceType: 'application',
|
|
372
|
+
resourceName: 'customer-portal',
|
|
373
|
+
denied: false // ALLOW
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Result: User can access the `customer-portal` application.
|
|
378
|
+
</TabItem>
|
|
379
|
+
</Tabs>
|
|
380
|
+
|
|
381
|
+
### Rule Evaluation Order
|
|
382
|
+
|
|
383
|
+
Rules are evaluated in a specific order to determine authorization:
|
|
384
|
+
|
|
385
|
+
```
|
|
386
|
+
1. Check DENY rules first (explicit denials take precedence)
|
|
387
|
+
↓
|
|
388
|
+
2. If denied, return DENIED
|
|
389
|
+
↓
|
|
390
|
+
3. Check ALLOW rules
|
|
391
|
+
↓
|
|
392
|
+
4. If allowed, return ALLOWED
|
|
393
|
+
↓
|
|
394
|
+
5. Default: DENIED (fail-safe)
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
**Example**:
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
// User has these rules:
|
|
401
|
+
[
|
|
402
|
+
{ subject: 'john', resource: 'orders::read', denied: false }, // ALLOW
|
|
403
|
+
{ subject: 'john', resource: 'orders::write', denied: false }, // ALLOW
|
|
404
|
+
{ subject: 'john', resource: 'orders::delete', denied: true } // DENY
|
|
405
|
+
]
|
|
406
|
+
|
|
407
|
+
// Authorization checks:
|
|
408
|
+
isAuthorized('john', 'orders::read') → true (explicit allow)
|
|
409
|
+
isAuthorized('john', 'orders::write') → true (explicit allow)
|
|
410
|
+
isAuthorized('john', 'orders::delete') → false (explicit deny)
|
|
411
|
+
isAuthorized('john', 'orders::approve') → false (no rule, default deny)
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
## Authorization Flow
|
|
417
|
+
|
|
418
|
+
### High-Level Flow
|
|
419
|
+
|
|
420
|
+
```
|
|
421
|
+
1. User makes request
|
|
422
|
+
↓
|
|
423
|
+
2. Gateway authenticates user (JWT validation)
|
|
424
|
+
↓
|
|
425
|
+
3. Gateway extracts user's groups from JWT
|
|
426
|
+
↓
|
|
427
|
+
4. Gateway resolves tenant from request context
|
|
428
|
+
↓
|
|
429
|
+
5. Gateway queries authorization service for user's allowed resources
|
|
430
|
+
(filtered by tenant: includes tenant-specific + global rules)
|
|
431
|
+
↓
|
|
432
|
+
6. Authorization service evaluates rules and returns AllowedResources
|
|
433
|
+
↓
|
|
434
|
+
7. Gateway resolves target module from catalog
|
|
435
|
+
↓
|
|
436
|
+
8. Gateway checks if user is authorized for module + path + method
|
|
437
|
+
↓
|
|
438
|
+
9. If authorized, forward request; otherwise, return 403
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### Allowed Resources Structure
|
|
442
|
+
|
|
443
|
+
The authorization service returns a consolidated view of user permissions:
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
interface AllowedResources {
|
|
447
|
+
applications: Array<{
|
|
448
|
+
id: number;
|
|
449
|
+
name: string;
|
|
450
|
+
displayName: string;
|
|
451
|
+
description: string;
|
|
452
|
+
createdAt: string;
|
|
453
|
+
createdBy: string;
|
|
454
|
+
updatedAt: string;
|
|
455
|
+
updatedBy: string;
|
|
456
|
+
operations: Array<{
|
|
457
|
+
id: number;
|
|
458
|
+
name: string;
|
|
459
|
+
}>;
|
|
460
|
+
}>;
|
|
461
|
+
}
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
**Example**:
|
|
465
|
+
|
|
466
|
+
```json
|
|
467
|
+
{
|
|
468
|
+
"applications": [
|
|
469
|
+
{
|
|
470
|
+
"id": 1,
|
|
471
|
+
"name": "customer-portal",
|
|
472
|
+
"displayName": "Customer Portal"
|
|
473
|
+
"description": "",
|
|
474
|
+
"createdAt": "2026-01-01T12:00:00.000Z",
|
|
475
|
+
"createdBy": "service",
|
|
476
|
+
"updatedAt": "2026-01-01T12:00:00.000Z",
|
|
477
|
+
"updatedBy": "service",
|
|
478
|
+
"operations": [
|
|
479
|
+
{ "id": 101, "name": "users::read" },
|
|
480
|
+
{ "id": 102, "name": "users::write" },
|
|
481
|
+
{ "id": 201, "name": "orders::read" },
|
|
482
|
+
{ "id": 202, "name": "orders::write" }
|
|
483
|
+
]
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
"id": 2,
|
|
487
|
+
"name": "admin-console",
|
|
488
|
+
"displayName": "Admin Console"
|
|
489
|
+
"description": "",
|
|
490
|
+
"createdAt": "2026-01-01T12:00:00.000Z",
|
|
491
|
+
"createdBy": "service",
|
|
492
|
+
"updatedAt": "2026-01-01T12:00:00.000Z",
|
|
493
|
+
"updatedBy": "service",
|
|
494
|
+
"operations": [
|
|
495
|
+
{ "id": 301, "name": "admin::read" },
|
|
496
|
+
{ "id": 302, "name": "admin::write" }
|
|
497
|
+
]
|
|
498
|
+
}
|
|
499
|
+
]
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### Authorization Check Implementation
|
|
504
|
+
|
|
505
|
+
The authorization check happens in multiple locations:
|
|
506
|
+
|
|
507
|
+
<Tabs>
|
|
508
|
+
<TabItem label="Gateway Level">
|
|
509
|
+
```typescript
|
|
510
|
+
// apps/pae-nestjs-gateway-service/src/authorization/authorization.service.ts
|
|
511
|
+
|
|
512
|
+
async authorizeRequest(
|
|
513
|
+
context: ExecutionContext,
|
|
514
|
+
username: string,
|
|
515
|
+
tenantId?: number
|
|
516
|
+
): Promise<boolean> {
|
|
517
|
+
const requestMethod = context.switchToHttp().getRequest().method;
|
|
518
|
+
const requestPath = context.switchToHttp().getRequest().url;
|
|
519
|
+
|
|
520
|
+
// 1. Get user's allowed resources (filtered by tenant)
|
|
521
|
+
const allowedResources = await this.authzService.getAuthorizedResourcesForUser(username, tenantId);
|
|
522
|
+
|
|
523
|
+
// 2. Get catalog
|
|
524
|
+
const catalog = await this.catalogService.getCatalog();
|
|
525
|
+
|
|
526
|
+
// 3. Resolve target module
|
|
527
|
+
const targetModule = this.catalogService.resolveTargetModule(catalog, requestPath);
|
|
528
|
+
if (!targetModule) {
|
|
529
|
+
return false; // No module found
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// 4. Check authorization using pae-core-lib
|
|
533
|
+
const isAuthorized = paeInstance.isAuthorized(
|
|
534
|
+
targetModule,
|
|
535
|
+
catalog,
|
|
536
|
+
allowedResources,
|
|
537
|
+
requestPath,
|
|
538
|
+
requestMethod
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
// 5. Add authorization headers for downstream services
|
|
542
|
+
if (isAuthorized) {
|
|
543
|
+
this.addAuthzHeaders(context, allowedResources);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return isAuthorized;
|
|
547
|
+
}
|
|
548
|
+
```
|
|
549
|
+
</TabItem>
|
|
550
|
+
|
|
551
|
+
<TabItem label="Core Authorization Logic">
|
|
552
|
+
```typescript
|
|
553
|
+
// packages/pae-core-lib/src/authorization/isAuthorized/is-authorized.ts
|
|
554
|
+
|
|
555
|
+
export const isAuthorized = (
|
|
556
|
+
module: ModuleMetadata,
|
|
557
|
+
catalog: ModuleMetadata[],
|
|
558
|
+
allowedResources: AllowedResources,
|
|
559
|
+
requestPath?: string,
|
|
560
|
+
requestMethod: HttpMethod = 'GET'
|
|
561
|
+
): boolean => {
|
|
562
|
+
// 1. Get module requirements (what operations are needed)
|
|
563
|
+
const moduleRequirements = getModuleRequirements(
|
|
564
|
+
module,
|
|
565
|
+
catalog,
|
|
566
|
+
requestPath,
|
|
567
|
+
requestMethod
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
// 2. Validate user has required operations
|
|
571
|
+
return validateRequirements(moduleRequirements, allowedResources);
|
|
572
|
+
};
|
|
573
|
+
```
|
|
574
|
+
</TabItem>
|
|
575
|
+
|
|
576
|
+
<TabItem label="Frontend Level">
|
|
577
|
+
```typescript
|
|
578
|
+
// React component with authorization
|
|
579
|
+
|
|
580
|
+
import { Authorized, useAuth } from '@bluealba/pae-ui-react-core';
|
|
581
|
+
|
|
582
|
+
function OrdersPage() {
|
|
583
|
+
const { hasAccess } = useAuth();
|
|
584
|
+
|
|
585
|
+
return (
|
|
586
|
+
<div>
|
|
587
|
+
<h1>Orders</h1>
|
|
588
|
+
|
|
589
|
+
{/* Declarative - component only renders if authorized */}
|
|
590
|
+
<Authorized operation={["orders::read"]}>
|
|
591
|
+
<OrdersList />
|
|
592
|
+
</Authorized>
|
|
593
|
+
|
|
594
|
+
{/* Programmatic - conditional rendering */}
|
|
595
|
+
{hasAccess('orders::create') && (
|
|
596
|
+
<Button onClick={handleCreate}>Create Order</Button>
|
|
597
|
+
)}
|
|
598
|
+
|
|
599
|
+
{/* Check for every of multiple operations */}
|
|
600
|
+
{hasAccess(['orders::approve', 'orders::admin']) && (
|
|
601
|
+
<ApprovalPanel />
|
|
602
|
+
)}
|
|
603
|
+
</div>
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
```
|
|
607
|
+
</TabItem>
|
|
608
|
+
</Tabs>
|
|
609
|
+
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
## Module-Level Authorization
|
|
613
|
+
|
|
614
|
+
Modules in the catalog can specify required operations for access.
|
|
615
|
+
|
|
616
|
+
### Module Authorization Configuration
|
|
617
|
+
|
|
618
|
+
```typescript
|
|
619
|
+
interface CatalogModule {
|
|
620
|
+
id: string;
|
|
621
|
+
name: string;
|
|
622
|
+
baseUrl: string;
|
|
623
|
+
|
|
624
|
+
authorization: {
|
|
625
|
+
|
|
626
|
+
// Authorization configuration
|
|
627
|
+
operations?: string[]; // Required operations for module access
|
|
628
|
+
isPublic?: boolean; // If true, no authentication required
|
|
629
|
+
|
|
630
|
+
// Route-level operations
|
|
631
|
+
routes?: Array<{
|
|
632
|
+
pattern: string; // ex.: /cache/(.*)
|
|
633
|
+
methods: string[]; // ex.: ['POST', 'PUT']
|
|
634
|
+
operations: string[]; // Override operations for specific route
|
|
635
|
+
}>;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
}
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
**Example**:
|
|
642
|
+
|
|
643
|
+
```typescript
|
|
644
|
+
{
|
|
645
|
+
name: 'orders-service',
|
|
646
|
+
baseUrl: '/api/orders',
|
|
647
|
+
|
|
648
|
+
authorization: {
|
|
649
|
+
operations: ['orders::read'], // Default: need orders.read to access
|
|
650
|
+
|
|
651
|
+
routes: [
|
|
652
|
+
{
|
|
653
|
+
pattern: '/api/orders',
|
|
654
|
+
methods: ['GET'],
|
|
655
|
+
operations: ['orders::read'] // Can be more specific
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
pattern: '/api/orders',
|
|
659
|
+
methods: ['POST'],
|
|
660
|
+
operations: ['orders::create', 'orders::write']
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
pattern: '/api/orders/:id',
|
|
664
|
+
methods: ['DELETE'],
|
|
665
|
+
operations: ['orders::delete', 'orders::admin']
|
|
666
|
+
}
|
|
667
|
+
]
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
---
|
|
673
|
+
|
|
674
|
+
## Authorization Service Architecture
|
|
675
|
+
|
|
676
|
+
The authorization system consists of multiple services working together:
|
|
677
|
+
|
|
678
|
+
```
|
|
679
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
680
|
+
│ Authorization Service │
|
|
681
|
+
│ (NestJS microservice) │
|
|
682
|
+
│ │
|
|
683
|
+
│ ┌────────────────────────────────────────────────────────────┐ │
|
|
684
|
+
│ │ Applications API │ │
|
|
685
|
+
│ │ POST /applications - Create application │ │
|
|
686
|
+
│ │ GET /applications - List applications │ │
|
|
687
|
+
│ │ PATCH /applications/:applicationName - Update application │ │
|
|
688
|
+
│ │ DELETE /applications/:applicationName - Delete application │ │
|
|
689
|
+
│ └────────────────────────────────────────────────────────────┘ │
|
|
690
|
+
│ │
|
|
691
|
+
│ ┌────────────────────────────────────────────────────────────┐ │
|
|
692
|
+
│ │ Operations API │ │
|
|
693
|
+
│ │ POST /applications/:applicationName/operations - Create operation │ │
|
|
694
|
+
│ │ GET /applications/:applicationName/operations - List operations │ │
|
|
695
|
+
│ │ PATCH /applications/:applicationName/operations/:operationName - Update operation │ │
|
|
696
|
+
│ │ DELETE /applications/:applicationName/operations/:operationName - Delete operation │ │
|
|
697
|
+
│ └────────────────────────────────────────────────────────────┘ │
|
|
698
|
+
│ │
|
|
699
|
+
│ ┌────────────────────────────────────────────────────────────┐ │
|
|
700
|
+
│ │ Roles API │ │
|
|
701
|
+
│ │ POST /applications/:applicationName/roles - Create role │ │
|
|
702
|
+
│ │ GET /applications/:applicationName/roles - List roles │ │
|
|
703
|
+
│ │ PATCH /applications/:applicationName/roles/:roleName - Update role │ │
|
|
704
|
+
│ │ DELETE /applications/:applicationName/roles/:roleName - Delete role │ │
|
|
705
|
+
│ │ POST /roles/:roleName/operations/:operationName - Assign operation │ │
|
|
706
|
+
│ │ DELETE /roles/:roleName/operations/:operationName - Remove operation │ │
|
|
707
|
+
│ └────────────────────────────────────────────────────────────┘ │
|
|
708
|
+
│ │
|
|
709
|
+
│ ┌────────────────────────────────────────────────────────────┐ │
|
|
710
|
+
│ │ Rules API │ │
|
|
711
|
+
│ │ POST /applications/:applicationName/rules - Create rule │ │
|
|
712
|
+
│ │ GET /applications/:applicationName/rules - List rules │ │
|
|
713
|
+
│ │ PATCH /applications/:applicationName/rules/:id - Update rule │ │
|
|
714
|
+
│ │ DELETE /applications/:applicationName/rules/:id - Delete rule │ │
|
|
715
|
+
│ └────────────────────────────────────────────────────────────┘ │
|
|
716
|
+
│ │
|
|
717
|
+
│ ┌────────────────────────────────────────────────────────────┐ │
|
|
718
|
+
│ │ Authorization Query API │ │
|
|
719
|
+
│ │ GET /users/:username/allowed-resources │ │
|
|
720
|
+
│ │ → Returns AllowedResources for user │ │
|
|
721
|
+
│ │ GET /users/:username/allowed-applications │ │
|
|
722
|
+
│ │ → Returns applications user can access │ │
|
|
723
|
+
│ │ GET /users/:username/allowed-operations │ │
|
|
724
|
+
│ │ → Returns operations user can perform │ │
|
|
725
|
+
│ └────────────────────────────────────────────────────────────┘ │
|
|
726
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
---
|
|
730
|
+
|
|
731
|
+
## Database Schema
|
|
732
|
+
|
|
733
|
+
The authorization system uses the following database tables:
|
|
734
|
+
|
|
735
|
+
```sql
|
|
736
|
+
-- Applications
|
|
737
|
+
CREATE TABLE applications (
|
|
738
|
+
id SERIAL PRIMARY KEY,
|
|
739
|
+
name VARCHAR(255) UNIQUE NOT NULL,
|
|
740
|
+
display_name VARCHAR(255) NOT NULL,
|
|
741
|
+
description TEXT,
|
|
742
|
+
allowed_by_default BOOLEAN DEFAULT FALSE,
|
|
743
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
744
|
+
created_by VARCHAR(255) NOT NULL,
|
|
745
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
746
|
+
updated_by VARCHAR(255) NOT NULL
|
|
747
|
+
);
|
|
748
|
+
|
|
749
|
+
-- Operations
|
|
750
|
+
CREATE TABLE operations (
|
|
751
|
+
id SERIAL PRIMARY KEY,
|
|
752
|
+
name VARCHAR(255) NOT NULL,
|
|
753
|
+
description TEXT,
|
|
754
|
+
application_id INTEGER REFERENCES applications(id) ON DELETE CASCADE,
|
|
755
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
756
|
+
created_by VARCHAR(255) NOT NULL,
|
|
757
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
758
|
+
updated_by VARCHAR(255) NOT NULL,
|
|
759
|
+
UNIQUE(application_id, name)
|
|
760
|
+
);
|
|
761
|
+
|
|
762
|
+
-- Roles
|
|
763
|
+
CREATE TABLE roles (
|
|
764
|
+
id SERIAL PRIMARY KEY,
|
|
765
|
+
name VARCHAR(255) NOT NULL,
|
|
766
|
+
description TEXT,
|
|
767
|
+
application_id INTEGER REFERENCES applications(id) ON DELETE CASCADE,
|
|
768
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
769
|
+
created_by VARCHAR(255) NOT NULL,
|
|
770
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
771
|
+
updated_by VARCHAR(255) NOT NULL,
|
|
772
|
+
UNIQUE(application_id, name)
|
|
773
|
+
);
|
|
774
|
+
|
|
775
|
+
-- Role-Operation assignments
|
|
776
|
+
CREATE TABLE role_operations (
|
|
777
|
+
role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE,
|
|
778
|
+
operation_id INTEGER REFERENCES operations(id) ON DELETE CASCADE,
|
|
779
|
+
PRIMARY KEY (role_id, operation_id)
|
|
780
|
+
);
|
|
781
|
+
|
|
782
|
+
-- Rules
|
|
783
|
+
CREATE TABLE rules (
|
|
784
|
+
id SERIAL PRIMARY KEY,
|
|
785
|
+
subject_type VARCHAR(50) NOT NULL, -- 'user' or 'group'
|
|
786
|
+
subject VARCHAR(255) NOT NULL, -- username or group name
|
|
787
|
+
resource_type VARCHAR(50) NOT NULL, -- 'operation', 'role', 'application'
|
|
788
|
+
resource_id INTEGER NOT NULL,
|
|
789
|
+
resource_name VARCHAR(255) NOT NULL,
|
|
790
|
+
denied BOOLEAN DEFAULT FALSE,
|
|
791
|
+
application_id INTEGER REFERENCES applications(id) ON DELETE CASCADE,
|
|
792
|
+
tenant_id INTEGER REFERENCES tenants(id) ON DELETE CASCADE, -- NULL = global (all tenants)
|
|
793
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
794
|
+
created_by VARCHAR(255) NOT NULL,
|
|
795
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
796
|
+
updated_by VARCHAR(255) NOT NULL
|
|
797
|
+
);
|
|
798
|
+
|
|
799
|
+
-- Indexes for performance
|
|
800
|
+
CREATE INDEX idx_rules_subject ON rules(subject);
|
|
801
|
+
CREATE INDEX idx_rules_resource ON rules(resource_type, resource_id);
|
|
802
|
+
CREATE INDEX idx_rules_tenant ON rules(tenant_id);
|
|
803
|
+
CREATE INDEX idx_operations_app ON operations(application_id);
|
|
804
|
+
CREATE INDEX idx_roles_app ON roles(application_id);
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
---
|
|
808
|
+
|
|
809
|
+
## Performance Considerations
|
|
810
|
+
|
|
811
|
+
### Caching
|
|
812
|
+
|
|
813
|
+
Authorization data is cached to reduce database queries:
|
|
814
|
+
|
|
815
|
+
```typescript
|
|
816
|
+
// Cache user's allowed resources for 5 minutes
|
|
817
|
+
const cacheKey = `authz:user:${username}:resources`;
|
|
818
|
+
let allowedResources = await cache.get(cacheKey);
|
|
819
|
+
|
|
820
|
+
if (!allowedResources) {
|
|
821
|
+
allowedResources = await authzService.getAllowedResources(username);
|
|
822
|
+
await cache.set(cacheKey, allowedResources, 300); // 5 minutes TTL
|
|
823
|
+
}
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
### Optimization Strategies
|
|
827
|
+
|
|
828
|
+
<CardGrid>
|
|
829
|
+
<Card title="Batch Queries" icon="rocket">
|
|
830
|
+
Load all rules for a user in one query instead of multiple queries
|
|
831
|
+
</Card>
|
|
832
|
+
|
|
833
|
+
<Card title="Denormalization" icon="seti:db">
|
|
834
|
+
Store flattened permission data for faster lookups
|
|
835
|
+
</Card>
|
|
836
|
+
|
|
837
|
+
<Card title="Index Optimization" icon="magnifier">
|
|
838
|
+
Database indexes on subject, resource_type, and resource_id columns
|
|
839
|
+
</Card>
|
|
840
|
+
|
|
841
|
+
<Card title="Early Termination" icon="approve-check">
|
|
842
|
+
Return immediately on first DENY rule (no need to check further)
|
|
843
|
+
</Card>
|
|
844
|
+
</CardGrid>
|
|
845
|
+
|
|
846
|
+
---
|
|
847
|
+
|
|
848
|
+
## Security Considerations
|
|
849
|
+
|
|
850
|
+
<Aside type="caution" title="Authorization Security Best Practices">
|
|
851
|
+
|
|
852
|
+
**Principle of Least Privilege**:
|
|
853
|
+
- Grant users only the minimum permissions needed
|
|
854
|
+
- Prefer deny-by-default (explicit allow required)
|
|
855
|
+
- Regularly audit and review permissions
|
|
856
|
+
|
|
857
|
+
**Rule Management**:
|
|
858
|
+
- DENY rules take precedence over ALLOW rules
|
|
859
|
+
- Avoid wildcard operations (`*.*`) except for admins
|
|
860
|
+
- Document why DENY rules exist
|
|
861
|
+
|
|
862
|
+
**Context Validation**:
|
|
863
|
+
- Always validate tenant context
|
|
864
|
+
- Never trust client-provided authorization headers
|
|
865
|
+
- Verify user still has permissions (don't rely on stale tokens)
|
|
866
|
+
|
|
867
|
+
**Audit Trail**:
|
|
868
|
+
- Log all authorization failures
|
|
869
|
+
- Track who grants/revokes permissions
|
|
870
|
+
- Monitor for unusual permission changes
|
|
871
|
+
|
|
872
|
+
**Testing**:
|
|
873
|
+
- Unit test authorization logic extensively
|
|
874
|
+
- Test both positive and negative cases
|
|
875
|
+
- Verify DENY rules work correctly
|
|
876
|
+
- Test permission inheritance through groups
|
|
877
|
+
|
|
878
|
+
</Aside>
|
|
879
|
+
|
|
880
|
+
---
|
|
881
|
+
|
|
882
|
+
## Next Steps
|
|
883
|
+
|
|
884
|
+
- **[Gateway Architecture](/_/docs/architecture/gateway-architecture/)** - How gateway enforces authorization
|
|
885
|
+
- **[Authentication System](/_/docs/architecture/authentication-system/)** - User authentication and JWT
|
|
886
|
+
- **[Multi-Tenancy](/_/docs/architecture/multi-tenancy/)** - Tenant-scoped authorization
|