Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
export class FoundSecretDS {
id: string;
slug: string;
value?: string;
companyId: string;
createdAt: Date;
updatedAt: Date;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export class GetSecretDS {
userId: string;
slug: string;
masterPassword?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,6 @@ export class FoundSecretDto {
})
slug: string;

@ApiProperty({
type: String,
required: false,
description: 'Decrypted secret value (only included when retrieving a specific secret)',
example: 'my-secret-value-123',
})
value?: string;

@ApiProperty({
type: String,
description: 'Company ID that owns this secret',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { ForbiddenException, GoneException, Inject, Injectable, NotFoundException, Scope } from '@nestjs/common';
import { GoneException, Inject, Injectable, NotFoundException, Scope } from '@nestjs/common';
import AbstractUseCase from '../../../common/abstract-use.case.js';
import { BaseType } from '../../../common/data-injection.tokens.js';
import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js';
import { GetSecretDS } from '../application/data-structures/get-secret.ds.js';
import { FoundSecretDS } from '../application/data-structures/found-secret.ds.js';
import { IGetSecretBySlug } from './user-secret-use-cases.interface.js';
import { buildFoundSecretDS } from '../utils/build-found-secret.ds.js';
import { Encryptor } from '../../../helpers/encryption/encryptor.js';
import { SecretActionEnum } from '../../secret-access-log/secret-access-log.entity.js';
import { Messages } from '../../../exceptions/text/messages.js';

@Injectable({ scope: Scope.REQUEST })
Expand All @@ -20,7 +18,7 @@ export class GetSecretBySlugUseCase extends AbstractUseCase<GetSecretDS, FoundSe
}

protected async implementation(inputData: GetSecretDS): Promise<FoundSecretDS> {
const { userId, slug, masterPassword } = inputData;
const { userId, slug } = inputData;

const user = await this._dbContext.userRepository.findOne({
where: { id: userId },
Expand All @@ -41,35 +39,6 @@ export class GetSecretBySlugUseCase extends AbstractUseCase<GetSecretDS, FoundSe
throw new GoneException(Messages.SECRET_EXPIRED);
}

if (secret.masterEncryption && !masterPassword) {
throw new ForbiddenException(Messages.SECRET_MASTER_PASSWORD_REQUIRED);
}

if (secret.masterEncryption && masterPassword) {
const isValid = await Encryptor.verifyUserPassword(masterPassword, secret.masterHash);
if (!isValid) {
await this._dbContext.secretAccessLogRepository.createAccessLog(
secret.id,
userId,
SecretActionEnum.VIEW,
false,
Messages.SECRET_MASTER_PASSWORD_INVALID,
);
throw new ForbiddenException(Messages.SECRET_MASTER_PASSWORD_INVALID);
}
}

secret.lastAccessedAt = new Date();
await this._dbContext.userSecretRepository.save(secret);

await this._dbContext.secretAccessLogRepository.createAccessLog(secret.id, userId, SecretActionEnum.VIEW);

let decryptedValue = Encryptor.decryptData(secret.encryptedValue);

if (secret.masterEncryption && masterPassword) {
decryptedValue = Encryptor.decryptDataMasterPwd(decryptedValue, masterPassword);
}

return buildFoundSecretDS(secret, decryptedValue);
return buildFoundSecretDS(secret);
}
}
13 changes: 2 additions & 11 deletions backend/src/entities/user-secret/user-secret.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,9 @@ export class UserSecretController {
@ApiOperation({ summary: 'Get secret by slug' })
@ApiResponse({
status: 200,
description: 'Returns secret details with decrypted value.',
description: 'Returns secret metadata (value is never exposed).',
type: FoundSecretDto,
})
@ApiResponse({
status: 403,
description: 'Master password required or incorrect.',
})
@ApiResponse({
status: 404,
description: 'Secret or user not found.',
Expand All @@ -146,15 +142,10 @@ export class UserSecretController {
@ApiParam({ name: 'slug', type: String, description: 'Unique secret identifier', example: 'database-password' })
@UseGuards(CompanyUserGuard)
@Get('/secrets/:slug')
async getSecretBySlug(
@UserId() userId: string,
@Param('slug') slug: string,
@MasterPassword() masterPassword?: string,
): Promise<FoundSecretDto> {
async getSecretBySlug(@UserId() userId: string, @Param('slug') slug: string): Promise<FoundSecretDto> {
const foundSecret = await this.getSecretBySlugUseCase.execute({
userId,
slug,
masterPassword,
});
return buildFoundSecretDto(foundSecret);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ export function buildCreatedSecretDto(ds: CreatedSecretDS): FoundSecretDto {
return {
id: ds.id,
slug: ds.slug,
value: undefined,
companyId: ds.companyId,
createdAt: ds.createdAt,
updatedAt: ds.updatedAt,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { FoundSecretDS } from '../application/data-structures/found-secret.ds.js';
import { UserSecretEntity } from '../user-secret.entity.js';

export function buildFoundSecretDS(secret: UserSecretEntity, decryptedValue?: string): FoundSecretDS {
export function buildFoundSecretDS(secret: UserSecretEntity): FoundSecretDS {
return {
id: secret.id,
slug: secret.slug,
value: decryptedValue,
companyId: secret.companyId,
createdAt: secret.createdAt,
updatedAt: secret.updatedAt,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ export function buildFoundSecretDto(ds: FoundSecretDS): FoundSecretDto {
return {
id: ds.id,
slug: ds.slug,
value: ds.value,
companyId: ds.companyId,
createdAt: ds.createdAt,
updatedAt: ds.updatedAt,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ export function buildUpdatedSecretDto(ds: UpdatedSecretDS): FoundSecretDto {
return {
id: ds.id,
slug: ds.slug,
value: undefined,
companyId: ds.companyId,
createdAt: ds.createdAt,
updatedAt: ds.updatedAt,
Expand Down
29 changes: 16 additions & 13 deletions backend/test/ava-tests/non-saas-tests/non-saas-secrets-e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ test.serial(`${currentTest}?search=test - should filter secrets by slug`, async
});

currentTest = 'GET /secrets/:slug';
test.serial(`${currentTest} - should return secret with value`, async (t) => {
test.serial(`${currentTest} - should return secret metadata without value`, async (t) => {
// Create user with a secret to retrieve
const { token } = await setupUserWithSecrets([{ slug: 'get-test-api-key', value: 'sk-get-test-value' }]);

Expand All @@ -210,8 +210,7 @@ test.serial(`${currentTest} - should return secret with value`, async (t) => {
t.is(response.status, 200, response.text);
const responseBody = JSON.parse(response.text);
t.is(responseBody.slug, 'get-test-api-key');
t.truthy(responseBody.value);
t.truthy(responseBody.lastAccessedAt);
t.falsy(responseBody.value);
});

test.serial(`${currentTest} - should return 404 for non-existent secret`, async (t) => {
Expand Down Expand Up @@ -252,39 +251,43 @@ test.serial(`${currentTest} - should create secret with master password`, async
});

currentTest = 'GET /secrets/:slug';
test.serial(`${currentTest} - should require master password for protected secret`, async (t) => {
test.serial(`${currentTest} - should return protected secret metadata without master password`, async (t) => {
// Create user with a protected secret
const { token } = await setupUserWithSecrets([
{ slug: 'protected-secret-403', value: 'sensitive-data', masterEncryption: true, masterPassword: 'SecretPass123!' },
{ slug: 'protected-secret-200', value: 'sensitive-data', masterEncryption: true, masterPassword: 'SecretPass123!' },
]);

// Try to access without master password
// Access without master password should return metadata (no value is returned anyway)
const response = await request(app.getHttpServer())
.get('/secrets/protected-secret-403')
.get('/secrets/protected-secret-200')
.set('Cookie', token)
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');

t.is(response.status, 403, response.text);
t.is(response.status, 200, response.text);
const responseBody = JSON.parse(response.text);
t.is(responseBody.slug, 'protected-secret-200');
t.falsy(responseBody.value);
t.is(responseBody.masterEncryption, true);
});

test.serial(`${currentTest} - should return protected secret with correct master password`, async (t) => {
test.serial(`${currentTest} - should return protected secret metadata with master password (no value returned)`, async (t) => {
// Create user with a protected secret
const { token } = await setupUserWithSecrets([
{ slug: 'protected-secret-200', value: 'sensitive-data', masterEncryption: true, masterPassword: 'MasterPass123!' },
{ slug: 'protected-secret-with-pwd', value: 'sensitive-data', masterEncryption: true, masterPassword: 'MasterPass123!' },
]);

const response = await request(app.getHttpServer())
.get('/secrets/protected-secret-200')
.get('/secrets/protected-secret-with-pwd')
.set('Cookie', token)
.set('masterpwd', 'MasterPass123!')
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');

t.is(response.status, 200, response.text);
const responseBody = JSON.parse(response.text);
t.is(responseBody.slug, 'protected-secret-200');
t.truthy(responseBody.value);
t.is(responseBody.slug, 'protected-secret-with-pwd');
t.falsy(responseBody.value);
});

currentTest = 'PUT /secrets/:slug';
Expand Down
34 changes: 0 additions & 34 deletions frontend/buildspec.yml

This file was deleted.

Loading
Loading