2022-05-05 13:25:50 +00:00
|
|
|
// Copyright 2022 Cloudbase Solutions SRL
|
|
|
|
|
//
|
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
|
|
|
// not use this file except in compliance with the License. You may obtain
|
|
|
|
|
// a copy of the License at
|
|
|
|
|
//
|
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
//
|
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
|
|
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
|
|
|
// License for the specific language governing permissions and limitations
|
|
|
|
|
// under the License.
|
|
|
|
|
|
2022-05-03 19:49:14 +00:00
|
|
|
package auth
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2025-09-23 13:46:27 +03:00
|
|
|
"errors"
|
2022-05-03 19:49:14 +00:00
|
|
|
"fmt"
|
2024-01-30 11:07:55 +00:00
|
|
|
"log/slog"
|
2025-04-06 17:54:35 +00:00
|
|
|
"math"
|
2022-05-03 19:49:14 +00:00
|
|
|
"net/http"
|
2022-05-05 13:25:50 +00:00
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
2024-02-22 07:31:51 +01:00
|
|
|
jwt "github.com/golang-jwt/jwt/v5"
|
|
|
|
|
|
|
|
|
|
runnerErrors "github.com/cloudbase/garm-provider-common/errors"
|
|
|
|
|
commonParams "github.com/cloudbase/garm-provider-common/params"
|
2023-03-12 16:01:49 +02:00
|
|
|
"github.com/cloudbase/garm/config"
|
|
|
|
|
dbCommon "github.com/cloudbase/garm/database/common"
|
|
|
|
|
"github.com/cloudbase/garm/params"
|
|
|
|
|
"github.com/cloudbase/garm/runner/common"
|
2022-05-03 19:49:14 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// InstanceJWTClaims holds JWT claims
|
|
|
|
|
type InstanceJWTClaims struct {
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
PoolID string `json:"provider_id"`
|
|
|
|
|
// Scope is either repository or organization
|
2025-05-12 21:47:13 +00:00
|
|
|
Scope params.ForgeEntityType `json:"scope"`
|
2022-05-03 19:49:14 +00:00
|
|
|
// Entity is the repo or org name
|
2024-01-30 11:07:55 +00:00
|
|
|
Entity string `json:"entity"`
|
|
|
|
|
CreateAttempt int `json:"create_attempt"`
|
2025-05-19 19:45:45 +00:00
|
|
|
ForgeType string `json:"forge_type"`
|
2025-09-16 07:42:59 +00:00
|
|
|
IsAgent bool `json:"is_agent"`
|
2023-12-18 16:06:03 +00:00
|
|
|
jwt.RegisteredClaims
|
2022-05-03 19:49:14 +00:00
|
|
|
}
|
|
|
|
|
|
2024-06-20 15:28:56 +00:00
|
|
|
func NewInstanceTokenGetter(jwtSecret string) (InstanceTokenGetter, error) {
|
|
|
|
|
if jwtSecret == "" {
|
|
|
|
|
return nil, fmt.Errorf("jwt secret is required")
|
|
|
|
|
}
|
|
|
|
|
return &instanceToken{
|
|
|
|
|
jwtSecret: jwtSecret,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type instanceToken struct {
|
|
|
|
|
jwtSecret string
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-09 20:52:01 +00:00
|
|
|
func (i *instanceToken) NewInstanceJWTToken(instance params.Instance, entity params.ForgeEntity, ttlMinutes uint) (string, error) {
|
2022-07-17 07:21:20 +00:00
|
|
|
// Token expiration is equal to the bootstrap timeout set on the pool plus the polling
|
|
|
|
|
// interval garm uses to check for timed out runners. Runners that have not sent their info
|
|
|
|
|
// by the end of this interval are most likely failed and will be reaped by garm anyway.
|
2025-04-06 17:54:35 +00:00
|
|
|
var ttl int
|
|
|
|
|
if ttlMinutes > math.MaxInt {
|
|
|
|
|
ttl = math.MaxInt
|
|
|
|
|
} else {
|
|
|
|
|
ttl = int(ttlMinutes)
|
|
|
|
|
}
|
|
|
|
|
expireToken := time.Now().Add(time.Duration(ttl)*time.Minute + common.PoolReapTimeoutInterval)
|
2023-12-18 16:06:03 +00:00
|
|
|
expires := &jwt.NumericDate{
|
|
|
|
|
Time: expireToken,
|
|
|
|
|
}
|
2022-05-03 19:49:14 +00:00
|
|
|
claims := InstanceJWTClaims{
|
2023-12-18 16:06:03 +00:00
|
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
|
|
|
ExpiresAt: expires,
|
2022-05-04 11:44:10 +00:00
|
|
|
Issuer: "garm",
|
2022-05-03 19:49:14 +00:00
|
|
|
},
|
2024-01-30 11:07:55 +00:00
|
|
|
ID: instance.ID,
|
|
|
|
|
Name: instance.Name,
|
|
|
|
|
PoolID: instance.PoolID,
|
2025-09-09 20:52:01 +00:00
|
|
|
Scope: entity.EntityType,
|
2025-05-19 19:45:45 +00:00
|
|
|
Entity: entity.String(),
|
|
|
|
|
ForgeType: string(entity.Credentials.ForgeType),
|
2025-09-16 07:42:59 +00:00
|
|
|
IsAgent: false,
|
2024-01-30 11:07:55 +00:00
|
|
|
CreateAttempt: instance.CreateAttempt,
|
2022-05-03 19:49:14 +00:00
|
|
|
}
|
|
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
2024-06-20 15:28:56 +00:00
|
|
|
tokenString, err := token.SignedString([]byte(i.jwtSecret))
|
2022-05-03 19:49:14 +00:00
|
|
|
if err != nil {
|
2025-08-16 19:31:58 +00:00
|
|
|
return "", fmt.Errorf("error signing token: %w", err)
|
2022-05-03 19:49:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return tokenString, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// instanceMiddleware is the authentication middleware
|
|
|
|
|
// used with gorilla
|
|
|
|
|
type instanceMiddleware struct {
|
|
|
|
|
store dbCommon.Store
|
|
|
|
|
cfg config.JWTAuth
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewjwtMiddleware returns a populated jwtMiddleware
|
|
|
|
|
func NewInstanceMiddleware(store dbCommon.Store, cfg config.JWTAuth) (Middleware, error) {
|
|
|
|
|
return &instanceMiddleware{
|
|
|
|
|
store: store,
|
|
|
|
|
cfg: cfg,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-16 07:42:59 +00:00
|
|
|
func getForgeEntityFromInstance(ctx context.Context, store dbCommon.Store, instance params.Instance) (params.ForgeEntity, error) {
|
2025-09-23 13:46:27 +03:00
|
|
|
var entityGetter params.EntityGetter
|
|
|
|
|
var err error
|
|
|
|
|
switch {
|
|
|
|
|
case instance.PoolID != "":
|
2025-09-16 07:42:59 +00:00
|
|
|
entityGetter, err = store.GetPoolByID(ctx, instance.PoolID)
|
2025-09-23 13:46:27 +03:00
|
|
|
case instance.ScaleSetID != 0:
|
2025-09-16 07:42:59 +00:00
|
|
|
entityGetter, err = store.GetScaleSetByID(ctx, instance.ScaleSetID)
|
2025-09-23 13:46:27 +03:00
|
|
|
default:
|
|
|
|
|
return params.ForgeEntity{}, errors.New("instance not associated with a pool or scale set")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
slog.With(slog.Any("error", err)).ErrorContext(
|
|
|
|
|
ctx, "failed to get entity getter",
|
|
|
|
|
"instance", instance.Name)
|
|
|
|
|
return params.ForgeEntity{}, fmt.Errorf("error fetching entity getter: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
poolEntity, err := entityGetter.GetEntity()
|
|
|
|
|
if err != nil {
|
|
|
|
|
slog.With(slog.Any("error", err)).ErrorContext(
|
|
|
|
|
ctx, "failed to get entity",
|
|
|
|
|
"instance", instance.Name)
|
|
|
|
|
return params.ForgeEntity{}, fmt.Errorf("error fetching entity: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-16 07:42:59 +00:00
|
|
|
entity, err := store.GetForgeEntity(ctx, poolEntity.EntityType, poolEntity.ID)
|
2025-09-23 13:46:27 +03:00
|
|
|
if err != nil {
|
|
|
|
|
slog.With(slog.Any("error", err)).ErrorContext(
|
|
|
|
|
ctx, "failed to get entity",
|
|
|
|
|
"instance", instance.Name)
|
|
|
|
|
return params.ForgeEntity{}, fmt.Errorf("error fetching entity: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return entity, nil
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-03 19:49:14 +00:00
|
|
|
func (amw *instanceMiddleware) claimsToContext(ctx context.Context, claims *InstanceJWTClaims) (context.Context, error) {
|
|
|
|
|
if claims == nil {
|
2025-09-23 13:46:27 +03:00
|
|
|
slog.InfoContext(ctx, "no claims for instance")
|
2022-05-03 19:49:14 +00:00
|
|
|
return ctx, runnerErrors.ErrUnauthorized
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if claims.Name == "" {
|
2025-09-23 13:46:27 +03:00
|
|
|
slog.ErrorContext(ctx, "no name in calaims")
|
2022-05-03 19:49:14 +00:00
|
|
|
return nil, runnerErrors.ErrUnauthorized
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-16 23:00:55 +00:00
|
|
|
instanceInfo, err := amw.store.GetInstance(ctx, claims.Name)
|
2022-05-03 19:49:14 +00:00
|
|
|
if err != nil {
|
2025-09-23 13:46:27 +03:00
|
|
|
slog.ErrorContext(ctx, "failed to get instance", "error", err)
|
|
|
|
|
return ctx, runnerErrors.ErrUnauthorized
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-16 07:42:59 +00:00
|
|
|
entity, err := getForgeEntityFromInstance(ctx, amw.store, instanceInfo)
|
2025-09-23 13:46:27 +03:00
|
|
|
if err != nil {
|
|
|
|
|
slog.ErrorContext(ctx, "failed to get entity from instance", "error", err)
|
2022-05-03 19:49:14 +00:00
|
|
|
return ctx, runnerErrors.ErrUnauthorized
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-23 13:46:27 +03:00
|
|
|
ctx = PopulateInstanceContext(ctx, instanceInfo, entity, claims)
|
2022-05-03 19:49:14 +00:00
|
|
|
return ctx, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Middleware implements the middleware interface
|
|
|
|
|
func (amw *instanceMiddleware) Middleware(next http.Handler) http.Handler {
|
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2024-02-22 09:30:20 +01:00
|
|
|
// nolint:golangci-lint,godox
|
2022-05-03 19:49:14 +00:00
|
|
|
// TODO: Log error details when authentication fails
|
|
|
|
|
ctx := r.Context()
|
|
|
|
|
authorizationHeader := r.Header.Get("authorization")
|
|
|
|
|
if authorizationHeader == "" {
|
2024-01-05 23:32:16 +00:00
|
|
|
invalidAuthResponse(ctx, w)
|
2022-05-03 19:49:14 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bearerToken := strings.Split(authorizationHeader, " ")
|
|
|
|
|
if len(bearerToken) != 2 {
|
2024-01-05 23:32:16 +00:00
|
|
|
invalidAuthResponse(ctx, w)
|
2022-05-03 19:49:14 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
claims := &InstanceJWTClaims{}
|
|
|
|
|
token, err := jwt.ParseWithClaims(bearerToken[1], claims, func(token *jwt.Token) (interface{}, error) {
|
|
|
|
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
|
|
|
|
return nil, fmt.Errorf("invalid signing method")
|
|
|
|
|
}
|
|
|
|
|
return []byte(amw.cfg.Secret), nil
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
2024-01-05 23:32:16 +00:00
|
|
|
invalidAuthResponse(ctx, w)
|
2022-05-03 19:49:14 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !token.Valid {
|
2024-01-05 23:32:16 +00:00
|
|
|
invalidAuthResponse(ctx, w)
|
2022-05-03 19:49:14 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-09-16 07:42:59 +00:00
|
|
|
if claims.IsAgent {
|
|
|
|
|
invalidAuthResponse(ctx, w)
|
|
|
|
|
return
|
|
|
|
|
}
|
2022-05-03 19:49:14 +00:00
|
|
|
|
|
|
|
|
ctx, err = amw.claimsToContext(ctx, claims)
|
|
|
|
|
if err != nil {
|
2024-01-05 23:32:16 +00:00
|
|
|
invalidAuthResponse(ctx, w)
|
2022-05-03 19:49:14 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-09-23 13:46:27 +03:00
|
|
|
ctx = SetInstanceAuthToken(ctx, bearerToken[1])
|
2022-05-03 19:49:14 +00:00
|
|
|
|
2022-05-06 13:28:54 +00:00
|
|
|
if InstanceID(ctx) == "" {
|
2024-01-05 23:32:16 +00:00
|
|
|
invalidAuthResponse(ctx, w)
|
2022-12-01 18:00:22 +02:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runnerStatus := InstanceRunnerStatus(ctx)
|
2023-07-21 15:30:22 +00:00
|
|
|
if runnerStatus != params.RunnerInstalling && runnerStatus != params.RunnerPending {
|
2022-12-01 18:00:22 +02:00
|
|
|
// Instances that have finished installing can no longer authenticate to the API
|
2024-01-05 23:32:16 +00:00
|
|
|
invalidAuthResponse(ctx, w)
|
2022-12-01 18:00:22 +02:00
|
|
|
return
|
2022-05-06 13:28:54 +00:00
|
|
|
}
|
|
|
|
|
|
2024-01-30 11:07:55 +00:00
|
|
|
instanceParams, err := InstanceParams(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
slog.InfoContext(
|
|
|
|
|
ctx, "could not find instance params",
|
|
|
|
|
"runner_name", InstanceName(ctx))
|
|
|
|
|
invalidAuthResponse(ctx, w)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Token was generated for a previous attempt at creating this instance.
|
|
|
|
|
if claims.CreateAttempt != instanceParams.CreateAttempt {
|
|
|
|
|
slog.InfoContext(
|
|
|
|
|
ctx, "invalid token create attempt",
|
|
|
|
|
"runner_name", InstanceName(ctx),
|
|
|
|
|
"token_create_attempt", claims.CreateAttempt,
|
|
|
|
|
"instance_create_attempt", instanceParams.CreateAttempt)
|
|
|
|
|
invalidAuthResponse(ctx, w)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Only allow instances that are in the creating or running state to authenticate.
|
|
|
|
|
if instanceParams.Status != commonParams.InstanceCreating && instanceParams.Status != commonParams.InstanceRunning {
|
|
|
|
|
slog.InfoContext(
|
|
|
|
|
ctx, "invalid instance status",
|
|
|
|
|
"runner_name", InstanceName(ctx),
|
|
|
|
|
"status", instanceParams.Status)
|
|
|
|
|
invalidAuthResponse(ctx, w)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-03 19:49:14 +00:00
|
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
|
|
|
})
|
|
|
|
|
}
|