Skip to content
Open
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
23 changes: 15 additions & 8 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export interface Env {
PASSWORD?: string;
READONLY_USERNAME?: string;
READONLY_PASSWORD?: string;
// Set to "true" to serve pull requests (GET/HEAD) without authentication.
// Mutating methods (push, delete) still require credentials. Note this also
// exposes read-only discovery endpoints like /v2/_catalog anonymously.
ANONYMOUS_PULL?: string;
PUSH_COMPATIBILITY_MODE?: PushCompatibilityMode;
REGISTRIES_JSON?: string; // should be in the format of RegistryConfiguration[];
REGISTRY_CLIENT: Registry;
Expand All @@ -41,15 +45,18 @@ export default {
return new AuthErrorResponse(request);
}

const authMethod = await authenticationMethodFromEnv(env);
if (!authMethod) {
return new AuthErrorResponse(request);
}
const anonymousPull = env.ANONYMOUS_PULL === "true" && (request.method === "GET" || request.method === "HEAD");
if (!anonymousPull) {
const authMethod = await authenticationMethodFromEnv(env);
if (!authMethod) {
return new AuthErrorResponse(request);
}

const credentials = await authMethod.checkCredentials(request);
if (!credentials.verified) {
console.warn(`Not Authorized. authmode=${authMethod.authmode}. verified=false`);
return new AuthErrorResponse(request);
const credentials = await authMethod.checkCredentials(request);
if (!credentials.verified) {
console.warn(`Not Authorized. authmode=${authMethod.authmode}. verified=false`);
return new AuthErrorResponse(request);
}
}

env.REGISTRY_CLIENT = new R2Registry(env);
Expand Down
60 changes: 60 additions & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2380,3 +2380,63 @@ test("docker.io", () => {
}
}
});

describe("anonymous pull", () => {
async function fetchAnonymousPull(r: Request): Promise<Response> {
const ctx = createExecutionContext();
const res = await worker.fetch(r, { ...env, ANONYMOUS_PULL: "true" } as Env, ctx);
await waitOnExecutionContext(ctx);
return res as Response;
}

test("GET /v2/ needs no credentials when ANONYMOUS_PULL is enabled", async () => {
const res = await fetchAnonymousPull(createRequest("GET", "/v2/", null));
expect(res.status).toBe(200);
});

test("GET /v2/ still requires credentials when ANONYMOUS_PULL is disabled", async () => {
const res = await fetchUnauth(createRequest("GET", "/v2/", null));
expect(res.status).toBe(401);
});

test("manifests and blobs pull anonymously, with bad credentials ignored", async () => {
const name = "anonymous-pull-test";
const manifest = await generateManifest(name);
const { sha256 } = await uploadManifest(name, manifest, "latest");

const getRes = await fetchAnonymousPull(createRequest("GET", `/v2/${name}/manifests/latest`, null));
expect(getRes.status).toBe(200);
expect(getRes.headers.get("docker-content-digest")).toEqual(sha256);

const headRes = await fetchAnonymousPull(createRequest("HEAD", `/v2/${name}/manifests/latest`, null));
expect(headRes.status).toBe(200);

const imageManifest = getImageManifestV2(manifest);
const blobRes = await fetchAnonymousPull(
createRequest("GET", `/v2/${name}/blobs/${imageManifest.layers[0].digest}`, null),
);
expect(blobRes.status).toBe(200);

// Garbage credentials on a pull must not break anonymous access.
const badCredRes = await fetchAnonymousPull(
createRequest("GET", `/v2/${name}/manifests/latest`, null, {
Authorization: usernamePasswordToAuth("nobody", "wrong"),
}),
);
expect(badCredRes.status).toBe(200);
});

test("mutating methods still require credentials when ANONYMOUS_PULL is enabled", async () => {
const name = "anonymous-pull-test-mutations";
const uploadRes = await fetchAnonymousPull(createRequest("POST", `/v2/${name}/blobs/uploads/`, null));
expect(uploadRes.status).toBe(401);

const putRes = await fetchAnonymousPull(
createRequest("PUT", `/v2/${name}/manifests/latest`, new Blob(["{}"]).stream()),
);
expect(putRes.status).toBe(401);

const deleteRes = await fetchAnonymousPull(createRequest("DELETE", `/v2/${name}/manifests/latest`, null));
expect(deleteRes.status).toBe(401);
});
});