Skip to content

Commit 716647a

Browse files
authored
reorganize auth precedence levels in httpOCIRegistry
1 parent 19f369f commit 716647a

1 file changed

Lines changed: 110 additions & 85 deletions

File tree

src/spec-configuration/httpOCIRegistry.ts

Lines changed: 110 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -135,26 +135,23 @@ export async function requestEnsureAuthenticated(params: CommonParams, httpOptio
135135
// Attempts to get the Basic auth credentials for the provided registry.
136136
// This credential is used to offer the registry in exchange for a Bearer token.
137137
// These may be:
138-
// - Crafted from the GITHUB_TOKEN environment variables
139138
// - parsed out of a special DEVCONTAINERS_OCI_AUTH environment variable
140139
// - Read from a docker config file
140+
// - Read from a docker credential helper (https://docs.docker.com/engine/reference/commandline/login/#credentials-store)
141+
// - Crafted from the GITHUB_TOKEN environment variable
141142
// Returns:
142143
// - undefined: No credential was found.
143144
// - object: A credential was found.
144-
// - based64EncodedCredential: The credential, base64 encoded.
145+
// - based64EncodedCredential: The base64 encoded credential, if any.
145146
// - refreshToken: The refresh token, if any.
146147
async function getCredential(params: CommonParams, ociRef: OCIRef | OCICollectionRef): Promise<{ base64EncodedCredential: string | undefined; refreshToken: string | undefined } | undefined> {
147148
const { output, env } = params;
148149
const { registry } = ociRef;
149150

150-
if (!!env['GITHUB_TOKEN'] && registry === 'ghcr.io') {
151-
output.write('[httpOci] Using environment GITHUB_TOKEN for auth', LogLevel.Trace);
152-
const userToken = `USERNAME:${env['GITHUB_TOKEN']}`;
153-
return {
154-
base64EncodedCredential: Buffer.from(userToken).toString('base64'),
155-
refreshToken: undefined,
156-
};
157-
} else if (!!env['DEVCONTAINERS_OCI_AUTH']) {
151+
const githubToken = env['GITHUB_TOKEN'];
152+
const githubHost = env['GITHUB_HOST'];
153+
154+
if (!!env['DEVCONTAINERS_OCI_AUTH']) {
158155
// eg: DEVCONTAINERS_OCI_AUTH=service1|user1|token1,service2|user2|token2
159156
const authContexts = env['DEVCONTAINERS_OCI_AUTH'].split(',');
160157
const authContext = authContexts.find(a => a.split('|')[0] === registry);
@@ -168,76 +165,21 @@ async function getCredential(params: CommonParams, ociRef: OCIRef | OCICollectio
168165
refreshToken: undefined,
169166
};
170167
}
171-
} else {
172-
let configContainsAuth = false;
173-
try {
174-
const homeDir = os.homedir();
175-
if (homeDir) {
176-
const dockerConfigPath = path.join(homeDir, '.docker', 'config.json');
177-
if (await isLocalFile(dockerConfigPath)) {
178-
const dockerConfig: DockerConfigFile = jsonc.parse((await readLocalFile(dockerConfigPath)).toString());
179-
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-
}
195-
if (dockerConfig.auths && dockerConfig.auths[registry]) {
196-
output.write(`[httpOci] Found auths entry in '${dockerConfigPath}' for registry '${registry}'`, LogLevel.Trace);
197-
const auth = dockerConfig.auths[registry].auth;
198-
const identityToken = dockerConfig.auths[registry].identitytoken; // Refresh token, seen when running: 'az acr login -n <registry>'
199-
200-
if (identityToken) {
201-
return {
202-
refreshToken: identityToken,
203-
base64EncodedCredential: undefined,
204-
};
205-
}
206-
207-
// Without the presence of an `identityToken`, assume auth is a base64-encoded 'user:token'.
208-
return {
209-
base64EncodedCredential: auth,
210-
refreshToken: undefined,
211-
};
212-
}
213-
}
214-
}
215-
} catch (err) {
216-
output.write(`[httpOci] Failed to read docker config.json: ${err}`, LogLevel.Trace);
217-
}
168+
}
218169

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-
}
170+
// Attempt to use the docker config file or available credential helpers.
171+
const credentialFromDockerConfig = await getCredentialFromDockerConfigOrCredentialHelper(params, registry);
172+
if (credentialFromDockerConfig) {
173+
return credentialFromDockerConfig;
174+
}
175+
176+
if (registry === 'ghcr.io' && githubToken && (!githubHost || githubHost === 'ghcr.io')) {
177+
output.write('[httpOci] Using environment GITHUB_TOKEN for auth', LogLevel.Trace);
178+
const userToken = `USERNAME:${env['GITHUB_TOKEN']}`;
179+
return {
180+
base64EncodedCredential: Buffer.from(userToken).toString('base64'),
181+
refreshToken: undefined,
182+
};
241183
}
242184

243185
// Represents anonymous access.
@@ -249,17 +191,100 @@ async function existsInPath(filename: string): Promise<boolean> {
249191
if (!process.env.PATH) {
250192
return false;
251193
}
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;
194+
try {
195+
const paths = process.env.PATH.split(':');
196+
for (const path of paths) {
197+
const fullPath = `${path}/${filename}`;
198+
if (await isLocalFile(fullPath)) {
199+
return true;
200+
}
257201
}
202+
} catch (err) {
203+
return false;
258204
}
259205
return false;
260206
}
261207

262-
async function getCredentialFromHelper(params: CommonParams, registry: string, credHelperName: string) : Promise<{ base64EncodedCredential: string | undefined; refreshToken: string | undefined } | undefined>{
208+
209+
async function getCredentialFromDockerConfigOrCredentialHelper(params: CommonParams, registry: string) {
210+
const { output } = params;
211+
212+
let configContainsAuth = false;
213+
try {
214+
const homeDir = os.homedir();
215+
if (homeDir) {
216+
const dockerConfigPath = path.join(homeDir, '.docker', 'config.json');
217+
if (await isLocalFile(dockerConfigPath)) {
218+
const dockerConfig: DockerConfigFile = jsonc.parse((await readLocalFile(dockerConfigPath)).toString());
219+
220+
configContainsAuth = Object.keys(dockerConfig.credHelpers || {}).length > 0 || !!dockerConfig.credsStore || Object.keys(dockerConfig.auths || {}).length > 0;
221+
if (dockerConfig.credHelpers && dockerConfig.credHelpers[registry]) {
222+
const credHelper = dockerConfig.credHelpers[registry];
223+
output.write(`[httpOci] Found credential helper '${credHelper}' in '${dockerConfigPath}' registry '${registry}'`, LogLevel.Trace);
224+
const auth = await getCredentialFromHelper(params, registry, credHelper);
225+
if (auth) {
226+
return auth;
227+
}
228+
} else if (dockerConfig.credsStore) {
229+
output.write(`[httpOci] Invoking credsStore credential helper '${dockerConfig.credsStore}'`, LogLevel.Trace);
230+
const auth = await getCredentialFromHelper(params, registry, dockerConfig.credsStore);
231+
if (auth) {
232+
return auth;
233+
}
234+
}
235+
if (dockerConfig.auths && dockerConfig.auths[registry]) {
236+
output.write(`[httpOci] Found auths entry in '${dockerConfigPath}' for registry '${registry}'`, LogLevel.Trace);
237+
const auth = dockerConfig.auths[registry].auth;
238+
const identityToken = dockerConfig.auths[registry].identitytoken; // Refresh token, seen when running: 'az acr login -n <registry>'
239+
240+
if (identityToken) {
241+
return {
242+
refreshToken: identityToken,
243+
base64EncodedCredential: undefined,
244+
};
245+
}
246+
247+
// Without the presence of an `identityToken`, assume auth is a base64-encoded 'user:token'.
248+
return {
249+
base64EncodedCredential: auth,
250+
refreshToken: undefined,
251+
};
252+
}
253+
}
254+
}
255+
} catch (err) {
256+
output.write(`[httpOci] Failed to read docker config.json: ${err}`, LogLevel.Trace);
257+
return;
258+
}
259+
260+
if (!configContainsAuth) {
261+
let defaultCredHelper = '';
262+
// Try platform-specific default credential helper
263+
if (process.platform === 'linux') {
264+
if (await existsInPath('pass')) {
265+
defaultCredHelper = 'pass';
266+
} else {
267+
defaultCredHelper = 'secret';
268+
}
269+
} else if (process.platform === 'win32') {
270+
defaultCredHelper = 'wincred';
271+
} else if (process.platform === 'darwin') {
272+
defaultCredHelper = 'osxkeychain';
273+
}
274+
if (defaultCredHelper !== '') {
275+
output.write(`[httpOci] Invoking platform default credential helper '${defaultCredHelper}'`, LogLevel.Trace);
276+
const auth = await getCredentialFromHelper(params, registry, defaultCredHelper);
277+
if (auth) {
278+
return auth;
279+
}
280+
}
281+
}
282+
283+
// No auth found.
284+
return;
285+
}
286+
287+
async function getCredentialFromHelper(params: CommonParams, registry: string, credHelperName: string): Promise<{ base64EncodedCredential: string | undefined; refreshToken: string | undefined } | undefined> {
263288
const { output } = params;
264289

265290
let helperOutput: Buffer;

0 commit comments

Comments
 (0)