diff --git a/changelog.md b/changelog.md index d564c1b..ed95cc9 100644 --- a/changelog.md +++ b/changelog.md @@ -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 ### Features diff --git a/package.json b/package.json index f4c23d5..d4d7d88 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "@push.rocks/smartjwt": "^2.2.2", "@push.rocks/smartlog": "^3.2.2", "@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/smartnetwork": "^4.7.1", "@push.rocks/smartpath": "^6.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a3ed4c1..b4796ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,8 +69,8 @@ importers: specifier: ^3.0.3 version: 3.0.3 '@push.rocks/smartmigration': - specifier: 1.3.1 - version: 1.3.1(@push.rocks/smartbucket@4.6.1)(@push.rocks/smartdata@7.1.7(socks@2.8.8)) + specifier: 1.4.1 + version: 1.4.1(@push.rocks/smartbucket@4.6.1)(@push.rocks/smartdata@7.1.7(socks@2.8.8)) '@push.rocks/smartmta': specifier: ^5.3.3 version: 5.3.3 @@ -1366,8 +1366,8 @@ packages: '@push.rocks/smartmetrics@3.0.3': resolution: {integrity: sha512-RYY4NOla3kraZYVF9TBHgIz4/hSkqVDVNP7tLwhLK5mGBPBy8I/9NWXX6txZKQw6QihP85YD8mWUuUu2xS4D6Q==} - '@push.rocks/smartmigration@1.3.1': - resolution: {integrity: sha512-qU3vc4yCLn8vJQIEMQwS2Lq6Ra8ixSfjutnbR1L/hauCzFRCic3o/DnFKB7pjj5jWaqSDG5nlyeIliLmC5aGsg==} + '@push.rocks/smartmigration@1.4.1': + resolution: {integrity: sha512-kBvWuqBIIgkK2QskjHl0/MPLXYu4CDJDyuPc1KBDPBNejYIJp6hOZtbsmj4DYoNKsgFTpAALJn9JmUEdLe9E4g==} peerDependencies: '@push.rocks/smartbucket': ^4.6.1 '@push.rocks/smartdata': ^7.1.7 @@ -6508,7 +6508,7 @@ snapshots: '@push.rocks/smartdelay': 3.1.0 '@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: '@push.rocks/smartlog': 3.2.2 '@push.rocks/smartversion': 3.1.0 diff --git a/test/test.certificate-api-token.node.ts b/test/test.certificate-api-token.node.ts index 91da9a8..b20bbd7 100644 --- a/test/test.certificate-api-token.node.ts +++ b/test/test.certificate-api-token.node.ts @@ -56,6 +56,7 @@ const setupHandler = (scopes: TScope[], options?: { const opsServerRef: any = { typedrouter, adminHandler: { + validateIdentity: async () => null, adminIdentityGuard: { exec: async () => false, }, diff --git a/test/test.config-api-token.node.ts b/test/test.config-api-token.node.ts new file mode 100644 index 0000000..d18383e --- /dev/null +++ b/test/test.config-api-token.node.ts @@ -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, +) => { + 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(); diff --git a/test/test.migrations.node.ts b/test/test.migrations.node.ts new file mode 100644 index 0000000..30371bc --- /dev/null +++ b/test/test.migrations.node.ts @@ -0,0 +1,69 @@ +import { expect, tap } from '@git.zone/tstest/tapbundle'; + +import { createMigrationRunner } from '../ts_migrations/index.js'; + +function setPath(target: Record, 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, set: Record): 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(); diff --git a/test/test.ops-auth-helper.node.ts b/test/test.ops-auth-helper.node.ts new file mode 100644 index 0000000..85bb398 --- /dev/null +++ b/test/test.ops-auth-helper.node.ts @@ -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(); diff --git a/test/test.workhoster-handler.node.ts b/test/test.workhoster-handler.node.ts index 5388f0d..5626838 100644 --- a/test/test.workhoster-handler.node.ts +++ b/test/test.workhoster-handler.node.ts @@ -136,6 +136,9 @@ const setupHandler = (options: { const opsServerRef: any = { typedrouter, adminHandler: { + validateIdentity: async (identity: interfaces.data.IIdentity) => options.isAdmin + ? { ...identity, role: 'admin' } + : identity, adminIdentityGuard: { exec: async () => Boolean(options.isAdmin), }, diff --git a/ts/opsserver/classes.opsserver.ts b/ts/opsserver/classes.opsserver.ts index d413915..1763d6b 100644 --- a/ts/opsserver/classes.opsserver.ts +++ b/ts/opsserver/classes.opsserver.ts @@ -3,7 +3,6 @@ import * as plugins from '../plugins.js'; import * as paths from '../paths.js'; import * as handlers from './handlers/index.js'; import * as interfaces from '../../ts_interfaces/index.js'; -import { requireValidIdentity, requireAdminIdentity } from './helpers/guards.js'; export class OpsServer { public dcRouterRef: DcRouter; @@ -12,9 +11,9 @@ export class OpsServer { // Main TypedRouter — unauthenticated endpoints (login/logout/verify) and own-auth handlers public typedrouter = new plugins.typedrequest.TypedRouter(); - // Auth-enforced routers — middleware validates identity before any handler runs - public viewRouter = new plugins.typedrequest.TypedRouter<{ request: { identity: interfaces.data.IIdentity } }>(); - public adminRouter = new plugins.typedrequest.TypedRouter<{ request: { identity: interfaces.data.IIdentity } }>(); + // Grouped routers. Handlers enforce auth explicitly with per-endpoint scopes. + public viewRouter = new plugins.typedrequest.TypedRouter<{ request: { identity?: interfaces.data.IIdentity; apiToken?: string } }>(); + public adminRouter = new plugins.typedrequest.TypedRouter<{ request: { identity?: interfaces.data.IIdentity; apiToken?: string } }>(); // Handler instances public adminHandler!: handlers.AdminHandler; @@ -72,16 +71,6 @@ export class OpsServer { this.adminHandler = new handlers.AdminHandler(this); 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 this.typedrouter.addTypedRouter(this.viewRouter); this.typedrouter.addTypedRouter(this.adminRouter); diff --git a/ts/opsserver/handlers/acme-config.handler.ts b/ts/opsserver/handlers/acme-config.handler.ts index 229b42d..17b54c8 100644 --- a/ts/opsserver/handlers/acme-config.handler.ts +++ b/ts/opsserver/handlers/acme-config.handler.ts @@ -1,6 +1,7 @@ import * as plugins from '../../plugins.js'; import type { OpsServer } from '../classes.opsserver.js'; import * as interfaces from '../../../ts_interfaces/index.js'; +import { requireOpsAuth } from '../helpers/auth.js'; /** * CRUD handler for the singleton `AcmeConfigDoc`. @@ -20,29 +21,11 @@ export class AcmeConfigHandler { request: { identity?: interfaces.data.IIdentity; apiToken?: string }, requiredScope?: interfaces.data.TApiTokenScope, ): Promise { - if (request.identity?.jwt) { - try { - const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({ - identity: request.identity, - }); - 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'); + const auth = await requireOpsAuth(this.opsServerRef, request, { + scope: requiredScope, + requireAdminIdentity: requiredScope?.endsWith(':write'), + }); + return auth.userId; } private registerHandlers(): void { diff --git a/ts/opsserver/handlers/admin.handler.ts b/ts/opsserver/handlers/admin.handler.ts index 2014014..19f5e88 100644 --- a/ts/opsserver/handlers/admin.handler.ts +++ b/ts/opsserver/handlers/admin.handler.ts @@ -258,12 +258,18 @@ export class AdminHandler { this.opsServerRef.adminRouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'createInitialAdminUser', - async (dataArg) => this.createInitialAdminUser({ - email: dataArg.email, - name: dataArg.name, - password: dataArg.password, - enableIdpGlobalAuth: dataArg.enableIdpGlobalAuth, - }) + async (dataArg) => { + const isAdmin = await this.adminIdentityGuard.exec({ identity: dataArg.identity }); + if (!isAdmin) { + throw new plugins.typedrequest.TypedResponseError('admin identity required'); + } + 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( 'adminLogout', async (dataArg) => { - // In a real implementation, you might want to blacklist the JWT - // For now, just return success + const identity = await this.validateIdentity(dataArg.identity); + if (!identity) { + throw new plugins.typedrequest.TypedResponseError('identity is not valid'); + } return { success: true, }; @@ -314,52 +322,8 @@ export class AdminHandler { new plugins.typedrequest.TypedHandler( 'verifyIdentity', async (dataArg) => { - if (!dataArg.identity?.jwt) { - return { - 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, - }; - } + const identity = await this.validateIdentity(dataArg.identity); + return identity ? { valid: true, identity } : { valid: false }; } ) ); @@ -372,45 +336,7 @@ export class AdminHandler { identity: interfaces.data.IIdentity; }>( async (dataArg) => { - if (!dataArg.identity?.jwt) { - 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; - } + return Boolean(await this.validateIdentity(dataArg.identity)); }, { failedHint: 'identity is not valid', @@ -425,14 +351,8 @@ export class AdminHandler { identity: interfaces.data.IIdentity; }>( async (dataArg) => { - // First check if identity is valid - const isValid = await this.validIdentityGuard.exec(dataArg); - if (!isValid) { - return false; - } - - // Check if user has admin role - return dataArg.identity.role === 'admin'; + const identity = await this.validateIdentity(dataArg.identity); + return identity?.role === 'admin'; }, { failedHint: 'user is not admin', @@ -440,6 +360,49 @@ export class AdminHandler { } ); + public async validateIdentity( + identityArg?: interfaces.data.IIdentity, + ): Promise { + 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: { username: string; password: string; diff --git a/ts/opsserver/handlers/api-token.handler.ts b/ts/opsserver/handlers/api-token.handler.ts index 11049f7..72ab0cd 100644 --- a/ts/opsserver/handlers/api-token.handler.ts +++ b/ts/opsserver/handlers/api-token.handler.ts @@ -1,6 +1,7 @@ import * as plugins from '../../plugins.js'; import type { OpsServer } from '../classes.opsserver.js'; import * as interfaces from '../../../ts_interfaces/index.js'; +import { requireOpsAuth } from '../helpers/auth.js'; export class ApiTokenHandler { constructor(private opsServerRef: OpsServer) { @@ -17,6 +18,11 @@ export class ApiTokenHandler { new plugins.typedrequest.TypedHandler( 'createApiToken', async (dataArg) => { + const auth = await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'tokens:manage', + requireAdminIdentity: true, + requireAdminToken: true, + }); const manager = this.opsServerRef.dcRouterRef.apiTokenManager; if (!manager) { return { success: false, message: 'Token management not initialized' }; @@ -25,7 +31,7 @@ export class ApiTokenHandler { dataArg.name, dataArg.scopes, dataArg.expiresInDays ?? null, - dataArg.identity.userId, + auth.userId, dataArg.policy, ); return { success: true, tokenId: result.id, tokenValue: result.rawToken }; @@ -38,6 +44,11 @@ export class ApiTokenHandler { new plugins.typedrequest.TypedHandler( 'listApiTokens', async (dataArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'tokens:read', + requireAdminIdentity: true, + requireAdminToken: true, + }); const manager = this.opsServerRef.dcRouterRef.apiTokenManager; if (!manager) { return { tokens: [] }; @@ -52,6 +63,11 @@ export class ApiTokenHandler { new plugins.typedrequest.TypedHandler( 'revokeApiToken', async (dataArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'tokens:manage', + requireAdminIdentity: true, + requireAdminToken: true, + }); const manager = this.opsServerRef.dcRouterRef.apiTokenManager; if (!manager) { return { success: false, message: 'Token management not initialized' }; @@ -67,6 +83,11 @@ export class ApiTokenHandler { new plugins.typedrequest.TypedHandler( 'rollApiToken', async (dataArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'tokens:manage', + requireAdminIdentity: true, + requireAdminToken: true, + }); const manager = this.opsServerRef.dcRouterRef.apiTokenManager; if (!manager) { return { success: false, message: 'Token management not initialized' }; @@ -85,6 +106,11 @@ export class ApiTokenHandler { new plugins.typedrequest.TypedHandler( 'toggleApiToken', async (dataArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'tokens:manage', + requireAdminIdentity: true, + requireAdminToken: true, + }); const manager = this.opsServerRef.dcRouterRef.apiTokenManager; if (!manager) { return { success: false, message: 'Token management not initialized' }; diff --git a/ts/opsserver/handlers/certificate.handler.ts b/ts/opsserver/handlers/certificate.handler.ts index e5806dd..4606ce4 100644 --- a/ts/opsserver/handlers/certificate.handler.ts +++ b/ts/opsserver/handlers/certificate.handler.ts @@ -3,6 +3,7 @@ import type { OpsServer } from '../classes.opsserver.js'; import * as interfaces from '../../../ts_interfaces/index.js'; import { AcmeCertDoc, ProxyCertDoc } from '../../db/index.js'; import { logger } from '../../logger.js'; +import { requireOpsAuth } from '../helpers/auth.js'; /** * Mirrors `SmartacmeCertMatcher.getCertificateDomainNameByDomainName` from @@ -37,29 +38,11 @@ export class CertificateHandler { request: { identity?: interfaces.data.IIdentity; apiToken?: string }, requiredScope?: interfaces.data.TApiTokenScope, ): Promise { - if (request.identity?.jwt) { - try { - const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({ - identity: request.identity, - }); - 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'); + const auth = await requireOpsAuth(this.opsServerRef, request, { + scope: requiredScope, + requireAdminIdentity: requiredScope?.endsWith(':write'), + }); + return auth.userId; } private registerHandlers(): void { diff --git a/ts/opsserver/handlers/config.handler.ts b/ts/opsserver/handlers/config.handler.ts index f3d1a05..850eca2 100644 --- a/ts/opsserver/handlers/config.handler.ts +++ b/ts/opsserver/handlers/config.handler.ts @@ -2,6 +2,7 @@ import * as plugins from '../../plugins.js'; import * as paths from '../../paths.js'; import type { OpsServer } from '../classes.opsserver.js'; import * as interfaces from '../../../ts_interfaces/index.js'; +import { requireOpsAuth } from '../helpers/auth.js'; export class ConfigHandler { constructor(private opsServerRef: OpsServer) { @@ -17,6 +18,7 @@ export class ConfigHandler { new plugins.typedrequest.TypedHandler( 'getConfiguration', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'config:read' }); const config = await this.getConfiguration(); return { config, diff --git a/ts/opsserver/handlers/dns-provider.handler.ts b/ts/opsserver/handlers/dns-provider.handler.ts index 280fcc4..a349a8f 100644 --- a/ts/opsserver/handlers/dns-provider.handler.ts +++ b/ts/opsserver/handlers/dns-provider.handler.ts @@ -1,6 +1,7 @@ import * as plugins from '../../plugins.js'; import type { OpsServer } from '../classes.opsserver.js'; import * as interfaces from '../../../ts_interfaces/index.js'; +import { requireOpsAuth } from '../helpers/auth.js'; /** * CRUD + connection-test handlers for DnsProviderDoc. @@ -20,29 +21,11 @@ export class DnsProviderHandler { request: { identity?: interfaces.data.IIdentity; apiToken?: string }, requiredScope?: interfaces.data.TApiTokenScope, ): Promise { - if (request.identity?.jwt) { - try { - const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({ - identity: request.identity, - }); - 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'); + const auth = await requireOpsAuth(this.opsServerRef, request, { + scope: requiredScope, + requireAdminIdentity: requiredScope?.endsWith(':write'), + }); + return auth.userId; } private registerHandlers(): void { diff --git a/ts/opsserver/handlers/dns-record.handler.ts b/ts/opsserver/handlers/dns-record.handler.ts index 96c8c6b..7ac8c77 100644 --- a/ts/opsserver/handlers/dns-record.handler.ts +++ b/ts/opsserver/handlers/dns-record.handler.ts @@ -1,6 +1,7 @@ import * as plugins from '../../plugins.js'; import type { OpsServer } from '../classes.opsserver.js'; import * as interfaces from '../../../ts_interfaces/index.js'; +import { requireOpsAuth } from '../helpers/auth.js'; /** * CRUD handlers for DnsRecordDoc. @@ -17,29 +18,11 @@ export class DnsRecordHandler { request: { identity?: interfaces.data.IIdentity; apiToken?: string }, requiredScope?: interfaces.data.TApiTokenScope, ): Promise { - if (request.identity?.jwt) { - try { - const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({ - identity: request.identity, - }); - 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'); + const auth = await requireOpsAuth(this.opsServerRef, request, { + scope: requiredScope, + requireAdminIdentity: requiredScope?.endsWith(':write'), + }); + return auth.userId; } private registerHandlers(): void { diff --git a/ts/opsserver/handlers/domain.handler.ts b/ts/opsserver/handlers/domain.handler.ts index cd8717e..16a4508 100644 --- a/ts/opsserver/handlers/domain.handler.ts +++ b/ts/opsserver/handlers/domain.handler.ts @@ -1,6 +1,7 @@ import * as plugins from '../../plugins.js'; import type { OpsServer } from '../classes.opsserver.js'; import * as interfaces from '../../../ts_interfaces/index.js'; +import { requireOpsAuth } from '../helpers/auth.js'; /** * CRUD handlers for DomainDoc. @@ -17,29 +18,11 @@ export class DomainHandler { request: { identity?: interfaces.data.IIdentity; apiToken?: string }, requiredScope?: interfaces.data.TApiTokenScope, ): Promise { - if (request.identity?.jwt) { - try { - const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({ - identity: request.identity, - }); - 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'); + const auth = await requireOpsAuth(this.opsServerRef, request, { + scope: requiredScope, + requireAdminIdentity: requiredScope?.endsWith(':write'), + }); + return auth.userId; } private registerHandlers(): void { diff --git a/ts/opsserver/handlers/email-domain.handler.ts b/ts/opsserver/handlers/email-domain.handler.ts index b19eb1e..b05679b 100644 --- a/ts/opsserver/handlers/email-domain.handler.ts +++ b/ts/opsserver/handlers/email-domain.handler.ts @@ -1,6 +1,7 @@ import * as plugins from '../../plugins.js'; import type { OpsServer } from '../classes.opsserver.js'; import * as interfaces from '../../../ts_interfaces/index.js'; +import { requireOpsAuth } from '../helpers/auth.js'; /** * CRUD + DNS provisioning handler for email domains. @@ -19,29 +20,11 @@ export class EmailDomainHandler { request: { identity?: interfaces.data.IIdentity; apiToken?: string }, requiredScope?: interfaces.data.TApiTokenScope, ): Promise { - if (request.identity?.jwt) { - try { - const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({ - identity: request.identity, - }); - 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'); + const auth = await requireOpsAuth(this.opsServerRef, request, { + scope: requiredScope, + requireAdminIdentity: requiredScope?.endsWith(':write'), + }); + return auth.userId; } private get manager() { diff --git a/ts/opsserver/handlers/email-ops.handler.ts b/ts/opsserver/handlers/email-ops.handler.ts index bc520da..95d6c9b 100644 --- a/ts/opsserver/handlers/email-ops.handler.ts +++ b/ts/opsserver/handlers/email-ops.handler.ts @@ -1,6 +1,7 @@ import * as plugins from '../../plugins.js'; import type { OpsServer } from '../classes.opsserver.js'; import * as interfaces from '../../../ts_interfaces/index.js'; +import { requireOpsAuth } from '../helpers/auth.js'; export class EmailOpsHandler { constructor(private opsServerRef: OpsServer) { @@ -18,6 +19,7 @@ export class EmailOpsHandler { new plugins.typedrequest.TypedHandler( 'getAllEmails', async (dataArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'emails:read' }); const emails = this.getAllQueueEmails(); return { emails }; } @@ -29,6 +31,7 @@ export class EmailOpsHandler { new plugins.typedrequest.TypedHandler( 'getEmailDetail', async (dataArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'emails:read' }); const email = this.getEmailDetail(dataArg.emailId); return { email }; } @@ -42,6 +45,10 @@ export class EmailOpsHandler { new plugins.typedrequest.TypedHandler( 'resendEmail', async (dataArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'emails:write', + requireAdminIdentity: true, + }); const emailServer = this.opsServerRef.dcRouterRef.emailServer; if (!emailServer?.deliveryQueue) { return { success: false, error: 'Email server not available' }; diff --git a/ts/opsserver/handlers/logs.handler.ts b/ts/opsserver/handlers/logs.handler.ts index 9b0b259..12bbfd4 100644 --- a/ts/opsserver/handlers/logs.handler.ts +++ b/ts/opsserver/handlers/logs.handler.ts @@ -2,6 +2,7 @@ import * as plugins from '../../plugins.js'; import type { OpsServer } from '../classes.opsserver.js'; import * as interfaces from '../../../ts_interfaces/index.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 // the current OpsServer reference so it survives OpsServer restarts without @@ -40,6 +41,7 @@ export class LogsHandler { new plugins.typedrequest.TypedHandler( 'getRecentLogs', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'logs:read' }); const logs = await this.getRecentLogs( dataArg.level, dataArg.category, @@ -63,6 +65,7 @@ export class LogsHandler { new plugins.typedrequest.TypedHandler( 'getLogStream', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'logs:read' }); // Create a virtual stream for log streaming const virtualStream = new plugins.typedrequest.VirtualStream(); diff --git a/ts/opsserver/handlers/network-target.handler.ts b/ts/opsserver/handlers/network-target.handler.ts index 38d1efa..a0a6d5d 100644 --- a/ts/opsserver/handlers/network-target.handler.ts +++ b/ts/opsserver/handlers/network-target.handler.ts @@ -1,6 +1,7 @@ import * as plugins from '../../plugins.js'; import type { OpsServer } from '../classes.opsserver.js'; import * as interfaces from '../../../ts_interfaces/index.js'; +import { requireOpsAuth } from '../helpers/auth.js'; export class NetworkTargetHandler { public typedrouter = new plugins.typedrequest.TypedRouter(); @@ -14,29 +15,11 @@ export class NetworkTargetHandler { request: { identity?: interfaces.data.IIdentity; apiToken?: string }, requiredScope?: interfaces.data.TApiTokenScope, ): Promise { - if (request.identity?.jwt) { - try { - const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({ - identity: request.identity, - }); - 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'); + const auth = await requireOpsAuth(this.opsServerRef, request, { + scope: requiredScope, + requireAdminIdentity: requiredScope?.endsWith(':write'), + }); + return auth.userId; } private registerHandlers(): void { diff --git a/ts/opsserver/handlers/radius.handler.ts b/ts/opsserver/handlers/radius.handler.ts index 057a43f..769ecca 100644 --- a/ts/opsserver/handlers/radius.handler.ts +++ b/ts/opsserver/handlers/radius.handler.ts @@ -1,6 +1,7 @@ import * as plugins from '../../plugins.js'; import type { OpsServer } from '../classes.opsserver.js'; import * as interfaces from '../../../ts_interfaces/index.js'; +import { requireOpsAuth } from '../helpers/auth.js'; export class RadiusHandler { constructor(private opsServerRef: OpsServer) { @@ -19,6 +20,7 @@ export class RadiusHandler { new plugins.typedrequest.TypedHandler( 'getRadiusClients', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'radius:read' }); const radiusServer = this.opsServerRef.dcRouterRef.radiusServer; if (!radiusServer) { @@ -43,6 +45,10 @@ export class RadiusHandler { new plugins.typedrequest.TypedHandler( 'setRadiusClient', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'radius:write', + requireAdminIdentity: true, + }); const radiusServer = this.opsServerRef.dcRouterRef.radiusServer; if (!radiusServer) { @@ -64,6 +70,10 @@ export class RadiusHandler { new plugins.typedrequest.TypedHandler( 'removeRadiusClient', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'radius:write', + requireAdminIdentity: true, + }); const radiusServer = this.opsServerRef.dcRouterRef.radiusServer; if (!radiusServer) { @@ -88,6 +98,7 @@ export class RadiusHandler { new plugins.typedrequest.TypedHandler( 'getVlanMappings', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'radius:read' }); const radiusServer = this.opsServerRef.dcRouterRef.radiusServer; if (!radiusServer) { @@ -124,6 +135,10 @@ export class RadiusHandler { new plugins.typedrequest.TypedHandler( 'setVlanMapping', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'radius:write', + requireAdminIdentity: true, + }); const radiusServer = this.opsServerRef.dcRouterRef.radiusServer; if (!radiusServer) { @@ -156,6 +171,10 @@ export class RadiusHandler { new plugins.typedrequest.TypedHandler( 'removeVlanMapping', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'radius:write', + requireAdminIdentity: true, + }); const radiusServer = this.opsServerRef.dcRouterRef.radiusServer; if (!radiusServer) { @@ -177,6 +196,10 @@ export class RadiusHandler { new plugins.typedrequest.TypedHandler( 'updateVlanConfig', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'radius:write', + requireAdminIdentity: true, + }); const radiusServer = this.opsServerRef.dcRouterRef.radiusServer; if (!radiusServer) { @@ -209,6 +232,7 @@ export class RadiusHandler { new plugins.typedrequest.TypedHandler( 'testVlanAssignment', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'radius:read' }); const radiusServer = this.opsServerRef.dcRouterRef.radiusServer; if (!radiusServer) { @@ -243,6 +267,7 @@ export class RadiusHandler { new plugins.typedrequest.TypedHandler( 'getRadiusSessions', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'radius:read' }); const radiusServer = this.opsServerRef.dcRouterRef.radiusServer; if (!radiusServer) { @@ -292,6 +317,10 @@ export class RadiusHandler { new plugins.typedrequest.TypedHandler( 'disconnectRadiusSession', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'radius:write', + requireAdminIdentity: true, + }); const radiusServer = this.opsServerRef.dcRouterRef.radiusServer; if (!radiusServer) { @@ -317,6 +346,7 @@ export class RadiusHandler { new plugins.typedrequest.TypedHandler( 'getRadiusAccountingSummary', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'radius:read' }); const radiusServer = this.opsServerRef.dcRouterRef.radiusServer; if (!radiusServer) { @@ -354,6 +384,7 @@ export class RadiusHandler { new plugins.typedrequest.TypedHandler( 'getRadiusStatistics', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'radius:read' }); const radiusServer = this.opsServerRef.dcRouterRef.radiusServer; if (!radiusServer) { diff --git a/ts/opsserver/handlers/remoteingress.handler.ts b/ts/opsserver/handlers/remoteingress.handler.ts index e576660..e7993c6 100644 --- a/ts/opsserver/handlers/remoteingress.handler.ts +++ b/ts/opsserver/handlers/remoteingress.handler.ts @@ -1,6 +1,7 @@ import * as plugins from '../../plugins.js'; import type { OpsServer } from '../classes.opsserver.js'; import * as interfaces from '../../../ts_interfaces/index.js'; +import { requireOpsAuth } from '../helpers/auth.js'; export class RemoteIngressHandler { constructor(private opsServerRef: OpsServer) { @@ -18,6 +19,7 @@ export class RemoteIngressHandler { new plugins.typedrequest.TypedHandler( 'getRemoteIngresses', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'remote-ingress:read' }); const manager = this.opsServerRef.dcRouterRef.remoteIngressManager; if (!manager) { return { edges: [] }; @@ -46,6 +48,10 @@ export class RemoteIngressHandler { new plugins.typedrequest.TypedHandler( 'createRemoteIngress', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'remote-ingress:write', + requireAdminIdentity: true, + }); const manager = this.opsServerRef.dcRouterRef.remoteIngressManager; const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager; @@ -78,6 +84,10 @@ export class RemoteIngressHandler { new plugins.typedrequest.TypedHandler( 'deleteRemoteIngress', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'remote-ingress:write', + requireAdminIdentity: true, + }); const manager = this.opsServerRef.dcRouterRef.remoteIngressManager; const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager; @@ -103,6 +113,10 @@ export class RemoteIngressHandler { new plugins.typedrequest.TypedHandler( 'updateRemoteIngress', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'remote-ingress:write', + requireAdminIdentity: true, + }); const manager = this.opsServerRef.dcRouterRef.remoteIngressManager; const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager; @@ -148,6 +162,10 @@ export class RemoteIngressHandler { new plugins.typedrequest.TypedHandler( 'regenerateRemoteIngressSecret', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'remote-ingress:write', + requireAdminIdentity: true, + }); const manager = this.opsServerRef.dcRouterRef.remoteIngressManager; const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager; @@ -175,6 +193,7 @@ export class RemoteIngressHandler { new plugins.typedrequest.TypedHandler( 'getRemoteIngressStatus', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'remote-ingress:read' }); const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager; if (!tunnelManager) { return { statuses: [] }; @@ -189,6 +208,10 @@ export class RemoteIngressHandler { new plugins.typedrequest.TypedHandler( 'getRemoteIngressConnectionToken', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'remote-ingress:write', + requireAdminIdentity: true, + }); const manager = this.opsServerRef.dcRouterRef.remoteIngressManager; if (!manager) { return { success: false, message: 'RemoteIngress not configured' }; diff --git a/ts/opsserver/handlers/route-management.handler.ts b/ts/opsserver/handlers/route-management.handler.ts index 7a8e0aa..5b8b755 100644 --- a/ts/opsserver/handlers/route-management.handler.ts +++ b/ts/opsserver/handlers/route-management.handler.ts @@ -1,6 +1,7 @@ import * as plugins from '../../plugins.js'; import type { OpsServer } from '../classes.opsserver.js'; import * as interfaces from '../../../ts_interfaces/index.js'; +import { requireOpsAuth } from '../helpers/auth.js'; export class RouteManagementHandler { public typedrouter = new plugins.typedrequest.TypedRouter(); @@ -18,31 +19,11 @@ export class RouteManagementHandler { request: { identity?: interfaces.data.IIdentity; apiToken?: string }, requiredScope?: interfaces.data.TApiTokenScope, ): Promise { - // Try JWT identity first - if (request.identity?.jwt) { - try { - const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({ - identity: request.identity, - }); - 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'); + const auth = await requireOpsAuth(this.opsServerRef, request, { + scope: requiredScope, + requireAdminIdentity: requiredScope?.endsWith(':write'), + }); + return auth.userId; } private registerHandlers(): void { diff --git a/ts/opsserver/handlers/security.handler.ts b/ts/opsserver/handlers/security.handler.ts index 61eb5d0..d664f3b 100644 --- a/ts/opsserver/handlers/security.handler.ts +++ b/ts/opsserver/handlers/security.handler.ts @@ -2,6 +2,7 @@ import * as plugins from '../../plugins.js'; import type { OpsServer } from '../classes.opsserver.js'; import * as interfaces from '../../../ts_interfaces/index.js'; import { MetricsManager } from '../../monitoring/index.js'; +import { requireOpsAuth } from '../helpers/auth.js'; export class SecurityHandler { constructor(private opsServerRef: OpsServer) { @@ -17,6 +18,7 @@ export class SecurityHandler { new plugins.typedrequest.TypedHandler( 'getSecurityMetrics', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'security:read' }); const metrics = await this.collectSecurityMetrics(); return { metrics: { @@ -43,6 +45,7 @@ export class SecurityHandler { new plugins.typedrequest.TypedHandler( 'getActiveConnections', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'stats:read' }); const connections = await this.getActiveConnections(dataArg.protocol, dataArg.state); const connectionInfos: interfaces.data.IConnectionInfo[] = connections.map(conn => ({ id: conn.id, @@ -82,6 +85,7 @@ export class SecurityHandler { new plugins.typedrequest.TypedHandler( 'getNetworkStats', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'stats:read' }); // Get network stats from MetricsManager if available if (this.opsServerRef.dcRouterRef.metricsManager) { const networkStats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats(); @@ -136,6 +140,7 @@ export class SecurityHandler { new plugins.typedrequest.TypedHandler( 'getRateLimitStatus', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'security:read' }); const status = await this.getRateLimitStatus(dataArg.domain, dataArg.ip); const limits: interfaces.data.IRateLimitInfo[] = status.limits.map(limit => ({ domain: limit.identifier, @@ -161,7 +166,8 @@ export class SecurityHandler { router.addTypedHandler( new plugins.typedrequest.TypedHandler( 'listSecurityBlockRules', - async () => { + async (dataArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'security:read' }); const manager = this.opsServerRef.dcRouterRef.securityPolicyManager; return { rules: manager ? await manager.listBlockRules() : [] }; }, @@ -171,7 +177,8 @@ export class SecurityHandler { router.addTypedHandler( new plugins.typedrequest.TypedHandler( 'listIpIntelligence', - async () => { + async (dataArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'security:read' }); const manager = this.opsServerRef.dcRouterRef.securityPolicyManager; return { records: manager ? await manager.listIpIntelligence() : [] }; }, @@ -181,7 +188,8 @@ export class SecurityHandler { router.addTypedHandler( new plugins.typedrequest.TypedHandler( 'getCompiledSecurityPolicy', - async () => { + async (dataArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'security:read' }); const manager = this.opsServerRef.dcRouterRef.securityPolicyManager; return { policy: manager @@ -196,6 +204,7 @@ export class SecurityHandler { new plugins.typedrequest.TypedHandler( 'listSecurityPolicyAudit', async (dataArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'security:read' }); const manager = this.opsServerRef.dcRouterRef.securityPolicyManager; return { events: manager ? await manager.listAuditEvents(dataArg.limit || 100) : [] }; }, @@ -208,6 +217,10 @@ export class SecurityHandler { new plugins.typedrequest.TypedHandler( 'createSecurityBlockRule', async (dataArg) => { + const auth = await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'security:write', + requireAdminIdentity: true, + }); const manager = this.opsServerRef.dcRouterRef.securityPolicyManager; if (!manager) return { success: false, message: 'Security policy manager not initialized' }; const rule = await manager.createBlockRule({ @@ -216,7 +229,7 @@ export class SecurityHandler { matchMode: dataArg.matchMode, reason: dataArg.reason, enabled: dataArg.enabled, - }, dataArg.identity.userId); + }, auth.userId); return { success: true, rule }; }, ), @@ -226,6 +239,10 @@ export class SecurityHandler { new plugins.typedrequest.TypedHandler( 'updateSecurityBlockRule', async (dataArg) => { + const auth = await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'security:write', + requireAdminIdentity: true, + }); const manager = this.opsServerRef.dcRouterRef.securityPolicyManager; if (!manager) return { success: false, message: 'Security policy manager not initialized' }; const rule = await manager.updateBlockRule(dataArg.id, { @@ -233,7 +250,7 @@ export class SecurityHandler { matchMode: dataArg.matchMode, reason: dataArg.reason, enabled: dataArg.enabled, - }, dataArg.identity.userId); + }, auth.userId); return rule ? { success: true, rule } : { success: false, message: 'Rule not found' }; }, ), @@ -243,9 +260,13 @@ export class SecurityHandler { new plugins.typedrequest.TypedHandler( 'deleteSecurityBlockRule', async (dataArg) => { + const auth = await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'security:write', + requireAdminIdentity: true, + }); const manager = this.opsServerRef.dcRouterRef.securityPolicyManager; 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' }; }, ), @@ -255,6 +276,10 @@ export class SecurityHandler { new plugins.typedrequest.TypedHandler( 'refreshIpIntelligence', async (dataArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'security:write', + requireAdminIdentity: true, + }); const manager = this.opsServerRef.dcRouterRef.securityPolicyManager; if (!manager) return { success: false, message: 'Security policy manager not initialized' }; const record = await manager.refreshIpIntelligence(dataArg.ipAddress); diff --git a/ts/opsserver/handlers/source-profile.handler.ts b/ts/opsserver/handlers/source-profile.handler.ts index be90832..bfdedd7 100644 --- a/ts/opsserver/handlers/source-profile.handler.ts +++ b/ts/opsserver/handlers/source-profile.handler.ts @@ -1,6 +1,7 @@ import * as plugins from '../../plugins.js'; import type { OpsServer } from '../classes.opsserver.js'; import * as interfaces from '../../../ts_interfaces/index.js'; +import { requireOpsAuth } from '../helpers/auth.js'; export class SourceProfileHandler { public typedrouter = new plugins.typedrequest.TypedRouter(); @@ -14,29 +15,11 @@ export class SourceProfileHandler { request: { identity?: interfaces.data.IIdentity; apiToken?: string }, requiredScope?: interfaces.data.TApiTokenScope, ): Promise { - if (request.identity?.jwt) { - try { - const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({ - identity: request.identity, - }); - 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'); + const auth = await requireOpsAuth(this.opsServerRef, request, { + scope: requiredScope, + requireAdminIdentity: requiredScope?.endsWith(':write'), + }); + return auth.userId; } private registerHandlers(): void { diff --git a/ts/opsserver/handlers/stats.handler.ts b/ts/opsserver/handlers/stats.handler.ts index 9e9b8f1..5f0713c 100644 --- a/ts/opsserver/handlers/stats.handler.ts +++ b/ts/opsserver/handlers/stats.handler.ts @@ -4,6 +4,7 @@ import * as interfaces from '../../../ts_interfaces/index.js'; import { MetricsManager } from '../../monitoring/index.js'; import { SecurityLogger } from '../../security/classes.securitylogger.js'; import { commitinfo } from '../../00_commitinfo_data.js'; +import { requireOpsAuth } from '../helpers/auth.js'; export class StatsHandler { constructor(private opsServerRef: OpsServer) { @@ -19,6 +20,7 @@ export class StatsHandler { new plugins.typedrequest.TypedHandler( 'getServerStatistics', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'stats:read' }); const stats = await this.collectServerStats(); return { stats: { @@ -42,6 +44,7 @@ export class StatsHandler { new plugins.typedrequest.TypedHandler( 'getEmailStatistics', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'stats:read' }); const emailServer = this.opsServerRef.dcRouterRef.emailServer; if (!emailServer) { return { @@ -81,6 +84,7 @@ export class StatsHandler { new plugins.typedrequest.TypedHandler( 'getDnsStatistics', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'stats:read' }); const dnsServer = this.opsServerRef.dcRouterRef.dnsServer; if (!dnsServer) { return { @@ -118,6 +122,7 @@ export class StatsHandler { new plugins.typedrequest.TypedHandler( 'getQueueStatus', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'stats:read' }); const emailServer = this.opsServerRef.dcRouterRef.emailServer; const queues: interfaces.data.IQueueStatus[] = []; @@ -146,6 +151,7 @@ export class StatsHandler { new plugins.typedrequest.TypedHandler( 'getHealthStatus', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'stats:read' }); const health = await this.checkHealthStatus(); return { health: { @@ -171,6 +177,7 @@ export class StatsHandler { new plugins.typedrequest.TypedHandler( 'getCombinedMetrics', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'stats:read' }); const sections = dataArg.sections || { server: true, email: true, diff --git a/ts/opsserver/handlers/target-profile.handler.ts b/ts/opsserver/handlers/target-profile.handler.ts index 3b93b58..b7832dc 100644 --- a/ts/opsserver/handlers/target-profile.handler.ts +++ b/ts/opsserver/handlers/target-profile.handler.ts @@ -1,6 +1,7 @@ import * as plugins from '../../plugins.js'; import type { OpsServer } from '../classes.opsserver.js'; import * as interfaces from '../../../ts_interfaces/index.js'; +import { requireOpsAuth } from '../helpers/auth.js'; export class TargetProfileHandler { public typedrouter = new plugins.typedrequest.TypedRouter(); @@ -14,29 +15,11 @@ export class TargetProfileHandler { request: { identity?: interfaces.data.IIdentity; apiToken?: string }, requiredScope?: interfaces.data.TApiTokenScope, ): Promise { - if (request.identity?.jwt) { - try { - const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({ - identity: request.identity, - }); - 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'); + const auth = await requireOpsAuth(this.opsServerRef, request, { + scope: requiredScope, + requireAdminIdentity: requiredScope?.endsWith(':write'), + }); + return auth.userId; } private registerHandlers(): void { diff --git a/ts/opsserver/handlers/users.handler.ts b/ts/opsserver/handlers/users.handler.ts index 1d91cc2..08ec413 100644 --- a/ts/opsserver/handlers/users.handler.ts +++ b/ts/opsserver/handlers/users.handler.ts @@ -1,6 +1,7 @@ import * as plugins from '../../plugins.js'; import type { OpsServer } from '../classes.opsserver.js'; import * as interfaces from '../../../ts_interfaces/index.js'; +import { requireOpsAuth } from '../helpers/auth.js'; /** * Handler for OpsServer user accounts. Registers on adminRouter, @@ -20,7 +21,12 @@ export class UsersHandler { router.addTypedHandler( new plugins.typedrequest.TypedHandler( 'listUsers', - async (_dataArg) => { + async (dataArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'users:read', + requireAdminIdentity: true, + requireAdminToken: true, + }); const users = await this.opsServerRef.adminHandler.listUsers(); return { users }; }, @@ -30,23 +36,37 @@ export class UsersHandler { router.addTypedHandler( new plugins.typedrequest.TypedHandler( 'createUser', - async (dataArg) => this.opsServerRef.adminHandler.createUser({ - email: dataArg.email, - name: dataArg.name, - role: dataArg.role, - password: dataArg.password, - enableIdpGlobalAuth: dataArg.enableIdpGlobalAuth, - }), + async (dataArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'users:manage', + requireAdminIdentity: true, + requireAdminToken: true, + }); + return this.opsServerRef.adminHandler.createUser({ + email: dataArg.email, + name: dataArg.name, + role: dataArg.role, + password: dataArg.password, + enableIdpGlobalAuth: dataArg.enableIdpGlobalAuth, + }); + }, ), ); router.addTypedHandler( new plugins.typedrequest.TypedHandler( 'deleteUser', - async (dataArg) => this.opsServerRef.adminHandler.deleteUser({ - id: dataArg.id, - requestingUserId: dataArg.identity.userId, - }), + async (dataArg) => { + const auth = await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'users:manage', + requireAdminIdentity: true, + requireAdminToken: true, + }); + return this.opsServerRef.adminHandler.deleteUser({ + id: dataArg.id, + requestingUserId: auth.userId, + }); + }, ), ); } diff --git a/ts/opsserver/handlers/vpn.handler.ts b/ts/opsserver/handlers/vpn.handler.ts index bbf6933..bbcc72c 100644 --- a/ts/opsserver/handlers/vpn.handler.ts +++ b/ts/opsserver/handlers/vpn.handler.ts @@ -1,6 +1,7 @@ import * as plugins from '../../plugins.js'; import type { OpsServer } from '../classes.opsserver.js'; import * as interfaces from '../../../ts_interfaces/index.js'; +import { requireOpsAuth } from '../helpers/auth.js'; export class VpnHandler { constructor(private opsServerRef: OpsServer) { @@ -18,6 +19,7 @@ export class VpnHandler { new plugins.typedrequest.TypedHandler( 'getVpnClients', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'vpn:read' }); const manager = this.opsServerRef.dcRouterRef.vpnManager; if (!manager) { return { clients: [] }; @@ -49,6 +51,7 @@ export class VpnHandler { new plugins.typedrequest.TypedHandler( 'getVpnStatus', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'vpn:read' }); const manager = this.opsServerRef.dcRouterRef.vpnManager; const vpnConfig = this.opsServerRef.dcRouterRef.options.vpnConfig; if (!manager) { @@ -84,6 +87,7 @@ export class VpnHandler { new plugins.typedrequest.TypedHandler( 'getVpnConnectedClients', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'vpn:read' }); const manager = this.opsServerRef.dcRouterRef.vpnManager; if (!manager) { return { connectedClients: [] }; @@ -111,6 +115,10 @@ export class VpnHandler { new plugins.typedrequest.TypedHandler( 'createVpnClient', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'vpn:write', + requireAdminIdentity: true, + }); const manager = this.opsServerRef.dcRouterRef.vpnManager; if (!manager) { return { success: false, message: 'VPN not configured' }; @@ -168,6 +176,10 @@ export class VpnHandler { new plugins.typedrequest.TypedHandler( 'updateVpnClient', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'vpn:write', + requireAdminIdentity: true, + }); const manager = this.opsServerRef.dcRouterRef.vpnManager; if (!manager) { return { success: false, message: 'VPN not configured' }; @@ -198,6 +210,10 @@ export class VpnHandler { new plugins.typedrequest.TypedHandler( 'deleteVpnClient', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'vpn:write', + requireAdminIdentity: true, + }); const manager = this.opsServerRef.dcRouterRef.vpnManager; if (!manager) { return { success: false, message: 'VPN not configured' }; @@ -218,6 +234,10 @@ export class VpnHandler { new plugins.typedrequest.TypedHandler( 'enableVpnClient', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'vpn:write', + requireAdminIdentity: true, + }); const manager = this.opsServerRef.dcRouterRef.vpnManager; if (!manager) { return { success: false, message: 'VPN not configured' }; @@ -238,6 +258,10 @@ export class VpnHandler { new plugins.typedrequest.TypedHandler( 'disableVpnClient', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'vpn:write', + requireAdminIdentity: true, + }); const manager = this.opsServerRef.dcRouterRef.vpnManager; if (!manager) { return { success: false, message: 'VPN not configured' }; @@ -258,6 +282,10 @@ export class VpnHandler { new plugins.typedrequest.TypedHandler( 'rotateVpnClientKey', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'vpn:write', + requireAdminIdentity: true, + }); const manager = this.opsServerRef.dcRouterRef.vpnManager; if (!manager) { return { success: false, message: 'VPN not configured' }; @@ -281,6 +309,10 @@ export class VpnHandler { new plugins.typedrequest.TypedHandler( 'exportVpnClientConfig', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { + scope: 'vpn:write', + requireAdminIdentity: true, + }); const manager = this.opsServerRef.dcRouterRef.vpnManager; if (!manager) { return { success: false, message: 'VPN not configured' }; @@ -301,6 +333,7 @@ export class VpnHandler { new plugins.typedrequest.TypedHandler( 'getVpnClientTelemetry', async (dataArg, toolsArg) => { + await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'vpn:read' }); const manager = this.opsServerRef.dcRouterRef.vpnManager; if (!manager) { return { success: false, message: 'VPN not configured' }; diff --git a/ts/opsserver/handlers/workhoster.handler.ts b/ts/opsserver/handlers/workhoster.handler.ts index b58f564..95559aa 100644 --- a/ts/opsserver/handlers/workhoster.handler.ts +++ b/ts/opsserver/handlers/workhoster.handler.ts @@ -1,6 +1,7 @@ import * as plugins from '../../plugins.js'; import type { OpsServer } from '../classes.opsserver.js'; import * as interfaces from '../../../ts_interfaces/index.js'; +import { requireOpsAuth } from '../helpers/auth.js'; type TAuthContext = { userId: string; @@ -20,39 +21,23 @@ export class WorkHosterHandler { request: { identity?: interfaces.data.IIdentity; apiToken?: string }, requiredScope?: interfaces.data.TApiTokenScope, ): Promise { - if (request.identity?.jwt) { - try { - const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({ - identity: request.identity, - }); - 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'); + const auth = await requireOpsAuth(this.opsServerRef, request, { + scope: requiredScope, + requireAdminIdentity: requiredScope?.endsWith(':write'), + }); + return { userId: auth.userId, isAdmin: auth.isAdmin, token: auth.token }; } - private async requireAdmin(request: { identity?: interfaces.data.IIdentity }): Promise { - if (request.identity?.jwt) { - const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({ - identity: request.identity, - }); - if (isAdmin) return request.identity.userId; - } - throw new plugins.typedrequest.TypedResponseError('admin identity required'); + private async requireAdmin( + request: { identity?: interfaces.data.IIdentity; apiToken?: string }, + scope: interfaces.data.TApiTokenScope = 'gateway-clients:write', + ): Promise { + const auth = await requireOpsAuth(this.opsServerRef, request, { + scope, + requireAdminIdentity: true, + requireAdminToken: true, + }); + return auth.userId; } private registerHandlers(): void { @@ -83,7 +68,7 @@ export class WorkHosterHandler { new plugins.typedrequest.TypedHandler( 'listGatewayClients', async (dataArg) => { - await this.requireAdmin(dataArg); + await this.requireAdmin(dataArg, 'gateway-clients:read'); return { gatewayClients: await this.listManagedGatewayClients() }; }, ), @@ -154,7 +139,7 @@ export class WorkHosterHandler { new plugins.typedrequest.TypedHandler( 'createGatewayClientToken', 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 tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager; if (!gatewayClient || !gatewayClient.enabled) { diff --git a/ts/opsserver/helpers/auth.ts b/ts/opsserver/helpers/auth.ts new file mode 100644 index 0000000..970a279 --- /dev/null +++ b/ts/opsserver/helpers/auth.ts @@ -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 { + 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'); +} diff --git a/ts_interfaces/data/route-management.ts b/ts_interfaces/data/route-management.ts index 9abd857..9e303cd 100644 --- a/ts_interfaces/data/route-management.ts +++ b/ts_interfaces/data/route-management.ts @@ -8,22 +8,52 @@ export type IRouteSecurity = NonNullable; // Route Management Data Types // ============================================================================ -export type TApiTokenScope = - | '*' - | 'routes:read' | 'routes:write' - | 'config:read' - | 'certificates:read' | 'certificates:write' - | 'tokens:read' | 'tokens: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' - | 'gateway-clients:read' | 'gateway-clients:write' - | 'workhosters:read' | 'workhosters:write'; +export const apiTokenScopes = [ + '*', + 'routes:read', + 'routes:write', + 'config:read', + 'stats:read', + 'logs:read', + 'security:read', + 'security:write', + 'emails:read', + 'emails:write', + 'certificates:read', + 'certificates:write', + 'tokens:read', + 'tokens:manage', + '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'; /** @deprecated Use TGatewayClientType. */ diff --git a/ts_interfaces/requests/api-tokens.ts b/ts_interfaces/requests/api-tokens.ts index d054c9b..841ef9b 100644 --- a/ts_interfaces/requests/api-tokens.ts +++ b/ts_interfaces/requests/api-tokens.ts @@ -16,7 +16,8 @@ export interface IReq_CreateApiToken extends plugins.typedrequestInterfaces.impl > { method: 'createApiToken'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; name: string; scopes: TApiTokenScope[]; policy?: IApiTokenPolicy; @@ -39,7 +40,8 @@ export interface IReq_ListApiTokens extends plugins.typedrequestInterfaces.imple > { method: 'listApiTokens'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; }; response: { tokens: IApiTokenInfo[]; @@ -55,7 +57,8 @@ export interface IReq_RevokeApiToken extends plugins.typedrequestInterfaces.impl > { method: 'revokeApiToken'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; id: string; }; response: { @@ -74,7 +77,8 @@ export interface IReq_RollApiToken extends plugins.typedrequestInterfaces.implem > { method: 'rollApiToken'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; id: string; }; response: { @@ -93,7 +97,8 @@ export interface IReq_ToggleApiToken extends plugins.typedrequestInterfaces.impl > { method: 'toggleApiToken'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; id: string; enabled: boolean; }; diff --git a/ts_interfaces/requests/combined.stats.ts b/ts_interfaces/requests/combined.stats.ts index 47b27db..6474db2 100644 --- a/ts_interfaces/requests/combined.stats.ts +++ b/ts_interfaces/requests/combined.stats.ts @@ -3,7 +3,8 @@ import type * as data from '../data/index.js'; export interface IReq_GetCombinedMetrics { method: 'getCombinedMetrics'; request: { - identity: data.IIdentity; + identity?: data.IIdentity; + apiToken?: string; sections?: { server?: boolean; email?: boolean; @@ -26,4 +27,4 @@ export interface IReq_GetCombinedMetrics { }; timestamp: number; }; -} \ No newline at end of file +} diff --git a/ts_interfaces/requests/config.ts b/ts_interfaces/requests/config.ts index b8ac0d1..a88c7ba 100644 --- a/ts_interfaces/requests/config.ts +++ b/ts_interfaces/requests/config.ts @@ -82,7 +82,8 @@ export interface IReq_GetConfiguration extends plugins.typedrequestInterfaces.im > { method: 'getConfiguration'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; section?: string; }; response: { diff --git a/ts_interfaces/requests/email-ops.ts b/ts_interfaces/requests/email-ops.ts index 6e4bad6..00b2f28 100644 --- a/ts_interfaces/requests/email-ops.ts +++ b/ts_interfaces/requests/email-ops.ts @@ -68,7 +68,8 @@ export interface IReq_GetAllEmails extends plugins.typedrequestInterfaces.implem > { method: 'getAllEmails'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; }; response: { emails: IEmail[]; @@ -84,7 +85,8 @@ export interface IReq_GetEmailDetail extends plugins.typedrequestInterfaces.impl > { method: 'getEmailDetail'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; emailId: string; }; response: { @@ -101,7 +103,8 @@ export interface IReq_ResendEmail extends plugins.typedrequestInterfaces.impleme > { method: 'resendEmail'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; emailId: string; }; response: { diff --git a/ts_interfaces/requests/logs.ts b/ts_interfaces/requests/logs.ts index b07cebd..045fbda 100644 --- a/ts_interfaces/requests/logs.ts +++ b/ts_interfaces/requests/logs.ts @@ -9,7 +9,8 @@ export interface IReq_GetRecentLogs extends plugins.typedrequestInterfaces.imple > { method: 'getRecentLogs'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; level?: 'debug' | 'info' | 'warn' | 'error'; category?: 'smtp' | 'dns' | 'security' | 'system' | 'email'; limit?: number; @@ -31,7 +32,8 @@ export interface IReq_GetLogStream extends plugins.typedrequestInterfaces.implem > { method: 'getLogStream'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; follow?: boolean; filters?: { level?: string[]; @@ -53,4 +55,4 @@ export interface IReq_PushLogEntry extends plugins.typedrequestInterfaces.implem entry: statsInterfaces.ILogEntry; }; response: {}; -} \ No newline at end of file +} diff --git a/ts_interfaces/requests/radius.ts b/ts_interfaces/requests/radius.ts index 5ae502d..72c4696 100644 --- a/ts_interfaces/requests/radius.ts +++ b/ts_interfaces/requests/radius.ts @@ -14,7 +14,8 @@ export interface IReq_GetRadiusClients extends plugins.typedrequestInterfaces.im > { method: 'getRadiusClients'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; }; response: { clients: Array<{ @@ -35,7 +36,8 @@ export interface IReq_SetRadiusClient extends plugins.typedrequestInterfaces.imp > { method: 'setRadiusClient'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; client: { name: string; ipRange: string; @@ -59,7 +61,8 @@ export interface IReq_RemoveRadiusClient extends plugins.typedrequestInterfaces. > { method: 'removeRadiusClient'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; name: string; }; response: { @@ -81,7 +84,8 @@ export interface IReq_GetVlanMappings extends plugins.typedrequestInterfaces.imp > { method: 'getVlanMappings'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; }; response: { mappings: Array<{ @@ -108,7 +112,8 @@ export interface IReq_SetVlanMapping extends plugins.typedrequestInterfaces.impl > { method: 'setVlanMapping'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; mapping: { mac: string; vlan: number; @@ -139,7 +144,8 @@ export interface IReq_RemoveVlanMapping extends plugins.typedrequestInterfaces.i > { method: 'removeVlanMapping'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; mac: string; }; response: { @@ -157,7 +163,8 @@ export interface IReq_UpdateVlanConfig extends plugins.typedrequestInterfaces.im > { method: 'updateVlanConfig'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; defaultVlan?: number; allowUnknownMacs?: boolean; }; @@ -179,7 +186,8 @@ export interface IReq_TestVlanAssignment extends plugins.typedrequestInterfaces. > { method: 'testVlanAssignment'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; mac: string; }; response: { @@ -207,7 +215,8 @@ export interface IReq_GetRadiusSessions extends plugins.typedrequestInterfaces.i > { method: 'getRadiusSessions'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; filter?: { username?: string; nasIpAddress?: string; @@ -243,7 +252,8 @@ export interface IReq_DisconnectRadiusSession extends plugins.typedrequestInterf > { method: 'disconnectRadiusSession'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; sessionId: string; reason?: string; }; @@ -262,7 +272,8 @@ export interface IReq_GetRadiusAccountingSummary extends plugins.typedrequestInt > { method: 'getRadiusAccountingSummary'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; startTime: number; endTime: number; }; @@ -296,7 +307,8 @@ export interface IReq_GetRadiusStatistics extends plugins.typedrequestInterfaces > { method: 'getRadiusStatistics'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; }; response: { stats: { diff --git a/ts_interfaces/requests/remoteingress.ts b/ts_interfaces/requests/remoteingress.ts index 6d2e174..e0b780e 100644 --- a/ts_interfaces/requests/remoteingress.ts +++ b/ts_interfaces/requests/remoteingress.ts @@ -15,7 +15,8 @@ export interface IReq_CreateRemoteIngress extends plugins.typedrequestInterfaces > { method: 'createRemoteIngress'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; name: string; listenPorts?: number[]; autoDerivePorts?: boolean; @@ -36,7 +37,8 @@ export interface IReq_DeleteRemoteIngress extends plugins.typedrequestInterfaces > { method: 'deleteRemoteIngress'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; id: string; }; response: { @@ -54,7 +56,8 @@ export interface IReq_UpdateRemoteIngress extends plugins.typedrequestInterfaces > { method: 'updateRemoteIngress'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; id: string; name?: string; listenPorts?: number[]; @@ -77,7 +80,8 @@ export interface IReq_RegenerateRemoteIngressSecret extends plugins.typedrequest > { method: 'regenerateRemoteIngressSecret'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; id: string; }; response: { @@ -95,7 +99,8 @@ export interface IReq_GetRemoteIngresses extends plugins.typedrequestInterfaces. > { method: 'getRemoteIngresses'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; }; response: { edges: IRemoteIngress[]; @@ -111,7 +116,8 @@ export interface IReq_GetRemoteIngressStatus extends plugins.typedrequestInterfa > { method: 'getRemoteIngressStatus'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; }; response: { statuses: IRemoteIngressStatus[]; @@ -128,7 +134,8 @@ export interface IReq_GetRemoteIngressConnectionToken extends plugins.typedreque > { method: 'getRemoteIngressConnectionToken'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; edgeId: string; hubHost?: string; }; diff --git a/ts_interfaces/requests/security-policy.ts b/ts_interfaces/requests/security-policy.ts index 04a9baa..320946d 100644 --- a/ts_interfaces/requests/security-policy.ts +++ b/ts_interfaces/requests/security-policy.ts @@ -15,7 +15,8 @@ export interface IReq_ListSecurityBlockRules extends plugins.typedrequestInterfa > { method: 'listSecurityBlockRules'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; }; response: { rules: ISecurityBlockRule[]; @@ -28,7 +29,8 @@ export interface IReq_CreateSecurityBlockRule extends plugins.typedrequestInterf > { method: 'createSecurityBlockRule'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; type: TSecurityBlockRuleType; value: string; matchMode?: TSecurityBlockRuleMatchMode; @@ -48,7 +50,8 @@ export interface IReq_UpdateSecurityBlockRule extends plugins.typedrequestInterf > { method: 'updateSecurityBlockRule'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; id: string; value?: string; matchMode?: TSecurityBlockRuleMatchMode; @@ -68,7 +71,8 @@ export interface IReq_DeleteSecurityBlockRule extends plugins.typedrequestInterf > { method: 'deleteSecurityBlockRule'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; id: string; }; response: { @@ -83,7 +87,8 @@ export interface IReq_ListIpIntelligence extends plugins.typedrequestInterfaces. > { method: 'listIpIntelligence'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; }; response: { records: IIpIntelligenceRecord[]; @@ -96,7 +101,8 @@ export interface IReq_GetCompiledSecurityPolicy extends plugins.typedrequestInte > { method: 'getCompiledSecurityPolicy'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; }; response: { policy: ISecurityCompiledPolicy; @@ -109,7 +115,8 @@ export interface IReq_ListSecurityPolicyAudit extends plugins.typedrequestInterf > { method: 'listSecurityPolicyAudit'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; limit?: number; }; response: { @@ -123,7 +130,8 @@ export interface IReq_RefreshIpIntelligence extends plugins.typedrequestInterfac > { method: 'refreshIpIntelligence'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; ipAddress: string; }; response: { diff --git a/ts_interfaces/requests/stats.ts b/ts_interfaces/requests/stats.ts index 520c201..b16a54d 100644 --- a/ts_interfaces/requests/stats.ts +++ b/ts_interfaces/requests/stats.ts @@ -9,7 +9,8 @@ export interface IReq_GetServerStatistics extends plugins.typedrequestInterfaces > { method: 'getServerStatistics'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; includeHistory?: boolean; timeRange?: '1h' | '6h' | '24h' | '7d' | '30d'; }; @@ -29,7 +30,8 @@ export interface IReq_GetEmailStatistics extends plugins.typedrequestInterfaces. > { method: 'getEmailStatistics'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; timeRange?: '1h' | '6h' | '24h' | '7d' | '30d'; domain?: string; includeDetails?: boolean; @@ -49,7 +51,8 @@ export interface IReq_GetDnsStatistics extends plugins.typedrequestInterfaces.im > { method: 'getDnsStatistics'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; timeRange?: '1h' | '6h' | '24h' | '7d' | '30d'; domain?: string; includeQueryTypes?: boolean; @@ -69,7 +72,8 @@ export interface IReq_GetRateLimitStatus extends plugins.typedrequestInterfaces. > { method: 'getRateLimitStatus'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; domain?: string; ip?: string; includeBlocked?: boolean; @@ -91,7 +95,8 @@ export interface IReq_GetSecurityMetrics extends plugins.typedrequestInterfaces. > { method: 'getSecurityMetrics'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; timeRange?: '1h' | '6h' | '24h' | '7d' | '30d'; includeDetails?: boolean; }; @@ -112,7 +117,8 @@ export interface IReq_GetActiveConnections extends plugins.typedrequestInterface > { method: 'getActiveConnections'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; protocol?: 'smtp' | 'smtps' | 'http' | 'https'; state?: string; }; @@ -137,7 +143,8 @@ export interface IReq_GetQueueStatus extends plugins.typedrequestInterfaces.impl > { method: 'getQueueStatus'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; queueName?: string; }; response: { @@ -153,7 +160,8 @@ export interface IReq_GetHealthStatus extends plugins.typedrequestInterfaces.imp > { method: 'getHealthStatus'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; detailed?: boolean; }; response: { @@ -168,7 +176,8 @@ export interface IReq_GetNetworkStats extends plugins.typedrequestInterfaces.imp > { method: 'getNetworkStats'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; }; response: { connectionsByIP: Array<{ ip: string; count: number }>; @@ -185,4 +194,4 @@ export interface IReq_GetNetworkStats extends plugins.typedrequestInterfaces.imp frontendProtocols?: statsInterfaces.IProtocolDistribution | null; backendProtocols?: statsInterfaces.IProtocolDistribution | null; }; -} \ No newline at end of file +} diff --git a/ts_interfaces/requests/users.ts b/ts_interfaces/requests/users.ts index bf1534d..578d629 100644 --- a/ts_interfaces/requests/users.ts +++ b/ts_interfaces/requests/users.ts @@ -14,7 +14,8 @@ export interface IReq_ListUsers extends plugins.typedrequestInterfaces.implement > { method: 'listUsers'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; }; response: { users: IAdminUserProjection[]; @@ -30,7 +31,8 @@ export interface IReq_CreateUser extends plugins.typedrequestInterfaces.implemen > { method: 'createUser'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; email: string; name?: string; role: TUserManagementRole; @@ -53,7 +55,8 @@ export interface IReq_DeleteUser extends plugins.typedrequestInterfaces.implemen > { method: 'deleteUser'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; id: string; }; response: { diff --git a/ts_interfaces/requests/vpn.ts b/ts_interfaces/requests/vpn.ts index 28de074..71f97f3 100644 --- a/ts_interfaces/requests/vpn.ts +++ b/ts_interfaces/requests/vpn.ts @@ -15,7 +15,8 @@ export interface IReq_GetVpnClients extends plugins.typedrequestInterfaces.imple > { method: 'getVpnClients'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; }; response: { clients: IVpnClient[]; @@ -31,7 +32,8 @@ export interface IReq_GetVpnStatus extends plugins.typedrequestInterfaces.implem > { method: 'getVpnStatus'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; }; response: { status: IVpnServerStatus; @@ -47,7 +49,8 @@ export interface IReq_CreateVpnClient extends plugins.typedrequestInterfaces.imp > { method: 'createVpnClient'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; clientId: string; targetProfileIds?: string[]; description?: string; @@ -78,7 +81,8 @@ export interface IReq_UpdateVpnClient extends plugins.typedrequestInterfaces.imp > { method: 'updateVpnClient'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; clientId: string; description?: string; targetProfileIds?: string[]; @@ -106,7 +110,8 @@ export interface IReq_GetVpnConnectedClients extends plugins.typedrequestInterfa > { method: 'getVpnConnectedClients'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; }; response: { connectedClients: IVpnConnectedClient[]; @@ -122,7 +127,8 @@ export interface IReq_DeleteVpnClient extends plugins.typedrequestInterfaces.imp > { method: 'deleteVpnClient'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; clientId: string; }; response: { @@ -140,7 +146,8 @@ export interface IReq_EnableVpnClient extends plugins.typedrequestInterfaces.imp > { method: 'enableVpnClient'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; clientId: string; }; response: { @@ -158,7 +165,8 @@ export interface IReq_DisableVpnClient extends plugins.typedrequestInterfaces.im > { method: 'disableVpnClient'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; clientId: string; }; response: { @@ -176,7 +184,8 @@ export interface IReq_RotateVpnClientKey extends plugins.typedrequestInterfaces. > { method: 'rotateVpnClientKey'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; clientId: string; }; response: { @@ -196,7 +205,8 @@ export interface IReq_ExportVpnClientConfig extends plugins.typedrequestInterfac > { method: 'exportVpnClientConfig'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; clientId: string; format: 'smartvpn' | 'wireguard'; }; @@ -216,7 +226,8 @@ export interface IReq_GetVpnClientTelemetry extends plugins.typedrequestInterfac > { method: 'getVpnClientTelemetry'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; clientId: string; }; response: { diff --git a/ts_interfaces/requests/workhoster.ts b/ts_interfaces/requests/workhoster.ts index f3d3ee9..83686ec 100644 --- a/ts_interfaces/requests/workhoster.ts +++ b/ts_interfaces/requests/workhoster.ts @@ -53,7 +53,8 @@ export interface IReq_ListGatewayClients extends plugins.typedrequestInterfaces. > { method: 'listGatewayClients'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; }; response: { gatewayClients: IGatewayClient[]; @@ -66,7 +67,8 @@ export interface IReq_CreateGatewayClient extends plugins.typedrequestInterfaces > { method: 'createGatewayClient'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; id?: string; type: IGatewayClient['type']; name: string; @@ -88,7 +90,8 @@ export interface IReq_UpdateGatewayClient extends plugins.typedrequestInterfaces > { method: 'updateGatewayClient'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; id: string; name?: string; description?: string; @@ -110,7 +113,8 @@ export interface IReq_DeleteGatewayClient extends plugins.typedrequestInterfaces > { method: 'deleteGatewayClient'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; id: string; }; response: { @@ -125,7 +129,8 @@ export interface IReq_CreateGatewayClientToken extends plugins.typedrequestInter > { method: 'createGatewayClientToken'; request: { - identity: authInterfaces.IIdentity; + identity?: authInterfaces.IIdentity; + apiToken?: string; gatewayClientId: string; name?: string; expiresInDays?: number | null; diff --git a/ts_migrations/index.ts b/ts_migrations/index.ts index 2d84d16..0c8a17e 100644 --- a/ts_migrations/index.ts +++ b/ts_migrations/index.ts @@ -89,6 +89,8 @@ export async function createMigrationRunner( db: db as any, // Brand-new installs skip all migrations and stamp directly to the current version. 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 diff --git a/ts_web/elements/access/ops-view-apitokens.ts b/ts_web/elements/access/ops-view-apitokens.ts index f949f3c..97665b3 100644 --- a/ts_web/elements/access/ops-view-apitokens.ts +++ b/ts_web/elements/access/ops-view-apitokens.ts @@ -200,26 +200,7 @@ export class OpsViewApiTokens extends DeesElement { private async showCreateTokenDialog() { const { DeesModal } = await import('@design.estate/dees-catalog'); - const allScopes = [ - '*', - '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', - ]; + const allScopes = [...interfaces.data.apiTokenScopes]; await DeesModal.createAndShow({ heading: 'Create API Token',