Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cached store part #1: API #457

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5af848d
merged, tests wip
Jan 30, 2023
074705e
Merge branch 'master' of github.com:mattermost/mattermost-plugin-apps…
Jan 30, 2023
8d5038e
reduced diffs
Jan 30, 2023
6c28a9d
reduced diffs, 2
Jan 30, 2023
13fcd5f
adjustments
Jan 30, 2023
86c19f0
lint + test app
Jan 30, 2023
57d7b2e
simplify tests
Jan 30, 2023
e6358b3
reduce diffs
Jan 30, 2023
1850194
added ...Team tests
Jan 31, 2023
4814a39
added ...LeftTeam tests
Jan 31, 2023
a88c2f7
Reduced diffs: renamed test files back
Jan 31, 2023
cd43dc6
Reduced diffs: moved around lines
Jan 31, 2023
d6253ea
Merge branch 'master' of github.com:mattermost/mattermost-plugin-apps…
Feb 1, 2023
a7ce2d2
Update server/proxy/invoke_call.go
levb Feb 2, 2023
784e123
Merge branch 'master' into lev-MM-48793-bot-subs-simplified
levb Feb 2, 2023
5853490
PR feedback
Feb 2, 2023
337d824
Merge branch 'master' into lev-MM-48793-bot-subs-simplified
hanzei Feb 7, 2023
8e1004c
Better logging of bindings and calls
Feb 7, 2023
b76b401
Merge branch 'master' of github.com:mattermost/mattermost-plugin-apps…
Feb 7, 2023
3173ba5
removed a merge redundancy
Feb 8, 2023
32a13d1
Update server/proxy/bindings.go
levb Feb 18, 2023
f90f93b
Update server/proxy/invoke_call.go
levb Feb 18, 2023
e351680
PR feedback
Feb 19, 2023
5c6f5b4
Merge branch 'lev-better-log' of github.com:mattermost/mattermost-plu…
Feb 19, 2023
4a44d1c
PR feedback 2
Feb 19, 2023
3f9054e
API changes
Feb 19, 2023
5eac984
Merge branch 'lev-better-log' of github.com:mattermost/mattermost-plu…
Feb 19, 2023
061a175
Merge branch 'lev-MM-48793-bot-subs-simplified' of github.com:matterm…
Feb 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,14 @@ issues:
- bodyclose
- unused
- unparam
- staticcheck
- path: /
linters:
- staticcheck
text: "BotJoined"
- path: /
linters:
- staticcheck
text: "BotLeft"


2 changes: 1 addition & 1 deletion apps/goapp/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func (app *App) HandleCall(p string, h HandlerFunc) {
}
_ = httputils.WriteJSON(w, cresp)

creq.Log.With(cresp).Debugw("Call:")
creq.Log.With(cresp).Debugf("Call %s returned %s:", creq.Path, cresp.Type)
})
}

Expand Down
98 changes: 50 additions & 48 deletions apps/subscription.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,48 +22,49 @@ const (
// Requires: model.PermissionViewMembers.
SubjectUserCreated Subject = "user_created"

// SubjectUserJoinedChannel, SubjectUserLeftChannel watch the specified
// channel for users joining and leaving it.
// SubjectUserJoinedChannel, SubjectUserLeftChannel behave differently
// depending on the provided parameters. If no channel is specified, the
// subscription is system-wide, and the event is fired for every channel the
// subscriber joins or leaves. If a channel is specified (and the user has
// access to the channel), the event is fired for all users joining or
// leaving the channel.
// TeamID: must be empty.
// ChannelID: specifies the channel to watch.
// Expandable: Channel, User, ChannelMember.
// ChannelID: (optional) specifies the channel to watch.
// Expandable: Channel, Team, User, ChannelMember.
// Requires: model.PermissionReadChannel permission to ChannelID.
//
// TODO: add channel member as part of the subscribe call for the app's bot user?
// To receive and process these notifications as Bot, the bot must first be
// added as a channel member with an AddChannelMember API call.
SubjectUserJoinedChannel Subject = "user_joined_channel"
SubjectUserLeftChannel Subject = "user_left_channel"

// SubjectUserJoinedTeam, SubjectUserLeftTeam watch the specified
// team for users joining and leaving it.
// TeamID: specifies the team to watch.
// SubjectUserJoinedTeam, SubjectUserLeftTeam behave differently
// depending on the provided parameters. If no team is specified, the
// subscription is system-wide, and the event is fired for every team the
// subscriber joins or leaves. If a team is specified (and the user has
// access to the team), the event is fired for all users joining or
// leaving the team.
// TeamID: (optional) specifies the team to watch.
// ChannelID: must be empty.
// Expandable: Team, User, TeamMember.
// Requires: model.PermissionViewTeam
// Requires: model.PermissionViewTeam permission to TeamID.
//
// TODO: add team member as part of the subscribe call for the app's bot user?
// To receive and process these notifications as Bot, the bot must first be
// added as a channel member with an AddTeamMember API call.
// added as a team member with an AddTeamMember API call.
SubjectUserJoinedTeam Subject = "user_joined_team"
SubjectUserLeftTeam Subject = "user_left_team"

// SubjectBotJoinedChannel, SubjectBotLeftChannel watches for the event when
// the app's own bot is added to, or removed from any channel in the
// specified team.
// TeamID: specifies the team to watch.
// ChannelID: must be empty, all channels are watched.
// Expandable: Channel, User (will be the app's bot user), ChannelMember.
// Requires: none - if the event fires, the app's bot already has the permissions.
// SubjectBotJoinedChannel, SubjectBotLeftChannel, SubjectBotJoinedTeam,
// SubjectBotLeftTeam watch for the app's bot joining and leaving channels
// and teams.
//
// Deprecated: use SubjectUserJoinedChannel, etc. with no entity ID instead.
SubjectBotJoinedChannel Subject = "bot_joined_channel"
SubjectBotLeftChannel Subject = "bot_left_channel"

// SubjectBotJoinedTeam, SubjectBotLeftTeam system-wide watch for app's own
// bot added to, or removed from teams.
// TeamID: must be empty.
// ChannelID: must be empty.
// Expandable: Team, User (will be the app's bot user), TeamMember.
// Requires: none - if the event fires, the app's bot already has the permissions.
SubjectBotJoinedTeam Subject = "bot_joined_team"
SubjectBotLeftTeam Subject = "bot_left_team"
SubjectBotJoinedTeam Subject = "bot_joined_team"
SubjectBotLeftTeam Subject = "bot_left_team"

// SubjectChannelCreated watches for new channels in the specified team.
// TeamID: specifies the team to watch.
Expand All @@ -81,9 +82,9 @@ const (
// used to expand other entities.
// SubjectPostCreated Subject = "post_created"

// SubjectBotMentioned subscribes to MessageHasBeenPosted plugin events, specifically
// when the App's bot is mentioned in the post.
// SubjectBotMentioned Subject = "bot_mentioned"
// SubjectSelfMentioned subscribes to MessageHasBeenPosted plugin events, specifically
// when the subscriber is mentioned in the post.
// SubjectSelfMentioned Subject = "self_mentioned"
)

// Subscription is submitted by an app to the Subscribe API. It determines what
Expand Down Expand Up @@ -115,8 +116,7 @@ func (sub Subscription) Validate() error {
}

func (e Event) Validate() error {
var result error
return e.validate(result)
return e.validate(nil)
}

func (e Event) validate(appendTo error) error {
Expand All @@ -128,35 +128,37 @@ func (e Event) validate(appendTo error) error {
// Globally scoped, must not contain any extra qualifiers.
case SubjectUserCreated,
SubjectBotJoinedTeam,
SubjectBotLeftTeam /*, SubjectBotMentioned*/ :
SubjectBotLeftTeam,
SubjectBotJoinedChannel,
SubjectBotLeftChannel /*, SubjectSelfMentioned*/ :
if e.TeamID != "" {
appendTo = multierror.Append(appendTo, utils.NewInvalidError("%s is globally scoped; team_id and channel_id must both be empty", e.Subject))
appendTo = multierror.Append(appendTo, utils.NewInvalidError("%s is scoped globally; team_id and channel_id must both be empty", e.Subject))
}
if e.ChannelID != "" {
appendTo = multierror.Append(appendTo, utils.NewInvalidError("%s is globally scoped; team_id and channel_id must both be empty", e.Subject))
appendTo = multierror.Append(appendTo, utils.NewInvalidError("%s is scoped globally; team_id and channel_id must both be empty", e.Subject))
}

// Team scoped, require TeamID, no ChannelID
case SubjectUserJoinedTeam,
SubjectUserLeftTeam,
SubjectBotJoinedChannel,
SubjectBotLeftChannel,
SubjectChannelCreated:
// Team scoped, require TeamID, no ChannelID.
case SubjectChannelCreated:
if e.TeamID == "" {
appendTo = multierror.Append(appendTo, utils.NewInvalidError("%s is scoped to a team; teamID must not be empty", e.Subject))
appendTo = multierror.Append(appendTo, utils.NewInvalidError("%s is scoped to a team; team_id must not be empty", e.Subject))
}
if e.ChannelID != "" {
appendTo = multierror.Append(appendTo, utils.NewInvalidError("%s is scoped to a team; channelID must be empty", e.Subject))
appendTo = multierror.Append(appendTo, utils.NewInvalidError("%s is scoped to a team; channel_id must be empty", e.Subject))
}

// Channel scoped, require ChannelID, no TeamID
case SubjectUserJoinedChannel,
SubjectUserLeftChannel /*, SubjectPostCreated */ :
if e.TeamID != "" {
appendTo = multierror.Append(appendTo, utils.NewInvalidError("%s is scoped to a channel; teamID must be empty", e.Subject))
// Special case SubjectUserJoinedTeam, SubjectUserLeftTeam: optional TeamID,
// no ChannelID.
case SubjectUserJoinedTeam, SubjectUserLeftTeam:
if e.ChannelID != "" {
appendTo = multierror.Append(appendTo, utils.NewInvalidError("%s is scoped globally, or to a team; channel_id must be empty", e.Subject))
}
if e.ChannelID == "" {
appendTo = multierror.Append(appendTo, utils.NewInvalidError("%s is scoped to a channel; ChannelID must not be empty", e.Subject))

// Special case SubjectUserJoinedChannel, SubjectUserLeftChannel: optional ChannelID,
// no TeamID.
case SubjectUserJoinedChannel, SubjectUserLeftChannel:
if e.TeamID != "" {
appendTo = multierror.Append(appendTo, utils.NewInvalidError("%s is scoped globally, or to a channel; team_id must be empty", e.Subject))
}

default:
Expand Down
109 changes: 109 additions & 0 deletions apps/subscription_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See License for license information.

package apps_test

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/mattermost/mattermost-plugin-apps/apps"
)

func TestValidateSubscriptionEvent(t *testing.T) {
t.Parallel()

for _, tc := range []struct {
apps.Event
expectedError string
}{
// Good.
// {Event: apps.Event{Subject: apps.SubjectUserCreated}},
{Event: apps.Event{Subject: apps.SubjectUserJoinedChannel, ChannelID: "channelID"}},
{Event: apps.Event{Subject: apps.SubjectUserLeftChannel, ChannelID: "channelID"}},
{Event: apps.Event{Subject: apps.SubjectUserJoinedTeam, TeamID: "teamID"}},
{Event: apps.Event{Subject: apps.SubjectUserLeftTeam, TeamID: "teamID"}},
{Event: apps.Event{Subject: apps.SubjectUserJoinedChannel}},
{Event: apps.Event{Subject: apps.SubjectUserLeftChannel}},
{Event: apps.Event{Subject: apps.SubjectUserJoinedTeam}},
{Event: apps.Event{Subject: apps.SubjectUserLeftTeam}},
{Event: apps.Event{Subject: apps.SubjectBotJoinedChannel}},
{Event: apps.Event{Subject: apps.SubjectBotLeftChannel}},
{Event: apps.Event{Subject: apps.SubjectBotJoinedTeam}},
{Event: apps.Event{Subject: apps.SubjectBotLeftTeam}},
{Event: apps.Event{Subject: apps.SubjectChannelCreated, TeamID: "teamID"}},

// Bad.
{
Event: apps.Event{Subject: apps.SubjectUserCreated, TeamID: "teamID"},
expectedError: "user_created is scoped globally; team_id and channel_id must both be empty: invalid input",
},
{
Event: apps.Event{Subject: apps.SubjectUserJoinedChannel, TeamID: "teamID", ChannelID: "channelID"},
expectedError: "user_joined_channel is scoped globally, or to a channel; team_id must be empty: invalid input",
},
{
Event: apps.Event{Subject: apps.SubjectUserJoinedChannel, TeamID: "teamID"},
expectedError: "user_joined_channel is scoped globally, or to a channel; team_id must be empty: invalid input",
},
{
Event: apps.Event{Subject: apps.SubjectUserLeftChannel, TeamID: "teamID", ChannelID: "channelID"},
expectedError: "user_left_channel is scoped globally, or to a channel; team_id must be empty: invalid input",
},
{
Event: apps.Event{Subject: apps.SubjectUserLeftChannel, TeamID: "teamID"},
expectedError: "user_left_channel is scoped globally, or to a channel; team_id must be empty: invalid input",
},
{
Event: apps.Event{Subject: apps.SubjectUserJoinedTeam, TeamID: "teamID", ChannelID: "channelID"},
expectedError: "user_joined_team is scoped globally, or to a team; channel_id must be empty: invalid input",
},
{
Event: apps.Event{Subject: apps.SubjectUserJoinedTeam, ChannelID: "channelID"},
expectedError: "user_joined_team is scoped globally, or to a team; channel_id must be empty: invalid input",
},
{
Event: apps.Event{Subject: apps.SubjectUserLeftTeam, TeamID: "teamID", ChannelID: "channelID"},
expectedError: "user_left_team is scoped globally, or to a team; channel_id must be empty: invalid input",
},
{
Event: apps.Event{Subject: apps.SubjectUserLeftTeam, ChannelID: "channelID"},
expectedError: "user_left_team is scoped globally, or to a team; channel_id must be empty: invalid input",
},
{
Event: apps.Event{Subject: apps.SubjectBotJoinedChannel, TeamID: "teamID"},
expectedError: "bot_joined_channel is scoped globally; team_id and channel_id must both be empty: invalid input",
},
{
Event: apps.Event{Subject: apps.SubjectBotLeftChannel, TeamID: "teamID"},
expectedError: "bot_left_channel is scoped globally; team_id and channel_id must both be empty: invalid input",
},
{
Event: apps.Event{Subject: apps.SubjectBotJoinedTeam, TeamID: "teamID"},
expectedError: "bot_joined_team is scoped globally; team_id and channel_id must both be empty: invalid input",
},
{
Event: apps.Event{Subject: apps.SubjectBotLeftTeam, TeamID: "teamID"},
expectedError: "bot_left_team is scoped globally; team_id and channel_id must both be empty: invalid input",
},
{
Event: apps.Event{Subject: apps.SubjectChannelCreated},
expectedError: "channel_created is scoped to a team; team_id must not be empty: invalid input",
},
{
Event: apps.Event{Subject: apps.SubjectChannelCreated, TeamID: "teamID", ChannelID: "channelID"},
expectedError: "channel_created is scoped to a team; channel_id must be empty: invalid input",
},
} {
t.Run(tc.Event.String(), func(t *testing.T) {
err := tc.Event.Validate()
if tc.expectedError == "" {
assert.NoError(t, err)
} else {
assert.Error(t, err)
assert.Equal(t, "1 error occurred:\n\t* "+tc.expectedError+"\n\n", err.Error())
}
})
}
}
4 changes: 2 additions & 2 deletions build/custom.mk
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ export MM_SERVER_PATH
mock:
ifneq ($(HAS_SERVER),)
go install github.com/golang/mock/mockgen@v1.6.0
mockgen -destination server/mocks/mock_mmclient/mock_mmclient.go github.com/mattermost/mattermost-plugin-apps/server/mmclient Client
mockgen -destination server/mocks/mock_proxy/mock_expand_getter.go github.com/mattermost/mattermost-plugin-apps/server/proxy ExpandGetter
mockgen -destination server/mocks/mock_upstream/mock_upstream.go github.com/mattermost/mattermost-plugin-apps/upstream Upstream
mockgen -destination server/mocks/mock_store/mock_app.go github.com/mattermost/mattermost-plugin-apps/server/store AppStore
mockgen -destination server/mocks/mock_store/mock_appstore.go github.com/mattermost/mattermost-plugin-apps/server/store AppStore
mockgen -destination server/mocks/mock_store/mock_session.go github.com/mattermost/mattermost-plugin-apps/server/store SessionStore
endif

Expand Down
4 changes: 2 additions & 2 deletions server/appservices/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func (a *AppServices) StoreOAuth2App(r *incoming.Request, data []byte) error {
return err
}

r.Config().MattermostAPI().Frontend.PublishWebSocketEvent(config.WebSocketEventRefreshBindings, map[string]interface{}{}, &model.WebsocketBroadcast{})
r.API.Mattermost.Frontend.PublishWebSocketEvent(config.WebSocketEventRefreshBindings, map[string]interface{}{}, &model.WebsocketBroadcast{})

return nil
}
Expand Down Expand Up @@ -89,7 +89,7 @@ func (a *AppServices) StoreOAuth2User(r *incoming.Request, data []byte) error {
return err
}

r.Config().MattermostAPI().Frontend.PublishWebSocketEvent(config.WebSocketEventRefreshBindings, map[string]interface{}{}, &model.WebsocketBroadcast{UserId: actingUserID})
r.API.Mattermost.Frontend.PublishWebSocketEvent(config.WebSocketEventRefreshBindings, map[string]interface{}{}, &model.WebsocketBroadcast{UserId: actingUserID})
return nil
}

Expand Down
21 changes: 12 additions & 9 deletions server/appservices/subscriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,35 +171,38 @@ func (a *AppServices) unsubscribe(r *incoming.Request, ownerUserID string, e app

func (a *AppServices) hasPermissionToSubscribe(r *incoming.Request, sub apps.Subscription) func() error {
return func() error {
mm := r.Config().MattermostAPI()
userID := r.ActingUserID()

switch sub.Subject {
case apps.SubjectUserCreated:
if !mm.User.HasPermissionTo(userID, model.PermissionViewMembers) {
if !r.API.Mattermost.User.HasPermissionTo(userID, model.PermissionViewMembers) {
return errors.New("no permission to read user")
}

case apps.SubjectUserJoinedChannel, apps.SubjectUserLeftChannel /*, apps.SubjectPostCreated, apps.SubjectBotMentioned */ :
if !mm.User.HasPermissionToChannel(userID, sub.ChannelID, model.PermissionReadChannel) {
case apps.SubjectUserJoinedChannel, apps.SubjectUserLeftChannel:
if sub.ChannelID != "" && !r.API.Mattermost.User.HasPermissionToChannel(userID, sub.ChannelID, model.PermissionReadChannel) {
return errors.New("no permission to read channel")
}

case apps.SubjectUserJoinedTeam, apps.SubjectUserLeftTeam:
if !mm.User.HasPermissionToTeam(userID, sub.TeamID, model.PermissionViewTeam) {
if sub.TeamID != "" && !r.API.Mattermost.User.HasPermissionToTeam(userID, sub.TeamID, model.PermissionViewTeam) {
return errors.New("no permission to view team")
}

case apps.SubjectBotJoinedChannel,
apps.SubjectBotLeftChannel,
apps.SubjectBotJoinedTeam,
apps.SubjectBotLeftTeam:
// When the bot has joined an entity, it will have the permission to
// read it.
return nil
app, err := a.store.App.Get(r.SourceAppID())
if err != nil {
return errors.Wrapf(err, "failed to get app %s to validate subscription to %s", r.SourceAppID(), sub.Subject)
}
if r.ActingUserID() != app.BotUserID {
return errors.Errorf("%s can only be subscribed to by the app's bot", sub.Subject)
}

case apps.SubjectChannelCreated:
if !mm.User.HasPermissionToTeam(userID, sub.TeamID, model.PermissionListTeamChannels) {
if !r.API.Mattermost.User.HasPermissionToTeam(userID, sub.TeamID, model.PermissionListTeamChannels) {
return errors.New("no permission to list channels")
}

Expand Down
Loading