Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Этот коммит содержится в:
Michael Kochell
2022-05-25 12:36:08 -04:00
коммит произвёл GitHub
родитель 54a88fb532
Коммит ea5bfe3fd9
22 изменённых файлов: 634 добавлений и 22 удалений

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

@@ -68,6 +68,8 @@ type AppIface interface {
// If includeRemovedMembers is true, then channel members who left or were removed from the channel will
// be included; otherwise, they will be excluded.
ChannelMembersToAdd(since int64, channelID *string, includeRemovedMembers bool) ([]*model.UserChannelIDPair, *model.AppError)
// CheckFreemiumLimitsForConfigSave returns an error if the configuration being saved violates the Cloud Freemium limits
CheckFreemiumLimitsForConfigSave(oldConfig, newConfig *model.Config) *model.AppError
// CheckProviderAttributes returns the empty string if the patch can be applied without
// overriding attributes set by the user's login provider; otherwise, the name of the offending
// field is returned.
@@ -168,6 +170,8 @@ type AppIface interface {
GetFilteredUsersStats(options *model.UserCountOptions) (*model.UsersStats, *model.AppError)
// GetGroupsByTeam returns the paged list and the total count of group associated to the given team.
GetGroupsByTeam(teamID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, int, *model.AppError)
// GetIntegrationsUsage returns usage information on enabled integrations
GetIntegrationsUsage() (*model.IntegrationsUsage, *model.AppError)
// GetKnownUsers returns the list of user ids of users with any direct
// relationship with a user. That means any user sharing any channel, including
// direct and group channels.

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

@@ -375,6 +375,10 @@ func (w *LocalResponseWriter) WriteHeader(statusCode int) {
}
func (a *App) doPluginRequest(c *request.Context, method, rawURL string, values url.Values, body []byte) (*http.Response, *model.AppError) {
return a.ch.doPluginRequest(c, method, rawURL, values, body)
}
func (ch *Channels) doPluginRequest(c *request.Context, method, rawURL string, values url.Values, body []byte) (*http.Response, *model.AppError) {
rawURL = strings.TrimPrefix(rawURL, "/")
inURL, err := url.Parse(rawURL)
if err != nil {
@@ -423,7 +427,7 @@ func (a *App) doPluginRequest(c *request.Context, method, rawURL string, values
params["plugin_id"] = pluginID
r = mux.SetURLVars(r, params)
a.ch.ServePluginRequest(w, r)
ch.ServePluginRequest(w, r)
resp := &http.Response{
StatusCode: w.status,

110
app/integrations.go Обычный файл
Просмотреть файл

@@ -0,0 +1,110 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"sort"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
)
func (a *App) checkIntegrationLimitsForConfigSave(oldConfig, newConfig *model.Config) *model.AppError {
pluginIds := []string{}
for pluginId, newState := range newConfig.PluginSettings.PluginStates {
oldState, ok := oldConfig.PluginSettings.PluginStates[pluginId]
if newState.Enable && !(ok && oldState.Enable) {
pluginIds = append(pluginIds, pluginId)
}
}
if len(pluginIds) > 0 {
return a.checkIfIntegrationsMeetFreemiumLimits(pluginIds)
}
return nil
}
func (ch *Channels) getInstalledIntegrations() ([]*model.InstalledIntegration, *model.AppError) {
out := []*model.InstalledIntegration{}
pluginsEnvironment := ch.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return out, nil
}
plugins, err := pluginsEnvironment.Available()
if err != nil {
return nil, model.NewAppError("getInstalledIntegrations", "app.plugin.sync.read_local_folder.app_error", nil, err.Error(), 0)
}
pluginStates := ch.cfgSvc.Config().PluginSettings.PluginStates
for _, p := range plugins {
if _, ok := model.InstalledIntegrationsIgnoredPlugins[p.Manifest.Id]; !ok {
enabled := false
if state, ok := pluginStates[p.Manifest.Id]; ok {
enabled = state.Enable
}
integration := &model.InstalledIntegration{
Type: "plugin",
ID: p.Manifest.Id,
Name: p.Manifest.Name,
Version: p.Manifest.Version,
Enabled: enabled,
}
out = append(out, integration)
}
}
// Sort result alphabetically, by display name.
sort.SliceStable(out, func(i, j int) bool {
return strings.ToLower(out[i].Name) < strings.ToLower(out[j].Name)
})
return out, nil
}
func (a *App) checkIfIntegrationsMeetFreemiumLimits(originalPluginIds []string) *model.AppError {
if !a.Config().FeatureFlags.CloudFree {
return nil
}
pluginIds := map[string]bool{}
for _, pluginId := range originalPluginIds {
if _, ok := model.InstalledIntegrationsIgnoredPlugins[pluginId]; !ok {
pluginIds[pluginId] = true
}
}
limits, err := a.Cloud().GetCloudLimits("")
if err != nil {
return model.NewAppError("checkIfIntegrationMeetsFreemiumLimits", "api.cloud.request_error", nil, err.Error(), http.StatusInternalServerError)
}
if limits == nil || limits.Integrations == nil || limits.Integrations.Enabled == nil {
return nil
}
installed, appErr := a.ch.getInstalledIntegrations()
if appErr != nil {
return appErr
}
enableCount := len(pluginIds)
for _, integration := range installed {
if _, ok := pluginIds[integration.ID]; !ok && integration.Enabled {
enableCount++
}
}
limit := *limits.Integrations.Enabled
if enableCount > limit {
return model.NewAppError("checkIfIntegrationMeetsFreemiumLimits", "app.install_integration.reached_max_limit.error", map[string]interface{}{"NumIntegrations": limit}, "", http.StatusBadRequest)
}
return nil
}

74
app/integrations_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,74 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"testing"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/stretchr/testify/require"
)
func TestGetIntegrationsUsage(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
samplePluginCode := `
package main
import (
"github.com/mattermost/mattermost-server/v6/plugin"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`
setupMultiPluginAPITest(t,
[]string{samplePluginCode, samplePluginCode, samplePluginCode, samplePluginCode, samplePluginCode, samplePluginCode, samplePluginCode}, []string{
`{"id": "otherplugin", "name": "Other Plugin", "version": "1.2.0", "server": {"executable": "backend.exe"}}`,
`{"id": "mattermost-autolink", "name": "Autolink", "version": "1.2.0", "server": {"executable": "backend.exe"}}`,
`{"id": "playbooks", "name": "Playbooks", "version": "1.2.0", "server": {"executable": "backend.exe"}}`,
`{"id": "focalboard", "name": "Mattermost Boards", "version": "1.2.0", "server": {"executable": "backend.exe"}}`,
`{"id": "com.mattermost.calls", "name": "Calls", "version": "1.2.0", "server": {"executable": "backend.exe"}}`,
`{"id": "com.mattermost.nps", "name": "User Satisfaction Surveys", "version": "1.2.0", "server": {"executable": "backend.exe"}}`,
`{"id": "com.mattermost.apps", "server": {"executable": "backend.exe"}}`,
}, []string{"otherplugin", "mattermost-autolink", "playbooks", "focalboard", "com.mattermost.calls", "com.mattermost.nps", "com.mattermost.apps"},
true, th.App, th.Context)
integrations, appErr := th.App.ch.getInstalledIntegrations()
require.Nil(t, appErr)
expected := []*model.InstalledIntegration{
{
Type: "plugin",
ID: "mattermost-autolink",
Name: "Autolink",
Version: "1.2.0",
Enabled: true,
},
{
Type: "plugin",
ID: "otherplugin",
Name: "Other Plugin",
Version: "1.2.0",
Enabled: true,
},
}
require.Equal(t, expected, integrations)
usage, appErr := th.App.GetIntegrationsUsage()
require.Nil(t, appErr)
// 2 enabled integrations
expectedUsage := &model.IntegrationsUsage{
Enabled: 2,
}
require.Equal(t, expectedUsage, usage)
}

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

@@ -47,7 +47,7 @@ func (a *App) CompleteOnboarding(c *request.Context, request *model.CompleteOnbo
return
}
appErr = a.Channels().enablePlugin(id)
appErr = a.EnablePlugin(id)
if appErr != nil {
mlog.Error("Failed to enable plugin for onboarding", mlog.String("id", id), mlog.Err(appErr))
return

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

@@ -1227,6 +1227,28 @@ func (a *OpenTracingAppLayer) CheckForClientSideCert(r *http.Request) (string, s
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) CheckFreemiumLimitsForConfigSave(oldConfig *model.Config, newConfig *model.Config) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckFreemiumLimitsForConfigSave")
a.ctx = newCtx
a.app.Srv().Store.SetContext(newCtx)
defer func() {
a.app.Srv().Store.SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CheckFreemiumLimitsForConfigSave(oldConfig, newConfig)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) CheckIntegrity() <-chan model.IntegrityCheckResult {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckIntegrity")
@@ -6537,6 +6559,28 @@ func (a *OpenTracingAppLayer) GetIncomingWebhooksPageByUser(userID string, page
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetIntegrationsUsage() (*model.IntegrationsUsage, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetIntegrationsUsage")
a.ctx = newCtx
a.app.Srv().Store.SetContext(newCtx)
defer func() {
a.app.Srv().Store.SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetIntegrationsUsage()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetJob(id string) (*model.Job, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetJob")

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

@@ -156,6 +156,10 @@ func (ch *Channels) syncPluginsActiveState() {
if err := ch.notifyPluginStatusesChanged(); err != nil {
mlog.Warn("failed to notify plugin status changed", mlog.Err(err))
}
if err := ch.notifyIntegrationsUsageChanged(); err != nil {
mlog.Warn("Failed to notify integrations usage changed", mlog.Err(err))
}
}
func (a *App) NewPluginAPI(c *request.Context, manifest *model.Manifest) plugin.API {
@@ -370,6 +374,11 @@ func (a *App) GetActivePluginManifests() ([]*model.Manifest, *model.AppError) {
// activation if inactive anywhere in the cluster.
// Notifies cluster peers through config change.
func (a *App) EnablePlugin(id string) *model.AppError {
appErr := a.checkIfIntegrationsMeetFreemiumLimits([]string{id})
if appErr != nil {
return appErr
}
return a.ch.enablePlugin(id)
}
@@ -416,7 +425,12 @@ func (ch *Channels) enablePlugin(id string) *model.AppError {
// DisablePlugin will set the config for an installed plugin to disabled, triggering deactivation if active.
// Notifies cluster peers through config change.
func (a *App) DisablePlugin(id string) *model.AppError {
return a.ch.disablePlugin(id)
appErr := a.ch.disablePlugin(id)
if appErr != nil {
return appErr
}
return nil
}
func (ch *Channels) disablePlugin(id string) *model.AppError {
@@ -457,6 +471,20 @@ func (ch *Channels) disablePlugin(id string) *model.AppError {
return nil
}
func (ch *Channels) notifyIntegrationsUsageChanged() *model.AppError {
usage, appErr := ch.getIntegrationsUsage()
if appErr != nil {
return appErr
}
message := model.NewWebSocketEvent(model.WebsocketEventIntegrationsUsageChanged, "", "", "", nil)
message.Add("usage", usage)
message.GetBroadcast().ContainsSensitiveData = true
ch.Publish(message)
return nil
}
func (a *App) GetPlugins() (*model.PluginsResponse, *model.AppError) {
pluginsEnvironment := a.GetPluginsEnvironment()
if pluginsEnvironment == nil {
@@ -1064,12 +1092,12 @@ func getIcon(iconPath string) (string, error) {
func (ch *Channels) getPluginStateOverride(pluginID string) (bool, bool) {
switch pluginID {
case "com.mattermost.apps":
case model.PluginIdApps:
// Tie Apps proxy disabled status to the feature flag.
if !ch.cfgSvc.Config().FeatureFlags.AppsEnabled {
return true, false
}
case "com.mattermost.calls":
case model.PluginIdCalls:
if model.IsCloud() {
return true, ch.cfgSvc.Config().FeatureFlags.CallsEnabled
}

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

@@ -105,6 +105,10 @@ func (ch *Channels) installPluginFromData(data model.PluginEventData) {
if err := ch.notifyPluginStatusesChanged(); err != nil {
mlog.Error("Failed to notify plugin status changed", mlog.Err(err))
}
if err := ch.notifyIntegrationsUsageChanged(); err != nil {
mlog.Warn("Failed to notify integrations usage changed", mlog.Err(err))
}
}
func (ch *Channels) removePluginFromData(data model.PluginEventData) {
@@ -117,6 +121,10 @@ func (ch *Channels) removePluginFromData(data model.PluginEventData) {
if err := ch.notifyPluginStatusesChanged(); err != nil {
mlog.Warn("failed to notify plugin status changed", mlog.Err(err))
}
if err := ch.notifyIntegrationsUsageChanged(); err != nil {
mlog.Warn("Failed to notify integrations usage changed", mlog.Err(err))
}
}
// InstallPluginWithSignature verifies and installs plugin.
@@ -172,6 +180,10 @@ func (ch *Channels) installPlugin(pluginFile, signature io.ReadSeeker, installat
mlog.Warn("Failed to notify plugin status changed", mlog.Err(err))
}
if err := ch.notifyIntegrationsUsageChanged(); err != nil {
mlog.Warn("Failed to notify integrations usage changed", mlog.Err(err))
}
return manifest, nil
}
@@ -447,6 +459,10 @@ func (ch *Channels) RemovePlugin(id string) *model.AppError {
mlog.Warn("Failed to notify plugin status changed", mlog.Err(err))
}
if err := ch.notifyIntegrationsUsageChanged(); err != nil {
mlog.Warn("Failed to notify integrations usage changed", mlog.Err(err))
}
return nil
}

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

@@ -18,9 +18,11 @@ import (
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/app/request"
"github.com/mattermost/mattermost-server/v6/einterfaces/mocks"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
@@ -966,6 +968,70 @@ func TestProcessPrepackagedPlugins(t *testing.T) {
})
}
func TestEnablePluginWithCloudLimits(t *testing.T) {
th := Setup(t)
defer th.TearDown()
os.Setenv("MM_FEATUREFLAGS_CLOUDFREE", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_CLOUDFREE")
th.App.ReloadConfig()
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.Enable = true
*cfg.PluginSettings.RequirePluginSignature = false
cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: false}
cfg.PluginSettings.PluginStates["testplugin2"] = &model.PluginState{Enable: false}
})
cloud := &mocks.CloudInterface{}
cloud.Mock.On("GetCloudLimits", mock.Anything).Return(&model.ProductLimits{
Integrations: &model.IntegrationsLimits{
Enabled: model.NewInt(1),
},
}, nil)
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = cloud
env := th.App.GetPluginsEnvironment()
require.NotNil(t, env)
path, _ := fileutils.FindDir("tests")
fileReader, err := os.Open(filepath.Join(path, "testplugin.tar.gz"))
require.NoError(t, err)
defer fileReader.Close()
_, appErr := th.App.WriteFile(fileReader, getBundleStorePath("testplugin"))
checkNoError(t, appErr)
fileReader, err = os.Open(filepath.Join(path, "testplugin2.tar.gz"))
require.NoError(t, err)
defer fileReader.Close()
_, appErr = th.App.WriteFile(fileReader, getBundleStorePath("testplugin2"))
checkNoError(t, appErr)
appErr = th.App.SyncPlugins()
checkNoError(t, appErr)
appErr = th.App.EnablePlugin("testplugin")
checkNoError(t, appErr)
appErr = th.App.EnablePlugin("testplugin2")
checkError(t, appErr)
require.Equal(t, "app.install_integration.reached_max_limit.error", appErr.Id)
os.Unsetenv("MM_FEATUREFLAGS_CLOUDFREE")
th.App.ReloadConfig()
appErr = th.App.EnablePlugin("testplugin2")
checkNoError(t, appErr)
}
func TestGetPluginStateOverride(t *testing.T) {
th := Setup(t)
defer th.TearDown()

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

@@ -10,6 +10,41 @@ import (
"github.com/mattermost/mattermost-server/v6/utils"
)
// CheckFreemiumLimitsForConfigSave returns an error if the configuration being saved violates the Cloud Freemium limits
func (a *App) CheckFreemiumLimitsForConfigSave(oldConfig, newConfig *model.Config) *model.AppError {
if !a.Config().FeatureFlags.CloudFree {
return nil
}
appErr := a.checkIntegrationLimitsForConfigSave(oldConfig, newConfig)
if appErr != nil {
return appErr
}
return nil
}
// GetIntegrationsUsage returns usage information on enabled integrations
func (a *App) GetIntegrationsUsage() (*model.IntegrationsUsage, *model.AppError) {
return a.ch.getIntegrationsUsage()
}
func (ch *Channels) getIntegrationsUsage() (*model.IntegrationsUsage, *model.AppError) {
installed, appErr := ch.getInstalledIntegrations()
if appErr != nil {
return nil, appErr
}
var count = 0
for _, i := range installed {
if i.Enabled {
count++
}
}
return &model.IntegrationsUsage{Enabled: count}, nil
}
// GetPostsUsage returns "rounded off" total posts count like returns 900 instead of 987
func (a *App) GetPostsUsage() (int64, *model.AppError) {
count, err := a.Srv().Store.Post().AnalyticsPostCount(&model.PostCountOptions{ExcludeDeleted: true})

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

@@ -191,6 +191,10 @@ func (a *App) Publish(message *model.WebSocketEvent) {
a.Srv().Publish(message)
}
func (ch *Channels) Publish(message *model.WebSocketEvent) {
ch.srv.Publish(message)
}
func (s *Server) PublishSkipClusterSend(event *model.WebSocketEvent) {
if event.GetBroadcast().UserId != "" {
hub := s.GetHubForUserId(event.GetBroadcast().UserId)