Skip to content
Open
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
Expand Up @@ -6,7 +6,7 @@ export interface BucketWidgetParams {
prefix?: string;
region?: string;
account_id?: string;
access_key_id_secret_name: string;
secret_access_key_secret_name: string;
access_key_id: string;
access_key: string;
type?: 'file' | 'image';
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,12 @@ export class GetS3FileUrlUseCase
}

const accessKeySecret = await this._dbContext.userSecretRepository.findSecretBySlugAndCompanyId(
params.access_key_id_secret_name,
params.access_key_id,
user.company.id,
);

const secretKeySecret = await this._dbContext.userSecretRepository.findSecretBySlugAndCompanyId(
params.secret_access_key_secret_name,
params.access_key,
Comment on lines +84 to +89
Comment on lines +84 to +89
user.company.id,
Comment on lines 83 to 90
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle legacy keys (and clarify credential source) before secret lookup.

On Line 84 and Line 89, the code now assumes new keys only, but still treats values as secret slugs. This can break existing stored widgets using legacy keys and conflicts with the “direct credentials” contract.

Proposed compatibility patch
-		const accessKeySecret = await this._dbContext.userSecretRepository.findSecretBySlugAndCompanyId(
-			params.access_key_id,
+		const rawParams = params as Record<string, string | undefined>;
+		const accessKeySlug = rawParams.access_key_id ?? rawParams.access_key_id_secret_name;
+		const secretKeySlug = rawParams.access_key ?? rawParams.secret_access_key_secret_name;
+
+		if (!accessKeySlug || !secretKeySlug) {
+			throw new HttpException({ message: 'Bucket credentials params are missing' }, HttpStatus.BAD_REQUEST);
+		}
+
+		const accessKeySecret = await this._dbContext.userSecretRepository.findSecretBySlugAndCompanyId(
+			accessKeySlug,
 			user.company.id,
 		);
 
 		const secretKeySecret = await this._dbContext.userSecretRepository.findSecretBySlugAndCompanyId(
-			params.access_key,
+			secretKeySlug,
 			user.company.id,
 		);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/entities/s3-widget/use-cases/get-s3-file-url.use.case.ts` around
lines 83 - 90, The code currently always treats params.access_key_id and
params.access_key as secret slugs when calling
userSecretRepository.findSecretBySlugAndCompanyId; instead, detect legacy/direct
credentials first and only call findSecretBySlugAndCompanyId when the value is a
slug/secret reference. Update the logic around accessKeySecret and
secretKeySecret to: 1) check params.access_key_id and params.access_key for
legacy/direct patterns (e.g., AWS key format or non-slug indicator) and use them
directly as credentials if they match, otherwise 2) call
findSecretBySlugAndCompanyId(params.access_key_id, user.company.id) and
findSecretBySlugAndCompanyId(params.access_key, user.company.id) to resolve
secrets; ensure the resulting credential source (direct vs secret) is clearly
documented in the surrounding get-s3-file-url.use.case.ts flow and used
consistently downstream.

);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ export class GetS3UploadUrlUseCase
typeof widget.widget_params === 'string' ? JSON5.parse(widget.widget_params) : widget.widget_params;

const accessKeySecret = await this._dbContext.userSecretRepository.findSecretBySlugAndCompanyId(
params.access_key_id_secret_name,
params.access_key_id,
user.company.id,
);

const secretKeySecret = await this._dbContext.userSecretRepository.findSecretBySlugAndCompanyId(
params.secret_access_key_secret_name,
params.access_key,
Comment on lines +47 to +52
Comment on lines +47 to +52
user.company.id,
);

Expand Down
10 changes: 5 additions & 5 deletions backend/src/entities/widget/utils/validate-create-widgets-ds.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import JSON5 from 'json5';
import { BucketProviderEnum } from '../../s3-widget/application/data-structures/bucket-provider.enum.js';
import { EncryptionAlgorithmEnum } from '../../../enums/encryption-algorithm.enum.js';
import { WidgetTypeEnum } from '../../../enums/widget-type.enum.js';
import { Messages } from '../../../exceptions/text/messages.js';
import { Constants } from '../../../helpers/constants/constants.js';
import { getPropertyValueByDescriptor } from '../../../helpers/get-property-value-by-descriptor.js';
import { ConnectionEntity } from '../../connection/connection.entity.js';
import { BucketProviderEnum } from '../../s3-widget/application/data-structures/bucket-provider.enum.js';
import { ForeignKeyDSInfo } from '../../table/table-datastructures.js';
import { findTableFieldsUtil } from '../../table/utils/find-table-fields.util.js';
import { findTablesInConnectionUtil } from '../../table/utils/find-tables-in-connection.util.js';
Expand Down Expand Up @@ -103,11 +103,11 @@ export async function validateCreateWidgetsDs(
if (!widget_params.bucket) {
errors.push(Messages.WIDGET_REQUIRED_PARAMETER_MISSING('bucket'));
}
if (!widget_params.access_key_id_secret_name) {
errors.push(Messages.WIDGET_REQUIRED_PARAMETER_MISSING('access_key_id_secret_name'));
if (!widget_params.access_key_id) {
errors.push(Messages.WIDGET_REQUIRED_PARAMETER_MISSING('access_key_id'));
}
if (!widget_params.secret_access_key_secret_name) {
errors.push(Messages.WIDGET_REQUIRED_PARAMETER_MISSING('secret_access_key_secret_name'));
if (!widget_params.access_key) {
errors.push(Messages.WIDGET_REQUIRED_PARAMETER_MISSING('access_key'));
Comment on lines +106 to +110
}
if (
widget_params.provider &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ async function setupS3WidgetTestEnvironment(
bucket: 'test-bucket',
prefix: 'uploads',
region: 'us-east-1',
access_key_id_secret_name: 'test-aws-access-key',
secret_access_key_secret_name: 'test-aws-secret-key',
access_key_id: 'test-aws-access-key',
access_key: 'test-aws-secret-key',
Comment on lines +94 to +95
});

const widgetDto = {
Expand Down Expand Up @@ -596,8 +596,8 @@ test.serial(`${currentTest} - widget creation succeeds with provider 'digitaloce
provider: 'digitalocean',
bucket: 'test-bucket',
region: 'fra1',
access_key_id_secret_name: 'do-key',
secret_access_key_secret_name: 'do-secret',
access_key_id: 'do-key',
access_key: 'do-secret',
});

const response = await request(app.getHttpServer())
Expand Down Expand Up @@ -628,8 +628,8 @@ test.serial(`${currentTest} - widget creation rejected with unsupported provider
provider: 'not-a-real-provider',
bucket: 'test-bucket',
region: 'us-east-1',
access_key_id_secret_name: 'k',
secret_access_key_secret_name: 's',
access_key_id: 'k',
access_key: 's',
});

const response = await request(app.getHttpServer())
Expand Down Expand Up @@ -660,8 +660,8 @@ test.serial(`${currentTest} - widget creation rejected when cloudflare-r2 missin
provider: 'cloudflare-r2',
bucket: 'test-bucket',
region: 'auto',
access_key_id_secret_name: 'k',
secret_access_key_secret_name: 's',
access_key_id: 'k',
access_key: 's',
});

const response = await request(app.getHttpServer())
Expand Down Expand Up @@ -693,8 +693,8 @@ test.serial(`${currentTest} - widget creation succeeds with cloudflare-r2 and ac
bucket: 'test-bucket',
region: 'auto',
account_id: 'abc123def456',
access_key_id_secret_name: 'k',
secret_access_key_secret_name: 's',
access_key_id: 'k',
access_key: 's',
});

const response = await request(app.getHttpServer())
Expand All @@ -718,13 +718,13 @@ test.serial(`${currentTest} - widget creation succeeds with cloudflare-r2 and ac
t.is(response.status, 201);
});

test.serial(`${currentTest} - widget creation rejected when access_key_id_secret_name missing`, async (t) => {
test.serial(`${currentTest} - widget creation rejected when access_key_id missing`, async (t) => {
const { token, connectionId, testTableName } = await createConnectionAndTableWithS3Field();

const widgetParams = JSON.stringify({
bucket: 'test-bucket',
region: 'us-east-1',
secret_access_key_secret_name: 's',
access_key: 's',
Comment on lines +721 to +727
});

const response = await request(app.getHttpServer())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,8 @@ export class DbTableWidgetsComponent implements OnInit {
// region: Region for the bucket (default: us-east-1; for cloudflare-r2 use "auto")
// account_id: Cloudflare account ID. Required when provider is "cloudflare-r2".
// type: "file" (default) - accepts all file types, "image" - accepts only images
// access_key_id_secret_name: Unique identifier of the secret containing the Access Key ID
// secret_access_key_secret_name: Unique identifier of the secret containing the Secret Access Key
// access_key_id: Unique identifier of the secret containing the Access Key ID
// access_key: Unique identifier of the secret containing the Secret Access Key
//
// ⚠️ IMPORTANT: DO NOT INCLUDE BUCKET SECRETS DIRECTLY IN WIDGET SETTINGS!
// Store your credentials as secrets in Rocketadmin and reference them here by their unique identifiers to ensure security and prevent exposure of sensitive information.
Expand All @@ -293,8 +293,8 @@ export class DbTableWidgetsComponent implements OnInit {
"prefix": "uploads/",
"region": "us-east-1",
"type": "file",
"access_key_id_secret_name": "bucket-access-key-id",
"secret_access_key_secret_name": "bucket-secret-access-key"
"access_key_id": "COMPANY-SECRET/bucket-access-key-id",
"access_key": "COMPANY-SECRET/bucket-access-key"
Comment on lines +296 to +297
}
`,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ describe('S3EditComponent', () => {
bucket: 'test-bucket',
prefix: 'uploads/',
region: 'us-east-1',
access_key_id_secret_name: 'bucket-key',
secret_access_key_secret_name: 'bucket-secret',
access_key_id: 'COMPANY-SECRET/bucket-access-key-id',
access_key: 'COMPANY-SECRET/bucket-access-key',
},
name: 'Document Upload',
description: 'Upload documents to S3',
Expand All @@ -37,8 +37,8 @@ describe('S3EditComponent', () => {
bucket: 'test-bucket',
prefix: 'uploads/',
region: 'us-east-1',
access_key_id_secret_name: 'bucket-key',
secret_access_key_secret_name: 'bucket-secret',
access_key_id: 'COMPANY-SECRET/bucket-access-key-id',
access_key: 'COMPANY-SECRET/bucket-access-key',
}) as any,
name: 'Document Upload',
description: 'Upload documents to S3',
Expand Down Expand Up @@ -113,8 +113,8 @@ describe('S3EditComponent', () => {
bucket: 'test-bucket',
prefix: 'uploads/',
region: 'us-east-1',
access_key_id_secret_name: 'bucket-key',
secret_access_key_secret_name: 'bucket-secret',
access_key_id: 'COMPANY-SECRET/bucket-access-key-id',
access_key: 'COMPANY-SECRET/bucket-access-key',
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ interface BucketWidgetParams {
prefix?: string;
region?: string;
account_id?: string;
access_key_id_secret_name: string;
secret_access_key_secret_name: string;
access_key_id: string;
access_key: string;
type?: 'file' | 'image';
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { S3RecordViewComponent } from './s3.component';
import { S3Service } from 'src/app/services/s3.service';
import { ConnectionsService } from 'src/app/services/connections.service';
import { S3Service } from 'src/app/services/s3.service';
import { TablesService } from 'src/app/services/tables.service';
import { S3RecordViewComponent } from './s3.component';

describe('S3RecordViewComponent', () => {
let component: S3RecordViewComponent;
Expand Down Expand Up @@ -44,8 +44,8 @@ describe('S3RecordViewComponent', () => {
widget_params: {
bucket: 'my-bucket',
type: 'file',
access_key_id_secret_name: 'key',
secret_access_key_secret_name: 'secret',
access_key_id: 'COMPANY-SECRET/bucket-access-key-id',
access_key: 'COMPANY-SECRET/bucket-access-key',
},
});
component.ngOnInit();
Expand All @@ -58,8 +58,8 @@ describe('S3RecordViewComponent', () => {
widget_params: {
bucket: 'my-bucket',
type: 'image',
access_key_id_secret_name: 'key',
secret_access_key_secret_name: 'secret',
access_key_id: 'COMPANY-SECRET/bucket-access-key-id',
access_key: 'COMPANY-SECRET/bucket-access-key',
},
});
component.ngOnInit();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ interface BucketWidgetParams {
prefix?: string;
region?: string;
account_id?: string;
access_key_id_secret_name: string;
secret_access_key_secret_name: string;
access_key_id: string;
access_key: string;
type?: 'file' | 'image';
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { S3DisplayComponent } from './s3.component';
import { S3Service } from 'src/app/services/s3.service';
import { ConnectionsService } from 'src/app/services/connections.service';
import { S3Service } from 'src/app/services/s3.service';
import { TablesService } from 'src/app/services/tables.service';
import { vi } from 'vitest';
import { S3DisplayComponent } from './s3.component';

describe('S3DisplayComponent', () => {
let component: S3DisplayComponent;
Expand Down Expand Up @@ -46,8 +46,8 @@ describe('S3DisplayComponent', () => {
fixture.componentRef.setInput('widgetStructure', {
widget_params: {
bucket: 'my-bucket',
access_key_id_secret_name: 'key',
secret_access_key_secret_name: 'secret',
access_key_id: 'COMPANY-SECRET/bucket-access-key-id',
access_key: 'COMPANY-SECRET/bucket-access-key',
type: 'image',
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ interface BucketWidgetParams {
prefix?: string;
region?: string;
account_id?: string;
access_key_id_secret_name: string;
secret_access_key_secret_name: string;
access_key_id: string;
access_key: string;
type?: 'file' | 'image';
}

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { DynamicModule } from 'ng-dynamic-component';
import { SignalComponentIoModule } from 'ng-dynamic-component/signal-component-io';
import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
import { CookieService } from 'ngx-cookie-service';
import { MERMAID_OPTIONS, MarkdownModule, provideMarkdown } from 'ngx-markdown';
import { MarkdownModule, MERMAID_OPTIONS, provideMarkdown } from 'ngx-markdown';
import { NgxStripeModule } from 'ngx-stripe';
import { AppComponent } from './app/app.component';
import { AppRoutingModule } from './app/app-routing.module';
Expand Down
Loading