Skip to content

Commit fa49310

Browse files
authored
feat(cache): unified cache abstraction with in-memory and NATS KV backends (#2952)
Signed-off-by: Miguel Martinez Trivino <miguel@chainloop.dev>
1 parent 6fb0282 commit fa49310

18 files changed

Lines changed: 1021 additions & 149 deletions

File tree

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ values.local.yaml
5555
deployment/chainloop/charts/**/*.tgz
5656

5757
# Local development overrides
58-
app/controlplane/configs/config.local.yaml
58+
config.local.yaml
5959
devel/dex/config.dev.local.yaml
6060
devel/compose.override.yml
61-
.claude/worktrees/
61+
.claude/worktrees/

app/controlplane/cmd/wire.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,13 @@
2121
package main
2222

2323
import (
24+
"time"
25+
2426
conf "github.com/chainloop-dev/chainloop/app/controlplane/internal/conf/controlplane/config/v1"
2527
"github.com/chainloop-dev/chainloop/app/controlplane/internal/dispatcher"
2628
"github.com/chainloop-dev/chainloop/app/controlplane/internal/server"
2729
"github.com/chainloop-dev/chainloop/app/controlplane/internal/service"
30+
"github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/entities"
2831
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/auditor"
2932
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz"
3033
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz"
@@ -33,9 +36,12 @@ import (
3336
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/policies"
3437
"github.com/chainloop-dev/chainloop/app/controlplane/plugins/sdk/v1"
3538
"github.com/chainloop-dev/chainloop/pkg/blobmanager/loader"
39+
"github.com/chainloop-dev/chainloop/pkg/cache"
3640
"github.com/chainloop-dev/chainloop/pkg/credentials"
3741
"github.com/go-kratos/kratos/v2/log"
42+
"github.com/golang-jwt/jwt/v4"
3843
"github.com/google/wire"
44+
"github.com/nats-io/nats.go"
3945
)
4046

4147
func wireApp(*conf.Bootstrap, credentials.ReaderWriter, log.Logger, sdk.AvailablePlugins) (*app, func(), error) {
@@ -60,6 +66,7 @@ func wireApp(*conf.Bootstrap, credentials.ReaderWriter, log.Logger, sdk.Availabl
6066
newDataConf,
6167
newPolicyProviderConfig,
6268
newNatsConnection,
69+
cacheProviderSet,
6370
auditor.NewAuditLogPublisher,
6471
newCASServerOptions,
6572
newAuthAllowList,
@@ -127,3 +134,48 @@ func newCASServerOptions(in *conf.Bootstrap_CASServer) *biz.CASServerDefaultOpts
127134
func newAuthAllowList(conf *conf.Bootstrap) *pkgConf.AllowList {
128135
return conf.Auth.GetAllowList()
129136
}
137+
138+
var cacheProviderSet = wire.NewSet(
139+
newMembershipsCache,
140+
newClaimsCache,
141+
)
142+
143+
func newClaimsCache(conn *nats.Conn, logger log.Logger) (cache.Cache[*jwt.MapClaims], error) {
144+
l := log.NewHelper(logger)
145+
backend := "memory"
146+
opts := []cache.Option{cache.WithTTL(10 * time.Second), cache.WithLogger(&kratosLogAdapter{h: l})}
147+
if conn != nil {
148+
backend = "nats"
149+
opts = append(opts, cache.WithNATS(conn, "chainloop-jwt-claims"))
150+
}
151+
l.Infow("msg", "cache initialized", "bucket", "chainloop-jwt-claims", "backend", backend, "ttl", "10s")
152+
return cache.New[*jwt.MapClaims](opts...)
153+
}
154+
155+
func newMembershipsCache(conn *nats.Conn, logger log.Logger) (cache.Cache[*entities.Membership], error) {
156+
l := log.NewHelper(logger)
157+
backend := "memory"
158+
opts := []cache.Option{cache.WithTTL(time.Second), cache.WithLogger(&kratosLogAdapter{h: l})}
159+
if conn != nil {
160+
backend = "nats"
161+
opts = append(opts, cache.WithNATS(conn, "chainloop-memberships"))
162+
}
163+
l.Infow("msg", "cache initialized", "bucket", "chainloop-memberships", "backend", backend, "ttl", "1s")
164+
return cache.New[*entities.Membership](opts...)
165+
}
166+
167+
// kratosLogAdapter adapts kratos log.Helper (Debugw(...interface{})) to cache.Logger (Debugw(string, ...any)).
168+
type kratosLogAdapter struct{ h *log.Helper }
169+
170+
func (a *kratosLogAdapter) Debugw(msg string, keyvals ...any) {
171+
a.h.Debugw(append([]any{"msg", msg}, keyvals...)...)
172+
}
173+
func (a *kratosLogAdapter) Infow(msg string, keyvals ...any) {
174+
a.h.Infow(append([]any{"msg", msg}, keyvals...)...)
175+
}
176+
func (a *kratosLogAdapter) Warnw(msg string, keyvals ...any) {
177+
a.h.Warnw(append([]any{"msg", msg}, keyvals...)...)
178+
}
179+
func (a *kratosLogAdapter) Errorw(msg string, keyvals ...any) {
180+
a.h.Errorw(append([]any{"msg", msg}, keyvals...)...)
181+
}

app/controlplane/cmd/wire_gen.go

Lines changed: 67 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/internal/server/grpc.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ import (
2525
conf "github.com/chainloop-dev/chainloop/app/controlplane/internal/conf/controlplane/config/v1"
2626
"github.com/chainloop-dev/chainloop/app/controlplane/internal/sentrycontext"
2727
"github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/attjwtmiddleware"
28+
"github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/entities"
2829
authzMiddleware "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz/middleware"
2930
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz"
3031
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/jwt/user"
32+
"github.com/chainloop-dev/chainloop/pkg/cache"
3133

3234
"buf.build/go/protovalidate"
3335
"github.com/chainloop-dev/chainloop/app/controlplane/internal/service"
@@ -61,6 +63,8 @@ type Opts struct {
6163
OrganizationUseCase *biz.OrganizationUseCase
6264
WorkflowUseCase *biz.WorkflowUseCase
6365
MembershipUseCase *biz.MembershipUseCase
66+
MembershipsCache cache.Cache[*entities.Membership]
67+
ClaimsCache cache.Cache[*jwt.MapClaims]
6468
// Services
6569
WorkflowSvc *service.WorkflowService
6670
AuthSvc *service.AuthService
@@ -203,7 +207,7 @@ func craftMiddleware(opts *Opts) []middleware.Middleware {
203207
// 2.c - Set its user
204208
usercontext.WithCurrentUserMiddleware(opts.UserUseCase, logHelper),
205209
// Store all memberships in the context
206-
usercontext.WithCurrentMembershipsMiddleware(opts.MembershipUseCase),
210+
usercontext.WithCurrentMembershipsMiddleware(opts.MembershipUseCase, opts.MembershipsCache),
207211
selector.Server(
208212
// 2.d- Set its organization
209213
usercontext.WithCurrentOrganizationMiddleware(opts.UserUseCase, opts.OrganizationUseCase, logHelper),
@@ -235,6 +239,7 @@ func craftMiddleware(opts *Opts) []middleware.Middleware {
235239
attjwtmiddleware.NewUserTokenProvider(opts.AuthConfig.GeneratedJwsHmacSecret),
236240
// Delegated Federated provider
237241
attjwtmiddleware.WithFederatedProvider(opts.FederatedConfig),
242+
attjwtmiddleware.WithClaimsCache(opts.ClaimsCache),
238243
),
239244
// 2.a - Set its workflow and organization in the context
240245
usercontext.WithAttestationContextFromRobotAccount(opts.RobotAccountUseCase, opts.OrganizationUseCase, logHelper),
@@ -245,7 +250,7 @@ func craftMiddleware(opts *Opts) []middleware.Middleware {
245250
// 2.d - Set its robot account from federated delegation
246251
usercontext.WithAttestationContextFromFederatedInfo(opts.OrganizationUseCase, logHelper),
247252
// Store all memberships in the context
248-
usercontext.WithCurrentMembershipsMiddleware(opts.MembershipUseCase),
253+
usercontext.WithCurrentMembershipsMiddleware(opts.MembershipUseCase, opts.MembershipsCache),
249254
// 3 - Update API Token last usage
250255
usercontext.WithAPITokenUsageUpdater(opts.APITokenUseCase, logHelper),
251256
// 4 - Validate the CAS Backend is fully configured and valid

app/controlplane/internal/service/attestation.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2024-2025 The Chainloop Authors.
2+
// Copyright 2024-2026 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -34,6 +34,7 @@ import (
3434
casJWT "github.com/chainloop-dev/chainloop/internal/robotaccount/cas"
3535
"github.com/chainloop-dev/chainloop/pkg/attestation"
3636
"github.com/chainloop-dev/chainloop/pkg/attestation/renderer/chainloop"
37+
"github.com/chainloop-dev/chainloop/pkg/cache"
3738
"github.com/chainloop-dev/chainloop/pkg/credentials"
3839
errors "github.com/go-kratos/kratos/v2/errors"
3940
v1 "github.com/google/go-containerregistry/pkg/v1"
@@ -62,6 +63,7 @@ type AttestationService struct {
6263
signingUseCase *biz.SigningUseCase
6364
userUseCase *biz.UserUseCase
6465
bootstrapConfig *conf.Bootstrap
66+
membershipsCache cache.Cache[*entities.Membership]
6567
}
6668

6769
type NewAttestationServiceOpts struct {
@@ -83,6 +85,7 @@ type NewAttestationServiceOpts struct {
8385
SigningUseCase *biz.SigningUseCase
8486
UserUC *biz.UserUseCase
8587
BootstrapConfig *conf.Bootstrap
88+
MembershipsCache cache.Cache[*entities.Membership]
8689
Opts []NewOpt
8790
}
8891

@@ -107,6 +110,7 @@ func NewAttestationService(opts *NewAttestationServiceOpts) *AttestationService
107110
signingUseCase: opts.SigningUseCase,
108111
userUseCase: opts.UserUC,
109112
bootstrapConfig: opts.BootstrapConfig,
113+
membershipsCache: opts.MembershipsCache,
110114
}
111115
}
112116

@@ -797,7 +801,9 @@ func (s *AttestationService) FindOrCreateWorkflow(ctx context.Context, req *cpAP
797801
}
798802

799803
// reset RBAC cache, since we might have created a new project
800-
usercontext.ResetMembershipsCache()
804+
if s.membershipsCache != nil {
805+
_ = s.membershipsCache.Purge(ctx)
806+
}
801807

802808
return &cpAPI.FindOrCreateWorkflowResponse{Result: bizWorkflowToPb(wf)}, nil
803809
}

app/controlplane/internal/usercontext/attjwtmiddleware/attmiddleware.go

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2024-2025 The Chainloop Authors.
2+
// Copyright 2024-2026 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -24,19 +24,18 @@ import (
2424
"io"
2525
"net/http"
2626
"strings"
27-
"time"
2827

2928
v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
3029
conf "github.com/chainloop-dev/chainloop/app/controlplane/internal/conf/controlplane/config/v1"
3130
"github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/entities"
3231
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/jwt/apitoken"
3332
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/jwt/robotaccount"
3433
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/jwt/user"
34+
"github.com/chainloop-dev/chainloop/pkg/cache"
3535
errorsAPI "github.com/go-kratos/kratos/v2/errors"
3636
"github.com/go-kratos/kratos/v2/log"
3737
"github.com/go-kratos/kratos/v2/middleware"
3838
"github.com/go-kratos/kratos/v2/transport"
39-
"github.com/hashicorp/golang-lru/v2/expirable"
4039

4140
"github.com/golang-jwt/jwt/v4"
4241
)
@@ -199,6 +198,12 @@ func WithVerifyAudienceFunc(f VerifyAudienceFunc) TokenProviderOption {
199198
type options struct {
200199
tokenProviders []providerOption
201200
federatedAuthURL string
201+
claimsCache cache.Cache[*jwt.MapClaims]
202+
}
203+
204+
// WithClaimsCache sets an external cache for federated JWT claims.
205+
func WithClaimsCache(c cache.Cache[*jwt.MapClaims]) JWTOption {
206+
return func(o *options) { o.claimsCache = c }
202207
}
203208

204209
func withTokenProvider(providerKey string, opts ...TokenProviderOption) JWTOption {
@@ -251,9 +256,6 @@ func WithJWTMulti(l log.Logger, opts ...JWTOption) middleware.Middleware {
251256
logger.Infof("federated authentication enabled, using URL: %s", o.federatedAuthURL)
252257
}
253258

254-
// claims cache with 10s TTL and unlimited keys
255-
claimsCache := expirable.NewLRU[string, *jwt.MapClaims](0, nil, time.Second*10)
256-
257259
return func(handler middleware.Handler) middleware.Handler {
258260
return func(ctx context.Context, req interface{}) (interface{}, error) {
259261
if header, ok := transport.FromServerContext(ctx); ok {
@@ -286,7 +288,7 @@ func WithJWTMulti(l log.Logger, opts ...JWTOption) middleware.Middleware {
286288
}
287289

288290
logger.Infof("calling federated provider, orgName: %s", orgName)
289-
claims, err := callFederatedProvider(o.federatedAuthURL, jwtToken, orgName, claimsCache)
291+
claims, err := callFederatedProvider(ctx, o.federatedAuthURL, jwtToken, orgName, o.claimsCache)
290292
if err != nil {
291293
// if we receive an error from upstream we want to expose it to the user, for example if the federated provider
292294
// is saying that the token is invalid
@@ -333,9 +335,9 @@ func WithJWTMulti(l log.Logger, opts ...JWTOption) middleware.Middleware {
333335

334336
// callFederatedProvider calls the federated provider to verify the token
335337
// it returns the claims of the token if the token is valid and verified
336-
func callFederatedProvider(verifyURL string, jwtToken, orgName string, cache *expirable.LRU[string, *jwt.MapClaims]) (*jwt.MapClaims, error) {
338+
func callFederatedProvider(ctx context.Context, verifyURL string, jwtToken, orgName string, claimsCache cache.Cache[*jwt.MapClaims]) (*jwt.MapClaims, error) {
337339
cacheKey := fmt.Sprintf("%s:%s", jwtToken, orgName)
338-
if claims, ok := cache.Get(cacheKey); ok {
340+
if claims, ok, _ := claimsCache.Get(ctx, cacheKey); ok {
339341
return claims, nil
340342
}
341343

@@ -391,7 +393,7 @@ func callFederatedProvider(verifyURL string, jwtToken, orgName string, cache *ex
391393
"orgName": response.OrgName,
392394
}
393395

394-
cache.Add(cacheKey, claims)
396+
_ = claimsCache.Set(ctx, cacheKey, claims)
395397

396398
return claims, nil
397399
}

0 commit comments

Comments
 (0)