feat(ops-auth): add scoped API token auth across ops endpoints

This commit is contained in:
2026-05-19 22:24:37 +00:00
parent 53d7c5350e
commit 77c1738390
47 changed files with 909 additions and 511 deletions
+7
View File
@@ -6,6 +6,13 @@
### Features
- add scoped API token auth across ops endpoints (ops-auth)
- introduces a shared requireOpsAuth helper that validates JWT identities and API tokens with scope and admin-policy checks
- applies explicit per-endpoint authorization across config, logs, stats, security, VPN, RADIUS, remote ingress, users, API tokens, and related ops handlers
- extends request interfaces and UI scope definitions to support apiToken-based access and adds tests for auth behavior and migration bridging
## 2026-05-19 - 13.31.0 ## 2026-05-19 - 13.31.0
### Features ### Features
+1 -1
View File
@@ -51,7 +51,7 @@
"@push.rocks/smartjwt": "^2.2.2", "@push.rocks/smartjwt": "^2.2.2",
"@push.rocks/smartlog": "^3.2.2", "@push.rocks/smartlog": "^3.2.2",
"@push.rocks/smartmetrics": "^3.0.3", "@push.rocks/smartmetrics": "^3.0.3",
"@push.rocks/smartmigration": "1.3.1", "@push.rocks/smartmigration": "1.4.1",
"@push.rocks/smartmta": "^5.3.3", "@push.rocks/smartmta": "^5.3.3",
"@push.rocks/smartnetwork": "^4.7.1", "@push.rocks/smartnetwork": "^4.7.1",
"@push.rocks/smartpath": "^6.0.0", "@push.rocks/smartpath": "^6.0.0",
+5 -5
View File
@@ -69,8 +69,8 @@ importers:
specifier: ^3.0.3 specifier: ^3.0.3
version: 3.0.3 version: 3.0.3
'@push.rocks/smartmigration': '@push.rocks/smartmigration':
specifier: 1.3.1 specifier: 1.4.1
version: 1.3.1(@push.rocks/smartbucket@4.6.1)(@push.rocks/smartdata@7.1.7(socks@2.8.8)) version: 1.4.1(@push.rocks/smartbucket@4.6.1)(@push.rocks/smartdata@7.1.7(socks@2.8.8))
'@push.rocks/smartmta': '@push.rocks/smartmta':
specifier: ^5.3.3 specifier: ^5.3.3
version: 5.3.3 version: 5.3.3
@@ -1366,8 +1366,8 @@ packages:
'@push.rocks/smartmetrics@3.0.3': '@push.rocks/smartmetrics@3.0.3':
resolution: {integrity: sha512-RYY4NOla3kraZYVF9TBHgIz4/hSkqVDVNP7tLwhLK5mGBPBy8I/9NWXX6txZKQw6QihP85YD8mWUuUu2xS4D6Q==} resolution: {integrity: sha512-RYY4NOla3kraZYVF9TBHgIz4/hSkqVDVNP7tLwhLK5mGBPBy8I/9NWXX6txZKQw6QihP85YD8mWUuUu2xS4D6Q==}
'@push.rocks/smartmigration@1.3.1': '@push.rocks/smartmigration@1.4.1':
resolution: {integrity: sha512-qU3vc4yCLn8vJQIEMQwS2Lq6Ra8ixSfjutnbR1L/hauCzFRCic3o/DnFKB7pjj5jWaqSDG5nlyeIliLmC5aGsg==} resolution: {integrity: sha512-kBvWuqBIIgkK2QskjHl0/MPLXYu4CDJDyuPc1KBDPBNejYIJp6hOZtbsmj4DYoNKsgFTpAALJn9JmUEdLe9E4g==}
peerDependencies: peerDependencies:
'@push.rocks/smartbucket': ^4.6.1 '@push.rocks/smartbucket': ^4.6.1
'@push.rocks/smartdata': ^7.1.7 '@push.rocks/smartdata': ^7.1.7
@@ -6508,7 +6508,7 @@ snapshots:
'@push.rocks/smartdelay': 3.1.0 '@push.rocks/smartdelay': 3.1.0
'@push.rocks/smartlog': 3.2.2 '@push.rocks/smartlog': 3.2.2
'@push.rocks/smartmigration@1.3.1(@push.rocks/smartbucket@4.6.1)(@push.rocks/smartdata@7.1.7(socks@2.8.8))': '@push.rocks/smartmigration@1.4.1(@push.rocks/smartbucket@4.6.1)(@push.rocks/smartdata@7.1.7(socks@2.8.8))':
dependencies: dependencies:
'@push.rocks/smartlog': 3.2.2 '@push.rocks/smartlog': 3.2.2
'@push.rocks/smartversion': 3.1.0 '@push.rocks/smartversion': 3.1.0
+1
View File
@@ -56,6 +56,7 @@ const setupHandler = (scopes: TScope[], options?: {
const opsServerRef: any = { const opsServerRef: any = {
typedrouter, typedrouter,
adminHandler: { adminHandler: {
validateIdentity: async () => null,
adminIdentityGuard: { adminIdentityGuard: {
exec: async () => false, exec: async () => false,
}, },
+79
View File
@@ -0,0 +1,79 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { ConfigHandler } from '../ts/opsserver/handlers/config.handler.js';
import * as plugins from '../ts/plugins.js';
import * as interfaces from '../ts_interfaces/index.js';
const fireTypedRequest = async (
router: plugins.typedrequest.TypedRouter,
method: string,
request: Record<string, any>,
) => {
return await router.routeAndAddResponse({
method,
request,
response: {},
correlation: {
id: `${method}-${Date.now()}-${Math.random().toString(16).slice(2)}`,
phase: 'request',
},
} as any, { localRequest: true, skipHooks: true }) as any;
};
const makeOpsServer = (scopes: interfaces.data.TApiTokenScope[]) => {
const router = new plugins.typedrequest.TypedRouter();
const token = {
id: 'token-1',
name: 'config-token',
tokenHash: 'hash',
scopes,
createdBy: 'token-user',
createdAt: Date.now(),
expiresAt: null,
lastUsedAt: null,
enabled: true,
} as interfaces.data.IStoredApiToken;
const opsServerRef = {
viewRouter: router,
adminHandler: {
validateIdentity: async () => null,
},
dcRouterRef: {
options: {
dbConfig: { enabled: false },
},
resolvedPaths: {
dcrouterHomeDir: '/tmp/dcrouter-home',
dataDir: '/tmp/dcrouter-data',
defaultTsmDbPath: '/tmp/dcrouter-data/db',
},
detectedPublicIp: null,
apiTokenManager: {
validateToken: async (rawTokenArg: string) => rawTokenArg === 'valid-token' ? token : null,
hasScope: (storedTokenArg: interfaces.data.IStoredApiToken, scopeArg: interfaces.data.TApiTokenScope) => storedTokenArg.scopes.includes(scopeArg),
},
},
} as any;
new ConfigHandler(opsServerRef);
return router;
};
tap.test('ConfigHandler accepts API token with config:read', async () => {
const router = makeOpsServer(['config:read']);
const result = await fireTypedRequest(router, 'getConfiguration', {
apiToken: 'valid-token',
});
expect(result.error).toBeUndefined();
expect(result.response.config.system.baseDir).toEqual('/tmp/dcrouter-home');
});
tap.test('ConfigHandler rejects API token without config:read', async () => {
const router = makeOpsServer(['logs:read']);
const result = await fireTypedRequest(router, 'getConfiguration', {
apiToken: 'valid-token',
});
expect(result.error?.text).toEqual('insufficient scope');
});
export default tap.start();
+69
View File
@@ -0,0 +1,69 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { createMigrationRunner } from '../ts_migrations/index.js';
function setPath(target: Record<string, any>, path: string, value: unknown): void {
const parts = path.split('.');
let cursor = target;
for (const part of parts.slice(0, -1)) {
cursor[part] = cursor[part] || {};
cursor = cursor[part];
}
cursor[parts[parts.length - 1]] = value;
}
function applySet(document: Record<string, any>, set: Record<string, unknown>): void {
for (const [key, value] of Object.entries(set)) {
setPath(document, key, value);
}
}
function createFakeDb(currentVersion: string) {
const ledgerDocument = {
nameId: 'smartmigration:smartmigration',
data: {
currentVersion,
steps: {},
lock: { holder: null, acquiredAt: null, expiresAt: null },
checkpoints: {},
},
};
const emptyCollection = {
find: () => ({
async *[Symbol.asyncIterator]() {},
}),
updateMany: async () => ({ modifiedCount: 0 }),
};
const ledgerCollection = {
createIndex: async () => undefined,
findOne: async () => structuredClone(ledgerDocument),
findOneAndUpdate: async (_query: unknown, update: any) => {
applySet(ledgerDocument, update.$set || {});
return structuredClone(ledgerDocument);
},
updateOne: async (_query: unknown, update: any) => {
applySet(ledgerDocument, update.$set || {});
return { matchedCount: 1, modifiedCount: 1, upsertedCount: 0 };
},
};
return {
mongoDb: {
collection: (name: string) =>
name === 'SmartdataEasyStore' ? ledgerCollection : emptyCollection,
},
};
}
tap.test('migration runner bridges old package-version targets without real schema steps', async () => {
const runner = await createMigrationRunner(createFakeDb('13.16.0'), '13.31.0');
const result = await runner.run();
expect(result.currentVersionBefore).toEqual('13.16.0');
expect(result.currentVersionAfter).toEqual('13.31.0');
expect(result.stepsApplied).toHaveLength(3);
});
export default tap.start();
+126
View File
@@ -0,0 +1,126 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { requireOpsAuth } from '../ts/opsserver/helpers/auth.js';
import * as interfaces from '../ts_interfaces/index.js';
type TScope = interfaces.data.TApiTokenScope;
const makeIdentity = (role: string = 'user'): interfaces.data.IIdentity => ({
jwt: `jwt-${role}`,
userId: `${role}-user`,
name: role,
expiresAt: Date.now() + 3600000,
role,
});
const makeOpsServer = (options: {
identityRole?: string | null;
tokenScopes?: TScope[];
tokenPolicy?: interfaces.data.IApiTokenPolicy;
}) => {
const token = {
id: 'token-1',
name: 'test-token',
tokenHash: 'hash',
scopes: options.tokenScopes || [],
policy: options.tokenPolicy,
createdAt: Date.now(),
expiresAt: null,
lastUsedAt: null,
createdBy: 'token-user',
enabled: true,
} as interfaces.data.IStoredApiToken;
return {
adminHandler: {
validateIdentity: async (identityArg?: interfaces.data.IIdentity) => {
if (!identityArg || options.identityRole === null) return null;
return { ...identityArg, role: options.identityRole || identityArg.role || 'user' };
},
},
dcRouterRef: {
apiTokenManager: {
validateToken: async (rawTokenArg: string) => rawTokenArg === 'valid-token' ? token : null,
hasScope: (storedTokenArg: interfaces.data.IStoredApiToken, scopeArg: TScope) => {
if (storedTokenArg.policy?.role === 'admin') return true;
return storedTokenArg.scopes.includes('*') || storedTokenArg.scopes.includes(scopeArg) || Boolean(storedTokenArg.policy?.scopes?.includes(scopeArg));
},
},
},
} as any;
};
const getErrorText = (errorArg: unknown) => {
return (errorArg as any).errorText || (errorArg as any).text || (errorArg as Error).message;
};
tap.test('requireOpsAuth accepts valid JWT identity for read endpoints', async () => {
const auth = await requireOpsAuth(
makeOpsServer({ identityRole: 'user' }),
{ identity: makeIdentity('user') },
{ scope: 'config:read' },
);
expect(auth.type).toEqual('identity');
expect(auth.userId).toEqual('user-user');
expect(auth.isAdmin).toEqual(false);
});
tap.test('requireOpsAuth rejects non-admin JWT identity for admin identity requirements', async () => {
let errorText = '';
try {
await requireOpsAuth(
makeOpsServer({ identityRole: 'user' }),
{ identity: makeIdentity('user') },
{ scope: 'routes:write', requireAdminIdentity: true },
);
} catch (error) {
errorText = getErrorText(error);
}
expect(errorText).toEqual('admin identity required');
});
tap.test('requireOpsAuth accepts scoped API tokens', async () => {
const auth = await requireOpsAuth(
makeOpsServer({ identityRole: null, tokenScopes: ['logs:read'] }),
{ apiToken: 'valid-token' },
{ scope: 'logs:read' },
);
expect(auth.type).toEqual('apiToken');
expect(auth.userId).toEqual('token-user');
});
tap.test('requireOpsAuth rejects API tokens without the required scope', async () => {
let errorText = '';
try {
await requireOpsAuth(
makeOpsServer({ identityRole: null, tokenScopes: ['logs:read'] }),
{ apiToken: 'valid-token' },
{ scope: 'stats:read' },
);
} catch (error) {
errorText = getErrorText(error);
}
expect(errorText).toEqual('insufficient scope');
});
tap.test('requireOpsAuth requires admin policy for sensitive API-token operations', async () => {
let errorText = '';
try {
await requireOpsAuth(
makeOpsServer({ identityRole: null, tokenScopes: ['tokens:manage'] }),
{ apiToken: 'valid-token' },
{ scope: 'tokens:manage', requireAdminToken: true },
);
} catch (error) {
errorText = getErrorText(error);
}
expect(errorText).toEqual('admin API token required');
const auth = await requireOpsAuth(
makeOpsServer({ identityRole: null, tokenPolicy: { role: 'admin' } }),
{ apiToken: 'valid-token' },
{ scope: 'tokens:manage', requireAdminToken: true },
);
expect(auth.isAdmin).toEqual(true);
});
export default tap.start();
+3
View File
@@ -136,6 +136,9 @@ const setupHandler = (options: {
const opsServerRef: any = { const opsServerRef: any = {
typedrouter, typedrouter,
adminHandler: { adminHandler: {
validateIdentity: async (identity: interfaces.data.IIdentity) => options.isAdmin
? { ...identity, role: 'admin' }
: identity,
adminIdentityGuard: { adminIdentityGuard: {
exec: async () => Boolean(options.isAdmin), exec: async () => Boolean(options.isAdmin),
}, },
+3 -14
View File
@@ -3,7 +3,6 @@ import * as plugins from '../plugins.js';
import * as paths from '../paths.js'; import * as paths from '../paths.js';
import * as handlers from './handlers/index.js'; import * as handlers from './handlers/index.js';
import * as interfaces from '../../ts_interfaces/index.js'; import * as interfaces from '../../ts_interfaces/index.js';
import { requireValidIdentity, requireAdminIdentity } from './helpers/guards.js';
export class OpsServer { export class OpsServer {
public dcRouterRef: DcRouter; public dcRouterRef: DcRouter;
@@ -12,9 +11,9 @@ export class OpsServer {
// Main TypedRouter — unauthenticated endpoints (login/logout/verify) and own-auth handlers // Main TypedRouter — unauthenticated endpoints (login/logout/verify) and own-auth handlers
public typedrouter = new plugins.typedrequest.TypedRouter(); public typedrouter = new plugins.typedrequest.TypedRouter();
// Auth-enforced routers — middleware validates identity before any handler runs // Grouped routers. Handlers enforce auth explicitly with per-endpoint scopes.
public viewRouter = new plugins.typedrequest.TypedRouter<{ request: { identity: interfaces.data.IIdentity } }>(); public viewRouter = new plugins.typedrequest.TypedRouter<{ request: { identity?: interfaces.data.IIdentity; apiToken?: string } }>();
public adminRouter = new plugins.typedrequest.TypedRouter<{ request: { identity: interfaces.data.IIdentity } }>(); public adminRouter = new plugins.typedrequest.TypedRouter<{ request: { identity?: interfaces.data.IIdentity; apiToken?: string } }>();
// Handler instances // Handler instances
public adminHandler!: handlers.AdminHandler; public adminHandler!: handlers.AdminHandler;
@@ -72,16 +71,6 @@ export class OpsServer {
this.adminHandler = new handlers.AdminHandler(this); this.adminHandler = new handlers.AdminHandler(this);
await this.adminHandler.initialize(); await this.adminHandler.initialize();
// viewRouter middleware: requires valid identity (any logged-in user)
this.viewRouter.addMiddleware(async (typedRequest) => {
await requireValidIdentity(this.adminHandler, typedRequest.request);
});
// adminRouter middleware: requires admin identity
this.adminRouter.addMiddleware(async (typedRequest) => {
await requireAdminIdentity(this.adminHandler, typedRequest.request);
});
// Connect auth routers to the main typedrouter // Connect auth routers to the main typedrouter
this.typedrouter.addTypedRouter(this.viewRouter); this.typedrouter.addTypedRouter(this.viewRouter);
this.typedrouter.addTypedRouter(this.adminRouter); this.typedrouter.addTypedRouter(this.adminRouter);
+6 -23
View File
@@ -1,6 +1,7 @@
import * as plugins from '../../plugins.js'; import * as plugins from '../../plugins.js';
import type { OpsServer } from '../classes.opsserver.js'; import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js'; import * as interfaces from '../../../ts_interfaces/index.js';
import { requireOpsAuth } from '../helpers/auth.js';
/** /**
* CRUD handler for the singleton `AcmeConfigDoc`. * CRUD handler for the singleton `AcmeConfigDoc`.
@@ -20,29 +21,11 @@ export class AcmeConfigHandler {
request: { identity?: interfaces.data.IIdentity; apiToken?: string }, request: { identity?: interfaces.data.IIdentity; apiToken?: string },
requiredScope?: interfaces.data.TApiTokenScope, requiredScope?: interfaces.data.TApiTokenScope,
): Promise<string> { ): Promise<string> {
if (request.identity?.jwt) { const auth = await requireOpsAuth(this.opsServerRef, request, {
try { scope: requiredScope,
const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({ requireAdminIdentity: requiredScope?.endsWith(':write'),
identity: request.identity, });
}); return auth.userId;
if (isAdmin) return request.identity.userId;
} catch { /* fall through */ }
}
if (request.apiToken) {
const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
if (tokenManager) {
const token = await tokenManager.validateToken(request.apiToken);
if (token) {
if (!requiredScope || tokenManager.hasScope(token, requiredScope)) {
return token.createdBy;
}
throw new plugins.typedrequest.TypedResponseError('insufficient scope');
}
}
}
throw new plugins.typedrequest.TypedResponseError('unauthorized');
} }
private registerHandlers(): void { private registerHandlers(): void {
+64 -101
View File
@@ -258,12 +258,18 @@ export class AdminHandler {
this.opsServerRef.adminRouter.addTypedHandler( this.opsServerRef.adminRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateInitialAdminUser>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateInitialAdminUser>(
'createInitialAdminUser', 'createInitialAdminUser',
async (dataArg) => this.createInitialAdminUser({ async (dataArg) => {
email: dataArg.email, const isAdmin = await this.adminIdentityGuard.exec({ identity: dataArg.identity });
name: dataArg.name, if (!isAdmin) {
password: dataArg.password, throw new plugins.typedrequest.TypedResponseError('admin identity required');
enableIdpGlobalAuth: dataArg.enableIdpGlobalAuth, }
}) return this.createInitialAdminUser({
email: dataArg.email,
name: dataArg.name,
password: dataArg.password,
enableIdpGlobalAuth: dataArg.enableIdpGlobalAuth,
});
}
) )
); );
@@ -300,8 +306,10 @@ export class AdminHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_AdminLogout>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_AdminLogout>(
'adminLogout', 'adminLogout',
async (dataArg) => { async (dataArg) => {
// In a real implementation, you might want to blacklist the JWT const identity = await this.validateIdentity(dataArg.identity);
// For now, just return success if (!identity) {
throw new plugins.typedrequest.TypedResponseError('identity is not valid');
}
return { return {
success: true, success: true,
}; };
@@ -314,52 +322,8 @@ export class AdminHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_VerifyIdentity>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_VerifyIdentity>(
'verifyIdentity', 'verifyIdentity',
async (dataArg) => { async (dataArg) => {
if (!dataArg.identity?.jwt) { const identity = await this.validateIdentity(dataArg.identity);
return { return identity ? { valid: true, identity } : { valid: false };
valid: false,
};
}
try {
const jwtData = await this.smartjwtInstance.verifyJWTAndGetData(dataArg.identity.jwt);
// Check if expired
if (jwtData.expiresAt < Date.now()) {
return {
valid: false,
};
}
// Check if logged in
if (jwtData.status !== 'loggedIn') {
return {
valid: false,
};
}
const user = await this.resolveUser(jwtData.userId);
if (!user) {
return {
valid: false,
};
}
return {
valid: true,
identity: {
jwt: dataArg.identity.jwt,
userId: user.id,
name: user.name || user.username,
expiresAt: jwtData.expiresAt,
role: user.role,
type: 'user',
},
};
} catch (error) {
return {
valid: false,
};
}
} }
) )
); );
@@ -372,45 +336,7 @@ export class AdminHandler {
identity: interfaces.data.IIdentity; identity: interfaces.data.IIdentity;
}>( }>(
async (dataArg) => { async (dataArg) => {
if (!dataArg.identity?.jwt) { return Boolean(await this.validateIdentity(dataArg.identity));
return false;
}
try {
const jwtData = await this.smartjwtInstance.verifyJWTAndGetData(dataArg.identity.jwt);
// Check expiration
if (jwtData.expiresAt < Date.now()) {
return false;
}
// Check status
if (jwtData.status !== 'loggedIn') {
return false;
}
// Verify data hasn't been tampered with
if (dataArg.identity.expiresAt !== jwtData.expiresAt) {
return false;
}
if (dataArg.identity.userId !== jwtData.userId) {
return false;
}
const user = await this.resolveUser(jwtData.userId);
if (!user) {
return false;
}
if (dataArg.identity.role && dataArg.identity.role !== user.role) {
return false;
}
return true;
} catch (error) {
return false;
}
}, },
{ {
failedHint: 'identity is not valid', failedHint: 'identity is not valid',
@@ -425,14 +351,8 @@ export class AdminHandler {
identity: interfaces.data.IIdentity; identity: interfaces.data.IIdentity;
}>( }>(
async (dataArg) => { async (dataArg) => {
// First check if identity is valid const identity = await this.validateIdentity(dataArg.identity);
const isValid = await this.validIdentityGuard.exec(dataArg); return identity?.role === 'admin';
if (!isValid) {
return false;
}
// Check if user has admin role
return dataArg.identity.role === 'admin';
}, },
{ {
failedHint: 'user is not admin', failedHint: 'user is not admin',
@@ -440,6 +360,49 @@ export class AdminHandler {
} }
); );
public async validateIdentity(
identityArg?: interfaces.data.IIdentity,
): Promise<interfaces.data.IIdentity | null> {
if (!identityArg?.jwt) {
return null;
}
try {
const jwtData = await this.smartjwtInstance.verifyJWTAndGetData(identityArg.jwt);
if (jwtData.expiresAt < Date.now()) {
return null;
}
if (jwtData.status !== 'loggedIn') {
return null;
}
if (identityArg.expiresAt !== jwtData.expiresAt) {
return null;
}
if (identityArg.userId !== jwtData.userId) {
return null;
}
const user = await this.resolveUser(jwtData.userId);
if (!user) {
return null;
}
if (identityArg.role && identityArg.role !== user.role) {
return null;
}
return {
jwt: identityArg.jwt,
userId: user.id,
name: user.name || user.username,
expiresAt: jwtData.expiresAt,
role: user.role,
type: 'user',
};
} catch {
return null;
}
}
private async authenticateUser(optionsArg: { private async authenticateUser(optionsArg: {
username: string; username: string;
password: string; password: string;
+27 -1
View File
@@ -1,6 +1,7 @@
import * as plugins from '../../plugins.js'; import * as plugins from '../../plugins.js';
import type { OpsServer } from '../classes.opsserver.js'; import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js'; import * as interfaces from '../../../ts_interfaces/index.js';
import { requireOpsAuth } from '../helpers/auth.js';
export class ApiTokenHandler { export class ApiTokenHandler {
constructor(private opsServerRef: OpsServer) { constructor(private opsServerRef: OpsServer) {
@@ -17,6 +18,11 @@ export class ApiTokenHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateApiToken>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateApiToken>(
'createApiToken', 'createApiToken',
async (dataArg) => { async (dataArg) => {
const auth = await requireOpsAuth(this.opsServerRef, dataArg, {
scope: 'tokens:manage',
requireAdminIdentity: true,
requireAdminToken: true,
});
const manager = this.opsServerRef.dcRouterRef.apiTokenManager; const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
if (!manager) { if (!manager) {
return { success: false, message: 'Token management not initialized' }; return { success: false, message: 'Token management not initialized' };
@@ -25,7 +31,7 @@ export class ApiTokenHandler {
dataArg.name, dataArg.name,
dataArg.scopes, dataArg.scopes,
dataArg.expiresInDays ?? null, dataArg.expiresInDays ?? null,
dataArg.identity.userId, auth.userId,
dataArg.policy, dataArg.policy,
); );
return { success: true, tokenId: result.id, tokenValue: result.rawToken }; return { success: true, tokenId: result.id, tokenValue: result.rawToken };
@@ -38,6 +44,11 @@ export class ApiTokenHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListApiTokens>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListApiTokens>(
'listApiTokens', 'listApiTokens',
async (dataArg) => { async (dataArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, {
scope: 'tokens:read',
requireAdminIdentity: true,
requireAdminToken: true,
});
const manager = this.opsServerRef.dcRouterRef.apiTokenManager; const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
if (!manager) { if (!manager) {
return { tokens: [] }; return { tokens: [] };
@@ -52,6 +63,11 @@ export class ApiTokenHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RevokeApiToken>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RevokeApiToken>(
'revokeApiToken', 'revokeApiToken',
async (dataArg) => { async (dataArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, {
scope: 'tokens:manage',
requireAdminIdentity: true,
requireAdminToken: true,
});
const manager = this.opsServerRef.dcRouterRef.apiTokenManager; const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
if (!manager) { if (!manager) {
return { success: false, message: 'Token management not initialized' }; return { success: false, message: 'Token management not initialized' };
@@ -67,6 +83,11 @@ export class ApiTokenHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RollApiToken>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RollApiToken>(
'rollApiToken', 'rollApiToken',
async (dataArg) => { async (dataArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, {
scope: 'tokens:manage',
requireAdminIdentity: true,
requireAdminToken: true,
});
const manager = this.opsServerRef.dcRouterRef.apiTokenManager; const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
if (!manager) { if (!manager) {
return { success: false, message: 'Token management not initialized' }; return { success: false, message: 'Token management not initialized' };
@@ -85,6 +106,11 @@ export class ApiTokenHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ToggleApiToken>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ToggleApiToken>(
'toggleApiToken', 'toggleApiToken',
async (dataArg) => { async (dataArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, {
scope: 'tokens:manage',
requireAdminIdentity: true,
requireAdminToken: true,
});
const manager = this.opsServerRef.dcRouterRef.apiTokenManager; const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
if (!manager) { if (!manager) {
return { success: false, message: 'Token management not initialized' }; return { success: false, message: 'Token management not initialized' };
+6 -23
View File
@@ -3,6 +3,7 @@ import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js'; import * as interfaces from '../../../ts_interfaces/index.js';
import { AcmeCertDoc, ProxyCertDoc } from '../../db/index.js'; import { AcmeCertDoc, ProxyCertDoc } from '../../db/index.js';
import { logger } from '../../logger.js'; import { logger } from '../../logger.js';
import { requireOpsAuth } from '../helpers/auth.js';
/** /**
* Mirrors `SmartacmeCertMatcher.getCertificateDomainNameByDomainName` from * Mirrors `SmartacmeCertMatcher.getCertificateDomainNameByDomainName` from
@@ -37,29 +38,11 @@ export class CertificateHandler {
request: { identity?: interfaces.data.IIdentity; apiToken?: string }, request: { identity?: interfaces.data.IIdentity; apiToken?: string },
requiredScope?: interfaces.data.TApiTokenScope, requiredScope?: interfaces.data.TApiTokenScope,
): Promise<string> { ): Promise<string> {
if (request.identity?.jwt) { const auth = await requireOpsAuth(this.opsServerRef, request, {
try { scope: requiredScope,
const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({ requireAdminIdentity: requiredScope?.endsWith(':write'),
identity: request.identity, });
}); return auth.userId;
if (isAdmin) return request.identity.userId;
} catch { /* fall through */ }
}
if (request.apiToken) {
const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
if (tokenManager) {
const token = await tokenManager.validateToken(request.apiToken);
if (token) {
if (!requiredScope || tokenManager.hasScope(token, requiredScope)) {
return token.createdBy;
}
throw new plugins.typedrequest.TypedResponseError('insufficient scope');
}
}
}
throw new plugins.typedrequest.TypedResponseError('unauthorized');
} }
private registerHandlers(): void { private registerHandlers(): void {
+2
View File
@@ -2,6 +2,7 @@ import * as plugins from '../../plugins.js';
import * as paths from '../../paths.js'; import * as paths from '../../paths.js';
import type { OpsServer } from '../classes.opsserver.js'; import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js'; import * as interfaces from '../../../ts_interfaces/index.js';
import { requireOpsAuth } from '../helpers/auth.js';
export class ConfigHandler { export class ConfigHandler {
constructor(private opsServerRef: OpsServer) { constructor(private opsServerRef: OpsServer) {
@@ -17,6 +18,7 @@ export class ConfigHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetConfiguration>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetConfiguration>(
'getConfiguration', 'getConfiguration',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'config:read' });
const config = await this.getConfiguration(); const config = await this.getConfiguration();
return { return {
config, config,
+6 -23
View File
@@ -1,6 +1,7 @@
import * as plugins from '../../plugins.js'; import * as plugins from '../../plugins.js';
import type { OpsServer } from '../classes.opsserver.js'; import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js'; import * as interfaces from '../../../ts_interfaces/index.js';
import { requireOpsAuth } from '../helpers/auth.js';
/** /**
* CRUD + connection-test handlers for DnsProviderDoc. * CRUD + connection-test handlers for DnsProviderDoc.
@@ -20,29 +21,11 @@ export class DnsProviderHandler {
request: { identity?: interfaces.data.IIdentity; apiToken?: string }, request: { identity?: interfaces.data.IIdentity; apiToken?: string },
requiredScope?: interfaces.data.TApiTokenScope, requiredScope?: interfaces.data.TApiTokenScope,
): Promise<string> { ): Promise<string> {
if (request.identity?.jwt) { const auth = await requireOpsAuth(this.opsServerRef, request, {
try { scope: requiredScope,
const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({ requireAdminIdentity: requiredScope?.endsWith(':write'),
identity: request.identity, });
}); return auth.userId;
if (isAdmin) return request.identity.userId;
} catch { /* fall through */ }
}
if (request.apiToken) {
const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
if (tokenManager) {
const token = await tokenManager.validateToken(request.apiToken);
if (token) {
if (!requiredScope || tokenManager.hasScope(token, requiredScope)) {
return token.createdBy;
}
throw new plugins.typedrequest.TypedResponseError('insufficient scope');
}
}
}
throw new plugins.typedrequest.TypedResponseError('unauthorized');
} }
private registerHandlers(): void { private registerHandlers(): void {
+6 -23
View File
@@ -1,6 +1,7 @@
import * as plugins from '../../plugins.js'; import * as plugins from '../../plugins.js';
import type { OpsServer } from '../classes.opsserver.js'; import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js'; import * as interfaces from '../../../ts_interfaces/index.js';
import { requireOpsAuth } from '../helpers/auth.js';
/** /**
* CRUD handlers for DnsRecordDoc. * CRUD handlers for DnsRecordDoc.
@@ -17,29 +18,11 @@ export class DnsRecordHandler {
request: { identity?: interfaces.data.IIdentity; apiToken?: string }, request: { identity?: interfaces.data.IIdentity; apiToken?: string },
requiredScope?: interfaces.data.TApiTokenScope, requiredScope?: interfaces.data.TApiTokenScope,
): Promise<string> { ): Promise<string> {
if (request.identity?.jwt) { const auth = await requireOpsAuth(this.opsServerRef, request, {
try { scope: requiredScope,
const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({ requireAdminIdentity: requiredScope?.endsWith(':write'),
identity: request.identity, });
}); return auth.userId;
if (isAdmin) return request.identity.userId;
} catch { /* fall through */ }
}
if (request.apiToken) {
const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
if (tokenManager) {
const token = await tokenManager.validateToken(request.apiToken);
if (token) {
if (!requiredScope || tokenManager.hasScope(token, requiredScope)) {
return token.createdBy;
}
throw new plugins.typedrequest.TypedResponseError('insufficient scope');
}
}
}
throw new plugins.typedrequest.TypedResponseError('unauthorized');
} }
private registerHandlers(): void { private registerHandlers(): void {
+6 -23
View File
@@ -1,6 +1,7 @@
import * as plugins from '../../plugins.js'; import * as plugins from '../../plugins.js';
import type { OpsServer } from '../classes.opsserver.js'; import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js'; import * as interfaces from '../../../ts_interfaces/index.js';
import { requireOpsAuth } from '../helpers/auth.js';
/** /**
* CRUD handlers for DomainDoc. * CRUD handlers for DomainDoc.
@@ -17,29 +18,11 @@ export class DomainHandler {
request: { identity?: interfaces.data.IIdentity; apiToken?: string }, request: { identity?: interfaces.data.IIdentity; apiToken?: string },
requiredScope?: interfaces.data.TApiTokenScope, requiredScope?: interfaces.data.TApiTokenScope,
): Promise<string> { ): Promise<string> {
if (request.identity?.jwt) { const auth = await requireOpsAuth(this.opsServerRef, request, {
try { scope: requiredScope,
const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({ requireAdminIdentity: requiredScope?.endsWith(':write'),
identity: request.identity, });
}); return auth.userId;
if (isAdmin) return request.identity.userId;
} catch { /* fall through */ }
}
if (request.apiToken) {
const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
if (tokenManager) {
const token = await tokenManager.validateToken(request.apiToken);
if (token) {
if (!requiredScope || tokenManager.hasScope(token, requiredScope)) {
return token.createdBy;
}
throw new plugins.typedrequest.TypedResponseError('insufficient scope');
}
}
}
throw new plugins.typedrequest.TypedResponseError('unauthorized');
} }
private registerHandlers(): void { private registerHandlers(): void {
+6 -23
View File
@@ -1,6 +1,7 @@
import * as plugins from '../../plugins.js'; import * as plugins from '../../plugins.js';
import type { OpsServer } from '../classes.opsserver.js'; import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js'; import * as interfaces from '../../../ts_interfaces/index.js';
import { requireOpsAuth } from '../helpers/auth.js';
/** /**
* CRUD + DNS provisioning handler for email domains. * CRUD + DNS provisioning handler for email domains.
@@ -19,29 +20,11 @@ export class EmailDomainHandler {
request: { identity?: interfaces.data.IIdentity; apiToken?: string }, request: { identity?: interfaces.data.IIdentity; apiToken?: string },
requiredScope?: interfaces.data.TApiTokenScope, requiredScope?: interfaces.data.TApiTokenScope,
): Promise<string> { ): Promise<string> {
if (request.identity?.jwt) { const auth = await requireOpsAuth(this.opsServerRef, request, {
try { scope: requiredScope,
const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({ requireAdminIdentity: requiredScope?.endsWith(':write'),
identity: request.identity, });
}); return auth.userId;
if (isAdmin) return request.identity.userId;
} catch { /* fall through */ }
}
if (request.apiToken) {
const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
if (tokenManager) {
const token = await tokenManager.validateToken(request.apiToken);
if (token) {
if (!requiredScope || tokenManager.hasScope(token, requiredScope)) {
return token.createdBy;
}
throw new plugins.typedrequest.TypedResponseError('insufficient scope');
}
}
}
throw new plugins.typedrequest.TypedResponseError('unauthorized');
} }
private get manager() { private get manager() {
@@ -1,6 +1,7 @@
import * as plugins from '../../plugins.js'; import * as plugins from '../../plugins.js';
import type { OpsServer } from '../classes.opsserver.js'; import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js'; import * as interfaces from '../../../ts_interfaces/index.js';
import { requireOpsAuth } from '../helpers/auth.js';
export class EmailOpsHandler { export class EmailOpsHandler {
constructor(private opsServerRef: OpsServer) { constructor(private opsServerRef: OpsServer) {
@@ -18,6 +19,7 @@ export class EmailOpsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetAllEmails>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetAllEmails>(
'getAllEmails', 'getAllEmails',
async (dataArg) => { async (dataArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'emails:read' });
const emails = this.getAllQueueEmails(); const emails = this.getAllQueueEmails();
return { emails }; return { emails };
} }
@@ -29,6 +31,7 @@ export class EmailOpsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetEmailDetail>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetEmailDetail>(
'getEmailDetail', 'getEmailDetail',
async (dataArg) => { async (dataArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'emails:read' });
const email = this.getEmailDetail(dataArg.emailId); const email = this.getEmailDetail(dataArg.emailId);
return { email }; return { email };
} }
@@ -42,6 +45,10 @@ export class EmailOpsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ResendEmail>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ResendEmail>(
'resendEmail', 'resendEmail',
async (dataArg) => { async (dataArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, {
scope: 'emails:write',
requireAdminIdentity: true,
});
const emailServer = this.opsServerRef.dcRouterRef.emailServer; const emailServer = this.opsServerRef.dcRouterRef.emailServer;
if (!emailServer?.deliveryQueue) { if (!emailServer?.deliveryQueue) {
return { success: false, error: 'Email server not available' }; return { success: false, error: 'Email server not available' };
+3
View File
@@ -2,6 +2,7 @@ import * as plugins from '../../plugins.js';
import type { OpsServer } from '../classes.opsserver.js'; import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js'; import * as interfaces from '../../../ts_interfaces/index.js';
import { logBuffer, baseLogger } from '../../logger.js'; import { logBuffer, baseLogger } from '../../logger.js';
import { requireOpsAuth } from '../helpers/auth.js';
// Module-level singleton: the log push destination is added once and reuses // Module-level singleton: the log push destination is added once and reuses
// the current OpsServer reference so it survives OpsServer restarts without // the current OpsServer reference so it survives OpsServer restarts without
@@ -40,6 +41,7 @@ export class LogsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRecentLogs>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRecentLogs>(
'getRecentLogs', 'getRecentLogs',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'logs:read' });
const logs = await this.getRecentLogs( const logs = await this.getRecentLogs(
dataArg.level, dataArg.level,
dataArg.category, dataArg.category,
@@ -63,6 +65,7 @@ export class LogsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetLogStream>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetLogStream>(
'getLogStream', 'getLogStream',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'logs:read' });
// Create a virtual stream for log streaming // Create a virtual stream for log streaming
const virtualStream = new plugins.typedrequest.VirtualStream<Uint8Array>(); const virtualStream = new plugins.typedrequest.VirtualStream<Uint8Array>();
@@ -1,6 +1,7 @@
import * as plugins from '../../plugins.js'; import * as plugins from '../../plugins.js';
import type { OpsServer } from '../classes.opsserver.js'; import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js'; import * as interfaces from '../../../ts_interfaces/index.js';
import { requireOpsAuth } from '../helpers/auth.js';
export class NetworkTargetHandler { export class NetworkTargetHandler {
public typedrouter = new plugins.typedrequest.TypedRouter(); public typedrouter = new plugins.typedrequest.TypedRouter();
@@ -14,29 +15,11 @@ export class NetworkTargetHandler {
request: { identity?: interfaces.data.IIdentity; apiToken?: string }, request: { identity?: interfaces.data.IIdentity; apiToken?: string },
requiredScope?: interfaces.data.TApiTokenScope, requiredScope?: interfaces.data.TApiTokenScope,
): Promise<string> { ): Promise<string> {
if (request.identity?.jwt) { const auth = await requireOpsAuth(this.opsServerRef, request, {
try { scope: requiredScope,
const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({ requireAdminIdentity: requiredScope?.endsWith(':write'),
identity: request.identity, });
}); return auth.userId;
if (isAdmin) return request.identity.userId;
} catch { /* fall through */ }
}
if (request.apiToken) {
const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
if (tokenManager) {
const token = await tokenManager.validateToken(request.apiToken);
if (token) {
if (!requiredScope || tokenManager.hasScope(token, requiredScope)) {
return token.createdBy;
}
throw new plugins.typedrequest.TypedResponseError('insufficient scope');
}
}
}
throw new plugins.typedrequest.TypedResponseError('unauthorized');
} }
private registerHandlers(): void { private registerHandlers(): void {
+31
View File
@@ -1,6 +1,7 @@
import * as plugins from '../../plugins.js'; import * as plugins from '../../plugins.js';
import type { OpsServer } from '../classes.opsserver.js'; import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js'; import * as interfaces from '../../../ts_interfaces/index.js';
import { requireOpsAuth } from '../helpers/auth.js';
export class RadiusHandler { export class RadiusHandler {
constructor(private opsServerRef: OpsServer) { constructor(private opsServerRef: OpsServer) {
@@ -19,6 +20,7 @@ export class RadiusHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRadiusClients>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRadiusClients>(
'getRadiusClients', 'getRadiusClients',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'radius:read' });
const radiusServer = this.opsServerRef.dcRouterRef.radiusServer; const radiusServer = this.opsServerRef.dcRouterRef.radiusServer;
if (!radiusServer) { if (!radiusServer) {
@@ -43,6 +45,10 @@ export class RadiusHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SetRadiusClient>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SetRadiusClient>(
'setRadiusClient', 'setRadiusClient',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, {
scope: 'radius:write',
requireAdminIdentity: true,
});
const radiusServer = this.opsServerRef.dcRouterRef.radiusServer; const radiusServer = this.opsServerRef.dcRouterRef.radiusServer;
if (!radiusServer) { if (!radiusServer) {
@@ -64,6 +70,10 @@ export class RadiusHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RemoveRadiusClient>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RemoveRadiusClient>(
'removeRadiusClient', 'removeRadiusClient',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, {
scope: 'radius:write',
requireAdminIdentity: true,
});
const radiusServer = this.opsServerRef.dcRouterRef.radiusServer; const radiusServer = this.opsServerRef.dcRouterRef.radiusServer;
if (!radiusServer) { if (!radiusServer) {
@@ -88,6 +98,7 @@ export class RadiusHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetVlanMappings>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetVlanMappings>(
'getVlanMappings', 'getVlanMappings',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'radius:read' });
const radiusServer = this.opsServerRef.dcRouterRef.radiusServer; const radiusServer = this.opsServerRef.dcRouterRef.radiusServer;
if (!radiusServer) { if (!radiusServer) {
@@ -124,6 +135,10 @@ export class RadiusHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SetVlanMapping>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SetVlanMapping>(
'setVlanMapping', 'setVlanMapping',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, {
scope: 'radius:write',
requireAdminIdentity: true,
});
const radiusServer = this.opsServerRef.dcRouterRef.radiusServer; const radiusServer = this.opsServerRef.dcRouterRef.radiusServer;
if (!radiusServer) { if (!radiusServer) {
@@ -156,6 +171,10 @@ export class RadiusHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RemoveVlanMapping>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RemoveVlanMapping>(
'removeVlanMapping', 'removeVlanMapping',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, {
scope: 'radius:write',
requireAdminIdentity: true,
});
const radiusServer = this.opsServerRef.dcRouterRef.radiusServer; const radiusServer = this.opsServerRef.dcRouterRef.radiusServer;
if (!radiusServer) { if (!radiusServer) {
@@ -177,6 +196,10 @@ export class RadiusHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateVlanConfig>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateVlanConfig>(
'updateVlanConfig', 'updateVlanConfig',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, {
scope: 'radius:write',
requireAdminIdentity: true,
});
const radiusServer = this.opsServerRef.dcRouterRef.radiusServer; const radiusServer = this.opsServerRef.dcRouterRef.radiusServer;
if (!radiusServer) { if (!radiusServer) {
@@ -209,6 +232,7 @@ export class RadiusHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_TestVlanAssignment>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_TestVlanAssignment>(
'testVlanAssignment', 'testVlanAssignment',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'radius:read' });
const radiusServer = this.opsServerRef.dcRouterRef.radiusServer; const radiusServer = this.opsServerRef.dcRouterRef.radiusServer;
if (!radiusServer) { if (!radiusServer) {
@@ -243,6 +267,7 @@ export class RadiusHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRadiusSessions>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRadiusSessions>(
'getRadiusSessions', 'getRadiusSessions',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'radius:read' });
const radiusServer = this.opsServerRef.dcRouterRef.radiusServer; const radiusServer = this.opsServerRef.dcRouterRef.radiusServer;
if (!radiusServer) { if (!radiusServer) {
@@ -292,6 +317,10 @@ export class RadiusHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DisconnectRadiusSession>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DisconnectRadiusSession>(
'disconnectRadiusSession', 'disconnectRadiusSession',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, {
scope: 'radius:write',
requireAdminIdentity: true,
});
const radiusServer = this.opsServerRef.dcRouterRef.radiusServer; const radiusServer = this.opsServerRef.dcRouterRef.radiusServer;
if (!radiusServer) { if (!radiusServer) {
@@ -317,6 +346,7 @@ export class RadiusHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRadiusAccountingSummary>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRadiusAccountingSummary>(
'getRadiusAccountingSummary', 'getRadiusAccountingSummary',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'radius:read' });
const radiusServer = this.opsServerRef.dcRouterRef.radiusServer; const radiusServer = this.opsServerRef.dcRouterRef.radiusServer;
if (!radiusServer) { if (!radiusServer) {
@@ -354,6 +384,7 @@ export class RadiusHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRadiusStatistics>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRadiusStatistics>(
'getRadiusStatistics', 'getRadiusStatistics',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'radius:read' });
const radiusServer = this.opsServerRef.dcRouterRef.radiusServer; const radiusServer = this.opsServerRef.dcRouterRef.radiusServer;
if (!radiusServer) { if (!radiusServer) {
@@ -1,6 +1,7 @@
import * as plugins from '../../plugins.js'; import * as plugins from '../../plugins.js';
import type { OpsServer } from '../classes.opsserver.js'; import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js'; import * as interfaces from '../../../ts_interfaces/index.js';
import { requireOpsAuth } from '../helpers/auth.js';
export class RemoteIngressHandler { export class RemoteIngressHandler {
constructor(private opsServerRef: OpsServer) { constructor(private opsServerRef: OpsServer) {
@@ -18,6 +19,7 @@ export class RemoteIngressHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngresses>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngresses>(
'getRemoteIngresses', 'getRemoteIngresses',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'remote-ingress:read' });
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager; const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
if (!manager) { if (!manager) {
return { edges: [] }; return { edges: [] };
@@ -46,6 +48,10 @@ export class RemoteIngressHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateRemoteIngress>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateRemoteIngress>(
'createRemoteIngress', 'createRemoteIngress',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, {
scope: 'remote-ingress:write',
requireAdminIdentity: true,
});
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager; const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager; const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
@@ -78,6 +84,10 @@ export class RemoteIngressHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteRemoteIngress>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteRemoteIngress>(
'deleteRemoteIngress', 'deleteRemoteIngress',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, {
scope: 'remote-ingress:write',
requireAdminIdentity: true,
});
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager; const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager; const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
@@ -103,6 +113,10 @@ export class RemoteIngressHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateRemoteIngress>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateRemoteIngress>(
'updateRemoteIngress', 'updateRemoteIngress',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, {
scope: 'remote-ingress:write',
requireAdminIdentity: true,
});
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager; const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager; const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
@@ -148,6 +162,10 @@ export class RemoteIngressHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RegenerateRemoteIngressSecret>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RegenerateRemoteIngressSecret>(
'regenerateRemoteIngressSecret', 'regenerateRemoteIngressSecret',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, {
scope: 'remote-ingress:write',
requireAdminIdentity: true,
});
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager; const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager; const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
@@ -175,6 +193,7 @@ export class RemoteIngressHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngressStatus>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngressStatus>(
'getRemoteIngressStatus', 'getRemoteIngressStatus',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'remote-ingress:read' });
const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager; const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
if (!tunnelManager) { if (!tunnelManager) {
return { statuses: [] }; return { statuses: [] };
@@ -189,6 +208,10 @@ export class RemoteIngressHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngressConnectionToken>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngressConnectionToken>(
'getRemoteIngressConnectionToken', 'getRemoteIngressConnectionToken',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, {
scope: 'remote-ingress:write',
requireAdminIdentity: true,
});
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager; const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
if (!manager) { if (!manager) {
return { success: false, message: 'RemoteIngress not configured' }; return { success: false, message: 'RemoteIngress not configured' };
@@ -1,6 +1,7 @@
import * as plugins from '../../plugins.js'; import * as plugins from '../../plugins.js';
import type { OpsServer } from '../classes.opsserver.js'; import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js'; import * as interfaces from '../../../ts_interfaces/index.js';
import { requireOpsAuth } from '../helpers/auth.js';
export class RouteManagementHandler { export class RouteManagementHandler {
public typedrouter = new plugins.typedrequest.TypedRouter(); public typedrouter = new plugins.typedrequest.TypedRouter();
@@ -18,31 +19,11 @@ export class RouteManagementHandler {
request: { identity?: interfaces.data.IIdentity; apiToken?: string }, request: { identity?: interfaces.data.IIdentity; apiToken?: string },
requiredScope?: interfaces.data.TApiTokenScope, requiredScope?: interfaces.data.TApiTokenScope,
): Promise<string> { ): Promise<string> {
// Try JWT identity first const auth = await requireOpsAuth(this.opsServerRef, request, {
if (request.identity?.jwt) { scope: requiredScope,
try { requireAdminIdentity: requiredScope?.endsWith(':write'),
const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({ });
identity: request.identity, return auth.userId;
});
if (isAdmin) return request.identity.userId;
} catch { /* fall through */ }
}
// Try API token
if (request.apiToken) {
const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
if (tokenManager) {
const token = await tokenManager.validateToken(request.apiToken);
if (token) {
if (!requiredScope || tokenManager.hasScope(token, requiredScope)) {
return token.createdBy;
}
throw new plugins.typedrequest.TypedResponseError('insufficient scope');
}
}
}
throw new plugins.typedrequest.TypedResponseError('unauthorized');
} }
private registerHandlers(): void { private registerHandlers(): void {
+31 -6
View File
@@ -2,6 +2,7 @@ import * as plugins from '../../plugins.js';
import type { OpsServer } from '../classes.opsserver.js'; import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js'; import * as interfaces from '../../../ts_interfaces/index.js';
import { MetricsManager } from '../../monitoring/index.js'; import { MetricsManager } from '../../monitoring/index.js';
import { requireOpsAuth } from '../helpers/auth.js';
export class SecurityHandler { export class SecurityHandler {
constructor(private opsServerRef: OpsServer) { constructor(private opsServerRef: OpsServer) {
@@ -17,6 +18,7 @@ export class SecurityHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetSecurityMetrics>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetSecurityMetrics>(
'getSecurityMetrics', 'getSecurityMetrics',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'security:read' });
const metrics = await this.collectSecurityMetrics(); const metrics = await this.collectSecurityMetrics();
return { return {
metrics: { metrics: {
@@ -43,6 +45,7 @@ export class SecurityHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetActiveConnections>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetActiveConnections>(
'getActiveConnections', 'getActiveConnections',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'stats:read' });
const connections = await this.getActiveConnections(dataArg.protocol, dataArg.state); const connections = await this.getActiveConnections(dataArg.protocol, dataArg.state);
const connectionInfos: interfaces.data.IConnectionInfo[] = connections.map(conn => ({ const connectionInfos: interfaces.data.IConnectionInfo[] = connections.map(conn => ({
id: conn.id, id: conn.id,
@@ -82,6 +85,7 @@ export class SecurityHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetNetworkStats>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetNetworkStats>(
'getNetworkStats', 'getNetworkStats',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'stats:read' });
// Get network stats from MetricsManager if available // Get network stats from MetricsManager if available
if (this.opsServerRef.dcRouterRef.metricsManager) { if (this.opsServerRef.dcRouterRef.metricsManager) {
const networkStats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats(); const networkStats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats();
@@ -136,6 +140,7 @@ export class SecurityHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRateLimitStatus>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRateLimitStatus>(
'getRateLimitStatus', 'getRateLimitStatus',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'security:read' });
const status = await this.getRateLimitStatus(dataArg.domain, dataArg.ip); const status = await this.getRateLimitStatus(dataArg.domain, dataArg.ip);
const limits: interfaces.data.IRateLimitInfo[] = status.limits.map(limit => ({ const limits: interfaces.data.IRateLimitInfo[] = status.limits.map(limit => ({
domain: limit.identifier, domain: limit.identifier,
@@ -161,7 +166,8 @@ export class SecurityHandler {
router.addTypedHandler( router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListSecurityBlockRules>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListSecurityBlockRules>(
'listSecurityBlockRules', 'listSecurityBlockRules',
async () => { async (dataArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'security:read' });
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager; const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
return { rules: manager ? await manager.listBlockRules() : [] }; return { rules: manager ? await manager.listBlockRules() : [] };
}, },
@@ -171,7 +177,8 @@ export class SecurityHandler {
router.addTypedHandler( router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListIpIntelligence>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListIpIntelligence>(
'listIpIntelligence', 'listIpIntelligence',
async () => { async (dataArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'security:read' });
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager; const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
return { records: manager ? await manager.listIpIntelligence() : [] }; return { records: manager ? await manager.listIpIntelligence() : [] };
}, },
@@ -181,7 +188,8 @@ export class SecurityHandler {
router.addTypedHandler( router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetCompiledSecurityPolicy>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetCompiledSecurityPolicy>(
'getCompiledSecurityPolicy', 'getCompiledSecurityPolicy',
async () => { async (dataArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'security:read' });
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager; const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
return { return {
policy: manager policy: manager
@@ -196,6 +204,7 @@ export class SecurityHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListSecurityPolicyAudit>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListSecurityPolicyAudit>(
'listSecurityPolicyAudit', 'listSecurityPolicyAudit',
async (dataArg) => { async (dataArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'security:read' });
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager; const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
return { events: manager ? await manager.listAuditEvents(dataArg.limit || 100) : [] }; return { events: manager ? await manager.listAuditEvents(dataArg.limit || 100) : [] };
}, },
@@ -208,6 +217,10 @@ export class SecurityHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateSecurityBlockRule>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateSecurityBlockRule>(
'createSecurityBlockRule', 'createSecurityBlockRule',
async (dataArg) => { async (dataArg) => {
const auth = await requireOpsAuth(this.opsServerRef, dataArg, {
scope: 'security:write',
requireAdminIdentity: true,
});
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager; const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
if (!manager) return { success: false, message: 'Security policy manager not initialized' }; if (!manager) return { success: false, message: 'Security policy manager not initialized' };
const rule = await manager.createBlockRule({ const rule = await manager.createBlockRule({
@@ -216,7 +229,7 @@ export class SecurityHandler {
matchMode: dataArg.matchMode, matchMode: dataArg.matchMode,
reason: dataArg.reason, reason: dataArg.reason,
enabled: dataArg.enabled, enabled: dataArg.enabled,
}, dataArg.identity.userId); }, auth.userId);
return { success: true, rule }; return { success: true, rule };
}, },
), ),
@@ -226,6 +239,10 @@ export class SecurityHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateSecurityBlockRule>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateSecurityBlockRule>(
'updateSecurityBlockRule', 'updateSecurityBlockRule',
async (dataArg) => { async (dataArg) => {
const auth = await requireOpsAuth(this.opsServerRef, dataArg, {
scope: 'security:write',
requireAdminIdentity: true,
});
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager; const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
if (!manager) return { success: false, message: 'Security policy manager not initialized' }; if (!manager) return { success: false, message: 'Security policy manager not initialized' };
const rule = await manager.updateBlockRule(dataArg.id, { const rule = await manager.updateBlockRule(dataArg.id, {
@@ -233,7 +250,7 @@ export class SecurityHandler {
matchMode: dataArg.matchMode, matchMode: dataArg.matchMode,
reason: dataArg.reason, reason: dataArg.reason,
enabled: dataArg.enabled, enabled: dataArg.enabled,
}, dataArg.identity.userId); }, auth.userId);
return rule ? { success: true, rule } : { success: false, message: 'Rule not found' }; return rule ? { success: true, rule } : { success: false, message: 'Rule not found' };
}, },
), ),
@@ -243,9 +260,13 @@ export class SecurityHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteSecurityBlockRule>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteSecurityBlockRule>(
'deleteSecurityBlockRule', 'deleteSecurityBlockRule',
async (dataArg) => { async (dataArg) => {
const auth = await requireOpsAuth(this.opsServerRef, dataArg, {
scope: 'security:write',
requireAdminIdentity: true,
});
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager; const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
if (!manager) return { success: false, message: 'Security policy manager not initialized' }; if (!manager) return { success: false, message: 'Security policy manager not initialized' };
const success = await manager.deleteBlockRule(dataArg.id, dataArg.identity.userId); const success = await manager.deleteBlockRule(dataArg.id, auth.userId);
return { success, message: success ? undefined : 'Rule not found' }; return { success, message: success ? undefined : 'Rule not found' };
}, },
), ),
@@ -255,6 +276,10 @@ export class SecurityHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RefreshIpIntelligence>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RefreshIpIntelligence>(
'refreshIpIntelligence', 'refreshIpIntelligence',
async (dataArg) => { async (dataArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, {
scope: 'security:write',
requireAdminIdentity: true,
});
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager; const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
if (!manager) return { success: false, message: 'Security policy manager not initialized' }; if (!manager) return { success: false, message: 'Security policy manager not initialized' };
const record = await manager.refreshIpIntelligence(dataArg.ipAddress); const record = await manager.refreshIpIntelligence(dataArg.ipAddress);
@@ -1,6 +1,7 @@
import * as plugins from '../../plugins.js'; import * as plugins from '../../plugins.js';
import type { OpsServer } from '../classes.opsserver.js'; import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js'; import * as interfaces from '../../../ts_interfaces/index.js';
import { requireOpsAuth } from '../helpers/auth.js';
export class SourceProfileHandler { export class SourceProfileHandler {
public typedrouter = new plugins.typedrequest.TypedRouter(); public typedrouter = new plugins.typedrequest.TypedRouter();
@@ -14,29 +15,11 @@ export class SourceProfileHandler {
request: { identity?: interfaces.data.IIdentity; apiToken?: string }, request: { identity?: interfaces.data.IIdentity; apiToken?: string },
requiredScope?: interfaces.data.TApiTokenScope, requiredScope?: interfaces.data.TApiTokenScope,
): Promise<string> { ): Promise<string> {
if (request.identity?.jwt) { const auth = await requireOpsAuth(this.opsServerRef, request, {
try { scope: requiredScope,
const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({ requireAdminIdentity: requiredScope?.endsWith(':write'),
identity: request.identity, });
}); return auth.userId;
if (isAdmin) return request.identity.userId;
} catch { /* fall through */ }
}
if (request.apiToken) {
const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
if (tokenManager) {
const token = await tokenManager.validateToken(request.apiToken);
if (token) {
if (!requiredScope || tokenManager.hasScope(token, requiredScope)) {
return token.createdBy;
}
throw new plugins.typedrequest.TypedResponseError('insufficient scope');
}
}
}
throw new plugins.typedrequest.TypedResponseError('unauthorized');
} }
private registerHandlers(): void { private registerHandlers(): void {
+7
View File
@@ -4,6 +4,7 @@ import * as interfaces from '../../../ts_interfaces/index.js';
import { MetricsManager } from '../../monitoring/index.js'; import { MetricsManager } from '../../monitoring/index.js';
import { SecurityLogger } from '../../security/classes.securitylogger.js'; import { SecurityLogger } from '../../security/classes.securitylogger.js';
import { commitinfo } from '../../00_commitinfo_data.js'; import { commitinfo } from '../../00_commitinfo_data.js';
import { requireOpsAuth } from '../helpers/auth.js';
export class StatsHandler { export class StatsHandler {
constructor(private opsServerRef: OpsServer) { constructor(private opsServerRef: OpsServer) {
@@ -19,6 +20,7 @@ export class StatsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetServerStatistics>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetServerStatistics>(
'getServerStatistics', 'getServerStatistics',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'stats:read' });
const stats = await this.collectServerStats(); const stats = await this.collectServerStats();
return { return {
stats: { stats: {
@@ -42,6 +44,7 @@ export class StatsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetEmailStatistics>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetEmailStatistics>(
'getEmailStatistics', 'getEmailStatistics',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'stats:read' });
const emailServer = this.opsServerRef.dcRouterRef.emailServer; const emailServer = this.opsServerRef.dcRouterRef.emailServer;
if (!emailServer) { if (!emailServer) {
return { return {
@@ -81,6 +84,7 @@ export class StatsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetDnsStatistics>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetDnsStatistics>(
'getDnsStatistics', 'getDnsStatistics',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'stats:read' });
const dnsServer = this.opsServerRef.dcRouterRef.dnsServer; const dnsServer = this.opsServerRef.dcRouterRef.dnsServer;
if (!dnsServer) { if (!dnsServer) {
return { return {
@@ -118,6 +122,7 @@ export class StatsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetQueueStatus>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetQueueStatus>(
'getQueueStatus', 'getQueueStatus',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'stats:read' });
const emailServer = this.opsServerRef.dcRouterRef.emailServer; const emailServer = this.opsServerRef.dcRouterRef.emailServer;
const queues: interfaces.data.IQueueStatus[] = []; const queues: interfaces.data.IQueueStatus[] = [];
@@ -146,6 +151,7 @@ export class StatsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetHealthStatus>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetHealthStatus>(
'getHealthStatus', 'getHealthStatus',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'stats:read' });
const health = await this.checkHealthStatus(); const health = await this.checkHealthStatus();
return { return {
health: { health: {
@@ -171,6 +177,7 @@ export class StatsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetCombinedMetrics>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetCombinedMetrics>(
'getCombinedMetrics', 'getCombinedMetrics',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'stats:read' });
const sections = dataArg.sections || { const sections = dataArg.sections || {
server: true, server: true,
email: true, email: true,
@@ -1,6 +1,7 @@
import * as plugins from '../../plugins.js'; import * as plugins from '../../plugins.js';
import type { OpsServer } from '../classes.opsserver.js'; import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js'; import * as interfaces from '../../../ts_interfaces/index.js';
import { requireOpsAuth } from '../helpers/auth.js';
export class TargetProfileHandler { export class TargetProfileHandler {
public typedrouter = new plugins.typedrequest.TypedRouter(); public typedrouter = new plugins.typedrequest.TypedRouter();
@@ -14,29 +15,11 @@ export class TargetProfileHandler {
request: { identity?: interfaces.data.IIdentity; apiToken?: string }, request: { identity?: interfaces.data.IIdentity; apiToken?: string },
requiredScope?: interfaces.data.TApiTokenScope, requiredScope?: interfaces.data.TApiTokenScope,
): Promise<string> { ): Promise<string> {
if (request.identity?.jwt) { const auth = await requireOpsAuth(this.opsServerRef, request, {
try { scope: requiredScope,
const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({ requireAdminIdentity: requiredScope?.endsWith(':write'),
identity: request.identity, });
}); return auth.userId;
if (isAdmin) return request.identity.userId;
} catch { /* fall through */ }
}
if (request.apiToken) {
const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
if (tokenManager) {
const token = await tokenManager.validateToken(request.apiToken);
if (token) {
if (!requiredScope || tokenManager.hasScope(token, requiredScope)) {
return token.createdBy;
}
throw new plugins.typedrequest.TypedResponseError('insufficient scope');
}
}
}
throw new plugins.typedrequest.TypedResponseError('unauthorized');
} }
private registerHandlers(): void { private registerHandlers(): void {
+32 -12
View File
@@ -1,6 +1,7 @@
import * as plugins from '../../plugins.js'; import * as plugins from '../../plugins.js';
import type { OpsServer } from '../classes.opsserver.js'; import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js'; import * as interfaces from '../../../ts_interfaces/index.js';
import { requireOpsAuth } from '../helpers/auth.js';
/** /**
* Handler for OpsServer user accounts. Registers on adminRouter, * Handler for OpsServer user accounts. Registers on adminRouter,
@@ -20,7 +21,12 @@ export class UsersHandler {
router.addTypedHandler( router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListUsers>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListUsers>(
'listUsers', 'listUsers',
async (_dataArg) => { async (dataArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, {
scope: 'users:read',
requireAdminIdentity: true,
requireAdminToken: true,
});
const users = await this.opsServerRef.adminHandler.listUsers(); const users = await this.opsServerRef.adminHandler.listUsers();
return { users }; return { users };
}, },
@@ -30,23 +36,37 @@ export class UsersHandler {
router.addTypedHandler( router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateUser>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateUser>(
'createUser', 'createUser',
async (dataArg) => this.opsServerRef.adminHandler.createUser({ async (dataArg) => {
email: dataArg.email, await requireOpsAuth(this.opsServerRef, dataArg, {
name: dataArg.name, scope: 'users:manage',
role: dataArg.role, requireAdminIdentity: true,
password: dataArg.password, requireAdminToken: true,
enableIdpGlobalAuth: dataArg.enableIdpGlobalAuth, });
}), return this.opsServerRef.adminHandler.createUser({
email: dataArg.email,
name: dataArg.name,
role: dataArg.role,
password: dataArg.password,
enableIdpGlobalAuth: dataArg.enableIdpGlobalAuth,
});
},
), ),
); );
router.addTypedHandler( router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteUser>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteUser>(
'deleteUser', 'deleteUser',
async (dataArg) => this.opsServerRef.adminHandler.deleteUser({ async (dataArg) => {
id: dataArg.id, const auth = await requireOpsAuth(this.opsServerRef, dataArg, {
requestingUserId: dataArg.identity.userId, scope: 'users:manage',
}), requireAdminIdentity: true,
requireAdminToken: true,
});
return this.opsServerRef.adminHandler.deleteUser({
id: dataArg.id,
requestingUserId: auth.userId,
});
},
), ),
); );
} }
+33
View File
@@ -1,6 +1,7 @@
import * as plugins from '../../plugins.js'; import * as plugins from '../../plugins.js';
import type { OpsServer } from '../classes.opsserver.js'; import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js'; import * as interfaces from '../../../ts_interfaces/index.js';
import { requireOpsAuth } from '../helpers/auth.js';
export class VpnHandler { export class VpnHandler {
constructor(private opsServerRef: OpsServer) { constructor(private opsServerRef: OpsServer) {
@@ -18,6 +19,7 @@ export class VpnHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetVpnClients>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetVpnClients>(
'getVpnClients', 'getVpnClients',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'vpn:read' });
const manager = this.opsServerRef.dcRouterRef.vpnManager; const manager = this.opsServerRef.dcRouterRef.vpnManager;
if (!manager) { if (!manager) {
return { clients: [] }; return { clients: [] };
@@ -49,6 +51,7 @@ export class VpnHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetVpnStatus>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetVpnStatus>(
'getVpnStatus', 'getVpnStatus',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'vpn:read' });
const manager = this.opsServerRef.dcRouterRef.vpnManager; const manager = this.opsServerRef.dcRouterRef.vpnManager;
const vpnConfig = this.opsServerRef.dcRouterRef.options.vpnConfig; const vpnConfig = this.opsServerRef.dcRouterRef.options.vpnConfig;
if (!manager) { if (!manager) {
@@ -84,6 +87,7 @@ export class VpnHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetVpnConnectedClients>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetVpnConnectedClients>(
'getVpnConnectedClients', 'getVpnConnectedClients',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'vpn:read' });
const manager = this.opsServerRef.dcRouterRef.vpnManager; const manager = this.opsServerRef.dcRouterRef.vpnManager;
if (!manager) { if (!manager) {
return { connectedClients: [] }; return { connectedClients: [] };
@@ -111,6 +115,10 @@ export class VpnHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateVpnClient>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateVpnClient>(
'createVpnClient', 'createVpnClient',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, {
scope: 'vpn:write',
requireAdminIdentity: true,
});
const manager = this.opsServerRef.dcRouterRef.vpnManager; const manager = this.opsServerRef.dcRouterRef.vpnManager;
if (!manager) { if (!manager) {
return { success: false, message: 'VPN not configured' }; return { success: false, message: 'VPN not configured' };
@@ -168,6 +176,10 @@ export class VpnHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateVpnClient>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateVpnClient>(
'updateVpnClient', 'updateVpnClient',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, {
scope: 'vpn:write',
requireAdminIdentity: true,
});
const manager = this.opsServerRef.dcRouterRef.vpnManager; const manager = this.opsServerRef.dcRouterRef.vpnManager;
if (!manager) { if (!manager) {
return { success: false, message: 'VPN not configured' }; return { success: false, message: 'VPN not configured' };
@@ -198,6 +210,10 @@ export class VpnHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteVpnClient>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteVpnClient>(
'deleteVpnClient', 'deleteVpnClient',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, {
scope: 'vpn:write',
requireAdminIdentity: true,
});
const manager = this.opsServerRef.dcRouterRef.vpnManager; const manager = this.opsServerRef.dcRouterRef.vpnManager;
if (!manager) { if (!manager) {
return { success: false, message: 'VPN not configured' }; return { success: false, message: 'VPN not configured' };
@@ -218,6 +234,10 @@ export class VpnHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_EnableVpnClient>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_EnableVpnClient>(
'enableVpnClient', 'enableVpnClient',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, {
scope: 'vpn:write',
requireAdminIdentity: true,
});
const manager = this.opsServerRef.dcRouterRef.vpnManager; const manager = this.opsServerRef.dcRouterRef.vpnManager;
if (!manager) { if (!manager) {
return { success: false, message: 'VPN not configured' }; return { success: false, message: 'VPN not configured' };
@@ -238,6 +258,10 @@ export class VpnHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DisableVpnClient>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DisableVpnClient>(
'disableVpnClient', 'disableVpnClient',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, {
scope: 'vpn:write',
requireAdminIdentity: true,
});
const manager = this.opsServerRef.dcRouterRef.vpnManager; const manager = this.opsServerRef.dcRouterRef.vpnManager;
if (!manager) { if (!manager) {
return { success: false, message: 'VPN not configured' }; return { success: false, message: 'VPN not configured' };
@@ -258,6 +282,10 @@ export class VpnHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RotateVpnClientKey>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RotateVpnClientKey>(
'rotateVpnClientKey', 'rotateVpnClientKey',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, {
scope: 'vpn:write',
requireAdminIdentity: true,
});
const manager = this.opsServerRef.dcRouterRef.vpnManager; const manager = this.opsServerRef.dcRouterRef.vpnManager;
if (!manager) { if (!manager) {
return { success: false, message: 'VPN not configured' }; return { success: false, message: 'VPN not configured' };
@@ -281,6 +309,10 @@ export class VpnHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ExportVpnClientConfig>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ExportVpnClientConfig>(
'exportVpnClientConfig', 'exportVpnClientConfig',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, {
scope: 'vpn:write',
requireAdminIdentity: true,
});
const manager = this.opsServerRef.dcRouterRef.vpnManager; const manager = this.opsServerRef.dcRouterRef.vpnManager;
if (!manager) { if (!manager) {
return { success: false, message: 'VPN not configured' }; return { success: false, message: 'VPN not configured' };
@@ -301,6 +333,7 @@ export class VpnHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetVpnClientTelemetry>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetVpnClientTelemetry>(
'getVpnClientTelemetry', 'getVpnClientTelemetry',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'vpn:read' });
const manager = this.opsServerRef.dcRouterRef.vpnManager; const manager = this.opsServerRef.dcRouterRef.vpnManager;
if (!manager) { if (!manager) {
return { success: false, message: 'VPN not configured' }; return { success: false, message: 'VPN not configured' };
+18 -33
View File
@@ -1,6 +1,7 @@
import * as plugins from '../../plugins.js'; import * as plugins from '../../plugins.js';
import type { OpsServer } from '../classes.opsserver.js'; import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js'; import * as interfaces from '../../../ts_interfaces/index.js';
import { requireOpsAuth } from '../helpers/auth.js';
type TAuthContext = { type TAuthContext = {
userId: string; userId: string;
@@ -20,39 +21,23 @@ export class WorkHosterHandler {
request: { identity?: interfaces.data.IIdentity; apiToken?: string }, request: { identity?: interfaces.data.IIdentity; apiToken?: string },
requiredScope?: interfaces.data.TApiTokenScope, requiredScope?: interfaces.data.TApiTokenScope,
): Promise<TAuthContext> { ): Promise<TAuthContext> {
if (request.identity?.jwt) { const auth = await requireOpsAuth(this.opsServerRef, request, {
try { scope: requiredScope,
const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({ requireAdminIdentity: requiredScope?.endsWith(':write'),
identity: request.identity, });
}); return { userId: auth.userId, isAdmin: auth.isAdmin, token: auth.token };
if (isAdmin) return { userId: request.identity.userId, isAdmin: true };
} catch { /* fall through */ }
}
if (request.apiToken) {
const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
if (tokenManager) {
const token = await tokenManager.validateToken(request.apiToken);
if (token) {
if (!requiredScope || tokenManager.hasScope(token, requiredScope)) {
return { userId: token.createdBy, isAdmin: token.policy?.role === 'admin', token };
}
throw new plugins.typedrequest.TypedResponseError('insufficient scope');
}
}
}
throw new plugins.typedrequest.TypedResponseError('unauthorized');
} }
private async requireAdmin(request: { identity?: interfaces.data.IIdentity }): Promise<string> { private async requireAdmin(
if (request.identity?.jwt) { request: { identity?: interfaces.data.IIdentity; apiToken?: string },
const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({ scope: interfaces.data.TApiTokenScope = 'gateway-clients:write',
identity: request.identity, ): Promise<string> {
}); const auth = await requireOpsAuth(this.opsServerRef, request, {
if (isAdmin) return request.identity.userId; scope,
} requireAdminIdentity: true,
throw new plugins.typedrequest.TypedResponseError('admin identity required'); requireAdminToken: true,
});
return auth.userId;
} }
private registerHandlers(): void { private registerHandlers(): void {
@@ -83,7 +68,7 @@ export class WorkHosterHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListGatewayClients>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListGatewayClients>(
'listGatewayClients', 'listGatewayClients',
async (dataArg) => { async (dataArg) => {
await this.requireAdmin(dataArg); await this.requireAdmin(dataArg, 'gateway-clients:read');
return { gatewayClients: await this.listManagedGatewayClients() }; return { gatewayClients: await this.listManagedGatewayClients() };
}, },
), ),
@@ -154,7 +139,7 @@ export class WorkHosterHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateGatewayClientToken>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateGatewayClientToken>(
'createGatewayClientToken', 'createGatewayClientToken',
async (dataArg) => { async (dataArg) => {
const userId = await this.requireAdmin(dataArg); const userId = await this.requireAdmin(dataArg, 'tokens:manage');
const gatewayClient = await this.opsServerRef.dcRouterRef.gatewayClientManager?.getClient(dataArg.gatewayClientId); const gatewayClient = await this.opsServerRef.dcRouterRef.gatewayClientManager?.getClient(dataArg.gatewayClientId);
const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager; const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
if (!gatewayClient || !gatewayClient.enabled) { if (!gatewayClient || !gatewayClient.enabled) {
+91
View File
@@ -0,0 +1,91 @@
import * as plugins from '../../plugins.js';
import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js';
export interface IAuthRequest {
identity?: interfaces.data.IIdentity;
apiToken?: string;
}
export interface IAuthRequirement {
scope?: interfaces.data.TApiTokenScope;
requireAdminIdentity?: boolean;
requireAdminToken?: boolean;
}
export interface IAuthContext {
type: 'identity' | 'apiToken';
userId: string;
role?: string;
isAdmin: boolean;
scopes: interfaces.data.TApiTokenScope[];
identity?: interfaces.data.IIdentity;
token?: interfaces.data.IStoredApiToken;
}
const typedAuthError = (messageArg: string) => {
return new plugins.typedrequest.TypedResponseError(messageArg);
};
export async function requireOpsAuth(
opsServerRefArg: OpsServer,
requestArg: IAuthRequest,
requirementArg: IAuthRequirement = {},
): Promise<IAuthContext> {
let identityNeedsAdmin = false;
let tokenNeedsAdmin = false;
let tokenNeedsScope = false;
if (requestArg.identity?.jwt) {
const identity = await opsServerRefArg.adminHandler.validateIdentity(requestArg.identity);
if (identity) {
const isAdmin = identity.role === 'admin';
if (!requirementArg.requireAdminIdentity || isAdmin) {
return {
type: 'identity',
userId: identity.userId,
role: identity.role,
isAdmin,
scopes: [],
identity,
};
}
identityNeedsAdmin = true;
}
}
if (requestArg.apiToken) {
const tokenManager = opsServerRefArg.dcRouterRef.apiTokenManager;
const token = tokenManager ? await tokenManager.validateToken(requestArg.apiToken) : null;
if (token) {
if (requirementArg.requireAdminToken && token.policy?.role !== 'admin') {
tokenNeedsAdmin = true;
} else if (requirementArg.scope && !tokenManager!.hasScope(token, requirementArg.scope)) {
tokenNeedsScope = true;
} else {
const scopes = token.policy?.role === 'admin'
? ['*' as interfaces.data.TApiTokenScope]
: Array.from(new Set([...(token.scopes || []), ...(token.policy?.scopes || [])]));
return {
type: 'apiToken',
userId: token.createdBy,
role: token.policy?.role || 'operator',
isAdmin: token.policy?.role === 'admin',
scopes,
token,
};
}
}
}
if (tokenNeedsScope) {
throw typedAuthError('insufficient scope');
}
if (tokenNeedsAdmin) {
throw typedAuthError('admin API token required');
}
if (identityNeedsAdmin) {
throw typedAuthError('admin identity required');
}
throw typedAuthError('unauthorized');
}
+46 -16
View File
@@ -8,22 +8,52 @@ export type IRouteSecurity = NonNullable<IRouteConfig['security']>;
// Route Management Data Types // Route Management Data Types
// ============================================================================ // ============================================================================
export type TApiTokenScope = export const apiTokenScopes = [
| '*' '*',
| 'routes:read' | 'routes:write' 'routes:read',
| 'config:read' 'routes:write',
| 'certificates:read' | 'certificates:write' 'config:read',
| 'tokens:read' | 'tokens:manage' 'stats:read',
| 'source-profiles:read' | 'source-profiles:write' 'logs:read',
| 'target-profiles:read' | 'target-profiles:write' 'security:read',
| 'targets:read' | 'targets:write' 'security:write',
| 'dns-providers:read' | 'dns-providers:write' 'emails:read',
| 'domains:read' | 'domains:write' 'emails:write',
| 'dns-records:read' | 'dns-records:write' 'certificates:read',
| 'acme-config:read' | 'acme-config:write' 'certificates:write',
| 'email-domains:read' | 'email-domains:write' 'tokens:read',
| 'gateway-clients:read' | 'gateway-clients:write' 'tokens:manage',
| 'workhosters:read' | 'workhosters:write'; 'users:read',
'users:manage',
'source-profiles:read',
'source-profiles:write',
'target-profiles:read',
'target-profiles:write',
'targets:read',
'targets:write',
'dns-providers:read',
'dns-providers:write',
'domains:read',
'domains:write',
'dns-records:read',
'dns-records:write',
'acme-config:read',
'acme-config:write',
'email-domains:read',
'email-domains:write',
'remote-ingress:read',
'remote-ingress:write',
'vpn:read',
'vpn:write',
'radius:read',
'radius:write',
'gateway-clients:read',
'gateway-clients:write',
'workhosters:read',
'workhosters:write',
] as const;
export type TApiTokenScope = typeof apiTokenScopes[number];
export type TGatewayClientType = 'onebox' | 'cloudly' | 'custom'; export type TGatewayClientType = 'onebox' | 'cloudly' | 'custom';
/** @deprecated Use TGatewayClientType. */ /** @deprecated Use TGatewayClientType. */
+10 -5
View File
@@ -16,7 +16,8 @@ export interface IReq_CreateApiToken extends plugins.typedrequestInterfaces.impl
> { > {
method: 'createApiToken'; method: 'createApiToken';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
name: string; name: string;
scopes: TApiTokenScope[]; scopes: TApiTokenScope[];
policy?: IApiTokenPolicy; policy?: IApiTokenPolicy;
@@ -39,7 +40,8 @@ export interface IReq_ListApiTokens extends plugins.typedrequestInterfaces.imple
> { > {
method: 'listApiTokens'; method: 'listApiTokens';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
}; };
response: { response: {
tokens: IApiTokenInfo[]; tokens: IApiTokenInfo[];
@@ -55,7 +57,8 @@ export interface IReq_RevokeApiToken extends plugins.typedrequestInterfaces.impl
> { > {
method: 'revokeApiToken'; method: 'revokeApiToken';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
id: string; id: string;
}; };
response: { response: {
@@ -74,7 +77,8 @@ export interface IReq_RollApiToken extends plugins.typedrequestInterfaces.implem
> { > {
method: 'rollApiToken'; method: 'rollApiToken';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
id: string; id: string;
}; };
response: { response: {
@@ -93,7 +97,8 @@ export interface IReq_ToggleApiToken extends plugins.typedrequestInterfaces.impl
> { > {
method: 'toggleApiToken'; method: 'toggleApiToken';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
id: string; id: string;
enabled: boolean; enabled: boolean;
}; };
+3 -2
View File
@@ -3,7 +3,8 @@ import type * as data from '../data/index.js';
export interface IReq_GetCombinedMetrics { export interface IReq_GetCombinedMetrics {
method: 'getCombinedMetrics'; method: 'getCombinedMetrics';
request: { request: {
identity: data.IIdentity; identity?: data.IIdentity;
apiToken?: string;
sections?: { sections?: {
server?: boolean; server?: boolean;
email?: boolean; email?: boolean;
@@ -26,4 +27,4 @@ export interface IReq_GetCombinedMetrics {
}; };
timestamp: number; timestamp: number;
}; };
} }
+2 -1
View File
@@ -82,7 +82,8 @@ export interface IReq_GetConfiguration extends plugins.typedrequestInterfaces.im
> { > {
method: 'getConfiguration'; method: 'getConfiguration';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
section?: string; section?: string;
}; };
response: { response: {
+6 -3
View File
@@ -68,7 +68,8 @@ export interface IReq_GetAllEmails extends plugins.typedrequestInterfaces.implem
> { > {
method: 'getAllEmails'; method: 'getAllEmails';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
}; };
response: { response: {
emails: IEmail[]; emails: IEmail[];
@@ -84,7 +85,8 @@ export interface IReq_GetEmailDetail extends plugins.typedrequestInterfaces.impl
> { > {
method: 'getEmailDetail'; method: 'getEmailDetail';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
emailId: string; emailId: string;
}; };
response: { response: {
@@ -101,7 +103,8 @@ export interface IReq_ResendEmail extends plugins.typedrequestInterfaces.impleme
> { > {
method: 'resendEmail'; method: 'resendEmail';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
emailId: string; emailId: string;
}; };
response: { response: {
+5 -3
View File
@@ -9,7 +9,8 @@ export interface IReq_GetRecentLogs extends plugins.typedrequestInterfaces.imple
> { > {
method: 'getRecentLogs'; method: 'getRecentLogs';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
level?: 'debug' | 'info' | 'warn' | 'error'; level?: 'debug' | 'info' | 'warn' | 'error';
category?: 'smtp' | 'dns' | 'security' | 'system' | 'email'; category?: 'smtp' | 'dns' | 'security' | 'system' | 'email';
limit?: number; limit?: number;
@@ -31,7 +32,8 @@ export interface IReq_GetLogStream extends plugins.typedrequestInterfaces.implem
> { > {
method: 'getLogStream'; method: 'getLogStream';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
follow?: boolean; follow?: boolean;
filters?: { filters?: {
level?: string[]; level?: string[];
@@ -53,4 +55,4 @@ export interface IReq_PushLogEntry extends plugins.typedrequestInterfaces.implem
entry: statsInterfaces.ILogEntry; entry: statsInterfaces.ILogEntry;
}; };
response: {}; response: {};
} }
+24 -12
View File
@@ -14,7 +14,8 @@ export interface IReq_GetRadiusClients extends plugins.typedrequestInterfaces.im
> { > {
method: 'getRadiusClients'; method: 'getRadiusClients';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
}; };
response: { response: {
clients: Array<{ clients: Array<{
@@ -35,7 +36,8 @@ export interface IReq_SetRadiusClient extends plugins.typedrequestInterfaces.imp
> { > {
method: 'setRadiusClient'; method: 'setRadiusClient';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
client: { client: {
name: string; name: string;
ipRange: string; ipRange: string;
@@ -59,7 +61,8 @@ export interface IReq_RemoveRadiusClient extends plugins.typedrequestInterfaces.
> { > {
method: 'removeRadiusClient'; method: 'removeRadiusClient';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
name: string; name: string;
}; };
response: { response: {
@@ -81,7 +84,8 @@ export interface IReq_GetVlanMappings extends plugins.typedrequestInterfaces.imp
> { > {
method: 'getVlanMappings'; method: 'getVlanMappings';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
}; };
response: { response: {
mappings: Array<{ mappings: Array<{
@@ -108,7 +112,8 @@ export interface IReq_SetVlanMapping extends plugins.typedrequestInterfaces.impl
> { > {
method: 'setVlanMapping'; method: 'setVlanMapping';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
mapping: { mapping: {
mac: string; mac: string;
vlan: number; vlan: number;
@@ -139,7 +144,8 @@ export interface IReq_RemoveVlanMapping extends plugins.typedrequestInterfaces.i
> { > {
method: 'removeVlanMapping'; method: 'removeVlanMapping';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
mac: string; mac: string;
}; };
response: { response: {
@@ -157,7 +163,8 @@ export interface IReq_UpdateVlanConfig extends plugins.typedrequestInterfaces.im
> { > {
method: 'updateVlanConfig'; method: 'updateVlanConfig';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
defaultVlan?: number; defaultVlan?: number;
allowUnknownMacs?: boolean; allowUnknownMacs?: boolean;
}; };
@@ -179,7 +186,8 @@ export interface IReq_TestVlanAssignment extends plugins.typedrequestInterfaces.
> { > {
method: 'testVlanAssignment'; method: 'testVlanAssignment';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
mac: string; mac: string;
}; };
response: { response: {
@@ -207,7 +215,8 @@ export interface IReq_GetRadiusSessions extends plugins.typedrequestInterfaces.i
> { > {
method: 'getRadiusSessions'; method: 'getRadiusSessions';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
filter?: { filter?: {
username?: string; username?: string;
nasIpAddress?: string; nasIpAddress?: string;
@@ -243,7 +252,8 @@ export interface IReq_DisconnectRadiusSession extends plugins.typedrequestInterf
> { > {
method: 'disconnectRadiusSession'; method: 'disconnectRadiusSession';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
sessionId: string; sessionId: string;
reason?: string; reason?: string;
}; };
@@ -262,7 +272,8 @@ export interface IReq_GetRadiusAccountingSummary extends plugins.typedrequestInt
> { > {
method: 'getRadiusAccountingSummary'; method: 'getRadiusAccountingSummary';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
startTime: number; startTime: number;
endTime: number; endTime: number;
}; };
@@ -296,7 +307,8 @@ export interface IReq_GetRadiusStatistics extends plugins.typedrequestInterfaces
> { > {
method: 'getRadiusStatistics'; method: 'getRadiusStatistics';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
}; };
response: { response: {
stats: { stats: {
+14 -7
View File
@@ -15,7 +15,8 @@ export interface IReq_CreateRemoteIngress extends plugins.typedrequestInterfaces
> { > {
method: 'createRemoteIngress'; method: 'createRemoteIngress';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
name: string; name: string;
listenPorts?: number[]; listenPorts?: number[];
autoDerivePorts?: boolean; autoDerivePorts?: boolean;
@@ -36,7 +37,8 @@ export interface IReq_DeleteRemoteIngress extends plugins.typedrequestInterfaces
> { > {
method: 'deleteRemoteIngress'; method: 'deleteRemoteIngress';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
id: string; id: string;
}; };
response: { response: {
@@ -54,7 +56,8 @@ export interface IReq_UpdateRemoteIngress extends plugins.typedrequestInterfaces
> { > {
method: 'updateRemoteIngress'; method: 'updateRemoteIngress';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
id: string; id: string;
name?: string; name?: string;
listenPorts?: number[]; listenPorts?: number[];
@@ -77,7 +80,8 @@ export interface IReq_RegenerateRemoteIngressSecret extends plugins.typedrequest
> { > {
method: 'regenerateRemoteIngressSecret'; method: 'regenerateRemoteIngressSecret';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
id: string; id: string;
}; };
response: { response: {
@@ -95,7 +99,8 @@ export interface IReq_GetRemoteIngresses extends plugins.typedrequestInterfaces.
> { > {
method: 'getRemoteIngresses'; method: 'getRemoteIngresses';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
}; };
response: { response: {
edges: IRemoteIngress[]; edges: IRemoteIngress[];
@@ -111,7 +116,8 @@ export interface IReq_GetRemoteIngressStatus extends plugins.typedrequestInterfa
> { > {
method: 'getRemoteIngressStatus'; method: 'getRemoteIngressStatus';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
}; };
response: { response: {
statuses: IRemoteIngressStatus[]; statuses: IRemoteIngressStatus[];
@@ -128,7 +134,8 @@ export interface IReq_GetRemoteIngressConnectionToken extends plugins.typedreque
> { > {
method: 'getRemoteIngressConnectionToken'; method: 'getRemoteIngressConnectionToken';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
edgeId: string; edgeId: string;
hubHost?: string; hubHost?: string;
}; };
+16 -8
View File
@@ -15,7 +15,8 @@ export interface IReq_ListSecurityBlockRules extends plugins.typedrequestInterfa
> { > {
method: 'listSecurityBlockRules'; method: 'listSecurityBlockRules';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
}; };
response: { response: {
rules: ISecurityBlockRule[]; rules: ISecurityBlockRule[];
@@ -28,7 +29,8 @@ export interface IReq_CreateSecurityBlockRule extends plugins.typedrequestInterf
> { > {
method: 'createSecurityBlockRule'; method: 'createSecurityBlockRule';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
type: TSecurityBlockRuleType; type: TSecurityBlockRuleType;
value: string; value: string;
matchMode?: TSecurityBlockRuleMatchMode; matchMode?: TSecurityBlockRuleMatchMode;
@@ -48,7 +50,8 @@ export interface IReq_UpdateSecurityBlockRule extends plugins.typedrequestInterf
> { > {
method: 'updateSecurityBlockRule'; method: 'updateSecurityBlockRule';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
id: string; id: string;
value?: string; value?: string;
matchMode?: TSecurityBlockRuleMatchMode; matchMode?: TSecurityBlockRuleMatchMode;
@@ -68,7 +71,8 @@ export interface IReq_DeleteSecurityBlockRule extends plugins.typedrequestInterf
> { > {
method: 'deleteSecurityBlockRule'; method: 'deleteSecurityBlockRule';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
id: string; id: string;
}; };
response: { response: {
@@ -83,7 +87,8 @@ export interface IReq_ListIpIntelligence extends plugins.typedrequestInterfaces.
> { > {
method: 'listIpIntelligence'; method: 'listIpIntelligence';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
}; };
response: { response: {
records: IIpIntelligenceRecord[]; records: IIpIntelligenceRecord[];
@@ -96,7 +101,8 @@ export interface IReq_GetCompiledSecurityPolicy extends plugins.typedrequestInte
> { > {
method: 'getCompiledSecurityPolicy'; method: 'getCompiledSecurityPolicy';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
}; };
response: { response: {
policy: ISecurityCompiledPolicy; policy: ISecurityCompiledPolicy;
@@ -109,7 +115,8 @@ export interface IReq_ListSecurityPolicyAudit extends plugins.typedrequestInterf
> { > {
method: 'listSecurityPolicyAudit'; method: 'listSecurityPolicyAudit';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
limit?: number; limit?: number;
}; };
response: { response: {
@@ -123,7 +130,8 @@ export interface IReq_RefreshIpIntelligence extends plugins.typedrequestInterfac
> { > {
method: 'refreshIpIntelligence'; method: 'refreshIpIntelligence';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
ipAddress: string; ipAddress: string;
}; };
response: { response: {
+19 -10
View File
@@ -9,7 +9,8 @@ export interface IReq_GetServerStatistics extends plugins.typedrequestInterfaces
> { > {
method: 'getServerStatistics'; method: 'getServerStatistics';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
includeHistory?: boolean; includeHistory?: boolean;
timeRange?: '1h' | '6h' | '24h' | '7d' | '30d'; timeRange?: '1h' | '6h' | '24h' | '7d' | '30d';
}; };
@@ -29,7 +30,8 @@ export interface IReq_GetEmailStatistics extends plugins.typedrequestInterfaces.
> { > {
method: 'getEmailStatistics'; method: 'getEmailStatistics';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
timeRange?: '1h' | '6h' | '24h' | '7d' | '30d'; timeRange?: '1h' | '6h' | '24h' | '7d' | '30d';
domain?: string; domain?: string;
includeDetails?: boolean; includeDetails?: boolean;
@@ -49,7 +51,8 @@ export interface IReq_GetDnsStatistics extends plugins.typedrequestInterfaces.im
> { > {
method: 'getDnsStatistics'; method: 'getDnsStatistics';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
timeRange?: '1h' | '6h' | '24h' | '7d' | '30d'; timeRange?: '1h' | '6h' | '24h' | '7d' | '30d';
domain?: string; domain?: string;
includeQueryTypes?: boolean; includeQueryTypes?: boolean;
@@ -69,7 +72,8 @@ export interface IReq_GetRateLimitStatus extends plugins.typedrequestInterfaces.
> { > {
method: 'getRateLimitStatus'; method: 'getRateLimitStatus';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
domain?: string; domain?: string;
ip?: string; ip?: string;
includeBlocked?: boolean; includeBlocked?: boolean;
@@ -91,7 +95,8 @@ export interface IReq_GetSecurityMetrics extends plugins.typedrequestInterfaces.
> { > {
method: 'getSecurityMetrics'; method: 'getSecurityMetrics';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
timeRange?: '1h' | '6h' | '24h' | '7d' | '30d'; timeRange?: '1h' | '6h' | '24h' | '7d' | '30d';
includeDetails?: boolean; includeDetails?: boolean;
}; };
@@ -112,7 +117,8 @@ export interface IReq_GetActiveConnections extends plugins.typedrequestInterface
> { > {
method: 'getActiveConnections'; method: 'getActiveConnections';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
protocol?: 'smtp' | 'smtps' | 'http' | 'https'; protocol?: 'smtp' | 'smtps' | 'http' | 'https';
state?: string; state?: string;
}; };
@@ -137,7 +143,8 @@ export interface IReq_GetQueueStatus extends plugins.typedrequestInterfaces.impl
> { > {
method: 'getQueueStatus'; method: 'getQueueStatus';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
queueName?: string; queueName?: string;
}; };
response: { response: {
@@ -153,7 +160,8 @@ export interface IReq_GetHealthStatus extends plugins.typedrequestInterfaces.imp
> { > {
method: 'getHealthStatus'; method: 'getHealthStatus';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
detailed?: boolean; detailed?: boolean;
}; };
response: { response: {
@@ -168,7 +176,8 @@ export interface IReq_GetNetworkStats extends plugins.typedrequestInterfaces.imp
> { > {
method: 'getNetworkStats'; method: 'getNetworkStats';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
}; };
response: { response: {
connectionsByIP: Array<{ ip: string; count: number }>; connectionsByIP: Array<{ ip: string; count: number }>;
@@ -185,4 +194,4 @@ export interface IReq_GetNetworkStats extends plugins.typedrequestInterfaces.imp
frontendProtocols?: statsInterfaces.IProtocolDistribution | null; frontendProtocols?: statsInterfaces.IProtocolDistribution | null;
backendProtocols?: statsInterfaces.IProtocolDistribution | null; backendProtocols?: statsInterfaces.IProtocolDistribution | null;
}; };
} }
+6 -3
View File
@@ -14,7 +14,8 @@ export interface IReq_ListUsers extends plugins.typedrequestInterfaces.implement
> { > {
method: 'listUsers'; method: 'listUsers';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
}; };
response: { response: {
users: IAdminUserProjection[]; users: IAdminUserProjection[];
@@ -30,7 +31,8 @@ export interface IReq_CreateUser extends plugins.typedrequestInterfaces.implemen
> { > {
method: 'createUser'; method: 'createUser';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
email: string; email: string;
name?: string; name?: string;
role: TUserManagementRole; role: TUserManagementRole;
@@ -53,7 +55,8 @@ export interface IReq_DeleteUser extends plugins.typedrequestInterfaces.implemen
> { > {
method: 'deleteUser'; method: 'deleteUser';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
id: string; id: string;
}; };
response: { response: {
+22 -11
View File
@@ -15,7 +15,8 @@ export interface IReq_GetVpnClients extends plugins.typedrequestInterfaces.imple
> { > {
method: 'getVpnClients'; method: 'getVpnClients';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
}; };
response: { response: {
clients: IVpnClient[]; clients: IVpnClient[];
@@ -31,7 +32,8 @@ export interface IReq_GetVpnStatus extends plugins.typedrequestInterfaces.implem
> { > {
method: 'getVpnStatus'; method: 'getVpnStatus';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
}; };
response: { response: {
status: IVpnServerStatus; status: IVpnServerStatus;
@@ -47,7 +49,8 @@ export interface IReq_CreateVpnClient extends plugins.typedrequestInterfaces.imp
> { > {
method: 'createVpnClient'; method: 'createVpnClient';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
clientId: string; clientId: string;
targetProfileIds?: string[]; targetProfileIds?: string[];
description?: string; description?: string;
@@ -78,7 +81,8 @@ export interface IReq_UpdateVpnClient extends plugins.typedrequestInterfaces.imp
> { > {
method: 'updateVpnClient'; method: 'updateVpnClient';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
clientId: string; clientId: string;
description?: string; description?: string;
targetProfileIds?: string[]; targetProfileIds?: string[];
@@ -106,7 +110,8 @@ export interface IReq_GetVpnConnectedClients extends plugins.typedrequestInterfa
> { > {
method: 'getVpnConnectedClients'; method: 'getVpnConnectedClients';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
}; };
response: { response: {
connectedClients: IVpnConnectedClient[]; connectedClients: IVpnConnectedClient[];
@@ -122,7 +127,8 @@ export interface IReq_DeleteVpnClient extends plugins.typedrequestInterfaces.imp
> { > {
method: 'deleteVpnClient'; method: 'deleteVpnClient';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
clientId: string; clientId: string;
}; };
response: { response: {
@@ -140,7 +146,8 @@ export interface IReq_EnableVpnClient extends plugins.typedrequestInterfaces.imp
> { > {
method: 'enableVpnClient'; method: 'enableVpnClient';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
clientId: string; clientId: string;
}; };
response: { response: {
@@ -158,7 +165,8 @@ export interface IReq_DisableVpnClient extends plugins.typedrequestInterfaces.im
> { > {
method: 'disableVpnClient'; method: 'disableVpnClient';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
clientId: string; clientId: string;
}; };
response: { response: {
@@ -176,7 +184,8 @@ export interface IReq_RotateVpnClientKey extends plugins.typedrequestInterfaces.
> { > {
method: 'rotateVpnClientKey'; method: 'rotateVpnClientKey';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
clientId: string; clientId: string;
}; };
response: { response: {
@@ -196,7 +205,8 @@ export interface IReq_ExportVpnClientConfig extends plugins.typedrequestInterfac
> { > {
method: 'exportVpnClientConfig'; method: 'exportVpnClientConfig';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
clientId: string; clientId: string;
format: 'smartvpn' | 'wireguard'; format: 'smartvpn' | 'wireguard';
}; };
@@ -216,7 +226,8 @@ export interface IReq_GetVpnClientTelemetry extends plugins.typedrequestInterfac
> { > {
method: 'getVpnClientTelemetry'; method: 'getVpnClientTelemetry';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
clientId: string; clientId: string;
}; };
response: { response: {
+10 -5
View File
@@ -53,7 +53,8 @@ export interface IReq_ListGatewayClients extends plugins.typedrequestInterfaces.
> { > {
method: 'listGatewayClients'; method: 'listGatewayClients';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
}; };
response: { response: {
gatewayClients: IGatewayClient[]; gatewayClients: IGatewayClient[];
@@ -66,7 +67,8 @@ export interface IReq_CreateGatewayClient extends plugins.typedrequestInterfaces
> { > {
method: 'createGatewayClient'; method: 'createGatewayClient';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
id?: string; id?: string;
type: IGatewayClient['type']; type: IGatewayClient['type'];
name: string; name: string;
@@ -88,7 +90,8 @@ export interface IReq_UpdateGatewayClient extends plugins.typedrequestInterfaces
> { > {
method: 'updateGatewayClient'; method: 'updateGatewayClient';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
id: string; id: string;
name?: string; name?: string;
description?: string; description?: string;
@@ -110,7 +113,8 @@ export interface IReq_DeleteGatewayClient extends plugins.typedrequestInterfaces
> { > {
method: 'deleteGatewayClient'; method: 'deleteGatewayClient';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
id: string; id: string;
}; };
response: { response: {
@@ -125,7 +129,8 @@ export interface IReq_CreateGatewayClientToken extends plugins.typedrequestInter
> { > {
method: 'createGatewayClientToken'; method: 'createGatewayClientToken';
request: { request: {
identity: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string;
gatewayClientId: string; gatewayClientId: string;
name?: string; name?: string;
expiresInDays?: number | null; expiresInDays?: number | null;
+2
View File
@@ -89,6 +89,8 @@ export async function createMigrationRunner(
db: db as any, db: db as any,
// Brand-new installs skip all migrations and stamp directly to the current version. // Brand-new installs skip all migrations and stamp directly to the current version.
freshInstallVersion: targetVersion, freshInstallVersion: targetVersion,
// dcrouter uses the package version as targetVersion; bridge releases without DB changes.
targetVersionStrategy: 'bridge',
}); });
// Register steps in execution order. Each step's .from() must match the // Register steps in execution order. Each step's .from() must match the
+1 -20
View File
@@ -200,26 +200,7 @@ export class OpsViewApiTokens extends DeesElement {
private async showCreateTokenDialog() { private async showCreateTokenDialog() {
const { DeesModal } = await import('@design.estate/dees-catalog'); const { DeesModal } = await import('@design.estate/dees-catalog');
const allScopes = [ const allScopes = [...interfaces.data.apiTokenScopes];
'*',
'routes:read',
'routes:write',
'config:read',
'certificates:read',
'certificates:write',
'tokens:read',
'tokens:manage',
'domains:read',
'domains:write',
'dns-records:read',
'dns-records:write',
'email-domains:read',
'email-domains:write',
'gateway-clients:read',
'gateway-clients:write',
'workhosters:read',
'workhosters:write',
];
await DeesModal.createAndShow({ await DeesModal.createAndShow({
heading: 'Create API Token', heading: 'Create API Token',