Skip to content

Commit e0d3c80

Browse files
authored
feat(attestations): support API tokens for attestations (#763)
Signed-off-by: Javier Rodriguez <[email protected]>
1 parent 055c36c commit e0d3c80

File tree

11 files changed

+771
-220
lines changed

11 files changed

+771
-220
lines changed

app/controlplane/api/controlplane/v1/workflow_run.pb.go

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

app/controlplane/api/controlplane/v1/workflow_run.proto

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ service WorkflowRunService {
4343

4444
message AttestationServiceGetContractRequest {
4545
int32 contract_revision = 1;
46+
// This parameter is not needed by Robot Account since they have the workflowID embedded.
47+
// API Tokens will send the parameter explicitly
48+
string workflow_name = 2;
4649
}
4750

4851
message AttestationServiceGetContractResponse {

app/controlplane/api/gen/frontend/controlplane/v1/workflow_run.ts

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

app/controlplane/cmd/wire_gen.go

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

app/controlplane/internal/data/workflow.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,10 +203,11 @@ func (r *WorkflowRepo) FindByID(ctx context.Context, id uuid.UUID) (*biz.Workflo
203203
Where(workflow.DeletedAtIsNil(), workflow.ID(id)).
204204
WithContract().WithOrganization().
205205
Only(ctx)
206-
if err != nil && !ent.IsNotFound(err) {
206+
if err != nil {
207+
if ent.IsNotFound(err) {
208+
return nil, biz.NewErrNotFound("workflow")
209+
}
207210
return nil, err
208-
} else if workflow == nil {
209-
return nil, nil
210211
}
211212

212213
// Not efficient, we need to do a query limit = 1 grouped by workflowID

app/controlplane/internal/server/grpc.go

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,17 @@ import (
2626
authzMiddleware "github.com/chainloop-dev/chainloop/app/controlplane/internal/authz/middleware"
2727
"github.com/chainloop-dev/chainloop/app/controlplane/internal/biz"
2828
"github.com/chainloop-dev/chainloop/app/controlplane/internal/conf"
29-
"github.com/chainloop-dev/chainloop/app/controlplane/internal/jwt/robotaccount"
3029
"github.com/chainloop-dev/chainloop/app/controlplane/internal/jwt/user"
30+
"github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/attjwtmiddleware"
3131

3232
"github.com/bufbuild/protovalidate-go"
3333
"github.com/chainloop-dev/chainloop/app/controlplane/internal/service"
3434
"github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext"
3535
"github.com/chainloop-dev/chainloop/internal/credentials"
3636
"github.com/getsentry/sentry-go"
37-
jwt "github.com/golang-jwt/jwt/v4"
37+
"github.com/golang-jwt/jwt/v4"
3838

39-
errors "github.com/go-kratos/kratos/v2/errors"
39+
"github.com/go-kratos/kratos/v2/errors"
4040
"github.com/go-kratos/kratos/v2/log"
4141
"github.com/go-kratos/kratos/v2/middleware"
4242
jwtMiddleware "github.com/go-kratos/kratos/v2/middleware/auth/jwt"
@@ -58,6 +58,7 @@ type Opts struct {
5858
ReferrerUseCase *biz.ReferrerUseCase
5959
APITokenUseCase *biz.APITokenUseCase
6060
OrganizationUserCase *biz.OrganizationUseCase
61+
WorkflowUseCase *biz.WorkflowUseCase
6162
// Services
6263
WorkflowSvc *service.WorkflowService
6364
AuthSvc *service.AuthService
@@ -192,16 +193,17 @@ func craftMiddleware(opts *Opts) []middleware.Middleware {
192193
middlewares = append(middlewares,
193194
// if we require a robot account
194195
selector.Server(
195-
// 1 - Extract the robot account from the JWT
196-
jwtMiddleware.Server(func(_ *jwt.Token) (interface{}, error) {
197-
// TODO: add support to multiple signing methods and keys
198-
return []byte(opts.AuthConfig.GeneratedJwsHmacSecret), nil
199-
},
200-
jwtMiddleware.WithSigningMethod(robotaccount.SigningMethod),
201-
jwtMiddleware.WithClaims(func() jwt.Claims { return &robotaccount.CustomClaims{} }),
196+
// 1 - Extract information from the JWT by using the claims
197+
attjwtmiddleware.WithJWTMulti(
198+
// Robot account provider
199+
attjwtmiddleware.NewRobotAccountProvider(opts.AuthConfig.GeneratedJwsHmacSecret),
200+
// API Token provider
201+
attjwtmiddleware.NewAPITokenProvider(opts.AuthConfig.GeneratedJwsHmacSecret),
202202
),
203-
// 2 - Set its workflow and organization in the context
204-
usercontext.WithCurrentRobotAccount(opts.RobotAccountUseCase, logHelper),
203+
// 2.a - Set its workflow and organization in the context
204+
usercontext.WithAttestationContextFromRobotAccount(opts.RobotAccountUseCase, logHelper),
205+
// 2.b - Set its API token and Robot Account as alternative to the user
206+
usercontext.WithAttestationContextFromAPIToken(opts.APITokenUseCase, logHelper),
205207
).Match(requireRobotAccountMatcher()).Build(),
206208
)
207209

app/controlplane/internal/service/attestation.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,22 @@ func NewAttestationService(opts *NewAttestationServiceOpts) *AttestationService
9090
func (s *AttestationService) GetContract(ctx context.Context, req *cpAPI.AttestationServiceGetContractRequest) (*cpAPI.AttestationServiceGetContractResponse, error) {
9191
robotAccount := usercontext.CurrentRobotAccount(ctx)
9292
if robotAccount == nil {
93-
return nil, errors.NotFound("not found", "robot account not found")
93+
return nil, errors.NotFound("not found", "neither robot account nor API token found")
94+
}
95+
96+
// If WorkflowID is not set and no workflowName is provided, return BadRequest.
97+
if req.GetWorkflowName() == "" && robotAccount.WorkflowID == "" {
98+
return nil, errors.BadRequest("bad request", "when using an API Token, workflow name is required as parameter")
99+
}
100+
101+
// If WorkflowID is not set, retrieve it using the workflow name. This is needed since when coming from
102+
// an API Token request, the only information we have set is the workflow name not the ID
103+
if robotAccount.WorkflowID == "" {
104+
workflow, err := s.workflowUseCase.FindByNameInOrg(ctx, robotAccount.OrgID, req.GetWorkflowName())
105+
if err != nil {
106+
return nil, fmt.Errorf("error retrieving the workflow: %w", err)
107+
}
108+
robotAccount.WorkflowID = workflow.ID.String()
94109
}
95110

96111
// Find workflow

app/controlplane/internal/usercontext/apitoken_middleware.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/chainloop-dev/chainloop/app/controlplane/internal/authz"
2828
"github.com/chainloop-dev/chainloop/app/controlplane/internal/biz"
2929
"github.com/chainloop-dev/chainloop/app/controlplane/internal/jwt/apitoken"
30+
"github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/attjwtmiddleware"
3031
"github.com/go-kratos/kratos/v2/middleware"
3132
jwtMiddleware "github.com/go-kratos/kratos/v2/middleware/auth/jwt"
3233
)
@@ -101,6 +102,74 @@ func WithCurrentAPITokenAndOrgMiddleware(apiTokenUC *biz.APITokenUseCase, orgUC
101102
}
102103
}
103104

105+
// WithAttestationContextFromAPIToken injects the API-Token, organization + robot account to the context
106+
func WithAttestationContextFromAPIToken(apiTokenUC *biz.APITokenUseCase, logger *log.Helper) middleware.Middleware {
107+
return func(handler middleware.Handler) middleware.Handler {
108+
return func(ctx context.Context, req interface{}) (interface{}, error) {
109+
authInfo, ok := attjwtmiddleware.FromJWTAuthContext(ctx)
110+
// If not found means that there is no currentUser set in the context
111+
if !ok {
112+
logger.Warn("couldn't extract org/user, JWT parser middleware not running before this one?")
113+
return nil, errors.New("can't extract JWT info from the context")
114+
}
115+
116+
// If the token is not an API token, we don't need to do anything
117+
if authInfo.ProviderKey != attjwtmiddleware.APITokenProviderKey {
118+
return handler(ctx, req)
119+
}
120+
121+
genericClaims, ok := authInfo.Claims.(jwt.MapClaims)
122+
if !ok {
123+
return nil, errors.New("error mapping the claims")
124+
}
125+
126+
// We've received an API-token, double check its audience
127+
if !genericClaims.VerifyAudience(apitoken.Audience, true) {
128+
return nil, errors.New("unexpected token, invalid audience")
129+
}
130+
131+
var err error
132+
tokenID, ok := genericClaims["jti"].(string)
133+
if !ok || tokenID == "" {
134+
return nil, errors.New("error mapping the API-token claims")
135+
}
136+
137+
ctx, err = setRobotAccountFromAPIToken(ctx, apiTokenUC, tokenID)
138+
if err != nil {
139+
return nil, errors.New("error extracting organization from APIToken")
140+
}
141+
142+
logger.Infow("msg", "[authN] processed credentials", "id", tokenID, "type", "API-token")
143+
144+
return handler(ctx, req)
145+
}
146+
}
147+
}
148+
149+
func setRobotAccountFromAPIToken(ctx context.Context, apiTokenUC *biz.APITokenUseCase, tokenID string) (context.Context, error) {
150+
if tokenID == "" {
151+
return nil, errors.New("error retrieving the key ID from the API token")
152+
}
153+
154+
// Check that the token exists and is not revoked
155+
token, err := apiTokenUC.FindByID(ctx, tokenID)
156+
if err != nil {
157+
return nil, fmt.Errorf("error retrieving the API token: %w", err)
158+
} else if token == nil {
159+
return nil, errors.New("API token not found")
160+
}
161+
162+
// Note: Expiration time does not need to be checked because that's done at the JWT
163+
// verification layer, which happens before this middleware is called
164+
if token.RevokedAt != nil {
165+
return nil, errors.New("API token revoked")
166+
}
167+
168+
ctx = withRobotAccount(ctx, &RobotAccount{OrgID: token.OrganizationID.String()})
169+
170+
return ctx, nil
171+
}
172+
104173
// Set the current organization and API-Token in the context
105174
func setCurrentOrgAndAPIToken(ctx context.Context, apiTokenUC *biz.APITokenUseCase, orgUC *biz.OrganizationUseCase, tokenID string) (context.Context, error) {
106175
if tokenID == "" {

0 commit comments

Comments
 (0)