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()
Этот коммит содержится в:
Joram Wilander
2017-09-01 09:00:27 -04:00
коммит произвёл GitHub
родитель 74b5e52c4e
Коммит 899ab31fff
28 изменённых файлов: 965 добавлений и 76 удалений

Просмотреть файл

@@ -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))

Просмотреть файл

@@ -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

Просмотреть файл

@@ -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
}

120
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)
}

115
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
}

Просмотреть файл

@@ -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)))
}