mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-12 15:06:27 +00:00
feat(config): multi-org secrets decrypt (#21147)
This commit is contained in:
parent
c99c7dac3d
commit
cec2e14a64
3 changed files with 59 additions and 9 deletions
|
@ -802,6 +802,8 @@ For the full list of available managers, see the [Supported Managers](https://do
|
||||||
|
|
||||||
## encrypted
|
## 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.
|
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 -->
|
<!-- prettier-ignore -->
|
||||||
|
@ -809,6 +811,8 @@ See [Private module support](https://docs.renovatebot.com/getting-started/privat
|
||||||
Encrypted secrets must have at least an org/group scope, and optionally a repository scope.
|
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.
|
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
|
## excludeCommitPaths
|
||||||
|
|
||||||
Be careful you know what you're doing with this option.
|
Be careful you know what you're doing with this option.
|
||||||
|
|
|
@ -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 () => {
|
it('handles PGP org/repo constraint', async () => {
|
||||||
GlobalConfig.set({ privateKey: privateKeyPgp });
|
GlobalConfig.set({ privateKey: privateKeyPgp });
|
||||||
config.encrypted = {
|
config.encrypted = {
|
||||||
|
@ -174,5 +191,22 @@ describe('config/decrypt', () => {
|
||||||
CONFIG_VALIDATION
|
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
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { logger } from '../logger';
|
||||||
import { maskToken } from '../util/mask';
|
import { maskToken } from '../util/mask';
|
||||||
import { regEx } from '../util/regex';
|
import { regEx } from '../util/regex';
|
||||||
import { addSecretForSanitizing } from '../util/sanitize';
|
import { addSecretForSanitizing } from '../util/sanitize';
|
||||||
|
import { ensureTrailingSlash } from '../util/url';
|
||||||
import { GlobalConfig } from './global';
|
import { GlobalConfig } from './global';
|
||||||
import { DecryptedObject } from './schema';
|
import { DecryptedObject } from './schema';
|
||||||
import type { RenovateConfig } from './types';
|
import type { RenovateConfig } from './types';
|
||||||
|
@ -106,33 +107,44 @@ export async function tryDecrypt(
|
||||||
const { o: org, r: repo, v: value } = decryptedObj.data;
|
const { o: org, r: repo, v: value } = decryptedObj.data;
|
||||||
if (is.nonEmptyString(value)) {
|
if (is.nonEmptyString(value)) {
|
||||||
if (is.nonEmptyString(org)) {
|
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)) {
|
if (is.nonEmptyString(repo)) {
|
||||||
const scopedRepository = `${orgName}/${repo}`;
|
const scopedRepos = orgPrefixes.map((orgPrefix) =>
|
||||||
if (scopedRepository.toLowerCase() === repository.toLowerCase()) {
|
`${orgPrefix}${repo}`.toUpperCase()
|
||||||
|
);
|
||||||
|
if (scopedRepos.some((r) => r === repository.toUpperCase())) {
|
||||||
decryptedStr = value;
|
decryptedStr = value;
|
||||||
} else {
|
} else {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
{ scopedRepository },
|
{ scopedRepos },
|
||||||
'Secret is scoped to a different repository'
|
'Secret is scoped to a different repository'
|
||||||
);
|
);
|
||||||
const error = new Error('config-validation');
|
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;
|
throw error;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const scopedOrg = `${orgName}/`;
|
|
||||||
if (
|
if (
|
||||||
repository.toLowerCase().startsWith(scopedOrg.toLowerCase())
|
orgPrefixes.some((orgPrefix) =>
|
||||||
|
repository.toUpperCase().startsWith(orgPrefix)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
decryptedStr = value;
|
decryptedStr = value;
|
||||||
} else {
|
} else {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
{ scopedOrg },
|
{ orgPrefixes },
|
||||||
'Secret is scoped to a different org'
|
'Secret is scoped to a different org'
|
||||||
);
|
);
|
||||||
const error = new Error('config-validation');
|
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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue