Files
at-container-registry/pkg/appview/storage/context.go
T
Evan JarrettandClaude Opus 5 a7569a7717 registry: allow anonymous pull of public images
Credential-less pulls of public images. /auth/token issues a pull-only
token with an empty subject when no Basic auth is present; the
destination hold still enforces captain.Public, and push or delete always
challenges.

  - token.IsPullOnlyScope and AuthMethodAnonymous;
    Handler.issueAnonymousToken skips the authorizer gate and the
    service-auth pre-mint, since there is no identity to reconcile and no
    AppView-to-hold service token to bind. The token is still stamped
    with the resolved registry domain, so anonymous pull works on
    secondary front doors whose access controller demands their own
    audience.
  - auth.allow_anonymous_pull (default true) turns it fully off, restoring
    the previous always-challenge behavior. Mirrored into the deploy
    template, since the default means existing deploys pick this up.
  - RegistryContext.Anonymous is plumbed from the middleware.
  - ProxyBlobStore sends no Authorization header when the service token is
    empty, and returns 401 rather than 403 for anonymous denials so Docker
    prompts for credentials, including when a stale captain cache lets the
    request through and the hold says private.
  - BearerChallenge wraps the /v2/ subtree so a 401 raised deep in the
    stack via errcode.ServeJSON still carries WWW-Authenticate.
    Distribution's own scoped challenges are left alone.

IsPullOnlyScope allowlists the pull action instead of denylisting push and
delete. Distribution's actionSet.contains treats "*" as *every* action, so
a scope of `repository:victim/img:*` names neither denied string and would
have handed an unauthenticated caller a token valid for push and delete on
someone else's repository — clearing the authgate entirely, since anonymous
tokens deliberately skip it. Writes would still have failed further down
(no PDS credential), but the gate itself was bypassable. Now every
requested action must be exactly "pull". Covered by new claims tests.

Unresolvable identities return NAME_UNKNOWN instead of a bare error that
distribution renders as 500. This path was previously unreachable without
credentials; anonymous pull opens it to the internet, and a 5xx on
arbitrary input both misreports a bad request as a server fault and sends
clients that retry 5xx into a retry loop. That loop was real: in the auth
matrix, regclient spent 83s on a single case before this fix, and the
suite now runs in 5s.

Stat preserves an authorization verdict from getPresignedURL rather than
flattening it to ErrBlobUnknown. Distribution calls Stat before ServeBlob
on GET and HEAD, so without this an anonymous pull from a private hold
answered 404 and BearerChallenge had no 401 to annotate — the 401 path
above could never actually reach a client.

The auth matrix is updated to match: anonymous pull of the seeded public
repo now succeeds, anonymous push is denied against a real identity's
namespace (rather than an unresolvable one, which was testing name
resolution rather than authorization), and a new case pins the
NAME_UNKNOWN behavior for an unknown identity.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 21:14:58 -05:00

89 lines
3.9 KiB
Go

package storage
import (
"context"
"atcr.io/pkg/appview/readme"
"atcr.io/pkg/atproto"
"atcr.io/pkg/auth"
"atcr.io/pkg/auth/oauth"
)
// PushWebhookDispatcher dispatches webhooks fired after a successful push.
// Today that covers the "push" trigger and the edge-triggered "quota" trigger
// (each user's storage usage is re-evaluated against per-webhook thresholds).
// Defined here (in storage) to avoid import cycles with the webhooks package.
type PushWebhookDispatcher interface {
DispatchForPush(ctx context.Context, event PushWebhookEvent)
DispatchForQuota(ctx context.Context, event QuotaWebhookEvent)
}
// QuotaWebhookEvent contains the data needed to evaluate and dispatch quota
// threshold webhooks for a user's storage on a hold.
type QuotaWebhookEvent struct {
UserDID string
UserHandle string
HoldDID string
HoldEndpoint string
}
// ManifestReferenceChecker checks if a manifest digest is referenced as a child
// of a manifest list (multi-arch image). Used to protect manifest list children
// from auto-removal when their parent list is still tagged.
type ManifestReferenceChecker interface {
IsManifestReferenced(did, digest string) (bool, error)
}
// PushWebhookEvent contains the data needed to dispatch a push webhook.
type PushWebhookEvent struct {
OwnerDID string
OwnerHandle string
PusherDID string
PusherHandle string
Repository string
Tag string
Digest string
MediaType string
HoldDID string
HoldEndpoint string
}
// HoldDIDLookup interface for querying and updating hold DIDs in manifests
type HoldDIDLookup interface {
GetLatestHoldDIDForRepo(did, repository string) (string, error)
UpdateManifestHoldDID(did, oldHoldDID, newHoldDID string) (int64, error)
GetDistinctManifestHoldDIDs(did string) ([]string, error)
}
// RegistryContext bundles all the context needed for registry operations
// This includes both per-request data (DID, hold) and shared services
type RegistryContext struct {
// Per-request identity and routing information
// Owner = the user whose repository is being accessed
// Puller = the authenticated user making the request (from JWT Subject)
DID string // Owner's DID - whose repo is being accessed (e.g., "did:plc:abc123")
Handle string // Owner's handle (e.g., "alice.bsky.social")
HoldDID string // Hold service DID (e.g., "did:web:hold01.atcr.io" or "did:plc:abc123")
HoldURL string // Resolved HTTP URL for the hold service
PDSEndpoint string // Owner's PDS endpoint URL
Repository string // Image repository name (e.g., "debian")
ServiceToken string // Service token for hold authentication (from puller's PDS)
ATProtoClient *atproto.Client // Authenticated ATProto client for the owner
AuthMethod string // Auth method used ("oauth" or "app_password")
PullerDID string // Puller's DID - who is making the request (from JWT Subject)
PullerPDSEndpoint string // Puller's PDS endpoint URL
HasPushScope bool // Whether the JWT token has push scope (used to filter pull stats)
Anonymous bool // Request carries no puller identity (anonymous pull); hold decides via captain.Public
// Per-request user preferences
AutoRemoveUntagged bool // Whether to auto-delete untagged manifests on tag overwrite
// Shared services (same for all requests)
Database HoldDIDLookup // Database for hold DID lookups
Authorizer auth.HoldAuthorizer // Hold access authorization
Refresher *oauth.Refresher // OAuth session manager
ReadmeFetcher *readme.Fetcher // README fetcher for repo pages
WebhookDispatcher PushWebhookDispatcher // Push webhook dispatcher (nil if not configured)
ManifestRefChecker ManifestReferenceChecker // Checks if digest is a manifest list child (nil-safe)
}