summaryrefslogtreecommitdiffstats
path: root/api4
diff options
context:
space:
mode:
authorJoram Wilander <jwawilander@gmail.com>2017-09-01 09:00:27 -0400
committerGitHub <noreply@github.com>2017-09-01 09:00:27 -0400
commit899ab31fff9b34bc125faf75b79a89e390deb2cf (patch)
tree41dc5832268504e54a0b2188eedcf89b7828dd12 /api4
parent74b5e52c4eb54000dcb5a7b46c0977d732bce80f (diff)
downloadchat-899ab31fff9b34bc125faf75b79a89e390deb2cf.tar.gz
chat-899ab31fff9b34bc125faf75b79a89e390deb2cf.tar.bz2
chat-899ab31fff9b34bc125faf75b79a89e390deb2cf.zip
Implement experimental REST API endpoints for plugins (#7279)
* Implement experimental REST API endpoints for plugins * Updates per feedback and rebase * Update tests * Further updates * Update extraction of plugins * Use OS temp dir for plugins instead of search path * Fail extraction on paths that attempt to traverse upward * Update pluginenv ActivePlugins()
Diffstat (limited to 'api4')
-rw-r--r--api4/api.go7
-rw-r--r--api4/context.go12
-rw-r--r--api4/params.go5
-rw-r--r--api4/plugin.go120
-rw-r--r--api4/plugin_test.go115
-rw-r--r--api4/system.go1
6 files changed, 260 insertions, 0 deletions
diff --git a/api4/api.go b/api4/api.go
index 8ed94c193..3a4f2c412 100644
--- a/api4/api.go
+++ b/api4/api.go
@@ -53,6 +53,9 @@ type Routes struct {
Files *mux.Router // 'api/v4/files'
File *mux.Router // 'api/v4/files/{file_id:[A-Za-z0-9]+}'
+ Plugins *mux.Router // 'api/v4/plugins'
+ Plugin *mux.Router // 'api/v4/plugins/{plugin_id:[A-Za-z0-9_-]+}'
+
PublicFile *mux.Router // 'files/{file_id:[A-Za-z0-9]+}/public'
Commands *mux.Router // 'api/v4/commands'
@@ -146,6 +149,9 @@ func InitApi(full bool) {
BaseRoutes.File = BaseRoutes.Files.PathPrefix("/{file_id:[A-Za-z0-9]+}").Subrouter()
BaseRoutes.PublicFile = BaseRoutes.Root.PathPrefix("/files/{file_id:[A-Za-z0-9]+}/public").Subrouter()
+ BaseRoutes.Plugins = BaseRoutes.ApiRoot.PathPrefix("/plugins").Subrouter()
+ BaseRoutes.Plugin = BaseRoutes.Plugins.PathPrefix("/{plugin_id:[A-Za-z0-9\\_\\-]+}").Subrouter()
+
BaseRoutes.Commands = BaseRoutes.ApiRoot.PathPrefix("/commands").Subrouter()
BaseRoutes.Command = BaseRoutes.Commands.PathPrefix("/{command_id:[A-Za-z0-9]+}").Subrouter()
@@ -205,6 +211,7 @@ func InitApi(full bool) {
InitReaction()
InitWebrtc()
InitOpenGraph()
+ InitPlugin()
app.Srv.Router.Handle("/api/v4/{anything:.*}", http.HandlerFunc(Handle404))
diff --git a/api4/context.go b/api4/context.go
index 3ea67b30c..2c0e54ea0 100644
--- a/api4/context.go
+++ b/api4/context.go
@@ -428,6 +428,18 @@ func (c *Context) RequireFileId() *Context {
return c
}
+func (c *Context) RequirePluginId() *Context {
+ if c.Err != nil {
+ return c
+ }
+
+ if len(c.Params.PluginId) == 0 {
+ c.SetInvalidUrlParam("plugin_id")
+ }
+
+ return c
+}
+
func (c *Context) RequireReportId() *Context {
if c.Err != nil {
return c
diff --git a/api4/params.go b/api4/params.go
index 8b1d0febe..1f0fe8e63 100644
--- a/api4/params.go
+++ b/api4/params.go
@@ -24,6 +24,7 @@ type ApiParams struct {
ChannelId string
PostId string
FileId string
+ PluginId string
CommandId string
HookId string
ReportId string
@@ -78,6 +79,10 @@ func ApiParamsFromRequest(r *http.Request) *ApiParams {
params.FileId = val
}
+ if val, ok := props["plugin_id"]; ok {
+ params.PluginId = val
+ }
+
if val, ok := props["command_id"]; ok {
params.CommandId = val
}
diff --git a/api4/plugin.go b/api4/plugin.go
new file mode 100644
index 000000000..109695174
--- /dev/null
+++ b/api4/plugin.go
@@ -0,0 +1,120 @@
+// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+// EXPERIMENTAL - SUBJECT TO CHANGE
+
+package api4
+
+import (
+ "net/http"
+
+ l4g "github.com/alecthomas/log4go"
+ "github.com/mattermost/platform/app"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+)
+
+const (
+ MAXIMUM_PLUGIN_FILE_SIZE = 50 * 1024 * 1024
+)
+
+func InitPlugin() {
+ l4g.Debug("EXPERIMENTAL: Initializing plugin api")
+
+ BaseRoutes.Plugins.Handle("", ApiSessionRequired(uploadPlugin)).Methods("POST")
+ BaseRoutes.Plugins.Handle("", ApiSessionRequired(getPlugins)).Methods("GET")
+ BaseRoutes.Plugin.Handle("", ApiSessionRequired(removePlugin)).Methods("DELETE")
+
+}
+
+func uploadPlugin(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !*utils.Cfg.PluginSettings.Enable {
+ c.Err = model.NewAppError("uploadPlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
+ return
+ }
+
+ if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
+ return
+ }
+
+ if err := r.ParseMultipartForm(MAXIMUM_PLUGIN_FILE_SIZE); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ m := r.MultipartForm
+
+ pluginArray, ok := m.File["plugin"]
+ if !ok {
+ c.Err = model.NewAppError("uploadPlugin", "api.plugin.upload.no_file.app_error", nil, "", http.StatusBadRequest)
+ return
+ }
+
+ if len(pluginArray) <= 0 {
+ c.Err = model.NewAppError("uploadPlugin", "api.plugin.upload.array.app_error", nil, "", http.StatusBadRequest)
+ return
+ }
+
+ file, err := pluginArray[0].Open()
+ if err != nil {
+ c.Err = model.NewAppError("uploadPlugin", "api.plugin.upload.file.app_error", nil, "", http.StatusBadRequest)
+ return
+ }
+ defer file.Close()
+
+ manifest, unpackErr := app.UnpackAndActivatePlugin(file)
+
+ if unpackErr != nil {
+ c.Err = unpackErr
+ return
+ }
+
+ w.WriteHeader(http.StatusCreated)
+ w.Write([]byte(manifest.ToJson()))
+}
+
+func getPlugins(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !*utils.Cfg.PluginSettings.Enable {
+ c.Err = model.NewAppError("getPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
+ return
+ }
+
+ if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
+ return
+ }
+
+ manifests, err := app.GetActivePluginManifests()
+ if err != nil {
+ c.Err = err
+ return
+ }
+
+ w.Write([]byte(model.ManifestListToJson(manifests)))
+}
+
+func removePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
+ c.RequirePluginId()
+ if c.Err != nil {
+ return
+ }
+
+ if !*utils.Cfg.PluginSettings.Enable {
+ c.Err = model.NewAppError("getPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
+ return
+ }
+
+ if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
+ return
+ }
+
+ err := app.RemovePlugin(c.Params.PluginId)
+ if err != nil {
+ c.Err = err
+ return
+ }
+
+ ReturnStatusOK(w)
+}
diff --git a/api4/plugin_test.go b/api4/plugin_test.go
new file mode 100644
index 000000000..c1d6c987c
--- /dev/null
+++ b/api4/plugin_test.go
@@ -0,0 +1,115 @@
+// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api4
+
+import (
+ "bytes"
+ "io/ioutil"
+ "os"
+ "testing"
+
+ "github.com/mattermost/platform/app"
+ "github.com/mattermost/platform/utils"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestPlugin(t *testing.T) {
+ pluginDir, err := ioutil.TempDir("", "mm-plugin-test")
+ require.NoError(t, err)
+ defer func() {
+ os.RemoveAll(pluginDir)
+ }()
+ webappDir, err := ioutil.TempDir("", "mm-webapp-test")
+ require.NoError(t, err)
+ defer func() {
+ os.RemoveAll(webappDir)
+ }()
+
+ th := Setup().InitBasic().InitSystemAdmin()
+ defer TearDown()
+
+ app.StartupPlugins(pluginDir, webappDir)
+
+ enablePlugins := *utils.Cfg.PluginSettings.Enable
+ defer func() {
+ *utils.Cfg.PluginSettings.Enable = enablePlugins
+ }()
+ *utils.Cfg.PluginSettings.Enable = true
+
+ path, _ := utils.FindDir("tests")
+ file, err := os.Open(path + "/testplugin.tar.gz")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer file.Close()
+
+ // Successful upload
+ manifest, resp := th.SystemAdminClient.UploadPlugin(file)
+ defer func() {
+ os.RemoveAll("plugins/testplugin")
+ }()
+ CheckNoError(t, resp)
+
+ assert.Equal(t, "testplugin", manifest.Id)
+
+ // Upload error cases
+ _, resp = th.SystemAdminClient.UploadPlugin(bytes.NewReader([]byte("badfile")))
+ CheckBadRequestStatus(t, resp)
+
+ *utils.Cfg.PluginSettings.Enable = false
+ _, resp = th.SystemAdminClient.UploadPlugin(file)
+ CheckNotImplementedStatus(t, resp)
+
+ *utils.Cfg.PluginSettings.Enable = true
+ _, resp = th.Client.UploadPlugin(file)
+ CheckForbiddenStatus(t, resp)
+
+ // Successful get
+ manifests, resp := th.SystemAdminClient.GetPlugins()
+ CheckNoError(t, resp)
+
+ found := false
+ for _, m := range manifests {
+ if m.Id == manifest.Id {
+ found = true
+ }
+ }
+
+ assert.True(t, found)
+
+ // Get error cases
+ *utils.Cfg.PluginSettings.Enable = false
+ _, resp = th.SystemAdminClient.GetPlugins()
+ CheckNotImplementedStatus(t, resp)
+
+ *utils.Cfg.PluginSettings.Enable = true
+ _, resp = th.Client.GetPlugins()
+ CheckForbiddenStatus(t, resp)
+
+ // Successful remove
+ ok, resp := th.SystemAdminClient.RemovePlugin(manifest.Id)
+ CheckNoError(t, resp)
+
+ assert.True(t, ok)
+
+ // Remove error cases
+ ok, resp = th.SystemAdminClient.RemovePlugin(manifest.Id)
+ CheckBadRequestStatus(t, resp)
+
+ assert.False(t, ok)
+
+ *utils.Cfg.PluginSettings.Enable = false
+ _, resp = th.SystemAdminClient.RemovePlugin(manifest.Id)
+ CheckNotImplementedStatus(t, resp)
+
+ *utils.Cfg.PluginSettings.Enable = true
+ _, resp = th.Client.RemovePlugin(manifest.Id)
+ CheckForbiddenStatus(t, resp)
+
+ _, resp = th.SystemAdminClient.RemovePlugin("bad.id")
+ CheckNotFoundStatus(t, resp)
+
+ app.Srv.PluginEnv = nil
+}
diff --git a/api4/system.go b/api4/system.go
index 2ad408e13..8f98afedb 100644
--- a/api4/system.go
+++ b/api4/system.go
@@ -244,6 +244,7 @@ func getClientConfig(c *Context, w http.ResponseWriter, r *http.Request) {
}
respCfg["NoAccounts"] = strconv.FormatBool(app.IsFirstUserAccount())
+ respCfg["Plugins"] = app.GetPluginsForClientConfig()
w.Write([]byte(model.MapToJson(respCfg)))
}