Skip to content

Commit 66117b4

Browse files
authored
Add support for Docker credential helpers (#460)
This adds the ability to retrieve registry credentials from a credential helper specified in `credHelpers` or `credsStore` inside ~/.docker/config.json.
1 parent c552376 commit 66117b4

1 file changed

Lines changed: 105 additions & 3 deletions

File tree

src/spec-configuration/httpOCIRegistry.ts

Lines changed: 105 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as os from 'os';
22
import * as path from 'path';
33
import * as jsonc from 'jsonc-parser';
44

5+
import { runCommandNoPty, plainExec } from '../spec-common/commonUtils';
56
import { request, requestResolveHeaders } from '../spec-utils/httpRequest';
67
import { LogLevel } from '../spec-utils/log';
78
import { isLocalFile, readLocalFile } from '../spec-utils/pfs';
@@ -16,6 +17,15 @@ interface DockerConfigFile {
1617
identitytoken?: string; // Used by Azure Container Registry
1718
};
1819
};
20+
credHelpers: {
21+
[registry: string]: string;
22+
};
23+
credsStore: string;
24+
}
25+
26+
interface CredentialHelperResult {
27+
Username: string;
28+
Secret: string;
1929
}
2030

2131
// WWW-Authenticate Regex
@@ -137,8 +147,6 @@ async function getCredential(params: CommonParams, ociRef: OCIRef | OCICollectio
137147
const { output, env } = params;
138148
const { registry } = ociRef;
139149

140-
// TODO: Ask docker credential helper for credentials.
141-
142150
if (!!env['GITHUB_TOKEN'] && registry === 'ghcr.io') {
143151
output.write('[httpOci] Using environment GITHUB_TOKEN for auth', LogLevel.Trace);
144152
const userToken = `USERNAME:${env['GITHUB_TOKEN']}`;
@@ -161,15 +169,31 @@ async function getCredential(params: CommonParams, ociRef: OCIRef | OCICollectio
161169
};
162170
}
163171
} else {
172+
let configContainsAuth = false;
164173
try {
165174
const homeDir = os.homedir();
166175
if (homeDir) {
167176
const dockerConfigPath = path.join(homeDir, '.docker', 'config.json');
168177
if (await isLocalFile(dockerConfigPath)) {
169178
const dockerConfig: DockerConfigFile = jsonc.parse((await readLocalFile(dockerConfigPath)).toString());
170179

180+
configContainsAuth = Object.keys(dockerConfig.credHelpers).length > 0 || !!dockerConfig.credsStore || Object.keys(dockerConfig.auths).length > 0;
181+
if (dockerConfig.credHelpers && dockerConfig.credHelpers[registry]) {
182+
const credHelper = dockerConfig.credHelpers[registry];
183+
output.write(`[httpOci] Found credential helper '${credHelper}' in '${dockerConfigPath}' registry '${registry}'`, LogLevel.Trace);
184+
const auth = await getCredentialFromHelper(params, registry, credHelper);
185+
if (auth) {
186+
return auth;
187+
}
188+
} else if (dockerConfig.credsStore) {
189+
output.write(`[httpOci] Invoking credsStore credential helper '${dockerConfig.credsStore}'`, LogLevel.Trace);
190+
const auth = await getCredentialFromHelper(params, registry, dockerConfig.credsStore);
191+
if (auth) {
192+
return auth;
193+
}
194+
}
171195
if (dockerConfig.auths && dockerConfig.auths[registry]) {
172-
output.write(`[httpOci] Found entry in config.json for registry '${registry}'`, LogLevel.Trace);
196+
output.write(`[httpOci] Found auths entry in '${dockerConfigPath}' for registry '${registry}'`, LogLevel.Trace);
173197
const auth = dockerConfig.auths[registry].auth;
174198
const identityToken = dockerConfig.auths[registry].identitytoken; // Refresh token, seen when running: 'az acr login -n <registry>'
175199

@@ -191,13 +215,91 @@ async function getCredential(params: CommonParams, ociRef: OCIRef | OCICollectio
191215
} catch (err) {
192216
output.write(`[httpOci] Failed to read docker config.json: ${err}`, LogLevel.Trace);
193217
}
218+
219+
if (!configContainsAuth) {
220+
let defaultCredHelper = '';
221+
// Try platform-specific default credential helper
222+
if (process.platform === 'linux') {
223+
if (await existsInPath('pass')) {
224+
defaultCredHelper = 'pass';
225+
} else {
226+
defaultCredHelper = 'secret';
227+
}
228+
} else if (process.platform === 'win32') {
229+
defaultCredHelper = 'wincred';
230+
} else if (process.platform === 'darwin') {
231+
defaultCredHelper = 'osxkeychain';
232+
}
233+
if (defaultCredHelper !== '') {
234+
output.write(`[httpOci] Invoking platform default credential helper '${defaultCredHelper}'`, LogLevel.Trace);
235+
const auth = await getCredentialFromHelper(params, registry, defaultCredHelper);
236+
if (auth) {
237+
return auth;
238+
}
239+
}
240+
}
194241
}
195242

196243
// Represents anonymous access.
197244
output.write(`[httpOci] No authentication credentials found for registry '${registry}'. Accessing anonymously.`, LogLevel.Trace);
198245
return;
199246
}
200247

248+
async function existsInPath(filename: string): Promise<boolean> {
249+
if (!process.env.PATH) {
250+
return false;
251+
}
252+
const paths = process.env.PATH.split(':');
253+
for (const path of paths) {
254+
const fullPath = `${path}/${filename}`;
255+
if (await isLocalFile(fullPath)) {
256+
return true;
257+
}
258+
}
259+
return false;
260+
}
261+
262+
async function getCredentialFromHelper(params: CommonParams, registry: string, credHelperName: string) : Promise<{ base64EncodedCredential: string | undefined; refreshToken: string | undefined } | undefined>{
263+
const { output } = params;
264+
265+
let helperOutput: Buffer;
266+
try {
267+
const { stdout } = await runCommandNoPty({
268+
exec: plainExec(undefined),
269+
cmd: 'docker-credential-'+credHelperName,
270+
args: ['get'],
271+
stdin: Buffer.from(registry, 'utf-8'),
272+
output,
273+
});
274+
helperOutput = stdout;
275+
} catch (err) {
276+
output.write(`[httpOci] Failed to execute credential helper ${credHelperName}`, LogLevel.Error);
277+
return undefined;
278+
}
279+
if (helperOutput.length === 0) {
280+
return undefined;
281+
}
282+
283+
let errors: jsonc.ParseError[] = [];
284+
const creds:CredentialHelperResult = jsonc.parse(helperOutput.toString(), errors);
285+
if (errors.length !== 0) {
286+
output.write(`[httpOci] Credential helper ${credHelperName} returned non-JSON response "${helperOutput.toString()}" for registry ${registry}`, LogLevel.Warning);
287+
return undefined;
288+
}
289+
290+
if (creds.Username === '<token>') {
291+
return {
292+
refreshToken: creds.Secret,
293+
base64EncodedCredential: undefined,
294+
};
295+
}
296+
const userToken = `${creds.Username}:${creds.Secret}`;
297+
return {
298+
base64EncodedCredential: Buffer.from(userToken).toString('base64'),
299+
refreshToken: undefined,
300+
};
301+
}
302+
201303
// https://docs.docker.com/registry/spec/auth/token/#requesting-a-token
202304
async function fetchRegistryBearerToken(params: CommonParams, ociRef: OCIRef | OCICollectionRef, wwwAuthenticateData: { realm: string; service: string; scope: string }): Promise<string | undefined> {
203305
const { output } = params;

0 commit comments

Comments
 (0)