diff options
-rw-r--r-- | api4/apitestlib.go | 8 | ||||
-rw-r--r-- | api4/context.go | 12 | ||||
-rw-r--r-- | api4/context_test.go | 31 | ||||
-rw-r--r-- | api4/webhook.go | 75 | ||||
-rw-r--r-- | api4/webhook_test.go | 108 | ||||
-rw-r--r-- | model/client4.go | 24 | ||||
-rw-r--r-- | store/sql_webhook_store.go | 5 | ||||
-rw-r--r-- | store/sql_webhook_store_test.go | 6 |
8 files changed, 269 insertions, 0 deletions
diff --git a/api4/apitestlib.go b/api4/apitestlib.go index 2647f20f6..3d2feaf6e 100644 --- a/api4/apitestlib.go +++ b/api4/apitestlib.go @@ -441,6 +441,14 @@ func CheckNotImplementedStatus(t *testing.T, resp *model.Response) { } } +func CheckOKStatus(t *testing.T, resp *model.Response) { + CheckNoError(t, resp) + + if resp.StatusCode != http.StatusOK { + t.Fatalf("wrong status code. expected %d got %d", http.StatusOK, resp.StatusCode) + } +} + func CheckErrorMessage(t *testing.T, resp *model.Response, errorId string) { if resp.Error == nil { debug.PrintStack() diff --git a/api4/context.go b/api4/context.go index c30a975f2..f9460f53b 100644 --- a/api4/context.go +++ b/api4/context.go @@ -455,3 +455,15 @@ func (c *Context) RequirePreferenceName() *Context { return c } + +func (c *Context) RequireHookId() *Context { + if c.Err != nil { + return c + } + + if len(c.Params.HookId) != 26 { + c.SetInvalidUrlParam("hook_id") + } + + return c +} diff --git a/api4/context_test.go b/api4/context_test.go new file mode 100644 index 000000000..302b7b24b --- /dev/null +++ b/api4/context_test.go @@ -0,0 +1,31 @@ +package api4 + +import ( + "net/http" + "testing" +) + +func TestRequireHookId(t *testing.T) { + c := &Context{} + t.Run("WhenHookIdIsValid", func(t *testing.T) { + c.Params = &ApiParams{HookId: "abcdefghijklmnopqrstuvwxyz"} + c.RequireHookId() + + if c.Err != nil { + t.Fatal("Hook Id is Valid. Should not have set error in context") + } + }) + + t.Run("WhenHookIdIsInvalid", func(t *testing.T) { + c.Params = &ApiParams{HookId: "abc"} + c.RequireHookId() + + if c.Err == nil { + t.Fatal("Should have set Error in context") + } + + if c.Err.StatusCode != http.StatusBadRequest { + t.Fatal("Should have set status as 400") + } + }) +} diff --git a/api4/webhook.go b/api4/webhook.go index 9efab6ae2..19a851390 100644 --- a/api4/webhook.go +++ b/api4/webhook.go @@ -17,6 +17,9 @@ func InitWebhook() { BaseRoutes.IncomingHooks.Handle("", ApiSessionRequired(createIncomingHook)).Methods("POST") BaseRoutes.IncomingHooks.Handle("", ApiSessionRequired(getIncomingHooks)).Methods("GET") + + BaseRoutes.IncomingHook.Handle("", ApiSessionRequired(getIncomingHook)).Methods("GET") + BaseRoutes.IncomingHook.Handle("", ApiSessionRequired(deleteIncomingHook)).Methods("DELETE") } func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) { @@ -83,3 +86,75 @@ func getIncomingHooks(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.IncomingWebhookListToJson(hooks))) } + +func getIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireHookId() + if c.Err != nil { + return + } + + hookID := c.Params.HookId + + var err *model.AppError + var hook *model.IncomingWebhook + var channel *model.Channel + + if hook, err = app.GetIncomingWebhook(hookID); err != nil { + c.Err = err + return + } else { + channel, err = app.GetChannel(hook.ChannelId) + if err != nil { + c.Err = err + return + } + + if !app.SessionHasPermissionToTeam(c.Session, hook.TeamId, model.PERMISSION_MANAGE_WEBHOOKS) || + (channel.Type != model.CHANNEL_OPEN && !app.SessionHasPermissionToChannel(c.Session, hook.ChannelId, model.PERMISSION_READ_CHANNEL)) { + c.LogAudit("fail - bad permissions") + c.SetPermissionError(model.PERMISSION_MANAGE_WEBHOOKS) + return + } else { + w.Write([]byte(hook.ToJson())) + return + } + } +} + +func deleteIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireHookId() + if c.Err != nil { + return + } + + hookID := c.Params.HookId + + var err *model.AppError + var hook *model.IncomingWebhook + var channel *model.Channel + + if hook, err = app.GetIncomingWebhook(hookID); err != nil { + c.Err = err + return + } else { + channel, err = app.GetChannel(hook.ChannelId) + if err != nil { + c.Err = err + return + } + + if !app.SessionHasPermissionToTeam(c.Session, hook.TeamId, model.PERMISSION_MANAGE_WEBHOOKS) || + (channel.Type != model.CHANNEL_OPEN && !app.SessionHasPermissionToChannel(c.Session, hook.ChannelId, model.PERMISSION_READ_CHANNEL)) { + c.LogAudit("fail - bad permissions") + c.SetPermissionError(model.PERMISSION_MANAGE_WEBHOOKS) + return + } else { + if err = app.DeleteIncomingWebhook(hookID); err != nil { + c.Err = err + return + } + + ReturnStatusOK(w) + } + } +} diff --git a/api4/webhook_test.go b/api4/webhook_test.go index a6705f6e1..bfd75c7ec 100644 --- a/api4/webhook_test.go +++ b/api4/webhook_test.go @@ -148,3 +148,111 @@ func TestGetIncomingWebhooks(t *testing.T) { _, resp = Client.GetIncomingWebhooks(0, 1000, "") CheckUnauthorizedStatus(t, resp) } + +func TestGetIncomingWebhook(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.SystemAdminClient + + enableIncomingHooks := utils.Cfg.ServiceSettings.EnableIncomingWebhooks + enableAdminOnlyHooks := utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations + defer func() { + utils.Cfg.ServiceSettings.EnableIncomingWebhooks = enableIncomingHooks + utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = enableAdminOnlyHooks + utils.SetDefaultRolesBasedOnConfig() + }() + utils.Cfg.ServiceSettings.EnableIncomingWebhooks = true + *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = true + utils.SetDefaultRolesBasedOnConfig() + + var resp *model.Response + var rhook *model.IncomingWebhook + var hook *model.IncomingWebhook + + t.Run("WhenHookExists", func(t *testing.T) { + hook = &model.IncomingWebhook{ChannelId: th.BasicChannel.Id} + rhook, resp = Client.CreateIncomingWebhook(hook) + CheckNoError(t, resp) + + hook, resp = Client.GetIncomingWebhook(rhook.Id, "") + CheckOKStatus(t, resp) + }) + + t.Run("WhenHookDoesNotExist", func(t *testing.T) { + hook, resp = Client.GetIncomingWebhook(model.NewId(), "") + CheckNotFoundStatus(t, resp) + }) + + t.Run("WhenInvalidHookID", func(t *testing.T) { + hook, resp = Client.GetIncomingWebhook("abc", "") + CheckBadRequestStatus(t, resp) + }) + + t.Run("WhenUserDoesNotHavePemissions", func(t *testing.T) { + th.LoginBasic() + Client = th.Client + + _, resp = Client.GetIncomingWebhook(rhook.Id, "") + CheckForbiddenStatus(t, resp) + }) +} + +func TestDeleteIncomingWebhook(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.SystemAdminClient + + enableIncomingHooks := utils.Cfg.ServiceSettings.EnableIncomingWebhooks + enableAdminOnlyHooks := utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations + defer func() { + utils.Cfg.ServiceSettings.EnableIncomingWebhooks = enableIncomingHooks + utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = enableAdminOnlyHooks + utils.SetDefaultRolesBasedOnConfig() + }() + utils.Cfg.ServiceSettings.EnableIncomingWebhooks = true + *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = true + utils.SetDefaultRolesBasedOnConfig() + + var resp *model.Response + var rhook *model.IncomingWebhook + var hook *model.IncomingWebhook + var status bool + + t.Run("WhenInvalidHookID", func(t *testing.T) { + status, resp = Client.DeleteIncomingWebhook("abc") + CheckBadRequestStatus(t, resp) + }) + + t.Run("WhenHookDoesNotExist", func(t *testing.T) { + status, resp = Client.DeleteIncomingWebhook(model.NewId()) + CheckNotFoundStatus(t, resp) + }) + + t.Run("WhenHookExists", func(t *testing.T) { + hook = &model.IncomingWebhook{ChannelId: th.BasicChannel.Id} + rhook, resp = Client.CreateIncomingWebhook(hook) + CheckNoError(t, resp) + + if status, resp = Client.DeleteIncomingWebhook(rhook.Id); !status { + t.Fatal("Delete should have succeeded") + } else { + CheckOKStatus(t, resp) + } + + // Get now should not return this deleted hook + _, resp = Client.GetIncomingWebhook(rhook.Id, "") + CheckNotFoundStatus(t, resp) + }) + + t.Run("WhenUserDoesNotHavePemissions", func(t *testing.T) { + hook = &model.IncomingWebhook{ChannelId: th.BasicChannel.Id} + rhook, resp = Client.CreateIncomingWebhook(hook) + CheckNoError(t, resp) + + th.LoginBasic() + Client = th.Client + + _, resp = Client.DeleteIncomingWebhook(rhook.Id) + CheckForbiddenStatus(t, resp) + }) +} diff --git a/model/client4.go b/model/client4.go index 9b0cce294..71d37341d 100644 --- a/model/client4.go +++ b/model/client4.go @@ -142,6 +142,10 @@ func (c *Client4) GetIncomingWebhooksRoute() string { return fmt.Sprintf("/hooks/incoming") } +func (c *Client4) GetIncomingWebhookRoute(hookID string) string { + return fmt.Sprintf(c.GetIncomingWebhooksRoute()+"/%v", hookID) +} + func (c *Client4) GetPreferencesRoute(userId string) string { return fmt.Sprintf(c.GetUserRoute(userId) + "/preferences") } @@ -968,6 +972,26 @@ func (c *Client4) GetIncomingWebhooksForTeam(teamId string, page int, perPage in } } +// GetIncomingWebhook returns an Incoming webhook given the hook ID +func (c *Client4) GetIncomingWebhook(hookID string, etag string) (*IncomingWebhook, *Response) { + if r, err := c.DoApiGet(c.GetIncomingWebhookRoute(hookID), etag); err != nil { + return nil, &Response{StatusCode: r.StatusCode, Error: err} + } else { + defer closeBody(r) + return IncomingWebhookFromJson(r.Body), BuildResponse(r) + } +} + +// DeleteIncomingWebhook deletes and Incoming Webhook given the hook ID +func (c *Client4) DeleteIncomingWebhook(hookID string) (bool, *Response) { + if r, err := c.DoApiDelete(c.GetIncomingWebhookRoute(hookID)); err != nil { + return false, &Response{StatusCode: r.StatusCode, Error: err} + } else { + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) + } +} + // Preferences Section // GetPreferences returns the user's preferences diff --git a/store/sql_webhook_store.go b/store/sql_webhook_store.go index 355678064..e0e6562bf 100644 --- a/store/sql_webhook_store.go +++ b/store/sql_webhook_store.go @@ -6,6 +6,8 @@ package store import ( "net/http" + "database/sql" + "github.com/mattermost/platform/einterfaces" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" @@ -155,6 +157,9 @@ func (s SqlWebhookStore) GetIncoming(id string, allowFromCache bool) StoreChanne if err := s.GetReplica().SelectOne(&webhook, "SELECT * FROM IncomingWebhooks WHERE Id = :Id AND DeleteAt = 0", map[string]interface{}{"Id": id}); err != nil { result.Err = model.NewLocAppError("SqlWebhookStore.GetIncoming", "store.sql_webhooks.get_incoming.app_error", nil, "id="+id+", err="+err.Error()) + if err == sql.ErrNoRows { + result.Err.StatusCode = http.StatusNotFound + } } if result.Err == nil { diff --git a/store/sql_webhook_store_test.go b/store/sql_webhook_store_test.go index e1aaad1b7..20bb8c151 100644 --- a/store/sql_webhook_store_test.go +++ b/store/sql_webhook_store_test.go @@ -6,6 +6,8 @@ package store import ( "testing" + "net/http" + "github.com/mattermost/platform/model" ) @@ -72,6 +74,10 @@ func TestWebhookStoreGetIncoming(t *testing.T) { if err := (<-store.Webhook().GetIncoming("123", true)).Err; err == nil { t.Fatal("Missing id should have failed") } + + if err := (<-store.Webhook().GetIncoming("123", true)).Err; err.StatusCode != http.StatusNotFound { + t.Fatal("Should have set the status as not found for missing id") + } } func TestWebhookStoreGetIncomingList(t *testing.T) { |