Skip to content

Commit f4bcd41

Browse files
authored
Merge branch 'master' into opa-v1.0.0
2 parents 90d0e68 + 8d4721f commit f4bcd41

File tree

15 files changed

+194
-132
lines changed

15 files changed

+194
-132
lines changed

.clusterfuzzlite/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM gcr.io/oss-fuzz-base/base-builder-go@sha256:bab77046ede6fae6f9ab931ada4b17a5adaf71842ac93e32300d06d5b9829891
1+
FROM gcr.io/oss-fuzz-base/base-builder-go@sha256:9bf7fad8ca02443224c7518392d80c97a62b8cb0822f03aadf9193a7e27346f0
22

33
COPY . $SRC/skipper
44
COPY ./.clusterfuzzlite/build.sh $SRC/

filters/auth/authclient.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type authClient struct {
3232

3333
type tokeninfoClient interface {
3434
getTokeninfo(token string, ctx filters.FilterContext) (map[string]any, error)
35+
Close()
3536
}
3637

3738
var _ tokeninfoClient = &authClient{}

filters/auth/grant.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,11 +175,7 @@ func (f *grantFilter) setupToken(token *oauth2.Token, tokeninfo map[string]inter
175175

176176
// By piggy-backing on the OIDC token container,
177177
// we gain downstream compatibility with the oidcClaimsQuery filter.
178-
ctx.StateBag()[oidcClaimsCacheKey] = tokenContainer{
179-
OAuth2Token: token,
180-
Subject: subject,
181-
Claims: tokeninfo,
182-
}
178+
SetOIDCClaims(ctx, tokeninfo)
183179

184180
// Set the tokeninfo also in the tokeninfoCacheKey state bag, so we
185181
// can reuse e.g. the forwardToken() filter.

filters/auth/grant_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ func newAuthProxy(t *testing.T, config *auth.OAuthConfig, routes []*eskip.Route,
210210
fr.Register(config.NewGrantCallback())
211211
fr.Register(config.NewGrantClaimsQuery())
212212
fr.Register(config.NewGrantLogout())
213+
fr.Register(auth.NewOIDCQueryClaimsFilter())
213214

214215
pc := proxytest.Config{
215216
RoutingOptions: routing.Options{
@@ -331,6 +332,7 @@ func TestGrantFlow(t *testing.T) {
331332
config := newGrantTestConfig(tokeninfo.URL, provider.URL)
332333

333334
routes := eskip.MustParse(`* -> oauthGrant()
335+
-> oidcClaimsQuery("/:sub")
334336
-> status(204)
335337
-> setResponseHeader("Backend-Request-Cookie", "${request.header.Cookie}")
336338
-> <shunt>

filters/auth/main_test.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,7 @@ func TestMain(m *testing.M) {
1818

1919
func cleanupAuthClients() {
2020
for _, c := range tokeninfoAuthClient {
21-
if ac, ok := c.(*authClient); ok {
22-
ac.Close()
23-
} else if cc, ok := c.(*tokeninfoCache); ok {
24-
cc.client.(*authClient).Close()
25-
}
21+
c.Close()
2622
}
2723

2824
for _, c := range issuerAuthClient {

filters/auth/oidc_introspection.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ func NewOIDCQueryClaimsFilter() filters.Spec {
4242
}
4343
}
4444

45+
// Sets OIDC claims in the state bag.
46+
// Intended for use with the oidcClaimsQuery filter.
47+
func SetOIDCClaims(ctx filters.FilterContext, claims map[string]interface{}) {
48+
ctx.StateBag()[oidcClaimsCacheKey] = tokenContainer{
49+
Claims: claims,
50+
}
51+
}
52+
4553
func (spec *oidcIntrospectionSpec) Name() string {
4654
switch spec.typ {
4755
case checkOIDCQueryClaims:

filters/auth/tokeninfo.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/opentracing/opentracing-go"
1313
"github.com/zalando/skipper/filters"
1414
"github.com/zalando/skipper/filters/annotate"
15+
"github.com/zalando/skipper/metrics"
1516
)
1617

1718
const (
@@ -32,9 +33,10 @@ type TokeninfoOptions struct {
3233
Timeout time.Duration
3334
MaxIdleConns int
3435
Tracer opentracing.Tracer
36+
Metrics metrics.Metrics
3537

3638
// CacheSize configures the maximum number of cached tokens.
37-
// The cache evicts least recently used items first.
39+
// The cache periodically evicts random items when number of cached tokens exceeds CacheSize.
3840
// Zero value disables tokeninfo cache.
3941
CacheSize int
4042

@@ -100,7 +102,7 @@ func (o *TokeninfoOptions) newTokeninfoClient() (tokeninfoClient, error) {
100102
}
101103

102104
if o.CacheSize > 0 {
103-
c = newTokeninfoCache(c, o.CacheSize, o.CacheTTL)
105+
c = newTokeninfoCache(c, o.Metrics, o.CacheSize, o.CacheTTL)
104106
}
105107
return c, nil
106108
}

filters/auth/tokeninfocache.go

Lines changed: 79 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,55 @@
11
package auth
22

33
import (
4-
"container/list"
4+
"maps"
55
"sync"
6+
"sync/atomic"
67
"time"
78

89
"github.com/zalando/skipper/filters"
10+
"github.com/zalando/skipper/metrics"
911
)
1012

1113
type (
1214
tokeninfoCache struct {
13-
client tokeninfoClient
14-
size int
15-
ttl time.Duration
16-
now func() time.Time
17-
18-
mu sync.Mutex
19-
cache map[string]*entry
20-
// least recently used token at the end
21-
history *list.List
15+
client tokeninfoClient
16+
metrics metrics.Metrics
17+
size int
18+
ttl time.Duration
19+
now func() time.Time
20+
21+
cache sync.Map // map[string]*entry
22+
count atomic.Int64 // estimated number of cached entries, see https://github.com/golang/go/issues/20680
23+
quit chan struct{}
2224
}
2325

2426
entry struct {
25-
cachedAt time.Time
26-
expiresAt time.Time
27-
info map[string]any
28-
// reference in the history
29-
href *list.Element
27+
expiresAt time.Time
28+
info map[string]any
29+
infoExpiresAt time.Time
3030
}
3131
)
3232

3333
var _ tokeninfoClient = &tokeninfoCache{}
3434

3535
const expiresInField = "expires_in"
3636

37-
func newTokeninfoCache(client tokeninfoClient, size int, ttl time.Duration) *tokeninfoCache {
38-
return &tokeninfoCache{
37+
func newTokeninfoCache(client tokeninfoClient, metrics metrics.Metrics, size int, ttl time.Duration) *tokeninfoCache {
38+
c := &tokeninfoCache{
3939
client: client,
40+
metrics: metrics,
4041
size: size,
4142
ttl: ttl,
4243
now: time.Now,
43-
cache: make(map[string]*entry, size),
44-
history: list.New(),
44+
quit: make(chan struct{}),
4545
}
46+
go c.evictLoop()
47+
return c
48+
}
49+
50+
func (c *tokeninfoCache) Close() {
51+
c.client.Close()
52+
close(c.quit)
4653
}
4754

4855
func (c *tokeninfoCache) getTokeninfo(token string, ctx filters.FilterContext) (map[string]any, error) {
@@ -58,35 +65,21 @@ func (c *tokeninfoCache) getTokeninfo(token string, ctx filters.FilterContext) (
5865
}
5966

6067
func (c *tokeninfoCache) cached(token string) map[string]any {
61-
now := c.now()
62-
63-
c.mu.Lock()
64-
65-
if e, ok := c.cache[token]; ok {
68+
if v, ok := c.cache.Load(token); ok {
69+
now := c.now()
70+
e := v.(*entry)
6671
if now.Before(e.expiresAt) {
67-
c.history.MoveToFront(e.href)
68-
cachedInfo := e.info
69-
c.mu.Unlock()
70-
7172
// It might be ok to return cached value
7273
// without adjusting "expires_in" to avoid copy
7374
// if caller never modifies the result and
7475
// when "expires_in" did not change (same second)
7576
// or for small TTL values
76-
info := shallowCopyOf(cachedInfo)
77+
info := maps.Clone(e.info)
7778

78-
elapsed := now.Sub(e.cachedAt).Truncate(time.Second).Seconds()
79-
info[expiresInField] = info[expiresInField].(float64) - elapsed
79+
info[expiresInField] = e.infoExpiresAt.Sub(now).Truncate(time.Second).Seconds()
8080
return info
81-
} else {
82-
// remove expired
83-
delete(c.cache, token)
84-
c.history.Remove(e.href)
8581
}
8682
}
87-
88-
c.mu.Unlock()
89-
9083
return nil
9184
}
9285

@@ -95,38 +88,62 @@ func (c *tokeninfoCache) tryCache(token string, info map[string]any) {
9588
if expiresIn <= 0 {
9689
return
9790
}
98-
if c.ttl > 0 && expiresIn > c.ttl {
99-
expiresIn = c.ttl
100-
}
10191

10292
now := c.now()
103-
expiresAt := now.Add(expiresIn)
93+
e := &entry{
94+
info: info,
95+
infoExpiresAt: now.Add(expiresIn),
96+
}
97+
98+
if c.ttl > 0 && expiresIn > c.ttl {
99+
e.expiresAt = now.Add(c.ttl)
100+
} else {
101+
e.expiresAt = e.infoExpiresAt
102+
}
104103

105-
c.mu.Lock()
106-
defer c.mu.Unlock()
104+
if _, loaded := c.cache.Swap(token, e); !loaded {
105+
c.count.Add(1)
106+
}
107+
}
107108

108-
if e, ok := c.cache[token]; ok {
109-
// update
110-
e.cachedAt = now
111-
e.expiresAt = expiresAt
112-
e.info = info
113-
c.history.MoveToFront(e.href)
114-
return
109+
func (c *tokeninfoCache) evictLoop() {
110+
ticker := time.NewTicker(time.Minute)
111+
defer ticker.Stop()
112+
for {
113+
select {
114+
case <-c.quit:
115+
return
116+
case <-ticker.C:
117+
c.evict()
118+
}
115119
}
120+
}
116121

117-
// create
118-
c.cache[token] = &entry{
119-
cachedAt: now,
120-
expiresAt: expiresAt,
121-
info: info,
122-
href: c.history.PushFront(token),
122+
func (c *tokeninfoCache) evict() {
123+
now := c.now()
124+
// Evict expired entries
125+
c.cache.Range(func(key, value any) bool {
126+
e := value.(*entry)
127+
if now.After(e.expiresAt) {
128+
if c.cache.CompareAndDelete(key, value) {
129+
c.count.Add(-1)
130+
}
131+
}
132+
return true
133+
})
134+
135+
// Evict random entries until the cache size is within limits
136+
if c.count.Load() > int64(c.size) {
137+
c.cache.Range(func(key, value any) bool {
138+
if c.cache.CompareAndDelete(key, value) {
139+
c.count.Add(-1)
140+
}
141+
return c.count.Load() > int64(c.size)
142+
})
123143
}
124144

125-
// remove least used
126-
if len(c.cache) > c.size {
127-
leastUsed := c.history.Back()
128-
delete(c.cache, leastUsed.Value.(string))
129-
c.history.Remove(leastUsed)
145+
if c.metrics != nil {
146+
c.metrics.UpdateGauge("tokeninfocache.count", float64(c.count.Load()))
130147
}
131148
}
132149

@@ -141,11 +158,3 @@ func expiresIn(info map[string]any) time.Duration {
141158
}
142159
return 0
143160
}
144-
145-
func shallowCopyOf(info map[string]any) map[string]any {
146-
m := make(map[string]any, len(info))
147-
for k, v := range info {
148-
m[k] = v
149-
}
150-
return m
151-
}

0 commit comments

Comments
 (0)