Skip to content

Commit 01e24c3

Browse files
committed
Add credHelpers to produce credentials
1 parent f8b63d6 commit 01e24c3

File tree

4 files changed

+174
-18
lines changed

4 files changed

+174
-18
lines changed

.changeset/plenty-knives-kneel.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sigstore/oci': minor
3+
---
4+
5+
getRegistryCredentials can now use credHelpers from docker configuration file

eslint.config.mjs

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const compat = new FlatCompat({
1414
});
1515

1616
export default [{
17-
ignores: ["**/node_modules", "**/dist", "**/__generated__", "**/__fixtures__", "**/jest.config.js", "**/jest.config.base.js"],
17+
ignores: ["**/node_modules", "**/dist", "**/__generated__", "**/__fixtures__", "**/jest.config.js", "**/jest.config.base.js", "**/hack"],
1818
}, ...compat.extends(
1919
"eslint:recommended",
2020
"plugin:@typescript-eslint/eslint-recommended",
@@ -32,4 +32,4 @@ export default [{
3232
"@typescript-eslint/no-unused-vars": ["error", { "caughtErrors": "none" }],
3333
"@typescript-eslint/no-require-imports": "off"
3434
},
35-
}];
35+
}];

packages/oci/src/__tests__/credentials.test.ts

+93
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ limitations under the License.
1515
*/
1616
import fs from 'fs';
1717
import os from 'os';
18+
import child_process, { ExecFileSyncOptions } from 'node:child_process';
1819
import path from 'path';
1920
import {
2021
fromBasicAuth,
@@ -25,18 +26,38 @@ import {
2526
describe('getRegistryCredentials', () => {
2627
const registryName = 'my-registry';
2728
const imageName = `${registryName}/my-image`;
29+
const badRegistryName = 'bad-registry';
30+
const imageNameBadRegistry = `${badRegistryName}/my-image`;
2831
let homedirSpy: jest.SpyInstance<string, []> | undefined;
32+
let execSpy: jest.SpyInstance<string | Buffer, [file: string, args?: readonly string[] | undefined, options?: ExecFileSyncOptions | undefined]> | undefined;
2933
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'get-reg-creds-'));
3034
const dockerDir = path.join(tempDir, '.docker');
3135
fs.mkdirSync(dockerDir, { recursive: true });
3236

3337
beforeEach(() => {
3438
homedirSpy = jest.spyOn(os, 'homedir');
3539
homedirSpy.mockReturnValue(tempDir);
40+
41+
execSpy = jest.spyOn(child_process, 'execFileSync');
42+
execSpy?.mockImplementation((file, args, options) => {
43+
if (file!=="docker-credential-fake" || args?.length!=1 || args[0]!=="get"){
44+
throw "Invalid arguments";
45+
}
46+
47+
if (options?.input === `${registryName}`) {
48+
const credentials = {
49+
Username: 'username',
50+
Secret: 'password'
51+
};
52+
return JSON.stringify(credentials);
53+
}
54+
throw "Invalid registry";
55+
});
3656
});
3757

3858
afterEach(() => {
3959
homedirSpy?.mockRestore();
60+
execSpy?.mockRestore();
4061
});
4162

4263
afterAll(() => {
@@ -168,8 +189,80 @@ describe('getRegistryCredentials', () => {
168189
expect(creds).toEqual({ username, password });
169190
});
170191
});
192+
193+
describe('when credHelper exit in error', () => {
194+
const dockerConfig = {
195+
credHelpers: {
196+
[badRegistryName]: "fake"
197+
}
198+
};
199+
200+
beforeEach(() => {
201+
fs.writeFileSync(
202+
path.join(tempDir, '.docker', 'config.json'),
203+
JSON.stringify(dockerConfig),
204+
{}
205+
);
206+
});
207+
208+
it('throws an error', () => {
209+
expect(() => getRegistryCredentials(imageNameBadRegistry)).toThrow(
210+
/Failed to get credentials from helper fake for registry bad-registry/i
211+
);
212+
});
213+
});
214+
215+
216+
describe('when credHelper exist for the registry', () => {
217+
const username = 'username';
218+
const password = 'password';
219+
220+
const dockerConfig = {
221+
credHelpers: {
222+
[registryName]: "fake"
223+
}
224+
};
225+
226+
beforeEach(() => {
227+
fs.writeFileSync(
228+
path.join(tempDir, '.docker', 'config.json'),
229+
JSON.stringify(dockerConfig),
230+
{}
231+
);
232+
});
233+
234+
it('returns the credentials', () => {
235+
const creds = getRegistryCredentials(imageName);
236+
237+
expect(creds).toEqual({ username, password });
238+
});
239+
});
240+
241+
describe('when credsStore exist', () => {
242+
const username = 'username';
243+
const password = 'password';
244+
245+
const dockerConfig = {
246+
credsStore: "fake"
247+
};
248+
249+
beforeEach(() => {
250+
fs.writeFileSync(
251+
path.join(tempDir, '.docker', 'config.json'),
252+
JSON.stringify(dockerConfig),
253+
{}
254+
);
255+
});
256+
257+
it('returns the credentials', () => {
258+
const creds = getRegistryCredentials(imageName);
259+
260+
expect(creds).toEqual({ username, password });
261+
});
262+
});
171263
});
172264

265+
173266
describe('toBasicAuth', () => {
174267
const creds = { username: 'user', password: 'pass' };
175268
const expected = 'dXNlcjpwYXNz';

packages/oci/src/credentials.ts

+74-16
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ limitations under the License.
1616
import fs from 'node:fs';
1717
import os from 'node:os';
1818
import path from 'node:path';
19+
import { execFileSync } from 'node:child_process';
1920
import { parseImageName } from './name';
2021

2122
export type Credentials = {
@@ -24,32 +25,21 @@ export type Credentials = {
2425
};
2526

2627
type DockerConifg = {
27-
auths?: { [registry: string]: { auth: string; identitytoken?: string } };
28+
credsStore?: string,
29+
auths?: { [registry: string]: { auth: string; identitytoken?: string } },
30+
credHelpers?: { [registry: string]: string },
2831
};
2932

3033
// Returns the credentials for a given registry by reading the Docker config
31-
// file.
32-
export const getRegistryCredentials = (imageName: string): Credentials => {
33-
const { registry } = parseImageName(imageName);
34-
const dockerConfigFile = path.join(os.homedir(), '.docker', 'config.json');
35-
36-
let content: string | undefined;
37-
try {
38-
content = fs.readFileSync(dockerConfigFile, 'utf8');
39-
} catch (err) {
40-
throw new Error(`No credential file found at ${dockerConfigFile}`);
41-
}
42-
43-
const dockerConfig: DockerConifg = JSON.parse(content);
44-
34+
function credentialFromAuths(dockerConfig: DockerConifg, registry: string) {
4535
const credKey =
4636
Object.keys(dockerConfig?.auths || {}).find((key) =>
4737
key.includes(registry)
4838
) || registry;
4939
const creds = dockerConfig?.auths?.[credKey];
5040

5141
if (!creds) {
52-
throw new Error(`No credentials found for registry ${registry}`);
42+
return null;
5343
}
5444

5545
// Extract username/password from auth string
@@ -59,6 +49,74 @@ export const getRegistryCredentials = (imageName: string): Credentials => {
5949
const pass = creds.identitytoken ? creds.identitytoken : password;
6050

6151
return { username, password: pass };
52+
}
53+
54+
function credentialFromCredHelpers(dockerConfig: DockerConifg, registry: string) {
55+
// Check if the registry has a credHelper and use it if it does
56+
const helper = dockerConfig?.credHelpers?.[registry];
57+
58+
if (!helper) {
59+
return null;
60+
}
61+
62+
return launchHelper(helper, registry);
63+
}
64+
65+
function credentialFromCredsStore(dockerConfig: DockerConifg, registry: string) {
66+
// If the credsStore is set, use it to get the credentials
67+
const helper = dockerConfig?.credsStore;
68+
69+
if (!helper) {
70+
return null;
71+
}
72+
73+
return launchHelper(helper, registry);
74+
}
75+
76+
function launchHelper(helper: string, registry: string) {
77+
// Get the credentials from the helper.
78+
// Parameter for helper is 'get' and registry is passed as input
79+
// The helper should return a JSON object with the keys "Username" and "Secret"
80+
try {
81+
const output = execFileSync(`docker-credential-${helper}`, ["get"], {
82+
input: registry,
83+
}).toString();
84+
85+
const { Username: username, Secret: password } = JSON.parse(output);
86+
87+
return { username, password };
88+
} catch (err) {
89+
throw new Error(`Failed to get credentials from helper ${helper} for registry ${registry}: ${err}`);
90+
}
91+
}
92+
93+
// file.
94+
export const getRegistryCredentials = (imageName: string): Credentials => {
95+
const { registry } = parseImageName(imageName);
96+
const dockerConfigFile = path.join(os.homedir(), '.docker', 'config.json');
97+
98+
let content: string | undefined;
99+
try {
100+
content = fs.readFileSync(dockerConfigFile, 'utf8');
101+
} catch (err) {
102+
throw new Error(`No credential file found at ${dockerConfigFile}`);
103+
}
104+
105+
const dockerConfig: DockerConifg = JSON.parse(content);
106+
107+
const fromAuths=credentialFromAuths(dockerConfig, registry);
108+
if (fromAuths) {
109+
return fromAuths;
110+
}
111+
const fromCredHelpers=credentialFromCredHelpers(dockerConfig, registry);
112+
if (fromCredHelpers) {
113+
return fromCredHelpers;
114+
}
115+
const fromCredsStore=credentialFromCredsStore(dockerConfig, registry);
116+
if (fromCredsStore) {
117+
return fromCredsStore;
118+
}
119+
throw new Error(`No credentials found for registry ${registry}`);
62120
};
63121

64122
// Encode the username and password as base64-encoded basicauth value

0 commit comments

Comments
 (0)