Don't notify non-board members of card changes (#2718)

* Don't notify non-board members of card changes
- include permissions service in notifications backends
- use permissions service to ensure @mentions are to users on team
- use permissions service to ensure subscribers are members of board
This commit is contained in:
Doug Lauder 2022-04-07 11:42:32 -04:00 committed by GitHub
parent d8c017d25a
commit ff020d85e3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 101 additions and 104 deletions

View file

@ -8,6 +8,7 @@ import (
"github.com/mattermost/focalboard/server/services/notify/notifymentions" "github.com/mattermost/focalboard/server/services/notify/notifymentions"
"github.com/mattermost/focalboard/server/services/notify/notifysubscriptions" "github.com/mattermost/focalboard/server/services/notify/notifysubscriptions"
"github.com/mattermost/focalboard/server/services/notify/plugindelivery" "github.com/mattermost/focalboard/server/services/notify/plugindelivery"
"github.com/mattermost/focalboard/server/services/permissions"
"github.com/mattermost/focalboard/server/services/store" "github.com/mattermost/focalboard/server/services/store"
"github.com/mattermost/focalboard/server/ws" "github.com/mattermost/focalboard/server/ws"
@ -28,17 +29,18 @@ const (
type notifyBackendParams struct { type notifyBackendParams struct {
cfg *config.Configuration cfg *config.Configuration
client *pluginapi.Client client *pluginapi.Client
permissions permissions.PermissionsService
serverRoot string serverRoot string
logger *mlog.Logger logger *mlog.Logger
} }
func createMentionsNotifyBackend(params notifyBackendParams) (*notifymentions.Backend, error) { func createMentionsNotifyBackend(params notifyBackendParams) (*notifymentions.Backend, error) {
delivery, err := createDelivery(params.client, params.serverRoot) delivery, err := createDelivery(params.client, params.serverRoot, params.permissions)
if err != nil { if err != nil {
return nil, err return nil, err
} }
backend := notifymentions.New(delivery, params.logger) backend := notifymentions.New(delivery, params.permissions, params.logger)
return backend, nil return backend, nil
} }
@ -46,7 +48,7 @@ func createMentionsNotifyBackend(params notifyBackendParams) (*notifymentions.Ba
func createSubscriptionsNotifyBackend(params notifyBackendParams, store store.Store, func createSubscriptionsNotifyBackend(params notifyBackendParams, store store.Store,
wsPluginAdapter ws.PluginAdapterInterface) (*notifysubscriptions.Backend, error) { wsPluginAdapter ws.PluginAdapterInterface) (*notifysubscriptions.Backend, error) {
// //
delivery, err := createDelivery(params.client, params.serverRoot) delivery, err := createDelivery(params.client, params.serverRoot, params.permissions)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -54,6 +56,7 @@ func createSubscriptionsNotifyBackend(params notifyBackendParams, store store.St
backendParams := notifysubscriptions.BackendParams{ backendParams := notifysubscriptions.BackendParams{
ServerRoot: params.serverRoot, ServerRoot: params.serverRoot,
Store: store, Store: store,
Permissions: params.permissions,
Delivery: delivery, Delivery: delivery,
WSAdapter: wsPluginAdapter, WSAdapter: wsPluginAdapter,
Logger: params.logger, Logger: params.logger,
@ -65,7 +68,7 @@ func createSubscriptionsNotifyBackend(params notifyBackendParams, store store.St
return backend, nil return backend, nil
} }
func createDelivery(client *pluginapi.Client, serverRoot string) (*plugindelivery.PluginDelivery, error) { func createDelivery(client *pluginapi.Client, serverRoot string, permissions permissions.PermissionsService) (*plugindelivery.PluginDelivery, error) {
bot := &model.Bot{ bot := &model.Bot{
Username: botUsername, Username: botUsername,
DisplayName: botDisplayname, DisplayName: botDisplayname,
@ -78,7 +81,7 @@ func createDelivery(client *pluginapi.Client, serverRoot string) (*plugindeliver
pluginAPI := &pluginAPIAdapter{client: client} pluginAPI := &pluginAPIAdapter{client: client}
return plugindelivery.New(botID, serverRoot, pluginAPI), nil return plugindelivery.New(botID, serverRoot, pluginAPI, permissions), nil
} }
type pluginAPIAdapter struct { type pluginAPIAdapter struct {

View file

@ -122,6 +122,7 @@ func (p *Plugin) OnActivate() error {
backendParams := notifyBackendParams{ backendParams := notifyBackendParams{
cfg: cfg, cfg: cfg,
client: client, client: client,
permissions: permissionsService,
serverRoot: baseURL + "/boards", serverRoot: baseURL + "/boards",
logger: logger, logger: logger,
} }

View file

@ -9,6 +9,7 @@ import (
"github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/notify" "github.com/mattermost/focalboard/server/services/notify"
"github.com/mattermost/focalboard/server/services/permissions"
"github.com/wiggin77/merror" "github.com/wiggin77/merror"
"github.com/mattermost/mattermost-server/v6/shared/mlog" "github.com/mattermost/mattermost-server/v6/shared/mlog"
@ -25,15 +26,17 @@ type MentionListener interface {
// Backend provides the notification backend for @mentions. // Backend provides the notification backend for @mentions.
type Backend struct { type Backend struct {
delivery MentionDelivery delivery MentionDelivery
permissions permissions.PermissionsService
logger *mlog.Logger logger *mlog.Logger
mux sync.RWMutex mux sync.RWMutex
listeners []MentionListener listeners []MentionListener
} }
func New(delivery MentionDelivery, logger *mlog.Logger) *Backend { func New(delivery MentionDelivery, permissions permissions.PermissionsService, logger *mlog.Logger) *Backend {
return &Backend{ return &Backend{
delivery: delivery, delivery: delivery,
permissions: permissions,
logger: logger, logger: logger,
} }
} }
@ -80,7 +83,9 @@ func (b *Backend) BlockChanged(evt notify.BlockChangeEvent) error {
return nil return nil
} }
if evt.BlockChanged.Type != model.TypeText && evt.BlockChanged.Type != model.TypeComment { switch evt.BlockChanged.Type {
case model.TypeText, model.TypeComment, model.TypeImage:
default:
return nil return nil
} }

View file

@ -10,6 +10,7 @@ import (
"time" "time"
"github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/permissions"
"github.com/mattermost/focalboard/server/services/store" "github.com/mattermost/focalboard/server/services/store"
"github.com/mattermost/focalboard/server/utils" "github.com/mattermost/focalboard/server/utils"
"github.com/wiggin77/merror" "github.com/wiggin77/merror"
@ -33,6 +34,7 @@ var (
type notifier struct { type notifier struct {
serverRoot string serverRoot string
store Store store Store
permissions permissions.PermissionsService
delivery SubscriptionDelivery delivery SubscriptionDelivery
logger *mlog.Logger logger *mlog.Logger
@ -46,6 +48,7 @@ func newNotifier(params BackendParams) *notifier {
return &notifier{ return &notifier{
serverRoot: params.ServerRoot, serverRoot: params.ServerRoot,
store: params.Store, store: params.Store,
permissions: params.Permissions,
delivery: params.Delivery, delivery: params.Delivery,
logger: params.Logger, logger: params.Logger,
done: nil, done: nil,
@ -216,7 +219,7 @@ func (n *notifier) notifySubscribers(hint *model.NotificationHint) error {
// don't notify the author of their own changes. // don't notify the author of their own changes.
authorName, isAuthor := diffAuthors[sub.SubscriberID] authorName, isAuthor := diffAuthors[sub.SubscriberID]
if isAuthor && len(diffAuthors) == 1 { if isAuthor && len(diffAuthors) == 1 {
n.logger.Debug("notifySubscribers - deliver, skipping author", n.logger.Debug("notifySubscribers - skipping author",
mlog.Any("hint", hint), mlog.Any("hint", hint),
mlog.String("author_id", sub.SubscriberID), mlog.String("author_id", sub.SubscriberID),
mlog.String("author_username", authorName), mlog.String("author_username", authorName),
@ -224,6 +227,16 @@ func (n *notifier) notifySubscribers(hint *model.NotificationHint) error {
continue continue
} }
// make sure the subscriber still has permissions for the board.
if !n.permissions.HasPermissionToBoard(sub.SubscriberID, board.ID, model.PermissionViewBoard) {
n.logger.Debug("notifySubscribers - skipping non-board member",
mlog.Any("hint", hint),
mlog.String("subscriber_id", sub.SubscriberID),
mlog.String("board_id", board.ID),
)
continue
}
n.logger.Debug("notifySubscribers - deliver", n.logger.Debug("notifySubscribers - deliver",
mlog.Any("hint", hint), mlog.Any("hint", hint),
mlog.String("modified_by_id", hint.ModifiedByID), mlog.String("modified_by_id", hint.ModifiedByID),

View file

@ -17,6 +17,8 @@ type Store interface {
GetUserByID(userID string) (*model.User, error) GetUserByID(userID string) (*model.User, error)
GetMemberForBoard(boardID, userID string) (*model.BoardMember, error)
CreateSubscription(sub *model.Subscription) (*model.Subscription, error) CreateSubscription(sub *model.Subscription) (*model.Subscription, error)
GetSubscribersForBlock(blockID string) ([]*model.Subscriber, error) GetSubscribersForBlock(blockID string) ([]*model.Subscriber, error)
GetSubscribersCountForBlock(blockID string) (int, error) GetSubscribersCountForBlock(blockID string) (int, error)

View file

@ -9,6 +9,7 @@ import (
"github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/notify" "github.com/mattermost/focalboard/server/services/notify"
"github.com/mattermost/focalboard/server/services/permissions"
"github.com/mattermost/focalboard/server/ws" "github.com/mattermost/focalboard/server/ws"
"github.com/wiggin77/merror" "github.com/wiggin77/merror"
@ -22,6 +23,7 @@ const (
type BackendParams struct { type BackendParams struct {
ServerRoot string ServerRoot string
Store Store Store Store
Permissions permissions.PermissionsService
Delivery SubscriptionDelivery Delivery SubscriptionDelivery
WSAdapter ws.Adapter WSAdapter ws.Adapter
Logger *mlog.Logger Logger *mlog.Logger
@ -32,6 +34,7 @@ type BackendParams struct {
// Backend provides the notification backend for subscriptions. // Backend provides the notification backend for subscriptions.
type Backend struct { type Backend struct {
store Store store Store
permissions permissions.PermissionsService
delivery SubscriptionDelivery delivery SubscriptionDelivery
notifier *notifier notifier *notifier
wsAdapter ws.Adapter wsAdapter ws.Adapter
@ -44,6 +47,7 @@ func New(params BackendParams) *Backend {
return &Backend{ return &Backend{
store: params.Store, store: params.Store,
delivery: params.Delivery, delivery: params.Delivery,
permissions: params.Permissions,
notifier: newNotifier(params), notifier: newNotifier(params),
wsAdapter: params.WSAdapter, wsAdapter: params.WSAdapter,
logger: params.Logger, logger: params.Logger,
@ -179,6 +183,10 @@ func (b *Backend) OnMention(userID string, evt notify.BlockChangeEvent) {
return return
} }
// TODO: Automatically add user to board? Fail and show UI? Waiting for PM decision.
// Currently the subscription created below will only notify if the user is already
// a member of the board.
sub := &model.Subscription{ sub := &model.Subscription{
BlockType: model.TypeCard, BlockType: model.TypeCard,
BlockID: evt.Card.ID, BlockID: evt.Card.ID,

View file

@ -4,6 +4,7 @@
package plugindelivery package plugindelivery
import ( import (
"errors"
"fmt" "fmt"
"github.com/mattermost/focalboard/server/services/notify" "github.com/mattermost/focalboard/server/services/notify"
@ -12,9 +13,13 @@ import (
"github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/model"
) )
var (
ErrMentionPermission = errors.New("mention not permitted")
)
// MentionDeliver notifies a user they have been mentioned in a block. // MentionDeliver notifies a user they have been mentioned in a block.
func (pd *PluginDelivery) MentionDeliver(mentionUsername string, extract string, evt notify.BlockChangeEvent) (string, error) { func (pd *PluginDelivery) MentionDeliver(mentionUsername string, extract string, evt notify.BlockChangeEvent) (string, error) {
member, err := teamMemberFromUsername(pd.api, mentionUsername, evt.TeamID) user, err := userByUsername(pd.api, mentionUsername)
if err != nil { if err != nil {
if isErrNotFound(err) { if isErrNotFound(err) {
// not really an error; could just be someone typed "@sometext" // not really an error; could just be someone typed "@sometext"
@ -24,12 +29,17 @@ func (pd *PluginDelivery) MentionDeliver(mentionUsername string, extract string,
} }
} }
// make sure mentioned user has permissions to team.
if !pd.permissions.HasPermissionToTeam(user.Id, evt.TeamID, model.PermissionViewTeam) {
return "", fmt.Errorf("mentioned user %s not member of team %s: %w", user.Id, evt.TeamID, ErrMentionPermission)
}
author, err := pd.api.GetUserByID(evt.ModifiedByID) author, err := pd.api.GetUserByID(evt.ModifiedByID)
if err != nil { if err != nil {
return "", fmt.Errorf("cannot find user: %w", err) return "", fmt.Errorf("cannot find user: %w", err)
} }
channel, err := pd.api.GetDirectChannel(member.UserId, pd.botID) channel, err := pd.api.GetDirectChannel(user.Id, pd.botID)
if err != nil { if err != nil {
return "", fmt.Errorf("cannot get direct channel: %w", err) return "", fmt.Errorf("cannot get direct channel: %w", err)
} }
@ -40,5 +50,5 @@ func (pd *PluginDelivery) MentionDeliver(mentionUsername string, extract string,
ChannelId: channel.Id, ChannelId: channel.Id,
Message: formatMessage(author.Username, extract, evt.Card.Title, link, evt.BlockChanged), Message: formatMessage(author.Username, extract, evt.Card.Title, link, evt.BlockChanged),
} }
return member.UserId, pd.api.CreatePost(post) return user.Id, pd.api.CreatePost(post)
} }

View file

@ -4,10 +4,7 @@
package plugindelivery package plugindelivery
import ( import (
"fmt" "github.com/mattermost/focalboard/server/services/permissions"
"github.com/mattermost/focalboard/server/services/notify"
"github.com/mattermost/focalboard/server/utils"
mm_model "github.com/mattermost/mattermost-server/v6/model" mm_model "github.com/mattermost/mattermost-server/v6/model"
) )
@ -45,42 +42,14 @@ type PluginDelivery struct {
botID string botID string
serverRoot string serverRoot string
api PluginAPI api PluginAPI
permissions permissions.PermissionsService
} }
func New(botID string, serverRoot string, api PluginAPI) *PluginDelivery { func New(botID string, serverRoot string, api PluginAPI, permissions permissions.PermissionsService) *PluginDelivery {
return &PluginDelivery{ return &PluginDelivery{
botID: botID, botID: botID,
serverRoot: serverRoot, serverRoot: serverRoot,
api: api, api: api,
permissions: permissions,
} }
} }
func (pd *PluginDelivery) Deliver(mentionUsername string, extract string, evt notify.BlockChangeEvent) error {
member, err := teamMemberFromUsername(pd.api, mentionUsername, evt.TeamID)
if err != nil {
if isErrNotFound(err) {
// not really an error; could just be someone typed "@sometext"
return nil
} else {
return fmt.Errorf("cannot lookup mentioned user: %w", err)
}
}
author, err := pd.api.GetUserByID(evt.ModifiedByID)
if err != nil {
return fmt.Errorf("cannot find user: %w", err)
}
channel, err := pd.api.GetDirectChannel(member.UserId, pd.botID)
if err != nil {
return fmt.Errorf("cannot get direct channel: %w", err)
}
link := utils.MakeCardLink(pd.serverRoot, evt.TeamID, evt.Board.ID, evt.Card.ID)
post := &mm_model.Post{
UserId: pd.botID,
ChannelId: channel.Id,
Message: formatMessage(author.Username, extract, evt.Card.Title, link, evt.BlockChanged),
}
return pd.api.CreatePost(post)
}

View file

@ -13,7 +13,7 @@ const (
usernameSpecialChars = ".-_ " usernameSpecialChars = ".-_ "
) )
func teamMemberFromUsername(api PluginAPI, username string, teamID string) (*mm_model.TeamMember, error) { func userByUsername(api PluginAPI, username string) (*mm_model.User, error) {
// check for usernames that might have trailing punctuation // check for usernames that might have trailing punctuation
var user *mm_model.User var user *mm_model.User
var err error var err error
@ -36,13 +36,7 @@ func teamMemberFromUsername(api PluginAPI, username string, teamID string) (*mm_
return nil, err return nil, err
} }
// make sure user is member of team. return user, nil
member, err := api.GetTeamMember(teamID, user.Id)
if err != nil {
return nil, err
}
return member, nil
} }
// trimUsernameSpecialChar tries to remove the last character from word if it // trimUsernameSpecialChar tries to remove the last character from word if it

View file

@ -42,43 +42,35 @@ var (
} }
) )
func userToMember(user *mm_model.User, teamID string) *mm_model.TeamMember { func Test_userByUsername(t *testing.T) {
return &mm_model.TeamMember{
TeamId: teamID,
UserId: user.Id,
}
}
func Test_teamMemberFromUsername(t *testing.T) {
delivery := newPlugAPIMock(mockUsers) delivery := newPlugAPIMock(mockUsers)
tests := []struct { tests := []struct {
name string name string
uname string uname string
teamID string teamID string
want *mm_model.TeamMember want *mm_model.User
wantErr bool wantErr bool
}{ }{
{name: "user1", uname: user1.Username, teamID: defTeamID, want: userToMember(user1, defTeamID), wantErr: false}, {name: "user1", uname: user1.Username, want: user1, wantErr: false},
{name: "user1 with period", uname: user1.Username + ".", teamID: defTeamID, want: userToMember(user1, defTeamID), wantErr: false}, {name: "user1 with period", uname: user1.Username + ".", want: user1, wantErr: false},
{name: "user1 with period plus more", uname: user1.Username + ". ", teamID: defTeamID, want: userToMember(user1, defTeamID), wantErr: false}, {name: "user1 with period plus more", uname: user1.Username + ". ", want: user1, wantErr: false},
{name: "user2 with periods", uname: user2.Username + "...", teamID: defTeamID, want: userToMember(user2, defTeamID), wantErr: false}, {name: "user2 with periods", uname: user2.Username + "...", want: user2, wantErr: false},
{name: "user2 with underscore", uname: user2.Username + "_", teamID: defTeamID, want: userToMember(user2, defTeamID), wantErr: false}, {name: "user2 with underscore", uname: user2.Username + "_", want: user2, wantErr: false},
{name: "user2 with hyphen plus more", uname: user2.Username + "- ", teamID: defTeamID, want: userToMember(user2, defTeamID), wantErr: false}, {name: "user2 with hyphen plus more", uname: user2.Username + "- ", want: user2, wantErr: false},
{name: "user2 with hyphen plus all", uname: user2.Username + ".-_ ", teamID: defTeamID, want: userToMember(user2, defTeamID), wantErr: false}, {name: "user2 with hyphen plus all", uname: user2.Username + ".-_ ", want: user2, wantErr: false},
{name: "user3 with underscore", uname: user3.Username + "_", teamID: defTeamID, want: userToMember(user3, defTeamID), wantErr: false}, {name: "user3 with underscore", uname: user3.Username + "_", want: user3, wantErr: false},
{name: "user4 missing", uname: user4.Username, want: nil, teamID: defTeamID, wantErr: true}, {name: "user4 missing", uname: user4.Username, want: nil, wantErr: true},
{name: "user5 wrong team", uname: user5.Username, teamID: "bogus_team", want: nil, wantErr: true},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := teamMemberFromUsername(delivery, tt.uname, tt.teamID) got, err := userByUsername(delivery, tt.uname)
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Errorf("userFromUsername() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("userByUsername() error = %v, wantErr %v", err, tt.wantErr)
return return
} }
if !reflect.DeepEqual(got, tt.want) { if !reflect.DeepEqual(got, tt.want) {
t.Errorf("userFromUsername()\ngot:\n%v\nwant:\n%v\n", got, tt.want) t.Errorf("userByUsername()\ngot:\n%v\nwant:\n%v\n", got, tt.want)
} }
}) })
} }