Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions __tests__/context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,87 @@ test('getAuthList uses the default Docker Hub registry when computing scoped con
});
});

test('getAuthList supports @ scopes appended to the registry config dir', async () => {
process.env['INPUT_USERNAME'] = 'dbowie';
process.env['INPUT_PASSWORD'] = 'groundcontrol';
process.env['INPUT_SCOPE'] = '@push';
process.env['INPUT_LOGOUT'] = 'false';
const [auth] = getAuthList(getInputs());
expect(auth).toMatchObject({
configDir: path.join(Buildx.configDir, 'config', 'registry-1.docker.io') + '@push'
});
});

test('getAuthList supports repository scopes with appended actions', async () => {
process.env['INPUT_USERNAME'] = 'dbowie';
process.env['INPUT_PASSWORD'] = 'groundcontrol';
process.env['INPUT_SCOPE'] = 'docker/buildx-bin@push';
process.env['INPUT_LOGOUT'] = 'false';
const [auth] = getAuthList(getInputs());
expect(auth).toMatchObject({
configDir: path.join(Buildx.configDir, 'config', 'registry-1.docker.io', 'docker', 'buildx-bin@push')
});
});

test('getAuthList supports comma-separated scope actions', async () => {
process.env['INPUT_USERNAME'] = 'dbowie';
process.env['INPUT_PASSWORD'] = 'groundcontrol';
process.env['INPUT_SCOPE'] = 'docker/buildx-bin@pull,push';
process.env['INPUT_LOGOUT'] = 'false';
const [auth] = getAuthList(getInputs());
expect(auth).toMatchObject({
configDir: path.join(Buildx.configDir, 'config', 'registry-1.docker.io', 'docker', 'buildx-bin@pull,push')
});
});

// prettier-ignore
test.each([
'../../../../work/leaked',
'..',
'foo/../../../../etc',
'@../../../leaked',
'foo/bar@../../../leaked',
'@push@pull',
'foo/bar@push@pull',
'@push,',
'@,push',
'@pull,,push',
'@Push',
path.join(path.parse(Buildx.configDir).root, 'work', 'leaked')
])('getAuthList rejects unsafe or unsupported scope path: %s', async scope => {
expect(() => {
getAuthList({
registry: '',
username: 'dbowie',
password: 'groundcontrol',
scope,
ecr: '',
logout: false,
registryAuth: ''
});
}).toThrow(/Invalid scope/);
});

// prettier-ignore
test.each([
'../../../../work/leaked',
'..',
'foo/../../../../etc',
path.join(path.parse(Buildx.configDir).root, 'work', 'leaked')
])('getAuthList rejects unsafe registry path: %s', async registry => {
expect(() => {
getAuthList({
registry,
username: 'dbowie',
password: 'groundcontrol',
scope: '@push',
ecr: '',
logout: false,
registryAuth: ''
});
}).toThrow(/Invalid registry/);
});

test('getAuthList skips secret masking when registry-auth password is absent', async () => {
const stdoutWriteSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
const [auth] = getAuthList({
Expand Down
216 changes: 108 additions & 108 deletions dist/index.cjs

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions dist/index.cjs.map

Large diffs are not rendered by default.

32 changes: 26 additions & 6 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,33 @@ export function scopeToConfigDir(registry: string, scope?: string): string {
if (scopeDisabled() || !scope || scope === '') {
return '';
}
let configDir = path.join(Buildx.configDir, 'config', registry === 'docker.io' ? 'registry-1.docker.io' : registry);
if (scope.startsWith('@')) {
configDir += scope;
} else {
configDir = path.join(configDir, scope);
const configRoot = path.resolve(Buildx.configDir, 'config');
const registryDir = path.resolve(configRoot, registry === 'docker.io' ? 'registry-1.docker.io' : registry);
if (!isChildPath(configRoot, registryDir)) {
throw new Error(`Invalid registry '${registry}': resolved config path escapes the Buildx config directory`);
}
const scopeParts = scope.split('@');
if (scopeParts.length > 2) {
throw new Error(`Invalid scope '${scope}': scope can contain at most one @ separator`);
}
const [scopePath, scopeActions] = scopeParts;
if (scopeActions !== undefined && !/^[a-z]+(,[a-z]+)*$/.test(scopeActions)) {
throw new Error(`Invalid scope '${scope}': scope actions must be lowercase names separated by commas`);
}
const scopeSuffix = scopeActions === undefined ? '' : `@${scopeActions}`;
if (scopePath === '') {
return `${registryDir}${scopeSuffix}`;
}
return configDir;
const configDir = path.resolve(registryDir, scopePath);
if (!isChildPath(registryDir, configDir)) {
throw new Error(`Invalid scope '${scope}': resolved config path escapes the Buildx config directory`);
}
return `${configDir}${scopeSuffix}`;
}

function isChildPath(parent: string, child: string): boolean {
const relativePath = path.relative(parent, child);
return relativePath !== '' && relativePath !== '..' && !relativePath.startsWith(`..${path.sep}`) && !path.isAbsolute(relativePath);
}

function scopeDisabled(): boolean {
Expand Down