From c3cd04fdce5bc7ec209c65e24cce1dffbf9f4c9e Mon Sep 17 00:00:00 2001 From: s3lph <5564491+s3lph@users.noreply.github.com> Date: Sun, 16 Apr 2023 23:57:56 +0200 Subject: [PATCH 001/346] feat: implement oidc authentication --- api-docs.yml | 61 +++++++++++++ api/login.go | 203 +++++++++++++++++++++++++++++++++++++++++ api/router.go | 4 +- api/users.go | 4 +- go.mod | 11 ++- go.sum | 34 +++++++ util/config.go | 15 +++ web/src/views/Auth.vue | 24 +++++ 8 files changed, 350 insertions(+), 6 deletions(-) diff --git a/api-docs.yml b/api-docs.yml index 24544ed9..6c537807 100644 --- a/api-docs.yml +++ b/api-docs.yml @@ -44,6 +44,24 @@ definitions: format: password description: Password + LoginMetadata: + type: object + properties: + oidc_providers: + type: array + description: List of OIDC providers + items: + type: object + properties: + id: + type: string + description: ID of the provider, used in the login URL + x-example: mysso + name: + type: string + description: Text to show on the login button + x-example: Sign in with MySSO + UserRequest: type: object properties: @@ -610,6 +628,17 @@ paths: # Authentication /auth/login: + get: + tags: + - authentication + summary: Fetches login metadata + description: Fetches metadata for login, such as available OIDC providers + security: [] + responses: + 200: + description: Login metadata + schema: + $ref: "#/definitions/LoginMetadata" post: tags: - authentication @@ -637,6 +666,38 @@ paths: 204: description: Your session was successfully nuked + /auth/oidc/{provider_id}/login: + parameters: + - name: provider_id + in: path + type: string + required: true + x-example: "mysso" + get: + tags: + - authentication + summary: Begin OIDC authentication flow and redirect to OIDC provider + description: The user agent is redirected to this endpoint when chosing to sign in via OIDC + responses: + 302: + description: Redirection to the OIDC provider on success, or to the login page on error + + /auth/oidc/{provider_id}/redirect: + parameters: + - name: provider_id + in: path + type: string + required: true + x-example: "mysso" + get: + tags: + - authentication + summary: Finish OIDC authentication flow, upon succes you will be logged in + description: The user agent is redirected here by the OIDC provider to complete authentication + responses: + 302: + description: Redirection to the Semaphore root URL on success, or to the login page on error + # User Tokens /user/: get: diff --git a/api/login.go b/api/login.go index 4b3b3d1f..074e14da 100644 --- a/api/login.go +++ b/api/login.go @@ -1,18 +1,25 @@ package api import ( + "context" "crypto/tls" + "encoding/base64" "encoding/json" "fmt" + "math/rand" "net/http" + "net/url" "strings" "time" "golang.org/x/crypto/bcrypt" + "golang.org/x/oauth2" "github.com/ansible-semaphore/semaphore/api/helpers" "github.com/ansible-semaphore/semaphore/db" + "github.com/coreos/go-oidc/v3/oidc" "github.com/go-ldap/ldap/v3" + "github.com/gorilla/mux" log "github.com/Sirupsen/logrus" "github.com/ansible-semaphore/semaphore/util" @@ -192,8 +199,33 @@ func loginByLDAP(store db.Store, ldapUser db.User) (user db.User, err error) { return } +type loginMetadataOidcProvider struct { + ID string `json:"id"` + Name string `json:"name"` +} + +type loginMetadata struct { + OidcProviders []loginMetadataOidcProvider `json:"oidc_providers"` +} + // nolint: gocyclo func login(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + config := &loginMetadata{ + OidcProviders: make([]loginMetadataOidcProvider, len(util.Config.OidcProviders)), + } + i := 0 + for k, v := range util.Config.OidcProviders { + config.OidcProviders[i] = loginMetadataOidcProvider{ + ID: k, + Name: v.DisplayName, + } + i++ + } + helpers.WriteJSON(w, http.StatusOK, config) + return + } + var login struct { Auth string `json:"auth" binding:"required"` Password string `json:"password" binding:"required"` @@ -264,3 +296,174 @@ func logout(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } + +func getOidcProvider(id string, ctx context.Context) (*oidc.Provider, *oauth2.Config, error) { + provider, ok := util.Config.OidcProviders[id] + if !ok { + return nil, nil, fmt.Errorf("No such provider: %s", id) + } + oidcProvider, err := oidc.NewProvider(ctx, provider.AutoDiscovery) + if err != nil { + return nil, nil, err + } + + oauthConfig := oauth2.Config{ + Endpoint: oidcProvider.Endpoint(), + ClientID: provider.ClientID, + ClientSecret: provider.ClientSecret, + RedirectURL: provider.RedirectURL, + Scopes: provider.Scopes, + } + if len(oauthConfig.RedirectURL) == 0 { + rurl, err := url.JoinPath(util.Config.WebHost, "api/auth/oidc", id, "redirect") + if err != nil { + return nil, nil, err + } + oauthConfig.RedirectURL = rurl + } + if len(oauthConfig.Scopes) == 0 { + oauthConfig.Scopes = []string{"openid", "profile", "email"} + } + return oidcProvider, &oauthConfig, nil +} + +func oidcLogin(w http.ResponseWriter, r *http.Request) { + pid := mux.Vars(r)["provider"] + ctx := context.Background() + _, oauth, err := getOidcProvider(pid, ctx) + if err != nil { + log.Error(err.Error()) + http.Redirect(w, r, "/auth/login", http.StatusTemporaryRedirect) + return + } + state := generateStateOauthCookie(w) + u := oauth.AuthCodeURL(state) + http.Redirect(w, r, u, http.StatusTemporaryRedirect) +} + +func generateStateOauthCookie(w http.ResponseWriter) string { + expiration := time.Now().Add(365 * 24 * time.Hour) + + b := make([]byte, 16) + rand.Read(b) + oauthState := base64.URLEncoding.EncodeToString(b) + cookie := http.Cookie{Name: "oauthstate", Value: oauthState, Expires: expiration} + http.SetCookie(w, &cookie) + + return oauthState +} + +func oidcRedirect(w http.ResponseWriter, r *http.Request) { + pid := mux.Vars(r)["provider"] + oauthState, err := r.Cookie("oauthstate") + if err != nil { + log.Error(err.Error()) + http.Redirect(w, r, "/auth/login", http.StatusTemporaryRedirect) + return + } + + ctx := context.Background() + _oidc, oauth, err := getOidcProvider(pid, ctx) + if err != nil { + log.Error(err.Error()) + http.Redirect(w, r, "/auth/login", http.StatusTemporaryRedirect) + return + } + provider, ok := util.Config.OidcProviders[pid] + if !ok { + log.Error(fmt.Errorf("No such provider: %s", pid)) + http.Redirect(w, r, "/auth/login", http.StatusTemporaryRedirect) + return + } + verifier := _oidc.Verifier(&oidc.Config{ClientID: oauth.ClientID}) + + if r.FormValue("state") != oauthState.Value { + http.Redirect(w, r, "/auth/login", http.StatusTemporaryRedirect) + return + } + + oauth2Token, err := oauth.Exchange(ctx, r.URL.Query().Get("code")) + if err != nil { + log.Error(err.Error()) + http.Redirect(w, r, "/auth/login", http.StatusTemporaryRedirect) + return + } + + // Extract the ID Token from OAuth2 token. + rawIDToken, ok := oauth2Token.Extra("id_token").(string) + if !ok { + log.Error(fmt.Errorf("id_token is missing in token response")) + http.Redirect(w, r, "/auth/login", http.StatusTemporaryRedirect) + return + } + + // Parse and verify ID Token payload. + idToken, err := verifier.Verify(ctx, rawIDToken) + if err != nil { + log.Error(err.Error()) + http.Redirect(w, r, "/auth/login", http.StatusTemporaryRedirect) + return + } + + // Extract custom claims + claims := make(map[string]interface{}) + if err := idToken.Claims(&claims); err != nil { + log.Error(err.Error()) + http.Redirect(w, r, "/auth/login", http.StatusTemporaryRedirect) + return + } + + if len(provider.UsernameClaim) == 0 { + provider.UsernameClaim = "preferred_username" + } + usernameClaim, ok := claims[provider.UsernameClaim].(string) + if !ok { + log.Error(fmt.Errorf("Claim '%s' missing from id_token or not a string", provider.UsernameClaim)) + http.Redirect(w, r, "/auth/login", http.StatusTemporaryRedirect) + return + } + if len(provider.NameClaim) == 0 { + provider.NameClaim = "preferred_username" + } + nameClaim, ok := claims[provider.NameClaim].(string) + if !ok { + log.Error(fmt.Errorf("Claim '%s' missing from id_token or not a string", provider.NameClaim)) + http.Redirect(w, r, "/auth/login", http.StatusTemporaryRedirect) + return + } + if len(provider.EmailClaim) == 0 { + provider.EmailClaim = "email" + } + emailClaim, ok := claims[provider.EmailClaim].(string) + if !ok { + log.Error(fmt.Errorf("Claim '%s' missing from id_token or not a string", provider.EmailClaim)) + http.Redirect(w, r, "/auth/login", http.StatusTemporaryRedirect) + return + } + + user, err := helpers.Store(r).GetUserByLoginOrEmail(usernameClaim, emailClaim) + if err != nil { + user = db.User{ + Username: usernameClaim, + Name: nameClaim, + Email: emailClaim, + External: true, + } + user, err = helpers.Store(r).CreateUserWithoutPassword(user) + if err != nil { + log.Error(err.Error()) + http.Redirect(w, r, "/auth/login", http.StatusTemporaryRedirect) + return + } + } + + if !user.External { + log.Error(fmt.Errorf("OIDC user '%s' conflicts with local user", user.Username)) + http.Redirect(w, r, "/auth/login", http.StatusTemporaryRedirect) + return + } + + createSession(w, r, user) + + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) +} diff --git a/api/router.go b/api/router.go index 1a51c31d..bbde018a 100644 --- a/api/router.go +++ b/api/router.go @@ -81,8 +81,10 @@ func Route() *mux.Router { publicAPIRouter.Use(StoreMiddleware, JSONMiddleware) - publicAPIRouter.HandleFunc("/auth/login", login).Methods("POST") + publicAPIRouter.HandleFunc("/auth/login", login).Methods("GET", "POST") publicAPIRouter.HandleFunc("/auth/logout", logout).Methods("POST") + publicAPIRouter.HandleFunc("/auth/oidc/{provider}/login", oidcLogin).Methods("GET") + publicAPIRouter.HandleFunc("/auth/oidc/{provider}/redirect", oidcRedirect).Methods("GET") authenticatedWS := r.PathPrefix(webPath + "api").Subrouter() authenticatedWS.Use(JSONMiddleware, authenticationWithStore) diff --git a/api/users.go b/api/users.go index 2296300e..5146dcdc 100644 --- a/api/users.go +++ b/api/users.go @@ -94,7 +94,7 @@ func updateUser(w http.ResponseWriter, r *http.Request) { } if oldUser.External && oldUser.Username != user.Username { - log.Warn("Username is not editable for external LDAP users") + log.Warn("Username is not editable for external users") w.WriteHeader(http.StatusBadRequest) return } @@ -124,7 +124,7 @@ func updateUserPassword(w http.ResponseWriter, r *http.Request) { } if user.External { - log.Warn("Password is not editable for external LDAP users") + log.Warn("Password is not editable for external users") w.WriteHeader(http.StatusBadRequest) return } diff --git a/go.mod b/go.mod index 296c301a..98265632 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.18 require ( github.com/Sirupsen/logrus v1.0.4 + github.com/coreos/go-oidc/v3 v3.5.0 github.com/go-git/go-git/v5 v5.4.2 github.com/go-gorp/gorp/v3 v3.0.2 github.com/go-ldap/ldap/v3 v3.4.1 @@ -22,6 +23,7 @@ require ( github.com/spf13/cobra v1.2.1 go.etcd.io/bbolt v1.3.2 golang.org/x/crypto v0.3.0 + golang.org/x/oauth2 v0.7.0 ) require ( @@ -34,6 +36,8 @@ require ( github.com/go-asn1-ber/asn1-ber v1.5.1 // indirect github.com/go-git/gcfg v1.5.0 // indirect github.com/go-git/go-billy/v5 v5.4.0 // indirect + github.com/go-jose/go-jose/v3 v3.0.0 // indirect + github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/imdario/mergo v0.3.13 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect @@ -46,10 +50,11 @@ require ( github.com/sergi/go-diff v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - golang.org/x/net v0.2.0 // indirect - golang.org/x/sys v0.3.0 // indirect - golang.org/x/term v0.2.0 // indirect + golang.org/x/net v0.9.0 // indirect + golang.org/x/sys v0.7.0 // indirect + golang.org/x/term v0.7.0 // indirect google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.28.0 // indirect gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect diff --git a/go.sum b/go.sum index af36d09c..7c973bbc 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,7 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= @@ -73,6 +74,8 @@ github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtM github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/coreos/go-oidc/v3 v3.5.0 h1:VxKtbccHZxs8juq7RdJntSqtXFtde9YpNpGn0yqgEHw= +github.com/coreos/go-oidc/v3 v3.5.0/go.mod h1:ecXRtV4romGPeO6ieExAsUK9cb/3fp9hXNz1tlv8PIM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= @@ -113,6 +116,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gorp/gorp/v3 v3.0.2 h1:ULqJXIekoqMx29FI5ekXXFoH1dT2Vc8UhnRzBg+Emz4= github.com/go-gorp/gorp/v3 v3.0.2/go.mod h1:BJ3q1ejpV8cVALtcXvXaXyTOlMmJhWDxTmncaR6rwBY= +github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= +github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-ldap/ldap/v3 v3.4.1 h1:fU/0xli6HY02ocbMuozHAYsaHLcnkLjvho2r5a34BUU= github.com/go-ldap/ldap/v3 v3.4.1/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg= github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= @@ -149,6 +154,7 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -164,6 +170,8 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= @@ -332,6 +340,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk= @@ -355,6 +364,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -399,6 +409,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -437,8 +448,13 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -451,6 +467,9 @@ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk= +golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= +golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -462,6 +481,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -512,12 +532,20 @@ golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -526,7 +554,10 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -581,6 +612,7 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -688,6 +720,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/util/config.go b/util/config.go index 1dd779c2..328411f1 100644 --- a/util/config.go +++ b/util/config.go @@ -49,6 +49,18 @@ type ldapMappings struct { CN string `json:"cn"` } +type oidcProvider struct { + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` + RedirectURL string `json:"redirect_url"` + Scopes []string `json:"scopes"` + DisplayName string `json:"display_name"` + AutoDiscovery string `json:"provider_url"` + UsernameClaim string `json:"username_claim"` + NameClaim string `json:"name_claim"` + EmailClaim string `json:"email_claim"` +} + // ConfigType mapping between Config and the json file that sets it type ConfigType struct { MySQL DbConfig `json:"mysql"` @@ -94,6 +106,9 @@ type ConfigType struct { LdapSearchFilter string `json:"ldap_searchfilter"` LdapMappings ldapMappings `json:"ldap_mappings"` + // oidc settings + OidcProviders map[string]oidcProvider `json:"oidc_providers"` + // telegram alerting TelegramChat string `json:"telegram_chat"` TelegramToken string `json:"telegram_token"` diff --git a/web/src/views/Auth.vue b/web/src/views/Auth.vue index f297ce5b..e161043e 100644 --- a/web/src/views/Auth.vue +++ b/web/src/views/Auth.vue @@ -111,6 +111,17 @@ Sign In + + {{ provider.name }} + +
Don't have account or can't sign in?
@@ -138,6 +149,8 @@ export default { username: null, loginHelpDialog: null, + + oidcProviders: [], }; }, @@ -145,6 +158,13 @@ export default { if (this.isAuthenticated()) { document.location = document.baseURI; } + await axios({ + method: 'get', + url: '/api/auth/login', + responseType: 'json', + }).then((resp) => { + this.oidcProviders = resp.data.oidc_providers; + }); }, methods: { @@ -191,6 +211,10 @@ export default { this.signInProcess = false; } }, + + async oidcSignIn(provider) { + document.location = `/api/auth/oidc/${provider}/login`; + }, }, }; From 2896dc72d988a31308ed5922ca30728cee2ed915 Mon Sep 17 00:00:00 2001 From: s3lph <5564491+s3lph@users.noreply.github.com> Date: Mon, 17 Apr 2023 22:57:50 +0200 Subject: [PATCH 002/346] feat(oidc): add config option to manually configure provider endpoints --- api/login.go | 18 +++++++++++++++--- util/config.go | 28 +++++++++++++++++++--------- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/api/login.go b/api/login.go index 074e14da..c90bcc17 100644 --- a/api/login.go +++ b/api/login.go @@ -302,9 +302,21 @@ func getOidcProvider(id string, ctx context.Context) (*oidc.Provider, *oauth2.Co if !ok { return nil, nil, fmt.Errorf("No such provider: %s", id) } - oidcProvider, err := oidc.NewProvider(ctx, provider.AutoDiscovery) - if err != nil { - return nil, nil, err + config := oidc.ProviderConfig{ + IssuerURL: provider.Endpoint.IssuerURL, + AuthURL: provider.Endpoint.AuthURL, + TokenURL: provider.Endpoint.TokenURL, + UserInfoURL: provider.Endpoint.UserInfoURL, + JWKSURL: provider.Endpoint.JWKSURL, + Algorithms: provider.Endpoint.Algorithms, + } + oidcProvider := config.NewProvider(ctx) + var err error + if len(provider.AutoDiscovery) > 0 { + oidcProvider, err = oidc.NewProvider(ctx, provider.AutoDiscovery) + if err != nil { + return nil, nil, err + } } oauthConfig := oauth2.Config{ diff --git a/util/config.go b/util/config.go index 328411f1..5fa3ae6a 100644 --- a/util/config.go +++ b/util/config.go @@ -49,16 +49,26 @@ type ldapMappings struct { CN string `json:"cn"` } +type oidcEndpoint struct { + IssuerURL string `json:"issuer"` + AuthURL string `json:"auth"` + TokenURL string `json:"token"` + UserInfoURL string `json:"userinfo"` + JWKSURL string `json:"jwks"` + Algorithms []string `json:"algorithms"` +} + type oidcProvider struct { - ClientID string `json:"client_id"` - ClientSecret string `json:"client_secret"` - RedirectURL string `json:"redirect_url"` - Scopes []string `json:"scopes"` - DisplayName string `json:"display_name"` - AutoDiscovery string `json:"provider_url"` - UsernameClaim string `json:"username_claim"` - NameClaim string `json:"name_claim"` - EmailClaim string `json:"email_claim"` + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` + RedirectURL string `json:"redirect_url"` + Scopes []string `json:"scopes"` + DisplayName string `json:"display_name"` + AutoDiscovery string `json:"provider_url"` + Endpoint oidcEndpoint `json:"endpoint"` + UsernameClaim string `json:"username_claim"` + NameClaim string `json:"name_claim"` + EmailClaim string `json:"email_claim"` } // ConfigType mapping between Config and the json file that sets it From c089ee239a4abd7c3dd4dda2fd97fa55b1599548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bjarne=20Ro=C3=9F?= Date: Tue, 13 Jun 2023 11:35:24 +0200 Subject: [PATCH 003/346] Update Template.go --- db/Template.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/db/Template.go b/db/Template.go index 059ca24d..2147cba9 100644 --- a/db/Template.go +++ b/db/Template.go @@ -97,9 +97,9 @@ func FillTemplates(d Store, templates []Template) (err error) { for i := range templates { tpl := &templates[i] var tasks []TaskWithTpl - tasks, err = d.GetTemplateTasks(tpl.ProjectID, tpl.ID, RetrieveQueryParams{Count: 1}) if err != nil { return + tasks, err = d.GetTemplateTasks(tpl.ProjectID, tpl.ID, RetrieveQueryParams{Count: 1}) } if len(tasks) > 0 { tpl.LastTask = &tasks[0] @@ -127,6 +127,15 @@ func FillTemplate(d Store, template *Template) (err error) { if template.SurveyVarsJSON != nil { err = json.Unmarshal([]byte(*template.SurveyVarsJSON), &template.SurveyVars) } + + var tasks []TaskWithTpl + if err != nil { + return + tasks, err = d.GetTemplateTasks(tpl.ProjectID, tpl.ID, RetrieveQueryParams{Count: 1}) + } + if len(tasks) > 0 { + template.LastTask = &tasks[0] + } return } From fa9c43decfa2c489ca1b894437126268796c5fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bjarne=20Ro=C3=9F?= Date: Tue, 13 Jun 2023 11:38:00 +0200 Subject: [PATCH 004/346] Oops --- db/Template.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/Template.go b/db/Template.go index 2147cba9..2402c24c 100644 --- a/db/Template.go +++ b/db/Template.go @@ -97,9 +97,9 @@ func FillTemplates(d Store, templates []Template) (err error) { for i := range templates { tpl := &templates[i] var tasks []TaskWithTpl + tasks, err = d.GetTemplateTasks(tpl.ProjectID, tpl.ID, RetrieveQueryParams{Count: 1}) if err != nil { return - tasks, err = d.GetTemplateTasks(tpl.ProjectID, tpl.ID, RetrieveQueryParams{Count: 1}) } if len(tasks) > 0 { tpl.LastTask = &tasks[0] From 763bd6e5627496f0b511587e1524902b7f275b39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bjarne=20Ro=C3=9F?= Date: Tue, 13 Jun 2023 11:53:37 +0200 Subject: [PATCH 005/346] Update Template.go --- db/Template.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/Template.go b/db/Template.go index 2402c24c..85a34c63 100644 --- a/db/Template.go +++ b/db/Template.go @@ -131,7 +131,7 @@ func FillTemplate(d Store, template *Template) (err error) { var tasks []TaskWithTpl if err != nil { return - tasks, err = d.GetTemplateTasks(tpl.ProjectID, tpl.ID, RetrieveQueryParams{Count: 1}) + tasks, err = d.GetTemplateTasks(template.ProjectID, template.ID, RetrieveQueryParams{Count: 1}) } if len(tasks) > 0 { template.LastTask = &tasks[0] From d589598f902cde42f6b913b805139e668e09305c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bjarne=20Ro=C3=9F?= Date: Tue, 13 Jun 2023 12:08:29 +0200 Subject: [PATCH 006/346] Update Template.go --- db/Template.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/db/Template.go b/db/Template.go index 85a34c63..33a1adf4 100644 --- a/db/Template.go +++ b/db/Template.go @@ -123,19 +123,19 @@ func FillTemplate(d Store, template *Template) (err error) { if err != nil { return } - - if template.SurveyVarsJSON != nil { - err = json.Unmarshal([]byte(*template.SurveyVarsJSON), &template.SurveyVars) - } var tasks []TaskWithTpl + tasks, err = d.GetTemplateTasks(template.ProjectID, template.ID, RetrieveQueryParams{Count: 1}) if err != nil { return - tasks, err = d.GetTemplateTasks(template.ProjectID, template.ID, RetrieveQueryParams{Count: 1}) } if len(tasks) > 0 { template.LastTask = &tasks[0] } + if template.SurveyVarsJSON != nil { + err = json.Unmarshal([]byte(*template.SurveyVarsJSON), &template.SurveyVars) + } + return } From b65eb63a49d38000061b67f6a790c39d23d6771c Mon Sep 17 00:00:00 2001 From: Andrey Chausenko Date: Tue, 20 Jun 2023 13:57:05 +1000 Subject: [PATCH 007/346] Correctly display relative time for non-UTC timezones --- web/src/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/main.js b/web/src/main.js index 2a177102..172b150e 100644 --- a/web/src/main.js +++ b/web/src/main.js @@ -12,7 +12,7 @@ const convert = new Convert(); axios.defaults.baseURL = document.baseURI; Vue.config.productionTip = false; -Vue.filter('formatDate', (value) => (value ? moment(String(value)).fromNow() : '—')); +Vue.filter('formatDate', (value) => (value ? moment.utc(String(value)).local(true).fromNow() : '—')); Vue.filter('formatTime', (value) => (value ? moment(String(value)).format('LTS') : '—')); Vue.filter('formatLog', (value) => (value ? convert.toHtml(String(value)) : value)); From efdf4140f0c8dabd03b181ad7e3366f3b5e2f968 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric?= Date: Tue, 20 Jun 2023 11:17:49 +0200 Subject: [PATCH 008/346] feat: provide task ID from semaphore_vars --- services/tasks/runner.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/tasks/runner.go b/services/tasks/runner.go index 51fdf3a7..283bc0ca 100644 --- a/services/tasks/runner.go +++ b/services/tasks/runner.go @@ -627,6 +627,8 @@ func (t *TaskRunner) getEnvironmentExtraVars() (str string, err error) { taskDetails := make(map[string]interface{}) + taskDetails["id"] = t.task.ID + if t.task.Message != "" { taskDetails["message"] = t.task.Message } From 653029c94d428e1415415cf8ffe1433882893d48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric?= Date: Tue, 20 Jun 2023 14:18:57 +0200 Subject: [PATCH 009/346] Update runner.go --- services/tasks/runner.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/tasks/runner.go b/services/tasks/runner.go index 283bc0ca..ab8dd975 100644 --- a/services/tasks/runner.go +++ b/services/tasks/runner.go @@ -626,8 +626,8 @@ func (t *TaskRunner) getEnvironmentExtraVars() (str string, err error) { } taskDetails := make(map[string]interface{}) - - taskDetails["id"] = t.task.ID + + taskDetails["id"] = t.task.ID if t.task.Message != "" { taskDetails["message"] = t.task.Message From ad30f8252c34bacda128449a0417287771828fbc Mon Sep 17 00:00:00 2001 From: Daniel Torlop Date: Wed, 21 Jun 2023 22:16:26 +0200 Subject: [PATCH 010/346] fix: email alert --- services/tasks/alert.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/tasks/alert.go b/services/tasks/alert.go index c5ad4f26..ff54342e 100644 --- a/services/tasks/alert.go +++ b/services/tasks/alert.go @@ -60,7 +60,7 @@ func (t *TaskRunner) sendMailAlert() { userObj, err := t.pool.store.GetUser(user) if !userObj.Alert { - return + continue } t.panicOnError(err, "Can't find user Email!") From a370b42d82dfce7a251514c6655c1cac99e2c01f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Neftal=C3=AD=20Yagua?= Date: Fri, 30 Jun 2023 01:31:03 -0500 Subject: [PATCH 011/346] Create semaphore.spec Fedora 38 package arch x86_64 https://copr.fedorainfracloud.org/coprs/neftaliyagua/semaphore/ sudo dnf copr enable neftaliyagua/semaphore sudo dnf install semaphore sudo semaphore-setup Note: Don't forget to put the configuration path to /etc/semaphore sudo systemctl enable --now ansible-semaphore Browser http://localhost:3000 --- deployment/semaphore.spec | 85 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 deployment/semaphore.spec diff --git a/deployment/semaphore.spec b/deployment/semaphore.spec new file mode 100644 index 00000000..789f0d6c --- /dev/null +++ b/deployment/semaphore.spec @@ -0,0 +1,85 @@ +%global debug_package %{nil} +%global _missing_build_ids_terminate_build 0 +%global _dwz_low_mem_die_limit 0 + +Name: semaphore +Version: 2.8.90 +Release: 1%{?dist} +Summary: Ansible Semaphore is a modern UI for Ansible. It lets you easily run Ansible playbooks, get notifications about fails, control access to deployment system. + +License: MIT +URL: https://github.com/ansible-semaphore/semaphore +Source: https://github.com/ansible-semaphore/semaphore/archive/refs/tags/v2.8.90.zip + +BuildRequires: golang +BuildRequires: nodejs +BuildRequires: nodejs-npm +BuildRequires: go-task +BuildRequires: git +BuildRequires: systemd-rpm-macros + +Requires: ansible + +%description +Ansible Semaphore is a modern UI for Ansible. It lets you easily run Ansible playbooks, get notifications about fails, control access to deployment system. + +%prep +%setup -q + +%build +export SEMAPHORE_VERSION="development" +export SEMAPHORE_ARCH="linux_amd64" +export SEMAPHORE_CONFIG_PATH="./etc/semaphore" +export APP_ROOT="./ansible-semaphore/" + +if ! [[ "$PATH" =~ "$HOME/go/bin:" ]] +then + PATH="$HOME/go/bin:$PATH" +fi +export PATH +##go install github.com/gobuffalo/packr/v2@latest +go-task all + +cat > ansible-semaphore.service < semaphore-setup < Date: Mon, 3 Jul 2023 10:40:17 +0200 Subject: [PATCH 012/346] test: fix test after MR --- services/tasks/runner_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/tasks/runner_test.go b/services/tasks/runner_test.go index 7bd1347d..b4ae0010 100644 --- a/services/tasks/runner_test.go +++ b/services/tasks/runner_test.go @@ -216,7 +216,7 @@ func TestTaskGetPlaybookArgs(t *testing.T) { } res := strings.Join(args, " ") - if res != "-i /tmp/inventory_0 --private-key=/tmp/access_key_0 --extra-vars {\"semaphore_vars\":{\"task_details\":{}}} test.yml" { + if res != "-i /tmp/inventory_0 --private-key=/tmp/access_key_0 --extra-vars {\"semaphore_vars\":{\"task_details\":{\"id\":0}}} test.yml" { t.Fatal("incorrect result") } } @@ -254,7 +254,7 @@ func TestTaskGetPlaybookArgs2(t *testing.T) { } res := strings.Join(args, " ") - if res != "-i /tmp/inventory_0 --extra-vars=@/tmp/access_key_0 --extra-vars {\"semaphore_vars\":{\"task_details\":{}}} test.yml" { + if res != "-i /tmp/inventory_0 --extra-vars=@/tmp/access_key_0 --extra-vars {\"semaphore_vars\":{\"task_details\":{\"id\":0}}} test.yml" { t.Fatal("incorrect result") } } @@ -292,7 +292,7 @@ func TestTaskGetPlaybookArgs3(t *testing.T) { } res := strings.Join(args, " ") - if res != "-i /tmp/inventory_0 --extra-vars=@/tmp/access_key_0 --extra-vars {\"semaphore_vars\":{\"task_details\":{}}} test.yml" { + if res != "-i /tmp/inventory_0 --extra-vars=@/tmp/access_key_0 --extra-vars {\"semaphore_vars\":{\"task_details\":{\"id\":0}}} test.yml" { t.Fatal("incorrect result") } } From d884330f625124182487c1998518548a46bc70b8 Mon Sep 17 00:00:00 2001 From: Philipp Schosteritsch Date: Tue, 4 Jul 2023 16:47:29 +0200 Subject: [PATCH 013/346] committing implementation --- web/package.json | 97 ++++----- web/src/App.vue | 48 ++--- web/src/components/ChangePasswordForm.vue | 4 +- web/src/components/EditDialog.vue | 2 +- web/src/components/EditViewsForm.vue | 4 +- web/src/components/EnvironmentForm.vue | 15 +- web/src/components/InventoryForm.vue | 24 +-- web/src/components/KeyForm.vue | 31 ++- web/src/components/NewTaskDialog.vue | 6 +- web/src/components/ObjectRefsDialog.vue | 5 +- web/src/components/ObjectRefsView.vue | 2 +- web/src/components/ProjectForm.vue | 14 +- web/src/components/RepositoryForm.vue | 26 +-- web/src/components/SurveyVars.vue | 22 +- web/src/components/TableSettingsSheet.vue | 2 +- web/src/components/TaskForm.vue | 31 ++- web/src/components/TaskList.vue | 20 +- web/src/components/TaskLogView.vue | 8 +- web/src/components/TeamMemberForm.vue | 6 +- web/src/components/TemplateForm.vue | 89 ++++---- web/src/components/UserForm.vue | 20 +- web/src/event-bus.js | 3 +- web/src/lang/en.js | 234 ++++++++++++++++++++++ web/src/lang/fr.js | 230 +++++++++++++++++++++ web/src/lang/index.js | 8 + web/src/main.js | 4 +- web/src/plugins/i18.js | 12 ++ web/src/views/Auth.vue | 33 +-- web/src/views/Users.vue | 24 +-- web/src/views/project/Activity.vue | 14 +- web/src/views/project/Environment.vue | 16 +- web/src/views/project/History.vue | 20 +- web/src/views/project/Inventory.vue | 20 +- web/src/views/project/Keys.vue | 18 +- web/src/views/project/New.vue | 4 +- web/src/views/project/Repositories.vue | 20 +- web/src/views/project/Settings.vue | 19 +- web/src/views/project/Team.vue | 22 +- web/src/views/project/TemplateView.vue | 29 +-- web/src/views/project/Templates.vue | 35 ++-- 40 files changed, 860 insertions(+), 381 deletions(-) create mode 100644 web/src/lang/en.js create mode 100644 web/src/lang/fr.js create mode 100644 web/src/lang/index.js create mode 100644 web/src/plugins/i18.js diff --git a/web/package.json b/web/package.json index e5b411bf..1ad478fc 100644 --- a/web/package.json +++ b/web/package.json @@ -1,48 +1,49 @@ -{ - "name": "web", - "version": "0.1.0", - "private": true, - "scripts": { - "serve": "vue-cli-service serve", - "build": "vue-cli-service build", - "test:unit": "vue-cli-service test:unit", - "lint": "vue-cli-service lint" - }, - "dependencies": { - "@mdi/font": "^5.9.55", - "ansi-to-html": "^0.7.2", - "axios": "^0.21.4", - "core-js": "^3.23.2", - "moment": "^2.29.3", - "vue": "^2.6.14", - "vue-codemirror": "^4.0.6", - "vue-router": "^3.5.4", - "vuedraggable": "^2.24.3", - "vuetify": "^2.6.6" - }, - "devDependencies": { - "@vue/cli-plugin-babel": "^5.0.6", - "@vue/cli-plugin-eslint": "^5.0.6", - "@vue/cli-plugin-router": "^5.0.6", - "@vue/cli-plugin-unit-mocha": "^5.0.6", - "@vue/cli-service": "^5.0.6", - "@vue/eslint-config-airbnb": "^6.0.0", - "@vue/test-utils": "^1.3.0", - "babel-eslint": "^10.1.0", - "chai": "^4.3.6", - "eslint": "^7.32.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-vue": "^9.1.1", - "eslint-plugin-vuejs-accessibility": "^1.2.0", - "glob-parent": ">=5.1.2", - "nanoid": ">=3.1.31", - "nyc": "^15.1.0", - "sass": "~1.32.12", - "sass-loader": "^13.0.0", - "stylus": "^0.54.8", - "stylus-loader": "^3.0.2", - "vue-cli-plugin-vuetify": "~2.0.7", - "vue-template-compiler": "^2.6.14", - "vuetify-loader": "^1.8.0" - } -} +{ + "name": "web", + "version": "0.1.0", + "private": true, + "scripts": { + "serve": "vue-cli-service serve", + "build": "vue-cli-service build", + "test:unit": "vue-cli-service test:unit", + "lint": "vue-cli-service lint" + }, + "dependencies": { + "@mdi/font": "^5.9.55", + "ansi-to-html": "^0.7.2", + "axios": "^0.21.4", + "core-js": "^3.23.2", + "moment": "^2.29.3", + "vue": "^2.6.14", + "vue-i18n": "^8.18.2", + "vue-codemirror": "^4.0.6", + "vue-router": "^3.5.4", + "vuedraggable": "^2.24.3", + "vuetify": "^2.6.6" + }, + "devDependencies": { + "@vue/cli-plugin-babel": "^5.0.6", + "@vue/cli-plugin-eslint": "^5.0.6", + "@vue/cli-plugin-router": "^5.0.6", + "@vue/cli-plugin-unit-mocha": "^5.0.6", + "@vue/cli-service": "^5.0.6", + "@vue/eslint-config-airbnb": "^6.0.0", + "@vue/test-utils": "^1.3.0", + "babel-eslint": "^10.1.0", + "chai": "^4.3.6", + "eslint": "^7.32.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-vue": "^9.1.1", + "eslint-plugin-vuejs-accessibility": "^1.2.0", + "glob-parent": ">=5.1.2", + "nanoid": ">=3.1.31", + "nyc": "^15.1.0", + "sass": "~1.32.12", + "sass-loader": "^13.0.0", + "stylus": "^0.54.8", + "stylus-loader": "^3.0.2", + "vue-cli-plugin-vuetify": "~2.0.7", + "vue-template-compiler": "^2.6.14", + "vuetify-loader": "^1.8.0" + } +} diff --git a/web/src/App.vue b/web/src/App.vue index edaa7521..98a5b4b4 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -3,7 +3,7 @@ @@ -22,7 +22,7 @@ @@ -54,7 +54,7 @@ >{{ template ? template.name : null }} mdi-chevron-right - Task #{{ task ? task.id : null }} + {{ $t('task', {expr: task ? task.id : null}) }} @@ -73,7 +73,7 @@ diff --git a/web/src/components/EnvironmentForm.vue b/web/src/components/EnvironmentForm.vue index 3b63330b..6befbd38 100644 --- a/web/src/components/EnvironmentForm.vue +++ b/web/src/components/EnvironmentForm.vue @@ -13,33 +13,33 @@ - Extra variables + {{ $t('extraVariables') }} - Environment variables + {{ $t('environmentVariables') }} - Environment and extra variables must be valid JSON. - Example: + {{ $t('environmentAndExtraVariablesMustBeValidJsonExample') }}
{
   "var_available_in_playbook_1": 1245,
   "var_available_in_playbook_2": "test"
diff --git a/web/src/components/InventoryForm.vue b/web/src/components/InventoryForm.vue
index 45a534ec..9bba99fc 100644
--- a/web/src/components/InventoryForm.vue
+++ b/web/src/components/InventoryForm.vue
@@ -13,26 +13,26 @@
 
     
 
     
 
     
 
     
-      Static inventory example:
+      {{ $t('staticInventoryExample') }}
       
[website]
 172.18.8.40
 172.18.8.41
@@ -88,7 +88,7 @@ type="info" v-if="item.type === 'static-yaml'" > - Static YAML inventory example: + {{ $t('staticYamlInventoryExample') }}
all:
   children:
     website:
diff --git a/web/src/components/KeyForm.vue b/web/src/components/KeyForm.vue
index cfb4cf22..5eaeeea7 100644
--- a/web/src/components/KeyForm.vue
+++ b/web/src/components/KeyForm.vue
@@ -14,16 +14,16 @@
 
     
 
     
 
     
@@ -65,15 +65,15 @@
     
 
     
 
@@ -83,8 +83,7 @@
         type="info"
         v-if="item.type === 'none'"
     >
-      Use this type of key for HTTPS repositories and for
-      playbooks which use non-SSH connections.
+      {{ $t('useThisTypeOfKeyForHttpsRepositoriesAndForPlaybook') }}
     
   
 
@@ -97,13 +96,13 @@ export default {
     return {
       inventoryTypes: [{
         id: 'ssh',
-        name: 'SSH Key',
+        name: `${this.$t('keyFormSshKey')}`,
       }, {
         id: 'login_password',
-        name: 'Login with password',
+        name: `${this.$t('keyFormLoginPassword')}`,
       }, {
         id: 'none',
-        name: 'None',
+        name: `${this.$t('keyFormNone')}`,
       }],
     };
   },
diff --git a/web/src/components/NewTaskDialog.vue b/web/src/components/NewTaskDialog.vue
index 9c579cd9..a65a505f 100644
--- a/web/src/components/NewTaskDialog.vue
+++ b/web/src/components/NewTaskDialog.vue
@@ -1,8 +1,8 @@
 
 
     
           
-

Credentials to access to the Git repository. It should be:

+

{{ $t('credentialsToAccessToTheGitRepositoryItShouldBe') }}

    -
  • SSH if you use Git or SSH URL.
  • -
  • None if you use HTTPS or file URL.
  • +
  • {{ $t('ssh') }} {{ $t('ifYouUseGitOrSshUrl') }}
  • +
  • {{ $t('none') }} {{ $t('ifYouUseHttpsOrFileUrl') }}
diff --git a/web/src/components/SurveyVars.vue b/web/src/components/SurveyVars.vue index 3f78ec4d..5d7fe664 100644 --- a/web/src/components/SurveyVars.vue +++ b/web/src/components/SurveyVars.vue @@ -14,33 +14,33 @@ v-if="editedVar != null" > @@ -52,14 +52,14 @@ text @click="editDialog = false" > - Cancel + {{ $t('cancel') }} - {{ editedVarIndex == null ? 'Add' : 'Save' }} + {{ editedVarIndex == null ? $t('add') : $t('save') }} @@ -73,7 +73,7 @@ 'rgba(200, 200, 200, 0.38)' : 'rgba(0, 0, 0, 0.38)' }"> - Survey Variables + {{ $t('surveyVariables') }} - + Add variable + + {{ $t('addVariable') }} diff --git a/web/src/components/TableSettingsSheet.vue b/web/src/components/TableSettingsSheet.vue index 90826bfb..1aa8e7a1 100644 --- a/web/src/components/TableSettingsSheet.vue +++ b/web/src/components/TableSettingsSheet.vue @@ -1,6 +1,6 @@ diff --git a/web/src/components/TemplateForm.vue b/web/src/components/TemplateForm.vue index 74357121..b81fefbc 100644 --- a/web/src/components/TemplateForm.vue +++ b/web/src/components/TemplateForm.vue @@ -19,34 +19,33 @@ >

- Defines start version of your artifact. - Each run increments the artifact version. + {{ $t('definesStartVersionOfYourArtifactEachRunIncrements') }}

- For more information about building, see the + {{ $t('forMoreInformationAboutBuildingSeeThe') }} Task Template reference. + >{{ $t('taskTemplateReference') }}.

- Defines what artifact should be deployed when the task run. + {{ $t('definesWhatArtifactShouldBeDeployedWhenTheTaskRun') }}

- For more information about deploying, see the + {{ $t('forMoreInformationAboutDeployingSeeThe') }} Task Template reference. + >{{ $t('taskTemplateReference2') }}.

-

Defines autorun schedule.

+

{{ $t('definesAutorunSchedule') }}

- For more information about cron, see the + {{ $t('forMoreInformationAboutCronSeeThe') }} Cron expression format reference. + >{{ $t('cronExpressionFormatReference') }}.

@@ -72,7 +71,7 @@ :key="key" > {{ TEMPLATE_TYPE_ICONS[key] }} - {{ TEMPLATE_TYPE_TITLES[key] }} + {{ $t(TEMPLATE_TYPE_TITLES[key]) }} @@ -80,11 +79,11 @@ @@ -92,11 +91,11 @@ @@ -115,8 +114,8 @@ - I want to run a task by the cron only for for new commits of some repository + {{ $t('iWantToRunATaskByTheCronOnlyForForNewCommitsOfSome') }} - Read the - docs - to learn more about Cron. + {{ $t('readThe') }} + {{ $t('docs') }} + {{ $t('toLearnMoreAboutCron') }} @@ -299,17 +298,11 @@ v-model="item.arguments" :options="cmOptions" :disabled="formSaving" - placeholder='CLI Args (JSON array). Example: -[ - "-i", - "@myinventory.sh", - "--private-key=/there/id_rsa", - "-vvvv" -]' + :placeholder="$t('cliArgsJsonArrayExampleIMyinventoryshPrivatekeythe2')" /> diff --git a/web/src/components/UserForm.vue b/web/src/components/UserForm.vue index 52d049d3..c001ce54 100644 --- a/web/src/components/UserForm.vue +++ b/web/src/components/UserForm.vue @@ -13,45 +13,45 @@ diff --git a/web/src/event-bus.js b/web/src/event-bus.js index 0948c2e5..be734930 100644 --- a/web/src/event-bus.js +++ b/web/src/event-bus.js @@ -1,3 +1,4 @@ import Vue from 'vue'; +import i18n from '@/plugins/i18'; -export default new Vue(); +export default new Vue(i18n); diff --git a/web/src/lang/en.js b/web/src/lang/en.js new file mode 100644 index 00000000..78e41a8b --- /dev/null +++ b/web/src/lang/en.js @@ -0,0 +1,234 @@ +export default { + incorrectUsrPwd: 'Incorrect login or password', + askDeleteUser: 'Do you really want to delete this user?', + askDeleteTemp: 'Do you really want to delete this template?', + askDeleteEnv: 'Do you really want to delete this environment?', + askDeleteInv: 'Do you really want to delete this inventor?', + askDeleteKey: 'Do you really want to delete this key?', + askDeleteRepo: 'Do you really want to delete this repository?', + askDeleteProj: 'Do you really want to delete this project?', + askDeleteTMem: 'Do you really want to delete this team member?', + edit: 'Edit', + nnew: 'New', + keyFormSshKey: 'SSH Key', + keyFormLoginPassword: 'Login with password', + keyFormNone: 'None', + incorrectUrl: 'Incorrect URL', + username: 'Username', + username_required: 'Username is required', + dashboard: 'Dashboard', + history: 'History', + activity: 'Activity', + settings: 'Settings', + signIn: 'Sign In', + password: 'Password', + changePassword: 'Change password', + editUser: 'Edit User', + newProject: 'New Project', + close: 'Close', + newProject2: 'New project...', + demoMode: 'DEMO MODE', + task: 'Task #{expr}', + youCanRunAnyTasks: 'You can run any tasks', + youHaveReadonlyAccess: 'You have read-only access', + taskTemplates: 'Task Templates', + inventory: 'Inventory', + environment: 'Environment', + keyStore: 'Key Store', + repositories: 'Repositories', + darkMode: 'Dark Mode', + team: 'Team', + users: 'Users', + editAccount: 'Edit Account', + signOut: 'Sign Out', + error: 'Error', + refreshPage: 'Refresh Page', + relogin: 'Relogin', + howToFixSigninIssues: 'How to fix sign-in issues', + firstlyYouNeedAccessToTheServerWhereSemaphoreRunni: 'Firstly, you need access to the server where Semaphore running.', + executeTheFollowingCommandOnTheServerToSeeExisting: 'Execute the following command on the server to see existing users:', + semaphoreUserList: 'semaphore user list', + youCanChangePasswordOfExistingUser: 'You can change password of existing user:', + semaphoreUserChangebyloginLoginUser123Password: 'semaphore user change-by-login --login user123 --password {makePasswordExample}', + orCreateNewAdminUser: 'Or create new admin user:', + close2: 'Close', + semaphore: 'SEMAPHORE', + dontHaveAccountOrCantSignIn: 'Don\'\'t have account or can\'\'t sign in?', + password2: 'Password', + cancel: 'Cancel', + noViews: 'No views', + addView: 'Add view', + editEnvironment: 'Edit Environment', + deleteEnvironment: 'Delete environment', + environment2: 'Environment', + newEnvironment: 'New Environment', + environmentName: 'Environment Name', + extraVariables: 'Extra variables', + enterExtraVariablesJson: 'Enter extra variables JSON...', + environmentVariables: 'Environment variables', + enterEnvJson: 'Enter env JSON...', + environmentAndExtraVariablesMustBeValidJsonExample: 'Environment and extra variables must be valid JSON. Example:', + dashboard2: 'Dashboard', + ansibleSemaphore: 'Ansible Semaphore', + wereSorryButHtmlwebpackpluginoptionstitleDoesntWor: 'We\'\'re sorry but <%= htmlWebpackPlugin.options.title %> doesn\'\'t work properly without JavaScript enabled. Please enable it to continue.', + deleteInventory: 'Delete inventory', + newInventory: 'New Inventory', + name: 'Name', + userCredentials: 'User Credentials', + sudoCredentialsOptional: 'Sudo Credentials (Optional)', + type: 'Type', + pathToInventoryFile: 'Path to Inventory file', + enterInventory: 'Enter inventory...', + staticInventoryExample: 'Static inventory example:', + staticYamlInventoryExample: 'Static YAML inventory example:', + keyName: 'Key Name', + loginOptional: 'Login (Optional)', + usernameOptional: 'Username (Optional)', + privateKey: 'Private Key', + override: 'Override', + useThisTypeOfKeyForHttpsRepositoriesAndForPlaybook: 'Use this type of key for HTTPS repositories and for playbooks which use non-SSH connections.', + deleteKey: 'Delete key', + newKey: 'New Key', + create: 'Create', + newTask: 'New Task', + cantDeleteThe: 'Can\'\'t delete the {objectTitle}', + theCantBeDeletedBecauseItUsedByTheResourcesBelow: 'The {objectTitle} can\'\'t be deleted because it used by the resources below', + projectName: 'Project Name', + allowAlertsForThisProject: 'Allow alerts for this project', + telegramChatIdOptional: 'Telegram Chat ID (Optional)', + maxNumberOfParallelTasksOptional: 'Max number of parallel tasks (Optional)', + deleteRepository: 'Delete repository', + newRepository: 'New Repository', + urlOrPath: 'URL or path', + absPath: 'abs. path', + branch: 'Branch', + accessKey: 'Access Key', + credentialsToAccessToTheGitRepositoryItShouldBe: 'Credentials to access to the Git repository. It should be:', + ifYouUseGitOrSshUrl: 'if you use Git or SSH URL.', + ifYouUseHttpsOrFileUrl: 'if you use HTTPS or file URL.', + none: 'None', + ssh: 'SSH', + deleteProject: 'Delete project', + save: 'Save', + deleteProject2: 'Delete Project', + onceYouDeleteAProjectThereIsNoGoingBackPleaseBeCer: 'Once you delete a project, there is no going back. Please be certain.', + name2: 'Name *', + title: 'Title *', + description: 'Description', + required: 'Required', + key: '{expr}', + surveyVariables: 'Survey Variables', + addVariable: 'Add variable', + columns: 'Columns', + buildVersion: 'Build Version', + messageOptional: 'Message (Optional)', + debug: 'Debug', + dryRun: 'Dry Run', + diff: 'Diff', + advanced: 'Advanced', + hide: 'Hide', + pleaseAllowOverridingCliArgumentInTaskTemplateSett: 'Please allow overriding CLI argument in Task Template settings', + cliArgsJsonArrayExampleIMyinventoryshPrivatekeythe: 'CLI Args (JSON array). Example: [ "-i", "@myinventory.sh", "--private-key=/there/id_rsa", "-vvvv" ]', + started: 'Started', + author: 'Author', + duration: 'Duration', + stop: 'Stop', + deleteTeamMember: 'Delete team member', + team2: 'Team', + newTeamMember: 'New Team Member', + user: 'User', + administrator: 'Administrator', + definesStartVersionOfYourArtifactEachRunIncrements: 'Defines start version of your artifact. Each run increments the artifact version.', + forMoreInformationAboutBuildingSeeThe: 'For more information about building, see the', + taskTemplateReference: 'Task Template reference', + definesWhatArtifactShouldBeDeployedWhenTheTaskRun: 'Defines what artifact should be deployed when the task run.', + forMoreInformationAboutDeployingSeeThe: 'For more information about deploying, see the', + taskTemplateReference2: 'Task Template reference', + definesAutorunSchedule: 'Defines autorun schedule.', + forMoreInformationAboutCronSeeThe: 'For more information about cron, see the', + cronExpressionFormatReference: 'Cron expression format reference', + startVersion: 'Start Version', + example000: 'Example: 0.0.0', + buildTemplate: 'Build Template', + autorun: 'Autorun', + playbookFilename: 'Playbook Filename *', + exampleSiteyml: 'Example: site.yml', + inventory2: 'Inventory *', + repository: 'Repository *', + environment3: 'Environment *', + vaultPassword: 'Vault Password', + vaultPassword2: 'Vault Password', + view: 'View', + cron: 'Cron', + iWantToRunATaskByTheCronOnlyForForNewCommitsOfSome: 'I want to run a task by the cron only for for new commits of some repository', + repository2: 'Repository', + cronChecksNewCommitBeforeRun: 'Cron checks new commit before run', + readThe: 'Read the', + toLearnMoreAboutCron: 'to learn more about Cron.', + suppressSuccessAlerts: 'Suppress success alerts', + cliArgsJsonArrayExampleIMyinventoryshPrivatekeythe2: 'CLI Args (JSON array). Example: [ "-i", "@myinventory.sh", "--private-key=/there/id_rsa", "-vvvv" ]', + allowCliArgsInTask: 'Allow CLI args in Task', + docs: 'docs', + editViews: 'Edit Views', + newTemplate: 'New template', + taskTemplates2: 'Task Templates', + all: 'All', + notLaunched: 'Not launched', + by: 'by {user_name} {formatDate}', + editTemplate: 'Edit Template', + newTemplate2: 'New Template', + deleteTemplate: 'Delete template', + playbook: 'Playbook', + email: 'Email', + adminUser: 'Admin user', + sendAlerts: 'Send alerts', + deleteUser: 'Delete user', + newUser: 'New User', + re: 'Re{getActionButtonTitle}', + teamMember: '{expr} Team Member', + taskId: 'Task ID', + version: 'Version', + status: 'Status', + start: 'Start', + actions: 'Actions', + alert: 'Alert', + admin: 'Admin', + external: 'External', + time: 'Time', + path: 'Path', + gitUrl: 'Git URL', + sshKey: 'SSH Key', + lastTask: 'Last Task', + task2: 'Task', + build: 'Build', + deploy: 'Deploy', + run: 'Run', + add: 'Add', + password_required: 'Password is required', + name_required: 'Name is required', + user_credentials_required: 'User credentials are required', + type_required: 'Type is required', + path_required: 'Path to Inventory file is required', + private_key_required: 'Private key is required', + project_name_required: 'Project name is required', + repository_required: 'Repository is required', + branch_required: 'Branch is required', + key_required: 'Key is required', + user_required: 'User is required', + build_version_required: 'Build version is required', + title_required: 'Title is required', + isRequired: 'is required', + mustBeInteger: 'Must be integer', + mustBe0OrGreater: 'Must be 0 or greater', + start_version_required: 'Start version is required', + playbook_filename_required: 'Playbook filename is required', + inventory_required: 'Inventory is required', + environment_required: 'Environment is required', + email_required: 'Email is required', + build_template_required: 'Build template is required', + Task: 'Task', + Build: 'Build', + Deploy: 'Deploy', + Run: 'Run', + +}; diff --git a/web/src/lang/fr.js b/web/src/lang/fr.js new file mode 100644 index 00000000..66ef7c82 --- /dev/null +++ b/web/src/lang/fr.js @@ -0,0 +1,230 @@ +export default { + incorrectUsrPwd: 'Identifiant ou mot de passe incorrect', + askDeleteUser: 'Voulez-vous vraiment supprimer cet utilisateur ?', + askDeleteTemp: 'Voulez-vous vraiment supprimer ce modèle ?', + askDeleteEnv: 'Voulez-vous vraiment supprimer cet environnement ?', + askDeleteInv: 'Voulez-vous vraiment supprimer cet inventeur ?', + askDeleteKey: 'Voulez-vous vraiment supprimer cette clé ?', + askDeleteRepo: 'Voulez-vous vraiment supprimer ce dépôt ?', + askDeleteProj: 'Voulez-vous vraiment supprimer ce projet ?', + askDeleteTMem: 'Voulez-vous vraiment supprimer ce membre de l\'équipe ?', + edit: 'Modifier', + nnew: 'Nouveau', + keyFormSshKey: 'Clé SSH', + keyFormLoginPassword: 'Connectez-vous avec mot de passe', + keyFormNone: 'Aucune', + incorrectUrl: 'URL incorrecte', + username: 'Nom d\'utilisateur', + username_required: 'Le nom d\'utilisateur est requis', + dashboard: 'Tableau de bord', + history: 'Historique', + activity: 'Activité', + settings: 'Paramètres', + signIn: 'Se connecter', + password: 'Mot de passe', + changePassword: 'Changer le mot de passe', + editUser: 'Modifier l\'utilisateur', + newProject: 'Nouveau projet', + close: 'Fermer', + newProject2: 'Nouveau projet...', + demoMode: 'MODE DE DÉMONSTRATION', + task: 'Tâche n°{expr}', + youCanRunAnyTasks: 'Vous pouvez exécuter n\'importe quelle tâche', + youHaveReadonlyAccess: 'Vous avez un accès en lecture seule', + taskTemplates: 'Modèles de tâches', + inventory: 'Inventaire', + environment: 'Environnement', + keyStore: 'Magasin de clés', + repositories: 'Dépôts', + darkMode: 'Mode sombre', + team: 'Équipe', + users: 'Utilisateurs', + editAccount: 'Modifier le compte', + signOut: 'Déconnexion', + error: 'Erreur', + refreshPage: 'Actualiser la page', + relogin: 'Se reconnecter', + howToFixSigninIssues: 'Comment résoudre les problèmes de connexion', + firstlyYouNeedAccessToTheServerWhereSemaphoreRunni: 'Tout d\'abord, vous avez besoin d\'accéder au serveur où Semaphore est en cours d\'exécution.', + executeTheFollowingCommandOnTheServerToSeeExisting: 'Exécutez la commande suivante sur le serveur pour voir les utilisateurs existants :', + semaphoreUserList: 'liste d\'utilisateurs Semaphore', + youCanChangePasswordOfExistingUser: 'Vous pouvez changer le mot de passe de l\'utilisateur existant :', + semaphoreUserChangebyloginLoginUser123Password: 'semaphore user change-by-login --login user123 --password {makePasswordExample}', + orCreateNewAdminUser: 'Ou créer un nouvel utilisateur administrateur :', + close2: 'Fermer', + semaphore: 'SEMAPHORE', + dontHaveAccountOrCantSignIn: 'Vous n\'avez pas de compte ou vous ne pouvez pas vous connecter ?', + password2: 'Mot de passe', + cancel: 'Annuler', + noViews: 'Aucune vue', + addView: 'Ajouter une vue', + editEnvironment: 'Modifier l\'environnement', + deleteEnvironment: 'Supprimer l\'environnement', + environment2: 'Environnement', + newEnvironment: 'Nouvel environnement', + environmentName: 'Nom de l\'environnement', + extraVariables: 'Variables supplémentaires', + enterExtraVariablesJson: 'Saisissez JSON pour les variables supplémentaires...', + environmentVariables: 'Variables d\'environnement', + enterEnvJson: 'Saisissez JSON pour l\'environnement...', + environmentAndExtraVariablesMustBeValidJsonExample: 'L\'environnement et les variables supplémentaires doivent être un JSON valide. Exemple :', + dashboard2: 'Tableau de bord', + ansibleSemaphore: 'Semaphore Ansible', + wereSorryButHtmlwebpackpluginoptionstitleDoesntWor: 'Nous sommes désolés, mais <%= htmlWebpackPlugin.options.title %> ne fonctionne pas correctement sans JavaScript. Veuillez l\'activer pour continuer.', + deleteInventory: 'Supprimer l\'inventaire', + newInventory: 'Nouvel inventaire', + name: 'Nom', + userCredentials: 'Identifiants utilisateur', + sudoCredentialsOptional: 'Identifiants Sudo (facultatif)', + type: 'Type', + pathToInventoryFile: 'Chemin vers le fichier d\'inventaire', + enterInventory: 'Entrer l\'inventaire...', + staticInventoryExample: 'Exemple d\'inventaire statique:', + staticYamlInventoryExample: 'Exemple d\'inventaire YAML statique:', + keyName: 'Nom de la clé', + loginOptional: 'Connexion (facultatif)', + usernameOptional: 'Nom d\'utilisateur (facultatif)', + privateKey: 'Clé privée', + override: 'Remplacer', + useThisTypeOfKeyForHttpsRepositoriesAndForPlaybook: 'Utilisez ce type de clé pour les référentiels HTTPS et pour les playbooks qui utilisent des connexions non-SSH.', + deleteKey: 'Supprimer la clé', + newKey: 'Nouvelle clé', + create: 'Créer', + newTask: 'Nouvelle tâche', + cantDeleteThe: 'Impossible de supprimer {objectTitle}', + theCantBeDeletedBecauseItUsedByTheResourcesBelow: 'Le {objectTitle} ne peut pas être supprimé car il est utilisé par les ressources ci-dessous', + projectName: 'Nom du projet', + allowAlertsForThisProject: 'Autoriser les alertes pour ce projet', + telegramChatIdOptional: 'Identifiant de chat Telegram (facultatif)', + maxNumberOfParallelTasksOptional: 'Nombre maximal de tâches en parallèle (facultatif)', + deleteRepository: 'Supprimer le référentiel', + newRepository: 'Nouveau référentiel', + urlOrPath: 'URL ou chemin', + absPath: 'chemin absolu', + branch: 'Branche', + accessKey: 'Clé d\'accès', + credentialsToAccessToTheGitRepositoryItShouldBe: 'Identifiants pour accéder au référentiel Git. Il doit être :', + ifYouUseGitOrSshUrl: 'si vous utilisez une URL Git ou SSH.', + ifYouUseHttpsOrFileUrl: 'si vous utilisez une URL HTTPS ou fichier.', + none: 'Aucun', + ssh: 'SSH', + deleteProject: 'Supprimer le projet', + save: 'Enregistrer', + deleteProject2: 'Supprimer le projet', + onceYouDeleteAProjectThereIsNoGoingBackPleaseBeCer: 'Une fois que vous avez supprimé un projet, il n\'y a pas de retour en arrière possible. Veuillez être certain.', + name2: 'Nom *', + title: 'Titre *', + description: 'Description', + required: 'Requis', + key: '{expr}', + surveyVariables: 'Variables d\'enquête', + addVariable: 'Ajouter une variable', + columns: 'Colonnes', + buildVersion: 'Version de construction', + messageOptional: 'Message (facultatif)', + debug: 'Debug', + dryRun: 'Simulation', + diff: 'Différence', + advanced: 'Avancé', + hide: 'Cacher', + pleaseAllowOverridingCliArgumentInTaskTemplateSett: 'Veuillez autoriser le remplacement de l\'argument CLI dans les paramètres du modèle de tâche', + cliArgsJsonArrayExampleIMyinventoryshPrivatekeythe: 'Arguments CLI (tableau JSON). Exemple: [ "-i", "@myinventory.sh", "--private-key=/there/id_rsa", "-vvvv" ]', + started: 'Démarré', + author: 'Auteur', + duration: 'Durée', + stop: 'Arrêter', + deleteTeamMember: 'Supprimer un membre de l\'équipe', + team2: 'Équipe', + newTeamMember: 'Nouveau membre de l\'équipe', + user: 'Utilisateur', + administrator: 'Administrateur', + definesStartVersionOfYourArtifactEachRunIncrements: 'Définit la version de départ de votre artefact. Chaque exécution incrémente la version de l\'artefact.', + forMoreInformationAboutBuildingSeeThe: 'Pour plus d\'informations sur la construction, voir la', + taskTemplateReference: 'Référence du modèle de tâche', + definesWhatArtifactShouldBeDeployedWhenTheTaskRun: 'Définit l\'artefact qui doit être déployé lorsque la tâche est exécutée.', + forMoreInformationAboutDeployingSeeThe: 'Pour plus d\'informations sur le déploiement, voir la', + taskTemplateReference2: 'Référence du modèle de tâche', + definesAutorunSchedule: 'Définit le calendrier d\'exécution automatique.', + forMoreInformationAboutCronSeeThe: 'Pour plus d\'informations sur Cron, voir la', + cronExpressionFormatReference: 'Référence du format d\'expression Cron', + startVersion: 'Version de départ', + example000: 'Exemple: 0.0.0', + buildTemplate: 'Modèle de construction', + autorun: 'Exécution automatique', + playbookFilename: 'Nom de fichier du playbook *', + exampleSiteyml: 'Exemple: site.yml', + inventory2: 'Inventaire *', + repository: 'Dépôt *', + environment3: 'Environnement *', + vaultPassword: 'Mot de passe du coffre-fort', + vaultPassword2: 'Mot de passe du coffre-fort', + view: 'Vue', + cron: 'Cron', + iWantToRunATaskByTheCronOnlyForForNewCommitsOfSome: 'Je veux exécuter une tâche avec le cron uniquement pour les nouveaux commits d\'un dépôt spécifique', + repository2: 'Dépôt', + cronChecksNewCommitBeforeRun: 'Cron vérifie les nouveaux commits avant l\'exécution', + readThe: 'Lire la', + toLearnMoreAboutCron: 'pour en savoir plus sur Cron.', + suppressSuccessAlerts: 'Supprimer les alertes de réussite', + cliArgsJsonArrayExampleIMyinventoryshPrivatekeythe2: 'Arguments CLI (tableau JSON). Exemple: [ "-i", "@myinventory.sh", "--private-key=/there/id_rsa", "-vvvv" ]', + allowCliArgsInTask: 'Autoriser les arguments CLI dans la tâche', + docs: 'docs', + editViews: 'Modifier les vues', + newTemplate: 'Nouveau modèle', + taskTemplates2: 'Modèles de tâches', + all: 'Tous', + notLaunched: 'Non lancé', + by: 'par {user_name} {formatDate}', + editTemplate: 'Modifier le modèle', + newTemplate2: 'Nouveau modèle', + deleteTemplate: 'Supprimer le modèle', + playbook: 'Playbook', + email: 'Email', + adminUser: 'Utilisateur admin', + sendAlerts: 'Envoyer des alertes', + deleteUser: 'Supprimer l\'utilisateur', + newUser: 'Nouvel utilisateur', + re: 'Re{getActionButtonTitle}', + teamMember: '{expr} Membre de l\'équipe', + taskId: 'ID de tâche', + version: 'Version', + status: 'Statut', + start: 'Début', + actions: 'Actions', + alert: 'Alerte', + admin: 'Administrateur', + external: 'Externe', + time: 'Temps', + path: 'Chemin', + gitUrl: 'URL Git', + sshKey: 'Clé SSH', + lastTask: 'Dernière tâche', + task2: 'Tâche', + add: 'Ajouter', + password_required: 'Le mot de passe est requis', + name_required: 'Le nom est requis', + user_credentials_required: 'Les informations d\'identification de l\'utilisateur sont requises', + type_required: 'Le type est requis', + path_required: 'Le chemin vers le fichier d\'inventaire est requis', + private_key_required: 'La clé privée est requise', + project_name_required: 'Le nom du projet est requis', + repository_required: 'Le dépôt est requis', + branch_required: 'La branche est requise', + key_required: 'La clé est requise', + user_required: 'L\'utilisateur est requis', + build_version_required: 'La version de construction est requise', + title_required: 'Le titre est requis', + isRequired: 'est requis', + mustBeInteger: 'Doit être un entier', + mustBe0OrGreater: 'Doit être égal à 0 ou supérieur', + start_version_required: 'La version de départ est requise', + playbook_filename_required: 'Le nom de fichier du playbook est requis', + inventory_required: 'L\'inventaire est requis', + environment_required: 'L\'environnement est requis', + email_required: 'L\'adresse e-mail est requise', + build_template_required: 'Le modèle de construction est requis', + Task: 'Tâche', + Build: 'Construire', + Deploy: 'Déployer', + Run: 'Exécuter', +}; diff --git a/web/src/lang/index.js b/web/src/lang/index.js new file mode 100644 index 00000000..49a24255 --- /dev/null +++ b/web/src/lang/index.js @@ -0,0 +1,8 @@ +const files = require.context('.', false, /\.js$/); +const messages = {}; +files.keys().forEach((key) => { + if (key === './index.js') return; + messages[key.replace(/(\.\/|\.js)/g, '')] = files(key).default; +}); +const languages = Object.keys(messages); +export { messages, languages }; diff --git a/web/src/main.js b/web/src/main.js index 172b150e..fd96ddfc 100644 --- a/web/src/main.js +++ b/web/src/main.js @@ -6,13 +6,14 @@ import App from './App.vue'; import router from './router'; import vuetify from './plugins/vuetify'; import './assets/scss/main.scss'; +import i18n from './plugins/i18'; const convert = new Convert(); axios.defaults.baseURL = document.baseURI; Vue.config.productionTip = false; -Vue.filter('formatDate', (value) => (value ? moment.utc(String(value)).local(true).fromNow() : '—')); +Vue.filter('formatDate', (value) => (value ? moment(String(value)).fromNow() : '—')); Vue.filter('formatTime', (value) => (value ? moment(String(value)).format('LTS') : '—')); Vue.filter('formatLog', (value) => (value ? convert.toHtml(String(value)) : value)); @@ -51,5 +52,6 @@ Vue.filter('formatMilliseconds', (value) => { new Vue({ router, vuetify, + i18n, render: (h) => h(App), }).$mount('#app'); diff --git a/web/src/plugins/i18.js b/web/src/plugins/i18.js new file mode 100644 index 00000000..135a4a71 --- /dev/null +++ b/web/src/plugins/i18.js @@ -0,0 +1,12 @@ +import Vue from 'vue'; +import VueI18n from 'vue-i18n'; +import { messages } from '../lang'; + +Vue.use(VueI18n); +const locale = navigator.language.split('-')[0]; +export default new VueI18n({ + fallbackLocale: 'en', + locale, + messages, + silentFallbackWarn: true, +}); diff --git a/web/src/views/Auth.vue b/web/src/views/Auth.vue index f297ce5b..28150648 100644 --- a/web/src/views/Auth.vue +++ b/web/src/views/Auth.vue @@ -3,7 +3,7 @@ - How to fix sign-in issues + {{ $t('howToFixSigninIssues') }} mdi-close @@ -11,10 +11,10 @@

- Firstly, you need access to the server where Semaphore running. + {{ $t('firstlyYouNeedAccessToTheServerWhereSemaphoreRunni') }}

- Execute the following command on the server to see existing users: + {{ $t('executeTheFollowingCommandOnTheServerToSeeExisting') }}

- semaphore user list + {{ $t('semaphoreUserList') }}

- You can change password of existing user: + {{ $t('youCanChangePasswordOfExistingUser') }}

- semaphore user change-by-login --login user123 --password {{ makePasswordExample() }} + {{ $t('semaphoreUserChangebyloginLoginUser123Password', {makePasswordExample: + makePasswordExample()}) }}

- Or create new admin user: + {{ $t('orCreateNewAdminUser') }}

- Close + {{ $t('close2') }}
@@ -74,7 +75,7 @@ v-model="signInFormValid" style="width: 300px; height: 300px;" > -

SEMAPHORE

+

{{ $t('semaphore') }}

- Sign In + {{ $t('signIn') }} @@ -183,7 +184,7 @@ export default { document.location = document.baseURI; } catch (err) { if (err.response.status === 401) { - this.signInError = 'Incorrect login or password'; + this.signInError = this.$t('incorrectUsrPwd'); } else { this.signInError = getErrorMessage(err); } diff --git a/web/src/views/Users.vue b/web/src/views/Users.vue index 1cd8944e..8b498b51 100644 --- a/web/src/views/Users.vue +++ b/web/src/views/Users.vue @@ -3,7 +3,7 @@ From d3923f18b305095327c8e567a2fc39f08e5b0fe7 Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Sun, 17 Sep 2023 16:15:44 +0200 Subject: [PATCH 112/346] feat: admin can all --- api/events.go | 6 ++++-- api/projects/project.go | 2 +- api/projects/projects.go | 8 ++++++- api/projects/users.go | 30 +++++++++++++++++++------- api/users.go | 10 ++++----- db/ProjectUser.go | 6 +----- db/Store.go | 1 + db/bolt/project.go | 6 ++++++ db/sql/project.go | 15 +++++++++++++ web/src/App.vue | 8 +++++-- web/src/components/ItemListPageBase.js | 4 ++++ web/src/views/project/New.vue | 2 +- 12 files changed, 73 insertions(+), 25 deletions(-) diff --git a/api/events.go b/api/events.go index d392ef41..b14a8371 100644 --- a/api/events.go +++ b/api/events.go @@ -8,7 +8,7 @@ import ( "github.com/gorilla/context" ) -//nolint: gocyclo +// nolint: gocyclo func getEvents(w http.ResponseWriter, r *http.Request, limit int) { user := context.Get(r, "user").(*db.User) projectObj, exists := context.GetOk(r, "project") @@ -19,7 +19,9 @@ func getEvents(w http.ResponseWriter, r *http.Request, limit int) { if exists { project := projectObj.(db.Project) - _, err = helpers.Store(r).GetProjectUser(project.ID, user.ID) + if !user.Admin { // check permissions to view events + _, err = helpers.Store(r).GetProjectUser(project.ID, user.ID) + } if err != nil { helpers.WriteError(w, err) diff --git a/api/projects/project.go b/api/projects/project.go index 6dcadede..d3c01a8d 100644 --- a/api/projects/project.go +++ b/api/projects/project.go @@ -26,7 +26,7 @@ func ProjectMiddleware(next http.Handler) http.Handler { // check if user in project's team projectUser, err := helpers.Store(r).GetProjectUser(projectID, user.ID) - if err != nil { + if !user.Admin && err != nil { helpers.WriteError(w, err) return } diff --git a/api/projects/projects.go b/api/projects/projects.go index 83f3f3e4..a3c219c1 100644 --- a/api/projects/projects.go +++ b/api/projects/projects.go @@ -14,7 +14,13 @@ import ( func GetProjects(w http.ResponseWriter, r *http.Request) { user := context.Get(r, "user").(*db.User) - projects, err := helpers.Store(r).GetProjects(user.ID) + var err error + var projects []db.Project + if user.Admin { + projects, err = helpers.Store(r).GetAllProjects() + } else { + projects, err = helpers.Store(r).GetProjects(user.ID) + } if err != nil { helpers.WriteError(w, err) diff --git a/api/projects/users.go b/api/projects/users.go index c185e604..46415cdc 100644 --- a/api/projects/users.go +++ b/api/projects/users.go @@ -1,6 +1,7 @@ package projects import ( + "fmt" "github.com/ansible-semaphore/semaphore/api/helpers" "github.com/ansible-semaphore/semaphore/db" "net/http" @@ -108,24 +109,30 @@ func AddUser(w http.ResponseWriter, r *http.Request) { // RemoveUser removes a user from a project team func RemoveUser(w http.ResponseWriter, r *http.Request) { project := context.Get(r, "project").(db.Project) - projectUser := context.Get(r, "projectUser").(db.User) + me := context.Get(r, "user").(*db.User) // logged in user + targetUser := context.Get(r, "projectUser").(db.User) // target user + targetUserRole := context.Get(r, "projectUserRole").(db.ProjectUserRole) - err := helpers.Store(r).DeleteProjectUser(project.ID, projectUser.ID) + if !me.Admin && targetUser.ID == me.ID && targetUserRole == db.ProjectOwner { + helpers.WriteError(w, fmt.Errorf("owner can not left the project")) + return + } + + err := helpers.Store(r).DeleteProjectUser(project.ID, targetUser.ID) if err != nil { helpers.WriteError(w, err) return } - user := context.Get(r, "user").(*db.User) objType := db.EventUser - desc := "User ID " + strconv.Itoa(projectUser.ID) + " removed from team" + desc := "User ID " + strconv.Itoa(targetUser.ID) + " removed from team" _, err = helpers.Store(r).CreateEvent(db.Event{ - UserID: &user.ID, + UserID: &me.ID, ProjectID: &project.ID, ObjectType: &objType, - ObjectID: &projectUser.ID, + ObjectID: &targetUser.ID, Description: &desc, }) @@ -138,7 +145,14 @@ func RemoveUser(w http.ResponseWriter, r *http.Request) { func UpdateUser(w http.ResponseWriter, r *http.Request) { project := context.Get(r, "project").(db.Project) - user := context.Get(r, "projectUser").(db.User) + me := context.Get(r, "user").(*db.User) // logged in user + targetUser := context.Get(r, "projectUser").(db.User) + targetUserRole := context.Get(r, "projectUserRole").(db.ProjectUserRole) + + if !me.Admin && targetUser.ID == me.ID && targetUserRole == db.ProjectOwner { + helpers.WriteError(w, fmt.Errorf("owner can not change his role in the project")) + return + } var projectUser struct { Role db.ProjectUserRole `json:"role"` @@ -154,7 +168,7 @@ func UpdateUser(w http.ResponseWriter, r *http.Request) { } err := helpers.Store(r).UpdateProjectUser(db.ProjectUser{ - UserID: user.ID, + UserID: targetUser.ID, ProjectID: project.ID, Role: projectUser.Role, }) diff --git a/api/users.go b/api/users.go index 5146dcdc..7b488a1d 100644 --- a/api/users.go +++ b/api/users.go @@ -73,7 +73,7 @@ func getUserMiddleware(next http.Handler) http.Handler { } func updateUser(w http.ResponseWriter, r *http.Request) { - oldUser := context.Get(r, "_user").(db.User) + targetUser := context.Get(r, "_user").(db.User) editor := context.Get(r, "user").(*db.User) var user db.UserWithPwd @@ -81,25 +81,25 @@ func updateUser(w http.ResponseWriter, r *http.Request) { return } - if !editor.Admin && editor.ID != oldUser.ID { + if !editor.Admin && editor.ID != targetUser.ID { log.Warn(editor.Username + " is not permitted to edit users") w.WriteHeader(http.StatusUnauthorized) return } - if editor.ID == oldUser.ID && oldUser.Admin != user.Admin { + if editor.ID == targetUser.ID && targetUser.Admin != user.Admin { log.Warn("User can't edit his own role") w.WriteHeader(http.StatusUnauthorized) return } - if oldUser.External && oldUser.Username != user.Username { + if targetUser.External && targetUser.Username != user.Username { log.Warn("Username is not editable for external users") w.WriteHeader(http.StatusBadRequest) return } - user.ID = oldUser.ID + user.ID = targetUser.ID if err := helpers.Store(r).UpdateUser(user); err != nil { log.Error(err.Error()) w.WriteHeader(http.StatusBadRequest) diff --git a/db/ProjectUser.go b/db/ProjectUser.go index a5a75714..7977e15c 100644 --- a/db/ProjectUser.go +++ b/db/ProjectUser.go @@ -7,6 +7,7 @@ const ( ProjectManager ProjectUserRole = "manager" ProjectTaskRunner ProjectUserRole = "task_runner" ProjectGuest ProjectUserRole = "guest" + ProjectNone ProjectUserRole = "" ) type ProjectUserPermission int @@ -37,11 +38,6 @@ type ProjectUser struct { Role ProjectUserRole `db:"role" json:"role"` } -func (u *ProjectUser) Can(permissions ProjectUserPermission) bool { - userPermissions := rolePermissions[u.Role] - return (userPermissions & permissions) == permissions -} - func (r ProjectUserRole) Can(permissions ProjectUserPermission) bool { return (rolePermissions[r] & permissions) == permissions } diff --git a/db/Store.go b/db/Store.go index 22d41cb2..fedb67ad 100644 --- a/db/Store.go +++ b/db/Store.go @@ -143,6 +143,7 @@ type Store interface { GetUserByLoginOrEmail(login string, email string) (User, error) GetProject(projectID int) (Project, error) + GetAllProjects() ([]Project, error) GetProjects(userID int) ([]Project, error) CreateProject(project Project) (Project, error) DeleteProject(projectID int) error diff --git a/db/bolt/project.go b/db/bolt/project.go index b36c0e20..45156808 100644 --- a/db/bolt/project.go +++ b/db/bolt/project.go @@ -17,6 +17,12 @@ func (d *BoltDb) CreateProject(project db.Project) (db.Project, error) { return newProject.(db.Project), nil } +func (d *BoltDb) GetAllProjects() (projects []db.Project, err error) { + err = d.getObjects(0, db.ProjectProps, db.RetrieveQueryParams{}, nil, &projects) + + return +} + func (d *BoltDb) GetProjects(userID int) (projects []db.Project, err error) { projects = make([]db.Project, 0) diff --git a/db/sql/project.go b/db/sql/project.go index 63e36e1e..4f787402 100644 --- a/db/sql/project.go +++ b/db/sql/project.go @@ -23,6 +23,21 @@ func (d *SqlDb) CreateProject(project db.Project) (newProject db.Project, err er return } +func (d *SqlDb) GetAllProjects() (projects []db.Project, err error) { + query, args, err := squirrel.Select("p.*"). + From("project as p"). + OrderBy("p.name"). + ToSql() + + if err != nil { + return + } + + _, err = d.selectAll(&projects, query, args...) + + return +} + func (d *SqlDb) GetProjects(userID int) (projects []db.Project, err error) { query, args, err := squirrel.Select("p.*"). From("project as p"). diff --git a/web/src/App.vue b/web/src/App.vue index 7e6c1d0e..353c22a8 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -314,11 +314,14 @@ v-on="on" > - mdi-account + mdi-account - {{ user.name }} + + {{ user.name }} + admin + @@ -366,6 +369,7 @@ :projectId="projectId" :userPermissions="userRole.permissions" :userId="user ? user.id : null" + :isAdmin="user ? user.admin : false" > diff --git a/web/src/components/ItemListPageBase.js b/web/src/components/ItemListPageBase.js index 979438ea..98064544 100644 --- a/web/src/components/ItemListPageBase.js +++ b/web/src/components/ItemListPageBase.js @@ -18,6 +18,7 @@ export default { projectId: Number, userId: Number, userPermissions: Number, + isAdmin: Boolean, }, data() { @@ -51,6 +52,9 @@ export default { }, can(permission) { + if (this.isAdmin) { + return true; + } // eslint-disable-next-line no-bitwise return (this.userPermissions & permission) === permission; }, diff --git a/web/src/views/project/New.vue b/web/src/views/project/New.vue index 8c61f6c7..fe052ad2 100644 --- a/web/src/views/project/New.vue +++ b/web/src/views/project/New.vue @@ -8,7 +8,7 @@
- +
From f1c872d0d21fd8651e0c2933c1093a3728c7e89b Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Sun, 17 Sep 2023 16:19:47 +0200 Subject: [PATCH 113/346] fix: team management --- web/src/views/project/Team.vue | 4 ---- 1 file changed, 4 deletions(-) diff --git a/web/src/views/project/Team.vue b/web/src/views/project/Team.vue index ea2cddcc..8d23ea74 100644 --- a/web/src/views/project/Team.vue +++ b/web/src/views/project/Team.vue @@ -60,7 +60,6 @@ From 1fc842c32145983d3eec3d8cb8b32d559e0b3aef Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Sun, 17 Sep 2023 16:33:42 +0200 Subject: [PATCH 115/346] fix(ui): admin label --- web/src/App.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/App.vue b/web/src/App.vue index f99a2ad3..19da7999 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -325,7 +325,7 @@ - admin + admin From cb2bcd8f0b63a92cbb71aa9c24726a5f4381bbfc Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Sun, 17 Sep 2023 21:55:14 +0200 Subject: [PATCH 116/346] feat(be): create demo project --- api/projects/projects.go | 111 ++++++++++++++++++------- db/sql/project.go | 10 ++- web/src/components/ItemFormBase.js | 3 +- web/src/components/ProjectForm.vue | 6 -- web/src/lang/ru.js | 12 +-- web/src/views/project/New.vue | 9 +- web/src/views/project/TemplateView.vue | 2 +- 7 files changed, 102 insertions(+), 51 deletions(-) diff --git a/api/projects/projects.go b/api/projects/projects.go index a3c219c1..ffc9506b 100644 --- a/api/projects/projects.go +++ b/api/projects/projects.go @@ -40,7 +40,22 @@ func createDemoProject(projectID int, store db.Store) (err error) { var prodInv db.Inventory noneKey, err = store.CreateAccessKey(db.AccessKey{ - Name: "None", + Name: "None", + Type: db.AccessKeyNone, + ProjectID: &projectID, + }) + + if err != nil { + return + } + + noneKey, err = store.CreateAccessKey(db.AccessKey{ + Name: "Vault Password", + Type: db.AccessKeyLoginPassword, + ProjectID: &projectID, + LoginPassword: db.LoginPassword{ + Password: "RAX6yKN7sBn2qDagRPls", + }, }) if err != nil { @@ -48,7 +63,7 @@ func createDemoProject(projectID int, store db.Store) (err error) { } demoRepo, err = store.CreateRepository(db.Repository{ - Name: "Demo Project", + Name: "Demo", ProjectID: projectID, GitURL: "https://github.com/semaphoreui/demo-project.git", GitBranch: "main", @@ -72,8 +87,9 @@ func createDemoProject(projectID int, store db.Store) (err error) { buildInv, err = store.CreateInventory(db.Inventory{ Name: "Build", ProjectID: projectID, - Inventory: "[builder]\nlocalhost", + Inventory: "[builder]\nlocalhost ansible_connection=local", Type: "static", + SSHKeyID: &noneKey.ID, }) if err != nil { @@ -81,7 +97,10 @@ func createDemoProject(projectID int, store db.Store) (err error) { } devInv, err = store.CreateInventory(db.Inventory{ + Name: "Dev", ProjectID: projectID, + Inventory: "/invs/dev/hosts", + Type: "file", }) if err != nil { @@ -89,48 +108,78 @@ func createDemoProject(projectID int, store db.Store) (err error) { } prodInv, err = store.CreateInventory(db.Inventory{ + Name: "Prod", ProjectID: projectID, + Inventory: "/invs/prod/hosts", + Type: "file", }) + var desc string + if err != nil { return } + desc = "This task pings the website to provide real word example of using Semaphore." _, err = store.CreateTemplate(db.Template{ - Name: "Build", - Playbook: "build.yml", - ProjectID: projectID, - InventoryID: buildInv.ID, - EnvironmentID: &emptyEnv.ID, - RepositoryID: demoRepo.ID, - }) - - if err != nil { - return - } - - _, err = store.CreateTemplate(db.Template{ - Name: "Deploy to Dev", - Playbook: "deploy.yml", - ProjectID: projectID, - InventoryID: devInv.ID, - EnvironmentID: &emptyEnv.ID, - RepositoryID: demoRepo.ID, - }) - - if err != nil { - return - } - - _, err = store.CreateTemplate(db.Template{ - Name: "Deploy to Production", - Playbook: "deploy.yml", + Name: "Ping Site", + Playbook: "ping.yml", + Description: &desc, ProjectID: projectID, InventoryID: prodInv.ID, EnvironmentID: &emptyEnv.ID, RepositoryID: demoRepo.ID, }) + if err != nil { + return + } + + desc = "Creates artifact and store it in the cache." + + var startVersion = "1.0.0" + buildTpl, err := store.CreateTemplate(db.Template{ + Name: "Build", + Playbook: "build.yml", + Type: db.TemplateBuild, + ProjectID: projectID, + InventoryID: buildInv.ID, + EnvironmentID: &emptyEnv.ID, + RepositoryID: demoRepo.ID, + StartVersion: &startVersion, + }) + + if err != nil { + return + } + + _, err = store.CreateTemplate(db.Template{ + Name: "Deploy to Dev", + Type: db.TemplateDeploy, + Playbook: "deploy.yml", + ProjectID: projectID, + InventoryID: devInv.ID, + EnvironmentID: &emptyEnv.ID, + RepositoryID: demoRepo.ID, + BuildTemplateID: &buildTpl.ID, + Autorun: true, + }) + + if err != nil { + return + } + + _, err = store.CreateTemplate(db.Template{ + Name: "Deploy to Production", + Type: db.TemplateDeploy, + Playbook: "deploy.yml", + ProjectID: projectID, + InventoryID: prodInv.ID, + EnvironmentID: &emptyEnv.ID, + RepositoryID: demoRepo.ID, + BuildTemplateID: &buildTpl.ID, + }) + return } diff --git a/db/sql/project.go b/db/sql/project.go index 4f787402..ebfb6f6f 100644 --- a/db/sql/project.go +++ b/db/sql/project.go @@ -71,6 +71,14 @@ func (d *SqlDb) GetProject(projectID int) (project db.Project, err error) { } func (d *SqlDb) DeleteProject(projectID int) error { + + //tpls, err := d.GetTemplates(projectID, db.TemplateFilter{}, db.RetrieveQueryParams{}) + // + //if err != nil { + // return err + //} + // TODO: sort projects + tx, err := d.sql.Begin() if err != nil { @@ -90,7 +98,7 @@ func (d *SqlDb) DeleteProject(projectID int) error { _, err = tx.Exec(d.PrepareQuery(statement), projectID) if err != nil { - err = tx.Rollback() + _ = tx.Rollback() return err } } diff --git a/web/src/components/ItemFormBase.js b/web/src/components/ItemFormBase.js index ca2acf2a..8fd74c08 100644 --- a/web/src/components/ItemFormBase.js +++ b/web/src/components/ItemFormBase.js @@ -132,7 +132,7 @@ export default { * Saves or creates item via API. * @returns {Promise} null if validation didn't pass or user data if user saved. */ - async save() { + async save(data = {}) { this.formError = null; if (!this.$refs.form.validate()) { @@ -155,6 +155,7 @@ export default { data: { ...this.item, project_id: this.projectId, + ...data, }, ...(this.getRequestOptions()), })).data; diff --git a/web/src/components/ProjectForm.vue b/web/src/components/ProjectForm.vue index 6e598704..927ab651 100644 --- a/web/src/components/ProjectForm.vue +++ b/web/src/components/ProjectForm.vue @@ -48,9 +48,6 @@ import ItemFormBase from '@/components/ItemFormBase'; export default { - props: { - demoProject: Boolean, - }, mixins: [ItemFormBase], methods: { getItemsUrl() { @@ -59,9 +56,6 @@ export default { getSingleItemUrl() { return `/api/project/${this.itemId}`; }, - beforeSave() { - this.item.demo = this.demoProject; - }, }, }; diff --git a/web/src/lang/ru.js b/web/src/lang/ru.js index 25276fe3..32b67eb2 100644 --- a/web/src/lang/ru.js +++ b/web/src/lang/ru.js @@ -200,9 +200,9 @@ export default { gitUrl: 'Git URL', sshKey: 'SSH ключ', lastTask: 'Последняя задача', - task2: 'Задача', - build: 'Сборка', - deploy: 'Развертывать', + // task2: 'Задача', + // build: 'Сборка', + // deploy: 'Развертывать', run: 'Запуск', add: 'Добавить', password_required: 'Требуется пароль', @@ -227,9 +227,9 @@ export default { environment_required: 'Требуется окружение', email_required: 'Требуется почта', build_template_required: 'Требуется шаблон сборки', - Task: 'Задача', - Build: 'Сборка', - Deploy: 'Развертывать', + // Task: 'Задача', + // Build: 'Сборка', + // Deploy: 'Развертывать', Run: 'Запуск', }; diff --git a/web/src/views/project/New.vue b/web/src/views/project/New.vue index fe052ad2..6c45ee51 100644 --- a/web/src/views/project/New.vue +++ b/web/src/views/project/New.vue @@ -8,7 +8,7 @@
- +
@@ -33,7 +33,6 @@ export default { components: { ProjectForm }, data() { return { - demoProject: false, }; }, @@ -50,13 +49,13 @@ export default { }, async createProject() { - this.demoProject = false; await this.$refs.editForm.save(); }, async createDemoProject() { - this.demoProject = true; - await this.$refs.editForm.save(); + await this.$refs.editForm.save({ + demo: true, + }); }, }, }; diff --git a/web/src/views/project/TemplateView.vue b/web/src/views/project/TemplateView.vue index b0095123..c209df11 100644 --- a/web/src/views/project/TemplateView.vue +++ b/web/src/views/project/TemplateView.vue @@ -63,7 +63,7 @@ From e6c72fb330fffec7e521ae372e1da0ed7f875003 Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Sun, 17 Sep 2023 22:06:28 +0200 Subject: [PATCH 117/346] fix(be): ignore max parallel tasks if it is 0 --- services/tasks/TaskPool.go | 2 +- web/src/lang/pt.js | 12 ++++++------ web/src/views/project/History.vue | 1 + 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/services/tasks/TaskPool.go b/services/tasks/TaskPool.go index 6bcd1b0d..d5d3d0cc 100644 --- a/services/tasks/TaskPool.go +++ b/services/tasks/TaskPool.go @@ -178,7 +178,7 @@ func (p *TaskPool) Run() { func (p *TaskPool) blocks(t *TaskRunner) bool { - if len(p.runningTasks) >= util.Config.MaxParallelTasks { + if util.Config.MaxParallelTasks > 0 && len(p.runningTasks) >= util.Config.MaxParallelTasks { return true } diff --git a/web/src/lang/pt.js b/web/src/lang/pt.js index 337ea13a..7769cb0f 100644 --- a/web/src/lang/pt.js +++ b/web/src/lang/pt.js @@ -200,9 +200,9 @@ export default { gitUrl: 'URL Git', sshKey: 'Chave SSH', lastTask: 'Última Tarefa', - task2: 'Tarefa', - build: 'Compilar', - deploy: 'Implementar', + // task2: 'Tarefa', + // build: 'Compilar', + // deploy: 'Implementar', run: 'Executar', add: 'Adicionar', password_required: 'Palavra-passe obrigatória', @@ -227,8 +227,8 @@ export default { environment_required: 'Ambiente obrigatório', email_required: 'E-mail obrigatório', build_template_required: 'Modelo de compilação obrigatório', - Task: 'Tarefa', - Build: 'Compilar', - Deploy: 'Implementar', + // Task: 'Tarefa', + // Build: 'Compilar', + // Deploy: 'Implementar', Run: 'Executar', }; diff --git a/web/src/views/project/History.vue b/web/src/views/project/History.vue index 2d9abc34..0a7b2633 100644 --- a/web/src/views/project/History.vue +++ b/web/src/views/project/History.vue @@ -16,6 +16,7 @@ :to="`/project/${projectId}/settings`" >{{ $t('settings') }} + {{ $t('billing') }} Date: Sun, 17 Sep 2023 22:24:57 +0200 Subject: [PATCH 118/346] fix(demo): fill required fields --- api/projects/projects.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/api/projects/projects.go b/api/projects/projects.go index ffc9506b..c06ec44c 100644 --- a/api/projects/projects.go +++ b/api/projects/projects.go @@ -49,7 +49,7 @@ func createDemoProject(projectID int, store db.Store) (err error) { return } - noneKey, err = store.CreateAccessKey(db.AccessKey{ + vaultKey, err := store.CreateAccessKey(db.AccessKey{ Name: "Vault Password", Type: db.AccessKeyLoginPassword, ProjectID: &projectID, @@ -101,6 +101,7 @@ func createDemoProject(projectID int, store db.Store) (err error) { ProjectID: projectID, Inventory: "/invs/dev/hosts", Type: "file", + SSHKeyID: &noneKey.ID, }) if err != nil { @@ -112,6 +113,7 @@ func createDemoProject(projectID int, store db.Store) (err error) { ProjectID: projectID, Inventory: "/invs/prod/hosts", Type: "file", + SSHKeyID: &noneKey.ID, }) var desc string @@ -163,6 +165,7 @@ func createDemoProject(projectID int, store db.Store) (err error) { RepositoryID: demoRepo.ID, BuildTemplateID: &buildTpl.ID, Autorun: true, + VaultKeyID: &vaultKey.ID, }) if err != nil { @@ -178,6 +181,7 @@ func createDemoProject(projectID int, store db.Store) (err error) { EnvironmentID: &emptyEnv.ID, RepositoryID: demoRepo.ID, BuildTemplateID: &buildTpl.ID, + VaultKeyID: &vaultKey.ID, }) return From e706d92ee04cfb3d69d46a39d433f7e21651d325 Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Sun, 17 Sep 2023 23:33:56 +0200 Subject: [PATCH 119/346] feat(ui): change button color --- web/src/views/project/History.vue | 1 - web/src/views/project/New.vue | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/web/src/views/project/History.vue b/web/src/views/project/History.vue index 0a7b2633..2d9abc34 100644 --- a/web/src/views/project/History.vue +++ b/web/src/views/project/History.vue @@ -16,7 +16,6 @@ :to="`/project/${projectId}/settings`" >{{ $t('settings') }} - {{ $t('billing') }} Create Demo Project {{ $t('create') }} From 80d7c784fe29bf4116154b32195e7e7656ab647a Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Mon, 18 Sep 2023 19:46:55 +0200 Subject: [PATCH 120/346] fix: limit data by users --- api/projects/users.go | 25 +++++++++++++++++++++---- api/users.go | 23 ++++++++++++++++++++++- web/src/components/TeamMemberForm.vue | 2 +- web/src/views/Users.vue | 2 +- web/src/views/project/Keys.vue | 2 +- web/src/views/project/Team.vue | 5 ----- 6 files changed, 46 insertions(+), 13 deletions(-) diff --git a/api/projects/users.go b/api/projects/users.go index 46415cdc..faba3f99 100644 --- a/api/projects/users.go +++ b/api/projects/users.go @@ -2,13 +2,12 @@ package projects import ( "fmt" + log "github.com/Sirupsen/logrus" "github.com/ansible-semaphore/semaphore/api/helpers" "github.com/ansible-semaphore/semaphore/db" + "github.com/gorilla/context" "net/http" "strconv" - - log "github.com/Sirupsen/logrus" - "github.com/gorilla/context" ) // UserMiddleware ensures a user exists and loads it to the context @@ -39,6 +38,13 @@ func UserMiddleware(next http.Handler) http.Handler { }) } +type projUser struct { + ID int `json:"id"` + Username string `json:"username"` + Name string `json:"name"` + Role db.ProjectUserRole `json:"role"` +} + // GetUsers returns all users in a project func GetUsers(w http.ResponseWriter, r *http.Request) { @@ -56,7 +62,18 @@ func GetUsers(w http.ResponseWriter, r *http.Request) { return } - helpers.WriteJSON(w, http.StatusOK, users) + var result []projUser + + for _, user := range users { + result = append(result, projUser{ + ID: user.ID, + Name: user.Name, + Username: user.Username, + Role: user.Role, + }) + } + + helpers.WriteJSON(w, http.StatusOK, result) } // AddUser adds a user to a projects team in the database diff --git a/api/users.go b/api/users.go index 7b488a1d..5cc195d2 100644 --- a/api/users.go +++ b/api/users.go @@ -10,14 +10,35 @@ import ( "github.com/gorilla/context" ) +type minimalUser struct { + ID int `json:"id"` + Username string `json:"username"` + Name string `json:"name"` +} + func getUsers(w http.ResponseWriter, r *http.Request) { + currentUser := context.Get(r, "user").(*db.User) users, err := helpers.Store(r).GetUsers(db.RetrieveQueryParams{}) if err != nil { panic(err) } - helpers.WriteJSON(w, http.StatusOK, users) + if currentUser.Admin { + helpers.WriteJSON(w, http.StatusOK, users) + } else { + var result []minimalUser + + for _, user := range users { + result = append(result, minimalUser{ + ID: user.ID, + Name: user.Name, + Username: user.Username, + }) + } + + helpers.WriteJSON(w, http.StatusOK, result) + } } func addUser(w http.ResponseWriter, r *http.Request) { diff --git a/web/src/components/TeamMemberForm.vue b/web/src/components/TeamMemberForm.vue index 45f5dd23..177a73ef 100644 --- a/web/src/components/TeamMemberForm.vue +++ b/web/src/components/TeamMemberForm.vue @@ -16,7 +16,7 @@ :label="$t('user')" :items="users" item-value="id" - item-text="name" + :item-text="(itm) => `${itm.username} (${itm.name})`" :rules="[v => !!v || $t('user_required')]" required :disabled="formSaving" diff --git a/web/src/views/Users.vue b/web/src/views/Users.vue index 840eb6af..c81ece0e 100644 --- a/web/src/views/Users.vue +++ b/web/src/views/Users.vue @@ -21,7 +21,7 @@ diff --git a/web/src/views/project/Keys.vue b/web/src/views/project/Keys.vue index 96a7f9ec..e1195f72 100644 --- a/web/src/views/project/Keys.vue +++ b/web/src/views/project/Keys.vue @@ -29,7 +29,7 @@ diff --git a/web/src/views/project/Team.vue b/web/src/views/project/Team.vue index 8d23ea74..82c95088 100644 --- a/web/src/views/project/Team.vue +++ b/web/src/views/project/Team.vue @@ -111,11 +111,6 @@ export default { text: this.$i18n.t('username'), value: 'username', }, - { - text: this.$i18n.t('email'), - value: 'email', - width: '50%', - }, { text: this.$i18n.t('role'), value: 'role', From 46ea9b37a1cda9d685643db140007e7c0bf315aa Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Mon, 18 Sep 2023 19:49:55 +0200 Subject: [PATCH 121/346] chore: update spec --- api-docs.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/api-docs.yml b/api-docs.yml index e5908826..5310a3e0 100644 --- a/api-docs.yml +++ b/api-docs.yml @@ -115,12 +115,22 @@ definitions: type: string created: type: string -# pattern: ^\d{4}-(?:0[0-9]{1}|1[0-2]{1})-[0-9]{2}T\d{2}:\d{2}:\d{2}Z$ alert: type: boolean admin: type: boolean + ProjectUser: + type: object + properties: + id: + type: integer + minimum: 1 + name: + type: string + username: + type: string + APIToken: type: object properties: @@ -1065,7 +1075,7 @@ paths: schema: type: array items: - $ref: "#/definitions/User" + $ref: "#/definitions/ProjectUser" post: tags: - project From 5a1357724d2e5d15b0a869af35e6448835b8151b Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Mon, 18 Sep 2023 21:43:13 +0200 Subject: [PATCH 122/346] feat: left project --- api/projects/project.go | 6 +++--- api/projects/users.go | 23 +++++++++++++++++------ api/router.go | 5 +++++ web/src/App.vue | 9 +++++---- web/src/components/ItemListPageBase.js | 1 + web/src/views/project/Team.vue | 16 ++++++++++++++++ 6 files changed, 47 insertions(+), 13 deletions(-) diff --git a/api/projects/project.go b/api/projects/project.go index d3c01a8d..68d075a8 100644 --- a/api/projects/project.go +++ b/api/projects/project.go @@ -48,10 +48,10 @@ func ProjectMiddleware(next http.Handler) http.Handler { func GetMustCanMiddleware(permissions db.ProjectUserPermission) mux.MiddlewareFunc { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - user := context.Get(r, "user").(*db.User) - projectUserRole := context.Get(r, "projectUserRole").(db.ProjectUserRole) + me := context.Get(r, "user").(*db.User) + myRole := context.Get(r, "projectUserRole").(db.ProjectUserRole) - if !user.Admin && r.Method != "GET" && r.Method != "HEAD" && !projectUserRole.Can(permissions) { + if !me.Admin && r.Method != "GET" && r.Method != "HEAD" && !myRole.Can(permissions) { w.WriteHeader(http.StatusForbidden) return } diff --git a/api/projects/users.go b/api/projects/users.go index faba3f99..94af62bc 100644 --- a/api/projects/users.go +++ b/api/projects/users.go @@ -123,14 +123,13 @@ func AddUser(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } -// RemoveUser removes a user from a project team -func RemoveUser(w http.ResponseWriter, r *http.Request) { +// removeUser removes a user from a project team +func removeUser(targetUser db.User, w http.ResponseWriter, r *http.Request) { project := context.Get(r, "project").(db.Project) - me := context.Get(r, "user").(*db.User) // logged in user - targetUser := context.Get(r, "projectUser").(db.User) // target user - targetUserRole := context.Get(r, "projectUserRole").(db.ProjectUserRole) + me := context.Get(r, "user").(*db.User) // logged in user + myRole := context.Get(r, "projectUserRole").(db.ProjectUserRole) - if !me.Admin && targetUser.ID == me.ID && targetUserRole == db.ProjectOwner { + if !me.Admin && targetUser.ID == me.ID && myRole == db.ProjectOwner { helpers.WriteError(w, fmt.Errorf("owner can not left the project")) return } @@ -160,6 +159,18 @@ func RemoveUser(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } +// LeftProject removes a user from a project team +func LeftProject(w http.ResponseWriter, r *http.Request) { + me := context.Get(r, "user").(*db.User) // logged in user + removeUser(*me, w, r) +} + +// RemoveUser removes a user from a project team +func RemoveUser(w http.ResponseWriter, r *http.Request) { + targetUser := context.Get(r, "projectUser").(db.User) // target user + removeUser(targetUser, w, r) +} + func UpdateUser(w http.ResponseWriter, r *http.Request) { project := context.Get(r, "project").(db.Project) me := context.Get(r, "user").(*db.User) // logged in user diff --git a/api/router.go b/api/router.go index b970f787..cd22f4af 100644 --- a/api/router.go +++ b/api/router.go @@ -187,9 +187,14 @@ func Route() *mux.Router { projectAdminAPI.Methods("PUT").HandlerFunc(projects.UpdateProject) projectAdminAPI.Methods("DELETE").HandlerFunc(projects.DeleteProject) + meAPI := authenticatedAPI.Path("/project/{project_id}/me").Subrouter() + meAPI.Use(projects.ProjectMiddleware) + meAPI.HandleFunc("", projects.LeftProject).Methods("DELETE") + // // Manage project users projectAdminUsersAPI := authenticatedAPI.PathPrefix("/project/{project_id}").Subrouter() + projectAdminUsersAPI.Use(projects.ProjectMiddleware, projects.GetMustCanMiddleware(db.CanManageProjectUsers)) projectAdminUsersAPI.Path("/users").HandlerFunc(projects.AddUser).Methods("POST") diff --git a/web/src/App.vue b/web/src/App.vue index 19da7999..a1254278 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -371,9 +371,10 @@ @@ -617,7 +618,7 @@ export default { return { drawer: null, user: null, - userRole: 0, + userRole: null, systemInfo: null, state: 'loading', snackbar: false, diff --git a/web/src/components/ItemListPageBase.js b/web/src/components/ItemListPageBase.js index 98064544..de5179cb 100644 --- a/web/src/components/ItemListPageBase.js +++ b/web/src/components/ItemListPageBase.js @@ -18,6 +18,7 @@ export default { projectId: Number, userId: Number, userPermissions: Number, + userRole: String, isAdmin: Boolean, }, diff --git a/web/src/views/project/Team.vue b/web/src/views/project/Team.vue index 82c95088..8235ee2b 100644 --- a/web/src/views/project/Team.vue +++ b/web/src/views/project/Team.vue @@ -29,6 +29,13 @@ {{ $t('team2') }} + Left Project + Date: Mon, 18 Sep 2023 22:04:23 +0200 Subject: [PATCH 123/346] fix(be): init array my empty --- api/projects/users.go | 2 +- api/users.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/projects/users.go b/api/projects/users.go index 94af62bc..25115307 100644 --- a/api/projects/users.go +++ b/api/projects/users.go @@ -62,7 +62,7 @@ func GetUsers(w http.ResponseWriter, r *http.Request) { return } - var result []projUser + var result = make([]projUser, 0) for _, user := range users { result = append(result, projUser{ diff --git a/api/users.go b/api/users.go index 5cc195d2..18893b0b 100644 --- a/api/users.go +++ b/api/users.go @@ -27,7 +27,7 @@ func getUsers(w http.ResponseWriter, r *http.Request) { if currentUser.Admin { helpers.WriteJSON(w, http.StatusOK, users) } else { - var result []minimalUser + var result = make([]minimalUser, 0) for _, user := range users { result = append(result, minimalUser{ From f767ac931a24b29c5dbd14930f591fa402635156 Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Tue, 19 Sep 2023 15:35:59 +0200 Subject: [PATCH 124/346] feat(billing): add billing block --- api/user.go | 2 + util/config.go | 2 + web/src/App.vue | 1 + web/src/components/ItemListPageBase.js | 1 + web/src/router/index.js | 5 +++ web/src/views/project/Activity.vue | 5 +++ web/src/views/project/Billing.vue | 59 ++++++++++++++++++++++++++ web/src/views/project/History.vue | 5 +++ web/src/views/project/Settings.vue | 4 ++ 9 files changed, 84 insertions(+) create mode 100644 web/src/views/project/Billing.vue diff --git a/api/user.go b/api/user.go index dc9e7222..1ed8d09c 100644 --- a/api/user.go +++ b/api/user.go @@ -22,10 +22,12 @@ func getUser(w http.ResponseWriter, r *http.Request) { var user struct { db.User CanCreateProject bool `json:"can_create_project"` + Billing bool `json:"billing"` } user.User = *context.Get(r, "user").(*db.User) user.CanCreateProject = user.Admin || util.Config.NonAdminCanCreateProject + user.Billing = util.Config.BillingEnabled helpers.WriteJSON(w, http.StatusOK, user) } diff --git a/util/config.go b/util/config.go index fd906018..7ad57cba 100644 --- a/util/config.go +++ b/util/config.go @@ -180,6 +180,8 @@ type ConfigType struct { UseRemoteRunner bool `json:"use_remote_runner" env:"SEMAPHORE_USE_REMOTE_RUNNER"` Runner RunnerSettings `json:"runner"` + + BillingEnabled bool `json:"billing_enabled"` } // Config exposes the application configuration storage for use in the application diff --git a/web/src/App.vue b/web/src/App.vue index a1254278..a20542c5 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -375,6 +375,7 @@ :userRole="(userRole || {}).role" :userId="(user || {}).id" :isAdmin="(user || {}).admin" + :user="user" > diff --git a/web/src/components/ItemListPageBase.js b/web/src/components/ItemListPageBase.js index de5179cb..be3269ed 100644 --- a/web/src/components/ItemListPageBase.js +++ b/web/src/components/ItemListPageBase.js @@ -20,6 +20,7 @@ export default { userPermissions: Number, userRole: String, isAdmin: Boolean, + user: Object, }, data() { diff --git a/web/src/router/index.js b/web/src/router/index.js index 95e31f1b..e3a6969b 100644 --- a/web/src/router/index.js +++ b/web/src/router/index.js @@ -2,6 +2,7 @@ import Vue from 'vue'; import VueRouter from 'vue-router'; import History from '../views/project/History.vue'; import Activity from '../views/project/Activity.vue'; +import Billing from '../views/project/Billing.vue'; import Settings from '../views/project/Settings.vue'; import Templates from '../views/project/Templates.vue'; import TemplateView from '../views/project/TemplateView.vue'; @@ -37,6 +38,10 @@ const routes = [ path: '/project/:projectId/settings', component: Settings, }, + { + path: '/project/:projectId/billing', + component: Billing, + }, { path: '/project/:projectId/templates', component: Templates, diff --git a/web/src/views/project/Activity.vue b/web/src/views/project/Activity.vue index 6cbcddf0..6c4ccaf6 100644 --- a/web/src/views/project/Activity.vue +++ b/web/src/views/project/Activity.vue @@ -15,6 +15,11 @@ > {{ $t('settings') }} + Billing Soon +
+ + + + + {{ $t('dashboard') }} + + + + {{ $t('history') }} + {{ $t('activity') }} + {{ $t('settings') }} + Billing Soon + +
+ Soon +
+
+ + + diff --git a/web/src/views/project/History.vue b/web/src/views/project/History.vue index 2d9abc34..c5b15cce 100644 --- a/web/src/views/project/History.vue +++ b/web/src/views/project/History.vue @@ -16,6 +16,11 @@ :to="`/project/${projectId}/settings`" >{{ $t('settings') }} + Billing Soon {{ $t('history') }} {{ $t('activity') }} {{ $t('settings') }} + Billing Soon
From 384108513dc47467265255827537ffbddba16178 Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Tue, 19 Sep 2023 17:00:46 +0200 Subject: [PATCH 125/346] feat(billing): coming soon --- web/src/views/project/Billing.vue | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/web/src/views/project/Billing.vue b/web/src/views/project/Billing.vue index dd662d5b..5fe8f12e 100644 --- a/web/src/views/project/Billing.vue +++ b/web/src/views/project/Billing.vue @@ -21,9 +21,17 @@ :to="`/project/${projectId}/billing`" >Billing Soon -
- Soon -
+ + +

+ Coming soon +

+
The billing will be available soon.
+
- From 7e7a543e0397c3fad8212202f88a10f7d2fcb4ba Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Mon, 25 Dec 2023 04:00:28 +0500 Subject: [PATCH 168/346] feat(backend): default dialect to bolt --- util/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/config.go b/util/config.go index 941970a9..1b8962df 100644 --- a/util/config.go +++ b/util/config.go @@ -111,7 +111,7 @@ type ConfigType struct { BoltDb DbConfig `json:"bolt"` Postgres DbConfig `json:"postgres"` - Dialect string `json:"dialect" rule:"^mysql|bolt|postgres$" env:"SEMAPHORE_DB_DIALECT"` + Dialect string `json:"dialect" default:"bolt" rule:"^mysql|bolt|postgres$" env:"SEMAPHORE_DB_DIALECT"` // Format `:port_num` eg, :3000 // if : is missing it will be corrected From 34485b7b8e825a1a1f0e671e3d2a876c7d1c1796 Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Mon, 25 Dec 2023 04:17:12 +0500 Subject: [PATCH 169/346] feat(backend): add config option max_task_duration_sec --- services/tasks/RemoteJob.go | 12 ++++++++++++ util/config.go | 2 ++ 2 files changed, 14 insertions(+) diff --git a/services/tasks/RemoteJob.go b/services/tasks/RemoteJob.go index f38f4d28..90a9dc2f 100644 --- a/services/tasks/RemoteJob.go +++ b/services/tasks/RemoteJob.go @@ -7,6 +7,7 @@ import ( "github.com/ansible-semaphore/semaphore/db" "github.com/ansible-semaphore/semaphore/db_lib" "github.com/ansible-semaphore/semaphore/lib" + "github.com/ansible-semaphore/semaphore/util" "net/http" "time" ) @@ -120,7 +121,16 @@ func (t *RemoteJob) Run(username string, incomingVersion *string) (err error) { tsk.RunnerID = runner.ID + startTime := time.Now() + + taskTimedOut := false + for { + if util.Config.MaxTaskDurationSec > 0 && int(time.Now().Sub(startTime).Seconds()) > util.Config.MaxTaskDurationSec { + taskTimedOut = true + break + } + time.Sleep(1_000_000_000) tsk = t.taskPool.GetTask(t.Task.ID) if tsk.Task.Status == lib.TaskSuccessStatus || @@ -138,6 +148,8 @@ func (t *RemoteJob) Run(username string, incomingVersion *string) (err error) { if tsk.Task.Status == lib.TaskFailStatus { err = fmt.Errorf("task failed") + } else if taskTimedOut { + err = fmt.Errorf("task timed out") } return diff --git a/util/config.go b/util/config.go index 1b8962df..32c70c34 100644 --- a/util/config.go +++ b/util/config.go @@ -169,6 +169,8 @@ type ConfigType struct { // oidc settings OidcProviders map[string]OidcProvider `json:"oidc_providers"` + MaxTaskDurationSec int `json:"max_task_duration_sec" env:"MAX_TASK_DURATION_SEC"` + // task concurrency MaxParallelTasks int `json:"max_parallel_tasks" default:"10" rule:"^[0-9]{1,10}$" env:"SEMAPHORE_MAX_PARALLEL_TASKS"` From 144a15f96fc0e45aba4de7b4f23b74f58583c7ef Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Mon, 25 Dec 2023 14:49:47 +0500 Subject: [PATCH 170/346] fix(runner): check runner id in request --- api/runners/runners.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/api/runners/runners.go b/api/runners/runners.go index bb1c3ca2..fa73a7ed 100644 --- a/api/runners/runners.go +++ b/api/runners/runners.go @@ -102,6 +102,9 @@ func GetRunner(w http.ResponseWriter, r *http.Request) { } func UpdateRunner(w http.ResponseWriter, r *http.Request) { + + runner := context.Get(r, "runner").(db.Runner) + var body runners.RunnerProgress if !helpers.Bind(w, r, &body) { @@ -126,6 +129,11 @@ func UpdateRunner(w http.ResponseWriter, r *http.Request) { continue } + if tsk.RunnerID != runner.ID { + // TODO: add error message + continue + } + for _, logRecord := range job.LogRecords { tsk.Log2(logRecord.Message, logRecord.Time) } From c00f5a1ae4cae3f127aca3fd622fbd7f93f61119 Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Mon, 25 Dec 2023 21:10:39 +0500 Subject: [PATCH 171/346] feat: update go to v1.20 (#1690) * feat: update go to v1.20 * ci: golang version to 20 in github actions * ci: fix golang version --- .github/workflows/beta.yml | 4 ++-- .github/workflows/dev.yml | 6 +++--- .github/workflows/release.yml | 4 ++-- deployment/docker/ci/Dockerfile | 2 +- deployment/docker/ci/dredd.Dockerfile | 2 +- deployment/docker/dev/Dockerfile | 2 +- deployment/docker/prod/Dockerfile | 2 +- deployment/docker/prod/buildx.Dockerfile | 2 +- deployment/docker/prod/runner.buildx.Dockerfile | 2 +- go.mod | 2 +- 10 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index 6111603f..a818cada 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -9,7 +9,7 @@ jobs: runs-on: [ubuntu-latest] steps: - uses: actions/setup-go@v3 - with: { go-version: 1.19 } + with: { go-version: '1.20' } - uses: actions/setup-node@v3 with: { node-version: '16' } @@ -36,7 +36,7 @@ jobs: runs-on: [ubuntu-latest] steps: - uses: actions/setup-go@v3 - with: { go-version: 1.19 } + with: { go-version: '1.20' } - run: go install github.com/go-task/task/v3/cmd/task@latest diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index c8eef63b..3b225ff9 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -11,7 +11,7 @@ jobs: runs-on: [ubuntu-latest] steps: - uses: actions/setup-go@v3 - with: { go-version: 1.19 } + with: { go-version: '1.20' } - uses: actions/setup-node@v3 with: { node-version: '16' } @@ -81,7 +81,7 @@ jobs: needs: [test-db-migration] steps: - uses: actions/setup-go@v3 - with: { go-version: 1.19 } + with: { go-version: '1.20' } - run: go install github.com/go-task/task/v3/cmd/task@latest @@ -97,7 +97,7 @@ jobs: if: github.ref == 'refs/heads/develop' steps: - uses: actions/setup-go@v3 - with: { go-version: 1.19 } + with: { go-version: '1.20' } - run: go install github.com/go-task/task/v3/cmd/task@latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8bf4e53d..d1086820 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ jobs: runs-on: [ubuntu-latest] steps: - uses: actions/setup-go@v3 - with: { go-version: 1.19 } + with: { go-version: '1.20' } - uses: actions/setup-node@v3 with: { node-version: '16' } @@ -36,7 +36,7 @@ jobs: runs-on: [ubuntu-latest] steps: - uses: actions/setup-go@v3 - with: { go-version: 1.19 } + with: { go-version: '1.20' } - run: go install github.com/go-task/task/v3/cmd/task@latest diff --git a/deployment/docker/ci/Dockerfile b/deployment/docker/ci/Dockerfile index ea0db484..fe521da2 100644 --- a/deployment/docker/ci/Dockerfile +++ b/deployment/docker/ci/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.19-alpine3.18 +FROM golang:1.20-alpine3.18 ENV SEMAPHORE_VERSION="development" SEMAPHORE_ARCH="linux_amd64" \ SEMAPHORE_CONFIG_PATH="${SEMAPHORE_CONFIG_PATH:-/etc/semaphore}" \ diff --git a/deployment/docker/ci/dredd.Dockerfile b/deployment/docker/ci/dredd.Dockerfile index eae72d81..e7d96b36 100644 --- a/deployment/docker/ci/dredd.Dockerfile +++ b/deployment/docker/ci/dredd.Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.19-alpine3.18 as golang +FROM golang:1.20-alpine3.18 as golang RUN apk add --no-cache curl git diff --git a/deployment/docker/dev/Dockerfile b/deployment/docker/dev/Dockerfile index 4b3f28fc..b3b55c68 100644 --- a/deployment/docker/dev/Dockerfile +++ b/deployment/docker/dev/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.19-alpine3.18 +FROM golang:1.20-alpine3.18 ENV SEMAPHORE_VERSION="development" SEMAPHORE_ARCH="linux_amd64" \ SEMAPHORE_CONFIG_PATH="${SEMAPHORE_CONFIG_PATH:-/etc/semaphore}" \ diff --git a/deployment/docker/prod/Dockerfile b/deployment/docker/prod/Dockerfile index 746ab1a0..d8c93932 100644 --- a/deployment/docker/prod/Dockerfile +++ b/deployment/docker/prod/Dockerfile @@ -1,5 +1,5 @@ # ansible-semaphore production image -FROM golang:1.19-alpine3.18 as builder +FROM golang:1.20-alpine3.18 as builder COPY ./ /go/src/github.com/ansible-semaphore/semaphore WORKDIR /go/src/github.com/ansible-semaphore/semaphore diff --git a/deployment/docker/prod/buildx.Dockerfile b/deployment/docker/prod/buildx.Dockerfile index b4beefee..e7eef805 100644 --- a/deployment/docker/prod/buildx.Dockerfile +++ b/deployment/docker/prod/buildx.Dockerfile @@ -1,5 +1,5 @@ # ansible-semaphore production image -FROM --platform=$BUILDPLATFORM golang:1.19-alpine3.18 as builder +FROM --platform=$BUILDPLATFORM golang:1.20-alpine3.18 as builder COPY ./ /go/src/github.com/ansible-semaphore/semaphore WORKDIR /go/src/github.com/ansible-semaphore/semaphore diff --git a/deployment/docker/prod/runner.buildx.Dockerfile b/deployment/docker/prod/runner.buildx.Dockerfile index bf7b0bed..bc56e65e 100644 --- a/deployment/docker/prod/runner.buildx.Dockerfile +++ b/deployment/docker/prod/runner.buildx.Dockerfile @@ -1,5 +1,5 @@ # ansible-semaphore production image -FROM --platform=$BUILDPLATFORM golang:1.19-alpine3.18 as builder +FROM --platform=$BUILDPLATFORM golang:1.20-alpine3.18 as builder COPY ./ /go/src/github.com/ansible-semaphore/semaphore WORKDIR /go/src/github.com/ansible-semaphore/semaphore diff --git a/go.mod b/go.mod index 39a1279c..2399e391 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/ansible-semaphore/semaphore -go 1.19 +go 1.20 require ( github.com/Sirupsen/logrus v1.0.4 From 0f0587d755a7537c0135e369f9e5c6507c2b748c Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Mon, 25 Dec 2023 22:08:47 +0500 Subject: [PATCH 172/346] fix(backend): add password to update method --- db/sql/environment.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/db/sql/environment.go b/db/sql/environment.go index 79106fd4..31b745a2 100644 --- a/db/sql/environment.go +++ b/db/sql/environment.go @@ -28,10 +28,11 @@ func (d *SqlDb) UpdateEnvironment(env db.Environment) error { } _, err = d.exec( - "update project__environment set name=?, json=?, env=? where id=?", + "update project__environment set name=?, json=?, env=?, password=? where id=?", env.Name, env.JSON, env.ENV, + env.Password, env.ID) return err } From 395e9d1520135bc99df3afd31825827bcc485a33 Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Sun, 7 Jan 2024 16:45:52 +0500 Subject: [PATCH 173/346] ci: add docker file for Runner --- deployment/docker/prod/runner.Dockerfile | 34 ++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 deployment/docker/prod/runner.Dockerfile diff --git a/deployment/docker/prod/runner.Dockerfile b/deployment/docker/prod/runner.Dockerfile new file mode 100644 index 00000000..e6fc2c86 --- /dev/null +++ b/deployment/docker/prod/runner.Dockerfile @@ -0,0 +1,34 @@ +FROM dind-ansible:latest + +RUN apk add --no-cache wget git rsync + +RUN adduser -D -u 1001 -G root semaphore && \ + mkdir -p /tmp/semaphore && \ + mkdir -p /etc/semaphore && \ + mkdir -p /var/lib/semaphore && \ + chown -R semaphore:0 /tmp/semaphore && \ + chown -R semaphore:0 /etc/semaphore && \ + chown -R semaphore:0 /var/lib/semaphore + +RUN wget https://raw.githubusercontent.com/ansible-semaphore/semaphore/develop/deployment/docker/common/runner-wrapper -P /usr/local/bin/ && chmod +x /usr/local/bin/runner-wrapper +RUN wget https://github.com/ansible-semaphore/semaphore/releases/download/v2.9.37/semaphore_2.9.37_linux_amd64.tar.gz -O - | tar -xz -C /usr/local/bin/ semaphore + +RUN chown -R semaphore:0 /usr/local/bin/runner-wrapper &&\ + chown -R semaphore:0 /usr/local/bin/semaphore &&\ + chmod +x /usr/local/bin/runner-wrapper &&\ + chmod +x /usr/local/bin/semaphore + +WORKDIR /home/semaphore +USER 1001 + +RUN mkdir ./venv + +RUN python3 -m venv ./venv --system-site-packages && \ + source ./venv/bin/activate && \ + pip3 install --upgrade pip + +RUN pip3 install boto3 botocore + +RUN echo '{"tmp_path": "/tmp/semaphore","dialect": "bolt", "runner": {"config_file": "/var/lib/semaphore/runner.json"}}' > /etc/semaphore/config.json + +CMD [ "/usr/local/bin/runner-wrapper" ] \ No newline at end of file From f47c2ee4076f734dfabac0ed75d7e4e47d4ad19c Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Sun, 7 Jan 2024 18:36:48 +0500 Subject: [PATCH 174/346] fix(runner): chanage log messages --- services/runners/JobPool.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/services/runners/JobPool.go b/services/runners/JobPool.go index 7443f68d..b05f1728 100644 --- a/services/runners/JobPool.go +++ b/services/runners/JobPool.go @@ -356,13 +356,15 @@ func (p *JobPool) tryRegisterRunner() bool { req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBytes)) if err != nil { - fmt.Println("Error creating request:", err) + log.Error("Error creating request:", err) + //fmt.Println("Error creating request:", err) return false } resp, err := client.Do(req) if err != nil || resp.StatusCode != 200 { - fmt.Println("Error making request:", err) + log.Error("Error making request:", err) + //fmt.Println("Error making request:", err) return false } @@ -409,22 +411,29 @@ func (p *JobPool) checkNewJobs() { } resp, err := client.Do(req) + if err != nil { fmt.Println("Error making request:", err) return } + + if resp.StatusCode != 200 { + log.Error("Checking new jobs error, server returns code ", resp.StatusCode) + return + } + defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { - fmt.Println("Error reading response body:", err) + log.Error("Checking new jobs, error reading response body:", err) return } var response RunnerState err = json.Unmarshal(body, &response) if err != nil { - fmt.Println("Error parsing JSON:", err) + log.Error("Checking new jobs, parsing JSON error:", err) return } From 446515fd1a6d3b60a1c894a03bc4185a0deb7c17 Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Sun, 7 Jan 2024 18:37:22 +0500 Subject: [PATCH 175/346] fix(runner): status code condition --- services/runners/JobPool.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/runners/JobPool.go b/services/runners/JobPool.go index b05f1728..377f76c1 100644 --- a/services/runners/JobPool.go +++ b/services/runners/JobPool.go @@ -417,7 +417,7 @@ func (p *JobPool) checkNewJobs() { return } - if resp.StatusCode != 200 { + if resp.StatusCode >= 400 { log.Error("Checking new jobs error, server returns code ", resp.StatusCode) return } From 1ae8eb13768244b6eeec253a92840de9dcece32c Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Sun, 7 Jan 2024 21:23:47 +0500 Subject: [PATCH 176/346] ci(runner): add required dep --- deployment/docker/prod/runner.Dockerfile | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/deployment/docker/prod/runner.Dockerfile b/deployment/docker/prod/runner.Dockerfile index e6fc2c86..5cc317c6 100644 --- a/deployment/docker/prod/runner.Dockerfile +++ b/deployment/docker/prod/runner.Dockerfile @@ -2,7 +2,7 @@ FROM dind-ansible:latest RUN apk add --no-cache wget git rsync -RUN adduser -D -u 1001 -G root semaphore && \ +RUN adduser -D -u 1001 -G root -G docker semaphore && \ mkdir -p /tmp/semaphore && \ mkdir -p /etc/semaphore && \ mkdir -p /var/lib/semaphore && \ @@ -25,9 +25,7 @@ RUN mkdir ./venv RUN python3 -m venv ./venv --system-site-packages && \ source ./venv/bin/activate && \ - pip3 install --upgrade pip - -RUN pip3 install boto3 botocore + pip3 install --upgrade pip boto3 botocore requests RUN echo '{"tmp_path": "/tmp/semaphore","dialect": "bolt", "runner": {"config_file": "/var/lib/semaphore/runner.json"}}' > /etc/semaphore/config.json From 5596943433366105a6fd63357db51d9e9f319c50 Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Sun, 7 Jan 2024 21:32:30 +0500 Subject: [PATCH 177/346] fix(runner): check token --- api/runners/runners.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/api/runners/runners.go b/api/runners/runners.go index fa73a7ed..89d6669e 100644 --- a/api/runners/runners.go +++ b/api/runners/runners.go @@ -13,6 +13,8 @@ import ( func RunnerMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("X-API-Token") + runnerID, err := helpers.GetIntParam("runner_id", w, r) if err != nil { @@ -26,6 +28,13 @@ func RunnerMiddleware(next http.Handler) http.Handler { runner, err := store.GetGlobalRunner(runnerID) + if runner.Token != token { + helpers.WriteJSON(w, http.StatusUnauthorized, map[string]string{ + "error": "Invalid token", + }) + return + } + if err != nil { helpers.WriteJSON(w, http.StatusNotFound, map[string]string{ "error": "Runner not found", From 82636936222d287290cc9be8e9526b44b101c37e Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Sun, 7 Jan 2024 21:35:02 +0500 Subject: [PATCH 178/346] fix(runner): check token --- services/runners/JobPool.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/runners/JobPool.go b/services/runners/JobPool.go index 377f76c1..156e4b1b 100644 --- a/services/runners/JobPool.go +++ b/services/runners/JobPool.go @@ -405,6 +405,8 @@ func (p *JobPool) checkNewJobs() { req, err := http.NewRequest("GET", url, nil) + req.Header.Set("X-API-Token", p.config.Token) + if err != nil { fmt.Println("Error creating request:", err) return From 7d99fd2e7d7b7adffb480549cb858788b9e5b103 Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Sun, 7 Jan 2024 22:25:52 +0500 Subject: [PATCH 179/346] feat(runner): support env vars --- services/runners/JobPool.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/services/runners/JobPool.go b/services/runners/JobPool.go index 156e4b1b..ca15b3b2 100644 --- a/services/runners/JobPool.go +++ b/services/runners/JobPool.go @@ -312,6 +312,24 @@ func (p *JobPool) tryRegisterRunner() bool { return true } + if os.Getenv("SEMAPHORE_RUNNER_ID") != "" { + + runnerId, err := strconv.Atoi(os.Getenv("SEMAPHORE_RUNNER_ID")) + + if err != nil { + panic(err) + } + + if os.Getenv("SEMAPHORE_RUNNER_TOKEN") == "" { + panic(fmt.Errorf("runner token required")) + } + + p.config = &RunnerConfig{ + RunnerID: runnerId, + Token: os.Getenv("SEMAPHORE_RUNNER_TOKEN"), + } + } + log.Info("Trying to register on server") _, err := os.Stat(util.Config.Runner.ConfigFile) From f7da53c75cffeeb7a45e242888fc4f1a5e004a4c Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Mon, 8 Jan 2024 00:50:37 +0500 Subject: [PATCH 180/346] fix(runner): pass token in PUT request --- api/runners/runners.go | 19 +++++++++++++------ services/runners/JobPool.go | 10 ++++++---- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/api/runners/runners.go b/api/runners/runners.go index 89d6669e..78c598bf 100644 --- a/api/runners/runners.go +++ b/api/runners/runners.go @@ -15,6 +15,13 @@ func RunnerMiddleware(next http.Handler) http.Handler { token := r.Header.Get("X-API-Token") + if token == "" { + helpers.WriteJSON(w, http.StatusUnauthorized, map[string]string{ + "error": "Invalid token", + }) + return + } + runnerID, err := helpers.GetIntParam("runner_id", w, r) if err != nil { @@ -28,16 +35,16 @@ func RunnerMiddleware(next http.Handler) http.Handler { runner, err := store.GetGlobalRunner(runnerID) - if runner.Token != token { - helpers.WriteJSON(w, http.StatusUnauthorized, map[string]string{ - "error": "Invalid token", + if err != nil { + helpers.WriteJSON(w, http.StatusNotFound, map[string]string{ + "error": "Runner not found", }) return } - if err != nil { - helpers.WriteJSON(w, http.StatusNotFound, map[string]string{ - "error": "Runner not found", + if runner.Token != token { + helpers.WriteJSON(w, http.StatusUnauthorized, map[string]string{ + "error": "Invalid token", }) return } diff --git a/services/runners/JobPool.go b/services/runners/JobPool.go index ca15b3b2..7d184a2e 100644 --- a/services/runners/JobPool.go +++ b/services/runners/JobPool.go @@ -298,6 +298,8 @@ func (p *JobPool) sendProgress() { return } + req.Header.Set("X-API-Token", p.config.Token) + resp, err := client.Do(req) if err != nil { fmt.Println("Error making request:", err) @@ -312,6 +314,8 @@ func (p *JobPool) tryRegisterRunner() bool { return true } + log.Info("Trying to register on server") + if os.Getenv("SEMAPHORE_RUNNER_ID") != "" { runnerId, err := strconv.Atoi(os.Getenv("SEMAPHORE_RUNNER_ID")) @@ -328,9 +332,9 @@ func (p *JobPool) tryRegisterRunner() bool { RunnerID: runnerId, Token: os.Getenv("SEMAPHORE_RUNNER_TOKEN"), } - } - log.Info("Trying to register on server") + return true + } _, err := os.Stat(util.Config.Runner.ConfigFile) @@ -375,14 +379,12 @@ func (p *JobPool) tryRegisterRunner() bool { req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBytes)) if err != nil { log.Error("Error creating request:", err) - //fmt.Println("Error creating request:", err) return false } resp, err := client.Do(req) if err != nil || resp.StatusCode != 200 { log.Error("Error making request:", err) - //fmt.Println("Error making request:", err) return false } From 54d103105fc52d5cf1e05e1302cd7c5f58b12007 Mon Sep 17 00:00:00 2001 From: Andreas Marschke Date: Mon, 3 Jul 2023 01:41:13 +0200 Subject: [PATCH 181/346] Webhook Feature implementation --- api-docs.yml | 458 ++++++++- api/projects/webhook.go | 201 ++++ api/projects/webhookextractor.go | 226 +++++ api/projects/webhookextractvalue.go | 193 ++++ api/projects/webhookmatcher.go | 204 ++++ api/router.go | 42 +- api/webhook.go | 247 +++++ db/Event.go | 7 + db/Migration.go | 1 + db/Store.go | 85 ++ db/Webhook.go | 175 ++++ db/bolt/BoltDb.go | 51 + db/bolt/webhook.go | 274 ++++++ db/sql/SqlDb.go | 895 ++++++++++++------ db/sql/SqlDb_test.go | 28 +- db/sql/access_key.go | 7 +- db/sql/environment.go | 7 +- db/sql/event.go | 88 +- db/sql/inventory.go | 4 +- db/sql/migration.go | 2 + db/sql/migrations/v2.9.7.sql | 42 + db/sql/webhook.go | 434 +++++++++ go.mod | 1 + go.sum | 2 + web/src/App.vue | 10 + .../components/WebhookExtractValueForm.vue | 127 +++ web/src/components/WebhookExtractorBase.js | 5 + .../WebhookExtractorChildValueFormBase.js | 60 ++ web/src/components/WebhookExtractorForm.vue | 43 + .../components/WebhookExtractorFormBase.js | 59 ++ .../components/WebhookExtractorRefsView.vue | 26 + web/src/components/WebhookExtractorsBase.js | 6 + web/src/components/WebhookForm.vue | 64 ++ web/src/components/WebhookMatcherForm.vue | 148 +++ web/src/components/WebhookRefsView.vue | 26 + web/src/lib/constants.js | 36 + web/src/router/index.js | 15 + web/src/views/project/WebhookExtractValue.vue | 174 ++++ web/src/views/project/WebhookExtractor.vue | 36 + web/src/views/project/WebhookExtractors.vue | 143 +++ web/src/views/project/WebhookMatcher.vue | 180 ++++ web/src/views/project/Webhooks.vue | 140 +++ 42 files changed, 4585 insertions(+), 387 deletions(-) create mode 100644 api/projects/webhook.go create mode 100644 api/projects/webhookextractor.go create mode 100644 api/projects/webhookextractvalue.go create mode 100644 api/projects/webhookmatcher.go create mode 100644 api/webhook.go create mode 100644 db/Webhook.go create mode 100644 db/bolt/webhook.go create mode 100644 db/sql/migrations/v2.9.7.sql create mode 100644 db/sql/webhook.go create mode 100644 web/src/components/WebhookExtractValueForm.vue create mode 100644 web/src/components/WebhookExtractorBase.js create mode 100644 web/src/components/WebhookExtractorChildValueFormBase.js create mode 100644 web/src/components/WebhookExtractorForm.vue create mode 100644 web/src/components/WebhookExtractorFormBase.js create mode 100644 web/src/components/WebhookExtractorRefsView.vue create mode 100644 web/src/components/WebhookExtractorsBase.js create mode 100644 web/src/components/WebhookForm.vue create mode 100644 web/src/components/WebhookMatcherForm.vue create mode 100644 web/src/components/WebhookRefsView.vue create mode 100644 web/src/views/project/WebhookExtractValue.vue create mode 100644 web/src/views/project/WebhookExtractor.vue create mode 100644 web/src/views/project/WebhookExtractors.vue create mode 100644 web/src/views/project/WebhookMatcher.vue create mode 100644 web/src/views/project/Webhooks.vue diff --git a/api-docs.yml b/api-docs.yml index 5310a3e0..61184e17 100644 --- a/api-docs.yml +++ b/api-docs.yml @@ -19,6 +19,8 @@ tags: description: Everything related to a project - name: user description: User-related API + - name: webhook + description: Webhook API schemes: - http @@ -180,7 +182,6 @@ definitions: type: integer minimum: 0 - AccessKeyRequest: type: object properties: @@ -334,22 +335,164 @@ definitions: type: string enum: [static, static-yaml, file] + WebhookRequest: + type: object + properties: + name: + type: string + example: deploy + project_id: + type: integer + minimum: 1 + template_id: + type: integer + minimum: 1 + + Webhook: + type: object + properties: + id: + type: integer + name: + type: string + project_id: + type: integer + template_id: + type: integer + + WebhookExtractorRequest: + type: object + properties: + name: + type: string + example: deploy + webhook_id: + type: integer + minimum: 1 + WebhookExtractor: + type: object + properties: + id: + type: integer + name: + type: string + webhook_id: + type: integer + + WebhookExtractValueRequest: + type: object + properties: + name: + type: string + example: deploy + extractor_id: + type: integer + minimum: 1 + value_source: + type: string + enum: [body, header] + body_data_type: + type: string + enum: [json, xml, string] + key: + type: string + example: key + variable: + type: string + example: variable + + WebhookExtractValue: + type: object + properties: + id: + type: integer + name: + type: string + example: extract this value + extractor_id: + type: integer + minimum: 1 + value_source: + type: string + enum: [body, header] + body_data_type: + type: string + enum: [json, xml, string] + key: + type: string + example: key + variable: + type: string + example: variable + + WebhookMatcherRequest: + type: object + properties: + name: + type: string + example: deploy + extractor_id: + type: integer + minimum: 1 + match_type: + type: string + enum: [body, header] + method: + type: string + enum: [equals, unequals, contains] + body_data_type: + type: string + enum: [json, xml, string] + key: + type: string + example: key + value: + type: string + example: value + + WebhookMatcher: + type: object + properties: + id: + type: integer + name: + type: string + example: deploy + extractor_id: + type: integer + minimum: 1 + match_type: + type: string + enum: [body, header] + method: + type: string + enum: [equals, unequals, contains] + body_data_type: + type: string + enum: [json, xml, string] + key: + type: string + example: key + value: + type: string + example: value + RepositoryRequest: - type: object - properties: - name: - type: string - example: Test - project_id: - type: integer - git_url: - type: string - example: git@example.com - git_branch: - type: string - example: master - ssh_key_id: - type: integer + type: object + properties: + name: + type: string + example: Test + project_id: + type: integer + git_url: + type: string + example: git@example.com + git_branch: + type: string + example: master + ssh_key_id: + type: integer Repository: type: object properties: @@ -521,19 +664,18 @@ definitions: template_id: type: integer - ViewRequest: - type: object - properties: - title: - type: string - example: Test - project_id: - type: integer - minimum: 1 - position: - type: integer - minimum: 1 + type: object + properties: + title: + type: string + example: Test + project_id: + type: integer + minimum: 1 + position: + type: integer + minimum: 1 View: type: object properties: @@ -668,6 +810,35 @@ parameters: type: integer required: true x-example: 10 + webhook_id: + name: webhook_id + description: webhook ID + in: path + type: integer + required: true + x-example: 11 + extractor_id: + name: extractor_id + description: Extractor ID + in: path + type: integer + required: true + x-example: 12 + extractvalue_id: + name: extractvalue_id + description: ExtractValue ID + in: path + type: integer + required: true + x-example: 12 + matcher_id: + name: matcher_id + description: Matcher ID + in: path + type: integer + required: true + x-example: 12 + paths: /ping: get: @@ -1125,6 +1296,237 @@ paths: 204: description: User updated + /project/{project_id}/webhooks: + parameters: + - $ref: "#/parameters/project_id" + get: + tags: + - project + summary: Get Webhooks linked to project + responses: + 200: + description: Webhooks + schema: + type: array + items: + $ref: "#/definitions/Webhook" + post: + tags: + - project + summary: Add Webhook + parameters: + - name: Webhook + in: body + required: true + schema: + $ref: "#/definitions/Webhook" + responses: + 204: + description: Webhook Created + 400: + description: Bad Webhook params + /project/{project_id}/webhook/{webhook_id}: + parameters: + - $ref: "#/parameters/project_id" + - $ref: "#/parameters/webhook_id" + put: + tags: + - project + summary: Updates Webhook + parameters: + - name: Webhook + in: body + required: true + schema: + $ref: "#/definitions/WebhookRequest" + responses: + 204: + description: Webhook updated + 400: + description: Bad webhook parameter + delete: + tags: + - project + summary: Removes webhook + responses: + 204: + description: webhook removed + /project/{project_id}/webhook/{webhook_id}/extractors: + parameters: + - $ref: "#/parameters/project_id" + - $ref: "#/parameters/webhook_id" + get: + tags: + - webhook + summary: Get Webhook Extractors linked to project + responses: + 200: + description: Webhook Extractors + schema: + type: array + items: + $ref: "#/definitions/WebhookExtractor" + post: + tags: + - project + summary: Add Webhook Extractor + parameters: + - name: Webhook Extractor + in: body + required: true + schema: + $ref: "#/definitions/WebhookExtractor" + responses: + 204: + description: Webhook Extractor Created + 400: + description: Bad Webhook Extractor params + /project/{project_id}/webhook/{webhook_id}/extractor/{extractor_id}: + parameters: + - $ref: "#/parameters/project_id" + - $ref: "#/parameters/webhook_id" + - $ref: "#/parameters/extractor_id" + put: + tags: + - webhook + summary: Updates Webhook extractor + parameters: + - name: Webhook Extractor + in: body + required: true + schema: + $ref: "#/definitions/WebhookExtractorRequest" + responses: + 204: + description: Webhook Extractor updated + 400: + description: Bad webhook extractor parameter + delete: + tags: + - webhook + summary: Removes webhook extractor + responses: + 204: + description: webhook extractor removed + /project/{project_id}/webhook/{webhook_id}/extractor/{extractor_id}/values: + parameters: + - $ref: "#/parameters/project_id" + - $ref: "#/parameters/webhook_id" + - $ref: "#/parameters/extractor_id" + get: + tags: + - webhook + summary: Get Webhook Extracted Values linked to webhook extractor + responses: + 200: + description: Webhook Extracted Value + schema: + type: array + items: + $ref: "#/definitions/WebhookExtractValue" + post: + tags: + - project + summary: Add Webhook Extracted Value + parameters: + - name: Webhook Extracted Value + in: body + required: true + schema: + $ref: "#/definitions/WebhookExtractValue" + responses: + 204: + description: Webhook Extract Value Created + 400: + description: Bad Webhook Extract Value params + /project/{project_id}/webhook/{webhook_id}/extractor/{extractor_id}/value/{extractvalue_id}: + parameters: + - $ref: "#/parameters/project_id" + - $ref: "#/parameters/webhook_id" + - $ref: "#/parameters/extractor_id" + - $ref: "#/parameters/extractvalue_id" + put: + tags: + - webhook + summary: Updates Webhook ExtractValue + parameters: + - name: Webhook ExtractValue + in: body + required: true + schema: + $ref: "#/definitions/WebhookExtractValueRequest" + responses: + 204: + description: Webhook Extract Value updated + 400: + description: Bad webhook extract value parameter + delete: + tags: + - webhook + summary: Removes webhook extract value + responses: + 204: + description: webhook extract value removed + /project/{project_id}/webhook/{webhook_id}/extractor/{extractor_id}/matchers: + parameters: + - $ref: "#/parameters/project_id" + - $ref: "#/parameters/webhook_id" + - $ref: "#/parameters/extractor_id" + get: + tags: + - webhook + summary: Get Webhook Matcher linked to webhook extractor + responses: + 200: + description: Webhook Matcher + schema: + type: array + items: + $ref: "#/definitions/WebhookMatcher" + post: + tags: + - project + summary: Add Webhook Matcher + parameters: + - name: Webhook Matcher + in: body + required: true + schema: + $ref: "#/definitions/WebhookMatcher" + responses: + 204: + description: Webhook Matcher Created + 400: + description: Bad Webhook Matcher params + /project/{project_id}/webhook/{webhook_id}/extractor/{extractor_id}/matcher/{matcher_id}: + parameters: + - $ref: "#/parameters/project_id" + - $ref: "#/parameters/webhook_id" + - $ref: "#/parameters/extractor_id" + - $ref: "#/parameters/matcher_id" + put: + tags: + - webhook + summary: Updates Webhook Matcher + parameters: + - name: Webhook Matcher + in: body + required: true + schema: + $ref: "#/definitions/WebhookMatcherRequest" + responses: + 204: + description: Webhook Matcher updated + 400: + description: Bad webhook matcher parameter + delete: + tags: + - webhook + summary: Removes webhook matcher + responses: + 204: + description: webhook matcher removed + # project access keys /project/{project_id}/keys: parameters: diff --git a/api/projects/webhook.go b/api/projects/webhook.go new file mode 100644 index 00000000..638dbdaf --- /dev/null +++ b/api/projects/webhook.go @@ -0,0 +1,201 @@ +package projects + +import ( + log "github.com/Sirupsen/logrus" + "github.com/ansible-semaphore/semaphore/api/helpers" + "github.com/ansible-semaphore/semaphore/db" + "net/http" + + "github.com/gorilla/context" +) + +func WebhookMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + webhook_id, err := helpers.GetIntParam("webhook_id", w, r) + + if err != nil { + helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{ + "error": "Invalid webhook ID", + }); + } + + project := context.Get(r, "project").(db.Project) + webhook, err := helpers.Store(r).GetWebhook(project.ID, webhook_id) + + if err != nil { + helpers.WriteError(w, err) + return + } + + context.Set(r, "webhook", webhook) + next.ServeHTTP(w, r) + }) +} + +func GetWebhook(w http.ResponseWriter, r *http.Request) { + webhook := context.Get(r, "webhook").(db.Webhook) + helpers.WriteJSON(w, http.StatusOK, webhook) +} + + +func GetWebhooks(w http.ResponseWriter, r *http.Request) { + project := context.Get(r, "project").(db.Project) + webhooks, err := helpers.Store(r).GetWebhooks(project.ID, helpers.QueryParams(r.URL)) + + if err != nil { + helpers.WriteError(w, err) + return + } + + helpers.WriteJSON(w, http.StatusOK, webhooks) +} + +func GetWebhookRefs (w http.ResponseWriter, r *http.Request) { + webhook_id, err := helpers.GetIntParam("webhook_id", w, r) + + if err != nil { + helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{ + "error": "Invalid Webhook ID", + }) + } + + project := context.Get(r, "project").(db.Project) + + if err != nil { + helpers.WriteError(w, err) + return + } + refs, err := helpers.Store(r).GetWebhookRefs(project.ID, webhook_id) + + if err != nil { + helpers.WriteError(w, err) + return + } + + helpers.WriteJSON(w, http.StatusOK, refs) +} + + +func AddWebhook(w http.ResponseWriter, r *http.Request) { + project := context.Get(r, "project").(db.Project) + var webhook db.Webhook + + if !helpers.Bind(w, r, &webhook) { + return + } + + if webhook.ProjectID != project.ID { + helpers.WriteJSON(w, http.StatusBadRequest, map[string]string { + "error": "Project ID in body and URL must be the same", + }) + return + } + err := webhook.Validate() + if err != nil { + helpers.WriteJSON(w, http.StatusBadRequest, map[string]string { + "error": err.Error(), + }) + return + } + + newWebhook, errWebhook := helpers.Store(r).CreateWebhook(webhook) + + if errWebhook != nil { + helpers.WriteError(w, errWebhook) + return + } + + user := context.Get(r, "user").(*db.User) + + objType := db.EventWebhook + desc := "Webhook " + webhook.Name + " created" + _, err = helpers.Store(r).CreateEvent(db.Event{ + UserID: &user.ID, + ProjectID: &project.ID, + ObjectType: &objType, + ObjectID: &newWebhook.ID, + Description: &desc, + }) + + + w.WriteHeader(http.StatusNoContent) +} + + +func UpdateWebhook(w http.ResponseWriter, r *http.Request) { + oldWebhook := context.Get(r, "webhook").(db.Webhook) + var webhook db.Webhook + + if !helpers.Bind(w, r, &webhook) { + return + } + + if webhook.ID != oldWebhook.ID { + helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{ + "error": "Webhook ID in body and URL must be the same", + }) + return + } + + if webhook.ProjectID != oldWebhook.ProjectID { + helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{ + "error": "Project ID in body and URL must be the same", + }) + return + } + + err := helpers.Store(r).UpdateWebhook(webhook) + + if err != nil { + helpers.WriteError(w, err) + return + } + + user := context.Get(r, "user").(*db.User) + + desc := "Webhook (" + webhook.Name + ") updated" + objType := db.EventWebhook + + _, err = helpers.Store(r).CreateEvent(db.Event{ + UserID: &user.ID, + ProjectID: &webhook.ProjectID, + Description: &desc, + ObjectID: &webhook.ID, + ObjectType: &objType, + }) + + if err != nil { + log.Error(err) + } + + w.WriteHeader(http.StatusNoContent) +} + + +func DeleteWebhook(w http.ResponseWriter, r *http.Request) { + webhook := context.Get(r, "webhook").(db.Webhook) + project := context.Get(r, "project").(db.Project) + + err := helpers.Store(r).DeleteWebhook(project.ID, webhook.ID) + if err == db.ErrInvalidOperation { + helpers.WriteJSON(w, http.StatusBadRequest, map[string]interface{}{ + "error": "Webhook failed to be deleted", + }) + } + + user := context.Get(r, "user").(*db.User) + + desc := "Webhook " + webhook.Name + " deleted" + + _, err = helpers.Store(r).CreateEvent(db.Event{ + UserID: &user.ID, + ProjectID: &project.ID, + Description: &desc, + }) + + if err != nil { + log.Error(err) + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/api/projects/webhookextractor.go b/api/projects/webhookextractor.go new file mode 100644 index 00000000..0a0e4442 --- /dev/null +++ b/api/projects/webhookextractor.go @@ -0,0 +1,226 @@ +package projects + +import ( + log "github.com/Sirupsen/logrus" + "fmt" + + "github.com/ansible-semaphore/semaphore/api/helpers" + "github.com/ansible-semaphore/semaphore/db" + "net/http" + + "github.com/gorilla/context" +) + +func WebhookExtractorMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + extractor_id, err := helpers.GetIntParam("extractor_id", w, r) + + if err != nil { + helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{ + "error": "Invalid extractor ID", + }) + return + } + webhook := context.Get(r, "webhook").(db.Webhook) + var extractor db.WebhookExtractor + extractor, err = helpers.Store(r).GetWebhookExtractor(extractor_id, webhook.ID) + + if err != nil { + helpers.WriteError(w, err) + return + } + + context.Set(r, "extractor", extractor) + next.ServeHTTP(w, r) + }) +} + +func GetWebhookExtractor(w http.ResponseWriter, r *http.Request) { + extractor := context.Get(r, "extractor").(db.WebhookExtractor) + + helpers.WriteJSON(w, http.StatusOK, extractor) +} + + +func GetWebhookExtractors(w http.ResponseWriter, r *http.Request) { + webhook := context.Get(r, "webhook").(db.Webhook) + extractors, err := helpers.Store(r).GetWebhookExtractors(webhook.ID, helpers.QueryParams(r.URL)) + + if err != nil { + helpers.WriteError(w, err) + return + } + + helpers.WriteJSON(w, http.StatusOK, extractors) +} + +func AddWebhookExtractor(w http.ResponseWriter, r *http.Request) { + webhook := context.Get(r, "webhook").(db.Webhook) + var extractor db.WebhookExtractor + + if !helpers.Bind(w, r, &extractor) { + return + } + + if extractor.WebhookID != webhook.ID { + helpers.WriteJSON(w, http.StatusBadRequest, map[string]string { + "error": "Webhook ID in body and URL must be the same", + }) + return + } + + if err := extractor.Validate(); err != nil { + helpers.WriteJSON(w, http.StatusBadRequest, map[string]string { + "error": err.Error(), + }) + return + } + + newExtractor, err := helpers.Store(r).CreateWebhookExtractor(extractor) + + if err != nil { + helpers.WriteError(w, err) + return + } + + user := context.Get(r, "user").(*db.User) + + desc := "WebhookExtractor (" + newExtractor.Name + ") created" + objType := db.EventWebhookExtractor + + _, err = helpers.Store(r).CreateEvent(db.Event{ + UserID: &user.ID, + ProjectID: &extractor.WebhookID, + Description: &desc, + ObjectID: &extractor.ID, + ObjectType: &objType, + }) + + if err != nil { + log.Error(err) + } + + w.WriteHeader(http.StatusNoContent) +} + +func UpdateWebhookExtractor(w http.ResponseWriter, r *http.Request) { + oldExtractor := context.Get(r, "extractor").(db.WebhookExtractor) + var extractor db.WebhookExtractor + + if !helpers.Bind(w, r, &extractor) { + return + } + + if extractor.ID != oldExtractor.ID { + helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{ + "error": "WebhookExtractor ID in body and URL must be the same", + }) + return + } + + if extractor.WebhookID != oldExtractor.WebhookID { + helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{ + "error": "Webhook ID in body and URL must be the same", + }) + return + } + + err := helpers.Store(r).UpdateWebhookExtractor(extractor) + + if err != nil { + helpers.WriteError(w, err) + return + } + + user := context.Get(r, "user").(*db.User) + + desc := "WebhookExtractor (" + extractor.Name + ") updated" + objType := db.EventWebhookExtractor + + _, err = helpers.Store(r).CreateEvent(db.Event{ + UserID: &user.ID, + ProjectID: &extractor.WebhookID, + Description: &desc, + ObjectID: &extractor.ID, + ObjectType: &objType, + }) + + if err != nil { + log.Error(err) + } + + w.WriteHeader(http.StatusNoContent) +} + +func GetWebhookExtractorRefs (w http.ResponseWriter, r *http.Request) { + extractor_id, err := helpers.GetIntParam("extractor_id", w, r) + + log.Info(fmt.Sprintf("Extractor ID: %v", extractor_id)) + fmt.Println(fmt.Sprintf("Extractor ID: %v", extractor_id)) + if err != nil { + helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{ + "error": "Invalid Extractor ID", + }) + } + + webhook := context.Get(r, "webhook").(db.Webhook) + var extractor db.WebhookExtractor + extractor, err = helpers.Store(r).GetWebhookExtractor(extractor_id, webhook.ID) + + if err != nil { + helpers.WriteError(w, err) + return + } + refs, err := helpers.Store(r).GetWebhookExtractorRefs(extractor.WebhookID, extractor.ID) + log.Info(fmt.Sprintf("References found: %v", refs)) + if err != nil { + helpers.WriteError(w, err) + return + } + + helpers.WriteJSON(w, http.StatusOK, refs) +} + +func DeleteWebhookExtractor(w http.ResponseWriter, r *http.Request) { + extractor_id, err := helpers.GetIntParam("extractor_id", w, r) + log.Info(fmt.Sprintf("Delete requested for: %v", extractor_id)) + if err != nil { + helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{ + "error": "Invalid Extractor ID", + }) + } + + webhook := context.Get(r, "webhook").(db.Webhook) + var extractor db.WebhookExtractor + extractor, err = helpers.Store(r).GetWebhookExtractor(extractor_id, webhook.ID) + + if err != nil { + helpers.WriteError(w, err) + return + } + + err = helpers.Store(r).DeleteWebhookExtractor(webhook.ID, extractor.ID) + if err == db.ErrInvalidOperation { + helpers.WriteJSON(w, http.StatusBadRequest, map[string]interface{}{ + "error": "Webhook Extractor failed to be deleted", + }) + } + + user := context.Get(r, "user").(*db.User) + desc := "Webhook Extractor (" + extractor.Name + ") deleted" + objType := db.EventWebhookExtractor + + _, err = helpers.Store(r).CreateEvent(db.Event{ + UserID: &user.ID, + ProjectID: &webhook.ProjectID, + WebhookID: &webhook.ID, + ObjectType: &objType, + Description: &desc, + }) + + if err != nil { + log.Error(err) + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/api/projects/webhookextractvalue.go b/api/projects/webhookextractvalue.go new file mode 100644 index 00000000..1d696b49 --- /dev/null +++ b/api/projects/webhookextractvalue.go @@ -0,0 +1,193 @@ +package projects + +import ( + log "github.com/Sirupsen/logrus" + "github.com/ansible-semaphore/semaphore/api/helpers" + "github.com/ansible-semaphore/semaphore/db" + "net/http" + "github.com/gorilla/context" +) + +func GetWebhookExtractValue(w http.ResponseWriter, r *http.Request) { + value_id, err := helpers.GetIntParam("value_id", w, r) + + if err != nil { + helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{ + "error": "Invalid WebhookExtractValue ID", + }) + } + + extractor := context.Get(r, "extractor").(db.WebhookExtractor) + var value db.WebhookExtractValue + value, err = helpers.Store(r).GetWebhookExtractValue(value_id, extractor.ID) + + helpers.WriteJSON(w, http.StatusOK, value) +} + +func GetWebhookExtractValues(w http.ResponseWriter, r *http.Request) { + extractor := context.Get(r, "extractor").(db.WebhookExtractor) + values, err := helpers.Store(r).GetWebhookExtractValues(extractor.ID, helpers.QueryParams(r.URL)) + + if err != nil { + helpers.WriteError(w, err) + return + } + + helpers.WriteJSON(w, http.StatusOK, values) +} + +func AddWebhookExtractValue(w http.ResponseWriter, r *http.Request) { + extractor := context.Get(r, "extractor").(db.WebhookExtractor) + project := context.Get(r, "project").(db.Project) + webhook := context.Get(r, "webhook").(db.Webhook) + + var value db.WebhookExtractValue + + if !helpers.Bind(w, r, &value) { + return + } + + if value.ExtractorID != extractor.ID { + helpers.WriteJSON(w, http.StatusBadRequest, map[string]string { + "error": "Extractor ID in body and URL must be the same", + }) + return + } + + if err := value.Validate(); err != nil { + helpers.WriteJSON(w, http.StatusBadRequest, map[string]string { + "error": err.Error(), + }) + return + } + + newValue, err := helpers.Store(r).CreateWebhookExtractValue(value) + + if err != nil { + helpers.WriteError(w, err) + return + } + + user := context.Get(r, "user").(*db.User) + + objType := db.EventWebhookExtractValue + desc := "Webhook Extracted Value" + newValue.Name + " created" + _, err = helpers.Store(r).CreateEvent(db.Event{ + UserID: &user.ID, + ProjectID: &project.ID, + WebhookID: &webhook.ID, + ExtractorID: &extractor.ID, + ObjectType: &objType, + ObjectID: &value.ID, + Description: &desc, + }) + + w.WriteHeader(http.StatusNoContent) +} + +func UpdateWebhookExtractValue(w http.ResponseWriter, r *http.Request) { + value_id, err := helpers.GetIntParam("value_id", w, r) + + if err != nil { + helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{ + "error": "Invalid Value ID", + }) + } + extractor := context.Get(r, "extractor").(db.WebhookExtractor) + + var value db.WebhookExtractValue + value, err = helpers.Store(r).GetWebhookExtractValue(value_id, extractor.ID) + + if !helpers.Bind(w, r, &value) { + return + } + + err = helpers.Store(r).UpdateWebhookExtractValue(value) + + if err != nil { + helpers.WriteError(w, err) + return + } + + user := context.Get(r, "user").(*db.User) + webhook := context.Get(r, "webhook").(db.Webhook) + + desc := "WebhookExtractValue (" + value.String() + ") updated" + + objType := db.EventWebhookExtractValue + + _, err = helpers.Store(r).CreateEvent(db.Event{ + UserID: &user.ID, + ProjectID: &webhook.ProjectID, + WebhookID: &webhook.ID, + ExtractorID: &extractor.ID, + Description: &desc, + ObjectID: &value.ID, + ObjectType: &objType, + }) + + if err != nil { + log.Error(err) + } + + w.WriteHeader(http.StatusNoContent) +} + +func GetWebhookExtractValueRefs(w http.ResponseWriter, r *http.Request) { + value_id, err := helpers.GetIntParam("value_id", w, r) + + if err != nil { + helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{ + "error": "Invalid Value ID", + }) + } + extractor := context.Get(r, "extractor").(db.WebhookExtractor) + var value db.WebhookExtractValue + value, err = helpers.Store(r).GetWebhookExtractValue(value_id, extractor.ID) + + refs, err := helpers.Store(r).GetWebhookExtractValueRefs(value.ExtractorID, value.ID) + if err != nil { + helpers.WriteError(w, err) + return + } + + helpers.WriteJSON(w, http.StatusOK, refs) +} + +func DeleteWebhookExtractValue(w http.ResponseWriter, r *http.Request) { + value_id, err := helpers.GetIntParam("value_id", w, r) + if err != nil { + helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{ + "error": "Invalid Value ID", + }) + } + + extractor := context.Get(r, "extractor").(db.WebhookExtractor) + var value db.WebhookExtractValue + value, err = helpers.Store(r).GetWebhookExtractValue(value_id, extractor.ID) + + err = helpers.Store(r).DeleteWebhookExtractValue(extractor.ID, value.ID) + if err == db.ErrInvalidOperation { + helpers.WriteJSON(w, http.StatusBadRequest, map[string]interface{}{ + "error": "Webhook Extract Value failed to be deleted", + }) + } + + user := context.Get(r, "user").(*db.User) + project := context.Get(r, "project").(db.Project) + webhook := context.Get(r, "webhook").(db.Webhook) + + desc := "Webhook Extract Value (" + value.String() + ") deleted" + _, err = helpers.Store(r).CreateEvent(db.Event{ + UserID: &user.ID, + ProjectID: &project.ID, + WebhookID: &webhook.ID, + ExtractorID: &extractor.ID, + Description: &desc, + }) + + if err != nil { + log.Error(err) + } + w.WriteHeader(http.StatusNoContent) +} diff --git a/api/projects/webhookmatcher.go b/api/projects/webhookmatcher.go new file mode 100644 index 00000000..497368e7 --- /dev/null +++ b/api/projects/webhookmatcher.go @@ -0,0 +1,204 @@ +package projects + +import ( + // "strconv" + "fmt" + log "github.com/Sirupsen/logrus" + "github.com/ansible-semaphore/semaphore/api/helpers" + "github.com/ansible-semaphore/semaphore/db" + "net/http" + "github.com/gorilla/context" +) + +func GetWebhookMatcher(w http.ResponseWriter, r *http.Request) { + matcher_id, err := helpers.GetIntParam("matcher_id", w, r) + + if err != nil { + helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{ + "error": "Invalid Matcher ID", + }) + } + extractor := context.Get(r, "extractor").(db.WebhookExtractor) + var matcher db.WebhookMatcher + matcher, err = helpers.Store(r).GetWebhookMatcher(matcher_id, extractor.ID) + + log.Info(fmt.Sprintf("ExtractorID From Context %v, matcher from API %v, matcher from DB %v", extractor.ID, matcher_id, matcher.ID)) + + helpers.WriteJSON(w, http.StatusOK, matcher) +} + +func GetWebhookMatcherRefs(w http.ResponseWriter, r *http.Request) { + matcher_id, err := helpers.GetIntParam("matcher_id", w, r) + + if err != nil { + helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{ + "error": "Invalid Matcher ID", + }) + } + extractor := context.Get(r, "extractor").(db.WebhookExtractor) + var matcher db.WebhookMatcher + matcher, err = helpers.Store(r).GetWebhookMatcher(matcher_id, extractor.ID) + + refs, err := helpers.Store(r).GetWebhookMatcherRefs(matcher.ExtractorID, matcher.ID) + if err != nil { + helpers.WriteError(w, err) + return + } + + helpers.WriteJSON(w, http.StatusOK, refs) +} + + +func GetWebhookMatchers(w http.ResponseWriter, r *http.Request) { + extractor := context.Get(r, "extractor").(db.WebhookExtractor) + + matchers, err := helpers.Store(r).GetWebhookMatchers(extractor.ID, helpers.QueryParams(r.URL)) + + if err != nil { + helpers.WriteError(w, err) + return + } + + helpers.WriteJSON(w, http.StatusOK, matchers) +} + +func AddWebhookMatcher(w http.ResponseWriter, r *http.Request) { + extractor := context.Get(r, "extractor").(db.WebhookExtractor) + var matcher db.WebhookMatcher + if !helpers.Bind(w, r, &matcher) { + return + } + + if matcher.ExtractorID != extractor.ID { + helpers.WriteJSON(w, http.StatusBadRequest, map[string]string { + "error": "Extractor ID in body and URL must be the same", + }) + return + } + + err := matcher.Validate() + + if err != nil { + helpers.WriteJSON(w, http.StatusBadRequest, map[string]string { + "error": err.Error(), + }) + return + } + + newMatcher, err := helpers.Store(r).CreateWebhookMatcher(matcher) + + if err != nil { + helpers.WriteError(w, err) + return + } + + user := context.Get(r, "user").(*db.User) + webhook := context.Get(r, "webhook").(db.Webhook) + project := context.Get(r, "project").(db.Project) + + objType := db.EventWebhookMatcher + desc := "Webhook Matcher " + matcher.Name + " created" + + _, err = helpers.Store(r).CreateEvent(db.Event{ + UserID: &user.ID, + ProjectID: &project.ID, + WebhookID: &webhook.ID, + ExtractorID: &extractor.ID, + ObjectType: &objType, + ObjectID: &newMatcher.ID, + Description: &desc, + }) + + helpers.WriteJSON(w, http.StatusOK, newMatcher) +} + +func UpdateWebhookMatcher(w http.ResponseWriter, r *http.Request) { + matcher_id, err := helpers.GetIntParam("matcher_id", w, r) + + if err != nil { + helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{ + "error": "Invalid Matcher ID", + }) + } + extractor := context.Get(r, "extractor").(db.WebhookExtractor) + + var matcher db.WebhookMatcher + + if !helpers.Bind(w, r, &matcher) { + return + } + + log.Info(fmt.Sprintf("Updating API Matcher %v for Extractor %v, matcher ID: %v", matcher_id, extractor.ID, matcher.ID)) + + err = helpers.Store(r).UpdateWebhookMatcher(matcher) + log.Info(fmt.Sprintf("Err %s", err)) + + if err != nil { + helpers.WriteError(w, err) + return + } + + user := context.Get(r, "user").(*db.User) + webhook := context.Get(r, "webhook").(db.Webhook) + + desc := "WebhookMatcher (" + matcher.String() + ") updated" + + objType := db.EventWebhookMatcher + + _, err = helpers.Store(r).CreateEvent(db.Event{ + UserID: &user.ID, + ProjectID: &webhook.ProjectID, + WebhookID: &webhook.ID, + ExtractorID: &extractor.ID, + Description: &desc, + ObjectID: &matcher.ID, + ObjectType: &objType, + }) + + if err != nil { + log.Error(err) + } + + w.WriteHeader(http.StatusNoContent) +} + +func DeleteWebhookMatcher(w http.ResponseWriter, r *http.Request) { + matcher_id, err := helpers.GetIntParam("matcher_id", w, r) + + if err != nil { + helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{ + "error": "Invalid Matcher ID", + }) + } + + extractor := context.Get(r, "extractor").(db.WebhookExtractor) + var matcher db.WebhookMatcher + matcher, err = helpers.Store(r).GetWebhookMatcher(matcher_id, extractor.ID) + + + err = helpers.Store(r).DeleteWebhookMatcher(extractor.ID, matcher.ID) + if err == db.ErrInvalidOperation { + helpers.WriteJSON(w, http.StatusBadRequest, map[string]interface{}{ + "error": "Webhook Matcher failed to be deleted", + }) + } + + user := context.Get(r, "user").(*db.User) + project := context.Get(r, "project").(db.Project) + webhook := context.Get(r, "webhook").(db.Webhook) + + desc := "Webhook Matcher (" + matcher.String() + ") deleted" + _, err = helpers.Store(r).CreateEvent(db.Event{ + UserID: &user.ID, + ProjectID: &project.ID, + WebhookID: &webhook.ID, + ExtractorID: &extractor.ID, + Description: &desc, + }) + + if err != nil { + log.Error(err) + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/api/router.go b/api/router.go index cd22f4af..165cf756 100644 --- a/api/router.go +++ b/api/router.go @@ -80,7 +80,6 @@ func Route() *mux.Router { pingRouter.Methods("GET", "HEAD").HandlerFunc(pongHandler) publicAPIRouter := r.PathPrefix(webPath + "api").Subrouter() - publicAPIRouter.Use(StoreMiddleware, JSONMiddleware) publicAPIRouter.HandleFunc("/runners", runners.RegisterRunner).Methods("POST") @@ -94,6 +93,10 @@ func Route() *mux.Router { routersAPI.Path("/runners/{runner_id}").HandlerFunc(runners.GetRunner).Methods("GET", "HEAD") routersAPI.Path("/runners/{runner_id}").HandlerFunc(runners.UpdateRunner).Methods("PUT") + publicWebHookRouter := r.PathPrefix(webPath + "webhook").Subrouter() + publicWebHookRouter.Use(StoreMiddleware, JSONMiddleware) + publicWebHookRouter.HandleFunc("/endpoint", ReceiveWebhook).Methods("POST", "GET", "OPTIONS") + authenticatedWS := r.PathPrefix(webPath + "api").Subrouter() authenticatedWS.Use(JSONMiddleware, authenticationWithStore) authenticatedWS.Path("/ws").HandlerFunc(sockets.Handler).Methods("GET", "HEAD") @@ -180,6 +183,43 @@ func Route() *mux.Router { projectUserAPI.Path("/views").HandlerFunc(projects.AddView).Methods("POST") projectUserAPI.Path("/views/positions").HandlerFunc(projects.SetViewPositions).Methods("POST") + projectUserAPI.Path("/webhooks").HandlerFunc(projects.GetWebhooks).Methods("GET", "HEAD") + projectUserAPI.Path("/webhooks").HandlerFunc(projects.AddWebhook).Methods("POST") + + projectWebhooksAPI := projectUserAPI.PathPrefix("/webhook").Subrouter() + projectWebhooksAPI.Use(projects.WebhookMiddleware, projects.MustBeAdmin) + + projectWebhooksAPI.Path("/{webhook_id}").Methods("GET", "HEAD").HandlerFunc(projects.GetWebhook) + projectWebhooksAPI.Path("/{webhook_id}").Methods("PUT").HandlerFunc(projects.UpdateWebhook) + projectWebhooksAPI.Path("/{webhook_id}").Methods("DELETE").HandlerFunc(projects.DeleteWebhook) + + projectWebhooksAPI.Path("/{webhook_id}/extractors").HandlerFunc(projects.GetWebhookExtractors).Methods("GET", "HEAD") + projectWebhooksAPI.Path("/{webhook_id}/extractors").HandlerFunc(projects.AddWebhookExtractor).Methods("POST") + projectWebhooksAPI.Path("/{webhook_id}/refs").HandlerFunc(projects.GetWebhookRefs).Methods("GET") + + projectWebhookExtractorAPI := projectWebhooksAPI.PathPrefix("/{webhook_id}/extractor").Subrouter() + projectWebhookExtractorAPI.Use(projects.WebhookExtractorMiddleware) + + projectWebhookExtractorAPI.Path("/{extractor_id}/refs").HandlerFunc(projects.GetWebhookExtractorRefs).Methods("GET") + projectWebhookExtractorAPI.Path("/{extractor_id}/matchers").HandlerFunc(projects.GetWebhookMatchers).Methods("GET", "HEAD") + projectWebhookExtractorAPI.Path("/{extractor_id}/matchers").HandlerFunc(projects.AddWebhookMatcher).Methods("POST") + projectWebhookExtractorAPI.Path("/{extractor_id}/values").HandlerFunc(projects.GetWebhookExtractValues).Methods("GET", "HEAD") + projectWebhookExtractorAPI.Path("/{extractor_id}/values").HandlerFunc(projects.AddWebhookExtractValue).Methods("POST") + + projectWebhookExtractorAPI.Path("/{extractor_id}/matcher/{matcher_id}").Methods("GET", "HEAD").HandlerFunc(projects.GetWebhookMatcher) + projectWebhookExtractorAPI.Path("/{extractor_id}/matcher/{matcher_id}").Methods("PUT").HandlerFunc(projects.UpdateWebhookMatcher) + projectWebhookExtractorAPI.Path("/{extractor_id}/matcher/{matcher_id}").Methods("DELETE").HandlerFunc(projects.DeleteWebhookMatcher) + projectWebhookExtractorAPI.Path("/{extractor_id}/matcher/{matcher_id}/refs").Methods("GET", "HEAD").HandlerFunc(projects.GetWebhookMatcherRefs) + + projectWebhookExtractorAPI.Path("/{extractor_id}/value/{value_id}").Methods("GET", "HEAD").HandlerFunc(projects.GetWebhookExtractValue) + projectWebhookExtractorAPI.Path("/{extractor_id}/value/{value_id}").Methods("PUT").HandlerFunc(projects.UpdateWebhookExtractValue) + projectWebhookExtractorAPI.Path("/{extractor_id}/value/{value_id}").Methods("DELETE").HandlerFunc(projects.DeleteWebhookExtractValue) + projectWebhookExtractorAPI.Path("/{extractor_id}/value/{value_id}/refs").Methods("GET").HandlerFunc(projects.GetWebhookExtractValueRefs) + + projectWebhookExtractorAPI.Path("/{extractor_id}").Methods("GET", "HEAD").HandlerFunc(projects.GetWebhookExtractor) + projectWebhookExtractorAPI.Path("/{extractor_id}").Methods("PUT").HandlerFunc(projects.UpdateWebhookExtractor) + projectWebhookExtractorAPI.Path("/{extractor_id}").Methods("DELETE").HandlerFunc(projects.DeleteWebhookExtractor) + // // Updating and deleting project projectAdminAPI := authenticatedAPI.Path("/project/{project_id}").Subrouter() diff --git a/api/webhook.go b/api/webhook.go new file mode 100644 index 00000000..a114485e --- /dev/null +++ b/api/webhook.go @@ -0,0 +1,247 @@ +package api + +import ( + log "github.com/Sirupsen/logrus" + "fmt" + "encoding/json" + "bytes" + "strings" + "golang.org/x/exp/slices" + "github.com/ansible-semaphore/semaphore/api/helpers" + "github.com/ansible-semaphore/semaphore/db" + "net/http" + "io" + jsonq "github.com/thedevsaddam/gojsonq/v2" +) + +func ReceiveWebhook(w http.ResponseWriter, r *http.Request) { + log.Info(fmt.Sprintf("Receiving Webhook from: %s", r.RemoteAddr)) + + var err error + // var projects []db.Project + var extractors []db.WebhookExtractor + extractors, err = helpers.Store(r).GetAllWebhookExtractors() + + var foundExtractors = make([]db.WebhookExtractor, 0) + for _, extractor := range extractors { + var matchers []db.WebhookMatcher + matchers, err = helpers.Store(r).GetWebhookMatchers(extractor.ID, db.RetrieveQueryParams{}) + if err != nil { + log.Error(err) + } + var matched = false + + for _, matcher := range matchers { + if Match(matcher, r) { + matched = true + continue + } else { + matched = false + break + } + } + // If all Matched... + if matched { + foundExtractors = append(foundExtractors, extractor) + } + } + + // Iterate over all Extractors that matched + if len(foundExtractors) > 0 { + var webhookIDs = make([]int, 0) + var extractorIDs = make([]int, 0) + + for _, extractor := range foundExtractors { + webhookIDs = append(webhookIDs, extractor.WebhookID) + } + + for _, extractor := range foundExtractors { + extractorIDs = append(extractorIDs, extractor.ID) + } + + var allWebhookExtractorIDs []int = make([]int, 0) + var webhooks []db.Webhook + webhooks, err = helpers.Store(r).GetAllWebhooks() + for _, id := range webhookIDs { + var extractorsForWebhook []db.WebhookExtractor + extractorsForWebhook, err = helpers.Store(r).GetWebhookExtractors(id, db.RetrieveQueryParams{}) + + for _, extractor := range extractorsForWebhook { + allWebhookExtractorIDs = append(allWebhookExtractorIDs, extractor.ID) + } + + var found = false + for _, webhookExtractorID := range extractorIDs { + if slices.Contains(allWebhookExtractorIDs, webhookExtractorID) { + found = true + continue + } else { + found = false + break + } + } + + // if all extractors for a webhook matched during search + if found { + var webhook db.Webhook + webhook = FindWebhook(webhooks, id) + if webhook.ID != id { + log.Error(fmt.Sprintf("Could not find webhook ID: %v", id)) + continue + } + RunWebhook(webhook, r) + } + } + } + + w.WriteHeader(http.StatusNoContent) +} + +func FindWebhook(webhooks []db.Webhook, id int) (webhook db.Webhook) { + for _, webhook := range webhooks { + if webhook.ID == id { + return webhook + } + } + return db.Webhook{} +} + +func UniqueWebhooks(webhooks []db.Webhook) []db.Webhook { + var unique []db.Webhook +webhookLoop: + for _, v := range webhooks { + for i, u := range unique { + if v.ID == u.ID { + unique[i] = v + continue webhookLoop + } + } + unique = append(unique, v) + } + return unique +} + +func UniqueExtractors(extractors []db.WebhookExtractor) []db.WebhookExtractor { + var unique []db.WebhookExtractor +webhookLoop: + for _, v := range extractors { + for i, u := range unique { + if v.ID == u.ID { + unique[i] = v + continue webhookLoop + } + } + unique = append(unique, v) + } + return unique +} + +func Match(matcher db.WebhookMatcher, r *http.Request) (matched bool) { + if matcher.MatchType == db.WebhookMatchHeader { + var header_value string = r.Header.Get(matcher.Key) + return MatchCompare(header_value, matcher.Method, matcher.Value) + } else if matcher.MatchType == db.WebhookMatchBody { + bodyBytes, err := io.ReadAll(r.Body) + + if err != nil { + log.Fatalln(err) + return false + } + var body = string(bodyBytes) + if matcher.BodyDataType == db.WebhookBodyDataJSON { + var jsonBytes bytes.Buffer + jsonq.New().FromString(body).From(matcher.Key).Writer(&jsonBytes) + var jsonString = jsonBytes.String() + if err != nil { + log.Error(fmt.Sprintf("Failed to marshal JSON contents of body. %v", err)) + } + return MatchCompare(jsonString, matcher.Method, matcher.Value) + } else if matcher.BodyDataType == db.WebhookBodyDataString { + return MatchCompare(body, matcher.Method, matcher.Value) + } else if matcher.BodyDataType == db.WebhookBodyDataXML { + // XXX: TBI + return false + } + } + return false +} + +func MatchCompare(value string, method db.WebhookMatchMethodType, expected string) (bool) { + if method == db.WebhookMatchMethodEquals { + return value == expected + } else if method == db.WebhookMatchMethodEquals { + return value != expected + } else if method == db.WebhookMatchMethodContains { + return strings.Contains(value, expected) + } + return false +} + +func RunWebhook(webhook db.Webhook, r *http.Request) { + extractors, err := helpers.Store(r).GetWebhookExtractors(webhook.ID, db.RetrieveQueryParams{}); + if err != nil { + log.Error(err) + return + } + + if err != nil { + log.Error(err) + return + } + + var extractValues []db.WebhookExtractValue = make([]db.WebhookExtractValue, 0) + for _, extractor := range extractors { + extractValuesForExtractor, err := helpers.Store(r).GetWebhookExtractValues(extractor.ID, db.RetrieveQueryParams{}) + if err != nil { + log.Error(err) + } + for _, extraExtractor := range extractValuesForExtractor { + extractValues = append(extractValues, extraExtractor) + } + } + + var extractedResults = Extract(extractValues, r) + + // XXX: LOG AN EVENT HERE + environmentJSONBytes, err := json.Marshal(extractedResults) + var environmentJSONString = string(environmentJSONBytes) + var taskDefinition = db.Task{ + TemplateID: webhook.TemplateID, + ProjectID: webhook.ProjectID, + Debug: true, + Environment: environmentJSONString, + } + + var user db.User + user, err = helpers.Store(r).GetUser(1) + + helpers.TaskPool(r).AddTask(taskDefinition, &user.ID, webhook.ProjectID) +} + +func Extract(extractValues []db.WebhookExtractValue, r *http.Request) (result map[string]string) { + result = make(map[string]string) + + for _, extractValue := range extractValues { + if extractValue.ValueSource == db.WebhookExtractHeaderValue { + result[extractValue.Variable] = r.Header.Get(extractValue.Key) + } else if extractValue.ValueSource == db.WebhookExtractBodyValue { + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + log.Fatalln(err) + return + } + var body = string(bodyBytes) + + if extractValue.BodyDataType == db.WebhookBodyDataJSON { + var jsonBytes bytes.Buffer + jsonq.New().FromString(body).From(extractValue.Key).Writer(&jsonBytes) + result[extractValue.Variable] = jsonBytes.String() + } else if extractValue.BodyDataType == db.WebhookBodyDataString { + result[extractValue.Variable] = body + } else if extractValue.BodyDataType == db.WebhookBodyDataXML { + // XXX: TBI + } + } + } + return +} diff --git a/db/Event.go b/db/Event.go index f53c6bad..7bb14a9d 100644 --- a/db/Event.go +++ b/db/Event.go @@ -9,6 +9,9 @@ type Event struct { ID int `db:"id" json:"-"` UserID *int `db:"user_id" json:"user_id"` ProjectID *int `db:"project_id" json:"project_id"` + WebhookID *int `db:"webhook_id" json:"webhook_id"` + ExtractorID *int `db:"extractor_id" json:"extractor_id"` + ObjectID *int `db:"object_id" json:"object_id"` ObjectType *EventObjectType `db:"object_type" json:"object_type"` Description *string `db:"description" json:"description"` @@ -32,6 +35,10 @@ const ( EventTemplate EventObjectType = "template" EventUser EventObjectType = "user" EventView EventObjectType = "view" + EventWebhook EventObjectType = "webhook" + EventWebhookExtractor EventObjectType = "webhookextractor" + EventWebhookExtractValue EventObjectType = "webhookextractvalue" + EventWebhookMatcher EventObjectType = "webhookmatcher" ) func FillEvents(d Store, events []Event) (err error) { diff --git a/db/Migration.go b/db/Migration.go index 170ac2ab..4d2136d4 100644 --- a/db/Migration.go +++ b/db/Migration.go @@ -61,6 +61,7 @@ func GetMigrations() []Migration { {Version: "2.8.58"}, {Version: "2.8.91"}, {Version: "2.9.6"}, + {Version: "2.9.7"}, } } diff --git a/db/Store.go b/db/Store.go index fedb67ad..889964bf 100644 --- a/db/Store.go +++ b/db/Store.go @@ -51,6 +51,19 @@ type ObjectReferrers struct { Repositories []ObjectReferrer `json:"repositories"` } +type WebhookReferrers struct { + WebhookExtractors []ObjectReferrer `json:"extractors"` +} + +type WebhookExtractorReferrers struct { + WebhookMatchers []ObjectReferrer `json:"matchers"` + WebhookExtractValues []ObjectReferrer `json:"values"` +} + +type WebhookExtractorChildReferrers struct { + WebhookExtractors []ObjectReferrer `json:"extracors"` +} + // ObjectProps describe database entities. // It mainly used for NoSQL implementations (currently BoltDB) to preserve same // data structure of different implementations and easy change it if required. @@ -126,6 +139,41 @@ type Store interface { GetAccessKeys(projectID int, params RetrieveQueryParams) ([]AccessKey, error) RekeyAccessKeys(oldKey string) error + CreateWebhook(webhook Webhook) (newWebhook Webhook, err error) + GetWebhooks(projectID int, params RetrieveQueryParams) ([]Webhook, error) + GetWebhook(webhookID int, projectID int) (webhook Webhook, err error) + UpdateWebhook(webhook Webhook) error + GetWebhookRefs(projectID int, webhookID int) (WebhookReferrers, error) + DeleteWebhook(projectID int, webhookID int) error + GetAllWebhooks() ([]Webhook, error) + + CreateWebhookExtractor(webhookExtractor WebhookExtractor) (newWebhookExtractor WebhookExtractor, err error) + GetWebhookExtractors(webhookID int, params RetrieveQueryParams) ([]WebhookExtractor, error) + GetWebhookExtractor(extractorID int, webhookID int) (extractor WebhookExtractor, err error) + UpdateWebhookExtractor(webhookExtractor WebhookExtractor) error + GetWebhookExtractorRefs(webhookID int, extractorID int) (WebhookExtractorReferrers, error) + DeleteWebhookExtractor(webhookID int, extractorID int) error + GetWebhookExtractorsByWebhookID(webhookID int) ([]WebhookExtractor, error) + GetAllWebhookExtractors() ([]WebhookExtractor, error) + + CreateWebhookExtractValue(value WebhookExtractValue) (newValue WebhookExtractValue, err error) + GetWebhookExtractValues(extractorID int, params RetrieveQueryParams) ([]WebhookExtractValue, error) + GetWebhookExtractValue(valueID int, extractorID int) (value WebhookExtractValue, err error) + UpdateWebhookExtractValue(webhookExtractValue WebhookExtractValue) error + GetWebhookExtractValueRefs(extractorID int, valueID int) (WebhookExtractorChildReferrers, error) + DeleteWebhookExtractValue(extractorID int, valueID int) error + GetWebhookExtractValuesByExtractorID(extractorID int) ([]WebhookExtractValue, error) + GetAllWebhookExtractValues() ([]WebhookExtractValue, error) + + CreateWebhookMatcher(matcher WebhookMatcher) (newMatcher WebhookMatcher, err error) + GetWebhookMatchers(extractorID int, params RetrieveQueryParams) ([]WebhookMatcher, error) + GetAllWebhookMatchers() ([]WebhookMatcher, error) + GetWebhookMatcher(matcherID int, extractorID int) (matcher WebhookMatcher, err error) + UpdateWebhookMatcher(webhookMatcher WebhookMatcher) error + GetWebhookMatcherRefs(extractorID int, matcherID int) (WebhookExtractorChildReferrers, error) + DeleteWebhookMatcher(extractorID int, matcherID int) error + GetWebhookMatchersByExtractorID(extractorID int) ([]WebhookMatcher, error) + UpdateAccessKey(accessKey AccessKey) error CreateAccessKey(accessKey AccessKey) (AccessKey, error) DeleteAccessKey(projectID int, accessKeyID int) error @@ -221,6 +269,42 @@ var AccessKeyProps = ObjectProps{ DefaultSortingColumn: "name", } +var WebhookProps = ObjectProps{ + TableName: "project__webhook", + Type: reflect.TypeOf(Webhook{}), + PrimaryColumnName: "id", + ReferringColumnSuffix: "webhook_id", + SortableColumns: []string{"name"}, + DefaultSortingColumn: "name", +} + +var WebhookExtractorProps = ObjectProps{ + TableName: "project__webhook_extractor", + Type: reflect.TypeOf(WebhookExtractor{}), + PrimaryColumnName: "id", + ReferringColumnSuffix: "extractor_id", + SortableColumns: []string{"name"}, + DefaultSortingColumn: "name", +} + +var WebhookExtractValueProps = ObjectProps{ + TableName: "project__webhook_extract_value", + Type: reflect.TypeOf(WebhookExtractValue{}), + PrimaryColumnName: "id", + ReferringColumnSuffix: "extract_value_id", + SortableColumns: []string{"name"}, + DefaultSortingColumn: "name", +} + +var WebhookMatcherProps = ObjectProps{ + TableName: "project__webhook_matcher", + Type: reflect.TypeOf(WebhookMatcher{}), + PrimaryColumnName: "id", + ReferringColumnSuffix: "matcher_id", + SortableColumns: []string{"name"}, + DefaultSortingColumn: "name", +} + var EnvironmentProps = ObjectProps{ TableName: "project__environment", Type: reflect.TypeOf(Environment{}), @@ -272,6 +356,7 @@ var ProjectProps = ObjectProps{ TableName: "project", Type: reflect.TypeOf(Project{}), PrimaryColumnName: "id", + ReferringColumnSuffix: "project_id", DefaultSortingColumn: "name", IsGlobal: true, } diff --git a/db/Webhook.go b/db/Webhook.go new file mode 100644 index 00000000..ba2e8f70 --- /dev/null +++ b/db/Webhook.go @@ -0,0 +1,175 @@ +package db + +import ( + "strings" + "strconv" +) + +type WebhookMatchType string + +const ( + WebhookMatchHeader WebhookMatchType = "header" + WebhookMatchBody WebhookMatchType = "body" +) + +type WebhookMatchMethodType string + +const ( + WebhookMatchMethodEquals WebhookMatchMethodType = "equals" + WebhookMatchMethodUnEquals WebhookMatchMethodType = "unequals" + WebhookMatchMethodContains WebhookMatchMethodType = "contains" +) + +type WebhookBodyDataType string + +const ( + WebhookBodyDataJSON WebhookBodyDataType = "json" + WebhookBodyDataXML WebhookBodyDataType = "xml" + WebhookBodyDataString WebhookBodyDataType = "string" +) + +type WebhookMatcher struct { + ID int `db:"id" json:"id"` + Name string `db:"name" json:"name"` + ExtractorID int `db:"extractor_id" json:"extractor_id"` + MatchType WebhookMatchType `db:"match_type" json:"match_type"` + Method WebhookMatchMethodType `db:"method" json:"method"` + BodyDataType WebhookBodyDataType `db:"body_data_type" json:"body_data_type"` + Key string `db:"key" json:"key"` + Value string `db:"value" json:"value"` +} + +type WebhookExtractValueSource string + +const ( + WebhookExtractBodyValue WebhookExtractValueSource = "body" + WebhookExtractHeaderValue WebhookExtractValueSource = "header" +) + +type WebhookExtractValue struct { + ID int `db:"id" json:"id"` + Name string `db:"name" json:"name"` + ExtractorID int `db:"extractor_id" json:"extractor_id"` + ValueSource WebhookExtractValueSource `db:"value_source" json:"value_source"` + BodyDataType WebhookBodyDataType `db:"body_data_type" json:"body_data_type"` + Key string `db:"key" json:"key"` + Variable string `db:"variable" json:"variable"` +} + +type WebhookExtractor struct { + ID int `db:"id" json:"id"` + Name string `db:"name" json:"name"` + WebhookID int `db:"webhook_id" json:"webhook_id"` +} + +type Webhook struct { + ID int `db:"id" json:"id"` + Name string `db:"name" json:"name"` + ProjectID int `db:"project_id" json:"project_id"` + TemplateID int `db:"template_id" json:"template_id"` +} + +func (env *Webhook) Validate() error { + if env.Name == "" { + return &ValidationError{"No Name set for webhook"} + } + return nil +} + +func (env *WebhookMatcher) Validate() error { + if env.MatchType == "" { + return &ValidationError{"No Match Type set"} + } else { + if env.Key == "" { + return &ValidationError{"No key set"} + } + if env.Value == "" { + return &ValidationError{"No value set"} + } + + } + + if env.Name == "" { + return &ValidationError{"No Name set for webhook"} + } + + return nil +} + +func (env *WebhookExtractor) Validate() error { + if env.Name == "" { + return &ValidationError{"No Name set for webhook"} + } + + return nil +} + + +func (env *WebhookExtractValue) Validate() error { + if env.ValueSource == "" { + return &ValidationError{"No Value Source defined"} + } + + if env.Name == "" { + return &ValidationError{"No Name set for webhook"} + } + + if env.ValueSource == WebhookExtractBodyValue { + if env.BodyDataType == "" { + return &ValidationError{"Value Source but no body data type set"} + } + + if env.BodyDataType == WebhookBodyDataJSON { + if env.Key == "" { + return &ValidationError{"No Key set for JSON Body Data extraction."} + } + } + } + + if env.ValueSource == WebhookExtractHeaderValue { + if env.Key == "" { + return &ValidationError{"Value Source set but no Key set"} + } + } + + return nil +} + +func (matcher *WebhookMatcher) String() string { + var builder strings.Builder + // ID:1234 body/json key == value on Extractor: 1234 + builder.WriteString("ID:" + strconv.Itoa(matcher.ID) + " " + string(matcher.MatchType)) + + if matcher.MatchType == WebhookMatchBody { + builder.WriteString("/" + string(matcher.BodyDataType)) + } + + builder.WriteString(" " + matcher.Key + " ") + + if matcher.Method == WebhookMatchMethodEquals { + builder.WriteString("==") + } else if matcher.Method == WebhookMatchMethodUnEquals { + builder.WriteString("!=") + } else if matcher.Method == WebhookMatchMethodContains { + builder.WriteString(" contains ") + } + + builder.WriteString(matcher.Value + ", on Extractor: " + strconv.Itoa(matcher.ExtractorID)) + + return builder.String() +} + +func (value *WebhookExtractValue) String() string { + var builder strings.Builder + + // ID:1234 body/json from key as argument + builder.WriteString("ID:" + strconv.Itoa(value.ID) + " " + string(value.ValueSource)) + + if value.ValueSource == WebhookExtractBodyValue { + builder.WriteString("/" + string(value.BodyDataType)) + } + + builder.WriteString(" from " + value.Key + " as " + value.Variable) + + return builder.String() +} diff --git a/db/bolt/BoltDb.go b/db/bolt/BoltDb.go index f6529ead..ecf007a3 100644 --- a/db/bolt/BoltDb.go +++ b/db/bolt/BoltDb.go @@ -548,6 +548,57 @@ func (d *BoltDb) createObject(bucketID int, props db.ObjectProps, object interfa return object, err } +func (d *BoltDb) getWebhookRefs(projectID int, objectProps db.ObjectProps, objectID int) (refs db.WebhookReferrers, err error) { + refs.WebhookExtractors, err = d.getReferringObjectByParentID(projectID, objectProps, objectID, db.WebhookExtractorProps) + + return +} + +func (d *BoltDb) getWebhookExtractorRefs(webhookID int, objectProps db.ObjectProps, objectID int) (refs db.WebhookExtractorReferrers, err error) { + refs.WebhookMatchers, err = d.getReferringObjectByParentID(webhookID, objectProps, objectID, db.WebhookMatcherProps) + if err != nil { + return + } + + refs.WebhookExtractValues, err = d.getReferringObjectByParentID(webhookID, objectProps, objectID, db.WebhookExtractValueProps) + if err != nil { + return + } + + return +} + +func (d *BoltDb) getWebhookExtractorChildrenRefs(extractorID int, objectProps db.ObjectProps, objectID int) (refs db.WebhookExtractorChildReferrers, err error) { + refs.WebhookExtractors, err = d.getReferringObjectByParentID(objectID, objectProps, extractorID, db.WebhookExtractorProps) + if err != nil { + return + } + + return +} + +func (d *BoltDb) getReferringObjectByParentID(parentID int, objProps db.ObjectProps, objID int, referringObjectProps db.ObjectProps) (referringObjs []db.ObjectReferrer, err error) { + referringObjs = make([]db.ObjectReferrer, 0) + + var referringObjectOfType reflect.Value = reflect.New(reflect.SliceOf(referringObjectProps.Type)) + err = d.getObjects(parentID, referringObjectProps, db.RetrieveQueryParams{}, func (referringObj interface{}) bool { + return isObjectReferredBy(objProps, intObjectID(objID), referringObj) + }, referringObjectOfType.Interface()) + + if err != nil { + return + } + + for i := 0; i < referringObjectOfType.Elem().Len(); i++ { + referringObjs = append(referringObjs, db.ObjectReferrer{ + ID: int(referringObjectOfType.Elem().Index(i).FieldByName("ID").Int()), + Name: referringObjectOfType.Elem().Index(i).FieldByName("Name").String(), + }) + } + + return +} + func (d *BoltDb) getObjectRefs(projectID int, objectProps db.ObjectProps, objectID int) (refs db.ObjectReferrers, err error) { refs.Templates, err = d.getObjectRefsFrom(projectID, objectProps, intObjectID(objectID), db.TemplateProps) if err != nil { diff --git a/db/bolt/webhook.go b/db/bolt/webhook.go new file mode 100644 index 00000000..794473f3 --- /dev/null +++ b/db/bolt/webhook.go @@ -0,0 +1,274 @@ +package bolt + +import ( + "github.com/ansible-semaphore/semaphore/db" +) + +/* + Webhooks +*/ +func (d *BoltDb) CreateWebhook(webhook db.Webhook) (db.Webhook, error) { + err := webhook.Validate() + + if err != nil { + return db.Webhook{}, err + } + + newWebhook, err := d.createObject(webhook.ProjectID, db.WebhookProps, webhook) + return newWebhook.(db.Webhook), err +} + +func (d *BoltDb) GetWebhooks(projectID int, params db.RetrieveQueryParams) (webhooks []db.Webhook, err error) { + err = d.getObjects(projectID, db.WebhookProps, params, nil, &webhooks) + return webhooks, err +} + +func (d *BoltDb) GetWebhook(projectID int, webhookID int) (webhook db.Webhook, err error) { + err = d.getObject(projectID, db.WebhookProps, intObjectID(webhookID), &webhook) + if err != nil { + return + } + + return +} + +func (d *BoltDb) GetAllWebhooks() ([]db.Webhook, error) { + return []db.Webhook{}, nil +} + +func (d *BoltDb) UpdateWebhook(webhook db.Webhook) error { + err := webhook.Validate() + + if err != nil { + return err + } + + return d.updateObject(webhook.ProjectID, db.WebhookProps, webhook) + +} + +func (d *BoltDb) GetWebhookRefs(projectID int, webhookID int) (db.WebhookReferrers, error) { + //return d.getObjectRefs(projectID, db.WebhookProps, webhookID) + return db.WebhookReferrers{}, nil +} + +/* + Webhook Extractors +*/ +func (d *BoltDb) GetWebhookExtractorsByWebhookID(webhookID int) (extractors []db.WebhookExtractor, err error) { + err = d.getObjects(webhookID, db.WebhookExtractorProps, db.RetrieveQueryParams{}, nil, &extractors) + return extractors, err +} + +func (d *BoltDb) CreateWebhookExtractor(webhookExtractor db.WebhookExtractor) (db.WebhookExtractor, error) { + err := webhookExtractor.Validate() + + if err != nil { + return db.WebhookExtractor{}, err + } + + newWebhookExtractor, err := d.createObject(webhookExtractor.WebhookID, db.WebhookExtractorProps, webhookExtractor) + return newWebhookExtractor.(db.WebhookExtractor), err +} + +func (d *BoltDb) GetAllWebhookExtractors() ([]db.WebhookExtractor, error) { + + return []db.WebhookExtractor{}, nil +} + +func (d *BoltDb) GetWebhookExtractors(webhookID int, params db.RetrieveQueryParams) ([]db.WebhookExtractor, error) { + var extractors []db.WebhookExtractor + err := d.getObjects(webhookID, db.WebhookExtractorProps, params, nil, &extractors) + + return extractors, err +} + +func (d *BoltDb) GetWebhookExtractor(extractorID int, webhookID int) (db.WebhookExtractor, error) { + var extractor db.WebhookExtractor + err := d.getObject(webhookID, db.WebhookExtractorProps, intObjectID(extractorID), &extractor) + + return extractor, err + +} + +func (d *BoltDb) UpdateWebhookExtractor(webhookExtractor db.WebhookExtractor) error { + err := webhookExtractor.Validate() + + if err != nil { + return err + } + + return d.updateObject(webhookExtractor.WebhookID, db.WebhookExtractorProps, webhookExtractor) +} + +func (d *BoltDb) GetWebhookExtractorRefs(webhookID int, extractorID int) (db.WebhookExtractorReferrers, error) { + return d.getWebhookExtractorRefs(webhookID, db.WebhookExtractorProps, extractorID) +} + +/* + Webhook ExtractValue +*/ +func (d *BoltDb) GetWebhookExtractValuesByExtractorID(extractorID int) (values []db.WebhookExtractValue, err error) { + err = d.getObjects(extractorID, db.WebhookExtractValueProps, db.RetrieveQueryParams{}, nil, &values) + return values, err +} + +func (d *BoltDb) DeleteWebhookExtractValue(extractorID int, valueID int) error { + return d.deleteObject(extractorID, db.WebhookExtractValueProps, intObjectID(valueID), nil) +} + +func (d *BoltDb) GetWebhookMatchersByExtractorID(extractorID int) (matchers []db.WebhookMatcher, err error) { + err = d.getObjects(extractorID, db.WebhookMatcherProps, db.RetrieveQueryParams{}, nil, &matchers) + + return matchers, err +} + +func (d *BoltDb) GetAllWebhookMatchers() (matchers []db.WebhookMatcher, err error) { + err = d.getObjects(0, db.WebhookMatcherProps, db.RetrieveQueryParams{}, nil, &matchers) + + return matchers, err +} + + +func (d *BoltDb) DeleteWebhookExtractor(webhookID int, extractorID int) error { + values, err := d.GetWebhookExtractValuesByExtractorID(extractorID) + + if err != nil { + return err + } + + for value := range values { + d.DeleteWebhookExtractValue(extractorID, values[value].ID) + } + + matchers, err := d.GetWebhookMatchersByExtractorID(extractorID) + + if err != nil { + return err + } + + for matcher := range matchers { + d.DeleteWebhookMatcher(extractorID, matchers[matcher].ID) + } + return d.deleteObject(webhookID, db.WebhookExtractorProps, intObjectID(extractorID), nil) +} + + +func (d *BoltDb) CreateWebhookExtractValue(value db.WebhookExtractValue) (db.WebhookExtractValue, error) { + err := value.Validate() + + if err != nil { + return db.WebhookExtractValue{}, err + } + + newValue, err := d.createObject(0, db.WebhookExtractValueProps, value) + return newValue.(db.WebhookExtractValue), err + +} + +func (d *BoltDb) GetWebhookExtractValues(extractorID int, params db.RetrieveQueryParams) ([]db.WebhookExtractValue, error) { + var values []db.WebhookExtractValue + err := d.getObjects(extractorID, db.WebhookExtractValueProps, params, nil, &values) + return values, err +} + +func (d *BoltDb) GetAllWebhookExtractValues() (matchers []db.WebhookExtractValue, err error) { + err = d.getObjects(0, db.WebhookExtractValueProps, db.RetrieveQueryParams{}, nil, &matchers) + + return matchers, err +} + + +func (d *BoltDb) GetWebhookExtractValue(extractorID int, valueID int) (value db.WebhookExtractValue, err error) { + err = d.getObject(extractorID, db.WebhookExtractValueProps, intObjectID(valueID), &value) + return value, err +} + +func (d *BoltDb) UpdateWebhookExtractValue(webhookExtractValue db.WebhookExtractValue) error { + err := webhookExtractValue.Validate() + + if err != nil { + return err + } + + return d.updateObject(webhookExtractValue.ExtractorID, db.WebhookExtractValueProps, webhookExtractValue) +} + +func (d *BoltDb) GetWebhookExtractValueRefs(extractorID int, valueID int) (db.WebhookExtractorChildReferrers, error) { + return d.getWebhookExtractorChildrenRefs(extractorID, db.WebhookExtractValueProps, valueID) +} +/* + Webhook Matcher +*/ +func (d *BoltDb) CreateWebhookMatcher(matcher db.WebhookMatcher) (db.WebhookMatcher, error) { + err := matcher.Validate() + + if err != nil { + return db.WebhookMatcher{}, err + } + newMatcher, err := d.createObject(0, db.WebhookMatcherProps, matcher) + return newMatcher.(db.WebhookMatcher), err +} + +func (d *BoltDb) GetWebhookMatchers(extractorID int, params db.RetrieveQueryParams) (matchers []db.WebhookMatcher, err error) { + matchers = make([]db.WebhookMatcher, 0) + var allMatchers []db.WebhookMatcher + + err = d.getObjects(extractorID, db.WebhookMatcherProps, db.RetrieveQueryParams{}, nil, &allMatchers) + + if err != nil { + return + } + + for _, v := range allMatchers { + if v.ExtractorID == extractorID { + matchers = append(matchers, v) + } + } + + return +} + +func (d *BoltDb) GetWebhookMatcher(matcherID int, extractorID int) (matcher db.WebhookMatcher, err error) { + var matchers []db.WebhookMatcher + matchers, err = d.GetWebhookMatchers(extractorID, db.RetrieveQueryParams{}) + + for _, v := range matchers { + if v.ID == matcherID { + matcher = v + } + } + + return +} + +func (d *BoltDb) UpdateWebhookMatcher(webhookMatcher db.WebhookMatcher) error { + err := webhookMatcher.Validate() + + if err != nil { + return err + } + + return d.updateObject(webhookMatcher.ExtractorID, db.WebhookMatcherProps, webhookMatcher) +} + +func (d *BoltDb) DeleteWebhookMatcher(extractorID int, matcherID int) error { + return d.deleteObject(0, db.WebhookMatcherProps, intObjectID(matcherID), nil) +} +func (d *BoltDb) DeleteWebhook(projectID int, webhookID int) error { + extractors, err := d.GetWebhookExtractorsByWebhookID(webhookID) + + if err != nil { + return err + } + + for extractor := range extractors { + d.DeleteWebhookExtractor(webhookID, extractors[extractor].ID) + } + + return d.deleteObject(projectID, db.WebhookProps, intObjectID(webhookID), nil) +} + +func (d *BoltDb) GetWebhookMatcherRefs(extractorID int, valueID int) (db.WebhookExtractorChildReferrers, error) { + return d.getWebhookExtractorChildrenRefs(extractorID, db.WebhookMatcherProps, valueID) +} diff --git a/db/sql/SqlDb.go b/db/sql/SqlDb.go index 6a391f30..896e7c86 100644 --- a/db/sql/SqlDb.go +++ b/db/sql/SqlDb.go @@ -1,142 +1,142 @@ package sql import ( - "database/sql" - "fmt" - log "github.com/Sirupsen/logrus" - "github.com/ansible-semaphore/semaphore/db" - "github.com/ansible-semaphore/semaphore/util" - "github.com/go-gorp/gorp/v3" - _ "github.com/go-sql-driver/mysql" // imports mysql driver - "github.com/gobuffalo/packr" - _ "github.com/lib/pq" - "github.com/masterminds/squirrel" - "reflect" - "regexp" - "strconv" - "strings" + "database/sql" + "fmt" + log "github.com/Sirupsen/logrus" + "github.com/ansible-semaphore/semaphore/db" + "github.com/ansible-semaphore/semaphore/util" + "github.com/go-gorp/gorp/v3" + _ "github.com/go-sql-driver/mysql" // imports mysql driver + "github.com/gobuffalo/packr" + _ "github.com/lib/pq" + "github.com/masterminds/squirrel" + "reflect" + "regexp" + "strconv" + "strings" ) type SqlDb struct { - sql *gorp.DbMap + sql *gorp.DbMap } var initialSQL = ` create table ` + "`migrations`" + ` ( - ` + "`version`" + ` varchar(255) not null primary key, - ` + "`upgraded_date`" + ` datetime null, - ` + "`notes`" + ` text null + ` + "`version`" + ` varchar(255) not null primary key, + ` + "`upgraded_date`" + ` datetime null, + ` + "`notes`" + ` text null ); ` var dbAssets = packr.NewBox("./migrations") func containsStr(arr []string, str string) bool { - for _, a := range arr { - if a == str { - return true - } - } - return false + for _, a := range arr { + if a == str { + return true + } + } + return false } func handleRollbackError(err error) { - if err != nil { - log.Warn(err.Error()) - } + if err != nil { + log.Warn(err.Error()) + } } var ( - identifierQuoteRE = regexp.MustCompile("`") + identifierQuoteRE = regexp.MustCompile("`") ) // validateMutationResult checks the success of the update query func validateMutationResult(res sql.Result, err error) error { - if err != nil { - if strings.Contains(err.Error(), "foreign key") { - err = db.ErrInvalidOperation - } - return err - } + if err != nil { + if strings.Contains(err.Error(), "foreign key") { + err = db.ErrInvalidOperation + } + return err + } - affected, err := res.RowsAffected() + affected, err := res.RowsAffected() - if err != nil { - return err - } + if err != nil { + return err + } - if affected == 0 { - return db.ErrNotFound - } + if affected == 0 { + return db.ErrNotFound + } - return nil + return nil } func (d *SqlDb) prepareQueryWithDialect(query string, dialect gorp.Dialect) string { - switch dialect.(type) { - case gorp.PostgresDialect: - var queryBuilder strings.Builder - argNum := 1 - for _, r := range query { - switch r { - case '?': - queryBuilder.WriteString("$" + strconv.Itoa(argNum)) - argNum++ - case '`': - queryBuilder.WriteRune('"') - default: - queryBuilder.WriteRune(r) - } - } - query = queryBuilder.String() - } - return query + switch dialect.(type) { + case gorp.PostgresDialect: + var queryBuilder strings.Builder + argNum := 1 + for _, r := range query { + switch r { + case '?': + queryBuilder.WriteString("$" + strconv.Itoa(argNum)) + argNum++ + case '`': + queryBuilder.WriteRune('"') + default: + queryBuilder.WriteRune(r) + } + } + query = queryBuilder.String() + } + return query } func (d *SqlDb) PrepareQuery(query string) string { - return d.prepareQueryWithDialect(query, d.sql.Dialect) + return d.prepareQueryWithDialect(query, d.sql.Dialect) } func (d *SqlDb) insert(primaryKeyColumnName string, query string, args ...interface{}) (int, error) { - var insertId int64 + var insertId int64 - switch d.sql.Dialect.(type) { - case gorp.PostgresDialect: - query += " returning " + primaryKeyColumnName + switch d.sql.Dialect.(type) { + case gorp.PostgresDialect: + query += " returning " + primaryKeyColumnName - err := d.sql.QueryRow(d.PrepareQuery(query), args...).Scan(&insertId) + err := d.sql.QueryRow(d.PrepareQuery(query), args...).Scan(&insertId) - if err != nil { - return 0, err - } - default: - res, err := d.exec(query, args...) + if err != nil { + return 0, err + } + default: + res, err := d.exec(query, args...) - if err != nil { - return 0, err - } + if err != nil { + return 0, err + } - insertId, err = res.LastInsertId() + insertId, err = res.LastInsertId() - if err != nil { - return 0, err - } - } + if err != nil { + return 0, err + } + } - return int(insertId), nil + return int(insertId), nil } func (d *SqlDb) exec(query string, args ...interface{}) (sql.Result, error) { - q := d.PrepareQuery(query) - return d.sql.Exec(q, args...) + q := d.PrepareQuery(query) + return d.sql.Exec(q, args...) } func (d *SqlDb) selectOne(holder interface{}, query string, args ...interface{}) error { - return d.sql.SelectOne(holder, d.PrepareQuery(query), args...) + return d.sql.SelectOne(holder, d.PrepareQuery(query), args...) } func (d *SqlDb) selectAll(i interface{}, query string, args ...interface{}) ([]interface{}, error) { - q := d.PrepareQuery(query) - return d.sql.Select(i, q, args...) + q := d.PrepareQuery(query) + return d.sql.Select(i, q, args...) } func connect() (*sql.DB, error) { @@ -155,312 +155,593 @@ func connect() (*sql.DB, error) { } func createDb() error { - cfg, err := util.Config.GetDBConfig() - if err != nil { - return err - } + cfg, err := util.Config.GetDBConfig() + if err != nil { + return err + } - if !cfg.HasSupportMultipleDatabases() { - return nil - } + if !cfg.HasSupportMultipleDatabases() { + return nil + } - connectionString, err := cfg.GetConnectionString(false) - if err != nil { - return err - } + connectionString, err := cfg.GetConnectionString(false) + if err != nil { + return err + } + + conn, err := sql.Open(cfg.Dialect, connectionString) + if err != nil { + return err + } - conn, err := sql.Open(cfg.Dialect, connectionString) - if err != nil { - return err - } + _, err = conn.Exec("create database " + cfg.GetDbName()) - _, err = conn.Exec("create database " + cfg.GetDbName()) + if err != nil { + log.Warn(err.Error()) + } - if err != nil { - log.Warn(err.Error()) - } - - return nil + return nil } -func (d *SqlDb) getObject(projectID int, props db.ObjectProps, objectID int, object interface{}) (err error) { - q := squirrel.Select("*"). - From(props.TableName). - Where("id=?", objectID) +func (d *SqlDb) getObjectByReferrer(referrerID int, referringObjectProps db.ObjectProps, props db.ObjectProps, objectID int, object interface{}) (err error) { + query, args, err := squirrel.Select("*"). + From(props.TableName). + Where("id=?", objectID). + Where(referringObjectProps.ReferringColumnSuffix + "=?", referrerID). + ToSql() - if props.IsGlobal { - q = q.Where("project_id is null") - } else { - q = q.Where("project_id=?", projectID) - } + if err != nil { + return + } - query, args, err := q.ToSql() + err = d.selectOne(object, query, args...) - if err != nil { - return - } + if err == sql.ErrNoRows { + err = db.ErrNotFound + } - err = d.selectOne(object, query, args...) + return +} - if err == sql.ErrNoRows { - err = db.ErrNotFound - } +func (d *SqlDb) getObjectsByReferrer(referrerID int, referringObjectProps db.ObjectProps, props db.ObjectProps, params db.RetrieveQueryParams, objects interface{}) (err error) { + var referringColumn = referringObjectProps.ReferringColumnSuffix - return + q := squirrel.Select("*"). + From(props.TableName+" pe"). + Where("pe." + referringColumn + "=?", referrerID) + + orderDirection := "ASC" + if params.SortInverted { + orderDirection = "DESC" + } + + orderColumn := props.DefaultSortingColumn + if containsStr(props.SortableColumns, params.SortBy) { + orderColumn = params.SortBy + } + + if orderColumn != "" { + q = q.OrderBy("pe." + orderColumn + " " + orderDirection) + } + + query, args, err := q.ToSql() + + if err != nil { + return + } + + _, err = d.selectAll(objects, query, args...) + + return } func (d *SqlDb) getObjects(projectID int, props db.ObjectProps, params db.RetrieveQueryParams, objects interface{}) (err error) { - q := squirrel.Select("*"). - From(props.TableName + " pe") + q := squirrel.Select("*"). + From(props.TableName + " pe") - if props.IsGlobal { - q = q.Where("pe.project_id is null") - } else { - q = q.Where("pe.project_id=?", projectID) - } + if props.IsGlobal { + q = q.Where("pe.project_id is null") + } else { + q = q.Where("pe.project_id=?", projectID) + } + + orderDirection := "ASC" - orderDirection := "ASC" - if params.SortInverted { - orderDirection = "DESC" - } + if params.SortInverted { + orderDirection = "DESC" + } - orderColumn := props.DefaultSortingColumn - if containsStr(props.SortableColumns, params.SortBy) { - orderColumn = params.SortBy - } + orderColumn := props.DefaultSortingColumn + if containsStr(props.SortableColumns, params.SortBy) { + orderColumn = params.SortBy + } - if orderColumn != "" { - q = q.OrderBy("pe." + orderColumn + " " + orderDirection) - } + if orderColumn != "" { + q = q.OrderBy("pe." + orderColumn + " " + orderDirection) + } - query, args, err := q.ToSql() + query, args, err := q.ToSql() - if err != nil { - return - } + if err != nil { + return + } - _, err = d.selectAll(objects, query, args...) + _, err = d.selectAll(objects, query, args...) - return + return +} + +func (d *SqlDb) getObject(projectID int, props db.ObjectProps, objectID int, objects interface{}) (err error) { + return d.getObjectByReferrer(projectID, db.ProjectProps, props, objectID, objects) +} +func (d *SqlDb) getObjects(projectID int, props db.ObjectProps, params db.RetrieveQueryParams, objects interface{}) (err error) { + return d.getObjectsByReferrer(projectID, db.ProjectProps, props, params, objects); +} + +func (d *SqlDb) deleteByReferrer(referrerID int, referringObjectProps db.ObjectProps, props db.ObjectProps, objectID int) error { + var referringColumn = referringObjectProps.ReferringColumnSuffix + + return validateMutationResult( + d.exec( + "delete from " + props.TableName + " where " + referringColumn + "=? and id=?", + referrerID, + objectID)) } func (d *SqlDb) deleteObject(projectID int, props db.ObjectProps, objectID int) error { - if props.IsGlobal { - return validateMutationResult( - d.exec( - "delete from "+props.TableName+" where id=?", - objectID)) - } else { - return validateMutationResult( - d.exec( - "delete from "+props.TableName+" where project_id=? and id=?", - projectID, - objectID)) - } + if props.IsGlobal { + return validateMutationResult( + d.exec( + "delete from "+props.TableName+" where id=?", + objectID)) + } else { + return validateMutationResult( + d.exec( + "delete from "+props.TableName+" where project_id=? and id=?", + projectID, + objectID)) + } + return d.deleteByReferrer(projectID, db.ProjectProps, props, objectID) +} + +func (d *SqlDb) deleteObjectByReferencedID(referencedID int, referencedProps db.ObjectProps, props db.ObjectProps, objectID int) error { + field := referencedProps.ReferringColumnSuffix + + return validateMutationResult( + d.exec("delete from " + props.TableName + " t where t." + field + "=? and t.id=?", referencedID, objectID)) } func (d *SqlDb) Close(token string) { - err := d.sql.Db.Close() - if err != nil { - panic(err) - } + err := d.sql.Db.Close() + if err != nil { + panic(err) + } } func (d *SqlDb) PermanentConnection() bool { - return true + return true } func (d *SqlDb) Connect(token string) { - sqlDb, err := connect() - if err != nil { - panic(err) - } + sqlDb, err := connect() + if err != nil { + panic(err) + } - if err := sqlDb.Ping(); err != nil { - if err = createDb(); err != nil { - panic(err) - } + if err := sqlDb.Ping(); err != nil { + if err = createDb(); err != nil { + panic(err) + } - sqlDb, err = connect() - if err != nil { - panic(err) - } + sqlDb, err = connect() + if err != nil { + panic(err) + } - if err = sqlDb.Ping(); err != nil { - panic(err) - } - } + if err = sqlDb.Ping(); err != nil { + panic(err) + } + } - cfg, err := util.Config.GetDBConfig() - if err != nil { - panic(err) - } + cfg, err := util.Config.GetDBConfig() + if err != nil { + panic(err) + } - var dialect gorp.Dialect + var dialect gorp.Dialect - switch cfg.Dialect { - case util.DbDriverMySQL: - dialect = gorp.MySQLDialect{Engine: "InnoDB", Encoding: "UTF8"} - case util.DbDriverPostgres: - dialect = gorp.PostgresDialect{} - } + switch cfg.Dialect { + case util.DbDriverMySQL: + dialect = gorp.MySQLDialect{Engine: "InnoDB", Encoding: "UTF8"} + case util.DbDriverPostgres: + dialect = gorp.PostgresDialect{} + } - d.sql = &gorp.DbMap{Db: sqlDb, Dialect: dialect} + d.sql = &gorp.DbMap{Db: sqlDb, Dialect: dialect} + + d.sql.AddTableWithName(db.APIToken{}, "user__token").SetKeys(false, "id") + d.sql.AddTableWithName(db.AccessKey{}, "access_key").SetKeys(true, "id") + d.sql.AddTableWithName(db.Environment{}, "project__environment").SetKeys(true, "id") + d.sql.AddTableWithName(db.Inventory{}, "project__inventory").SetKeys(true, "id") + d.sql.AddTableWithName(db.Project{}, "project").SetKeys(true, "id") + d.sql.AddTableWithName(db.Repository{}, "project__repository").SetKeys(true, "id") + d.sql.AddTableWithName(db.Task{}, "task").SetKeys(true, "id") + d.sql.AddTableWithName(db.TaskOutput{}, "task__output").SetUniqueTogether("task_id", "time") + d.sql.AddTableWithName(db.Template{}, "project__template").SetKeys(true, "id") + d.sql.AddTableWithName(db.User{}, "user").SetKeys(true, "id") + d.sql.AddTableWithName(db.Session{}, "session").SetKeys(true, "id") + d.sql.AddTableWithName(db.Webhook{}, "webhook").SetKeys(true, "id") + d.sql.AddTableWithName(db.WebhookExtractor{}, "webhookextractor").SetKeys(true, "id") + d.sql.AddTableWithName(db.WebhookMatcher{}, "webhookmatcher").SetKeys(true, "id") + d.sql.AddTableWithName(db.WebhookExtractValue{}, "webhookextractvalue").SetKeys(true, "id") - d.sql.AddTableWithName(db.APIToken{}, "user__token").SetKeys(false, "id") - d.sql.AddTableWithName(db.AccessKey{}, "access_key").SetKeys(true, "id") - d.sql.AddTableWithName(db.Environment{}, "project__environment").SetKeys(true, "id") - d.sql.AddTableWithName(db.Inventory{}, "project__inventory").SetKeys(true, "id") - d.sql.AddTableWithName(db.Project{}, "project").SetKeys(true, "id") - d.sql.AddTableWithName(db.Repository{}, "project__repository").SetKeys(true, "id") - d.sql.AddTableWithName(db.Task{}, "task").SetKeys(true, "id") - d.sql.AddTableWithName(db.TaskOutput{}, "task__output").SetUniqueTogether("task_id", "time") - d.sql.AddTableWithName(db.Template{}, "project__template").SetKeys(true, "id") - d.sql.AddTableWithName(db.User{}, "user").SetKeys(true, "id") - d.sql.AddTableWithName(db.Session{}, "session").SetKeys(true, "id") } func getSqlForTable(tableName string, p db.RetrieveQueryParams) (string, []interface{}, error) { - if p.Offset > 0 && p.Count <= 0 { - return "", nil, fmt.Errorf("offset cannot be without limit") - } + if p.Offset > 0 && p.Count <= 0 { + return "", nil, fmt.Errorf("offset cannot be without limit") + } - q := squirrel.Select("*"). - From("`" + tableName + "`") + q := squirrel.Select("*"). + From("`" + tableName + "`") - if p.SortBy != "" { - sortDirection := "ASC" - if p.SortInverted { - sortDirection = "DESC" - } + if p.SortBy != "" { + sortDirection := "ASC" + if p.SortInverted { + sortDirection = "DESC" + } - q = q.OrderBy(p.SortBy + " " + sortDirection) - } + q = q.OrderBy(p.SortBy + " " + sortDirection) + } - if p.Offset > 0 || p.Count > 0 { - q = q.Offset(uint64(p.Offset)) - } + if p.Offset > 0 || p.Count > 0 { + q = q.Offset(uint64(p.Offset)) + } - if p.Count > 0 { - q = q.Limit(uint64(p.Count)) - } + if p.Count > 0 { + q = q.Limit(uint64(p.Count)) + } - return q.ToSql() + return q.ToSql() } func (d *SqlDb) getObjectRefs(projectID int, objectProps db.ObjectProps, objectID int) (refs db.ObjectReferrers, err error) { - refs.Templates, err = d.getObjectRefsFrom(projectID, objectProps, objectID, db.TemplateProps) - if err != nil { - return - } + refs.Templates, err = d.getObjectRefsFrom(projectID, objectProps, objectID, db.TemplateProps) + if err != nil { + return + } - refs.Repositories, err = d.getObjectRefsFrom(projectID, objectProps, objectID, db.RepositoryProps) - if err != nil { - return - } + refs.Repositories, err = d.getObjectRefsFrom(projectID, objectProps, objectID, db.RepositoryProps) + if err != nil { + return + } - refs.Inventories, err = d.getObjectRefsFrom(projectID, objectProps, objectID, db.InventoryProps) - if err != nil { - return - } + refs.Inventories, err = d.getObjectRefsFrom(projectID, objectProps, objectID, db.InventoryProps) + if err != nil { + return + } - templates, err := d.getObjectRefsFrom(projectID, objectProps, objectID, db.ScheduleProps) - if err != nil { - return - } + templates, err := d.getObjectRefsFrom(projectID, objectProps, objectID, db.ScheduleProps) + if err != nil { + return + } - for _, st := range templates { - exists := false - for _, tpl := range refs.Templates { - if tpl.ID == st.ID { - exists = true - break - } - } - if exists { - continue - } - refs.Templates = append(refs.Templates, st) - } + for _, st := range templates { + exists := false + for _, tpl := range refs.Templates { + if tpl.ID == st.ID { + exists = true + break + } + } + if exists { + continue + } + refs.Templates = append(refs.Templates, st) + } - return + return } func (d *SqlDb) getObjectRefsFrom( - projectID int, - objectProps db.ObjectProps, - objectID int, - referringObjectProps db.ObjectProps, + projectID int, + objectProps db.ObjectProps, + objectID int, + referringObjectProps db.ObjectProps, ) (referringObjs []db.ObjectReferrer, err error) { - referringObjs = make([]db.ObjectReferrer, 0) + referringObjs = make([]db.ObjectReferrer, 0) - fields, err := objectProps.GetReferringFieldsFrom(referringObjectProps.Type) + fields, err := objectProps.GetReferringFieldsFrom(referringObjectProps.Type) - cond := "" - vals := []interface{}{projectID} + cond := "" + vals := []interface{}{projectID} - for _, f := range fields { - if cond != "" { - cond += " or " - } + for _, f := range fields { + if cond != "" { + cond += " or " + } - cond += f + " = ?" + cond += f + " = ?" - vals = append(vals, objectID) - } + vals = append(vals, objectID) + } - if cond == "" { - return - } + if cond == "" { + return + } - var referringObjects reflect.Value + var referringObjects reflect.Value - if referringObjectProps.Type == db.ScheduleProps.Type { - var referringSchedules []db.Schedule - _, err = d.selectAll(&referringSchedules, "select template_id id from project__schedule where project_id = ? and ("+cond+")", vals...) + if referringObjectProps.Type == db.ScheduleProps.Type { + var referringSchedules []db.Schedule + _, err = d.selectAll(&referringSchedules, "select template_id id from project__schedule where project_id = ? and ("+cond+")", vals...) - if err != nil { - return - } + if err != nil { + return + } - if len(referringSchedules) == 0 { - return - } + if len(referringSchedules) == 0 { + return + } - var ids []string - for _, schedule := range referringSchedules { - ids = append(ids, strconv.Itoa(schedule.ID)) - } + var ids []string + for _, schedule := range referringSchedules { + ids = append(ids, strconv.Itoa(schedule.ID)) + } - referringObjects = reflect.New(reflect.SliceOf(db.TemplateProps.Type)) - _, err = d.selectAll(referringObjects.Interface(), - "select id, name from project__template where id in ("+strings.Join(ids, ",")+")") - } else { - referringObjects = reflect.New(reflect.SliceOf(referringObjectProps.Type)) - _, err = d.selectAll( - referringObjects.Interface(), - "select id, name from "+referringObjectProps.TableName+" where project_id = ? and "+cond, - vals...) - } + referringObjects = reflect.New(reflect.SliceOf(db.TemplateProps.Type)) + _, err = d.selectAll(referringObjects.Interface(), + "select id, name from project__template where id in ("+strings.Join(ids, ",")+")") + } else { + referringObjects = reflect.New(reflect.SliceOf(referringObjectProps.Type)) + _, err = d.selectAll( + referringObjects.Interface(), + "select id, name from "+referringObjectProps.TableName+" where project_id = ? and "+cond, + vals...) + } - if err != nil { - return - } + if err != nil { + return + } - for i := 0; i < referringObjects.Elem().Len(); i++ { - id := int(referringObjects.Elem().Index(i).FieldByName("ID").Int()) - name := referringObjects.Elem().Index(i).FieldByName("Name").String() - referringObjs = append(referringObjs, db.ObjectReferrer{ID: id, Name: name}) - } + for i := 0; i < referringObjects.Elem().Len(); i++ { + id := int(referringObjects.Elem().Index(i).FieldByName("ID").Int()) + name := referringObjects.Elem().Index(i).FieldByName("Name").String() + referringObjs = append(referringObjs, db.ObjectReferrer{ID: id, Name: name}) + } - return + return } func (d *SqlDb) Sql() *gorp.DbMap { - return d.sql + return d.sql } func (d *SqlDb) IsInitialized() (bool, error) { - _, err := d.sql.SelectInt(d.PrepareQuery("select count(1) from migrations")) - return err == nil, nil + _, err := d.sql.SelectInt(d.PrepareQuery("select count(1) from migrations")) + return err == nil, nil +} + +/** + GENERIC IMPLEMENTATION + **/ + +func InsertTemplateFromType(typeInstance interface{}) (string, []interface{}) { + val := reflect.Indirect(reflect.ValueOf(typeInstance)) + typeFieldSize := val.Type().NumField() + + fields := "" + values := "" + args := make([]interface{}, 0) + + if typeFieldSize > 1 { + fields += "(" + values += "(" + } + + for i := 0; i < typeFieldSize; i++ { + if val.Type().Field(i).Name == "ID" { + continue + } + fields += val.Type().Field(i).Tag.Get("db") + values += "?" + args = append(args, val.Field(i)) + if i != (typeFieldSize - 1) { + fields += ", " + values += ", " + } + } + + if typeFieldSize > 1 { + fields += ")" + values += ")" + } + + return fields + " values " + values, args +} + +func AddParams(params db.RetrieveQueryParams, q *squirrel.SelectBuilder, props db.ObjectProps) { + orderDirection := "ASC" + if params.SortInverted { + orderDirection = "DESC" + } + + orderColumn := props.DefaultSortingColumn + if containsStr(props.SortableColumns, params.SortBy) { + orderColumn = params.SortBy + } + + if orderColumn != "" { + q.OrderBy("t." + orderColumn + " " + orderDirection) + } +} + + + +func (d *SqlDb) GetObject(props db.ObjectProps, ID int) (object interface{}, err error) { + query, args, err := squirrel.Select("t.*"). + From(props.TableName + " as t"). + Where(squirrel.Eq{"t.id": ID}). + OrderBy("t.id"). + ToSql() + + if err != nil { + return + } + err = d.selectOne(&object, query, args...) + + return +} + +func (d *SqlDb) CreateObject(props db.ObjectProps, object interface{}) (newObject interface{}, err error) { + //err = newObject.Validate() + + if err != nil { + return + } + + template, args := InsertTemplateFromType(newObject) + insertID, err := d.insert( + "id", + "insert into " + props.TableName + " " + template, args...) + + if err != nil { + return + } + + newObject = object + + + v := reflect.ValueOf(newObject) + field := v.FieldByName("ID") + field.SetInt(int64(insertID)) + + return +} + +func (d *SqlDb) GetObjectsByForeignKeyQuery(props db.ObjectProps, foreignID int, foreignProps db.ObjectProps, params db.RetrieveQueryParams, objects interface{}) (err error) { + q := squirrel.Select("*"). + From(props.TableName + " as t"). + Where(foreignProps.ReferringColumnSuffix + "=?", foreignID) + + AddParams(params, &q, props) + + query, args, err := q. + OrderBy("t.id"). + ToSql() + + if err != nil { + return + } + err = d.selectOne(&objects, query, args...) + + return +} + +func (d *SqlDb) GetAllObjectsByForeignKey(props db.ObjectProps, foreignID int, foreignProps db.ObjectProps) (objects interface{}, err error) { + query, args, err := squirrel.Select("*"). + From(props.TableName + " as t"). + Where(foreignProps.ReferringColumnSuffix + "=?", foreignID). + OrderBy("t.id"). + ToSql() + + if err != nil { + return + } + + results, errQuery := d.selectAll(&objects, query, args...) + + return results, errQuery +} + +func (d *SqlDb) GetAllObjects(props db.ObjectProps) (objects interface{}, err error) { + query, args, err := squirrel.Select("*"). + From(props.TableName + " as t"). + OrderBy("t.id"). + ToSql() + + if err != nil { + return + } + var results []interface{} + results, err = d.selectAll(&objects, query, args...) + + return results, err + +} + +// Retrieve the Matchers & Values referncing `id' from WebhookExtractor +// -- +// Examples: +// referrerCollection := db.ObjectReferrers{} +// d.GetReferencesForForeignKey(db.ProjectProps, id, map[string]db.ObjectProps{ +// 'Templates': db.TemplateProps, +// 'Inventories': db.InventoryProps, +// 'Repositories': db.RepositoryProps +// }, &referrerCollection) +// +// // +// +// referrerCollection := db.WebhookExtractorReferrers{} +// d.GetReferencesForForeignKey(db.WebhookProps, id, map[string]db.ObjectProps{ +// "Matchers": db.WebhookMatcherProps, +// "Values": db.WebhookExtractValueProps +// }, &referrerCollection) +func (d *SqlDb) GetReferencesForForeignKey(objectProps db.ObjectProps, objectID int, referrerMapping map[string]db.ObjectProps, referrerCollection *interface{}) (err error) { + + for key, value := range referrerMapping { + //v := reflect.ValueOf(referrerCollection) + referrers, errRef := d.GetObjectReferences(objectProps, value, objectID) + + if errRef != nil { + return errRef + } + reflect.ValueOf(referrerCollection).FieldByName(key).Set(reflect.ValueOf(referrers)) + } + + return +} + +// Find Object Referrers for objectID based on referring column taken from referringObjectProps +// Example: +// GetObjectReferences(db.WebhookMatchers, db.WebhookExtractorProps, extractorID) +func (d *SqlDb) GetObjectReferences(objectProps db.ObjectProps, referringObjectProps db.ObjectProps, objectID int) (referringObjs []db.ObjectReferrer, err error) { + referringObjs = make([]db.ObjectReferrer, 0) + + fields, err := objectProps.GetReferringFieldsFrom(objectProps.Type) + + cond := "" + vals := []interface{}{} + + for _, f := range fields { + if cond != "" { + cond += " or " + } + + cond += f + " = ?" + + vals = append(vals, objectID) + } + + if cond == "" { + return + } + + referringObjects := reflect.New(reflect.SliceOf(referringObjectProps.Type)) + _, err = d.selectAll( + referringObjects.Interface(), + "select id, name from "+referringObjectProps.TableName+" where " + objectProps.ReferringColumnSuffix + " = ? and "+cond, + vals...) + + if err != nil { + return + } + + for i := 0; i < referringObjects.Elem().Len(); i++ { + id := int(referringObjects.Elem().Index(i).FieldByName("ID").Int()) + name := referringObjects.Elem().Index(i).FieldByName("Name").String() + referringObjs = append(referringObjs, db.ObjectReferrer{ID: id, Name: name}) + } + + return } diff --git a/db/sql/SqlDb_test.go b/db/sql/SqlDb_test.go index c834e868..dfd2d381 100644 --- a/db/sql/SqlDb_test.go +++ b/db/sql/SqlDb_test.go @@ -2,13 +2,39 @@ package sql import ( "github.com/go-gorp/gorp/v3" + "github.com/go-sql-driver/mysql" "testing" ) +var pool *mockDriver; + +type sqlmock struct { + ordered bool + dsn string + opened int + drv *mockDriver + converter driver.ValueConverter + queryMatcher QueryMatcher + monitorPings bool + + expected []expectation +} + +func init() { + pool = &mockDriver{ + conns: make(map[string]*sqlmock), + } + SqlDb.sql.Register("sqlmock", pool) +} + func TestValidatePort(t *testing.T) { d := SqlDb{} q := d.prepareQueryWithDialect("select * from `test` where id = ?, email = ?", gorp.PostgresDialect{}) if q != "select * from \"test\" where id = $1, email = $2" { t.Error("invalid postgres query") } -} \ No newline at end of file +} + +func TestGetAllObjects(t *testing.T) { + +} diff --git a/db/sql/access_key.go b/db/sql/access_key.go index 00d36182..cd1f7b8a 100644 --- a/db/sql/access_key.go +++ b/db/sql/access_key.go @@ -6,7 +6,12 @@ import ( ) func (d *SqlDb) GetAccessKey(projectID int, accessKeyID int) (key db.AccessKey, err error) { - err = d.getObject(projectID, db.AccessKeyProps, accessKeyID, &key) + var keyObj interface{} + keyObj, err = d.GetObject(db.AccessKeyProps, accessKeyID) + key = keyObj.(db.AccessKey) + if err != nil { + return + } return } diff --git a/db/sql/environment.go b/db/sql/environment.go index 31b745a2..cfc23f3c 100644 --- a/db/sql/environment.go +++ b/db/sql/environment.go @@ -4,10 +4,9 @@ import ( "github.com/ansible-semaphore/semaphore/db" ) -func (d *SqlDb) GetEnvironment(projectID int, environmentID int) (db.Environment, error) { - var environment db.Environment - err := d.getObject(projectID, db.EnvironmentProps, environmentID, &environment) - return environment, err +func (d *SqlDb) GetEnvironment(projectID int, environmentID int) (environment db.Environment, err error) { + err = d.getObject(projectID, db.EnvironmentProps, environmentID, &environment) + return } func (d *SqlDb) GetEnvironmentRefs(projectID int, environmentID int) (db.ObjectReferrers, error) { diff --git a/db/sql/event.go b/db/sql/event.go index 7dcf83df..773c97dc 100644 --- a/db/sql/event.go +++ b/db/sql/event.go @@ -1,72 +1,72 @@ package sql import ( - "github.com/ansible-semaphore/semaphore/db" - "github.com/masterminds/squirrel" - "time" + "github.com/ansible-semaphore/semaphore/db" + "github.com/masterminds/squirrel" + "time" ) func (d *SqlDb) getEvents(q squirrel.SelectBuilder, params db.RetrieveQueryParams) (events []db.Event, err error) { - if params.Count > 0 { - q = q.Limit(uint64(params.Count)) - } + if params.Count > 0 { + q = q.Limit(uint64(params.Count)) + } - query, args, err := q.ToSql() + query, args, err := q.ToSql() - if err != nil { - return - } + if err != nil { + return + } - _, err = d.selectAll(&events, query, args...) + _, err = d.selectAll(&events, query, args...) - if err != nil { - return - } + if err != nil { + return + } - err = db.FillEvents(d, events) + err = db.FillEvents(d, events) - return + return } func (d *SqlDb) CreateEvent(evt db.Event) (newEvent db.Event, err error) { - var created = time.Now() + var created = time.Now() - _, err = d.exec( - "insert into event(user_id, project_id, object_id, object_type, description, created) values (?, ?, ?, ?, ?, ?)", - evt.UserID, - evt.ProjectID, - evt.ObjectID, - evt.ObjectType, - evt.Description, - created) + _, err = d.exec( + "insert into event(user_id, project_id, object_id, object_type, description, created) values (?, ?, ?, ?, ?, ?)", + evt.UserID, + evt.ProjectID, + evt.ObjectID, + evt.ObjectType, + evt.Description, + created) - if err != nil { - return - } + if err != nil { + return + } - newEvent = evt - newEvent.Created = created - return + newEvent = evt + newEvent.Created = created + return } func (d *SqlDb) GetUserEvents(userID int, params db.RetrieveQueryParams) ([]db.Event, error) { - q := squirrel.Select("event.*, p.name as project_name"). - From("event"). - LeftJoin("project as p on event.project_id=p.id"). - OrderBy("created desc"). - LeftJoin("project__user as pu on pu.project_id=p.id"). - Where("p.id IS NULL or pu.user_id=?", userID) + q := squirrel.Select("event.*, p.name as project_name"). + From("event"). + LeftJoin("project as p on event.project_id=p.id"). + OrderBy("created desc"). + LeftJoin("project__user as pu on pu.project_id=p.id"). + Where("p.id IS NULL or pu.user_id=?", userID) - return d.getEvents(q, params) + return d.getEvents(q, params) } func (d *SqlDb) GetEvents(projectID int, params db.RetrieveQueryParams) ([]db.Event, error) { - q := squirrel.Select("event.*, p.name as project_name"). - From("event"). - LeftJoin("project as p on event.project_id=p.id"). - OrderBy("created desc"). - Where("event.project_id=?", projectID) + q := squirrel.Select("event.*, p.name as project_name"). + From("event"). + LeftJoin("project as p on event.project_id=p.id"). + OrderBy("created desc"). + Where("event.project_id=?", projectID) - return d.getEvents(q, params) + return d.getEvents(q, params) } diff --git a/db/sql/inventory.go b/db/sql/inventory.go index 7ea3fcd6..28747a1a 100644 --- a/db/sql/inventory.go +++ b/db/sql/inventory.go @@ -3,7 +3,9 @@ package sql import "github.com/ansible-semaphore/semaphore/db" func (d *SqlDb) GetInventory(projectID int, inventoryID int) (inventory db.Inventory, err error) { - err = d.getObject(projectID, db.InventoryProps, inventoryID, &inventory) + var invObj interface{} + invObj, err = d.GetObject(db.InventoryProps, inventoryID) + inventory = invObj.(db.Inventory) if err != nil { return } diff --git a/db/sql/migration.go b/db/sql/migration.go index dee59943..0ac20cf5 100644 --- a/db/sql/migration.go +++ b/db/sql/migration.go @@ -35,6 +35,8 @@ func getVersionErrPath(version db.Migration) string { // getVersionSQL takes a path to an SQL file and returns it from packr as // a slice of strings separated by newlines func getVersionSQL(path string) (queries []string) { + log.Info(fmt.Sprintf("GetVersionSQL: %s", path)) + sql, err := dbAssets.MustString(path) if err != nil { panic(err) diff --git a/db/sql/migrations/v2.9.7.sql b/db/sql/migrations/v2.9.7.sql new file mode 100644 index 00000000..98a63fec --- /dev/null +++ b/db/sql/migrations/v2.9.7.sql @@ -0,0 +1,42 @@ +create table project__webhook ( + `id` integer primary key autoincrement, + `name` varchar(255) not null, + `project_id` int not null, + `template_id` int null, + + foreign key (`project_id`) references project(`id`) on delete cascade, + foreign key (`template_id`) references project__template(`id`) +); + +create table project__webhook_extractor ( + `id` integer primary key autoincrement, + `name` varchar(255) not null, + `webhook_id` int not null, + + foreign key (`webhook_id`) references project__webhook(`id`) on delete cascade +); + +create table project__webhook_extract_value ( + `id` integer primary key autoincrement, + `name` varchar(255) not null, + `extractor_id` int not null, + `value_source` varchar(255) not null, + `body_data_type` varchar(255) null, + `key` varchar(255) null, + `variable` varchar(255) null, + + foreign key (`extractor_id`) references project__webhook_extractor(`id`) on delete cascade +); + +create table project__webhook_matcher ( + `id` integer primary key autoincrement, + `name` varchar(255) not null, + `extractor_id` int not null, + `match_type` varchar(255) null, + `method` varchar(255) null, + `body_data_type` varchar(255) null, + `key` varchar(510) null, + `value` varchar(510) null, + + foreign key (`extractor_id`) references project__webhook_extractor(`id`) on delete cascade +); diff --git a/db/sql/webhook.go b/db/sql/webhook.go new file mode 100644 index 00000000..a2b637d8 --- /dev/null +++ b/db/sql/webhook.go @@ -0,0 +1,434 @@ +package sql + +import ( + "github.com/ansible-semaphore/semaphore/db" + "github.com/masterminds/squirrel" + "strings" + // fmt" + log "github.com/Sirupsen/logrus" +) + +func (d *SqlDb) CreateWebhook(webhook db.Webhook) (newWebhook db.Webhook, err error) { + err = webhook.Validate() + + if err != nil { + return + } + + insertID, err := d.insert( + "id", + "insert into project__webhook (project_id, name, template_id) values (?, ?, ?)", + webhook.ProjectID, + webhook.Name, + webhook.TemplateID) + + if err != nil { + return + } + + newWebhook = webhook + newWebhook.ID = insertID + + return +} + +func (d *SqlDb) GetWebhooks(projectID int, params db.RetrieveQueryParams) (webhooks []db.Webhook, err error) { + err = d.getObjects(projectID, db.WebhookProps, params, &webhooks) + return webhooks, err +} + +func (d *SqlDb) GetAllWebhooks() (webhooks []db.Webhook, err error) { + var webhookObjects interface{} + webhookObjects, err = d.GetAllObjects(db.WebhookProps) + webhooks = webhookObjects.([]db.Webhook) + return +} + +func (d *SqlDb) GetWebhook(projectID int, webhookID int) (webhook db.Webhook, err error) { + query, args, err := squirrel.Select("w.*"). + From("project__webhook as w"). + Where(squirrel.And{ + squirrel.Eq{"w.id": webhookID}, + }). + OrderBy("w.id"). + ToSql() + + if err != nil { + return + } + err = d.selectOne(&webhook, query, args...) + + return +} + +func (d *SqlDb) GetWebhookRefs(projectID int, webhookID int) (referrers db.WebhookReferrers, err error) { + var extractorReferrer []db.ObjectReferrer + extractorReferrer, err = d.GetObjectReferences(db.WebhookProps, db.WebhookExtractorProps, webhookID) + referrers = db.WebhookReferrers{ + WebhookExtractors: extractorReferrer, + } + return +} + +func (d *SqlDb) GetWebhookExtractorsByWebhookID(webhookID int) ([]db.WebhookExtractor, error) { + var extractors []db.WebhookExtractor + err := d.GetObjectsByForeignKeyQuery(db.WebhookExtractorProps, webhookID, db.WebhookProps, db.RetrieveQueryParams{}, &extractors); + return extractors, err +} + +func (d *SqlDb) DeleteWebhook(projectID int, webhookID int) error { + extractors, err := d.GetWebhookExtractorsByWebhookID(webhookID) + + if err != nil { + return err + } + + for extractor := range extractors { + d.DeleteWebhookExtractor(webhookID, extractors[extractor].ID) + } + return d.deleteObject(projectID, db.WebhookProps, webhookID) +} + +func (d *SqlDb) UpdateWebhook(webhook db.Webhook) error { + err := webhook.Validate() + + if err != nil { + return err + } + + _, err = d.exec( + "update project__webhook set name=?, template_id=? where id=?", + webhook.Name, + webhook.TemplateID, + webhook.ID) + + return err +} + +func (d *SqlDb) CreateWebhookExtractor(webhookExtractor db.WebhookExtractor) (newWebhookExtractor db.WebhookExtractor, err error) { + err = webhookExtractor.Validate() + + if err != nil { + return + } + + insertID, err := d.insert( + "id", + "insert into project__webhook_extractor (name, webhook_id) values (?, ?)", + webhookExtractor.Name, + webhookExtractor.WebhookID) + + if err != nil { + return + } + + newWebhookExtractor = webhookExtractor + newWebhookExtractor.ID = insertID + + return +} + +func (d *SqlDb) GetWebhookExtractor(extractorID int, webhookID int) (extractor db.WebhookExtractor, err error) { + query, args, err := squirrel.Select("e.*"). + From("project__webhook_extractor as e"). + Where(squirrel.And{ + squirrel.Eq{"webhook_id": webhookID}, + squirrel.Eq{"id": extractorID}, + }). + OrderBy("e.name"). + ToSql() + + if err != nil { + return + } + + err = d.selectOne(&extractor, query, args...) + + return extractor, err +} + +func (d *SqlDb) GetAllWebhookExtractors() (extractors []db.WebhookExtractor, err error) { + var extractorObjects interface{} + extractorObjects, err = d.GetAllObjects(db.WebhookExtractorProps) + extractors = extractorObjects.([]db.WebhookExtractor) + return +} + +func (d *SqlDb) GetWebhookExtractors(webhookID int, params db.RetrieveQueryParams) ([]db.WebhookExtractor, error) { + var extractors []db.WebhookExtractor + err := d.getObjectsByReferrer(webhookID, db.WebhookProps, db.WebhookExtractorProps, params, &extractors) + + return extractors, err +} + +func (d *SqlDb) GetWebhookExtractorRefs(webhookID int, extractorID int) (refs db.WebhookExtractorReferrers, err error) { + refs.WebhookMatchers, err = d.GetObjectReferences(db.WebhookMatcherProps, db.WebhookExtractorProps, extractorID) + refs.WebhookExtractValues, err = d.GetObjectReferences(db.WebhookExtractValueProps, db.WebhookExtractorProps, extractorID) + + return +} + +func (d *SqlDb) GetWebhookExtractValuesByExtractorID(extractorID int) (values []db.WebhookExtractValue, err error) { + var sqlError error + query, args, sqlError := squirrel.Select("v.*"). + From("project__webhook_extract_value as v"). + Where(squirrel.Eq{"extractor_id": extractorID}). + OrderBy("v.id"). + ToSql() + + if sqlError != nil { + return []db.WebhookExtractValue{}, sqlError + } + + err = d.selectOne(&values, query, args...) + + return values, err +} + +func (d *SqlDb) GetWebhookMatchersByExtractorID(extractorID int) (matchers []db.WebhookMatcher, err error) { + var sqlError error + query, args, sqlError := squirrel.Select("m.*"). + From("project__webhook_matcher as m"). + Where(squirrel.Eq{"extractor_id": extractorID}). + OrderBy("m.id"). + ToSql() + + if sqlError != nil { + return []db.WebhookMatcher{}, sqlError + } + + err = d.selectOne(&matchers, query, args...) + + return matchers, err +} + +func (d *SqlDb) DeleteWebhookExtractor(webhookID int, extractorID int) error { + values, err := d.GetWebhookExtractValuesByExtractorID(extractorID) + if err != nil && !strings.Contains(err.Error(), "no rows in result set") { + return err + } + + for value := range values { + + err = d.DeleteWebhookExtractValue(extractorID, values[value].ID) + if err != nil && !strings.Contains(err.Error(), "no rows in result set") { + log.Error(err) + return err + } + } + + matchers, errExtractor := d.GetWebhookMatchersByExtractorID(extractorID) + if errExtractor != nil && !strings.Contains(errExtractor.Error(), "no rows in result set") { + log.Error(errExtractor) + return errExtractor + } + + for matcher := range matchers { + err = d.DeleteWebhookMatcher(extractorID, matchers[matcher].ID) + if err != nil && !strings.Contains(err.Error(), "no rows in result set") { + log.Error(err) + return err + } + } + + return d.deleteObjectByReferencedID(webhookID, db.WebhookProps, db.WebhookExtractorProps, extractorID) +} + + +func (d *SqlDb) UpdateWebhookExtractor(webhookExtractor db.WebhookExtractor) error { + err := webhookExtractor.Validate() + + if err != nil { + return err + } + + _, err = d.exec( + "update project__webhook_extractor set name=? where id=?", + webhookExtractor.Name, + webhookExtractor.ID) + + return err +} + +func (d *SqlDb) CreateWebhookExtractValue(value db.WebhookExtractValue) (newValue db.WebhookExtractValue, err error) { + err = value.Validate() + + if err != nil { + return + } + + insertID, err := d.insert("id", + "insert into project__webhook_extract_value (value_source, body_data_type, key, variable, name, extractor_id) values (?, ?, ?, ?, ?, ?)", + value.ValueSource, + value.BodyDataType, + value.Key, + value.Variable, + value.Name, + value.ExtractorID) + + if err != nil { + return + } + + newValue = value + newValue.ID = insertID + + return +} + +func (d *SqlDb) GetWebhookExtractValues(extractorID int, params db.RetrieveQueryParams) ([]db.WebhookExtractValue, error) { + var values []db.WebhookExtractValue + err := d.getObjectsByReferrer(extractorID, db.WebhookExtractorProps, db.WebhookExtractValueProps, params, &values) + return values, err +} + +func (d *SqlDb) GetAllWebhookExtractValues() (values []db.WebhookExtractValue, err error) { + var valueObjects interface{} + valueObjects, err = d.GetAllObjects(db.WebhookExtractValueProps) + values = valueObjects.([]db.WebhookExtractValue) + return +} + +func (d *SqlDb) GetWebhookExtractValue(valueID int, extractorID int) (value db.WebhookExtractValue, err error) { + query, args, err := squirrel.Select("v.*"). + From("project__webhook_extract_value as v"). + Where(squirrel.Eq{"id": valueID}). + OrderBy("v.id"). + ToSql() + + if err != nil { + return + } + + err = d.selectOne(&value, query, args...) + + return value, err +} + +func (d *SqlDb) GetWebhookExtractValueRefs(extractorID int, valueID int) (refs db.WebhookExtractorChildReferrers, err error) { + refs.WebhookExtractors, err = d.GetObjectReferences(db.WebhookExtractorProps, db.WebhookExtractValueProps, extractorID) + return +} + +func (d *SqlDb) DeleteWebhookExtractValue(extractorID int, valueID int) error { + return d.deleteObjectByReferencedID(extractorID, db.WebhookExtractorProps, db.WebhookExtractValueProps, valueID) +} + + +func (d *SqlDb) UpdateWebhookExtractValue(webhookExtractValue db.WebhookExtractValue) error { + err := webhookExtractValue.Validate() + + if err != nil { + return err + } + + _, err = d.exec( + "update project__webhook_extract_value set value_source=?, body_data_type=?, key=?, variable=?, name=? where id=?", + webhookExtractValue.ValueSource, + webhookExtractValue.BodyDataType, + webhookExtractValue.Key, + webhookExtractValue.Variable, + webhookExtractValue.Name, + webhookExtractValue.ID) + + return err +} + +func (d *SqlDb) CreateWebhookMatcher(matcher db.WebhookMatcher) (newMatcher db.WebhookMatcher, err error) { + err = matcher.Validate() + + if err != nil { + return + } + + insertID, err := d.insert( + "id", + "insert into project__webhook_matcher (match_type, method, body_data_type, key, value, extractor_id, name) values (?, ?, ?, ?, ?, ?, ?)", + matcher.MatchType, + matcher.Method, + matcher.BodyDataType, + matcher.Key, + matcher.Value, + matcher.ExtractorID, + matcher.Name) + + if err != nil { + return + } + + newMatcher = matcher + newMatcher.ID = insertID + + return +} + +func (d *SqlDb) GetWebhookMatchers(extractorID int, params db.RetrieveQueryParams) (matchers []db.WebhookMatcher, err error) { + query, args, err := squirrel.Select("m.*"). + From("project__webhook_matcher as m"). + Where(squirrel.Eq{"extractor_id": extractorID}). + OrderBy("m.id"). + ToSql() + + if err != nil { + return + } + + _, err = d.selectAll(&matchers, query, args...) + + return +} + +func (d *SqlDb) GetAllWebhookMatchers() (matchers []db.WebhookMatcher, err error) { + var matcherObjects interface{} + matcherObjects, err = d.GetAllObjects(db.WebhookMatcherProps) + matchers = matcherObjects.([]db.WebhookMatcher) + + return +} + +func (d *SqlDb) GetWebhookMatcher(matcherID int, extractorID int) (matcher db.WebhookMatcher, err error) { + query, args, err := squirrel.Select("m.*"). + From("project__webhook_matcher as m"). + Where(squirrel.Eq{"id": matcherID}). + OrderBy("m.id"). + ToSql() + + if err != nil { + return + } + + err = d.selectOne(&matcher, query, args...) + + return matcher, err +} + +func (d *SqlDb) GetWebhookMatcherRefs(extractorID int, matcherID int) (refs db.WebhookExtractorChildReferrers, err error) { + refs.WebhookExtractors, err = d.GetObjectReferences(db.WebhookExtractorProps, db.WebhookMatcherProps, matcherID) + + return +} + +func (d *SqlDb) DeleteWebhookMatcher(extractorID int, matcherID int) error { + return d.deleteObjectByReferencedID(extractorID, db.WebhookExtractorProps, db.WebhookMatcherProps, matcherID) +} + + +func (d *SqlDb) UpdateWebhookMatcher(webhookMatcher db.WebhookMatcher) error { + err := webhookMatcher.Validate() + + if err != nil { + return err + } + + _, err = d.exec( + "update project__webhook_matcher set match_type=?, method=?, body_data_type=?, key=?, value=?, name=? where id=?", + webhookMatcher.MatchType, + webhookMatcher.Method, + webhookMatcher.BodyDataType, + webhookMatcher.Key, + webhookMatcher.Value, + webhookMatcher.Name, + webhookMatcher.ID) + + return err +} diff --git a/go.mod b/go.mod index 2399e391..86200b64 100644 --- a/go.mod +++ b/go.mod @@ -52,6 +52,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/sergi/go-diff v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/thedevsaddam/gojsonq/v2 v2.5.2 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect golang.org/x/net v0.9.0 // indirect golang.org/x/sys v0.7.0 // indirect diff --git a/go.sum b/go.sum index dbcf731d..e1a9ea7d 100644 --- a/go.sum +++ b/go.sum @@ -331,6 +331,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/thedevsaddam/gojsonq/v2 v2.5.2 h1:CoMVaYyKFsVj6TjU6APqAhAvC07hTI6IQen8PHzHYY0= +github.com/thedevsaddam/gojsonq/v2 v2.5.2/go.mod h1:bv6Xa7kWy82uT0LnXPE2SzGqTj33TAEeR560MdJkiXs= github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= diff --git a/web/src/App.vue b/web/src/App.vue index afd85392..e96a6a2c 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -250,6 +250,16 @@ + + + mdi-hook + + + + Webhooks + + + mdi-account-multiple diff --git a/web/src/components/WebhookExtractValueForm.vue b/web/src/components/WebhookExtractValueForm.vue new file mode 100644 index 00000000..d955a8e0 --- /dev/null +++ b/web/src/components/WebhookExtractValueForm.vue @@ -0,0 +1,127 @@ + + diff --git a/web/src/components/WebhookExtractorBase.js b/web/src/components/WebhookExtractorBase.js new file mode 100644 index 00000000..b767075f --- /dev/null +++ b/web/src/components/WebhookExtractorBase.js @@ -0,0 +1,5 @@ +export default { + props: { + extractorId: Number, + }, +}; diff --git a/web/src/components/WebhookExtractorChildValueFormBase.js b/web/src/components/WebhookExtractorChildValueFormBase.js new file mode 100644 index 00000000..fc22e9ce --- /dev/null +++ b/web/src/components/WebhookExtractorChildValueFormBase.js @@ -0,0 +1,60 @@ +import axios from 'axios'; +import { getErrorMessage } from '@/lib/error'; + +export default { + props: { + webhookId: [Number, String], + extractorId: [Number, String], + }, + methods: { + /** + * Saves or creates item via API. + * @returns {Promise} null if validation didn't pass or user data if user saved. + */ + async save() { + this.formError = null; + + if (!this.$refs.form.validate()) { + this.$emit('error', {}); + return null; + } + + this.formSaving = true; + let item; + + try { + await this.beforeSave(); + + item = (await axios({ + method: this.isNew ? 'post' : 'put', + url: this.isNew + ? this.getItemsUrl() + : this.getSingleItemUrl(), + responseType: 'json', + data: { + ...this.item, + webhook_id: this.webhookId, + extractor_id: this.extractorId, + }, + ...(this.getRequestOptions()), + })).data; + + await this.afterSave(item); + + this.$emit('save', { + item: item || this.item, + action: this.isNew ? 'new' : 'edit', + }); + } catch (err) { + this.formError = getErrorMessage(err); + this.$emit('error', { + message: this.formError, + }); + } finally { + this.formSaving = false; + } + + return item || this.item; + }, + }, +}; diff --git a/web/src/components/WebhookExtractorForm.vue b/web/src/components/WebhookExtractorForm.vue new file mode 100644 index 00000000..b9b2db3b --- /dev/null +++ b/web/src/components/WebhookExtractorForm.vue @@ -0,0 +1,43 @@ + + diff --git a/web/src/components/WebhookExtractorFormBase.js b/web/src/components/WebhookExtractorFormBase.js new file mode 100644 index 00000000..d38dba81 --- /dev/null +++ b/web/src/components/WebhookExtractorFormBase.js @@ -0,0 +1,59 @@ +import axios from 'axios'; +import { getErrorMessage } from '@/lib/error'; + +export default { + props: { + webhookId: [Number, String], + }, + methods: { + /** + * Saves or creates item via API. + * @returns {Promise} null if validation didn't pass or user data if user saved. + */ + async save() { + this.formError = null; + + if (!this.$refs.form.validate()) { + this.$emit('error', {}); + return null; + } + + this.formSaving = true; + let item; + + try { + await this.beforeSave(); + + item = (await axios({ + method: this.isNew ? 'post' : 'put', + url: this.isNew + ? this.getItemsUrl() + : this.getSingleItemUrl(), + responseType: 'json', + data: { + ...this.item, + project_id: this.$route.params.projectId, + webhook_id: this.webhookId, + }, + ...(this.getRequestOptions()), + })).data; + + await this.afterSave(item); + + this.$emit('save', { + item: item || this.item, + action: this.isNew ? 'new' : 'edit', + }); + } catch (err) { + this.formError = getErrorMessage(err); + this.$emit('error', { + message: this.formError, + }); + } finally { + this.formSaving = false; + } + + return item || this.item; + }, + }, +}; diff --git a/web/src/components/WebhookExtractorRefsView.vue b/web/src/components/WebhookExtractorRefsView.vue new file mode 100644 index 00000000..8a605038 --- /dev/null +++ b/web/src/components/WebhookExtractorRefsView.vue @@ -0,0 +1,26 @@ + diff --git a/web/src/components/WebhookExtractorsBase.js b/web/src/components/WebhookExtractorsBase.js new file mode 100644 index 00000000..1b9baf55 --- /dev/null +++ b/web/src/components/WebhookExtractorsBase.js @@ -0,0 +1,6 @@ +export default { + props: { + webhookId: Number, + projectId: Number, + }, +}; diff --git a/web/src/components/WebhookForm.vue b/web/src/components/WebhookForm.vue new file mode 100644 index 00000000..f6a81d0f --- /dev/null +++ b/web/src/components/WebhookForm.vue @@ -0,0 +1,64 @@ + + diff --git a/web/src/components/WebhookMatcherForm.vue b/web/src/components/WebhookMatcherForm.vue new file mode 100644 index 00000000..d3d1874e --- /dev/null +++ b/web/src/components/WebhookMatcherForm.vue @@ -0,0 +1,148 @@ + + diff --git a/web/src/components/WebhookRefsView.vue b/web/src/components/WebhookRefsView.vue new file mode 100644 index 00000000..47648e29 --- /dev/null +++ b/web/src/components/WebhookRefsView.vue @@ -0,0 +1,26 @@ + diff --git a/web/src/lib/constants.js b/web/src/lib/constants.js index 6cf9ed82..f564fae8 100644 --- a/web/src/lib/constants.js +++ b/web/src/lib/constants.js @@ -36,3 +36,39 @@ export const USER_ROLES = [{ slug: 'guest', title: 'Guest', }]; + +export const MATCHER_TYPE_TITLES = { + '': 'Matcher', + body: 'Body', + header: 'Header', +}; + +export const MATCHER_TYPE_ICONS = { + '': 'Matcher', + body: 'mdi-page-layout-body', + header: 'mdi-web', +}; + +export const EXTRACT_VALUE_TYPE_TITLES = { + '': 'ExtractValue', + body: 'Body', + header: 'Header', +}; + +export const EXTRACT_VALUE_TYPE_ICONS = { + '': 'ExtractValue', + body: 'mdi-page-layout-body', + header: 'mdi-web', +}; + +export const EXTRACT_VALUE_BODY_DATA_TYPE_TITLES = { + '': 'BodyDataType', + json: 'JSON', + str: 'String', +}; + +export const EXTRACT_VALUE_BODY_DATA_TYPE_ICONS = { + '': 'BodyDataType', + json: 'mdi-code-json', + str: 'mdi-text', +}; diff --git a/web/src/router/index.js b/web/src/router/index.js index 95e31f1b..fb2115ae 100644 --- a/web/src/router/index.js +++ b/web/src/router/index.js @@ -13,6 +13,9 @@ import Team from '../views/project/Team.vue'; import Users from '../views/Users.vue'; import Auth from '../views/Auth.vue'; import New from '../views/project/New.vue'; +import Webhooks from '../views/project/Webhooks.vue'; +import WebhookExtractors from '../views/project/WebhookExtractors.vue'; +import WebhookExtractor from '../views/project/WebhookExtractor.vue'; Vue.use(VueRouter); @@ -65,6 +68,18 @@ const routes = [ path: '/project/:projectId/inventory', component: Inventory, }, + { + path: '/project/:projectId/webhooks', + component: Webhooks, + }, + { + path: '/project/:projectId/webhook/:webhookId', + component: WebhookExtractors, + }, + { + path: '/project/:projectId/webhook/:webhookId/extractor/:extractorId', + component: WebhookExtractor, + }, { path: '/project/:projectId/repositories', component: Repositories, diff --git a/web/src/views/project/WebhookExtractValue.vue b/web/src/views/project/WebhookExtractValue.vue new file mode 100644 index 00000000..5bafdfbd --- /dev/null +++ b/web/src/views/project/WebhookExtractValue.vue @@ -0,0 +1,174 @@ + + diff --git a/web/src/views/project/WebhookExtractor.vue b/web/src/views/project/WebhookExtractor.vue new file mode 100644 index 00000000..91877b0a --- /dev/null +++ b/web/src/views/project/WebhookExtractor.vue @@ -0,0 +1,36 @@ + + diff --git a/web/src/views/project/WebhookExtractors.vue b/web/src/views/project/WebhookExtractors.vue new file mode 100644 index 00000000..48373167 --- /dev/null +++ b/web/src/views/project/WebhookExtractors.vue @@ -0,0 +1,143 @@ + + diff --git a/web/src/views/project/WebhookMatcher.vue b/web/src/views/project/WebhookMatcher.vue new file mode 100644 index 00000000..59ef495f --- /dev/null +++ b/web/src/views/project/WebhookMatcher.vue @@ -0,0 +1,180 @@ + + diff --git a/web/src/views/project/Webhooks.vue b/web/src/views/project/Webhooks.vue new file mode 100644 index 00000000..fd130788 --- /dev/null +++ b/web/src/views/project/Webhooks.vue @@ -0,0 +1,140 @@ + + From 0443699c96dc5d7ad59a48404ba363728ccea503 Mon Sep 17 00:00:00 2001 From: Andreas Marschke Date: Sat, 2 Dec 2023 16:41:09 +0100 Subject: [PATCH 182/346] Include slices --- go.mod | 1 + 1 file changed, 1 insertion(+) diff --git a/go.mod b/go.mod index 86200b64..28fbd6c6 100644 --- a/go.mod +++ b/go.mod @@ -54,6 +54,7 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/thedevsaddam/gojsonq/v2 v2.5.2 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect + golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect golang.org/x/net v0.9.0 // indirect golang.org/x/sys v0.7.0 // indirect golang.org/x/term v0.7.0 // indirect From 7fd87d737be687584f96a7180036bbf407e9409d Mon Sep 17 00:00:00 2001 From: Andreas Marschke Date: Sat, 2 Dec 2023 16:47:36 +0100 Subject: [PATCH 183/346] go.sum addition --- go.sum | 2 ++ 1 file changed, 2 insertions(+) diff --git a/go.sum b/go.sum index e1a9ea7d..147b5e63 100644 --- a/go.sum +++ b/go.sum @@ -385,6 +385,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= +golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= From 8d0e39b0658df92e9f1873286842316b7fe1c1f7 Mon Sep 17 00:00:00 2001 From: Andreas Marschke Date: Sat, 2 Dec 2023 18:49:53 +0100 Subject: [PATCH 184/346] Fix method calls, add is null for globals --- api/router.go | 1 - db/sql/SqlDb.go | 47 +++++++++----------------------------------- db/sql/SqlDb_test.go | 25 ----------------------- 3 files changed, 9 insertions(+), 64 deletions(-) diff --git a/api/router.go b/api/router.go index 165cf756..88e592b1 100644 --- a/api/router.go +++ b/api/router.go @@ -187,7 +187,6 @@ func Route() *mux.Router { projectUserAPI.Path("/webhooks").HandlerFunc(projects.AddWebhook).Methods("POST") projectWebhooksAPI := projectUserAPI.PathPrefix("/webhook").Subrouter() - projectWebhooksAPI.Use(projects.WebhookMiddleware, projects.MustBeAdmin) projectWebhooksAPI.Path("/{webhook_id}").Methods("GET", "HEAD").HandlerFunc(projects.GetWebhook) projectWebhooksAPI.Path("/{webhook_id}").Methods("PUT").HandlerFunc(projects.UpdateWebhook) diff --git a/db/sql/SqlDb.go b/db/sql/SqlDb.go index 896e7c86..51240d1d 100644 --- a/db/sql/SqlDb.go +++ b/db/sql/SqlDb.go @@ -1,3 +1,4 @@ + package sql import ( @@ -207,8 +208,13 @@ func (d *SqlDb) getObjectsByReferrer(referrerID int, referringObjectProps db.Obj var referringColumn = referringObjectProps.ReferringColumnSuffix q := squirrel.Select("*"). - From(props.TableName+" pe"). - Where("pe." + referringColumn + "=?", referrerID) + From(props.TableName+" pe") + + if props.IsGlobal { + q = q.Where("pe." + referringColumn + " is null") + } else { + q = q.Where("pe." + referringColumn + "=?", referrerID) + } orderDirection := "ASC" if params.SortInverted { @@ -235,45 +241,10 @@ func (d *SqlDb) getObjectsByReferrer(referrerID int, referringObjectProps db.Obj return } -func (d *SqlDb) getObjects(projectID int, props db.ObjectProps, params db.RetrieveQueryParams, objects interface{}) (err error) { - q := squirrel.Select("*"). - From(props.TableName + " pe") - - if props.IsGlobal { - q = q.Where("pe.project_id is null") - } else { - q = q.Where("pe.project_id=?", projectID) - } - - orderDirection := "ASC" - - if params.SortInverted { - orderDirection = "DESC" - } - - orderColumn := props.DefaultSortingColumn - if containsStr(props.SortableColumns, params.SortBy) { - orderColumn = params.SortBy - } - - if orderColumn != "" { - q = q.OrderBy("pe." + orderColumn + " " + orderDirection) - } - - query, args, err := q.ToSql() - - if err != nil { - return - } - - _, err = d.selectAll(objects, query, args...) - - return -} - func (d *SqlDb) getObject(projectID int, props db.ObjectProps, objectID int, objects interface{}) (err error) { return d.getObjectByReferrer(projectID, db.ProjectProps, props, objectID, objects) } + func (d *SqlDb) getObjects(projectID int, props db.ObjectProps, params db.RetrieveQueryParams, objects interface{}) (err error) { return d.getObjectsByReferrer(projectID, db.ProjectProps, props, params, objects); } diff --git a/db/sql/SqlDb_test.go b/db/sql/SqlDb_test.go index dfd2d381..7f1372f8 100644 --- a/db/sql/SqlDb_test.go +++ b/db/sql/SqlDb_test.go @@ -6,27 +6,6 @@ import ( "testing" ) -var pool *mockDriver; - -type sqlmock struct { - ordered bool - dsn string - opened int - drv *mockDriver - converter driver.ValueConverter - queryMatcher QueryMatcher - monitorPings bool - - expected []expectation -} - -func init() { - pool = &mockDriver{ - conns: make(map[string]*sqlmock), - } - SqlDb.sql.Register("sqlmock", pool) -} - func TestValidatePort(t *testing.T) { d := SqlDb{} q := d.prepareQueryWithDialect("select * from `test` where id = ?, email = ?", gorp.PostgresDialect{}) @@ -34,7 +13,3 @@ func TestValidatePort(t *testing.T) { t.Error("invalid postgres query") } } - -func TestGetAllObjects(t *testing.T) { - -} From 055ecb8a11bf47cc61d1661e66b907cc28f91dec Mon Sep 17 00:00:00 2001 From: Andreas Marschke Date: Wed, 3 Jan 2024 02:43:15 +0100 Subject: [PATCH 185/346] Remove dead code and unused dependency --- db/sql/SqlDb.go | 1 - db/sql/SqlDb_test.go | 1 - 2 files changed, 2 deletions(-) diff --git a/db/sql/SqlDb.go b/db/sql/SqlDb.go index 51240d1d..113554be 100644 --- a/db/sql/SqlDb.go +++ b/db/sql/SqlDb.go @@ -272,7 +272,6 @@ func (d *SqlDb) deleteObject(projectID int, props db.ObjectProps, objectID int) projectID, objectID)) } - return d.deleteByReferrer(projectID, db.ProjectProps, props, objectID) } func (d *SqlDb) deleteObjectByReferencedID(referencedID int, referencedProps db.ObjectProps, props db.ObjectProps, objectID int) error { diff --git a/db/sql/SqlDb_test.go b/db/sql/SqlDb_test.go index 7f1372f8..ee57d6b9 100644 --- a/db/sql/SqlDb_test.go +++ b/db/sql/SqlDb_test.go @@ -2,7 +2,6 @@ package sql import ( "github.com/go-gorp/gorp/v3" - "github.com/go-sql-driver/mysql" "testing" ) From 183a8536f1a0f74a58608408e299d6b4bd269255 Mon Sep 17 00:00:00 2001 From: Andreas Marschke Date: Thu, 4 Jan 2024 15:18:59 +0100 Subject: [PATCH 186/346] Fix param reception --- api/projects/webhook.go | 11 +++---- api/projects/webhookextractor.go | 31 +++++++++++-------- api/projects/webhookextractvalue.go | 14 ++++----- api/projects/webhookmatcher.go | 15 +++++---- db/Store.go | 8 +++-- db/bolt/webhook.go | 6 ++-- db/sql/webhook.go | 2 +- web/src/views/project/WebhookExtractValue.vue | 12 +++---- web/src/views/project/WebhookExtractor.vue | 5 +++ web/src/views/project/WebhookExtractors.vue | 6 ++++ web/src/views/project/WebhookMatcher.vue | 18 +++++------ web/src/views/project/Webhooks.vue | 6 ++++ 12 files changed, 78 insertions(+), 56 deletions(-) diff --git a/api/projects/webhook.go b/api/projects/webhook.go index 638dbdaf..b1a8bc69 100644 --- a/api/projects/webhook.go +++ b/api/projects/webhook.go @@ -5,7 +5,7 @@ import ( "github.com/ansible-semaphore/semaphore/api/helpers" "github.com/ansible-semaphore/semaphore/db" "net/http" - + "fmt" "github.com/gorilla/context" ) @@ -19,8 +19,7 @@ func WebhookMiddleware(next http.Handler) http.Handler { }); } - project := context.Get(r, "project").(db.Project) - webhook, err := helpers.Store(r).GetWebhook(project.ID, webhook_id) + webhook, err := helpers.Store(r).GetWebhook(webhook_id) if err != nil { helpers.WriteError(w, err) @@ -173,10 +172,10 @@ func UpdateWebhook(w http.ResponseWriter, r *http.Request) { func DeleteWebhook(w http.ResponseWriter, r *http.Request) { - webhook := context.Get(r, "webhook").(db.Webhook) + webhook_id, err := helpers.GetIntParam("webhook_id", w, r) project := context.Get(r, "project").(db.Project) - err := helpers.Store(r).DeleteWebhook(project.ID, webhook.ID) + err = helpers.Store(r).DeleteWebhook(project.ID, webhook_id) if err == db.ErrInvalidOperation { helpers.WriteJSON(w, http.StatusBadRequest, map[string]interface{}{ "error": "Webhook failed to be deleted", @@ -185,7 +184,7 @@ func DeleteWebhook(w http.ResponseWriter, r *http.Request) { user := context.Get(r, "user").(*db.User) - desc := "Webhook " + webhook.Name + " deleted" + desc := fmt.Sprintf("Webhook %v deleted", webhook_id) _, err = helpers.Store(r).CreateEvent(db.Event{ UserID: &user.ID, diff --git a/api/projects/webhookextractor.go b/api/projects/webhookextractor.go index 0a0e4442..07a69a68 100644 --- a/api/projects/webhookextractor.go +++ b/api/projects/webhookextractor.go @@ -21,9 +21,10 @@ func WebhookExtractorMiddleware(next http.Handler) http.Handler { }) return } - webhook := context.Get(r, "webhook").(db.Webhook) + + webhook_id, err := helpers.GetIntParam("webhook_id", w, r) var extractor db.WebhookExtractor - extractor, err = helpers.Store(r).GetWebhookExtractor(extractor_id, webhook.ID) + extractor, err = helpers.Store(r).GetWebhookExtractor(extractor_id, webhook_id) if err != nil { helpers.WriteError(w, err) @@ -43,8 +44,8 @@ func GetWebhookExtractor(w http.ResponseWriter, r *http.Request) { func GetWebhookExtractors(w http.ResponseWriter, r *http.Request) { - webhook := context.Get(r, "webhook").(db.Webhook) - extractors, err := helpers.Store(r).GetWebhookExtractors(webhook.ID, helpers.QueryParams(r.URL)) + webhook_id, err := helpers.GetIntParam("webhook_id", w, r) + extractors, err := helpers.Store(r).GetWebhookExtractors(webhook_id, helpers.QueryParams(r.URL)) if err != nil { helpers.WriteError(w, err) @@ -55,14 +56,14 @@ func GetWebhookExtractors(w http.ResponseWriter, r *http.Request) { } func AddWebhookExtractor(w http.ResponseWriter, r *http.Request) { - webhook := context.Get(r, "webhook").(db.Webhook) + webhook_id, err := helpers.GetIntParam("webhook_id", w, r) var extractor db.WebhookExtractor if !helpers.Bind(w, r, &extractor) { return } - if extractor.WebhookID != webhook.ID { + if extractor.WebhookID != webhook_id { helpers.WriteJSON(w, http.StatusBadRequest, map[string]string { "error": "Webhook ID in body and URL must be the same", }) @@ -163,9 +164,10 @@ func GetWebhookExtractorRefs (w http.ResponseWriter, r *http.Request) { }) } - webhook := context.Get(r, "webhook").(db.Webhook) + webhook_id, err := helpers.GetIntParam("webhook_id", w, r) + var extractor db.WebhookExtractor - extractor, err = helpers.Store(r).GetWebhookExtractor(extractor_id, webhook.ID) + extractor, err = helpers.Store(r).GetWebhookExtractor(extractor_id, webhook_id) if err != nil { helpers.WriteError(w, err) @@ -183,23 +185,26 @@ func GetWebhookExtractorRefs (w http.ResponseWriter, r *http.Request) { func DeleteWebhookExtractor(w http.ResponseWriter, r *http.Request) { extractor_id, err := helpers.GetIntParam("extractor_id", w, r) - log.Info(fmt.Sprintf("Delete requested for: %v", extractor_id)) + webhook_id, err := helpers.GetIntParam("webhook_id", w, r) + + log.Info(fmt.Sprintf("Delete requested for: %v", extractor_id)) if err != nil { helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{ "error": "Invalid Extractor ID", }) } - webhook := context.Get(r, "webhook").(db.Webhook) var extractor db.WebhookExtractor - extractor, err = helpers.Store(r).GetWebhookExtractor(extractor_id, webhook.ID) + var webhook db.Webhook + extractor, err = helpers.Store(r).GetWebhookExtractor(extractor_id, webhook_id) + webhook, err = helpers.Store(r).GetWebhook(webhook_id) if err != nil { helpers.WriteError(w, err) return } - err = helpers.Store(r).DeleteWebhookExtractor(webhook.ID, extractor.ID) + err = helpers.Store(r).DeleteWebhookExtractor(webhook_id, extractor.ID) if err == db.ErrInvalidOperation { helpers.WriteJSON(w, http.StatusBadRequest, map[string]interface{}{ "error": "Webhook Extractor failed to be deleted", @@ -213,7 +218,7 @@ func DeleteWebhookExtractor(w http.ResponseWriter, r *http.Request) { _, err = helpers.Store(r).CreateEvent(db.Event{ UserID: &user.ID, ProjectID: &webhook.ProjectID, - WebhookID: &webhook.ID, + WebhookID: &webhook_id, ObjectType: &objType, Description: &desc, }) diff --git a/api/projects/webhookextractvalue.go b/api/projects/webhookextractvalue.go index 1d696b49..61b66b97 100644 --- a/api/projects/webhookextractvalue.go +++ b/api/projects/webhookextractvalue.go @@ -39,7 +39,7 @@ func GetWebhookExtractValues(w http.ResponseWriter, r *http.Request) { func AddWebhookExtractValue(w http.ResponseWriter, r *http.Request) { extractor := context.Get(r, "extractor").(db.WebhookExtractor) project := context.Get(r, "project").(db.Project) - webhook := context.Get(r, "webhook").(db.Webhook) + webhook_id, err := helpers.GetIntParam("webhook_id", w, r) var value db.WebhookExtractValue @@ -75,7 +75,7 @@ func AddWebhookExtractValue(w http.ResponseWriter, r *http.Request) { _, err = helpers.Store(r).CreateEvent(db.Event{ UserID: &user.ID, ProjectID: &project.ID, - WebhookID: &webhook.ID, + WebhookID: &webhook_id, ExtractorID: &extractor.ID, ObjectType: &objType, ObjectID: &value.ID, @@ -110,7 +110,7 @@ func UpdateWebhookExtractValue(w http.ResponseWriter, r *http.Request) { } user := context.Get(r, "user").(*db.User) - webhook := context.Get(r, "webhook").(db.Webhook) + webhook_id, err := helpers.GetIntParam("webhook_id", w, r) desc := "WebhookExtractValue (" + value.String() + ") updated" @@ -118,8 +118,8 @@ func UpdateWebhookExtractValue(w http.ResponseWriter, r *http.Request) { _, err = helpers.Store(r).CreateEvent(db.Event{ UserID: &user.ID, - ProjectID: &webhook.ProjectID, - WebhookID: &webhook.ID, + + WebhookID: &webhook_id, ExtractorID: &extractor.ID, Description: &desc, ObjectID: &value.ID, @@ -175,13 +175,13 @@ func DeleteWebhookExtractValue(w http.ResponseWriter, r *http.Request) { user := context.Get(r, "user").(*db.User) project := context.Get(r, "project").(db.Project) - webhook := context.Get(r, "webhook").(db.Webhook) + webhook_id, err := helpers.GetIntParam("webhook_id", w, r) desc := "Webhook Extract Value (" + value.String() + ") deleted" _, err = helpers.Store(r).CreateEvent(db.Event{ UserID: &user.ID, ProjectID: &project.ID, - WebhookID: &webhook.ID, + WebhookID: &webhook_id, ExtractorID: &extractor.ID, Description: &desc, }) diff --git a/api/projects/webhookmatcher.go b/api/projects/webhookmatcher.go index 497368e7..f9619cff 100644 --- a/api/projects/webhookmatcher.go +++ b/api/projects/webhookmatcher.go @@ -93,7 +93,7 @@ func AddWebhookMatcher(w http.ResponseWriter, r *http.Request) { } user := context.Get(r, "user").(*db.User) - webhook := context.Get(r, "webhook").(db.Webhook) + webhook_id, err := helpers.GetIntParam("webhook_id", w, r) project := context.Get(r, "project").(db.Project) objType := db.EventWebhookMatcher @@ -102,7 +102,7 @@ func AddWebhookMatcher(w http.ResponseWriter, r *http.Request) { _, err = helpers.Store(r).CreateEvent(db.Event{ UserID: &user.ID, ProjectID: &project.ID, - WebhookID: &webhook.ID, + WebhookID: &webhook_id, ExtractorID: &extractor.ID, ObjectType: &objType, ObjectID: &newMatcher.ID, @@ -139,7 +139,7 @@ func UpdateWebhookMatcher(w http.ResponseWriter, r *http.Request) { } user := context.Get(r, "user").(*db.User) - webhook := context.Get(r, "webhook").(db.Webhook) + webhook_id, err := helpers.GetIntParam("webhook_id", w, r) desc := "WebhookMatcher (" + matcher.String() + ") updated" @@ -147,8 +147,7 @@ func UpdateWebhookMatcher(w http.ResponseWriter, r *http.Request) { _, err = helpers.Store(r).CreateEvent(db.Event{ UserID: &user.ID, - ProjectID: &webhook.ProjectID, - WebhookID: &webhook.ID, + WebhookID: &webhook_id, ExtractorID: &extractor.ID, Description: &desc, ObjectID: &matcher.ID, @@ -185,13 +184,13 @@ func DeleteWebhookMatcher(w http.ResponseWriter, r *http.Request) { user := context.Get(r, "user").(*db.User) project := context.Get(r, "project").(db.Project) - webhook := context.Get(r, "webhook").(db.Webhook) - + webhook_id, err := helpers.GetIntParam("webhook_id", w, r) + desc := "Webhook Matcher (" + matcher.String() + ") deleted" _, err = helpers.Store(r).CreateEvent(db.Event{ UserID: &user.ID, ProjectID: &project.ID, - WebhookID: &webhook.ID, + WebhookID: &webhook_id, ExtractorID: &extractor.ID, Description: &desc, }) diff --git a/db/Store.go b/db/Store.go index 889964bf..9bb301b5 100644 --- a/db/Store.go +++ b/db/Store.go @@ -141,7 +141,7 @@ type Store interface { CreateWebhook(webhook Webhook) (newWebhook Webhook, err error) GetWebhooks(projectID int, params RetrieveQueryParams) ([]Webhook, error) - GetWebhook(webhookID int, projectID int) (webhook Webhook, err error) + GetWebhook(webhookID int) (webhook Webhook, err error) UpdateWebhook(webhook Webhook) error GetWebhookRefs(projectID int, webhookID int) (WebhookReferrers, error) DeleteWebhook(projectID int, webhookID int) error @@ -273,6 +273,7 @@ var WebhookProps = ObjectProps{ TableName: "project__webhook", Type: reflect.TypeOf(Webhook{}), PrimaryColumnName: "id", + IsGlobal: true, ReferringColumnSuffix: "webhook_id", SortableColumns: []string{"name"}, DefaultSortingColumn: "name", @@ -281,7 +282,8 @@ var WebhookProps = ObjectProps{ var WebhookExtractorProps = ObjectProps{ TableName: "project__webhook_extractor", Type: reflect.TypeOf(WebhookExtractor{}), - PrimaryColumnName: "id", + PrimaryColumnName: "id", + IsGlobal: true, ReferringColumnSuffix: "extractor_id", SortableColumns: []string{"name"}, DefaultSortingColumn: "name", @@ -291,6 +293,7 @@ var WebhookExtractValueProps = ObjectProps{ TableName: "project__webhook_extract_value", Type: reflect.TypeOf(WebhookExtractValue{}), PrimaryColumnName: "id", + IsGlobal: true, ReferringColumnSuffix: "extract_value_id", SortableColumns: []string{"name"}, DefaultSortingColumn: "name", @@ -300,6 +303,7 @@ var WebhookMatcherProps = ObjectProps{ TableName: "project__webhook_matcher", Type: reflect.TypeOf(WebhookMatcher{}), PrimaryColumnName: "id", + IsGlobal: true, ReferringColumnSuffix: "matcher_id", SortableColumns: []string{"name"}, DefaultSortingColumn: "name", diff --git a/db/bolt/webhook.go b/db/bolt/webhook.go index 794473f3..080e3b47 100644 --- a/db/bolt/webhook.go +++ b/db/bolt/webhook.go @@ -14,7 +14,7 @@ func (d *BoltDb) CreateWebhook(webhook db.Webhook) (db.Webhook, error) { return db.Webhook{}, err } - newWebhook, err := d.createObject(webhook.ProjectID, db.WebhookProps, webhook) + newWebhook, err := d.createObject(0, db.WebhookProps, webhook) return newWebhook.(db.Webhook), err } @@ -23,8 +23,8 @@ func (d *BoltDb) GetWebhooks(projectID int, params db.RetrieveQueryParams) (webh return webhooks, err } -func (d *BoltDb) GetWebhook(projectID int, webhookID int) (webhook db.Webhook, err error) { - err = d.getObject(projectID, db.WebhookProps, intObjectID(webhookID), &webhook) +func (d *BoltDb) GetWebhook(webhookID int) (webhook db.Webhook, err error) { + err = d.getObject(0, db.WebhookProps, intObjectID(webhookID), &webhook) if err != nil { return } diff --git a/db/sql/webhook.go b/db/sql/webhook.go index a2b637d8..eeec0fb8 100644 --- a/db/sql/webhook.go +++ b/db/sql/webhook.go @@ -44,7 +44,7 @@ func (d *SqlDb) GetAllWebhooks() (webhooks []db.Webhook, err error) { return } -func (d *SqlDb) GetWebhook(projectID int, webhookID int) (webhook db.Webhook, err error) { +func (d *SqlDb) GetWebhook(webhookID int) (webhook db.Webhook, err error) { query, args, err := squirrel.Select("w.*"). From("project__webhook as w"). Where(squirrel.And{ diff --git a/web/src/views/project/WebhookExtractValue.vue b/web/src/views/project/WebhookExtractValue.vue index 5bafdfbd..47d3acc9 100644 --- a/web/src/views/project/WebhookExtractValue.vue +++ b/web/src/views/project/WebhookExtractValue.vue @@ -65,7 +65,6 @@ - diff --git a/web/src/views/project/WebhookExtractors.vue b/web/src/views/project/WebhookExtractors.vue index 48373167..b29912c6 100644 --- a/web/src/views/project/WebhookExtractors.vue +++ b/web/src/views/project/WebhookExtractors.vue @@ -93,6 +93,8 @@ diff --git a/web/src/views/project/WebhookExtractors.vue b/web/src/views/project/WebhookExtractors.vue index b29912c6..749ef7a7 100644 --- a/web/src/views/project/WebhookExtractors.vue +++ b/web/src/views/project/WebhookExtractors.vue @@ -1,5 +1,5 @@