mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 15:49:16 -05:00
* Accept Swift source repository PURLs in the denylist and retention rules Closes #613. pkg:swift/github.com/apple/swift-nio, as SBOM tools and GHSA write Swift packages, is resolved to its registry identity through the upstream's /identifiers: - Denylist entries are resolved at startup, and the proxy refuses to start when one can't be, since a skipped entry would let the version through. - Retention keys are accepted at validation and kept pending. The retention loop resolves them before each sweep, and Swift isn't swept while any is pending, so a rule that keeps a package is never skipped. The mirror's source URL helper moves to the handler as SwiftSourceURL. * Resolve Swift source denylist entries in proxy mirror too serve resolved source-form Swift denylist entries through the upstream, but proxy mirror built its denylist directly, so such an entry never matched a mirror download. Both now use server.ConfigureDenylist, and neither runs when an entry can't be resolved. * Apply Swift source policies to every listed identity A Swift registry may list several identities for one source repository, but a denylist entry or retention rule naming the repository only covered the first. Denying pkg:swift/github.com/acme/widget@1.0.0 let acme.widget through when the registry listed other.widget first, and a never-evict rule cleared the cached apple.swift-nio archive when other.swift-nio came first. Both now cover every identity the registry lists. A retention rule that conflicts with another rule for any of them stays pending, with none applied. Mirroring a source PURL still downloads the first identity.
169 lines
5.8 KiB
Go
169 lines
5.8 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/git-pkgs/cooldown"
|
|
"github.com/git-pkgs/proxy/internal/retention"
|
|
"github.com/git-pkgs/purl"
|
|
)
|
|
|
|
// parseDayDuration parses a duration that may use a "d" suffix for days, as
|
|
// cooldown values do. Day counts that are not finite or exceed
|
|
// maxDurationDays are rejected, since converting them to a time.Duration
|
|
// would overflow.
|
|
func parseDayDuration(s string) (time.Duration, error) {
|
|
if num, ok := strings.CutSuffix(strings.TrimSpace(s), "d"); ok {
|
|
days, err := strconv.ParseFloat(num, 64)
|
|
if err == nil && (math.IsNaN(days) || math.IsInf(days, 0) || math.Abs(days) > maxDurationDays) {
|
|
return 0, fmt.Errorf("invalid duration %q: day count out of range", s)
|
|
}
|
|
}
|
|
return cooldown.ParseDuration(s)
|
|
}
|
|
|
|
// parseRetentionDuration parses one retention value. field names the
|
|
// setting in error messages.
|
|
func parseRetentionDuration(field, value string) (time.Duration, error) {
|
|
d, err := parseDayDuration(value)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("invalid %s: %w", field, err)
|
|
}
|
|
if d < 0 {
|
|
return 0, fmt.Errorf("invalid %s %q: must not be negative", field, value)
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
// RetentionRules validates storage.retention against the ecosystems
|
|
// registered with reg and resolves it. An ecosystem or package that reg
|
|
// does not support is an error, so a rule is never silently ignored.
|
|
func (c *Config) RetentionRules(reg *retention.Registry) (retention.Rules, error) {
|
|
r := c.Storage.Retention
|
|
rules := retention.Rules{
|
|
Ecosystems: map[string]time.Duration{},
|
|
Packages: map[string]time.Duration{},
|
|
Sources: map[string]map[string]time.Duration{},
|
|
}
|
|
|
|
var err error
|
|
if rules.Default, err = parseRetentionDuration("storage.retention.default", r.Default); err != nil {
|
|
return retention.Rules{}, err
|
|
}
|
|
|
|
if r.SweepInterval != "" {
|
|
d, err := parseDayDuration(r.SweepInterval)
|
|
if err != nil {
|
|
return retention.Rules{}, fmt.Errorf("invalid storage.retention.sweep_interval: %w", err)
|
|
}
|
|
if d < minRetentionSweepInterval {
|
|
return retention.Rules{}, fmt.Errorf("invalid storage.retention.sweep_interval %q: must be at least %s",
|
|
r.SweepInterval, minRetentionSweepInterval)
|
|
}
|
|
}
|
|
|
|
for _, key := range sortedKeys(r.Ecosystems) {
|
|
field := "storage.retention.ecosystems." + key
|
|
if !retention.IsKnown(key) {
|
|
return retention.Rules{}, fmt.Errorf("invalid %s: unknown ecosystem %q (valid: %s)",
|
|
field, key, strings.Join(retention.KnownKeys, ", "))
|
|
}
|
|
if _, ok := reg.Lookup(key); !ok {
|
|
return retention.Rules{}, fmt.Errorf("invalid %s: retention is not supported for %q in this version", field, key)
|
|
}
|
|
d, err := parseRetentionDuration(field, r.Ecosystems[key])
|
|
if err != nil {
|
|
return retention.Rules{}, err
|
|
}
|
|
rules.Ecosystems[key] = d
|
|
}
|
|
|
|
for _, key := range sortedKeys(r.Packages) {
|
|
field := fmt.Sprintf("storage.retention.packages[%q]", key)
|
|
canonical, sourceOf, err := retentionPackageKey(reg, key)
|
|
if err != nil {
|
|
return retention.Rules{}, fmt.Errorf("invalid %s: %w", field, err)
|
|
}
|
|
d, err := parseRetentionDuration(field, r.Packages[key])
|
|
if err != nil {
|
|
return retention.Rules{}, err
|
|
}
|
|
if sourceOf != "" {
|
|
if rules.Sources[sourceOf] == nil {
|
|
rules.Sources[sourceOf] = map[string]time.Duration{}
|
|
}
|
|
rules.Sources[sourceOf][key] = d
|
|
continue
|
|
}
|
|
if prev, ok := rules.Packages[canonical]; ok && prev != d {
|
|
return retention.Rules{}, fmt.Errorf("invalid %s: conflicts with another key for package %s", field, canonical)
|
|
}
|
|
rules.Packages[canonical] = d
|
|
}
|
|
|
|
return rules, nil
|
|
}
|
|
|
|
// retentionPackageKey returns the stored package PURL a configured package
|
|
// key stands for, or, for a key that has to be resolved through the upstream
|
|
// first, the ecosystem key it belongs to.
|
|
func retentionPackageKey(reg *retention.Registry, key string) (canonical, sourceOf string, _ error) {
|
|
// A julia PURL needs a uuid qualifier to parse at all, and the julia
|
|
// handler stores none, so no key could ever match one of its packages.
|
|
if strings.HasPrefix(strings.ToLower(key), "pkg:julia/") {
|
|
return "", "", fmt.Errorf("package rules are not supported for julia")
|
|
}
|
|
p, err := purl.Parse(key)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("not a valid package PURL: %w", err)
|
|
}
|
|
if p.Version != "" {
|
|
return "", "", fmt.Errorf("package PURL must not carry a version")
|
|
}
|
|
// A rule names a whole package; qualifiers and subpaths would be
|
|
// dropped on the way to the stored PURL, so refuse them rather than
|
|
// let "?type=pom" quietly cover every file of the package.
|
|
if len(p.Qualifiers) > 0 || p.Subpath != "" {
|
|
return "", "", fmt.Errorf("package PURL must not carry qualifiers or a subpath")
|
|
}
|
|
if canonical, ok := reg.Canonical(p); ok {
|
|
return canonical, "", nil
|
|
}
|
|
if ecosystem, ok := reg.Source(p); ok {
|
|
return "", ecosystem, nil
|
|
}
|
|
ecosystem, registered := reg.KeyForDBEcosystem(purl.PURLTypeToEcosystem(p.Type))
|
|
if !registered {
|
|
return "", "", fmt.Errorf("retention is not supported for %s packages in this version", p.Type)
|
|
}
|
|
if spec, _ := reg.Lookup(ecosystem); spec.CanonicalPackage == nil {
|
|
return "", "", fmt.Errorf("package rules are not supported for %s; use storage.retention.ecosystems.%s", ecosystem, ecosystem)
|
|
}
|
|
return "", "", fmt.Errorf("does not match how %s packages are stored; see the package rule forms in docs/configuration.md", ecosystem)
|
|
}
|
|
|
|
func sortedKeys(m map[string]string) []string {
|
|
keys := make([]string, 0, len(m))
|
|
for k := range m {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
|
|
// ParseRetentionSweepInterval returns how often the retention sweep runs.
|
|
func (c *Config) ParseRetentionSweepInterval() time.Duration {
|
|
if c.Storage.Retention.SweepInterval == "" {
|
|
return defaultRetentionSweepInterval
|
|
}
|
|
d, err := parseDayDuration(c.Storage.Retention.SweepInterval)
|
|
if err != nil || d < minRetentionSweepInterval {
|
|
return defaultRetentionSweepInterval
|
|
}
|
|
return d
|
|
}
|