package oci import ( "io" "net/http" "strings" "git.wntrmute.dev/kyle/mcr/internal/auth" "git.wntrmute.dev/kyle/mcr/internal/db" "git.wntrmute.dev/kyle/mcr/internal/policy" "git.wntrmute.dev/kyle/mcr/internal/storage" ) // DBQuerier provides the database operations needed by OCI handlers. type DBQuerier interface { GetRepositoryByName(name string) (int64, error) GetManifestByTag(repoID int64, tag string) (*db.ManifestRow, error) GetManifestByDigest(repoID int64, digest string) (*db.ManifestRow, error) BlobExistsInRepo(repoID int64, digest string) (bool, error) ListTags(repoID int64, after string, limit int) ([]string, error) ListRepositoryNames(after string, limit int) ([]string, error) // Push operations GetOrCreateRepository(name string) (int64, error) BlobExists(digest string) (bool, error) InsertBlob(digest string, size int64) error PushManifest(p db.PushManifestParams) error // Upload operations CreateUpload(uuid string, repoID int64) error GetUpload(uuid string) (*db.UploadRow, error) UpdateUploadOffset(uuid string, offset int64) error DeleteUpload(uuid string) error // Delete operations DeleteManifest(repoID int64, digest string) error DeleteBlobFromRepo(repoID int64, digest string) error } // BlobStore provides read and write access to blob storage. type BlobStore interface { Open(digest string) (io.ReadCloser, error) Stat(digest string) (int64, error) StartUpload(uuid string) (*storage.BlobWriter, error) } // PolicyEval evaluates access control policies. type PolicyEval interface { Evaluate(input policy.PolicyInput) (policy.Effect, *policy.Rule) } // AuditFunc records audit events. Follows the same signature pattern as // db.WriteAuditEvent but without an error return — audit failures should // not block request processing. type AuditFunc func(eventType, actorID, repository, digest, ip string, details map[string]string) // Handler serves OCI Distribution Spec endpoints. type Handler struct { db DBQuerier blobs BlobStore policy PolicyEval auditFn AuditFunc uploads *uploadManager } // NewHandler creates a new OCI handler. func NewHandler(querier DBQuerier, blobs BlobStore, pol PolicyEval, auditFn AuditFunc) *Handler { return &Handler{ db: querier, blobs: blobs, policy: pol, auditFn: auditFn, uploads: newUploadManager(), } } // isDigest returns true if the reference looks like a digest (sha256:...). func isDigest(ref string) bool { return strings.HasPrefix(ref, "sha256:") } // checkPolicy evaluates the policy for the given action and repository. // Returns true if access is allowed, false if denied (and writes the OCI error). func (h *Handler) checkPolicy(w http.ResponseWriter, r *http.Request, action policy.Action, repo string) bool { claims := auth.ClaimsFromContext(r.Context()) if claims == nil { writeOCIError(w, "UNAUTHORIZED", http.StatusUnauthorized, "authentication required") return false } input := policy.PolicyInput{ Subject: claims.Subject, AccountType: claims.AccountType, Roles: claims.Roles, Action: action, Repository: repo, } effect, _ := h.policy.Evaluate(input) if effect == policy.Deny { if h.auditFn != nil { h.auditFn("policy_deny", claims.Subject, repo, "", r.RemoteAddr, map[string]string{ "action": string(action), }) } writeOCIError(w, "DENIED", http.StatusForbidden, "access denied by policy") return false } return true } // audit records an audit event if an audit function is configured. func (h *Handler) audit(r *http.Request, eventType, repo, digest string) { if h.auditFn == nil { return } claims := auth.ClaimsFromContext(r.Context()) actorID := "" if claims != nil { actorID = claims.Subject } h.auditFn(eventType, actorID, repo, digest, r.RemoteAddr, nil) }