Replace MCR's custom auth, admin, and logging interceptors with the
shared mcdsl grpcserver package. This eliminates ~110 lines of
interceptor code and uses the same method-map auth pattern used by
metacrypt.
Key changes:
- server.go: delegate to mcdslgrpc.New() for TLS, logging, and auth
- interceptors.go: replaced with MethodMap definition (public, auth-required, admin-required)
- Handler files: switch from auth.ClaimsFromContext to mcdslauth.TokenInfoFromContext
- auth/client.go: add Authenticator() accessor for the underlying mcdsl authenticator
- Tests: use mock MCIAS HTTP server instead of fakeValidator interface
- Vendor: add mcdsl/grpcserver to vendor directory
ListRepositories and GetRepository are now explicitly auth-required
(not admin-required), matching the REST API. Previously they were
implicitly auth-required by not being in the bypass or admin maps.
Security: method map uses default-deny -- unmapped RPCs are rejected.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Every 500 response in the OCI package silently discarded the actual
error, making production debugging impossible. Add slog.Error before
each 500 response with the error and relevant context (repo, digest,
tag, uuid). Add slog.Info for state-mutating successes (manifest push,
blob upload complete, deletions).
Logger is injected into the OCI Handler via constructor, falling back
to slog.Default() if nil.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SQLite's last_insert_rowid() only updates on actual INSERTs, not
ON CONFLICT DO UPDATE. When pushing a second tag for an existing
manifest digest, the upsert fires the conflict branch (no new row),
so LastInsertId() returns a stale ID from a previous insert. This
caused manifest_blobs and tags to reference the wrong manifest,
producing a 500 on the PUT manifest response.
Replace LastInsertId() with a SELECT id WHERE repository_id AND
digest query within the same transaction.
Security: manifest_blobs and tag foreign keys now always reference
the correct manifest.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous default policy required both AccountTypes=["human"] and
Roles=["user"], but MCIAS validate responses don't reliably include
these fields. For a private registry, any successfully authenticated
caller should have content access. Admin-only operations (policy
management) still require the admin role.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
OCI clients (podman, docker) require an absolute URL in the
WWW-Authenticate realm. Derive it from the request Host header
so it works behind any proxy. Add service_name to rift config.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
NewRouter now accepts an optional OCI handler to mount inside the
authenticated /v2 route group, avoiding chi's Mount conflict on
an existing path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Now that mcdsl/auth.TokenInfo carries AccountType (from the updated
MCIAS validate response), the MCR auth shim passes it through to
Claims.AccountType. Policy engine rules matching on account type
now work correctly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- db.Open: delegate to mcdsl/db.Open
- db.Migrate: rewrite migrations as mcdsl/db.Migration SQL strings,
delegate to mcdsl/db.Migrate; keep SchemaVersion via mcdsl
- auth: thin shim wrapping mcdsl/auth.Authenticator, keeps Claims
type (with Subject, AccountType, Roles) for policy engine compat;
delete cache.go (handled by mcdsl/auth); add ErrForbidden
- config: embed mcdsl/config.Base for standard sections (Server with
Duration fields, Database, MCIAS, Log); keep StorageConfig and
WebConfig as MCR-specific; use mcdsl/config.Load[T] + Validator
- WriteTimeout now defaults to 30s (mcdsl default, was 0)
- All existing tests pass (auth tests rewritten for new shim API,
cache expiry test removed — caching tested in mcdsl)
- Net -464 lines
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 11 implements the admin CLI with dual REST/gRPC transport,
global flags (--server, --grpc, --token, --ca-cert, --json), and
all commands: status, repo list/delete, policy CRUD, audit tail,
gc trigger/status/reconcile, and snapshot.
Phase 12 implements the HTMX web UI with chi router, session-based
auth (HttpOnly/Secure/SameSite=Strict cookies), CSRF protection
(HMAC-SHA256 signed double-submit), and pages for dashboard,
repositories, manifest detail, policy management, and audit log.
Security: CSRF via signed double-submit cookie, session cookies
with HttpOnly/Secure/SameSite=Strict, TLS 1.3 minimum on all
connections, form body size limits via http.MaxBytesReader.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Proto definitions for 4 services (RegistryService, PolicyService,
AuditService, AdminService) with hand-written Go stubs using JSON
codec until protobuf tooling is available.
Interceptor chain: logging (method, peer IP, duration, never logs
auth metadata) → auth (bearer token via MCIAS, Health bypasses) →
admin (role check for GC, policy, delete, audit RPCs).
All RPCs share business logic with REST handlers via internal/db
and internal/gc packages. TLS 1.3 minimum on gRPC listener.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GC engine (internal/gc/): Collector.Run() implements the two-phase
algorithm — Phase 1 finds unreferenced blobs and deletes DB rows in
a single transaction, Phase 2 deletes blob files from storage.
Registry-wide mutex blocks concurrent GC runs. Collector.Reconcile()
scans filesystem for orphaned files with no DB row (crash recovery).
Wired into admin_gc.go: POST /v1/gc now launches the real collector
in a goroutine with gc_started/gc_completed audit events.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Manifest delete (DELETE /v2/<name>/manifests/<digest>): rejects tag
references with 405 UNSUPPORTED per OCI spec, cascades to tags and
manifest_blobs via ON DELETE CASCADE, returns 202 Accepted.
Blob delete (DELETE /v2/<name>/blobs/<digest>): removes manifest_blobs
associations only — blob row and file are preserved for GC to handle,
since other repos may reference the same content-addressed blob.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
internal/policy/:
Priority-based policy engine per ARCHITECTURE.md §4. Stateless
Evaluate() sorts rules by priority, collects all matches, deny-wins
over allow, default-deny if no match. Rule matching: all populated
fields ANDed, empty fields are wildcards, repository glob via
path.Match. Built-in defaults: admin wildcard (all actions), human
user content access (pull/push/delete/catalog), version check
(always accessible). Engine wrapper with sync.RWMutex-protected
cache, SetRules merges with defaults, Reload loads from RuleStore.
internal/db/:
LoadEnabledPolicyRules() parses rule_json column from policy_rules
table into []policy.Rule, filtered by enabled=1, ordered by priority.
internal/server/:
RequirePolicy middleware extracts claims from context, repo from chi
URL param, evaluates policy, returns OCI DENIED (403) on deny with
optional audit callback.
69 tests passing across all packages.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 2 — internal/storage/:
Content-addressed blob storage with atomic writes via rename.
BlobWriter stages data in uploads dir with running SHA-256 hash,
commits by verifying digest then renaming to layers/sha256/<prefix>/<hex>.
Reader provides Open, Stat, Delete, Exists with digest validation.
Phase 3 — internal/auth/ + internal/server/:
MCIAS client with Login and ValidateToken, 30s SHA-256-keyed cache
with lazy eviction and injectable clock for testing. TLS 1.3 minimum
with optional custom CA cert.
Chi router with RequireAuth middleware (Bearer token extraction,
WWW-Authenticate header, OCI error format), token endpoint (Basic
auth → bearer exchange via MCIAS), and /v2/ version check handler.
52 tests passing (14 storage + 9 auth + 9 server + 20 existing).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>