Skip to content

Commit 5c5e0c4

Browse files
chrmartiCopilot
andcommitted
Honor OCI auth options in feature tests
Propagate OCI authentication hardening through feature test launches and preserve credentials across trusted Docker Hub registry aliases. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6360fbe commit 5c5e0c4

4 files changed

Lines changed: 24 additions & 12 deletions

File tree

‎src/spec-configuration/httpOCIRegistry.ts‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ export async function requestEnsureAuthenticated(params: CommonParams, httpOptio
176176
}
177177
const initialAttemptRes = await requestResolveHeaders(httpOptions, output);
178178
const registryUrl = new URL(initialAttemptRes.responseUrl);
179-
const challengeFromRequestedRegistry = requestedRegistryUrl.origin.toLowerCase() === registryUrl.origin.toLowerCase();
179+
const challengeFromOCIRegistry = isOCIRegistryOrigin(registryUrl, ociRef);
180180

181181
// For anything except a 401 (invalid/no token) or 403 (insufficient scope)
182182
// response simply return the original response to the caller.
@@ -186,7 +186,7 @@ export async function requestEnsureAuthenticated(params: CommonParams, httpOptio
186186
}
187187

188188
// -- 'responseAttempt' status code was 401 or 403 at this point.
189-
if (!requestCanUseRegistryCredentials || !challengeFromRequestedRegistry) {
189+
if (!requestCanUseRegistryCredentials || !challengeFromOCIRegistry) {
190190
recordOCIAuthDiagnostic(params, 'registryRedirectWouldPreventCredentialForwarding', `Request to '${requestedRegistryUrl.host}' with authentication challenge from '${registryUrl.host}' would prevent forwarding registry '${ociRef.registry}' credentials with OCI auth hardening.`);
191191
}
192192

@@ -204,8 +204,8 @@ export async function requestEnsureAuthenticated(params: CommonParams, httpOptio
204204

205205
output.write(`[httpOci] Attempting to authenticate via 'Basic' auth.`, LogLevel.Trace);
206206

207-
if (params.ociAuthHardening && (!requestCanUseRegistryCredentials || !challengeFromRequestedRegistry)) {
208-
output.write(`[httpOci] ERR: Refusing to send registry '${ociRef.registry}' credentials to '${requestedRegistryUrl.host}'.`, LogLevel.Error);
207+
if (params.ociAuthHardening && (!requestCanUseRegistryCredentials || !challengeFromOCIRegistry)) {
208+
output.write(`[httpOci] ERR: Refusing to send registry '${ociRef.registry}' credentials to '${registryUrl.host}'.`, LogLevel.Error);
209209
return;
210210
}
211211
const credential = await getCredential(params, ociRef);
@@ -258,7 +258,7 @@ export async function requestEnsureAuthenticated(params: CommonParams, httpOptio
258258
scope: scopeGroup ? scopeGroup[1] : '',
259259
};
260260

261-
const challengeCanUseRequestedRegistryCredentials = !params.ociAuthHardening || (requestCanUseRegistryCredentials && challengeFromRequestedRegistry);
261+
const challengeCanUseRequestedRegistryCredentials = !params.ociAuthHardening || (requestCanUseRegistryCredentials && challengeFromOCIRegistry);
262262
const bearerToken = await fetchRegistryBearerToken(params, ociRef, challengeCanUseRequestedRegistryCredentials, wwwAuthenticateData);
263263
if (!bearerToken) {
264264
output.write(`[httpOci] ERR: Failed to fetch Bearer token from registry.`, LogLevel.Error);
@@ -278,7 +278,7 @@ export async function requestEnsureAuthenticated(params: CommonParams, httpOptio
278278
output.write(`[httpOci] ${reattemptRes.statusCode} on reattempt after auth: ${httpOptions.url}`, LogLevel.Trace);
279279

280280
// Cache the auth header if the request did not result in an unauthorized response.
281-
if (reattemptRes.statusCode !== 401 && (!params.ociAuthHardening || (requestCanUseRegistryCredentials && challengeFromRequestedRegistry))) {
281+
if (reattemptRes.statusCode !== 401 && (!params.ociAuthHardening || (requestCanUseRegistryCredentials && challengeFromOCIRegistry))) {
282282
params.cachedAuthHeader[ociRef.registry] = httpOptions.headers.authorization;
283283
}
284284

‎src/spec-node/featuresCLI/test.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { CLIHost, getCLIHost } from '../../spec-common/cliHost';
33
import { loadNativeModule } from '../../spec-common/commonUtils';
44
import { LogLevel, mapLogLevel } from '../../spec-utils/log';
55
import { getPackageConfig, PackageConfiguration } from '../../spec-utils/product';
6-
import { UnpackArgv } from '../devContainersSpecCLI';
6+
import { OciAuthArgs, UnpackArgv } from '../devContainersSpecCLI';
77
import { doFeaturesTestCommand } from './testCommandImpl';
88
import { runAsyncHandler } from '../utils';
99

@@ -47,7 +47,7 @@ export function featuresTestOptions(y: Argv) {
4747
});
4848
}
4949

50-
export type FeaturesTestArgs = UnpackArgv<ReturnType<typeof featuresTestOptions>>;
50+
export type FeaturesTestArgs = UnpackArgv<ReturnType<typeof featuresTestOptions>> & OciAuthArgs;
5151
export interface FeaturesTestCommandInput {
5252
cliHost: CLIHost;
5353
pkg: PackageConfiguration;
@@ -64,6 +64,8 @@ export interface FeaturesTestCommandInput {
6464
logLevel: LogLevel;
6565
preserveTestContainers: boolean;
6666
quiet: boolean;
67+
allowedCrossOriginAuthHosts: string[];
68+
ociAuthHardening: boolean;
6769
disposables: (() => Promise<unknown> | undefined)[];
6870
}
6971

@@ -86,6 +88,8 @@ async function featuresTest({
8688
'log-level': inputLogLevel,
8789
'preserve-test-containers': preserveTestContainers,
8890
quiet,
91+
'allow-cross-origin-auth-host': allowedCrossOriginAuthHosts = [],
92+
'oci-auth-hardening': ociAuthHardening = false,
8993
}: FeaturesTestArgs) {
9094
const disposables: (() => Promise<unknown> | undefined)[] = [];
9195
const dispose = async () => {
@@ -117,6 +121,8 @@ async function featuresTest({
117121
permitRandomization,
118122
remoteUser,
119123
preserveTestContainers,
124+
allowedCrossOriginAuthHosts,
125+
ociAuthHardening,
120126
disposables
121127
};
122128

‎src/spec-node/featuresCLI/testCommandImpl.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,8 @@ async function launchProject(params: DockerResolverParameters, workspaceFolder:
559559
skipFeatureAutoMapping: common.skipFeatureAutoMapping,
560560
skipPersistingCustomizationsFromFeatures: common.skipPersistingCustomizationsFromFeatures,
561561
omitConfigRemotEnvFromMetadata: common.omitConfigRemotEnvFromMetadata,
562+
allowedCrossOriginAuthHosts: common.allowedCrossOriginAuthHosts,
563+
ociAuthHardening: common.ociAuthHardening,
562564
log: text => quiet ? null : process.stderr.write(text),
563565
dotfiles: {}
564566
};
@@ -625,7 +627,7 @@ async function exec(cmd: string, args: string[], workspaceFolder: string, inject
625627
}
626628

627629
async function generateDockerParams(workspaceFolder: string, args: FeaturesTestCommandInput): Promise<DockerResolverParameters> {
628-
const { logLevel, quiet, disposables } = args;
630+
const { logLevel, quiet, allowedCrossOriginAuthHosts, ociAuthHardening, disposables } = args;
629631
return await createDockerParams({
630632
workspaceFolder,
631633
additionalLabels: [],
@@ -662,6 +664,8 @@ async function generateDockerParams(workspaceFolder: string, args: FeaturesTestC
662664
skipFeatureAutoMapping: false,
663665
skipPostAttach: false,
664666
skipPersistingCustomizationsFromFeatures: false,
667+
allowedCrossOriginAuthHosts,
668+
ociAuthHardening,
665669
dotfiles: {}
666670
}, disposables);
667671
}

‎src/test/httpOCIRegistry.test.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,13 @@ describe('OCI registry authentication', () => {
135135
assert.isFalse(isOCIRegistryOrigin(new URL('http://registry.example/v2/'), ociRef));
136136
});
137137

138-
it('accepts Docker Hub distribution aliases', () => {
139-
assert.isTrue(isOCIRegistryOrigin(new URL('https://registry-1.docker.io/v2/'), {
138+
it('accepts both origins of a Docker Hub distribution alias redirect', () => {
139+
const dockerHubRef = {
140140
...ociRef,
141141
registry: 'docker.io',
142-
}));
142+
};
143+
assert.isTrue(isOCIRegistryOrigin(new URL('https://docker.io/v2/'), dockerHubRef));
144+
assert.isTrue(isOCIRegistryOrigin(new URL('https://registry-1.docker.io/v2/'), dockerHubRef));
143145
});
144146
});
145147

0 commit comments

Comments
 (0)