mirror of
https://github.com/simon987/task_tracker.git
synced 2025-04-19 18:16:45 +00:00
Implement per-project webhook secret
This commit is contained in:
parent
c736cc3d98
commit
397805f915
27
api/git.go
27
api/git.go
@ -16,13 +16,6 @@ import (
|
|||||||
|
|
||||||
func (api *WebAPI) ReceiveGitWebHook(r *Request) {
|
func (api *WebAPI) ReceiveGitWebHook(r *Request) {
|
||||||
|
|
||||||
if !signatureValid(r) {
|
|
||||||
logrus.Error("WebHook signature does not match!")
|
|
||||||
r.Ctx.SetStatusCode(403)
|
|
||||||
_, _ = fmt.Fprintf(r.Ctx, "Signature does not match")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
payload := &GitPayload{}
|
payload := &GitPayload{}
|
||||||
err := json.Unmarshal(r.Ctx.Request.Body(), payload)
|
err := json.Unmarshal(r.Ctx.Request.Body(), payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -35,11 +28,27 @@ func (api *WebAPI) ReceiveGitWebHook(r *Request) {
|
|||||||
}).Info("Received git WebHook")
|
}).Info("Received git WebHook")
|
||||||
|
|
||||||
if !isProductionBranch(payload) {
|
if !isProductionBranch(payload) {
|
||||||
|
r.Ctx.SetStatusCode(400)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
project := api.getAssociatedProject(payload)
|
project := api.getAssociatedProject(payload)
|
||||||
if project == nil {
|
if project == nil {
|
||||||
|
r.Ctx.SetStatusCode(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
signature, err := api.Database.GetWebhookSecret(project.Id)
|
||||||
|
if err != nil {
|
||||||
|
_, _ = fmt.Fprintf(r.Ctx, err.Error())
|
||||||
|
r.Ctx.SetStatusCode(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !signatureValid(r, signature) {
|
||||||
|
logrus.Error("WebHook signature does not match!")
|
||||||
|
r.Ctx.SetStatusCode(403)
|
||||||
|
_, _ = fmt.Fprintf(r.Ctx, "Signature does not match")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,7 +59,7 @@ func (api *WebAPI) ReceiveGitWebHook(r *Request) {
|
|||||||
handleErr(err, r)
|
handleErr(err, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func signatureValid(r *Request) (matches bool) {
|
func signatureValid(r *Request, webhookSignature string) (matches bool) {
|
||||||
|
|
||||||
signature := parseSignatureFromRequest(r.Ctx)
|
signature := parseSignatureFromRequest(r.Ctx)
|
||||||
|
|
||||||
@ -60,7 +69,7 @@ func signatureValid(r *Request) (matches bool) {
|
|||||||
|
|
||||||
body := r.Ctx.PostBody()
|
body := r.Ctx.PostBody()
|
||||||
|
|
||||||
mac := hmac.New(getHashFuncFromConfig(), config.Cfg.WebHookSecret)
|
mac := hmac.New(getHashFuncFromConfig(), []byte(webhookSignature))
|
||||||
mac.Write(body)
|
mac.Write(body)
|
||||||
|
|
||||||
expectedMac := hex.EncodeToString(mac.Sum(nil))
|
expectedMac := hex.EncodeToString(mac.Sum(nil))
|
||||||
|
@ -93,6 +93,8 @@ func New() *WebAPI {
|
|||||||
api.router.POST("/project/reject_request/:id/:wid", LogRequestMiddleware(api.RejectAccessRequest))
|
api.router.POST("/project/reject_request/:id/:wid", LogRequestMiddleware(api.RejectAccessRequest))
|
||||||
api.router.GET("/project/secret/:id", LogRequestMiddleware(api.GetSecret))
|
api.router.GET("/project/secret/:id", LogRequestMiddleware(api.GetSecret))
|
||||||
api.router.POST("/project/secret/:id", LogRequestMiddleware(api.SetSecret))
|
api.router.POST("/project/secret/:id", LogRequestMiddleware(api.SetSecret))
|
||||||
|
api.router.GET("/project/webhook_secret/:id", LogRequestMiddleware(api.GetWebhookSecret))
|
||||||
|
api.router.POST("/project/webhook_secret/:id", LogRequestMiddleware(api.SetWebhookSecret))
|
||||||
|
|
||||||
api.router.POST("/task/submit", LogRequestMiddleware(api.SubmitTask))
|
api.router.POST("/task/submit", LogRequestMiddleware(api.SubmitTask))
|
||||||
api.router.GET("/task/get/:project", LogRequestMiddleware(api.GetTaskFromProject))
|
api.router.GET("/task/get/:project", LogRequestMiddleware(api.GetTaskFromProject))
|
||||||
|
@ -292,3 +292,10 @@ type SetSecretRequest struct {
|
|||||||
type GetSecretResponse struct {
|
type GetSecretResponse struct {
|
||||||
Secret string `json:"secret"`
|
Secret string `json:"secret"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SetWebhookSecretRequest struct {
|
||||||
|
WebhookSecret string `json:"webhook_secret"`
|
||||||
|
}
|
||||||
|
type GetWebhookSecretResponse struct {
|
||||||
|
WebhookSecret string `json:"webhook_secret"`
|
||||||
|
}
|
||||||
|
101
api/project.go
101
api/project.go
@ -3,6 +3,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/simon987/task_tracker/storage"
|
"github.com/simon987/task_tracker/storage"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
@ -97,7 +98,9 @@ func (api *WebAPI) CreateProject(r *Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := api.Database.SaveProject(project)
|
webhookSecret := makeWebhookSecret()
|
||||||
|
|
||||||
|
id, err := api.Database.SaveProject(project, webhookSecret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.Json(JsonResponse{
|
r.Json(JsonResponse{
|
||||||
Ok: false,
|
Ok: false,
|
||||||
@ -107,7 +110,7 @@ func (api *WebAPI) CreateProject(r *Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
api.Database.SetManagerRoleOn(manager.(*storage.Manager).Id, id,
|
api.Database.SetManagerRoleOn(manager.(*storage.Manager).Id, id,
|
||||||
storage.ROLE_MANAGE_ACCESS|storage.ROLE_READ|storage.ROLE_EDIT)
|
storage.RoleManageAccess|storage.RoleRead|storage.RoleEdit|storage.RoleSecret)
|
||||||
r.OkJson(JsonResponse{
|
r.OkJson(JsonResponse{
|
||||||
Ok: true,
|
Ok: true,
|
||||||
Content: CreateProjectResponse{
|
Content: CreateProjectResponse{
|
||||||
@ -119,6 +122,10 @@ func (api *WebAPI) CreateProject(r *Request) {
|
|||||||
}).Debug("Created project")
|
}).Debug("Created project")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeWebhookSecret() string {
|
||||||
|
return uuid.New().String()
|
||||||
|
}
|
||||||
|
|
||||||
func (api *WebAPI) UpdateProject(r *Request) {
|
func (api *WebAPI) UpdateProject(r *Request) {
|
||||||
|
|
||||||
id, err := strconv.ParseInt(r.Ctx.UserValue("id").(string), 10, 64)
|
id, err := strconv.ParseInt(r.Ctx.UserValue("id").(string), 10, 64)
|
||||||
@ -163,7 +170,7 @@ func (api *WebAPI) UpdateProject(r *Request) {
|
|||||||
sess := api.Session.StartFasthttp(r.Ctx)
|
sess := api.Session.StartFasthttp(r.Ctx)
|
||||||
manager := sess.Get("manager")
|
manager := sess.Get("manager")
|
||||||
|
|
||||||
if !isActionOnProjectAuthorized(project.Id, manager, storage.ROLE_EDIT, api.Database) {
|
if !isActionOnProjectAuthorized(project.Id, manager, storage.RoleEdit, api.Database) {
|
||||||
r.Json(JsonResponse{
|
r.Json(JsonResponse{
|
||||||
Ok: false,
|
Ok: false,
|
||||||
Message: "Unauthorized",
|
Message: "Unauthorized",
|
||||||
@ -238,7 +245,7 @@ func isProjectReadAuthorized(project *storage.Project, manager interface{}, db *
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
role := db.GetManagerRoleOn(manager.(*storage.Manager), project.Id)
|
role := db.GetManagerRoleOn(manager.(*storage.Manager), project.Id)
|
||||||
if role&storage.ROLE_READ == 1 {
|
if role&storage.RoleRead == 1 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,7 +309,7 @@ func (api *WebAPI) GetWorkerAccessListForProject(r *Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isActionOnProjectAuthorized(id, manager, storage.ROLE_MANAGE_ACCESS, api.Database) {
|
if !isActionOnProjectAuthorized(id, manager, storage.RoleManageAccess, api.Database) {
|
||||||
r.Json(JsonResponse{
|
r.Json(JsonResponse{
|
||||||
Ok: false,
|
Ok: false,
|
||||||
Message: "Unauthorized",
|
Message: "Unauthorized",
|
||||||
@ -391,7 +398,7 @@ func (api *WebAPI) AcceptAccessRequest(r *Request) {
|
|||||||
sess := api.Session.StartFasthttp(r.Ctx)
|
sess := api.Session.StartFasthttp(r.Ctx)
|
||||||
manager := sess.Get("manager")
|
manager := sess.Get("manager")
|
||||||
|
|
||||||
if !isActionOnProjectAuthorized(pid, manager, storage.ROLE_MANAGE_ACCESS, api.Database) {
|
if !isActionOnProjectAuthorized(pid, manager, storage.RoleManageAccess, api.Database) {
|
||||||
r.Json(JsonResponse{
|
r.Json(JsonResponse{
|
||||||
Message: "Unauthorized",
|
Message: "Unauthorized",
|
||||||
Ok: false,
|
Ok: false,
|
||||||
@ -471,7 +478,7 @@ func (api *WebAPI) SetManagerRoleOnProject(r *Request) {
|
|||||||
sess := api.Session.StartFasthttp(r.Ctx)
|
sess := api.Session.StartFasthttp(r.Ctx)
|
||||||
manager := sess.Get("manager")
|
manager := sess.Get("manager")
|
||||||
|
|
||||||
if !isActionOnProjectAuthorized(pid, manager, storage.ROLE_MANAGE_ACCESS, api.Database) {
|
if !isActionOnProjectAuthorized(pid, manager, storage.RoleManageAccess, api.Database) {
|
||||||
r.Json(JsonResponse{
|
r.Json(JsonResponse{
|
||||||
Message: "Unauthorized",
|
Message: "Unauthorized",
|
||||||
Ok: false,
|
Ok: false,
|
||||||
@ -500,7 +507,7 @@ func (api *WebAPI) SetSecret(r *Request) {
|
|||||||
sess := api.Session.StartFasthttp(r.Ctx)
|
sess := api.Session.StartFasthttp(r.Ctx)
|
||||||
manager := sess.Get("manager")
|
manager := sess.Get("manager")
|
||||||
|
|
||||||
if !isActionOnProjectAuthorized(pid, manager, storage.ROLE_EDIT, api.Database) {
|
if !isActionOnProjectAuthorized(pid, manager, storage.RoleSecret, api.Database) {
|
||||||
r.Json(JsonResponse{
|
r.Json(JsonResponse{
|
||||||
Ok: false,
|
Ok: false,
|
||||||
Message: "Unauthorized",
|
Message: "Unauthorized",
|
||||||
@ -560,7 +567,7 @@ func (api *WebAPI) GetSecret(r *Request) {
|
|||||||
sess := api.Session.StartFasthttp(r.Ctx)
|
sess := api.Session.StartFasthttp(r.Ctx)
|
||||||
manager := sess.Get("manager")
|
manager := sess.Get("manager")
|
||||||
|
|
||||||
if !isActionOnProjectAuthorized(pid, manager, storage.ROLE_EDIT, api.Database) {
|
if !isActionOnProjectAuthorized(pid, manager, storage.RoleSecret, api.Database) {
|
||||||
r.Json(JsonResponse{
|
r.Json(JsonResponse{
|
||||||
Ok: false,
|
Ok: false,
|
||||||
Message: "Unauthorized",
|
Message: "Unauthorized",
|
||||||
@ -576,3 +583,79 @@ func (api *WebAPI) GetSecret(r *Request) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *WebAPI) GetWebhookSecret(r *Request) {
|
||||||
|
|
||||||
|
pid, err := strconv.ParseInt(r.Ctx.UserValue("id").(string), 10, 64)
|
||||||
|
if err != nil || pid <= 0 {
|
||||||
|
r.Json(JsonResponse{
|
||||||
|
Ok: false,
|
||||||
|
Message: "Invalid project id",
|
||||||
|
}, 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sess := api.Session.StartFasthttp(r.Ctx)
|
||||||
|
manager := sess.Get("manager")
|
||||||
|
|
||||||
|
if !isActionOnProjectAuthorized(pid, manager, storage.RoleSecret, api.Database) {
|
||||||
|
r.Json(JsonResponse{
|
||||||
|
Ok: false,
|
||||||
|
Message: "Unauthorized",
|
||||||
|
}, 403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
secret, err := api.Database.GetWebhookSecret(pid)
|
||||||
|
r.OkJson(JsonResponse{
|
||||||
|
Ok: true,
|
||||||
|
Content: GetWebhookSecretResponse{
|
||||||
|
WebhookSecret: secret,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *WebAPI) SetWebhookSecret(r *Request) {
|
||||||
|
|
||||||
|
pid, err := strconv.ParseInt(r.Ctx.UserValue("id").(string), 10, 64)
|
||||||
|
if err != nil || pid <= 0 {
|
||||||
|
r.Json(JsonResponse{
|
||||||
|
Ok: false,
|
||||||
|
Message: "Invalid project id",
|
||||||
|
}, 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &SetWebhookSecretRequest{}
|
||||||
|
err = json.Unmarshal(r.Ctx.Request.Body(), req)
|
||||||
|
if err != nil {
|
||||||
|
r.Json(JsonResponse{
|
||||||
|
Ok: false,
|
||||||
|
Message: "Could not parse request",
|
||||||
|
}, 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sess := api.Session.StartFasthttp(r.Ctx)
|
||||||
|
manager := sess.Get("manager")
|
||||||
|
|
||||||
|
if !isActionOnProjectAuthorized(pid, manager, storage.RoleSecret, api.Database) {
|
||||||
|
r.Json(JsonResponse{
|
||||||
|
Ok: false,
|
||||||
|
Message: "Unauthorized",
|
||||||
|
}, 403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = api.Database.SetWebhookSecret(pid, req.WebhookSecret)
|
||||||
|
if err == nil {
|
||||||
|
r.OkJson(JsonResponse{
|
||||||
|
Ok: true,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
r.OkJson(JsonResponse{
|
||||||
|
Ok: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -7,7 +7,6 @@ database:
|
|||||||
log_levels: ["error", "info", "warn"]
|
log_levels: ["error", "info", "warn"]
|
||||||
|
|
||||||
git:
|
git:
|
||||||
webhook_secret: "very_secret_secret"
|
|
||||||
# Github: sha1, Gogs: sha256
|
# Github: sha1, Gogs: sha256
|
||||||
webhook_hash: "sha256"
|
webhook_hash: "sha256"
|
||||||
# Github: 'X-Hub-Signature', Gogs: 'X-Gogs-Signature'
|
# Github: 'X-Hub-Signature', Gogs: 'X-Gogs-Signature'
|
||||||
|
@ -11,10 +11,11 @@ import (
|
|||||||
type ManagerRole int
|
type ManagerRole int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ROLE_NONE ManagerRole = 0
|
RoleNone ManagerRole = 0
|
||||||
ROLE_READ ManagerRole = 1
|
RoleRead ManagerRole = 1
|
||||||
ROLE_EDIT ManagerRole = 2
|
RoleEdit ManagerRole = 2
|
||||||
ROLE_MANAGE_ACCESS ManagerRole = 4
|
RoleManageAccess ManagerRole = 4
|
||||||
|
RoleSecret ManagerRole = 8
|
||||||
)
|
)
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
@ -142,7 +143,7 @@ func (database *Database) GetManagerRoleOn(manager *Manager, projectId int64) Ma
|
|||||||
var role ManagerRole
|
var role ManagerRole
|
||||||
err := row.Scan(&role)
|
err := row.Scan(&role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ROLE_NONE
|
return RoleNone
|
||||||
}
|
}
|
||||||
|
|
||||||
return role
|
return role
|
||||||
|
@ -25,14 +25,14 @@ type AssignedTasks struct {
|
|||||||
TaskCount int64 `json:"task_count"`
|
TaskCount int64 `json:"task_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (database *Database) SaveProject(project *Project) (int64, error) {
|
func (database *Database) SaveProject(project *Project, webhookSecret string) (int64, error) {
|
||||||
db := database.getDB()
|
db := database.getDB()
|
||||||
|
|
||||||
row := db.QueryRow(`INSERT INTO project (name, git_repo, clone_url, version, priority,
|
row := db.QueryRow(`INSERT INTO project (name, git_repo, clone_url, version, priority,
|
||||||
motd, public, hidden, chain, paused)
|
motd, public, hidden, chain, paused, webhook_secret)
|
||||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NULLIF($9, 0),$10) RETURNING id`,
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NULLIF($9, 0),$10,$11) RETURNING id`,
|
||||||
project.Name, project.GitRepo, project.CloneUrl, project.Version, project.Priority, project.Motd,
|
project.Name, project.GitRepo, project.CloneUrl, project.Version, project.Priority, project.Motd,
|
||||||
project.Public, project.Hidden, project.Chain, project.Paused)
|
project.Public, project.Hidden, project.Chain, project.Paused, webhookSecret)
|
||||||
|
|
||||||
var id int64
|
var id int64
|
||||||
err := row.Scan(&id)
|
err := row.Scan(&id)
|
||||||
@ -225,3 +225,23 @@ func (database *Database) SetSecret(pid int64, secret string) {
|
|||||||
"project": pid,
|
"project": pid,
|
||||||
}).Info("Set secret")
|
}).Info("Set secret")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (database *Database) GetWebhookSecret(pid int64) (secret string, err error) {
|
||||||
|
db := database.getDB()
|
||||||
|
row := db.QueryRow(`SELECT webhook_secret FROM project WHERE id=$1`, pid)
|
||||||
|
err = row.Scan(&secret)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (database *Database) SetWebhookSecret(pid int64, secret string) (err error) {
|
||||||
|
db := database.getDB()
|
||||||
|
res, err := db.Exec(`UPDATE project SET webhook_secret=$1 WHERE id=$2`, secret, pid)
|
||||||
|
handleErr(err)
|
||||||
|
|
||||||
|
rowsAffected, _ := res.RowsAffected()
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"project": pid,
|
||||||
|
"rowsAffected": rowsAffected,
|
||||||
|
}).Trace("Update webhook secret")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
@ -3,6 +3,7 @@ package test
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"github.com/simon987/task_tracker/api"
|
"github.com/simon987/task_tracker/api"
|
||||||
"github.com/simon987/task_tracker/config"
|
"github.com/simon987/task_tracker/config"
|
||||||
"golang.org/x/net/publicsuffix"
|
"golang.org/x/net/publicsuffix"
|
||||||
@ -177,3 +178,15 @@ func getSessionCtx(username string, password string, admin bool) *http.Client {
|
|||||||
|
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setRoleOnProject(req api.SetManagerRoleOnProjectRequest, pid int64, s *http.Client) (ar api.JsonResponse) {
|
||||||
|
r := Post(fmt.Sprintf("/manager/set_role_for_project/%d", pid), req, nil, s)
|
||||||
|
UnmarshalResponse(r, &ar)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAccountDetails(s *http.Client) (ar AccountAR) {
|
||||||
|
r := Get("/account", nil, s)
|
||||||
|
UnmarshalResponse(r, &ar)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
@ -15,7 +15,7 @@ func TestWebHookNoSignature(t *testing.T) {
|
|||||||
|
|
||||||
r := Post("/git/receivehook", api.GitPayload{}, nil, nil)
|
r := Post("/git/receivehook", api.GitPayload{}, nil, nil)
|
||||||
|
|
||||||
if r.StatusCode != 403 {
|
if r.StatusCode == 200 {
|
||||||
t.Error()
|
t.Error()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -28,7 +28,7 @@ func TestWebHookInvalidSignature(t *testing.T) {
|
|||||||
client := http.Client{}
|
client := http.Client{}
|
||||||
r, _ := client.Do(req)
|
r, _ := client.Do(req)
|
||||||
|
|
||||||
if r.StatusCode != 403 {
|
if r.StatusCode == 200 {
|
||||||
t.Error()
|
t.Error()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -41,10 +41,12 @@ func TestWebHookDontUpdateVersion(t *testing.T) {
|
|||||||
GitRepo: "username/not_this_one",
|
GitRepo: "username/not_this_one",
|
||||||
}).Content
|
}).Content
|
||||||
|
|
||||||
|
webhookSecret := getWebhookSecret(resp.Id, testAdminCtx).Content.WebhookSecret
|
||||||
|
|
||||||
body := []byte(`{"ref": "refs/heads/master", "after": "new", "repository": {"full_name": "username/repo_name"}}`)
|
body := []byte(`{"ref": "refs/heads/master", "after": "new", "repository": {"full_name": "username/repo_name"}}`)
|
||||||
bodyReader := bytes.NewReader(body)
|
bodyReader := bytes.NewReader(body)
|
||||||
|
|
||||||
mac := hmac.New(crypto.SHA1.New, config.Cfg.WebHookSecret)
|
mac := hmac.New(crypto.SHA1.New, []byte(webhookSecret))
|
||||||
mac.Write(body)
|
mac.Write(body)
|
||||||
signature := hex.EncodeToString(mac.Sum(nil))
|
signature := hex.EncodeToString(mac.Sum(nil))
|
||||||
signature = "sha1=" + signature
|
signature = "sha1=" + signature
|
||||||
@ -53,11 +55,7 @@ func TestWebHookDontUpdateVersion(t *testing.T) {
|
|||||||
req.Header.Add("X-Hub-Signature", signature)
|
req.Header.Add("X-Hub-Signature", signature)
|
||||||
|
|
||||||
client := http.Client{}
|
client := http.Client{}
|
||||||
r, _ := client.Do(req)
|
_, _ = client.Do(req)
|
||||||
|
|
||||||
if r.StatusCode != 200 {
|
|
||||||
t.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
getResp := getProjectAsAdmin(resp.Id).Content
|
getResp := getProjectAsAdmin(resp.Id).Content
|
||||||
|
|
||||||
@ -76,7 +74,9 @@ func TestWebHookUpdateVersion(t *testing.T) {
|
|||||||
body := []byte(`{"ref": "refs/heads/master", "after": "new", "repository": {"full_name": "username/repo_name"}}`)
|
body := []byte(`{"ref": "refs/heads/master", "after": "new", "repository": {"full_name": "username/repo_name"}}`)
|
||||||
bodyReader := bytes.NewReader(body)
|
bodyReader := bytes.NewReader(body)
|
||||||
|
|
||||||
mac := hmac.New(crypto.SHA1.New, config.Cfg.WebHookSecret)
|
webhookSecret := getWebhookSecret(resp.Id, testAdminCtx).Content.WebhookSecret
|
||||||
|
|
||||||
|
mac := hmac.New(crypto.SHA1.New, []byte(webhookSecret))
|
||||||
mac.Write(body)
|
mac.Write(body)
|
||||||
signature := hex.EncodeToString(mac.Sum(nil))
|
signature := hex.EncodeToString(mac.Sum(nil))
|
||||||
signature = "sha1=" + signature
|
signature = "sha1=" + signature
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/simon987/task_tracker/api"
|
"github.com/simon987/task_tracker/api"
|
||||||
|
"github.com/simon987/task_tracker/storage"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
@ -465,6 +466,82 @@ func TestPausedProjectShouldNotDispatchTasks(t *testing.T) {
|
|||||||
}, testProject, testAdminCtx)
|
}, testProject, testAdminCtx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetWebhookSecret(t *testing.T) {
|
||||||
|
|
||||||
|
resp := getWebhookSecret(testProject, testAdminCtx)
|
||||||
|
|
||||||
|
if resp.Ok != true {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
if len(resp.Content.WebhookSecret) <= 0 {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetWebhookSecret(t *testing.T) {
|
||||||
|
|
||||||
|
resp1 := setWebhookSecret(api.SetWebhookSecretRequest{
|
||||||
|
WebhookSecret: "new",
|
||||||
|
}, testProject, testAdminCtx)
|
||||||
|
|
||||||
|
if resp1.Ok != true {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := getWebhookSecret(testProject, testAdminCtx)
|
||||||
|
|
||||||
|
if resp.Ok != true {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
if resp.Content.WebhookSecret != "new" {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetWebhookRequiresRole(t *testing.T) {
|
||||||
|
|
||||||
|
otherUser := getSessionCtx("testwebhookrole", "testwebhookrole", false)
|
||||||
|
otherUserId := getAccountDetails(otherUser).Content.Id
|
||||||
|
|
||||||
|
user := getSessionCtx("testwebhookroleu", "testwebhookroleu", false)
|
||||||
|
userId := getAccountDetails(user).Content.Id
|
||||||
|
|
||||||
|
resp := setRoleOnProject(api.SetManagerRoleOnProjectRequest{
|
||||||
|
Role: storage.RoleEdit | storage.RoleManageAccess | storage.RoleRead,
|
||||||
|
Manager: otherUserId,
|
||||||
|
}, testProject, testAdminCtx)
|
||||||
|
if resp.Ok != true {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
resp = setRoleOnProject(api.SetManagerRoleOnProjectRequest{
|
||||||
|
Role: storage.RoleSecret,
|
||||||
|
Manager: userId,
|
||||||
|
}, testProject, testAdminCtx)
|
||||||
|
if resp.Ok != true {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
rUser := setWebhookSecret(api.SetWebhookSecretRequest{
|
||||||
|
WebhookSecret: "test",
|
||||||
|
}, testProject, user)
|
||||||
|
rOther := setWebhookSecret(api.SetWebhookSecretRequest{
|
||||||
|
WebhookSecret: "test",
|
||||||
|
}, testProject, otherUser)
|
||||||
|
rGuest := setWebhookSecret(api.SetWebhookSecretRequest{
|
||||||
|
WebhookSecret: "test",
|
||||||
|
}, testProject, nil)
|
||||||
|
|
||||||
|
if rUser.Ok != true {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
if rOther.Ok != false {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
if rGuest.Ok != false {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func createProjectAsAdmin(req api.CreateProjectRequest) CreateProjectAR {
|
func createProjectAsAdmin(req api.CreateProjectRequest) CreateProjectAR {
|
||||||
return createProject(req, testAdminCtx)
|
return createProject(req, testAdminCtx)
|
||||||
}
|
}
|
||||||
@ -502,3 +579,15 @@ func getProjectList(s *http.Client) (ar ProjectListAR) {
|
|||||||
UnmarshalResponse(r, &ar)
|
UnmarshalResponse(r, &ar)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getWebhookSecret(pid int64, s *http.Client) (ar WebhookSecretAR) {
|
||||||
|
r := Get(fmt.Sprintf("/project/webhook_secret/%d", pid), nil, s)
|
||||||
|
UnmarshalResponse(r, &ar)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func setWebhookSecret(req api.SetWebhookSecretRequest, pid int64, s *http.Client) (ar api.JsonResponse) {
|
||||||
|
r := Post(fmt.Sprintf("/project/webhook_secret/%d", pid), req, nil, s)
|
||||||
|
UnmarshalResponse(r, &ar)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
@ -172,3 +172,19 @@ type ReleaseAR struct {
|
|||||||
Updated bool `json:"updated"`
|
Updated bool `json:"updated"`
|
||||||
} `json:"content"`
|
} `json:"content"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WebhookSecretAR struct {
|
||||||
|
Ok bool `json:"ok"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Content struct {
|
||||||
|
WebhookSecret string `json:"webhook_secret"`
|
||||||
|
} `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccountAR struct {
|
||||||
|
Ok bool `json:"ok"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Content struct {
|
||||||
|
*storage.Manager `json:"manager"`
|
||||||
|
} `json:"content"`
|
||||||
|
}
|
||||||
|
@ -6,7 +6,6 @@ database:
|
|||||||
log_levels: ["debug", "error", "trace", "info", "warn"]
|
log_levels: ["debug", "error", "trace", "info", "warn"]
|
||||||
|
|
||||||
git:
|
git:
|
||||||
webhook_secret: "very_secret_secret"
|
|
||||||
webhook_hash: "sha1"
|
webhook_hash: "sha1"
|
||||||
webhook_sig_header: "X-Hub-Signature"
|
webhook_sig_header: "X-Hub-Signature"
|
||||||
|
|
||||||
|
@ -25,7 +25,8 @@ CREATE TABLE project
|
|||||||
git_repo TEXT NOT NULL,
|
git_repo TEXT NOT NULL,
|
||||||
version TEXT NOT NULL,
|
version TEXT NOT NULL,
|
||||||
motd TEXT NOT NULL,
|
motd TEXT NOT NULL,
|
||||||
secret TEXT NOT NULL DEFAULT '{}'
|
secret TEXT NOT NULL DEFAULT '{}',
|
||||||
|
webhook_secret TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE worker_access
|
CREATE TABLE worker_access
|
||||||
|
@ -106,4 +106,12 @@ export class ApiService {
|
|||||||
return this.http.post(this.url + `/project/secret/${pid}`, {"secret": secret})
|
return this.http.post(this.url + `/project/secret/${pid}`, {"secret": secret})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getWebhookSecret(pid: number) {
|
||||||
|
return this.http.get(this.url + `/project/webhook_secret/${pid}`,)
|
||||||
|
}
|
||||||
|
|
||||||
|
setWebhookSecret(pid: number, secret: string) {
|
||||||
|
return this.http.post(this.url + `/project/webhook_secret/${pid}`, {"webhook_secret": secret})
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -51,4 +51,16 @@ export class ManagerRoleOnProject {
|
|||||||
this.role &= ~4
|
this.role &= ~4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get secretRole(): boolean {
|
||||||
|
return (this.role & 8) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
set secretRole(role: boolean) {
|
||||||
|
if (role) {
|
||||||
|
this.role |= 8
|
||||||
|
} else {
|
||||||
|
this.role &= ~8
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -366,13 +366,14 @@ export class ProjectDashboardComponent implements OnInit {
|
|||||||
this.dialog.open(AreYouSureComponent, {
|
this.dialog.open(AreYouSureComponent, {
|
||||||
width: '250px',
|
width: '250px',
|
||||||
}).afterClosed().subscribe(result => {
|
}).afterClosed().subscribe(result => {
|
||||||
if (result) {
|
|
||||||
this.project.paused = paused;
|
this.project.paused = paused;
|
||||||
this.apiService.updateProject(this.project).subscribe(() => {
|
this.apiService.updateProject(this.project).subscribe(() => {
|
||||||
this.translate.get("messenger.acknowledged").subscribe(t =>
|
this.translate.get("messenger.acknowledged").subscribe(t =>
|
||||||
this.messenger.show(t))
|
this.messenger.show(t))
|
||||||
|
}, error => {
|
||||||
|
this.translate.get("messenger.unauthorized").subscribe(t =>
|
||||||
|
this.messenger.show(t))
|
||||||
})
|
})
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@
|
|||||||
<button mat-raised-button color="primary"
|
<button mat-raised-button color="primary"
|
||||||
*ngIf="authService.logged"
|
*ngIf="authService.logged"
|
||||||
[routerLink]="'/project/' + project.id + '/secret'">
|
[routerLink]="'/project/' + project.id + '/secret'">
|
||||||
|
<mat-icon>security</mat-icon>
|
||||||
{{"project.secret" | translate}}</button>
|
{{"project.secret" | translate}}</button>
|
||||||
</div>
|
</div>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
|
@ -55,6 +55,10 @@
|
|||||||
(change)="onRoleChange(m)"
|
(change)="onRoleChange(m)"
|
||||||
[disabled]="m.manager.id==auth.account.id"
|
[disabled]="m.manager.id==auth.account.id"
|
||||||
>{{"perms.manage"|translate}}</mat-checkbox>
|
>{{"perms.manage"|translate}}</mat-checkbox>
|
||||||
|
<mat-checkbox [(ngModel)]="m.secretRole"
|
||||||
|
(change)="onRoleChange(m)"
|
||||||
|
[disabled]="m.manager.id==auth.account.id"
|
||||||
|
>{{"perms.secret"|translate}}</mat-checkbox>
|
||||||
</mat-list-item>
|
</mat-list-item>
|
||||||
</mat-list>
|
</mat-list>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
|
@ -1,22 +1,47 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<mat-card>
|
<mat-card class="mat-elevation-z8">
|
||||||
|
<button mat-button [title]="'perms.refresh' | translate" style="float:right"
|
||||||
|
(click)="refresh()">
|
||||||
|
<mat-icon>refresh</mat-icon>
|
||||||
|
</button>
|
||||||
<mat-card-header>
|
<mat-card-header>
|
||||||
<mat-card-title>{{"secret.title" | translate}}</mat-card-title>
|
<mat-card-title>{{"secret.title" | translate}}</mat-card-title>
|
||||||
<mat-card-subtitle>{{"secret.subtitle" | translate}}</mat-card-subtitle>
|
<mat-card-subtitle>{{"secret.subtitle" | translate}}</mat-card-subtitle>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
|
|
||||||
|
<mat-card-content>
|
||||||
<mat-form-field appearance="outline">
|
<mat-form-field appearance="outline">
|
||||||
<mat-label>{{"secret.secret" | translate}}</mat-label>
|
<mat-label>{{"secret.secret" | translate}}</mat-label>
|
||||||
<textarea matInput [(ngModel)]="secret"
|
<textarea matInput [(ngModel)]="secret"
|
||||||
[placeholder]="'secret.secret'|translate"
|
[placeholder]="'secret.secret'|translate"
|
||||||
name="secret"></textarea>
|
name="secret"></textarea>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
</mat-card-content>
|
||||||
|
<mat-card-actions>
|
||||||
|
<button mat-raised-button color="primary"
|
||||||
|
(click)="onUpdate()">{{"secret.update" | translate}}
|
||||||
|
</button>
|
||||||
|
</mat-card-actions>
|
||||||
|
</mat-card>
|
||||||
|
|
||||||
|
<mat-card style="margin-top: 2em" class="mat-elevation-z8">
|
||||||
|
<mat-card-header>
|
||||||
|
<mat-card-title>{{"secret.webhook_title" | translate}}</mat-card-title>
|
||||||
|
<mat-card-subtitle>{{"secret.webhook_subtitle" | translate}}</mat-card-subtitle>
|
||||||
|
</mat-card-header>
|
||||||
|
<mat-card-content>
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>{{"secret.webhook_secret" | translate}}</mat-label>
|
||||||
|
<textarea matInput [(ngModel)]="webhookSecret"
|
||||||
|
[placeholder]="'secret.webhook_secret'|translate"
|
||||||
|
name="webhook_secret"></textarea>
|
||||||
|
</mat-form-field>
|
||||||
|
</mat-card-content>
|
||||||
|
|
||||||
<mat-card-actions>
|
<mat-card-actions>
|
||||||
<button mat-raised-button [routerLink]="'../'">Back</button>
|
<button mat-raised-button [routerLink]="'../'">Back</button>
|
||||||
<button mat-raised-button color="primary"
|
<button mat-raised-button color="primary"
|
||||||
(click)="onUpdate()">{{"secret.update" | translate}}</button>
|
(click)="onWebhookUpdate()">{{"secret.update" | translate}}</button>
|
||||||
</mat-card-actions>
|
</mat-card-actions>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,6 +13,7 @@ import {MessengerService} from "../messenger.service";
|
|||||||
export class ProjectSecretComponent implements OnInit {
|
export class ProjectSecretComponent implements OnInit {
|
||||||
|
|
||||||
secret: string;
|
secret: string;
|
||||||
|
webhookSecret: string;
|
||||||
projectId: number;
|
projectId: number;
|
||||||
|
|
||||||
constructor(private auth: AuthService,
|
constructor(private auth: AuthService,
|
||||||
@ -26,6 +27,7 @@ export class ProjectSecretComponent implements OnInit {
|
|||||||
this.route.params.subscribe(params => {
|
this.route.params.subscribe(params => {
|
||||||
this.projectId = params["id"];
|
this.projectId = params["id"];
|
||||||
this.getSecret();
|
this.getSecret();
|
||||||
|
this.getWebhookSecret();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,10 +39,28 @@ export class ProjectSecretComponent implements OnInit {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getWebhookSecret() {
|
||||||
|
this.apiService.getWebhookSecret(this.projectId).subscribe(data => {
|
||||||
|
this.webhookSecret = data["content"]["webhook_secret"]
|
||||||
|
}, error => {
|
||||||
|
this.translate.get("messenger.unauthorized").subscribe(t => this.messenger.show(t))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
onUpdate() {
|
onUpdate() {
|
||||||
this.apiService.setSecret(this.projectId, this.secret).subscribe(data => {
|
this.apiService.setSecret(this.projectId, this.secret).subscribe(data => {
|
||||||
this.translate.get("secret.ok").subscribe(t => this.messenger.show(t))
|
this.translate.get("secret.ok").subscribe(t => this.messenger.show(t))
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onWebhookUpdate() {
|
||||||
|
this.apiService.setWebhookSecret(this.projectId, this.webhookSecret).subscribe(data => {
|
||||||
|
this.translate.get("secret.ok").subscribe(t => this.messenger.show(t))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
this.getWebhookSecret();
|
||||||
|
this.getSecret();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,8 @@
|
|||||||
"login": "Login",
|
"login": "Login",
|
||||||
"worker_dashboard": "Workers",
|
"worker_dashboard": "Workers",
|
||||||
"account": "Account",
|
"account": "Account",
|
||||||
"manager_list": "Managers"
|
"manager_list": "Managers",
|
||||||
|
"back": "back"
|
||||||
},
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"title": "Logs",
|
"title": "Logs",
|
||||||
@ -119,6 +120,7 @@
|
|||||||
"read": "Read",
|
"read": "Read",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"manage": "Manage permissions",
|
"manage": "Manage permissions",
|
||||||
|
"secret": "Configure secrets",
|
||||||
"set": "Changes saved",
|
"set": "Changes saved",
|
||||||
"workers": "Workers",
|
"workers": "Workers",
|
||||||
"no_workers": "No workers have explicit access to this project",
|
"no_workers": "No workers have explicit access to this project",
|
||||||
@ -152,6 +154,9 @@
|
|||||||
"secret": {
|
"secret": {
|
||||||
"title": "Project secret",
|
"title": "Project secret",
|
||||||
"subtitle": "You can set project configuration here. It is only accessible by workers with ASSIGN access to this project",
|
"subtitle": "You can set project configuration here. It is only accessible by workers with ASSIGN access to this project",
|
||||||
|
"webhook_title": "Webhook secret",
|
||||||
|
"webhook_subtitle": "You need to supply this to Github/Gogs for delivery to work",
|
||||||
|
"webhook_secret": "Webhook secret",
|
||||||
"secret": "Secret",
|
"secret": "Secret",
|
||||||
"update": "Update",
|
"update": "Update",
|
||||||
"ok": "Updated"
|
"ok": "Updated"
|
||||||
|
@ -8,7 +8,8 @@
|
|||||||
"login": "Ouvrir un session",
|
"login": "Ouvrir un session",
|
||||||
"worker_dashboard": "Workers",
|
"worker_dashboard": "Workers",
|
||||||
"account": "Compte",
|
"account": "Compte",
|
||||||
"manager_list": "Managers"
|
"manager_list": "Managers",
|
||||||
|
"back": "Arrière"
|
||||||
},
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"title": "Journaux",
|
"title": "Journaux",
|
||||||
@ -116,11 +117,11 @@
|
|||||||
"read": "Lecture",
|
"read": "Lecture",
|
||||||
"edit": "Modification",
|
"edit": "Modification",
|
||||||
"manage": "Gestion des permissions",
|
"manage": "Gestion des permissions",
|
||||||
|
"secret": "Gestion des secrets",
|
||||||
"set": "Changements enregistrés",
|
"set": "Changements enregistrés",
|
||||||
"workers": "Workers",
|
"workers": "Workers",
|
||||||
"no_workers": "Aucun Worker n'a explicitement accès à ce projet",
|
"no_workers": "Aucun Worker n'a explicitement accès à ce projet",
|
||||||
"managers": "Managers",
|
"managers": "Managers"
|
||||||
"secret": "Secret"
|
|
||||||
},
|
},
|
||||||
"messenger": {
|
"messenger": {
|
||||||
"close": "Fermer",
|
"close": "Fermer",
|
||||||
@ -148,8 +149,11 @@
|
|||||||
"alias": "Alias pertinent"
|
"alias": "Alias pertinent"
|
||||||
},
|
},
|
||||||
"secret": {
|
"secret": {
|
||||||
"title": "Secret",
|
"title": "Secret du projet",
|
||||||
"subtitle": "Vous pouvez définir la configuration du projet ici. Ce n'est accessible que par les Workers ayant accès au projet",
|
"subtitle": "Vous pouvez définir la configuration du projet ici. Ce n'est accessible que par les Workers ayant accès au projet",
|
||||||
|
"webhook_title": "Secret (Webhook)",
|
||||||
|
"webhook_subtitle": "Vous devez le donner à Github/Gogs.",
|
||||||
|
"webhook_secret": "Secret (Webhook)",
|
||||||
"secret": "Secret",
|
"secret": "Secret",
|
||||||
"update": "Mettre à jour",
|
"update": "Mettre à jour",
|
||||||
"ok": "Mis à jour"
|
"ok": "Mis à jour"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user