Browse Source

Implement JIRA webhook creation

Also start including unauthorised webhook creation semantics, though not
finished yet. Change the config format of `JIRAService` to make more sense
and allow multiple realms per room.
kegan/jira-webhooks
Kegan Dougal 8 years ago
parent
commit
684d54d6b4
  1. 89
      src/github.com/matrix-org/go-neb/services/jira/jira.go
  2. 169
      src/github.com/matrix-org/go-neb/services/jira/webhook/webhook.go

89
src/github.com/matrix-org/go-neb/services/jira/jira.go

@ -10,6 +10,7 @@ import (
"github.com/matrix-org/go-neb/matrix"
"github.com/matrix-org/go-neb/plugin"
"github.com/matrix-org/go-neb/realms/jira"
"github.com/matrix-org/go-neb/services/jira/webhook"
"github.com/matrix-org/go-neb/types"
"html"
"net/http"
@ -22,14 +23,16 @@ var issueKeyRegex = regexp.MustCompile("([A-z]+)-([0-9]+)")
var projectKeyRegex = regexp.MustCompile("^[A-z]+$")
type jiraService struct {
id string
BotUserID string
ClientUserID string
Rooms map[string]struct { // room_id => {}
RealmID string // Determines the JIRA endpoint
Projects map[string]struct { // SYN => {}
Expand bool
Track bool
id string
webhookEndpointURL string
BotUserID string
ClientUserID string
Rooms map[string]struct { // room_id => {}
Realms map[string]struct { // realm_id => {} Determines the JIRA endpoint
Projects map[string]struct { // SYN => {}
Expand bool
Track bool
}
}
}
}
@ -44,8 +47,29 @@ func (s *jiraService) RoomIDs() []string {
}
return keys
}
func (s *jiraService) Register() error { return nil }
func (s *jiraService) PostRegister(old types.Service) {}
func (s *jiraService) Register() error {
// We only ever make 1 JIRA webhook which listens for all projects and then filter
// on receive. So we simply need to know if we need to make a webhook or not. We
// need to do this for each unique realm.
for realmID, pkeys := range projectsAndRealmsToTrack(s) {
realm, err := database.GetServiceDB().LoadAuthRealm(realmID)
if err != nil {
return err
}
jrealm, ok := realm.(*realms.JIRARealm)
if !ok {
return errors.New("Realm ID doesn't map to a JIRA realm")
}
if err = webhook.RegisterHook(jrealm, pkeys, s.ClientUserID, s.webhookEndpointURL); err != nil {
return err
}
}
return nil
}
func (s *jiraService) PostRegister(old types.Service) {
// TODO: We don't remove old JIRA webhooks for now. Let the admin sort it out.
}
func (s *jiraService) cmdJiraCreate(roomID, userID string, args []string) (interface{}, error) {
// E.g jira create PROJ "Issue title" "Issue desc"
@ -125,21 +149,36 @@ func (s *jiraService) expandIssue(roomID, userID, issueKey string) interface{} {
}
projectKey := groups[1]
if !s.Rooms[roomID].Projects[projectKey].Expand {
// find which realm this project belongs to.
// TODO: Multiple realms with the same pkey will be randomly chosen.
realmID := ""
for r, realmConfig := range s.Rooms[roomID].Realms {
for pkey, projectConfig := range realmConfig.Projects {
if pkey == projectKey && projectConfig.Expand {
realmID = r
break
}
}
if realmID != "" {
break
}
}
if realmID == "" {
return nil
}
r, err := database.GetServiceDB().LoadAuthRealm(s.Rooms[roomID].RealmID)
r, err := database.GetServiceDB().LoadAuthRealm(realmID)
if err != nil {
logger.WithFields(log.Fields{
"realm_id": s.Rooms[roomID].RealmID,
"realm_id": realmID,
log.ErrorKey: err,
}).Print("Failed to load realm")
return nil
}
jrealm, ok := r.(*realms.JIRARealm)
if !ok {
logger.WithField("realm_id", s.Rooms[roomID].RealmID).Print(
logger.WithField("realm_id", realmID).Print(
"Realm cannot be typecast to JIRARealm",
)
}
@ -194,8 +233,9 @@ func (s *jiraService) Plugin(roomID string) plugin.Plugin {
},
}
}
func (s *jiraService) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli *matrix.Client) {
w.WriteHeader(200) // Do nothing
webhook.OnReceiveRequest(w, req, cli)
}
func (s *jiraService) projectToRealm(userID, pkey string) (*realms.JIRARealm, error) {
@ -261,6 +301,23 @@ func (s *jiraService) projectToRealm(userID, pkey string) (*realms.JIRARealm, er
return nil, nil
}
// Returns realm_id => [PROJ, ECT, KEYS]
func projectsAndRealmsToTrack(s *jiraService) map[string][]string {
ridsToProjects := make(map[string][]string)
for _, roomConfig := range s.Rooms {
for realmID, realmConfig := range roomConfig.Realms {
for projectKey, projectConfig := range realmConfig.Projects {
if projectConfig.Track {
ridsToProjects[realmID] = append(
ridsToProjects[realmID], projectKey,
)
}
}
}
}
return ridsToProjects
}
func htmlSummaryForIssue(issue *jira.Issue) string {
// form a summary of the issue being affected e.g:
// "Flibble Wibble [P1, In Progress]"
@ -281,6 +338,6 @@ func htmlSummaryForIssue(issue *jira.Issue) string {
func init() {
types.RegisterService(func(serviceID, webhookEndpointURL string) types.Service {
return &jiraService{id: serviceID}
return &jiraService{id: serviceID, webhookEndpointURL: webhookEndpointURL}
})
}

169
src/github.com/matrix-org/go-neb/services/jira/webhook/webhook.go

@ -0,0 +1,169 @@
package webhook
import (
"fmt"
log "github.com/Sirupsen/logrus"
"github.com/andygrunwald/go-jira"
"github.com/matrix-org/go-neb/errors"
"github.com/matrix-org/go-neb/matrix"
"github.com/matrix-org/go-neb/realms/jira"
"net/http"
)
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"`
}
// RegisterHook checks to see if this user is allowed to track the given projects and then tracks them.
func RegisterHook(jrealm *realms.JIRARealm, projects []string, 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, httpErr := getWebhook(cli, webhookEndpointURL)
if httpErr != nil {
if httpErr.Code != 403 {
logger.WithError(httpErr).Print("Failed to GET webhook")
return httpErr
}
// 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.
httpErr = checkProjectsArePublic(jrealm, projects, userID)
if httpErr != nil {
logger.WithError(httpErr).Print("Failed to assert that all projects are public")
return httpErr
}
// 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.
// TODO: Check for an existing webhook for this realm (flag on realm?)
return fmt.Errorf("Not supported yet")
}
// 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
func OnReceiveRequest(w http.ResponseWriter, req *http.Request, cli *matrix.Client) {
w.WriteHeader(200) // Do nothing
}
func createWebhook(jrealm *realms.JIRARealm, webhookEndpointURL, userID string) error {
cli, err := jrealm.JIRAClient(userID, false)
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")
return nil
}
func getWebhook(cli *jira.Client, webhookEndpointURL string) (*jiraWebhook, *errors.HTTPError) {
req, err := cli.NewRequest("GET", "rest/webhooks/1.0/webhook", nil)
if err != nil {
return nil, &errors.HTTPError{err, "Failed to prepare webhook request", 500}
}
var webhookList []jiraWebhook
res, err := cli.Do(req, &webhookList)
if err != nil {
return nil, &errors.HTTPError{err, "Failed to query webhooks", 502}
}
if res.StatusCode < 200 || res.StatusCode >= 300 {
return nil, &errors.HTTPError{
err,
fmt.Sprintf("Querying webhook returned HTTP %d", res.StatusCode),
403,
}
}
log.Print("Retrieved ", len(webhookList), " webhooks")
var nebWH *jiraWebhook
for _, wh := range webhookList {
if wh.URL == webhookEndpointURL {
nebWH = &wh
break
}
}
return nebWH, nil
}
func checkProjectsArePublic(jrealm *realms.JIRARealm, projects []string, userID string) *errors.HTTPError {
publicCli, err := jrealm.JIRAClient("", true)
if err != nil {
return &errors.HTTPError{err, "Cannot create public JIRA client", 500}
}
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 &errors.HTTPError{err, "Failed to create project URL", 500}
}
res, err := publicCli.Do(req, nil)
if err != nil {
return &errors.HTTPError{err, fmt.Sprintf("Failed to query project %s", projectKey), 500}
}
if res.StatusCode < 200 || res.StatusCode >= 300 {
return &errors.HTTPError{err, fmt.Sprintf("Project %s is not public. (HTTP %d)", projectKey, res.StatusCode), 403}
}
}
return nil
}
Loading…
Cancel
Save