@serve.zone/dcrouter 12.1.0 → 12.2.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_serve/bundle.js +750 -688
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +6 -1
- package/dist_ts/classes.dcrouter.js +11 -3
- package/dist_ts/config/classes.db-seeder.d.ts +25 -0
- package/dist_ts/config/classes.db-seeder.js +69 -0
- package/dist_ts/config/classes.reference-resolver.d.ts +80 -0
- package/dist_ts/config/classes.reference-resolver.js +482 -0
- package/dist_ts/config/classes.route-config-manager.d.ts +13 -3
- package/dist_ts/config/classes.route-config-manager.js +53 -3
- package/dist_ts/config/index.d.ts +2 -0
- package/dist_ts/config/index.js +3 -1
- package/dist_ts/db/documents/classes.network-target.doc.d.ts +15 -0
- package/dist_ts/db/documents/classes.network-target.doc.js +118 -0
- package/dist_ts/db/documents/classes.security-profile.doc.d.ts +16 -0
- package/dist_ts/db/documents/classes.security-profile.doc.js +118 -0
- package/dist_ts/db/documents/classes.stored-route.doc.d.ts +2 -0
- package/dist_ts/db/documents/classes.stored-route.doc.js +8 -2
- package/dist_ts/db/documents/index.d.ts +2 -0
- package/dist_ts/db/documents/index.js +3 -1
- package/dist_ts/opsserver/classes.opsserver.d.ts +2 -0
- package/dist_ts/opsserver/classes.opsserver.js +5 -1
- package/dist_ts/opsserver/handlers/index.d.ts +2 -0
- package/dist_ts/opsserver/handlers/index.js +3 -1
- package/dist_ts/opsserver/handlers/network-target.handler.d.ts +10 -0
- package/dist_ts/opsserver/handlers/network-target.handler.js +117 -0
- package/dist_ts/opsserver/handlers/route-management.handler.js +3 -2
- package/dist_ts/opsserver/handlers/security-profile.handler.d.ts +10 -0
- package/dist_ts/opsserver/handlers/security-profile.handler.js +119 -0
- package/dist_ts_interfaces/data/route-management.d.ts +48 -1
- package/dist_ts_interfaces/requests/index.d.ts +2 -0
- package/dist_ts_interfaces/requests/index.js +3 -1
- package/dist_ts_interfaces/requests/network-targets.d.ts +102 -0
- package/dist_ts_interfaces/requests/network-targets.js +2 -0
- package/dist_ts_interfaces/requests/route-management.d.ts +3 -1
- package/dist_ts_interfaces/requests/security-profiles.d.ts +102 -0
- package/dist_ts_interfaces/requests/security-profiles.js +2 -0
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate.d.ts +43 -0
- package/dist_ts_web/appstate.js +176 -2
- package/dist_ts_web/elements/index.d.ts +2 -0
- package/dist_ts_web/elements/index.js +3 -1
- package/dist_ts_web/elements/ops-dashboard.js +13 -1
- package/dist_ts_web/elements/ops-view-networktargets.d.ts +17 -0
- package/dist_ts_web/elements/ops-view-networktargets.js +246 -0
- package/dist_ts_web/elements/ops-view-securityprofiles.d.ts +17 -0
- package/dist_ts_web/elements/ops-view-securityprofiles.js +275 -0
- package/dist_ts_web/router.d.ts +1 -1
- package/dist_ts_web/router.js +2 -2
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +19 -1
- package/ts/config/classes.db-seeder.ts +95 -0
- package/ts/config/classes.reference-resolver.ts +576 -0
- package/ts/config/classes.route-config-manager.ts +64 -1
- package/ts/config/index.ts +3 -1
- package/ts/db/documents/classes.network-target.doc.ts +48 -0
- package/ts/db/documents/classes.security-profile.doc.ts +49 -0
- package/ts/db/documents/classes.stored-route.doc.ts +4 -0
- package/ts/db/documents/index.ts +2 -0
- package/ts/opsserver/classes.opsserver.ts +4 -0
- package/ts/opsserver/handlers/index.ts +3 -1
- package/ts/opsserver/handlers/network-target.handler.ts +167 -0
- package/ts/opsserver/handlers/route-management.handler.ts +2 -1
- package/ts/opsserver/handlers/security-profile.handler.ts +169 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +243 -1
- package/ts_web/elements/index.ts +2 -0
- package/ts_web/elements/ops-dashboard.ts +12 -0
- package/ts_web/elements/ops-view-networktargets.ts +214 -0
- package/ts_web/elements/ops-view-securityprofiles.ts +242 -0
- package/ts_web/router.ts +1 -1
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import { logger } from '../logger.js';
|
|
3
|
+
import { SecurityProfileDoc, NetworkTargetDoc, StoredRouteDoc } from '../db/index.js';
|
|
4
|
+
import type {
|
|
5
|
+
ISecurityProfile,
|
|
6
|
+
INetworkTarget,
|
|
7
|
+
IRouteMetadata,
|
|
8
|
+
IStoredRoute,
|
|
9
|
+
IRouteSecurity,
|
|
10
|
+
} from '../../ts_interfaces/data/route-management.js';
|
|
11
|
+
|
|
12
|
+
const MAX_INHERITANCE_DEPTH = 5;
|
|
13
|
+
|
|
14
|
+
export class ReferenceResolver {
|
|
15
|
+
private profiles = new Map<string, ISecurityProfile>();
|
|
16
|
+
private targets = new Map<string, INetworkTarget>();
|
|
17
|
+
|
|
18
|
+
// =========================================================================
|
|
19
|
+
// Lifecycle
|
|
20
|
+
// =========================================================================
|
|
21
|
+
|
|
22
|
+
public async initialize(): Promise<void> {
|
|
23
|
+
await this.loadProfiles();
|
|
24
|
+
await this.loadTargets();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// =========================================================================
|
|
28
|
+
// Profile CRUD
|
|
29
|
+
// =========================================================================
|
|
30
|
+
|
|
31
|
+
public async createProfile(data: {
|
|
32
|
+
name: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
security: IRouteSecurity;
|
|
35
|
+
extendsProfiles?: string[];
|
|
36
|
+
createdBy: string;
|
|
37
|
+
}): Promise<string> {
|
|
38
|
+
const id = plugins.uuid.v4();
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
|
|
41
|
+
const profile: ISecurityProfile = {
|
|
42
|
+
id,
|
|
43
|
+
name: data.name,
|
|
44
|
+
description: data.description,
|
|
45
|
+
security: data.security,
|
|
46
|
+
extendsProfiles: data.extendsProfiles,
|
|
47
|
+
createdAt: now,
|
|
48
|
+
updatedAt: now,
|
|
49
|
+
createdBy: data.createdBy,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
this.profiles.set(id, profile);
|
|
53
|
+
await this.persistProfile(profile);
|
|
54
|
+
logger.log('info', `Created security profile '${profile.name}' (${id})`);
|
|
55
|
+
return id;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public async updateProfile(
|
|
59
|
+
id: string,
|
|
60
|
+
patch: Partial<Omit<ISecurityProfile, 'id' | 'createdAt' | 'createdBy'>>,
|
|
61
|
+
): Promise<{ affectedRouteIds: string[] }> {
|
|
62
|
+
const profile = this.profiles.get(id);
|
|
63
|
+
if (!profile) {
|
|
64
|
+
throw new Error(`Security profile '${id}' not found`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (patch.name !== undefined) profile.name = patch.name;
|
|
68
|
+
if (patch.description !== undefined) profile.description = patch.description;
|
|
69
|
+
if (patch.security !== undefined) profile.security = patch.security;
|
|
70
|
+
if (patch.extendsProfiles !== undefined) profile.extendsProfiles = patch.extendsProfiles;
|
|
71
|
+
profile.updatedAt = Date.now();
|
|
72
|
+
|
|
73
|
+
await this.persistProfile(profile);
|
|
74
|
+
logger.log('info', `Updated security profile '${profile.name}' (${id})`);
|
|
75
|
+
|
|
76
|
+
// Find routes referencing this profile
|
|
77
|
+
const affectedRouteIds = await this.findRoutesByProfileRef(id);
|
|
78
|
+
return { affectedRouteIds };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public async deleteProfile(
|
|
82
|
+
id: string,
|
|
83
|
+
force: boolean,
|
|
84
|
+
storedRoutes?: Map<string, IStoredRoute>,
|
|
85
|
+
): Promise<{ success: boolean; message?: string }> {
|
|
86
|
+
const profile = this.profiles.get(id);
|
|
87
|
+
if (!profile) {
|
|
88
|
+
return { success: false, message: `Security profile '${id}' not found` };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check usage
|
|
92
|
+
const affectedIds = storedRoutes
|
|
93
|
+
? this.findRoutesByProfileRefSync(id, storedRoutes)
|
|
94
|
+
: await this.findRoutesByProfileRef(id);
|
|
95
|
+
|
|
96
|
+
if (affectedIds.length > 0 && !force) {
|
|
97
|
+
return {
|
|
98
|
+
success: false,
|
|
99
|
+
message: `Profile '${profile.name}' is in use by ${affectedIds.length} route(s). Use force=true to delete.`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Delete from DB
|
|
104
|
+
const doc = await SecurityProfileDoc.findById(id);
|
|
105
|
+
if (doc) await doc.delete();
|
|
106
|
+
this.profiles.delete(id);
|
|
107
|
+
|
|
108
|
+
// If force-deleting with referencing routes, clear refs but keep resolved values
|
|
109
|
+
if (affectedIds.length > 0) {
|
|
110
|
+
await this.clearProfileRefsOnRoutes(affectedIds);
|
|
111
|
+
logger.log('warn', `Force-deleted profile '${profile.name}'; cleared refs on ${affectedIds.length} route(s)`);
|
|
112
|
+
} else {
|
|
113
|
+
logger.log('info', `Deleted security profile '${profile.name}' (${id})`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { success: true };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public getProfile(id: string): ISecurityProfile | undefined {
|
|
120
|
+
return this.profiles.get(id);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
public getProfileByName(name: string): ISecurityProfile | undefined {
|
|
124
|
+
for (const profile of this.profiles.values()) {
|
|
125
|
+
if (profile.name === name) return profile;
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public listProfiles(): ISecurityProfile[] {
|
|
131
|
+
return [...this.profiles.values()];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public getProfileUsage(storedRoutes: Map<string, IStoredRoute>): Map<string, Array<{ id: string; routeName: string }>> {
|
|
135
|
+
const usage = new Map<string, Array<{ id: string; routeName: string }>>();
|
|
136
|
+
for (const profile of this.profiles.values()) {
|
|
137
|
+
usage.set(profile.id, []);
|
|
138
|
+
}
|
|
139
|
+
for (const [routeId, stored] of storedRoutes) {
|
|
140
|
+
const ref = stored.metadata?.securityProfileRef;
|
|
141
|
+
if (ref && usage.has(ref)) {
|
|
142
|
+
usage.get(ref)!.push({ id: routeId, routeName: stored.route.name || routeId });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return usage;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public getProfileUsageForId(
|
|
149
|
+
profileId: string,
|
|
150
|
+
storedRoutes: Map<string, IStoredRoute>,
|
|
151
|
+
): Array<{ id: string; routeName: string }> {
|
|
152
|
+
const routes: Array<{ id: string; routeName: string }> = [];
|
|
153
|
+
for (const [routeId, stored] of storedRoutes) {
|
|
154
|
+
if (stored.metadata?.securityProfileRef === profileId) {
|
|
155
|
+
routes.push({ id: routeId, routeName: stored.route.name || routeId });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return routes;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// =========================================================================
|
|
162
|
+
// Target CRUD
|
|
163
|
+
// =========================================================================
|
|
164
|
+
|
|
165
|
+
public async createTarget(data: {
|
|
166
|
+
name: string;
|
|
167
|
+
description?: string;
|
|
168
|
+
host: string | string[];
|
|
169
|
+
port: number;
|
|
170
|
+
createdBy: string;
|
|
171
|
+
}): Promise<string> {
|
|
172
|
+
const id = plugins.uuid.v4();
|
|
173
|
+
const now = Date.now();
|
|
174
|
+
|
|
175
|
+
const target: INetworkTarget = {
|
|
176
|
+
id,
|
|
177
|
+
name: data.name,
|
|
178
|
+
description: data.description,
|
|
179
|
+
host: data.host,
|
|
180
|
+
port: data.port,
|
|
181
|
+
createdAt: now,
|
|
182
|
+
updatedAt: now,
|
|
183
|
+
createdBy: data.createdBy,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
this.targets.set(id, target);
|
|
187
|
+
await this.persistTarget(target);
|
|
188
|
+
logger.log('info', `Created network target '${target.name}' (${id})`);
|
|
189
|
+
return id;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
public async updateTarget(
|
|
193
|
+
id: string,
|
|
194
|
+
patch: Partial<Omit<INetworkTarget, 'id' | 'createdAt' | 'createdBy'>>,
|
|
195
|
+
): Promise<{ affectedRouteIds: string[] }> {
|
|
196
|
+
const target = this.targets.get(id);
|
|
197
|
+
if (!target) {
|
|
198
|
+
throw new Error(`Network target '${id}' not found`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (patch.name !== undefined) target.name = patch.name;
|
|
202
|
+
if (patch.description !== undefined) target.description = patch.description;
|
|
203
|
+
if (patch.host !== undefined) target.host = patch.host;
|
|
204
|
+
if (patch.port !== undefined) target.port = patch.port;
|
|
205
|
+
target.updatedAt = Date.now();
|
|
206
|
+
|
|
207
|
+
await this.persistTarget(target);
|
|
208
|
+
logger.log('info', `Updated network target '${target.name}' (${id})`);
|
|
209
|
+
|
|
210
|
+
const affectedRouteIds = await this.findRoutesByTargetRef(id);
|
|
211
|
+
return { affectedRouteIds };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
public async deleteTarget(
|
|
215
|
+
id: string,
|
|
216
|
+
force: boolean,
|
|
217
|
+
storedRoutes?: Map<string, IStoredRoute>,
|
|
218
|
+
): Promise<{ success: boolean; message?: string }> {
|
|
219
|
+
const target = this.targets.get(id);
|
|
220
|
+
if (!target) {
|
|
221
|
+
return { success: false, message: `Network target '${id}' not found` };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const affectedIds = storedRoutes
|
|
225
|
+
? this.findRoutesByTargetRefSync(id, storedRoutes)
|
|
226
|
+
: await this.findRoutesByTargetRef(id);
|
|
227
|
+
|
|
228
|
+
if (affectedIds.length > 0 && !force) {
|
|
229
|
+
return {
|
|
230
|
+
success: false,
|
|
231
|
+
message: `Target '${target.name}' is in use by ${affectedIds.length} route(s). Use force=true to delete.`,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const doc = await NetworkTargetDoc.findById(id);
|
|
236
|
+
if (doc) await doc.delete();
|
|
237
|
+
this.targets.delete(id);
|
|
238
|
+
|
|
239
|
+
if (affectedIds.length > 0) {
|
|
240
|
+
await this.clearTargetRefsOnRoutes(affectedIds);
|
|
241
|
+
logger.log('warn', `Force-deleted target '${target.name}'; cleared refs on ${affectedIds.length} route(s)`);
|
|
242
|
+
} else {
|
|
243
|
+
logger.log('info', `Deleted network target '${target.name}' (${id})`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return { success: true };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
public getTarget(id: string): INetworkTarget | undefined {
|
|
250
|
+
return this.targets.get(id);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
public getTargetByName(name: string): INetworkTarget | undefined {
|
|
254
|
+
for (const target of this.targets.values()) {
|
|
255
|
+
if (target.name === name) return target;
|
|
256
|
+
}
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
public listTargets(): INetworkTarget[] {
|
|
261
|
+
return [...this.targets.values()];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
public getTargetUsageForId(
|
|
265
|
+
targetId: string,
|
|
266
|
+
storedRoutes: Map<string, IStoredRoute>,
|
|
267
|
+
): Array<{ id: string; routeName: string }> {
|
|
268
|
+
const routes: Array<{ id: string; routeName: string }> = [];
|
|
269
|
+
for (const [routeId, stored] of storedRoutes) {
|
|
270
|
+
if (stored.metadata?.networkTargetRef === targetId) {
|
|
271
|
+
routes.push({ id: routeId, routeName: stored.route.name || routeId });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return routes;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// =========================================================================
|
|
278
|
+
// Resolution
|
|
279
|
+
// =========================================================================
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Resolve references for a single route.
|
|
283
|
+
* Materializes security profile and/or network target into the route's fields.
|
|
284
|
+
* Returns the resolved route and updated metadata.
|
|
285
|
+
*/
|
|
286
|
+
public resolveRoute(
|
|
287
|
+
route: plugins.smartproxy.IRouteConfig,
|
|
288
|
+
metadata?: IRouteMetadata,
|
|
289
|
+
): { route: plugins.smartproxy.IRouteConfig; metadata: IRouteMetadata } {
|
|
290
|
+
const resolvedMetadata: IRouteMetadata = { ...metadata };
|
|
291
|
+
|
|
292
|
+
if (resolvedMetadata.securityProfileRef) {
|
|
293
|
+
const resolvedSecurity = this.resolveSecurityProfile(resolvedMetadata.securityProfileRef);
|
|
294
|
+
if (resolvedSecurity) {
|
|
295
|
+
const profile = this.profiles.get(resolvedMetadata.securityProfileRef);
|
|
296
|
+
// Merge: profile provides base, route's inline values override
|
|
297
|
+
route = {
|
|
298
|
+
...route,
|
|
299
|
+
security: this.mergeSecurityFields(resolvedSecurity, route.security),
|
|
300
|
+
};
|
|
301
|
+
resolvedMetadata.securityProfileName = profile?.name;
|
|
302
|
+
resolvedMetadata.lastResolvedAt = Date.now();
|
|
303
|
+
} else {
|
|
304
|
+
logger.log('warn', `Security profile '${resolvedMetadata.securityProfileRef}' not found during resolution`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (resolvedMetadata.networkTargetRef) {
|
|
309
|
+
const target = this.targets.get(resolvedMetadata.networkTargetRef);
|
|
310
|
+
if (target) {
|
|
311
|
+
route = {
|
|
312
|
+
...route,
|
|
313
|
+
action: {
|
|
314
|
+
...route.action,
|
|
315
|
+
targets: [{
|
|
316
|
+
host: target.host as string,
|
|
317
|
+
port: target.port,
|
|
318
|
+
}],
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
resolvedMetadata.networkTargetName = target.name;
|
|
322
|
+
resolvedMetadata.lastResolvedAt = Date.now();
|
|
323
|
+
} else {
|
|
324
|
+
logger.log('warn', `Network target '${resolvedMetadata.networkTargetRef}' not found during resolution`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return { route, metadata: resolvedMetadata };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// =========================================================================
|
|
332
|
+
// Reference lookup helpers
|
|
333
|
+
// =========================================================================
|
|
334
|
+
|
|
335
|
+
public async findRoutesByProfileRef(profileId: string): Promise<string[]> {
|
|
336
|
+
const docs = await StoredRouteDoc.findAll();
|
|
337
|
+
return docs
|
|
338
|
+
.filter((doc) => doc.metadata?.securityProfileRef === profileId)
|
|
339
|
+
.map((doc) => doc.id);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
public async findRoutesByTargetRef(targetId: string): Promise<string[]> {
|
|
343
|
+
const docs = await StoredRouteDoc.findAll();
|
|
344
|
+
return docs
|
|
345
|
+
.filter((doc) => doc.metadata?.networkTargetRef === targetId)
|
|
346
|
+
.map((doc) => doc.id);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
public findRoutesByProfileRefSync(profileId: string, storedRoutes: Map<string, IStoredRoute>): string[] {
|
|
350
|
+
const ids: string[] = [];
|
|
351
|
+
for (const [routeId, stored] of storedRoutes) {
|
|
352
|
+
if (stored.metadata?.securityProfileRef === profileId) {
|
|
353
|
+
ids.push(routeId);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return ids;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
public findRoutesByTargetRefSync(targetId: string, storedRoutes: Map<string, IStoredRoute>): string[] {
|
|
360
|
+
const ids: string[] = [];
|
|
361
|
+
for (const [routeId, stored] of storedRoutes) {
|
|
362
|
+
if (stored.metadata?.networkTargetRef === targetId) {
|
|
363
|
+
ids.push(routeId);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return ids;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// =========================================================================
|
|
370
|
+
// Private: security profile resolution with inheritance
|
|
371
|
+
// =========================================================================
|
|
372
|
+
|
|
373
|
+
private resolveSecurityProfile(
|
|
374
|
+
profileId: string,
|
|
375
|
+
visited: Set<string> = new Set(),
|
|
376
|
+
depth: number = 0,
|
|
377
|
+
): IRouteSecurity | null {
|
|
378
|
+
if (depth > MAX_INHERITANCE_DEPTH) {
|
|
379
|
+
logger.log('warn', `Max inheritance depth (${MAX_INHERITANCE_DEPTH}) exceeded resolving profile '${profileId}'`);
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (visited.has(profileId)) {
|
|
384
|
+
logger.log('warn', `Circular inheritance detected for profile '${profileId}'`);
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const profile = this.profiles.get(profileId);
|
|
389
|
+
if (!profile) return null;
|
|
390
|
+
|
|
391
|
+
visited.add(profileId);
|
|
392
|
+
|
|
393
|
+
// Start with an empty base
|
|
394
|
+
let baseSecurity: IRouteSecurity = {};
|
|
395
|
+
|
|
396
|
+
// Resolve parent profiles first (top-down, later overrides earlier)
|
|
397
|
+
if (profile.extendsProfiles?.length) {
|
|
398
|
+
for (const parentId of profile.extendsProfiles) {
|
|
399
|
+
const parentSecurity = this.resolveSecurityProfile(parentId, new Set(visited), depth + 1);
|
|
400
|
+
if (parentSecurity) {
|
|
401
|
+
baseSecurity = this.mergeSecurityFields(baseSecurity, parentSecurity);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Apply this profile's security on top
|
|
407
|
+
return this.mergeSecurityFields(baseSecurity, profile.security);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Merge two IRouteSecurity objects.
|
|
412
|
+
* `override` values take precedence over `base` values.
|
|
413
|
+
* For ipAllowList/ipBlockList: union arrays and deduplicate.
|
|
414
|
+
* For scalar/object fields: override wins if present.
|
|
415
|
+
*/
|
|
416
|
+
private mergeSecurityFields(
|
|
417
|
+
base: IRouteSecurity | undefined,
|
|
418
|
+
override: IRouteSecurity | undefined,
|
|
419
|
+
): IRouteSecurity {
|
|
420
|
+
if (!base && !override) return {};
|
|
421
|
+
if (!base) return { ...override };
|
|
422
|
+
if (!override) return { ...base };
|
|
423
|
+
|
|
424
|
+
const merged: IRouteSecurity = { ...base };
|
|
425
|
+
|
|
426
|
+
// IP lists: union
|
|
427
|
+
if (override.ipAllowList || base.ipAllowList) {
|
|
428
|
+
merged.ipAllowList = [...new Set([
|
|
429
|
+
...(base.ipAllowList || []),
|
|
430
|
+
...(override.ipAllowList || []),
|
|
431
|
+
])];
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (override.ipBlockList || base.ipBlockList) {
|
|
435
|
+
merged.ipBlockList = [...new Set([
|
|
436
|
+
...(base.ipBlockList || []),
|
|
437
|
+
...(override.ipBlockList || []),
|
|
438
|
+
])];
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Scalar/object fields: override wins
|
|
442
|
+
if (override.maxConnections !== undefined) merged.maxConnections = override.maxConnections;
|
|
443
|
+
if (override.rateLimit !== undefined) merged.rateLimit = override.rateLimit;
|
|
444
|
+
if (override.authentication !== undefined) merged.authentication = override.authentication;
|
|
445
|
+
if (override.basicAuth !== undefined) merged.basicAuth = override.basicAuth;
|
|
446
|
+
if (override.jwtAuth !== undefined) merged.jwtAuth = override.jwtAuth;
|
|
447
|
+
|
|
448
|
+
return merged;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// =========================================================================
|
|
452
|
+
// Private: persistence
|
|
453
|
+
// =========================================================================
|
|
454
|
+
|
|
455
|
+
private async loadProfiles(): Promise<void> {
|
|
456
|
+
const docs = await SecurityProfileDoc.findAll();
|
|
457
|
+
for (const doc of docs) {
|
|
458
|
+
if (doc.id) {
|
|
459
|
+
this.profiles.set(doc.id, {
|
|
460
|
+
id: doc.id,
|
|
461
|
+
name: doc.name,
|
|
462
|
+
description: doc.description,
|
|
463
|
+
security: doc.security,
|
|
464
|
+
extendsProfiles: doc.extendsProfiles,
|
|
465
|
+
createdAt: doc.createdAt,
|
|
466
|
+
updatedAt: doc.updatedAt,
|
|
467
|
+
createdBy: doc.createdBy,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (this.profiles.size > 0) {
|
|
472
|
+
logger.log('info', `Loaded ${this.profiles.size} security profile(s) from storage`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private async loadTargets(): Promise<void> {
|
|
477
|
+
const docs = await NetworkTargetDoc.findAll();
|
|
478
|
+
for (const doc of docs) {
|
|
479
|
+
if (doc.id) {
|
|
480
|
+
this.targets.set(doc.id, {
|
|
481
|
+
id: doc.id,
|
|
482
|
+
name: doc.name,
|
|
483
|
+
description: doc.description,
|
|
484
|
+
host: doc.host,
|
|
485
|
+
port: doc.port,
|
|
486
|
+
createdAt: doc.createdAt,
|
|
487
|
+
updatedAt: doc.updatedAt,
|
|
488
|
+
createdBy: doc.createdBy,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (this.targets.size > 0) {
|
|
493
|
+
logger.log('info', `Loaded ${this.targets.size} network target(s) from storage`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private async persistProfile(profile: ISecurityProfile): Promise<void> {
|
|
498
|
+
const existingDoc = await SecurityProfileDoc.findById(profile.id);
|
|
499
|
+
if (existingDoc) {
|
|
500
|
+
existingDoc.name = profile.name;
|
|
501
|
+
existingDoc.description = profile.description;
|
|
502
|
+
existingDoc.security = profile.security;
|
|
503
|
+
existingDoc.extendsProfiles = profile.extendsProfiles;
|
|
504
|
+
existingDoc.updatedAt = profile.updatedAt;
|
|
505
|
+
await existingDoc.save();
|
|
506
|
+
} else {
|
|
507
|
+
const doc = new SecurityProfileDoc();
|
|
508
|
+
doc.id = profile.id;
|
|
509
|
+
doc.name = profile.name;
|
|
510
|
+
doc.description = profile.description;
|
|
511
|
+
doc.security = profile.security;
|
|
512
|
+
doc.extendsProfiles = profile.extendsProfiles;
|
|
513
|
+
doc.createdAt = profile.createdAt;
|
|
514
|
+
doc.updatedAt = profile.updatedAt;
|
|
515
|
+
doc.createdBy = profile.createdBy;
|
|
516
|
+
await doc.save();
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private async persistTarget(target: INetworkTarget): Promise<void> {
|
|
521
|
+
const existingDoc = await NetworkTargetDoc.findById(target.id);
|
|
522
|
+
if (existingDoc) {
|
|
523
|
+
existingDoc.name = target.name;
|
|
524
|
+
existingDoc.description = target.description;
|
|
525
|
+
existingDoc.host = target.host;
|
|
526
|
+
existingDoc.port = target.port;
|
|
527
|
+
existingDoc.updatedAt = target.updatedAt;
|
|
528
|
+
await existingDoc.save();
|
|
529
|
+
} else {
|
|
530
|
+
const doc = new NetworkTargetDoc();
|
|
531
|
+
doc.id = target.id;
|
|
532
|
+
doc.name = target.name;
|
|
533
|
+
doc.description = target.description;
|
|
534
|
+
doc.host = target.host;
|
|
535
|
+
doc.port = target.port;
|
|
536
|
+
doc.createdAt = target.createdAt;
|
|
537
|
+
doc.updatedAt = target.updatedAt;
|
|
538
|
+
doc.createdBy = target.createdBy;
|
|
539
|
+
await doc.save();
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// =========================================================================
|
|
544
|
+
// Private: ref cleanup on force-delete
|
|
545
|
+
// =========================================================================
|
|
546
|
+
|
|
547
|
+
private async clearProfileRefsOnRoutes(routeIds: string[]): Promise<void> {
|
|
548
|
+
for (const routeId of routeIds) {
|
|
549
|
+
const doc = await StoredRouteDoc.findById(routeId);
|
|
550
|
+
if (doc?.metadata) {
|
|
551
|
+
doc.metadata = {
|
|
552
|
+
...doc.metadata,
|
|
553
|
+
securityProfileRef: undefined,
|
|
554
|
+
securityProfileName: undefined,
|
|
555
|
+
};
|
|
556
|
+
doc.updatedAt = Date.now();
|
|
557
|
+
await doc.save();
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
private async clearTargetRefsOnRoutes(routeIds: string[]): Promise<void> {
|
|
563
|
+
for (const routeId of routeIds) {
|
|
564
|
+
const doc = await StoredRouteDoc.findById(routeId);
|
|
565
|
+
if (doc?.metadata) {
|
|
566
|
+
doc.metadata = {
|
|
567
|
+
...doc.metadata,
|
|
568
|
+
networkTargetRef: undefined,
|
|
569
|
+
networkTargetName: undefined,
|
|
570
|
+
};
|
|
571
|
+
doc.updatedAt = Date.now();
|
|
572
|
+
await doc.save();
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|