MM-16368 - Plugin Signing (#13017)
* [MM-18757] POST handler for `/plugins/marketplace` (#12372) * Implement installMarketplacePlugin * Add InstallMarketplacePlugin endpoint * Fix go.mod * merge with master * Fix go.mod * Fix plugin tests * Move get plugin to marketplace client * Fix stylistic concerns * Add trailing newline to the go.mod * [MM-16586] Add plugin signature settings (#12390) * MM-17149 - Extend config.json for marketplace settings (#11933) * MM-17149 - Extend config.json for marketplace settings * Renamed MarketplaceUrl, tracking default marketplace url * Added EnableMarketplace to the client config * Revert "Added EnableMarketplace to the client config" This reverts commit 0f982c4c661c2cd9bb96264e9a01a2363c40d9c5. * MM-17149 - Added EnableMarketplace to the client config (#11958) * Added EnableMarketplace to the client config * Moved EnableMarketplace setting out of limited client configuration * Add public key settings to the config.json * Rename PublicKeys to SignaturePublicKeyFiles * Change filepath.Split to Base * Remove additional prints * Force extention of a public key file * Remove config validation * Remove error on delete * Remove config cloning * Add error messages * Add plugin public key tests * Rename extension to PluginSignaturePublicKeyFileExtention * Remove EnforceVerification * Change []*PublicKeyDescription to []string * Change .asc extension to .plugin.asc * Change ordering of public methods * Change plugin key commands * Update examples in the plugin key commands * Remove forcing extention * Add verify signature in settings * Fix tabbing * Fix naming * Remove unused text * Remove unused text * Update command examples * Fix unit tests * Change errors.New to errors.Wrap * Fix verbose flag * Change .asc to .gpg * Fix } * Change AddPublicKey signature * Change public.key extension * Add plugin public key command tests * Update en.json * Bootstrap the public keys * Update en.json * Fix en.json * Fix en.json * Bootstrap hard-coded public key * Remove unused texts in en.json * Change file to name * Add license header * Update development public key * Remove writeFile method * Remove .plugin.asc extension * Rename publiKey to mattermostPublicKey * Remove init_public_keys string * GolangCI * Closing file handlers * Fixed test that was installing nps plugin * [MM-19798] Implement plugin signature verification (#12768) * MM-17149 - Extend config.json for marketplace settings (#11933) * MM-17149 - Extend config.json for marketplace settings * Renamed MarketplaceUrl, tracking default marketplace url * Added EnableMarketplace to the client config * Revert "Added EnableMarketplace to the client config" This reverts commit 0f982c4c661c2cd9bb96264e9a01a2363c40d9c5. * MM-17149 - Added EnableMarketplace to the client config (#11958) * Added EnableMarketplace to the client config * Moved EnableMarketplace setting out of limited client configuration * Add public key settings to the config.json * Rename PublicKeys to SignaturePublicKeyFiles * Change filepath.Split to Base * Remove additional prints * Force extention of a public key file * Remove config validation * Remove error on delete * Remove config cloning * Add error messages * Add plugin public key tests * Rename extension to PluginSignaturePublicKeyFileExtention * Remove EnforceVerification * Change []*PublicKeyDescription to []string * Change .asc extension to .plugin.asc * Change ordering of public methods * Change plugin key commands * Update examples in the plugin key commands * Remove forcing extention * Add verify signature in settings * Fix tabbing * Fix naming * Remove unused text * Remove unused text * Update command examples * Fix unit tests * Change errors.New to errors.Wrap * Fix verbose flag * Change .asc to .gpg * Fix } * Change AddPublicKey signature * Change public.key extension * Add plugin public key command tests * Update en.json * Bootstrap the public keys * Update en.json * Fix en.json * Fix en.json * Bootstrap hard-coded public key * Remove unused texts in en.json * Change file to name * Add license header * Implement plugin signature verification * Remove benburker openpgp * Update en.json * Update development public key * Add support of multiple signatures in filestore * Update en.json * Run go mod vendor * Fix style * Remove writeFile method * Remove .plugin.asc extension * Rename publiKey to mattermostPublicKey * Verify plugin with mattermost public key * Remove init_public_keys string * Add InstallPluginWithSignature method and Refactor * Add signature verification on claster notification * Remove armored signature headers * Add error strings * Fix en.json * Change signatureStorePath * Implement minor fixes * Refactor plugin install methods * Add installPlugin method to uploadPlugin * Update en.json * Refactor installPlugin * Limit number of signatures * Close signatures * Fix helper function * Fix fromReadCloseSeekerToReadSeeker * Cleaned up ReadCloseSeeker for signatures * Remove signature truncation on FS * GolangCI * Add tests for armored signatures and plugin uploads * Fix nil slice issue * Fix TestPluginSync * Fixed tests * Return io.ReadSeeker from downloadFromUrl * Add log for the found plugins in the file store * Remove logging plugin detection info * [MM-20134] Consume and store single-signature for each plugin (#13081) * Consume and store single-signature for each plugin * Fix en.json * Remove saveSignature method * Remove public key hash * PR Feedback * refactored config * PR feedback
Этот коммит содержится в:
109
api4/plugin.go
109
api4/plugin.go
@@ -8,6 +8,7 @@ package api4
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -31,6 +32,7 @@ func (api *API) InitPlugin() {
|
||||
api.BaseRoutes.Plugins.Handle("", api.ApiSessionRequired(getPlugins)).Methods("GET")
|
||||
api.BaseRoutes.Plugin.Handle("", api.ApiSessionRequired(removePlugin)).Methods("DELETE")
|
||||
api.BaseRoutes.Plugins.Handle("/install_from_url", api.ApiSessionRequired(installPluginFromUrl)).Methods("POST")
|
||||
api.BaseRoutes.Plugins.Handle("/marketplace", api.ApiSessionRequired(installMarketplacePlugin)).Methods("POST")
|
||||
|
||||
api.BaseRoutes.Plugins.Handle("/statuses", api.ApiSessionRequired(getPluginStatuses)).Methods("GET")
|
||||
api.BaseRoutes.Plugin.Handle("/enable", api.ApiSessionRequired(enablePlugin)).Methods("POST")
|
||||
@@ -42,7 +44,8 @@ func (api *API) InitPlugin() {
|
||||
}
|
||||
|
||||
func uploadPlugin(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !*c.App.Config().PluginSettings.Enable || !*c.App.Config().PluginSettings.EnableUploads {
|
||||
config := c.App.Config()
|
||||
if !*config.PluginSettings.Enable || !*config.PluginSettings.EnableUploads || *config.PluginSettings.RequirePluginSignature {
|
||||
c.Err = model.NewAppError("uploadPlugin", "app.plugin.upload_disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
@@ -81,19 +84,12 @@ func uploadPlugin(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if len(m.Value["force"]) > 0 && m.Value["force"][0] == "true" {
|
||||
force = true
|
||||
}
|
||||
manifest, unpackErr := c.App.InstallPlugin(file, force)
|
||||
|
||||
if unpackErr != nil {
|
||||
c.Err = unpackErr
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Write([]byte(manifest.ToJson()))
|
||||
installPlugin(c, w, file, force)
|
||||
}
|
||||
|
||||
func installPluginFromUrl(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !*c.App.Config().PluginSettings.Enable {
|
||||
if !*c.App.Config().PluginSettings.Enable || *c.App.Config().PluginSettings.RequirePluginSignature {
|
||||
c.Err = model.NewAppError("installPluginFromUrl", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
@@ -103,46 +99,57 @@ func installPluginFromUrl(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
force := r.URL.Query().Get("force") == "true"
|
||||
downloadUrl := r.URL.Query().Get("plugin_download_url")
|
||||
|
||||
if !model.IsValidHttpUrl(downloadUrl) {
|
||||
c.Err = model.NewAppError("installPluginFromUrl", "api.plugin.install.invalid_url.app_error", nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
u, err := url.ParseRequestURI(downloadUrl)
|
||||
pluginFile, err := downloadFromUrl(c, downloadUrl)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("installPluginFromUrl", "api.plugin.install.invalid_url.app_error", nil, "", http.StatusBadRequest)
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if !*c.App.Config().PluginSettings.AllowInsecureDownloadUrl && u.Scheme != "https" {
|
||||
c.Err = model.NewAppError("installPluginFromUrl", "api.plugin.install.insecure_url.app_error", nil, "", http.StatusBadRequest)
|
||||
installPlugin(c, w, pluginFile, force)
|
||||
}
|
||||
|
||||
func installMarketplacePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !*c.App.Config().PluginSettings.Enable {
|
||||
c.Err = model.NewAppError("installMarketplacePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
client := c.App.HTTPService.MakeClient(true)
|
||||
client.Timeout = INSTALL_PLUGIN_FROM_URL_HTTP_REQUEST_TIMEOUT
|
||||
if !*c.App.Config().PluginSettings.EnableMarketplace {
|
||||
c.Err = model.NewAppError("installMarketplacePlugin", "app.plugin.marketplace_disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := client.Get(downloadUrl)
|
||||
if !c.App.SessionHasPermissionTo(c.App.Session, model.PERMISSION_MANAGE_SYSTEM) {
|
||||
c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
|
||||
return
|
||||
}
|
||||
|
||||
pluginRequest, err := model.PluginRequestFromReader(r.Body)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("installPluginFromUrl", "api.plugin.install.download_failed.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
c.Err = model.NewAppError("installMarketplacePlugin", "app.plugin.marketplace_plugin_request.app_error", nil, err.Error(), http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
force := false
|
||||
if r.URL.Query().Get("force") == "true" {
|
||||
force = true
|
||||
plugin, appErr := c.App.GetMarketplacePlugin(pluginRequest)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
fileBytes, err := ioutil.ReadAll(resp.Body)
|
||||
pluginFile, appErr := downloadFromUrl(c, plugin.DownloadURL)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
signature, err := plugin.DecodeSignature()
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("installPluginFromUrl", "api.plugin.install.reading_stream_failed.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
c.Err = model.NewAppError("installMarketplacePlugin", "app.plugin.signature_decode.app_error", nil, err.Error(), http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
manifest, appErr := c.App.InstallPlugin(bytes.NewReader(fileBytes), force)
|
||||
manifest, appErr := c.App.InstallPluginWithSignature(pluginFile, signature)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
@@ -348,3 +355,43 @@ func parseMarketplacePluginFilter(u *url.URL) (*model.MarketplacePluginFilter, e
|
||||
ServerVersion: serverVersion,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func downloadFromUrl(c *Context, downloadUrl string) (io.ReadSeeker, *model.AppError) {
|
||||
if !model.IsValidHttpUrl(downloadUrl) {
|
||||
return nil, model.NewAppError("downloadFromUrl", "api.plugin.install.invalid_url.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
u, err := url.ParseRequestURI(downloadUrl)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("downloadFromUrl", "api.plugin.install.invalid_url.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
if !*c.App.Config().PluginSettings.AllowInsecureDownloadUrl && u.Scheme != "https" {
|
||||
return nil, model.NewAppError("downloadFromUrl", "api.plugin.install.insecure_url.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
client := c.App.HTTPService.MakeClient(true)
|
||||
client.Timeout = INSTALL_PLUGIN_FROM_URL_HTTP_REQUEST_TIMEOUT
|
||||
|
||||
resp, err := client.Get(downloadUrl)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("downloadFromUrl", "api.plugin.install.download_failed.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
fileBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("downloadFromUrl", "api.plugin.install.reading_stream_failed.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
return bytes.NewReader(fileBytes), nil
|
||||
}
|
||||
|
||||
func installPlugin(c *Context, w http.ResponseWriter, plugin io.ReadSeeker, force bool) {
|
||||
manifest, appErr := c.App.InstallPlugin(plugin, force)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Write([]byte(manifest.ToJson()))
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user