diff --git a/api/admin/worker.go b/api/admin/worker.go new file mode 100644 index 000000000..8c0f9978a --- /dev/null +++ b/api/admin/worker.go @@ -0,0 +1,73 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package admin + +import ( + "fmt" + "net/http" + + "github.com/go-vela/server/internal/token" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" +) + +// swagger:operation POST /api/v1/admin/workers/{worker}/register-token admin RegisterToken +// +// Get a worker registration token +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: worker +// description: Hostname of the worker +// required: true +// type: string +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully generated registration token +// schema: +// "$ref": "#/definitions/Token" +// '401': +// description: Unauthorized +// schema: +// "$ref": "#/definitions/Error" + +// RegisterToken represents the API handler to +// generate a registration token for onboarding a worker. +func RegisterToken(c *gin.Context) { + // retrieve user from context + u := user.Retrieve(c) + + logrus.Infof("Platform admin %s: generating registration token", u.GetName()) + + host := util.PathParameter(c, "worker") + + tm := c.MustGet("token-manager").(*token.Manager) + rmto := &token.MintTokenOpts{ + Hostname: host, + TokenType: constants.WorkerRegisterTokenType, + TokenDuration: tm.WorkerRegisterTokenDuration, + } + + rt, err := tm.MintToken(rmto) + if err != nil { + retErr := fmt.Errorf("unable to generate registration token: %w", err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, library.Token{Token: &rt}) +} diff --git a/api/build.go b/api/build.go index 5be0c08d6..cb27dab14 100644 --- a/api/build.go +++ b/api/build.go @@ -1764,8 +1764,26 @@ func CancelBuild(c *gin.Context) { return } + tm := c.MustGet("token-manager").(*token.Manager) + + // set mint token options + mto := &token.MintTokenOpts{ + Hostname: "vela-server", + TokenType: constants.WorkerAuthTokenType, + TokenDuration: time.Minute * 1, + } + + // mint token + tkn, err := tm.MintToken(mto) + if err != nil { + retErr := fmt.Errorf("unable to generate auth token: %w", err) + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + // add the token to authenticate to the worker - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.MustGet("secret").(string))) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tkn)) // perform the request to the worker resp, err := client.Do(req) diff --git a/api/token.go b/api/token.go index 87a49288a..6c76439ec 100644 --- a/api/token.go +++ b/api/token.go @@ -7,9 +7,11 @@ package api import ( "fmt" "net/http" + "strings" "github.com/go-vela/server/internal/token" "github.com/go-vela/server/router/middleware/auth" + "github.com/go-vela/server/router/middleware/claims" "github.com/go-vela/server/util" "github.com/go-vela/types/library" @@ -65,3 +67,38 @@ func RefreshAccessToken(c *gin.Context) { c.JSON(http.StatusOK, library.Token{Token: &newAccessToken}) } + +// swagger:operation GET /validate-token authenticate ValidateServerToken +// +// Validate a server token +// +// --- +// produces: +// - application/json +// security: +// - CookieAuth: [] +// responses: +// '200': +// description: Successfully validated a token +// schema: +// type: string +// '401': +// description: Unauthorized +// schema: +// "$ref": "#/definitions/Error" + +// ValidateServerToken will return the claims of a valid server token +// if it is provided in the auth header. +func ValidateServerToken(c *gin.Context) { + cl := claims.Retrieve(c) + + if !strings.EqualFold(cl.Subject, "vela-server") { + retErr := fmt.Errorf("token is not a valid server token") + + util.HandleError(c, http.StatusUnauthorized, retErr) + + return + } + + c.JSON(http.StatusOK, "valid server token") +} diff --git a/api/worker.go b/api/worker.go index 4951b182a..4ef613e78 100644 --- a/api/worker.go +++ b/api/worker.go @@ -7,8 +7,12 @@ package api import ( "fmt" "net/http" + "time" + "github.com/go-vela/server/internal/token" + "github.com/go-vela/server/router/middleware/claims" "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/types/constants" "github.com/go-vela/server/database" "github.com/go-vela/server/router/middleware/worker" @@ -38,9 +42,9 @@ import ( // - ApiKeyAuth: [] // responses: // '201': -// description: Successfully created the worker +// description: Successfully created the worker and retrieved auth token // schema: -// type: string +// "$ref": "#definitions/Token" // '400': // description: Unable to create the worker // schema: @@ -55,6 +59,7 @@ import ( func CreateWorker(c *gin.Context) { // capture middleware values u := user.Retrieve(c) + cl := claims.Retrieve(c) // capture body from API request input := new(library.Worker) @@ -85,7 +90,46 @@ func CreateWorker(c *gin.Context) { return } - c.JSON(http.StatusCreated, fmt.Sprintf("worker %s created", input.GetHostname())) + switch cl.TokenType { + // if symmetric token configured, send back symmetric token + case constants.ServerWorkerTokenType: + if secret, ok := c.Value("secret").(string); ok { + tkn := new(library.Token) + tkn.SetToken(secret) + c.JSON(http.StatusCreated, tkn) + + return + } + + retErr := fmt.Errorf("symmetric token provided but not configured in server") + util.HandleError(c, http.StatusBadRequest, retErr) + + return + // if worker register token, send back auth token + default: + tm := c.MustGet("token-manager").(*token.Manager) + + wmto := &token.MintTokenOpts{ + TokenType: constants.WorkerAuthTokenType, + TokenDuration: tm.WorkerAuthTokenDuration, + Hostname: cl.Subject, + } + + tkn := new(library.Token) + + wt, err := tm.MintToken(wmto) + if err != nil { + retErr := fmt.Errorf("unable to generate auth token for worker %s: %w", input.GetHostname(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + tkn.SetToken(wt) + + c.JSON(http.StatusCreated, tkn) + } } // swagger:operation GET /api/v1/workers workers GetWorkers @@ -226,7 +270,7 @@ func GetWorker(c *gin.Context) { // "$ref": "#/definitions/Error" // UpdateWorker represents the API handler to -// create a worker in the configured backend. +// update a worker in the configured backend. func UpdateWorker(c *gin.Context) { // capture middleware values u := user.Retrieve(c) @@ -267,11 +311,6 @@ func UpdateWorker(c *gin.Context) { w.SetActive(input.GetActive()) } - if input.GetLastCheckedIn() > 0 { - // update LastCheckedIn if set - w.SetLastCheckedIn(input.GetLastCheckedIn()) - } - // send API call to update the worker err = database.FromContext(c).UpdateWorker(w) if err != nil { @@ -288,6 +327,108 @@ func UpdateWorker(c *gin.Context) { c.JSON(http.StatusOK, w) } +// swagger:operation POST /api/v1/workers/{worker}/refresh workers RefreshWorkerAuth +// +// Refresh authorization token for worker +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: worker +// description: Name of the worker +// required: true +// type: string +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully refreshed auth +// schema: +// "$ref": "#/definitions/Token" +// '400': +// description: Unable to refresh worker auth +// schema: +// "$ref": "#/definitions/Error" +// '404': +// description: Unable to refresh worker auth +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to refresh worker auth +// schema: +// "$ref": "#/definitions/Error" + +// RefreshWorkerAuth represents the API handler to +// refresh the auth token for a worker. +func RefreshWorkerAuth(c *gin.Context) { + // capture middleware values + w := worker.Retrieve(c) + cl := claims.Retrieve(c) + + // set last checked in time + w.SetLastCheckedIn(time.Now().Unix()) + + // send API call to update the worker + err := database.FromContext(c).UpdateWorker(w) + if err != nil { + retErr := fmt.Errorf("unable to update worker %s: %w", w.GetHostname(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "worker": w.GetHostname(), + }).Infof("refreshing worker %s authentication", w.GetHostname()) + + switch cl.TokenType { + // if symmetric token configured, send back symmetric token + case constants.ServerWorkerTokenType: + if secret, ok := c.Value("secret").(string); ok { + tkn := new(library.Token) + tkn.SetToken(secret) + c.JSON(http.StatusOK, tkn) + + return + } + + retErr := fmt.Errorf("symmetric token provided but not configured in server") + util.HandleError(c, http.StatusBadRequest, retErr) + + return + // if worker auth / register token, send back auth token + case constants.WorkerAuthTokenType, constants.WorkerRegisterTokenType: + tm := c.MustGet("token-manager").(*token.Manager) + + wmto := &token.MintTokenOpts{ + TokenType: constants.WorkerAuthTokenType, + TokenDuration: tm.WorkerAuthTokenDuration, + Hostname: cl.Subject, + } + + tkn := new(library.Token) + + wt, err := tm.MintToken(wmto) + if err != nil { + retErr := fmt.Errorf("unable to generate auth token for worker %s: %w", w.GetHostname(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + tkn.SetToken(wt) + + c.JSON(http.StatusOK, tkn) + } +} + // swagger:operation DELETE /api/v1/workers/{worker} workers DeleteWorker // // Delete a worker for the configured backend diff --git a/cmd/vela-server/main.go b/cmd/vela-server/main.go index fd1ebfedf..0dff41473 100644 --- a/cmd/vela-server/main.go +++ b/cmd/vela-server/main.go @@ -147,6 +147,18 @@ func main() { Usage: "sets the duration of the buffer for build token expiration based on repo build timeout", Value: 5 * time.Minute, }, + &cli.DurationFlag{ + EnvVars: []string{"VELA_WORKER_AUTH_TOKEN_DURATION", "WORKER_AUTH_TOKEN_DURATION"}, + Name: "worker-auth-token-duration", + Usage: "sets the duration of the worker auth token", + Value: 20 * time.Minute, + }, + &cli.DurationFlag{ + EnvVars: []string{"VELA_WORKER_REGISTER_TOKEN_DURATION", "WORKER_REGISTER_TOKEN_DURATION"}, + Name: "worker-register-token-duration", + Usage: "sets the duration of the worker register token", + Value: 1 * time.Minute, + }, // Compiler Flags &cli.BoolFlag{ EnvVars: []string{"VELA_COMPILER_GITHUB", "COMPILER_GITHUB"}, diff --git a/cmd/vela-server/token.go b/cmd/vela-server/token.go index eae471373..2f406a16b 100644 --- a/cmd/vela-server/token.go +++ b/cmd/vela-server/token.go @@ -19,11 +19,13 @@ func setupTokenManager(c *cli.Context) *token.Manager { logrus.Debug("Creating token manager from CLI configuration") tm := &token.Manager{ - PrivateKey: c.String("vela-server-private-key"), - SignMethod: jwt.SigningMethodHS256, - UserAccessTokenDuration: c.Duration("user-access-token-duration"), - UserRefreshTokenDuration: c.Duration("user-refresh-token-duration"), - BuildTokenBufferDuration: c.Duration("build-token-buffer-duration"), + PrivateKey: c.String("vela-server-private-key"), + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: c.Duration("user-access-token-duration"), + UserRefreshTokenDuration: c.Duration("user-refresh-token-duration"), + BuildTokenBufferDuration: c.Duration("build-token-buffer-duration"), + WorkerAuthTokenDuration: c.Duration("worker-auth-token-duration"), + WorkerRegisterTokenDuration: c.Duration("worker-register-token-duration"), } return tm diff --git a/cmd/vela-server/validate.go b/cmd/vela-server/validate.go index 6b82e6b4e..bce8b12fb 100644 --- a/cmd/vela-server/validate.go +++ b/cmd/vela-server/validate.go @@ -52,10 +52,6 @@ func validateCore(c *cli.Context) error { return fmt.Errorf("clone-image (VELA_CLONE_IMAGE) flag is not properly configured") } - if len(c.String("vela-secret")) == 0 { - return fmt.Errorf("vela-secret (VELA_SECRET) flag is not properly configured") - } - if len(c.String("vela-server-private-key")) == 0 { return fmt.Errorf("vela-server-private-key (VELA_SERVER_PRIVATE_KEY) flag is not properly configured") } diff --git a/go.mod b/go.mod index 372f560f1..01e869534 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/drone/envsubst v1.0.3 github.com/gin-gonic/gin v1.9.0 github.com/go-playground/assert/v2 v2.2.0 - github.com/go-vela/types v0.18.1 + github.com/go-vela/types v0.18.2-0.20230321015315-6c723879639c github.com/golang-jwt/jwt/v4 v4.5.0 github.com/google/go-cmp v0.5.9 github.com/google/go-github/v50 v50.1.0 diff --git a/go.sum b/go.sum index 9e3ba8725..9b8e2d8a7 100644 --- a/go.sum +++ b/go.sum @@ -148,8 +148,8 @@ github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyh github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= -github.com/go-vela/types v0.18.1 h1:V/luHLnCEaJhD1m9PZCZicIasg8Op6MCK+utkz+gQiU= -github.com/go-vela/types v0.18.1/go.mod h1:6MzMhLaXKSZ9wiJveieqnBd2+4ZMS7yv7+POGSITyS8= +github.com/go-vela/types v0.18.2-0.20230321015315-6c723879639c h1:lnCL1knUGvgZQG4YBHSs/CZnxNBfqFUBlGhyq9LO9uk= +github.com/go-vela/types v0.18.2-0.20230321015315-6c723879639c/go.mod h1:6MzMhLaXKSZ9wiJveieqnBd2+4ZMS7yv7+POGSITyS8= github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA= github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= diff --git a/internal/token/manager.go b/internal/token/manager.go index 121e4fa0f..71d82fb49 100644 --- a/internal/token/manager.go +++ b/internal/token/manager.go @@ -25,4 +25,10 @@ type Manager struct { // BuildTokenBufferDuration specifies the additional token duration of build tokens beyond repo timeout BuildTokenBufferDuration time.Duration + + // WorkerAuthTokenDuration specifies the token duration for worker auth (check in) + WorkerAuthTokenDuration time.Duration + + // WorkerRegisterTokenDuration specifies the token duration for worker register + WorkerRegisterTokenDuration time.Duration } diff --git a/internal/token/mint.go b/internal/token/mint.go index e90c0c34f..6f1a23941 100644 --- a/internal/token/mint.go +++ b/internal/token/mint.go @@ -69,6 +69,13 @@ func (tm *Manager) MintToken(mto *MintTokenOpts) (string, error) { claims.Repo = mto.Repo claims.Subject = mto.Hostname + case constants.WorkerAuthTokenType, constants.WorkerRegisterTokenType: + if len(mto.Hostname) == 0 { + return "", fmt.Errorf("missing host name for %s token", mto.TokenType) + } + + claims.Subject = mto.Hostname + default: return "", errors.New("invalid token type") } diff --git a/mock/server/authentication.go b/mock/server/authentication.go index e75217565..ea7bb76a1 100644 --- a/mock/server/authentication.go +++ b/mock/server/authentication.go @@ -73,3 +73,17 @@ func getAuthenticateFromToken(c *gin.Context) { c.JSON(http.StatusOK, body) } + +// validateToken returns mock response for a http GET. +// +// Don't pass "Authorization" in header to receive an unauthorized error message. +func validateToken(c *gin.Context) { + err := "error" + + token := c.Request.Header.Get("Authorization") + if len(token) == 0 { + c.AbortWithStatusJSON(http.StatusUnauthorized, types.Error{Message: &err}) + } + + c.JSON(http.StatusOK, "vela-server") +} diff --git a/mock/server/server.go b/mock/server/server.go index 1b1ebd7c7..e13368711 100644 --- a/mock/server/server.go +++ b/mock/server/server.go @@ -37,6 +37,7 @@ func FakeHandler() http.Handler { e.PUT("/api/v1/admin/step", updateStep) e.GET("/api/v1/admin/users", getUsers) e.PUT("/api/v1/admin/user", updateUser) + e.POST("/api/v1/admin/workers/:worker/register-token", registerToken) // mock endpoints for build calls e.GET("/api/v1/repos/:org/:repo/builds/:build", getBuild) @@ -128,12 +129,14 @@ func FakeHandler() http.Handler { e.GET("/api/v1/workers/:worker", getWorker) e.POST("/api/v1/workers", addWorker) e.PUT("/api/v1/workers/:worker", updateWorker) + e.POST("/api/v1/workers/:worker/refresh", refreshWorkerAuth) e.DELETE("/api/v1/workers/:worker", removeWorker) // mock endpoints for authentication calls e.GET("/token-refresh", getTokenRefresh) e.GET("/authenticate", getAuthenticate) e.POST("/authenticate/token", getAuthenticateFromToken) + e.GET("/validate-token", validateToken) return e } diff --git a/mock/server/worker.go b/mock/server/worker.go index ed79507bb..51aedfa80 100644 --- a/mock/server/worker.go +++ b/mock/server/worker.go @@ -58,6 +58,23 @@ const ( "last_checked_in": 1602612590 } ]` + + // AddWorkerResp represents a JSON return for adding a worker. + AddWorkerResp = `{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ3b3JrZXIiLCJpYXQiOjE1MTYyMzkwMjIsInRva2VuX3R5cGUiOiJXb3JrZXJBdXRoIn0.qeULIimCJlrwsE0JykNpzBmMaHUbvfk0vkyAz2eEo38" + }` + + // RefreshWorkerAuthResp represents a JSON return for refreshing a worker's authentication. + RefreshWorkerAuthResp = `{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ3b3JrZXIiLCJpYXQiOjE1MTYyMzkwMjIsInRva2VuX3R5cGUiOiJXb3JrZXJBdXRoIn0.qeULIimCJlrwsE0JykNpzBmMaHUbvfk0vkyAz2eEo38" + }` + + // RegisterTokenResp represents a JSON return for an admin requesting a registration token. + // + //nolint:gosec // not actual credentials + RegisterTokenResp = `{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ3b3JrZXIiLCJpYXQiOjE1MTYyMzkwMjIsInRva2VuX3R5cGUiOiJXb3JrZXJSZWdpc3RlciJ9.gEzKaZB-sDd_gFCVF5uGo2mcf3iy9CrXDTLPZ6PTsTc" + }` ) // getWorkers returns mock JSON for a http GET. @@ -92,9 +109,9 @@ func getWorker(c *gin.Context) { // addWorker returns mock JSON for a http POST. func addWorker(c *gin.Context) { - data := []byte(WorkerResp) + data := []byte(AddWorkerResp) - var body library.Worker + var body library.Token _ = json.Unmarshal(data, &body) c.JSON(http.StatusCreated, body) @@ -122,6 +139,28 @@ func updateWorker(c *gin.Context) { c.JSON(http.StatusOK, body) } +// refreshWorkerAuth has a param :worker returns mock JSON for a http PUT. +// +// Pass "0" to :worker to test receiving a http 404 response. +func refreshWorkerAuth(c *gin.Context) { + w := c.Param("worker") + + if strings.EqualFold(w, "0") { + msg := fmt.Sprintf("Worker %s does not exist", w) + + c.AbortWithStatusJSON(http.StatusNotFound, types.Error{Message: &msg}) + + return + } + + data := []byte(RefreshWorkerAuthResp) + + var body library.Token + _ = json.Unmarshal(data, &body) + + c.JSON(http.StatusOK, body) +} + // removeWorker has a param :worker returns mock JSON for a http DELETE. // // Pass "0" to :worker to test receiving a http 404 response. @@ -138,3 +177,25 @@ func removeWorker(c *gin.Context) { c.JSON(http.StatusOK, fmt.Sprintf("Worker %s removed", w)) } + +// registerToken has a param :worker returns mock JSON for a http POST. +// +// Pass "0" to :worker to test receiving a http 401 response. +func registerToken(c *gin.Context) { + w := c.Param("worker") + + if strings.EqualFold(w, "0") { + msg := fmt.Sprintf("user %s is not a platform admin", w) + + c.AbortWithStatusJSON(http.StatusUnauthorized, types.Error{Message: &msg}) + + return + } + + data := []byte(RegisterTokenResp) + + var body library.Token + _ = json.Unmarshal(data, &body) + + c.JSON(http.StatusCreated, body) +} diff --git a/router/admin.go b/router/admin.go index f3dbb680f..f7fddf95f 100644 --- a/router/admin.go +++ b/router/admin.go @@ -53,5 +53,8 @@ func AdminHandlers(base *gin.RouterGroup) { // Admin user endpoint _admin.PUT("/user", admin.UpdateUser) + + // Admin worker endpoint + _admin.POST("/workers/:worker/register-token", admin.RegisterToken) } // end of admin endpoints } diff --git a/router/build.go b/router/build.go index 3abf130d9..c2e34e188 100644 --- a/router/build.go +++ b/router/build.go @@ -58,7 +58,7 @@ func BuildHandlers(base *gin.RouterGroup) { build.DELETE("", perm.MustPlatformAdmin(), api.DeleteBuild) build.DELETE("/cancel", executors.Establish(), perm.MustWrite(), api.CancelBuild) build.GET("/logs", perm.MustRead(), api.GetBuildLogs) - build.GET("/token", perm.MustWorker(), api.GetBuildToken) + build.GET("/token", perm.MustWorkerAuthToken(), api.GetBuildToken) // Service endpoints // * Log endpoints diff --git a/router/middleware/claims/claims.go b/router/middleware/claims/claims.go index bc0726c7a..5ba2784f6 100644 --- a/router/middleware/claims/claims.go +++ b/router/middleware/claims/claims.go @@ -24,8 +24,6 @@ func Retrieve(c *gin.Context) *token.Claims { // Establish sets the claims in the given context. func Establish() gin.HandlerFunc { return func(c *gin.Context) { - claims := new(token.Claims) - tm := c.MustGet("token-manager").(*token.Manager) // get the access token from the request at, err := auth.RetrieveAccessToken(c.Request) @@ -34,15 +32,18 @@ func Establish() gin.HandlerFunc { return } - // special handling for workers - secret := c.MustGet("secret").(string) - if strings.EqualFold(at, secret) { - claims.Subject = "vela-worker" - claims.TokenType = constants.ServerWorkerTokenType - ToContext(c, claims) - c.Next() + claims := new(token.Claims) - return + // special handling for workers if symmetric token is provided + if secret, ok := c.Value("secret").(string); ok { + if strings.EqualFold(at, secret) { + claims.Subject = "vela-worker" + claims.TokenType = constants.ServerWorkerTokenType + ToContext(c, claims) + c.Next() + + return + } } // parse and validate the token and return the associated the user diff --git a/router/middleware/claims/claims_test.go b/router/middleware/claims/claims_test.go index 239c97296..914c5882e 100644 --- a/router/middleware/claims/claims_test.go +++ b/router/middleware/claims/claims_test.go @@ -65,10 +65,12 @@ func TestClaims_Establish(t *testing.T) { user.SetFavorites([]string{}) tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, - UserAccessTokenDuration: time.Minute * 5, - UserRefreshTokenDuration: time.Minute * 30, + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + WorkerAuthTokenDuration: time.Minute * 20, + WorkerRegisterTokenDuration: time.Minute * 1, } now := time.Now() @@ -122,6 +124,42 @@ func TestClaims_Establish(t *testing.T) { CtxRequest: "/repos/foo/bar/builds/1", Endpoint: "repos/:org/:repo/builds/:build", }, + { + TokenType: constants.WorkerAuthTokenType, + WantClaims: &token.Claims{ + TokenType: constants.WorkerAuthTokenType, + RegisteredClaims: jwt.RegisteredClaims{ + Subject: "host", + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(now.Add(tm.WorkerAuthTokenDuration)), + }, + }, + Mto: &token.MintTokenOpts{ + Hostname: "host", + TokenDuration: tm.WorkerAuthTokenDuration, + TokenType: constants.WorkerAuthTokenType, + }, + CtxRequest: "/workers/host", + Endpoint: "/workers/:hostname", + }, + { + TokenType: constants.WorkerRegisterTokenType, + WantClaims: &token.Claims{ + TokenType: constants.WorkerRegisterTokenType, + RegisteredClaims: jwt.RegisteredClaims{ + Subject: "host", + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(now.Add(tm.WorkerRegisterTokenDuration)), + }, + }, + Mto: &token.MintTokenOpts{ + Hostname: "host", + TokenDuration: tm.WorkerRegisterTokenDuration, + TokenType: constants.WorkerRegisterTokenType, + }, + CtxRequest: "/workers/host/register", + Endpoint: "workers/:hostname/register", + }, { TokenType: constants.ServerWorkerTokenType, WantClaims: &token.Claims{ @@ -135,17 +173,6 @@ func TestClaims_Establish(t *testing.T) { }, } - // setup database - db, _ := sqlite.NewTest() - - defer func() { - db.Sqlite.Exec("delete from users;") - _sql, _ := db.Sqlite.DB() - _sql.Close() - }() - - _ = db.CreateUser(user) - got := new(token.Claims) gin.SetMode(gin.TestMode) @@ -160,6 +187,7 @@ func TestClaims_Establish(t *testing.T) { if strings.EqualFold(tt.TokenType, constants.ServerWorkerTokenType) { tkn = "very-secret" + engine.Use(func(c *gin.Context) { c.Set("secret", "very-secret") }) } else { tkn, _ = tm.MintToken(tt.Mto) } @@ -171,8 +199,6 @@ func TestClaims_Establish(t *testing.T) { // setup vela mock server engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) - engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) - engine.Use(func(c *gin.Context) { c.Set("secret", "very-secret") }) engine.Use(Establish()) engine.PUT(tt.Endpoint, func(c *gin.Context) { got = Retrieve(c) diff --git a/router/middleware/executors/executors.go b/router/middleware/executors/executors.go index 96155eacb..145d134fb 100644 --- a/router/middleware/executors/executors.go +++ b/router/middleware/executors/executors.go @@ -14,8 +14,10 @@ import ( "github.com/gin-gonic/gin" "github.com/go-vela/server/database" + "github.com/go-vela/server/internal/token" "github.com/go-vela/server/router/middleware/build" "github.com/go-vela/server/util" + "github.com/go-vela/types/constants" "github.com/go-vela/types/library" ) @@ -51,8 +53,26 @@ func Establish() gin.HandlerFunc { return } - // add the token to authenticate to the worker as a header - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.MustGet("secret").(string))) + tm := c.MustGet("token-manager").(*token.Manager) + + // set mint token options + mto := &token.MintTokenOpts{ + Hostname: "vela-server", + TokenType: constants.WorkerAuthTokenType, + TokenDuration: time.Minute * 1, + } + + // mint token + tkn, err := tm.MintToken(mto) + if err != nil { + retErr := fmt.Errorf("unable to generate auth token: %w", err) + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // add the token to authenticate to the worker + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tkn)) // make the request to the worker and check the response resp, err := client.Do(req) diff --git a/router/middleware/perm/perm.go b/router/middleware/perm/perm.go index 1cc6e397d..812c9a8fd 100644 --- a/router/middleware/perm/perm.go +++ b/router/middleware/perm/perm.go @@ -55,34 +55,65 @@ func MustPlatformAdmin() gin.HandlerFunc { } } -// MustWorker ensures the request is coming from an agent. -func MustWorker() gin.HandlerFunc { +// MustWorkerRegisterToken ensures the token is a registration token retrieved by a platform admin. +func MustWorkerRegisterToken() gin.HandlerFunc { return func(c *gin.Context) { cl := claims.Retrieve(c) - // global permissions bypass - if cl.IsAdmin { - logrus.WithFields(logrus.Fields{ - "user": cl.Subject, - }).Debugf("user %s has platform admin permissions", cl.Subject) + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "user": cl.Subject, + }).Debugf("verifying user %s has a registration token for worker", cl.Subject) + + switch cl.TokenType { + case constants.WorkerRegisterTokenType: + return + case constants.ServerWorkerTokenType: + if strings.EqualFold(cl.Subject, "vela-worker") { + return + } + + retErr := fmt.Errorf("server-worker token provided but does not match configuration") + util.HandleError(c, http.StatusBadRequest, retErr) + + return + default: + retErr := fmt.Errorf("invalid token type: must provide a worker registration token") + util.HandleError(c, http.StatusUnauthorized, retErr) return } + } +} + +// MustWorkerAuthToken ensures the token is a worker auth token. +func MustWorkerAuthToken() gin.HandlerFunc { + return func(c *gin.Context) { + cl := claims.Retrieve(c) // update engine logger with API metadata // // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields logrus.WithFields(logrus.Fields{ - "subject": cl.Subject, - }).Debugf("verifying user %s is a worker", cl.Subject) + "worker": cl.Subject, + }).Debugf("verifying worker %s has a valid auth token", cl.Subject) - // validate claims as worker - switch { - case strings.EqualFold(cl.Subject, "vela-worker") && strings.EqualFold(cl.TokenType, constants.ServerWorkerTokenType): + switch cl.TokenType { + case constants.WorkerAuthTokenType, constants.WorkerRegisterTokenType: return + case constants.ServerWorkerTokenType: + if strings.EqualFold(cl.Subject, "vela-worker") { + return + } + + retErr := fmt.Errorf("server-worker token provided but does not match configuration") + util.HandleError(c, http.StatusBadRequest, retErr) + return default: - retErr := fmt.Errorf("user %s is not a worker", cl.Subject) + retErr := fmt.Errorf("invalid token type: must provide a worker auth token") util.HandleError(c, http.StatusUnauthorized, retErr) return diff --git a/router/middleware/perm/perm_test.go b/router/middleware/perm/perm_test.go index 93f4ddff9..61ebf596e 100644 --- a/router/middleware/perm/perm_test.go +++ b/router/middleware/perm/perm_test.go @@ -188,17 +188,25 @@ func TestPerm_MustPlatformAdmin_NotAdmin(t *testing.T) { } } -func TestPerm_MustWorker(t *testing.T) { +func TestPerm_MustWorkerRegisterToken(t *testing.T) { // setup types - secret := "superSecret" - tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, - UserAccessTokenDuration: time.Minute * 5, - UserRefreshTokenDuration: time.Minute * 30, + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + WorkerRegisterTokenDuration: time.Minute * 1, + WorkerAuthTokenDuration: time.Minute * 15, + } + + mto := &token.MintTokenOpts{ + Hostname: "worker", + TokenDuration: tm.WorkerRegisterTokenDuration, + TokenType: constants.WorkerRegisterTokenType, } + tok, _ := tm.MintToken(mto) + // setup context gin.SetMode(gin.TestMode) @@ -206,14 +214,13 @@ func TestPerm_MustWorker(t *testing.T) { context, engine := gin.CreateTestContext(resp) context.Request, _ = http.NewRequest(http.MethodGet, "/test/foo/bar", nil) - context.Request.Header.Add("Authorization", fmt.Sprint(secret)) + context.Request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tok)) // setup vela mock server - engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) engine.Use(claims.Establish()) engine.Use(user.Establish()) - engine.Use(MustWorker()) + engine.Use(MustWorkerRegisterToken()) engine.GET("/test/:org/:repo", func(c *gin.Context) { c.Status(http.StatusOK) }) @@ -225,14 +232,11 @@ func TestPerm_MustWorker(t *testing.T) { engine.ServeHTTP(context.Writer, context.Request) if resp.Code != http.StatusOK { - t.Errorf("MustWorker returned %v, want %v", resp.Code, http.StatusOK) + t.Errorf("MustWorkerRegisterToken returned %v, want %v", resp.Code, http.StatusOK) } } -func TestPerm_MustWorker_PlatAdmin(t *testing.T) { - // setup types - secret := "superSecret" - +func TestPerm_MustWorkerRegisterToken_PlatAdmin(t *testing.T) { tm := &token.Manager{ PrivateKey: "123abc", SignMethod: jwt.SigningMethodHS256, @@ -276,12 +280,11 @@ func TestPerm_MustWorker_PlatAdmin(t *testing.T) { context.Request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tok)) // setup vela mock server - engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) engine.Use(claims.Establish()) engine.Use(user.Establish()) - engine.Use(MustWorker()) + engine.Use(MustWorkerRegisterToken()) engine.GET("/test/:org/:repo", func(c *gin.Context) { c.Status(http.StatusOK) }) @@ -292,33 +295,26 @@ func TestPerm_MustWorker_PlatAdmin(t *testing.T) { // run test engine.ServeHTTP(context.Writer, context.Request) - if resp.Code != http.StatusOK { - t.Errorf("MustWorker returned %v, want %v", resp.Code, http.StatusOK) + if resp.Code != http.StatusUnauthorized { + t.Errorf("MustWorkerRegisterToken returned %v, want %v", resp.Code, http.StatusUnauthorized) } } -func TestPerm_MustWorker_UserNamedVelaWorker(t *testing.T) { +func TestPerm_MustWorkerAuthToken(t *testing.T) { // setup types - secret := "superSecret" - tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, - UserAccessTokenDuration: time.Minute * 5, - UserRefreshTokenDuration: time.Minute * 30, + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + WorkerRegisterTokenDuration: time.Minute * 1, + WorkerAuthTokenDuration: time.Minute * 15, } - u := new(library.User) - u.SetID(1) - u.SetName("vela-worker") - u.SetToken("bar") - u.SetHash("baz") - u.SetAdmin(false) - mto := &token.MintTokenOpts{ - User: u, - TokenDuration: tm.UserAccessTokenDuration, - TokenType: constants.UserAccessTokenType, + Hostname: "worker", + TokenDuration: tm.WorkerAuthTokenDuration, + TokenType: constants.WorkerAuthTokenType, } tok, _ := tm.MintToken(mto) @@ -329,27 +325,56 @@ func TestPerm_MustWorker_UserNamedVelaWorker(t *testing.T) { resp := httptest.NewRecorder() context, engine := gin.CreateTestContext(resp) - // setup database - db, _ := sqlite.NewTest() + context.Request, _ = http.NewRequest(http.MethodGet, "/test/foo/bar", nil) + context.Request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tok)) - defer func() { - db.Sqlite.Exec("delete from users;") - _sql, _ := db.Sqlite.DB() - _sql.Close() - }() + // setup vela mock server + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) + engine.Use(claims.Establish()) + engine.Use(user.Establish()) + engine.Use(MustWorkerAuthToken()) + engine.GET("/test/:org/:repo", func(c *gin.Context) { + c.Status(http.StatusOK) + }) - _ = db.CreateUser(u) + s1 := httptest.NewServer(engine) + defer s1.Close() + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusOK { + t.Errorf("MustWorkerAuthToken returned %v, want %v", resp.Code, http.StatusOK) + } +} + +func TestPerm_MustWorkerAuth_ServerWorkerToken(t *testing.T) { + // setup types + secret := "superSecret" + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + WorkerRegisterTokenDuration: time.Minute * 1, + WorkerAuthTokenDuration: time.Minute * 15, + } + + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) context.Request, _ = http.NewRequest(http.MethodGet, "/test/foo/bar", nil) - context.Request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tok)) + context.Request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", secret)) // setup vela mock server engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) - engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) engine.Use(claims.Establish()) engine.Use(user.Establish()) - engine.Use(MustWorker()) + engine.Use(MustWorkerAuthToken()) engine.GET("/test/:org/:repo", func(c *gin.Context) { c.Status(http.StatusOK) }) @@ -360,8 +385,8 @@ func TestPerm_MustWorker_UserNamedVelaWorker(t *testing.T) { // run test engine.ServeHTTP(context.Writer, context.Request) - if resp.Code != http.StatusUnauthorized { - t.Errorf("MustWorker returned %v, want %v", resp.Code, http.StatusUnauthorized) + if resp.Code != http.StatusOK { + t.Errorf("MustWorkerAuthToken returned %v, want %v", resp.Code, http.StatusOK) } } diff --git a/router/router.go b/router/router.go index 3327e4a7b..f9d1f7b24 100644 --- a/router/router.go +++ b/router/router.go @@ -77,6 +77,9 @@ func Load(options ...gin.HandlerFunc) *gin.Engine { // Metric endpoint r.GET("/metrics", api.CustomMetrics, gin.WrapH(api.BaseMetrics())) + // Validate Server Token endpoint + r.GET("/validate-token", claims.Establish(), api.ValidateServerToken) + // Version endpoint r.GET("/version", api.Version) diff --git a/router/worker.go b/router/worker.go index 7853c2a82..100a24975 100644 --- a/router/worker.go +++ b/router/worker.go @@ -19,19 +19,21 @@ import ( // GET /api/v1/workers // GET /api/v1/workers/:worker // PUT /api/v1/workers/:worker +// POST /api/v1/workers/:worker/refresh // DELETE /api/v1/workers/:worker . func WorkerHandlers(base *gin.RouterGroup) { // Workers endpoints workers := base.Group("/workers") { - workers.POST("", perm.MustWorker(), middleware.Payload(), api.CreateWorker) + workers.POST("", perm.MustWorkerRegisterToken(), middleware.Payload(), api.CreateWorker) workers.GET("", api.GetWorkers) // Worker endpoints w := workers.Group("/:worker") { w.GET("", worker.Establish(), api.GetWorker) - w.PUT("", perm.MustWorker(), worker.Establish(), api.UpdateWorker) + w.PUT("", perm.MustPlatformAdmin(), worker.Establish(), api.UpdateWorker) + w.POST("/refresh", perm.MustWorkerAuthToken(), worker.Establish(), api.RefreshWorkerAuth) w.DELETE("", perm.MustPlatformAdmin(), worker.Establish(), api.DeleteWorker) } // end of worker endpoints } // end of workers endpoints