Browse Source

Update go-neb to use util.JSONResponse

pull/162/head
Kegan Dougal 7 years ago
parent
commit
302a0238e2
  1. 109
      src/github.com/matrix-org/go-neb/api/handlers/auth.go
  2. 22
      src/github.com/matrix-org/go-neb/api/handlers/client.go
  3. 7
      src/github.com/matrix-org/go-neb/api/handlers/heartbeat.go
  4. 75
      src/github.com/matrix-org/go-neb/api/handlers/service.go
  5. 17
      src/github.com/matrix-org/go-neb/services/github/webhook/webhook.go
  6. 4
      src/github.com/matrix-org/go-neb/services/jira/jira.go
  7. 50
      src/github.com/matrix-org/go-neb/services/jira/webhook/webhook.go

109
src/github.com/matrix-org/go-neb/api/handlers/auth.go

@ -40,36 +40,43 @@ type RequestAuthSession struct {
// {
// // AuthRealm-specific information
// }
func (h *RequestAuthSession) OnIncomingRequest(req *http.Request) (interface{}, *util.HTTPError) {
func (h *RequestAuthSession) OnIncomingRequest(req *http.Request) util.JSONResponse {
logger := util.GetLogger(req.Context())
if req.Method != "POST" {
return nil, &util.HTTPError{nil, "Unsupported Method", 405}
return util.MessageResponse(405, "Unsupported Method")
}
var body api.RequestAuthSessionRequest
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
return nil, &util.HTTPError{err, "Error parsing request JSON", 400}
return util.MessageResponse(400, "Error parsing request JSON")
}
log.WithFields(log.Fields{
logger.WithFields(log.Fields{
"realm_id": body.RealmID,
"user_id": body.UserID,
}).Print("Incoming auth session request")
if err := body.Check(); err != nil {
return nil, &util.HTTPError{err, err.Error(), 400}
logger.WithError(err).Info("Failed Check")
return util.MessageResponse(400, err.Error())
}
realm, err := h.Db.LoadAuthRealm(body.RealmID)
if err != nil {
return nil, &util.HTTPError{err, "Unknown RealmID", 400}
logger.WithError(err).Info("Failed to LoadAuthRealm")
return util.MessageResponse(400, "Unknown RealmID")
}
response := realm.RequestAuthSession(body.UserID, body.Config)
if response == nil {
return nil, &util.HTTPError{nil, "Failed to request auth session", 500}
logger.WithField("body", body).Error("Failed to RequestAuthSession")
return util.MessageResponse(500, "Failed to request auth session")
}
metrics.IncrementAuthSession(realm.Type())
return response, nil
return util.JSONResponse{
Code: 200,
JSON: response,
}
}
// RemoveAuthSession represents an HTTP handler capable of processing /admin/removeAuthSession requests.
@ -90,36 +97,41 @@ type RemoveAuthSession struct {
// Response:
// HTTP/1.1 200 OK
// {}
func (h *RemoveAuthSession) OnIncomingRequest(req *http.Request) (interface{}, *util.HTTPError) {
func (h *RemoveAuthSession) OnIncomingRequest(req *http.Request) util.JSONResponse {
logger := util.GetLogger(req.Context())
if req.Method != "POST" {
return nil, &util.HTTPError{nil, "Unsupported Method", 405}
return util.MessageResponse(405, "Unsupported Method")
}
var body struct {
RealmID string
UserID string
}
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
return nil, &util.HTTPError{err, "Error parsing request JSON", 400}
return util.MessageResponse(400, "Error parsing request JSON")
}
log.WithFields(log.Fields{
logger.WithFields(log.Fields{
"realm_id": body.RealmID,
"user_id": body.UserID,
}).Print("Incoming remove auth session request")
if body.UserID == "" || body.RealmID == "" {
return nil, &util.HTTPError{nil, `Must supply a "UserID", a "RealmID"`, 400}
return util.MessageResponse(400, `Must supply a "UserID", a "RealmID"`)
}
_, err := h.Db.LoadAuthRealm(body.RealmID)
if err != nil {
return nil, &util.HTTPError{err, "Unknown RealmID", 400}
return util.MessageResponse(400, "Unknown RealmID")
}
if err := h.Db.RemoveAuthSession(body.RealmID, body.UserID); err != nil {
return nil, &util.HTTPError{err, "Failed to remove auth session", 500}
logger.WithError(err).Error("Failed to RemoveAuthSession")
return util.MessageResponse(500, "Failed to remove auth session")
}
return []byte(`{}`), nil
return util.JSONResponse{
Code: 200,
JSON: struct{}{},
}
}
// RealmRedirect represents an HTTP handler which can process incoming redirects for auth realms.
@ -186,39 +198,44 @@ type ConfigureAuthRealm struct {
// // New auth realm config information
// },
// }
func (h *ConfigureAuthRealm) OnIncomingRequest(req *http.Request) (interface{}, *util.HTTPError) {
func (h *ConfigureAuthRealm) OnIncomingRequest(req *http.Request) util.JSONResponse {
logger := util.GetLogger(req.Context())
if req.Method != "POST" {
return nil, &util.HTTPError{nil, "Unsupported Method", 405}
return util.MessageResponse(405, "Unsupported Method")
}
var body api.ConfigureAuthRealmRequest
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
return nil, &util.HTTPError{err, "Error parsing request JSON", 400}
return util.MessageResponse(400, "Error parsing request JSON")
}
if err := body.Check(); err != nil {
return nil, &util.HTTPError{err, err.Error(), 400}
return util.MessageResponse(400, err.Error())
}
realm, err := types.CreateAuthRealm(body.ID, body.Type, body.Config)
if err != nil {
return nil, &util.HTTPError{err, "Error parsing config JSON", 400}
return util.MessageResponse(400, "Error parsing config JSON")
}
if err = realm.Register(); err != nil {
return nil, &util.HTTPError{err, "Error registering auth realm", 400}
return util.MessageResponse(400, "Error registering auth realm")
}
oldRealm, err := h.Db.StoreAuthRealm(realm)
if err != nil {
return nil, &util.HTTPError{err, "Error storing realm", 500}
logger.WithError(err).Error("Failed to StoreAuthRealm")
return util.MessageResponse(500, "Error storing realm")
}
return &struct {
ID string
Type string
OldConfig types.AuthRealm
NewConfig types.AuthRealm
}{body.ID, body.Type, oldRealm, realm}, nil
return util.JSONResponse{
Code: 200,
JSON: struct {
ID string
Type string
OldConfig types.AuthRealm
NewConfig types.AuthRealm
}{body.ID, body.Type, oldRealm, realm},
}
}
// GetSession represents an HTTP handler capable of processing /admin/getSession requests.
@ -252,35 +269,43 @@ type GetSession struct {
// {
// "Authenticated": false
// }
func (h *GetSession) OnIncomingRequest(req *http.Request) (interface{}, *util.HTTPError) {
func (h *GetSession) OnIncomingRequest(req *http.Request) util.JSONResponse {
logger := util.GetLogger(req.Context())
if req.Method != "POST" {
return nil, &util.HTTPError{nil, "Unsupported Method", 405}
return util.MessageResponse(405, "Unsupported Method")
}
var body struct {
RealmID string
UserID string
}
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
return nil, &util.HTTPError{err, "Error parsing request JSON", 400}
return util.MessageResponse(400, "Error parsing request JSON")
}
if body.RealmID == "" || body.UserID == "" {
return nil, &util.HTTPError{nil, `Must supply a "RealmID" and "UserID"`, 400}
return util.MessageResponse(400, `Must supply a "RealmID" and "UserID"`)
}
session, err := h.Db.LoadAuthSessionByUser(body.RealmID, body.UserID)
if err != nil && err != sql.ErrNoRows {
return nil, &util.HTTPError{err, `Failed to load session`, 500}
logger.WithError(err).WithField("body", body).Error("Failed to LoadAuthSessionByUser")
return util.MessageResponse(500, `Failed to load session`)
}
if err == sql.ErrNoRows {
return &struct {
Authenticated bool
}{false}, nil
return util.JSONResponse{
Code: 200,
JSON: struct {
Authenticated bool
}{false},
}
}
return &struct {
ID string
Authenticated bool
Info interface{}
}{session.ID(), session.Authenticated(), session.Info()}, nil
return util.JSONResponse{
Code: 200,
JSON: struct {
ID string
Authenticated bool
Info interface{}
}{session.ID(), session.Authenticated(), session.Info()},
}
}

22
src/github.com/matrix-org/go-neb/api/handlers/client.go

@ -39,27 +39,31 @@ type ConfigureClient struct {
// // The new api.ClientConfig
// }
// }
func (s *ConfigureClient) OnIncomingRequest(req *http.Request) (interface{}, *util.HTTPError) {
func (s *ConfigureClient) OnIncomingRequest(req *http.Request) util.JSONResponse {
if req.Method != "POST" {
return nil, &util.HTTPError{nil, "Unsupported Method", 405}
return util.MessageResponse(405, "Unsupported Method")
}
var body api.ClientConfig
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
return nil, &util.HTTPError{err, "Error parsing request JSON", 400}
return util.MessageResponse(400, "Error parsing request JSON")
}
if err := body.Check(); err != nil {
return nil, &util.HTTPError{err, "Error parsing client config", 400}
return util.MessageResponse(400, "Error parsing client config")
}
oldClient, err := s.Clients.Update(body)
if err != nil {
return nil, &util.HTTPError{err, "Error storing token", 500}
util.GetLogger(req.Context()).WithError(err).WithField("body", body).Error("Failed to Clients.Update")
return util.MessageResponse(500, "Error storing token")
}
return &struct {
OldClient api.ClientConfig
NewClient api.ClientConfig
}{oldClient, body}, nil
return util.JSONResponse{
Code: 200,
JSON: struct {
OldClient api.ClientConfig
NewClient api.ClientConfig
}{oldClient, body},
}
}

7
src/github.com/matrix-org/go-neb/api/handlers/heartbeat.go

@ -26,6 +26,9 @@ type Heartbeat struct{}
// Response:
// HTTP/1.1 200 OK
// {}
func (*Heartbeat) OnIncomingRequest(req *http.Request) (interface{}, *util.HTTPError) {
return &struct{}{}, nil
func (*Heartbeat) OnIncomingRequest(req *http.Request) util.JSONResponse {
return util.JSONResponse{
Code: 200,
JSON: struct{}{},
}
}

75
src/github.com/matrix-org/go-neb/api/handlers/service.go

@ -76,16 +76,17 @@ func (s *ConfigureService) getMutexForServiceID(serviceID string) *sync.Mutex {
// // new service-specific config information
// },
// }
func (s *ConfigureService) OnIncomingRequest(req *http.Request) (interface{}, *util.HTTPError) {
func (s *ConfigureService) OnIncomingRequest(req *http.Request) util.JSONResponse {
if req.Method != "POST" {
return nil, &util.HTTPError{nil, "Unsupported Method", 405}
return util.MessageResponse(405, "Unsupported Method")
}
service, httpErr := s.createService(req)
if httpErr != nil {
return nil, httpErr
return *httpErr
}
log.WithFields(log.Fields{
logger := util.GetLogger(req.Context())
logger.WithFields(log.Fields{
"service_id": service.ServiceID(),
"service_type": service.ServiceType(),
"service_user_id": service.ServiceUserID(),
@ -98,32 +99,34 @@ func (s *ConfigureService) OnIncomingRequest(req *http.Request) (interface{}, *u
old, err := s.db.LoadService(service.ServiceID())
if err != nil && err != sql.ErrNoRows {
return nil, &util.HTTPError{err, "Error loading old service", 500}
logger.WithError(err).Error("Failed to LoadService")
return util.MessageResponse(500, "Error loading old service")
}
client, err := s.clients.Client(service.ServiceUserID())
if err != nil {
return nil, &util.HTTPError{err, "Unknown matrix client", 400}
return util.MessageResponse(400, "Unknown matrix client")
}
if err := checkClientForService(service, client); err != nil {
return nil, &util.HTTPError{err, err.Error(), 400}
return util.MessageResponse(400, err.Error())
}
if err = service.Register(old, client); err != nil {
return nil, &util.HTTPError{err, "Failed to register service: " + err.Error(), 500}
return util.MessageResponse(500, "Failed to register service: "+err.Error())
}
oldService, err := s.db.StoreService(service)
if err != nil {
return nil, &util.HTTPError{err, "Error storing service", 500}
logger.WithError(err).Error("Failed to StoreService")
return util.MessageResponse(500, "Error storing service")
}
// Start any polling NOW because they may decide to stop it in PostRegister, and we want to make
// sure we'll actually stop.
if _, ok := service.(types.Poller); ok {
if err := polling.StartPolling(service); err != nil {
log.WithFields(log.Fields{
logger.WithFields(log.Fields{
"service_id": service.ServiceID(),
log.ErrorKey: err,
}).Error("Failed to start poll loop.")
@ -133,27 +136,33 @@ func (s *ConfigureService) OnIncomingRequest(req *http.Request) (interface{}, *u
service.PostRegister(old)
metrics.IncrementConfigureService(service.ServiceType())
return &struct {
ID string
Type string
OldConfig types.Service
NewConfig types.Service
}{service.ServiceID(), service.ServiceType(), oldService, service}, nil
return util.JSONResponse{
Code: 200,
JSON: struct {
ID string
Type string
OldConfig types.Service
NewConfig types.Service
}{service.ServiceID(), service.ServiceType(), oldService, service},
}
}
func (s *ConfigureService) createService(req *http.Request) (types.Service, *util.HTTPError) {
func (s *ConfigureService) createService(req *http.Request) (types.Service, *util.JSONResponse) {
var body api.ConfigureServiceRequest
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
return nil, &util.HTTPError{err, "Error parsing request JSON", 400}
res := util.MessageResponse(400, "Error parsing request JSON")
return nil, &res
}
if err := body.Check(); err != nil {
return nil, &util.HTTPError{err, err.Error(), 400}
res := util.MessageResponse(400, err.Error())
return nil, &res
}
service, err := types.CreateService(body.ID, body.Type, body.UserID, body.Config)
if err != nil {
return nil, &util.HTTPError{err, "Error parsing config JSON", 400}
res := util.MessageResponse(400, "Error parsing config JSON")
return nil, &res
}
return service, nil
}
@ -182,34 +191,38 @@ type GetService struct {
// // service-specific config information
// }
// }
func (h *GetService) OnIncomingRequest(req *http.Request) (interface{}, *util.HTTPError) {
func (h *GetService) OnIncomingRequest(req *http.Request) util.JSONResponse {
if req.Method != "POST" {
return nil, &util.HTTPError{nil, "Unsupported Method", 405}
return util.MessageResponse(405, "Unsupported Method")
}
var body struct {
ID string
}
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
return nil, &util.HTTPError{err, "Error parsing request JSON", 400}
return util.MessageResponse(400, "Error parsing request JSON")
}
if body.ID == "" {
return nil, &util.HTTPError{nil, `Must supply a "ID"`, 400}
return util.MessageResponse(400, `Must supply a "ID"`)
}
srv, err := h.Db.LoadService(body.ID)
if err != nil {
if err == sql.ErrNoRows {
return nil, &util.HTTPError{err, `Service not found`, 404}
return util.MessageResponse(404, `Service not found`)
}
return nil, &util.HTTPError{err, `Failed to load service`, 500}
util.GetLogger(req.Context()).WithError(err).Error("Failed to LoadService")
return util.MessageResponse(500, `Failed to load service`)
}
return &struct {
ID string
Type string
Config types.Service
}{srv.ServiceID(), srv.ServiceType(), srv}, nil
return util.JSONResponse{
Code: 200,
JSON: struct {
ID string
Type string
Config types.Service
}{srv.ServiceID(), srv.ServiceType(), srv},
}
}
func checkClientForService(service types.Service, client *gomatrix.Client) error {

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

@ -21,14 +21,15 @@ import (
// matrix message to send, along with parsed repo information.
// The secretToken, if supplied, will be used to verify the request is from
// Github. If it isn't, an error is returned.
func OnReceiveRequest(r *http.Request, secretToken string) (string, *github.Repository, *gomatrix.HTMLMessage, *util.HTTPError) {
func OnReceiveRequest(r *http.Request, secretToken string) (string, *github.Repository, *gomatrix.HTMLMessage, *util.JSONResponse) {
// Verify the HMAC signature if NEB was configured with a secret token
eventType := r.Header.Get("X-GitHub-Event")
signatureSHA1 := r.Header.Get("X-Hub-Signature")
content, err := ioutil.ReadAll(r.Body)
if err != nil {
log.WithError(err).Print("Failed to read Github webhook body")
return "", nil, nil, &util.HTTPError{nil, "Failed to parse body", 400}
resErr := util.MessageResponse(400, "Failed to parse body")
return "", nil, nil, &resErr
}
// Verify request if a secret token has been supplied.
if secretToken != "" {
@ -38,14 +39,16 @@ func OnReceiveRequest(r *http.Request, secretToken string) (string, *github.Repo
if err != nil {
log.WithError(err).WithField("X-Hub-Signature", sigHex).Print(
"Failed to decode signature as hex.")
return "", nil, nil, &util.HTTPError{nil, "Failed to decode signature", 400}
resErr := util.MessageResponse(400, "Failed to decode signature")
return "", nil, nil, &resErr
}
if !checkMAC([]byte(content), sigBytes, []byte(secretToken)) {
log.WithFields(log.Fields{
"X-Hub-Signature": signatureSHA1,
}).Print("Received Github event which failed MAC check.")
return "", nil, nil, &util.HTTPError{nil, "Bad signature", 403}
resErr := util.MessageResponse(403, "Bad signature")
return "", nil, nil, &resErr
}
}
@ -58,13 +61,15 @@ func OnReceiveRequest(r *http.Request, secretToken string) (string, *github.Repo
// Github will send a "ping" event when the webhook is first created. We need
// to return a 200 in order for the webhook to be marked as "up" (this doesn't
// affect delivery, just the tick/cross status flag).
return "", nil, nil, &util.HTTPError{nil, "pong", 200}
res := util.MessageResponse(200, "pong")
return "", nil, nil, &res
}
htmlStr, repo, refinedType, err := parseGithubEvent(eventType, content)
if err != nil {
log.WithError(err).Print("Failed to parse github event")
return "", nil, nil, &util.HTTPError{nil, "Failed to parse github event", 500}
resErr := util.MessageResponse(500, "Failed to parse github event")
return "", nil, nil, &resErr
}
msg := gomatrix.GetHTMLMessage("m.notice", htmlStr)

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

@ -271,8 +271,8 @@ func (s *Service) Expansions(cli *gomatrix.Client) []types.Expansion {
func (s *Service) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli *gomatrix.Client) {
eventProjectKey, event, httpErr := webhook.OnReceiveRequest(req)
if httpErr != nil {
log.WithError(httpErr).Print("Failed to handle JIRA webhook")
w.WriteHeader(500)
log.Print("Failed to handle JIRA webhook")
w.WriteHeader(httpErr.Code)
return
}
// grab base jira url

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

@ -65,19 +65,19 @@ func RegisterHook(jrealm *jira.Realm, projects []string, userID, webhookEndpoint
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
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.
httpErr = checkProjectsArePublic(jrealm, projects, userID)
if httpErr != nil {
logger.WithError(httpErr).Print("Failed to assert that all projects are public")
return httpErr
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
@ -101,17 +101,19 @@ func RegisterHook(jrealm *jira.Realm, projects []string, userID, webhookEndpoint
// 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.HTTPError) {
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 {
return "", nil, &util.HTTPError{err, "Failed to parse request JSON", 400}
resErr := util.MessageResponse(400, "Failed to parse request JSON")
return "", nil, &resErr
}
if err != nil {
return "", nil, &util.HTTPError{err, "Failed to parse JIRA URL", 400}
resErr := util.MessageResponse(400, "Failed to parse JIRA URL")
return "", nil, &resErr
}
projKey := strings.Split(whe.Issue.Key, "-")[0]
projKey = strings.ToUpper(projKey)
@ -153,22 +155,18 @@ func createWebhook(jrealm *jira.Realm, webhookEndpointURL, userID string) error
return err
}
func getWebhook(cli *gojira.Client, webhookEndpointURL string) (*jiraWebhook, *util.HTTPError) {
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, &util.HTTPError{err, "Failed to prepare webhook request", 500}
return nil, false, fmt.Errorf("Failed to prepare webhook request")
}
var webhookList []jiraWebhook
res, err := cli.Do(req, &webhookList)
if err != nil {
return nil, &util.HTTPError{err, "Failed to query webhooks", 502}
return nil, false, fmt.Errorf("Failed to query webhooks")
}
if res.StatusCode < 200 || res.StatusCode >= 300 {
return nil, &util.HTTPError{
err,
fmt.Sprintf("Querying webhook returned HTTP %d", res.StatusCode),
403,
}
return nil, true, fmt.Errorf("Querying webhook returned HTTP %d", res.StatusCode)
}
log.Print("Retrieved ", len(webhookList), " webhooks")
var nebWH *jiraWebhook
@ -178,26 +176,26 @@ func getWebhook(cli *gojira.Client, webhookEndpointURL string) (*jiraWebhook, *u
break
}
}
return nebWH, nil
return nebWH, false, nil
}
func checkProjectsArePublic(jrealm *jira.Realm, projects []string, userID string) *util.HTTPError {
func checkProjectsArePublic(jrealm *jira.Realm, projects []string, userID string) error {
publicCli, err := jrealm.JIRAClient("", true)
if err != nil {
return &util.HTTPError{err, "Cannot create public JIRA client", 500}
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 &util.HTTPError{err, "Failed to create project URL", 500}
return fmt.Errorf("Failed to create project URL for project %s", projectKey)
}
res, err := publicCli.Do(req, nil)
if err != nil {
return &util.HTTPError{err, fmt.Sprintf("Failed to query project %s", projectKey), 500}
return fmt.Errorf("Failed to query project %s", projectKey)
}
if res.StatusCode < 200 || res.StatusCode >= 300 {
return &util.HTTPError{err, fmt.Sprintf("Project %s is not public. (HTTP %d)", projectKey, res.StatusCode), 403}
return fmt.Errorf("Project %s is not public. (HTTP %d)", projectKey, res.StatusCode)
}
}
return nil

Loading…
Cancel
Save