From d5a36750ca04a8b42121a2c8d4394de8726cef9e Mon Sep 17 00:00:00 2001 From: Casey Brooks Date: Thu, 19 Mar 2026 17:05:55 +0000 Subject: [PATCH 1/2] refactor: remove grpc auth --- .../containers.docker.integration.test.ts | 11 +- __tests__/helpers/grpc-test-client.ts | 11 +- src/contracts/auth.ts | 165 --------------- src/contracts/json.ts | 23 --- src/index.ts | 2 - src/service/config.ts | 10 - src/service/grpc/server.ts | 193 +----------------- src/service/main.ts | 5 +- test/e2e/docker-runner.e2e.test.ts | 7 +- 9 files changed, 10 insertions(+), 417 deletions(-) delete mode 100644 src/contracts/auth.ts delete mode 100644 src/contracts/json.ts diff --git a/__tests__/containers.docker.integration.test.ts b/__tests__/containers.docker.integration.test.ts index 8c33788..f8f64f5 100644 --- a/__tests__/containers.docker.integration.test.ts +++ b/__tests__/containers.docker.integration.test.ts @@ -5,7 +5,7 @@ import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'; import { Server, ServerCredentials, credentials } from '@grpc/grpc-js'; import type { RunnerConfig } from '../src/service/config'; -import { ContainerService, NonceCache } from '../src'; +import { ContainerService } from '../src'; import { createRunnerGrpcServer } from '../src/service/grpc/server'; import { RunnerServiceGrpcClient } from '../src/proto/grpc.js'; import { createGrpcTestClient, type GrpcTestClient } from './helpers/grpc-test-client'; @@ -24,8 +24,6 @@ if (shouldSkip) { console.warn(`Skipping docker-runner docker-backed integration tests: ${reason}`); } -const RUNNER_SECRET = 'docker-runner-integration-secret'; - describeOrSkip('docker-runner docker-backed container lifecycle', () => { let grpcAddress: string; let client: InstanceType; @@ -35,24 +33,21 @@ describeOrSkip('docker-runner docker-backed container lifecycle', () => { beforeAll(async () => { const config: RunnerConfig = { - sharedSecret: RUNNER_SECRET, - signatureTtlMs: 60_000, dockerSocket: hasSocket ? DEFAULT_SOCKET : '', logLevel: 'error', grpcHost: '127.0.0.1', grpcPort: 0, }; - const nonceCache = new NonceCache({ ttlMs: config.signatureTtlMs }); const previousSocket = process.env.DOCKER_SOCKET; if (config.dockerSocket) { process.env.DOCKER_SOCKET = config.dockerSocket; } const containers = new ContainerService(); - const server = createRunnerGrpcServer({ config, containers, nonceCache }); + const server = createRunnerGrpcServer({ config, containers }); const address = await bindServer(server, config.grpcHost); grpcAddress = address; client = new RunnerServiceGrpcClient(address, credentials.createInsecure()); - grpcTestClient = createGrpcTestClient({ client, secret: RUNNER_SECRET }); + grpcTestClient = createGrpcTestClient({ client }); await grpcTestClient.ready(); shutdown = async () => { await new Promise((resolve) => { diff --git a/__tests__/helpers/grpc-test-client.ts b/__tests__/helpers/grpc-test-client.ts index 358703a..5ae59fa 100644 --- a/__tests__/helpers/grpc-test-client.ts +++ b/__tests__/helpers/grpc-test-client.ts @@ -1,7 +1,6 @@ import { Metadata, type ServiceError } from '@grpc/grpc-js'; import { create } from '@bufbuild/protobuf'; -import { buildAuthHeaders } from '../../src/contracts/auth'; import { containerOptsToStartWorkloadRequest } from '../../src/contracts/workload.grpc'; import { RUNNER_SERVICE_INSPECT_WORKLOAD_PATH, @@ -51,17 +50,11 @@ export type GrpcTestClient = { export function createGrpcTestClient(options: { client: RunnerServiceGrpcClientInstance; - secret: string; }): GrpcTestClient { - const { client, secret } = options; + const { client } = options; const metadataFor = (path: string): Metadata => { - const headers = buildAuthHeaders({ method: 'POST', path, body: '', secret }); - const metadata = new Metadata(); - for (const [key, value] of Object.entries(headers)) { - metadata.set(key, value); - } - return metadata; + return new Metadata(); }; const unary = async ( diff --git a/src/contracts/auth.ts b/src/contracts/auth.ts deleted file mode 100644 index 0ffbdc0..0000000 --- a/src/contracts/auth.ts +++ /dev/null @@ -1,165 +0,0 @@ -import crypto from 'node:crypto'; -import { canonicalJsonStringify } from './json.ts'; - -export type SignatureHeaders = { - timestamp: string; - nonce: string; - signature: string; -}; - -const HEADER_TIMESTAMP = 'x-dr-timestamp'; -const HEADER_NONCE = 'x-dr-nonce'; -const HEADER_SIGNATURE = 'x-dr-signature'; - -export const REQUIRED_HEADERS = [HEADER_TIMESTAMP, HEADER_NONCE, HEADER_SIGNATURE]; - -export function hashBody(body: string | Buffer): string { - const data = typeof body === 'string' ? Buffer.from(body) : body; - return crypto.createHash('sha256').update(data).digest('base64'); -} - -export function buildSignaturePayload(parts: { - method: string; - path: string; - timestamp: string; - nonce: string; - bodyHash: string; -}): string { - return `${parts.method.toUpperCase()}\n${parts.path}\n${parts.timestamp}\n${parts.nonce}\n${parts.bodyHash}`; -} - -export function signPayload(secret: string, payload: string): string { - return crypto.createHmac('sha256', secret).update(payload).digest('base64'); -} - -export function canonicalBodyString(body: unknown): string { - if (body === undefined || body === null || body === '') return ''; - if (typeof body === 'string') return body; - return canonicalJsonStringify(body); -} - -export type NonceCacheOptions = { - ttlMs?: number; - maxEntries?: number; -}; - -export class NonceCache { - private readonly ttlMs: number; - private readonly maxEntries: number; - private readonly store = new Map(); - - constructor(options: NonceCacheOptions = {}) { - this.ttlMs = typeof options.ttlMs === 'number' ? options.ttlMs : 60_000; - this.maxEntries = typeof options.maxEntries === 'number' ? options.maxEntries : 1000; - } - - has(nonce: string): boolean { - this.evictExpired(); - return this.store.has(nonce); - } - - add(nonce: string): void { - this.evictExpired(); - if (this.store.size >= this.maxEntries) { - const [firstKey] = this.store.keys(); - if (firstKey) this.store.delete(firstKey); - } - this.store.set(nonce, Date.now()); - } - - private evictExpired(): void { - const now = Date.now(); - for (const [nonce, ts] of this.store.entries()) { - if (now - ts > this.ttlMs) this.store.delete(nonce); - } - } -} - -export type BuildHeadersInput = { - method: string; - path: string; - body?: unknown; - secret: string; - timestamp?: number; - nonce?: string; -}; - -export function buildAuthHeaders(input: BuildHeadersInput): Record { - const timestamp = (input.timestamp ?? Date.now()).toString(); - const nonce = input.nonce ?? crypto.randomUUID(); - const bodyString = canonicalBodyString(input.body ?? ''); - const bodyHash = hashBody(bodyString); - const payload = buildSignaturePayload({ - method: input.method, - path: input.path, - timestamp, - nonce, - bodyHash, - }); - const signature = signPayload(input.secret, payload); - return { - [HEADER_TIMESTAMP]: timestamp, - [HEADER_NONCE]: nonce, - [HEADER_SIGNATURE]: signature, - }; -} - -export type VerifyHeadersInput = { - headers: Record; - method: string; - path: string; - body?: unknown; - secret: string; - clockSkewMs?: number; - nonceCache: NonceCache; -}; - -export function extractHeader(headers: VerifyHeadersInput['headers'], name: string): string | undefined { - const value = headers[name] ?? headers[name.toLowerCase()]; - if (Array.isArray(value)) return value[0]; - return value as string | undefined; -} - -export function verifyAuthHeaders(input: VerifyHeadersInput): { ok: boolean; code?: string; message?: string } { - const clockSkewMs = typeof input.clockSkewMs === 'number' ? input.clockSkewMs : 60_000; - const timestampStr = extractHeader(input.headers, HEADER_TIMESTAMP); - const nonce = extractHeader(input.headers, HEADER_NONCE); - const signature = extractHeader(input.headers, HEADER_SIGNATURE); - if (!timestampStr || !nonce || !signature) { - return { ok: false, code: 'missing_headers', message: 'Authentication headers missing' }; - } - const timestampNum = Number(timestampStr); - if (!Number.isFinite(timestampNum)) { - return { ok: false, code: 'invalid_timestamp', message: 'Timestamp invalid' }; - } - const now = Date.now(); - if (Math.abs(now - timestampNum) > clockSkewMs) { - return { ok: false, code: 'timestamp_out_of_range', message: 'Timestamp outside allowed skew' }; - } - if (input.nonceCache.has(nonce)) { - return { ok: false, code: 'replayed_nonce', message: 'Nonce already used' }; - } - const bodyString = canonicalBodyString(input.body ?? ''); - const bodyHash = hashBody(bodyString); - const payload = buildSignaturePayload({ - method: input.method, - path: input.path, - timestamp: timestampStr, - nonce, - bodyHash, - }); - const expectedSignature = signPayload(input.secret, payload); - if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) { - return { ok: false, code: 'invalid_signature', message: 'Signature mismatch' }; - } - input.nonceCache.add(nonce); - return { ok: true }; -} - -export function headerNames() { - return { - timestamp: HEADER_TIMESTAMP, - nonce: HEADER_NONCE, - signature: HEADER_SIGNATURE, - }; -} diff --git a/src/contracts/json.ts b/src/contracts/json.ts deleted file mode 100644 index 7f7c22b..0000000 --- a/src/contracts/json.ts +++ /dev/null @@ -1,23 +0,0 @@ -const isBufferLike = (val: unknown): val is Buffer | Uint8Array => - typeof Buffer !== 'undefined' && (Buffer.isBuffer(val) || val instanceof Uint8Array); - -export function canonicalize(value: unknown): unknown { - if (value instanceof Date) return value.toISOString(); - if (isBufferLike(value)) return Buffer.from(value).toString('base64'); - if (Array.isArray(value)) { - return value.map((item) => canonicalize(item)); - } - if (value && typeof value === 'object') { - const entries = Object.entries(value as Record); - return Object.fromEntries( - entries - .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)) - .map(([key, val]) => [key, canonicalize(val)]), - ); - } - return value; -} - -export function canonicalJsonStringify(value: unknown): string { - return JSON.stringify(canonicalize(value)); -} diff --git a/src/index.ts b/src/index.ts index 433a1ff..fb0aac0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,8 +6,6 @@ export * from './lib/containerRegistry.port.ts'; export * from './lib/dockerClient.port.ts'; export * from './lib/execTimeout.ts'; export * from './lib/types.ts'; -export * from './contracts/auth.ts'; -export * from './contracts/json.ts'; export * from './contracts/api.ts'; export * from './contracts/workload.grpc.ts'; export { createRunnerGrpcServer } from './service/grpc/server.ts'; diff --git a/src/service/config.ts b/src/service/config.ts index 8091fe1..ccf6af1 100644 --- a/src/service/config.ts +++ b/src/service/config.ts @@ -9,14 +9,6 @@ const runnerConfigSchema = z.object({ return Number.isFinite(num) ? num : 50051; }), grpcHost: z.string().default('0.0.0.0'), - sharedSecret: z.string().min(1, 'DOCKER_RUNNER_SHARED_SECRET is required'), - signatureTtlMs: z - .union([z.string(), z.number()]) - .default('60000') - .transform((value) => { - const num = typeof value === 'number' ? value : Number(value); - return Number.isFinite(num) ? num : 60_000; - }), dockerSocket: z.string().default('/var/run/docker.sock'), logLevel: z.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal']).default('info'), }); @@ -28,8 +20,6 @@ export function loadRunnerConfig(env: NodeJS.ProcessEnv = process.env): RunnerCo const parsed = runnerConfigSchema.safeParse({ grpcPort: grpcPortEnv, grpcHost: env.DOCKER_RUNNER_GRPC_HOST, - sharedSecret: env.DOCKER_RUNNER_SHARED_SECRET, - signatureTtlMs: env.DOCKER_RUNNER_SIGNATURE_TTL_MS, dockerSocket: env.DOCKER_SOCKET ?? env.DOCKER_RUNNER_SOCKET, logLevel: env.DOCKER_RUNNER_LOG_LEVEL, }); diff --git a/src/service/grpc/server.ts b/src/service/grpc/server.ts index 07e2070..53d5b23 100644 --- a/src/service/grpc/server.ts +++ b/src/service/grpc/server.ts @@ -69,29 +69,11 @@ import { WorkloadContainersSchema, WorkloadStatus, } from '../../proto/gen/agynio/api/runner/v1/runner_pb.js'; -import { - RUNNER_SERVICE_CANCEL_EXEC_PATH, - RUNNER_SERVICE_EXEC_PATH, - RUNNER_SERVICE_FIND_WORKLOADS_BY_LABELS_PATH, - RUNNER_SERVICE_GET_WORKLOAD_LABELS_PATH, - RUNNER_SERVICE_INSPECT_WORKLOAD_PATH, - RUNNER_SERVICE_LIST_WORKLOADS_BY_VOLUME_PATH, - RUNNER_SERVICE_PUT_ARCHIVE_PATH, - RUNNER_SERVICE_READY_PATH, - RUNNER_SERVICE_REMOVE_VOLUME_PATH, - RUNNER_SERVICE_REMOVE_WORKLOAD_PATH, - RUNNER_SERVICE_START_WORKLOAD_PATH, - RUNNER_SERVICE_STOP_WORKLOAD_PATH, - RUNNER_SERVICE_STREAM_EVENTS_PATH, - RUNNER_SERVICE_STREAM_WORKLOAD_LOGS_PATH, - RUNNER_SERVICE_TOUCH_WORKLOAD_PATH, - runnerServiceGrpcDefinition, -} from '../../proto/grpc.js'; +import { runnerServiceGrpcDefinition } from '../../proto/grpc.js'; import { timestampFromDate } from '@bufbuild/protobuf/wkt'; import { create } from '@bufbuild/protobuf'; -import type { ContainerService, InteractiveExecSession, LogsStreamSession, NonceCache } from '../../index.ts'; +import type { ContainerService, InteractiveExecSession, LogsStreamSession } from '../../index.ts'; import type { ContainerHandle } from '../../lib/container.handle.ts'; -import { verifyAuthHeaders } from '../../index.ts'; import type { RunnerConfig } from '../config.ts'; import { createDockerEventsParser } from '../dockerEvents.parser.ts'; import { startWorkloadRequestToContainerOpts } from '../../contracts/workload.grpc.ts'; @@ -101,7 +83,6 @@ type ExecStream = ServerDuplexStream; export type RunnerGrpcOptions = { config: RunnerConfig; containers: ContainerService; - nonceCache: NonceCache; }; type ExecutionContext = { @@ -330,15 +311,6 @@ function coerceDuration(value?: bigint): number | undefined { return num; } -function metadataToHeaders(metadata: Metadata): Record { - const raw = metadata.getMap(); - const headers: Record = {}; - for (const [key, value] of Object.entries(raw)) { - headers[key] = typeof value === 'string' ? value : value.toString('utf8'); - } - return headers; -} - function createRunnerError(code: string, message: string, retryable: boolean) { return create(ExecErrorSchema, { code, message, retryable }); } @@ -359,27 +331,6 @@ function writeResponse(call: ExecStream, response: ExecResponse): void { } } -function verifyGrpcAuth({ - metadata, - secret, - nonceCache, - path, -}: { - metadata: Metadata; - secret: string; - nonceCache: NonceCache; - path: string; -}) { - return verifyAuthHeaders({ - headers: metadataToHeaders(metadata), - method: 'POST', - path, - body: '', - secret, - nonceCache, - }); -} - function utf8Tail(data: string, maxBytes: number): Uint8Array { if (maxBytes <= 0) return new Uint8Array(); const encoded = utf8Encoder.encode(data); @@ -398,15 +349,6 @@ export function createRunnerGrpcServer(opts: RunnerGrpcOptions): Server { call: ServerUnaryCall, callback: (error: ServiceError | null, value?: ReadyResponse) => void, ) => { - const verification = verifyGrpcAuth({ - metadata: call.metadata, - secret: opts.config.sharedSecret, - nonceCache: opts.nonceCache, - path: RUNNER_SERVICE_READY_PATH, - }); - if (!verification.ok) { - return callback(toServiceError(status.UNAUTHENTICATED, verification.message ?? 'unauthorized')); - } try { await opts.containers.getDocker().ping(); } catch (error) { @@ -420,15 +362,6 @@ export function createRunnerGrpcServer(opts: RunnerGrpcOptions): Server { call: ServerUnaryCall, callback: (error: ServiceError | null, value?: StartWorkloadResponse) => void, ) => { - const verification = verifyGrpcAuth({ - metadata: call.metadata, - secret: opts.config.sharedSecret, - nonceCache: opts.nonceCache, - path: RUNNER_SERVICE_START_WORKLOAD_PATH, - }); - if (!verification.ok) { - return callback(toServiceError(status.UNAUTHENTICATED, verification.message ?? 'unauthorized')); - } if (!call.request?.main) { return callback(toServiceError(status.INVALID_ARGUMENT, 'main_container_required')); } @@ -535,15 +468,6 @@ export function createRunnerGrpcServer(opts: RunnerGrpcOptions): Server { call: ServerUnaryCall, callback: (error: ServiceError | null, value?: StopWorkloadResponse) => void, ) => { - const verification = verifyGrpcAuth({ - metadata: call.metadata, - secret: opts.config.sharedSecret, - nonceCache: opts.nonceCache, - path: RUNNER_SERVICE_STOP_WORKLOAD_PATH, - }); - if (!verification.ok) { - return callback(toServiceError(status.UNAUTHENTICATED, verification.message ?? 'unauthorized')); - } const workloadId = call.request.workloadId?.trim(); if (!workloadId) { return callback(toServiceError(status.INVALID_ARGUMENT, 'workload_id_required')); @@ -564,15 +488,6 @@ export function createRunnerGrpcServer(opts: RunnerGrpcOptions): Server { call: ServerUnaryCall, callback: (error: ServiceError | null, value?: RemoveWorkloadResponse) => void, ) => { - const verification = verifyGrpcAuth({ - metadata: call.metadata, - secret: opts.config.sharedSecret, - nonceCache: opts.nonceCache, - path: RUNNER_SERVICE_REMOVE_WORKLOAD_PATH, - }); - if (!verification.ok) { - return callback(toServiceError(status.UNAUTHENTICATED, verification.message ?? 'unauthorized')); - } const workloadId = call.request.workloadId?.trim(); if (!workloadId) { return callback(toServiceError(status.INVALID_ARGUMENT, 'workload_id_required')); @@ -595,15 +510,6 @@ export function createRunnerGrpcServer(opts: RunnerGrpcOptions): Server { call: ServerUnaryCall, callback: (error: ServiceError | null, value?: InspectWorkloadResponse) => void, ) => { - const verification = verifyGrpcAuth({ - metadata: call.metadata, - secret: opts.config.sharedSecret, - nonceCache: opts.nonceCache, - path: RUNNER_SERVICE_INSPECT_WORKLOAD_PATH, - }); - if (!verification.ok) { - return callback(toServiceError(status.UNAUTHENTICATED, verification.message ?? 'unauthorized')); - } const workloadId = call.request.workloadId?.trim(); if (!workloadId) { return callback(toServiceError(status.INVALID_ARGUMENT, 'workload_id_required')); @@ -645,15 +551,6 @@ export function createRunnerGrpcServer(opts: RunnerGrpcOptions): Server { call: ServerUnaryCall, callback: (error: ServiceError | null, value?: GetWorkloadLabelsResponse) => void, ) => { - const verification = verifyGrpcAuth({ - metadata: call.metadata, - secret: opts.config.sharedSecret, - nonceCache: opts.nonceCache, - path: RUNNER_SERVICE_GET_WORKLOAD_LABELS_PATH, - }); - if (!verification.ok) { - return callback(toServiceError(status.UNAUTHENTICATED, verification.message ?? 'unauthorized')); - } const workloadId = call.request.workloadId?.trim(); if (!workloadId) { return callback(toServiceError(status.INVALID_ARGUMENT, 'workload_id_required')); @@ -669,15 +566,6 @@ export function createRunnerGrpcServer(opts: RunnerGrpcOptions): Server { call: ServerUnaryCall, callback: (error: ServiceError | null, value?: FindWorkloadsByLabelsResponse) => void, ) => { - const verification = verifyGrpcAuth({ - metadata: call.metadata, - secret: opts.config.sharedSecret, - nonceCache: opts.nonceCache, - path: RUNNER_SERVICE_FIND_WORKLOADS_BY_LABELS_PATH, - }); - if (!verification.ok) { - return callback(toServiceError(status.UNAUTHENTICATED, verification.message ?? 'unauthorized')); - } const labels = call.request.labels ?? {}; if (!labels || Object.keys(labels).length === 0) { return callback(toServiceError(status.INVALID_ARGUMENT, 'labels_required')); @@ -698,15 +586,6 @@ export function createRunnerGrpcServer(opts: RunnerGrpcOptions): Server { call: ServerUnaryCall, callback: (error: ServiceError | null, value?: ListWorkloadsByVolumeResponse) => void, ) => { - const verification = verifyGrpcAuth({ - metadata: call.metadata, - secret: opts.config.sharedSecret, - nonceCache: opts.nonceCache, - path: RUNNER_SERVICE_LIST_WORKLOADS_BY_VOLUME_PATH, - }); - if (!verification.ok) { - return callback(toServiceError(status.UNAUTHENTICATED, verification.message ?? 'unauthorized')); - } const volumeName = call.request.volumeName?.trim(); if (!volumeName) { return callback(toServiceError(status.INVALID_ARGUMENT, 'volume_name_required')); @@ -722,15 +601,6 @@ export function createRunnerGrpcServer(opts: RunnerGrpcOptions): Server { call: ServerUnaryCall, callback: (error: ServiceError | null, value?: RemoveVolumeResponse) => void, ) => { - const verification = verifyGrpcAuth({ - metadata: call.metadata, - secret: opts.config.sharedSecret, - nonceCache: opts.nonceCache, - path: RUNNER_SERVICE_REMOVE_VOLUME_PATH, - }); - if (!verification.ok) { - return callback(toServiceError(status.UNAUTHENTICATED, verification.message ?? 'unauthorized')); - } const volumeName = call.request.volumeName?.trim(); if (!volumeName) { return callback(toServiceError(status.INVALID_ARGUMENT, 'volume_name_required')); @@ -746,15 +616,6 @@ export function createRunnerGrpcServer(opts: RunnerGrpcOptions): Server { call: ServerUnaryCall, callback: (error: ServiceError | null, value?: TouchWorkloadResponse) => void, ) => { - const verification = verifyGrpcAuth({ - metadata: call.metadata, - secret: opts.config.sharedSecret, - nonceCache: opts.nonceCache, - path: RUNNER_SERVICE_TOUCH_WORKLOAD_PATH, - }); - if (!verification.ok) { - return callback(toServiceError(status.UNAUTHENTICATED, verification.message ?? 'unauthorized')); - } const workloadId = call.request.workloadId?.trim(); if (!workloadId) { return callback(toServiceError(status.INVALID_ARGUMENT, 'workload_id_required')); @@ -770,15 +631,6 @@ export function createRunnerGrpcServer(opts: RunnerGrpcOptions): Server { call: ServerUnaryCall, callback: (error: ServiceError | null, value?: PutArchiveResponse) => void, ) => { - const verification = verifyGrpcAuth({ - metadata: call.metadata, - secret: opts.config.sharedSecret, - nonceCache: opts.nonceCache, - path: RUNNER_SERVICE_PUT_ARCHIVE_PATH, - }); - if (!verification.ok) { - return callback(toServiceError(status.UNAUTHENTICATED, verification.message ?? 'unauthorized')); - } const workloadId = call.request.workloadId?.trim(); const targetPath = call.request.path?.trim(); if (!workloadId || !targetPath) { @@ -796,16 +648,6 @@ export function createRunnerGrpcServer(opts: RunnerGrpcOptions): Server { streamWorkloadLogs: async ( call: ServerWritableStream, ) => { - const verification = verifyGrpcAuth({ - metadata: call.metadata, - secret: opts.config.sharedSecret, - nonceCache: opts.nonceCache, - path: RUNNER_SERVICE_STREAM_WORKLOAD_LOGS_PATH, - }); - if (!verification.ok) { - call.emit('error', toServiceError(status.UNAUTHENTICATED, verification.message ?? 'unauthorized')); - return; - } const workloadId = call.request.workloadId?.trim(); if (!workloadId) { call.emit('error', toServiceError(status.INVALID_ARGUMENT, 'workload_id_required')); @@ -917,17 +759,6 @@ export function createRunnerGrpcServer(opts: RunnerGrpcOptions): Server { streamEvents: async ( call: ServerWritableStream, ) => { - const verification = verifyGrpcAuth({ - metadata: call.metadata, - secret: opts.config.sharedSecret, - nonceCache: opts.nonceCache, - path: RUNNER_SERVICE_STREAM_EVENTS_PATH, - }); - if (!verification.ok) { - call.emit('error', toServiceError(status.UNAUTHENTICATED, verification.message ?? 'unauthorized')); - return; - } - const since = bigintToNumber(call.request.since); const filters = buildEventFilters(call.request.filters ?? []); @@ -1030,17 +861,6 @@ export function createRunnerGrpcServer(opts: RunnerGrpcOptions): Server { call.once('close', onClosed); }, exec: (call: ExecStream) => { - const verification = verifyGrpcAuth({ - metadata: call.metadata, - secret: opts.config.sharedSecret, - nonceCache: opts.nonceCache, - path: RUNNER_SERVICE_EXEC_PATH, - }); - if (!verification.ok) { - call.emit('error', toServiceError(status.UNAUTHENTICATED, verification.message ?? 'unauthorized')); - return; - } - let ctx: ExecutionContext | undefined; const clearTimers = clearExecutionTimers; @@ -1343,15 +1163,6 @@ export function createRunnerGrpcServer(opts: RunnerGrpcOptions): Server { call: ServerUnaryCall, callback: (error: ServiceError | null, value?: CancelExecutionResponse) => void, ) => { - const verification = verifyGrpcAuth({ - metadata: call.metadata, - secret: opts.config.sharedSecret, - nonceCache: opts.nonceCache, - path: RUNNER_SERVICE_CANCEL_EXEC_PATH, - }); - if (!verification.ok) { - return callback(toServiceError(status.UNAUTHENTICATED, verification.message ?? 'unauthorized')); - } const ctx = activeExecutions.get(call.request.executionId); if (!ctx) { return callback(null, create(CancelExecutionResponseSchema, { cancelled: false })); diff --git a/src/service/main.ts b/src/service/main.ts index 329fc4e..8b03811 100644 --- a/src/service/main.ts +++ b/src/service/main.ts @@ -1,7 +1,7 @@ import './env.ts'; import { ServerCredentials } from '@grpc/grpc-js'; -import { ContainerService, NonceCache } from '../index.ts'; +import { ContainerService } from '../index.ts'; import { loadRunnerConfig } from './config.ts'; import { createRunnerGrpcServer } from './grpc/server.ts'; @@ -14,8 +14,7 @@ async function bootstrap(): Promise { } const containers = new ContainerService(); - const nonceCache = new NonceCache({ ttlMs: config.signatureTtlMs }); - const grpcServer = createRunnerGrpcServer({ config, containers, nonceCache }); + const grpcServer = createRunnerGrpcServer({ config, containers }); const grpcAddress = `${config.grpcHost}:${config.grpcPort}`; await new Promise((resolve, reject) => { grpcServer.bindAsync(grpcAddress, ServerCredentials.createInsecure(), (err) => { diff --git a/test/e2e/docker-runner.e2e.test.ts b/test/e2e/docker-runner.e2e.test.ts index 54de644..93ab734 100644 --- a/test/e2e/docker-runner.e2e.test.ts +++ b/test/e2e/docker-runner.e2e.test.ts @@ -7,11 +7,6 @@ import { RunnerServiceGrpcClient, type RunnerServiceGrpcClientInstance } from '. import { createGrpcTestClient, type GrpcTestClient } from '../../__tests__/helpers/grpc-test-client'; const grpcAddress = process.env.DOCKER_RUNNER_GRPC_URL ?? 'localhost:50051'; -const sharedSecret = process.env.DOCKER_RUNNER_SHARED_SECRET; - -if (!sharedSecret) { - throw new Error('DOCKER_RUNNER_SHARED_SECRET is required for e2e tests'); -} describe('docker-runner e2e', () => { let client: RunnerServiceGrpcClientInstance; @@ -20,7 +15,7 @@ describe('docker-runner e2e', () => { beforeAll(async () => { client = new RunnerServiceGrpcClient(grpcAddress, credentials.createInsecure()); - grpcTestClient = createGrpcTestClient({ client, secret: sharedSecret }); + grpcTestClient = createGrpcTestClient({ client }); await grpcTestClient.ready(); }, 30_000); From 7b4277b0d35bf8a8ca24c2bb2fc9c2ef4cc5983d Mon Sep 17 00:00:00 2001 From: Casey Brooks Date: Thu, 19 Mar 2026 17:15:05 +0000 Subject: [PATCH 2/2] refactor: simplify grpc test helpers --- __tests__/helpers/grpc-test-client.ts | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/__tests__/helpers/grpc-test-client.ts b/__tests__/helpers/grpc-test-client.ts index 5ae59fa..535db22 100644 --- a/__tests__/helpers/grpc-test-client.ts +++ b/__tests__/helpers/grpc-test-client.ts @@ -2,14 +2,7 @@ import { Metadata, type ServiceError } from '@grpc/grpc-js'; import { create } from '@bufbuild/protobuf'; import { containerOptsToStartWorkloadRequest } from '../../src/contracts/workload.grpc'; -import { - RUNNER_SERVICE_INSPECT_WORKLOAD_PATH, - RUNNER_SERVICE_READY_PATH, - RUNNER_SERVICE_REMOVE_WORKLOAD_PATH, - RUNNER_SERVICE_START_WORKLOAD_PATH, - RUNNER_SERVICE_STOP_WORKLOAD_PATH, - type RunnerServiceGrpcClientInstance, -} from '../../src/proto/grpc.js'; +import { type RunnerServiceGrpcClientInstance } from '../../src/proto/grpc.js'; import { InspectWorkloadRequestSchema, ReadyRequestSchema, @@ -28,9 +21,8 @@ export type StartWorkloadInput = { }; export type GrpcTestClient = { - metadataFor: (path: string) => Metadata; + metadataFor: () => Metadata; unary: ( - path: string, request: Request, invoke: ( req: Request, @@ -53,12 +45,11 @@ export function createGrpcTestClient(options: { }): GrpcTestClient { const { client } = options; - const metadataFor = (path: string): Metadata => { + const metadataFor = (): Metadata => { return new Metadata(); }; const unary = async ( - path: string, request: Request, invoke: ( req: Request, @@ -66,7 +57,7 @@ export function createGrpcTestClient(options: { callback: (err: ServiceError | null, response?: Response) => void, ) => void, ): Promise => { - const metadata = metadataFor(path); + const metadata = metadataFor(); return new Promise((resolve, reject) => { invoke(request, metadata, (err, response) => { if (err) { @@ -85,14 +76,14 @@ export function createGrpcTestClient(options: { name: opts.name, autoRemove: opts.autoRemove, }); - return unary(RUNNER_SERVICE_START_WORKLOAD_PATH, request, (req, metadata, callback) => { + return unary(request, (req, metadata, callback) => { client.startWorkload(req, metadata, callback); }); }; const stopContainer = async (containerId: string, timeoutSec = 1): Promise => { const request = create(StopWorkloadRequestSchema, { workloadId: containerId, timeoutSec }); - await unary(RUNNER_SERVICE_STOP_WORKLOAD_PATH, request, (req, metadata, callback) => { + await unary(request, (req, metadata, callback) => { client.stopWorkload(req, metadata, callback); }); }; @@ -106,21 +97,21 @@ export function createGrpcTestClient(options: { force: options.force ?? false, removeVolumes: options.removeVolumes ?? false, }); - await unary(RUNNER_SERVICE_REMOVE_WORKLOAD_PATH, request, (req, metadata, callback) => { + await unary(request, (req, metadata, callback) => { client.removeWorkload(req, metadata, callback); }); }; const inspectContainer = async (containerId: string): Promise => { const request = create(InspectWorkloadRequestSchema, { workloadId: containerId }); - return unary(RUNNER_SERVICE_INSPECT_WORKLOAD_PATH, request, (req, metadata, callback) => { + return unary(request, (req, metadata, callback) => { client.inspectWorkload(req, metadata, callback); }); }; const ready = async (): Promise => { const request = create(ReadyRequestSchema, {}); - return unary(RUNNER_SERVICE_READY_PATH, request, (req, metadata, callback) => { + return unary(request, (req, metadata, callback) => { client.ready(req, metadata, callback); }); };