Skip to content

Commit fd72555

Browse files
authored
feat: allow to perform attestations with user token (#1883)
Signed-off-by: Miguel Martinez <miguel@chainloop.dev>
1 parent ff35657 commit fd72555

3 files changed

Lines changed: 85 additions & 2 deletions

File tree

app/controlplane/internal/server/grpc.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,14 +213,18 @@ func craftMiddleware(opts *Opts) []middleware.Middleware {
213213
attjwtmiddleware.NewRobotAccountProvider(opts.AuthConfig.GeneratedJwsHmacSecret),
214214
// API Token provider
215215
attjwtmiddleware.NewAPITokenProvider(opts.AuthConfig.GeneratedJwsHmacSecret),
216+
// User Token provider
217+
attjwtmiddleware.NewUserTokenProvider(opts.AuthConfig.GeneratedJwsHmacSecret),
216218
// Delegated Federated provider
217219
attjwtmiddleware.WithFederatedProvider(opts.FederatedConfig),
218220
),
219221
// 2.a - Set its workflow and organization in the context
220222
usercontext.WithAttestationContextFromRobotAccount(opts.RobotAccountUseCase, opts.OrganizationUseCase, logHelper),
221223
// 2.b - Set its API token and Robot Account as alternative to the user
222224
usercontext.WithAttestationContextFromAPIToken(opts.APITokenUseCase, opts.OrganizationUseCase, logHelper),
223-
// 2.c - Set its robot account from federated delegation
225+
// 2.c - Set Attestation context from user token
226+
usercontext.WithAttestationContextFromUser(opts.UserUseCase, logHelper),
227+
// 2.d - Set its robot account from federated delegation
224228
usercontext.WithAttestationContextFromFederatedInfo(opts.OrganizationUseCase, logHelper),
225229
).Match(requireRobotAccountMatcher()).Build(),
226230
)

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ const (
5252
RobotAccountProviderKey = "robotAccountProvider"
5353
// APITokenProviderKey is the key for api token provider
5454
APITokenProviderKey = "apiTokenProvider"
55+
// UserTokenProviderKey is the key for user token provider
56+
UserTokenProviderKey = "userTokenProvider"
5557
// FederatedProviderKey is the key for federated token provider
5658
FederatedProviderKey = "federatedProvider"
5759
)
@@ -131,6 +133,24 @@ func NewAPITokenProvider(signingSecret string) JWTOption {
131133
)
132134
}
133135

136+
func NewUserTokenProvider(signingSecret string) JWTOption {
137+
return withTokenProvider(
138+
UserTokenProviderKey,
139+
WithVerifyAudienceFunc(func(token *jwt.Token) bool {
140+
genericClaims, ok := token.Claims.(jwt.MapClaims)
141+
if !ok {
142+
return false
143+
}
144+
145+
return genericClaims.VerifyAudience(user.Audience, true)
146+
}),
147+
WithSigningMethod(user.SigningMethod),
148+
WithKeyFunc(func(_ *jwt.Token) (interface{}, error) {
149+
return []byte(signingSecret), nil
150+
}),
151+
)
152+
}
153+
134154
type JWTAuthContext struct {
135155
Claims jwt.Claims
136156
ProviderKey string

app/controlplane/internal/usercontext/currentuser_middleware.go

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2024 The Chainloop Authors.
2+
// Copyright 2024-2025 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.
@@ -23,7 +23,9 @@ import (
2323
"github.com/go-kratos/kratos/v2/log"
2424
"github.com/golang-jwt/jwt/v4"
2525

26+
"github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/attjwtmiddleware"
2627
"github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/entities"
28+
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz"
2729
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz"
2830
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/jwt/user"
2931
"github.com/go-kratos/kratos/v2/middleware"
@@ -82,3 +84,60 @@ func setCurrentUser(ctx context.Context, userUC biz.UserOrgFinder, userID string
8284

8385
return entities.WithCurrentUser(ctx, &entities.User{Email: u.Email, ID: u.ID, CreatedAt: u.CreatedAt}), nil
8486
}
87+
88+
// Middleware that injects the current user + organization to the context during the attestation process
89+
// it leverages the existing middlewares to set the current user and organization
90+
// but with a skipping behavior since that's the one required by the attMiddleware multi-selector
91+
func WithAttestationContextFromUser(userUC *biz.UserUseCase, logger *log.Helper) middleware.Middleware {
92+
return func(handler middleware.Handler) middleware.Handler {
93+
return func(ctx context.Context, req interface{}) (interface{}, error) {
94+
// If the token is not an user token, we don't need to do anything
95+
// note that this middleware is called by a multi-middleware that works in cascade mode
96+
authInfo, ok := attjwtmiddleware.FromJWTAuthContext(ctx)
97+
// If not found means that there is no currentUser set in the context
98+
if !ok {
99+
logger.Warn("couldn't extract org/user, JWT parser middleware not running before this one?")
100+
return nil, errors.New("can't extract JWT info from the context")
101+
}
102+
103+
if authInfo.ProviderKey != attjwtmiddleware.UserTokenProviderKey {
104+
return handler(ctx, req)
105+
}
106+
107+
// set the raw claims in the default context field so the user middleware can understand it
108+
ctx = jwtMiddleware.NewContext(ctx, authInfo.Claims)
109+
// We received a user token during the attestation process
110+
// 1 - Set the current user from the user token
111+
// 2 - Set the current organization for the user token from the DB, header or default
112+
// 3 - Check if the user has permissions to perform attestations in the organization
113+
// 4 - Set the robot account
114+
// NOTE: we reuse the existing middlewares to set the current user and organization by wrapping the call
115+
// Now we can load the organization using the other middleware we have set
116+
return WithCurrentUserMiddleware(userUC, logger)(func(ctx context.Context, req any) (any, error) {
117+
return WithCurrentOrganizationMiddleware(userUC, logger)(func(ctx context.Context, req any) (any, error) {
118+
org := entities.CurrentOrg(ctx)
119+
if org == nil {
120+
return nil, errors.New("organization not found")
121+
}
122+
123+
subject := CurrentAuthzSubject(ctx)
124+
if subject == "" {
125+
return nil, errors.New("missing authorization subject")
126+
}
127+
128+
// Load the authorization subject from the context which might be related to a currentUser or an APItoken
129+
// TODO: move to authz middleware once we add support for all the tokens
130+
// for now in that middleware we are not mapping admins nor owners to a specific role
131+
if subject != string(authz.RoleAdmin) && subject != string(authz.RoleOwner) {
132+
return nil, fmt.Errorf("your user doesn't have permissions to perform attestations in this organization, role=%s, orgID=%s", subject, org.ID)
133+
}
134+
135+
ctx = withRobotAccount(ctx, &RobotAccount{OrgID: org.ID, ProviderKey: attjwtmiddleware.UserTokenProviderKey})
136+
logger.Infow("msg", "[authN] processed credentials", "type", attjwtmiddleware.UserTokenProviderKey)
137+
138+
return handler(ctx, req)
139+
})(ctx, req)
140+
})(ctx, req)
141+
}
142+
}
143+
}

0 commit comments

Comments
 (0)