Skip to content

Commit

Permalink
Add credHelpers to produce credentials
Browse files Browse the repository at this point in the history
Signed-off-by: GregoireW <[email protected]>
  • Loading branch information
GregoireW committed Sep 3, 2024
1 parent f8b63d6 commit dfc4123
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 18 deletions.
5 changes: 5 additions & 0 deletions .changeset/plenty-knives-kneel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sigstore/oci': minor
---

getRegistryCredentials can now use credHelpers from docker configuration file
4 changes: 2 additions & 2 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const compat = new FlatCompat({
});

export default [{
ignores: ["**/node_modules", "**/dist", "**/__generated__", "**/__fixtures__", "**/jest.config.js", "**/jest.config.base.js"],
ignores: ["**/node_modules", "**/dist", "**/__generated__", "**/__fixtures__", "**/jest.config.js", "**/jest.config.base.js", "**/hack"],
}, ...compat.extends(
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
Expand All @@ -32,4 +32,4 @@ export default [{
"@typescript-eslint/no-unused-vars": ["error", { "caughtErrors": "none" }],
"@typescript-eslint/no-require-imports": "off"
},
}];
}];
93 changes: 93 additions & 0 deletions packages/oci/src/__tests__/credentials.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ limitations under the License.
*/
import fs from 'fs';
import os from 'os';
import child_process, { ExecFileSyncOptions } from 'node:child_process';
import path from 'path';
import {
fromBasicAuth,
Expand All @@ -25,18 +26,38 @@ import {
describe('getRegistryCredentials', () => {
const registryName = 'my-registry';
const imageName = `${registryName}/my-image`;
const badRegistryName = 'bad-registry';
const imageNameBadRegistry = `${badRegistryName}/my-image`;
let homedirSpy: jest.SpyInstance<string, []> | undefined;
let execSpy: jest.SpyInstance<string | Buffer, [file: string, args?: readonly string[] | undefined, options?: ExecFileSyncOptions | undefined]> | undefined;
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'get-reg-creds-'));
const dockerDir = path.join(tempDir, '.docker');
fs.mkdirSync(dockerDir, { recursive: true });

beforeEach(() => {
homedirSpy = jest.spyOn(os, 'homedir');
homedirSpy.mockReturnValue(tempDir);

execSpy = jest.spyOn(child_process, 'execFileSync');
execSpy?.mockImplementation((file, args, options) => {
if (file!=="docker-credential-fake" || args?.length!=1 || args[0]!=="get"){
throw "Invalid arguments";
}

if (options?.input === `${registryName}`) {
const credentials = {
Username: 'username',
Secret: 'password'
};
return JSON.stringify(credentials);
}
throw "Invalid registry";
});
});

afterEach(() => {
homedirSpy?.mockRestore();
execSpy?.mockRestore();
});

afterAll(() => {
Expand Down Expand Up @@ -168,8 +189,80 @@ describe('getRegistryCredentials', () => {
expect(creds).toEqual({ username, password });
});
});

describe('when credHelper exit in error', () => {
const dockerConfig = {
credHelpers: {
[badRegistryName]: "fake"
}
};

beforeEach(() => {
fs.writeFileSync(
path.join(tempDir, '.docker', 'config.json'),
JSON.stringify(dockerConfig),
{}
);
});

it('throws an error', () => {
expect(() => getRegistryCredentials(imageNameBadRegistry)).toThrow(
/Failed to get credentials from helper fake for registry bad-registry/i
);
});
});


describe('when credHelper exist for the registry', () => {
const username = 'username';
const password = 'password';

const dockerConfig = {
credHelpers: {
[registryName]: "fake"
}
};

beforeEach(() => {
fs.writeFileSync(
path.join(tempDir, '.docker', 'config.json'),
JSON.stringify(dockerConfig),
{}
);
});

it('returns the credentials', () => {
const creds = getRegistryCredentials(imageName);

expect(creds).toEqual({ username, password });
});
});

describe('when credsStore exist', () => {
const username = 'username';
const password = 'password';

const dockerConfig = {
credsStore: "fake"
};

beforeEach(() => {
fs.writeFileSync(
path.join(tempDir, '.docker', 'config.json'),
JSON.stringify(dockerConfig),
{}
);
});

it('returns the credentials', () => {
const creds = getRegistryCredentials(imageName);

expect(creds).toEqual({ username, password });
});
});
});


describe('toBasicAuth', () => {
const creds = { username: 'user', password: 'pass' };
const expected = 'dXNlcjpwYXNz';
Expand Down
90 changes: 74 additions & 16 deletions packages/oci/src/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ limitations under the License.
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { execFileSync } from 'node:child_process';
import { parseImageName } from './name';

export type Credentials = {
Expand All @@ -24,32 +25,21 @@ export type Credentials = {
};

type DockerConifg = {
auths?: { [registry: string]: { auth: string; identitytoken?: string } };
credsStore?: string,
auths?: { [registry: string]: { auth: string; identitytoken?: string } },
credHelpers?: { [registry: string]: string },
};

// Returns the credentials for a given registry by reading the Docker config
// file.
export const getRegistryCredentials = (imageName: string): Credentials => {
const { registry } = parseImageName(imageName);
const dockerConfigFile = path.join(os.homedir(), '.docker', 'config.json');

let content: string | undefined;
try {
content = fs.readFileSync(dockerConfigFile, 'utf8');
} catch (err) {
throw new Error(`No credential file found at ${dockerConfigFile}`);
}

const dockerConfig: DockerConifg = JSON.parse(content);

function credentialFromAuths(dockerConfig: DockerConifg, registry: string) {
const credKey =
Object.keys(dockerConfig?.auths || {}).find((key) =>
key.includes(registry)
) || registry;
const creds = dockerConfig?.auths?.[credKey];

if (!creds) {
throw new Error(`No credentials found for registry ${registry}`);
return null;
}

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

return { username, password: pass };
}

function credentialFromCredHelpers(dockerConfig: DockerConifg, registry: string) {
// Check if the registry has a credHelper and use it if it does
const helper = dockerConfig?.credHelpers?.[registry];

if (!helper) {
return null;
}

return launchHelper(helper, registry);
}

function credentialFromCredsStore(dockerConfig: DockerConifg, registry: string) {
// If the credsStore is set, use it to get the credentials
const helper = dockerConfig?.credsStore;

if (!helper) {
return null;
}

return launchHelper(helper, registry);
}

function launchHelper(helper: string, registry: string) {
// Get the credentials from the helper.
// Parameter for helper is 'get' and registry is passed as input
// The helper should return a JSON object with the keys "Username" and "Secret"
try {
const output = execFileSync(`docker-credential-${helper}`, ["get"], {
input: registry,
}).toString();

const { Username: username, Secret: password } = JSON.parse(output);

return { username, password };
} catch (err) {
throw new Error(`Failed to get credentials from helper ${helper} for registry ${registry}: ${err}`);
}
}

// file.
export const getRegistryCredentials = (imageName: string): Credentials => {
const { registry } = parseImageName(imageName);
const dockerConfigFile = path.join(os.homedir(), '.docker', 'config.json');

let content: string | undefined;
try {
content = fs.readFileSync(dockerConfigFile, 'utf8');
} catch (err) {
throw new Error(`No credential file found at ${dockerConfigFile}`);
}

const dockerConfig: DockerConifg = JSON.parse(content);

const fromAuths=credentialFromAuths(dockerConfig, registry);
if (fromAuths) {
return fromAuths;
}
const fromCredHelpers=credentialFromCredHelpers(dockerConfig, registry);
if (fromCredHelpers) {
return fromCredHelpers;
}
const fromCredsStore=credentialFromCredsStore(dockerConfig, registry);
if (fromCredsStore) {
return fromCredsStore;
}
throw new Error(`No credentials found for registry ${registry}`);
};

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

0 comments on commit dfc4123

Please sign in to comment.