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
+
{ "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') }} @@ -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 @@[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') }} @@ -10,7 +10,7 @@ diff --git a/web/src/components/ObjectRefsDialog.vue b/web/src/components/ObjectRefsDialog.vue index bb03cf25..6dc93b3a 100644 --- a/web/src/components/ObjectRefsDialog.vue +++ b/web/src/components/ObjectRefsDialog.vue @@ -4,7 +4,8 @@ max-width="400" >{{ TEMPLATE_TYPE_ICONS[templateType] }} mdi-chevron-right - +- diff --git a/web/src/components/ObjectRefsView.vue b/web/src/components/ObjectRefsView.vue index e37dc688..0121f5f3 100644 --- a/web/src/components/ObjectRefsView.vue +++ b/web/src/components/ObjectRefsView.vue @@ -3,7 +3,7 @@Can't delete the {{ objectTitle }} +{{ $t('cantDeleteThe', {objectTitle: objectTitle}) }} + Close + >{{ $t('close2') }} - The {{ objectTitle }} can't be deleted because it used by the resources below + {{ $t('theCantBeDeletedBecauseItUsedByTheResourcesBelow', {objectTitle: objectTitle}) }} - abs. path + {{ $t('absPath') }} @@ -84,10 +84,10 @@ -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" >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') }}@@ -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)' }"> - +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 @@ - + Add variable + + {{ $t('addVariable') }} - Columns
+{{ $t('columns') }}
@@ -53,8 +53,9 @@ v-model="editedEnvironment[v.name]" :required="v.required" :rules="[ - val => !v.required || !!val || v.title + ' is required', - val => !val || v.type !== 'int' || /^\d+$/.test(val) || v.title + ' must be integer', + val => !v.required || !!val || v.title + $t('isRequired'), + val => !val || v.type !== 'int' || /^\d+$/.test(val) || + v.title + ' ' + $t('mustBeInteger'), ]" /> @@ -62,21 +63,21 @@ - Debug+--vvvv
{{ $t('debug') }}--vvvv
- Dry Run+--check
{{ $t('dryRun') }}--check
@@ -84,14 +85,14 @@ @@ -103,7 +104,7 @@ text class="mb-2" > - Please allow overriding CLI argument in Task Template settings - Diff+--diff
{{ $t('diff') }}--diff
+ {{ $t('pleaseAllowOverridingCliArgumentInTaskTemplateSett') }}
diff --git a/web/src/components/TaskList.vue b/web/src/components/TaskList.vue index 5e69be13..4bff083d 100644 --- a/web/src/components/TaskList.vue +++ b/web/src/components/TaskList.vue @@ -2,14 +2,14 @@@@ -115,8 +114,8 @@diff --git a/web/src/components/TeamMemberForm.vue b/web/src/components/TeamMemberForm.vue index 5db39c39..ba382024 100644 --- a/web/src/components/TeamMemberForm.vue +++ b/web/src/components/TeamMemberForm.vue @@ -13,18 +13,18 @@{{ TEMPLATE_TYPE_ICONS[template.type] }} mdi-chevron-right - + @@ -97,37 +97,37 @@ export default { return { headers: [ { - text: 'Task ID', + text: this.$i18n.t('taskId'), value: 'id', sortable: false, }, { - text: 'Version', + text: this.$i18n.t('version'), value: 'version', sortable: false, }, { - text: 'Status', + text: this.$i18n.t('status'), value: 'status', sortable: false, }, { - text: 'User', + text: this.$i18n.t('user'), value: 'user_name', sortable: false, }, { - text: 'Start', + text: this.$i18n.t('start'), value: 'start', sortable: false, }, { - text: 'Duration', + text: this.$i18n.t('duration'), value: 'end', sortable: false, }, { - text: 'Actions', + text: this.$i18n.t('actions'), value: 'actions', sortable: false, width: '0%', @@ -158,7 +158,7 @@ export default { })).data; }, getActionButtonTitle() { - return TEMPLATE_TYPE_ACTION_TITLES[this.template.type]; + return this.$i18n.t(TEMPLATE_TYPE_ACTION_TITLES[this.template.type]); }, onTaskCreated(e) { diff --git a/web/src/components/TaskLogView.vue b/web/src/components/TaskLogView.vue index b231e597..a4f7414e 100644 --- a/web/src/components/TaskLogView.vue +++ b/web/src/components/TaskLogView.vue @@ -24,7 +24,7 @@@@ -34,7 +34,7 @@ - Author +{{ $t('author') }} {{ user.name }} - Started +{{ $t('started') }} {{ item.start | formatDate }} @@ -45,7 +45,7 @@- Duration +{{ $t('duration') }} {{ [item.start, item.end] | formatMilliseconds }} @@ -70,7 +70,7 @@ v-if="item.status === 'running' || item.status === 'waiting'" @click="stopTask()" > - Stop + {{ $t('stop') }}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') }}.
-@@ -72,7 +71,7 @@ :key="key" >Defines autorun schedule.
+{{ $t('definesAutorunSchedule') }}
- For more information about cron, see the + {{ $t('forMoreInformationAboutCronSeeThe') }} Cron expression format reference. + >{{ $t('cronExpressionFormatReference') }}.
{{ TEMPLATE_TYPE_ICONS[key] }} - {{ TEMPLATE_TYPE_TITLES[key] }} + {{ $t(TEMPLATE_TYPE_TITLES[key]) }} @@ -80,11 +79,11 @@@@ -92,11 +91,11 @@ - 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 @@ @@ -74,7 +75,7 @@ v-model="signInFormValid" style="width: 300px; height: 300px;" > - - 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') }} 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 @@ @@ -19,8 +19,8 @@ @@ -33,12 +33,12 @@ > mdi-arrow-left -Users +{{ $t('users') }} New User + >{{ $t('newUser') }}@@ -35,19 +35,19 @@ export default { getHeaders() { return [ { - text: 'Time', + text: this.$i18n.t('time'), value: 'created', sortable: false, width: '20%', }, { - text: 'User', + text: this.$i18n.t('user'), value: 'username', sortable: false, width: '10%', }, { - text: 'Description', + text: this.$i18n.t('description'), value: 'description', sortable: false, width: '70%', diff --git a/web/src/views/project/Environment.vue b/web/src/views/project/Environment.vue index 4f84bfc8..25590af8 100644 --- a/web/src/views/project/Environment.vue +++ b/web/src/views/project/Environment.vue @@ -2,8 +2,8 @@ - Dashboard +{{ $t('dashboard') }} - History -Activity -Settings +{{ $t('history') }} +{{ $t('activity') }} +{{ $t('settings') }} @@ -27,20 +27,20 @@ /> @@ -84,12 +84,12 @@ export default { methods: { getHeaders() { return [{ - text: 'Name', + text: this.$i18n.t('name'), value: 'name', width: '100%', }, { - text: 'Actions', + text: this.$i18n.t('actions'), value: 'actions', sortable: false, }]; diff --git a/web/src/views/project/History.vue b/web/src/views/project/History.vue index 56ebe097..177338bd 100644 --- a/web/src/views/project/History.vue +++ b/web/src/views/project/History.vue @@ -3,7 +3,7 @@ - Environment +{{ $t('environment2') }} New Environment + >{{ $t('newEnvironment') }} - Dashboard + {{ $t('dashboard2') }} - - +[![Twitter](https://img.shields.io/twitter/follow/semaphoreui?style=social&logo=twitter)](https://twitter.com/semaphoreui) + +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/fiftin) Ansible Semaphore is a modern UI for Ansible. It lets you easily run Ansible playbooks, get notifications about fails, control access to deployment system. @@ -16,23 +13,6 @@ If your project has grown and deploying from the terminal is no longer for you t ![responsive-ui-phone1](https://user-images.githubusercontent.com/914224/134777345-8789d9e4-ff0d-439c-b80e-ddc56b74fcee.png) - - - - - ## Installation ### Full documentation @@ -48,6 +28,8 @@ sudo semaphore user add --admin --name "Your Name" --login your_login --email yo ### Docker +https://hub.docker.com/r/semaphoreui/semaphore + `docker-compose.yml` for minimal configuration: ```yaml @@ -66,7 +48,6 @@ services: - /path/to/data/home:/etc/semaphore # config.json location - /path/to/data/lib:/var/lib/semaphore # database.boltdb location (Not required if using mysql or postgres) ``` -https://hub.docker.com/r/semaphoreui/semaphore ## Demo @@ -93,9 +74,6 @@ All releases after 2.5.1 are signed with the gpg public key If you like Ansible Semaphore, you can support the project development on [Ko-fi](https://ko-fi.com/fiftin). -[](https://ko-fi.com/fiftin) - - ## License MIT License From 4fef07bd3d21f96aa4b34b18a4dd599ae3f103f3 Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Sat, 8 Jul 2023 20:24:35 +0200 Subject: [PATCH 026/346] feat: update go to 1.19 --- .github/workflows/dev.yml | 11 ++++++----- .github/workflows/release.yml | 4 ++-- .github/workflows/test.yml | 2 +- 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 +- go.mod | 2 +- 9 files changed, 15 insertions(+), 14 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 413fc0f9..c6894721 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -3,13 +3,14 @@ on: push: branches: - develop + - go19 jobs: build-local: runs-on: [ubuntu-latest] steps: - uses: actions/setup-go@v3 - with: { go-version: 1.18 } + with: { go-version: 1.19 } - uses: actions/setup-node@v3 with: { node-version: '16' } @@ -41,7 +42,7 @@ jobs: # needs: build-local # steps: # - uses: actions/setup-go@v3 -# with: { go-version: 1.18 } +# with: { go-version: 1.19 } # # - run: go install github.com/go-task/task/v3/cmd/task@latest # @@ -97,7 +98,7 @@ jobs: needs: [test-db-migration] steps: - uses: actions/setup-go@v3 - with: { go-version: 1.18 } + with: { go-version: 1.19 } - run: go install github.com/go-task/task/v3/cmd/task@latest @@ -112,7 +113,7 @@ jobs: needs: [test-integration] steps: - uses: actions/setup-go@v3 - with: { go-version: 1.18 } + with: { go-version: 1.19 } - run: go install github.com/go-task/task/v3/cmd/task@latest @@ -146,7 +147,7 @@ jobs: # runs-on: [ubuntu-latest] # steps: # - uses: actions/setup-go@v3 - # with: { go-version: 1.18 } + # with: { go-version: 1.19 } # - 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 99ebf7e8..16f43ffb 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.18 } + with: { go-version: 1.19 } - 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.18 } + with: { go-version: 1.19 } - run: go install github.com/go-task/task/v3/cmd/task@latest diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7d5e734d..2bde4f5a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: runs-on: [ubuntu-latest] steps: - uses: actions/setup-go@v3 - with: { go-version: 1.18 } + with: { go-version: 1.19 } - 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 c70ff0a1..103db9ee 100644 --- a/deployment/docker/ci/Dockerfile +++ b/deployment/docker/ci/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.18.3-alpine3.16 +FROM golang:1.19-alpine3.16 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 28a3d196..125496e2 100644 --- a/deployment/docker/ci/dredd.Dockerfile +++ b/deployment/docker/ci/dredd.Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.18.3-alpine3.16 as golang +FROM golang:1.19-alpine3.16 as golang RUN apk add --no-cache curl git diff --git a/deployment/docker/dev/Dockerfile b/deployment/docker/dev/Dockerfile index 86ac6e37..4446fec0 100644 --- a/deployment/docker/dev/Dockerfile +++ b/deployment/docker/dev/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.18.3-alpine3.16 +FROM golang:1.19-alpine3.16 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 ca1e4afb..086d8362 100644 --- a/deployment/docker/prod/Dockerfile +++ b/deployment/docker/prod/Dockerfile @@ -1,5 +1,5 @@ # ansible-semaphore production image -FROM golang:1.18.3-alpine3.16 as builder +FROM golang:1.19-alpine3.16 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 a1e19f84..11ec28db 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.18.3-alpine3.16 as builder +FROM --platform=$BUILDPLATFORM golang:1.19-alpine3.16 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 98265632..82af413a 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/ansible-semaphore/semaphore -go 1.18 +go 1.19 require ( github.com/Sirupsen/logrus v1.0.4 From 87d983556f6b50ec3c4e6f06c81bdca4a7f51bcf Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Sat, 8 Jul 2023 23:35:39 +0200 Subject: [PATCH 027/346] refactor(be): create middleware to check permissions --- api/projects/project.go | 48 +++++++++++++++++++++++------------------ api/router.go | 26 +++++++++++++++++----- db/ProjectUser.go | 13 ++++++----- 3 files changed, 56 insertions(+), 31 deletions(-) diff --git a/api/projects/project.go b/api/projects/project.go index e1d13b26..9a407550 100644 --- a/api/projects/project.go +++ b/api/projects/project.go @@ -3,6 +3,7 @@ package projects import ( "github.com/ansible-semaphore/semaphore/api/helpers" "github.com/ansible-semaphore/semaphore/db" + "github.com/gorilla/mux" "net/http" "github.com/gorilla/context" @@ -22,7 +23,7 @@ func ProjectMiddleware(next http.Handler) http.Handler { return } - // check if user it project's team + // check if user in project's team _, err = helpers.Store(r).GetProjectUser(projectID, user.ID) if err != nil { @@ -42,31 +43,36 @@ func ProjectMiddleware(next http.Handler) http.Handler { }) } -// MustBeAdmin ensures that the user has administrator rights -func MustBeAdmin(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - project := context.Get(r, "project").(db.Project) - user := context.Get(r, "user").(*db.User) +// GetMustCanMiddlewareFor ensures that the user has administrator rights +func GetMustCanMiddlewareFor(permissions db.ProjectUserPermission) mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + project := context.Get(r, "project").(db.Project) + user := context.Get(r, "user").(*db.User) - projectUser, err := helpers.Store(r).GetProjectUser(project.ID, user.ID) + if !user.Admin { + // check if user in project's team + projectUser, err := helpers.Store(r).GetProjectUser(project.ID, user.ID) - if err == db.ErrNotFound { - w.WriteHeader(http.StatusForbidden) - return - } + if err == db.ErrNotFound { + w.WriteHeader(http.StatusForbidden) + return + } - if err != nil { - helpers.WriteError(w, err) - return - } + if err != nil { + helpers.WriteError(w, err) + return + } - if projectUser.Role != db.ProjectOwner { - w.WriteHeader(http.StatusForbidden) - return - } + if r.Method != "GET" && r.Method != "HEAD" && !projectUser.Can(permissions) { + w.WriteHeader(http.StatusForbidden) + return + } + } - next.ServeHTTP(w, r) - }) + next.ServeHTTP(w, r) + }) + } } // GetProject returns a project details diff --git a/api/router.go b/api/router.go index c44e81c2..1948e8a0 100644 --- a/api/router.go +++ b/api/router.go @@ -123,8 +123,20 @@ func Route() *mux.Router { projectGet.Use(projects.ProjectMiddleware) projectGet.Methods("GET", "HEAD").HandlerFunc(projects.GetProject) + // + // Start and Stop tasks + projectTaskStart := authenticatedAPI.PathPrefix("/project/{project_id}").Subrouter() + projectTaskStart.Use(projects.ProjectMiddleware, projects.GetMustCanMiddlewareFor(db.CanRunProjectTasks)) + projectTaskStart.Path("/tasks").HandlerFunc(projects.AddTask).Methods("POST") + + projectTaskStop := authenticatedAPI.PathPrefix("/tasks").Subrouter() + projectTaskStop.Use(projects.ProjectMiddleware, projects.GetTaskMiddleware, projects.GetMustCanMiddlewareFor(db.CanRunProjectTasks)) + projectTaskStop.HandleFunc("/{task_id}/stop", projects.StopTask).Methods("POST") + + // + // Project resources CRUD projectUserAPI := authenticatedAPI.PathPrefix("/project/{project_id}").Subrouter() - projectUserAPI.Use(projects.ProjectMiddleware) + projectUserAPI.Use(projects.ProjectMiddleware, projects.GetMustCanMiddlewareFor(db.CanManageProjectResources)) projectUserAPI.Path("/events").HandlerFunc(getAllEvents).Methods("GET", "HEAD") projectUserAPI.HandleFunc("/events/last", getLastEvents).Methods("GET", "HEAD") @@ -145,7 +157,6 @@ func Route() *mux.Router { projectUserAPI.Path("/tasks").HandlerFunc(projects.GetAllTasks).Methods("GET", "HEAD") projectUserAPI.HandleFunc("/tasks/last", projects.GetLastTasks).Methods("GET", "HEAD") - projectUserAPI.Path("/tasks").HandlerFunc(projects.AddTask).Methods("POST") projectUserAPI.Path("/templates").HandlerFunc(projects.GetTemplates).Methods("GET", "HEAD") projectUserAPI.Path("/templates").HandlerFunc(projects.AddTemplate).Methods("POST") @@ -157,13 +168,17 @@ func Route() *mux.Router { projectUserAPI.Path("/views").HandlerFunc(projects.AddView).Methods("POST") projectUserAPI.Path("/views/positions").HandlerFunc(projects.SetViewPositions).Methods("POST") + // + // Updating and deleting project projectAdminAPI := authenticatedAPI.Path("/project/{project_id}").Subrouter() - projectAdminAPI.Use(projects.ProjectMiddleware, projects.MustBeAdmin) + projectAdminAPI.Use(projects.ProjectMiddleware, projects.GetMustCanMiddlewareFor(db.CanUpdateProject)) projectAdminAPI.Methods("PUT").HandlerFunc(projects.UpdateProject) projectAdminAPI.Methods("DELETE").HandlerFunc(projects.DeleteProject) + // + // Manage project users projectAdminUsersAPI := authenticatedAPI.PathPrefix("/project/{project_id}").Subrouter() - projectAdminUsersAPI.Use(projects.ProjectMiddleware, projects.MustBeAdmin) + projectAdminUsersAPI.Use(projects.ProjectMiddleware, projects.GetMustCanMiddlewareFor(db.CanManageProjectUsers)) projectAdminUsersAPI.Path("/users").HandlerFunc(projects.AddUser).Methods("POST") projectUserManagement := projectAdminUsersAPI.PathPrefix("/users").Subrouter() @@ -173,6 +188,8 @@ func Route() *mux.Router { projectUserManagement.HandleFunc("/{user_id}", projects.UpdateUser).Methods("PUT") projectUserManagement.HandleFunc("/{user_id}", projects.RemoveUser).Methods("DELETE") + // + // Project resources CRUD (continue) projectKeyManagement := projectUserAPI.PathPrefix("/keys").Subrouter() projectKeyManagement.Use(projects.KeyMiddleware) @@ -222,7 +239,6 @@ func Route() *mux.Router { projectTaskManagement.HandleFunc("/{task_id}/output", projects.GetTaskOutput).Methods("GET", "HEAD") projectTaskManagement.HandleFunc("/{task_id}", projects.GetTask).Methods("GET", "HEAD") projectTaskManagement.HandleFunc("/{task_id}", projects.RemoveTask).Methods("DELETE") - projectTaskManagement.HandleFunc("/{task_id}/stop", projects.StopTask).Methods("POST") projectScheduleManagement := projectUserAPI.PathPrefix("/schedules").Subrouter() projectScheduleManagement.Use(projects.SchedulesMiddleware) diff --git a/db/ProjectUser.go b/db/ProjectUser.go index d49acca2..81268f92 100644 --- a/db/ProjectUser.go +++ b/db/ProjectUser.go @@ -4,6 +4,7 @@ type ProjectUserRole string const ( ProjectOwner ProjectUserRole = "owner" + ProjectManager ProjectUserRole = "manager" ProjectTaskRunner ProjectUserRole = "task_runner" ProjectGuest ProjectUserRole = "guest" ) @@ -11,14 +12,16 @@ const ( type ProjectUserPermission int const ( - ProjectUserCanRunTask ProjectUserPermission = 1 << iota - ProjectCanEditProjectSettings - ProjectCanRunTasks + CanRunProjectTasks ProjectUserPermission = 1 << iota + CanUpdateProject + CanManageProjectResources + CanManageProjectUsers ) var rolePermissions = map[ProjectUserRole]ProjectUserPermission{ - ProjectOwner: ProjectUserCanRunTask | ProjectCanEditProjectSettings | ProjectCanRunTasks, - ProjectTaskRunner: ProjectCanRunTasks, + ProjectOwner: CanRunProjectTasks | CanUpdateProject | CanManageProjectResources, + ProjectManager: CanRunProjectTasks | CanManageProjectResources, + ProjectTaskRunner: CanRunProjectTasks, ProjectGuest: 0, } From 93e42b7023392d4acb59035d3981357135417a1d Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Sun, 9 Jul 2023 10:23:13 +0200 Subject: [PATCH 028/346] fix(roles): validate user role in project when add or update --- api-docs.yml | 1 + api/projects/users.go | 10 ++++++++++ db/ProjectUser.go | 5 +++++ 3 files changed, 16 insertions(+) diff --git a/api-docs.yml b/api-docs.yml index f074a396..9c42c63c 100644 --- a/api-docs.yml +++ b/api-docs.yml @@ -928,6 +928,7 @@ paths: minimum: 2 role: type: string + example: owner responses: 204: description: User added diff --git a/api/projects/users.go b/api/projects/users.go index 88159592..c185e604 100644 --- a/api/projects/users.go +++ b/api/projects/users.go @@ -70,6 +70,11 @@ func AddUser(w http.ResponseWriter, r *http.Request) { return } + if !projectUser.Role.IsValid() { + w.WriteHeader(http.StatusBadRequest) + return + } + _, err := helpers.Store(r).CreateProjectUser(db.ProjectUser{ ProjectID: project.ID, UserID: projectUser.UserID, @@ -143,6 +148,11 @@ func UpdateUser(w http.ResponseWriter, r *http.Request) { return } + if !projectUser.Role.IsValid() { + w.WriteHeader(http.StatusBadRequest) + return + } + err := helpers.Store(r).UpdateProjectUser(db.ProjectUser{ UserID: user.ID, ProjectID: project.ID, diff --git a/db/ProjectUser.go b/db/ProjectUser.go index 81268f92..6b561abe 100644 --- a/db/ProjectUser.go +++ b/db/ProjectUser.go @@ -25,6 +25,11 @@ var rolePermissions = map[ProjectUserRole]ProjectUserPermission{ ProjectGuest: 0, } +func (r ProjectUserRole) IsValid() bool { + _, ok := rolePermissions[r] + return ok +} + type ProjectUser struct { ID int `db:"id" json:"-"` ProjectID int `db:"project_id" json:"project_id"` From 0914aaa3326a06358a9a3e24ff686d6621c063da Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Sun, 9 Jul 2023 10:55:46 +0200 Subject: [PATCH 029/346] test(be): fix dredd test --- .dredd/hooks/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.dredd/hooks/main.go b/.dredd/hooks/main.go index ca48c1e1..ab9b5cb2 100644 --- a/.dredd/hooks/main.go +++ b/.dredd/hooks/main.go @@ -74,7 +74,7 @@ func main() { dbConnect() defer store.Close("") deleteUserProjectRelation(userProject.ID, userPathTestUser.ID) - transaction.Request.Body = "{ \"user_id\": " + strconv.Itoa(userPathTestUser.ID) + ",\"admin\": true}" + transaction.Request.Body = "{ \"user_id\": " + strconv.Itoa(userPathTestUser.ID) + ",\"role\": \"owner\"}" }) h.Before("project > /api/project/{project_id}/keys/{key_id} > Updates access key > 204 > application/json", capabilityWrapper("access_key")) From bfa9a3c00b2dcf39df71dd0bdd854536e643d75e Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Sun, 9 Jul 2023 11:35:52 +0200 Subject: [PATCH 030/346] fix(be): migration for bolt --- db/bolt/migration_2_8_91.go | 2 +- db/bolt/migration_2_8_91_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/db/bolt/migration_2_8_91.go b/db/bolt/migration_2_8_91.go index 259f172a..ef9be2e8 100644 --- a/db/bolt/migration_2_8_91.go +++ b/db/bolt/migration_2_8_91.go @@ -22,7 +22,7 @@ func (d migration_2_8_91) Apply() (err error) { for projectID, projectUsers := range usersByProjectMap { for userId, userData := range projectUsers { - if userData["admin"] == "true" { + if userData["admin"] == true { userData["role"] = "owner" } else { userData["role"] = "task_runner" diff --git a/db/bolt/migration_2_8_91_test.go b/db/bolt/migration_2_8_91_test.go index 730385db..8308a7a2 100644 --- a/db/bolt/migration_2_8_91_test.go +++ b/db/bolt/migration_2_8_91_test.go @@ -26,7 +26,7 @@ func TestMigration_2_8_91_Apply(t *testing.T) { } err = r.Put([]byte("0000000001"), - []byte("{\"id\":\"1\",\"project_id\":\"1\",\"admin\": \"true\"}")) + []byte("{\"id\":\"1\",\"project_id\":\"1\",\"admin\": true}")) return err }) From 0b81623b09a04cd9ede632614abfa11c9d2547dd Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Sat, 22 Jul 2023 22:47:12 +0200 Subject: [PATCH 031/346] fix(migrations): manager is default role --- db/sql/migrations/v2.8.91.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/sql/migrations/v2.8.91.sql b/db/sql/migrations/v2.8.91.sql index 95b0278e..f688bf60 100644 --- a/db/sql/migrations/v2.8.91.sql +++ b/db/sql/migrations/v2.8.91.sql @@ -1,4 +1,4 @@ -ALTER TABLE project__user ADD `role` varchar(50) NOT NULL DEFAULT 'task_runner'; +ALTER TABLE project__user ADD `role` varchar(50) NOT NULL DEFAULT 'manager'; UPDATE project__user SET `role` = 'owner' WHERE `admin`; From 517ad4dc97f72d99480555edcccdd91dfba3dfd6 Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Sat, 22 Jul 2023 22:48:10 +0200 Subject: [PATCH 032/346] feat(ui): add roles to UI --- db/bolt/migration_2_8_91.go | 2 +- web/src/views/project/Team.vue | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/db/bolt/migration_2_8_91.go b/db/bolt/migration_2_8_91.go index ef9be2e8..b477be44 100644 --- a/db/bolt/migration_2_8_91.go +++ b/db/bolt/migration_2_8_91.go @@ -25,7 +25,7 @@ func (d migration_2_8_91) Apply() (err error) { if userData["admin"] == true { userData["role"] = "owner" } else { - userData["role"] = "task_runner" + userData["role"] = "manager" } delete(userData, "admin") err = d.setObject(projectID, "user", userId, userData) diff --git a/web/src/views/project/Team.vue b/web/src/views/project/Team.vue index f43948ed..6aff2bf8 100644 --- a/web/src/views/project/Team.vue +++ b/web/src/views/project/Team.vue @@ -79,9 +79,15 @@ export default { roles: [{ slug: 'owner', title: 'Owner', + }, { + slug: 'manager', + title: 'Manger', }, { slug: 'task_runner', title: 'Task Runner', + }, { + slug: 'guest', + title: 'Guest', }], }; }, From adbbe87e74deadcf2fa8f9b4bc8b887ea0cb493a Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Sun, 23 Jul 2023 02:23:25 +0200 Subject: [PATCH 033/346] chore: public url --- cli/setup/setup.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/setup/setup.go b/cli/setup/setup.go index 959eb7b9..3245d8b2 100644 --- a/cli/setup/setup.go +++ b/cli/setup/setup.go @@ -50,7 +50,7 @@ func InteractiveSetup(conf *util.ConfigType) { askValue("Playbook path", defaultPlaybookPath, &conf.TmpPath) conf.TmpPath = filepath.Clean(conf.TmpPath) - askValue("Web root URL (optional, see https://github.com/ansible-semaphore/semaphore/wiki/Web-root-URL)", "", &conf.WebHost) + askValue("Public URL (optional, example: https://example.com/semaphore)", "", &conf.WebHost) askConfirmation("Enable email alerts?", false, &conf.EmailAlert) if conf.EmailAlert { From 034a4b4bbe70fb748d889fbcb305f2357689eb82 Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Sun, 23 Jul 2023 15:55:24 +0200 Subject: [PATCH 034/346] fix(ui): typo --- web/src/lang/en.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lang/en.js b/web/src/lang/en.js index e682df78..9af61562 100644 --- a/web/src/lang/en.js +++ b/web/src/lang/en.js @@ -53,7 +53,7 @@ export default { orCreateNewAdminUser: 'Or create new admin user:', close2: 'Close', semaphore: 'SEMAPHORE', - dontHaveAccountOrCantSignIn: 'Don\'\'t have account or can\'\'t sign in?', + dontHaveAccountOrCantSignIn: 'Don\'t have account or can\'t sign in?', password2: 'Password', cancel: 'Cancel', noViews: 'No views', From e2df7758a1d5dfec395dc33f20df622afb732500 Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Sun, 23 Jul 2023 16:18:02 +0200 Subject: [PATCH 035/346] refactor(be): config struct --- lib/GitClientFactory.go | 6 ++++-- util/config.go | 42 ++++++++++++++++++++++++----------------- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/lib/GitClientFactory.go b/lib/GitClientFactory.go index 0042a2d7..c7c98644 100644 --- a/lib/GitClientFactory.go +++ b/lib/GitClientFactory.go @@ -3,9 +3,11 @@ package lib import "github.com/ansible-semaphore/semaphore/util" func CreateDefaultGitClient() GitClient { - switch util.Config.GitClient { - case "go_git": + switch util.Config.GitClientId { + case util.GoGitClientId: return CreateGoGitClient() + case util.CmdGitClientId: + return CreateCmdGitClient() default: return CreateCmdGitClient() } diff --git a/util/config.go b/util/config.go index e3032164..03464832 100644 --- a/util/config.go +++ b/util/config.go @@ -71,6 +71,17 @@ type oidcProvider struct { EmailClaim string `json:"email_claim"` } +type GitClientId string + +const ( + // GoGitClientId is builtin Git client. It is not require external dependencies and is preferred. + // Use it if you don't need external SSH authorization. + GoGitClientId GitClientId = "go_git" + // CmdGitClientId is external Git client. + // Default Git client. It is use external Git binary to clone repositories. + CmdGitClientId GitClientId = "cmd_git" +) + // ConfigType mapping between Config and the json file that sets it type ConfigType struct { MySQL DbConfig `json:"mysql"` @@ -90,6 +101,10 @@ type ConfigType struct { // semaphore stores ephemeral projects here TmpPath string `json:"tmp_path"` + // SshConfigPath is a path to the custom SSH config file. + // Default path is ~/.ssh/config. + SshConfigPath string `json:"ssh_config_path"` + // cookie hashing & encryption CookieHash string `json:"cookie_hash"` CookieEncryption string `json:"cookie_encryption"` @@ -122,29 +137,22 @@ type ConfigType struct { // telegram alerting TelegramChat string `json:"telegram_chat"` TelegramToken string `json:"telegram_token"` - - // slack alerting - SlackUrl string `json:"slack_url"` + SlackUrl string `json:"slack_url"` // task concurrency MaxParallelTasks int `json:"max_parallel_tasks"` - // configType field ordering with bools at end reduces struct size - // (maligned check) - // feature switches - EmailAlert bool `json:"email_alert"` - EmailSecure bool `json:"email_secure"` - TelegramAlert bool `json:"telegram_alert"` - SlackAlert bool `json:"slack_alert"` - LdapEnable bool `json:"ldap_enable"` - LdapNeedTLS bool `json:"ldap_needtls"` + EmailAlert bool `json:"email_alert"` + EmailSecure bool `json:"email_secure"` + TelegramAlert bool `json:"telegram_alert"` + SlackAlert bool `json:"slack_alert"` + LdapEnable bool `json:"ldap_enable"` + LdapNeedTLS bool `json:"ldap_needtls"` + PasswordLoginDisabled bool `json:"password_login_disable"` + DemoMode bool `json:"demo_mode"` - SshConfigPath string `json:"ssh_config_path"` - - DemoMode bool `json:"demo_mode"` - - GitClient string `json:"git_client"` + GitClientId GitClientId `json:"git_client"` } // Config exposes the application configuration storage for use in the application From 9457bf1c028bc005e90907d5e01cb0cd32a547b9 Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Sun, 23 Jul 2023 16:26:36 +0200 Subject: [PATCH 036/346] chore: rename config param --- util/config.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/util/config.go b/util/config.go index 03464832..c70ec494 100644 --- a/util/config.go +++ b/util/config.go @@ -143,14 +143,14 @@ type ConfigType struct { MaxParallelTasks int `json:"max_parallel_tasks"` // feature switches - EmailAlert bool `json:"email_alert"` - EmailSecure bool `json:"email_secure"` - TelegramAlert bool `json:"telegram_alert"` - SlackAlert bool `json:"slack_alert"` - LdapEnable bool `json:"ldap_enable"` - LdapNeedTLS bool `json:"ldap_needtls"` - PasswordLoginDisabled bool `json:"password_login_disable"` - DemoMode bool `json:"demo_mode"` + EmailAlert bool `json:"email_alert"` + EmailSecure bool `json:"email_secure"` + TelegramAlert bool `json:"telegram_alert"` + SlackAlert bool `json:"slack_alert"` + LdapEnable bool `json:"ldap_enable"` + LdapNeedTLS bool `json:"ldap_needtls"` + PasswordLoginDisable bool `json:"password_login_disable"` + DemoMode bool `json:"demo_mode"` GitClientId GitClientId `json:"git_client"` } From 4380a9ab311f42f8aec8f7b20aa184f16cf59d10 Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Sun, 23 Jul 2023 23:34:40 +0200 Subject: [PATCH 037/346] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index f5454be8..58bda94f 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,8 @@ API description: https://ansible-semaphore.com/api-docs/ ## Contributing +If you want to write an article about Ansible or Semaphore, contact [@fiftin](https://github.com/fiftin) and we will place your article in our [Blog](https://www.ansible-semaphore.com/blog/) with link to your profile. + PR's & UX reviews are welcome! Please follow the [contribution](https://github.com/ansible-semaphore/semaphore/blob/develop/CONTRIBUTING.md) guide. Any questions, please open an issue. From 1145eec9a4c7b0f9099756f9d85c967c439b046a Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Mon, 24 Jul 2023 16:04:03 +0200 Subject: [PATCH 038/346] feat(be): add config options --- api/login.go | 6 +++-- api/projects/projects.go | 3 ++- util/config.go | 37 +++++++++++++++--------------- web/src/views/Auth.vue | 5 ++++ web/src/views/project/Settings.vue | 2 +- 5 files changed, 31 insertions(+), 22 deletions(-) diff --git a/api/login.go b/api/login.go index c90bcc17..1106494d 100644 --- a/api/login.go +++ b/api/login.go @@ -205,14 +205,16 @@ type loginMetadataOidcProvider struct { } type loginMetadata struct { - OidcProviders []loginMetadataOidcProvider `json:"oidc_providers"` + OidcProviders []loginMetadataOidcProvider `json:"oidc_providers"` + LoginWithPassword bool `json:"login_with_password"` } // nolint: gocyclo func login(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { config := &loginMetadata{ - OidcProviders: make([]loginMetadataOidcProvider, len(util.Config.OidcProviders)), + OidcProviders: make([]loginMetadataOidcProvider, len(util.Config.OidcProviders)), + LoginWithPassword: !util.Config.PasswordLoginDisable, } i := 0 for k, v := range util.Config.OidcProviders { diff --git a/api/projects/projects.go b/api/projects/projects.go index 7775a223..23ee7c1f 100644 --- a/api/projects/projects.go +++ b/api/projects/projects.go @@ -4,6 +4,7 @@ import ( log "github.com/Sirupsen/logrus" "github.com/ansible-semaphore/semaphore/api/helpers" "github.com/ansible-semaphore/semaphore/db" + "github.com/ansible-semaphore/semaphore/util" "net/http" "github.com/gorilla/context" @@ -29,7 +30,7 @@ func AddProject(w http.ResponseWriter, r *http.Request) { user := context.Get(r, "user").(*db.User) - if !user.Admin { + if !user.Admin && !util.Config.NonAdminCanCreateProject { log.Warn(user.Username + " is not permitted to edit users") w.WriteHeader(http.StatusUnauthorized) return diff --git a/util/config.go b/util/config.go index c70ec494..3946e2f4 100644 --- a/util/config.go +++ b/util/config.go @@ -105,6 +105,11 @@ type ConfigType struct { // Default path is ~/.ssh/config. SshConfigPath string `json:"ssh_config_path"` + GitClientId GitClientId `json:"git_client"` + + // web host + WebHost string `json:"web_host"` + // cookie hashing & encryption CookieHash string `json:"cookie_hash"` CookieEncryption string `json:"cookie_encryption"` @@ -114,45 +119,41 @@ type ConfigType struct { AccessKeyEncryption string `json:"access_key_encryption"` // email alerting + EmailAlert bool `json:"email_alert"` EmailSender string `json:"email_sender"` EmailHost string `json:"email_host"` EmailPort string `json:"email_port"` EmailUsername string `json:"email_username"` EmailPassword string `json:"email_password"` - - // web host - WebHost string `json:"web_host"` + EmailSecure bool `json:"email_secure"` // ldap settings + LdapEnable bool `json:"ldap_enable"` LdapBindDN string `json:"ldap_binddn"` LdapBindPassword string `json:"ldap_bindpassword"` LdapServer string `json:"ldap_server"` LdapSearchDN string `json:"ldap_searchdn"` LdapSearchFilter string `json:"ldap_searchfilter"` LdapMappings ldapMappings `json:"ldap_mappings"` + LdapNeedTLS bool `json:"ldap_needtls"` + + // telegram and slack alerting + TelegramAlert bool `json:"telegram_alert"` + TelegramChat string `json:"telegram_chat"` + TelegramToken string `json:"telegram_token"` + SlackAlert bool `json:"slack_alert"` + SlackUrl string `json:"slack_url"` // oidc settings OidcProviders map[string]oidcProvider `json:"oidc_providers"` - // telegram alerting - TelegramChat string `json:"telegram_chat"` - TelegramToken string `json:"telegram_token"` - SlackUrl string `json:"slack_url"` - // task concurrency MaxParallelTasks int `json:"max_parallel_tasks"` // feature switches - EmailAlert bool `json:"email_alert"` - EmailSecure bool `json:"email_secure"` - TelegramAlert bool `json:"telegram_alert"` - SlackAlert bool `json:"slack_alert"` - LdapEnable bool `json:"ldap_enable"` - LdapNeedTLS bool `json:"ldap_needtls"` - PasswordLoginDisable bool `json:"password_login_disable"` - DemoMode bool `json:"demo_mode"` - - GitClientId GitClientId `json:"git_client"` + DemoMode bool `json:"demo_mode"` // Deprecated, will be deleted soon + PasswordLoginDisable bool `json:"password_login_disable"` + NonAdminCanCreateProject bool `json:"non_admin_can_create_project"` } // Config exposes the application configuration storage for use in the application diff --git a/web/src/views/Auth.vue b/web/src/views/Auth.vue index b55d3605..6d722f28 100644 --- a/web/src/views/Auth.vue +++ b/web/src/views/Auth.vue @@ -90,6 +90,7 @@ :rules="[v => !!v || $t('username_required')]" required :disabled="signInProcess" + v-if="loginWithPassword" > {{ $t('signIn') }} @@ -152,6 +155,7 @@ export default { loginHelpDialog: null, oidcProviders: [], + loginWithPassword: null, }; }, @@ -165,6 +169,7 @@ export default { responseType: 'json', }).then((resp) => { this.oidcProviders = resp.data.oidc_providers; + this.loginWithPassword = resp.data.login_with_password; }); }, diff --git a/web/src/views/project/Settings.vue b/web/src/views/project/Settings.vue index fc8c3af2..bfd2d60f 100644 --- a/web/src/views/project/Settings.vue +++ b/web/src/views/project/Settings.vue @@ -3,7 +3,7 @@From 8e74da0ec159faf28c5660cb099bd547872e5927 Mon Sep 17 00:00:00 2001 From: don Rumata Date: Wed, 26 Jul 2023 21:21:08 +0300 Subject: [PATCH 039/346] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B2=D0=BE=D0=B4=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D1=80=D1=83=D1=81=D1=81=D0=BA=D0=B8=D0=B9=20=D1=8F?= =?UTF-8?q?=D0=B7=D1=8B=D0=BA.=20=D0=A1=D0=BF=D0=B0=D1=81=D0=B8=D0=B1?= =?UTF-8?q?=D0=BE=20@qarkai=20=D0=B7=D0=B0=20=D0=BF=D0=BE=D0=BC=D0=BE?= =?UTF-8?q?=D1=89=D1=8C.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/lang/ru.js | 235 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 web/src/lang/ru.js diff --git a/web/src/lang/ru.js b/web/src/lang/ru.js new file mode 100644 index 00000000..25276fe3 --- /dev/null +++ b/web/src/lang/ru.js @@ -0,0 +1,235 @@ +export default { + incorrectUsrPwd: 'Некорректный логин или пароль', + askDeleteUser: 'Вы действительно хотите удалить этого пользователя?', + askDeleteTemp: 'Вы действительно хотите удалить этот шаблон?', + askDeleteEnv: 'Вы действительно хотите удалить это окружение?', + askDeleteInv: 'Вы действительно хотите удалить этот инвентарь?', + askDeleteKey: 'Вы действительно хотите удалить этот ключ?', + askDeleteRepo: 'Вы действительно хотите удалить этот репозиторий?', + askDeleteProj: 'Вы действительно хотите удалить этот проект?', + askDeleteTMem: 'Вы действительно хотите удалить этого участника команды?', + edit: 'Изменить', + nnew: 'Новый', + keyFormSshKey: 'SSH ключ', + keyFormLoginPassword: 'Логин с паролем', + keyFormNone: 'Ничего', + incorrectUrl: 'Некорректный URL', + username: 'Имя пользователя', + username_required: 'Имя пользователя обязательно', + dashboard: 'Панель', + history: 'История', + activity: 'Активность', + settings: 'Настройки', + signIn: 'Войти', + password: 'Пароль', + changePassword: 'Изменить пароль', + editUser: 'Изменить пользователя', + newProject: 'Новый проект', + close: 'Закрыть', + newProject2: 'Новый проект...', + demoMode: 'Демо режим', + task: 'Задача #{expr}', + youCanRunAnyTasks: 'Вы можете запускать любые задачи', + youHaveReadonlyAccess: 'Вы имеете доступ "только для чтения"', + taskTemplates: 'Шаблоны задач', + inventory: 'Инвентарь', + environment: 'Окружение', + keyStore: 'Хранилище ключей', + repositories: 'Репозитории', + darkMode: 'Тёмная тема', + team: 'Команда', + users: 'Пользователи', + editAccount: 'Изменить учётную запись', + signOut: 'Выйти', + error: 'Ошибка', + refreshPage: 'Обновить страницу', + relogin: 'Перезайти', + howToFixSigninIssues: 'Как устранить проблемы со входом в систему', + firstlyYouNeedAccessToTheServerWhereSemaphoreRunni: 'Во-первых, необходим доступ к серверу, на котором работает Semaphore.', + executeTheFollowingCommandOnTheServerToSeeExisting: 'Выполните следующую команду на сервере, чтобы увидеть существующих пользователей:', + semaphoreUserList: 'Список пользователей semaphore', + youCanChangePasswordOfExistingUser: 'Вы можете изменить пароль существующего пользователя:', + semaphoreUserChangebyloginLoginUser123Password: 'semaphore user change-by-login --login user123 --password {makePasswordExample}', + orCreateNewAdminUser: 'Или создать нового пользователя с поными правами:', + close2: 'Закрыть', + semaphore: 'Семафор', + dontHaveAccountOrCantSignIn: 'Нет учётной записи или не можете войти?', + password2: 'Пароль', + cancel: 'Закрыть', + noViews: 'Нет видов', + addView: 'Добавить вид', + editEnvironment: 'Изменить окружение', + deleteEnvironment: 'Удалить окружение', + environment2: 'Окружение', + newEnvironment: 'Новое окружение', + environmentName: 'Имя окружения', + extraVariables: 'Дополнительные переменные', + enterExtraVariablesJson: 'Ввести дополнительные переменные в формате JSON...', + environmentVariables: 'Переменные окружения', + enterEnvJson: 'Введите переменную окружения в формате JSON...', + environmentAndExtraVariablesMustBeValidJsonExample: 'Окружениеи дополнительные переменные должны быть корректным JSON. Например:', + dashboard2: 'Панель', + ansibleSemaphore: 'Ансибл Семафор', + wereSorryButHtmlwebpackpluginoptionstitleDoesntWor: 'Извините, но <%= htmlWebpackPlugin.options.title %> не работает без включенного JavaScript. Пожалуйста включите, чтобы продолжить', + deleteInventory: 'Удалить инвентарь', + newInventory: 'Новый инвентарь', + name: 'Имя', + userCredentials: 'Учётные данные пользователя', + sudoCredentialsOptional: 'Повышенные учётные данные (дополнительно)', + type: 'Тип', + pathToInventoryFile: 'Путь до файла инвентаря', + enterInventory: 'Введите инвентарь...', + staticInventoryExample: 'Пример статичного инвентаря:', + staticYamlInventoryExample: 'Пример статичного инвентаря в YAML:', + keyName: 'Имя ключа', + loginOptional: 'Логин (дополнительно)', + usernameOptional: 'Имя пользователя (дополнительно)', + privateKey: 'Закрытый ключ', + override: 'Переопределить', + useThisTypeOfKeyForHttpsRepositoriesAndForPlaybook: 'Используйте этот тип ключа для HTTPS-репозиториев и для плейбуков, использующих не-SSH-соединения.', + deleteKey: 'Удалить ключ', + newKey: 'Новый ключ', + create: 'Создать', + newTask: 'Новая задача', + cantDeleteThe: 'Невозможно удалить {objectTitle}', + theCantBeDeletedBecauseItUsedByTheResourcesBelow: '{objectTitle} нельзя удалить, так как он используется следующими ресурсами', + projectName: 'Имя проекта', + allowAlertsForThisProject: 'Разрешить оповещения для этого проекта', + telegramChatIdOptional: 'Идентификатор чата Телеграм (дополнительно)', + maxNumberOfParallelTasksOptional: 'Максимальное число параллельных задач (дополнительно)', + deleteRepository: 'Удалить репозиторий', + newRepository: 'Новый репозиторий', + urlOrPath: 'URL или путь', + absPath: 'abs. path', + branch: 'Ветка', + accessKey: 'Ключ доступа', + credentialsToAccessToTheGitRepositoryItShouldBe: 'Учетные данные для доступа к репозиториям Git. Должен быть:', + ifYouUseGitOrSshUrl: 'если вы используете Git или SSH URL.', + ifYouUseHttpsOrFileUrl: 'если вы используете HTTPS или file URL.', + none: 'Ничего', + ssh: 'SSH', + deleteProject: 'Удалить проект', + save: 'Сохранить', + deleteProject2: 'Удалить проект', + onceYouDeleteAProjectThereIsNoGoingBackPleaseBeCer: 'Как только вы удалите проект, вернуть его будет невозможно. Пожалуйста, будьте осторожны.', + name2: 'Имя *', + title: 'Заголовок *', + description: 'Описание', + required: 'Необходимый', + key: '{expr}', + surveyVariables: 'Опрос переменных', + addVariable: 'Добавить переменную', + columns: 'Столбцы', + buildVersion: 'Версия сборки', + messageOptional: 'Сообщение (дополнительно)', + debug: 'Отладка', + dryRun: 'Пробный запуск', + diff: 'Разница', + advanced: 'Расширенный', + hide: 'Спрятать', + pleaseAllowOverridingCliArgumentInTaskTemplateSett: 'Пожалуйста, разрешите переопределение аргумента CLI в настройках шаблона задачи', + cliArgsJsonArrayExampleIMyinventoryshPrivatekeythe: 'Аргументы CLI (массив JSON). Например: [ "-i", "@myinventory.sh", "--private-key=/there/id_rsa", "-vvvv" ]', + started: 'Начал', + author: 'Автор', + duration: 'Продолжительность', + stop: 'Стоп', + deleteTeamMember: 'Удалить члена команды', + team2: 'Команда', + newTeamMember: 'Новый член команды', + user: 'Пользователь', + administrator: 'Администратор', + definesStartVersionOfYourArtifactEachRunIncrements: 'Определяет начальную версию вашего артефакта. Каждый запуск увеличивает версию артефакта.', + forMoreInformationAboutBuildingSeeThe: 'Дополнительную информацию о построении смотрите', + taskTemplateReference: 'Справочник по шаблону задачи', + definesWhatArtifactShouldBeDeployedWhenTheTaskRun: 'Определяет, какой артефакт должен быть развернут при запуске задачи.', + forMoreInformationAboutDeployingSeeThe: 'Дополнительные сведения о развертывании смотрите', + taskTemplateReference2: 'Справочник по шаблону задачи', + definesAutorunSchedule: 'Определяет расписание автозапуска.', + forMoreInformationAboutCronSeeThe: 'Дополнительные сведения о Крон см.', + cronExpressionFormatReference: 'Документация по формату выражений Крон', + startVersion: 'Начальная версия', + example000: 'Например: 0.0.0', + buildTemplate: 'Шаблон сборки', + autorun: 'Автозапуск', + playbookFilename: 'Имя файла плейбука *', + exampleSiteyml: 'Например: site.yml', + inventory2: 'Инвентарь *', + репозиторий: 'Репозиторий *', + environment3: 'Окружение *', + vaultPassword: 'Vault пароль', + vaultPassword2: 'Vault пароль', + view: 'Вид', + cron: 'Крон', + iWantToRunATaskByTheCronOnlyForForNewCommitsOfSome: 'Я хочу запускать задачу по Крону только для новых коммитов некоторых репозиториев', + репозиторий2: 'Репозиторий', + cronChecksNewCommitBeforeRun: 'Проверять через Крон новые коммиты до запуска', + readThe: 'Читать', + toLearnMoreAboutCron: 'чтобы узнать больше о Крон.', + suppressSuccessAlerts: 'Скрыть оповещения об успехе', + cliArgsJsonArrayExampleIMyinventoryshPrivatekeythe2: 'Аргументы CLI (массив JSON). Например: [ "-i", "@myinventory.sh", "--private-key=/there/id_rsa", "-vvvv" ]', + allowCliArgsInTask: 'Разрешить рагументы CLI в задаче', + docs: 'Документация', + editViews: 'Изменить вид', + newTemplate: 'Новый шаблон', + taskTemplates2: 'Шаблоны задач', + all: 'Все', + notLaunched: 'Не запущен', + by: 'к {user_name} {formatDate}', + editTemplate: 'Изменить шаблон', + newTemplate2: 'Новый шаблон', + deleteTemplate: 'Удалить шаблон', + playbook: 'Плейбук', + email: 'Почта', + adminUser: 'Администратор', + sendAlerts: 'Отправить оповещение', + deleteUser: 'Удалить пользователя', + newUser: 'Новый пользователь', + re: 'Пере{getActionButtonTitle}', + teamMember: '{expr} Team Member', + taskId: 'Номер задачи', + version: 'Версия', + status: 'Статус', + start: 'Начать', + actions: 'Действия', + alert: 'Оповещение', + admin: 'Администратор', + role: 'Роль', + external: 'Внешний', + time: 'Время', + path: 'Путь', + gitUrl: 'Git URL', + sshKey: 'SSH ключ', + lastTask: 'Последняя задача', + task2: 'Задача', + build: 'Сборка', + deploy: 'Развертывать', + run: 'Запуск', + add: 'Добавить', + password_required: 'Требуется пароль', + name_required: 'Требуется имя', + user_credentials_required: 'Требуются учетные данные пользователя', + type_required: 'Требуется тип', + path_required: 'Требуется путь до файла инвенторя', + private_key_required: 'Требуется приватный ключ', + project_name_required: 'Требуется имя проекта', + репозиторий_required: 'Требуется репозиторий', + branch_required: 'Требуется ветка', + key_required: 'Требуется ключ', + user_required: 'Требуется пользователь', + build_version_required: 'Требуется номер сборки', + title_required: 'Требуется заголовок', + isRequired: 'требуется', + mustBeInteger: 'Должно быть целым числом', + mustBe0OrGreater: 'Должно быть 0 или больше', + start_version_required: 'Требуется стартовая версия', + playbook_filename_required: 'Требуется имя файла плейбука', + inventory_required: 'Требуется инвентарь', + environment_required: 'Требуется окружение', + email_required: 'Требуется почта', + build_template_required: 'Требуется шаблон сборки', + Task: 'Задача', + Build: 'Сборка', + Deploy: 'Развертывать', + Run: 'Запуск', + +}; From 4a86c19601b0766e57d256a5062cc9c61b87d199 Mon Sep 17 00:00:00 2001 From: William Edwards Date: Thu, 27 Jul 2023 18:51:21 +0200 Subject: [PATCH 040/346] Fix manager typo --- web/src/views/project/Team.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/views/project/Team.vue b/web/src/views/project/Team.vue index 6aff2bf8..c5ac539f 100644 --- a/web/src/views/project/Team.vue +++ b/web/src/views/project/Team.vue @@ -81,7 +81,7 @@ export default { title: 'Owner', }, { slug: 'manager', - title: 'Manger', + title: 'Manager', }, { slug: 'task_runner', title: 'Task Runner', From 421e862786531ef027d8bad9b768fd6cd2889e09 Mon Sep 17 00:00:00 2001 From: AnsibleGuy Date: Sat, 5 Aug 2023 15:56:39 +0200 Subject: [PATCH 041/346] feat: added basic config validation, loading all settings from environment-variables, dynamic applying of default-values to settings, tests for config-loading and -validation --- db/AccessKey.go | 4 +- util/config.go | 237 +++++++++++++++++++++++++++++++------ util/config_test.go | 280 +++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 469 insertions(+), 52 deletions(-) diff --git a/db/AccessKey.go b/db/AccessKey.go index a0c93f03..65e14b84 100644 --- a/db/AccessKey.go +++ b/db/AccessKey.go @@ -198,7 +198,7 @@ func (key *AccessKey) SerializeSecret() error { return fmt.Errorf("invalid access token type") } - encryptionString := util.Config.GetAccessKeyEncryption() + encryptionString := util.Config.AccessKeyEncryption if encryptionString == "" { secret := base64.StdEncoding.EncodeToString(plaintext) @@ -279,7 +279,7 @@ func (key *AccessKey) DeserializeSecret() error { return err } - encryptionString := util.Config.GetAccessKeyEncryption() + encryptionString := util.Config.AccessKeyEncryption if encryptionString == "" { err = key.unmarshalAppropriateField(ciphertext) diff --git a/util/config.go b/util/config.go index 3946e2f4..0061c6dd 100644 --- a/util/config.go +++ b/util/config.go @@ -6,7 +6,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/google/go-github/github" "io" "net/url" "os" @@ -14,7 +13,11 @@ import ( "path" "path/filepath" "strings" + "reflect" + "regexp" + "strconv" + "github.com/google/go-github/github" "github.com/gorilla/securecookie" ) @@ -115,7 +118,6 @@ type ConfigType struct { CookieEncryption string `json:"cookie_encryption"` // AccessKeyEncryption is BASE64 encoded byte array used // for encrypting and decrypting access keys stored in database. - // Do not use it! Use method GetAccessKeyEncryption instead of it. AccessKeyEncryption string `json:"access_key_encryption"` // email alerting @@ -159,25 +161,96 @@ type ConfigType struct { // Config exposes the application configuration storage for use in the application var Config *ConfigType +var ( + // default config values + configDefaults = map[string]interface{}{ + "Port": ":3000", + "TmpPath": "/tmp/semaphore", + "GitClientId": GoGitClientId, + } + + // mapping internal config to env-vars + // todo: special cases - SEMAPHORE_DB_PORT, SEMAPHORE_DB_PATH (bolt), SEMAPHORE_CONFIG_PATH, OPENID for 1 provider if it makes sense + ConfigEnvironmentalVars = map[string]string{ + "Dialect": "SEMAPHORE_DB_DIALECT", + "MySQL.Hostname": "SEMAPHORE_DB_HOST", + "MySQL.Username": "SEMAPHORE_DB_USER", + "MySQL.Password": "SEMAPHORE_DB_PASS", + "MySQL.DbName": "SEMAPHORE_DB", + "Postgres.Hostname": "SEMAPHORE_DB_HOST", + "Postgres.Username": "SEMAPHORE_DB_USER", + "Postgres.Password": "SEMAPHORE_DB_PASS", + "Postgres.DbName": "SEMAPHORE_DB", + "BoltDb.Hostname": "SEMAPHORE_DB_HOST", + "Port": "SEMAPHORE_PORT", + "Interface": "SEMAPHORE_INTERFACE", + "TmpPath": "SEMAPHORE_TMP_PATH", + "SshConfigPath": "SEMAPHORE_TMP_PATH", + "GitClientId": "SEMAPHORE_GIT_CLIENT", + "WebHost": "SEMAPHORE_WEB_ROOT", + "CookieHash": "SEMAPHORE_COOKIE_HASH", + "CookieEncryption": "SEMAPHORE_COOKIE_ENCRYPTION", + "AccessKeyEncryption": "SEMAPHORE_ACCESS_KEY_ENCRYPTION", + "EmailAlert": "SEMAPHORE_EMAIL_ALERT", + "EmailSender": "SEMAPHORE_EMAIL_SENDER", + "EmailHost": "SEMAPHORE_EMAIL_HOST", + "EmailPort": "SEMAPHORE_EMAIL_PORT", + "EmailUsername": "SEMAPHORE_EMAIL_USER", + "EmailPassword": "SEMAPHORE_EMAIL_PASSWORD", + "EmailSecure": "SEMAPHORE_EMAIL_SECURE", + "LdapEnable": "SEMAPHORE_LDAP_ACTIVATED", + "LdapBindDN": "SEMAPHORE_LDAP_DN_BIND", + "LdapBindPassword": "SEMAPHORE_LDAP_PASSWORD", + "LdapServer": "SEMAPHORE_LDAP_HOST", + "LdapSearchDN": "SEMAPHORE_LDAP_DN_SEARCH", + "LdapSearchFilter": "SEMAPHORE_LDAP_SEARCH_FILTER", + "LdapMappings.DN": "SEMAPHORE_LDAP_MAPPING_DN", + "LdapMappings.UID": "SEMAPHORE_LDAP_MAPPING_USERNAME", + "LdapMappings.CN": "SEMAPHORE_LDAP_MAPPING_FULLNAME", + "LdapMappings.Mail": "SEMAPHORE_LDAP_MAPPING_EMAIL", + "LdapNeedTLS": "SEMAPHORE_LDAP_NEEDTLS", + "TelegramAlert": "SEMAPHORE_TELEGRAM_ALERT", + "TelegramChat": "SEMAPHORE_TELEGRAM_CHAT", + "TelegramToken": "SEMAPHORE_TELEGRAM_TOKEN", + "SlackAlert": "SEMAPHORE_SLACK_ALERT", + "SlackUrl": "SEMAPHORE_SLACK_URL", + "MaxParallelTasks": "SEMAPHORE_MAX_PARALLEL_TASKS", + } + + // basic config validation using regex + /* NOTE: other basic regex could be used: + ipv4: ^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$ + ipv6: ^(?:[A-Fa-f0-9]{1,4}:|:){3,7}[A-Fa-f0-9]{1,4}$ + domain: ^([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*\.)+[a-zA-Z]{2,}$ + path+filename: ^([\\/[a-zA-Z0-9_\\-${}:~]*]*\\/)?[a-zA-Z0-9\\.~_${}\\-:]*$ + email address: ^(|.*@[A-Za-z0-9-\\.]*)$ + */ + configValidationRegex = map[string]string{ + "Dialect": "^mysql|bolt|postgres$", + "Port": "^:([0-9]{1,5})$", // can have false-negatives + "GitClientId": "^go_git|cmd_git$", + "CookieHash": "^[-A-Za-z0-9+=\\/]{40,}$", // base64 + "CookieEncryption": "^[-A-Za-z0-9+=\\/]{40,}$", // base64 + "AccessKeyEncryption": "^[-A-Za-z0-9+=\\/]{40,}$", // base64 + "EmailPort": "^(|[0-9]{1,5})$", // can have false-negatives + "MaxParallelTasks": "^[0-9]{1,10}$", // 0-9999999999 + } +) + // ToJSON returns a JSON string of the config func (conf *ConfigType) ToJSON() ([]byte, error) { return json.MarshalIndent(&conf, " ", "\t") } -func (conf *ConfigType) GetAccessKeyEncryption() string { - ret := os.Getenv("SEMAPHORE_ACCESS_KEY_ENCRYPTION") - - if ret == "" { - ret = conf.AccessKeyEncryption - } - - return ret -} - // ConfigInit reads in cli flags, and switches actions appropriately on them func ConfigInit(configPath string) { - loadConfig(configPath) - validateConfig() + fmt.Println("Loading config") + loadConfigFile(configPath) + loadConfigEnvironment() + loadConfigDefaults() + + fmt.Println("Validating config") + validateConfig(exitOnConfigError) var encryption []byte @@ -193,7 +266,7 @@ func ConfigInit(configPath string) { } } -func loadConfig(configPath string) { +func loadConfigFile(configPath string) { if configPath == "" { configPath = os.Getenv("SEMAPHORE_CONFIG_PATH") } @@ -203,7 +276,7 @@ func loadConfig(configPath string) { if configPath == "" { cwd, err := os.Getwd() - exitOnConfigError(err) + exitOnConfigFileError(err) paths := []string{ path.Join(cwd, "config.json"), "/usr/local/etc/semaphore/config.json", @@ -221,46 +294,140 @@ func loadConfig(configPath string) { decodeConfig(file) break } - exitOnConfigError(err) + exitOnConfigFileError(err) } else { p := configPath file, err := os.Open(p) - exitOnConfigError(err) + exitOnConfigFileError(err) decodeConfig(file) } } -func validateConfig() { +func loadConfigDefaults() { - validatePort() - - if len(Config.TmpPath) == 0 { - Config.TmpPath = "/tmp/semaphore" + for attribute, defaultValue := range configDefaults { + if len(getConfigValue(attribute)) == 0 { + setConfigValue(attribute, defaultValue) + } } - if Config.MaxParallelTasks < 1 { - Config.MaxParallelTasks = 10 - } } -func validatePort() { +func castStringToInt(value string) int { - //TODO - why do we do this only with this variable? - if len(os.Getenv("PORT")) > 0 { - Config.Port = ":" + os.Getenv("PORT") + valueInt, err := strconv.Atoi(value) + if err != nil { + panic(err) } - if len(Config.Port) == 0 { - Config.Port = ":3000" + return valueInt + +} + +func castStringToBool(value string) bool { + + var valueBool bool + if value == "1" || strings.ToLower(value) == "true" { + valueBool = true + } else { + valueBool = false } + return valueBool + +} + +func setConfigValue(path string, value interface{}) { + + attribute := reflect.ValueOf(Config) + + for _, nested := range strings.Split(path, ".") { + attribute = reflect.Indirect(attribute).FieldByName(nested) + } + if attribute.IsValid() { + switch attribute.Kind() { + case reflect.Int: + if reflect.ValueOf(value).Kind() != reflect.Int { + value = castStringToInt(fmt.Sprintf("%v", reflect.ValueOf(value))) + } + case reflect.Bool: + if reflect.ValueOf(value).Kind() != reflect.Bool { + value = castStringToBool(fmt.Sprintf("%v", reflect.ValueOf(value))) + } + } + attribute.Set(reflect.ValueOf(value)) + } + +} + +func getConfigValue(path string) string { + + attribute := reflect.ValueOf(Config) + nested_path := strings.Split(path, ".") + + for i, nested := range nested_path { + attribute = reflect.Indirect(attribute).FieldByName(nested) + lastDepth := len(nested_path) == i+1 + if !lastDepth && attribute.Kind() != reflect.Struct || lastDepth && attribute.Kind() == reflect.Invalid { + panic(fmt.Errorf("got non-existent config attribute '%v'", path)) + } + } + + return fmt.Sprintf("%v", attribute) +} + +func validateConfig(errorFunc func(string)) { + if !strings.HasPrefix(Config.Port, ":") { Config.Port = ":" + Config.Port } + + for attribute, validateRegex := range configValidationRegex { + value := getConfigValue(attribute) + match, _ := regexp.MatchString(validateRegex, value) + if !match { + if !strings.Contains(attribute, "assword") && !strings.Contains(attribute, "ecret") { + errorFunc(fmt.Sprintf( + "value of setting '%v' is not valid: '%v' (Must match regex: '%v')", + attribute, value, validateRegex, + )) + } else { + errorFunc(fmt.Sprintf( + "value of setting '%v' is not valid! (Must match regex: '%v')", + attribute, validateRegex, + )) + } + } + } + } -func exitOnConfigError(err error) { +func loadConfigEnvironment() { + + for attribute, envVar := range ConfigEnvironmentalVars { + // skip unused db-dialects as they use the same env-vars + if strings.Contains(attribute, "MySQL") && Config.Dialect != DbDriverMySQL { + continue + } else if strings.Contains(attribute, "Postgres") && Config.Dialect != DbDriverPostgres { + continue + } else if strings.Contains(attribute, "BoldDb") && Config.Dialect != DbDriverBolt { + continue + } + + envValue, exists := os.LookupEnv(envVar) + if exists && len(envValue) > 0 { + setConfigValue(attribute, envValue) + } + } + +} + +func exitOnConfigError(msg string) { + fmt.Println(msg) + os.Exit(1) +} + +func exitOnConfigFileError(err error) { if err != nil { - fmt.Println("Cannot Find configuration! Use --config parameter to point to a JSON file generated by `semaphore setup`.") - os.Exit(1) + exitOnConfigError("Cannot Find configuration! Use --config parameter to point to a JSON file generated by `semaphore setup`.") } } diff --git a/util/config_test.go b/util/config_test.go index 73bf866b..7bafcbb9 100644 --- a/util/config_test.go +++ b/util/config_test.go @@ -1,28 +1,278 @@ package util import ( + "fmt" "os" "testing" ) -func TestValidatePort(t *testing.T) { +func mockError(msg string) { + panic(msg) +} + +func TestCastStringToInt(t *testing.T) { + + var errMsg string = "Cast string => int failed" + + if castStringToInt("5") != 5 { + t.Error(errMsg) + } + if castStringToInt("0") != 0 { + t.Error(errMsg) + } + if castStringToInt("-1") != -1 { + t.Error(errMsg) + } + if castStringToInt("999") != 999 { + t.Error(errMsg) + } + if castStringToInt("999") != 999 { + t.Error(errMsg) + } + defer func() { + if r := recover(); r == nil { + t.Errorf("Cast string => int did not panic on invalid input") + } + }() + castStringToInt("xxx") + +} + +func TestCastStringToBool(t *testing.T) { + + var errMsg string = "Cast string => bool failed" + + if castStringToBool("1") != true { + t.Error(errMsg) + } + if castStringToBool("0") != false { + t.Error(errMsg) + } + if castStringToBool("true") != true { + t.Error(errMsg) + } + if castStringToBool("false") != false { + t.Error(errMsg) + } + if castStringToBool("xxx") != false { + t.Error(errMsg) + } + if castStringToBool("") != false { + t.Error(errMsg) + } + +} + +func TestGetConfigValue(t *testing.T) { Config = new(ConfigType) - Config.Port = "" - validatePort() + + var testPort string = "1337" + var testCookieHash string = "0Sn+edH3doJ4EO4Rl49Y0KrxjUkXuVtR5zKHGGWerxQ=" + var testMaxParallelTasks int = 5 + var testLdapNeedTls bool = true + var testDbHost string = "192.168.0.1" + + Config.Port = testPort + Config.CookieHash = testCookieHash + Config.MaxParallelTasks = testMaxParallelTasks + Config.LdapNeedTLS = testLdapNeedTls + Config.BoltDb.Hostname = testDbHost + + if getConfigValue("Port") != testPort { + t.Error("Could not get value for config attribute 'Port'!") + } + if getConfigValue("CookieHash") != testCookieHash { + t.Error("Could not get value for config attribute 'CookieHash'!") + } + if getConfigValue("MaxParallelTasks") != fmt.Sprintf("%v", testMaxParallelTasks) { + t.Error("Could not get value for config attribute 'MaxParallelTasks'!") + } + if getConfigValue("LdapNeedTLS") != fmt.Sprintf("%v", testLdapNeedTls) { + t.Error("Could not get value for config attribute 'LdapNeedTLS'!") + } + if getConfigValue("BoltDb.Hostname") != fmt.Sprintf("%v", testDbHost) { + t.Error("Could not get value for config attribute 'BoltDb.Hostname'!") + } + + defer func() { + if r := recover(); r == nil { + t.Error("Did not fail on non-existent config attribute!") + } + }() + + getConfigValue("Not.Existent") + +} + +func TestSetConfigValue(t *testing.T) { + + Config = new(ConfigType) + + var testPort string = "1337" + var testCookieHash string = "0Sn+edH3doJ4EO4Rl49Y0KrxjUkXuVtR5zKHGGWerxQ=" + var testMaxParallelTasks int = 5 + var testLdapNeedTls bool = true + var testDbHost string = "192.168.0.1" + var testEmailSecure string = "1" + var expectEmailSecure bool = true + + setConfigValue("Port", testPort) + setConfigValue("CookieHash", testCookieHash) + setConfigValue("MaxParallelTasks", testMaxParallelTasks) + setConfigValue("LdapNeedTLS", testLdapNeedTls) + setConfigValue("BoltDb.Hostname", testDbHost) + setConfigValue("EmailSecure", testEmailSecure) + + if Config.Port != testPort { + t.Error("Could not set value for config attribute 'Port'!") + } + if Config.CookieHash != testCookieHash { + t.Error("Could not set value for config attribute 'CookieHash'!") + } + if Config.MaxParallelTasks != testMaxParallelTasks { + t.Error("Could not set value for config attribute 'MaxParallelTasks'!") + } + if Config.LdapNeedTLS != testLdapNeedTls { + t.Error("Could not set value for config attribute 'LdapNeedTls'!") + } + if Config.BoltDb.Hostname != testDbHost { + t.Error("Could not set value for config attribute 'BoltDb.Hostname'!") + } + if Config.EmailSecure != expectEmailSecure { + t.Error("Could not set value for config attribute 'EmailSecure'!") + } + + defer func() { + if r := recover(); r == nil { + t.Error("Did not fail on non-existent config attribute!") + } + }() + + setConfigValue("Not.Existent", "someValue") + +} + +func TestLoadConfigEnvironmet(t *testing.T) { + + Config = new(ConfigType) + Config.Dialect = DbDriverBolt + + var envPort string = "1337" + var envCookieHash string = "0Sn+edH3doJ4EO4Rl49Y0KrxjUkXuVtR5zKHGGWerxQ=" + var envAccessKeyEncryption string = "1/wRYXQltDGwbzNZRP9ZfJb2IoWcn1hYrxA0vOdvVos=" + var envMaxParallelTasks string = "5" + var expectMaxParallelTasks int = 5 + var expectLdapNeedTls bool = true + var envLdapNeedTls string = "1" + var envDbHost string = "192.168.0.1" + + os.Setenv("SEMAPHORE_PORT", envPort) + os.Setenv("SEMAPHORE_COOKIE_HASH", envCookieHash) + os.Setenv("SEMAPHORE_ACCESS_KEY_ENCRYPTION", envAccessKeyEncryption) + os.Setenv("SEMAPHORE_MAX_PARALLEL_TASKS", envMaxParallelTasks) + os.Setenv("SEMAPHORE_LDAP_NEEDTLS", envLdapNeedTls) + os.Setenv("SEMAPHORE_DB_HOST", envDbHost) + + loadConfigEnvironment() + + if Config.Port != envPort { + t.Error("Setting 'Port' was not loaded from environment-vars!") + } + if Config.CookieHash != envCookieHash { + t.Error("Setting 'CookieHash' was not loaded from environment-vars!") + } + if Config.AccessKeyEncryption != envAccessKeyEncryption { + t.Error("Setting 'AccessKeyEncryption' was not loaded from environment-vars!") + } + if Config.MaxParallelTasks != expectMaxParallelTasks { + t.Error("Setting 'MaxParallelTasks' was not loaded from environment-vars!") + } + if Config.LdapNeedTLS != expectLdapNeedTls { + t.Error("Setting 'LdapNeedTLS' was not loaded from environment-vars!") + } + if Config.BoltDb.Hostname != envDbHost { + t.Error("Setting 'BoltDb.Hostname' was not loaded from environment-vars!") + } + if Config.MySQL.Hostname == envDbHost || Config.Postgres.Hostname == envDbHost { + t.Error("DB-Hostname was not loaded for inactive DB-dialects!") + } + +} + +func TestLoadConfigDefaults(t *testing.T) { + + Config = new(ConfigType) + var errMsg string = "Failed to load config-default" + + loadConfigDefaults() + if Config.Port != ":3000" { - t.Error("no port should get set to default") + t.Error(errMsg) } - - Config.Port = "4000" - validatePort() - if Config.Port != ":4000" { - t.Error("Port without : suffix should have it added") - } - - os.Setenv("PORT", "5000") - validatePort() - if Config.Port != ":5000" { - t.Error("Port value should be overwritten by env var, and it should be prefixed appropriately") + if Config.TmpPath != "/tmp/semaphore" { + t.Error(errMsg) } } + +func ensureConfigValidationFailure(t *testing.T, attribute string, value interface{}) { + + defer func() { + if r := recover(); r == nil { + t.Errorf( + "Config validation for attribute '%v' did not fail! (value '%v')", + attribute, value, + ) + } + }() + validateConfig(mockError) + +} + +func TestValidateConfig(t *testing.T) { + + Config = new(ConfigType) + + var testPort string = ":3000" + var testDbDialect DbDriver = DbDriverBolt + var testCookieHash string = "0Sn+edH3doJ4EO4Rl49Y0KrxjUkXuVtR5zKHGGWerxQ=" + var testMaxParallelTasks int = 0 + + Config.Port = testPort + Config.Dialect = testDbDialect + Config.CookieHash = testCookieHash + Config.MaxParallelTasks = testMaxParallelTasks + Config.GitClientId = GoGitClientId + Config.CookieEncryption = testCookieHash + Config.AccessKeyEncryption = testCookieHash + validateConfig(mockError) + + Config.Port = "INVALID" + ensureConfigValidationFailure(t, "Port", Config.Port) + + Config.Port = ":100000" + ensureConfigValidationFailure(t, "Port", Config.Port) + Config.Port = testPort + + Config.MaxParallelTasks = -1 + ensureConfigValidationFailure(t, "MaxParallelTasks", Config.MaxParallelTasks) + Config.MaxParallelTasks = testMaxParallelTasks + + Config.CookieHash = "\"0Sn+edH3doJ4EO4Rl49Y0KrxjUkXuVtR5zKHGGWerxQ=\"" + ensureConfigValidationFailure(t, "CookieHash", Config.CookieHash) + + Config.CookieHash = "!)394340" + ensureConfigValidationFailure(t, "CookieHash", Config.CookieHash) + + Config.CookieHash = "" + ensureConfigValidationFailure(t, "CookieHash", Config.CookieHash) + + Config.CookieHash = "TQwjDZ5fIQtaIw==" + ensureConfigValidationFailure(t, "CookieHash", Config.CookieHash) + Config.CookieHash = testCookieHash + + Config.Dialect = "someOtherDB" + ensureConfigValidationFailure(t, "Dialect", Config.Dialect) + Config.Dialect = testDbDialect + +} From 372d555227639a6c26664bbda8094fcd111e827d Mon Sep 17 00:00:00 2001 From: AnsibleGuy Date: Sat, 5 Aug 2023 18:27:14 +0200 Subject: [PATCH 042/346] feat: added basic github issue-forms --- .github/ISSUE_TEMPLATES/documentation.yml | 48 ++++++ .github/ISSUE_TEMPLATES/feature_request.yml | 94 +++++++++++ .github/ISSUE_TEMPLATES/problem.yml | 169 ++++++++++++++++++++ .github/ISSUE_TEMPLATES/question.yml | 45 ++++++ 4 files changed, 356 insertions(+) create mode 100644 .github/ISSUE_TEMPLATES/documentation.yml create mode 100644 .github/ISSUE_TEMPLATES/feature_request.yml create mode 100644 .github/ISSUE_TEMPLATES/problem.yml create mode 100644 .github/ISSUE_TEMPLATES/question.yml diff --git a/.github/ISSUE_TEMPLATES/documentation.yml b/.github/ISSUE_TEMPLATES/documentation.yml new file mode 100644 index 00000000..2ad799b5 --- /dev/null +++ b/.github/ISSUE_TEMPLATES/documentation.yml @@ -0,0 +1,48 @@ +--- + +name: Documentation +description: You have a found missing or invalid documentation +title: "Docs: " +labels: ['documentation', 'triage'] + +body: + - type: markdown + attributes: + value: | + Please make sure to go through these steps **before opening an issue**: + + - [ ] Read the [documentation](https://docs.ansible-semaphore.com/) + - [ ] Read the [troubleshooting guide](https://docs.ansible-semaphore.com/administration-guide/troubleshooting) + - [ ] Read the [documentation regarding manual installations](https://docs.ansible-semaphore.com/administration-guide/installation_manually) if you did install Semaphore that way + + - [ ] Check if there are existing [issues](https://github.com/ansible-semaphore/semaphore/issues) or [discussions](https://github.com/ansible-semaphore/semaphore/discussions) regarding your topic + + - type: textarea + id: problem + attributes: + label: Problem + description: | + Describe what part of the documentation is missing or wrong! + Please also tell us what you would expected to find. + What would you change or add? + validations: + required: true + + - type: dropdown + id: related-to + attributes: + label: Related to + description: | + To what parts of Semaphore is the documentation related? (if any) + + multiple: true + options: + - Web-Frontend (what users interact with) + - Web-Backend (APIs) + - Service (scheduled tasks, alerts) + - Ansible (task execution) + - Configuration + - Database + - Docker + validations: + required: false diff --git a/.github/ISSUE_TEMPLATES/feature_request.yml b/.github/ISSUE_TEMPLATES/feature_request.yml new file mode 100644 index 00000000..a72371aa --- /dev/null +++ b/.github/ISSUE_TEMPLATES/feature_request.yml @@ -0,0 +1,94 @@ +--- + +name: Feature request +description: You would like to have a new feature implemented +title: "Feature: " +labels: ['feature', 'triage'] + +body: + - type: markdown + attributes: + value: | + Please make sure to go through these steps **before opening an issue**: + + - [ ] Read the [documentation](https://docs.ansible-semaphore.com/) + - [ ] Read the [troubleshooting guide](https://docs.ansible-semaphore.com/administration-guide/troubleshooting) + - [ ] Read the [documentation regarding manual installations](https://docs.ansible-semaphore.com/administration-guide/installation_manually) if you did install Semaphore that way + + - [ ] Check if there are existing [issues](https://github.com/ansible-semaphore/semaphore/issues) or [discussions](https://github.com/ansible-semaphore/semaphore/discussions) regarding your topic + + - type: dropdown + id: related-to + attributes: + label: Related to + description: | + To what parts of Semaphore is the feature related? + + multiple: true + options: + - Web-Frontend (what users interact with) + - Web-Backend (APIs) + - Service (scheduled tasks, alerts) + - Ansible (task execution) + - Configuration + - Database + - Docker + - Other + validations: + required: true + + - type: dropdown + id: impact + attributes: + label: Impact + description: | + What impact would the feature have for Semaphore users? + + multiple: false + options: + - nice to have + - nice to have for enterprise usage + - better user experience + - security improvements + - major improvement to user experience + - must have for enterprise usage + - must have + validations: + required: true + + - type: textarea + id: feature + attributes: + label: Missing Feature + description: | + Describe the feature you are missing. + Why would you like to see such a feature being implemented? + + validations: + required: true + + - type: textarea + id: implementation + attributes: + label: Implementation + description: | + Please think about how the feature should be implemented. + What would you suggest? + How should it look and behave? + + validations: + required: true + + - type: textarea + id: design + attributes: + label: Design + description: | + If you have programming experience yourself: + Please provide us with an example how you would design this feature. + + What edge-cases need to be covered? + Are there relations to other components that need to be though of? + + validations: + required: false diff --git a/.github/ISSUE_TEMPLATES/problem.yml b/.github/ISSUE_TEMPLATES/problem.yml new file mode 100644 index 00000000..6c43526e --- /dev/null +++ b/.github/ISSUE_TEMPLATES/problem.yml @@ -0,0 +1,169 @@ +--- + +name: Problem +description: You have encountered problems when using Semaphore +title: "Problem: " +labels: ['problem', 'triage'] + +body: + - type: markdown + attributes: + value: | + Please make sure to go through these steps **before opening an issue**: + + - [ ] Read the [documentation](https://docs.ansible-semaphore.com/) + - [ ] Read the [troubleshooting guide](https://docs.ansible-semaphore.com/administration-guide/troubleshooting) + - [ ] Read the [documentation regarding manual installations](https://docs.ansible-semaphore.com/administration-guide/installation_manually) if you don't use docker + + - [ ] Check if there are existing [issues](https://github.com/ansible-semaphore/semaphore/issues) or [discussions](https://github.com/ansible-semaphore/semaphore/discussions) regarding your topic + + - type: textarea + id: problem + attributes: + label: Issue + description: | + Describe the problem you encountered and tell us what you would have expected to happen + + validations: + required: true + + - type: dropdown + id: impact + attributes: + label: Impact + description: | + What parts of Semaphore are impacted by the problem? + + multiple: true + options: + - Web-Frontend (what users interact with) + - Web-Backend (APIs) + - Service (scheduled tasks, alerts) + - Ansible (task execution) + - Configuration + - Database + - Docker + - Semaphore Project + - Other + validations: + required: true + + - type: dropdown + id: install-method + attributes: + label: Installation method + description: | + How did you install Semaphore? + + multiple: false + options: + - Docker + - Package + - Binary + - Snap + validations: + required: true + + - type: dropdown + id: browsers + attributes: + label: Browser + description: | + If the problem occurs in the Semaphore WebUI - in what browsers do you see it? + + multiple: true + options: + - Firefox + - Chrome + - Safari + - Microsoft Edge + - Opera + + - type: textarea + id: version-semaphore + attributes: + label: Semaphore Version + description: | + What version of Semaphore are you running? + > Command: `semaphore version` + validations: + required: true + + - type: textarea + id: version-ansible + attributes: + label: Ansible Version + description: | + If your problem occurs when executing a task: + > What version of Ansible are you running? + > Command: `ansible --version` + + If your problem occurs when executing a specific Ansible Module: + > Provide the Ansible Module versions! + > Command: `ansible-galaxy collection list` + + render: bash + validations: + required: false + + - type: textarea + id: logs + attributes: + label: Logs & errors + description: | + Provide logs and error messages you have encountered! + + Logs of the service: + > Docker command: `docker logs ` + > Systemd command: `journalctl -u --no-pager --full -n 250` + + If the error occurs in the WebUI: + > please add a screenshot + > check your browser console for errors (`F12` in most browsers) + + Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. + + validations: + required: false + + - type: textarea + id: manual-installation + attributes: + label: Manual installation - system information + description: | + If you have installed Semaphore using the package or binary: + + Please share your operating system & -version! + > Command: `uname -a` + + What reverse proxy are you using? + validations: + required: false + + - type: textarea + id: config + attributes: + label: Configuration + description: | + Please provide Semaphore configuration related to your problem - like: + * Config file options + * Environment variables + * WebUI configuration + * Task templates + * Inventories + * Environment + * Repositories + * ... + + validations: + required: false + + - type: textarea + id: additional + attributes: + label: Additional information + description: | + Do you have additional information that could help troubleshoot the problem? + + validations: + required: false diff --git a/.github/ISSUE_TEMPLATES/question.yml b/.github/ISSUE_TEMPLATES/question.yml new file mode 100644 index 00000000..859f58b8 --- /dev/null +++ b/.github/ISSUE_TEMPLATES/question.yml @@ -0,0 +1,45 @@ +--- + +name: Question +description: You have a question on how to use Semaphore +title: "Question: " +labels: ['question', 'triage'] + +body: + - type: markdown + attributes: + value: | + Please make sure to go through these steps **before opening an issue**: + + - [ ] Read the [documentation](https://docs.ansible-semaphore.com/) + - [ ] Read the [troubleshooting guide](https://docs.ansible-semaphore.com/administration-guide/troubleshooting) + - [ ] Read the [documentation regarding manual installations](https://docs.ansible-semaphore.com/administration-guide/installation_manually) if you did install Semaphore that way + + - [ ] Check if there are existing [issues](https://github.com/ansible-semaphore/semaphore/issues) or [discussions](https://github.com/ansible-semaphore/semaphore/discussions) regarding your topic + + - type: textarea + id: question + attributes: + label: Question + validations: + required: true + + - type: dropdown + id: related-to + attributes: + label: Related to + description: | + To what parts of Semaphore is the question related? (if any) + + multiple: true + options: + - Web-Frontend (what users interact with) + - Web-Backend (APIs) + - Service (scheduled tasks, alerts) + - Ansible (task execution) + - Configuration + - Database + - Documentation + - Docker + validations: + required: false From 07ee77d6dbe313eab286b67668f8e58f8648613f Mon Sep 17 00:00:00 2001 From: AnsibleGuy Date: Sun, 6 Aug 2023 11:01:24 +0200 Subject: [PATCH 043/346] feat: config-validation - minor fixes --- util/config.go | 4 +++- util/config_test.go | 23 +++++++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/util/config.go b/util/config.go index 0061c6dd..efe0164b 100644 --- a/util/config.go +++ b/util/config.go @@ -12,10 +12,10 @@ import ( "os/exec" "path" "path/filepath" - "strings" "reflect" "regexp" "strconv" + "strings" "github.com/google/go-github/github" "github.com/gorilla/securecookie" @@ -354,6 +354,8 @@ func setConfigValue(path string, value interface{}) { } } attribute.Set(reflect.ValueOf(value)) + } else { + panic(fmt.Errorf("got non-existent config attribute '%v'", path)) } } diff --git a/util/config_test.go b/util/config_test.go index 7bafcbb9..e0d15940 100644 --- a/util/config_test.go +++ b/util/config_test.go @@ -26,9 +26,7 @@ func TestCastStringToInt(t *testing.T) { if castStringToInt("999") != 999 { t.Error(errMsg) } - if castStringToInt("999") != 999 { - t.Error(errMsg) - } + defer func() { if r := recover(); r == nil { t.Errorf("Cast string => int did not panic on invalid input") @@ -100,7 +98,13 @@ func TestGetConfigValue(t *testing.T) { t.Error("Did not fail on non-existent config attribute!") } }() + getConfigValue("NotExistent") + defer func() { + if r := recover(); r == nil { + t.Error("Did not fail on non-existent config attribute!") + } + }() getConfigValue("Not.Existent") } @@ -148,7 +152,13 @@ func TestSetConfigValue(t *testing.T) { t.Error("Did not fail on non-existent config attribute!") } }() + setConfigValue("NotExistent", "someValue") + defer func() { + if r := recover(); r == nil { + t.Error("Did not fail on non-existent config attribute!") + } + }() setConfigValue("Not.Existent", "someValue") } @@ -195,7 +205,8 @@ func TestLoadConfigEnvironmet(t *testing.T) { t.Error("Setting 'BoltDb.Hostname' was not loaded from environment-vars!") } if Config.MySQL.Hostname == envDbHost || Config.Postgres.Hostname == envDbHost { - t.Error("DB-Hostname was not loaded for inactive DB-dialects!") + // inactive db-dialects could be set as they share the same env-vars; but should be ignored + t.Error("DB-Hostname was loaded for inactive DB-dialects!") } } @@ -258,7 +269,7 @@ func TestValidateConfig(t *testing.T) { ensureConfigValidationFailure(t, "MaxParallelTasks", Config.MaxParallelTasks) Config.MaxParallelTasks = testMaxParallelTasks - Config.CookieHash = "\"0Sn+edH3doJ4EO4Rl49Y0KrxjUkXuVtR5zKHGGWerxQ=\"" + Config.CookieHash = "\"0Sn+edH3doJ4EO4Rl49Y0KrxjUkXuVtR5zKHGGWerxQ=\"" // invalid with quotes (can happen when supplied as env-var) ensureConfigValidationFailure(t, "CookieHash", Config.CookieHash) Config.CookieHash = "!)394340" @@ -267,7 +278,7 @@ func TestValidateConfig(t *testing.T) { Config.CookieHash = "" ensureConfigValidationFailure(t, "CookieHash", Config.CookieHash) - Config.CookieHash = "TQwjDZ5fIQtaIw==" + Config.CookieHash = "TQwjDZ5fIQtaIw==" // valid b64, but too small ensureConfigValidationFailure(t, "CookieHash", Config.CookieHash) Config.CookieHash = testCookieHash From 8870c5cdeaf2e2cf5095c25ed3f5f66ffe3e7dba Mon Sep 17 00:00:00 2001 From: Freebase3941 <19652056+Freebase394@users.noreply.github.com> Date: Fri, 11 Aug 2023 10:43:05 +0100 Subject: [PATCH 044/346] Create pt.js - Portuguese language added Create pt.js - Portuguese language added - By OnlyHardOfficial --- web/src/lang/pt.js | 234 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 web/src/lang/pt.js diff --git a/web/src/lang/pt.js b/web/src/lang/pt.js new file mode 100644 index 00000000..337ea13a --- /dev/null +++ b/web/src/lang/pt.js @@ -0,0 +1,234 @@ +export default { + incorrectUsrPwd: 'Nome de utilizador ou palavra-passe incorretos', + askDeleteUser: 'Tem a certeza de que deseja eliminar este utilizador?', + askDeleteTemp: 'Tem a certeza de que deseja eliminar este modelo?', + askDeleteEnv: 'Tem a certeza de que deseja eliminar este ambiente?', + askDeleteInv: 'Tem a certeza de que deseja eliminar este inventário?', + askDeleteKey: 'Tem a certeza de que deseja eliminar esta chave?', + askDeleteRepo: 'Tem a certeza de que deseja eliminar este repositório?', + askDeleteProj: 'Tem a certeza de que deseja eliminar este projeto?', + askDeleteTMem: 'Tem a certeza de que deseja eliminar este membro da equipa?', + edit: 'Editar', + nnew: 'Novo', + keyFormSshKey: 'Chave SSH', + keyFormLoginPassword: 'Iniciar sessão com palavra-passe', + keyFormNone: 'Nenhum', + incorrectUrl: 'URL incorreto', + username: 'Nome de utilizador', + username_required: 'Nome de utilizador obrigatório', + dashboard: 'Painel de Controlo', + history: 'Histórico', + activity: 'Atividade', + settings: 'Definições', + signIn: 'Iniciar sessão', + password: 'Palavra-passe', + changePassword: 'Alterar palavra-passe', + editUser: 'Editar Utilizador', + newProject: 'Novo Projeto', + close: 'Fechar', + newProject2: 'Novo projeto...', + demoMode: 'MODO DE DEMONSTRAÇÃO', + task: 'Tarefa #{expr}', + youCanRunAnyTasks: 'Pode executar qualquer tarefa', + youHaveReadonlyAccess: 'Tem acesso apenas de leitura', + taskTemplates: 'Modelos de Tarefas', + inventory: 'Inventário', + environment: 'Ambiente', + keyStore: 'Armazenamento de Chaves', + repositories: 'Repositórios', + darkMode: 'Modo Escuro', + team: 'Equipa', + users: 'Utilizadores', + editAccount: 'Editar Conta', + signOut: 'Terminar sessão', + error: 'Erro', + refreshPage: 'Atualizar Página', + relogin: 'Iniciar sessão novamente', + howToFixSigninIssues: 'Como corrigir problemas de início de sessão', + firstlyYouNeedAccessToTheServerWhereSemaphoreRunni: 'Primeiro, precisa de acesso ao servidor onde o Semaphore está a correr.', + executeTheFollowingCommandOnTheServerToSeeExisting: 'Execute o seguinte comando no servidor para ver os utilizadores existentes:', + semaphoreUserList: 'semaphore user list', + youCanChangePasswordOfExistingUser: 'Pode alterar a palavra-passe do utilizador existente:', + semaphoreUserChangebyloginLoginUser123Password: 'semaphore user change-by-login --login user123 --password {makePasswordExample}', + orCreateNewAdminUser: 'Ou criar um novo utilizador administrador:', + close2: 'Fechar', + semaphore: 'SEMAPHORE', + dontHaveAccountOrCantSignIn: 'Não tem uma conta ou não consegue iniciar sessão?', + password2: 'Palavra-passe', + cancel: 'Cancelar', + noViews: 'Sem vistas', + addView: 'Adicionar vista', + editEnvironment: 'Editar Ambiente', + deleteEnvironment: 'Eliminar ambiente', + environment2: 'Ambiente', + newEnvironment: 'Novo Ambiente', + environmentName: 'Nome do Ambiente', + extraVariables: 'Variáveis Extra', + enterExtraVariablesJson: 'Introduza JSON de variáveis extra...', + environmentVariables: 'Variáveis de Ambiente', + enterEnvJson: 'Introduza JSON de ambiente...', + environmentAndExtraVariablesMustBeValidJsonExample: 'O ambiente e as variáveis extra devem ser JSON válidos. Exemplo:', + dashboard2: 'Painel de Controlo', + ansibleSemaphore: 'Semaphore Ansible', + wereSorryButHtmlwebpackpluginoptionstitleDoesntWor: 'Lamentamos, mas <%= htmlWebpackPlugin.options.title %> não funciona corretamente sem JavaScript ativado. Por favor, ative-o para continuar.', + deleteInventory: 'Eliminar inventário', + newInventory: 'Novo Inventário', + name: 'Nome', + userCredentials: 'Credenciais de Utilizador', + sudoCredentialsOptional: 'Credenciais Sudo (Opcional)', + type: 'Tipo', + pathToInventoryFile: 'Caminho para o ficheiro de Inventário', + enterInventory: 'Introduza o inventário...', + staticInventoryExample: 'Exemplo de inventário estático:', + staticYamlInventoryExample: 'Exemplo de inventário YAML estático:', + keyName: 'Nome da Chave', + loginOptional: 'Início de sessão (Opcional)', + usernameOptional: 'Nome de utilizador (Opcional)', + privateKey: 'Chave Privada', + override: 'Substituir', + useThisTypeOfKeyForHttpsRepositoriesAndForPlaybook: 'Utilize este tipo de chave para repositórios HTTPS e para playbooks que utilizem ligações não SSH.', + deleteKey: 'Eliminar chave', + newKey: 'Nova Chave', + create: 'Criar', + newTask: 'Nova Tarefa', + cantDeleteThe: 'Não é possível eliminar o {objectTitle}', + theCantBeDeletedBecauseItUsedByTheResourcesBelow: 'O {objectTitle} não pode ser eliminado porque está a ser utilizado pelos recursos abaixo', + projectName: 'Nome do Projeto', + allowAlertsForThisProject: 'Permitir alertas para este projeto', + telegramChatIdOptional: 'ID de Chat do Telegram (Opcional)', + maxNumberOfParallelTasksOptional: 'Número Máximo de Tarefas Paralelas (Opcional)', + deleteRepository: 'Eliminar repositório', + newRepository: 'Novo Repositório', + urlOrPath: 'URL ou caminho', + absPath: 'caminho abs.', + branch: 'Ramo', + accessKey: 'Chave de Acesso', + credentialsToAccessToTheGitRepositoryItShouldBe: 'Credenciais para aceder ao repositório Git. Deve ser:', + ifYouUseGitOrSshUrl: 'se utilizar o URL Git ou SSH.', + ifYouUseHttpsOrFileUrl: 'se utilizar o URL HTTPS ou ficheiro.', + none: 'Nenhum', + ssh: 'SSH', + deleteProject: 'Eliminar projeto', + save: 'Guardar', + deleteProject2: 'Eliminar Projeto', + onceYouDeleteAProjectThereIsNoGoingBackPleaseBeCer: 'Depois de eliminar um projeto, não há volta atrás. Por favor, tenha a certeza.', + name2: 'Nome *', + title: 'Título *', + description: 'Descrição', + required: 'Obrigatório', + key: '{expr}', + surveyVariables: 'Variáveis de Inquérito', + addVariable: 'Adicionar variável', + columns: 'Colunas', + buildVersion: 'Versão de Compilação', + messageOptional: 'Mensagem (Opcional)', + debug: 'Depuração', + dryRun: 'Execução a seco', + diff: 'Diferença', + advanced: 'Avançado', + hide: 'Ocultar', + pleaseAllowOverridingCliArgumentInTaskTemplateSett: 'Por favor, permita a substituição do argumento CLI nas definições do Modelo de Tarefa', + cliArgsJsonArrayExampleIMyinventoryshPrivatekeythe: 'Argumentos CLI (matriz JSON). Exemplo: [ "-i", "@myinventory.sh", "--private-key=/there/id_rsa", "-vvvv" ]', + started: 'Iniciado', + author: 'Autor', + duration: 'Duração', + stop: 'Parar', + deleteTeamMember: 'Eliminar membro da equipa', + team2: 'Equipa', + newTeamMember: 'Novo Membro da Equipa', + user: 'Utilizador', + administrator: 'Administrador', + definesStartVersionOfYourArtifactEachRunIncrements: 'Define a versão de início do seu artefacto. Cada execução incrementa a versão do artefacto.', + forMoreInformationAboutBuildingSeeThe: 'Para mais informações sobre a construção, consulte a', + taskTemplateReference: 'Referência do Modelo de Tarefa', + definesWhatArtifactShouldBeDeployedWhenTheTaskRun: 'Define qual artefacto deve ser implementado quando a tarefa for executada.', + forMoreInformationAboutDeployingSeeThe: 'Para mais informações sobre a implementação, consulte a', + taskTemplateReference2: 'Referência do Modelo de Tarefa', + definesAutorunSchedule: 'Define o cronograma de autorun.', + forMoreInformationAboutCronSeeThe: 'Para mais informações sobre o cron, consulte a', + cronExpressionFormatReference: 'Referência do Formato de Expressão Cron', + startVersion: 'Versão de Início', + example000: 'Exemplo: 0.0.0', + buildTemplate: 'Modelo de Compilação', + autorun: 'Autorun', + playbookFilename: 'Nome do Ficheiro de Playbook *', + exampleSiteyml: 'Exemplo: site.yml', + inventory2: 'Inventário *', + repository: 'Repositório *', + environment3: 'Ambiente *', + vaultPassword: 'Palavra-passe Vault', + vaultPassword2: 'Palavra-passe Vault', + view: 'Vista', + cron: 'Cron', + iWantToRunATaskByTheCronOnlyForForNewCommitsOfSome: 'Quero executar uma tarefa pelo cron apenas para novos commits de algum repositório', + repository2: 'Repositório', + cronChecksNewCommitBeforeRun: 'O cron verifica um novo commit antes da execução', + readThe: 'Leia o', + toLearnMoreAboutCron: 'para saber mais sobre o Cron.', + suppressSuccessAlerts: 'Suprimir alertas de sucesso', + cliArgsJsonArrayExampleIMyinventoryshPrivatekeythe2: 'Argumentos CLI (matriz JSON). Exemplo: [ "-i", "@myinventory.sh", "--private-key=/there/id_rsa", "-vvvv" ]', + allowCliArgsInTask: 'Permitir argumentos CLI na Tarefa', + docs: 'documentação', + editViews: 'Editar Vistas', + newTemplate: 'Novo modelo', + taskTemplates2: 'Modelos de Tarefas', + all: 'Tudo', + notLaunched: 'Não lançado', + by: 'por {user_name} {formatDate}', + editTemplate: 'Editar Modelo', + newTemplate2: 'Novo Modelo', + deleteTemplate: 'Eliminar modelo', + playbook: 'Playbook', + email: 'E-mail', + adminUser: 'Utilizador Administrador', + sendAlerts: 'Enviar alertas', + deleteUser: 'Eliminar utilizador', + newUser: 'Novo Utilizador', + re: 'Re{getActionButtonTitle}', + teamMember: '{expr} Membro da Equipa', + taskId: 'ID da Tarefa', + version: 'Versão', + status: 'Estado', + start: 'Iniciar', + actions: 'Ações', + alert: 'Alerta', + admin: 'Administrador', + role: 'Função', + external: 'Externo', + time: 'Hora', + path: 'Caminho', + gitUrl: 'URL Git', + sshKey: 'Chave SSH', + lastTask: 'Última Tarefa', + task2: 'Tarefa', + build: 'Compilar', + deploy: 'Implementar', + run: 'Executar', + add: 'Adicionar', + password_required: 'Palavra-passe obrigatória', + name_required: 'Nome obrigatório', + user_credentials_required: 'Credenciais de utilizador obrigatórias', + type_required: 'Tipo obrigatório', + path_required: 'Caminho para o ficheiro de Inventário obrigatório', + private_key_required: 'Chave privada obrigatória', + project_name_required: 'Nome do projeto obrigatório', + repository_required: 'Repositório obrigatório', + branch_required: 'Ramo obrigatório', + key_required: 'Chave obrigatória', + user_required: 'Utilizador obrigatório', + build_version_required: 'Versão de compilação obrigatória', + title_required: 'Título obrigatório', + isRequired: 'é obrigatório', + mustBeInteger: 'Deve ser um número inteiro', + mustBe0OrGreater: 'Deve ser 0 ou superior', + start_version_required: 'Versão de início obrigatória', + playbook_filename_required: 'Nome do ficheiro de playbook obrigatório', + inventory_required: 'Inventário obrigatório', + 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', + Run: 'Executar', +}; From 3208dbfaf793a7d56a897c57280618b6d32e3489 Mon Sep 17 00:00:00 2001 From: Serhii Korobkov Date: Wed, 16 Aug 2023 17:04:49 +0300 Subject: [PATCH 045/346] Issue #1376 --- api/router.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/router.go b/api/router.go index 5c63a86f..84931ad8 100644 --- a/api/router.go +++ b/api/router.go @@ -2,14 +2,14 @@ package api import ( "fmt" - "github.com/ansible-semaphore/semaphore/api/helpers" - "github.com/ansible-semaphore/semaphore/db" "net/http" "os" "strings" + "github.com/ansible-semaphore/semaphore/api/helpers" "github.com/ansible-semaphore/semaphore/api/projects" "github.com/ansible-semaphore/semaphore/api/sockets" + "github.com/ansible-semaphore/semaphore/db" "github.com/ansible-semaphore/semaphore/util" "github.com/gobuffalo/packr" "github.com/gorilla/mux" @@ -131,9 +131,9 @@ func Route() *mux.Router { projectTaskStart.Use(projects.ProjectMiddleware, projects.GetMustCanMiddlewareFor(db.CanRunProjectTasks)) projectTaskStart.Path("/tasks").HandlerFunc(projects.AddTask).Methods("POST") - projectTaskStop := authenticatedAPI.PathPrefix("/tasks").Subrouter() + projectTaskStop := authenticatedAPI.PathPrefix("/project/{project_id}").Subrouter() projectTaskStop.Use(projects.ProjectMiddleware, projects.GetTaskMiddleware, projects.GetMustCanMiddlewareFor(db.CanRunProjectTasks)) - projectTaskStop.HandleFunc("/{task_id}/stop", projects.StopTask).Methods("POST") + projectTaskStop.HandleFunc("/tasks/{task_id}/stop", projects.StopTask).Methods("POST") // // Project resources CRUD From 8e1460b4e5d678496a8528dc5d112c6a4ad0c6d5 Mon Sep 17 00:00:00 2001 From: Dragon Date: Thu, 17 Aug 2023 12:15:15 +0800 Subject: [PATCH 046/346] feat: add chinese lang support. --- web/src/lang/zh.js | 235 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 web/src/lang/zh.js diff --git a/web/src/lang/zh.js b/web/src/lang/zh.js new file mode 100644 index 00000000..dceef9bc --- /dev/null +++ b/web/src/lang/zh.js @@ -0,0 +1,235 @@ +export default { + incorrectUsrPwd: '用户名或密码错误', + askDeleteUser: '您确定要删除此用户吗?', + askDeleteTemp: '您确实要删除此模板吗?', + askDeleteEnv: '您确实要删除此环境吗?', + askDeleteInv: '您确实要删除此主机配置吗?', + askDeleteKey: '您确定要删除此密钥吗?', + askDeleteRepo: '您确定要删除此存储库吗?', + askDeleteProj: '您确定要删除此项目吗?', + askDeleteTMem: '您确定要删除此团队成员吗?', + edit: '编辑', + nnew: '新建', + keyFormSshKey: 'SSH 密钥', + keyFormLoginPassword: '使用密码登录', + keyFormNone: '无', + incorrectUrl: 'URL地址不正确', + username: '用户名', + username_required: '未填写用户名', + dashboard: '控制台', + history: '历史', + activity: '活动', + settings: '设置', + signIn: '登录', + password: '密码', + changePassword: '更改密码', + editUser: '编译用户', + newProject: '新建项目', + close: '关闭', + newProject2: '新建项目...', + demoMode: 'DEMO MODE', + task: '任务 #{expr}', + youCanRunAnyTasks: '您可以运行任何任务', + youHaveReadonlyAccess: '您只有只读访问权限', + taskTemplates: '任务模板', + inventory: '主机配置', + environment: '环境', + keyStore: '密钥库', + repositories: '存储库', + darkMode: '暗色模式', + team: '团队', + users: '用户', + editAccount: '账户编辑', + signOut: '注销', + error: '错误', + refreshPage: '刷新页面', + relogin: '重新登录', + howToFixSigninIssues: '如何解决登录问题', + firstlyYouNeedAccessToTheServerWhereSemaphoreRunni: '首先,您需要登录运行 Semaphore 的服务器。', + executeTheFollowingCommandOnTheServerToSeeExisting: '在服务器上执行以下命令查看现有用户:', + semaphoreUserList: 'semaphore user list', + youCanChangePasswordOfExistingUser: '您可以更改现有用户的密码:', + semaphoreUserChangebyloginLoginUser123Password: 'semaphore user change-by-login --login user123 --password {makePasswordExample}', + orCreateNewAdminUser: '或者创建新的管理员用户:', + close2: '关闭', + semaphore: 'SEMAPHORE', + dontHaveAccountOrCantSignIn: '没有帐户或无法登录?', + password2: '密码', + cancel: '关闭', + noViews: '没有分组视图', + addView: '新增分组视图', + editEnvironment: '编辑环境', + deleteEnvironment: '删除环境', + environment2: '环境', + newEnvironment: '新增环境', + environmentName: '环境名称', + extraVariables: '扩展变量', + enterExtraVariablesJson: '添加额外的Json格式变量...', + environmentVariables: '环境变量', + enterEnvJson: '添加额外的Json格式环境变量...', + environmentAndExtraVariablesMustBeValidJsonExample: '环境变量和额外变量必须是有效的 JSON。例如:', + dashboard2: '控制台', + ansibleSemaphore: 'Ansible Semaphore', + wereSorryButHtmlwebpackpluginoptionstitleDoesntWor: '抱歉,如果未启用 JavaScript,<%= htmlWebpackPlugin.options.title %> 将无法正常工作。请启用它以继续。', + deleteInventory: '删除主机配置', + newInventory: '新增主机配置', + name: '名称', + userCredentials: '用户凭据', + sudoCredentialsOptional: 'Sudo 凭据(可选)', + type: '类型', + pathToInventoryFile: '主机配置文件路径', + enterInventory: '编辑主机配置...', + staticInventoryExample: '静态主机配置示例:', + staticYamlInventoryExample: '静态 YAML 格式主机配置示例:', + keyName: '凭据名称', + loginOptional: '登录名 (可选)', + usernameOptional: '用户名 (可选)', + privateKey: '私钥', + override: '覆盖', + useThisTypeOfKeyForHttpsRepositoriesAndForPlaybook: '对于 HTTPS 存储库和使用非 SSH 连接的 playbook,请使用此类密钥。', + deleteKey: '删除凭据', + newKey: '新增凭据', + create: '创建', + newTask: '创建任务', + cantDeleteThe: '无法删除 {objectTitle}', + theCantBeDeletedBecauseItUsedByTheResourcesBelow: '无法删除 {objectTitle},因为它已被如下资源使用', + projectName: '项目名称', + allowAlertsForThisProject: '在此项目开启通知', + telegramChatIdOptional: 'Telegram Chat ID (可选)', + maxNumberOfParallelTasksOptional: '最大并行任务数 (可选)', + deleteRepository: '删除存储库', + newRepository: '新增存储库', + urlOrPath: 'URL 或路径', + absPath: 'abs. path', + branch: '分支', + accessKey: '访问凭证', + credentialsToAccessToTheGitRepositoryItShouldBe: '访问 Git 存储库的凭据。它应该是:', + ifYouUseGitOrSshUrl: '如果您使用 Git 或 SSH URL.', + ifYouUseHttpsOrFileUrl: '如果您使用 HTTPS 或文件 URL.', + none: 'None', + ssh: 'SSH', + deleteProject: '删除项目', + save: '保存', + deleteProject2: '删除项目', + onceYouDeleteAProjectThereIsNoGoingBackPleaseBeCer: '一旦删除项目,就无法恢复!!!', + name2: '名称 *', + title: '标题 *', + description: '说明', + required: '必需', + key: '{expr}', + surveyVariables: '列出变量', + addVariable: '添加变量', + columns: '列', + buildVersion: '编译版本', + messageOptional: '说明 (可选)', + debug: '调试模式', + dryRun: 'Dry Run', + 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', + role: 'Role', + 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', + +}; From 5b2632ca6422e75e3d271051f86b67572b99c771 Mon Sep 17 00:00:00 2001 From: Dragon Date: Thu, 17 Aug 2023 12:54:37 +0800 Subject: [PATCH 047/346] feat: add chinese lang support. --- web/src/lang/zh.js | 198 ++++++++++++++++++++++----------------------- 1 file changed, 99 insertions(+), 99 deletions(-) diff --git a/web/src/lang/zh.js b/web/src/lang/zh.js index dceef9bc..c7c06d98 100644 --- a/web/src/lang/zh.js +++ b/web/src/lang/zh.js @@ -125,111 +125,111 @@ export default { debug: '调试模式', dryRun: 'Dry Run', 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', + advanced: '高级选项', + hide: '隐藏', + pleaseAllowOverridingCliArgumentInTaskTemplateSett: '请在设置中开启 “允许在任务中自定义 CLI 参数”', + cliArgsJsonArrayExampleIMyinventoryshPrivatekeythe: 'CLI 测试 (JSON 数组). 例如: [ "-i", "@myinventory.sh", "--private-key=/there/id_rsa", "-vvvv" ]', + started: '已启动', + author: '关联用户', + duration: '说明', + stop: '停止', + deleteTeamMember: '删除团队成员', + team2: '团队', + newTeamMember: '新增团队成员', + user: '用户', + administrator: '管理员', + definesStartVersionOfYourArtifactEachRunIncrements: '定义起始版本,每次运行都会增加版本。', + forMoreInformationAboutBuildingSeeThe: '有关构建的更多信息,请参阅', + taskTemplateReference: '任务模板参考', + definesWhatArtifactShouldBeDeployedWhenTheTaskRun: '定义任务运行时应部署哪些产物。', + forMoreInformationAboutDeployingSeeThe: '有关部署的更多信息,请参阅', + taskTemplateReference2: '任务模板参考', + definesAutorunSchedule: '定义计划任务', + forMoreInformationAboutCronSeeThe: '有关 cron 的更多信息,请参阅', + cronExpressionFormatReference: 'Cron 表达式格式参考', + startVersion: '开始版本', + example000: '例如: 0.0.0', + buildTemplate: '构建模板', + autorun: '自动运行', + playbookFilename: 'Playbook 文件名称 *', + exampleSiteyml: '例如: site.yml', + inventory2: '主机配置 *', + repository: '存储库 *', + environment3: '环境 *', + vaultPassword: 'Vault 密码', + vaultPassword2: 'Vault 密码', 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.', + iWantToRunATaskByTheCronOnlyForForNewCommitsOfSome: '我想通过 cron 运行一个任务,仅用于某些存储库的新提交', + repository2: '存储库', + cronChecksNewCommitBeforeRun: 'Cron 在运行前检查新的提交', + readThe: '阅读', + toLearnMoreAboutCron: '了解有关 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', + cliArgsJsonArrayExampleIMyinventoryshPrivatekeythe2: 'CLI 参数 (JSON 数组格式). 例如: [ "-i", "@myinventory.sh", "--private-key=/there/id_rsa", "-vvvv" ]', + allowCliArgsInTask: '允许任务中自定义 CLI 参数', + docs: '文档', + editViews: '编辑视图', + newTemplate: '新增模板', + taskTemplates2: '任务模板', + all: '全部', + notLaunched: '未启动', by: 'by {user_name} {formatDate}', - editTemplate: 'Edit Template', - newTemplate2: 'New Template', - deleteTemplate: 'Delete template', + editTemplate: '编辑模板', + newTemplate2: '新建模板', + deleteTemplate: '删除模板', playbook: 'Playbook', - email: 'Email', - adminUser: 'Admin user', - sendAlerts: 'Send alerts', - deleteUser: 'Delete user', - newUser: 'New User', + email: '邮箱', + adminUser: '管理员用户', + sendAlerts: '发送通知', + deleteUser: '删除用户', + newUser: '新增用户', re: 'Re{getActionButtonTitle}', teamMember: '{expr} Team Member', - taskId: 'Task ID', - version: 'Version', - status: 'Status', - start: 'Start', - actions: 'Actions', - alert: 'Alert', - admin: 'Admin', - role: 'Role', - 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', + taskId: '任务 ID', + version: '版本', + status: '状态', + start: '启动', + actions: '任务', + alert: '通知', + admin: '管理员', + role: '角色', + external: '扩展', + time: '时间', + path: '路径', + gitUrl: 'Git 存储库链接', + sshKey: 'SSH 密钥', + lastTask: '最后一次任务', + task2: '任务', + build: '编译', + deploy: '部署', + run: '执行', + add: '添加', + password_required: '密码是必填项', + name_required: '名称是必填项', + user_credentials_required: '用户凭证是必填项', + type_required: '类型是是必填项', + path_required: '主机配置文件路径是必填项', + private_key_required: '私钥是是必填项', + project_name_required: '项目名称是必填项', + repository_required: '存储库地址是必填项', + branch_required: '分支是必填项', + key_required: 'Key 是必填项', + user_required: '用户是必填项', + build_version_required: '编译版本是必填项', + title_required: '标题是必填项', + isRequired: '是必填项', + mustBeInteger: '必须是整数', + mustBe0OrGreater: '必须大于或等于 0', + start_version_required: '开始版本是必填项', + playbook_filename_required: 'Playbook 文件名称是必填项', + inventory_required: '主机配置是必填项', + environment_required: '环境配置是必填项', + email_required: '邮箱是必填项', + build_template_required: '编译模板是必填项', + Task: '任务', + Build: '编译', + Deploy: '部署', + Run: '运行', }; From d9a0a4d0fae8627b3d0a1bab5ab034666d3c8e00 Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Sat, 26 Aug 2023 13:16:25 +0200 Subject: [PATCH 048/346] fix(be): do not expire session for demo mode --- api/auth.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/auth.go b/api/auth.go index 4a708219..e7a42731 100644 --- a/api/auth.go +++ b/api/auth.go @@ -61,7 +61,7 @@ func authenticationHandler(w http.ResponseWriter, r *http.Request) bool { return false } - if time.Since(session.LastActive).Hours() > 7*24 { + if time.Since(session.LastActive).Hours() > 7*24 && !util.Config.DemoMode { // more than week old unused session // destroy. if err := helpers.Store(r).ExpireSession(userID, sessionID); err != nil { @@ -119,7 +119,7 @@ func authenticationWithStore(next http.Handler) http.Handler { store := helpers.Store(r) var ok bool - + db.StoreSession(store, r.URL.String(), func() { ok = authenticationHandler(w, r) }) From b522169832dd5eee2bc2a30047db1edc02d116a0 Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Sat, 26 Aug 2023 18:48:16 +0200 Subject: [PATCH 049/346] test: check role permissions --- api/projects/project.go | 34 +++++++++++++--------------------- db/ProjectUser.go | 12 ++++++++++-- db/ProjectUser_test.go | 15 +++++++++++++++ 3 files changed, 38 insertions(+), 23 deletions(-) create mode 100644 db/ProjectUser_test.go diff --git a/api/projects/project.go b/api/projects/project.go index 9a407550..866a14af 100644 --- a/api/projects/project.go +++ b/api/projects/project.go @@ -24,7 +24,7 @@ func ProjectMiddleware(next http.Handler) http.Handler { } // check if user in project's team - _, err = helpers.Store(r).GetProjectUser(projectID, user.ID) + projectUser, err := helpers.Store(r).GetProjectUser(projectID, user.ID) if err != nil { helpers.WriteError(w, err) @@ -38,6 +38,7 @@ func ProjectMiddleware(next http.Handler) http.Handler { return } + context.Set(r, "projectUserRole", projectUser.Role) context.Set(r, "project", project) next.ServeHTTP(w, r) }) @@ -47,27 +48,12 @@ func ProjectMiddleware(next http.Handler) http.Handler { func GetMustCanMiddlewareFor(permissions db.ProjectUserPermission) mux.MiddlewareFunc { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - project := context.Get(r, "project").(db.Project) user := context.Get(r, "user").(*db.User) + projectUserRole := context.Get(r, "projectUserRole").(db.ProjectUserRole) - if !user.Admin { - // check if user in project's team - projectUser, err := helpers.Store(r).GetProjectUser(project.ID, user.ID) - - if err == db.ErrNotFound { - w.WriteHeader(http.StatusForbidden) - return - } - - if err != nil { - helpers.WriteError(w, err) - return - } - - if r.Method != "GET" && r.Method != "HEAD" && !projectUser.Can(permissions) { - w.WriteHeader(http.StatusForbidden) - return - } + if !user.Admin && r.Method != "GET" && r.Method != "HEAD" && !projectUserRole.Can(permissions) { + w.WriteHeader(http.StatusForbidden) + return } next.ServeHTTP(w, r) @@ -77,7 +63,13 @@ func GetMustCanMiddlewareFor(permissions db.ProjectUserPermission) mux.Middlewar // GetProject returns a project details func GetProject(w http.ResponseWriter, r *http.Request) { - helpers.WriteJSON(w, http.StatusOK, context.Get(r, "project")) + var project struct { + db.Project + UserPermissions db.ProjectUserPermission `json:"userPermissions"` + } + project.Project = context.Get(r, "project").(db.Project) + project.UserPermissions = context.Get(r, "projectUserRole").(db.ProjectUserRole).GetPermissions() + helpers.WriteJSON(w, http.StatusOK, project) } // UpdateProject saves updated project details to the database diff --git a/db/ProjectUser.go b/db/ProjectUser.go index 6b561abe..ce768170 100644 --- a/db/ProjectUser.go +++ b/db/ProjectUser.go @@ -19,7 +19,7 @@ const ( ) var rolePermissions = map[ProjectUserRole]ProjectUserPermission{ - ProjectOwner: CanRunProjectTasks | CanUpdateProject | CanManageProjectResources, + ProjectOwner: CanRunProjectTasks | CanManageProjectResources | CanUpdateProject, ProjectManager: CanRunProjectTasks | CanManageProjectResources, ProjectTaskRunner: CanRunProjectTasks, ProjectGuest: 0, @@ -39,5 +39,13 @@ type ProjectUser struct { func (u *ProjectUser) Can(permissions ProjectUserPermission) bool { userPermissions := rolePermissions[u.Role] - return (userPermissions & userPermissions) == permissions + return (userPermissions & permissions) == permissions +} + +func (r ProjectUserRole) Can(permissions ProjectUserPermission) bool { + return (rolePermissions[r] & permissions) == permissions +} + +func (r ProjectUserRole) GetPermissions() ProjectUserPermission { + return rolePermissions[r] } diff --git a/db/ProjectUser_test.go b/db/ProjectUser_test.go new file mode 100644 index 00000000..08be5e95 --- /dev/null +++ b/db/ProjectUser_test.go @@ -0,0 +1,15 @@ +package db + +import ( + "testing" +) + +func TestProjectUsers_RoleCan(t *testing.T) { + if !ProjectManager.Can(CanManageProjectResources) { + t.Fatal() + } + + if ProjectManager.Can(CanUpdateProject) { + t.Fatal() + } +} From 4398544e91d59c8daed3023279ef86f2b2ea9266 Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Sat, 26 Aug 2023 20:43:42 +0200 Subject: [PATCH 050/346] feat(fe): handle permissions on UI --- api-docs.yml | 20 +++ api/projects/project.go | 20 ++- api/router.go | 12 +- api/user.go | 11 +- web/src/App.vue | 218 +++++++++++++------------ web/src/components/ItemListPageBase.js | 9 + web/src/components/TeamMemberForm.vue | 16 +- web/src/lib/constants.js | 21 +++ web/src/views/project/Activity.vue | 15 +- web/src/views/project/Environment.vue | 1 + web/src/views/project/History.vue | 7 +- web/src/views/project/Inventory.vue | 1 + web/src/views/project/Keys.vue | 1 + web/src/views/project/Repositories.vue | 1 + web/src/views/project/Team.vue | 23 +-- web/src/views/project/Templates.vue | 10 +- 16 files changed, 246 insertions(+), 140 deletions(-) diff --git a/api-docs.yml b/api-docs.yml index b4c8b1fb..1c182f24 100644 --- a/api-docs.yml +++ b/api-docs.yml @@ -928,6 +928,26 @@ paths: 204: description: Project deleted + + /project/{project_id}/permissions: + parameters: + - $ref: "#/parameters/project_id" + get: + tags: + - project + summary: Fetch permissions of the current user for project + responses: + 200: + description: Permissions + schema: + type: object + properties: + role: + type: string + permissions: + type: number + + /project/{project_id}/events: parameters: - $ref: '#/parameters/project_id' diff --git a/api/projects/project.go b/api/projects/project.go index 866a14af..6dcadede 100644 --- a/api/projects/project.go +++ b/api/projects/project.go @@ -44,8 +44,8 @@ func ProjectMiddleware(next http.Handler) http.Handler { }) } -// GetMustCanMiddlewareFor ensures that the user has administrator rights -func GetMustCanMiddlewareFor(permissions db.ProjectUserPermission) mux.MiddlewareFunc { +// GetMustCanMiddleware ensures that the user has administrator rights +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) @@ -63,13 +63,17 @@ func GetMustCanMiddlewareFor(permissions db.ProjectUserPermission) mux.Middlewar // GetProject returns a project details func GetProject(w http.ResponseWriter, r *http.Request) { - var project struct { - db.Project - UserPermissions db.ProjectUserPermission `json:"userPermissions"` + helpers.WriteJSON(w, http.StatusOK, context.Get(r, "project")) +} + +func GetUserRole(w http.ResponseWriter, r *http.Request) { + var permissions struct { + Role db.ProjectUserRole `json:"role"` + Permissions db.ProjectUserPermission `json:"permissions"` } - project.Project = context.Get(r, "project").(db.Project) - project.UserPermissions = context.Get(r, "projectUserRole").(db.ProjectUserRole).GetPermissions() - helpers.WriteJSON(w, http.StatusOK, project) + permissions.Role = context.Get(r, "projectUserRole").(db.ProjectUserRole) + permissions.Permissions = permissions.Role.GetPermissions() + helpers.WriteJSON(w, http.StatusOK, permissions) } // UpdateProject saves updated project details to the database diff --git a/api/router.go b/api/router.go index 5c63a86f..3a055882 100644 --- a/api/router.go +++ b/api/router.go @@ -128,17 +128,19 @@ func Route() *mux.Router { // // Start and Stop tasks projectTaskStart := authenticatedAPI.PathPrefix("/project/{project_id}").Subrouter() - projectTaskStart.Use(projects.ProjectMiddleware, projects.GetMustCanMiddlewareFor(db.CanRunProjectTasks)) + projectTaskStart.Use(projects.ProjectMiddleware, projects.GetMustCanMiddleware(db.CanRunProjectTasks)) projectTaskStart.Path("/tasks").HandlerFunc(projects.AddTask).Methods("POST") projectTaskStop := authenticatedAPI.PathPrefix("/tasks").Subrouter() - projectTaskStop.Use(projects.ProjectMiddleware, projects.GetTaskMiddleware, projects.GetMustCanMiddlewareFor(db.CanRunProjectTasks)) + projectTaskStop.Use(projects.ProjectMiddleware, projects.GetTaskMiddleware, projects.GetMustCanMiddleware(db.CanRunProjectTasks)) projectTaskStop.HandleFunc("/{task_id}/stop", projects.StopTask).Methods("POST") // // Project resources CRUD projectUserAPI := authenticatedAPI.PathPrefix("/project/{project_id}").Subrouter() - projectUserAPI.Use(projects.ProjectMiddleware, projects.GetMustCanMiddlewareFor(db.CanManageProjectResources)) + projectUserAPI.Use(projects.ProjectMiddleware, projects.GetMustCanMiddleware(db.CanManageProjectResources)) + + projectUserAPI.Path("/role").HandlerFunc(projects.GetUserRole).Methods("GET", "HEAD") projectUserAPI.Path("/events").HandlerFunc(getAllEvents).Methods("GET", "HEAD") projectUserAPI.HandleFunc("/events/last", getLastEvents).Methods("GET", "HEAD") @@ -173,14 +175,14 @@ func Route() *mux.Router { // // Updating and deleting project projectAdminAPI := authenticatedAPI.Path("/project/{project_id}").Subrouter() - projectAdminAPI.Use(projects.ProjectMiddleware, projects.GetMustCanMiddlewareFor(db.CanUpdateProject)) + projectAdminAPI.Use(projects.ProjectMiddleware, projects.GetMustCanMiddleware(db.CanUpdateProject)) projectAdminAPI.Methods("PUT").HandlerFunc(projects.UpdateProject) projectAdminAPI.Methods("DELETE").HandlerFunc(projects.DeleteProject) // // Manage project users projectAdminUsersAPI := authenticatedAPI.PathPrefix("/project/{project_id}").Subrouter() - projectAdminUsersAPI.Use(projects.ProjectMiddleware, projects.GetMustCanMiddlewareFor(db.CanManageProjectUsers)) + projectAdminUsersAPI.Use(projects.ProjectMiddleware, projects.GetMustCanMiddleware(db.CanManageProjectUsers)) projectAdminUsersAPI.Path("/users").HandlerFunc(projects.AddUser).Methods("POST") projectUserManagement := projectAdminUsersAPI.PathPrefix("/users").Subrouter() diff --git a/api/user.go b/api/user.go index 41e8fa4d..dc9e7222 100644 --- a/api/user.go +++ b/api/user.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "github.com/ansible-semaphore/semaphore/api/helpers" "github.com/ansible-semaphore/semaphore/db" + "github.com/ansible-semaphore/semaphore/util" "github.com/gorilla/context" "github.com/gorilla/mux" "io" @@ -18,7 +19,15 @@ func getUser(w http.ResponseWriter, r *http.Request) { return } - helpers.WriteJSON(w, http.StatusOK, context.Get(r, "user")) + var user struct { + db.User + CanCreateProject bool `json:"can_create_project"` + } + + user.User = *context.Get(r, "user").(*db.User) + user.CanCreateProject = user.Admin || util.Config.NonAdminCanCreateProject + + helpers.WriteJSON(w, http.StatusOK, user) } func getAPITokens(w http.ResponseWriter, r *http.Request) { diff --git a/web/src/App.vue b/web/src/App.vue index 98a5b4b4..a4b23904 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -1,56 +1,56 @@ {{ template ? template.name : null }} mdi-chevron-right @@ -59,8 +59,8 @@@@ -71,61 +71,61 @@ mdi-close {{ snackbarText }} {{ $t('close') }} {{ getProjectInitials(project) }} @@ -135,6 +135,7 @@{{ project.name }} +{{ userRole.role }} @@ -145,16 +146,16 @@ - {{ getProjectInitials(item) }} @@ -162,7 +163,7 @@{{ item.name }} + @@ -283,9 +284,9 @@ > mdi-plus mdi-account @@ -341,14 +342,14 @@--> - - - + + + - - - - + + + + @@ -365,23 +366,27 @@ - + @@ -389,12 +394,12 @@ 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 @@ @@ -421,6 +426,7 @@ .v-dialog > .v-card > .v-card__title { flex-wrap: nowrap; overflow: hidden; + & * { white-space: nowrap; } @@ -489,7 +495,7 @@ } & > td:first-child { - //font-weight: bold !important; + //font-weight: bold !important; } } @@ -560,6 +566,7 @@ export default { return { drawer: null, user: null, + userRole: 0, systemInfo: null, state: 'loading', snackbar: false, @@ -580,8 +587,8 @@ export default { watch: { async projects(val) { if (val.length === 0 - && this.$route.path.startsWith('/project/') - && this.$route.path !== '/project/new') { + && this.$route.path.startsWith('/project/') + && this.$route.path !== '/project/new') { await this.$router.push({ path: '/project/new' }); } }, @@ -783,8 +790,8 @@ export default { // try to find project and switch to it if URL not pointing to any project if (this.$route.path === '/' - || this.$route.path === '/project' - || (this.$route.path.startsWith('/project/'))) { + || this.$route.path === '/project' + || (this.$route.path.startsWith('/project/'))) { await this.trySelectMostSuitableProject(); } @@ -812,7 +819,7 @@ export default { } if ((projectId == null || !this.projects.some((p) => p.id === projectId)) - && localStorage.getItem('projectId')) { + && localStorage.getItem('projectId')) { projectId = parseInt(localStorage.getItem('projectId'), 10); } @@ -826,10 +833,17 @@ export default { }, async selectProject(projectId) { + this.userRole = (await axios({ + method: 'get', + url: `/api/project/${projectId}/role`, + responseType: 'json', + })).data; + localStorage.setItem('projectId', projectId); if (this.projectId === projectId) { return; } + await this.$router.push({ path: `/project/${projectId}` }); }, @@ -861,7 +875,7 @@ export default { getProjectColor(projectData) { const projectIndex = this.projects.length - - this.projects.findIndex((p) => p.id === projectData.id); + - this.projects.findIndex((p) => p.id === projectData.id); return PROJECT_COLORS[projectIndex % PROJECT_COLORS.length]; }, diff --git a/web/src/components/ItemListPageBase.js b/web/src/components/ItemListPageBase.js index b37cb373..c28e7682 100644 --- a/web/src/components/ItemListPageBase.js +++ b/web/src/components/ItemListPageBase.js @@ -5,6 +5,7 @@ import YesNoDialog from '@/components/YesNoDialog.vue'; import ObjectRefsDialog from '@/components/ObjectRefsDialog.vue'; import { getErrorMessage } from '@/lib/error'; +import { USER_PERMISSIONS } from '@/lib/constants'; export default { components: { @@ -16,6 +17,7 @@ export default { props: { projectId: Number, userId: Number, + userPermissions: Number, }, data() { @@ -29,6 +31,8 @@ export default { itemRefs: null, itemRefsDialog: null, + + USER_PERMISSIONS, }; }, @@ -38,6 +42,11 @@ export default { }, methods: { + can(permission) { + // eslint-disable-next-line no-bitwise + return (this.userPermissions & permission) === permission; + }, + // eslint-disable-next-line no-empty-function async beforeLoadItems() { }, diff --git a/web/src/components/TeamMemberForm.vue b/web/src/components/TeamMemberForm.vue index ba382024..45f5dd23 100644 --- a/web/src/components/TeamMemberForm.vue +++ b/web/src/components/TeamMemberForm.vue @@ -22,15 +22,22 @@ :disabled="formSaving" > -
+ diff --git a/web/src/plugins/i18.js b/web/src/plugins/i18.js index 135a4a71..9dfa7b8e 100644 --- a/web/src/plugins/i18.js +++ b/web/src/plugins/i18.js @@ -3,7 +3,13 @@ import VueI18n from 'vue-i18n'; import { messages } from '../lang'; Vue.use(VueI18n); -const locale = navigator.language.split('-')[0]; + +let locale = localStorage.getItem('lang'); + +if (!locale) { + locale = navigator.language.split('-')[0]; +} + export default new VueI18n({ fallbackLocale: 'en', locale, diff --git a/web/src/views/project/Activity.vue b/web/src/views/project/Activity.vue index 55036462..6cbcddf0 100644 --- a/web/src/views/project/Activity.vue +++ b/web/src/views/project/Activity.vue @@ -12,7 +12,8 @@ v-if="can(USER_PERMISSIONS.updateProject)" key="settings" :to="`/project/${projectId}/settings`" - >{{ $t('settings') }} + > + {{ $t('settings') }} diff --git a/web/src/views/project/New.vue b/web/src/views/project/New.vue index a2aa3bf5..8c61f6c7 100644 --- a/web/src/views/project/New.vue +++ b/web/src/views/project/New.vue @@ -8,10 +8,14 @@ @@ -29,6 +33,7 @@ export default { components: { ProjectForm }, data() { return { + demoProject: false, }; }, @@ -45,6 +50,12 @@ export default { }, async createProject() { + this.demoProject = false; + await this.$refs.editForm.save(); + }, + + async createDemoProject() { + this.demoProject = true; await this.$refs.editForm.save(); }, }, From b31033323a618da963e2346c0b51a0524eefae12 Mon Sep 17 00:00:00 2001 From: Denis Gukov-+ +Create Demo Project +{{ $t('create') }} Date: Sun, 17 Sep 2023 15:17:15 +0200 Subject: [PATCH 111/346] feat: restrict manager permissions --- db/ProjectUser.go | 2 +- web/src/App.vue | 1 + web/src/components/UserForm.vue | 10 +++++++--- web/src/views/Users.vue | 1 + 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/db/ProjectUser.go b/db/ProjectUser.go index fa337351..a5a75714 100644 --- a/db/ProjectUser.go +++ b/db/ProjectUser.go @@ -20,7 +20,7 @@ const ( var rolePermissions = map[ProjectUserRole]ProjectUserPermission{ ProjectOwner: CanRunProjectTasks | CanManageProjectResources | CanUpdateProject | CanManageProjectUsers, - ProjectManager: CanRunProjectTasks | CanManageProjectResources | CanManageProjectUsers, + ProjectManager: CanRunProjectTasks | CanManageProjectResources, ProjectTaskRunner: CanRunProjectTasks, ProjectGuest: 0, } diff --git a/web/src/App.vue b/web/src/App.vue index 8f289955..7e6c1d0e 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -34,6 +34,7 @@ @error="onError" :need-save="needSave" :need-reset="needReset" + :is-admin="user.admin" /> diff --git a/web/src/components/UserForm.vue b/web/src/components/UserForm.vue index c001ce54..f5efc1f9 100644 --- a/web/src/components/UserForm.vue +++ b/web/src/components/UserForm.vue @@ -24,7 +24,7 @@ :label="$t('username')" :rules="[v => !!v || $t('user_name_required')]" required - :disabled="formSaving" + :disabled="item.external || formSaving" > 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 - @@ -366,6 +369,7 @@ :projectId="projectId" :userPermissions="userRole.permissions" :userId="user ? user.id : null" + :isAdmin="user ? user.admin : false" >{{ user.name }} ++ {{ user.name }} + admin +-+ From f1c872d0d21fd8651e0c2933c1093a3728c7e89b Mon Sep 17 00:00:00 2001 From: Denis GukovDate: 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 @@ @@ -137,9 +136,6 @@ export default { getEventName() { return 'i-repositories'; }, - isUserAdmin() { - return (this.items.find((x) => x.id === this.userId) || {}).admin; - }, }, }; From b2cecc79fbb25661ada3465180f9db59fbea8e55 Mon Sep 17 00:00:00 2001 From: Denis Gukov From 1fc842c32145983d3eec3d8cb8b32d559e0b3aef Mon Sep 17 00:00:00 2001 From: Denis GukovDate: Sun, 17 Sep 2023 16:30:39 +0200 Subject: [PATCH 114/346] feat(ui): admin badge --- web/src/App.vue | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/web/src/App.vue b/web/src/App.vue index 353c22a8..f99a2ad3 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -314,15 +314,19 @@ v-on="on" > - mdi-account +mdi-account + + {{ user.name }} - +admin + admin +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 @@ - From cb2bcd8f0b63a92cbb71aa9c24726a5f4381bbfc Mon Sep 17 00:00:00 2001 From: Denis Gukovadmin +admin 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 GukovDate: 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 + ++ + + 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') }} ++ + + + ++ {{ $t('dashboard') }} ++ +{{ $t('history') }} +{{ $t('activity') }} +{{ $t('settings') }} +Billing +Soon + Soon ++Billing Soon {{ $t('history') }} {{ $t('activity') }} {{ $t('settings') }} +Billing Soon From 384108513dc47467265255827537ffbddba16178 Mon Sep 17 00:00:00 2001 From: Denis Gukov- From 7e7a543e0397c3fad8212202f88a10f7d2fcb4ba Mon Sep 17 00:00:00 2001 From: Denis GukovDate: 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.+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{{ formError }} + ++ + ++ +++ ++ ++ ++ + } 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{{ formError }} + ++ } 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 @@ + ++ The {{ objectTitle }} can't be deleted because it used by the resources below + +++++ +mdi-{{ s.icon }} {{ s.title }}: ++ ++{{ t.name }} + ++ + + 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 @@ + +{{ formError }} + ++ + + + + + 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 @@ + +{{ formError }} ++ + ++ ++++ + + + + ++ 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 @@ + ++ The {{ objectTitle }} can't be deleted because it used by the resources below + +++++ +mdi-{{ s.icon }} {{ s.title }}: ++ ++{{ t.name }} + +++ + 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 @@ + ++ + + ++ + + + + + + ++ ExtractValue ++ New Extracted Value ++ + {{ item.name }} + + + +{{ item.value_source }}
+ + +{{ item.body_data_type }}
+ + +{{ item.key }}
+ + +{{ item.variable }}
+ + + +++ ++ + +mdi-delete ++ +mdi-pencil +++ + 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 @@ + ++ + + ++ + + + + + + ++ Extractor + + + + + + + + + + + + + ++ New Extractor ++ + +{{ item.name }} + + + + +++ ++ + +mdi-delete ++ +mdi-pencil +++ + 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 @@ + ++ + + ++ + + + + + + + ++ Matcher ++ New Matcher ++ + {{ item.name }} + + + +{{ item.match_type }}
+ + +{{ item.method }}
+ + + {{ item.body_data_type || "N/A" }} + + +{{ item.key }}
+ + +{{ item.value }}
+ + + +++ ++ + +mdi-delete ++ +mdi-pencil +++ + From 0443699c96dc5d7ad59a48404ba363728ccea503 Mon Sep 17 00:00:00 2001 From: Andreas Marschke+ + + ++ + + + + + + + ++ Webhook ++ New Webhook ++ + +{{ item.name }} + + + ++ + + +{{ templates.find((t) => t.id === item.template_id).name }}
+++ ++ + +mdi-delete ++ +mdi-pencil +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 @@ {{ item.variable }}
-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 @@ - +- Extractor - - - - - - - - - - - - ++ + Webhooks + +mdi-chevron-right + +mdi-chevron-right + +diff --git a/web/src/views/project/IntegrationMatcher.vue b/web/src/views/project/IntegrationMatcher.vue index a75fce00..675d7fc4 100644 --- a/web/src/views/project/IntegrationMatcher.vue +++ b/web/src/views/project/IntegrationMatcher.vue @@ -10,7 +10,7 @@ > @@ -95,12 +95,11 @@ import ItemListPageBase from '@/components/ItemListPageBase'; import IntegrationExtractorsBase from '@/components/IntegrationExtractorsBase'; -import IntegrationExtractorBase from '@/components/IntegrationExtractorBase'; import IntegrationMatcherForm from '@/components/IntegrationMatcherForm.vue'; export default { - mixins: [ItemListPageBase, IntegrationExtractorsBase, IntegrationExtractorBase], + mixins: [ItemListPageBase, IntegrationExtractorsBase], components: { IntegrationMatcherForm }, computed: { projectId() { @@ -115,12 +114,6 @@ export default { } return this.$route.params.integrationId; }, - extractorId() { - if (/^-?\d+$/.test(this.$route.params.extractorId)) { - return parseInt(this.$route.params.extractorId, 10); - } - return this.$route.params.extractorId; - }, }, methods: { allowActions() { @@ -164,10 +157,10 @@ export default { }]; }, getItemsUrl() { - return `/api/project/${this.projectId}/integrations/${this.integrationId}/extractors/${this.extractorId}/matchers`; + return `/api/project/${this.projectId}/integrations/${this.integrationId}/matchers`; }, getSingleItemUrl() { - return `/api/project/${this.projectId}/integrations/${this.integrationId}/extractors/${this.extractorId}/matchers/${this.itemId}`; + return `/api/project/${this.projectId}/integrations/${this.integrationId}/matchers/${this.itemId}`; }, getEventName() { return 'w-integration-matcher'; From 2af6cdf07c1256f7c6a45d2fb7a1a357b1836ad5 Mon Sep 17 00:00:00 2001 From: fiftin Date: Wed, 6 Mar 2024 15:07:33 +0100 Subject: [PATCH 303/346] feat(ui): breadcrubs --- web/src/views/project/IntegrationExtractorCrumb.vue | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/web/src/views/project/IntegrationExtractorCrumb.vue b/web/src/views/project/IntegrationExtractorCrumb.vue index 397c9933..85d7d83f 100644 --- a/web/src/views/project/IntegrationExtractorCrumb.vue +++ b/web/src/views/project/IntegrationExtractorCrumb.vue @@ -2,18 +2,18 @@ From ab90e98d3b157a1864e9485a29534c91a3ec83ba Mon Sep 17 00:00:00 2001 From: fiftin+ + - {{ integration.name }} + :to="`/project/${projectId}/integrations/`" + > + Integrations mdi-chevron-right - -mdi-chevron-right - +Date: Wed, 6 Mar 2024 15:51:59 +0100 Subject: [PATCH 304/346] refactor: rename func param --- .dredd/hooks/capabilities.go | 1 - .dredd/hooks/helpers.go | 4 +- db/Event.go | 1 - db/bolt/BoltDb.go | 4 +- db/bolt/webhook.go | 36 +++++++-------- db/sql/SqlDb.go | 2 +- db/sql/integration.go | 46 +++++++++---------- .../project/IntegrationExtractorCrumb.vue | 16 +------ 8 files changed, 48 insertions(+), 62 deletions(-) diff --git a/.dredd/hooks/capabilities.go b/.dredd/hooks/capabilities.go index 5a0918e7..07a2d6ca 100644 --- a/.dredd/hooks/capabilities.go +++ b/.dredd/hooks/capabilities.go @@ -29,7 +29,6 @@ var inventoryID int var environmentID int var templateID int var integrationID int -var integrationExtractorID int var integrationExtractValueID int var integrationMatchID int diff --git a/.dredd/hooks/helpers.go b/.dredd/hooks/helpers.go index b1d37083..9571d6d3 100644 --- a/.dredd/hooks/helpers.go +++ b/.dredd/hooks/helpers.go @@ -245,7 +245,7 @@ func addIntegration() *db.Integration { func addIntegrationExtractValue() *db.IntegrationExtractValue { integrationextractvalue, err := store.CreateIntegrationExtractValue(userProject.ID, db.IntegrationExtractValue{ Name: "Value", - IntegrationID: integrationExtractorID, + IntegrationID: integrationID, ValueSource: db.IntegrationExtractBodyValue, BodyDataType: db.IntegrationBodyDataJSON, Key: "key", @@ -262,7 +262,7 @@ func addIntegrationExtractValue() *db.IntegrationExtractValue { func addIntegrationMatcher() *db.IntegrationMatcher { integrationmatch, err := store.CreateIntegrationMatcher(userProject.ID, db.IntegrationMatcher{ Name: "matcher", - IntegrationID: integrationExtractorID, + IntegrationID: integrationID, MatchType: "body", Method: "equals", BodyDataType: "json", diff --git a/db/Event.go b/db/Event.go index 248aa0ff..e4914458 100644 --- a/db/Event.go +++ b/db/Event.go @@ -35,7 +35,6 @@ const ( EventUser EventObjectType = "user" EventView EventObjectType = "view" EventIntegration EventObjectType = "integration" - EventIntegrationExtractor EventObjectType = "integrationextractor" EventIntegrationExtractValue EventObjectType = "integrationextractvalue" EventIntegrationMatcher EventObjectType = "integrationmatcher" ) diff --git a/db/bolt/BoltDb.go b/db/bolt/BoltDb.go index 8303dfde..91690b21 100644 --- a/db/bolt/BoltDb.go +++ b/db/bolt/BoltDb.go @@ -554,8 +554,8 @@ func (d *BoltDb) getIntegrationRefs(projectID int, objectProps db.ObjectProps, o return } -func (d *BoltDb) getIntegrationExtractorChildrenRefs(extractorID int, objectProps db.ObjectProps, objectID int) (refs db.IntegrationExtractorChildReferrers, err error) { - //refs.IntegrationExtractors, err = d.getReferringObjectByParentID(objectID, objectProps, extractorID, db.IntegrationExtractorProps) +func (d *BoltDb) getIntegrationExtractorChildrenRefs(integrationID int, objectProps db.ObjectProps, objectID int) (refs db.IntegrationExtractorChildReferrers, err error) { + //refs.IntegrationExtractors, err = d.getReferringObjectByParentID(objectID, objectProps, integrationID, db.IntegrationExtractorProps) //if err != nil { // return //} diff --git a/db/bolt/webhook.go b/db/bolt/webhook.go index 7b067fdb..c6f56b60 100644 --- a/db/bolt/webhook.go +++ b/db/bolt/webhook.go @@ -55,17 +55,17 @@ Integration Extractors /* Integration ExtractValue */ -func (d *BoltDb) GetIntegrationExtractValuesByExtractorID(extractorID int) (values []db.IntegrationExtractValue, err error) { - err = d.getObjects(extractorID, db.IntegrationExtractValueProps, db.RetrieveQueryParams{}, nil, &values) +func (d *BoltDb) GetIntegrationExtractValuesByExtractorID(integrationID int) (values []db.IntegrationExtractValue, err error) { + err = d.getObjects(integrationID, db.IntegrationExtractValueProps, db.RetrieveQueryParams{}, nil, &values) return values, err } -func (d *BoltDb) DeleteIntegrationExtractValue(projectID int, valueID int, extractorID int) error { +func (d *BoltDb) DeleteIntegrationExtractValue(projectID int, valueID int, integrationID int) error { return d.deleteObject(projectID, db.IntegrationExtractValueProps, intObjectID(valueID), nil) } -func (d *BoltDb) GetIntegrationMatchersByExtractorID(extractorID int) (matchers []db.IntegrationMatcher, err error) { - err = d.getObjects(extractorID, db.IntegrationMatcherProps, db.RetrieveQueryParams{}, nil, &matchers) +func (d *BoltDb) GetIntegrationMatchersByExtractorID(integrationID int) (matchers []db.IntegrationMatcher, err error) { + err = d.getObjects(integrationID, db.IntegrationMatcherProps, db.RetrieveQueryParams{}, nil, &matchers) return matchers, err } @@ -82,7 +82,7 @@ func (d *BoltDb) CreateIntegrationExtractValue(projectId int, value db.Integrati } -func (d *BoltDb) GetIntegrationExtractValues(projectID int, params db.RetrieveQueryParams, extractorID int) (values []db.IntegrationExtractValue, err error) { +func (d *BoltDb) GetIntegrationExtractValues(projectID int, params db.RetrieveQueryParams, integrationID int) (values []db.IntegrationExtractValue, err error) { values = make([]db.IntegrationExtractValue, 0) var allValues []db.IntegrationExtractValue @@ -93,7 +93,7 @@ func (d *BoltDb) GetIntegrationExtractValues(projectID int, params db.RetrieveQu } for _, v := range allValues { - if v.IntegrationID == extractorID { + if v.IntegrationID == integrationID { values = append(values, v) } } @@ -101,7 +101,7 @@ func (d *BoltDb) GetIntegrationExtractValues(projectID int, params db.RetrieveQu return } -func (d *BoltDb) GetIntegrationExtractValue(projectID int, valueID int, extractorID int) (value db.IntegrationExtractValue, err error) { +func (d *BoltDb) GetIntegrationExtractValue(projectID int, valueID int, integrationID int) (value db.IntegrationExtractValue, err error) { err = d.getObject(projectID, db.IntegrationExtractValueProps, intObjectID(valueID), &value) return value, err } @@ -116,7 +116,7 @@ func (d *BoltDb) UpdateIntegrationExtractValue(projectID int, integrationExtract return d.updateObject(projectID, db.IntegrationExtractValueProps, integrationExtractValue) } -func (d *BoltDb) GetIntegrationExtractValueRefs(projectID int, valueID int, extractorID int) (db.IntegrationExtractorChildReferrers, error) { +func (d *BoltDb) GetIntegrationExtractValueRefs(projectID int, valueID int, integrationID int) (db.IntegrationExtractorChildReferrers, error) { return d.getIntegrationExtractorChildrenRefs(projectID, db.IntegrationExtractValueProps, valueID) } @@ -133,7 +133,7 @@ func (d *BoltDb) CreateIntegrationMatcher(projectID int, matcher db.IntegrationM return newMatcher.(db.IntegrationMatcher), err } -func (d *BoltDb) GetIntegrationMatchers(projectID int, params db.RetrieveQueryParams, extractorID int) (matchers []db.IntegrationMatcher, err error) { +func (d *BoltDb) GetIntegrationMatchers(projectID int, params db.RetrieveQueryParams, integrationID int) (matchers []db.IntegrationMatcher, err error) { matchers = make([]db.IntegrationMatcher, 0) var allMatchers []db.IntegrationMatcher @@ -144,7 +144,7 @@ func (d *BoltDb) GetIntegrationMatchers(projectID int, params db.RetrieveQueryPa } for _, v := range allMatchers { - if v.IntegrationID == extractorID { + if v.IntegrationID == integrationID { matchers = append(matchers, v) } } @@ -152,9 +152,9 @@ func (d *BoltDb) GetIntegrationMatchers(projectID int, params db.RetrieveQueryPa return } -func (d *BoltDb) GetIntegrationMatcher(projectID int, matcherID int, extractorID int) (matcher db.IntegrationMatcher, err error) { +func (d *BoltDb) GetIntegrationMatcher(projectID int, matcherID int, integrationID int) (matcher db.IntegrationMatcher, err error) { var matchers []db.IntegrationMatcher - matchers, err = d.GetIntegrationMatchers(projectID, db.RetrieveQueryParams{}, extractorID) + matchers, err = d.GetIntegrationMatchers(projectID, db.RetrieveQueryParams{}, integrationID) for _, v := range matchers { if v.ID == matcherID { @@ -175,23 +175,23 @@ func (d *BoltDb) UpdateIntegrationMatcher(projectID int, integrationMatcher db.I return d.updateObject(projectID, db.IntegrationMatcherProps, integrationMatcher) } -func (d *BoltDb) DeleteIntegrationMatcher(projectID int, matcherID int, extractorID int) error { +func (d *BoltDb) DeleteIntegrationMatcher(projectID int, matcherID int, integrationID int) error { return d.deleteObject(projectID, db.IntegrationMatcherProps, intObjectID(matcherID), nil) } func (d *BoltDb) DeleteIntegration(projectID int, integrationID int) error { - extractors, err := d.GetIntegrationMatchers(projectID, db.RetrieveQueryParams{}, integrationID) + matchers, err := d.GetIntegrationMatchers(projectID, db.RetrieveQueryParams{}, integrationID) if err != nil { return err } - for extractor := range extractors { - d.DeleteIntegrationMatcher(projectID, extractors[extractor].ID, integrationID) + for m := range matchers { + d.DeleteIntegrationMatcher(projectID, matchers[m].ID, integrationID) } return d.deleteObject(projectID, db.IntegrationProps, intObjectID(integrationID), nil) } -func (d *BoltDb) GetIntegrationMatcherRefs(projectID int, matcherID int, extractorID int) (db.IntegrationExtractorChildReferrers, error) { +func (d *BoltDb) GetIntegrationMatcherRefs(projectID int, matcherID int, integrationID int) (db.IntegrationExtractorChildReferrers, error) { return d.getIntegrationExtractorChildrenRefs(projectID, db.IntegrationMatcherProps, matcherID) } diff --git a/db/sql/SqlDb.go b/db/sql/SqlDb.go index 507a14ac..15276434 100644 --- a/db/sql/SqlDb.go +++ b/db/sql/SqlDb.go @@ -733,7 +733,7 @@ func (d *SqlDb) GetReferencesForForeignKey(objectProps db.ObjectProps, objectID // Find Object Referrers for objectID based on referring column taken from referringObjectProps // Example: -// GetObjectReferences(db.WebhookMatchers, db.WebhookExtractorProps, extractorID) +// GetObjectReferences(db.WebhookMatchers, db.WebhookExtractorProps, integrationID) func (d *SqlDb) GetObjectReferences(objectProps db.ObjectProps, referringObjectProps db.ObjectProps, objectID int) (referringObjs []db.ObjectReferrer, err error) { referringObjs = make([]db.ObjectReferrer, 0) diff --git a/db/sql/integration.go b/db/sql/integration.go index dd436357..a6b6eeff 100644 --- a/db/sql/integration.go +++ b/db/sql/integration.go @@ -92,11 +92,11 @@ func (d *SqlDb) UpdateIntegration(integration db.Integration) error { return err } -func (d *SqlDb) GetIntegrationExtractValuesByExtractorID(extractorID int) (values []db.IntegrationExtractValue, err error) { +func (d *SqlDb) GetIntegrationExtractValuesByExtractorID(integrationID int) (values []db.IntegrationExtractValue, err error) { var sqlError error query, args, sqlError := squirrel.Select("v.*"). From("project__integration_extract_value as v"). - Where(squirrel.Eq{"integration_id": extractorID}). + Where(squirrel.Eq{"integration_id": integrationID}). OrderBy("v.id"). ToSql() @@ -109,11 +109,11 @@ func (d *SqlDb) GetIntegrationExtractValuesByExtractorID(extractorID int) (value return values, err } -func (d *SqlDb) GetIntegrationMatchersByExtractorID(extractorID int) (matchers []db.IntegrationMatcher, err error) { +func (d *SqlDb) GetIntegrationMatchersByExtractorID(integrationID int) (matchers []db.IntegrationMatcher, err error) { var sqlError error query, args, sqlError := squirrel.Select("m.*"). From("project__integration_matcher as m"). - Where(squirrel.Eq{"integration_id": extractorID}). + Where(squirrel.Eq{"integration_id": integrationID}). OrderBy("m.id"). ToSql() @@ -126,36 +126,36 @@ func (d *SqlDb) GetIntegrationMatchersByExtractorID(extractorID int) (matchers [ return matchers, err } -//func (d *SqlDb) DeleteIntegrationExtractor(projectID int, extractorID int, integrationID int) error { -// values, err := d.GetIntegrationExtractValuesByExtractorID(extractorID) +//func (d *SqlDb) DeleteIntegrationExtractor(projectID int, integrationID int, integrationID int) error { +// values, err := d.GetIntegrationExtractValuesByExtractorID(integrationID) // if err != nil && !strings.Contains(err.Error(), "no rows in result set") { // return err // } // // for value := range values { // -// err = d.DeleteIntegrationExtractValue(0, values[value].ID, extractorID) +// err = d.DeleteIntegrationExtractValue(0, values[value].ID, integrationID) // if err != nil && !strings.Contains(err.Error(), "no rows in result set") { // log.Error(err) // return err // } // } // -// matchers, errExtractor := d.GetIntegrationMatchersByExtractorID(extractorID) +// matchers, errExtractor := d.GetIntegrationMatchersByExtractorID(integrationID) // if errExtractor != nil && !strings.Contains(errExtractor.Error(), "no rows in result set") { // log.Error(errExtractor) // return errExtractor // } // // for matcher := range matchers { -// err = d.DeleteIntegrationMatcher(0, matchers[matcher].ID, extractorID) +// err = d.DeleteIntegrationMatcher(0, matchers[matcher].ID, integrationID) // if err != nil && !strings.Contains(err.Error(), "no rows in result set") { // log.Error(err) // return err // } // } // -// return d.deleteObjectByReferencedID(integrationID, db.IntegrationProps, db.IntegrationExtractorProps, extractorID) +// return d.deleteObjectByReferencedID(integrationID, db.IntegrationProps, db.IntegrationExtractorProps, integrationID) //} // //func (d *SqlDb) UpdateIntegrationExtractor(projectID int, integrationExtractor db.IntegrationExtractor) error { @@ -201,9 +201,9 @@ func (d *SqlDb) CreateIntegrationExtractValue(projectId int, value db.Integratio return } -func (d *SqlDb) GetIntegrationExtractValues(projectID int, params db.RetrieveQueryParams, extractorID int) ([]db.IntegrationExtractValue, error) { +func (d *SqlDb) GetIntegrationExtractValues(projectID int, params db.RetrieveQueryParams, integrationID int) ([]db.IntegrationExtractValue, error) { var values []db.IntegrationExtractValue - err := d.getObjectsByReferrer(extractorID, db.IntegrationProps, db.IntegrationExtractValueProps, params, &values) + err := d.getObjectsByReferrer(integrationID, db.IntegrationProps, db.IntegrationExtractValueProps, params, &values) return values, err } @@ -214,7 +214,7 @@ func (d *SqlDb) GetAllIntegrationExtractValues() (values []db.IntegrationExtract return } -func (d *SqlDb) GetIntegrationExtractValue(projectID int, valueID int, extractorID int) (value db.IntegrationExtractValue, err error) { +func (d *SqlDb) GetIntegrationExtractValue(projectID int, valueID int, integrationID int) (value db.IntegrationExtractValue, err error) { query, args, err := squirrel.Select("v.*"). From("project__integration_extract_value as v"). Where(squirrel.Eq{"id": valueID}). @@ -230,13 +230,13 @@ func (d *SqlDb) GetIntegrationExtractValue(projectID int, valueID int, extractor return value, err } -func (d *SqlDb) GetIntegrationExtractValueRefs(projectID int, valueID int, extractorID int) (refs db.IntegrationExtractorChildReferrers, err error) { - refs.Integrations, err = d.GetObjectReferences(db.IntegrationProps, db.IntegrationExtractValueProps, extractorID) +func (d *SqlDb) GetIntegrationExtractValueRefs(projectID int, valueID int, integrationID int) (refs db.IntegrationExtractorChildReferrers, err error) { + refs.Integrations, err = d.GetObjectReferences(db.IntegrationProps, db.IntegrationExtractValueProps, integrationID) return } -func (d *SqlDb) DeleteIntegrationExtractValue(projectID int, valueID int, extractorID int) error { - return d.deleteObjectByReferencedID(extractorID, db.IntegrationProps, db.IntegrationExtractValueProps, valueID) +func (d *SqlDb) DeleteIntegrationExtractValue(projectID int, valueID int, integrationID int) error { + return d.deleteObjectByReferencedID(integrationID, db.IntegrationProps, db.IntegrationExtractValueProps, valueID) } func (d *SqlDb) UpdateIntegrationExtractValue(projectID int, integrationExtractValue db.IntegrationExtractValue) error { @@ -288,10 +288,10 @@ func (d *SqlDb) CreateIntegrationMatcher(projectID int, matcher db.IntegrationMa return } -func (d *SqlDb) GetIntegrationMatchers(projectID int, params db.RetrieveQueryParams, extractorID int) (matchers []db.IntegrationMatcher, err error) { +func (d *SqlDb) GetIntegrationMatchers(projectID int, params db.RetrieveQueryParams, integrationID int) (matchers []db.IntegrationMatcher, err error) { query, args, err := squirrel.Select("m.*"). From("project__integration_matcher as m"). - Where(squirrel.Eq{"integration_id": extractorID}). + Where(squirrel.Eq{"integration_id": integrationID}). OrderBy("m.id"). ToSql() @@ -312,7 +312,7 @@ func (d *SqlDb) GetAllIntegrationMatchers() (matchers []db.IntegrationMatcher, e return } -func (d *SqlDb) GetIntegrationMatcher(projectID int, matcherID int, extractorID int) (matcher db.IntegrationMatcher, err error) { +func (d *SqlDb) GetIntegrationMatcher(projectID int, matcherID int, integrationID int) (matcher db.IntegrationMatcher, err error) { query, args, err := squirrel.Select("m.*"). From("project__integration_matcher as m"). Where(squirrel.Eq{"id": matcherID}). @@ -328,14 +328,14 @@ func (d *SqlDb) GetIntegrationMatcher(projectID int, matcherID int, extractorID return matcher, err } -func (d *SqlDb) GetIntegrationMatcherRefs(projectID int, matcherID int, extractorID int) (refs db.IntegrationExtractorChildReferrers, err error) { +func (d *SqlDb) GetIntegrationMatcherRefs(projectID int, matcherID int, integrationID int) (refs db.IntegrationExtractorChildReferrers, err error) { refs.Integrations, err = d.GetObjectReferences(db.IntegrationProps, db.IntegrationMatcherProps, matcherID) return } -func (d *SqlDb) DeleteIntegrationMatcher(projectID int, matcherID int, extractorID int) error { - return d.deleteObjectByReferencedID(extractorID, db.IntegrationProps, db.IntegrationMatcherProps, matcherID) +func (d *SqlDb) DeleteIntegrationMatcher(projectID int, matcherID int, integrationID int) error { + return d.deleteObjectByReferencedID(integrationID, db.IntegrationProps, db.IntegrationMatcherProps, matcherID) } func (d *SqlDb) UpdateIntegrationMatcher(projectID int, integrationMatcher db.IntegrationMatcher) error { diff --git a/web/src/views/project/IntegrationExtractorCrumb.vue b/web/src/views/project/IntegrationExtractorCrumb.vue index 85d7d83f..1b8ba340 100644 --- a/web/src/views/project/IntegrationExtractorCrumb.vue +++ b/web/src/views/project/IntegrationExtractorCrumb.vue @@ -1,5 +1,5 @@ - ++@@ -40,12 +40,6 @@ export default { url: `/api/project/${this.projectId}/integrations/${this.integrationId}`, responseType: 'json', })).data; - - this.extractor = (await axios({ - method: 'get', - url: `/api/project/${this.projectId}/integrations/${this.integrationId}`, - responseType: 'json', - })).data; }, computed: { @@ -61,12 +55,6 @@ export default { } return this.$route.params.integrationId; }, - extractorId() { - if (/^-?\d+$/.test(this.$route.params.extractorId)) { - return parseInt(this.$route.params.extractorId, 10); - } - return this.$route.params.extractorId; - }, }, methods: { allowActions() { @@ -76,7 +64,7 @@ export default { return []; }, getItemsUrl() { - return `/api/project/${this.projectId}/integrations/${this.integrationId}/extractors`; + return `/api/project/${this.projectId}/integrations`; }, getSingleItemUrl() { return `/api/project/${this.projectId}/integrations/${this.integrationId}`; From 3a0b7346dc6ea1927a56fe6dc35df706bc7df389 Mon Sep 17 00:00:00 2001 From: fiftin Date: Wed, 6 Mar 2024 16:28:54 +0100 Subject: [PATCH 305/346] feat: update ids --- .dredd/hooks/main.go | 6 +++--- api-docs.yml | 8 ++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.dredd/hooks/main.go b/.dredd/hooks/main.go index 782a9d45..8027143c 100644 --- a/.dredd/hooks/main.go +++ b/.dredd/hooks/main.go @@ -87,10 +87,10 @@ func main() { h.Before("integration > /api/project/{project_id}/integrations/{integration_id}/values > Get Integration Extracted Values linked to integration extractor > 200 > application/json", capabilityWrapper("integrationextractvalue")) h.Before("integration > /api/project/{project_id}/integrations/{integration_id}/values > Add Integration Extracted Value > 204 > application/json", capabilityWrapper("integrationextractvalue")) h.Before("integration > /api/project/{project_id}/integrations/{integration_id}/values/{extractvalue_id} > Removes integration extract value > 204 > application/json", capabilityWrapper("integrationextractvalue")) - h.Before("integration > /api/project/{project_id}/integrations/{integration_id}/values > Add Integration Extracted Value > 204 > application/json", capabilityWrapper("integrationextractor")) + h.Before("integration > /api/project/{project_id}/integrations/{integration_id}/values > Add Integration Extracted Value > 204 > application/json", capabilityWrapper("integration")) h.Before("integration > /api/project/{project_id}/integrations/{integration_id}/values/{extractvalue_id} > Updates Integration ExtractValue > 204 > application/json", capabilityWrapper("integrationextractvalue")) - h.Before("integration > /api/project/{project_id}/integrations/{integration_id}/matchers > Get Integration Matcher linked to integration extractor > 200 > application/json", capabilityWrapper("integrationextractor")) - h.Before("integration > /api/project/{project_id}/integrations/{integration_id}/matchers > Add Integration Matcher > 204 > application/json", capabilityWrapper("integrationextractor")) + h.Before("integration > /api/project/{project_id}/integrations/{integration_id}/matchers > Get Integration Matcher linked to integration extractor > 200 > application/json", capabilityWrapper("integration")) + h.Before("integration > /api/project/{project_id}/integrations/{integration_id}/matchers > Add Integration Matcher > 204 > application/json", capabilityWrapper("integration")) h.Before("integration > /api/project/{project_id}/integrations/{integration_id}/matchers/{matcher_id} > Updates Integration Matcher > 204 > application/json", capabilityWrapper("integrationmatcher")) h.Before("project > /api/project/{project_id}/keys/{key_id} > Updates access key > 204 > application/json", capabilityWrapper("access_key")) diff --git a/api-docs.yml b/api-docs.yml index 30323239..fbb93e5c 100644 --- a/api-docs.yml +++ b/api-docs.yml @@ -542,6 +542,8 @@ definitions: variable: type: string example: variable + integration_id: + type: integer IntegrationMatcherRequest: type: object @@ -570,6 +572,8 @@ definitions: properties: id: type: integer + integration_id: + type: integer name: type: string example: deploy @@ -937,14 +941,14 @@ parameters: in: path type: integer required: true - x-example: 13 + x-example: 12 matcher_id: name: matcher_id description: matcher ID in: path type: integer required: true - x-example: 14 + x-example: 13 paths: /ping: From 595fb28f801219a8cc7c84e843ddc61f319096f2 Mon Sep 17 00:00:00 2001 From: fiftin Date: Wed, 6 Mar 2024 21:24:07 +0100 Subject: [PATCH 306/346] feat(ui): add integration settings --- web/src/components/IntegrationForm.vue | 75 +++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 8 deletions(-) diff --git a/web/src/components/IntegrationForm.vue b/web/src/components/IntegrationForm.vue index ff7288f9..462489cc 100644 --- a/web/src/components/IntegrationForm.vue +++ b/web/src/components/IntegrationForm.vue @@ -3,12 +3,14 @@ ref="form" lazy-validation v-model="formValid" + v-if="isLoaded" > {{ formError }} + >{{ formError }} ++ + + + + From 193d57a66ce07c2d918da44321ad0b278d1cc2e4 Mon Sep 17 00:00:00 2001 From: fiftin Date: Wed, 6 Mar 2024 22:17:34 +0100 Subject: [PATCH 307/346] fix(be): add migation --- api/integration.go | 6 ++--- api/projects/integration.go | 6 ++--- api/router.go | 2 +- db/Migration.go | 1 + db/sql/migrations/v2.9.60.sql | 16 +++++++++---- db/sql/migrations/v2.9.61.sql | 44 +++++++++++++++++++++++++++++++++++ 6 files changed, 64 insertions(+), 11 deletions(-) create mode 100644 db/sql/migrations/v2.9.61.sql diff --git a/api/integration.go b/api/integration.go index 073f96ca..78e49dda 100644 --- a/api/integration.go +++ b/api/integration.go @@ -42,7 +42,8 @@ func ReceiveIntegration(w http.ResponseWriter, r *http.Request) { var err error - project := context.Get(r, "project").(db.Project) + //project := context.Get(r, "project").(db.Project) + projectId, err := helpers.GetIntParam("project_id", w, r) integration := context.Get(r, "integration").(db.Integration) switch integration.AuthMethod { @@ -70,7 +71,7 @@ func ReceiveIntegration(w http.ResponseWriter, r *http.Request) { } var matchers []db.IntegrationMatcher - matchers, err = helpers.Store(r).GetIntegrationMatchers(project.ID, db.RetrieveQueryParams{}, integration.ID) + matchers, err = helpers.Store(r).GetIntegrationMatchers(projectId, db.RetrieveQueryParams{}, integration.ID) if err != nil { log.Error(err) } @@ -86,7 +87,6 @@ func ReceiveIntegration(w http.ResponseWriter, r *http.Request) { } } - // Iterate over all Extractors that matched if !matched { w.WriteHeader(http.StatusNoContent) return diff --git a/api/projects/integration.go b/api/projects/integration.go index 36d949a3..c4c5eb29 100644 --- a/api/projects/integration.go +++ b/api/projects/integration.go @@ -13,8 +13,8 @@ import ( func IntegrationMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - integration_id, err := helpers.GetIntParam("integration_id", w, r) - project := context.Get(r, "project").(db.Project) + integrationId, err := helpers.GetIntParam("integration_id", w, r) + projectId, err := helpers.GetIntParam("project_id", w, r) if err != nil { helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{ @@ -23,7 +23,7 @@ func IntegrationMiddleware(next http.Handler) http.Handler { return } - integration, err := helpers.Store(r).GetIntegration(project.ID, integration_id) + integration, err := helpers.Store(r).GetIntegration(projectId, integrationId) if err != nil { helpers.WriteError(w, err) diff --git a/api/router.go b/api/router.go index 82d83fd7..7e223a70 100644 --- a/api/router.go +++ b/api/router.go @@ -91,7 +91,7 @@ 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 + "integration/{project_id}").Subrouter() + publicWebHookRouter := r.PathPrefix(webPath + "api/project/{project_id}/integrations/{integration_id}").Subrouter() publicWebHookRouter.Use(StoreMiddleware, JSONMiddleware, projects.IntegrationMiddleware) publicWebHookRouter.HandleFunc("/endpoint", ReceiveIntegration).Methods("POST", "GET", "OPTIONS") diff --git a/db/Migration.go b/db/Migration.go index 9f206516..1b22b542 100644 --- a/db/Migration.go +++ b/db/Migration.go @@ -63,6 +63,7 @@ func GetMigrations() []Migration { {Version: "2.9.6"}, {Version: "2.9.46"}, {Version: "2.9.60"}, + {Version: "2.9.61"}, } } diff --git a/db/sql/migrations/v2.9.60.sql b/db/sql/migrations/v2.9.60.sql index aea6ed5a..19fde627 100644 --- a/db/sql/migrations/v2.9.60.sql +++ b/db/sql/migrations/v2.9.60.sql @@ -12,27 +12,35 @@ create table project__integration ( foreign key (`auth_secret_id`) references access_key(`id`) on delete set null ); -create table project__integration_extract_value ( +create table project__integration_extractor ( `id` integer primary key autoincrement, `name` varchar(255) not null, `integration_id` int not null, + + foreign key (`integration_id`) references project__integration(`id`) on delete cascade +); + +create table project__integration_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 (`integration_id`) references project__integration(`id`) on delete cascade + foreign key (`extractor_id`) references project__integration_extractor(`id`) on delete cascade ); create table project__integration_matcher ( `id` integer primary key autoincrement, `name` varchar(255) not null, - `integration_id` int 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 (`integration_id`) references project__integration(`id`) on delete cascade + foreign key (`extractor_id`) references project__integration_extractor(`id`) on delete cascade ); diff --git a/db/sql/migrations/v2.9.61.sql b/db/sql/migrations/v2.9.61.sql new file mode 100644 index 00000000..38c2ad16 --- /dev/null +++ b/db/sql/migrations/v2.9.61.sql @@ -0,0 +1,44 @@ +drop table project__integration_matcher; +drop table project__integration_extract_value; +drop table project__integration_extractor; +drop table project__integration; + +create table project__integration ( + `id` integer primary key autoincrement, + `name` varchar(255) not null, + `project_id` int not null, + `template_id` int not null, + `auth_method` varchar(15) not null default 'none', + `auth_secret_id` int, + `auth_header` varchar(255), + `searchable` bool not null default false, + + foreign key (`project_id`) references project(`id`) on delete cascade, + foreign key (`template_id`) references project__template(`id`) on delete cascade, + foreign key (`auth_secret_id`) references access_key(`id`) on delete set null +); + +create table project__integration_extract_value ( + `id` integer primary key autoincrement, + `name` varchar(255) not null, + `integration_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 (`integration_id`) references project__integration(`id`) on delete cascade +); + +create table project__integration_matcher ( + `id` integer primary key autoincrement, + `name` varchar(255) not null, + `integration_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 (`integration_id`) references project__integration(`id`) on delete cascade +); From dc9b2c271b054a3c5ac1bc610b0373006bb2edf6 Mon Sep 17 00:00:00 2001 From: fiftin Date: Wed, 6 Mar 2024 22:20:23 +0100 Subject: [PATCH 308/346] feat: add field searchable for intagrations --- db/Integration.go | 1 + db/sql/integration.go | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/db/Integration.go b/db/Integration.go index ba7893e4..568d3d93 100644 --- a/db/Integration.go +++ b/db/Integration.go @@ -73,6 +73,7 @@ type Integration struct { AuthSecretID *int `db:"auth_secret_id" json:"auth_secret_id"` AuthHeader string `db:"auth_header" json:"auth_header"` AuthSecret AccessKey `db:"-" json:"-"` + Searchable bool `db:"searchable" json:"searchable"` } func (env *Integration) Validate() error { diff --git a/db/sql/integration.go b/db/sql/integration.go index a6b6eeff..6604abb1 100644 --- a/db/sql/integration.go +++ b/db/sql/integration.go @@ -15,14 +15,15 @@ func (d *SqlDb) CreateIntegration(integration db.Integration) (newIntegration db insertID, err := d.insert( "id", "insert into project__integration "+ - "(project_id, name, template_id, auth_method, auth_secret_id, auth_header) values "+ - "(?, ?, ?, ?, ?, ?)", + "(project_id, name, template_id, auth_method, auth_secret_id, auth_header, searchable) values "+ + "(?, ?, ?, ?, ?, ?, ?)", integration.ProjectID, integration.Name, integration.TemplateID, integration.AuthMethod, integration.AuthSecretID, - integration.AuthHeader) + integration.AuthHeader, + integration.Searchable) if err != nil { return @@ -81,12 +82,13 @@ func (d *SqlDb) UpdateIntegration(integration db.Integration) error { } _, err = d.exec( - "update project__integration set `name`=?, template_id=?, auth_method=?, auth_secret_id=?, auth_header=? where `id`=?", + "update project__integration set `name`=?, template_id=?, auth_method=?, auth_secret_id=?, auth_header=?, searchable=? where `id`=?", integration.Name, integration.TemplateID, integration.AuthMethod, integration.AuthSecretID, integration.AuthHeader, + integration.Searchable, integration.ID) return err From e8a679cec60482e9685b55178f894fca4d6d1b79 Mon Sep 17 00:00:00 2001 From: fiftin Date: Wed, 6 Mar 2024 22:47:01 +0100 Subject: [PATCH 309/346] feat: add flag for integrations --- api/integration.go | 7 +++++++ util/config.go | 14 +++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/api/integration.go b/api/integration.go index 78e49dda..876284b7 100644 --- a/api/integration.go +++ b/api/integration.go @@ -6,6 +6,7 @@ import ( "crypto/sha1" "encoding/json" "fmt" + "github.com/ansible-semaphore/semaphore/util" "github.com/gorilla/context" "io" "net/http" @@ -38,6 +39,12 @@ func HashPayload(secret string, playloadBody []byte) string { } func ReceiveIntegration(w http.ResponseWriter, r *http.Request) { + + if !util.Config.IntegrationsEnable { + w.WriteHeader(http.StatusNotFound) + return + } + log.Info(fmt.Sprintf("Receiving Integration from: %s", r.RemoteAddr)) var err error diff --git a/util/config.go b/util/config.go index f1479b76..40b6fb9c 100644 --- a/util/config.go +++ b/util/config.go @@ -163,15 +163,14 @@ type ConfigType struct { LdapNeedTLS bool `json:"ldap_needtls" env:"SEMAPHORE_LDAP_NEEDTLS"` // Telegram, Slack and Microsoft Teams alerting - TelegramAlert bool `json:"telegram_alert" env:"SEMAPHORE_TELEGRAM_ALERT"` - TelegramChat string `json:"telegram_chat" env:"SEMAPHORE_TELEGRAM_CHAT"` - TelegramToken string `json:"telegram_token" env:"SEMAPHORE_TELEGRAM_TOKEN"` - SlackAlert bool `json:"slack_alert" env:"SEMAPHORE_SLACK_ALERT"` - SlackUrl string `json:"slack_url" env:"SEMAPHORE_SLACK_URL"` + TelegramAlert bool `json:"telegram_alert" env:"SEMAPHORE_TELEGRAM_ALERT"` + TelegramChat string `json:"telegram_chat" env:"SEMAPHORE_TELEGRAM_CHAT"` + TelegramToken string `json:"telegram_token" env:"SEMAPHORE_TELEGRAM_TOKEN"` + SlackAlert bool `json:"slack_alert" env:"SEMAPHORE_SLACK_ALERT"` + SlackUrl string `json:"slack_url" env:"SEMAPHORE_SLACK_URL"` MicrosoftTeamsAlert bool `json:"microsoft_teams_alert" env:"SEMAPHORE_MICROSOFT_TEAMS_ALERT"` MicrosoftTeamsUrl string `json:"microsoft_teams_url" env:"SEMAPHORE_MICROSOFT_TEAMS_URL"` - // oidc settings OidcProviders map[string]OidcProvider `json:"oidc_providers"` @@ -186,7 +185,8 @@ type ConfigType struct { PasswordLoginDisable bool `json:"password_login_disable" env:"SEMAPHORE_PASSWORD_LOGIN_DISABLED"` NonAdminCanCreateProject bool `json:"non_admin_can_create_project" env:"SEMAPHORE_NON_ADMIN_CAN_CREATE_PROJECT"` - UseRemoteRunner bool `json:"use_remote_runner" env:"SEMAPHORE_USE_REMOTE_RUNNER"` + UseRemoteRunner bool `json:"use_remote_runner" env:"SEMAPHORE_USE_REMOTE_RUNNER"` + IntegrationsEnable bool `json:"integrations_enable" env:"SEMAPHORE_INTEGRATIONS_ENABLE"` Runner RunnerSettings `json:"runner"` } From 80407b36b5ad691489fddde0af9f5827389341e7 Mon Sep 17 00:00:00 2001 From: fiftin Date: Wed, 6 Mar 2024 22:48:40 +0100 Subject: [PATCH 310/346] feat: pass integations flag to user info --- api/user.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/user.go b/api/user.go index dc9e7222..1d910362 100644 --- a/api/user.go +++ b/api/user.go @@ -21,11 +21,13 @@ func getUser(w http.ResponseWriter, r *http.Request) { var user struct { db.User - CanCreateProject bool `json:"can_create_project"` + CanCreateProject bool `json:"can_create_project"` + IntegrationsEnable bool `json:"integrations_enable"` } user.User = *context.Get(r, "user").(*db.User) user.CanCreateProject = user.Admin || util.Config.NonAdminCanCreateProject + user.IntegrationsEnable = util.Config.IntegrationsEnable helpers.WriteJSON(w, http.StatusOK, user) } From 2637543ddd69ac09130ce49a78857bf214645f27 Mon Sep 17 00:00:00 2001 From: fiftin Date: Wed, 6 Mar 2024 22:52:26 +0100 Subject: [PATCH 311/346] feat(ui): hide integations if not enabled --- web/src/App.vue | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/web/src/App.vue b/web/src/App.vue index 556c9fb6..5a92feac 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -260,7 +260,11 @@ - + @@ -291,7 +295,7 @@ persistent-hint > - mdi-connection + @@ -537,7 +541,7 @@ } & > td:first-child { - //font-weight: bold !important; + //font-weight: bold !important; } } From 3e052de57f77b02466911748791658e65ce94c83 Mon Sep 17 00:00:00 2001 From: fiftin Date: Thu, 7 Mar 2024 09:49:34 +0100 Subject: [PATCH 312/346] feat(integrations): add alias table --- db/sql/migrations/v2.9.61.sql | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/db/sql/migrations/v2.9.61.sql b/db/sql/migrations/v2.9.61.sql index 38c2ad16..a06722c2 100644 --- a/db/sql/migrations/v2.9.61.sql +++ b/db/sql/migrations/v2.9.61.sql @@ -42,3 +42,16 @@ create table project__integration_matcher ( foreign key (`integration_id`) references project__integration(`id`) on delete cascade ); + +create table project__integration_alias ( + `id` integer primary key autoincrement, + `alias` varchar(50) not null, + `project_id` int not null, + `integration_id` int, + + foreign key (`project_id`) references project(`id`) on delete cascade, + foreign key (`integration_id`) references project__integration(`id`) on delete cascade, + + unique (`alias`), + unique (`project_id`) +); From 2c2e7df311c60aee5bbc60e027b6cd9e750aa77c Mon Sep 17 00:00:00 2001 From: fiftin Date: Thu, 7 Mar 2024 10:32:25 +0100 Subject: [PATCH 313/346] feat(integrations): add alias methods --- db/Integration.go | 7 ++ db/Store.go | 12 +++ db/bolt/{webhook.go => integrations.go} | 53 ++++++++----- db/sql/integration.go | 101 +++++------------------- db/sql/migrations/v2.9.61.sql | 2 +- 5 files changed, 75 insertions(+), 100 deletions(-) rename db/bolt/{webhook.go => integrations.go} (84%) diff --git a/db/Integration.go b/db/Integration.go index 568d3d93..b6fb0e1e 100644 --- a/db/Integration.go +++ b/db/Integration.go @@ -64,6 +64,13 @@ type IntegrationExtractValue struct { Variable string `db:"variable" json:"variable"` } +type IntegrationAlias struct { + ID int `db:"id" json:"id"` + Alias string `db:"alias" json:"alias"` + ProjectID int `db:"project_id" json:"project_id"` + IntegrationID *int `db:"integration_id" json:"integration_id"` +} + type Integration struct { ID int `db:"id" json:"id"` Name string `db:"name" json:"name"` diff --git a/db/Store.go b/db/Store.go index f874602f..af0c7ebe 100644 --- a/db/Store.go +++ b/db/Store.go @@ -156,6 +156,12 @@ type Store interface { GetIntegrationMatcherRefs(projectID int, matcherID int, integrationID int) (IntegrationExtractorChildReferrers, error) DeleteIntegrationMatcher(projectID int, matcherID int, integrationID int) error + CreateIntegrationAlias(alias IntegrationAlias) (IntegrationAlias, error) + GetIntegrationAlias(projectID int, integrationID *int) (IntegrationAlias, error) + GetIntegrationAliasByAlias(alias string) (IntegrationAlias, error) + UpdateIntegrationAlias(alias IntegrationAlias) error + DeleteIntegrationAlias(projectID int, integrationID *int) error + UpdateAccessKey(accessKey AccessKey) error CreateAccessKey(accessKey AccessKey) (AccessKey, error) DeleteAccessKey(projectID int, accessKeyID int) error @@ -276,6 +282,12 @@ var IntegrationMatcherProps = ObjectProps{ DefaultSortingColumn: "name", } +var IntegrationAliasProps = ObjectProps{ + TableName: "project__integration_alias", + Type: reflect.TypeOf(IntegrationAlias{}), + PrimaryColumnName: "id", +} + var EnvironmentProps = ObjectProps{ TableName: "project__environment", Type: reflect.TypeOf(Environment{}), diff --git a/db/bolt/webhook.go b/db/bolt/integrations.go similarity index 84% rename from db/bolt/webhook.go rename to db/bolt/integrations.go index c6f56b60..f2b56923 100644 --- a/db/bolt/webhook.go +++ b/db/bolt/integrations.go @@ -2,6 +2,7 @@ package bolt import ( "github.com/ansible-semaphore/semaphore/db" + "reflect" ) /* @@ -48,28 +49,10 @@ func (d *BoltDb) GetIntegrationRefs(projectID int, integrationID int) (db.Integr return db.IntegrationReferrers{}, nil } -/* -Integration Extractors -*/ - -/* -Integration ExtractValue -*/ -func (d *BoltDb) GetIntegrationExtractValuesByExtractorID(integrationID int) (values []db.IntegrationExtractValue, err error) { - err = d.getObjects(integrationID, db.IntegrationExtractValueProps, db.RetrieveQueryParams{}, nil, &values) - return values, err -} - func (d *BoltDb) DeleteIntegrationExtractValue(projectID int, valueID int, integrationID int) error { return d.deleteObject(projectID, db.IntegrationExtractValueProps, intObjectID(valueID), nil) } -func (d *BoltDb) GetIntegrationMatchersByExtractorID(integrationID int) (matchers []db.IntegrationMatcher, err error) { - err = d.getObjects(integrationID, db.IntegrationMatcherProps, db.RetrieveQueryParams{}, nil, &matchers) - - return matchers, err -} - func (d *BoltDb) CreateIntegrationExtractValue(projectId int, value db.IntegrationExtractValue) (db.IntegrationExtractValue, error) { err := value.Validate() @@ -195,3 +178,37 @@ func (d *BoltDb) DeleteIntegration(projectID int, integrationID int) error { func (d *BoltDb) GetIntegrationMatcherRefs(projectID int, matcherID int, integrationID int) (db.IntegrationExtractorChildReferrers, error) { return d.getIntegrationExtractorChildrenRefs(projectID, db.IntegrationMatcherProps, matcherID) } + +var integrationAliasProps = db.ObjectProps{ + TableName: "integration_alias", + Type: reflect.TypeOf(db.IntegrationAlias{}), + PrimaryColumnName: "alias", +} + +func (d *BoltDb) CreateIntegrationAlias(alias db.IntegrationAlias) (res db.IntegrationAlias, err error) { + return +} + +func (d *BoltDb) GetIntegrationAlias(projectID int, integrationID *int) (res db.IntegrationAlias, err error) { + if integrationID == nil { + projectLevelIntegrationId := -1 + integrationID = &projectLevelIntegrationId + } + err = d.getObject(projectID, db.IntegrationAliasProps, intObjectID(*integrationID), &res) + return +} + +func (d *BoltDb) GetIntegrationAliasByAlias(alias string) (res db.IntegrationAlias, err error) { + + err = d.getObject(-1, integrationAliasProps, strObjectID(alias), &res) + + return +} + +func (d *BoltDb) UpdateIntegrationAlias(alias db.IntegrationAlias) error { + return nil +} + +func (d *BoltDb) DeleteIntegrationAlias(projectID int, integrationID *int) error { + return nil +} diff --git a/db/sql/integration.go b/db/sql/integration.go index 6604abb1..619e2268 100644 --- a/db/sql/integration.go +++ b/db/sql/integration.go @@ -94,87 +94,6 @@ func (d *SqlDb) UpdateIntegration(integration db.Integration) error { return err } -func (d *SqlDb) GetIntegrationExtractValuesByExtractorID(integrationID int) (values []db.IntegrationExtractValue, err error) { - var sqlError error - query, args, sqlError := squirrel.Select("v.*"). - From("project__integration_extract_value as v"). - Where(squirrel.Eq{"integration_id": integrationID}). - OrderBy("v.id"). - ToSql() - - if sqlError != nil { - return []db.IntegrationExtractValue{}, sqlError - } - - err = d.selectOne(&values, query, args...) - - return values, err -} - -func (d *SqlDb) GetIntegrationMatchersByExtractorID(integrationID int) (matchers []db.IntegrationMatcher, err error) { - var sqlError error - query, args, sqlError := squirrel.Select("m.*"). - From("project__integration_matcher as m"). - Where(squirrel.Eq{"integration_id": integrationID}). - OrderBy("m.id"). - ToSql() - - if sqlError != nil { - return []db.IntegrationMatcher{}, sqlError - } - - err = d.selectOne(&matchers, query, args...) - - return matchers, err -} - -//func (d *SqlDb) DeleteIntegrationExtractor(projectID int, integrationID int, integrationID int) error { -// values, err := d.GetIntegrationExtractValuesByExtractorID(integrationID) -// if err != nil && !strings.Contains(err.Error(), "no rows in result set") { -// return err -// } -// -// for value := range values { -// -// err = d.DeleteIntegrationExtractValue(0, values[value].ID, integrationID) -// if err != nil && !strings.Contains(err.Error(), "no rows in result set") { -// log.Error(err) -// return err -// } -// } -// -// matchers, errExtractor := d.GetIntegrationMatchersByExtractorID(integrationID) -// if errExtractor != nil && !strings.Contains(errExtractor.Error(), "no rows in result set") { -// log.Error(errExtractor) -// return errExtractor -// } -// -// for matcher := range matchers { -// err = d.DeleteIntegrationMatcher(0, matchers[matcher].ID, integrationID) -// if err != nil && !strings.Contains(err.Error(), "no rows in result set") { -// log.Error(err) -// return err -// } -// } -// -// return d.deleteObjectByReferencedID(integrationID, db.IntegrationProps, db.IntegrationExtractorProps, integrationID) -//} -// -//func (d *SqlDb) UpdateIntegrationExtractor(projectID int, integrationExtractor db.IntegrationExtractor) error { -// err := integrationExtractor.Validate() -// -// if err != nil { -// return err -// } -// -// _, err = d.exec( -// "update project__integration_extractor set name=? where id=?", -// integrationExtractor.Name, -// integrationExtractor.ID) -// -// return err -//} - func (d *SqlDb) CreateIntegrationExtractValue(projectId int, value db.IntegrationExtractValue) (newValue db.IntegrationExtractValue, err error) { err = value.Validate() @@ -359,3 +278,23 @@ func (d *SqlDb) UpdateIntegrationMatcher(projectID int, integrationMatcher db.In return err } + +func (d *SqlDb) CreateIntegrationAlias(alias db.IntegrationAlias) (res db.IntegrationAlias, err error) { + return +} + +func (d *SqlDb) GetIntegrationAlias(projectID int, integrationID *int) (res db.IntegrationAlias, err error) { + return +} + +func (d *SqlDb) GetIntegrationAliasByAlias(alias string) (res db.IntegrationAlias, err error) { + return +} + +func (d *SqlDb) UpdateIntegrationAlias(alias db.IntegrationAlias) error { + return nil +} + +func (d *SqlDb) DeleteIntegrationAlias(projectID int, integrationID *int) error { + return nil +} diff --git a/db/sql/migrations/v2.9.61.sql b/db/sql/migrations/v2.9.61.sql index a06722c2..8f3fbf71 100644 --- a/db/sql/migrations/v2.9.61.sql +++ b/db/sql/migrations/v2.9.61.sql @@ -53,5 +53,5 @@ create table project__integration_alias ( foreign key (`integration_id`) references project__integration(`id`) on delete cascade, unique (`alias`), - unique (`project_id`) + unique (`project_id`, `integration_id`) ); From 13e1cac83efba51c51ed0c030c9d4886b2318c75 Mon Sep 17 00:00:00 2001 From: fiftin Date: Thu, 7 Mar 2024 11:06:12 +0100 Subject: [PATCH 314/346] feat(integrations): manipulation by aliases in boltdb --- db/bolt/BoltDb.go | 144 +++++++++++++++++++++------------------- db/bolt/integrations.go | 73 ++++++++++++++++++-- 2 files changed, 144 insertions(+), 73 deletions(-) diff --git a/db/bolt/BoltDb.go b/db/bolt/BoltDb.go index 91690b21..32b1416e 100644 --- a/db/bolt/BoltDb.go +++ b/db/bolt/BoltDb.go @@ -460,92 +460,98 @@ func (d *BoltDb) updateObject(bucketID int, props db.ObjectProps, object interfa }) } -func (d *BoltDb) createObject(bucketID int, props db.ObjectProps, object interface{}) (interface{}, error) { - err := d.db.Update(func(tx *bbolt.Tx) error { - b, err := tx.CreateBucketIfNotExists(makeBucketId(props, bucketID)) +func (d *BoltDb) createObjectTx(tx *bbolt.Tx, bucketID int, props db.ObjectProps, object interface{}) (interface{}, error) { + b, err := tx.CreateBucketIfNotExists(makeBucketId(props, bucketID)) - if err != nil { - return err + if err != nil { + return nil, err + } + + objPtr := reflect.ValueOf(&object).Elem() + + tmpObj := reflect.New(objPtr.Elem().Type()).Elem() + tmpObj.Set(objPtr.Elem()) + + var objID objectID + + if props.PrimaryColumnName != "" { + idFieldName, err2 := getFieldNameByTagSuffix(reflect.TypeOf(object), "db", props.PrimaryColumnName) + + if err2 != nil { + return nil, err2 } - objPtr := reflect.ValueOf(&object).Elem() + idValue := tmpObj.FieldByName(idFieldName) - tmpObj := reflect.New(objPtr.Elem().Type()).Elem() - tmpObj.Set(objPtr.Elem()) - - var objID objectID - - if props.PrimaryColumnName != "" { - idFieldName, err2 := getFieldNameByTagSuffix(reflect.TypeOf(object), "db", props.PrimaryColumnName) - - if err2 != nil { - return err2 - } - - idValue := tmpObj.FieldByName(idFieldName) - - switch idValue.Kind() { - case reflect.Int, - reflect.Int8, - reflect.Int16, - reflect.Int32, - reflect.Int64, - reflect.Uint, - reflect.Uint8, - reflect.Uint16, - reflect.Uint32, - reflect.Uint64: - if idValue.Int() == 0 { - id, err3 := b.NextSequence() - if err3 != nil { - return err3 - } - if props.SortInverted { - id = MaxID - id - } - idValue.SetInt(int64(id)) - } - - objID = intObjectID(idValue.Int()) - case reflect.String: - if idValue.String() == "" { - return fmt.Errorf("object ID can not be empty string") - } - objID = strObjectID(idValue.String()) - case reflect.Invalid: + switch idValue.Kind() { + case reflect.Int, + reflect.Int8, + reflect.Int16, + reflect.Int32, + reflect.Int64, + reflect.Uint, + reflect.Uint8, + reflect.Uint16, + reflect.Uint32, + reflect.Uint64: + if idValue.Int() == 0 { id, err3 := b.NextSequence() if err3 != nil { - return err3 + return nil, err3 } - objID = intObjectID(id) - default: - return fmt.Errorf("unsupported ID type") + if props.SortInverted { + id = MaxID - id + } + idValue.SetInt(int64(id)) } - } else { - id, err2 := b.NextSequence() - if err2 != nil { - return err2 + + objID = intObjectID(idValue.Int()) + case reflect.String: + if idValue.String() == "" { + return nil, fmt.Errorf("object ID can not be empty string") } - if props.SortInverted { - id = MaxID - id + objID = strObjectID(idValue.String()) + case reflect.Invalid: + id, err3 := b.NextSequence() + if err3 != nil { + return nil, err3 } objID = intObjectID(id) + default: + return nil, fmt.Errorf("unsupported ID type") } - - if objID == nil { - return fmt.Errorf("object ID can not be nil") + } else { + id, err2 := b.NextSequence() + if err2 != nil { + return nil, err2 } - - objPtr.Set(tmpObj) - str, err := marshalObject(object) - if err != nil { - return err + if props.SortInverted { + id = MaxID - id } + objID = intObjectID(id) + } - return b.Put(objID.ToBytes(), str) + if objID == nil { + return nil, fmt.Errorf("object ID can not be nil") + } + + objPtr.Set(tmpObj) + str, err := marshalObject(object) + if err != nil { + return nil, err + } + + return nil, b.Put(objID.ToBytes(), str) +} + +func (d *BoltDb) createObject(bucketID int, props db.ObjectProps, object interface{}) (res interface{}, err error) { + + _ = d.db.Update(func(tx *bbolt.Tx) error { + res, err = d.createObjectTx(tx, bucketID, props, object) + return err }) - return object, err + return } func (d *BoltDb) getIntegrationRefs(projectID int, objectProps db.ObjectProps, objectID int) (refs db.IntegrationReferrers, err error) { diff --git a/db/bolt/integrations.go b/db/bolt/integrations.go index f2b56923..ec8c07d9 100644 --- a/db/bolt/integrations.go +++ b/db/bolt/integrations.go @@ -2,6 +2,7 @@ package bolt import ( "github.com/ansible-semaphore/semaphore/db" + "go.etcd.io/bbolt" "reflect" ) @@ -185,13 +186,29 @@ var integrationAliasProps = db.ObjectProps{ PrimaryColumnName: "alias", } +var projectLevelIntegrationId = -1 + func (d *BoltDb) CreateIntegrationAlias(alias db.IntegrationAlias) (res db.IntegrationAlias, err error) { + newAlias, err := d.createObject(alias.ProjectID, db.IntegrationAliasProps, alias) + + if err != nil { + return + } + + res = newAlias.(db.IntegrationAlias) + + _, err = d.createObject(-1, integrationAliasProps, alias) + + if err != nil { + _ = d.DeleteIntegrationAlias(alias.ProjectID, alias.IntegrationID) + return + } + return } func (d *BoltDb) GetIntegrationAlias(projectID int, integrationID *int) (res db.IntegrationAlias, err error) { if integrationID == nil { - projectLevelIntegrationId := -1 integrationID = &projectLevelIntegrationId } err = d.getObject(projectID, db.IntegrationAliasProps, intObjectID(*integrationID), &res) @@ -206,9 +223,57 @@ func (d *BoltDb) GetIntegrationAliasByAlias(alias string) (res db.IntegrationAli } func (d *BoltDb) UpdateIntegrationAlias(alias db.IntegrationAlias) error { - return nil + + var integrationID int + if alias.IntegrationID == nil { + integrationID = projectLevelIntegrationId + } else { + integrationID = *alias.IntegrationID + } + + oldAlias, err := d.GetIntegrationAlias(alias.ProjectID, &integrationID) + if err != nil { + return err + } + + err = d.db.Update(func(tx *bbolt.Tx) error { + err := d.updateObjectTx(tx, alias.ProjectID, db.IntegrationAliasProps, alias) + if err != nil { + return err + } + + err = d.deleteObject(-1, integrationAliasProps, strObjectID(oldAlias.Alias), tx) + if err != nil { + return err + } + + _, err = d.createObjectTx(tx, -1, integrationAliasProps, strObjectID(alias.Alias)) + + return err + }) + + return err } -func (d *BoltDb) DeleteIntegrationAlias(projectID int, integrationID *int) error { - return nil +func (d *BoltDb) DeleteIntegrationAlias(projectID int, integrationID *int) (err error) { + if integrationID == nil { + integrationID = &projectLevelIntegrationId + } + + alias, err := d.GetIntegrationAlias(projectID, integrationID) + if err != nil { + return + } + + err = d.deleteObject(projectID, db.IntegrationAliasProps, intObjectID(*integrationID), nil) + if err != nil { + return + } + + err = d.deleteObject(-1, integrationAliasProps, strObjectID(alias.Alias), nil) + if err != nil { + return + } + + return } From b1396dcae2d613da643f9c44eb211386462f9518 Mon Sep 17 00:00:00 2001 From: fiftin Date: Thu, 7 Mar 2024 19:50:48 +0100 Subject: [PATCH 315/346] feat: add project type --- db/Migration.go | 1 + db/Project.go | 1 + db/sql/migrations/v2.9.62.sql | 1 + 3 files changed, 3 insertions(+) create mode 100644 db/sql/migrations/v2.9.62.sql diff --git a/db/Migration.go b/db/Migration.go index 1b22b542..57702ff2 100644 --- a/db/Migration.go +++ b/db/Migration.go @@ -64,6 +64,7 @@ func GetMigrations() []Migration { {Version: "2.9.46"}, {Version: "2.9.60"}, {Version: "2.9.61"}, + {Version: "2.9.62"}, } } diff --git a/db/Project.go b/db/Project.go index 859f10e7..725d26db 100644 --- a/db/Project.go +++ b/db/Project.go @@ -12,4 +12,5 @@ type Project struct { Alert bool `db:"alert" json:"alert"` AlertChat *string `db:"alert_chat" json:"alert_chat"` MaxParallelTasks int `db:"max_parallel_tasks" json:"max_parallel_tasks"` + Type string `db:"type" json:"type"` } diff --git a/db/sql/migrations/v2.9.62.sql b/db/sql/migrations/v2.9.62.sql new file mode 100644 index 00000000..f97c3d63 --- /dev/null +++ b/db/sql/migrations/v2.9.62.sql @@ -0,0 +1 @@ +alter table project add `type` varchar(20) default ''; \ No newline at end of file From 60976d1afb485bd7f53bf71514aa4fbad63c0f0c Mon Sep 17 00:00:00 2001 From: fiftin Date: Fri, 8 Mar 2024 18:13:16 +0100 Subject: [PATCH 316/346] fix(bolt): return not nil --- db/bolt/BoltDb.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/bolt/BoltDb.go b/db/bolt/BoltDb.go index 32b1416e..be92587f 100644 --- a/db/bolt/BoltDb.go +++ b/db/bolt/BoltDb.go @@ -541,7 +541,7 @@ func (d *BoltDb) createObjectTx(tx *bbolt.Tx, bucketID int, props db.ObjectProps return nil, err } - return nil, b.Put(objID.ToBytes(), str) + return object, b.Put(objID.ToBytes(), str) } func (d *BoltDb) createObject(bucketID int, props db.ObjectProps, object interface{}) (res interface{}, err error) { From 48febdeb0302fde27f2401aa53984a8e9806d6c1 Mon Sep 17 00:00:00 2001 From: fiftin Date: Sat, 9 Mar 2024 14:51:32 +0100 Subject: [PATCH 317/346] feat(ui): support confirmations on ui --- web/src/components/TaskLogView.vue | 59 ++++++++++++++++++++++++------ web/src/components/TaskStatus.vue | 12 ++++++ web/src/lang/en.js | 1 + 3 files changed, 60 insertions(+), 12 deletions(-) diff --git a/web/src/components/TaskLogView.vue b/web/src/components/TaskLogView.vue index a071d9fc..8d622c20 100644 --- a/web/src/components/TaskLogView.vue +++ b/web/src/components/TaskLogView.vue @@ -1,9 +1,9 @@ {{ item.message }} @@ -25,7 +25,7 @@@@ -34,7 +34,7 @@ {{ $t('author') }} -{{ user.name }} +{{ user.name || '-' }} - {{ $t('started') }} +{{ $t('started') || '-' }} {{ item.start | formatDate }} @@ -45,7 +45,7 @@- {{ $t('duration') }} +{{ $t('duration') || '-' }} {{ [item.start, item.end] | formatMilliseconds }} @@ -64,11 +64,37 @@+ Please confirm this task. +++ {{ $t('confirmTask') }} + + +{{ item.status === 'stopping' ? $t('forceStop') : $t('stop') }} @@ -90,7 +116,7 @@ overflow: auto; font-family: monospace; margin: 0 -24px; - padding: 5px 10px; + padding: 5px 10px 50px; } .task-log-view--with-message .task-log-records { @@ -156,7 +182,7 @@ export default { computed: { canStop() { - return ['running', 'stopping', 'waiting', 'starting'].includes(this.item.status); + return ['running', 'stopping', 'waiting', 'starting', 'waiting_confirmation', 'confirmed'].includes(this.item.status); }, }, @@ -166,6 +192,15 @@ export default { }, methods: { + async confirmTask() { + await axios({ + method: 'post', + url: `/api/project/${this.projectId}/tasks/${this.itemId}/confirm`, + responseType: 'json', + data: {}, + }); + }, + async stopTask(force) { await axios({ method: 'post', diff --git a/web/src/components/TaskStatus.vue b/web/src/components/TaskStatus.vue index e3062eee..fdac5532 100644 --- a/web/src/components/TaskStatus.vue +++ b/web/src/components/TaskStatus.vue @@ -43,6 +43,10 @@ export default { return 'mdi-stop-circle'; case TaskStatus.STOPPED: return 'mdi-stop-circle'; + case TaskStatus.CONFIRMED: + return 'mdi-check-circle'; + case TaskStatus.WAITING_CONFIRMATION: + return 'mdi-pause-circle'; default: throw new Error(`Unknown task status ${status}`); } @@ -64,6 +68,10 @@ export default { return 'Stopping...'; case TaskStatus.STOPPED: return 'Stopped'; + case TaskStatus.CONFIRMED: + return 'Confirmed'; + case TaskStatus.WAITING_CONFIRMATION: + return 'Waiting confirmation'; default: throw new Error(`Unknown task status ${status}`); } @@ -85,6 +93,10 @@ export default { return ''; case TaskStatus.STOPPED: return ''; + case TaskStatus.CONFIRMED: + return 'warning'; + case TaskStatus.WAITING_CONFIRMATION: + return 'warning'; default: throw new Error(`Unknown task status ${status}`); } diff --git a/web/src/lang/en.js b/web/src/lang/en.js index cb84beb9..edd8683a 100644 --- a/web/src/lang/en.js +++ b/web/src/lang/en.js @@ -137,6 +137,7 @@ export default { duration: 'Duration', stop: 'Stop', forceStop: 'Force Stop', + confirmTask: 'Confirm', deleteTeamMember: 'Delete team member', team2: 'Team', newTeamMember: 'New Team Member', From bdd758e59df8acc89338c2ee2eba2ac01d2fe312 Mon Sep 17 00:00:00 2001 From: fiftinDate: Sat, 9 Mar 2024 14:54:27 +0100 Subject: [PATCH 318/346] feat(be): add confirm endpoint --- api/projects/tasks.go | 20 +++++++++++++++++++- api/router.go | 1 + services/tasks/TaskPool.go | 6 +++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/api/projects/tasks.go b/api/projects/tasks.go index ee237a52..d45a67c3 100644 --- a/api/projects/tasks.go +++ b/api/projects/tasks.go @@ -1,11 +1,11 @@ package projects import ( - log "github.com/sirupsen/logrus" "github.com/ansible-semaphore/semaphore/api/helpers" "github.com/ansible-semaphore/semaphore/db" "github.com/ansible-semaphore/semaphore/util" "github.com/gorilla/context" + log "github.com/sirupsen/logrus" "net/http" "strconv" ) @@ -121,6 +121,24 @@ func GetTaskOutput(w http.ResponseWriter, r *http.Request) { helpers.WriteJSON(w, http.StatusOK, output) } +func ConfirmTask(w http.ResponseWriter, r *http.Request) { + targetTask := context.Get(r, "task").(db.Task) + project := context.Get(r, "project").(db.Project) + + if targetTask.ProjectID != project.ID { + w.WriteHeader(http.StatusBadRequest) + return + } + + err := helpers.TaskPool(r).ConfirmTask(targetTask) + if err != nil { + helpers.WriteError(w, err) + return + } + + w.WriteHeader(http.StatusNoContent) +} + func StopTask(w http.ResponseWriter, r *http.Request) { targetTask := context.Get(r, "task").(db.Task) project := context.Get(r, "project").(db.Project) diff --git a/api/router.go b/api/router.go index 7e223a70..c464c9de 100644 --- a/api/router.go +++ b/api/router.go @@ -144,6 +144,7 @@ func Route() *mux.Router { projectTaskStop := authenticatedAPI.PathPrefix("/project/{project_id}").Subrouter() projectTaskStop.Use(projects.ProjectMiddleware, projects.GetTaskMiddleware, projects.GetMustCanMiddleware(db.CanRunProjectTasks)) projectTaskStop.HandleFunc("/tasks/{task_id}/stop", projects.StopTask).Methods("POST") + projectTaskStop.HandleFunc("/tasks/{task_id}/confirm", projects.StopTask).Methods("POST") // // Project resources CRUD diff --git a/services/tasks/TaskPool.go b/services/tasks/TaskPool.go index 4fb86810..2ddcaf7e 100644 --- a/services/tasks/TaskPool.go +++ b/services/tasks/TaskPool.go @@ -9,8 +9,8 @@ import ( "strings" "time" - log "github.com/sirupsen/logrus" "github.com/ansible-semaphore/semaphore/util" + log "github.com/sirupsen/logrus" ) type logRecord struct { @@ -215,6 +215,10 @@ func CreateTaskPool(store db.Store) TaskPool { } } +func (p *TaskPool) ConfirmTask(targetTask db.Task) error { + return nil +} + func (p *TaskPool) StopTask(targetTask db.Task, forceStop bool) error { tsk := p.GetTask(targetTask.ID) if tsk == nil { // task not active, but exists in database From c150d90a75ef2a3f348bca4fa660950e654baa96 Mon Sep 17 00:00:00 2001 From: fiftin Date: Sat, 9 Mar 2024 15:01:20 +0100 Subject: [PATCH 319/346] feat(be): implement confirmation endpoint --- services/tasks/TaskPool.go | 9 +++++++++ web/src/components/TaskLogView.vue | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/services/tasks/TaskPool.go b/services/tasks/TaskPool.go index 2ddcaf7e..eaf46e29 100644 --- a/services/tasks/TaskPool.go +++ b/services/tasks/TaskPool.go @@ -1,6 +1,7 @@ package tasks import ( + "fmt" "github.com/ansible-semaphore/semaphore/db" "github.com/ansible-semaphore/semaphore/db_lib" "github.com/ansible-semaphore/semaphore/lib" @@ -216,6 +217,14 @@ func CreateTaskPool(store db.Store) TaskPool { } func (p *TaskPool) ConfirmTask(targetTask db.Task) error { + tsk := p.GetTask(targetTask.ID) + + if tsk == nil { // task not active, but exists in database + return fmt.Errorf("task is not active") + } + + tsk.SetStatus(lib.TaskConfirmed) + return nil } diff --git a/web/src/components/TaskLogView.vue b/web/src/components/TaskLogView.vue index 8d622c20..0e2060a2 100644 --- a/web/src/components/TaskLogView.vue +++ b/web/src/components/TaskLogView.vue @@ -13,7 +13,7 @@ - +From 1951e8ebb2795098260afe1fc848e802811a1db9 Mon Sep 17 00:00:00 2001 From: fiftinDate: Sat, 9 Mar 2024 20:03:19 +0100 Subject: [PATCH 320/346] feat: add type for inventory --- db/sql/migrations/v2.9.62.sql | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/db/sql/migrations/v2.9.62.sql b/db/sql/migrations/v2.9.62.sql index f97c3d63..d9a0ba20 100644 --- a/db/sql/migrations/v2.9.62.sql +++ b/db/sql/migrations/v2.9.62.sql @@ -1 +1,3 @@ -alter table project add `type` varchar(20) default ''; \ No newline at end of file +alter table project add `type` varchar(20) default ''; + +alter table project__inventory add `type` varchar(20) default ''; \ No newline at end of file From 84f7dae074b751009ca6cd7711938341ed7e64e1 Mon Sep 17 00:00:00 2001 From: fiftin Date: Sat, 9 Mar 2024 20:05:12 +0100 Subject: [PATCH 321/346] feat: remove type for inventory :) --- db/sql/migrations/v2.9.62.sql | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/db/sql/migrations/v2.9.62.sql b/db/sql/migrations/v2.9.62.sql index d9a0ba20..f97c3d63 100644 --- a/db/sql/migrations/v2.9.62.sql +++ b/db/sql/migrations/v2.9.62.sql @@ -1,3 +1 @@ -alter table project add `type` varchar(20) default ''; - -alter table project__inventory add `type` varchar(20) default ''; \ No newline at end of file +alter table project add `type` varchar(20) default ''; \ No newline at end of file From 479eb889f8fe50a46d2dfe4da386d381b22c7ba8 Mon Sep 17 00:00:00 2001 From: fiftin Date: Sat, 9 Mar 2024 20:23:38 +0100 Subject: [PATCH 322/346] refactor: add type InventoryType --- db/Inventory.go | 11 +++++++---- services/project/types.go | 10 +++++----- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/db/Inventory.go b/db/Inventory.go index 740b9b41..1561fc93 100644 --- a/db/Inventory.go +++ b/db/Inventory.go @@ -1,9 +1,12 @@ package db +type InventoryType string + const ( - InventoryStatic = "static" - InventoryStaticYaml = "static-yaml" - InventoryFile = "file" + InventoryStatic InventoryType = "static" + InventoryStaticYaml InventoryType = "static-yaml" + // InventoryFile means that it is path to the Ansible inventory file + InventoryFile InventoryType = "file" ) // Inventory is the model of an ansible inventory file @@ -21,7 +24,7 @@ type Inventory struct { BecomeKey AccessKey `db:"-" json:"-"` // static/file - Type string `db:"type" json:"type"` + Type InventoryType `db:"type" json:"type"` } func FillInventory(d Store, inventory *Inventory) (err error) { diff --git a/services/project/types.go b/services/project/types.go index b287df03..87807ade 100644 --- a/services/project/types.go +++ b/services/project/types.go @@ -50,11 +50,11 @@ type BackupView struct { } type BackupInventory struct { - Name string `json:"name"` - Inventory string `json:"inventory"` - SSHKey *string `json:"ssh_key"` - BecomeKey *string `json:"become_key"` - Type string `json:"type"` + Name string `json:"name"` + Inventory string `json:"inventory"` + SSHKey *string `json:"ssh_key"` + BecomeKey *string `json:"become_key"` + Type db.InventoryType `json:"type"` } type BackupRepository struct { From 2bdcc8b043852ec79f75fd3c57e8b35ae62d51af Mon Sep 17 00:00:00 2001 From: fiftin Date: Sun, 10 Mar 2024 13:08:45 +0100 Subject: [PATCH 323/346] feat: add fields to migrations --- db/sql/migrations/v2.9.62.sql | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/db/sql/migrations/v2.9.62.sql b/db/sql/migrations/v2.9.62.sql index f97c3d63..4dd158f6 100644 --- a/db/sql/migrations/v2.9.62.sql +++ b/db/sql/migrations/v2.9.62.sql @@ -1 +1,5 @@ -alter table project add `type` varchar(20) default ''; \ No newline at end of file +alter table project add `type` varchar(20) default ''; + +alter table task add `inventory_id` int null references project__inventory(`id`) on delete set null; + +alter table project__inventory add `holder_id` int null references project__template(`id`) on delete cascade; \ No newline at end of file From a90b270dc56105d4feaf92fb2034d17bf5a8df1a Mon Sep 17 00:00:00 2001 From: fiftin Date: Sun, 10 Mar 2024 13:13:44 +0100 Subject: [PATCH 324/346] feat(be): add fields to the models --- db/Inventory.go | 2 ++ db/Task.go | 2 ++ db/sql/inventory.go | 9 ++++++--- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/db/Inventory.go b/db/Inventory.go index 1561fc93..2ca4f8a5 100644 --- a/db/Inventory.go +++ b/db/Inventory.go @@ -25,6 +25,8 @@ type Inventory struct { // static/file Type InventoryType `db:"type" json:"type"` + + HolderID *int `db:"holder_id" json:"holder_id"` } func FillInventory(d Store, inventory *Inventory) (err error) { diff --git a/db/Task.go b/db/Task.go index 72a5a441..5f99fa99 100644 --- a/db/Task.go +++ b/db/Task.go @@ -44,6 +44,8 @@ type Task struct { Version *string `db:"version" json:"version"` Arguments *string `db:"arguments" json:"arguments"` + + InventoryID *int `db:"inventory_id" json:"inventory_id"` } func (task *Task) GetIncomingVersion(d Store) *string { diff --git a/db/sql/inventory.go b/db/sql/inventory.go index 656e2d30..5087e99e 100644 --- a/db/sql/inventory.go +++ b/db/sql/inventory.go @@ -28,12 +28,13 @@ func (d *SqlDb) DeleteInventory(projectID int, inventoryID int) error { func (d *SqlDb) UpdateInventory(inventory db.Inventory) error { _, err := d.exec( - "update project__inventory set name=?, type=?, ssh_key_id=?, inventory=?, become_key_id=? where id=?", + "update project__inventory set name=?, type=?, ssh_key_id=?, inventory=?, become_key_id=?, holder_id=? where id=?", inventory.Name, inventory.Type, inventory.SSHKeyID, inventory.Inventory, inventory.BecomeKeyID, + inventory.HolderID, inventory.ID) return err @@ -42,13 +43,15 @@ func (d *SqlDb) UpdateInventory(inventory db.Inventory) error { func (d *SqlDb) CreateInventory(inventory db.Inventory) (newInventory db.Inventory, err error) { insertID, err := d.insert( "id", - "insert into project__inventory (project_id, name, type, ssh_key_id, inventory, become_key_id) values (?, ?, ?, ?, ?, ?)", + "insert into project__inventory (project_id, name, type, ssh_key_id, inventory, become_key_id, holder_id) values "+ + "(?, ?, ?, ?, ?, ?, ?)", inventory.ProjectID, inventory.Name, inventory.Type, inventory.SSHKeyID, inventory.Inventory, - inventory.BecomeKeyID) + inventory.BecomeKeyID, + inventory.HolderID) if err != nil { return From 710c7df27532a1c8acde0a0ab79ae0740ca10d90 Mon Sep 17 00:00:00 2001 From: fiftin Date: Sun, 10 Mar 2024 18:55:42 +0100 Subject: [PATCH 325/346] feat: implement options for boltdb --- db/Option.go | 6 ++++ db/Store.go | 10 +++++++ db/bolt/option.go | 32 +++++++++++++++++++++ db/bolt/option_test.go | 54 +++++++++++++++++++++++++++++++++++ db/sql/migrations/v2.9.62.sql | 7 ++++- db/sql/option.go | 9 ++++++ 6 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 db/Option.go create mode 100644 db/bolt/option.go create mode 100644 db/bolt/option_test.go create mode 100644 db/sql/option.go diff --git a/db/Option.go b/db/Option.go new file mode 100644 index 00000000..b9b806a9 --- /dev/null +++ b/db/Option.go @@ -0,0 +1,6 @@ +package db + +type Option struct { + Key string `db:"key" json:"key"` + Value string `db:"value" json:"value"` +} diff --git a/db/Store.go b/db/Store.go index af0c7ebe..6c4c03de 100644 --- a/db/Store.go +++ b/db/Store.go @@ -109,6 +109,9 @@ type Store interface { // if a rollback exists TryRollbackMigration(version Migration) + GetOption(key string) (string, error) + SetOption(key string, value string) error + GetEnvironment(projectID int, environmentID int) (Environment, error) GetEnvironmentRefs(projectID int, environmentID int) (ObjectReferrers, error) GetEnvironments(projectID int, params RetrieveQueryParams) ([]Environment, error) @@ -390,6 +393,13 @@ var GlobalRunnerProps = ObjectProps{ IsGlobal: true, } +var OptionProps = ObjectProps{ + TableName: "option", + Type: reflect.TypeOf(Option{}), + PrimaryColumnName: "key", + IsGlobal: true, +} + func (p ObjectProps) GetReferringFieldsFrom(t reflect.Type) (fields []string, err error) { n := t.NumField() for i := 0; i < n; i++ { diff --git a/db/bolt/option.go b/db/bolt/option.go new file mode 100644 index 00000000..0eed2616 --- /dev/null +++ b/db/bolt/option.go @@ -0,0 +1,32 @@ +package bolt + +import ( + "errors" + "github.com/ansible-semaphore/semaphore/db" +) + +func (d *BoltDb) SetOption(key string, value string) error { + + opt := db.Option{ + Key: key, + Value: value, + } + + _, err := d.GetOption(key) + + if errors.Is(err, db.ErrNotFound) { + _, err = d.createObject(-1, db.OptionProps, opt) + return err + } else { + err = d.updateObject(-1, db.OptionProps, opt) + } + + return err +} + +func (d *BoltDb) GetOption(key string) (value string, err error) { + var option db.Option + err = d.getObject(-1, db.OptionProps, strObjectID(key), &option) + value = option.Value + return +} diff --git a/db/bolt/option_test.go b/db/bolt/option_test.go new file mode 100644 index 00000000..4704747d --- /dev/null +++ b/db/bolt/option_test.go @@ -0,0 +1,54 @@ +package bolt + +import ( + "errors" + "github.com/ansible-semaphore/semaphore/db" + "testing" +) + +func TestGetOption(t *testing.T) { + store := CreateTestStore() + + _, err := store.GetOption("unknown_option") + + if !errors.Is(err, db.ErrNotFound) { + t.Fatal("Result must be nil for non-existent option") + } +} + +func TestGetSetOption(t *testing.T) { + store := CreateTestStore() + + err := store.SetOption("age", "33") + + if err != nil { + t.Fatal("Can not save option") + } + + val, err := store.GetOption("age") + + if err != nil { + t.Fatal("Can not get option") + } + + if val != "33" { + t.Fatal("Invalid option value") + } + + err = store.SetOption("age", "22") + + if err != nil { + t.Fatal("Can not save option") + } + + val, err = store.GetOption("age") + + if err != nil { + t.Fatal("Can not get option") + } + + if val != "22" { + t.Fatal("Invalid option value") + } + +} diff --git a/db/sql/migrations/v2.9.62.sql b/db/sql/migrations/v2.9.62.sql index 4dd158f6..db41a716 100644 --- a/db/sql/migrations/v2.9.62.sql +++ b/db/sql/migrations/v2.9.62.sql @@ -2,4 +2,9 @@ alter table project add `type` varchar(20) default ''; alter table task add `inventory_id` int null references project__inventory(`id`) on delete set null; -alter table project__inventory add `holder_id` int null references project__template(`id`) on delete cascade; \ No newline at end of file +alter table project__inventory add `holder_id` int null references project__template(`id`) on delete cascade; + +create table `option` ( + `key` varchar(255) primary key not null, + `value` varchar(255) not null +); diff --git a/db/sql/option.go b/db/sql/option.go new file mode 100644 index 00000000..1361aadb --- /dev/null +++ b/db/sql/option.go @@ -0,0 +1,9 @@ +package sql + +func (d *SqlDb) SetOption(key string, value string) error { + return nil +} + +func (d *SqlDb) GetOption(key string) (value string, err error) { + return +} From 522513b375c6a38a5f561767d6354df557578e65 Mon Sep 17 00:00:00 2001 From: fiftin Date: Sun, 10 Mar 2024 19:00:15 +0100 Subject: [PATCH 326/346] feat(be): implement options for boltdb --- db/bolt/option.go | 16 ++++++++++++++-- db/bolt/option_test.go | 8 +++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/db/bolt/option.go b/db/bolt/option.go index 0eed2616..dbec854d 100644 --- a/db/bolt/option.go +++ b/db/bolt/option.go @@ -12,7 +12,7 @@ func (d *BoltDb) SetOption(key string, value string) error { Value: value, } - _, err := d.GetOption(key) + _, err := d.getOption(key) if errors.Is(err, db.ErrNotFound) { _, err = d.createObject(-1, db.OptionProps, opt) @@ -24,9 +24,21 @@ func (d *BoltDb) SetOption(key string, value string) error { return err } -func (d *BoltDb) GetOption(key string) (value string, err error) { +func (d *BoltDb) getOption(key string) (value string, err error) { var option db.Option err = d.getObject(-1, db.OptionProps, strObjectID(key), &option) value = option.Value return } + +func (d *BoltDb) GetOption(key string) (value string, err error) { + var option db.Option + err = d.getObject(-1, db.OptionProps, strObjectID(key), &option) + value = option.Value + + if errors.Is(err, db.ErrNotFound) { + err = nil + } + + return +} diff --git a/db/bolt/option_test.go b/db/bolt/option_test.go index 4704747d..38204faf 100644 --- a/db/bolt/option_test.go +++ b/db/bolt/option_test.go @@ -1,18 +1,16 @@ package bolt import ( - "errors" - "github.com/ansible-semaphore/semaphore/db" "testing" ) func TestGetOption(t *testing.T) { store := CreateTestStore() - _, err := store.GetOption("unknown_option") + val, err := store.GetOption("unknown_option") - if !errors.Is(err, db.ErrNotFound) { - t.Fatal("Result must be nil for non-existent option") + if err != nil && val != "" { + t.Fatal("Result must be empty string for non-existent option") } } From 51c4a95268fcdfad2f8ea8371575e02828ef7cce Mon Sep 17 00:00:00 2001 From: fiftin Date: Sun, 10 Mar 2024 19:14:08 +0100 Subject: [PATCH 327/346] feat: implement options for sql --- db/sql/option.go | 51 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/db/sql/option.go b/db/sql/option.go index 1361aadb..c74f167b 100644 --- a/db/sql/option.go +++ b/db/sql/option.go @@ -1,9 +1,58 @@ package sql +import ( + "database/sql" + "errors" + "github.com/Masterminds/squirrel" + "github.com/ansible-semaphore/semaphore/db" +) + func (d *SqlDb) SetOption(key string, value string) error { - return nil + _, err := d.getOption(key) + + if errors.Is(err, db.ErrNotFound) { + _, err = d.exec("update option set value=? where key=?", key) + } else { + _, err = d.insert( + "key", + "insert into option (key, value) values (?, ?)", + key, value) + } + + return err +} + +func (d *SqlDb) getOption(key string) (value string, err error) { + q := squirrel.Select("*"). + From(db.OptionProps.TableName). + Where("key=?", key) + + query, args, err := q.ToSql() + + if err != nil { + return + } + + var opt db.Option + + err = d.selectOne(&opt, query, args...) + + if errors.Is(err, sql.ErrNoRows) { + err = db.ErrNotFound + } + + value = opt.Value + + return } func (d *SqlDb) GetOption(key string) (value string, err error) { + + value, err = d.getOption(key) + + if errors.Is(err, db.ErrNotFound) { + err = nil + } + return } From 741a6748fdf24d93f7e84cfe1ad87851d0ac8991 Mon Sep 17 00:00:00 2001 From: "gavrilov.nikita" Date: Sun, 10 Mar 2024 22:07:19 +0300 Subject: [PATCH 328/346] Fix deprecation io/ioutil --- cli/setup/setup.go | 3 +-- db/AccessKey.go | 11 +++++------ db_lib/AnsibleApp.go | 5 ++--- services/runners/JobPool.go | 8 ++++---- services/tasks/LobalJob_inventory.go | 4 ++-- 5 files changed, 14 insertions(+), 17 deletions(-) diff --git a/cli/setup/setup.go b/cli/setup/setup.go index db7c6c5f..039d6ec5 100644 --- a/cli/setup/setup.go +++ b/cli/setup/setup.go @@ -2,7 +2,6 @@ package setup import ( "fmt" - "io/ioutil" "os" "path/filepath" "strings" @@ -156,7 +155,7 @@ func SaveConfig(config *util.ConfigType) (configPath string) { } configPath = filepath.Join(configDirectory, "config.json") - if err = ioutil.WriteFile(configPath, bytes, 0644); err != nil { + if err = os.WriteFile(configPath, bytes, 0644); err != nil { panic(err) } diff --git a/db/AccessKey.go b/db/AccessKey.go index 67239047..45aa126a 100644 --- a/db/AccessKey.go +++ b/db/AccessKey.go @@ -9,7 +9,6 @@ import ( "fmt" "github.com/ansible-semaphore/semaphore/lib" "io" - "io/ioutil" "math/big" "os" "path" @@ -132,12 +131,12 @@ func (key *AccessKey) Install(usage AccessKeyRole, logger lib.Logger) (installat agent, err = key.startSshAgent(logger) installation.SshAgent = &agent - //err = ioutil.WriteFile(installationPath, []byte(key.SshKey.PrivateKey+"\n"), 0600) + //err = os.WriteFile(installationPath, []byte(key.SshKey.PrivateKey+"\n"), 0600) } case AccessKeyRoleAnsiblePasswordVault: switch key.Type { case AccessKeyLoginPassword: - err = ioutil.WriteFile(installationPath, []byte(key.LoginPassword.Password), 0600) + err = os.WriteFile(installationPath, []byte(key.LoginPassword.Password), 0600) } case AccessKeyRoleAnsibleBecomeUser: switch key.Type { @@ -152,7 +151,7 @@ func (key *AccessKey) Install(usage AccessKeyRole, logger lib.Logger) (installat if err != nil { return } - err = ioutil.WriteFile(installationPath, bytes, 0600) + err = os.WriteFile(installationPath, bytes, 0600) default: err = fmt.Errorf("access key type not supported for ansible user") } @@ -162,7 +161,7 @@ func (key *AccessKey) Install(usage AccessKeyRole, logger lib.Logger) (installat var agent lib.SshAgent agent, err = key.startSshAgent(logger) installation.SshAgent = &agent - //err = ioutil.WriteFile(installationPath, []byte(key.SshKey.PrivateKey+"\n"), 0600) + //err = os.WriteFile(installationPath, []byte(key.SshKey.PrivateKey+"\n"), 0600) case AccessKeyLoginPassword: content := make(map[string]string) content["ansible_user"] = key.LoginPassword.Login @@ -172,7 +171,7 @@ func (key *AccessKey) Install(usage AccessKeyRole, logger lib.Logger) (installat if err != nil { return } - err = ioutil.WriteFile(installationPath, bytes, 0600) + err = os.WriteFile(installationPath, bytes, 0600) default: err = fmt.Errorf("access key type not supported for ansible user") diff --git a/db_lib/AnsibleApp.go b/db_lib/AnsibleApp.go index 601280c1..f685a735 100644 --- a/db_lib/AnsibleApp.go +++ b/db_lib/AnsibleApp.go @@ -6,7 +6,6 @@ import ( "github.com/ansible-semaphore/semaphore/db" "github.com/ansible-semaphore/semaphore/lib" "io" - "io/ioutil" "os" "path" ) @@ -26,7 +25,7 @@ func getMD5Hash(filepath string) (string, error) { } func hasRequirementsChanges(requirementsFilePath string, requirementsHashFilePath string) bool { - oldFileMD5HashBytes, err := ioutil.ReadFile(requirementsHashFilePath) + oldFileMD5HashBytes, err := os.ReadFile(requirementsHashFilePath) if err != nil { return true } @@ -45,7 +44,7 @@ func writeMD5Hash(requirementsFile string, requirementsHashFile string) error { return err } - return ioutil.WriteFile(requirementsHashFile, []byte(newFileMD5Hash), 0644) + return os.WriteFile(requirementsHashFile, []byte(newFileMD5Hash), 0644) } type AnsibleApp struct { diff --git a/services/runners/JobPool.go b/services/runners/JobPool.go index 00ea6b5c..76d6904f 100644 --- a/services/runners/JobPool.go +++ b/services/runners/JobPool.go @@ -10,13 +10,13 @@ import ( "bytes" "encoding/json" "fmt" - log "github.com/sirupsen/logrus" "github.com/ansible-semaphore/semaphore/db" "github.com/ansible-semaphore/semaphore/db_lib" "github.com/ansible-semaphore/semaphore/lib" "github.com/ansible-semaphore/semaphore/services/tasks" "github.com/ansible-semaphore/semaphore/util" - "io/ioutil" + log "github.com/sirupsen/logrus" + "io" "net/http" "os" "os/exec" @@ -388,7 +388,7 @@ func (p *JobPool) tryRegisterRunner() bool { return false } - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { fmt.Println("Error reading response body:", err) return false @@ -446,7 +446,7 @@ func (p *JobPool) checkNewJobs() { defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { log.Error("Checking new jobs, error reading response body:", err) return diff --git a/services/tasks/LobalJob_inventory.go b/services/tasks/LobalJob_inventory.go index c5aeb005..0b72d631 100644 --- a/services/tasks/LobalJob_inventory.go +++ b/services/tasks/LobalJob_inventory.go @@ -2,7 +2,7 @@ package tasks import ( "github.com/ansible-semaphore/semaphore/db" - "io/ioutil" + "os" "strconv" "github.com/ansible-semaphore/semaphore/util" @@ -39,7 +39,7 @@ func (t *LocalJob) installStaticInventory() error { } // create inventory file - return ioutil.WriteFile(path, []byte(t.Inventory.Inventory), 0664) + return os.WriteFile(path, []byte(t.Inventory.Inventory), 0664) } func (t *LocalJob) destroyKeys() { From f6f3e4228f33a95e3276fef3f97850d248fcb1f9 Mon Sep 17 00:00:00 2001 From: fiftin Date: Sun, 10 Mar 2024 22:51:28 +0100 Subject: [PATCH 329/346] feat: make inventory optional for template --- api/projects/projects.go | 10 +++++----- db/Template.go | 2 +- services/project/backup.go | 5 ++++- services/project/restore.go | 2 +- services/tasks/TaskRunner.go | 10 ++++++---- 5 files changed, 17 insertions(+), 12 deletions(-) diff --git a/api/projects/projects.go b/api/projects/projects.go index 5e91ce40..d4aebe83 100644 --- a/api/projects/projects.go +++ b/api/projects/projects.go @@ -1,10 +1,10 @@ package projects import ( - log "github.com/sirupsen/logrus" "github.com/ansible-semaphore/semaphore/api/helpers" "github.com/ansible-semaphore/semaphore/db" "github.com/ansible-semaphore/semaphore/util" + log "github.com/sirupsen/logrus" "net/http" "github.com/gorilla/context" @@ -128,7 +128,7 @@ func createDemoProject(projectID int, store db.Store) (err error) { Playbook: "ping.yml", Description: &desc, ProjectID: projectID, - InventoryID: prodInv.ID, + InventoryID: &prodInv.ID, EnvironmentID: &emptyEnv.ID, RepositoryID: demoRepo.ID, }) @@ -145,7 +145,7 @@ func createDemoProject(projectID int, store db.Store) (err error) { Playbook: "build.yml", Type: db.TemplateBuild, ProjectID: projectID, - InventoryID: buildInv.ID, + InventoryID: &buildInv.ID, EnvironmentID: &emptyEnv.ID, RepositoryID: demoRepo.ID, StartVersion: &startVersion, @@ -160,7 +160,7 @@ func createDemoProject(projectID int, store db.Store) (err error) { Type: db.TemplateDeploy, Playbook: "deploy.yml", ProjectID: projectID, - InventoryID: devInv.ID, + InventoryID: &devInv.ID, EnvironmentID: &emptyEnv.ID, RepositoryID: demoRepo.ID, BuildTemplateID: &buildTpl.ID, @@ -177,7 +177,7 @@ func createDemoProject(projectID int, store db.Store) (err error) { Type: db.TemplateDeploy, Playbook: "deploy.yml", ProjectID: projectID, - InventoryID: prodInv.ID, + InventoryID: &prodInv.ID, EnvironmentID: &emptyEnv.ID, RepositoryID: demoRepo.ID, BuildTemplateID: &buildTpl.ID, diff --git a/db/Template.go b/db/Template.go index 42183457..a689cd4c 100644 --- a/db/Template.go +++ b/db/Template.go @@ -44,7 +44,7 @@ type Template struct { ID int `db:"id" json:"id"` ProjectID int `db:"project_id" json:"project_id"` - InventoryID int `db:"inventory_id" json:"inventory_id"` + InventoryID *int `db:"inventory_id" json:"inventory_id"` RepositoryID int `db:"repository_id" json:"repository_id"` EnvironmentID *int `db:"environment_id" json:"environment_id"` diff --git a/services/project/backup.go b/services/project/backup.go index b120919e..ff6b3be1 100644 --- a/services/project/backup.go +++ b/services/project/backup.go @@ -236,7 +236,10 @@ func (b *BackupDB) format() (*BackupFormat, error) { BuildTemplate, _ = findNameByID[db.Template](*o.BuildTemplateID, b.templates) } Repository, _ := findNameByID[db.Repository](o.RepositoryID, b.repositories) - Inventory, _ := findNameByID[db.Inventory](o.InventoryID, b.inventories) + var Inventory *string = nil + if o.InventoryID != nil { + Inventory, _ = findNameByID[db.Inventory](*o.InventoryID, b.inventories) + } templates[i] = BackupTemplate{ Name: o.Name, diff --git a/services/project/restore.go b/services/project/restore.go index 21dcfa3c..d2746645 100644 --- a/services/project/restore.go +++ b/services/project/restore.go @@ -241,7 +241,7 @@ func (e BackupTemplate) Restore(store db.Store, b *BackupDB) error { template, err := store.CreateTemplate( db.Template{ ProjectID: b.meta.ID, - InventoryID: InventoryID, + InventoryID: &InventoryID, EnvironmentID: &EnvironmentID, RepositoryID: RepositoryID, ViewID: ViewID, diff --git a/services/tasks/TaskRunner.go b/services/tasks/TaskRunner.go index 9f0da756..5fcb47fa 100644 --- a/services/tasks/TaskRunner.go +++ b/services/tasks/TaskRunner.go @@ -8,10 +8,10 @@ import ( "strings" "time" - log "github.com/sirupsen/logrus" "github.com/ansible-semaphore/semaphore/api/sockets" "github.com/ansible-semaphore/semaphore/db" "github.com/ansible-semaphore/semaphore/util" + log "github.com/sirupsen/logrus" ) type Job interface { @@ -265,9 +265,11 @@ func (t *TaskRunner) populateDetails() error { } // get inventory - t.Inventory, err = t.pool.store.GetInventory(t.Template.ProjectID, t.Template.InventoryID) - if err != nil { - return t.prepareError(err, "Template Inventory not found!") + if t.Template.InventoryID != nil { + t.Inventory, err = t.pool.store.GetInventory(t.Template.ProjectID, *t.Template.InventoryID) + if err != nil { + return t.prepareError(err, "Template Inventory not found!") + } } // get repository From 8a6d5821f8d2bfb4305202879564e042f50840cd Mon Sep 17 00:00:00 2001 From: fiftin Date: Sun, 10 Mar 2024 22:56:58 +0100 Subject: [PATCH 330/346] Revert "feat: make inventory optional for template" This reverts commit f6f3e4228f33a95e3276fef3f97850d248fcb1f9. --- api/projects/projects.go | 10 +++++----- db/Template.go | 2 +- services/project/backup.go | 5 +---- services/project/restore.go | 2 +- services/tasks/TaskRunner.go | 10 ++++------ 5 files changed, 12 insertions(+), 17 deletions(-) diff --git a/api/projects/projects.go b/api/projects/projects.go index d4aebe83..5e91ce40 100644 --- a/api/projects/projects.go +++ b/api/projects/projects.go @@ -1,10 +1,10 @@ package projects import ( + log "github.com/sirupsen/logrus" "github.com/ansible-semaphore/semaphore/api/helpers" "github.com/ansible-semaphore/semaphore/db" "github.com/ansible-semaphore/semaphore/util" - log "github.com/sirupsen/logrus" "net/http" "github.com/gorilla/context" @@ -128,7 +128,7 @@ func createDemoProject(projectID int, store db.Store) (err error) { Playbook: "ping.yml", Description: &desc, ProjectID: projectID, - InventoryID: &prodInv.ID, + InventoryID: prodInv.ID, EnvironmentID: &emptyEnv.ID, RepositoryID: demoRepo.ID, }) @@ -145,7 +145,7 @@ func createDemoProject(projectID int, store db.Store) (err error) { Playbook: "build.yml", Type: db.TemplateBuild, ProjectID: projectID, - InventoryID: &buildInv.ID, + InventoryID: buildInv.ID, EnvironmentID: &emptyEnv.ID, RepositoryID: demoRepo.ID, StartVersion: &startVersion, @@ -160,7 +160,7 @@ func createDemoProject(projectID int, store db.Store) (err error) { Type: db.TemplateDeploy, Playbook: "deploy.yml", ProjectID: projectID, - InventoryID: &devInv.ID, + InventoryID: devInv.ID, EnvironmentID: &emptyEnv.ID, RepositoryID: demoRepo.ID, BuildTemplateID: &buildTpl.ID, @@ -177,7 +177,7 @@ func createDemoProject(projectID int, store db.Store) (err error) { Type: db.TemplateDeploy, Playbook: "deploy.yml", ProjectID: projectID, - InventoryID: &prodInv.ID, + InventoryID: prodInv.ID, EnvironmentID: &emptyEnv.ID, RepositoryID: demoRepo.ID, BuildTemplateID: &buildTpl.ID, diff --git a/db/Template.go b/db/Template.go index a689cd4c..42183457 100644 --- a/db/Template.go +++ b/db/Template.go @@ -44,7 +44,7 @@ type Template struct { ID int `db:"id" json:"id"` ProjectID int `db:"project_id" json:"project_id"` - InventoryID *int `db:"inventory_id" json:"inventory_id"` + InventoryID int `db:"inventory_id" json:"inventory_id"` RepositoryID int `db:"repository_id" json:"repository_id"` EnvironmentID *int `db:"environment_id" json:"environment_id"` diff --git a/services/project/backup.go b/services/project/backup.go index ff6b3be1..b120919e 100644 --- a/services/project/backup.go +++ b/services/project/backup.go @@ -236,10 +236,7 @@ func (b *BackupDB) format() (*BackupFormat, error) { BuildTemplate, _ = findNameByID[db.Template](*o.BuildTemplateID, b.templates) } Repository, _ := findNameByID[db.Repository](o.RepositoryID, b.repositories) - var Inventory *string = nil - if o.InventoryID != nil { - Inventory, _ = findNameByID[db.Inventory](*o.InventoryID, b.inventories) - } + Inventory, _ := findNameByID[db.Inventory](o.InventoryID, b.inventories) templates[i] = BackupTemplate{ Name: o.Name, diff --git a/services/project/restore.go b/services/project/restore.go index d2746645..21dcfa3c 100644 --- a/services/project/restore.go +++ b/services/project/restore.go @@ -241,7 +241,7 @@ func (e BackupTemplate) Restore(store db.Store, b *BackupDB) error { template, err := store.CreateTemplate( db.Template{ ProjectID: b.meta.ID, - InventoryID: &InventoryID, + InventoryID: InventoryID, EnvironmentID: &EnvironmentID, RepositoryID: RepositoryID, ViewID: ViewID, diff --git a/services/tasks/TaskRunner.go b/services/tasks/TaskRunner.go index 5fcb47fa..9f0da756 100644 --- a/services/tasks/TaskRunner.go +++ b/services/tasks/TaskRunner.go @@ -8,10 +8,10 @@ import ( "strings" "time" + log "github.com/sirupsen/logrus" "github.com/ansible-semaphore/semaphore/api/sockets" "github.com/ansible-semaphore/semaphore/db" "github.com/ansible-semaphore/semaphore/util" - log "github.com/sirupsen/logrus" ) type Job interface { @@ -265,11 +265,9 @@ func (t *TaskRunner) populateDetails() error { } // get inventory - if t.Template.InventoryID != nil { - t.Inventory, err = t.pool.store.GetInventory(t.Template.ProjectID, *t.Template.InventoryID) - if err != nil { - return t.prepareError(err, "Template Inventory not found!") - } + t.Inventory, err = t.pool.store.GetInventory(t.Template.ProjectID, t.Template.InventoryID) + if err != nil { + return t.prepareError(err, "Template Inventory not found!") } // get repository From 4c8312bc9b9d329f2baec4db95a0815b748726e7 Mon Sep 17 00:00:00 2001 From: fiftin Date: Sun, 10 Mar 2024 22:58:35 +0100 Subject: [PATCH 331/346] feat(be): add none inventory type --- db/Inventory.go | 1 + 1 file changed, 1 insertion(+) diff --git a/db/Inventory.go b/db/Inventory.go index 2ca4f8a5..815c3acc 100644 --- a/db/Inventory.go +++ b/db/Inventory.go @@ -3,6 +3,7 @@ package db type InventoryType string const ( + InventoryNone InventoryType = "none" InventoryStatic InventoryType = "static" InventoryStaticYaml InventoryType = "static-yaml" // InventoryFile means that it is path to the Ansible inventory file From 4a5ca60c7085bd59577f254ec8cf35121d33f48b Mon Sep 17 00:00:00 2001 From: fiftin Date: Sun, 10 Mar 2024 23:06:17 +0100 Subject: [PATCH 332/346] feat: create none inventory by default --- api/projects/projects.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/api/projects/projects.go b/api/projects/projects.go index 5e91ce40..63d23275 100644 --- a/api/projects/projects.go +++ b/api/projects/projects.go @@ -1,10 +1,10 @@ package projects import ( - log "github.com/sirupsen/logrus" "github.com/ansible-semaphore/semaphore/api/helpers" "github.com/ansible-semaphore/semaphore/db" "github.com/ansible-semaphore/semaphore/util" + log "github.com/sirupsen/logrus" "net/http" "github.com/gorilla/context" @@ -223,6 +223,17 @@ func AddProject(w http.ResponseWriter, r *http.Request) { return } + _, err = store.CreateInventory(db.Inventory{ + Name: "None", + ProjectID: body.ID, + Type: "none", + }) + + if err != nil { + helpers.WriteError(w, err) + return + } + if bodyWithDemo.Demo { err = createDemoProject(body.ID, store) From b073e5ca8e0ab894e2dcb086a94189571762ddc1 Mon Sep 17 00:00:00 2001 From: fiftin Date: Sun, 10 Mar 2024 23:58:25 +0100 Subject: [PATCH 333/346] feat(ui): rename restore button --- web/src/App.vue | 2 +- web/src/lang/de.js | 2 +- web/src/lang/en.js | 2 +- web/src/lang/es.js | 2 +- web/src/lang/fr.js | 2 +- web/src/lang/it.js | 2 +- web/src/lang/nl.js | 2 +- web/src/lang/pl.js | 2 +- web/src/lang/pt.js | 2 +- web/src/lang/pt_br.js | 2 +- web/src/lang/ru.js | 2 +- web/src/lang/zh_hans.js | 2 +- web/src/lang/zh_hant.js | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/web/src/App.vue b/web/src/App.vue index 5a92feac..e33da7c1 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -180,7 +180,7 @@ - {{ $t('restore') }} + {{ $t('restoreProject') }} diff --git a/web/src/lang/de.js b/web/src/lang/de.js index 3c7d1ee7..0bb6f8ed 100644 --- a/web/src/lang/de.js +++ b/web/src/lang/de.js @@ -1,7 +1,7 @@ export default { backup: 'Sicherung', downloadTheProjectBackupFile: 'Laden Sie die Projektsicherungsdatei (in JSON) herunter.', - restore: 'Wiederherstellen...', + restoreProject: 'Wiederherstellen...', incorrectUsrPwd: 'Falscher Benutzername oder falsches Passwort', askDeleteUser: 'Soll dieser Benutzer gelöscht werden?', askDeleteTemp: 'Soll diese Vorlage gelöscht werden?', diff --git a/web/src/lang/en.js b/web/src/lang/en.js index edd8683a..10dfef42 100644 --- a/web/src/lang/en.js +++ b/web/src/lang/en.js @@ -1,7 +1,7 @@ export default { backup: 'Backup', downloadTheProjectBackupFile: 'Download the project backup file (in json)', - restore: 'Restore...', + restoreProject: 'Restore Project...', incorrectUsrPwd: 'Incorrect login or password', askDeleteUser: 'Do you really want to delete this user?', askDeleteTemp: 'Do you really want to delete this template?', diff --git a/web/src/lang/es.js b/web/src/lang/es.js index e4938a3a..f0b37b59 100644 --- a/web/src/lang/es.js +++ b/web/src/lang/es.js @@ -1,7 +1,7 @@ export default { backup: 'Respaldo', downloadTheProjectBackupFile: 'Descargue el archivo de copia de seguridad del proyecto (en json)', - restore: 'Restaurar...', + restoreProject: 'Restaurar...', incorrectUsrPwd: 'Usuario o contraseña incorrecta', askDeleteUser: '¿Realmente desea eliminar este usuario?', askDeleteTemp: '¿Realmente desea eliminar esta plantilla?', diff --git a/web/src/lang/fr.js b/web/src/lang/fr.js index fedf3106..f147bf66 100644 --- a/web/src/lang/fr.js +++ b/web/src/lang/fr.js @@ -1,7 +1,7 @@ export default { backup: 'Sauvegarde', downloadTheProjectBackupFile: 'Téléchargez le fichier de sauvegarde du projet (en json)', - restore: 'Restaurer...', + restoreProject: 'Restaurer...', incorrectUsrPwd: 'Identifiant ou mot de passe incorrect', askDeleteUser: 'Voulez-vous vraiment supprimer cet utilisateur ?', askDeleteTemp: 'Voulez-vous vraiment supprimer ce modèle ?', diff --git a/web/src/lang/it.js b/web/src/lang/it.js index 9849a1d4..a1c6ed4b 100644 --- a/web/src/lang/it.js +++ b/web/src/lang/it.js @@ -1,7 +1,7 @@ export default { backup: 'Backup', downloadTheProjectBackupFile: 'Scarica il file di backup del progetto (in json)', - restore: 'Ristabilire...', + restoreProject: 'Ristabilire...', incorrectUsrPwd: 'Nome utente o password errati', askDeleteUser: 'Vuoi davvero eliminare questo utente?', askDeleteTemp: 'Vuoi davvero eliminare questo modello?', diff --git a/web/src/lang/nl.js b/web/src/lang/nl.js index c0f07133..01f202e7 100644 --- a/web/src/lang/nl.js +++ b/web/src/lang/nl.js @@ -1,7 +1,7 @@ export default { backup: 'Back-up', downloadTheProjectBackupFile: 'Download het back-upbestand van het project (in json)', - restore: 'Herstellen...', + restoreProject: 'Herstellen...', incorrectUsrPwd: 'Onjuiste gebruikersnaam of wachtwoord', askDeleteUser: 'Wilt u deze gebruiker echt verwijderen?', askDeleteTemp: 'Wilt u deze sjabloon echt verwijderen?', diff --git a/web/src/lang/pl.js b/web/src/lang/pl.js index 20124110..12ea15ac 100644 --- a/web/src/lang/pl.js +++ b/web/src/lang/pl.js @@ -1,7 +1,7 @@ export default { backup: 'Kopia zapasowa', downloadTheProjectBackupFile: 'Pobierz plik kopii zapasowej projektu (w formacie json)', - restore: 'Przywrócić...', + restoreProject: 'Przywrócić...', incorrectUsrPwd: 'Nieprawidłowa nazwa użytkownika lub hasło.', askDeleteUser: 'Czy na pewno chcesz usunąć tego użytkownika?', askDeleteTemp: 'Czy na pewno chcesz usunąć ten szablon?', diff --git a/web/src/lang/pt.js b/web/src/lang/pt.js index 9912cff4..ef297b5d 100644 --- a/web/src/lang/pt.js +++ b/web/src/lang/pt.js @@ -1,7 +1,7 @@ export default { backup: 'Cópia de segurança', downloadTheProjectBackupFile: 'Baixe o arquivo de backup do projeto (em json)', - restore: 'Restaurar...', + restoreProject: 'Restaurar...', incorrectUsrPwd: 'Nome de utilizador ou palavra-passe incorretos', askDeleteUser: 'Tem a certeza de que deseja eliminar este utilizador?', askDeleteTemp: 'Tem a certeza de que deseja eliminar este modelo?', diff --git a/web/src/lang/pt_br.js b/web/src/lang/pt_br.js index 2b4401b6..878b7442 100644 --- a/web/src/lang/pt_br.js +++ b/web/src/lang/pt_br.js @@ -1,7 +1,7 @@ export default { backup: 'Cópia de segurança', downloadTheProjectBackupFile: 'Baixe o arquivo de backup do projeto (em json)', - restore: 'Restaurar...', + restoreProject: 'Restaurar...', incorrectUsrPwd: 'Usuário ou senha incorretos', askDeleteUser: 'Você realmente deseja excluir este usuário?', askDeleteTemp: 'Você realmente deseja excluir este modelo?', diff --git a/web/src/lang/ru.js b/web/src/lang/ru.js index 29b67d24..dae26e9b 100644 --- a/web/src/lang/ru.js +++ b/web/src/lang/ru.js @@ -1,7 +1,7 @@ export default { backup: 'Бэкап', downloadTheProjectBackupFile: 'Выгрузить файл резервной копии проекта (в формате JSON)', - restore: 'Восстановить...', + restoreProject: 'Восстановить...', incorrectUsrPwd: 'Некорректный логин или пароль', askDeleteUser: 'Вы действительно хотите удалить этого пользователя?', askDeleteTemp: 'Вы действительно хотите удалить этот шаблон?', diff --git a/web/src/lang/zh_hans.js b/web/src/lang/zh_hans.js index b0b22a56..08d4fb57 100644 --- a/web/src/lang/zh_hans.js +++ b/web/src/lang/zh_hans.js @@ -1,7 +1,7 @@ export default { backup: '备份', downloadTheProjectBackupFile: '下载项目备份文件(json格式)', - restore: '恢复...', + restoreProject: '恢复...', incorrectUsrPwd: '用户名或密码错误', askDeleteUser: '您确定要删除此用户吗?', askDeleteTemp: '您确实要删除此模板吗?', diff --git a/web/src/lang/zh_hant.js b/web/src/lang/zh_hant.js index a07822f5..924d717f 100644 --- a/web/src/lang/zh_hant.js +++ b/web/src/lang/zh_hant.js @@ -1,7 +1,7 @@ export default { backup: '備份', downloadTheProjectBackupFile: '下載專案備份檔(json格式)', - restore: '恢复...', + restoreProject: '恢复...', incorrectUsrPwd: '使用者名稱或密碼錯誤', askDeleteUser: '您確定要刪除此使用者嗎? ', askDeleteTemp: '您確實要刪除此範本嗎? ', From f59d48c641d122b63a439162becf61cef2d19973 Mon Sep 17 00:00:00 2001 From: fiftinDate: Mon, 11 Mar 2024 00:23:34 +0100 Subject: [PATCH 334/346] feat(ui): forward query string --- api/login.go | 15 +++++++++++---- web/src/views/Auth.vue | 4 ++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/api/login.go b/api/login.go index dcd022ce..9a37d972 100644 --- a/api/login.go +++ b/api/login.go @@ -23,8 +23,8 @@ import ( "github.com/go-ldap/ldap/v3" "github.com/gorilla/mux" - log "github.com/sirupsen/logrus" "github.com/ansible-semaphore/semaphore/util" + log "github.com/sirupsen/logrus" ) func tryFindLDAPUser(username, password string) (*db.User, error) { @@ -296,7 +296,7 @@ func logout(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } -func getOidcProvider(id string, ctx context.Context) (*oidc.Provider, *oauth2.Config, error) { +func getOidcProvider(id string, ctx context.Context, redirectQueryString string) (*oidc.Provider, *oauth2.Config, error) { provider, ok := util.Config.OidcProviders[id] if !ok { return nil, nil, fmt.Errorf("No such provider: %s", id) @@ -344,6 +344,12 @@ func getOidcProvider(id string, ctx context.Context) (*oidc.Provider, *oauth2.Co if err != nil { return nil, nil, err } + if redirectQueryString != "" { + if !strings.HasPrefix(redirectQueryString, "?") { + rurl += "?" + } + rurl += redirectQueryString + } oauthConfig.RedirectURL = rurl } if len(oauthConfig.Scopes) == 0 { @@ -355,7 +361,8 @@ func getOidcProvider(id string, ctx context.Context) (*oidc.Provider, *oauth2.Co func oidcLogin(w http.ResponseWriter, r *http.Request) { pid := mux.Vars(r)["provider"] ctx := context.Background() - _, oauth, err := getOidcProvider(pid, ctx) + + _, oauth, err := getOidcProvider(pid, ctx, r.URL.RawQuery) if err != nil { log.Error(err.Error()) http.Redirect(w, r, "/auth/login", http.StatusTemporaryRedirect) @@ -482,7 +489,7 @@ func oidcRedirect(w http.ResponseWriter, r *http.Request) { } ctx := context.Background() - _oidc, oauth, err := getOidcProvider(pid, ctx) + _oidc, oauth, err := getOidcProvider(pid, ctx, r.URL.RawQuery) if err != nil { log.Error(err.Error()) http.Redirect(w, r, "/auth/login", http.StatusTemporaryRedirect) diff --git a/web/src/views/Auth.vue b/web/src/views/Auth.vue index ec56be6d..e5c5b0cf 100644 --- a/web/src/views/Auth.vue +++ b/web/src/views/Auth.vue @@ -219,7 +219,7 @@ export default { password: this.password, }, }); - document.location = document.baseURI; + document.location = document.baseURI + window.location.search; } catch (err) { if (err.response.status === 401) { this.signInError = this.$t('incorrectUsrPwd'); @@ -232,7 +232,7 @@ export default { }, async oidcSignIn(provider) { - document.location = `/api/auth/oidc/${provider}/login`; + document.location = `/api/auth/oidc/${provider}/login${window.location.search}`; }, }, }; From 0704828119a121db9be63e0e5fd798a09ff07e64 Mon Sep 17 00:00:00 2001 From: fiftin Date: Mon, 11 Mar 2024 01:04:47 +0100 Subject: [PATCH 335/346] fix(be): save project type --- db/sql/project.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/db/sql/project.go b/db/sql/project.go index 60e463e4..051ebaf5 100644 --- a/db/sql/project.go +++ b/db/sql/project.go @@ -1,8 +1,8 @@ package sql import ( - "github.com/ansible-semaphore/semaphore/db" "github.com/Masterminds/squirrel" + "github.com/ansible-semaphore/semaphore/db" "time" ) @@ -11,8 +11,8 @@ func (d *SqlDb) CreateProject(project db.Project) (newProject db.Project, err er insertId, err := d.insert( "id", - "insert into project(name, created) values (?, ?)", - project.Name, project.Created) + "insert into project(name, created, type) values (?, ?, ?)", + project.Name, project.Created, project.Type) if err != nil { return From 09e94e717d14e4651249a287e98af1e04e4479f6 Mon Sep 17 00:00:00 2001 From: fiftin Date: Mon, 11 Mar 2024 01:10:42 +0100 Subject: [PATCH 336/346] feat(ui): check project type for side panel --- web/src/App.vue | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/web/src/App.vue b/web/src/App.vue index e33da7c1..56edd58b 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -210,7 +210,7 @@ - + - @@ -220,7 +220,11 @@ mdi-check-all + - @@ -230,7 +234,11 @@ mdi-monitor-multiple + - @@ -240,7 +248,11 @@ mdi-code-braces + - @@ -250,7 +262,11 @@ mdi-key-change + @@ -261,7 +277,7 @@ mdi-git From d9ea092c6c68ed5261e2d803e0ecd12f9b555dbe Mon Sep 17 00:00:00 2001 From: fiftin -Date: Mon, 11 Mar 2024 01:27:23 +0100 Subject: [PATCH 337/346] feat(ui): pass project type to router view --- web/src/App.vue | 1 + web/src/components/ItemListPageBase.js | 1 + 2 files changed, 2 insertions(+) diff --git a/web/src/App.vue b/web/src/App.vue index 56edd58b..6d371718 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -423,6 +423,7 @@ Date: Mon, 11 Mar 2024 01:31:32 +0100 Subject: [PATCH 338/346] feat(ui): how history only for '' project type --- web/src/views/project/Activity.vue | 6 +++++- web/src/views/project/History.vue | 6 +++++- web/src/views/project/Settings.vue | 7 ++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/web/src/views/project/Activity.vue b/web/src/views/project/Activity.vue index 6c230d22..4ade504b 100644 --- a/web/src/views/project/Activity.vue +++ b/web/src/views/project/Activity.vue @@ -6,7 +6,11 @@ - {{ $t('history') }} +{{ $t('history') }} {{ $t('activity') }} - {{ $t('history') }} +{{ $t('history') }} {{ $t('activity') }} - @@ -76,6 +80,7 @@ export default { components: { YesNoDialog, ProjectForm }, props: { projectId: Number, + projectType: String, }, data() { From 31e8caf314635579260331cb883faa96700b5007 Mon Sep 17 00:00:00 2001 From: fiftin{{ $t('history') }} +{{ $t('history') }} {{ $t('activity') }} {{ $t('settings') }} Date: Mon, 11 Mar 2024 02:00:50 +0100 Subject: [PATCH 339/346] feat(ui): new project type --- web/src/App.vue | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/src/App.vue b/web/src/App.vue index 6d371718..a92ce8df 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -79,6 +79,7 @@ > {{ item.name }} + @@ -700,6 +704,7 @@ export default { snackbarColor: '', projects: null, newProjectDialog: null, + newProjectType: '', userDialog: null, passwordDialog: null, From 678260970f943ea82bdef4e15f312b0c38422b43 Mon Sep 17 00:00:00 2001 From: fiftin mdi-plus Date: Mon, 11 Mar 2024 02:22:17 +0100 Subject: [PATCH 340/346] fix(be): create none access key --- api/projects/projects.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/api/projects/projects.go b/api/projects/projects.go index 63d23275..9a436814 100644 --- a/api/projects/projects.go +++ b/api/projects/projects.go @@ -223,10 +223,22 @@ func AddProject(w http.ResponseWriter, r *http.Request) { return } + noneKey, err := store.CreateAccessKey(db.AccessKey{ + Name: "None", + Type: db.AccessKeyNone, + ProjectID: &body.ID, + }) + + if err != nil { + helpers.WriteError(w, err) + return + } + _, err = store.CreateInventory(db.Inventory{ Name: "None", ProjectID: body.ID, Type: "none", + SSHKeyID: &noneKey.ID, }) if err != nil { From 123135dd9db5439a5307173e0c44c2d283926370 Mon Sep 17 00:00:00 2001 From: fiftin Date: Mon, 11 Mar 2024 15:17:53 +0100 Subject: [PATCH 341/346] feat(be): sort oauth providers --- api/login.go | 9 +++++++++ util/config.go | 1 + web/src/App.vue | 3 +++ web/src/views/Auth.vue | 12 +++++++++++- 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/api/login.go b/api/login.go index 9a37d972..f51b73a9 100644 --- a/api/login.go +++ b/api/login.go @@ -10,6 +10,7 @@ import ( "net/http" "net/url" "os" + "sort" "strconv" "strings" "time" @@ -212,6 +213,7 @@ func login(w http.ResponseWriter, r *http.Request) { LoginWithPassword: !util.Config.PasswordLoginDisable, } i := 0 + for k, v := range util.Config.OidcProviders { config.OidcProviders[i] = loginMetadataOidcProvider{ ID: k, @@ -221,6 +223,13 @@ func login(w http.ResponseWriter, r *http.Request) { } i++ } + + sort.Slice(config.OidcProviders, func(i, j int) bool { + a := util.Config.OidcProviders[config.OidcProviders[i].ID] + b := util.Config.OidcProviders[config.OidcProviders[j].ID] + return a.Order < b.Order + }) + helpers.WriteJSON(w, http.StatusOK, config) return } diff --git a/util/config.go b/util/config.go index 40b6fb9c..4d5d7adf 100644 --- a/util/config.go +++ b/util/config.go @@ -75,6 +75,7 @@ type OidcProvider struct { NameClaim string `json:"name_claim" default:"preferred_username"` EmailClaim string `json:"email_claim" default:"email"` EmailSuffix string `json:"email_suffix"` + Order int `json:"order"` } const ( diff --git a/web/src/App.vue b/web/src/App.vue index a92ce8df..7fc4b53d 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -773,6 +773,9 @@ export default { }, project() { + if (this.projects == null) { + return null; + } return this.projects.find((x) => x.id === this.projectId); }, diff --git a/web/src/views/Auth.vue b/web/src/views/Auth.vue index e5c5b0cf..eda5bf7e 100644 --- a/web/src/views/Auth.vue +++ b/web/src/views/Auth.vue @@ -77,8 +77,18 @@ ref="signInForm" lazy-validation v-model="signInFormValid" - style="width: 300px; height: 300px;" + style="width: 300px;" > + + + {{ $t('semaphore') }}
Date: Mon, 11 Mar 2024 18:25:19 +0100 Subject: [PATCH 342/346] feat(auth): support redirect path for oauth --- api/login.go | 30 +++++++++++++++++++----------- api/router.go | 1 + 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/api/login.go b/api/login.go index f51b73a9..6eb87541 100644 --- a/api/login.go +++ b/api/login.go @@ -305,7 +305,7 @@ func logout(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } -func getOidcProvider(id string, ctx context.Context, redirectQueryString string) (*oidc.Provider, *oauth2.Config, error) { +func getOidcProvider(id string, ctx context.Context, redirectPath string) (*oidc.Provider, *oauth2.Config, error) { provider, ok := util.Config.OidcProviders[id] if !ok { return nil, nil, fmt.Errorf("No such provider: %s", id) @@ -341,11 +341,17 @@ func getOidcProvider(id string, ctx context.Context, redirectQueryString string) } } + if redirectPath != "" { + if !strings.HasPrefix(redirectPath, "/") { + redirectPath = "/" + redirectPath + } + } + oauthConfig := oauth2.Config{ Endpoint: oidcProvider.Endpoint(), ClientID: clientID, ClientSecret: clientSecret, - RedirectURL: provider.RedirectURL, + RedirectURL: provider.RedirectURL + redirectPath, Scopes: provider.Scopes, } if len(oauthConfig.RedirectURL) == 0 { @@ -353,13 +359,7 @@ func getOidcProvider(id string, ctx context.Context, redirectQueryString string) if err != nil { return nil, nil, err } - if redirectQueryString != "" { - if !strings.HasPrefix(redirectQueryString, "?") { - rurl += "?" - } - rurl += redirectQueryString - } - oauthConfig.RedirectURL = rurl + oauthConfig.RedirectURL = rurl + redirectPath } if len(oauthConfig.Scopes) == 0 { oauthConfig.Scopes = []string{"openid", "profile", "email"} @@ -371,7 +371,13 @@ func oidcLogin(w http.ResponseWriter, r *http.Request) { pid := mux.Vars(r)["provider"] ctx := context.Background() - _, oauth, err := getOidcProvider(pid, ctx, r.URL.RawQuery) + redirectPath := "" + + if r.URL.Query()["redirect"] != nil { + redirectPath = r.URL.Query()["redirect"][0] + } + + _, oauth, err := getOidcProvider(pid, ctx, redirectPath) if err != nil { log.Error(err.Error()) http.Redirect(w, r, "/auth/login", http.StatusTemporaryRedirect) @@ -586,5 +592,7 @@ func oidcRedirect(w http.ResponseWriter, r *http.Request) { createSession(w, r, user) - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + redirectPath := mux.Vars(r)["redirect_path"] + + http.Redirect(w, r, "/"+redirectPath, http.StatusTemporaryRedirect) } diff --git a/api/router.go b/api/router.go index c464c9de..4b40a48f 100644 --- a/api/router.go +++ b/api/router.go @@ -85,6 +85,7 @@ func Route() *mux.Router { 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") + publicAPIRouter.HandleFunc("/auth/oidc/{provider}/redirect/{redirect_path:.*}", oidcRedirect).Methods("GET") routersAPI := r.PathPrefix(webPath + "api").Subrouter() routersAPI.Use(StoreMiddleware, JSONMiddleware, runners.RunnerMiddleware) From 3404c40c576ed63c4759ef7170e6caef378669a1 Mon Sep 17 00:00:00 2001 From: fiftin Date: Mon, 11 Mar 2024 18:25:32 +0100 Subject: [PATCH 343/346] feat(auth): support redirect path for oauth --- api/login.go | 1 + 1 file changed, 1 insertion(+) diff --git a/api/login.go b/api/login.go index 6eb87541..a6ef4721 100644 --- a/api/login.go +++ b/api/login.go @@ -374,6 +374,7 @@ func oidcLogin(w http.ResponseWriter, r *http.Request) { redirectPath := "" if r.URL.Query()["redirect"] != nil { + // TODO: validate path redirectPath = r.URL.Query()["redirect"][0] } From 0ee2d5fc16c9c002dea6ccc8aa836abf258f7517 Mon Sep 17 00:00:00 2001 From: fiftin Date: Mon, 11 Mar 2024 19:39:11 +0100 Subject: [PATCH 344/346] fix(auth): check redirect urls --- api/login.go | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/api/login.go b/api/login.go index a6ef4721..0197e656 100644 --- a/api/login.go +++ b/api/login.go @@ -345,6 +345,18 @@ func getOidcProvider(id string, ctx context.Context, redirectPath string) (*oidc if !strings.HasPrefix(redirectPath, "/") { redirectPath = "/" + redirectPath } + + providerUrl, err := url.Parse(provider.RedirectURL) + + if err != nil { + return nil, nil, err + } + + if redirectPath == providerUrl.Path { + redirectPath = "" + } else if strings.HasPrefix(redirectPath, providerUrl.Path+"/") { + redirectPath = redirectPath[len(providerUrl.Path):] + } } oauthConfig := oauth2.Config{ @@ -359,7 +371,12 @@ func getOidcProvider(id string, ctx context.Context, redirectPath string) (*oidc if err != nil { return nil, nil, err } - oauthConfig.RedirectURL = rurl + redirectPath + + oauthConfig.RedirectURL = rurl + + if rurl != redirectPath { + oauthConfig.RedirectURL += redirectPath + } } if len(oauthConfig.Scopes) == 0 { oauthConfig.Scopes = []string{"openid", "profile", "email"} @@ -505,7 +522,8 @@ func oidcRedirect(w http.ResponseWriter, r *http.Request) { } ctx := context.Background() - _oidc, oauth, err := getOidcProvider(pid, ctx, r.URL.RawQuery) + + _oidc, oauth, err := getOidcProvider(pid, ctx, r.URL.Path) if err != nil { log.Error(err.Error()) http.Redirect(w, r, "/auth/login", http.StatusTemporaryRedirect) From 8486b4338094f436ee00653450ba69095da6721f Mon Sep 17 00:00:00 2001 From: fiftin Date: Mon, 11 Mar 2024 23:12:57 +0100 Subject: [PATCH 345/346] fix(be0: set null instead of cascade for holder_id --- db/sql/migrations/v2.9.62.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/sql/migrations/v2.9.62.sql b/db/sql/migrations/v2.9.62.sql index db41a716..57490645 100644 --- a/db/sql/migrations/v2.9.62.sql +++ b/db/sql/migrations/v2.9.62.sql @@ -2,7 +2,7 @@ alter table project add `type` varchar(20) default ''; alter table task add `inventory_id` int null references project__inventory(`id`) on delete set null; -alter table project__inventory add `holder_id` int null references project__template(`id`) on delete cascade; +alter table project__inventory add `holder_id` int null references project__template(`id`) on delete set null; create table `option` ( `key` varchar(255) primary key not null, From 02899c9ccb6481eb0767a979c1f50ff96dcdef6c Mon Sep 17 00:00:00 2001 From: fiftin Date: Tue, 12 Mar 2024 01:44:04 +0100 Subject: [PATCH 346/346] feat: status updating --- api/router.go | 2 +- db_lib/AnsibleApp.go | 3 ++- db_lib/LocalApp.go | 2 +- services/runners/JobPool.go | 5 +++-- services/tasks/LocalJob.go | 12 ------------ services/tasks/TaskPool.go | 5 +++-- services/tasks/TaskRunner.go | 6 +++++- 7 files changed, 15 insertions(+), 20 deletions(-) diff --git a/api/router.go b/api/router.go index 4b40a48f..847148a8 100644 --- a/api/router.go +++ b/api/router.go @@ -145,7 +145,7 @@ func Route() *mux.Router { projectTaskStop := authenticatedAPI.PathPrefix("/project/{project_id}").Subrouter() projectTaskStop.Use(projects.ProjectMiddleware, projects.GetTaskMiddleware, projects.GetMustCanMiddleware(db.CanRunProjectTasks)) projectTaskStop.HandleFunc("/tasks/{task_id}/stop", projects.StopTask).Methods("POST") - projectTaskStop.HandleFunc("/tasks/{task_id}/confirm", projects.StopTask).Methods("POST") + projectTaskStop.HandleFunc("/tasks/{task_id}/confirm", projects.ConfirmTask).Methods("POST") // // Project resources CRUD diff --git a/db_lib/AnsibleApp.go b/db_lib/AnsibleApp.go index f685a735..9c322eee 100644 --- a/db_lib/AnsibleApp.go +++ b/db_lib/AnsibleApp.go @@ -54,8 +54,9 @@ type AnsibleApp struct { Repository db.Repository } -func (t *AnsibleApp) SetLogger(logger lib.Logger) { +func (t *AnsibleApp) SetLogger(logger lib.Logger) lib.Logger { t.Logger = logger + return logger } func (t *AnsibleApp) Run(args []string, environmentVars *[]string, cb func(*os.Process)) error { diff --git a/db_lib/LocalApp.go b/db_lib/LocalApp.go index a440d2f6..fc74d1ad 100644 --- a/db_lib/LocalApp.go +++ b/db_lib/LocalApp.go @@ -6,7 +6,7 @@ import ( ) type LocalApp interface { - SetLogger(logger lib.Logger) + SetLogger(logger lib.Logger) lib.Logger InstallRequirements() error Run(args []string, environmentVars *[]string, cb func(*os.Process)) error } diff --git a/services/runners/JobPool.go b/services/runners/JobPool.go index 76d6904f..03251dc0 100644 --- a/services/runners/JobPool.go +++ b/services/runners/JobPool.go @@ -146,6 +146,7 @@ func (p *runningJob) Log(msg string) { func (p *runningJob) SetStatus(status lib.TaskStatus) { p.status = status + p.job.SetStatus(status) } func (p *runningJob) LogCmd(cmd *exec.Cmd) { @@ -212,8 +213,8 @@ func (p *JobPool) Run() { p.runningJobs[t.job.Task.ID] = &runningJob{ job: t.job, } - t.job.Logger = p.runningJobs[t.job.Task.ID] - t.job.App.SetLogger(t.job.Logger) + + t.job.Logger = t.job.App.SetLogger(p.runningJobs[t.job.Task.ID]) go func(runningJob *runningJob) { runningJob.SetStatus(lib.TaskRunningStatus) diff --git a/services/tasks/LocalJob.go b/services/tasks/LocalJob.go index c60a6ddc..badfb204 100644 --- a/services/tasks/LocalJob.go +++ b/services/tasks/LocalJob.go @@ -282,8 +282,6 @@ func (t *LocalJob) getPlaybookArgs(username string, incomingVersion *string) (ar func (t *LocalJob) Run(username string, incomingVersion *string) (err error) { - t.SetStatus(lib.TaskRunningStatus) - err = t.prepareRun() if err != nil { return err @@ -312,16 +310,6 @@ func (t *LocalJob) Run(username string, incomingVersion *string) (err error) { } if t.Inventory.SSHKey.Type == db.AccessKeySSH && t.Inventory.SSHKeyID != nil { - - //var sshAgent lib.SshAgent - //sshAgent, err = t.Inventory.StartSshAgent(t.Logger) - // - //if err != nil { - // return - //} - // - //defer sshAgent.Close() - environmentVariables = append(environmentVariables, fmt.Sprintf("SSH_AUTH_SOCK=%s", t.sshKeyInstallation.SshAgent.SocketFile)) } diff --git a/services/tasks/TaskPool.go b/services/tasks/TaskPool.go index eaf46e29..7fcd6abc 100644 --- a/services/tasks/TaskPool.go +++ b/services/tasks/TaskPool.go @@ -367,14 +367,15 @@ func (p *TaskPool) AddTask(taskObj db.Task, userID *int, projectID int) (newTask taskPool: p, } } else { + app := db_lib.CreateApp(taskRunner.Template, taskRunner.Repository, &taskRunner) job = &LocalJob{ Task: taskRunner.Task, Template: taskRunner.Template, Inventory: taskRunner.Inventory, Repository: taskRunner.Repository, Environment: taskRunner.Environment, - Logger: &taskRunner, - App: db_lib.CreateApp(taskRunner.Template, taskRunner.Repository, &taskRunner), + Logger: app.SetLogger(&taskRunner), + App: app, } } diff --git a/services/tasks/TaskRunner.go b/services/tasks/TaskRunner.go index 9f0da756..d7220529 100644 --- a/services/tasks/TaskRunner.go +++ b/services/tasks/TaskRunner.go @@ -8,10 +8,10 @@ import ( "strings" "time" - log "github.com/sirupsen/logrus" "github.com/ansible-semaphore/semaphore/api/sockets" "github.com/ansible-semaphore/semaphore/db" "github.com/ansible-semaphore/semaphore/util" + log "github.com/sirupsen/logrus" ) type Job interface { @@ -71,6 +71,10 @@ func (t *TaskRunner) SetStatus(status lib.TaskStatus) { t.saveStatus() + if localJob, ok := t.job.(*LocalJob); ok { + localJob.SetStatus(status) + } + if status == lib.TaskFailStatus { t.sendMailAlert() }