fix(core): Redact credentials (#13263)

This commit is contained in:
Tomi Turtiainen
2025-02-14 16:46:21 +02:00
committed by GitHub
parent d116f121e3
commit 052f17744d
7 changed files with 157 additions and 63 deletions
@@ -130,7 +130,7 @@ export class CredentialsController {
}
const mergedCredentials = deepCopy(credentials);
const decryptedData = this.credentialsService.decrypt(storedCredential);
const decryptedData = this.credentialsService.decrypt(storedCredential, true);
// When a sharee (or project viewer) opens a credential, the fields and the
// credential data are missing so the payload will be empty
@@ -143,14 +143,14 @@ export class CredentialsController {
mergedCredentials,
);
if (mergedCredentials.data && storedCredential) {
if (mergedCredentials.data) {
mergedCredentials.data = this.credentialsService.unredact(
mergedCredentials.data,
decryptedData,
);
}
return await this.credentialsService.test(req.user, mergedCredentials);
return await this.credentialsService.test(req.user.id, mergedCredentials);
}
@Post('/')
@@ -176,18 +176,22 @@ export class CredentialsController {
@Patch('/:credentialId')
@ProjectScope('credential:update')
async updateCredentials(req: CredentialRequest.Update) {
const { credentialId } = req.params;
const {
body,
user,
params: { credentialId },
} = req;
const credential = await this.sharedCredentialsRepository.findCredentialForUser(
credentialId,
req.user,
user,
['credential:update'],
);
if (!credential) {
this.logger.info('Attempt to update credential blocked due to lack of permissions', {
credentialId,
userId: req.user.id,
userId: user.id,
});
throw new NotFoundError(
'Credential to be updated not found. You can only update credentials owned by you',
@@ -199,6 +203,8 @@ export class CredentialsController {
}
const decryptedData = this.credentialsService.decrypt(credential, true);
// We never want to allow users to change the oauthTokenData
delete body.data?.oauthTokenData;
const preparedCredentialData = await this.credentialsService.prepareUpdateData(
req.body,
decryptedData,
@@ -87,7 +87,7 @@ export class EnterpriseCredentialsService {
if (credential) {
// Decrypt the data if we found the credential with the `credential:update`
// scope.
decryptedData = this.credentialsService.decrypt(credential, true);
decryptedData = this.credentialsService.decrypt(credential);
} else {
// Otherwise try to find them with only the `credential:read` scope. In
// that case we return them without the decrypted data.
@@ -109,6 +109,11 @@ export class EnterpriseCredentialsService {
const { data: _, ...rest } = credential;
if (decryptedData) {
// We never want to expose the oauthTokenData to the frontend, but it
// expects it to check if the credential is already connected.
if (decryptedData?.oauthTokenData) {
decryptedData.oauthTokenData = true;
}
return { data: decryptedData, ...rest };
}
@@ -112,9 +112,16 @@ export class CredentialsService {
if (includeData) {
credentials = credentials.map((c: CredentialsEntity & ScopesField) => {
const data = c.scopes.includes('credential:update') ? this.decrypt(c) : undefined;
// We never want to expose the oauthTokenData to the frontend, but it
// expects it to check if the credential is already connected.
if (data?.oauthTokenData) {
data.oauthTokenData = true;
}
return {
...c,
data: c.scopes.includes('credential:update') ? this.decrypt(c) : undefined,
data,
} as unknown as CredentialsEntity;
});
}
@@ -441,8 +448,8 @@ export class CredentialsService {
await this.credentialsRepository.remove(credential);
}
async test(user: User, credentials: ICredentialsDecrypted) {
return await this.credentialsTester.testCredentials(user, credentials.type, credentials);
async test(userId: User['id'], credentials: ICredentialsDecrypted) {
return await this.credentialsTester.testCredentials(userId, credentials.type, credentials);
}
// Take data and replace all sensitive values with a sentinel value.
@@ -553,7 +560,7 @@ export class CredentialsService {
if (sharing) {
// Decrypt the data if we found the credential with the `credential:update`
// scope.
decryptedData = this.decrypt(sharing.credentials, true);
decryptedData = this.decrypt(sharing.credentials);
} else {
// Otherwise try to find them with only the `credential:read` scope. In
// that case we return them without the decrypted data.
@@ -569,6 +576,11 @@ export class CredentialsService {
const { data: _, ...rest } = credential;
if (decryptedData) {
// We never want to expose the oauthTokenData to the frontend, but it
// expects it to check if the credential is already connected.
if (decryptedData?.oauthTokenData) {
decryptedData.oauthTokenData = true;
}
return { data: decryptedData, ...rest };
}
return { ...rest };
@@ -172,7 +172,7 @@ export class CredentialsTester {
// eslint-disable-next-line complexity
async testCredentials(
user: User,
userId: User['id'],
credentialType: string,
credentialsDecrypted: ICredentialsDecrypted,
): Promise<INodeCredentialTestResult> {
@@ -186,7 +186,7 @@ export class CredentialsTester {
if (credentialsDecrypted.data) {
try {
const additionalData = await WorkflowExecuteAdditionalData.getBase(user.id);
const additionalData = await WorkflowExecuteAdditionalData.getBase(userId);
credentialsDecrypted.data = this.credentialsHelper.applyDefaultsAndOverwrites(
additionalData,
credentialsDecrypted.data,
@@ -292,7 +292,7 @@ export class CredentialsTester {
},
};
const additionalData = await WorkflowExecuteAdditionalData.getBase(user.id, node.parameters);
const additionalData = await WorkflowExecuteAdditionalData.getBase(userId, node.parameters);
const executeData: IExecuteData = { node, data: {}, source: null };
const executeFunctions = new ExecuteContext(