Skip to content

Commit cfef0db

Browse files
authored
Do not cache auth probe denials (#31)
1 parent ee54ecb commit cfef0db

3 files changed

Lines changed: 38 additions & 64 deletions

File tree

docs/authorization.md

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -158,16 +158,13 @@ repository permissions change rarely or when the mapping is used for content
158158
whose access is primarily stable. Shorter TTLs keep permission changes visible
159159
sooner at the cost of more probes to the upstream Drupal URL.
160160

161-
Triplet caches successful probes, 401, 403, and 404 responses for the
162-
configured TTL. Other upstream errors are not cached.
163-
164-
Negative auth-probe caching is conservative. For 401, 403, and 404 probe
165-
responses, Triplet checks the upstream `Last-Modified` header before caching the
166-
denial. If `Last-Modified` parses and is newer than
167-
5 minutes ago, the denial is not cached. This avoids holding a stale denial
168-
while repository access rules or file publication are still settling. If
169-
`Last-Modified` is absent, unparseable, or older than that minimum age window,
170-
the denial can be cached for the configured auth TTL.
161+
Triplet caches successful probes for the configured TTL. It does not cache
162+
401/403 authorization denials, because those commonly happen while Drupal is
163+
still publishing or recalculating access. Other upstream errors are not cached.
164+
165+
It also does not cache 404 not-found responses. A 404 can happen while Drupal
166+
or Fedora is still publishing a just-created file, so Triplet rechecks upstream
167+
on the next request instead of holding a stale miss.
171168

172169
The image cache invalidation route also clears matching auth-probe entries when
173170
the source backend supports it.

internal/storage/local_url.go

Lines changed: 15 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,12 @@ type LocalURLFallback struct {
4444

4545
// LocalURLMapping maps a URL identifier prefix to a local file source.
4646
type LocalURLMapping struct {
47-
Prefix string
48-
File *FileOpener
49-
OCFL bool
50-
AuthProbe bool
51-
AuthCacheTTL time.Duration
52-
AuthErrorCacheMinAge time.Duration
53-
AuthCacheMaxEntries int
47+
Prefix string
48+
File *FileOpener
49+
OCFL bool
50+
AuthProbe bool
51+
AuthCacheTTL time.Duration
52+
AuthCacheMaxEntries int
5453
}
5554

5655
type authCacheEntry struct {
@@ -65,10 +64,9 @@ var errAuthProbeHeadUnsupported = errors.New("auth probe head unsupported")
6564
type authCacheTier string
6665

6766
const (
68-
authCacheTierAnonymous authCacheTier = "anonymous"
69-
authCacheTierAuthenticated authCacheTier = "authenticated"
70-
defaultAuthErrorCacheMinAge = 5 * time.Minute
71-
defaultAuthCacheMaxEntries = 4096
67+
authCacheTierAnonymous authCacheTier = "anonymous"
68+
authCacheTierAuthenticated authCacheTier = "authenticated"
69+
defaultAuthCacheMaxEntries = 4096
7270
)
7371

7472
// Open implements Opener.
@@ -315,7 +313,7 @@ func (l *LocalURLFallback) authorize(ctx context.Context, identifier string, map
315313
}
316314
return l.authorizeAuthenticated(ctx, identifier, mapping, headers)
317315
}
318-
anonErr := l.probeCached(ctx, anonKey, identifier, nil, mapping.anonymousAuthTTL(), mapping.authErrorCacheMinAge(), mapping.authCacheMaxEntries())
316+
anonErr := l.probeCached(ctx, anonKey, identifier, nil, mapping.anonymousAuthTTL(), mapping.authCacheMaxEntries())
319317
if anonErr == nil || !hasAuthHeaders(headers) {
320318
return anonErr
321319
}
@@ -330,10 +328,10 @@ func (l *LocalURLFallback) authorizeAuthenticated(ctx context.Context, identifie
330328
if err, ok := l.cachedAuth(key); ok {
331329
return err
332330
}
333-
return l.probeCached(ctx, key, identifier, headers, mapping.authenticatedAuthTTL(), mapping.authErrorCacheMinAge(), mapping.authCacheMaxEntries())
331+
return l.probeCached(ctx, key, identifier, headers, mapping.authenticatedAuthTTL(), mapping.authCacheMaxEntries())
334332
}
335333

336-
func (l *LocalURLFallback) probeCached(ctx context.Context, key, identifier string, headers http.Header, ttl, errorMinAge time.Duration, maxEntries int) error {
334+
func (l *LocalURLFallback) probeCached(ctx context.Context, key, identifier string, headers http.Header, ttl time.Duration, maxEntries int) error {
337335
if wait, ok := l.beginAuthProbe(key); ok {
338336
l.debug(ctx, "local url auth probe waiting on in-flight probe", identifier, "cache_key", redact.Hash(key))
339337
select {
@@ -344,8 +342,8 @@ func (l *LocalURLFallback) probeCached(ctx context.Context, key, identifier stri
344342
return ctx.Err()
345343
}
346344
}
347-
respHeader, err := l.probe(ctx, identifier, headers)
348-
cacheable := cacheableAuthProbeResult(err) && cacheableAuthProbeResponse(err, respHeader, errorMinAge, time.Now())
345+
_, err := l.probe(ctx, identifier, headers)
346+
cacheable := cacheableAuthProbeResult(err)
349347
l.debug(ctx, "local url auth probe completed", identifier, "cache_key", redact.Hash(key), "cacheable", cacheable, "ttl", ttl.String(), "err", err)
350348
if ttl > 0 && cacheable {
351349
l.storeAuth(key, authCacheScope(key), identifier, err, ttl, maxEntries)
@@ -388,18 +386,7 @@ func hasAuthHeaders(headers http.Header) bool {
388386
}
389387

390388
func cacheableAuthProbeResult(err error) bool {
391-
return err == nil || errors.Is(err, ErrForbidden) || errors.Is(err, ErrNotFound)
392-
}
393-
394-
func cacheableAuthProbeResponse(err error, header http.Header, errorMinAge time.Duration, now time.Time) bool {
395-
if err == nil || (!errors.Is(err, ErrForbidden) && !errors.Is(err, ErrNotFound)) || errorMinAge <= 0 {
396-
return true
397-
}
398-
modified, parseErr := http.ParseTime(header.Get("Last-Modified"))
399-
if parseErr != nil {
400-
return true
401-
}
402-
return !modified.After(now.Add(-errorMinAge))
389+
return err == nil
403390
}
404391

405392
func (m LocalURLMapping) anonymousAuthTTL() time.Duration {
@@ -410,13 +397,6 @@ func (m LocalURLMapping) authenticatedAuthTTL() time.Duration {
410397
return m.AuthCacheTTL
411398
}
412399

413-
func (m LocalURLMapping) authErrorCacheMinAge() time.Duration {
414-
if m.AuthErrorCacheMinAge > 0 {
415-
return m.AuthErrorCacheMinAge
416-
}
417-
return defaultAuthErrorCacheMinAge
418-
}
419-
420400
func (m LocalURLMapping) authCacheMaxEntries() int {
421401
if m.AuthCacheMaxEntries > 0 {
422402
return m.AuthCacheMaxEntries

internal/storage/local_url_test.go

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,7 @@ func TestLocalURLFallbackAuthenticatedAuthProbeTTL(t *testing.T) {
574574
time.Sleep(ttl - time.Nanosecond)
575575
synctest.Wait()
576576
openAndClose(t, op, ctx, identifier)
577-
if got := anonProbes.Load(); got != 1 {
577+
if got := anonProbes.Load(); got != 2 {
578578
t.Fatalf("anonymous probes before auth ttl = %d", got)
579579
}
580580
if got := authProbes.Load(); got != 1 {
@@ -584,7 +584,7 @@ func TestLocalURLFallbackAuthenticatedAuthProbeTTL(t *testing.T) {
584584
time.Sleep(2 * time.Nanosecond)
585585
synctest.Wait()
586586
openAndClose(t, op, ctx, identifier)
587-
if got := anonProbes.Load(); got != 2 {
587+
if got := anonProbes.Load(); got != 3 {
588588
t.Fatalf("anonymous probes after auth ttl = %d", got)
589589
}
590590
if got := authProbes.Load(); got != 2 {
@@ -636,7 +636,7 @@ func TestLocalURLFallbackAuthProbeAnonymousSucceedsAndCaches(t *testing.T) {
636636
}
637637
}
638638

639-
func TestLocalURLFallbackAuthProbeDoesNotCacheRecentError(t *testing.T) {
639+
func TestLocalURLFallbackAuthProbeDoesNotCacheForbidden(t *testing.T) {
640640
root := t.TempDir()
641641
if err := os.WriteFile(filepath.Join(root, "private.jp2"), []byte("private"), 0o600); err != nil {
642642
t.Fatal(err)
@@ -654,11 +654,10 @@ func TestLocalURLFallbackAuthProbeDoesNotCacheRecentError(t *testing.T) {
654654
}
655655
op := &LocalURLFallback{
656656
Mappings: []LocalURLMapping{{
657-
Prefix: srv.URL + "/system/files",
658-
File: fileOp,
659-
AuthProbe: true,
660-
AuthCacheTTL: time.Minute,
661-
AuthErrorCacheMinAge: time.Hour,
657+
Prefix: srv.URL + "/system/files",
658+
File: fileOp,
659+
AuthProbe: true,
660+
AuthCacheTTL: time.Minute,
662661
}},
663662
Fallback: errOpener{},
664663
AuthFallback: testAuthHTTP(t, srv),
@@ -675,16 +674,15 @@ func TestLocalURLFallbackAuthProbeDoesNotCacheRecentError(t *testing.T) {
675674
}
676675
}
677676

678-
func TestLocalURLFallbackAuthProbeCachesOldError(t *testing.T) {
677+
func TestLocalURLFallbackAuthProbeDoesNotCacheNotFound(t *testing.T) {
679678
root := t.TempDir()
680679
if err := os.WriteFile(filepath.Join(root, "private.jp2"), []byte("private"), 0o600); err != nil {
681680
t.Fatal(err)
682681
}
683682
var probes atomic.Int32
684683
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
685684
probes.Add(1)
686-
w.Header().Set("Last-Modified", time.Now().Add(-2*time.Hour).UTC().Format(http.TimeFormat))
687-
w.WriteHeader(http.StatusForbidden)
685+
w.WriteHeader(http.StatusNotFound)
688686
}))
689687
defer srv.Close()
690688
fileOp, err := NewFileOpener(root)
@@ -693,23 +691,22 @@ func TestLocalURLFallbackAuthProbeCachesOldError(t *testing.T) {
693691
}
694692
op := &LocalURLFallback{
695693
Mappings: []LocalURLMapping{{
696-
Prefix: srv.URL + "/system/files",
697-
File: fileOp,
698-
AuthProbe: true,
699-
AuthCacheTTL: time.Minute,
700-
AuthErrorCacheMinAge: time.Hour,
694+
Prefix: srv.URL + "/system/files",
695+
File: fileOp,
696+
AuthProbe: true,
697+
AuthCacheTTL: time.Minute,
701698
}},
702699
Fallback: errOpener{},
703700
AuthFallback: testAuthHTTP(t, srv),
704701
}
705702

706703
for i := 0; i < 2; i++ {
707704
_, _, err := op.Open(context.Background(), srv.URL+"/system/files/private.jp2")
708-
if !errors.Is(err, ErrForbidden) {
705+
if !errors.Is(err, ErrNotFound) {
709706
t.Fatalf("err = %v", err)
710707
}
711708
}
712-
if got := probes.Load(); got != 1 {
709+
if got := probes.Load(); got != 2 {
713710
t.Fatalf("probes = %d", got)
714711
}
715712
}
@@ -753,7 +750,7 @@ func TestLocalURLFallbackAuthProbeFallsBackToCredentialedCache(t *testing.T) {
753750
}
754751
_ = rc.Close()
755752
}
756-
if got := probes.Load(); got != 2 {
753+
if got := probes.Load(); got != 3 {
757754
t.Fatalf("probes = %d", got)
758755
}
759756
}

0 commit comments

Comments
 (0)