[CONNECTED_ACCOUNT_BREAKING_CHANGE] Encrypt ConnectedAccount connectionParameters (#20673)
# Introduction Prevent any cross user `connectedAccount` `connectionParamaters` leak Also encrypt in db all `connectionParameters` password Never return any password through `DTO` anymore The settings now allow update mutation without providing the password in edition mode Verified all `connectionParameters.password` interaction ## Integration tests - Added more coverage for both failing and successful paths - Introduced a new env var that allow bypass the provider connection test ## Legacy connected Account decryption support Stop allowing non encrypted decryption on `accessToken` and `refreshToken`, only allow legacy decryption on refactored `connectionParameters` ## Upsert ownership Completely got rid of the connected workspace schema context which is legacy Also now a user can only upsert a connected account for him only.. ## New UI <img width="1770" height="1852" alt="image" src="https://github.com/user-attachments/assets/55c1dc89-42ff-4084-95e2-cc5f9e23753b" /> If in edition the password is by default disabled It needs to be selected as being edited to be enabled ## Next - Refactor tool permissions flag not to include connected accounts - Remove the legacy connected standard object - Refactor and improve connected account resolver auth
This commit is contained in:
+133
@@ -0,0 +1,133 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { DataSource, QueryRunner } from 'typeorm';
|
||||
|
||||
import { type ImapSmtpCaldavParams } from 'src/engine/core-modules/imap-smtp-caldav-connection/types/imap-smtp-caldav-connection.type';
|
||||
import { SECRET_ENCRYPTION_ENVELOPE_V2_PREFIX } from 'src/engine/core-modules/secret-encryption/constants/secret-encryption.constant';
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { SlowInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/slow-instance-command.interface';
|
||||
import { ConnectedAccountTokenEncryptionService } from 'src/engine/metadata-modules/connected-account/services/connected-account-token-encryption.service';
|
||||
import { ACCOUNT_TYPES } from 'twenty-shared/constants';
|
||||
|
||||
const BACKFILL_BATCH_SIZE = 500;
|
||||
|
||||
const CHECK_CONSTRAINT_NAME =
|
||||
'CHK_connectedAccount_connectionParameters_encrypted';
|
||||
|
||||
type ConnectionParametersRow = {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
connectionParameters: ImapSmtpCaldavParams | null;
|
||||
};
|
||||
|
||||
const hasPlaintextPassword = (params: ImapSmtpCaldavParams): boolean => {
|
||||
for (const protocol of ACCOUNT_TYPES) {
|
||||
const protocolParams = params[protocol];
|
||||
|
||||
if (
|
||||
isDefined(protocolParams?.password) &&
|
||||
!protocolParams.password.startsWith(SECRET_ENCRYPTION_ENVELOPE_V2_PREFIX)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
@RegisteredInstanceCommand('2.7.0', 1798000010000, { type: 'slow' })
|
||||
export class EncryptConnectionParametersSlowInstanceCommand
|
||||
implements SlowInstanceCommand
|
||||
{
|
||||
constructor(
|
||||
private readonly connectedAccountTokenEncryptionService: ConnectedAccountTokenEncryptionService,
|
||||
) {}
|
||||
|
||||
async runDataMigration(dataSource: DataSource): Promise<void> {
|
||||
let cursor = '00000000-0000-0000-0000-000000000000';
|
||||
|
||||
while (true) {
|
||||
const rows: ConnectionParametersRow[] = await dataSource.query(
|
||||
`SELECT id, "workspaceId", "connectionParameters"
|
||||
FROM "core"."connectedAccount"
|
||||
WHERE id > $1
|
||||
AND "connectionParameters" IS NOT NULL
|
||||
ORDER BY id
|
||||
LIMIT $2`,
|
||||
[cursor, BACKFILL_BATCH_SIZE],
|
||||
);
|
||||
|
||||
if (rows.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
for (const row of rows) {
|
||||
if (!isDefined(row.connectionParameters)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!hasPlaintextPassword(row.connectionParameters)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const plaintextOnly: ImapSmtpCaldavParams = {};
|
||||
|
||||
for (const protocol of ACCOUNT_TYPES) {
|
||||
const protocolParams = row.connectionParameters[protocol];
|
||||
|
||||
if (
|
||||
isDefined(protocolParams?.password) &&
|
||||
!protocolParams.password.startsWith(
|
||||
SECRET_ENCRYPTION_ENVELOPE_V2_PREFIX,
|
||||
)
|
||||
) {
|
||||
plaintextOnly[protocol] = protocolParams;
|
||||
}
|
||||
}
|
||||
|
||||
const encrypted =
|
||||
this.connectedAccountTokenEncryptionService.encryptConnectionParameters(
|
||||
{
|
||||
connectionParameters: plaintextOnly,
|
||||
workspaceId: row.workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
const merged: ImapSmtpCaldavParams = {
|
||||
...row.connectionParameters,
|
||||
...encrypted,
|
||||
};
|
||||
|
||||
await dataSource.query(
|
||||
`UPDATE "core"."connectedAccount"
|
||||
SET "connectionParameters" = $2
|
||||
WHERE id = $1`,
|
||||
[row.id, JSON.stringify(merged)],
|
||||
);
|
||||
}
|
||||
|
||||
cursor = rows[rows.length - 1].id;
|
||||
}
|
||||
}
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."connectedAccount"
|
||||
ADD CONSTRAINT "${CHECK_CONSTRAINT_NAME}"
|
||||
CHECK (
|
||||
"connectionParameters" IS NULL
|
||||
OR (
|
||||
(("connectionParameters"->'IMAP'->>'password') IS NULL OR ("connectionParameters"->'IMAP'->>'password') LIKE 'enc:v2:%')
|
||||
AND (("connectionParameters"->'SMTP'->>'password') IS NULL OR ("connectionParameters"->'SMTP'->>'password') LIKE 'enc:v2:%')
|
||||
AND (("connectionParameters"->'CALDAV'->>'password') IS NULL OR ("connectionParameters"->'CALDAV'->>'password') LIKE 'enc:v2:%')
|
||||
)
|
||||
)`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."connectedAccount"
|
||||
DROP CONSTRAINT IF EXISTS "${CHECK_CONSTRAINT_NAME}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
@@ -47,6 +47,7 @@ import { EncryptApplicationRegistrationVariableSlowInstanceCommand } from 'src/d
|
||||
import { EncryptSigningKeyPrivateKeysSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-5/2-5-instance-command-slow-1798000007000-encrypt-signing-key-private-keys';
|
||||
import { EncryptSensitiveConfigStorageSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-5/2-5-instance-command-slow-1798000008000-encrypt-sensitive-config-storage';
|
||||
import { EncryptTotpSecretsSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-5/2-5-instance-command-slow-1798000009000-encrypt-totp-secrets';
|
||||
import { EncryptConnectionParametersSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-7/2-7-instance-command-slow-1798000010000-encrypt-connection-parameters';
|
||||
import { AddSubFieldNameToViewSortFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-5/2-5-instance-command-fast-1778502963794-add-sub-field-name-to-view-sort';
|
||||
import { DropPostgresCredentialsTableFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-5/2-5-instance-command-fast-1798500000000-drop-postgres-credentials-table';
|
||||
import { AddRelationTargetFieldMetadataIdToViewFilterFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-6/2-6-instance-command-fast-1798000005000-add-relation-target-field-metadata-id-to-view-filter';
|
||||
@@ -100,6 +101,7 @@ export const INSTANCE_COMMANDS = [
|
||||
EncryptSigningKeyPrivateKeysSlowInstanceCommand,
|
||||
EncryptSensitiveConfigStorageSlowInstanceCommand,
|
||||
EncryptTotpSecretsSlowInstanceCommand,
|
||||
EncryptConnectionParametersSlowInstanceCommand,
|
||||
AddSubFieldNameToViewSortFastInstanceCommand,
|
||||
DropPostgresCredentialsTableFastInstanceCommand,
|
||||
AddRelationTargetFieldMetadataIdToViewFilterFastInstanceCommand,
|
||||
|
||||
Reference in New Issue
Block a user