fix: Decrypt encrypted front component variables (#23494)
## Summary Fixes #23492 Fixes front-component application variables returning their encrypted at-rest value instead of their configured plaintext value. Non-secret application variables (`isSecret: false`) are now decrypted server-side before being injected into the front-component environment. Secret variables remain excluded and are never decrypted or exposed to the browser. ## Root cause The front-component resolver filtered secret application variables correctly, but forwarded the cached `encryptedValue` directly. As a result, `getApplicationVariable()` returned an `enc:v2:...` envelope rather than the configured value. ## Changes - Decrypt recognized versioned envelopes for non-secret application variables. - Preserve empty and legacy/plain values unchanged for backwards compatibility. - Add `SecretEncryptionModule` to the front-component module. - Add coverage for: - decrypting public variables; - retaining plaintext compatibility; - excluding secret variables without attempting decryption. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23494?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: prastoin <paul@twenty.com>
This commit is contained in:
+134
@@ -0,0 +1,134 @@
|
||||
import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util';
|
||||
import { cleanupApplicationAndAppRegistration } from 'test/integration/metadata/suites/application/utils/cleanup-application-and-app-registration.util';
|
||||
import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util';
|
||||
import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util';
|
||||
import { uploadApplicationFile } from 'test/integration/metadata/suites/application/utils/upload-application-file.util';
|
||||
import { findFrontComponent } from 'test/integration/metadata/suites/front-component/utils/find-front-component.util';
|
||||
import { findFrontComponents } from 'test/integration/metadata/suites/front-component/utils/find-front-components.util';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const TEST_APP_ID = uuidv4();
|
||||
const TEST_ROLE_ID = uuidv4();
|
||||
const FRONT_COMPONENT_ID = uuidv4();
|
||||
const PUBLIC_VARIABLE_ID = uuidv4();
|
||||
const SECRET_VARIABLE_ID = uuidv4();
|
||||
|
||||
const BUILT_COMPONENT_PATH = 'src/front-components/variables.mjs';
|
||||
const PUBLIC_VARIABLE_VALUE = 'pk.public-access-token';
|
||||
|
||||
const buildManifest = (): Manifest => {
|
||||
const baseManifest = buildBaseManifest({
|
||||
appId: TEST_APP_ID,
|
||||
roleId: TEST_ROLE_ID,
|
||||
});
|
||||
|
||||
return {
|
||||
...baseManifest,
|
||||
application: {
|
||||
...baseManifest.application,
|
||||
applicationVariables: {
|
||||
PUBLIC_ACCESS_TOKEN: {
|
||||
universalIdentifier: PUBLIC_VARIABLE_ID,
|
||||
value: PUBLIC_VARIABLE_VALUE,
|
||||
},
|
||||
API_SECRET: {
|
||||
universalIdentifier: SECRET_VARIABLE_ID,
|
||||
isSecret: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
frontComponents: [
|
||||
{
|
||||
universalIdentifier: FRONT_COMPONENT_ID,
|
||||
name: 'VariablesComponent',
|
||||
description: 'A front component reading application variables',
|
||||
sourceComponentPath: 'src/front-components/variables.tsx',
|
||||
builtComponentPath: BUILT_COMPONENT_PATH,
|
||||
builtComponentChecksum: 'variables-checksum',
|
||||
componentName: 'VariablesComponent',
|
||||
isHeadless: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
describe('Front component application variables', () => {
|
||||
let frontComponentId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupApplicationForSync({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
name: 'Test Application Variables App',
|
||||
description: 'App for testing front component application variables',
|
||||
sourcePath: 'test-application-variables',
|
||||
});
|
||||
|
||||
jest.useRealTimers();
|
||||
|
||||
await uploadApplicationFile({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
fileFolder: 'BuiltFrontComponent',
|
||||
filePath: BUILT_COMPONENT_PATH,
|
||||
fileBuffer: Buffer.from('dummy built component content'),
|
||||
filename: 'variables.mjs',
|
||||
contentType: 'application/javascript',
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
await syncApplication({
|
||||
manifest: buildManifest(),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const { data } = await findFrontComponents({});
|
||||
|
||||
const syncedFrontComponent = data.frontComponents.find(
|
||||
({ universalIdentifier }) => universalIdentifier === FRONT_COMPONENT_ID,
|
||||
);
|
||||
|
||||
if (!isDefined(syncedFrontComponent)) {
|
||||
throw new Error('Synced front component was not found');
|
||||
}
|
||||
|
||||
frontComponentId = syncedFrontComponent.id;
|
||||
}, 60000);
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanupApplicationAndAppRegistration({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
});
|
||||
});
|
||||
|
||||
it('should store application variable values encrypted at rest', async () => {
|
||||
const rows = await globalThis.testDataSource.query(
|
||||
`SELECT key, value FROM core."applicationVariable"
|
||||
WHERE "universalIdentifier" = ANY($1)`,
|
||||
[[PUBLIC_VARIABLE_ID, SECRET_VARIABLE_ID]],
|
||||
);
|
||||
|
||||
const publicVariable = rows.find(
|
||||
({ key }: { key: string }) => key === 'PUBLIC_ACCESS_TOKEN',
|
||||
);
|
||||
|
||||
expect(publicVariable.value).toMatch(/^enc:v2:/);
|
||||
expect(publicVariable.value).not.toContain(PUBLIC_VARIABLE_VALUE);
|
||||
});
|
||||
|
||||
it('should expose non-secret application variables decrypted and exclude secret ones', async () => {
|
||||
const { data } = await findFrontComponent({
|
||||
input: { id: frontComponentId },
|
||||
gqlFields: `
|
||||
id
|
||||
applicationVariables
|
||||
`,
|
||||
});
|
||||
|
||||
expect(data.frontComponent.applicationVariables).toEqual({
|
||||
PUBLIC_ACCESS_TOKEN: PUBLIC_VARIABLE_VALUE,
|
||||
});
|
||||
});
|
||||
});
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
|
||||
|
||||
const DEFAULT_FRONT_COMPONENTS_GQL_FIELDS = `
|
||||
id
|
||||
name
|
||||
universalIdentifier
|
||||
applicationId
|
||||
`;
|
||||
|
||||
export const findFrontComponentsQueryFactory = ({
|
||||
gqlFields = DEFAULT_FRONT_COMPONENTS_GQL_FIELDS,
|
||||
}: Partial<PerformMetadataQueryParams<undefined>>) => ({
|
||||
query: gql`
|
||||
query FrontComponents {
|
||||
frontComponents {
|
||||
${gqlFields}
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { findFrontComponentsQueryFactory } from 'test/integration/metadata/suites/front-component/utils/find-front-components-query-factory.util';
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
|
||||
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
|
||||
import { type FrontComponentDTO } from 'src/engine/metadata-modules/front-component/dtos/front-component.dto';
|
||||
|
||||
export const findFrontComponents = async ({
|
||||
gqlFields,
|
||||
expectToFail = false,
|
||||
token,
|
||||
}: Partial<PerformMetadataQueryParams<undefined>>): CommonResponseBody<{
|
||||
frontComponents: FrontComponentDTO[];
|
||||
}> => {
|
||||
const graphqlOperation = findFrontComponentsQueryFactory({ gqlFields });
|
||||
|
||||
const response = await makeMetadataAPIRequest(graphqlOperation, token);
|
||||
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Finding front components should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Finding front components has failed but should not',
|
||||
});
|
||||
}
|
||||
|
||||
return { data: response.body.data, errors: response.body.errors };
|
||||
};
|
||||
Reference in New Issue
Block a user