Merge branch 'master' into mark-as-unread
Этот коммит содержится в:
@@ -139,7 +139,7 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod
|
||||
updateMentionChans = append(updateMentionChans, umc)
|
||||
}
|
||||
|
||||
notification := &postNotification{
|
||||
notification := &PostNotification{
|
||||
post: post,
|
||||
channel: channel,
|
||||
profileMap: profileMap,
|
||||
@@ -691,7 +691,7 @@ func addMentionKeywordsForUser(keywords map[string][]string, profile *model.User
|
||||
}
|
||||
|
||||
// Represents either an email or push notification and contains the fields required to send it to any user.
|
||||
type postNotification struct {
|
||||
type PostNotification struct {
|
||||
channel *model.Channel
|
||||
post *model.Post
|
||||
profileMap map[string]*model.User
|
||||
@@ -701,7 +701,7 @@ type postNotification struct {
|
||||
// Returns the name of the channel for this notification. For direct messages, this is the sender's name
|
||||
// preceeded by an at sign. For group messages, this is a comma-separated list of the members of the
|
||||
// channel, with an option to exclude the recipient of the message from that list.
|
||||
func (n *postNotification) GetChannelName(userNameFormat string, excludeId string) string {
|
||||
func (n *PostNotification) GetChannelName(userNameFormat, excludeId string) string {
|
||||
switch n.channel.Type {
|
||||
case model.CHANNEL_DIRECT:
|
||||
return n.sender.GetDisplayNameWithPrefix(userNameFormat, "@")
|
||||
@@ -723,7 +723,7 @@ func (n *postNotification) GetChannelName(userNameFormat string, excludeId strin
|
||||
|
||||
// Returns the name of the sender of this notification, accounting for things like system messages
|
||||
// and whether or not the username has been overridden by an integration.
|
||||
func (n *postNotification) GetSenderName(userNameFormat string, overridesAllowed bool) string {
|
||||
func (n *PostNotification) GetSenderName(userNameFormat string, overridesAllowed bool) string {
|
||||
if n.post.IsSystemMessage() {
|
||||
return utils.T("system.message.name")
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
)
|
||||
|
||||
func (a *App) sendNotificationEmail(notification *postNotification, user *model.User, team *model.Team) *model.AppError {
|
||||
func (a *App) sendNotificationEmail(notification *PostNotification, user *model.User, team *model.Team) *model.AppError {
|
||||
channel := notification.channel
|
||||
post := notification.post
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ func (a *App) sendPushNotificationToAllSessions(msg *model.PushNotification, use
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) sendPushNotification(notification *postNotification, user *model.User, explicitMention, channelWideMention bool, replyToThreadType string) {
|
||||
func (a *App) sendPushNotification(notification *PostNotification, user *model.User, explicitMention, channelWideMention bool, replyToThreadType string) {
|
||||
cfg := a.Config()
|
||||
channel := notification.channel
|
||||
post := notification.post
|
||||
@@ -134,6 +134,22 @@ func (a *App) sendPushNotification(notification *postNotification, user *model.U
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) getFetchedPushNotificationMessage(postMessage, senderName, channelType string, hasFiles bool, userLocale i18n.TranslateFunc) string {
|
||||
// If the post only has images then push an appropriate message
|
||||
if len(postMessage) == 0 && hasFiles {
|
||||
if channelType == model.CHANNEL_DIRECT {
|
||||
return strings.Trim(userLocale("api.post.send_notifications_and_forget.push_image_only"), " ")
|
||||
}
|
||||
return senderName + userLocale("api.post.send_notifications_and_forget.push_image_only")
|
||||
}
|
||||
|
||||
if channelType == model.CHANNEL_DIRECT {
|
||||
return model.ClearMentionTags(postMessage)
|
||||
}
|
||||
|
||||
return senderName + ": " + model.ClearMentionTags(postMessage)
|
||||
}
|
||||
|
||||
func (a *App) getPushNotificationMessage(postMessage string, explicitMention, channelWideMention, hasFiles bool,
|
||||
senderName, channelName, channelType, replyToThreadType string, userLocale i18n.TranslateFunc) string {
|
||||
|
||||
@@ -428,9 +444,113 @@ func DoesStatusAllowPushNotification(userNotifyProps model.StringMap, status *mo
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *App) BuildFetchedPushNotificationMessage(postId string, userId string) (model.PushNotification, *model.AppError) {
|
||||
msg := model.PushNotification{
|
||||
Type: model.PUSH_TYPE_ID_LOADED,
|
||||
Category: model.CATEGORY_CAN_REPLY,
|
||||
Version: model.PUSH_MESSAGE_V2,
|
||||
}
|
||||
|
||||
post, err := a.GetSinglePost(postId)
|
||||
if err != nil {
|
||||
return msg, err
|
||||
}
|
||||
|
||||
channel, err := a.GetChannel(post.ChannelId)
|
||||
if err != nil {
|
||||
return msg, err
|
||||
}
|
||||
|
||||
user, err := a.GetUser(userId)
|
||||
if err != nil {
|
||||
return msg, err
|
||||
}
|
||||
|
||||
sender, err := a.GetUser(post.UserId)
|
||||
if err != nil {
|
||||
return msg, err
|
||||
}
|
||||
|
||||
msg.PostId = post.Id
|
||||
msg.RootId = post.RootId
|
||||
msg.SenderId = post.UserId
|
||||
msg.ChannelId = channel.Id
|
||||
msg.TeamId = channel.TeamId
|
||||
|
||||
notification := &PostNotification{
|
||||
post: post,
|
||||
channel: channel,
|
||||
sender: sender,
|
||||
}
|
||||
|
||||
cfg := a.Config()
|
||||
nameFormat := a.GetNotificationNameFormat(user)
|
||||
channelName := notification.GetChannelName(nameFormat, user.Id)
|
||||
senderName := notification.GetSenderName(nameFormat, *cfg.ServiceSettings.EnablePostUsernameOverride)
|
||||
|
||||
msg.ChannelName = channelName
|
||||
|
||||
msg.SenderName = senderName
|
||||
if ou, ok := post.Props["override_username"].(string); ok && *cfg.ServiceSettings.EnablePostUsernameOverride {
|
||||
msg.OverrideUsername = ou
|
||||
msg.SenderName = ou
|
||||
}
|
||||
|
||||
if oi, ok := post.Props["override_icon_url"].(string); ok && *cfg.ServiceSettings.EnablePostIconOverride {
|
||||
msg.OverrideIconUrl = oi
|
||||
}
|
||||
|
||||
if fw, ok := post.Props["from_webhook"].(string); ok {
|
||||
msg.FromWebhook = fw
|
||||
}
|
||||
|
||||
userLocale := utils.GetUserTranslations(user.Locale)
|
||||
hasFiles := post.FileIds != nil && len(post.FileIds) > 0
|
||||
|
||||
msg.Message = a.getFetchedPushNotificationMessage(post.Message, msg.SenderName, channel.Type, hasFiles, userLocale)
|
||||
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func (a *App) BuildPushNotificationMessage(post *model.Post, user *model.User, channel *model.Channel, channelName string, senderName string,
|
||||
explicitMention bool, channelWideMention bool, replyToThreadType string) (*model.PushNotification, *model.AppError) {
|
||||
|
||||
var msg *model.PushNotification
|
||||
|
||||
cfg := a.Config()
|
||||
contentsConfig := *cfg.EmailSettings.PushNotificationContents
|
||||
if contentsConfig == model.ID_LOADED_NOTIFICATION {
|
||||
msg = a.buildIdLoadedPushNotificationMessage(post, user)
|
||||
} else {
|
||||
msg = a.buildFullPushNotificationMessage(post, user, channel, channelName, senderName, explicitMention, channelWideMention, replyToThreadType)
|
||||
}
|
||||
|
||||
badge, err := a.getPushNotificationBadge(user, channel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
msg.Badge = badge
|
||||
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func (a *App) buildIdLoadedPushNotificationMessage(post *model.Post, user *model.User) *model.PushNotification {
|
||||
userLocale := utils.GetUserTranslations(user.Locale)
|
||||
msg := &model.PushNotification{
|
||||
PostId: post.Id,
|
||||
ChannelId: post.ChannelId,
|
||||
Category: model.CATEGORY_CAN_REPLY,
|
||||
Version: model.PUSH_MESSAGE_V2,
|
||||
Type: model.PUSH_TYPE_ID_LOADED,
|
||||
Message: userLocale("api.push_notification.id_loaded.default_message"),
|
||||
}
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
func (a *App) buildFullPushNotificationMessage(post *model.Post, user *model.User, channel *model.Channel, channelName string, senderName string,
|
||||
explicitMention bool, channelWideMention bool, replyToThreadType string) *model.PushNotification {
|
||||
|
||||
msg := &model.PushNotification{
|
||||
Category: model.CATEGORY_CAN_REPLY,
|
||||
Version: model.PUSH_MESSAGE_V2,
|
||||
@@ -442,22 +562,6 @@ func (a *App) BuildPushNotificationMessage(post *model.Post, user *model.User, c
|
||||
SenderId: post.UserId,
|
||||
}
|
||||
|
||||
if user.NotifyProps["push"] == "all" {
|
||||
unreadCount, err := a.Srv.Store.User().GetAnyUnreadPostCountForChannel(user.Id, channel.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msg.Badge = int(unreadCount)
|
||||
} else {
|
||||
unreadCount, err := a.Srv.Store.User().GetUnreadCount(user.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msg.Badge = int(unreadCount)
|
||||
}
|
||||
|
||||
cfg := a.Config()
|
||||
contentsConfig := *cfg.EmailSettings.PushNotificationContents
|
||||
if contentsConfig != model.GENERIC_NO_CHANNEL_NOTIFICATION || channel.Type == model.CHANNEL_DIRECT {
|
||||
@@ -483,5 +587,18 @@ func (a *App) BuildPushNotificationMessage(post *model.Post, user *model.User, c
|
||||
|
||||
msg.Message = a.getPushNotificationMessage(post.Message, explicitMention, channelWideMention, hasFiles, msg.SenderName, channelName, channel.Type, replyToThreadType, userLocale)
|
||||
|
||||
return msg, nil
|
||||
return msg
|
||||
}
|
||||
|
||||
func (a *App) getPushNotificationBadge(user *model.User, channel *model.Channel) (int, *model.AppError) {
|
||||
var unreadCount int64
|
||||
var err *model.AppError
|
||||
|
||||
if user.NotifyProps["push"] == "all" {
|
||||
unreadCount, err = a.Srv.Store.User().GetAnyUnreadPostCountForChannel(user.Id, channel.Id)
|
||||
} else {
|
||||
unreadCount, err = a.Srv.Store.User().GetUnreadCount(user.Id)
|
||||
}
|
||||
|
||||
return int(unreadCount), err
|
||||
}
|
||||
|
||||
@@ -899,7 +899,7 @@ func TestGetPushNotificationMessage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPushNotificationMessage(t *testing.T) {
|
||||
func TestBuildPushNotificationMessageMentions(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
@@ -949,3 +949,67 @@ func TestBuildPushNotificationMessage(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPushNotificationMessageContents(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
team := th.CreateTeam()
|
||||
sender := th.CreateUser()
|
||||
receiver := th.CreateUser()
|
||||
th.LinkUserToTeam(sender, team)
|
||||
th.LinkUserToTeam(receiver, team)
|
||||
channel := th.CreateChannel(team)
|
||||
th.AddUserToChannel(sender, channel)
|
||||
th.AddUserToChannel(receiver, channel)
|
||||
|
||||
post := th.CreatePost(channel)
|
||||
|
||||
explicitMention := false
|
||||
channelWideMention := false
|
||||
replyToThreadType := ""
|
||||
|
||||
receiverLocale := utils.GetUserTranslations(receiver.Locale)
|
||||
|
||||
for name, tc := range map[string]struct {
|
||||
contentsConfig string
|
||||
expectedMsg *model.PushNotification
|
||||
}{
|
||||
"only post ID, channel ID, and message included in push notification": {
|
||||
contentsConfig: model.ID_LOADED_NOTIFICATION,
|
||||
expectedMsg: &model.PushNotification{
|
||||
PostId: post.Id,
|
||||
ChannelId: post.ChannelId,
|
||||
Category: model.CATEGORY_CAN_REPLY,
|
||||
Version: model.PUSH_MESSAGE_V2,
|
||||
Type: model.PUSH_TYPE_ID_LOADED,
|
||||
Message: receiverLocale("api.push_notification.id_loaded.default_message"),
|
||||
},
|
||||
},
|
||||
"full contents included in push notification": {
|
||||
contentsConfig: model.GENERIC_NOTIFICATION,
|
||||
expectedMsg: &model.PushNotification{
|
||||
Category: model.CATEGORY_CAN_REPLY,
|
||||
Version: model.PUSH_MESSAGE_V2,
|
||||
Type: model.PUSH_TYPE_MESSAGE,
|
||||
PostId: post.Id,
|
||||
TeamId: channel.TeamId,
|
||||
ChannelId: channel.Id,
|
||||
ChannelName: channel.Name,
|
||||
RootId: post.RootId,
|
||||
SenderId: post.UserId,
|
||||
SenderName: sender.Username,
|
||||
Message: fmt.Sprintf("%s posted a message.", sender.Username),
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.EmailSettings.PushNotificationContents = tc.contentsConfig })
|
||||
|
||||
msg, err := th.App.BuildPushNotificationMessage(post, receiver, channel, channel.Name, sender.Username, explicitMention, channelWideMention, replyToThreadType)
|
||||
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, tc.expectedMsg, msg)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1540,7 +1540,7 @@ func TestPostNotificationGetChannelName(t *testing.T) {
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
notification := &postNotification{
|
||||
notification := &PostNotification{
|
||||
channel: testCase.channel,
|
||||
sender: sender,
|
||||
profileMap: profileMap,
|
||||
@@ -1625,7 +1625,7 @@ func TestPostNotificationGetSenderName(t *testing.T) {
|
||||
post = testCase.post
|
||||
}
|
||||
|
||||
notification := &postNotification{
|
||||
notification := &PostNotification{
|
||||
channel: channel,
|
||||
post: post,
|
||||
sender: sender,
|
||||
|
||||
108
app/plugin.go
108
app/plugin.go
@@ -19,6 +19,12 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type pluginSignaturePath struct {
|
||||
pluginId string
|
||||
path string
|
||||
signaturePath string
|
||||
}
|
||||
|
||||
// GetPluginsEnvironment returns the plugin environment for use if plugins are enabled and
|
||||
// initialized.
|
||||
//
|
||||
@@ -164,10 +170,18 @@ func (a *App) InitPlugins(pluginDir, webappPluginDir string) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if fileReader, err := os.Open(walkPath); err != nil {
|
||||
fileReader, err := os.Open(walkPath)
|
||||
if err != nil {
|
||||
mlog.Error("Failed to open prepackaged plugin", mlog.Err(err), mlog.String("path", walkPath))
|
||||
} else if _, err := a.installPluginLocally(fileReader, true); err != nil {
|
||||
mlog.Error("Failed to unpack prepackaged plugin", mlog.Err(err), mlog.String("path", walkPath))
|
||||
return nil
|
||||
}
|
||||
defer fileReader.Close()
|
||||
|
||||
mlog.Debug("Installing prepackaged plugin", mlog.String("path", walkPath))
|
||||
|
||||
_, appErr := a.installPluginLocally(fileReader, nil, installPluginLocallyOnlyIfNewOrUpgrade)
|
||||
if appErr != nil {
|
||||
mlog.Error("Failed to unpack prepackaged plugin", mlog.Err(appErr), mlog.String("path", walkPath))
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -226,35 +240,33 @@ func (a *App) SyncPlugins() *model.AppError {
|
||||
}
|
||||
|
||||
// Install plugins from the file store.
|
||||
fileStorePaths, appErr := a.ListDirectory(fileStorePluginFolder)
|
||||
pluginSignaturePathMap, appErr := a.getPluginsFromFolder()
|
||||
if appErr != nil {
|
||||
return model.NewAppError("SyncPlugins", "app.plugin.sync.list_filestore.app_error", nil, appErr.Error(), http.StatusInternalServerError)
|
||||
return appErr
|
||||
}
|
||||
if len(fileStorePaths) == 0 {
|
||||
mlog.Info("Found no files in plugins file store")
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, path := range fileStorePaths {
|
||||
if !strings.HasSuffix(path, ".tar.gz") {
|
||||
mlog.Warn("Ignoring non-plugin in file store", mlog.String("bundle", path))
|
||||
continue
|
||||
}
|
||||
|
||||
var reader filesstore.ReadCloseSeeker
|
||||
reader, appErr = a.FileReader(path)
|
||||
for _, plugin := range pluginSignaturePathMap {
|
||||
reader, appErr := a.FileReader(plugin.path)
|
||||
if appErr != nil {
|
||||
mlog.Error("Failed to open plugin bundle from file store.", mlog.String("bundle", path), mlog.Err(appErr))
|
||||
mlog.Error("Failed to open plugin bundle from file store.", mlog.String("bundle", plugin.path), mlog.Err(appErr))
|
||||
continue
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
mlog.Info("Syncing plugin from file store", mlog.String("bundle", path))
|
||||
if _, err := a.installPluginLocally(reader, true); err != nil {
|
||||
mlog.Error("Failed to sync plugin from file store", mlog.String("bundle", path), mlog.Err(err))
|
||||
var signature filesstore.ReadCloseSeeker
|
||||
if *a.Config().PluginSettings.RequirePluginSignature {
|
||||
signature, appErr = a.FileReader(plugin.signaturePath)
|
||||
if appErr != nil {
|
||||
mlog.Error("Failed to open plugin signature from file store.", mlog.Err(appErr))
|
||||
continue
|
||||
}
|
||||
defer signature.Close()
|
||||
}
|
||||
|
||||
mlog.Info("Syncing plugin from file store", mlog.String("bundle", plugin.path))
|
||||
if _, err := a.installPluginLocally(reader, signature, installPluginLocallyAlways); err != nil {
|
||||
mlog.Error("Failed to sync plugin from file store", mlog.String("bundle", plugin.path), mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -364,7 +376,7 @@ func (a *App) DisablePlugin(id string) *model.AppError {
|
||||
}
|
||||
|
||||
if manifest == nil {
|
||||
return model.NewAppError("DisablePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest)
|
||||
return model.NewAppError("DisablePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusNotFound)
|
||||
}
|
||||
|
||||
a.UpdateConfig(func(cfg *model.Config) {
|
||||
@@ -410,6 +422,24 @@ func (a *App) GetPlugins() (*model.PluginsResponse, *model.AppError) {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetMarketplacePlugin returns plugin from marketplace-server
|
||||
func (a *App) GetMarketplacePlugin(request *model.InstallMarketplacePluginRequest) (*model.BaseMarketplacePlugin, *model.AppError) {
|
||||
marketplaceClient, err := marketplace.NewClient(
|
||||
*a.Config().PluginSettings.MarketplaceUrl,
|
||||
a.HTTPService,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetMarketplacePlugin", "app.plugin.marketplace_client.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
filter := &model.MarketplacePluginFilter{Filter: request.Id}
|
||||
plugin, err := marketplaceClient.GetPlugin(filter, request.Version)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetMarketplacePlugin", "app.plugin.marketplace_plugins.not_found.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return plugin, nil
|
||||
}
|
||||
|
||||
// GetMarketplacePlugins returns a list of plugins from the marketplace-server,
|
||||
// and plugins that are installed locally.
|
||||
func (a *App) GetMarketplacePlugins(filter *model.MarketplacePluginFilter) ([]*model.MarketplacePlugin, *model.AppError) {
|
||||
@@ -557,3 +587,33 @@ func (a *App) notifyPluginEnabled(manifest *model.Manifest) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) getPluginsFromFolder() (map[string]*pluginSignaturePath, *model.AppError) {
|
||||
fileStorePaths, appErr := a.ListDirectory(fileStorePluginFolder)
|
||||
if appErr != nil {
|
||||
return nil, model.NewAppError("getPluginsFromDir", "app.plugin.sync.list_filestore.app_error", nil, appErr.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
pluginSignaturePathMap := make(map[string]*pluginSignaturePath)
|
||||
for _, path := range fileStorePaths {
|
||||
if strings.HasSuffix(path, ".tar.gz") {
|
||||
id := strings.TrimSuffix(filepath.Base(path), ".tar.gz")
|
||||
helper := &pluginSignaturePath{
|
||||
pluginId: id,
|
||||
path: path,
|
||||
signaturePath: "",
|
||||
}
|
||||
pluginSignaturePathMap[id] = helper
|
||||
}
|
||||
}
|
||||
for _, path := range fileStorePaths {
|
||||
if strings.HasSuffix(path, ".sig") {
|
||||
id := strings.TrimSuffix(filepath.Base(path), ".sig")
|
||||
if val, ok := pluginSignaturePathMap[id]; !ok {
|
||||
mlog.Error("Unknown signature", mlog.String("path", path))
|
||||
} else {
|
||||
val.signaturePath = path
|
||||
}
|
||||
}
|
||||
}
|
||||
return pluginSignaturePathMap, nil
|
||||
}
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
// Finally, in addition to managed plugins, note that there are unmanaged and prepackaged plugins.
|
||||
// Unmanaged plugins are plugins installed manually to the configured local directory (PluginSettings.Directory).
|
||||
// Prepackaged plugins are included with the server. They otherwise follow the above flow, except do not get uploaded
|
||||
// to the filestore. Prepackaged plugins override all other plugins with the same plugin id. Managed plugins
|
||||
// override unmanaged plugins with the same plugin id.
|
||||
// to the filestore. Prepackaged plugins override all other plugins with the same plugin id, but only when the prepackaged
|
||||
// plugin is newer. Managed plugins unconditionally override unmanaged plugins with the same plugin id.
|
||||
//
|
||||
package app
|
||||
|
||||
@@ -44,9 +44,11 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/services/filesstore"
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
)
|
||||
|
||||
@@ -60,16 +62,38 @@ const fileStorePluginFolder = "plugins"
|
||||
func (a *App) InstallPluginFromData(data model.PluginEventData) {
|
||||
mlog.Debug("Installing plugin as per cluster message", mlog.String("plugin_id", data.Id))
|
||||
|
||||
fileStorePath := a.getBundleStorePath(data.Id)
|
||||
reader, appErr := a.FileReader(fileStorePath)
|
||||
pluginSignaturePathMap, appErr := a.getPluginsFromFolder()
|
||||
if appErr != nil {
|
||||
mlog.Error("Failed to open plugin bundle from filestore.", mlog.String("path", fileStorePath), mlog.Err(appErr))
|
||||
mlog.Error("Failed to get plugin signatures from filestore. Can't install plugin from data.", mlog.Err(appErr))
|
||||
return
|
||||
}
|
||||
plugin, ok := pluginSignaturePathMap[data.Id]
|
||||
if !ok {
|
||||
mlog.Error("Failed to get plugin signature from filestore. Can't install plugin from data.", mlog.String("plugin id", data.Id))
|
||||
return
|
||||
}
|
||||
|
||||
reader, appErr := a.FileReader(plugin.path)
|
||||
if appErr != nil {
|
||||
mlog.Error("Failed to open plugin bundle from file store.", mlog.String("bundle", plugin.path), mlog.Err(appErr))
|
||||
return
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
manifest, appErr := a.installPluginLocally(reader, true)
|
||||
var signature filesstore.ReadCloseSeeker
|
||||
if *a.Config().PluginSettings.RequirePluginSignature {
|
||||
signature, appErr = a.FileReader(plugin.signaturePath)
|
||||
if appErr != nil {
|
||||
mlog.Error("Failed to open plugin signature from file store.", mlog.Err(appErr))
|
||||
return
|
||||
}
|
||||
defer signature.Close()
|
||||
}
|
||||
|
||||
manifest, appErr := a.installPluginLocally(reader, signature, installPluginLocallyAlways)
|
||||
if appErr != nil {
|
||||
mlog.Error("Failed to unpack plugin from filestore", mlog.Err(appErr), mlog.String("path", fileStorePath))
|
||||
mlog.Error("Failed to sync plugin from file store", mlog.String("bundle", plugin.path), mlog.Err(appErr))
|
||||
return
|
||||
}
|
||||
|
||||
if err := a.notifyPluginEnabled(manifest); err != nil {
|
||||
@@ -93,20 +117,36 @@ func (a *App) RemovePluginFromData(data model.PluginEventData) {
|
||||
}
|
||||
}
|
||||
|
||||
// InstallPlugin unpacks and installs a plugin but does not enable or activate it.
|
||||
func (a *App) InstallPlugin(pluginFile io.ReadSeeker, replace bool) (*model.Manifest, *model.AppError) {
|
||||
return a.installPlugin(pluginFile, replace)
|
||||
// InstallPluginWithSignature verifies and installs plugin.
|
||||
func (a *App) InstallPluginWithSignature(pluginFile, signature io.ReadSeeker) (*model.Manifest, *model.AppError) {
|
||||
return a.installPlugin(pluginFile, signature, installPluginLocallyAlways)
|
||||
}
|
||||
|
||||
func (a *App) installPlugin(pluginFile io.ReadSeeker, replace bool) (*model.Manifest, *model.AppError) {
|
||||
manifest, appErr := a.installPluginLocally(pluginFile, replace)
|
||||
// InstallPlugin unpacks and installs a plugin but does not enable or activate it.
|
||||
func (a *App) InstallPlugin(pluginFile io.ReadSeeker, replace bool) (*model.Manifest, *model.AppError) {
|
||||
installationStrategy := installPluginLocallyOnlyIfNew
|
||||
if replace {
|
||||
installationStrategy = installPluginLocallyAlways
|
||||
}
|
||||
|
||||
return a.installPlugin(pluginFile, nil, installationStrategy)
|
||||
}
|
||||
|
||||
func (a *App) installPlugin(pluginFile, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) {
|
||||
manifest, appErr := a.installPluginLocally(pluginFile, signature, installationStrategy)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
if signature != nil {
|
||||
signature.Seek(0, 0)
|
||||
if _, appErr = a.WriteFile(signature, a.getSignatureStorePath(manifest.Id)); appErr != nil {
|
||||
return nil, model.NewAppError("saveSignature", "app.plugin.store_signature.app_error", nil, appErr.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// Store bundle in the file store to allow access from other servers.
|
||||
pluginFile.Seek(0, 0)
|
||||
|
||||
if _, appErr := a.WriteFile(pluginFile, a.getBundleStorePath(manifest.Id)); appErr != nil {
|
||||
return nil, model.NewAppError("uploadPlugin", "app.plugin.store_bundle.app_error", nil, appErr.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
@@ -129,11 +169,28 @@ func (a *App) installPlugin(pluginFile io.ReadSeeker, replace bool) (*model.Mani
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
func (a *App) installPluginLocally(pluginFile io.ReadSeeker, replace bool) (*model.Manifest, *model.AppError) {
|
||||
type pluginInstallationStrategy int
|
||||
|
||||
const (
|
||||
// installPluginLocallyOnlyIfNew installs the given plugin locally only if no plugin with the same id has been unpacked.
|
||||
installPluginLocallyOnlyIfNew pluginInstallationStrategy = iota
|
||||
// installPluginLocallyOnlyIfNewOrUpgrade installs the given plugin locally only if no plugin with the same id has been unpacked, or if such a plugin is older.
|
||||
installPluginLocallyOnlyIfNewOrUpgrade
|
||||
// installPluginLocallyAlways unconditionally installs the given plugin locally only, clobbering any existing plugin with the same id.
|
||||
installPluginLocallyAlways
|
||||
)
|
||||
|
||||
func (a *App) installPluginLocally(pluginFile, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) {
|
||||
pluginsEnvironment := a.GetPluginsEnvironment()
|
||||
if pluginsEnvironment == nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
// verify signature
|
||||
if signature != nil {
|
||||
if err := a.VerifyPlugin(pluginFile, signature); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
tmpDir, err := ioutil.TempDir("", "plugintmp")
|
||||
if err != nil {
|
||||
@@ -141,6 +198,7 @@ func (a *App) installPluginLocally(pluginFile io.ReadSeeker, replace bool) (*mod
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
pluginFile.Seek(0, 0)
|
||||
if err = utils.ExtractTarGz(pluginFile, tmpDir); err != nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.extract.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
@@ -169,16 +227,45 @@ func (a *App) installPluginLocally(pluginFile io.ReadSeeker, replace bool) (*mod
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.install.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Check that there is no plugin with the same ID
|
||||
// Check for plugins installed with the same ID.
|
||||
var existingManifest *model.Manifest
|
||||
for _, bundle := range bundles {
|
||||
if bundle.Manifest != nil && bundle.Manifest.Id == manifest.Id {
|
||||
if !replace {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.install_id.app_error", nil, "", http.StatusBadRequest)
|
||||
existingManifest = bundle.Manifest
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if existingManifest != nil {
|
||||
// Return an error if already installed and strategy disallows installation.
|
||||
if installationStrategy == installPluginLocallyOnlyIfNew {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.install_id.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// Skip installation if already installed and newer.
|
||||
if installationStrategy == installPluginLocallyOnlyIfNewOrUpgrade {
|
||||
var version, existingVersion semver.Version
|
||||
|
||||
version, err = semver.Parse(manifest.Version)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if err := a.removePluginLocally(manifest.Id); err != nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.install_id_failed_remove.app_error", nil, "", http.StatusBadRequest)
|
||||
existingVersion, err = semver.Parse(existingManifest.Version)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if version.LTE(existingVersion) {
|
||||
mlog.Debug("Skipping local installation of plugin since existing version is newer", mlog.String("plugin_id", manifest.Id))
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise remove the existing installation prior to install below.
|
||||
mlog.Debug("Removing existing installation of plugin before local install", mlog.String("plugin_id", existingManifest.Id), mlog.String("version", existingManifest.Version))
|
||||
if err := a.removePluginLocally(existingManifest.Id); err != nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.install_id_failed_remove.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,9 +327,12 @@ func (a *App) removePlugin(id string) *model.AppError {
|
||||
if !bundleExist {
|
||||
return nil
|
||||
}
|
||||
if err := a.RemoveFile(storePluginFileName); err != nil {
|
||||
if err = a.RemoveFile(storePluginFileName); err != nil {
|
||||
return model.NewAppError("removePlugin", "app.plugin.remove_bundle.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
if err = a.removeSignature(id); err != nil {
|
||||
mlog.Error("Can't remove signature", mlog.Err(err))
|
||||
}
|
||||
|
||||
a.notifyClusterPluginEvent(
|
||||
model.CLUSTER_EVENT_REMOVE_PLUGIN,
|
||||
@@ -280,7 +370,7 @@ func (a *App) removePluginLocally(id string) *model.AppError {
|
||||
}
|
||||
|
||||
if manifest == nil {
|
||||
return model.NewAppError("removePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest)
|
||||
return model.NewAppError("removePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusNotFound)
|
||||
}
|
||||
|
||||
pluginsEnvironment.Deactivate(id)
|
||||
@@ -294,6 +384,26 @@ func (a *App) removePluginLocally(id string) *model.AppError {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) removeSignature(pluginId string) *model.AppError {
|
||||
filePath := a.getSignatureStorePath(pluginId)
|
||||
exists, err := a.FileExists(filePath)
|
||||
if err != nil {
|
||||
return model.NewAppError("removeSignature", "app.plugin.remove_bundle.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
if !exists {
|
||||
mlog.Debug("no plugin signature to remove", mlog.String("plugin_id", pluginId))
|
||||
return nil
|
||||
}
|
||||
if err = a.RemoveFile(filePath); err != nil {
|
||||
return model.NewAppError("removeSignature", "app.plugin.remove_bundle.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) getBundleStorePath(id string) string {
|
||||
return filepath.Join(fileStorePluginFolder, fmt.Sprintf("%s.tar.gz", id))
|
||||
}
|
||||
|
||||
func (a *App) getSignatureStorePath(id string) string {
|
||||
return filepath.Join(fileStorePluginFolder, fmt.Sprintf("%s.sig", id))
|
||||
}
|
||||
|
||||
258
app/plugin_install_test.go
Обычный файл
258
app/plugin_install_test.go
Обычный файл
@@ -0,0 +1,258 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type nilReadSeeker struct {
|
||||
}
|
||||
|
||||
func (r *nilReadSeeker) Read(p []byte) (int, error) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
func (r *nilReadSeeker) Seek(offset int64, whence int) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
type testFile struct {
|
||||
Name, Body string
|
||||
}
|
||||
|
||||
func makeInMemoryGzipTarFile(t *testing.T, files []testFile) *bytes.Reader {
|
||||
var buf bytes.Buffer
|
||||
gzWriter := gzip.NewWriter(&buf)
|
||||
|
||||
tgz := tar.NewWriter(gzWriter)
|
||||
|
||||
for _, file := range files {
|
||||
hdr := &tar.Header{
|
||||
Name: file.Name,
|
||||
Mode: 0600,
|
||||
Size: int64(len(file.Body)),
|
||||
}
|
||||
err := tgz.WriteHeader(hdr)
|
||||
require.NoError(t, err, "failed to write %s to in-memory tar file", file.Name)
|
||||
_, err = tgz.Write([]byte(file.Body))
|
||||
require.NoError(t, err, "failed to write body of %s to in-memory tar file", file.Name)
|
||||
}
|
||||
err := tgz.Close()
|
||||
require.NoError(t, err, "failed to close in-memory tar file")
|
||||
|
||||
err = gzWriter.Close()
|
||||
require.NoError(t, err, "failed to close in-memory tar.gz file")
|
||||
|
||||
return bytes.NewReader(buf.Bytes())
|
||||
}
|
||||
|
||||
type byBundleInfoId []*model.BundleInfo
|
||||
|
||||
func (b byBundleInfoId) Len() int { return len(b) }
|
||||
func (b byBundleInfoId) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
|
||||
func (b byBundleInfoId) Less(i, j int) bool { return b[i].Manifest.Id < b[j].Manifest.Id }
|
||||
|
||||
func TestInstallPluginLocally(t *testing.T) {
|
||||
t.Run("invalid tar", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
actualManifest, appErr := th.App.installPluginLocally(&nilReadSeeker{}, nil, installPluginLocallyOnlyIfNew)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "app.plugin.extract.app_error", appErr.Id, appErr.Error())
|
||||
require.Nil(t, actualManifest)
|
||||
})
|
||||
|
||||
t.Run("missing manifest", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
reader := makeInMemoryGzipTarFile(t, []testFile{
|
||||
{"test", "test file"},
|
||||
})
|
||||
|
||||
actualManifest, appErr := th.App.installPluginLocally(reader, nil, installPluginLocallyOnlyIfNew)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "app.plugin.manifest.app_error", appErr.Id, appErr.Error())
|
||||
require.Nil(t, actualManifest)
|
||||
})
|
||||
|
||||
installPlugin := func(t *testing.T, th *TestHelper, id, version string, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) {
|
||||
t.Helper()
|
||||
|
||||
manifest := &model.Manifest{
|
||||
Id: id,
|
||||
Version: version,
|
||||
}
|
||||
reader := makeInMemoryGzipTarFile(t, []testFile{
|
||||
{"plugin.json", manifest.ToJson()},
|
||||
})
|
||||
|
||||
actualManifest, appError := th.App.installPluginLocally(reader, nil, installationStrategy)
|
||||
if actualManifest != nil {
|
||||
require.Equal(t, manifest, actualManifest)
|
||||
}
|
||||
|
||||
return actualManifest, appError
|
||||
}
|
||||
|
||||
t.Run("invalid plugin id", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
actualManifest, appErr := installPlugin(t, th, "invalid#plugin#id", "version", installPluginLocallyOnlyIfNew)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "app.plugin.invalid_id.app_error", appErr.Id, appErr.Error())
|
||||
require.Nil(t, actualManifest)
|
||||
})
|
||||
|
||||
// The following tests fail mysteriously on CI due to an unexpected bundle being present.
|
||||
// This exists to clean up manually until we figure out what test isn't cleaning up after
|
||||
// itself.
|
||||
cleanExistingBundles := func(t *testing.T, th *TestHelper) {
|
||||
pluginsEnvironment := th.App.GetPluginsEnvironment()
|
||||
require.NotNil(t, pluginsEnvironment)
|
||||
bundleInfos, err := pluginsEnvironment.Available()
|
||||
require.Nil(t, err)
|
||||
|
||||
for _, bundleInfo := range bundleInfos {
|
||||
err := th.App.removePluginLocally(bundleInfo.Manifest.Id)
|
||||
require.Nilf(t, err, "failed to remove existing plugin %s", bundleInfo.Manifest.Id)
|
||||
}
|
||||
}
|
||||
|
||||
assertBundleInfoManifests := func(t *testing.T, th *TestHelper, manifests []*model.Manifest) {
|
||||
pluginsEnvironment := th.App.GetPluginsEnvironment()
|
||||
require.NotNil(t, pluginsEnvironment)
|
||||
bundleInfos, err := pluginsEnvironment.Available()
|
||||
require.Nil(t, err)
|
||||
|
||||
sort.Sort(byBundleInfoId(bundleInfos))
|
||||
|
||||
actualManifests := make([]*model.Manifest, 0, len(bundleInfos))
|
||||
for _, bundleInfo := range bundleInfos {
|
||||
actualManifests = append(actualManifests, bundleInfo.Manifest)
|
||||
}
|
||||
|
||||
require.Equal(t, manifests, actualManifests)
|
||||
}
|
||||
|
||||
t.Run("no plugins already installed", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
cleanExistingBundles(t, th)
|
||||
|
||||
manifest, appErr := installPlugin(t, th, "valid", "0.0.1", installPluginLocallyOnlyIfNew)
|
||||
require.Nil(t, appErr)
|
||||
require.NotNil(t, manifest)
|
||||
|
||||
assertBundleInfoManifests(t, th, []*model.Manifest{manifest})
|
||||
})
|
||||
|
||||
t.Run("different plugin already installed", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
cleanExistingBundles(t, th)
|
||||
|
||||
otherManifest, appErr := installPlugin(t, th, "other", "0.0.1", installPluginLocallyOnlyIfNew)
|
||||
require.Nil(t, appErr)
|
||||
require.NotNil(t, otherManifest)
|
||||
|
||||
manifest, appErr := installPlugin(t, th, "valid", "0.0.1", installPluginLocallyOnlyIfNew)
|
||||
require.Nil(t, appErr)
|
||||
require.NotNil(t, manifest)
|
||||
|
||||
assertBundleInfoManifests(t, th, []*model.Manifest{otherManifest, manifest})
|
||||
})
|
||||
|
||||
t.Run("same plugin already installed", func(t *testing.T) {
|
||||
t.Run("install only if new", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
cleanExistingBundles(t, th)
|
||||
|
||||
existingManifest, appErr := installPlugin(t, th, "valid", "0.0.1", installPluginLocallyOnlyIfNew)
|
||||
require.Nil(t, appErr)
|
||||
require.NotNil(t, existingManifest)
|
||||
|
||||
manifest, appErr := installPlugin(t, th, "valid", "0.0.1", installPluginLocallyOnlyIfNew)
|
||||
require.NotNil(t, appErr)
|
||||
require.Equal(t, "app.plugin.install_id.app_error", appErr.Id, appErr.Error())
|
||||
require.Nil(t, manifest)
|
||||
|
||||
assertBundleInfoManifests(t, th, []*model.Manifest{existingManifest})
|
||||
})
|
||||
|
||||
t.Run("install if upgrade, but older", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
cleanExistingBundles(t, th)
|
||||
|
||||
existingManifest, appErr := installPlugin(t, th, "valid", "0.0.2", installPluginLocallyOnlyIfNewOrUpgrade)
|
||||
require.Nil(t, appErr)
|
||||
require.NotNil(t, existingManifest)
|
||||
|
||||
manifest, appErr := installPlugin(t, th, "valid", "0.0.1", installPluginLocallyOnlyIfNewOrUpgrade)
|
||||
require.Nil(t, appErr)
|
||||
require.Nil(t, manifest)
|
||||
|
||||
assertBundleInfoManifests(t, th, []*model.Manifest{existingManifest})
|
||||
})
|
||||
|
||||
t.Run("install if upgrade, but same version", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
cleanExistingBundles(t, th)
|
||||
|
||||
existingManifest, appErr := installPlugin(t, th, "valid", "0.0.2", installPluginLocallyOnlyIfNewOrUpgrade)
|
||||
require.Nil(t, appErr)
|
||||
require.NotNil(t, existingManifest)
|
||||
|
||||
manifest, appErr := installPlugin(t, th, "valid", "0.0.2", installPluginLocallyOnlyIfNewOrUpgrade)
|
||||
require.Nil(t, appErr)
|
||||
require.Nil(t, manifest)
|
||||
|
||||
assertBundleInfoManifests(t, th, []*model.Manifest{existingManifest})
|
||||
})
|
||||
|
||||
t.Run("install if upgrade, newer version", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
cleanExistingBundles(t, th)
|
||||
|
||||
existingManifest, appErr := installPlugin(t, th, "valid", "0.0.2", installPluginLocallyOnlyIfNewOrUpgrade)
|
||||
require.Nil(t, appErr)
|
||||
require.NotNil(t, existingManifest)
|
||||
|
||||
manifest, appErr := installPlugin(t, th, "valid", "0.0.3", installPluginLocallyOnlyIfNewOrUpgrade)
|
||||
require.Nil(t, appErr)
|
||||
require.NotNil(t, manifest)
|
||||
|
||||
assertBundleInfoManifests(t, th, []*model.Manifest{manifest})
|
||||
})
|
||||
|
||||
t.Run("install always, old version", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
cleanExistingBundles(t, th)
|
||||
|
||||
existingManifest, appErr := installPlugin(t, th, "valid", "0.0.2", installPluginLocallyAlways)
|
||||
require.Nil(t, appErr)
|
||||
require.NotNil(t, existingManifest)
|
||||
|
||||
manifest, appErr := installPlugin(t, th, "valid", "0.0.1", installPluginLocallyAlways)
|
||||
require.Nil(t, appErr)
|
||||
require.NotNil(t, manifest)
|
||||
|
||||
assertBundleInfoManifests(t, th, []*model.Manifest{manifest})
|
||||
})
|
||||
})
|
||||
}
|
||||
46
app/plugin_public_keys.go
Обычный файл
46
app/plugin_public_keys.go
Обычный файл
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
var mattermostPluginPublicKey []byte = []byte(`-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
=qIjw
|
||||
-----END PGP PUBLIC KEY BLOCK-----`)
|
||||
132
app/plugin_signature.go
Обычный файл
132
app/plugin_signature.go
Обычный файл
@@ -0,0 +1,132 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/openpgp"
|
||||
"golang.org/x/crypto/openpgp/armor"
|
||||
)
|
||||
|
||||
// GetPluginPublicKeyFiles returns all public keys listed in the config.
|
||||
func (a *App) GetPluginPublicKeyFiles() ([]string, *model.AppError) {
|
||||
return a.Config().PluginSettings.SignaturePublicKeyFiles, nil
|
||||
}
|
||||
|
||||
// GetPublicKey will return the actual public key saved in the `name` file.
|
||||
func (a *App) GetPublicKey(name string) ([]byte, *model.AppError) {
|
||||
data, err := a.Srv.configStore.GetFile(name)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetPublicKey", "app.plugin.get_public_key.get_file.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// AddPublicKey will add plugin public key to the config. Overwrites the previous file
|
||||
func (a *App) AddPublicKey(name string, key io.Reader) *model.AppError {
|
||||
if model.IsSamlFile(&a.Config().SamlSettings, name) {
|
||||
return model.NewAppError("AddPublicKey", "app.plugin.modify_saml.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
data, err := ioutil.ReadAll(key)
|
||||
if err != nil {
|
||||
return model.NewAppError("AddPublicKey", "app.plugin.write_file.read.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
err = a.Srv.configStore.SetFile(name, data)
|
||||
if err != nil {
|
||||
return model.NewAppError("AddPublicKey", "app.plugin.write_file.saving.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
a.UpdateConfig(func(cfg *model.Config) {
|
||||
if !utils.StringInSlice(name, cfg.PluginSettings.SignaturePublicKeyFiles) {
|
||||
cfg.PluginSettings.SignaturePublicKeyFiles = append(cfg.PluginSettings.SignaturePublicKeyFiles, name)
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeletePublicKey will delete plugin public key from the config.
|
||||
func (a *App) DeletePublicKey(name string) *model.AppError {
|
||||
if model.IsSamlFile(&a.Config().SamlSettings, name) {
|
||||
return model.NewAppError("AddPublicKey", "app.plugin.modify_saml.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
filename := filepath.Base(name)
|
||||
if err := a.Srv.configStore.RemoveFile(filename); err != nil {
|
||||
return model.NewAppError("DeletePublicKey", "app.plugin.delete_public_key.delete.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
a.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.PluginSettings.SignaturePublicKeyFiles = utils.RemoveStringFromSlice(filename, cfg.PluginSettings.SignaturePublicKeyFiles)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyPlugin checks that the given signature corresponds to the given plugin and matches a trusted certificate.
|
||||
func (a *App) VerifyPlugin(plugin, signature io.ReadSeeker) *model.AppError {
|
||||
if err := verifySignature(bytes.NewReader(mattermostPluginPublicKey), plugin, signature); err == nil {
|
||||
return nil
|
||||
}
|
||||
publicKeys, appErr := a.GetPluginPublicKeyFiles()
|
||||
if appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
for _, pk := range publicKeys {
|
||||
pkBytes, appErr := a.GetPublicKey(pk)
|
||||
if appErr != nil {
|
||||
mlog.Error("Unable to get public key for ", mlog.String("filename", pk))
|
||||
continue
|
||||
}
|
||||
publicKey := bytes.NewReader(pkBytes)
|
||||
plugin.Seek(0, 0)
|
||||
signature.Seek(0, 0)
|
||||
if err := verifySignature(publicKey, plugin, signature); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return model.NewAppError("VerifyPlugin", "api.plugin.verify_plugin.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func verifySignature(publicKey, message, signatrue io.Reader) error {
|
||||
pk, err := decodeIfArmored(publicKey)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "can't decode public key")
|
||||
}
|
||||
s, err := decodeIfArmored(signatrue)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "can't decode signature")
|
||||
}
|
||||
return verifyBinarySignature(pk, message, s)
|
||||
}
|
||||
|
||||
func verifyBinarySignature(publicKey, signedFile, signature io.Reader) error {
|
||||
keyring, err := openpgp.ReadKeyRing(publicKey)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "can't read public key")
|
||||
}
|
||||
if _, err = openpgp.CheckDetachedSignature(keyring, signedFile, signature); err != nil {
|
||||
return errors.Wrap(err, "error while checking the signature")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func decodeIfArmored(reader io.Reader) (io.Reader, error) {
|
||||
readBytes, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "can't read the file")
|
||||
}
|
||||
block, err := armor.Decode(bytes.NewReader(readBytes))
|
||||
if err != nil {
|
||||
return bytes.NewReader(readBytes), nil
|
||||
}
|
||||
return block.Body, nil
|
||||
}
|
||||
102
app/plugin_signature_test.go
Обычный файл
102
app/plugin_signature_test.go
Обычный файл
@@ -0,0 +1,102 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/utils/fileutils"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPluginPublicKeys(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
path, _ := fileutils.FindDir("tests")
|
||||
publicKeyFilename := "test-public-key.plugin.gpg"
|
||||
publicKey, err := ioutil.ReadFile(filepath.Join(path, publicKeyFilename))
|
||||
require.Nil(t, err)
|
||||
fileReader, err := os.Open(filepath.Join(path, publicKeyFilename))
|
||||
require.Nil(t, err)
|
||||
defer fileReader.Close()
|
||||
th.App.AddPublicKey(publicKeyFilename, fileReader)
|
||||
file, err := th.App.GetPublicKey(publicKeyFilename)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, publicKey, file)
|
||||
_, err = th.App.GetPublicKey("wrong file name")
|
||||
require.NotNil(t, err)
|
||||
_, err = th.App.GetPublicKey("wrong-file-name.plugin.gpg")
|
||||
require.NotNil(t, err)
|
||||
|
||||
err = th.App.DeletePublicKey("wrong file name")
|
||||
require.Nil(t, err)
|
||||
err = th.App.DeletePublicKey("wrong-file-name.plugin.gpg")
|
||||
require.Nil(t, err)
|
||||
|
||||
err = th.App.DeletePublicKey(publicKeyFilename)
|
||||
require.Nil(t, err)
|
||||
_, err = th.App.GetPublicKey(publicKeyFilename)
|
||||
require.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestVerifySignature(t *testing.T) {
|
||||
path, _ := fileutils.FindDir("tests")
|
||||
pluginFilename := "testplugin.tar.gz"
|
||||
signatureFilename := "testplugin.tar.gz.sig"
|
||||
armoredSignatureFilename := "testplugin.tar.gz.asc"
|
||||
publicKeyFilename := "development-public-key.gpg"
|
||||
armoredPublicKeyFilename := "development-public-key.asc"
|
||||
t.Run("verify armored signature and armored public key", func(t *testing.T) {
|
||||
publicKeyFileReader, err := os.Open(filepath.Join(path, armoredPublicKeyFilename))
|
||||
require.Nil(t, err)
|
||||
defer publicKeyFileReader.Close()
|
||||
pluginFileReader, err := os.Open(filepath.Join(path, pluginFilename))
|
||||
require.Nil(t, err)
|
||||
defer pluginFileReader.Close()
|
||||
signatureFileReader, err := os.Open(filepath.Join(path, armoredSignatureFilename))
|
||||
require.Nil(t, err)
|
||||
defer signatureFileReader.Close()
|
||||
require.Nil(t, verifySignature(publicKeyFileReader, pluginFileReader, signatureFileReader))
|
||||
})
|
||||
t.Run("verify non armored signature and armored public key", func(t *testing.T) {
|
||||
publicKeyFileReader, err := os.Open(filepath.Join(path, armoredPublicKeyFilename))
|
||||
require.Nil(t, err)
|
||||
defer publicKeyFileReader.Close()
|
||||
pluginFileReader, err := os.Open(filepath.Join(path, pluginFilename))
|
||||
require.Nil(t, err)
|
||||
defer pluginFileReader.Close()
|
||||
signatureFileReader, err := os.Open(filepath.Join(path, signatureFilename))
|
||||
require.Nil(t, err)
|
||||
defer signatureFileReader.Close()
|
||||
require.Nil(t, verifySignature(publicKeyFileReader, pluginFileReader, signatureFileReader))
|
||||
})
|
||||
t.Run("verify armored signature and non armored public key", func(t *testing.T) {
|
||||
publicKeyFileReader, err := os.Open(filepath.Join(path, publicKeyFilename))
|
||||
require.Nil(t, err)
|
||||
defer publicKeyFileReader.Close()
|
||||
pluginFileReader, err := os.Open(filepath.Join(path, pluginFilename))
|
||||
require.Nil(t, err)
|
||||
defer pluginFileReader.Close()
|
||||
armoredSignatureFileReader, err := os.Open(filepath.Join(path, armoredSignatureFilename))
|
||||
require.Nil(t, err)
|
||||
defer armoredSignatureFileReader.Close()
|
||||
require.Nil(t, verifySignature(publicKeyFileReader, pluginFileReader, armoredSignatureFileReader))
|
||||
})
|
||||
t.Run("verify non armored signature and non armored public key", func(t *testing.T) {
|
||||
publicKeyFileReader, err := os.Open(filepath.Join(path, publicKeyFilename))
|
||||
require.Nil(t, err)
|
||||
defer publicKeyFileReader.Close()
|
||||
pluginFileReader, err := os.Open(filepath.Join(path, pluginFilename))
|
||||
require.Nil(t, err)
|
||||
defer pluginFileReader.Close()
|
||||
signatureFileReader, err := os.Open(filepath.Join(path, signatureFilename))
|
||||
require.Nil(t, err)
|
||||
defer signatureFileReader.Close()
|
||||
require.Nil(t, verifySignature(publicKeyFileReader, pluginFileReader, signatureFileReader))
|
||||
})
|
||||
}
|
||||
@@ -483,7 +483,7 @@ func TestPluginSync(t *testing.T) {
|
||||
|
||||
s3Port := os.Getenv("CI_MINIO_PORT")
|
||||
if s3Port == "" {
|
||||
s3Port = "9001"
|
||||
s3Port = "9000"
|
||||
}
|
||||
|
||||
s3Endpoint := fmt.Sprintf("%s:%s", s3Host, s3Port)
|
||||
@@ -508,6 +508,7 @@ func TestPluginSync(t *testing.T) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
*cfg.PluginSettings.Directory = "./test-plugins"
|
||||
*cfg.PluginSettings.ClientDirectory = "./test-client-plugins"
|
||||
*cfg.PluginSettings.RequirePluginSignature = false
|
||||
})
|
||||
th.App.UpdateConfig(testCase.ConfigFunc)
|
||||
|
||||
@@ -530,7 +531,7 @@ func TestPluginSync(t *testing.T) {
|
||||
// Check if installed
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.Nil(t, err)
|
||||
require.True(t, len(pluginStatus) == 1)
|
||||
require.Len(t, pluginStatus, 1)
|
||||
require.Equal(t, pluginStatus[0].PluginId, "testplugin")
|
||||
|
||||
// Bundle removed from the file store case
|
||||
@@ -543,7 +544,54 @@ func TestPluginSync(t *testing.T) {
|
||||
// Check if removed
|
||||
pluginStatus, err = env.Statuses()
|
||||
require.Nil(t, err)
|
||||
require.True(t, len(pluginStatus) == 0)
|
||||
require.Len(t, pluginStatus, 0)
|
||||
|
||||
// RequirePluginSignature = true case
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.RequirePluginSignature = true
|
||||
})
|
||||
pluginFileReader, err := os.Open(filepath.Join(path, "testplugin.tar.gz"))
|
||||
require.NoError(t, err)
|
||||
defer pluginFileReader.Close()
|
||||
_, appErr = th.App.WriteFile(pluginFileReader, th.App.getBundleStorePath("testplugin.tar.gz"))
|
||||
checkNoError(t, appErr)
|
||||
// no signature
|
||||
appErr = th.App.SyncPlugins()
|
||||
checkNoError(t, appErr)
|
||||
pluginStatus, err = env.Statuses()
|
||||
require.Nil(t, err)
|
||||
require.Len(t, pluginStatus, 0)
|
||||
|
||||
// Wrong signature
|
||||
signatureFileReader, err := os.Open(filepath.Join(path, "testpluginv2.tar.gz.sig"))
|
||||
require.NoError(t, err)
|
||||
defer signatureFileReader.Close()
|
||||
filePath := fmt.Sprintf("%s.sig", th.App.getBundleStorePath("testplugin"))
|
||||
_, appErr = th.App.WriteFile(signatureFileReader, filePath)
|
||||
checkNoError(t, appErr)
|
||||
|
||||
appErr = th.App.SyncPlugins()
|
||||
checkNoError(t, appErr)
|
||||
|
||||
pluginStatus, err = env.Statuses()
|
||||
require.Nil(t, err)
|
||||
require.Len(t, pluginStatus, 0)
|
||||
|
||||
// Correct signature
|
||||
signatureFileReader, err = os.Open(filepath.Join(path, "testplugin.tar.gz.sig"))
|
||||
require.NoError(t, err)
|
||||
defer signatureFileReader.Close()
|
||||
filePath = fmt.Sprintf("%s.sig", th.App.getBundleStorePath("testplugin"))
|
||||
_, appErr = th.App.WriteFile(signatureFileReader, filePath)
|
||||
checkNoError(t, appErr)
|
||||
|
||||
appErr = th.App.SyncPlugins()
|
||||
checkNoError(t, appErr)
|
||||
|
||||
pluginStatus, err = env.Statuses()
|
||||
require.Nil(t, err)
|
||||
require.Len(t, pluginStatus, 1)
|
||||
require.Equal(t, pluginStatus[0].PluginId, "testplugin")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,6 +261,21 @@ func NewServer(options ...Option) (*Server, error) {
|
||||
s.StartElasticsearch()
|
||||
}
|
||||
|
||||
s.AddConfigListener(func(oldConfig *model.Config, newConfig *model.Config) {
|
||||
if *oldConfig.GuestAccountsSettings.Enable && !*newConfig.GuestAccountsSettings.Enable {
|
||||
if appErr := s.FakeApp().DeactivateGuests(); appErr != nil {
|
||||
mlog.Error("Unable to deactivate guest accounts", mlog.Err(appErr))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Disable active guest accounts on first run if guest accounts are disabled
|
||||
if !*s.Config().GuestAccountsSettings.Enable {
|
||||
if appErr := s.FakeApp().DeactivateGuests(); appErr != nil {
|
||||
mlog.Error("Unable to deactivate guest accounts", mlog.Err(appErr))
|
||||
}
|
||||
}
|
||||
|
||||
s.initJobs()
|
||||
|
||||
if s.runjobs {
|
||||
|
||||
13
app/team.go
13
app/team.go
@@ -1333,10 +1333,19 @@ func (a *App) GetTeamIdFromQuery(query url.Values) (string, *model.AppError) {
|
||||
}
|
||||
|
||||
func (a *App) SanitizeTeam(session model.Session, team *model.Team) *model.Team {
|
||||
if !a.SessionHasPermissionToTeam(session, team.Id, model.PERMISSION_MANAGE_TEAM) {
|
||||
team.Sanitize()
|
||||
if a.SessionHasPermissionToTeam(session, team.Id, model.PERMISSION_MANAGE_TEAM) {
|
||||
return team
|
||||
}
|
||||
|
||||
if a.SessionHasPermissionToTeam(session, team.Id, model.PERMISSION_INVITE_USER) {
|
||||
inviteId := team.InviteId
|
||||
team.Sanitize()
|
||||
team.InviteId = inviteId
|
||||
return team
|
||||
}
|
||||
|
||||
team.Sanitize()
|
||||
|
||||
return team
|
||||
}
|
||||
|
||||
|
||||
@@ -419,6 +419,7 @@ func TestSanitizeTeam(t *testing.T) {
|
||||
team := &model.Team{
|
||||
Id: model.NewId(),
|
||||
Email: th.MakeEmail(),
|
||||
InviteId: model.NewId(),
|
||||
AllowedDomains: "example.com",
|
||||
}
|
||||
|
||||
@@ -443,6 +444,7 @@ func TestSanitizeTeam(t *testing.T) {
|
||||
|
||||
sanitized := th.App.SanitizeTeam(session, copyTeam())
|
||||
require.Empty(t, sanitized.Email, "should've sanitized team")
|
||||
require.Empty(t, sanitized.InviteId, "should've sanitized inviteid")
|
||||
})
|
||||
|
||||
t.Run("user of the team", func(t *testing.T) {
|
||||
@@ -460,6 +462,7 @@ func TestSanitizeTeam(t *testing.T) {
|
||||
|
||||
sanitized := th.App.SanitizeTeam(session, copyTeam())
|
||||
require.Empty(t, sanitized.Email, "should've sanitized team")
|
||||
require.NotEmpty(t, sanitized.InviteId, "should have not sanitized inviteid")
|
||||
})
|
||||
|
||||
t.Run("team admin", func(t *testing.T) {
|
||||
@@ -477,6 +480,7 @@ func TestSanitizeTeam(t *testing.T) {
|
||||
|
||||
sanitized := th.App.SanitizeTeam(session, copyTeam())
|
||||
require.NotEmpty(t, sanitized.Email, "shouldn't have sanitized team")
|
||||
require.NotEmpty(t, sanitized.InviteId, "shouldn't have sanitized inviteid")
|
||||
})
|
||||
|
||||
t.Run("team admin of another team", func(t *testing.T) {
|
||||
@@ -494,6 +498,7 @@ func TestSanitizeTeam(t *testing.T) {
|
||||
|
||||
sanitized := th.App.SanitizeTeam(session, copyTeam())
|
||||
require.Empty(t, sanitized.Email, "should've sanitized team")
|
||||
require.Empty(t, sanitized.InviteId, "should've sanitized inviteid")
|
||||
})
|
||||
|
||||
t.Run("system admin, not a user of team", func(t *testing.T) {
|
||||
@@ -511,6 +516,7 @@ func TestSanitizeTeam(t *testing.T) {
|
||||
|
||||
sanitized := th.App.SanitizeTeam(session, copyTeam())
|
||||
require.NotEmpty(t, sanitized.Email, "shouldn't have sanitized team")
|
||||
require.NotEmpty(t, sanitized.InviteId, "shouldn't have sanitized inviteid")
|
||||
})
|
||||
|
||||
t.Run("system admin, user of team", func(t *testing.T) {
|
||||
@@ -528,6 +534,7 @@ func TestSanitizeTeam(t *testing.T) {
|
||||
|
||||
sanitized := th.App.SanitizeTeam(session, copyTeam())
|
||||
require.NotEmpty(t, sanitized.Email, "shouldn't have sanitized team")
|
||||
require.NotEmpty(t, sanitized.InviteId, "shouldn't have sanitized inviteid")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
39
app/user.go
39
app/user.go
@@ -943,28 +943,28 @@ func (a *App) UpdatePasswordAsUser(userId, currentPassword, newPassword string)
|
||||
return a.UpdatePasswordSendEmail(user, newPassword, T("api.user.update_password.menu"))
|
||||
}
|
||||
|
||||
func (a *App) userDeactivated(user *model.User) *model.AppError {
|
||||
if err := a.RevokeAllSessions(user.Id); err != nil {
|
||||
func (a *App) userDeactivated(userId string) *model.AppError {
|
||||
if err := a.RevokeAllSessions(userId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.SetStatusOffline(user.Id, false)
|
||||
a.SetStatusOffline(userId, false)
|
||||
|
||||
if *a.Config().ServiceSettings.DisableBotsWhenOwnerIsDeactivated {
|
||||
a.disableUserBots(user.Id)
|
||||
a.disableUserBots(userId)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) invalidateUserChannelMembersCaches(user *model.User) *model.AppError {
|
||||
teamsForUser, err := a.GetTeamsForUser(user.Id)
|
||||
func (a *App) invalidateUserChannelMembersCaches(userId string) *model.AppError {
|
||||
teamsForUser, err := a.GetTeamsForUser(userId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, team := range teamsForUser {
|
||||
channelsForUser, err := a.GetChannelsForUser(team.Id, user.Id, false)
|
||||
channelsForUser, err := a.GetChannelsForUser(team.Id, userId, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -992,12 +992,12 @@ func (a *App) UpdateActive(user *model.User, active bool) (*model.User, *model.A
|
||||
ruser := userUpdate.New
|
||||
|
||||
if !active {
|
||||
if err := a.userDeactivated(ruser); err != nil {
|
||||
if err := a.userDeactivated(ruser.Id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
a.invalidateUserChannelMembersCaches(user)
|
||||
a.invalidateUserChannelMembersCaches(user.Id)
|
||||
a.InvalidateCacheForUser(user.Id)
|
||||
|
||||
a.sendUpdatedUserEvent(*ruser)
|
||||
@@ -1005,6 +1005,27 @@ func (a *App) UpdateActive(user *model.User, active bool) (*model.User, *model.A
|
||||
return ruser, nil
|
||||
}
|
||||
|
||||
func (a *App) DeactivateGuests() *model.AppError {
|
||||
userIds, err := a.Srv.Store.User().DeactivateGuests()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, userId := range userIds {
|
||||
if err := a.userDeactivated(userId); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
a.Srv.Store.Channel().ClearCaches()
|
||||
a.Srv.Store.User().ClearCaches()
|
||||
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_GUESTS_DEACTIVATED, "", "", "", nil)
|
||||
a.Publish(message)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) GetSanitizeOptions(asAdmin bool) map[string]bool {
|
||||
options := a.Config().GetSanitizeOptions()
|
||||
if asAdmin {
|
||||
|
||||
@@ -1164,3 +1164,27 @@ func TestDemoteUserToGuest(t *testing.T) {
|
||||
assert.Len(t, *channelMembers, 3)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeactivateGuests(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
guest1 := th.CreateGuest()
|
||||
guest2 := th.CreateGuest()
|
||||
user := th.CreateUser()
|
||||
|
||||
err := th.App.DeactivateGuests()
|
||||
require.Nil(t, err)
|
||||
|
||||
guest1, err = th.App.GetUser(guest1.Id)
|
||||
assert.Nil(t, err)
|
||||
assert.NotEqual(t, int64(0), guest1.DeleteAt)
|
||||
|
||||
guest2, err = th.App.GetUser(guest2.Id)
|
||||
assert.Nil(t, err)
|
||||
assert.NotEqual(t, int64(0), guest2.DeleteAt)
|
||||
|
||||
user, err = th.App.GetUser(user.Id)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(0), user.DeleteAt)
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user