Skip to content
5 changes: 5 additions & 0 deletions .changeset/deploy-tooling-prevent-cleartext-credentials.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sap-ux/deploy-tooling": patch
---

fix(deploy-tooling): prevent cleartext credentials in deploy configuration
17 changes: 17 additions & 0 deletions packages/deploy-tooling/src/base/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,20 @@ function validateTarget(target: AbapTarget): AbapTarget {
return target;
}

/**
* Validates that credentials are provided as environment variable references, not as plain text.
*
* @param credentials - credentials to validate
*/
function validateCredentials(credentials: NonNullable<AbapDeployConfig['credentials']>): void {
const isEnvRef = (value: string | undefined): boolean => !value || value.startsWith('env:');
if (!isEnvRef(credentials.username) || !isEnvRef(credentials.password)) {
throw new Error(
'Credentials must be provided as environment variable references (e.g. env:MY_VAR), not as plain text.'
Comment thread
longieirl marked this conversation as resolved.
Outdated
);
}
}

/**
* Type checking the config object.
*
Expand Down Expand Up @@ -87,6 +101,9 @@ export function validateConfig(config: AbapDeployConfig | undefined, logger?: Lo
if (!config.app) {
throwConfigMissingError('app');
}
if (config.credentials) {
validateCredentials(config.credentials);
}

return config;
}
77 changes: 77 additions & 0 deletions packages/deploy-tooling/test/unit/base/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,5 +131,82 @@ describe('base/config', () => {
expect(() => validateConfig(config)).not.toThrow();
expect(config.app.package).toBe('$TMP');
});

describe('validateCredentials', () => {
test('throws when username is plaintext (no env: prefix)', () => {
// given a config with a plaintext username
const config = {
...validConfig,
credentials: { username: 'admin', password: 'env:MY_PASSWORD' }
} as AbapDeployConfig;

// when validateConfig is called
// then it throws with the expected message
expect(() => validateConfig(config)).toThrow(
'Credentials must be provided as environment variable references'
);
});

test('throws when password is plaintext (no env: prefix)', () => {
// given a config with a plaintext password
const config = {
...validConfig,
credentials: { username: 'env:MY_USER', password: 'secret' }
} as AbapDeployConfig;

// when validateConfig is called
// then it throws with the expected message
expect(() => validateConfig(config)).toThrow(
'Credentials must be provided as environment variable references'
);
});

test('throws when both username and password are plaintext', () => {
// given a config with plaintext username and password
const config = {
...validConfig,
credentials: { username: 'admin', password: 'secret' }
} as AbapDeployConfig;

// when validateConfig is called
// then it throws with the expected message
expect(() => validateConfig(config)).toThrow(
'Credentials must be provided as environment variable references'
);
});

test('passes when both username and password use env: prefix', () => {
// given a config with env-referenced credentials
const config = {
...validConfig,
credentials: { username: 'env:MY_USER', password: 'env:MY_PASSWORD' }
} as AbapDeployConfig;

// when validateConfig is called
// then it does not throw
expect(() => validateConfig(config)).not.toThrow();
});

test('passes when credentials are absent', () => {
// given a config without credentials
const config = { ...validConfig } as AbapDeployConfig;

// when validateConfig is called
// then it does not throw
expect(() => validateConfig(config)).not.toThrow();
});

test('passes when only username is set with env: prefix and password is absent', () => {
// given a config with only username set via env:
const config = {
...validConfig,
credentials: { username: 'env:MY_USER' }
} as unknown as AbapDeployConfig;

// when validateConfig is called
// then it does not throw
expect(() => validateConfig(config)).not.toThrow();
});
});
});
});
Loading