feat: harden control panel authentication
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { BadRequestException, ForbiddenException, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common';
|
||||
import { BadRequestException, ForbiddenException, HttpException, HttpStatus, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common';
|
||||
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
||||
import { execFileSync, spawn } from 'node:child_process';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
@@ -83,12 +83,15 @@ function safeTenant(value: string): string {
|
||||
|
||||
@Injectable()
|
||||
export class GoalsService {
|
||||
private readonly startWindows = new Map<string, number[]>();
|
||||
|
||||
async start(input: GoalStartInput, actor: SettingsActor): Promise<GoalJob> {
|
||||
this.requireRead(actor);
|
||||
const goal = String(input.goal ?? '').trim();
|
||||
if (goal.length < 10 || goal.length > 8000) {
|
||||
throw new BadRequestException('GOAL_LENGTH_INVALID');
|
||||
}
|
||||
this.enforceStartLimit(actor);
|
||||
|
||||
const connections = this.connections(actor);
|
||||
const local = connections.find((connection) => connection.connected && connection.kind === 'local');
|
||||
@@ -200,6 +203,7 @@ export class GoalsService {
|
||||
}
|
||||
|
||||
private async accountReviewer(): Promise<'claude' | 'codex' | ''> {
|
||||
if (process.env.CASAN_PROVIDER_ACCOUNT_AUTH_ENABLED !== '1') return '';
|
||||
const bridgeUrl = (process.env.CASAN_AUTH_BRIDGE_URL || '').replace(/\/$/, '');
|
||||
const bridgeToken = process.env.CASAN_AUTH_BRIDGE_TOKEN || '';
|
||||
if (!bridgeUrl || !bridgeToken) return '';
|
||||
@@ -219,6 +223,27 @@ export class GoalsService {
|
||||
return '';
|
||||
}
|
||||
|
||||
private enforceStartLimit(actor: SettingsActor): void {
|
||||
const key = `${safeTenant(actor.tenant)}:${actor.actor}`;
|
||||
const timestamp = Date.now();
|
||||
const recent = (this.startWindows.get(key) ?? []).filter((value) => timestamp - value < 10 * 60_000);
|
||||
if (recent.length >= 5) {
|
||||
throw new HttpException('GOAL_RATE_LIMITED', HttpStatus.TOO_MANY_REQUESTS);
|
||||
}
|
||||
const directory = join(APP_ROOT, '.specify', 'state', 'goals', safeTenant(actor.tenant));
|
||||
if (existsSync(directory)) {
|
||||
const active = readdirSync(directory)
|
||||
.filter((name) => /^[a-f0-9-]{36}\.json$/.test(name))
|
||||
.map((name) => parseJson<GoalJob>(readFileSync(join(directory, name), 'utf8')))
|
||||
.filter((job) => job?.actor === actor.actor && (job.status === 'queued' || job.status === 'running'));
|
||||
if (active.length >= 2) {
|
||||
throw new HttpException('GOAL_CONCURRENCY_LIMITED', HttpStatus.TOO_MANY_REQUESTS);
|
||||
}
|
||||
}
|
||||
recent.push(timestamp);
|
||||
this.startWindows.set(key, recent);
|
||||
}
|
||||
|
||||
private runPython(script: string, args: string[], environment: NodeJS.ProcessEnv): string {
|
||||
try {
|
||||
return execFileSync('python3', [script, ...args], {
|
||||
|
||||
@@ -27,6 +27,7 @@ const PROVIDERS = new Set(['codex', 'claude']);
|
||||
export class ProviderAuthService {
|
||||
private readonly bridgeUrl = (process.env.CASAN_AUTH_BRIDGE_URL || 'http://host.docker.internal:20130').replace(/\/$/, '');
|
||||
private readonly bridgeToken = process.env.CASAN_AUTH_BRIDGE_TOKEN || '';
|
||||
private readonly enabled = process.env.CASAN_PROVIDER_ACCOUNT_AUTH_ENABLED === '1';
|
||||
|
||||
async status(actor: SettingsActor): Promise<BridgeStatusResponse> {
|
||||
this.requireRead(actor);
|
||||
@@ -40,7 +41,7 @@ export class ProviderAuthService {
|
||||
}
|
||||
|
||||
private async bridgeRequest<T>(path: string, method: 'GET' | 'POST'): Promise<T> {
|
||||
if (!this.bridgeToken) throw new ServiceUnavailableException('PROVIDER_AUTH_BRIDGE_NOT_CONFIGURED');
|
||||
if (!this.enabled || !this.bridgeToken) throw new ServiceUnavailableException('PROVIDER_AUTH_BRIDGE_NOT_CONFIGURED');
|
||||
try {
|
||||
const response = await fetch(`${this.bridgeUrl}${path}`, {
|
||||
method,
|
||||
|
||||
Reference in New Issue
Block a user