From d23ca07133e9bc5eed14d87af563471b4ef963cd Mon Sep 17 00:00:00 2001 From: Daniel Schalla Date: Mon, 30 Jul 2018 20:55:38 +0200 Subject: Login Hooks (#9177) Tests; gofmt --- app/login.go | 22 +++++++ app/plugin_hooks_test.go | 142 +++++++++++++++++++++++++++++++++++++++++ plugin/client_rpc_generated.go | 69 ++++++++++++++++++++ plugin/hooks.go | 9 +++ plugin/plugintest/hooks.go | 43 +++++++++++++ 5 files changed, 285 insertions(+) diff --git a/app/login.go b/app/login.go index d3d2a423e..0d22f2635 100644 --- a/app/login.go +++ b/app/login.go @@ -11,6 +11,7 @@ import ( "github.com/avct/uasurfer" "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/plugin" "github.com/mattermost/mattermost-server/store" ) @@ -65,6 +66,27 @@ func (a *App) AuthenticateUserForLogin(id, loginId, password, mfaToken string, l return nil, err } + if a.PluginsReady() { + var rejectionReason string + pluginContext := &plugin.Context{} + a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + rejectionReason = hooks.UserWillLogIn(pluginContext, user) + return rejectionReason == "" + }, plugin.UserWillLogInId) + + if rejectionReason != "" { + return nil, model.NewAppError("AuthenticateUserForLogin", "Login rejected by plugin: "+rejectionReason, nil, "", http.StatusBadRequest) + } + + a.Go(func() { + pluginContext := &plugin.Context{} + a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + hooks.UserHasLoggedIn(pluginContext, user) + return true + }, plugin.UserHasLoggedInId) + }) + } + return user, nil } diff --git a/app/plugin_hooks_test.go b/app/plugin_hooks_test.go index 488d81757..3f447179f 100644 --- a/app/plugin_hooks_test.go +++ b/app/plugin_hooks_test.go @@ -19,6 +19,7 @@ import ( "github.com/mattermost/mattermost-server/plugin/plugintest/mock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "time" ) func compileGo(t *testing.T, sourceCode, outputPath string) { @@ -371,3 +372,144 @@ func TestHookFileWillBeUploaded(t *testing.T) { io.Copy(&resultBuf, fileReader) assert.Equal(t, "changedtext", resultBuf.String()) } + +func TestUserWillLogIn_Blocked(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + err := th.App.UpdatePassword(th.BasicUser, "hunter2") + + if err != nil { + t.Errorf("Error updating user password: %s", err) + } + + SetAppEnvironmentWithPlugins(t, + []string{ + ` + package main + + import ( + "github.com/mattermost/mattermost-server/plugin" + "github.com/mattermost/mattermost-server/model" + ) + + type MyPlugin struct { + plugin.MattermostPlugin + } + + func (p *MyPlugin) UserWillLogIn(c *plugin.Context, user *model.User) string { + return "Blocked By Plugin" + } + + func main() { + plugin.ClientMain(&MyPlugin{}) + } + `}, th.App, th.App.NewPluginAPI) + + user, err := th.App.AuthenticateUserForLogin("", th.BasicUser.Email, "hunter2", "", false) + + if user != nil { + t.Errorf("Expected nil, got %+v", user) + } + + if err == nil { + t.Errorf("Expected err, got nil") + } +} + +func TestUserWillLogInIn_Passed(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + err := th.App.UpdatePassword(th.BasicUser, "hunter2") + + if err != nil { + t.Errorf("Error updating user password: %s", err) + } + + SetAppEnvironmentWithPlugins(t, + []string{ + ` + package main + + import ( + "github.com/mattermost/mattermost-server/plugin" + "github.com/mattermost/mattermost-server/model" + ) + + type MyPlugin struct { + plugin.MattermostPlugin + } + + func (p *MyPlugin) UserWillLogIn(c *plugin.Context, user *model.User) string { + return "" + } + + func main() { + plugin.ClientMain(&MyPlugin{}) + } + `}, th.App, th.App.NewPluginAPI) + + user, err := th.App.AuthenticateUserForLogin("", th.BasicUser.Email, "hunter2", "", false) + + if user == nil { + t.Errorf("Expected user object, got nil") + } + + if err != nil { + t.Errorf("Expected nil, got %s", err) + } +} + +func TestUserHasLoggedIn(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + err := th.App.UpdatePassword(th.BasicUser, "hunter2") + + if err != nil { + t.Errorf("Error updating user password: %s", err) + } + + SetAppEnvironmentWithPlugins(t, + []string{ + ` + package main + + import ( + "github.com/mattermost/mattermost-server/plugin" + "github.com/mattermost/mattermost-server/model" + ) + + type MyPlugin struct { + plugin.MattermostPlugin + } + + func (p *MyPlugin) UserHasLoggedIn(c *plugin.Context, user *model.User) { + user.FirstName = "plugin-callback-success" + p.API.UpdateUser(user) + } + + func main() { + plugin.ClientMain(&MyPlugin{}) + } + `}, th.App, th.App.NewPluginAPI) + + user, err := th.App.AuthenticateUserForLogin("", th.BasicUser.Email, "hunter2", "", false) + + if user == nil { + t.Errorf("Expected user object, got nil") + } + + if err != nil { + t.Errorf("Expected nil, got %s", err) + } + + time.Sleep(2 * time.Second) + + user, err = th.App.GetUser(th.BasicUser.Id) + + if user.FirstName != "plugin-callback-success" { + t.Errorf("Expected firstname overwrite, got default") + } +} diff --git a/plugin/client_rpc_generated.go b/plugin/client_rpc_generated.go index b9c41606d..9eac71be4 100644 --- a/plugin/client_rpc_generated.go +++ b/plugin/client_rpc_generated.go @@ -432,6 +432,75 @@ func (s *hooksRPCServer) UserHasLeftTeam(args *Z_UserHasLeftTeamArgs, returns *Z return nil } +func init() { + hookNameToId["UserWillLogIn"] = UserWillLogInId +} + +type Z_UserWillLogInArgs struct { + A *Context + B *model.User +} + +type Z_UserWillLogInReturns struct { + A string +} + +func (g *hooksRPCClient) UserWillLogIn(c *Context, user *model.User) string { + _args := &Z_UserWillLogInArgs{c, user} + _returns := &Z_UserWillLogInReturns{} + if g.implemented[UserWillLogInId] { + if err := g.client.Call("Plugin.UserWillLogIn", _args, _returns); err != nil { + g.log.Error("RPC call UserWillLogIn to plugin failed.", mlog.Err(err)) + } + } + return _returns.A +} + +func (s *hooksRPCServer) UserWillLogIn(args *Z_UserWillLogInArgs, returns *Z_UserWillLogInReturns) error { + if hook, ok := s.impl.(interface { + UserWillLogIn(c *Context, user *model.User) string + }); ok { + returns.A = hook.UserWillLogIn(args.A, args.B) + } else { + return fmt.Errorf("Hook UserWillLogIn called but not implemented.") + } + return nil +} + +func init() { + hookNameToId["UserHasLoggedIn"] = UserHasLoggedInId +} + +type Z_UserHasLoggedInArgs struct { + A *Context + B *model.User +} + +type Z_UserHasLoggedInReturns struct { +} + +func (g *hooksRPCClient) UserHasLoggedIn(c *Context, user *model.User) { + _args := &Z_UserHasLoggedInArgs{c, user} + _returns := &Z_UserHasLoggedInReturns{} + if g.implemented[UserHasLoggedInId] { + if err := g.client.Call("Plugin.UserHasLoggedIn", _args, _returns); err != nil { + g.log.Error("RPC call UserHasLoggedIn to plugin failed.", mlog.Err(err)) + } + } + return +} + +func (s *hooksRPCServer) UserHasLoggedIn(args *Z_UserHasLoggedInArgs, returns *Z_UserHasLoggedInReturns) error { + if hook, ok := s.impl.(interface { + UserHasLoggedIn(c *Context, user *model.User) + }); ok { + hook.UserHasLoggedIn(args.A, args.B) + } else { + return fmt.Errorf("Hook UserHasLoggedIn called but not implemented.") + } + return nil +} + type Z_RegisterCommandArgs struct { A *model.Command } diff --git a/plugin/hooks.go b/plugin/hooks.go index c191652e3..363a69fc0 100644 --- a/plugin/hooks.go +++ b/plugin/hooks.go @@ -30,6 +30,8 @@ const ( UserHasLeftTeamId = 12 ChannelHasBeenCreatedId = 13 FileWillBeUploadedId = 14 + UserWillLogInId = 15 + UserHasLoggedInId = 16 TotalHooksId = iota ) @@ -119,6 +121,13 @@ type Hooks interface { // If actor is not nil, the user was removed from the team by the actor. UserHasLeftTeam(c *Context, teamMember *model.TeamMember, actor *model.User) + // UserWillLogIn before the login of the user is returned. Returning a non empty string will reject the login event. + // If you don't need to reject the login event, see UserHasLoggedIn + UserWillLogIn(c *Context, user *model.User) string + + // UserHasLoggedIn is invoked after a user has logged in. + UserHasLoggedIn(c *Context, user *model.User) + // FileWillBeUploaded is invoked when a file is uploaded, but before it is committed to backing store. // Read from file to retrieve the body of the uploaded file. You may modify the body of the file by writing to output. // Returned FileInfo will be used instead of input FileInfo. Return nil to reject the file upload and include a text reason as the second argument. diff --git a/plugin/plugintest/hooks.go b/plugin/plugintest/hooks.go index d88792f58..838cccc02 100644 --- a/plugin/plugintest/hooks.go +++ b/plugin/plugintest/hooks.go @@ -5,6 +5,7 @@ package plugintest import http "net/http" +import io "io" import mock "github.com/stretchr/testify/mock" import model "github.com/mattermost/mattermost-server/model" import plugin "github.com/mattermost/mattermost-server/plugin" @@ -44,6 +45,29 @@ func (_m *Hooks) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*mo return r0, r1 } +// FileWillBeUploaded provides a mock function with given fields: c, info, file, output +func (_m *Hooks) FileWillBeUploaded(c *plugin.Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string) { + ret := _m.Called(c, info, file, output) + + var r0 *model.FileInfo + if rf, ok := ret.Get(0).(func(*plugin.Context, *model.FileInfo, io.Reader, io.Writer) *model.FileInfo); ok { + r0 = rf(c, info, file, output) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.FileInfo) + } + } + + var r1 string + if rf, ok := ret.Get(1).(func(*plugin.Context, *model.FileInfo, io.Reader, io.Writer) string); ok { + r1 = rf(c, info, file, output) + } else { + r1 = ret.Get(1).(string) + } + + return r0, r1 +} + // Implemented provides a mock function with given fields: func (_m *Hooks) Implemented() ([]string, error) { ret := _m.Called() @@ -189,3 +213,22 @@ func (_m *Hooks) UserHasLeftChannel(c *plugin.Context, channelMember *model.Chan func (_m *Hooks) UserHasLeftTeam(c *plugin.Context, teamMember *model.TeamMember, actor *model.User) { _m.Called(c, teamMember, actor) } + +// UserHasLoggedIn provides a mock function with given fields: c, user +func (_m *Hooks) UserHasLoggedIn(c *plugin.Context, user *model.User) { + _m.Called(c, user) +} + +// UserWillLogIn provides a mock function with given fields: c, user +func (_m *Hooks) UserWillLogIn(c *plugin.Context, user *model.User) string { + ret := _m.Called(c, user) + + var r0 string + if rf, ok := ret.Get(0).(func(*plugin.Context, *model.User) string); ok { + r0 = rf(c, user) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} -- cgit v1.2.3-1-g7c22