mirror of https://github.com/matrix-org/go-neb.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
206 lines
7.2 KiB
206 lines
7.2 KiB
package webhook
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
gojira "github.com/andygrunwald/go-jira"
|
|
"github.com/matrix-org/go-neb/database"
|
|
"github.com/matrix-org/go-neb/realms/jira"
|
|
"github.com/matrix-org/util"
|
|
log "github.com/sirupsen/logrus"
|
|
"maunium.net/go/mautrix/id"
|
|
)
|
|
|
|
type jiraWebhook struct {
|
|
Name string `json:"name"`
|
|
URL string `json:"url"`
|
|
Events []string `json:"events"`
|
|
Filter string `json:"jqlFilter"`
|
|
Exclude bool `json:"excludeIssueDetails"`
|
|
// These fields are populated on GET
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
// Event represents an incoming JIRA webhook event
|
|
type Event struct {
|
|
WebhookEvent string `json:"webhookEvent"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
User gojira.User `json:"user"`
|
|
Issue gojira.Issue `json:"issue"`
|
|
}
|
|
|
|
// RegisterHook checks to see if this user is allowed to track the given projects and then tracks them.
|
|
func RegisterHook(jrealm *jira.Realm, projects []string, userID id.UserID, webhookEndpointURL string) error {
|
|
// Tracking means that a webhook may need to be created on the remote JIRA installation.
|
|
// We need to make sure that the user has permission to do this. If they don't, it may still be okay if
|
|
// there is an existing webhook set up for this installation by someone else, *PROVIDED* that the projects
|
|
// they wish to monitor are "public" (accessible by not logged in users).
|
|
//
|
|
// The methodology for this is as follows:
|
|
// - If they don't have a JIRA token for the remote install, fail.
|
|
// - Try to GET /webhooks. If this succeeds:
|
|
// * The user is an admin (only admins can GET webhooks)
|
|
// * If there is a NEB webhook already then return success.
|
|
// * Else create the webhook and then return success (if creation fails then fail).
|
|
// - Else:
|
|
// * The user is NOT an admin.
|
|
// * Are ALL the projects in the config public? If yes:
|
|
// - Is there an existing config for this remote JIRA installation? If yes:
|
|
// * Another user has setup a webhook. We can't check if the webhook is still alive though,
|
|
// return success.
|
|
// - Else:
|
|
// * There is no existing NEB webhook for this JIRA installation. The user cannot create a
|
|
// webhook to the JIRA installation, so fail.
|
|
// * Else:
|
|
// - There are private projects in the config and the user isn't an admin, so fail.
|
|
logger := log.WithFields(log.Fields{
|
|
"realm_id": jrealm.ID(),
|
|
"jira_url": jrealm.JIRAEndpoint,
|
|
"user_id": userID,
|
|
})
|
|
cli, err := jrealm.JIRAClient(userID, false)
|
|
if err != nil {
|
|
logger.WithError(err).Print("No JIRA client exists")
|
|
return err // no OAuth token on this JIRA endpoint
|
|
}
|
|
wh, forbidden, err := getWebhook(cli, webhookEndpointURL)
|
|
if err != nil {
|
|
if !forbidden {
|
|
logger.WithError(err).Print("Failed to GET webhook")
|
|
return err
|
|
}
|
|
// User is not a JIRA admin (cannot GET webhooks)
|
|
// The only way this is going to end well for this request is if all the projects
|
|
// are PUBLIC. That is, they can be accessed directly without an access token.
|
|
err = checkProjectsArePublic(jrealm, projects, userID)
|
|
if err != nil {
|
|
logger.WithError(err).Print("Failed to assert that all projects are public")
|
|
return err
|
|
}
|
|
|
|
// All projects that wish to be tracked are public, but the user cannot create
|
|
// webhooks. The only way this will work is if we already have a webhook for this
|
|
// JIRA endpoint.
|
|
if !jrealm.HasWebhook {
|
|
logger.Print("No webhook exists for this realm.")
|
|
return fmt.Errorf("Not authorised to create webhook: not an admin")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// The user is probably an admin (can query webhooks endpoint)
|
|
|
|
if wh != nil {
|
|
logger.Print("Webhook already exists")
|
|
return nil // we already have a NEB webhook :D
|
|
}
|
|
return createWebhook(jrealm, webhookEndpointURL, userID)
|
|
}
|
|
|
|
// OnReceiveRequest is called when JIRA hits NEB with an update.
|
|
// Returns the project key and webhook event, or an error.
|
|
func OnReceiveRequest(req *http.Request) (string, *Event, *util.JSONResponse) {
|
|
// extract the JIRA webhook event JSON
|
|
defer req.Body.Close()
|
|
var whe Event
|
|
err := json.NewDecoder(req.Body).Decode(&whe)
|
|
if err != nil {
|
|
resErr := util.MessageResponse(400, "Failed to parse request JSON")
|
|
return "", nil, &resErr
|
|
}
|
|
|
|
if err != nil {
|
|
resErr := util.MessageResponse(400, "Failed to parse JIRA URL")
|
|
return "", nil, &resErr
|
|
}
|
|
projKey := strings.Split(whe.Issue.Key, "-")[0]
|
|
projKey = strings.ToUpper(projKey)
|
|
return projKey, &whe, nil
|
|
}
|
|
|
|
func createWebhook(jrealm *jira.Realm, webhookEndpointURL string, userID id.UserID) error {
|
|
cli, err := jrealm.JIRAClient(userID, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req, err := cli.NewRequest("POST", "rest/webhooks/1.0/webhook", jiraWebhook{
|
|
Name: "Go-NEB",
|
|
URL: webhookEndpointURL,
|
|
Events: []string{"jira:issue_created", "jira:issue_deleted", "jira:issue_updated"},
|
|
Filter: "",
|
|
Exclude: false,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
res, err := cli.Do(req, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
|
return fmt.Errorf("Creating webhook returned HTTP %d", res.StatusCode)
|
|
}
|
|
log.WithFields(log.Fields{
|
|
"status_code": res.StatusCode,
|
|
"realm_id": jrealm.ID(),
|
|
"jira_url": jrealm.JIRAEndpoint,
|
|
}).Print("Created webhook")
|
|
|
|
// mark this on the realm and persist it.
|
|
jrealm.HasWebhook = true
|
|
_, err = database.GetServiceDB().StoreAuthRealm(jrealm)
|
|
return err
|
|
}
|
|
|
|
// Get an existing JIRA webhook. Returns the hook if it exists, or an error along with a bool
|
|
// which indicates if the request to retrieve the hook is not 2xx. If it is not 2xx, it is
|
|
// forbidden (different JIRA deployments return different codes ranging from 401/403/404/500).
|
|
func getWebhook(cli *gojira.Client, webhookEndpointURL string) (*jiraWebhook, bool, error) {
|
|
req, err := cli.NewRequest("GET", "rest/webhooks/1.0/webhook", nil)
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("Failed to prepare webhook request")
|
|
}
|
|
var webhookList []jiraWebhook
|
|
res, err := cli.Do(req, &webhookList)
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("Failed to query webhooks")
|
|
}
|
|
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
|
return nil, true, fmt.Errorf("Querying webhook returned HTTP %d", res.StatusCode)
|
|
}
|
|
log.Print("Retrieved ", len(webhookList), " webhooks")
|
|
var nebWH *jiraWebhook
|
|
for _, wh := range webhookList {
|
|
if wh.URL == webhookEndpointURL {
|
|
nebWH = &wh
|
|
break
|
|
}
|
|
}
|
|
return nebWH, false, nil
|
|
}
|
|
|
|
func checkProjectsArePublic(jrealm *jira.Realm, projects []string, userID id.UserID) error {
|
|
publicCli, err := jrealm.JIRAClient("", true)
|
|
if err != nil {
|
|
return fmt.Errorf("Cannot create public JIRA client")
|
|
}
|
|
for _, projectKey := range projects {
|
|
// check you can query this project with a public client
|
|
req, err := publicCli.NewRequest("GET", "rest/api/2/project/"+projectKey, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to create project URL for project %s", projectKey)
|
|
}
|
|
res, err := publicCli.Do(req, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to query project %s", projectKey)
|
|
}
|
|
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
|
return fmt.Errorf("Project %s is not public. (HTTP %d)", projectKey, res.StatusCode)
|
|
}
|
|
}
|
|
return nil
|
|
}
|