Harden platform security, reliability, and CI after full audit.

Close deployment IDOR and gate stub payment endpoints, add production
secret validation, health probes, Redis-backed build progress, GitHub
Actions CI, expanded tests, billing/k8s refactors, and ops runbooks.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
keyhan
2026-06-29 20:59:49 +03:30
parent a87bc49393
commit 837f0fa63f
83 changed files with 3953 additions and 1308 deletions
@@ -10,6 +10,7 @@ import { AuthGuard } from '@nestjs/passport';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { DeploymentsService } from './deployments.service';
import { RolesGuard } from '../common/guards/roles.guard';
import { UserRole } from '../common/enums';
@ApiTags('Deployments')
@ApiBearerAuth()
@@ -18,6 +19,12 @@ import { RolesGuard } from '../common/guards/roles.guard';
export class DeploymentsController {
constructor(private readonly deploymentsService: DeploymentsService) {}
private ownershipUserId(req: { user: { id: string; role: string } }): string | undefined {
const isStaff =
req.user.role === UserRole.ADMIN || req.user.role === UserRole.TECHNICAL;
return isStaff ? undefined : req.user.id;
}
@Post('applications/:appId/deploy')
@ApiOperation({ summary: 'Trigger a new deployment' })
async triggerDeployment(@Param('appId') appId: string, @Request() req: any) {
@@ -26,14 +33,14 @@ export class DeploymentsController {
@Get('applications/:appId')
@ApiOperation({ summary: 'List deployments for an application' })
async findByApplication(@Param('appId') appId: string) {
return this.deploymentsService.findByApplication(appId);
async findByApplication(@Param('appId') appId: string, @Request() req: any) {
return this.deploymentsService.findByApplication(appId, this.ownershipUserId(req));
}
@Get(':id')
@ApiOperation({ summary: 'Get deployment details' })
async findOne(@Param('id') id: string) {
return this.deploymentsService.findOne(id);
async findOne(@Param('id') id: string, @Request() req: any) {
return this.deploymentsService.findOne(id, this.ownershipUserId(req));
}
@Get('applications/:appId/logs')
@@ -0,0 +1,88 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { NotFoundException } from '@nestjs/common';
import { DeploymentsService } from './deployments.service';
import { Deployment } from './entities/deployment.entity';
import { ApplicationsService } from '../applications/applications.service';
import { KubernetesService } from '../kubernetes/kubernetes.service';
import { BuildService } from '../build/build.service';
import { ClustersService } from '../clusters/clusters.service';
describe('DeploymentsService authorization', () => {
let service: DeploymentsService;
const deploymentsRepository = {
find: jest.fn(),
findOne: jest.fn(),
createQueryBuilder: jest.fn(),
};
const applicationsService = {
findOne: jest.fn(),
};
beforeEach(async () => {
jest.clearAllMocks();
const module: TestingModule = await Test.createTestingModule({
providers: [
DeploymentsService,
{ provide: getRepositoryToken(Deployment), useValue: deploymentsRepository },
{ provide: ApplicationsService, useValue: applicationsService },
{ provide: KubernetesService, useValue: {} },
{ provide: BuildService, useValue: {} },
{ provide: ClustersService, useValue: {} },
],
}).compile();
service = module.get(DeploymentsService);
});
describe('findByApplication', () => {
it('verifies application ownership before listing deployments', async () => {
const appId = 'app-1';
const userId = 'user-1';
const deployments = [{ id: 'd-1', applicationId: appId }] as Deployment[];
applicationsService.findOne.mockResolvedValue({ id: appId, userId });
deploymentsRepository.find.mockResolvedValue(deployments);
const result = await service.findByApplication(appId, userId);
expect(applicationsService.findOne).toHaveBeenCalledWith(appId, userId);
expect(result).toEqual(deployments);
});
it('propagates NotFoundException when user does not own the app', async () => {
applicationsService.findOne.mockRejectedValue(new NotFoundException('Application not found'));
await expect(service.findByApplication('app-1', 'other-user')).rejects.toThrow(NotFoundException);
expect(deploymentsRepository.find).not.toHaveBeenCalled();
});
});
describe('findOne', () => {
it('verifies application ownership before returning deployment', async () => {
const deployment = {
id: 'd-1',
applicationId: 'app-1',
application: { id: 'app-1' },
} as Deployment;
deploymentsRepository.findOne.mockResolvedValue(deployment);
applicationsService.findOne.mockResolvedValue({ id: 'app-1', userId: 'user-1' });
const result = await service.findOne('d-1', 'user-1');
expect(applicationsService.findOne).toHaveBeenCalledWith('app-1', 'user-1');
expect(result).toBe(deployment);
});
it('throws when deployment does not exist', async () => {
deploymentsRepository.findOne.mockResolvedValue(null);
await expect(service.findOne('missing', 'user-1')).rejects.toThrow(NotFoundException);
expect(applicationsService.findOne).not.toHaveBeenCalled();
});
});
});
@@ -463,14 +463,15 @@ export class DeploymentsService {
}
}
async findByApplication(applicationId: string): Promise<Deployment[]> {
async findByApplication(applicationId: string, userId?: string): Promise<Deployment[]> {
await this.applicationsService.findOne(applicationId, userId);
return this.deploymentsRepository.find({
where: { applicationId },
order: { createdAt: 'DESC' },
});
}
async findOne(id: string): Promise<Deployment> {
async findOne(id: string, userId?: string): Promise<Deployment> {
const deployment = await this.deploymentsRepository.findOne({
where: { id },
relations: { application: true },
@@ -478,6 +479,7 @@ export class DeploymentsService {
if (!deployment) {
throw new NotFoundException('Deployment not found');
}
await this.applicationsService.findOne(deployment.applicationId, userId);
return deployment;
}
@@ -537,7 +539,7 @@ export class DeploymentsService {
if (!latest) return null;
const progress = this.buildService.getProgress(latest.id);
const progress = await this.buildService.getProgress(latest.id);
if (progress) return progress;
// No in-memory progress — infer from deployment status