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

8 years ago
  1. package webhook
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net/http"
  6. "strings"
  7. gojira "github.com/andygrunwald/go-jira"
  8. "github.com/matrix-org/go-neb/database"
  9. "github.com/matrix-org/go-neb/realms/jira"
  10. "github.com/matrix-org/util"
  11. log "github.com/sirupsen/logrus"
  12. "maunium.net/go/mautrix/id"
  13. )
  14. type jiraWebhook struct {
  15. Name string `json:"name"`
  16. URL string `json:"url"`
  17. Events []string `json:"events"`
  18. Filter string `json:"jqlFilter"`
  19. Exclude bool `json:"excludeIssueDetails"`
  20. // These fields are populated on GET
  21. Enabled bool `json:"enabled"`
  22. }
  23. // Event represents an incoming JIRA webhook event
  24. type Event struct {
  25. WebhookEvent string `json:"webhookEvent"`
  26. Timestamp int64 `json:"timestamp"`
  27. User gojira.User `json:"user"`
  28. Issue gojira.Issue `json:"issue"`
  29. }
  30. // RegisterHook checks to see if this user is allowed to track the given projects and then tracks them.
  31. func RegisterHook(jrealm *jira.Realm, projects []string, userID id.UserID, webhookEndpointURL string) error {
  32. // Tracking means that a webhook may need to be created on the remote JIRA installation.
  33. // We need to make sure that the user has permission to do this. If they don't, it may still be okay if
  34. // there is an existing webhook set up for this installation by someone else, *PROVIDED* that the projects
  35. // they wish to monitor are "public" (accessible by not logged in users).
  36. //
  37. // The methodology for this is as follows:
  38. // - If they don't have a JIRA token for the remote install, fail.
  39. // - Try to GET /webhooks. If this succeeds:
  40. // * The user is an admin (only admins can GET webhooks)
  41. // * If there is a NEB webhook already then return success.
  42. // * Else create the webhook and then return success (if creation fails then fail).
  43. // - Else:
  44. // * The user is NOT an admin.
  45. // * Are ALL the projects in the config public? If yes:
  46. // - Is there an existing config for this remote JIRA installation? If yes:
  47. // * Another user has setup a webhook. We can't check if the webhook is still alive though,
  48. // return success.
  49. // - Else:
  50. // * There is no existing NEB webhook for this JIRA installation. The user cannot create a
  51. // webhook to the JIRA installation, so fail.
  52. // * Else:
  53. // - There are private projects in the config and the user isn't an admin, so fail.
  54. logger := log.WithFields(log.Fields{
  55. "realm_id": jrealm.ID(),
  56. "jira_url": jrealm.JIRAEndpoint,
  57. "user_id": userID,
  58. })
  59. cli, err := jrealm.JIRAClient(userID, false)
  60. if err != nil {
  61. logger.WithError(err).Print("No JIRA client exists")
  62. return err // no OAuth token on this JIRA endpoint
  63. }
  64. wh, forbidden, err := getWebhook(cli, webhookEndpointURL)
  65. if err != nil {
  66. if !forbidden {
  67. logger.WithError(err).Print("Failed to GET webhook")
  68. return err
  69. }
  70. // User is not a JIRA admin (cannot GET webhooks)
  71. // The only way this is going to end well for this request is if all the projects
  72. // are PUBLIC. That is, they can be accessed directly without an access token.
  73. err = checkProjectsArePublic(jrealm, projects, userID)
  74. if err != nil {
  75. logger.WithError(err).Print("Failed to assert that all projects are public")
  76. return err
  77. }
  78. // All projects that wish to be tracked are public, but the user cannot create
  79. // webhooks. The only way this will work is if we already have a webhook for this
  80. // JIRA endpoint.
  81. if !jrealm.HasWebhook {
  82. logger.Print("No webhook exists for this realm.")
  83. return fmt.Errorf("Not authorised to create webhook: not an admin")
  84. }
  85. return nil
  86. }
  87. // The user is probably an admin (can query webhooks endpoint)
  88. if wh != nil {
  89. logger.Print("Webhook already exists")
  90. return nil // we already have a NEB webhook :D
  91. }
  92. return createWebhook(jrealm, webhookEndpointURL, userID)
  93. }
  94. // OnReceiveRequest is called when JIRA hits NEB with an update.
  95. // Returns the project key and webhook event, or an error.
  96. func OnReceiveRequest(req *http.Request) (string, *Event, *util.JSONResponse) {
  97. // extract the JIRA webhook event JSON
  98. defer req.Body.Close()
  99. var whe Event
  100. err := json.NewDecoder(req.Body).Decode(&whe)
  101. if err != nil {
  102. resErr := util.MessageResponse(400, "Failed to parse request JSON")
  103. return "", nil, &resErr
  104. }
  105. if err != nil {
  106. resErr := util.MessageResponse(400, "Failed to parse JIRA URL")
  107. return "", nil, &resErr
  108. }
  109. projKey := strings.Split(whe.Issue.Key, "-")[0]
  110. projKey = strings.ToUpper(projKey)
  111. return projKey, &whe, nil
  112. }
  113. func createWebhook(jrealm *jira.Realm, webhookEndpointURL string, userID id.UserID) error {
  114. cli, err := jrealm.JIRAClient(userID, false)
  115. if err != nil {
  116. return err
  117. }
  118. req, err := cli.NewRequest("POST", "rest/webhooks/1.0/webhook", jiraWebhook{
  119. Name: "Go-NEB",
  120. URL: webhookEndpointURL,
  121. Events: []string{"jira:issue_created", "jira:issue_deleted", "jira:issue_updated"},
  122. Filter: "",
  123. Exclude: false,
  124. })
  125. if err != nil {
  126. return err
  127. }
  128. res, err := cli.Do(req, nil)
  129. if err != nil {
  130. return err
  131. }
  132. if res.StatusCode < 200 || res.StatusCode >= 300 {
  133. return fmt.Errorf("Creating webhook returned HTTP %d", res.StatusCode)
  134. }
  135. log.WithFields(log.Fields{
  136. "status_code": res.StatusCode,
  137. "realm_id": jrealm.ID(),
  138. "jira_url": jrealm.JIRAEndpoint,
  139. }).Print("Created webhook")
  140. // mark this on the realm and persist it.
  141. jrealm.HasWebhook = true
  142. _, err = database.GetServiceDB().StoreAuthRealm(jrealm)
  143. return err
  144. }
  145. // Get an existing JIRA webhook. Returns the hook if it exists, or an error along with a bool
  146. // which indicates if the request to retrieve the hook is not 2xx. If it is not 2xx, it is
  147. // forbidden (different JIRA deployments return different codes ranging from 401/403/404/500).
  148. func getWebhook(cli *gojira.Client, webhookEndpointURL string) (*jiraWebhook, bool, error) {
  149. req, err := cli.NewRequest("GET", "rest/webhooks/1.0/webhook", nil)
  150. if err != nil {
  151. return nil, false, fmt.Errorf("Failed to prepare webhook request")
  152. }
  153. var webhookList []jiraWebhook
  154. res, err := cli.Do(req, &webhookList)
  155. if err != nil {
  156. return nil, false, fmt.Errorf("Failed to query webhooks")
  157. }
  158. if res.StatusCode < 200 || res.StatusCode >= 300 {
  159. return nil, true, fmt.Errorf("Querying webhook returned HTTP %d", res.StatusCode)
  160. }
  161. log.Print("Retrieved ", len(webhookList), " webhooks")
  162. var nebWH *jiraWebhook
  163. for _, wh := range webhookList {
  164. if wh.URL == webhookEndpointURL {
  165. nebWH = &wh
  166. break
  167. }
  168. }
  169. return nebWH, false, nil
  170. }
  171. func checkProjectsArePublic(jrealm *jira.Realm, projects []string, userID id.UserID) error {
  172. publicCli, err := jrealm.JIRAClient("", true)
  173. if err != nil {
  174. return fmt.Errorf("Cannot create public JIRA client")
  175. }
  176. for _, projectKey := range projects {
  177. // check you can query this project with a public client
  178. req, err := publicCli.NewRequest("GET", "rest/api/2/project/"+projectKey, nil)
  179. if err != nil {
  180. return fmt.Errorf("Failed to create project URL for project %s", projectKey)
  181. }
  182. res, err := publicCli.Do(req, nil)
  183. if err != nil {
  184. return fmt.Errorf("Failed to query project %s", projectKey)
  185. }
  186. if res.StatusCode < 200 || res.StatusCode >= 300 {
  187. return fmt.Errorf("Project %s is not public. (HTTP %d)", projectKey, res.StatusCode)
  188. }
  189. }
  190. return nil
  191. }