d8d5991977
This pull request makes the IMAP and SMTP encryption setting actually honor what the user selects. As per spec there's 3 modes: SSL/TLS (implicit TLS from the start), STARTTLS (it will attempt TLS but if the server doesn't support it, it gracefully falls back to plaintext), NONE (plaintext) Current implementation had a boolean flag for this, this replaces it with the 3 modes Upgrade command to migrate all existing accounts, to not risk breaking anyone's existing account in production we map each account to the mode that matches its current behavior, so nothing changes on the wire /closes #21300 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21562?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: Charles Bochet <charles@twenty.com>
179 lines
5.6 KiB
TypeScript
179 lines
5.6 KiB
TypeScript
import { ACCOUNT_TYPES, type AccountType } from 'twenty-shared/constants';
|
|
import { type DataSource } from 'typeorm';
|
|
|
|
import { deleteConnectedAccount } from 'test/integration/metadata/suites/connected-account/utils/delete-connected-account.util';
|
|
import { saveImapSmtpCaldavAccount } from 'test/integration/metadata/suites/connected-account/utils/save-imap-smtp-caldav-account.util';
|
|
import { runSecretEncryptionRotationCommand } from 'test/integration/secret-encryption/utils/run-secret-encryption-rotation-command.util';
|
|
import { buildSecretEncryptionServiceFromEnv } from 'test/integration/upgrade/utils/build-secret-encryption-service.util';
|
|
|
|
import { EmailConnectionSecurity } from 'src/engine/core-modules/imap-smtp-caldav-connection/enums/email-connection-security.enum';
|
|
import { type EncryptedImapSmtpCaldavParams } from 'src/engine/core-modules/imap-smtp-caldav-connection/types/imap-smtp-caldav-connection.type';
|
|
import { type SecretEncryptionService } from 'src/engine/core-modules/secret-encryption/secret-encryption.service';
|
|
|
|
const V2_ENVELOPE_REGEX = /^enc:v2:[0-9a-f]{8}:[A-Za-z0-9+/=]+$/;
|
|
|
|
const HANDLE = 'rotate-connection-parameters@example.com';
|
|
const IMAP_PASSWORD = 'rotation-test-imap-password';
|
|
const SMTP_PASSWORD = 'rotation-test-smtp-password';
|
|
const CALDAV_PASSWORD = 'rotation-test-caldav-password';
|
|
|
|
type ConnectionParametersRow = {
|
|
workspaceId: string;
|
|
connectionParameters: EncryptedImapSmtpCaldavParams;
|
|
};
|
|
|
|
const readConnectionParameters = async (
|
|
dataSource: DataSource,
|
|
connectedAccountId: string,
|
|
): Promise<ConnectionParametersRow> => {
|
|
const [row] = (await dataSource.query(
|
|
`SELECT "workspaceId", "connectionParameters"
|
|
FROM "core"."connectedAccount"
|
|
WHERE id = $1`,
|
|
[connectedAccountId],
|
|
)) as ConnectionParametersRow[];
|
|
|
|
expect(row).toBeDefined();
|
|
expect(row.connectionParameters).toBeDefined();
|
|
|
|
return row;
|
|
};
|
|
|
|
const expectAllPasswordsDecryptTo = ({
|
|
row,
|
|
secretEncryption,
|
|
expectedPlaintextByProtocol,
|
|
}: {
|
|
row: ConnectionParametersRow;
|
|
secretEncryption: SecretEncryptionService;
|
|
expectedPlaintextByProtocol: Record<AccountType, string>;
|
|
}): void => {
|
|
for (const protocol of ACCOUNT_TYPES) {
|
|
const params = row.connectionParameters[protocol];
|
|
|
|
expect(params).toBeDefined();
|
|
expect(params?.password).toMatch(V2_ENVELOPE_REGEX);
|
|
|
|
const decrypted = secretEncryption.decryptVersioned(params!.password, {
|
|
workspaceId: row.workspaceId,
|
|
});
|
|
|
|
expect(decrypted).toBe(expectedPlaintextByProtocol[protocol]);
|
|
}
|
|
};
|
|
|
|
describe('secret-encryption:rotate command — connection-parameters site (integration)', () => {
|
|
let dataSource: DataSource;
|
|
let secretEncryption: SecretEncryptionService;
|
|
let connectedAccountId: string;
|
|
|
|
beforeAll(async () => {
|
|
dataSource = global.testDataSource;
|
|
secretEncryption = buildSecretEncryptionServiceFromEnv();
|
|
|
|
const { data } = await saveImapSmtpCaldavAccount({
|
|
expectToFail: false,
|
|
input: {
|
|
handle: HANDLE,
|
|
connectionParameters: {
|
|
IMAP: {
|
|
host: 'imap.fastmail.com',
|
|
port: 993,
|
|
username: 'rotation@example.com',
|
|
password: IMAP_PASSWORD,
|
|
connectionSecurity: EmailConnectionSecurity.SSL_TLS,
|
|
},
|
|
SMTP: {
|
|
host: 'smtp.fastmail.com',
|
|
port: 465,
|
|
username: 'rotation@example.com',
|
|
password: SMTP_PASSWORD,
|
|
connectionSecurity: EmailConnectionSecurity.SSL_TLS,
|
|
},
|
|
CALDAV: {
|
|
host: 'caldav.fastmail.com',
|
|
port: 443,
|
|
username: 'rotation@example.com',
|
|
password: CALDAV_PASSWORD,
|
|
connectionSecurity: EmailConnectionSecurity.SSL_TLS,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
connectedAccountId = data.connectedAccountId as string;
|
|
}, 120000);
|
|
|
|
afterAll(async () => {
|
|
if (connectedAccountId !== undefined) {
|
|
await deleteConnectedAccount({
|
|
id: connectedAccountId,
|
|
expectToFail: false,
|
|
});
|
|
}
|
|
});
|
|
|
|
it('keeps every protocol password decryptable after running the rotation', async () => {
|
|
const beforeRotation = await readConnectionParameters(
|
|
dataSource,
|
|
connectedAccountId,
|
|
);
|
|
|
|
expectAllPasswordsDecryptTo({
|
|
row: beforeRotation,
|
|
secretEncryption,
|
|
expectedPlaintextByProtocol: {
|
|
IMAP: IMAP_PASSWORD,
|
|
SMTP: SMTP_PASSWORD,
|
|
CALDAV: CALDAV_PASSWORD,
|
|
},
|
|
});
|
|
|
|
await runSecretEncryptionRotationCommand();
|
|
|
|
const afterRotation = await readConnectionParameters(
|
|
dataSource,
|
|
connectedAccountId,
|
|
);
|
|
|
|
expectAllPasswordsDecryptTo({
|
|
row: afterRotation,
|
|
secretEncryption,
|
|
expectedPlaintextByProtocol: {
|
|
IMAP: IMAP_PASSWORD,
|
|
SMTP: SMTP_PASSWORD,
|
|
CALDAV: CALDAV_PASSWORD,
|
|
},
|
|
});
|
|
|
|
expect(afterRotation.connectionParameters.IMAP?.host).toBe(
|
|
'imap.fastmail.com',
|
|
);
|
|
expect(afterRotation.connectionParameters.SMTP?.port).toBe(465);
|
|
expect(afterRotation.connectionParameters.CALDAV?.username).toBe(
|
|
'rotation@example.com',
|
|
);
|
|
}, 90000);
|
|
|
|
it('is idempotent when targeting only the connection-parameters site', async () => {
|
|
await runSecretEncryptionRotationCommand({
|
|
site: 'connected-account-connection-parameters',
|
|
});
|
|
await runSecretEncryptionRotationCommand({
|
|
site: 'connected-account-connection-parameters',
|
|
});
|
|
|
|
const row = await readConnectionParameters(dataSource, connectedAccountId);
|
|
|
|
expectAllPasswordsDecryptTo({
|
|
row,
|
|
secretEncryption,
|
|
expectedPlaintextByProtocol: {
|
|
IMAP: IMAP_PASSWORD,
|
|
SMTP: SMTP_PASSWORD,
|
|
CALDAV: CALDAV_PASSWORD,
|
|
},
|
|
});
|
|
}, 120000);
|
|
});
|