830404b215
## 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>
135 lines
4.5 KiB
TypeScript
135 lines
4.5 KiB
TypeScript
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,
|
|
});
|
|
});
|
|
});
|