fail closed on quarantine misconfig and first-seen errors
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful

- backfill quarantine_days<=0 to 3 with migration 0006
- reject quarantine_enabled with quarantine_days<1 on create and update
- cap upstream Last-Modified at now
- answer 503 with Retry-After when the first-seen lookup fails
This commit is contained in:
2026-10-25 13:36:00 +11:00
parent 62c009b837
commit 7cd0edf7ff
9 changed files with 136 additions and 12 deletions
+17 -7
View File
@@ -674,24 +674,32 @@ func (e *Engine) checkUpstreamOnce(ctx context.Context, remote models.Remote, pa
}
// checkQuarantine refuses an uncached immutable artifact published less than
// quarantine_days ago. The publish time is the upstream Last-Modified, else
// the first time this path was seen.
// quarantine_days ago. The publish time is the upstream Last-Modified, capped
// at now, else the first time this path was seen.
func (e *Engine) checkQuarantine(ctx context.Context, remote models.Remote, path string, upstream http.Header) error {
if !remote.QuarantineEnabled || remote.QuarantineDays <= 0 {
if !remote.QuarantineEnabled {
return nil
}
if a, err := e.db.GetArtifact(ctx, remote.Name, path); err == nil && a != nil {
return nil
}
window := time.Duration(remote.QuarantineDays) * 24 * time.Hour
days := remote.QuarantineDays
if days < 1 {
days = models.DefaultQuarantineDays
}
window := time.Duration(days) * 24 * time.Hour
now := time.Now()
published, err := http.ParseTime(upstream.Get("Last-Modified"))
if err != nil {
if published, err = e.cache.FirstSeen(ctx, remote.Name, path, window+24*time.Hour); err != nil {
slog.Warn("first-seen lookup failed, quarantining", "remote", remote.Name, "path", path, "error", err)
published = time.Now()
slog.Warn("first-seen lookup failed", "remote", remote.Name, "path", path, "error", err)
return &ProxyError{Status: http.StatusServiceUnavailable, Message: "quarantine check unavailable", RetryAfter: 30}
}
}
if until := published.Add(window); time.Now().Before(until) {
if published.After(now) {
published = now
}
if until := published.Add(window); now.Before(until) {
return &ProxyError{
Status: http.StatusForbidden,
Message: fmt.Sprintf("quarantined: published %s, available from %s", published.UTC().Format(time.RFC3339), until.UTC().Format(time.RFC3339)),
@@ -863,6 +871,8 @@ type ProxyError struct {
// Published and Available are set only on a quarantine refusal.
Published time.Time
Available time.Time
// RetryAfter, in seconds, is sent as Retry-After when set.
RetryAfter int
}
func (e *ProxyError) Error() string { return e.Message }