diff --git a/api/admin/rotate_keys.go b/api/admin/rotate_keys.go new file mode 100644 index 000000000..56681a60c --- /dev/null +++ b/api/admin/rotate_keys.go @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 + +package admin + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" + + "github.com/go-vela/server/database" + "github.com/go-vela/server/util" +) + +// swagger:operation POST /api/v1/admin/rotate_oidc_keys admin AdminRotateOIDCKeys +// +// Rotate RSA Keys +// +// --- +// produces: +// - application/json +// parameters: +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully rotated OIDC provider keys +// schema: +// type: string +// '500': +// description: Error unable to rotate OIDC provider keys +// schema: +// "$ref": "#/definitions/Error" + +// RotateOIDCKeys represents the API handler to +// rotate RSA keys in OIDC provider service. +func RotateOIDCKeys(c *gin.Context) { + logrus.Info("Admin: rotating keys for OIDC provider") + + // capture middleware values + ctx := c.Request.Context() + + err := database.FromContext(c).RotateKeys(ctx) + if err != nil { + retErr := fmt.Errorf("unable to rotate keys: %w", err) + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, "keys rotated successfully") +} diff --git a/api/build/id_request_token.go b/api/build/id_request_token.go new file mode 100644 index 000000000..f5e3c9f93 --- /dev/null +++ b/api/build/id_request_token.go @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: Apache-2.0 + +package build + +import ( + "errors" + "fmt" + "net/http" + "strconv" + "time" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" + + "github.com/go-vela/server/constants" + "github.com/go-vela/server/internal/token" + "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/claims" + "github.com/go-vela/server/util" + "github.com/go-vela/types/library" +) + +// swagger:operation GET /api/v1/repos/{org}/{repo}/builds/{build}/id_request_token builds GetIDRequestToken +// +// Get a Vela OIDC request token associated with a build +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: build +// description: Build number +// required: true +// type: integer +// - in: query +// name: image +// description: Add image to token claims +// type: string +// - in: query +// name: request +// description: Add request input to token claims +// type: string +// - in: query +// name: commands +// description: Add commands input to token claims +// type: boolean +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved ID Request token +// schema: +// "$ref": "#/definitions/Token" +// '400': +// description: Bad request +// schema: +// "$ref": "#/definitions/Error" +// '401': +// description: Unauthorized request +// schema: +// "$ref": "#/definitions/Error" +// '404': +// description: Unable to find build +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to generate ID request token +// schema: +// "$ref": "#/definitions/Error" + +// GetIDRequestToken represents the API handler to generate and return an ID request token. +func GetIDRequestToken(c *gin.Context) { + // capture middleware values + b := build.Retrieve(c) + 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{ + "build": b.GetNumber(), + "org": b.GetRepo().GetOrg(), + "repo": b.GetRepo().GetName(), + "user": cl.Subject, + }).Infof("generating ID request token for build %s/%d", b.GetRepo().GetFullName(), b.GetNumber()) + + image := c.Query("image") + if len(image) == 0 { + retErr := errors.New("no step 'image' provided in query parameters") + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + request := c.Query("request") + if len(request) == 0 { + retErr := errors.New("no 'request' provided in query parameters") + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + commands, err := strconv.ParseBool(c.Query("commands")) + if err != nil { + retErr := fmt.Errorf("unable to parse 'commands' query parameter as boolean %s: %w", c.Query("commands"), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // retrieve token manager from context + tm := c.MustGet("token-manager").(*token.Manager) + + exp := (time.Duration(b.GetRepo().GetTimeout()) * time.Minute) + tm.BuildTokenBufferDuration + + // set mint token options + idmto := &token.MintTokenOpts{ + Build: b, + Repo: b.GetRepo().GetFullName(), + TokenType: constants.IDRequestTokenType, + TokenDuration: exp, + Image: util.Sanitize(image), + Request: util.Sanitize(request), + Commands: commands, + } + + // mint token + idrt, err := tm.MintToken(idmto) + if err != nil { + retErr := fmt.Errorf("unable to generate ID request token: %w", err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, library.Token{Token: &idrt}) +} diff --git a/api/build/id_token.go b/api/build/id_token.go new file mode 100644 index 000000000..a2b7fdab7 --- /dev/null +++ b/api/build/id_token.go @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: Apache-2.0 + +package build + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" + + "github.com/go-vela/server/constants" + "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/router/middleware/claims" + "github.com/go-vela/server/util" + "github.com/go-vela/types/library" +) + +// swagger:operation GET /api/v1/repos/{org}/{repo}/builds/{build}/id_token builds GetIDToken +// +// Get a Vela OIDC token associated with a build +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: build +// description: Build number +// required: true +// type: integer +// - in: query +// name: audience +// description: Add audience to token claims +// type: array +// items: +// type: string +// collectionFormat: multi +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved ID token +// schema: +// "$ref": "#/definitions/Token" +// '400': +// description: Bad request +// schema: +// "$ref": "#/definitions/Error" +// '401': +// description: Unauthorized request +// schema: +// "$ref": "#/definitions/Error" +// '404': +// description: Unable to find build +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to generate id token +// schema: +// "$ref": "#/definitions/Error" + +// GetIDToken represents the API handler to generate a id token. +func GetIDToken(c *gin.Context) { + // capture middleware values + b := build.Retrieve(c) + cl := claims.Retrieve(c) + ctx := c.Request.Context() + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "build": b.GetNumber(), + "org": b.GetRepo().GetOrg(), + "repo": b.GetRepo().GetName(), + "subject": cl.Subject, + }).Infof("generating ID token for build %s/%d", b.GetRepo().GetFullName(), b.GetNumber()) + + // retrieve token manager from context + tm := c.MustGet("token-manager").(*token.Manager) + + // set mint token options + idmto := &token.MintTokenOpts{ + Build: b, + Repo: b.GetRepo().GetFullName(), + TokenType: constants.IDTokenType, + TokenDuration: tm.IDTokenDuration, + Image: cl.Image, + Request: cl.Request, + Commands: cl.Commands, + } + + // if audience is provided, include that in claims + audience := []string{} + + if len(c.QueryArray("audience")) > 0 { + for _, a := range c.QueryArray("audience") { + if len(a) > 0 { + audience = append(audience, util.Sanitize(a)) + } + } + } + + if len(audience) == 0 { + retErr := fmt.Errorf("unable to generate ID token: %s", "no audience provided") + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + idmto.Audience = audience + + // mint token + idt, err := tm.MintIDToken(ctx, idmto, database.FromContext(c)) + if err != nil { + retErr := fmt.Errorf("unable to generate ID token: %w", err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, library.Token{Token: &idt}) +} diff --git a/api/build/token.go b/api/build/token.go index 4c6de18ec..fd0664684 100644 --- a/api/build/token.go +++ b/api/build/token.go @@ -99,7 +99,7 @@ func GetBuildToken(c *gin.Context) { // set mint token options bmto := &token.MintTokenOpts{ Hostname: cl.Subject, - BuildID: b.GetID(), + Build: b, Repo: r.GetFullName(), TokenType: constants.WorkerBuildTokenType, TokenDuration: exp, diff --git a/api/jwks.go b/api/jwks.go new file mode 100644 index 000000000..3d124f77d --- /dev/null +++ b/api/jwks.go @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 + +package api + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + + "github.com/go-vela/server/database" + "github.com/go-vela/server/util" +) + +// swagger:operation GET /_services/token/.well-known/jwks token GetJWKS +// +// Get the JWKS for the Vela OIDC service +// +// --- +// produces: +// - application/json +// parameters: +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved the Vela JWKS +// schema: +// "$ref": "#/definitions/JWKSet" +// '500': +// description: Unable to get the Vela JWKS +// schema: +// "$ref": "#/definitions/Error" + +// GetJWKS represents the API handler for requests to public keys in the Vela OpenID service. +func GetJWKS(c *gin.Context) { + // retrieve JWKs from the database + keys, err := database.FromContext(c).ListJWKs(c) + if err != nil { + retErr := fmt.Errorf("unable to get key set: %w", err) + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, keys) +} diff --git a/api/oi_config.go b/api/oi_config.go new file mode 100644 index 000000000..02edeb939 --- /dev/null +++ b/api/oi_config.go @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 + +package api + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" + + "github.com/go-vela/server/api/types" + "github.com/go-vela/server/internal" +) + +// swagger:operation GET /_services/token/.well-known/openid-configuration token GetOpenIDConfig +// +// Get the OpenID configuration for the Vela OIDC service +// +// --- +// produces: +// - application/json +// parameters: +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved the Vela OpenID Configuration +// schema: +// "$ref": "#/definitions/OpenIDConfig" + +// GetOpenIDConfig represents the API handler for requests for configurations in the Vela OpenID service. +func GetOpenIDConfig(c *gin.Context) { + m := c.MustGet("metadata").(*internal.Metadata) + config := types.OpenIDConfig{ + Issuer: fmt.Sprintf("%s/_services/token", m.Vela.Address), + JWKSAddress: fmt.Sprintf("%s/%s", m.Vela.Address, "_services/token/.well-known/jwks"), + SupportedClaims: []string{ + "sub", + "exp", + "iat", + "iss", + "aud", + "build_number", + "build_id", + "repo", + "token_type", + "actor", + "actor_scm_id", + "commands", + "image", + "request", + "event", + "sha", + "ref", + }, + Algorithms: []string{ + jwt.SigningMethodRS256.Name, + }, + } + + c.JSON(http.StatusOK, config) +} diff --git a/api/types/oidc.go b/api/types/oidc.go new file mode 100644 index 000000000..7598351f6 --- /dev/null +++ b/api/types/oidc.go @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + "github.com/golang-jwt/jwt/v5" + "github.com/lestrrat-go/jwx/jwk" +) + +// OpenIDConfig is a struct that represents the OpenID Connect configuration. +// +// swagger:model OpenIDConfig +type OpenIDConfig struct { + Issuer string `json:"issuer"` + JWKSAddress string `json:"jwks_uri"` + SupportedClaims []string `json:"supported_claims"` + Algorithms []string `json:"id_token_signing_alg_values_supported"` +} + +// OpenIDClaims struct is an extension of the JWT standard claims. It +// includes information relevant to OIDC services. +type OpenIDClaims struct { + BuildNumber int `json:"build_number,omitempty"` + BuildID int64 `json:"build_id,omitempty"` + Actor string `json:"actor,omitempty"` + ActorSCMID string `json:"actor_scm_id,omitempty"` + Repo string `json:"repo,omitempty"` + TokenType string `json:"token_type,omitempty"` + Image string `json:"image,omitempty"` + Request string `json:"request,omitempty"` + Commands bool `json:"commands,omitempty"` + Event string `json:"event,omitempty"` + Ref string `json:"ref,omitempty"` + SHA string `json:"sha,omitempty"` + jwt.RegisteredClaims +} + +// JWKSet is a wrapper of lestrrat-go/jwx/jwk.Set for API Swagger gen. +// +// swagger:model JWKSet +type JWKSet struct { + jwk.Set +} diff --git a/cmd/vela-server/main.go b/cmd/vela-server/main.go index 7fa51ca27..7dd589f07 100644 --- a/cmd/vela-server/main.go +++ b/cmd/vela-server/main.go @@ -181,6 +181,12 @@ func main() { Usage: "sets the duration of the worker register token", Value: 1 * time.Minute, }, + &cli.DurationFlag{ + EnvVars: []string{"VELA_OPEN_ID_TOKEN_DURATION", "OPEN_ID_TOKEN_DURATION"}, + Name: "id-token-duration", + Usage: "sets the duration of an OpenID token requested during a build (should be short)", + Value: 5 * time.Minute, + }, // Compiler Flags &cli.BoolFlag{ EnvVars: []string{"VELA_COMPILER_GITHUB", "COMPILER_GITHUB"}, diff --git a/cmd/vela-server/server.go b/cmd/vela-server/server.go index dd5b6d6c6..9472e9c54 100644 --- a/cmd/vela-server/server.go +++ b/cmd/vela-server/server.go @@ -103,6 +103,11 @@ func server(c *cli.Context) error { return err } + tm, err := setupTokenManager(c, database) + if err != nil { + return err + } + jitter := wait.Jitter(5*time.Second, 2.0) logrus.Infof("retrieving initial platform settings after %v delay", jitter) @@ -154,7 +159,7 @@ func server(c *cli.Context) error { middleware.Database(database), middleware.Logger(logrus.StandardLogger(), time.RFC3339), middleware.Metadata(metadata), - middleware.TokenManager(setupTokenManager(c)), + middleware.TokenManager(tm), middleware.Queue(queue), middleware.RequestVersion, middleware.Secret(c.String("vela-secret")), diff --git a/cmd/vela-server/token.go b/cmd/vela-server/token.go index 04a756fca..525ce1518 100644 --- a/cmd/vela-server/token.go +++ b/cmd/vela-server/token.go @@ -3,26 +3,35 @@ package main import ( - "github.com/golang-jwt/jwt/v5" + "fmt" + "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" + "github.com/go-vela/server/database" "github.com/go-vela/server/internal/token" ) // helper function to setup the tokenmanager from the CLI arguments. -func setupTokenManager(c *cli.Context) *token.Manager { +func setupTokenManager(c *cli.Context, db database.Interface) (*token.Manager, error) { logrus.Debug("Creating token manager from CLI configuration") tm := &token.Manager{ - PrivateKey: c.String("vela-server-private-key"), - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: c.String("vela-server-private-key"), 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"), + IDTokenDuration: c.Duration("id-token-duration"), + Issuer: fmt.Sprintf("%s/_services/token", c.String("server-addr")), + } + + // generate a new RSA key pair + err := tm.GenerateRSA(c.Context, db) + if err != nil { + return nil, err } - return tm + return tm, nil } diff --git a/compiler/native/environment.go b/compiler/native/environment.go index 1d0666e43..a2542fb3f 100644 --- a/compiler/native/environment.go +++ b/compiler/native/environment.go @@ -315,6 +315,7 @@ func environment(b *api.Build, m *internal.Metadata, r *api.Repo, u *api.User) m env["VELA_NETRC_MACHINE"] = m.Source.Host env["VELA_QUEUE"] = m.Queue.Driver env["VELA_SOURCE"] = m.Source.Driver + env["VELA_ID_TOKEN_REQUEST_URL"] = fmt.Sprintf("%s/api/v1/repos/%s/builds/%d/id_token", m.Vela.Address, r.GetFullName(), b.GetNumber()) channel = m.Queue.Channel workspace = fmt.Sprintf("%s/%s/%s/%s", workspace, m.Source.Host, r.GetOrg(), r.GetName()) } diff --git a/compiler/native/environment_test.go b/compiler/native/environment_test.go index 6d920021a..cb78fbd99 100644 --- a/compiler/native/environment_test.go +++ b/compiler/native/environment_test.go @@ -596,7 +596,7 @@ func TestNative_environment(t *testing.T) { m: &internal.Metadata{Database: &internal.Database{Driver: str, Host: str}, Queue: &internal.Queue{Channel: str, Driver: str, Host: str}, Source: &internal.Source{Driver: str, Host: str}, Vela: &internal.Vela{Address: str, WebAddress: str}}, r: &api.Repo{ID: &num64, Owner: &api.User{ID: &num64, Name: &str, Token: &str, Active: &booL, Admin: &booL}, Org: &str, Name: &str, FullName: &str, Link: &str, Clone: &str, Branch: &str, Topics: &topics, BuildLimit: &num64, Timeout: &num64, Visibility: &str, Private: &booL, Trusted: &booL, Active: &booL}, u: &api.User{ID: &num64, Name: &str, Token: &str, Active: &booL, Admin: &booL}, - want: map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "push", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_REF": "foo", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "true", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_EVENTS": "", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_APPROVED_AT": "0", "VELA_BUILD_APPROVED_BY": "", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "push", "VELA_BUILD_EVENT_ACTION": "", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_REF": "foo", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SENDER_SCM_ID": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_EVENTS": "", "VELA_REPO_APPROVE_BUILD": "", "VELA_REPO_BRANCH": "foo", "VELA_REPO_TOPICS": "cloud,security", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_OWNER": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo"}, + want: map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "push", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_REF": "foo", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "true", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_EVENTS": "", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_APPROVED_AT": "0", "VELA_BUILD_APPROVED_BY": "", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "push", "VELA_BUILD_EVENT_ACTION": "", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_REF": "foo", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SENDER_SCM_ID": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_EVENTS": "", "VELA_REPO_APPROVE_BUILD": "", "VELA_REPO_BRANCH": "foo", "VELA_REPO_TOPICS": "cloud,security", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_OWNER": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_ID_TOKEN_REQUEST_URL": "foo/api/v1/repos/foo/builds/1/id_token"}, }, // tag { @@ -605,7 +605,7 @@ func TestNative_environment(t *testing.T) { m: &internal.Metadata{Database: &internal.Database{Driver: str, Host: str}, Queue: &internal.Queue{Channel: str, Driver: str, Host: str}, Source: &internal.Source{Driver: str, Host: str}, Vela: &internal.Vela{Address: str, WebAddress: str}}, r: &api.Repo{ID: &num64, Owner: &api.User{ID: &num64, Name: &str, Token: &str, Active: &booL, Admin: &booL}, Org: &str, Name: &str, FullName: &str, Link: &str, Clone: &str, Branch: &str, Topics: &topics, BuildLimit: &num64, Timeout: &num64, Visibility: &str, Private: &booL, Trusted: &booL, Active: &booL}, u: &api.User{ID: &num64, Name: &str, Token: &str, Active: &booL, Admin: &booL}, - want: map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "tag", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_REF": "refs/tags/1", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TAG": "1", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "true", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_EVENTS": "", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_APPROVED_AT": "0", "VELA_BUILD_APPROVED_BY": "", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "tag", "VELA_BUILD_EVENT_ACTION": "", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_REF": "refs/tags/1", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SENDER_SCM_ID": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TAG": "1", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_EVENTS": "", "VELA_REPO_APPROVE_BUILD": "", "VELA_REPO_BRANCH": "foo", "VELA_REPO_TOPICS": "cloud,security", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_OWNER": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo"}, + want: map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "tag", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_REF": "refs/tags/1", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TAG": "1", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "true", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_EVENTS": "", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_APPROVED_AT": "0", "VELA_BUILD_APPROVED_BY": "", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "tag", "VELA_BUILD_EVENT_ACTION": "", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_REF": "refs/tags/1", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SENDER_SCM_ID": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TAG": "1", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_EVENTS": "", "VELA_REPO_APPROVE_BUILD": "", "VELA_REPO_BRANCH": "foo", "VELA_REPO_TOPICS": "cloud,security", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_OWNER": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_ID_TOKEN_REQUEST_URL": "foo/api/v1/repos/foo/builds/1/id_token"}, }, // pull_request { @@ -614,7 +614,7 @@ func TestNative_environment(t *testing.T) { m: &internal.Metadata{Database: &internal.Database{Driver: str, Host: str}, Queue: &internal.Queue{Channel: str, Driver: str, Host: str}, Source: &internal.Source{Driver: str, Host: str}, Vela: &internal.Vela{Address: str, WebAddress: str}}, r: &api.Repo{ID: &num64, Owner: &api.User{ID: &num64, Name: &str, Token: &str, Active: &booL, Admin: &booL}, Org: &str, Name: &str, FullName: &str, Link: &str, Clone: &str, Branch: &str, Topics: &topics, BuildLimit: &num64, Timeout: &num64, Visibility: &str, Private: &booL, Trusted: &booL, Active: &booL}, u: &api.User{ID: &num64, Name: &str, Token: &str, Active: &booL, Admin: &booL}, - want: map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "pull_request", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_PULL_REQUEST_NUMBER": "1", "BUILD_REF": "refs/pull/1/head", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "true", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_EVENTS": "", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_APPROVED_AT": "0", "VELA_BUILD_APPROVED_BY": "", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "pull_request", "VELA_BUILD_EVENT_ACTION": "opened", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_PULL_REQUEST": "1", "VELA_BUILD_REF": "refs/pull/1/head", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SENDER_SCM_ID": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_PULL_REQUEST": "1", "VELA_PULL_REQUEST_SOURCE": "", "VELA_PULL_REQUEST_TARGET": "foo", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_EVENTS": "", "VELA_REPO_APPROVE_BUILD": "", "VELA_REPO_BRANCH": "foo", "VELA_REPO_TOPICS": "cloud,security", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_OWNER": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo"}, + want: map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "pull_request", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_PULL_REQUEST_NUMBER": "1", "BUILD_REF": "refs/pull/1/head", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "true", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_EVENTS": "", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_APPROVED_AT": "0", "VELA_BUILD_APPROVED_BY": "", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "pull_request", "VELA_BUILD_EVENT_ACTION": "opened", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_PULL_REQUEST": "1", "VELA_BUILD_REF": "refs/pull/1/head", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SENDER_SCM_ID": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_PULL_REQUEST": "1", "VELA_PULL_REQUEST_SOURCE": "", "VELA_PULL_REQUEST_TARGET": "foo", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_EVENTS": "", "VELA_REPO_APPROVE_BUILD": "", "VELA_REPO_BRANCH": "foo", "VELA_REPO_TOPICS": "cloud,security", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_OWNER": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_ID_TOKEN_REQUEST_URL": "foo/api/v1/repos/foo/builds/1/id_token"}, }, // deployment { @@ -623,7 +623,7 @@ func TestNative_environment(t *testing.T) { m: &internal.Metadata{Database: &internal.Database{Driver: str, Host: str}, Queue: &internal.Queue{Channel: str, Driver: str, Host: str}, Source: &internal.Source{Driver: str, Host: str}, Vela: &internal.Vela{Address: str, WebAddress: str}}, r: &api.Repo{ID: &num64, Owner: &api.User{ID: &num64, Name: &str, Token: &str, Active: &booL, Admin: &booL}, Org: &str, Name: &str, FullName: &str, Link: &str, Clone: &str, Branch: &str, Topics: &topics, BuildLimit: &num64, Timeout: &num64, Visibility: &str, Private: &booL, Trusted: &booL, Active: &booL}, u: &api.User{ID: &num64, Name: &str, Token: &str, Active: &booL, Admin: &booL}, - want: map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "deployment", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_REF": "refs/pull/1/head", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TARGET": "production", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "true", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_EVENTS": "", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_APPROVED_AT": "0", "VELA_BUILD_APPROVED_BY": "", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "deployment", "VELA_BUILD_EVENT_ACTION": "", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_REF": "refs/pull/1/head", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SENDER_SCM_ID": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TARGET": "production", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DEPLOYMENT": "production", "VELA_DEPLOYMENT_NUMBER": "0", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_EVENTS": "", "VELA_REPO_APPROVE_BUILD": "", "VELA_REPO_BRANCH": "foo", "VELA_REPO_TOPICS": "cloud,security", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_OWNER": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo"}, + want: map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "deployment", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_REF": "refs/pull/1/head", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TARGET": "production", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "true", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_EVENTS": "", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_APPROVED_AT": "0", "VELA_BUILD_APPROVED_BY": "", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "deployment", "VELA_BUILD_EVENT_ACTION": "", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_REF": "refs/pull/1/head", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SENDER_SCM_ID": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TARGET": "production", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DEPLOYMENT": "production", "VELA_DEPLOYMENT_NUMBER": "0", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_EVENTS": "", "VELA_REPO_APPROVE_BUILD": "", "VELA_REPO_BRANCH": "foo", "VELA_REPO_TOPICS": "cloud,security", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_OWNER": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_ID_TOKEN_REQUEST_URL": "foo/api/v1/repos/foo/builds/1/id_token"}, }, } @@ -712,27 +712,27 @@ func Test_client_EnvironmentBuild(t *testing.T) { metadata: &internal.Metadata{Database: &internal.Database{Driver: str, Host: str}, Queue: &internal.Queue{Channel: str, Driver: str, Host: str}, Source: &internal.Source{Driver: str, Host: str}, Vela: &internal.Vela{Address: str, WebAddress: str}}, repo: &api.Repo{ID: &num64, Owner: &api.User{ID: &num64, Name: &str, Token: &str, Active: &booL, Admin: &booL}, Org: &str, Name: &str, FullName: &str, Link: &str, Clone: &str, Branch: &str, Topics: &topics, BuildLimit: &num64, Timeout: &num64, Visibility: &str, Private: &booL, Trusted: &booL, Active: &booL}, user: &api.User{ID: &num64, Name: &str, Token: &str, Active: &booL, Admin: &booL}, - }, map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "push", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_REF": "foo", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "true", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_EVENTS": "", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_APPROVED_AT": "0", "VELA_BUILD_APPROVED_BY": "", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "push", "VELA_BUILD_EVENT_ACTION": "", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_REF": "foo", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SENDER_SCM_ID": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_EVENTS": "", "VELA_REPO_APPROVE_BUILD": "", "VELA_REPO_OWNER": "foo", "VELA_REPO_BRANCH": "foo", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TOPICS": "cloud,security", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo"}}, + }, map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "push", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_REF": "foo", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "true", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_EVENTS": "", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_APPROVED_AT": "0", "VELA_BUILD_APPROVED_BY": "", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "push", "VELA_BUILD_EVENT_ACTION": "", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_REF": "foo", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SENDER_SCM_ID": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_EVENTS": "", "VELA_REPO_APPROVE_BUILD": "", "VELA_REPO_OWNER": "foo", "VELA_REPO_BRANCH": "foo", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TOPICS": "cloud,security", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_ID_TOKEN_REQUEST_URL": "foo/api/v1/repos/foo/builds/1/id_token"}}, {"tag", fields{ build: &api.Build{ID: &num64, Repo: &api.Repo{ID: &num64, Owner: &api.User{ID: &num64, Name: &str, Token: &str, Active: &booL, Admin: &booL}, Org: &str, Name: &str, FullName: &str, Link: &str, Clone: &str, Branch: &str, Topics: &topics, BuildLimit: &num64, Timeout: &num64, Visibility: &str, Private: &booL, Trusted: &booL, Active: &booL}, Number: &num, Parent: &num, Event: &tag, Status: &str, Error: &str, Enqueued: &num64, Created: &num64, Started: &num64, Finished: &num64, Deploy: &str, Clone: &str, Source: &str, Title: &str, Message: &str, Commit: &str, Sender: &str, SenderSCMID: &str, Author: &str, Branch: &str, Ref: &tagref, BaseRef: &str}, metadata: &internal.Metadata{Database: &internal.Database{Driver: str, Host: str}, Queue: &internal.Queue{Channel: str, Driver: str, Host: str}, Source: &internal.Source{Driver: str, Host: str}, Vela: &internal.Vela{Address: str, WebAddress: str}}, repo: &api.Repo{ID: &num64, Owner: &api.User{ID: &num64, Name: &str, Token: &str, Active: &booL, Admin: &booL}, Org: &str, Name: &str, FullName: &str, Link: &str, Clone: &str, Branch: &str, Topics: &topics, BuildLimit: &num64, Timeout: &num64, Visibility: &str, Private: &booL, Trusted: &booL, Active: &booL}, user: &api.User{ID: &num64, Name: &str, Token: &str, Active: &booL, Admin: &booL}, - }, map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "tag", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_REF": "refs/tags/1", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TAG": "1", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "true", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_EVENTS": "", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_APPROVED_AT": "0", "VELA_BUILD_APPROVED_BY": "", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "tag", "VELA_BUILD_EVENT_ACTION": "", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_REF": "refs/tags/1", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SENDER_SCM_ID": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TAG": "1", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_EVENTS": "", "VELA_REPO_APPROVE_BUILD": "", "VELA_REPO_OWNER": "foo", "VELA_REPO_BRANCH": "foo", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TOPICS": "cloud,security", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo"}, + }, map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "tag", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_REF": "refs/tags/1", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TAG": "1", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "true", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_EVENTS": "", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_APPROVED_AT": "0", "VELA_BUILD_APPROVED_BY": "", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "tag", "VELA_BUILD_EVENT_ACTION": "", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_REF": "refs/tags/1", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SENDER_SCM_ID": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TAG": "1", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_EVENTS": "", "VELA_REPO_APPROVE_BUILD": "", "VELA_REPO_OWNER": "foo", "VELA_REPO_BRANCH": "foo", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TOPICS": "cloud,security", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_ID_TOKEN_REQUEST_URL": "foo/api/v1/repos/foo/builds/1/id_token"}, }, {"pull_request", fields{ build: &api.Build{ID: &num64, Repo: &api.Repo{ID: &num64, Owner: &api.User{ID: &num64, Name: &str, Token: &str, Active: &booL, Admin: &booL}, Org: &str, Name: &str, FullName: &str, Link: &str, Clone: &str, Branch: &str, Topics: &topics, BuildLimit: &num64, Timeout: &num64, Visibility: &str, Private: &booL, Trusted: &booL, Active: &booL}, Number: &num, Parent: &num, Event: &pull, EventAction: &pullact, Status: &str, Error: &str, Enqueued: &num64, Created: &num64, Started: &num64, Finished: &num64, Deploy: &str, Clone: &str, Source: &str, Title: &str, Message: &str, Commit: &str, Sender: &str, SenderSCMID: &str, Author: &str, Branch: &str, Ref: &pullref, BaseRef: &str}, metadata: &internal.Metadata{Database: &internal.Database{Driver: str, Host: str}, Queue: &internal.Queue{Channel: str, Driver: str, Host: str}, Source: &internal.Source{Driver: str, Host: str}, Vela: &internal.Vela{Address: str, WebAddress: str}}, repo: &api.Repo{ID: &num64, Owner: &api.User{ID: &num64, Name: &str, Token: &str, Active: &booL, Admin: &booL}, Org: &str, Name: &str, FullName: &str, Link: &str, Clone: &str, Branch: &str, Topics: &topics, BuildLimit: &num64, Timeout: &num64, Visibility: &str, Private: &booL, Trusted: &booL, Active: &booL}, user: &api.User{ID: &num64, Name: &str, Token: &str, Active: &booL, Admin: &booL}, - }, map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "pull_request", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_PULL_REQUEST_NUMBER": "1", "BUILD_REF": "refs/pull/1/head", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "true", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_EVENTS": "", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_APPROVED_AT": "0", "VELA_BUILD_APPROVED_BY": "", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "pull_request", "VELA_BUILD_EVENT_ACTION": "opened", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_PULL_REQUEST": "1", "VELA_BUILD_REF": "refs/pull/1/head", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SENDER_SCM_ID": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_PULL_REQUEST": "1", "VELA_PULL_REQUEST_SOURCE": "", "VELA_PULL_REQUEST_TARGET": "foo", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_EVENTS": "", "VELA_REPO_APPROVE_BUILD": "", "VELA_REPO_OWNER": "foo", "VELA_REPO_BRANCH": "foo", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TOPICS": "cloud,security", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo"}, + }, map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "pull_request", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_PULL_REQUEST_NUMBER": "1", "BUILD_REF": "refs/pull/1/head", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "true", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_EVENTS": "", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_APPROVED_AT": "0", "VELA_BUILD_APPROVED_BY": "", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "pull_request", "VELA_BUILD_EVENT_ACTION": "opened", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_PULL_REQUEST": "1", "VELA_BUILD_REF": "refs/pull/1/head", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SENDER_SCM_ID": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_PULL_REQUEST": "1", "VELA_PULL_REQUEST_SOURCE": "", "VELA_PULL_REQUEST_TARGET": "foo", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_EVENTS": "", "VELA_REPO_APPROVE_BUILD": "", "VELA_REPO_OWNER": "foo", "VELA_REPO_BRANCH": "foo", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TOPICS": "cloud,security", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_ID_TOKEN_REQUEST_URL": "foo/api/v1/repos/foo/builds/1/id_token"}, }, {"deployment", fields{ build: &api.Build{ID: &num64, Repo: &api.Repo{ID: &num64, Owner: &api.User{ID: &num64, Name: &str, Token: &str, Active: &booL, Admin: &booL}, Org: &str, Name: &str, FullName: &str, Link: &str, Clone: &str, Branch: &str, Topics: &topics, BuildLimit: &num64, Timeout: &num64, Visibility: &str, Private: &booL, Trusted: &booL, Active: &booL}, Number: &num, Parent: &num, Event: &deploy, Status: &str, Error: &str, Enqueued: &num64, Created: &num64, Started: &num64, Finished: &num64, Deploy: &target, Clone: &str, Source: &str, Title: &str, Message: &str, Commit: &str, Sender: &str, SenderSCMID: &str, Author: &str, Branch: &str, Ref: &pullref, BaseRef: &str}, metadata: &internal.Metadata{Database: &internal.Database{Driver: str, Host: str}, Queue: &internal.Queue{Channel: str, Driver: str, Host: str}, Source: &internal.Source{Driver: str, Host: str}, Vela: &internal.Vela{Address: str, WebAddress: str}}, repo: &api.Repo{ID: &num64, Owner: &api.User{ID: &num64, Name: &str, Token: &str, Active: &booL, Admin: &booL}, Org: &str, Name: &str, FullName: &str, Link: &str, Clone: &str, Branch: &str, Topics: &topics, BuildLimit: &num64, Timeout: &num64, Visibility: &str, Private: &booL, Trusted: &booL, Active: &booL}, user: &api.User{ID: &num64, Name: &str, Token: &str, Active: &booL, Admin: &booL}, - }, map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "deployment", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_REF": "refs/pull/1/head", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TARGET": "production", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "true", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_EVENTS": "", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_APPROVED_AT": "0", "VELA_BUILD_APPROVED_BY": "", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "deployment", "VELA_BUILD_EVENT_ACTION": "", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_REF": "refs/pull/1/head", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SENDER_SCM_ID": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TARGET": "production", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DEPLOYMENT": "production", "VELA_DEPLOYMENT_NUMBER": "0", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_EVENTS": "", "VELA_REPO_APPROVE_BUILD": "", "VELA_REPO_OWNER": "foo", "VELA_REPO_BRANCH": "foo", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TOPICS": "cloud,security", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo"}, + }, map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "deployment", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_REF": "refs/pull/1/head", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TARGET": "production", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "true", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_EVENTS": "", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_APPROVED_AT": "0", "VELA_BUILD_APPROVED_BY": "", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "deployment", "VELA_BUILD_EVENT_ACTION": "", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_REF": "refs/pull/1/head", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SENDER_SCM_ID": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TARGET": "production", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DEPLOYMENT": "production", "VELA_DEPLOYMENT_NUMBER": "0", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_EVENTS": "", "VELA_REPO_APPROVE_BUILD": "", "VELA_REPO_OWNER": "foo", "VELA_REPO_BRANCH": "foo", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TOPICS": "cloud,security", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_ID_TOKEN_REQUEST_URL": "foo/api/v1/repos/foo/builds/1/id_token"}, }, } for _, tt := range tests { diff --git a/constants/secret.go b/constants/secret.go new file mode 100644 index 000000000..bb794f025 --- /dev/null +++ b/constants/secret.go @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 + +package constants + +// Secret types. +const ( + // SecretPullBuild defines the pull policy type for a secret. + SecretPullBuild = "build_start" + + // SecretPullStep defines the pull policy type for a secret. + SecretPullStep = "step_start" + + // SecretOrg defines the secret type for a secret scoped to a specific org. + SecretOrg = "org" + + // SecretRepo defines the secret type for a secret scoped to a specific repo. + SecretRepo = "repo" + + // SecretShared defines the secret type for a secret shared across the installation. + SecretShared = "shared" + + // SecretMask defines the secret mask to be used in place of secret values returned to users. + SecretMask = "[secure]" + + // SecretLogMask defines the secret mask to be used when distributing logs that contain secrets. + SecretLogMask = "***" + + // SecretRestrictedCharacters defines the set of characters that a secret name cannot contain. + // This matches the following characters: + // Equal Sign = + // Null Character \x00 + SecretRestrictedCharacters = "=\x00" +) diff --git a/constants/status.go b/constants/status.go new file mode 100644 index 000000000..489f4a7df --- /dev/null +++ b/constants/status.go @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 + +package constants + +// Build and step statuses. +const ( + // StatusError defines the status type for build and step error statuses. + StatusError = "error" + + // StatusFailure defines the status type for build and step failure statuses. + StatusFailure = "failure" + + // StatusKilled defines the status type for build and step killed statuses. + StatusKilled = "killed" + + // StatusCanceled defines the status type for build and step canceled statuses. + StatusCanceled = "canceled" + + // StatusPending defines the status type for build and step pending statuses. + StatusPending = "pending" + + // StatusPendingApproval defines the status type for a build waiting to be approved to run. + StatusPendingApproval = "pending approval" + + // StatusRunning defines the status type for build and step running statuses. + StatusRunning = "running" + + // StatusSuccess defines the status type for build and step success statuses. + StatusSuccess = "success" + + // StatusSkipped defines the status type for build and step skipped statuses. + StatusSkipped = "skipped" +) diff --git a/constants/table.go b/constants/table.go index 6d9ff76e8..736335482 100644 --- a/constants/table.go +++ b/constants/table.go @@ -5,4 +5,7 @@ package constants const ( // TableDashboard defines the table type for the database dashboards table. TableDashboard = "dashboards" + + // TableJWK defines the table type for the database jwks table. + TableJWK = "jwks" ) diff --git a/constants/token.go b/constants/token.go new file mode 100644 index 000000000..793b1f7c7 --- /dev/null +++ b/constants/token.go @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 + +package constants + +// Constants for tokens. +const ( + // RefreshTokenName is the name associated with the refresh token. + RefreshTokenName = "vela_refresh_token" + + // UserAccessTokenType is the name associated with the user access token type. + UserAccessTokenType = "UserAccess" + + // UserRefreshTokenType is the name associated with the user refresh token type. + UserRefreshTokenType = "UserRefresh" + + // WorkerAuthTokenType is the name associated with the worker authentication token type. + WorkerAuthTokenType = "WorkerAuth" + + // WorkerRegisterTokenType is the name associated with the worker registration token type. + WorkerRegisterTokenType = "WorkerRegister" + + // WorkerBuildTokenType is the name associated with the worker build token type. + WorkerBuildTokenType = "WorkerBuild" + + // ServerWorkerTokenType is the name associated with the server-worker symmetric token. + ServerWorkerTokenType = "ServerWorker" + + // IDRequestTokenType is the name associated with the id request token type. + IDRequestTokenType = "IDRequest" + + // IDTokenType is the name associated with the id token type. + IDTokenType = "ID" +) diff --git a/constants/visibility.go b/constants/visibility.go new file mode 100644 index 000000000..af2f35b04 --- /dev/null +++ b/constants/visibility.go @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 + +package constants + +// Repo visibility types. +const ( + // VisibilityPublic defines the visibility type for allowing any + // users in Vela to access their repo regardless of the access + // defined in the source control system. + VisibilityPublic = "public" + + // VisibilityPrivate defines the visibility type for only allowing + // users in Vela with pre-defined access in the source control + // system to access their repo. + VisibilityPrivate = "private" +) diff --git a/database/database.go b/database/database.go index 5a9e982b2..9ff3a2855 100644 --- a/database/database.go +++ b/database/database.go @@ -17,6 +17,7 @@ import ( "github.com/go-vela/server/database/deployment" "github.com/go-vela/server/database/executable" "github.com/go-vela/server/database/hook" + "github.com/go-vela/server/database/jwk" "github.com/go-vela/server/database/log" "github.com/go-vela/server/database/pipeline" "github.com/go-vela/server/database/repo" @@ -68,6 +69,7 @@ type ( executable.BuildExecutableInterface deployment.DeploymentInterface hook.HookInterface + jwk.JWKInterface log.LogInterface pipeline.PipelineInterface repo.RepoInterface diff --git a/database/integration_test.go b/database/integration_test.go index 8996ca9fe..9d70d53d2 100644 --- a/database/integration_test.go +++ b/database/integration_test.go @@ -12,6 +12,7 @@ import ( "github.com/adhocore/gronx" "github.com/google/go-cmp/cmp" + "github.com/lestrrat-go/jwx/jwk" api "github.com/go-vela/server/api/types" "github.com/go-vela/server/api/types/settings" @@ -20,6 +21,7 @@ import ( "github.com/go-vela/server/database/deployment" "github.com/go-vela/server/database/executable" "github.com/go-vela/server/database/hook" + dbJWK "github.com/go-vela/server/database/jwk" "github.com/go-vela/server/database/log" "github.com/go-vela/server/database/pipeline" "github.com/go-vela/server/database/repo" @@ -43,6 +45,7 @@ type Resources struct { Deployments []*library.Deployment Executables []*library.BuildExecutable Hooks []*library.Hook + JWKs jwk.Set Logs []*library.Log Pipelines []*library.Pipeline Repos []*api.Repo @@ -136,6 +139,8 @@ func TestDatabase_Integration(t *testing.T) { t.Run("test_hooks", func(t *testing.T) { testHooks(t, db, resources) }) + t.Run("test_jwks", func(t *testing.T) { testJWKs(t, db, resources) }) + t.Run("test_logs", func(t *testing.T) { testLogs(t, db, resources) }) t.Run("test_pipelines", func(t *testing.T) { testPipelines(t, db, resources) }) @@ -855,6 +860,89 @@ func testHooks(t *testing.T, db Interface, resources *Resources) { } } +func testJWKs(t *testing.T, db Interface, resources *Resources) { + // create a variable to track the number of methods called for jwks + methods := make(map[string]bool) + // capture the element type of the jwk interface + element := reflect.TypeOf(new(dbJWK.JWKInterface)).Elem() + // iterate through all methods found in the jwk interface + for i := 0; i < element.NumMethod(); i++ { + // skip tracking the methods to create indexes and tables for jwks + // since those are already called when the database engine starts + if strings.Contains(element.Method(i).Name, "Table") { + continue + } + + // add the method name to the list of functions + methods[element.Method(i).Name] = false + } + + for i := 0; i < resources.JWKs.Len(); i++ { + jk, _ := resources.JWKs.Get(i) + + jkPub, _ := jk.(jwk.RSAPublicKey) + + err := db.CreateJWK(context.TODO(), jkPub) + if err != nil { + t.Errorf("unable to create jwk %s: %v", jkPub.KeyID(), err) + } + } + methods["CreateJWK"] = true + + list, err := db.ListJWKs(context.TODO()) + if err != nil { + t.Errorf("unable to list jwks: %v", err) + } + + if !reflect.DeepEqual(resources.JWKs, list) { + t.Errorf("ListJWKs() mismatch, want %v, got %v", resources.JWKs, list) + } + + methods["ListJWKs"] = true + + for i := 0; i < resources.JWKs.Len(); i++ { + jk, _ := resources.JWKs.Get(i) + + jkPub, _ := jk.(jwk.RSAPublicKey) + + got, err := db.GetActiveJWK(context.TODO(), jkPub.KeyID()) + if err != nil { + t.Errorf("unable to get jwk %s: %v", jkPub.KeyID(), err) + } + + if !cmp.Equal(jkPub, got, testutils.JwkKeyOpts) { + t.Errorf("GetJWK() is %v, want %v", got, jkPub) + } + } + + methods["GetActiveJWK"] = true + + err = db.RotateKeys(context.TODO()) + if err != nil { + t.Errorf("unable to rotate keys: %v", err) + } + + for i := 0; i < resources.JWKs.Len(); i++ { + jk, _ := resources.JWKs.Get(i) + + jkPub, _ := jk.(jwk.RSAPublicKey) + + _, err := db.GetActiveJWK(context.TODO(), jkPub.KeyID()) + if err == nil { + t.Errorf("GetActiveJWK() should return err after rotation") + } + } + + methods["RotateKeys"] = true + + // ensure we called all the methods we expected to + for method, called := range methods { + if !called { + t.Errorf("method %s was not called for jwks", method) + } + } +} + func testLogs(t *testing.T, db Interface, resources *Resources) { // create a variable to track the number of methods called for logs methods := make(map[string]bool) @@ -2484,6 +2572,13 @@ func newResources() *Resources { hookThree.SetLink("https://github.com/github/octocat/settings/hooks/1") hookThree.SetWebhookID(78910) + jwkOne := testutils.JWK() + jwkTwo := testutils.JWK() + + jwkSet := jwk.NewSet() + jwkSet.Add(jwkOne) + jwkSet.Add(jwkTwo) + logServiceOne := new(library.Log) logServiceOne.SetID(1) logServiceOne.SetBuildID(1) @@ -2749,6 +2844,7 @@ func newResources() *Resources { Deployments: []*library.Deployment{deploymentOne, deploymentTwo}, Executables: []*library.BuildExecutable{executableOne, executableTwo}, Hooks: []*library.Hook{hookOne, hookTwo, hookThree}, + JWKs: jwkSet, Logs: []*library.Log{logServiceOne, logServiceTwo, logStepOne, logStepTwo}, Pipelines: []*library.Pipeline{pipelineOne, pipelineTwo}, Repos: []*api.Repo{repoOne, repoTwo}, diff --git a/database/interface.go b/database/interface.go index 0d82da099..b054d4329 100644 --- a/database/interface.go +++ b/database/interface.go @@ -8,6 +8,7 @@ import ( "github.com/go-vela/server/database/deployment" "github.com/go-vela/server/database/executable" "github.com/go-vela/server/database/hook" + "github.com/go-vela/server/database/jwk" "github.com/go-vela/server/database/log" "github.com/go-vela/server/database/pipeline" "github.com/go-vela/server/database/repo" @@ -53,6 +54,9 @@ type Interface interface { // HookInterface defines the interface for hooks stored in the database. hook.HookInterface + // JWKInterface defines the interface for JWKs stored in the database. + jwk.JWKInterface + // LogInterface defines the interface for logs stored in the database. log.LogInterface diff --git a/database/jwk/create.go b/database/jwk/create.go new file mode 100644 index 000000000..3f5896e41 --- /dev/null +++ b/database/jwk/create.go @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 + +package jwk + +import ( + "context" + "database/sql" + + "github.com/lestrrat-go/jwx/jwk" + "github.com/sirupsen/logrus" + + "github.com/go-vela/server/constants" + "github.com/go-vela/server/database/types" +) + +// CreateJWK creates a new JWK in the database. +func (e *engine) CreateJWK(_ context.Context, j jwk.RSAPublicKey) error { + e.logger.WithFields(logrus.Fields{ + "jwk": j.KeyID(), + }).Tracef("creating key %s in the database", j.KeyID()) + + key := types.JWKFromAPI(j) + key.Active = sql.NullBool{Bool: true, Valid: true} + + // send query to the database + return e.client.Table(constants.TableJWK).Create(key).Error +} diff --git a/database/jwk/create_test.go b/database/jwk/create_test.go new file mode 100644 index 000000000..2275a4c4e --- /dev/null +++ b/database/jwk/create_test.go @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 + +package jwk + +import ( + "context" + "encoding/json" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + + "github.com/go-vela/server/database/testutils" +) + +func TestJWK_Engine_CreateJWK(t *testing.T) { + // setup types + _jwk := testutils.JWK() + _jwkBytes, err := json.Marshal(_jwk) + if err != nil { + t.Errorf("unable to marshal JWK: %v", err) + } + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // ensure the mock expects the query + _mock.ExpectExec(`INSERT INTO "jwks" +("id","active","key") +VALUES ($1,$2,$3)`). + WithArgs(_jwk.KeyID(), true, _jwkBytes). + WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.CreateJWK(context.TODO(), _jwk) + + if test.failure { + if err == nil { + t.Errorf("CreateDashboard for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateDashboard for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/jwk/get.go b/database/jwk/get.go new file mode 100644 index 000000000..187753df8 --- /dev/null +++ b/database/jwk/get.go @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 + +package jwk + +import ( + "context" + + "github.com/lestrrat-go/jwx/jwk" + + "github.com/go-vela/server/constants" + "github.com/go-vela/server/database/types" +) + +// GetActiveJWK gets a JWK by UUID (kid) from the database if active. +func (e *engine) GetActiveJWK(_ context.Context, id string) (jwk.RSAPublicKey, error) { + e.logger.Tracef("getting key %s from the database", id) + + // variable to store query results + j := new(types.JWK) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableJWK). + Where("id = ?", id). + Where("active = ?", true). + Take(j). + Error + if err != nil { + return j.ToAPI(), err + } + + return j.ToAPI(), nil +} diff --git a/database/jwk/get_test.go b/database/jwk/get_test.go new file mode 100644 index 000000000..2cb131bae --- /dev/null +++ b/database/jwk/get_test.go @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Apache-2.0 + +package jwk + +import ( + "context" + "encoding/json" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/google/go-cmp/cmp" + "github.com/lestrrat-go/jwx/jwk" + + "github.com/go-vela/server/database/testutils" +) + +func TestJWK_Engine_GetJWK(t *testing.T) { + // setup types + _jwk := testutils.JWK() + _jwkBytes, err := json.Marshal(_jwk) + if err != nil { + t.Errorf("unable to marshal JWK: %v", err) + } + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "active", "key"}, + ).AddRow(_jwk.KeyID(), true, _jwkBytes) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "jwks" WHERE id = $1 AND active = $2 LIMIT $3`).WithArgs(_jwk.KeyID(), true, 1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err = _sqlite.CreateJWK(context.TODO(), _jwk) + if err != nil { + t.Errorf("unable to create test repo for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want jwk.RSAPublicKey + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _jwk, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _jwk, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.GetActiveJWK(context.TODO(), _jwk.KeyID()) + + if test.failure { + if err == nil { + t.Errorf("GetActiveJWK for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("GetActiveJWK for %s returned err: %v", test.name, err) + } + + if diff := cmp.Diff(test.want, got, testutils.JwkKeyOpts); diff != "" { + t.Errorf("GetActiveJWK mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/database/jwk/interface.go b/database/jwk/interface.go new file mode 100644 index 000000000..a83b1b300 --- /dev/null +++ b/database/jwk/interface.go @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 + +package jwk + +import ( + "context" + + "github.com/lestrrat-go/jwx/jwk" +) + +// JWKInterface represents the Vela interface for JWK +// functions with the supported Database backends. +// +//nolint:revive // ignore name stutter +type JWKInterface interface { + // JWK Data Definition Language Functions + // + // https://en.wikipedia.org/wiki/Data_definition_language + CreateJWKTable(context.Context, string) error + + // JWK Data Manipulation Language Functions + // + // https://en.wikipedia.org/wiki/Data_manipulation_language + + // CreateJWK defines a function that creates a JWK. + CreateJWK(context.Context, jwk.RSAPublicKey) error + // RotateKeys defines a function that rotates JWKs. + RotateKeys(context.Context) error + // ListJWKs defines a function that lists all JWKs configured. + ListJWKs(context.Context) (jwk.Set, error) + // GetJWK defines a function that gets a JWK by the provided key ID. + GetActiveJWK(context.Context, string) (jwk.RSAPublicKey, error) +} diff --git a/database/jwk/jwk.go b/database/jwk/jwk.go new file mode 100644 index 000000000..6b3acc33f --- /dev/null +++ b/database/jwk/jwk.go @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 + +package jwk + +import ( + "context" + "fmt" + + "github.com/sirupsen/logrus" + "gorm.io/gorm" + + "github.com/go-vela/server/constants" +) + +type ( + // config represents the settings required to create the engine that implements the JWKService interface. + config struct { + // specifies to skip creating tables and indexes for the JWK engine + SkipCreation bool + // specifies the driver for proper popping query + Driver string + } + + // engine represents the key set functionality that implements the JWKService interface. + engine struct { + // engine configuration settings used in key set functions + config *config + + ctx context.Context + + // gorm.io/gorm database client used in key set functions + // + // https://pkg.go.dev/gorm.io/gorm#DB + client *gorm.DB + + // sirupsen/logrus logger used in key set functions + // + // https://pkg.go.dev/github.com/sirupsen/logrus#Entry + logger *logrus.Entry + } +) + +// New creates and returns a Vela service for integrating with key sets in the database. +// +//nolint:revive // ignore returning unexported engine +func New(opts ...EngineOpt) (*engine, error) { + // create new JWK engine + e := new(engine) + + // create new fields + e.client = new(gorm.DB) + e.config = new(config) + e.logger = new(logrus.Entry) + + // apply all provided configuration options + for _, opt := range opts { + err := opt(e) + if err != nil { + return nil, err + } + } + + // check if we should skip creating key set database objects + if e.config.SkipCreation { + e.logger.Warning("skipping creation of key sets table and indexes in the database") + + return e, nil + } + + // create the JWK table + err := e.CreateJWKTable(e.ctx, e.client.Config.Dialector.Name()) + if err != nil { + return nil, fmt.Errorf("unable to create %s table: %w", constants.TableJWK, err) + } + + return e, nil +} diff --git a/database/jwk/jwk_test.go b/database/jwk/jwk_test.go new file mode 100644 index 000000000..95ff9a463 --- /dev/null +++ b/database/jwk/jwk_test.go @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: Apache-2.0 + +package jwk + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/sirupsen/logrus" + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func TestJWK_New(t *testing.T) { + // setup types + logger := logrus.NewEntry(logrus.StandardLogger()) + + _sql, _mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Errorf("unable to create new SQL mock: %v", err) + } + defer _sql.Close() + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + + _config := &gorm.Config{SkipDefaultTransaction: true} + + _postgres, err := gorm.Open(postgres.New(postgres.Config{Conn: _sql}), _config) + if err != nil { + t.Errorf("unable to create new postgres database: %v", err) + } + + _sqlite, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), _config) + if err != nil { + t.Errorf("unable to create new sqlite database: %v", err) + } + + defer func() { _sql, _ := _sqlite.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + client *gorm.DB + key string + logger *logrus.Entry + skipCreation bool + want *engine + }{ + { + failure: false, + name: "postgres", + client: _postgres, + key: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + logger: logger, + skipCreation: false, + want: &engine{ + client: _postgres, + config: &config{SkipCreation: false}, + logger: logger, + }, + }, + { + failure: false, + name: "sqlite3", + client: _sqlite, + key: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + logger: logger, + skipCreation: false, + want: &engine{ + client: _sqlite, + config: &config{SkipCreation: false}, + logger: logger, + }, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := New( + WithClient(test.client), + WithLogger(test.logger), + WithSkipCreation(test.skipCreation), + ) + + if test.failure { + if err == nil { + t.Errorf("New for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("New for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("New for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} + +// testPostgres is a helper function to create a Postgres engine for testing. +func testPostgres(t *testing.T) (*engine, sqlmock.Sqlmock) { + // create the new mock sql database + // + // https://pkg.go.dev/github.com/DATA-DOG/go-sqlmock#New + _sql, _mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Errorf("unable to create new SQL mock: %v", err) + } + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + + // create the new mock Postgres database client + // + // https://pkg.go.dev/gorm.io/gorm#Open + _postgres, err := gorm.Open( + postgres.New(postgres.Config{Conn: _sql}), + &gorm.Config{SkipDefaultTransaction: true}, + ) + if err != nil { + t.Errorf("unable to create new postgres database: %v", err) + } + + _engine, err := New( + WithClient(_postgres), + WithLogger(logrus.NewEntry(logrus.StandardLogger())), + WithSkipCreation(false), + ) + if err != nil { + t.Errorf("unable to create new postgres dashboard engine: %v", err) + } + + return _engine, _mock +} + +// testSqlite is a helper function to create a Sqlite engine for testing. +func testSqlite(t *testing.T) *engine { + _sqlite, err := gorm.Open( + sqlite.Open("file::memory:?cache=shared"), + &gorm.Config{SkipDefaultTransaction: true}, + ) + if err != nil { + t.Errorf("unable to create new sqlite database: %v", err) + } + + _engine, err := New( + WithClient(_sqlite), + WithLogger(logrus.NewEntry(logrus.StandardLogger())), + WithSkipCreation(false), + ) + if err != nil { + t.Errorf("unable to create new sqlite dashboard engine: %v", err) + } + + return _engine +} diff --git a/database/jwk/list.go b/database/jwk/list.go new file mode 100644 index 000000000..7c83b48b1 --- /dev/null +++ b/database/jwk/list.go @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 + +package jwk + +import ( + "context" + + "github.com/lestrrat-go/jwx/jwk" + + "github.com/go-vela/server/constants" + "github.com/go-vela/server/database/types" +) + +// ListJWKs gets a list of all configured JWKs from the database. +func (e *engine) ListJWKs(_ context.Context) (jwk.Set, error) { + e.logger.Trace("listing all jwks from the database") + + k := new([]types.JWK) + keySet := jwk.NewSet() + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableJWK). + Find(&k). + Error + if err != nil { + return nil, err + } + + // iterate through all query results + for _, key := range *k { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := key + + // convert query result to API type + keySet.Add(tmp.ToAPI()) + } + + return keySet, nil +} diff --git a/database/jwk/list_test.go b/database/jwk/list_test.go new file mode 100644 index 000000000..f4d54dcc2 --- /dev/null +++ b/database/jwk/list_test.go @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: Apache-2.0 + +package jwk + +import ( + "context" + "encoding/json" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/lestrrat-go/jwx/jwk" + + "github.com/go-vela/server/database/testutils" +) + +func TestJWK_Engine_ListJWKs(t *testing.T) { + // setup types + _jwkOne := testutils.JWK() + _jwkOneBytes, err := json.Marshal(_jwkOne) + if err != nil { + t.Errorf("unable to marshal JWK: %v", err) + } + + _jwkTwo := testutils.JWK() + _jwkTwoBytes, err := json.Marshal(_jwkTwo) + if err != nil { + t.Errorf("unable to marshal JWK: %v", err) + } + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "active", "key"}). + AddRow(_jwkOne.KeyID(), true, _jwkOneBytes). + AddRow(_jwkTwo.KeyID(), true, _jwkTwoBytes) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "jwks"`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err = _sqlite.CreateJWK(context.TODO(), _jwkOne) + if err != nil { + t.Errorf("unable to create test jwk for sqlite: %v", err) + } + + err = _sqlite.CreateJWK(context.TODO(), _jwkTwo) + if err != nil { + t.Errorf("unable to create test jwk for sqlite: %v", err) + } + + wantSet := jwk.NewSet() + wantSet.Add(_jwkOne) + wantSet.Add(_jwkTwo) + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want jwk.Set + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: wantSet, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: wantSet, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.ListJWKs(context.TODO()) + + if test.failure { + if err == nil { + t.Errorf("ListJWKs for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListJWKs for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListJWKs for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/jwk/opts.go b/database/jwk/opts.go new file mode 100644 index 000000000..b26329135 --- /dev/null +++ b/database/jwk/opts.go @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 + +package jwk + +import ( + "context" + + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +// EngineOpt represents a configuration option to initialize the database engine for key sets. +type EngineOpt func(*engine) error + +// WithClient sets the gorm.io/gorm client in the database engine for key sets. +func WithClient(client *gorm.DB) EngineOpt { + return func(e *engine) error { + // set the gorm.io/gorm client in the key set engine + e.client = client + + return nil + } +} + +// WithLogger sets the github.com/sirupsen/logrus logger in the database engine for key sets. +func WithLogger(logger *logrus.Entry) EngineOpt { + return func(e *engine) error { + // set the github.com/sirupsen/logrus logger in the key set engine + e.logger = logger + + return nil + } +} + +// WithSkipCreation sets the skip creation logic in the database engine for key sets. +func WithSkipCreation(skipCreation bool) EngineOpt { + return func(e *engine) error { + // set to skip creating tables and indexes in the key set engine + e.config.SkipCreation = skipCreation + + return nil + } +} + +// WithContext sets the context in the database engine for key sets. +func WithContext(ctx context.Context) EngineOpt { + return func(e *engine) error { + e.ctx = ctx + + return nil + } +} diff --git a/database/jwk/opts_test.go b/database/jwk/opts_test.go new file mode 100644 index 000000000..8a7192111 --- /dev/null +++ b/database/jwk/opts_test.go @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: Apache-2.0 + +package jwk + +import ( + "context" + "reflect" + "testing" + + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +func TestHook_EngineOpt_WithClient(t *testing.T) { + // setup types + e := &engine{client: new(gorm.DB)} + + // setup tests + tests := []struct { + failure bool + name string + client *gorm.DB + want *gorm.DB + }{ + { + failure: false, + name: "client set to new database", + client: new(gorm.DB), + want: new(gorm.DB), + }, + { + failure: false, + name: "client set to nil", + client: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithClient(test.client)(e) + + if test.failure { + if err == nil { + t.Errorf("WithClient for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithClient returned err: %v", err) + } + + if !reflect.DeepEqual(e.client, test.want) { + t.Errorf("WithClient is %v, want %v", e.client, test.want) + } + }) + } +} + +func TestHook_EngineOpt_WithLogger(t *testing.T) { + // setup types + e := &engine{logger: new(logrus.Entry)} + + // setup tests + tests := []struct { + failure bool + name string + logger *logrus.Entry + want *logrus.Entry + }{ + { + failure: false, + name: "logger set to new entry", + logger: new(logrus.Entry), + want: new(logrus.Entry), + }, + { + failure: false, + name: "logger set to nil", + logger: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithLogger(test.logger)(e) + + if test.failure { + if err == nil { + t.Errorf("WithLogger for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithLogger returned err: %v", err) + } + + if !reflect.DeepEqual(e.logger, test.want) { + t.Errorf("WithLogger is %v, want %v", e.logger, test.want) + } + }) + } +} + +func TestHook_EngineOpt_WithSkipCreation(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + skipCreation bool + want bool + }{ + { + failure: false, + name: "skip creation set to true", + skipCreation: true, + want: true, + }, + { + failure: false, + name: "skip creation set to false", + skipCreation: false, + want: false, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithSkipCreation(test.skipCreation)(e) + + if test.failure { + if err == nil { + t.Errorf("WithSkipCreation for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithSkipCreation returned err: %v", err) + } + + if !reflect.DeepEqual(e.config.SkipCreation, test.want) { + t.Errorf("WithSkipCreation is %v, want %v", e.config.SkipCreation, test.want) + } + }) + } +} + +func TestHook_EngineOpt_WithContext(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + ctx context.Context + want context.Context + }{ + { + failure: false, + name: "context set to TODO", + ctx: context.TODO(), + want: context.TODO(), + }, + { + failure: false, + name: "context set to nil", + ctx: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithContext(test.ctx)(e) + + if test.failure { + if err == nil { + t.Errorf("WithContext for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithContext returned err: %v", err) + } + + if !reflect.DeepEqual(e.ctx, test.want) { + t.Errorf("WithContext is %v, want %v", e.ctx, test.want) + } + }) + } +} diff --git a/database/jwk/rotate.go b/database/jwk/rotate.go new file mode 100644 index 000000000..82ff1c422 --- /dev/null +++ b/database/jwk/rotate.go @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 + +package jwk + +import ( + "context" + "database/sql" + + "github.com/go-vela/server/constants" + "github.com/go-vela/server/database/types" +) + +// RotateKeys removes all inactive keys and sets active keys to inactive. +func (e *engine) RotateKeys(_ context.Context) error { + e.logger.Trace("rotating jwks in the database") + + k := types.JWK{} + + // remove inactive keys + err := e.client. + Table(constants.TableJWK). + Where("active = ?", false). + Delete(&k). + Error + if err != nil { + return err + } + + // set active keys to inactive + err = e.client. + Table(constants.TableJWK). + Where("active = ?", true). + Update("active", sql.NullBool{Bool: false, Valid: true}). + Error + if err != nil { + return err + } + + return nil +} diff --git a/database/jwk/rotate_test.go b/database/jwk/rotate_test.go new file mode 100644 index 000000000..cd4acbb72 --- /dev/null +++ b/database/jwk/rotate_test.go @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: Apache-2.0 + +package jwk + +import ( + "context" + "encoding/json" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + + "github.com/go-vela/server/database/testutils" +) + +func TestJWK_Engine_RotateKeys(t *testing.T) { + // setup types + _jwkOne := testutils.JWK() + _jwkOneBytes, err := json.Marshal(_jwkOne) + if err != nil { + t.Errorf("unable to marshal JWK: %v", err) + } + + _jwkTwo := testutils.JWK() + _jwkTwoBytes, err := json.Marshal(_jwkTwo) + if err != nil { + t.Errorf("unable to marshal JWK: %v", err) + } + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "active", "key"}, + ).AddRow(_jwkOne.KeyID(), true, _jwkOneBytes) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "jwks" WHERE id = $1 AND active = $2 LIMIT $3`).WithArgs(_jwkOne.KeyID(), true, 1).WillReturnRows(_rows) + + // create expected result in mock + _rows = sqlmock.NewRows( + []string{"id", "active", "key"}, + ).AddRow(_jwkTwo.KeyID(), true, _jwkTwoBytes) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "jwks" WHERE id = $1 AND active = $2 LIMIT $3`).WithArgs(_jwkTwo.KeyID(), true, 1).WillReturnRows(_rows) + + _mock.ExpectExec(`DELETE FROM "jwks" WHERE active = $1`). + WithArgs(false). + WillReturnResult(sqlmock.NewResult(1, 1)) + + _mock.ExpectExec(`UPDATE "jwks" SET "active"=$1 WHERE active = $2`). + WithArgs(false, true). + WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err = _sqlite.CreateJWK(context.TODO(), _jwkOne) + if err != nil { + t.Errorf("unable to create test jwk for sqlite: %v", err) + } + + err = _sqlite.CreateJWK(context.TODO(), _jwkTwo) + if err != nil { + t.Errorf("unable to create test jwk for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, err := test.database.GetActiveJWK(context.TODO(), _jwkOne.KeyID()) + if err != nil { + t.Errorf("GetActiveJWK for %s returned err: %v", test.name, err) + } + + _, err = test.database.GetActiveJWK(context.TODO(), _jwkTwo.KeyID()) + if err != nil { + t.Errorf("GetActiveJWK for %s returned err: %v", test.name, err) + } + + err = test.database.RotateKeys(context.TODO()) + + if test.failure { + if err == nil { + t.Errorf("RotateKeys for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("RotateKeys for %s returned err: %v", test.name, err) + } + + _, err = test.database.GetActiveJWK(context.TODO(), _jwkOne.KeyID()) + if err == nil { + t.Errorf("GetActiveJWK for %s should have returned err", test.name) + } + + _, err = test.database.GetActiveJWK(context.TODO(), _jwkTwo.KeyID()) + if err == nil { + t.Errorf("GetActiveJWK for %s should have returned err", test.name) + } + }) + } +} diff --git a/database/jwk/table.go b/database/jwk/table.go new file mode 100644 index 000000000..6e2108de9 --- /dev/null +++ b/database/jwk/table.go @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 + +package jwk + +import ( + "context" + + "github.com/go-vela/types/constants" +) + +const ( + // CreatePostgresTable represents a query to create the Postgres jwks table. + CreatePostgresTable = ` +CREATE TABLE +IF NOT EXISTS +jwks ( + id UUID PRIMARY KEY, + active BOOLEAN, + key JSON DEFAULT NULL +); +` + + // CreateSqliteTable represents a query to create the Sqlite jwks table. + CreateSqliteTable = ` +CREATE TABLE +IF NOT EXISTS +jwks ( + id TEXT PRIMARY KEY, + active BOOLEAN, + key TEXT +); +` +) + +// CreateJWKTable creates the jwks table in the database. +func (e *engine) CreateJWKTable(ctx context.Context, driver string) error { + e.logger.Tracef("creating jwks table in the database") + + // handle the driver provided to create the table + switch driver { + case constants.DriverPostgres: + // create the jwks table for Postgres + return e.client.Exec(CreatePostgresTable).Error + case constants.DriverSqlite: + fallthrough + default: + // create the jwks table for Sqlite + return e.client.Exec(CreateSqliteTable).Error + } +} diff --git a/database/jwk/table_test.go b/database/jwk/table_test.go new file mode 100644 index 000000000..ec8626677 --- /dev/null +++ b/database/jwk/table_test.go @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 + +package jwk + +import ( + "context" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestJWK_Engine_CreateJWKTable(t *testing.T) { + // setup types + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.CreateJWKTable(context.TODO(), test.name) + + if test.failure { + if err == nil { + t.Errorf("CreateJWKTable for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateJWKTable for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/resource.go b/database/resource.go index 486d2a137..c11341c64 100644 --- a/database/resource.go +++ b/database/resource.go @@ -10,6 +10,7 @@ import ( "github.com/go-vela/server/database/deployment" "github.com/go-vela/server/database/executable" "github.com/go-vela/server/database/hook" + "github.com/go-vela/server/database/jwk" "github.com/go-vela/server/database/log" "github.com/go-vela/server/database/pipeline" "github.com/go-vela/server/database/repo" @@ -96,6 +97,17 @@ func (e *engine) NewResources(ctx context.Context) error { return err } + // create the database agnostic engine for JWKs + e.JWKInterface, err = jwk.New( + jwk.WithContext(e.ctx), + jwk.WithClient(e.client), + jwk.WithLogger(e.logger), + jwk.WithSkipCreation(e.config.SkipCreation), + ) + if err != nil { + return err + } + // create the database agnostic engine for logs e.LogInterface, err = log.New( log.WithContext(e.ctx), diff --git a/database/resource_test.go b/database/resource_test.go index a1371108b..cdc5dbaa3 100644 --- a/database/resource_test.go +++ b/database/resource_test.go @@ -13,6 +13,7 @@ import ( "github.com/go-vela/server/database/deployment" "github.com/go-vela/server/database/executable" "github.com/go-vela/server/database/hook" + "github.com/go-vela/server/database/jwk" "github.com/go-vela/server/database/log" "github.com/go-vela/server/database/pipeline" "github.com/go-vela/server/database/repo" @@ -47,6 +48,8 @@ func TestDatabase_Engine_NewResources(t *testing.T) { // ensure the mock expects the hook queries _mock.ExpectExec(hook.CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) _mock.ExpectExec(hook.CreateRepoIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + // ensure the mock expects the jwk queries + _mock.ExpectExec(jwk.CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) // ensure the mock expects the log queries _mock.ExpectExec(log.CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) _mock.ExpectExec(log.CreateBuildIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) diff --git a/database/testutils/api_resources.go b/database/testutils/api_resources.go index a418ee363..83a3d5c3c 100644 --- a/database/testutils/api_resources.go +++ b/database/testutils/api_resources.go @@ -3,6 +3,12 @@ package testutils import ( + "crypto/rand" + "crypto/rsa" + + "github.com/google/uuid" + "github.com/lestrrat-go/jwx/jwk" + api "github.com/go-vela/server/api/types" "github.com/go-vela/server/api/types/actions" "github.com/go-vela/types/library" @@ -269,3 +275,23 @@ func APIDashboardRepo() *api.DashboardRepo { Events: new([]string), } } + +func JWK() jwk.RSAPublicKey { + privateRSAKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil + } + + // assign KID to key pair + kid, err := uuid.NewV7() + if err != nil { + return nil + } + + j := jwk.NewRSAPublicKey() + _ = j.FromRaw(&privateRSAKey.PublicKey) + + _ = j.Set(jwk.KeyIDKey, kid.String()) + + return j +} diff --git a/database/testutils/mock_args.go b/database/testutils/mock_args.go index c2d8c562f..e6d8a9324 100644 --- a/database/testutils/mock_args.go +++ b/database/testutils/mock_args.go @@ -4,7 +4,11 @@ package testutils import ( "database/sql/driver" + "reflect" "time" + + "github.com/google/go-cmp/cmp" + "github.com/lestrrat-go/jwx/jwk" ) // This will be used with the github.com/DATA-DOG/go-sqlmock library to compare values @@ -33,3 +37,26 @@ func (t NowTimestamp) Match(v driver.Value) bool { return now-ts < 10 } + +var JwkKeyOpts = cmp.Options{ + cmp.FilterValues(func(x, y interface{}) bool { + _, xOk := x.(jwk.RSAPublicKey) + _, yOk := y.(jwk.RSAPublicKey) + return xOk && yOk + }, cmp.Comparer(func(x, y interface{}) bool { + xJWK := x.(jwk.RSAPublicKey) + yJWK := y.(jwk.RSAPublicKey) + + var rawXKey, rawYKey interface{} + + if err := xJWK.Raw(&rawXKey); err != nil { + return false + } + + if err := yJWK.Raw(&rawYKey); err != nil { + return false + } + + return reflect.DeepEqual(rawXKey, rawYKey) && xJWK.KeyID() == yJWK.KeyID() + })), +} diff --git a/database/types/jwk.go b/database/types/jwk.go new file mode 100644 index 000000000..486512da1 --- /dev/null +++ b/database/types/jwk.go @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + "database/sql" + "encoding/json" + "errors" + + "github.com/google/uuid" + "github.com/lestrrat-go/jwx/jwk" +) + +var ( + // ErrInvalidKID defines the error type when a + // JWK type has an invalid ID field provided. + ErrInvalidKID = errors.New("invalid key identifier provided") +) + +type ( + // JWK is the database representation of a jwk. + JWK struct { + ID uuid.UUID `gorm:"type:uuid"` + Active sql.NullBool `sql:"active"` + Key []byte `sql:"key"` + } +) + +// Nullify ensures the valid flag for +// the sql.Null types are properly set. +// +// When a field within the JWK type is the zero +// value for the field, the valid flag is set to +// false causing it to be NULL in the database. +func (j *JWK) Nullify() *JWK { + if j == nil { + return nil + } + + return j +} + +// ToAPI converts the JWK type +// to an API JWK type. +func (j *JWK) ToAPI() jwk.RSAPublicKey { + parsedKey, _ := jwk.ParseKey(j.Key) + + switch jwk := parsedKey.(type) { + case jwk.RSAPublicKey: + return jwk + default: + return nil + } +} + +// JWKFromAPI converts the API JWK type +// to a database JWK type. +func JWKFromAPI(j jwk.RSAPublicKey) *JWK { + var ( + id uuid.UUID + err error + ) + + id, err = uuid.Parse(j.KeyID()) + if err != nil { + return nil + } + + bytesKey, _ := json.Marshal(j) + + jwk := &JWK{ + ID: id, + Key: bytesKey, + } + + return jwk.Nullify() +} diff --git a/database/types/jwk_test.go b/database/types/jwk_test.go new file mode 100644 index 000000000..01fbf6ddc --- /dev/null +++ b/database/types/jwk_test.go @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + "database/sql" + "encoding/json" + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + + "github.com/go-vela/server/database/testutils" +) + +func TestTypes_JWK_Nullify(t *testing.T) { + // setup tests + var j *JWK + + tests := []struct { + JWK *JWK + want *JWK + }{ + { + JWK: testJWK(), + want: testJWK(), + }, + { + JWK: j, + want: nil, + }, + } + + // run tests + for _, test := range tests { + got := test.JWK.Nullify() + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("Nullify is %v, want %v", got, test.want) + } + } +} + +func TestTypes_JWK_ToAPI(t *testing.T) { + // setup types + want := testutils.JWK() + + wantBytes, err := json.Marshal(want) + if err != nil { + t.Errorf("unable to marshal JWK: %v", err) + } + + uuid, _ := uuid.Parse(want.KeyID()) + h := &JWK{ + ID: uuid, + Active: sql.NullBool{Bool: true, Valid: true}, + Key: wantBytes, + } + + // run test + got := h.ToAPI() + + if !reflect.DeepEqual(got, want) { + t.Errorf("ToAPI is %v, want %v", got, want) + } +} + +func TestTypes_JWKFromAPI(t *testing.T) { + j := testutils.JWK() + + jBytes, err := json.Marshal(j) + if err != nil { + t.Errorf("unable to marshal JWK: %v", err) + } + + uuid, err := uuid.Parse(j.KeyID()) + if err != nil { + t.Errorf("unable to parse JWK key id: %v", err) + } + + // setup types + want := &JWK{ + ID: uuid, + Active: sql.NullBool{Bool: false, Valid: false}, + Key: jBytes, + } + + // run test + got := JWKFromAPI(j) + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("JWKFromAPI() mismatch (-want +got):\n%s", diff) + } +} + +// testJWK is a test helper function to create a JWK +// type with all fields set to a fake value. +func testJWK() *JWK { + uuid, _ := uuid.Parse("c8da1302-07d6-11ea-882f-4893bca275b8") + + return &JWK{ + ID: uuid, + Active: sql.NullBool{Bool: true, Valid: true}, + } +} diff --git a/go.mod b/go.mod index 4993597bb..f9837b3eb 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/ghodss/yaml v1.0.0 github.com/gin-gonic/gin v1.9.1 github.com/go-playground/assert/v2 v2.2.0 - github.com/go-vela/types v0.23.4-0.20240417135026-fb4a95c30338 + github.com/go-vela/types v0.23.4-0.20240516161114-57d6b8f77b10 github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/go-cmp v0.6.0 github.com/google/go-github/v61 v61.0.0 @@ -46,6 +46,15 @@ require ( k8s.io/apimachinery v0.29.2 ) +require ( + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect + github.com/lestrrat-go/blackmagic v1.0.2 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/iter v1.0.2 // indirect + github.com/lestrrat-go/option v1.0.1 // indirect +) + require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect @@ -92,6 +101,7 @@ require ( github.com/klauspost/cpuid/v2 v2.2.4 // indirect github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.2.4 // indirect + github.com/lestrrat-go/jwx v1.2.29 github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.17 // indirect github.com/mitchellh/copystructure v1.0.0 // indirect diff --git a/go.sum b/go.sum index 75bac3763..0f39a0a81 100644 --- a/go.sum +++ b/go.sum @@ -56,6 +56,9 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= @@ -87,8 +90,8 @@ github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/go-vela/types v0.23.4-0.20240417135026-fb4a95c30338 h1:I0v47dOdAvjX7lOFN4s28uONChmluD6TNgFL1hpav60= -github.com/go-vela/types v0.23.4-0.20240417135026-fb4a95c30338/go.mod h1:vISsYDdjz9RPEK6qZ+MxtrdZEjTVU4K30NomB3826u8= +github.com/go-vela/types v0.23.4-0.20240516161114-57d6b8f77b10 h1:VQxIqxpJKIOzRnMi4z/d+EOo7jc5PXCnlUvZZl5ajzA= +github.com/go-vela/types v0.23.4-0.20240516161114-57d6b8f77b10/go.mod h1:vISsYDdjz9RPEK6qZ+MxtrdZEjTVU4K30NomB3826u8= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -184,6 +187,19 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= +github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= +github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx v1.2.29 h1:QT0utmUJ4/12rmsVQrJ3u55bycPkKqGYuGT4tyRhxSQ= +github.com/lestrrat-go/jwx v1.2.29/go.mod h1:hU8k2l6WF0ncx20uQdOmik/Gjg6E3/wIRtXSNFeZuB8= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= @@ -252,17 +268,20 @@ github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= @@ -306,6 +325,7 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= @@ -341,6 +361,7 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/internal/token/compose_test.go b/internal/token/compose_test.go index 0cbce444c..c6efd58ce 100644 --- a/internal/token/compose_test.go +++ b/internal/token/compose_test.go @@ -24,8 +24,7 @@ func TestToken_Compose(t *testing.T) { u.SetToken("bar") tm := &Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } @@ -47,7 +46,7 @@ func TestToken_Compose(t *testing.T) { tkn := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - want, err := tkn.SignedString([]byte(tm.PrivateKey)) + want, err := tkn.SignedString([]byte(tm.PrivateKeyHMAC)) if err != nil { t.Errorf("Unable to create test token: %v", err) } diff --git a/internal/token/generate_rsa.go b/internal/token/generate_rsa.go new file mode 100644 index 000000000..350ab8ea9 --- /dev/null +++ b/internal/token/generate_rsa.go @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: Apache-2.0 + +package token + +import ( + "context" + "crypto/rand" + "crypto/rsa" + + "github.com/google/uuid" + "github.com/lestrrat-go/jwx/jwk" + + "github.com/go-vela/server/database" +) + +// GenerateRSA creates an RSA key pair and sets it in the token manager and saves the JWK in the database. +func (tm *Manager) GenerateRSA(ctx context.Context, db database.Interface) error { + // generate key pair + privateRSAKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return err + } + + // assign KID to key pair + kid, err := uuid.NewV7() + if err != nil { + return err + } + + j := jwk.NewRSAPublicKey() + + err = j.FromRaw(&privateRSAKey.PublicKey) + if err != nil { + return err + } + + err = j.Set(jwk.KeyIDKey, kid.String()) + if err != nil { + return err + } + + // create the JWK in the database + err = db.CreateJWK(context.TODO(), j) + if err != nil { + return err + } + + // create the RSA key set for token manager + keySet := RSAKeySet{ + PrivateKey: privateRSAKey, + KID: kid.String(), + } + + tm.RSAKeySet = keySet + + return nil +} diff --git a/internal/token/manager.go b/internal/token/manager.go index c02484b87..8094bd131 100644 --- a/internal/token/manager.go +++ b/internal/token/manager.go @@ -3,17 +3,21 @@ package token import ( + "crypto/rsa" "time" - - "github.com/golang-jwt/jwt/v5" ) +type RSAKeySet struct { + PrivateKey *rsa.PrivateKey + KID string +} + type Manager struct { - // PrivateKey key used to sign tokens - PrivateKey string + // PrivateKeyHMAC is the private key used to sign and validate closed-system tokens + PrivateKeyHMAC string - // SignMethod method to sign tokens - SignMethod jwt.SigningMethod + // RSAKeySet is the private key used to sign and validate open-system tokens (OIDC) + RSAKeySet RSAKeySet // UserAccessTokenDuration specifies the token duration to use for users UserAccessTokenDuration time.Duration @@ -29,4 +33,10 @@ type Manager struct { // WorkerRegisterTokenDuration specifies the token duration for worker register WorkerRegisterTokenDuration time.Duration + + // IDTokenDuration specifies the token duration for ID tokens + IDTokenDuration time.Duration + + // Issuer specifies the issuer of the token + Issuer string } diff --git a/internal/token/mint.go b/internal/token/mint.go index f4a4338a7..1cbb65663 100644 --- a/internal/token/mint.go +++ b/internal/token/mint.go @@ -3,36 +3,48 @@ package token import ( + "context" "errors" "fmt" "time" "github.com/golang-jwt/jwt/v5" + "gorm.io/gorm" api "github.com/go-vela/server/api/types" - "github.com/go-vela/types/constants" + "github.com/go-vela/server/constants" + "github.com/go-vela/server/database" ) // Claims struct is an extension of the JWT standard claims. It // includes information about the user. type Claims struct { - BuildID int64 `json:"build_id"` - IsActive bool `json:"is_active"` - IsAdmin bool `json:"is_admin"` - Repo string `json:"repo"` - TokenType string `json:"token_type"` + BuildID int64 `json:"build_id,omitempty"` + BuildNumber int `json:"build_number,omitempty"` + Actor string `json:"actor,omitempty"` + IsActive bool `json:"is_active,omitempty"` + IsAdmin bool `json:"is_admin,omitempty"` + Repo string `json:"repo,omitempty"` + TokenType string `json:"token_type,omitempty"` + Image string `json:"image,omitempty"` + Request string `json:"request,omitempty"` + Commands bool `json:"commands,omitempty"` jwt.RegisteredClaims } // MintTokenOpts is a type to inform the token minter how to construct // the token. type MintTokenOpts struct { - BuildID int64 + Build *api.Build Hostname string Repo string TokenDuration time.Duration TokenType string User *api.User + Audience []string + Image string + Request string + Commands bool } // MintToken mints a Vela JWT Token given a set of options. @@ -52,7 +64,7 @@ func (tm *Manager) MintToken(mto *MintTokenOpts) (string, error) { claims.Subject = mto.User.GetName() case constants.WorkerBuildTokenType: - if mto.BuildID == 0 { + if mto.Build.GetID() == 0 { return "", errors.New("missing build id for build token") } @@ -64,7 +76,7 @@ func (tm *Manager) MintToken(mto *MintTokenOpts) (string, error) { return "", errors.New("missing host name for build token") } - claims.BuildID = mto.BuildID + claims.BuildID = mto.Build.GetID() claims.Repo = mto.Repo claims.Subject = mto.Hostname @@ -75,6 +87,28 @@ func (tm *Manager) MintToken(mto *MintTokenOpts) (string, error) { claims.Subject = mto.Hostname + case constants.IDRequestTokenType: + if len(mto.Repo) == 0 { + return "", errors.New("missing repo for ID request token") + } + + if mto.Build == nil { + return "", errors.New("missing build for ID request token") + } + + if mto.Build.GetID() == 0 { + return "", errors.New("missing build id for ID request token") + } + + claims.Repo = mto.Repo + claims.Subject = fmt.Sprintf("repo:%s:ref:%s:event:%s", mto.Repo, mto.Build.GetRef(), mto.Build.GetEvent()) + claims.BuildID = mto.Build.GetID() + claims.BuildNumber = mto.Build.GetNumber() + claims.Actor = mto.Build.GetSender() + claims.Image = mto.Image + claims.Request = mto.Request + claims.Commands = mto.Commands + default: return "", errors.New("invalid token type") } @@ -83,10 +117,81 @@ func (tm *Manager) MintToken(mto *MintTokenOpts) (string, error) { claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(mto.TokenDuration)) claims.TokenType = mto.TokenType - tk := jwt.NewWithClaims(tm.SignMethod, claims) + tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + //sign token with configured private signing key + token, err := tk.SignedString([]byte(tm.PrivateKeyHMAC)) + if err != nil { + return "", fmt.Errorf("unable to sign token: %w", err) + } + + return token, nil +} + +// MintIDToken mints a Vela JWT ID Token for a build. +func (tm *Manager) MintIDToken(ctx context.Context, mto *MintTokenOpts, db database.Interface) (string, error) { + // initialize claims struct + var claims = new(api.OpenIDClaims) + + // validate provided claims + if len(mto.Repo) == 0 { + return "", errors.New("missing repo for ID token") + } + + if mto.Build == nil { + return "", errors.New("missing build for ID token") + } + + if mto.Build.GetNumber() == 0 { + return "", errors.New("missing build id for ID token") + } + + if len(mto.Build.GetSender()) == 0 { + return "", errors.New("missing build sender for ID token") + } + + // set claims based on input + claims.Actor = mto.Build.GetSender() + claims.ActorSCMID = mto.Build.GetSenderSCMID() + claims.BuildNumber = mto.Build.GetNumber() + claims.BuildID = mto.Build.GetID() + claims.Repo = mto.Repo + claims.Event = fmt.Sprintf("%s:%s", mto.Build.GetEvent(), mto.Build.GetEventAction()) + claims.SHA = mto.Build.GetCommit() + claims.Ref = mto.Build.GetRef() + claims.Subject = fmt.Sprintf("repo:%s:ref:%s:event:%s", mto.Repo, mto.Build.GetRef(), mto.Build.GetEvent()) + claims.Audience = mto.Audience + claims.TokenType = mto.TokenType + claims.Image = mto.Image + claims.Request = mto.Request + claims.Commands = mto.Commands + + // set standard claims + claims.IssuedAt = jwt.NewNumericDate(time.Now()) + claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(mto.TokenDuration)) + claims.Issuer = tm.Issuer + + tk := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + + // verify key is active in the database before signing + _, err := db.GetActiveJWK(ctx, tm.RSAKeySet.KID) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return "", fmt.Errorf("unable to get active public key: %w", err) + } + + // generate a new RSA key pair if previous key is inactive (rotated) + err = tm.GenerateRSA(ctx, db) + if err != nil { + return "", fmt.Errorf("unable to generate RSA key pair: %w", err) + } + } + + // set KID header + tk.Header["kid"] = tm.RSAKeySet.KID //sign token with configured private signing key - token, err := tk.SignedString([]byte(tm.PrivateKey)) + token, err := tk.SignedString(tm.RSAKeySet.PrivateKey) if err != nil { return "", fmt.Errorf("unable to sign token: %w", err) } diff --git a/internal/token/parse.go b/internal/token/parse.go index c42d90686..d5db3041e 100644 --- a/internal/token/parse.go +++ b/internal/token/parse.go @@ -42,7 +42,7 @@ func (tm *Manager) ParseToken(token string) (*Claims, error) { return nil, errors.New("token has no expiration") } - return []byte(tm.PrivateKey), err + return []byte(tm.PrivateKeyHMAC), err }) if err != nil { diff --git a/internal/token/parse_test.go b/internal/token/parse_test.go index a3efd14ea..600efe35e 100644 --- a/internal/token/parse_test.go +++ b/internal/token/parse_test.go @@ -21,9 +21,13 @@ func TestTokenManager_ParseToken(t *testing.T) { u.SetName("foo") u.SetToken("bar") + b := new(api.Build) + b.SetID(1) + b.SetNumber(1) + b.SetSender("octocat") + tm := &Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } @@ -74,7 +78,7 @@ func TestTokenManager_ParseToken(t *testing.T) { { TokenType: constants.WorkerBuildTokenType, Mto: &MintTokenOpts{ - BuildID: 1, + Build: b, Repo: "foo/bar", Hostname: "worker", TokenType: constants.WorkerBuildTokenType, @@ -122,8 +126,7 @@ func TestTokenManager_ParseToken_Error_NoParse(t *testing.T) { u.SetToken("bar") tm := &Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } @@ -147,8 +150,7 @@ func TestTokenManager_ParseToken_Expired(t *testing.T) { u.SetToken("bar") tm := &Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } @@ -179,8 +181,7 @@ func TestTokenManager_ParseToken_NoSubject(t *testing.T) { u.SetToken("bar") tm := &Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } @@ -195,7 +196,7 @@ func TestTokenManager_ParseToken_NoSubject(t *testing.T) { } tkn := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - token, err := tkn.SignedString([]byte(tm.PrivateKey)) + token, err := tkn.SignedString([]byte(tm.PrivateKeyHMAC)) if err != nil { t.Errorf("Unable to create test token: %v", err) } @@ -219,8 +220,7 @@ func TestTokenManager_ParseToken_Error_InvalidSignature(t *testing.T) { u.SetToken("bar") tm := &Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } @@ -236,7 +236,7 @@ func TestTokenManager_ParseToken_Error_InvalidSignature(t *testing.T) { } tkn := jwt.NewWithClaims(jwt.SigningMethodHS512, claims) - token, err := tkn.SignedString([]byte(tm.PrivateKey)) + token, err := tkn.SignedString([]byte(tm.PrivateKeyHMAC)) if err != nil { t.Errorf("Unable to create test token: %v", err) } @@ -260,8 +260,7 @@ func TestToken_Parse_AccessToken_NoExpiration(t *testing.T) { u.SetToken("bar") tm := &Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } diff --git a/internal/token/refresh_test.go b/internal/token/refresh_test.go index 139bb6634..49c809eb6 100644 --- a/internal/token/refresh_test.go +++ b/internal/token/refresh_test.go @@ -10,7 +10,6 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt/v5" api "github.com/go-vela/server/api/types" "github.com/go-vela/server/database" @@ -25,8 +24,7 @@ func TestTokenManager_Refresh(t *testing.T) { u.SetToken("bar") tm := &Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } @@ -85,8 +83,7 @@ func TestTokenManager_Refresh_Expired(t *testing.T) { u.SetToken("bar") tm := &Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } diff --git a/mock/server/authentication.go b/mock/server/authentication.go index 094915768..6e9f4512b 100644 --- a/mock/server/authentication.go +++ b/mock/server/authentication.go @@ -7,7 +7,9 @@ import ( "net/http" "github.com/gin-gonic/gin" + "github.com/lestrrat-go/jwx/jwk" + api "github.com/go-vela/server/api/types" "github.com/go-vela/types" "github.com/go-vela/types/constants" "github.com/go-vela/types/library" @@ -19,6 +21,43 @@ const ( TokenRefreshResp = `{ "token": "header.payload.signature" }` + + // OpenIDConfigResp represents a JSON return for an OpenID configuration. + OpenIDConfigResp = `{ + "issuer": "https://vela.com/_services/token", + "jwks_uri": "https://vela.com/_services/token/.well-known/jwks", + "supported_claims": [ + "sub", + "exp", + "iat", + "iss", + "aud", + "build_number", + "build_id", + "repo", + "token_type", + "actor", + "actor_scm_id", + "commands", + "image", + "request" + ], + "id_token_signing_alg_values_supported": [ + "RS256" + ] +}` + + // JWKSResp represents a JSON return for the JWKS. + JWKSResp = `{ + "keys": [ + { + "e": "AQAB", + "kid": "f7ec4ab7-c9a2-440e-bfb3-83b6599479ea", + "kty": "RSA", + "n": "weh9G_J6yZEugOFo6MQ057t_ExafteA_zVRS3CEPWiOgBLLRymh-KS6aCW-kHVuyBsnWNrCcc5cRJ6ISFnQMtkJtbpV_72qbw0zhFLiYomZDh5nb5dqCoiWIVNG8_a_My9jhXAIghs8MLbG-_Tj9jZb3K3n3Ies-Cg1E5SWO3YX8I1_X7ZlgqhEbktoy2RvR_crQA_fi1jRW5Q6PldIJmu4FIeXN_ny_sgg6ZQtTImFderUy1aUxUnpjilU-yv13eJejYQnJ7rExJVsDqq3B_CnYD2ioJC6b7aoEPvCpZ_1VgTTnQt6nedmr2Hih3GHgDNsM-BFr63aG3qZ5v9bVRw" + } + ] +` ) // getTokenRefresh returns mock JSON for a http GET. @@ -100,3 +139,23 @@ func validateOAuthToken(c *gin.Context) { c.JSON(http.StatusOK, "oauth token was created by vela") } + +// openIDConfig returns a mock response for a http GET. +func openIDConfig(c *gin.Context) { + data := []byte(OpenIDConfigResp) + + var body api.OpenIDConfig + _ = json.Unmarshal(data, &body) + + c.JSON(http.StatusOK, body) +} + +// getJWKS returns a mock response for a http GET. +func getJWKS(c *gin.Context) { + data := []byte(JWKSResp) + + var body jwk.RSAPublicKey + _ = json.Unmarshal(data, &body) + + c.JSON(http.StatusOK, body) +} diff --git a/mock/server/build.go b/mock/server/build.go index 36e9234d2..ebbe44b88 100644 --- a/mock/server/build.go +++ b/mock/server/build.go @@ -203,6 +203,20 @@ const ( "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJidWlsZF9pZCI6MSwicmVwbyI6ImZvby9iYXIiLCJzdWIiOiJPY3RvY2F0IiwiaWF0IjoxNTE2MjM5MDIyfQ.hD7gXpaf9acnLBdOBa4GOEa5KZxdzd0ZvK6fGwaN4bc" }` + // IDTokenResp represents a JSON return for requesting an ID token. + // + //nolint:gosec // not actual credentials + IDTokenResp = `{ + "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImY3ZWM0YWI3LWM5YTItNDQwZS1iZmIzLTgzYjY1OTk0NzllYSJ9.eyJidWlsZF9udW1iZXIiOjEsImFjdG9yIjoiT2N0b2NhdCIsInJlcG8iOiJPY3RvY2F0L3Rlc3QiLCJ0b2tlbl90eXBlIjoiSUQiLCJpbWFnZSI6ImFscGluZTpsYXRlc3QiLCJjb21tYW5kcyI6dHJ1ZSwicmVxdWVzdCI6IndyaXRlIiwiaXNzIjoiaHR0cHM6Ly92ZWxhLmNvbS9fc2VydmljZXMvdG9rZW4iLCJzdWIiOiJyZXBvOk9jdG9jYXQvdGVzdDpyZWY6cmVmcy9oZWFkcy9tYWluOmV2ZW50OnB1c2giLCJleHAiOjE3MTQ0OTU4MjMsImlhdCI6MTcxNDQ5NTUyMywiYXVkIjpbInRlc3QiLCJleGFtcGxlIl19.nCniV3r0TjJT0JGRtcubhhJnffo_uBUcYvFRlqSOHEOjRQo5cSwNfiePVicfWQYNbLYeJ_7HmxT2C27jCa2MjaYNXFXoVVAP9Cl-LIMqkpiIG85gHlsJaJILT_a8a1F6an0gK1st-iPB6h7casMXR479zdWnVX30WEE7Ed34T-ee6MOxYZ6-VDsVvxVLYe8dYMxvJkBWDI31djqzYfdWWWyYTPqfLCyRaojZeiCzKMt7m5GDRiOLXooYorv3ybizFoupr4md3Kk9MlArgMn7PYNGnmZuzJ01JrIUk6FTVty638yUmEIgiW7sdLzZG9HJ3E3hS6WJleXj--E95wmyyw" + }` + + // IDTokenRequestTokenResp represents a JSON return for requesting an IDTokenRequestToken. + // + //nolint:gosec // not actual credentials + IDTokenRequestTokenResp = `{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImY3ZWM0YWI3LWM5YTItNDQwZS1iZmIzLTgzYjY1OTk0NzllYSJ9.eyJidWlsZF9pZCI6MSwiYnVpbGRfbnVtYmVyIjoxLCJhY3RvciI6Ik9jdG9jYXQiLCJyZXBvIjoiT2N0b2NhdC90ZXN0IiwidG9rZW5fdHlwZSI6IklEUmVxdWVzdCIsImltYWdlIjoiYWxwaW5lOmxhdGVzdCIsImNvbW1hbmRzIjp0cnVlLCJyZXF1ZXN0Ijoid3JpdGUiLCJpc3MiOiJodHRwczovL3ZlbGEuY29tL19zZXJ2aWNlcy90b2tlbiIsInN1YiI6InJlcG86T2N0b2NhdC90ZXN0OnJlZjpyZWZzL2hlYWRzL21haW46ZXZlbnQ6cHVzaCIsImV4cCI6MTcxNDQ5NTgyMywiaWF0IjoxNzE0NDk1NTIzfQ.l7ulJ7g5iTWGrR_IOBC2borJj2yixRAMZpsZEeaMvUw" + }` + // BuildExecutableResp represents a JSON return for requesting a build executable. BuildExecutableResp = `{ "id": 1 @@ -420,6 +434,46 @@ func buildToken(c *gin.Context) { c.JSON(http.StatusOK, body) } +// idToken has a param :build returns mock JSON for a http GET. +// +// Pass "0" to :build to test receiving a http 400 response. +func idToken(c *gin.Context) { + b := c.Param("build") + + if strings.EqualFold(b, "0") { + c.AbortWithStatusJSON(http.StatusBadRequest, "") + + return + } + + data := []byte(IDTokenResp) + + var body library.Token + _ = json.Unmarshal(data, &body) + + c.JSON(http.StatusOK, body) +} + +// idTokenRequestToken has a param :build returns mock JSON for a http GET. +// +// Pass "0" to :build to test receiving a http 400 response. +func idTokenRequestToken(c *gin.Context) { + b := c.Param("build") + + if strings.EqualFold(b, "0") { + c.AbortWithStatusJSON(http.StatusBadRequest, "") + + return + } + + data := []byte(IDTokenRequestTokenResp) + + var body library.Token + _ = json.Unmarshal(data, &body) + + c.JSON(http.StatusOK, body) +} + // buildExecutable has a param :build returns mock JSON for a http GET. // // Pass "0" to :build to test receiving a http 500 response. diff --git a/mock/server/server.go b/mock/server/server.go index 9527cbc75..4b249cec3 100644 --- a/mock/server/server.go +++ b/mock/server/server.go @@ -45,6 +45,8 @@ func FakeHandler() http.Handler { e.DELETE("/api/v1/repos/:org/:repo/builds/:build", removeBuild) e.GET("/api/v1/repos/:org/:repo/builds/:build/token", buildToken) e.GET("/api/v1/repos/:org/:repo/builds/:build/executable", buildExecutable) + e.GET("/api/v1/repos/:org/:repo/builds/:build/id_token", idToken) + e.GET("/api/v1/repos/:org/:repo/builds/:build/id_request_token", idTokenRequestToken) // mock endpoints for dashboard calls e.GET("/api/v1/dashboards/:dashboard", getDashboard) @@ -146,6 +148,10 @@ func FakeHandler() http.Handler { e.GET("/validate-token", validateToken) e.GET("/validate-oauth", validateOAuthToken) + // mock endpoints for oidc calls + e.GET("/_services/token/.well-known/openid-configuration", openIDConfig) + e.GET("/_services/token/.well-known/jwks", getJWKS) + // mock endpoint for queue credentials e.GET("/api/v1/queue/info", getQueueCreds) diff --git a/router/admin.go b/router/admin.go index 22f83973e..d349fd7b0 100644 --- a/router/admin.go +++ b/router/admin.go @@ -49,6 +49,9 @@ func AdminHandlers(base *gin.RouterGroup) { // Admin repo endpoint _admin.PUT("/repo", admin.UpdateRepo) + // Admin rotate keys endpoint + _admin.POST("/rotate_oidc_keys", admin.RotateOIDCKeys) + // Admin secret endpoint _admin.PUT("/secret", admin.UpdateSecret) diff --git a/router/build.go b/router/build.go index 44091b1f0..924576763 100644 --- a/router/build.go +++ b/router/build.go @@ -63,6 +63,8 @@ func BuildHandlers(base *gin.RouterGroup) { b.DELETE("/cancel", executors.Establish(), perm.MustWrite(), build.CancelBuild) b.GET("/logs", perm.MustRead(), log.ListLogsForBuild) b.GET("/token", perm.MustWorkerAuthToken(), build.GetBuildToken) + b.GET("/id_token", perm.MustIDRequestToken(), build.GetIDToken) + b.GET("/id_request_token", perm.MustBuildAccess(), build.GetIDRequestToken) b.GET("/graph", perm.MustRead(), build.GetBuildGraph) b.GET("/executable", perm.MustBuildAccess(), build.GetBuildExecutable) diff --git a/router/middleware/claims/claims_test.go b/router/middleware/claims/claims_test.go index 4ef838a3d..fa61c773f 100644 --- a/router/middleware/claims/claims_test.go +++ b/router/middleware/claims/claims_test.go @@ -60,9 +60,13 @@ func TestClaims_Establish(t *testing.T) { user.SetAdmin(false) user.SetFavorites([]string{}) + build := new(api.Build) + build.SetID(1) + build.SetNumber(1) + build.SetSender("octocat") + tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, WorkerAuthTokenDuration: time.Minute * 20, @@ -112,7 +116,7 @@ func TestClaims_Establish(t *testing.T) { }, Mto: &token.MintTokenOpts{ Hostname: "host", - BuildID: 1, + Build: build, Repo: "foo/bar", TokenDuration: time.Minute * 35, TokenType: constants.WorkerBuildTokenType, @@ -223,8 +227,7 @@ func TestClaims_Establish(t *testing.T) { func TestClaims_Establish_NoToken(t *testing.T) { // setup types tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } @@ -249,8 +252,7 @@ func TestClaims_Establish_NoToken(t *testing.T) { func TestClaims_Establish_BadToken(t *testing.T) { // setup types tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } diff --git a/router/middleware/perm/perm.go b/router/middleware/perm/perm.go index f32d34dea..e0f11979d 100644 --- a/router/middleware/perm/perm.go +++ b/router/middleware/perm/perm.go @@ -10,6 +10,7 @@ import ( "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/go-vela/server/constants" "github.com/go-vela/server/router/middleware/build" "github.com/go-vela/server/router/middleware/claims" "github.com/go-vela/server/router/middleware/org" @@ -17,7 +18,6 @@ import ( "github.com/go-vela/server/router/middleware/user" "github.com/go-vela/server/scm" "github.com/go-vela/server/util" - "github.com/go-vela/types/constants" ) // MustPlatformAdmin ensures the user has admin access to the platform. @@ -173,6 +173,48 @@ func MustBuildAccess() gin.HandlerFunc { } } +// MustIDRequestToken ensures the token is a valid ID request token for the appropriate build. +func MustIDRequestToken() gin.HandlerFunc { + return func(c *gin.Context) { + cl := claims.Retrieve(c) + b := build.Retrieve(c) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "repo": cl.Subject, + }).Debugf("verifying worker %s has a valid build token", cl.Subject) + + // verify expected type + if !strings.EqualFold(cl.TokenType, constants.IDRequestTokenType) { + retErr := fmt.Errorf("invalid token: must provide a valid request ID token") + util.HandleError(c, http.StatusUnauthorized, retErr) + + return + } + + // if build is not in a running state, then an ID token should not be needed + if !strings.EqualFold(b.GetStatus(), constants.StatusRunning) { + util.HandleError(c, http.StatusBadRequest, fmt.Errorf("invalid request")) + + return + } + + // verify expected build id + if b.GetID() != cl.BuildID { + logrus.WithFields(logrus.Fields{ + "user": cl.Subject, + "repo": cl.Repo, + "build": cl.BuildID, + }).Warnf("request ID token for build %d attempted to be used for %s build %d by %s", cl.BuildID, b.GetStatus(), b.GetID(), cl.Subject) + + retErr := fmt.Errorf("invalid token") + util.HandleError(c, http.StatusUnauthorized, retErr) + } + } +} + // MustSecretAdmin ensures the user has admin access to the org, repo or team. // //nolint:funlen // ignore function length diff --git a/router/middleware/perm/perm_test.go b/router/middleware/perm/perm_test.go index 8ca690b50..772aecf14 100644 --- a/router/middleware/perm/perm_test.go +++ b/router/middleware/perm/perm_test.go @@ -11,9 +11,9 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt/v5" api "github.com/go-vela/server/api/types" + "github.com/go-vela/server/constants" "github.com/go-vela/server/database" "github.com/go-vela/server/internal/token" "github.com/go-vela/server/router/middleware/build" @@ -23,7 +23,6 @@ import ( "github.com/go-vela/server/router/middleware/user" "github.com/go-vela/server/scm" "github.com/go-vela/server/scm/github" - "github.com/go-vela/types/constants" ) func TestPerm_MustPlatformAdmin(t *testing.T) { @@ -31,8 +30,7 @@ func TestPerm_MustPlatformAdmin(t *testing.T) { secret := "superSecret" tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } @@ -112,8 +110,7 @@ func TestPerm_MustPlatformAdmin_NotAdmin(t *testing.T) { secret := "superSecret" tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } @@ -191,8 +188,7 @@ func TestPerm_MustPlatformAdmin_NotAdmin(t *testing.T) { func TestPerm_MustWorkerRegisterToken(t *testing.T) { // setup types tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, WorkerRegisterTokenDuration: time.Minute * 1, @@ -238,8 +234,7 @@ func TestPerm_MustWorkerRegisterToken(t *testing.T) { func TestPerm_MustWorkerRegisterToken_PlatAdmin(t *testing.T) { tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } @@ -304,8 +299,7 @@ func TestPerm_MustWorkerRegisterToken_PlatAdmin(t *testing.T) { func TestPerm_MustWorkerAuthToken(t *testing.T) { // setup types tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, WorkerRegisterTokenDuration: time.Minute * 1, @@ -353,8 +347,7 @@ func TestPerm_MustWorkerAuth_ServerWorkerToken(t *testing.T) { // setup types secret := "superSecret" tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, WorkerRegisterTokenDuration: time.Minute * 1, @@ -413,15 +406,14 @@ func TestPerm_MustBuildAccess(t *testing.T) { b.SetNumber(1) tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } mto := &token.MintTokenOpts{ Hostname: "worker", - BuildID: 1, + Build: b, Repo: "foo/bar", TokenDuration: time.Minute * 30, TokenType: constants.WorkerBuildTokenType, @@ -508,8 +500,7 @@ func TestPerm_MustBuildAccess_PlatAdmin(t *testing.T) { u.SetAdmin(true) tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } @@ -596,16 +587,18 @@ func TestPerm_MustBuildToken_WrongBuild(t *testing.T) { b.SetRepo(r) b.SetNumber(1) + wB := new(api.Build) + wB.SetID(2) + tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } mto := &token.MintTokenOpts{ Hostname: "worker", - BuildID: 2, + Build: wB, Repo: "foo/bar", TokenDuration: time.Minute * 30, TokenType: constants.WorkerBuildTokenType, @@ -664,6 +657,288 @@ func TestPerm_MustBuildToken_WrongBuild(t *testing.T) { } } +func TestPerm_MustIDRequestToken(t *testing.T) { + // setup types + secret := "superSecret" + + owner := new(api.User) + owner.SetID(1) + + r := new(api.Repo) + r.SetID(1) + r.SetOwner(owner) + r.SetHash("baz") + r.SetOrg("foo") + r.SetName("bar") + r.SetFullName("foo/bar") + r.SetVisibility("public") + + b := new(api.Build) + b.SetID(1) + b.SetRepo(r) + b.SetNumber(1) + b.SetStatus(constants.StatusRunning) + b.SetCommit("456def") + + tm := &token.Manager{ + PrivateKeyHMAC: "123abc", + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + + mto := &token.MintTokenOpts{ + Hostname: "foo/bar/456def", + Build: b, + Repo: r.GetFullName(), + TokenDuration: time.Minute * 30, + TokenType: constants.IDRequestTokenType, + } + + tok, err := tm.MintToken(mto) + if err != nil { + t.Errorf("unable to mint token: %v", err) + } + + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + + // setup database + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + + ctx := _context.TODO() + + defer func() { + _ = db.DeleteBuild(ctx, b) + _ = db.DeleteRepo(_context.TODO(), r) + db.Close() + }() + + _, _ = db.CreateRepo(_context.TODO(), r) + _, _ = db.CreateBuild(ctx, b) + + context.Request, _ = http.NewRequest(http.MethodGet, "/test/foo/bar/builds/1", nil) + 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(func(c *gin.Context) { database.ToContext(c, db) }) + engine.Use(claims.Establish()) + engine.Use(user.Establish()) + engine.Use(org.Establish()) + engine.Use(repo.Establish()) + engine.Use(build.Establish()) + engine.Use(MustIDRequestToken()) + engine.GET("/test/:org/:repo/builds/:build", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + + s1 := httptest.NewServer(engine) + defer s1.Close() + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusOK { + t.Errorf("MustIDRequestToken returned %v, want %v: %v", resp.Code, http.StatusOK, resp.Body.String()) + } +} + +func TestPerm_MustIDRequestToken_NotRunning(t *testing.T) { + // setup types + secret := "superSecret" + + owner := new(api.User) + owner.SetID(1) + + r := new(api.Repo) + r.SetID(1) + r.SetOwner(owner) + r.SetHash("baz") + r.SetOrg("foo") + r.SetName("bar") + r.SetFullName("foo/bar") + r.SetVisibility("public") + + b := new(api.Build) + b.SetID(1) + b.SetRepo(r) + b.SetNumber(1) + b.SetStatus(constants.StatusSuccess) + b.SetCommit("456def") + + u := new(api.User) + u.SetID(1) + u.SetName("admin") + u.SetToken("bar") + u.SetAdmin(true) + + tm := &token.Manager{ + PrivateKeyHMAC: "123abc", + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + + mto := &token.MintTokenOpts{ + Hostname: "foo/bar/456def", + Build: b, + Repo: "foo/bar", + TokenDuration: time.Minute * 30, + TokenType: constants.IDRequestTokenType, + } + + tok, _ := tm.MintToken(mto) + + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + + ctx := _context.TODO() + + // setup database + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + + defer func() { + _ = db.DeleteBuild(ctx, b) + _ = db.DeleteRepo(_context.TODO(), r) + _ = db.DeleteUser(_context.TODO(), u) + db.Close() + }() + + _, _ = db.CreateRepo(_context.TODO(), r) + _, _ = db.CreateBuild(ctx, b) + _, _ = db.CreateUser(_context.TODO(), u) + + context.Request, _ = http.NewRequest(http.MethodGet, "/test/foo/bar/builds/1", nil) + 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(func(c *gin.Context) { database.ToContext(c, db) }) + engine.Use(claims.Establish()) + engine.Use(user.Establish()) + engine.Use(org.Establish()) + engine.Use(repo.Establish()) + engine.Use(build.Establish()) + engine.Use(MustIDRequestToken()) + engine.GET("/test/:org/:repo/builds/:build", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + + s1 := httptest.NewServer(engine) + defer s1.Close() + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusBadRequest { + t.Errorf("MustIDRequestToken returned %v, want %v", resp.Code, http.StatusOK) + } +} + +func TestPerm_MustIDRequestToken_WrongBuild(t *testing.T) { + // setup types + secret := "superSecret" + + owner := new(api.User) + owner.SetID(1) + + r := new(api.Repo) + r.SetID(1) + r.SetOwner(owner) + r.SetHash("baz") + r.SetOrg("foo") + r.SetName("bar") + r.SetFullName("foo/bar") + r.SetVisibility("public") + + b := new(api.Build) + b.SetID(1) + b.SetRepo(r) + b.SetNumber(1) + + wB := new(api.Build) + wB.SetID(2) + + tm := &token.Manager{ + PrivateKeyHMAC: "123abc", + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + + mto := &token.MintTokenOpts{ + Hostname: "foo/bar/456def", + Build: wB, + Repo: "foo/bar", + TokenDuration: time.Minute * 30, + TokenType: constants.IDRequestTokenType, + } + + tok, _ := tm.MintToken(mto) + + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + + ctx := _context.TODO() + + // setup database + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + + defer func() { + _ = db.DeleteBuild(ctx, b) + _ = db.DeleteRepo(_context.TODO(), r) + db.Close() + }() + + _, _ = db.CreateRepo(_context.TODO(), r) + _, _ = db.CreateBuild(ctx, b) + + context.Request, _ = http.NewRequest(http.MethodGet, "/test/foo/bar/builds/1", nil) + 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(func(c *gin.Context) { database.ToContext(c, db) }) + engine.Use(claims.Establish()) + engine.Use(user.Establish()) + engine.Use(org.Establish()) + engine.Use(repo.Establish()) + engine.Use(build.Establish()) + engine.Use(MustIDRequestToken()) + engine.GET("/test/:org/:repo/builds/:build", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + + s1 := httptest.NewServer(engine) + defer s1.Close() + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusBadRequest { + t.Errorf("MustBuildAccess returned %v, want %v", resp.Code, http.StatusBadRequest) + } +} + func TestPerm_MustSecretAdmin_BuildToken_Repo(t *testing.T) { // setup types secret := "superSecret" @@ -686,15 +961,14 @@ func TestPerm_MustSecretAdmin_BuildToken_Repo(t *testing.T) { b.SetNumber(1) tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } mto := &token.MintTokenOpts{ Hostname: "worker", - BuildID: 1, + Build: b, Repo: "foo/bar", TokenDuration: time.Minute * 30, TokenType: constants.WorkerBuildTokenType, @@ -772,15 +1046,14 @@ func TestPerm_MustSecretAdmin_BuildToken_Org(t *testing.T) { b.SetNumber(1) tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } mto := &token.MintTokenOpts{ Hostname: "worker", - BuildID: 1, + Build: b, Repo: "foo/bar", TokenDuration: time.Minute * 30, TokenType: constants.WorkerBuildTokenType, @@ -858,15 +1131,14 @@ func TestPerm_MustSecretAdmin_BuildToken_Shared(t *testing.T) { b.SetNumber(1) tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } mto := &token.MintTokenOpts{ Hostname: "worker", - BuildID: 1, + Build: b, Repo: "foo/bar", TokenDuration: time.Minute * 30, TokenType: constants.WorkerBuildTokenType, @@ -927,8 +1199,7 @@ func TestPerm_MustAdmin(t *testing.T) { secret := "superSecret" tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } @@ -1027,8 +1298,7 @@ func TestPerm_MustAdmin_PlatAdmin(t *testing.T) { secret := "superSecret" tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } @@ -1127,8 +1397,7 @@ func TestPerm_MustAdmin_NotAdmin(t *testing.T) { secret := "superSecret" tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } @@ -1227,8 +1496,7 @@ func TestPerm_MustWrite(t *testing.T) { secret := "superSecret" tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } @@ -1327,8 +1595,7 @@ func TestPerm_MustWrite_PlatAdmin(t *testing.T) { secret := "superSecret" tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } @@ -1427,8 +1694,7 @@ func TestPerm_MustWrite_RepoAdmin(t *testing.T) { secret := "superSecret" tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } @@ -1527,8 +1793,7 @@ func TestPerm_MustWrite_NotWrite(t *testing.T) { secret := "superSecret" tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } @@ -1627,8 +1892,7 @@ func TestPerm_MustRead(t *testing.T) { secret := "superSecret" tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } @@ -1727,8 +1991,7 @@ func TestPerm_MustRead_PlatAdmin(t *testing.T) { secret := "superSecret" tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } @@ -1827,8 +2090,7 @@ func TestPerm_MustRead_WorkerBuildToken(t *testing.T) { secret := "superSecret" tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } @@ -1854,7 +2116,7 @@ func TestPerm_MustRead_WorkerBuildToken(t *testing.T) { Hostname: "worker", TokenDuration: time.Minute * 35, TokenType: constants.WorkerBuildTokenType, - BuildID: 1, + Build: b, Repo: "foo/bar", } @@ -1916,8 +2178,7 @@ func TestPerm_MustRead_RepoAdmin(t *testing.T) { secret := "superSecret" tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } @@ -2016,8 +2277,7 @@ func TestPerm_MustRead_RepoWrite(t *testing.T) { secret := "superSecret" tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } @@ -2116,8 +2376,7 @@ func TestPerm_MustRead_RepoPublic(t *testing.T) { secret := "superSecret" tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } @@ -2216,8 +2475,7 @@ func TestPerm_MustRead_NotRead(t *testing.T) { secret := "superSecret" tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } diff --git a/router/middleware/pipeline/pipeline_test.go b/router/middleware/pipeline/pipeline_test.go index d7af22303..5a8c6d608 100644 --- a/router/middleware/pipeline/pipeline_test.go +++ b/router/middleware/pipeline/pipeline_test.go @@ -13,7 +13,6 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt/v5" "github.com/urfave/cli/v2" api "github.com/go-vela/server/api/types" @@ -227,8 +226,7 @@ func TestPipeline_Establish_NoPipeline(t *testing.T) { secret := "superSecret" tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } diff --git a/router/middleware/token_manager_test.go b/router/middleware/token_manager_test.go index 8f8bb4fa2..6daee4e88 100644 --- a/router/middleware/token_manager_test.go +++ b/router/middleware/token_manager_test.go @@ -21,7 +21,7 @@ func TestMiddleware_TokenManager(t *testing.T) { var got *token.Manager want := new(token.Manager) - want.PrivateKey = "123abc" + want.PrivateKeyHMAC = "123abc" // setup context gin.SetMode(gin.TestMode) diff --git a/router/middleware/user/user_test.go b/router/middleware/user/user_test.go index a6809c334..8e15f80f7 100644 --- a/router/middleware/user/user_test.go +++ b/router/middleware/user/user_test.go @@ -12,7 +12,6 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt/v5" api "github.com/go-vela/server/api/types" "github.com/go-vela/server/database" @@ -47,8 +46,7 @@ func TestUser_Establish(t *testing.T) { secret := "superSecret" tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } @@ -144,8 +142,7 @@ func TestUser_Establish_NoToken(t *testing.T) { // setup types secret := "superSecret" tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } @@ -183,8 +180,7 @@ func TestUser_Establish_DiffTokenType(t *testing.T) { secret := "superSecret" tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } @@ -232,8 +228,7 @@ func TestUser_Establish_NoAuthorizeUser(t *testing.T) { secret := "superSecret" tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } @@ -274,8 +269,7 @@ func TestUser_Establish_NoAuthorizeUser(t *testing.T) { func TestUser_Establish_NoUser(t *testing.T) { // setup types tm := &token.Manager{ - PrivateKey: "123abc", - SignMethod: jwt.SigningMethodHS256, + PrivateKeyHMAC: "123abc", UserAccessTokenDuration: time.Minute * 5, UserRefreshTokenDuration: time.Minute * 30, } diff --git a/router/router.go b/router/router.go index 49d8f6012..2e7aebbb2 100644 --- a/router/router.go +++ b/router/router.go @@ -89,6 +89,10 @@ func Load(options ...gin.HandlerFunc) *gin.Engine { // Webhook endpoint r.POST("/webhook", webhook.PostWebhook) + // JWKS endpoints + r.GET("/_services/token/.well-known/openid-configuration", api.GetOpenIDConfig) + r.GET("/_services/token/.well-known/jwks", api.GetJWKS) + // Authentication endpoints authenticate := r.Group("/authenticate") {