Skip to content

Commit

Permalink
feat(config): multi-org secrets decrypt (#21147)
Browse files Browse the repository at this point in the history
  • Loading branch information
rarkins committed Mar 25, 2023
1 parent c99c7da commit cec2e14
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 9 deletions.
4 changes: 4 additions & 0 deletions docs/usage/configuration-options.md
Expand Up @@ -802,13 +802,17 @@ For the full list of available managers, see the [Supported Managers](https://do

## encrypted

Use this to encrypt secrets in a way which can be stored in repository configs.

See [Private module support](https://docs.renovatebot.com/getting-started/private-packages) for details on how this is used to encrypt npm tokens.

<!-- prettier-ignore -->
!!! note
Encrypted secrets must have at least an org/group scope, and optionally a repository scope.
This means that Renovate will check if a secret's scope matches the current repository before applying it, and warn/discard if there is a mismatch.

Encrypted secrets typically have a single org, but you may encrypt a secret with more than one, e.g. specifying `org1,org2` to allow the secret to be used in both `org1` and `org2` organizations.

## excludeCommitPaths

Be careful you know what you're doing with this option.
Expand Down
34 changes: 34 additions & 0 deletions lib/config/decrypt.spec.ts
Expand Up @@ -161,6 +161,23 @@ describe('config/decrypt', () => {
);
});

it('handles PGP multi-org constraint', async () => {
GlobalConfig.set({ privateKey: privateKeyPgp });
config.encrypted = {
token:
'wcFMAw+4H7SgaqGOAQ//Yk4RTQoLEhO0TKxN2IUBrCi88ts+CG1SXKeL06sJ2qikN/3n2JYAGGKgkHRICfu5dOnsjyFdLJ1XWUrbsM3XgVWikMbrmzD1Xe7N5DsoZXlt4Wa9pZ+IkZuE6XcKKu9whIJ22ciEwCzFwDmk/CBshdCCVVQ3IYuM6uibEHn/AHQ8K15XhraiSzF6DbJpevs5Cy7b5YHFyE936H25CVnouUQnMPsirpQq3pYeMq/oOtV/m4mfRUUQ7MUxvtrwE4lq4hLjFu5n9rwlcqaFPl7I7BEM++1c9LFpYsP5mTS7hHCZ9wXBqER8fa3fKYx0bK1ihCpjP4zUkR7P/uhWDArXamv7gHX2Kj/Qsbegn7KjTdZlggAmaJl/CuSgCbhySy+E55g3Z1QFajiLRpQ5+RsWFDbbI08YEgzyQ0yNCaRvrkgo7kZ1D95rEGRfY96duOQbjzOEqtvYmFChdemZ2+f9Kh/JH1+X9ynxY/zYe/0p/U7WD3QNTYN18loc4aXiB1adXD5Ka2QfNroLudQBmLaJpJB6wASFfuxddsD5yRnO32NSdRaqIWC1x6ti3ZYJZ2RsNwJExPDzjpQTuMOH2jtpu3q7NHmW3snRKy2YAL2UjI0YdeKIlhc/qLCJt9MRcOxWYvujTMD/yGprhG44qf0jjMkJBu7NjuVIMONujabl9b7SUQGfO/t+3rMuC68bQdCGLlO8gf3hvtD99utzXphi6idjC0HKSW/9KzuMkm+syGmIAYq/0L3EFvpZ38uq7z8KzwFFQHI3sBA34bNEr5zpU5OMWg',
};
let res = await decryptConfig(config, repository);
expect(res.encrypted).toBeUndefined();
expect(res.token).toBe('123');
res = await decryptConfig(config, 'def/ghi');
expect(res.encrypted).toBeUndefined();
expect(res.token).toBe('123');
await expect(decryptConfig(config, 'wrong/org')).rejects.toThrow(
CONFIG_VALIDATION
);
});

it('handles PGP org/repo constraint', async () => {
GlobalConfig.set({ privateKey: privateKeyPgp });
config.encrypted = {
Expand All @@ -174,5 +191,22 @@ describe('config/decrypt', () => {
CONFIG_VALIDATION
);
});

it('handles PGP multi-org/repo constraint', async () => {
GlobalConfig.set({ privateKey: privateKeyPgp });
config.encrypted = {
token:
'wcFMAw+4H7SgaqGOARAAibXL3zr0KZawiND868UGdPpGRo1aVZfn0NUBHpm8mXfgB1rBHaLsP7qa8vxDHpwH9DRD1IyB4vvPUwtu7wmuv1Vtr596tD40CCcCZYB5JjZLWRF0O0xaZFCOi7Z9SqqdaOQoMScyvPO+3/lJkS7zmLllJFH0mQoX5Cr+owUAMSWqbeCQ9r/KAXpnhmpraDjTav48WulcdTMc8iQ/DHimcdzHErLOAjtiQi4OUe1GnDCcN76KQ+c+ZHySnkXrYi/DhOOu9qB4glJ5n68NueFja+8iR39z/wqCI6V6TIUiOyjFN86iVyNPQ4Otem3KuNwrnwSABLDqP491eUNjT8DUDffsyhNC9lnjQLmtViK0EN2yLVpMdHq9cq8lszBChB7gobD9rm8nUHnTuLf6yJvZOj6toD5Yqj8Ibj58wN90Q8CUsBp9/qp0J+hBVUPOx4sT6kM2p6YarlgX3mrIW5c1U+q1eDbCddLjHiU5cW7ja7o+cqlA6mbDRu3HthjBweiXTicXZcRu1o/wy/+laQQ95x5FzAXDnOwQUHBmpTDI3tUJvQ+oy8XyBBbyC0LsBye2c2SLkPJ4Ai3IMR+Mh8puSzVywTbneiAQNBzJHlj5l85nCF2tUjvNo3dWC+9mU5sfXg11iEC6LRbg+icjpqRtTjmQURtciKDUbibWacwU5T/SVAGPXnW7adBOS0PZPIZQcSwjchOdOl0IjzBy6ofu7ODdn2CXZXi8zbevTICXsHvjnW4MAj5oXrStxK3LkWyM3YBOLe7sOfWvWz7n9TM3dHg032navQ',
};
let res = await decryptConfig(config, repository);
expect(res.encrypted).toBeUndefined();
expect(res.token).toBe('123');
res = await decryptConfig(config, 'def/def');
expect(res.encrypted).toBeUndefined();
expect(res.token).toBe('123');
await expect(decryptConfig(config, 'abc/defg')).rejects.toThrow(
CONFIG_VALIDATION
);
});
});
});
30 changes: 21 additions & 9 deletions lib/config/decrypt.ts
Expand Up @@ -5,6 +5,7 @@ import { logger } from '../logger';
import { maskToken } from '../util/mask';
import { regEx } from '../util/regex';
import { addSecretForSanitizing } from '../util/sanitize';
import { ensureTrailingSlash } from '../util/url';
import { GlobalConfig } from './global';
import { DecryptedObject } from './schema';
import type { RenovateConfig } from './types';
Expand Down Expand Up @@ -106,33 +107,44 @@ export async function tryDecrypt(
const { o: org, r: repo, v: value } = decryptedObj.data;
if (is.nonEmptyString(value)) {
if (is.nonEmptyString(org)) {
const orgName = org.replace(regEx(/\/$/), ''); // Strip trailing slash
const orgPrefixes = org
.split(',')
.map((o) => o.trim())
.map((o) => o.toUpperCase())
.map((o) => ensureTrailingSlash(o));
if (is.nonEmptyString(repo)) {
const scopedRepository = `${orgName}/${repo}`;
if (scopedRepository.toLowerCase() === repository.toLowerCase()) {
const scopedRepos = orgPrefixes.map((orgPrefix) =>
`${orgPrefix}${repo}`.toUpperCase()
);
if (scopedRepos.some((r) => r === repository.toUpperCase())) {
decryptedStr = value;
} else {
logger.debug(
{ scopedRepository },
{ scopedRepos },
'Secret is scoped to a different repository'
);
const error = new Error('config-validation');
error.validationError = `Encrypted secret is scoped to a different repository: "${scopedRepository}".`;
error.validationError = `Encrypted secret is scoped to a different repository: "${scopedRepos.join(
','
)}".`;
throw error;
}
} else {
const scopedOrg = `${orgName}/`;
if (
repository.toLowerCase().startsWith(scopedOrg.toLowerCase())
orgPrefixes.some((orgPrefix) =>
repository.toUpperCase().startsWith(orgPrefix)
)
) {
decryptedStr = value;
} else {
logger.debug(
{ scopedOrg },
{ orgPrefixes },
'Secret is scoped to a different org'
);
const error = new Error('config-validation');
error.validationError = `Encrypted secret is scoped to a different org: "${scopedOrg}".`;
error.validationError = `Encrypted secret is scoped to a different org: "${orgPrefixes.join(
','
)}".`;
throw error;
}
}
Expand Down

0 comments on commit cec2e14

Please sign in to comment.