Full per-user data isolation across all tables: - Migration adds userId FK to hosts, host_groups, credentials, ssh_keys, connection_logs. Backfills existing data to admin@wraith.local. - All services scope queries by userId from JWT (req.user.sub). Users can only see/modify their own data. Cross-user access returns 403. - Two roles: admin (full access + user management) and user (own data only). - Admin endpoints: list/create/edit/delete users, reset password, reset TOTP. Protected by AdminGuard. Admins cannot delete themselves or remove own role. - JWT payload now includes role. Frontend auth store exposes isAdmin getter. - Seed script fixed: checks for admin@wraith.local specifically (not any user). Uses upsert, seeds with role=admin. Migration cleans up duplicate users. - Connection logs now attributed to the connecting user via WS auth. - Deleting a user CASCADEs to all their hosts, credentials, keys, and logs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
108 lines
3.5 KiB
TypeScript
108 lines
3.5 KiB
TypeScript
import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import { EncryptionService } from './encryption.service';
|
|
import { CreateSshKeyDto } from './dto/create-ssh-key.dto';
|
|
import { UpdateSshKeyDto } from './dto/update-ssh-key.dto';
|
|
import { createHash } from 'crypto';
|
|
|
|
@Injectable()
|
|
export class SshKeysService {
|
|
constructor(
|
|
private prisma: PrismaService,
|
|
private encryption: EncryptionService,
|
|
) {}
|
|
|
|
findAll(userId: number) {
|
|
return this.prisma.sshKey.findMany({
|
|
where: { userId },
|
|
select: { id: true, name: true, keyType: true, fingerprint: true, publicKey: true, createdAt: true },
|
|
orderBy: { name: 'asc' },
|
|
});
|
|
}
|
|
|
|
async findOne(id: number, userId?: number) {
|
|
const key = await this.prisma.sshKey.findUnique({
|
|
where: { id },
|
|
include: { credentials: { select: { id: true, name: true } } },
|
|
});
|
|
if (!key) throw new NotFoundException(`SSH key ${id} not found`);
|
|
if (userId !== undefined && key.userId !== userId) {
|
|
throw new ForbiddenException('Access denied');
|
|
}
|
|
// Never return encrypted private key over API
|
|
return {
|
|
id: key.id,
|
|
name: key.name,
|
|
keyType: key.keyType,
|
|
fingerprint: key.fingerprint,
|
|
publicKey: key.publicKey,
|
|
credentials: key.credentials,
|
|
createdAt: key.createdAt,
|
|
};
|
|
}
|
|
|
|
async create(userId: number, dto: CreateSshKeyDto) {
|
|
// Detect key type from private key content
|
|
const keyType = this.detectKeyType(dto.privateKey);
|
|
|
|
// Generate fingerprint from public key if provided, else from private key
|
|
const fingerprint = this.generateFingerprint(dto.publicKey || dto.privateKey);
|
|
|
|
// Encrypt sensitive data
|
|
const encryptedPrivateKey = this.encryption.encrypt(dto.privateKey);
|
|
const passphraseEncrypted = dto.passphrase
|
|
? this.encryption.encrypt(dto.passphrase)
|
|
: null;
|
|
|
|
return this.prisma.sshKey.create({
|
|
data: {
|
|
name: dto.name,
|
|
keyType,
|
|
fingerprint,
|
|
publicKey: dto.publicKey || null,
|
|
userId,
|
|
encryptedPrivateKey,
|
|
passphraseEncrypted,
|
|
},
|
|
});
|
|
}
|
|
|
|
async update(id: number, userId: number, dto: UpdateSshKeyDto) {
|
|
const key = await this.prisma.sshKey.findUnique({ where: { id } });
|
|
if (!key) throw new NotFoundException(`SSH key ${id} not found`);
|
|
if (key.userId !== userId) throw new ForbiddenException('Access denied');
|
|
|
|
const data: any = {};
|
|
if (dto.name) data.name = dto.name;
|
|
if (dto.passphrase !== undefined) {
|
|
data.passphraseEncrypted = dto.passphrase
|
|
? this.encryption.encrypt(dto.passphrase)
|
|
: null;
|
|
}
|
|
return this.prisma.sshKey.update({ where: { id }, data });
|
|
}
|
|
|
|
async remove(id: number, userId: number) {
|
|
const key = await this.prisma.sshKey.findUnique({ where: { id } });
|
|
if (!key) throw new NotFoundException(`SSH key ${id} not found`);
|
|
if (key.userId !== userId) throw new ForbiddenException('Access denied');
|
|
return this.prisma.sshKey.delete({ where: { id } });
|
|
}
|
|
|
|
private detectKeyType(privateKey: string): string {
|
|
if (privateKey.includes('RSA')) return 'rsa';
|
|
if (privateKey.includes('EC')) return 'ecdsa';
|
|
if (privateKey.includes('OPENSSH')) return 'ed25519'; // OpenSSH format, likely ed25519
|
|
return 'unknown';
|
|
}
|
|
|
|
private generateFingerprint(keyContent: string): string {
|
|
try {
|
|
const hash = createHash('sha256').update(keyContent.trim()).digest('base64');
|
|
return `SHA256:${hash}`;
|
|
} catch {
|
|
return 'unknown';
|
|
}
|
|
}
|
|
}
|