@serve.zone/dcrouter 13.31.0 → 13.32.1
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 +721 -721
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/config/classes.route-config-manager.d.ts +1 -0
- package/dist_ts/config/classes.route-config-manager.js +28 -3
- package/dist_ts/config/classes.target-profile-manager.d.ts +1 -0
- package/dist_ts/config/classes.target-profile-manager.js +11 -8
- package/dist_ts/opsserver/classes.opsserver.d.ts +4 -2
- package/dist_ts/opsserver/classes.opsserver.js +2 -11
- package/dist_ts/opsserver/handlers/acme-config.handler.js +7 -24
- package/dist_ts/opsserver/handlers/admin.handler.d.ts +3 -1
- package/dist_ts/opsserver/handlers/admin.handler.js +95 -110
- package/dist_ts/opsserver/handlers/api-token.handler.js +28 -2
- package/dist_ts/opsserver/handlers/certificate.handler.js +7 -24
- package/dist_ts/opsserver/handlers/config.handler.js +3 -1
- package/dist_ts/opsserver/handlers/dns-provider.handler.js +7 -24
- package/dist_ts/opsserver/handlers/dns-record.handler.js +7 -24
- package/dist_ts/opsserver/handlers/domain.handler.js +7 -24
- package/dist_ts/opsserver/handlers/email-domain.handler.js +7 -24
- package/dist_ts/opsserver/handlers/email-ops.handler.js +8 -1
- package/dist_ts/opsserver/handlers/logs.handler.js +4 -1
- package/dist_ts/opsserver/handlers/network-target.handler.js +7 -24
- package/dist_ts/opsserver/handlers/radius.handler.js +32 -1
- package/dist_ts/opsserver/handlers/remoteingress.handler.js +24 -1
- package/dist_ts/opsserver/handlers/route-management.handler.js +7 -26
- package/dist_ts/opsserver/handlers/security.handler.js +32 -7
- package/dist_ts/opsserver/handlers/source-profile.handler.js +7 -24
- package/dist_ts/opsserver/handlers/stats.handler.js +8 -1
- package/dist_ts/opsserver/handlers/target-profile.handler.js +7 -24
- package/dist_ts/opsserver/handlers/users.handler.js +33 -13
- package/dist_ts/opsserver/handlers/vpn.handler.js +34 -1
- package/dist_ts/opsserver/handlers/workhoster.handler.js +16 -35
- package/dist_ts/opsserver/helpers/auth.d.ts +21 -0
- package/dist_ts/opsserver/helpers/auth.js +63 -0
- package/dist_ts_interfaces/data/route-management.d.ts +2 -1
- package/dist_ts_interfaces/data/route-management.js +48 -2
- package/dist_ts_interfaces/requests/api-tokens.d.ts +10 -5
- package/dist_ts_interfaces/requests/combined.stats.d.ts +2 -1
- package/dist_ts_interfaces/requests/config.d.ts +2 -1
- package/dist_ts_interfaces/requests/email-ops.d.ts +6 -3
- package/dist_ts_interfaces/requests/logs.d.ts +4 -2
- package/dist_ts_interfaces/requests/radius.d.ts +24 -12
- package/dist_ts_interfaces/requests/remoteingress.d.ts +14 -7
- package/dist_ts_interfaces/requests/security-policy.d.ts +16 -8
- package/dist_ts_interfaces/requests/stats.d.ts +18 -9
- package/dist_ts_interfaces/requests/users.d.ts +6 -3
- package/dist_ts_interfaces/requests/vpn.d.ts +22 -11
- package/dist_ts_interfaces/requests/workhoster.d.ts +10 -5
- package/dist_ts_migrations/index.js +3 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/access/ops-view-apitokens.js +2 -21
- package/package.json +4 -4
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/config/classes.route-config-manager.ts +35 -2
- package/ts/config/classes.target-profile-manager.ts +10 -7
- package/ts/opsserver/classes.opsserver.ts +3 -14
- package/ts/opsserver/handlers/acme-config.handler.ts +6 -23
- package/ts/opsserver/handlers/admin.handler.ts +109 -123
- package/ts/opsserver/handlers/api-token.handler.ts +27 -1
- package/ts/opsserver/handlers/certificate.handler.ts +6 -23
- package/ts/opsserver/handlers/config.handler.ts +2 -0
- package/ts/opsserver/handlers/dns-provider.handler.ts +6 -23
- package/ts/opsserver/handlers/dns-record.handler.ts +6 -23
- package/ts/opsserver/handlers/domain.handler.ts +6 -23
- package/ts/opsserver/handlers/email-domain.handler.ts +6 -23
- package/ts/opsserver/handlers/email-ops.handler.ts +7 -0
- package/ts/opsserver/handlers/logs.handler.ts +3 -0
- package/ts/opsserver/handlers/network-target.handler.ts +6 -23
- package/ts/opsserver/handlers/radius.handler.ts +31 -0
- package/ts/opsserver/handlers/remoteingress.handler.ts +23 -0
- package/ts/opsserver/handlers/route-management.handler.ts +6 -25
- package/ts/opsserver/handlers/security.handler.ts +31 -6
- package/ts/opsserver/handlers/source-profile.handler.ts +6 -23
- package/ts/opsserver/handlers/stats.handler.ts +7 -0
- package/ts/opsserver/handlers/target-profile.handler.ts +6 -23
- package/ts/opsserver/handlers/users.handler.ts +32 -12
- package/ts/opsserver/handlers/vpn.handler.ts +33 -0
- package/ts/opsserver/handlers/workhoster.handler.ts +18 -33
- package/ts/opsserver/helpers/auth.ts +91 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/access/ops-view-apitokens.ts +1 -20
|
@@ -608,9 +608,23 @@ export class RouteConfigManager {
|
|
|
608
608
|
routeId?: string,
|
|
609
609
|
): plugins.smartproxy.IRouteConfig {
|
|
610
610
|
const dcRoute = route as IDcRouterRouteConfig;
|
|
611
|
-
if (!dcRoute.vpnOnly) return route;
|
|
612
|
-
|
|
613
611
|
const vpnEntries = this.getVpnClientIpsForRoute?.(dcRoute, routeId) || [];
|
|
612
|
+
|
|
613
|
+
if (!dcRoute.vpnOnly) {
|
|
614
|
+
const existingAllowList = route.security?.ipAllowList;
|
|
615
|
+
if (!Array.isArray(existingAllowList) || existingAllowList.length === 0 || vpnEntries.length === 0) {
|
|
616
|
+
return route;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return {
|
|
620
|
+
...route,
|
|
621
|
+
security: {
|
|
622
|
+
...route.security,
|
|
623
|
+
ipAllowList: this.mergeIpAllowEntries(existingAllowList as TIpAllowEntry[], vpnEntries),
|
|
624
|
+
},
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
614
628
|
const existingBlockList = route.security?.ipBlockList || [];
|
|
615
629
|
const ipBlockList = vpnEntries.length
|
|
616
630
|
? existingBlockList
|
|
@@ -625,4 +639,23 @@ export class RouteConfigManager {
|
|
|
625
639
|
},
|
|
626
640
|
};
|
|
627
641
|
}
|
|
642
|
+
|
|
643
|
+
private mergeIpAllowEntries(
|
|
644
|
+
existingEntries: TIpAllowEntry[],
|
|
645
|
+
vpnEntries: TIpAllowEntry[],
|
|
646
|
+
): TIpAllowEntry[] {
|
|
647
|
+
const merged: TIpAllowEntry[] = [];
|
|
648
|
+
const seen = new Set<string>();
|
|
649
|
+
|
|
650
|
+
for (const entry of [...existingEntries, ...vpnEntries]) {
|
|
651
|
+
const key = typeof entry === 'string'
|
|
652
|
+
? `ip:${entry}`
|
|
653
|
+
: `domain:${entry.ip}:${[...entry.domains].sort().join(',')}`;
|
|
654
|
+
if (seen.has(key)) continue;
|
|
655
|
+
seen.add(key);
|
|
656
|
+
merged.push(entry);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return merged;
|
|
660
|
+
}
|
|
628
661
|
}
|
|
@@ -217,7 +217,7 @@ export class TargetProfileManager {
|
|
|
217
217
|
allRoutes: Map<string, IRoute> = new Map(),
|
|
218
218
|
): Array<string | { ip: string; domains: string[] }> {
|
|
219
219
|
const entries: Array<string | { ip: string; domains: string[] }> = [];
|
|
220
|
-
const routeDomains
|
|
220
|
+
const routeDomains = this.getRouteDomains(route);
|
|
221
221
|
const routeNameIndex = this.buildRouteNameIndex(allRoutes);
|
|
222
222
|
|
|
223
223
|
for (const client of clients) {
|
|
@@ -298,11 +298,8 @@ export class TargetProfileManager {
|
|
|
298
298
|
profile,
|
|
299
299
|
routeNameIndex,
|
|
300
300
|
)) {
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
for (const d of routeDomains) {
|
|
304
|
-
domains.add(d);
|
|
305
|
-
}
|
|
301
|
+
for (const d of this.getRouteDomains(route.route as IDcRouterRouteConfig)) {
|
|
302
|
+
domains.add(d);
|
|
306
303
|
}
|
|
307
304
|
}
|
|
308
305
|
}
|
|
@@ -327,7 +324,7 @@ export class TargetProfileManager {
|
|
|
327
324
|
profile: ITargetProfile,
|
|
328
325
|
routeNameIndex: Map<string, string[]>,
|
|
329
326
|
): boolean {
|
|
330
|
-
const routeDomains
|
|
327
|
+
const routeDomains = this.getRouteDomains(route);
|
|
331
328
|
const result = this.routeMatchesProfileDetailed(
|
|
332
329
|
route,
|
|
333
330
|
routeId,
|
|
@@ -425,6 +422,12 @@ export class TargetProfileManager {
|
|
|
425
422
|
return false;
|
|
426
423
|
}
|
|
427
424
|
|
|
425
|
+
private getRouteDomains(route: IDcRouterRouteConfig): string[] {
|
|
426
|
+
const domains = (route.match as any)?.domains;
|
|
427
|
+
if (!domains) return [];
|
|
428
|
+
return Array.isArray(domains) ? domains : [domains];
|
|
429
|
+
}
|
|
430
|
+
|
|
428
431
|
private normalizeRouteRefs(routeRefs?: string[]): string[] | undefined {
|
|
429
432
|
const allRoutes = this.getAllRoutes?.() || new Map<string, IRoute>();
|
|
430
433
|
return this.normalizeRouteRefsAgainstRoutes(routeRefs, allRoutes, 'strict');
|
|
@@ -3,7 +3,6 @@ import * as plugins from '../plugins.js';
|
|
|
3
3
|
import * as paths from '../paths.js';
|
|
4
4
|
import * as handlers from './handlers/index.js';
|
|
5
5
|
import * as interfaces from '../../ts_interfaces/index.js';
|
|
6
|
-
import { requireValidIdentity, requireAdminIdentity } from './helpers/guards.js';
|
|
7
6
|
|
|
8
7
|
export class OpsServer {
|
|
9
8
|
public dcRouterRef: DcRouter;
|
|
@@ -12,9 +11,9 @@ export class OpsServer {
|
|
|
12
11
|
// Main TypedRouter — unauthenticated endpoints (login/logout/verify) and own-auth handlers
|
|
13
12
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
14
13
|
|
|
15
|
-
//
|
|
16
|
-
public viewRouter = new plugins.typedrequest.TypedRouter<{ request: { identity
|
|
17
|
-
public adminRouter = new plugins.typedrequest.TypedRouter<{ request: { identity
|
|
14
|
+
// Grouped routers. Handlers enforce auth explicitly with per-endpoint scopes.
|
|
15
|
+
public viewRouter = new plugins.typedrequest.TypedRouter<{ request: { identity?: interfaces.data.IIdentity; apiToken?: string } }>();
|
|
16
|
+
public adminRouter = new plugins.typedrequest.TypedRouter<{ request: { identity?: interfaces.data.IIdentity; apiToken?: string } }>();
|
|
18
17
|
|
|
19
18
|
// Handler instances
|
|
20
19
|
public adminHandler!: handlers.AdminHandler;
|
|
@@ -72,16 +71,6 @@ export class OpsServer {
|
|
|
72
71
|
this.adminHandler = new handlers.AdminHandler(this);
|
|
73
72
|
await this.adminHandler.initialize();
|
|
74
73
|
|
|
75
|
-
// viewRouter middleware: requires valid identity (any logged-in user)
|
|
76
|
-
this.viewRouter.addMiddleware(async (typedRequest) => {
|
|
77
|
-
await requireValidIdentity(this.adminHandler, typedRequest.request);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// adminRouter middleware: requires admin identity
|
|
81
|
-
this.adminRouter.addMiddleware(async (typedRequest) => {
|
|
82
|
-
await requireAdminIdentity(this.adminHandler, typedRequest.request);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
74
|
// Connect auth routers to the main typedrouter
|
|
86
75
|
this.typedrouter.addTypedRouter(this.viewRouter);
|
|
87
76
|
this.typedrouter.addTypedRouter(this.adminRouter);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as plugins from '../../plugins.js';
|
|
2
2
|
import type { OpsServer } from '../classes.opsserver.js';
|
|
3
3
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
|
4
|
+
import { requireOpsAuth } from '../helpers/auth.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* CRUD handler for the singleton `AcmeConfigDoc`.
|
|
@@ -20,29 +21,11 @@ export class AcmeConfigHandler {
|
|
|
20
21
|
request: { identity?: interfaces.data.IIdentity; apiToken?: string },
|
|
21
22
|
requiredScope?: interfaces.data.TApiTokenScope,
|
|
22
23
|
): Promise<string> {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (isAdmin) return request.identity.userId;
|
|
29
|
-
} catch { /* fall through */ }
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (request.apiToken) {
|
|
33
|
-
const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
|
34
|
-
if (tokenManager) {
|
|
35
|
-
const token = await tokenManager.validateToken(request.apiToken);
|
|
36
|
-
if (token) {
|
|
37
|
-
if (!requiredScope || tokenManager.hasScope(token, requiredScope)) {
|
|
38
|
-
return token.createdBy;
|
|
39
|
-
}
|
|
40
|
-
throw new plugins.typedrequest.TypedResponseError('insufficient scope');
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
throw new plugins.typedrequest.TypedResponseError('unauthorized');
|
|
24
|
+
const auth = await requireOpsAuth(this.opsServerRef, request, {
|
|
25
|
+
scope: requiredScope,
|
|
26
|
+
requireAdminIdentity: requiredScope?.endsWith(':write'),
|
|
27
|
+
});
|
|
28
|
+
return auth.userId;
|
|
46
29
|
}
|
|
47
30
|
|
|
48
31
|
private registerHandlers(): void {
|
|
@@ -24,7 +24,8 @@ export class AdminHandler {
|
|
|
24
24
|
// JWT instance
|
|
25
25
|
public smartjwtInstance!: plugins.smartjwt.SmartJwt<IJwtData>;
|
|
26
26
|
|
|
27
|
-
// Ephemeral bootstrap users.
|
|
27
|
+
// Ephemeral bootstrap users. DB-backed instances may use these only until the
|
|
28
|
+
// database is ready and the first persistent admin account has been created.
|
|
28
29
|
private users = new Map<string, {
|
|
29
30
|
id: string;
|
|
30
31
|
username: string;
|
|
@@ -87,9 +88,12 @@ export class AdminHandler {
|
|
|
87
88
|
* Used by UsersHandler to serve the admin-only listUsers endpoint.
|
|
88
89
|
*/
|
|
89
90
|
public async listUsers(): Promise<interfaces.requests.IAdminUserProjection[]> {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
const accountState = await this.getPersistentAccountState();
|
|
92
|
+
if (accountState.dbEnabled && !accountState.dbReady) {
|
|
93
|
+
throw new plugins.typedrequest.TypedResponseError('database is not ready');
|
|
94
|
+
}
|
|
95
|
+
if (accountState.hasPersistentAdmin) {
|
|
96
|
+
const accounts = await accountState.store!.listAccounts();
|
|
93
97
|
return accounts.map((accountArg) => this.accountToUser(accountArg));
|
|
94
98
|
}
|
|
95
99
|
|
|
@@ -101,16 +105,14 @@ export class AdminHandler {
|
|
|
101
105
|
}
|
|
102
106
|
|
|
103
107
|
public async getBootstrapStatus(): Promise<interfaces.requests.IReq_GetAdminBootstrapStatus['response']> {
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
const dbReady = !!store;
|
|
107
|
-
const hasPersistentAdmin = dbReady ? await store.hasActiveAdminAccount() : false;
|
|
108
|
+
const accountState = await this.getPersistentAccountState();
|
|
109
|
+
const bootstrapAvailable = !accountState.dbEnabled || (accountState.dbReady && !accountState.hasPersistentAdmin);
|
|
108
110
|
return {
|
|
109
|
-
dbEnabled,
|
|
110
|
-
dbReady,
|
|
111
|
-
hasPersistentAdmin,
|
|
112
|
-
needsBootstrap: dbEnabled && dbReady && !hasPersistentAdmin,
|
|
113
|
-
ephemeralAdminAvailable:
|
|
111
|
+
dbEnabled: accountState.dbEnabled,
|
|
112
|
+
dbReady: accountState.dbReady,
|
|
113
|
+
hasPersistentAdmin: accountState.hasPersistentAdmin,
|
|
114
|
+
needsBootstrap: accountState.dbEnabled && accountState.dbReady && !accountState.hasPersistentAdmin,
|
|
115
|
+
ephemeralAdminAvailable: bootstrapAvailable,
|
|
114
116
|
idpGlobalConfigured: this.isIdpGlobalConfigured(),
|
|
115
117
|
};
|
|
116
118
|
}
|
|
@@ -258,12 +260,18 @@ export class AdminHandler {
|
|
|
258
260
|
this.opsServerRef.adminRouter.addTypedHandler(
|
|
259
261
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateInitialAdminUser>(
|
|
260
262
|
'createInitialAdminUser',
|
|
261
|
-
async (dataArg) =>
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
263
|
+
async (dataArg) => {
|
|
264
|
+
const isAdmin = await this.adminIdentityGuard.exec({ identity: dataArg.identity });
|
|
265
|
+
if (!isAdmin) {
|
|
266
|
+
throw new plugins.typedrequest.TypedResponseError('admin identity required');
|
|
267
|
+
}
|
|
268
|
+
return this.createInitialAdminUser({
|
|
269
|
+
email: dataArg.email,
|
|
270
|
+
name: dataArg.name,
|
|
271
|
+
password: dataArg.password,
|
|
272
|
+
enableIdpGlobalAuth: dataArg.enableIdpGlobalAuth,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
267
275
|
)
|
|
268
276
|
);
|
|
269
277
|
|
|
@@ -300,8 +308,10 @@ export class AdminHandler {
|
|
|
300
308
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_AdminLogout>(
|
|
301
309
|
'adminLogout',
|
|
302
310
|
async (dataArg) => {
|
|
303
|
-
|
|
304
|
-
|
|
311
|
+
const identity = await this.validateIdentity(dataArg.identity);
|
|
312
|
+
if (!identity) {
|
|
313
|
+
throw new plugins.typedrequest.TypedResponseError('identity is not valid');
|
|
314
|
+
}
|
|
305
315
|
return {
|
|
306
316
|
success: true,
|
|
307
317
|
};
|
|
@@ -314,52 +324,8 @@ export class AdminHandler {
|
|
|
314
324
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_VerifyIdentity>(
|
|
315
325
|
'verifyIdentity',
|
|
316
326
|
async (dataArg) => {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
valid: false,
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
try {
|
|
324
|
-
const jwtData = await this.smartjwtInstance.verifyJWTAndGetData(dataArg.identity.jwt);
|
|
325
|
-
|
|
326
|
-
// Check if expired
|
|
327
|
-
if (jwtData.expiresAt < Date.now()) {
|
|
328
|
-
return {
|
|
329
|
-
valid: false,
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Check if logged in
|
|
334
|
-
if (jwtData.status !== 'loggedIn') {
|
|
335
|
-
return {
|
|
336
|
-
valid: false,
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
const user = await this.resolveUser(jwtData.userId);
|
|
341
|
-
if (!user) {
|
|
342
|
-
return {
|
|
343
|
-
valid: false,
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
return {
|
|
348
|
-
valid: true,
|
|
349
|
-
identity: {
|
|
350
|
-
jwt: dataArg.identity.jwt,
|
|
351
|
-
userId: user.id,
|
|
352
|
-
name: user.name || user.username,
|
|
353
|
-
expiresAt: jwtData.expiresAt,
|
|
354
|
-
role: user.role,
|
|
355
|
-
type: 'user',
|
|
356
|
-
},
|
|
357
|
-
};
|
|
358
|
-
} catch (error) {
|
|
359
|
-
return {
|
|
360
|
-
valid: false,
|
|
361
|
-
};
|
|
362
|
-
}
|
|
327
|
+
const identity = await this.validateIdentity(dataArg.identity);
|
|
328
|
+
return identity ? { valid: true, identity } : { valid: false };
|
|
363
329
|
}
|
|
364
330
|
)
|
|
365
331
|
);
|
|
@@ -372,45 +338,7 @@ export class AdminHandler {
|
|
|
372
338
|
identity: interfaces.data.IIdentity;
|
|
373
339
|
}>(
|
|
374
340
|
async (dataArg) => {
|
|
375
|
-
|
|
376
|
-
return false;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
try {
|
|
380
|
-
const jwtData = await this.smartjwtInstance.verifyJWTAndGetData(dataArg.identity.jwt);
|
|
381
|
-
|
|
382
|
-
// Check expiration
|
|
383
|
-
if (jwtData.expiresAt < Date.now()) {
|
|
384
|
-
return false;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// Check status
|
|
388
|
-
if (jwtData.status !== 'loggedIn') {
|
|
389
|
-
return false;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Verify data hasn't been tampered with
|
|
393
|
-
if (dataArg.identity.expiresAt !== jwtData.expiresAt) {
|
|
394
|
-
return false;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
if (dataArg.identity.userId !== jwtData.userId) {
|
|
398
|
-
return false;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
const user = await this.resolveUser(jwtData.userId);
|
|
402
|
-
if (!user) {
|
|
403
|
-
return false;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
if (dataArg.identity.role && dataArg.identity.role !== user.role) {
|
|
407
|
-
return false;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
return true;
|
|
411
|
-
} catch (error) {
|
|
412
|
-
return false;
|
|
413
|
-
}
|
|
341
|
+
return Boolean(await this.validateIdentity(dataArg.identity));
|
|
414
342
|
},
|
|
415
343
|
{
|
|
416
344
|
failedHint: 'identity is not valid',
|
|
@@ -425,14 +353,8 @@ export class AdminHandler {
|
|
|
425
353
|
identity: interfaces.data.IIdentity;
|
|
426
354
|
}>(
|
|
427
355
|
async (dataArg) => {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
if (!isValid) {
|
|
431
|
-
return false;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Check if user has admin role
|
|
435
|
-
return dataArg.identity.role === 'admin';
|
|
356
|
+
const identity = await this.validateIdentity(dataArg.identity);
|
|
357
|
+
return identity?.role === 'admin';
|
|
436
358
|
},
|
|
437
359
|
{
|
|
438
360
|
failedHint: 'user is not admin',
|
|
@@ -440,15 +362,62 @@ export class AdminHandler {
|
|
|
440
362
|
}
|
|
441
363
|
);
|
|
442
364
|
|
|
365
|
+
public async validateIdentity(
|
|
366
|
+
identityArg?: interfaces.data.IIdentity,
|
|
367
|
+
): Promise<interfaces.data.IIdentity | null> {
|
|
368
|
+
if (!identityArg?.jwt) {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
const jwtData = await this.smartjwtInstance.verifyJWTAndGetData(identityArg.jwt);
|
|
374
|
+
if (jwtData.expiresAt < Date.now()) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
if (jwtData.status !== 'loggedIn') {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
if (identityArg.expiresAt !== jwtData.expiresAt) {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
if (identityArg.userId !== jwtData.userId) {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const user = await this.resolveUser(jwtData.userId);
|
|
388
|
+
if (!user) {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
if (identityArg.role && identityArg.role !== user.role) {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
jwt: identityArg.jwt,
|
|
397
|
+
userId: user.id,
|
|
398
|
+
name: user.name || user.username,
|
|
399
|
+
expiresAt: jwtData.expiresAt,
|
|
400
|
+
role: user.role,
|
|
401
|
+
type: 'user',
|
|
402
|
+
};
|
|
403
|
+
} catch {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
443
408
|
private async authenticateUser(optionsArg: {
|
|
444
409
|
username: string;
|
|
445
410
|
password: string;
|
|
446
411
|
authSource?: interfaces.requests.TAdminLoginAuthSource;
|
|
447
412
|
}): Promise<TAdminUser | null> {
|
|
448
|
-
|
|
449
|
-
|
|
413
|
+
const accountState = await this.getPersistentAccountState();
|
|
414
|
+
if (accountState.dbEnabled && !accountState.dbReady) {
|
|
415
|
+
throw new plugins.typedrequest.TypedResponseError('database is not ready');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (accountState.hasPersistentAdmin) {
|
|
450
419
|
const authService = new plugins.idpSdkServer.AccountAuthService({
|
|
451
|
-
store: store!,
|
|
420
|
+
store: accountState.store!,
|
|
452
421
|
idpClient: this.getIdpClient() as plugins.idpSdkServer.IdpGlobalServerClient | undefined,
|
|
453
422
|
});
|
|
454
423
|
const result = await authService.authenticate({
|
|
@@ -468,8 +437,13 @@ export class AdminHandler {
|
|
|
468
437
|
}
|
|
469
438
|
|
|
470
439
|
private async resolveUser(userIdArg: string): Promise<TAdminUser | null> {
|
|
471
|
-
|
|
472
|
-
|
|
440
|
+
const accountState = await this.getPersistentAccountState();
|
|
441
|
+
if (accountState.dbEnabled && !accountState.dbReady) {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (accountState.hasPersistentAdmin) {
|
|
446
|
+
const account = await accountState.store!.getAccountById(userIdArg);
|
|
473
447
|
if (!account || account.status !== 'active') {
|
|
474
448
|
return null;
|
|
475
449
|
}
|
|
@@ -479,13 +453,25 @@ export class AdminHandler {
|
|
|
479
453
|
return this.users.get(userIdArg) || null;
|
|
480
454
|
}
|
|
481
455
|
|
|
482
|
-
private async
|
|
483
|
-
|
|
484
|
-
|
|
456
|
+
private async getPersistentAccountState(): Promise<{
|
|
457
|
+
dbEnabled: boolean;
|
|
458
|
+
dbReady: boolean;
|
|
459
|
+
store: plugins.idpSdkServer.SmartdataAccountStore | null;
|
|
460
|
+
hasPersistentAdmin: boolean;
|
|
461
|
+
}> {
|
|
462
|
+
const dbEnabled = this.isPersistenceEnabled();
|
|
463
|
+
const store = dbEnabled ? this.getAccountStore() : null;
|
|
464
|
+
const dbReady = !!store;
|
|
465
|
+
const hasPersistentAdmin = store ? await store.hasActiveAdminAccount() : false;
|
|
466
|
+
return { dbEnabled, dbReady, store, hasPersistentAdmin };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private isPersistenceEnabled(): boolean {
|
|
470
|
+
return this.opsServerRef.dcRouterRef.options.dbConfig?.enabled !== false;
|
|
485
471
|
}
|
|
486
472
|
|
|
487
473
|
private getAccountStore(): plugins.idpSdkServer.SmartdataAccountStore | null {
|
|
488
|
-
if (this.
|
|
474
|
+
if (!this.isPersistenceEnabled()) {
|
|
489
475
|
return null;
|
|
490
476
|
}
|
|
491
477
|
const dcRouterDb = this.opsServerRef.dcRouterRef.dcRouterDb;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as plugins from '../../plugins.js';
|
|
2
2
|
import type { OpsServer } from '../classes.opsserver.js';
|
|
3
3
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
|
4
|
+
import { requireOpsAuth } from '../helpers/auth.js';
|
|
4
5
|
|
|
5
6
|
export class ApiTokenHandler {
|
|
6
7
|
constructor(private opsServerRef: OpsServer) {
|
|
@@ -17,6 +18,11 @@ export class ApiTokenHandler {
|
|
|
17
18
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateApiToken>(
|
|
18
19
|
'createApiToken',
|
|
19
20
|
async (dataArg) => {
|
|
21
|
+
const auth = await requireOpsAuth(this.opsServerRef, dataArg, {
|
|
22
|
+
scope: 'tokens:manage',
|
|
23
|
+
requireAdminIdentity: true,
|
|
24
|
+
requireAdminToken: true,
|
|
25
|
+
});
|
|
20
26
|
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
|
21
27
|
if (!manager) {
|
|
22
28
|
return { success: false, message: 'Token management not initialized' };
|
|
@@ -25,7 +31,7 @@ export class ApiTokenHandler {
|
|
|
25
31
|
dataArg.name,
|
|
26
32
|
dataArg.scopes,
|
|
27
33
|
dataArg.expiresInDays ?? null,
|
|
28
|
-
|
|
34
|
+
auth.userId,
|
|
29
35
|
dataArg.policy,
|
|
30
36
|
);
|
|
31
37
|
return { success: true, tokenId: result.id, tokenValue: result.rawToken };
|
|
@@ -38,6 +44,11 @@ export class ApiTokenHandler {
|
|
|
38
44
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListApiTokens>(
|
|
39
45
|
'listApiTokens',
|
|
40
46
|
async (dataArg) => {
|
|
47
|
+
await requireOpsAuth(this.opsServerRef, dataArg, {
|
|
48
|
+
scope: 'tokens:read',
|
|
49
|
+
requireAdminIdentity: true,
|
|
50
|
+
requireAdminToken: true,
|
|
51
|
+
});
|
|
41
52
|
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
|
42
53
|
if (!manager) {
|
|
43
54
|
return { tokens: [] };
|
|
@@ -52,6 +63,11 @@ export class ApiTokenHandler {
|
|
|
52
63
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RevokeApiToken>(
|
|
53
64
|
'revokeApiToken',
|
|
54
65
|
async (dataArg) => {
|
|
66
|
+
await requireOpsAuth(this.opsServerRef, dataArg, {
|
|
67
|
+
scope: 'tokens:manage',
|
|
68
|
+
requireAdminIdentity: true,
|
|
69
|
+
requireAdminToken: true,
|
|
70
|
+
});
|
|
55
71
|
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
|
56
72
|
if (!manager) {
|
|
57
73
|
return { success: false, message: 'Token management not initialized' };
|
|
@@ -67,6 +83,11 @@ export class ApiTokenHandler {
|
|
|
67
83
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RollApiToken>(
|
|
68
84
|
'rollApiToken',
|
|
69
85
|
async (dataArg) => {
|
|
86
|
+
await requireOpsAuth(this.opsServerRef, dataArg, {
|
|
87
|
+
scope: 'tokens:manage',
|
|
88
|
+
requireAdminIdentity: true,
|
|
89
|
+
requireAdminToken: true,
|
|
90
|
+
});
|
|
70
91
|
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
|
71
92
|
if (!manager) {
|
|
72
93
|
return { success: false, message: 'Token management not initialized' };
|
|
@@ -85,6 +106,11 @@ export class ApiTokenHandler {
|
|
|
85
106
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ToggleApiToken>(
|
|
86
107
|
'toggleApiToken',
|
|
87
108
|
async (dataArg) => {
|
|
109
|
+
await requireOpsAuth(this.opsServerRef, dataArg, {
|
|
110
|
+
scope: 'tokens:manage',
|
|
111
|
+
requireAdminIdentity: true,
|
|
112
|
+
requireAdminToken: true,
|
|
113
|
+
});
|
|
88
114
|
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
|
89
115
|
if (!manager) {
|
|
90
116
|
return { success: false, message: 'Token management not initialized' };
|
|
@@ -3,6 +3,7 @@ import type { OpsServer } from '../classes.opsserver.js';
|
|
|
3
3
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
|
4
4
|
import { AcmeCertDoc, ProxyCertDoc } from '../../db/index.js';
|
|
5
5
|
import { logger } from '../../logger.js';
|
|
6
|
+
import { requireOpsAuth } from '../helpers/auth.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Mirrors `SmartacmeCertMatcher.getCertificateDomainNameByDomainName` from
|
|
@@ -37,29 +38,11 @@ export class CertificateHandler {
|
|
|
37
38
|
request: { identity?: interfaces.data.IIdentity; apiToken?: string },
|
|
38
39
|
requiredScope?: interfaces.data.TApiTokenScope,
|
|
39
40
|
): Promise<string> {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (isAdmin) return request.identity.userId;
|
|
46
|
-
} catch { /* fall through */ }
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (request.apiToken) {
|
|
50
|
-
const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
|
51
|
-
if (tokenManager) {
|
|
52
|
-
const token = await tokenManager.validateToken(request.apiToken);
|
|
53
|
-
if (token) {
|
|
54
|
-
if (!requiredScope || tokenManager.hasScope(token, requiredScope)) {
|
|
55
|
-
return token.createdBy;
|
|
56
|
-
}
|
|
57
|
-
throw new plugins.typedrequest.TypedResponseError('insufficient scope');
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
throw new plugins.typedrequest.TypedResponseError('unauthorized');
|
|
41
|
+
const auth = await requireOpsAuth(this.opsServerRef, request, {
|
|
42
|
+
scope: requiredScope,
|
|
43
|
+
requireAdminIdentity: requiredScope?.endsWith(':write'),
|
|
44
|
+
});
|
|
45
|
+
return auth.userId;
|
|
63
46
|
}
|
|
64
47
|
|
|
65
48
|
private registerHandlers(): void {
|
|
@@ -2,6 +2,7 @@ import * as plugins from '../../plugins.js';
|
|
|
2
2
|
import * as paths from '../../paths.js';
|
|
3
3
|
import type { OpsServer } from '../classes.opsserver.js';
|
|
4
4
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
|
5
|
+
import { requireOpsAuth } from '../helpers/auth.js';
|
|
5
6
|
|
|
6
7
|
export class ConfigHandler {
|
|
7
8
|
constructor(private opsServerRef: OpsServer) {
|
|
@@ -17,6 +18,7 @@ export class ConfigHandler {
|
|
|
17
18
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetConfiguration>(
|
|
18
19
|
'getConfiguration',
|
|
19
20
|
async (dataArg, toolsArg) => {
|
|
21
|
+
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'config:read' });
|
|
20
22
|
const config = await this.getConfiguration();
|
|
21
23
|
return {
|
|
22
24
|
config,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as plugins from '../../plugins.js';
|
|
2
2
|
import type { OpsServer } from '../classes.opsserver.js';
|
|
3
3
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
|
4
|
+
import { requireOpsAuth } from '../helpers/auth.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* CRUD + connection-test handlers for DnsProviderDoc.
|
|
@@ -20,29 +21,11 @@ export class DnsProviderHandler {
|
|
|
20
21
|
request: { identity?: interfaces.data.IIdentity; apiToken?: string },
|
|
21
22
|
requiredScope?: interfaces.data.TApiTokenScope,
|
|
22
23
|
): Promise<string> {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (isAdmin) return request.identity.userId;
|
|
29
|
-
} catch { /* fall through */ }
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (request.apiToken) {
|
|
33
|
-
const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
|
34
|
-
if (tokenManager) {
|
|
35
|
-
const token = await tokenManager.validateToken(request.apiToken);
|
|
36
|
-
if (token) {
|
|
37
|
-
if (!requiredScope || tokenManager.hasScope(token, requiredScope)) {
|
|
38
|
-
return token.createdBy;
|
|
39
|
-
}
|
|
40
|
-
throw new plugins.typedrequest.TypedResponseError('insufficient scope');
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
throw new plugins.typedrequest.TypedResponseError('unauthorized');
|
|
24
|
+
const auth = await requireOpsAuth(this.opsServerRef, request, {
|
|
25
|
+
scope: requiredScope,
|
|
26
|
+
requireAdminIdentity: requiredScope?.endsWith(':write'),
|
|
27
|
+
});
|
|
28
|
+
return auth.userId;
|
|
46
29
|
}
|
|
47
30
|
|
|
48
31
|
private registerHandlers(): void {
|