From fe8a0f64856ff93e0a8125e6fd134d682453321e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Mon, 22 Jul 2019 22:13:39 +0200 Subject: [PATCH] Guest accounts feature (#11428) * MM-14139: Creating permissions for invite/promote/demote guests (#10778) * MM-14139: Creating permissions for invite/promote/demote guests * Fixing tests * Adding invite guest api endpoint (#10792) * Adding invite guest api endpoint * Adding i18n * Adding some tests * WIP * Migrating Token.Extra info to bigger size (2048) * Fixing tests * Adding client function for invite guests * Adding send guests invites tests * Renaming file from guest to guest_invite * Adding Promote/Demote users from/to guest endpoints (#10791) * Adding Promote/Demote users from/to guest endpoints * Adding i18n translations * Adding the client functions * Using getQueryBuilder function * Addressing PR review comments * Adding default channels to users on promte from guest (#10851) * Adding default channels to users on promte from guest * Addressing PR review comments * Fixing merge problems * Sending websockets events on promote/demote (#11403) * Sending websockets events on promote/demote * Fixing merge problems * Fixing govet shadowing problem * Fixing feature branch tests * Avoiding leaking users data through websockets for guest accounts (#11489) * Avoiding leaking users data through websockets for guest accounts * Adding tests and fixing code error * Fixing i18n * Allow to enable/disable guests and other extra config settings (#11481) * Allow to enable/disable guests and other extra config settings * Fixing tests and moving license and config validation to api level * Update api4/role_test.go Co-Authored-By: George Goldberg * Update api4/role_test.go Co-Authored-By: George Goldberg * Fixing typo * fixing tests * Managing correctly the guest channel leave behavior (#11578) * MM-15134: Removing guests from teams or system on leave channels if needed * WIP * No deactivating the guest user when leave the last team * Adding a couple of tests * Fixing shadow variables * Fixing tests * fixing tests * fixing shadow variables * Adding guest counts for channel stats (#11646) * Adding guest counts for channel stats * Adding tests * Fixing tests * Fixing guest domain restrictions (#11660) * Adding needed migration for the database * Fixing migration --- api4/channel.go | 8 +- api4/role.go | 9 + api4/role_test.go | 26 +- api4/team.go | 37 ++ api4/team_test.go | 130 ++++++ api4/user.go | 112 ++++- app/app_test.go | 9 + app/channel.go | 31 +- app/channel_test.go | 57 +++ app/command_leave.go | 37 +- app/command_leave_test.go | 137 ++++-- app/email.go | 82 ++++ app/login.go | 5 + app/permissions_migrations.go | 19 + app/plugin_api.go | 6 +- app/session.go | 26 ++ app/session_test.go | 54 +++ app/team.go | 208 ++++++--- app/team_test.go | 46 +- app/user.go | 106 ++++- app/user_test.go | 273 +++++++++++- app/web_conn.go | 9 + app/web_hub.go | 1 + config/client.go | 1 + i18n/en.json | 124 ++++++ model/channel_stats.go | 1 + model/client4.go | 35 ++ model/config.go | 27 ++ model/guest_invite.go | 53 +++ model/license.go | 6 + model/license_test.go | 4 + model/permission.go | 25 ++ model/role.go | 3 + model/session.go | 1 + store/sqlstore/channel_store.go | 71 ++- store/sqlstore/session_store.go | 17 + store/sqlstore/tokens_store.go | 2 +- store/sqlstore/upgrade.go | 6 + store/sqlstore/user_store.go | 139 +++++- store/store.go | 6 + store/storetest/channel_store.go | 119 +++++ store/storetest/mocks/ChannelStore.go | 42 ++ store/storetest/mocks/SessionStore.go | 16 + store/storetest/mocks/UserStore.go | 32 ++ store/storetest/user_store.go | 600 ++++++++++++++++++++++++++ templates/invite_body.html | 1 + utils/license.go | 1 + web/context.go | 3 + 48 files changed, 2630 insertions(+), 133 deletions(-) create mode 100644 model/guest_invite.go diff --git a/api4/channel.go b/api4/channel.go index f7c7a72726..c4c5da5a37 100644 --- a/api4/channel.go +++ b/api4/channel.go @@ -486,7 +486,13 @@ func getChannelStats(c *Context, w http.ResponseWriter, r *http.Request) { return } - stats := model.ChannelStats{ChannelId: c.Params.ChannelId, MemberCount: memberCount} + guestCount, err := c.App.GetChannelGuestCount(c.Params.ChannelId) + if err != nil { + c.Err = err + return + } + + stats := model.ChannelStats{ChannelId: c.Params.ChannelId, MemberCount: memberCount, GuestCount: guestCount} w.Write([]byte(stats.ToJson())) } diff --git a/api4/role.go b/api4/role.go index cb2c10b6d9..cdb0800b15 100644 --- a/api4/role.go +++ b/api4/role.go @@ -97,6 +97,10 @@ func patchRole(c *Context, w http.ResponseWriter, r *http.Request) { } if c.App.License() == nil && patch.Permissions != nil { + if oldRole.Name == "system_guest" || oldRole.Name == "team_guest" || oldRole.Name == "channel_guest" { + c.Err = model.NewAppError("Api4.PatchRoles", "api.roles.patch_roles.license.error", nil, "", http.StatusNotImplemented) + return + } allowedPermissions := []string{ model.PERMISSION_CREATE_TEAM.Id, model.PERMISSION_MANAGE_INCOMING_WEBHOOKS.Id, @@ -125,6 +129,11 @@ func patchRole(c *Context, w http.ResponseWriter, r *http.Request) { } } + if c.App.License() != nil && (oldRole.Name == "system_guest" || oldRole.Name == "team_guest" || oldRole.Name == "channel_guest") && !*c.App.License().Features.GuestAccountsPermissions { + c.Err = model.NewAppError("Api4.PatchRoles", "api.roles.patch_roles.license.error", nil, "", http.StatusNotImplemented) + return + } + if !c.App.SessionHasPermissionTo(c.App.Session, model.PERMISSION_MANAGE_SYSTEM) { c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM) return diff --git a/api4/role_test.go b/api4/role_test.go index 0d1331e96a..c722ac4a78 100644 --- a/api4/role_test.go +++ b/api4/role_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/mattermost/mattermost-server/model" ) @@ -193,7 +194,9 @@ func TestPatchRole(t *testing.T) { CheckNotImplementedStatus(t, resp) // Add a license. - th.App.SetLicense(model.NewTestLicense()) + license := model.NewTestLicense() + license.Features.GuestAccountsPermissions = model.NewBool(false) + th.App.SetLicense(license) // Try again, should succeed received, resp = th.SystemAdminClient.PatchRole(role.Id, patch) @@ -205,4 +208,25 @@ func TestPatchRole(t *testing.T) { assert.Equal(t, received.Description, role.Description) assert.EqualValues(t, received.Permissions, []string{"manage_system", "manage_incoming_webhooks", "manage_outgoing_webhooks"}) assert.Equal(t, received.SchemeManaged, role.SchemeManaged) + + t.Run("Check guest permissions editing without E20 license", func(t *testing.T) { + license := model.NewTestLicense() + license.Features.GuestAccountsPermissions = model.NewBool(false) + th.App.SetLicense(license) + + guestRole, err := th.App.Srv.Store.Role().GetByName("system_guest") + require.Nil(t, err) + received, resp = th.SystemAdminClient.PatchRole(guestRole.Id, patch) + CheckNotImplementedStatus(t, resp) + }) + + t.Run("Check guest permissions editing with E20 license", func(t *testing.T) { + license := model.NewTestLicense() + license.Features.GuestAccountsPermissions = model.NewBool(true) + th.App.SetLicense(license) + guestRole, err := th.App.Srv.Store.Role().GetByName("system_guest") + require.Nil(t, err) + _, resp = th.SystemAdminClient.PatchRole(guestRole.Id, patch) + CheckNoError(t, resp) + }) } diff --git a/api4/team.go b/api4/team.go index ed7916eb1c..67a06aed26 100644 --- a/api4/team.go +++ b/api4/team.go @@ -66,6 +66,7 @@ func (api *API) InitTeam() { api.BaseRoutes.TeamMember.Handle("/schemeRoles", api.ApiSessionRequired(updateTeamMemberSchemeRoles)).Methods("PUT") api.BaseRoutes.Team.Handle("/import", api.ApiSessionRequired(importTeam)).Methods("POST") api.BaseRoutes.Team.Handle("/invite/email", api.ApiSessionRequired(inviteUsersToTeam)).Methods("POST") + api.BaseRoutes.Team.Handle("/invite-guests/email", api.ApiSessionRequired(inviteGuestsToChannels)).Methods("POST") api.BaseRoutes.Teams.Handle("/invites/email", api.ApiSessionRequired(invalidateAllEmailInvites)).Methods("DELETE") api.BaseRoutes.Teams.Handle("/invite/{invite_id:[A-Za-z0-9]+}", api.ApiHandler(getInviteInfo)).Methods("GET") @@ -913,6 +914,42 @@ func inviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request) { ReturnStatusOK(w) } +func inviteGuestsToChannels(c *Context, w http.ResponseWriter, r *http.Request) { + if c.App.License() == nil { + c.Err = model.NewAppError("Api4.InviteGuestsToChannels", "api.team.invate_guests_to_channels.license.error", nil, "", http.StatusNotImplemented) + return + } + + if !*c.App.Config().GuestAccountsSettings.Enable { + c.Err = model.NewAppError("Api4.InviteGuestsToChannels", "api.team.invate_guests_to_channels.disabled.error", nil, "", http.StatusNotImplemented) + return + } + + c.RequireTeamId() + if c.Err != nil { + return + } + + if !c.App.SessionHasPermissionToTeam(c.App.Session, c.Params.TeamId, model.PERMISSION_INVITE_GUEST) { + c.SetPermissionError(model.PERMISSION_INVITE_GUEST) + return + } + + guestsInvite := model.GuestsInviteFromJson(r.Body) + if err := guestsInvite.IsValid(); err != nil { + c.Err = err + return + } + + err := c.App.InviteGuestsToChannels(c.Params.TeamId, guestsInvite, c.App.Session.UserId) + if err != nil { + c.Err = err + return + } + + ReturnStatusOK(w) +} + func getInviteInfo(c *Context, w http.ResponseWriter, r *http.Request) { c.RequireInviteId() if c.Err != nil { diff --git a/api4/team_test.go b/api4/team_test.go index 267b4caf21..6170dc11e2 100644 --- a/api4/team_test.go +++ b/api4/team_test.go @@ -2290,6 +2290,136 @@ func TestInviteUsersToTeam(t *testing.T) { }) } +func TestInviteGuestsToTeam(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + guest1 := th.GenerateTestEmail() + guest2 := th.GenerateTestEmail() + + emailList := []string{guest1, guest2} + + //Delete all the messages before check the sample email + mailservice.DeleteMailBox(guest1) + mailservice.DeleteMailBox(guest2) + + enableEmailInvitations := *th.App.Config().ServiceSettings.EnableEmailInvitations + restrictCreationToDomains := th.App.Config().TeamSettings.RestrictCreationToDomains + enableGuestAccounts := *th.App.Config().GuestAccountsSettings.Enable + defer func() { + th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableEmailInvitations = &enableEmailInvitations }) + th.App.UpdateConfig(func(cfg *model.Config) { cfg.TeamSettings.RestrictCreationToDomains = restrictCreationToDomains }) + th.App.UpdateConfig(func(cfg *model.Config) { cfg.GuestAccountsSettings.Enable = &enableGuestAccounts }) + }() + + th.App.SetLicense(model.NewTestLicense("")) + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GuestAccountsSettings.Enable = false }) + _, resp := th.SystemAdminClient.InviteGuestsToTeam(th.BasicTeam.Id, emailList, []string{th.BasicChannel.Id}, "test-message") + assert.NotNil(t, resp.Error, "Should be disabled") + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GuestAccountsSettings.Enable = true }) + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableEmailInvitations = false }) + _, resp = th.SystemAdminClient.InviteGuestsToTeam(th.BasicTeam.Id, emailList, []string{th.BasicChannel.Id}, "test-message") + if resp.Error == nil { + t.Fatal("Should be disabled") + } + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableEmailInvitations = true }) + + th.App.SetLicense(nil) + + _, resp = th.SystemAdminClient.InviteGuestsToTeam(th.BasicTeam.Id, emailList, []string{th.BasicChannel.Id}, "test-message") + if resp.Error == nil { + t.Fatal("Should be disabled") + } + + th.App.SetLicense(model.NewTestLicense("")) + defer th.App.SetLicense(nil) + + okMsg, resp := th.SystemAdminClient.InviteGuestsToTeam(th.BasicTeam.Id, emailList, []string{th.BasicChannel.Id}, "test-message") + CheckNoError(t, resp) + if !okMsg { + t.Fatal("should return true") + } + + nameFormat := *th.App.Config().TeamSettings.TeammateNameDisplay + expectedSubject := utils.T("api.templates.invite_guest_subject", + map[string]interface{}{"SenderName": th.SystemAdminUser.GetDisplayName(nameFormat), + "TeamDisplayName": th.BasicTeam.DisplayName, + "SiteName": th.App.ClientConfig()["SiteName"]}) + + //Check if the email was send to the rigth email address + for _, email := range emailList { + var resultsMailbox mailservice.JSONMessageHeaderInbucket + err := mailservice.RetryInbucket(5, func() error { + var err error + resultsMailbox, err = mailservice.GetMailBox(email) + return err + }) + if err != nil { + t.Log(err) + t.Log("No email was received, maybe due load on the server. Disabling this verification") + } + if err == nil && len(resultsMailbox) > 0 { + if !strings.ContainsAny(resultsMailbox[len(resultsMailbox)-1].To[0], email) { + t.Fatal("Wrong To recipient") + } else { + if resultsEmail, err := mailservice.GetMessageFromMailbox(email, resultsMailbox[len(resultsMailbox)-1].ID); err == nil { + if resultsEmail.Subject != expectedSubject { + t.Log(resultsEmail.Subject) + t.Log(expectedSubject) + t.Fatal("Wrong Subject") + } + } + } + } + } + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.RestrictCreationToDomains = "@global.com,@common.com" }) + + t.Run("restricted domains", func(t *testing.T) { + err := th.App.InviteGuestsToChannels(th.BasicTeam.Id, &model.GuestsInvite{Emails: emailList, Channels: []string{th.BasicChannel.Id}, Message: "test message"}, th.BasicUser.Id) + + if err == nil { + t.Fatal("Adding users with non-restricted domains was allowed") + } + if err.Where != "InviteGuestsToChannels" || err.Id != "api.team.invite_members.invalid_email.app_error" { + t.Log(err) + t.Fatal("Got wrong error message!") + } + }) + + t.Run("override restricted domains", func(t *testing.T) { + th.BasicTeam.AllowedDomains = "invalid.com,common.com" + if _, err := th.App.UpdateTeam(th.BasicTeam); err == nil { + t.Fatal("Should not update the team") + } + + th.BasicTeam.AllowedDomains = "common.com" + if _, err := th.App.UpdateTeam(th.BasicTeam); err != nil { + t.Log(err) + t.Fatal("Should update the team") + } + + if err := th.App.InviteGuestsToChannels(th.BasicTeam.Id, &model.GuestsInvite{Emails: []string{"test@global.com"}, Channels: []string{th.BasicChannel.Id}, Message: "test message"}, th.BasicUser.Id); err == nil || err.Where != "InviteGuestsToChannels" { + t.Log(err) + t.Fatal("Per team restriction should take precedence over the global restriction") + } + + if err := th.App.InviteGuestsToChannels(th.BasicTeam.Id, &model.GuestsInvite{Emails: []string{"test@common.com"}, Channels: []string{th.BasicChannel.Id}, Message: "test message"}, th.BasicUser.Id); err != nil { + t.Log(err) + t.Fatal("Failed to invite user which was common between team and global domain restriction") + } + + if err := th.App.InviteGuestsToChannels(th.BasicTeam.Id, &model.GuestsInvite{Emails: []string{"test@invalid.com"}, Channels: []string{th.BasicChannel.Id}, Message: "test message"}, th.BasicUser.Id); err == nil { + t.Log(err) + t.Fatal("Should not invite user") + } + + }) +} + func TestGetTeamInviteInfo(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() diff --git a/api4/user.go b/api4/user.go index e4aa289eea..e4589ae85f 100644 --- a/api4/user.go +++ b/api4/user.go @@ -40,6 +40,8 @@ func (api *API) InitUser() { api.BaseRoutes.User.Handle("/roles", api.ApiSessionRequired(updateUserRoles)).Methods("PUT") api.BaseRoutes.User.Handle("/active", api.ApiSessionRequired(updateUserActive)).Methods("PUT") api.BaseRoutes.User.Handle("/password", api.ApiSessionRequired(updatePassword)).Methods("PUT") + api.BaseRoutes.User.Handle("/promote", api.ApiSessionRequired(promoteGuestToUser)).Methods("POST") + api.BaseRoutes.User.Handle("/demote", api.ApiSessionRequired(demoteUserToGuest)).Methods("POST") api.BaseRoutes.Users.Handle("/password/reset", api.ApiHandler(resetPassword)).Methods("POST") api.BaseRoutes.Users.Handle("/password/reset/send", api.ApiHandler(sendPasswordReset)).Methods("POST") api.BaseRoutes.Users.Handle("/email/verify", api.ApiHandler(verifyUserEmail)).Methods("POST") @@ -92,7 +94,24 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) { var ruser *model.User var err *model.AppError if len(tokenId) > 0 { - ruser, err = c.App.CreateUserWithToken(user, tokenId) + var token *model.Token + token, err = c.App.Srv.Store.Token().GetByToken(tokenId) + if err != nil { + c.Err = model.NewAppError("CreateUserWithToken", "api.user.create_user.signup_link_invalid.app_error", nil, err.Error(), http.StatusBadRequest) + return + } + + if token.Type == app.TOKEN_TYPE_GUEST_INVITATION { + if c.App.License() == nil { + c.Err = model.NewAppError("CreateUserWithToken", "api.user.create_user.guest_accounts.license.app_error", nil, "", http.StatusBadRequest) + return + } + if !*c.App.Config().GuestAccountsSettings.Enable { + c.Err = model.NewAppError("CreateUserWithToken", "api.user.create_user.guest_accounts.disabled.app_error", nil, "", http.StatusBadRequest) + return + } + } + ruser, err = c.App.CreateUserWithToken(user, token) } else if len(inviteId) > 0 { ruser, err = c.App.CreateUserWithInviteId(user, inviteId) } else if c.IsSystemAdmin() { @@ -1372,6 +1391,17 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) { return } + if user.IsGuest() { + if c.App.License() == nil { + c.Err = model.NewAppError("login", "api.user.login.guest_accounts.license.error", nil, "", http.StatusUnauthorized) + return + } + if !*c.App.Config().GuestAccountsSettings.Enable { + c.Err = model.NewAppError("login", "api.user.login.guest_accounts.disabled.error", nil, "", http.StatusUnauthorized) + return + } + } + c.LogAuditWithUserId(user.Id, "authenticated") session, err := c.App.DoLogin(w, r, user, deviceId) @@ -1941,3 +1971,83 @@ func getUserTermsOfService(c *Context, w http.ResponseWriter, r *http.Request) { } w.Write([]byte(result.ToJson())) } + +func promoteGuestToUser(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireUserId() + if c.Err != nil { + return + } + + if c.App.License() == nil { + c.Err = model.NewAppError("Api4.promoteGuestToUser", "api.team.promote_guest_to_user.license.error", nil, "", http.StatusNotImplemented) + return + } + + if !*c.App.Config().GuestAccountsSettings.Enable { + c.Err = model.NewAppError("Api4.promoteGuestToUser", "api.team.promote_guest_to_user.disabled.error", nil, "", http.StatusNotImplemented) + return + } + + if !c.App.SessionHasPermissionTo(c.App.Session, model.PERMISSION_PROMOTE_GUEST) { + c.SetPermissionError(model.PERMISSION_PROMOTE_GUEST) + return + } + + user, err := c.App.GetUser(c.Params.UserId) + if err != nil { + c.Err = err + return + } + + if !user.IsGuest() { + c.Err = model.NewAppError("Api4.promoteGuestToUser", "api.user.promote_guest_to_user.no_guest.app_error", nil, "", http.StatusNotImplemented) + return + } + + if err := c.App.PromoteGuestToUser(user, c.App.Session.UserId); err != nil { + c.Err = err + return + } + + ReturnStatusOK(w) +} + +func demoteUserToGuest(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireUserId() + if c.Err != nil { + return + } + + if c.App.License() == nil { + c.Err = model.NewAppError("Api4.demoteUserToGuest", "api.team.demote_user_to_guest.license.error", nil, "", http.StatusNotImplemented) + return + } + + if !*c.App.Config().GuestAccountsSettings.Enable { + c.Err = model.NewAppError("Api4.demoteUserToGuest", "api.team.demote_user_to_guest.disabled.error", nil, "", http.StatusNotImplemented) + return + } + + if !c.App.SessionHasPermissionTo(c.App.Session, model.PERMISSION_DEMOTE_TO_GUEST) { + c.SetPermissionError(model.PERMISSION_DEMOTE_TO_GUEST) + return + } + + user, err := c.App.GetUser(c.Params.UserId) + if err != nil { + c.Err = err + return + } + + if user.IsGuest() { + c.Err = model.NewAppError("Api4.demoteUserToGuest", "api.user.demote_user_to_guest.already_guest.app_error", nil, "", http.StatusNotImplemented) + return + } + + if err := c.App.DemoteUserToGuest(user); err != nil { + c.Err = err + return + } + + ReturnStatusOK(w) +} diff --git a/app/app_test.go b/app/app_test.go index 228af06ab2..647d2dc4b3 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -158,6 +158,9 @@ func TestDoAdvancedPermissionsMigration(t *testing.T) { model.PERMISSION_EDIT_OTHERS_POSTS.Id, model.PERMISSION_MANAGE_OAUTH.Id, model.PERMISSION_INVITE_USER.Id, + model.PERMISSION_INVITE_GUEST.Id, + model.PERMISSION_PROMOTE_GUEST.Id, + model.PERMISSION_DEMOTE_TO_GUEST.Id, model.PERMISSION_DELETE_POST.Id, model.PERMISSION_DELETE_OTHERS_POSTS.Id, model.PERMISSION_CREATE_TEAM.Id, @@ -340,6 +343,9 @@ func TestDoAdvancedPermissionsMigration(t *testing.T) { model.PERMISSION_EDIT_OTHERS_POSTS.Id, model.PERMISSION_MANAGE_OAUTH.Id, model.PERMISSION_INVITE_USER.Id, + model.PERMISSION_INVITE_GUEST.Id, + model.PERMISSION_PROMOTE_GUEST.Id, + model.PERMISSION_DEMOTE_TO_GUEST.Id, model.PERMISSION_DELETE_POST.Id, model.PERMISSION_DELETE_OTHERS_POSTS.Id, model.PERMISSION_CREATE_TEAM.Id, @@ -484,6 +490,9 @@ func TestDoEmojisPermissionsMigration(t *testing.T) { model.PERMISSION_EDIT_OTHERS_POSTS.Id, model.PERMISSION_MANAGE_OAUTH.Id, model.PERMISSION_INVITE_USER.Id, + model.PERMISSION_INVITE_GUEST.Id, + model.PERMISSION_PROMOTE_GUEST.Id, + model.PERMISSION_DEMOTE_TO_GUEST.Id, model.PERMISSION_DELETE_POST.Id, model.PERMISSION_DELETE_OTHERS_POSTS.Id, model.PERMISSION_CREATE_TEAM.Id, diff --git a/app/channel.go b/app/channel.go index a3bcb67407..b5ccfab098 100644 --- a/app/channel.go +++ b/app/channel.go @@ -1317,6 +1317,10 @@ func (a *App) GetChannelMemberCount(channelId string) (int64, *model.AppError) { return a.Srv.Store.Channel().GetMemberCount(channelId, true) } +func (a *App) GetChannelGuestCount(channelId string) (int64, *model.AppError) { + return a.Srv.Store.Channel().GetGuestCount(channelId, true) +} + func (a *App) GetChannelCounts(teamId string, userId string) (*model.ChannelCounts, *model.AppError) { return a.Srv.Store.Channel().GetChannelCounts(teamId, userId) } @@ -1577,8 +1581,16 @@ func (a *App) postRemoveFromChannelMessage(removerUserId string, removedUser *mo } func (a *App) removeUserFromChannel(userIdToRemove string, removerUserId string, channel *model.Channel) *model.AppError { + user, err := a.Srv.Store.User().Get(userIdToRemove) + if err != nil { + return err + } + isGuest := user.IsGuest() + if channel.Name == model.DEFAULT_CHANNEL { - return model.NewAppError("RemoveUserFromChannel", "api.channel.remove.default.app_error", map[string]interface{}{"Channel": model.DEFAULT_CHANNEL}, "", http.StatusBadRequest) + if !isGuest { + return model.NewAppError("RemoveUserFromChannel", "api.channel.remove.default.app_error", map[string]interface{}{"Channel": model.DEFAULT_CHANNEL}, "", http.StatusBadRequest) + } } if channel.IsGroupConstrained() && userIdToRemove != removerUserId { @@ -1603,6 +1615,23 @@ func (a *App) removeUserFromChannel(userIdToRemove string, removerUserId string, return err } + if isGuest { + currentMembers, err := a.GetChannelMembersForUser(channel.TeamId, userIdToRemove) + if err != nil { + return err + } + if len(*currentMembers) == 0 { + teamMember, err := a.GetTeamMember(channel.TeamId, userIdToRemove) + if err != nil { + return model.NewAppError("removeUserFromChannel", "api.team.remove_user_from_team.missing.app_error", nil, err.Error(), http.StatusBadRequest) + } + + if err = a.RemoveTeamMemberFromTeam(teamMember, removerUserId); err != nil { + return err + } + } + } + a.InvalidateCacheForUser(userIdToRemove) a.InvalidateCacheForChannelMembers(channel.Id) diff --git a/app/channel_test.go b/app/channel_test.go index cb91cfe72a..7414eb05fa 100644 --- a/app/channel_test.go +++ b/app/channel_test.go @@ -437,6 +437,63 @@ func TestAddUserToChannelCreatesChannelMemberHistoryRecord(t *testing.T) { assert.NotNil(t, histories[0].LeaveTime) }*/ +func TestLeaveDefaultChannel(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + guest := th.CreateGuest() + th.LinkUserToTeam(guest, th.BasicTeam) + + townSquare, err := th.App.GetChannelByName("town-square", th.BasicTeam.Id, false) + require.Nil(t, err) + th.AddUserToChannel(guest, townSquare) + th.AddUserToChannel(th.BasicUser, townSquare) + + t.Run("User tries to leave the default channel", func(t *testing.T) { + err = th.App.LeaveChannel(townSquare.Id, th.BasicUser.Id) + assert.NotNil(t, err, "It should fail to remove a regular user from the default channel") + assert.Equal(t, err.Id, "api.channel.remove.default.app_error") + _, err = th.App.GetChannelMember(townSquare.Id, th.BasicUser.Id) + assert.Nil(t, err) + }) + + t.Run("Guest leaves the default channel", func(t *testing.T) { + err = th.App.LeaveChannel(townSquare.Id, guest.Id) + assert.Nil(t, err, "It should allow to remove a guest user from the default channel") + _, err = th.App.GetChannelMember(townSquare.Id, guest.Id) + assert.NotNil(t, err) + }) +} + +func TestLeaveLastChannel(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + guest := th.CreateGuest() + th.LinkUserToTeam(guest, th.BasicTeam) + + townSquare, err := th.App.GetChannelByName("town-square", th.BasicTeam.Id, false) + require.Nil(t, err) + th.AddUserToChannel(guest, townSquare) + th.AddUserToChannel(guest, th.BasicChannel) + + t.Run("Guest leaves not last channel", func(t *testing.T) { + err = th.App.LeaveChannel(townSquare.Id, guest.Id) + require.Nil(t, err) + _, err = th.App.GetTeamMember(th.BasicTeam.Id, guest.Id) + assert.Nil(t, err, "It should maintain the team membership") + }) + + t.Run("Guest leaves last channel", func(t *testing.T) { + err = th.App.LeaveChannel(th.BasicChannel.Id, guest.Id) + assert.Nil(t, err, "It should allow to remove a guest user from the default channel") + _, err = th.App.GetChannelMember(th.BasicChannel.Id, guest.Id) + assert.NotNil(t, err) + _, err = th.App.GetTeamMember(th.BasicTeam.Id, guest.Id) + assert.Nil(t, err, "It should remove the team membership") + }) +} + func TestAddChannelMemberNoUserRequestor(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() diff --git a/app/command_leave.go b/app/command_leave.go index f3527eb6ff..3811cbbd9a 100644 --- a/app/command_leave.go +++ b/app/command_leave.go @@ -38,18 +38,41 @@ func (me *LeaveProvider) DoCommand(a *App, args *model.CommandArgs, message stri if channel, noChannelErr = a.GetChannel(args.ChannelId); noChannelErr != nil { return &model.CommandResponse{Text: args.T("api.command_leave.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } - if channel.Name == model.DEFAULT_CHANNEL { - return &model.CommandResponse{Text: args.T("api.channel.leave.default.app_error", map[string]interface{}{"Channel": model.DEFAULT_CHANNEL}), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} - } - err := a.LeaveChannel(args.ChannelId, args.UserId) - if err != nil { - return &model.CommandResponse{Text: args.T("api.command_leave.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} - } team, err := a.GetTeam(args.TeamId) if err != nil { return &model.CommandResponse{Text: args.T("api.command_leave.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } + err = a.LeaveChannel(args.ChannelId, args.UserId) + if err != nil { + if channel.Name == model.DEFAULT_CHANNEL { + return &model.CommandResponse{Text: args.T("api.channel.leave.default.app_error", map[string]interface{}{"Channel": model.DEFAULT_CHANNEL}), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + return &model.CommandResponse{Text: args.T("api.command_leave.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + + member, err := a.GetTeamMember(team.Id, args.UserId) + if err != nil || member.DeleteAt != 0 { + return &model.CommandResponse{GotoLocation: args.SiteURL + "/"} + } + + user, err := a.GetUser(args.UserId) + if err != nil { + return &model.CommandResponse{Text: args.T("api.command_leave.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + + if user.IsGuest() { + members, err := a.GetChannelMembersForUser(team.Id, args.UserId) + if err != nil || len(*members) == 0 { + return &model.CommandResponse{Text: args.T("api.command_leave.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + channel, err := a.GetChannel((*members)[0].ChannelId) + if err != nil { + return &model.CommandResponse{Text: args.T("api.command_leave.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + return &model.CommandResponse{GotoLocation: args.SiteURL + "/" + team.Name + "/channels/" + channel.Name} + } + return &model.CommandResponse{GotoLocation: args.SiteURL + "/" + team.Name + "/channels/" + model.DEFAULT_CHANNEL} } diff --git a/app/command_leave_test.go b/app/command_leave_test.go index a99f49dbbf..82ac179a19 100644 --- a/app/command_leave_test.go +++ b/app/command_leave_test.go @@ -5,9 +5,9 @@ package app import ( "testing" - "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/mattermost/mattermost-server/model" ) @@ -34,53 +34,114 @@ func TestLeaveProviderDoCommand(t *testing.T) { CreatorId: th.BasicUser.Id, }, false) + defaultChannel, err := th.App.GetChannelByName(model.DEFAULT_CHANNEL, th.BasicTeam.Id, false) + require.Nil(t, err) + + guest := th.CreateGuest() + th.App.AddUserToTeam(th.BasicTeam.Id, th.BasicUser.Id, th.BasicUser.Id) th.App.AddUserToChannel(th.BasicUser, publicChannel) th.App.AddUserToChannel(th.BasicUser, privateChannel) + th.App.AddUserToTeam(th.BasicTeam.Id, guest.Id, guest.Id) + th.App.AddUserToChannel(guest, publicChannel) + th.App.AddUserToChannel(guest, defaultChannel) - args := &model.CommandArgs{ - T: func(s string, args ...interface{}) string { return s }, - } + t.Run("Should error when no Channel ID in args", func(t *testing.T) { + args := &model.CommandArgs{ + UserId: th.BasicUser.Id, + T: func(s string, args ...interface{}) string { return s }, + } + actual := lp.DoCommand(th.App, args, "") + assert.Equal(t, "api.command_leave.fail.app_error", actual.Text) + assert.Equal(t, model.COMMAND_RESPONSE_TYPE_EPHEMERAL, actual.ResponseType) + }) - // Should error when no Channel ID in args - actual := lp.DoCommand(th.App, args, "") - assert.Equal(t, "api.command_leave.fail.app_error", actual.Text) - assert.Equal(t, model.COMMAND_RESPONSE_TYPE_EPHEMERAL, actual.ResponseType) + t.Run("Should error when no Team ID in args", func(t *testing.T) { + args := &model.CommandArgs{ + UserId: th.BasicUser.Id, + ChannelId: publicChannel.Id, + T: func(s string, args ...interface{}) string { return s }, + } + actual := lp.DoCommand(th.App, args, "") + assert.Equal(t, "api.command_leave.fail.app_error", actual.Text) + assert.Equal(t, model.COMMAND_RESPONSE_TYPE_EPHEMERAL, actual.ResponseType) + }) - // Should error when no Team ID in args - args.ChannelId = publicChannel.Id - actual = lp.DoCommand(th.App, args, "") - assert.Equal(t, "api.command_leave.fail.app_error", actual.Text) - assert.Equal(t, model.COMMAND_RESPONSE_TYPE_EPHEMERAL, actual.ResponseType) + t.Run("Leave a public channel", func(t *testing.T) { + args := &model.CommandArgs{ + UserId: th.BasicUser.Id, + ChannelId: publicChannel.Id, + T: func(s string, args ...interface{}) string { return s }, + TeamId: th.BasicTeam.Id, + SiteURL: "http://localhost:8065", + } + actual := lp.DoCommand(th.App, args, "") + assert.Equal(t, "", actual.Text) + assert.Equal(t, args.SiteURL+"/"+th.BasicTeam.Name+"/channels/"+model.DEFAULT_CHANNEL, actual.GotoLocation) + assert.Equal(t, "", actual.ResponseType) - // Leave a public channel - siteURL := "http://localhost:8065" - args.TeamId = th.BasicTeam.Id - args.SiteURL = siteURL - actual = lp.DoCommand(th.App, args, "") - assert.Equal(t, "", actual.Text) - assert.Equal(t, siteURL+"/"+th.BasicTeam.Name+"/channels/"+model.DEFAULT_CHANNEL, actual.GotoLocation) - assert.Equal(t, "", actual.ResponseType) + _, err = th.App.GetChannelMember(publicChannel.Id, th.BasicUser.Id) + assert.NotNil(t, err) + assert.NotNil(t, err.Id, "store.sql_channel.get_member.missing.app_error") + }) - time.Sleep(100 * time.Millisecond) + t.Run("Leave a private channel", func(t *testing.T) { + args := &model.CommandArgs{ + UserId: th.BasicUser.Id, + ChannelId: privateChannel.Id, + T: func(s string, args ...interface{}) string { return s }, + TeamId: th.BasicTeam.Id, + SiteURL: "http://localhost:8065", + } + actual := lp.DoCommand(th.App, args, "") + assert.Equal(t, "", actual.Text) + }) - member, err := th.App.GetChannelMember(publicChannel.Id, th.BasicUser.Id) - if member == nil { - t.Errorf("Expected member object, got nil") - } + t.Run("Should not leave a default channel", func(t *testing.T) { + args := &model.CommandArgs{ + UserId: th.BasicUser.Id, + ChannelId: defaultChannel.Id, + T: func(s string, args ...interface{}) string { return s }, + TeamId: th.BasicTeam.Id, + SiteURL: "http://localhost:8065", + } + actual := lp.DoCommand(th.App, args, "") + assert.Equal(t, "api.channel.leave.default.app_error", actual.Text) + }) - if err != nil { - t.Errorf("Expected nil object, got %s", err) - } + t.Run("Should allow to leave a default channel if user is guest", func(t *testing.T) { + args := &model.CommandArgs{ + UserId: guest.Id, + ChannelId: defaultChannel.Id, + T: func(s string, args ...interface{}) string { return s }, + TeamId: th.BasicTeam.Id, + SiteURL: "http://localhost:8065", + } + actual := lp.DoCommand(th.App, args, "") + assert.Equal(t, "", actual.Text) + assert.Equal(t, args.SiteURL+"/"+th.BasicTeam.Name+"/channels/"+publicChannel.Name, actual.GotoLocation) + assert.Equal(t, "", actual.ResponseType) - // Leave a private channel - args.ChannelId = privateChannel.Id - actual = lp.DoCommand(th.App, args, "") - assert.Equal(t, "", actual.Text) + _, err = th.App.GetChannelMember(defaultChannel.Id, guest.Id) + assert.NotNil(t, err) + assert.NotNil(t, err.Id, "store.sql_channel.get_member.missing.app_error") + }) - // Should not leave a default channel - defaultChannel, _ := th.App.GetChannelByName(model.DEFAULT_CHANNEL, th.BasicTeam.Id, false) - args.ChannelId = defaultChannel.Id - actual = lp.DoCommand(th.App, args, "") - assert.Equal(t, "api.channel.leave.default.app_error", actual.Text) + t.Run("Should redirect to the team if is the last channel", func(t *testing.T) { + args := &model.CommandArgs{ + UserId: guest.Id, + ChannelId: publicChannel.Id, + T: func(s string, args ...interface{}) string { return s }, + TeamId: th.BasicTeam.Id, + SiteURL: "http://localhost:8065", + } + actual := lp.DoCommand(th.App, args, "") + assert.Equal(t, "", actual.Text) + assert.Equal(t, args.SiteURL+"/", actual.GotoLocation) + assert.Equal(t, "", actual.ResponseType) + + _, err = th.App.GetChannelMember(publicChannel.Id, guest.Id) + assert.NotNil(t, err) + assert.NotNil(t, err.Id, "store.sql_channel.get_member.missing.app_error") + }) } diff --git a/app/email.go b/app/email.go index 4aaf225ea8..a58b4f092e 100644 --- a/app/email.go +++ b/app/email.go @@ -7,6 +7,7 @@ import ( "fmt" "net/url" "path" + "strings" "net/http" @@ -357,6 +358,87 @@ func (a *App) SendInviteEmails(team *model.Team, senderName string, senderUserId } } +func (a *App) SendGuestInviteEmails(team *model.Team, channels []*model.Channel, senderName string, senderUserId string, invites []string, siteURL string, message string) { + if a.Srv.EmailRateLimiter == nil { + a.Log.Error("Email invite not sent, rate limiting could not be setup.", mlog.String("user_id", senderUserId), mlog.String("team_id", team.Id)) + return + } + rateLimited, result, err := a.Srv.EmailRateLimiter.RateLimit(senderUserId, len(invites)) + if err != nil { + a.Log.Error("Error rate limiting invite email.", mlog.String("user_id", senderUserId), mlog.String("team_id", team.Id), mlog.Err(err)) + return + } + + if rateLimited { + a.Log.Error("Invite emails rate limited.", + mlog.String("user_id", senderUserId), + mlog.String("team_id", team.Id), + mlog.String("retry_after", result.RetryAfter.String()), + mlog.Err(err)) + return + } + + for _, invite := range invites { + if len(invite) > 0 { + senderRole := utils.T("api.team.invite_members.member") + + subject := utils.T("api.templates.invite_guest_subject", + map[string]interface{}{"SenderName": senderName, + "TeamDisplayName": team.DisplayName, + "SiteName": a.ClientConfig()["SiteName"]}) + + bodyPage := a.NewEmailTemplate("invite_body", model.DEFAULT_LOCALE) + bodyPage.Props["SiteURL"] = siteURL + bodyPage.Props["Title"] = utils.T("api.templates.invite_body.title") + bodyPage.Html["Info"] = utils.TranslateAsHtml(utils.T, "api.templates.invite_body_guest.info", + map[string]interface{}{"SenderStatus": senderRole, "SenderName": senderName, "TeamDisplayName": team.DisplayName}) + bodyPage.Props["Button"] = utils.T("api.templates.invite_body.button") + bodyPage.Props["Message"] = "" + if message != "" { + bodyPage.Props["Message"] = message + } + bodyPage.Html["ExtraInfo"] = utils.TranslateAsHtml(utils.T, "api.templates.invite_body.extra_info", + map[string]interface{}{"TeamDisplayName": team.DisplayName}) + bodyPage.Props["TeamURL"] = siteURL + "/" + team.Name + + channelIds := []string{} + for _, channel := range channels { + channelIds = append(channelIds, channel.Id) + } + + token := model.NewToken( + TOKEN_TYPE_GUEST_INVITATION, + model.MapToJson(map[string]string{ + "teamId": team.Id, + "channels": strings.Join(channelIds, " "), + "email": invite, + "guest": "true", + }), + ) + + props := make(map[string]string) + props["email"] = invite + props["display_name"] = team.DisplayName + props["name"] = team.Name + data := model.MapToJson(props) + + if err := a.Srv.Store.Token().Save(token); err != nil { + mlog.Error(fmt.Sprintf("Failed to send invite email successfully err=%v", err)) + continue + } + bodyPage.Props["Link"] = fmt.Sprintf("%s/signup_user_complete/?d=%s&t=%s", siteURL, url.QueryEscape(data), url.QueryEscape(token.Token)) + + if !*a.Config().EmailSettings.SendEmailNotifications { + mlog.Info(fmt.Sprintf("sending invitation to %v %v", invite, bodyPage.Props["Link"])) + } + + if err := a.SendMail(invite, subject, bodyPage.Render()); err != nil { + mlog.Error(fmt.Sprintf("Failed to send invite email successfully err=%v", err)) + } + } + } +} + func (a *App) NewEmailTemplate(name, locale string) *utils.HTMLTemplate { t := utils.NewHTMLTemplate(a.HTMLTemplates(), name) diff --git a/app/login.go b/app/login.go index a05c938696..8b4a7fcaf7 100644 --- a/app/login.go +++ b/app/login.go @@ -149,6 +149,11 @@ func (a *App) DoLogin(w http.ResponseWriter, r *http.Request, user *model.User, session.AddProp(model.SESSION_PROP_PLATFORM, plat) session.AddProp(model.SESSION_PROP_OS, os) session.AddProp(model.SESSION_PROP_BROWSER, fmt.Sprintf("%v/%v", bname, bversion)) + if user.IsGuest() { + session.AddProp(model.SESSION_PROP_IS_GUEST, "true") + } else { + session.AddProp(model.SESSION_PROP_IS_GUEST, "false") + } var err *model.AppError if session, err = a.CreateSession(session); err != nil { diff --git a/app/permissions_migrations.go b/app/permissions_migrations.go index d222dc733c..31ecc8318f 100644 --- a/app/permissions_migrations.go +++ b/app/permissions_migrations.go @@ -23,6 +23,7 @@ const ( MIGRATION_KEY_APPLY_CHANNEL_MANAGE_DELETE_TO_CHANNEL_USER = "apply_channel_manage_delete_to_channel_user" MIGRATION_KEY_REMOVE_CHANNEL_MANAGE_DELETE_FROM_TEAM_USER = "remove_channel_manage_delete_from_team_user" MIGRATION_KEY_VIEW_MEMBERS_NEW_PERMISSION = "view_members_new_permission" + MIGRATION_KEY_ADD_MANAGE_GUESTS_PERMISSIONS = "add_manage_guests_permissions" PERMISSION_MANAGE_SYSTEM = "manage_system" PERMISSION_MANAGE_EMOJIS = "manage_emojis" @@ -51,6 +52,10 @@ const ( PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES = "manage_public_channel_properties" PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES = "manage_private_channel_properties" PERMISSION_VIEW_MEMBERS = "view_members" + PERMISSION_INVITE_USER = "invite_user" + PERMISSION_INVITE_GUEST = "invite_guest" + PERMISSION_PROMOTE_GUEST = "promote_guest" + PERMISSION_DEMOTE_TO_GUEST = "demote_to_guest" ) func isRole(role string) func(string, map[string]map[string]bool) bool { @@ -273,6 +278,19 @@ func getViewMembersPermissionMigration() permissionsMap { } } +func getAddManageGuestsPermissionsMigration() permissionsMap { + return permissionsMap{ + permissionTransformation{ + On: isRole(model.SYSTEM_ADMIN_ROLE_ID), + Add: []string{PERMISSION_PROMOTE_GUEST, PERMISSION_DEMOTE_TO_GUEST}, + }, + permissionTransformation{ + On: permissionExists(PERMISSION_INVITE_USER), + Add: []string{PERMISSION_INVITE_GUEST}, + }, + } +} + // DoPermissionsMigrations execute all the permissions migrations need by the current version. func (a *App) DoPermissionsMigrations() *model.AppError { PermissionsMigrations := []struct { @@ -287,6 +305,7 @@ func (a *App) DoPermissionsMigrations() *model.AppError { {Key: MIGRATION_KEY_APPLY_CHANNEL_MANAGE_DELETE_TO_CHANNEL_USER, Migration: applyChannelManageDeleteToChannelUser}, {Key: MIGRATION_KEY_REMOVE_CHANNEL_MANAGE_DELETE_FROM_TEAM_USER, Migration: removeChannelManageDeleteFromTeamUser}, {Key: MIGRATION_KEY_VIEW_MEMBERS_NEW_PERMISSION, Migration: getViewMembersPermissionMigration}, + {Key: MIGRATION_KEY_ADD_MANAGE_GUESTS_PERMISSIONS, Migration: getAddManageGuestsPermissionsMigration}, } for _, migration := range PermissionsMigrations { diff --git a/app/plugin_api.go b/app/plugin_api.go index d8c60abe77..3794d0cc47 100644 --- a/app/plugin_api.go +++ b/app/plugin_api.go @@ -342,7 +342,11 @@ func (api *PluginAPI) GetChannelStats(channelId string) (*model.ChannelStats, *m if err != nil { return nil, err } - return &model.ChannelStats{ChannelId: channelId, MemberCount: memberCount}, nil + guestCount, err := api.app.GetChannelMemberCount(channelId) + if err != nil { + return nil, err + } + return &model.ChannelStats{ChannelId: channelId, MemberCount: memberCount, GuestCount: guestCount}, nil } func (api *PluginAPI) GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.AppError) { diff --git a/app/session.go b/app/session.go index 1f3f38457c..697b0e3ac4 100644 --- a/app/session.go +++ b/app/session.go @@ -93,6 +93,27 @@ func (a *App) GetSessions(userId string) ([]*model.Session, *model.AppError) { return a.Srv.Store.Session().GetSessions(userId) } +func (a *App) UpdateSessionsIsGuest(userId string, isGuest bool) { + sessions, err := a.Srv.Store.Session().GetSessions(userId) + if err != nil { + mlog.Error(fmt.Sprintf("Unable to get user sessions: userId=%s err=%s", userId, err.Error())) + } + + for _, session := range sessions { + if isGuest { + session.AddProp(model.SESSION_PROP_IS_GUEST, "true") + } else { + session.AddProp(model.SESSION_PROP_IS_GUEST, "false") + } + err := a.Srv.Store.Session().UpdateProps(session) + if err != nil { + mlog.Error(fmt.Sprintf("Unable to update isGuest session: %s", err.Error())) + continue + } + a.AddSessionToCache(session) + } +} + func (a *App) RevokeAllSessions(userId string) *model.AppError { sessions, err := a.Srv.Store.Session().GetSessions(userId) if err != nil { @@ -329,6 +350,11 @@ func (a *App) createSessionForUserAccessToken(tokenString string) (*model.Sessio if user.IsBot { session.AddProp(model.SESSION_PROP_IS_BOT, model.SESSION_PROP_IS_BOT_VALUE) } + if user.IsGuest() { + session.AddProp(model.SESSION_PROP_IS_GUEST, "true") + } else { + session.AddProp(model.SESSION_PROP_IS_GUEST, "false") + } session.SetExpireInDays(model.SESSION_USER_ACCESS_TOKEN_EXPIRY) session, err = a.Srv.Store.Session().Save(session) diff --git a/app/session_test.go b/app/session_test.go index bcb7b0f0c2..a268c1bb9f 100644 --- a/app/session_test.go +++ b/app/session_test.go @@ -123,3 +123,57 @@ func TestGetSessionIdleTimeoutInMinutes(t *testing.T) { _, err = th.App.GetSession(session.Token) assert.Nil(t, err) } + +func TestUpdateSessionOnPromoteDemote(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + th.App.SetLicense(model.NewTestLicense()) + + t.Run("Promote Guest to User updates the session", func(t *testing.T) { + guest := th.CreateGuest() + + session, err := th.App.CreateSession(&model.Session{UserId: guest.Id, Props: model.StringMap{model.SESSION_PROP_IS_GUEST: "true"}}) + require.Nil(t, err) + + rsession, err := th.App.GetSession(session.Token) + require.Nil(t, err) + assert.Equal(t, "true", rsession.Props[model.SESSION_PROP_IS_GUEST]) + + err = th.App.PromoteGuestToUser(guest, th.BasicUser.Id) + require.Nil(t, err) + + rsession, err = th.App.GetSession(session.Token) + require.Nil(t, err) + assert.Equal(t, "false", rsession.Props[model.SESSION_PROP_IS_GUEST]) + + th.App.ClearSessionCacheForUser(session.UserId) + + rsession, err = th.App.GetSession(session.Token) + require.Nil(t, err) + assert.Equal(t, "false", rsession.Props[model.SESSION_PROP_IS_GUEST]) + }) + + t.Run("Demote User to Guest updates the session", func(t *testing.T) { + user := th.CreateUser() + + session, err := th.App.CreateSession(&model.Session{UserId: user.Id, Props: model.StringMap{model.SESSION_PROP_IS_GUEST: "false"}}) + require.Nil(t, err) + + rsession, err := th.App.GetSession(session.Token) + require.Nil(t, err) + assert.Equal(t, "false", rsession.Props[model.SESSION_PROP_IS_GUEST]) + + err = th.App.DemoteUserToGuest(user) + require.Nil(t, err) + + rsession, err = th.App.GetSession(session.Token) + require.Nil(t, err) + assert.Equal(t, "true", rsession.Props[model.SESSION_PROP_IS_GUEST]) + + th.App.ClearSessionCacheForUser(session.UserId) + rsession, err = th.App.GetSession(session.Token) + require.Nil(t, err) + assert.Equal(t, "true", rsession.Props[model.SESSION_PROP_IS_GUEST]) + }) +} diff --git a/app/team.go b/app/team.go index 0e2fbd107c..41a981b5fa 100644 --- a/app/team.go +++ b/app/team.go @@ -421,11 +421,11 @@ func (a *App) AddUserToTeamByToken(userId string, tokenId string) (*model.Team, return nil, model.NewAppError("AddUserToTeamByToken", "api.user.create_user.signup_link_invalid.app_error", nil, err.Error(), http.StatusBadRequest) } - if token.Type != TOKEN_TYPE_TEAM_INVITATION { + if token.Type != TOKEN_TYPE_TEAM_INVITATION && token.Type != TOKEN_TYPE_GUEST_INVITATION { return nil, model.NewAppError("AddUserToTeamByToken", "api.user.create_user.signup_link_invalid.app_error", nil, "", http.StatusBadRequest) } - if model.GetMillis()-token.CreateAt >= TEAM_INVITATION_EXPIRY_TIME { + if model.GetMillis()-token.CreateAt >= INVITATION_EXPIRY_TIME { a.DeleteToken(token) return nil, model.NewAppError("AddUserToTeamByToken", "api.user.create_user.signup_link_expired.app_error", nil, "", http.StatusBadRequest) } @@ -462,10 +462,31 @@ func (a *App) AddUserToTeamByToken(userId string, tokenId string) (*model.Team, } user := result.Data.(*model.User) + if user.IsGuest() && token.Type == TOKEN_TYPE_TEAM_INVITATION { + return nil, model.NewAppError("AddUserToTeamByToken", "api.user.create_user.invalid_invitation_type.app_error", nil, "", http.StatusBadRequest) + } + if !user.IsGuest() && token.Type == TOKEN_TYPE_GUEST_INVITATION { + return nil, model.NewAppError("AddUserToTeamByToken", "api.user.create_user.invalid_invitation_type.app_error", nil, "", http.StatusBadRequest) + } + if err := a.JoinUserToTeam(team, user, ""); err != nil { return nil, err } + if token.Type == TOKEN_TYPE_GUEST_INVITATION { + channels, err := a.Srv.Store.Channel().GetChannelsByIds(strings.Split(tokenData["channels"], " ")) + if err != nil { + return nil, err + } + + for _, channel := range channels { + _, err := a.AddUserToChannel(user, channel) + if err != nil { + mlog.Error(err.Error()) + } + } + } + if err := a.DeleteToken(token); err != nil { return nil, err } @@ -589,9 +610,11 @@ func (a *App) JoinUserToTeam(team *model.Team, user *model.User, userRequestorId shouldBeAdmin := team.Email == user.Email - // Soft error if there is an issue joining the default channels - if err := a.JoinDefaultChannels(team.Id, user, shouldBeAdmin, userRequestorId); err != nil { - mlog.Error(fmt.Sprintf("Encountered an issue joining default channels err=%v", err), mlog.String("user_id", user.Id), mlog.String("team_id", team.Id)) + if !user.IsGuest() { + // Soft error if there is an issue joining the default channels + if err := a.JoinDefaultChannels(team.Id, user, shouldBeAdmin, userRequestorId); err != nil { + mlog.Error(fmt.Sprintf("Encountered an issue joining default channels err=%v", err), mlog.String("user_id", user.Id), mlog.String("team_id", team.Id)) + } } a.ClearSessionCacheForUser(user.Id) @@ -824,54 +847,18 @@ func (a *App) RemoveUserFromTeam(teamId string, userId string, requestorId strin return nil } -func (a *App) LeaveTeam(team *model.Team, user *model.User, requestorId string) *model.AppError { - teamMember, err := a.GetTeamMember(team.Id, user.Id) - if err != nil { - return model.NewAppError("LeaveTeam", "api.team.remove_user_from_team.missing.app_error", nil, err.Error(), http.StatusBadRequest) - } +func (a *App) RemoveTeamMemberFromTeam(teamMember *model.TeamMember, requestorId string) *model.AppError { + // Send the websocket message before we actually do the remove so the user being removed gets it. + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_LEAVE_TEAM, teamMember.TeamId, "", "", nil) + message.Add("user_id", teamMember.UserId) + message.Add("team_id", teamMember.TeamId) + a.Publish(message) - var channelList *model.ChannelList - - if channelList, err = a.Srv.Store.Channel().GetChannels(team.Id, user.Id, true); err != nil { - if err.Id == "store.sql_channel.get_channels.not_found.app_error" { - channelList = &model.ChannelList{} - } else { - return err - } - } - - for _, channel := range *channelList { - if !channel.IsGroupOrDirect() { - a.InvalidateCacheForChannelMembers(channel.Id) - if err = a.Srv.Store.Channel().RemoveMember(channel.Id, user.Id); err != nil { - return err - } - } - } - - channel, err := a.Srv.Store.Channel().GetByName(team.Id, model.DEFAULT_CHANNEL, false) + user, err := a.Srv.Store.User().Get(teamMember.UserId) if err != nil { return err } - if *a.Config().ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages { - if requestorId == user.Id { - if err := a.postLeaveTeamMessage(user, channel); err != nil { - mlog.Error(fmt.Sprint("Failed to post join/leave message", err)) - } - } else { - if err := a.postRemoveFromTeamMessage(user, channel); err != nil { - mlog.Error(fmt.Sprint("Failed to post join/leave message", err)) - } - } - } - - // Send the websocket message before we actually do the remove so the user being removed gets it. - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_LEAVE_TEAM, team.Id, "", "", nil) - message.Add("user_id", user.Id) - message.Add("team_id", team.Id) - a.Publish(message) - teamMember.Roles = "" teamMember.DeleteAt = model.GetMillis() @@ -908,7 +895,7 @@ func (a *App) LeaveTeam(team *model.Team, user *model.User, requestorId string) } // delete the preferences that set the last channel used in the team and other team specific preferences - if err := a.Srv.Store.Preference().DeleteCategory(user.Id, team.Id); err != nil { + if err := a.Srv.Store.Preference().DeleteCategory(user.Id, teamMember.TeamId); err != nil { return err } @@ -919,6 +906,56 @@ func (a *App) LeaveTeam(team *model.Team, user *model.User, requestorId string) return nil } +func (a *App) LeaveTeam(team *model.Team, user *model.User, requestorId string) *model.AppError { + teamMember, err := a.GetTeamMember(team.Id, user.Id) + if err != nil { + return model.NewAppError("LeaveTeam", "api.team.remove_user_from_team.missing.app_error", nil, err.Error(), http.StatusBadRequest) + } + + var channelList *model.ChannelList + + if channelList, err = a.Srv.Store.Channel().GetChannels(team.Id, user.Id, true); err != nil { + if err.Id == "store.sql_channel.get_channels.not_found.app_error" { + channelList = &model.ChannelList{} + } else { + return err + } + } + + for _, channel := range *channelList { + if !channel.IsGroupOrDirect() { + a.InvalidateCacheForChannelMembers(channel.Id) + if err = a.Srv.Store.Channel().RemoveMember(channel.Id, user.Id); err != nil { + return err + } + } + } + + channel, err := a.Srv.Store.Channel().GetByName(team.Id, model.DEFAULT_CHANNEL, false) + if err != nil { + return err + } + + if *a.Config().ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages { + if requestorId == user.Id { + if err = a.postLeaveTeamMessage(user, channel); err != nil { + mlog.Error(fmt.Sprint("Failed to post join/leave message", err)) + } + } else { + if err = a.postRemoveFromTeamMessage(user, channel); err != nil { + mlog.Error(fmt.Sprint("Failed to post join/leave message", err)) + } + } + } + + err = a.RemoveTeamMemberFromTeam(teamMember, requestorId) + if err != nil { + return err + } + + return nil +} + func (a *App) postLeaveTeamMessage(user *model.User, channel *model.Channel) *model.AppError { post := &model.Post{ ChannelId: channel.Id, @@ -1011,6 +1048,77 @@ func (a *App) InviteNewUsersToTeam(emailList []string, teamId, senderId string) return nil } +func (a *App) InviteGuestsToChannels(teamId string, guestsInvite *model.GuestsInvite, senderId string) *model.AppError { + if !*a.Config().ServiceSettings.EnableEmailInvitations { + return model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.disabled.app_error", nil, "", http.StatusNotImplemented) + } + + if err := guestsInvite.IsValid(); err != nil { + return err + } + + tchan := make(chan store.StoreResult, 1) + go func() { + team, err := a.Srv.Store.Team().Get(teamId) + tchan <- store.StoreResult{Data: team, Err: err} + close(tchan) + }() + cchan := make(chan store.StoreResult, 1) + go func() { + channels, err := a.Srv.Store.Channel().GetChannelsByIds(guestsInvite.Channels) + cchan <- store.StoreResult{Data: channels, Err: err} + close(cchan) + }() + uchan := make(chan store.StoreResult, 1) + go func() { + user, err := a.Srv.Store.User().Get(senderId) + uchan <- store.StoreResult{Data: user, Err: err} + close(uchan) + }() + + result := <-cchan + if result.Err != nil { + return result.Err + } + channels := result.Data.([]*model.Channel) + + result = <-uchan + if result.Err != nil { + return result.Err + } + user := result.Data.(*model.User) + + result = <-tchan + if result.Err != nil { + return result.Err + } + team := result.Data.(*model.Team) + + for _, channel := range channels { + if channel.TeamId != teamId { + return model.NewAppError("InviteGuestsToChannels", "api.team.invite_guests.channel_in_invalid_team.app_error", nil, "", http.StatusBadRequest) + } + } + + var invalidEmailList []string + for _, email := range guestsInvite.Emails { + if !a.isTeamEmailAddressAllowed(email, team.AllowedDomains) { + invalidEmailList = append(invalidEmailList, email) + } + } + + if len(invalidEmailList) > 0 { + s := strings.Join(invalidEmailList, ", ") + err := model.NewAppError("InviteGuestsToChannels", "api.team.invite_members.invalid_email.app_error", map[string]interface{}{"Addresses": s}, "", http.StatusBadRequest) + return err + } + + nameFormat := *a.Config().TeamSettings.TeammateNameDisplay + a.SendGuestInviteEmails(team, channels, user.GetDisplayName(nameFormat), user.Id, guestsInvite.Emails, a.GetSiteURL(), guestsInvite.Message) + + return nil +} + func (a *App) FindTeamByName(name string) bool { if _, err := a.Srv.Store.Team().GetByName(name); err != nil { return false @@ -1175,7 +1283,7 @@ func (a *App) GetTeamIdFromQuery(query url.Values) (string, *model.AppError) { return "", model.NewAppError("GetTeamIdFromQuery", "api.oauth.singup_with_oauth.invalid_link.app_error", nil, "", http.StatusBadRequest) } - if model.GetMillis()-token.CreateAt >= TEAM_INVITATION_EXPIRY_TIME { + if model.GetMillis()-token.CreateAt >= INVITATION_EXPIRY_TIME { a.DeleteToken(token) return "", model.NewAppError("GetTeamIdFromQuery", "api.oauth.singup_with_oauth.expired_link.app_error", nil, "", http.StatusBadRequest) } diff --git a/app/team_test.go b/app/team_test.go index efc4245f25..3da47b0c08 100644 --- a/app/team_test.go +++ b/app/team_test.go @@ -193,6 +193,7 @@ func TestAddUserToTeamByToken(t *testing.T) { user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: "passwd1", AuthService: ""} ruser, _ := th.App.CreateUser(&user) + rguest := th.CreateGuest() t.Run("invalid token", func(t *testing.T) { if _, err := th.App.AddUserToTeamByToken(ruser.Id, "123"); err == nil { @@ -217,7 +218,7 @@ func TestAddUserToTeamByToken(t *testing.T) { TOKEN_TYPE_TEAM_INVITATION, model.MapToJson(map[string]string{"teamId": th.BasicTeam.Id}), ) - token.CreateAt = model.GetMillis() - TEAM_INVITATION_EXPIRY_TIME - 1 + token.CreateAt = model.GetMillis() - INVITATION_EXPIRY_TIME - 1 require.Nil(t, th.App.Srv.Store.Token().Save(token)) defer th.App.DeleteToken(token) if _, err := th.App.AddUserToTeamByToken(ruser.Id, token.Token); err == nil { @@ -261,6 +262,49 @@ func TestAddUserToTeamByToken(t *testing.T) { } _, err := th.App.Srv.Store.Token().GetByToken(token.Token) require.NotNil(t, err, "The token must be deleted after be used") + + members, err := th.App.GetChannelMembersForUser(th.BasicTeam.Id, ruser.Id) + require.Nil(t, err) + assert.Len(t, *members, 2) + }) + + t.Run("invalid add a guest using a regular invite", func(t *testing.T) { + token := model.NewToken( + TOKEN_TYPE_TEAM_INVITATION, + model.MapToJson(map[string]string{"teamId": th.BasicTeam.Id}), + ) + require.Nil(t, th.App.Srv.Store.Token().Save(token)) + _, err := th.App.AddUserToTeamByToken(rguest.Id, token.Token) + assert.NotNil(t, err) + }) + + t.Run("invalid add a regular user using a guest invite", func(t *testing.T) { + token := model.NewToken( + TOKEN_TYPE_GUEST_INVITATION, + model.MapToJson(map[string]string{"teamId": th.BasicTeam.Id, "channels": th.BasicChannel.Id}), + ) + require.Nil(t, th.App.Srv.Store.Token().Save(token)) + _, err := th.App.AddUserToTeamByToken(ruser.Id, token.Token) + assert.NotNil(t, err) + }) + + t.Run("valid request from guest invite", func(t *testing.T) { + token := model.NewToken( + TOKEN_TYPE_GUEST_INVITATION, + model.MapToJson(map[string]string{"teamId": th.BasicTeam.Id, "channels": th.BasicChannel.Id}), + ) + require.Nil(t, th.App.Srv.Store.Token().Save(token)) + if _, err := th.App.AddUserToTeamByToken(rguest.Id, token.Token); err != nil { + t.Log(err) + t.Fatal("Should add user to the team") + } + _, err := th.App.Srv.Store.Token().GetByToken(token.Token) + require.NotNil(t, err, "The token must be deleted after be used") + + members, err := th.App.GetChannelMembersForUser(th.BasicTeam.Id, rguest.Id) + require.Nil(t, err) + require.Len(t, *members, 1) + assert.Equal(t, (*members)[0].ChannelId, th.BasicChannel.Id) }) t.Run("group-constrained team", func(t *testing.T) { diff --git a/app/user.go b/app/user.go index 680189aedf..b353b12b3f 100644 --- a/app/user.go +++ b/app/user.go @@ -40,26 +40,22 @@ const ( TOKEN_TYPE_PASSWORD_RECOVERY = "password_recovery" TOKEN_TYPE_VERIFY_EMAIL = "verify_email" TOKEN_TYPE_TEAM_INVITATION = "team_invitation" + TOKEN_TYPE_GUEST_INVITATION = "guest_invitation" PASSWORD_RECOVER_EXPIRY_TIME = 1000 * 60 * 60 // 1 hour - TEAM_INVITATION_EXPIRY_TIME = 1000 * 60 * 60 * 48 // 48 hours + INVITATION_EXPIRY_TIME = 1000 * 60 * 60 * 48 // 48 hours IMAGE_PROFILE_PIXEL_DIMENSION = 128 ) -func (a *App) CreateUserWithToken(user *model.User, tokenId string) (*model.User, *model.AppError) { +func (a *App) CreateUserWithToken(user *model.User, token *model.Token) (*model.User, *model.AppError) { if err := a.IsUserSignUpAllowed(); err != nil { return nil, err } - token, err := a.Srv.Store.Token().GetByToken(tokenId) - if err != nil { - return nil, model.NewAppError("CreateUserWithToken", "api.user.create_user.signup_link_invalid.app_error", nil, err.Error(), http.StatusBadRequest) - } - - if token.Type != TOKEN_TYPE_TEAM_INVITATION { + if token.Type != TOKEN_TYPE_TEAM_INVITATION && token.Type != TOKEN_TYPE_GUEST_INVITATION { return nil, model.NewAppError("CreateUserWithToken", "api.user.create_user.signup_link_invalid.app_error", nil, "", http.StatusBadRequest) } - if model.GetMillis()-token.CreateAt >= TEAM_INVITATION_EXPIRY_TIME { + if model.GetMillis()-token.CreateAt >= INVITATION_EXPIRY_TIME { a.DeleteToken(token) return nil, model.NewAppError("CreateUserWithToken", "api.user.create_user.signup_link_expired.app_error", nil, "", http.StatusBadRequest) } @@ -71,10 +67,20 @@ func (a *App) CreateUserWithToken(user *model.User, tokenId string) (*model.User return nil, err } + channels, err := a.Srv.Store.Channel().GetChannelsByIds(strings.Split(tokenData["channels"], " ")) + if err != nil { + return nil, err + } + user.Email = tokenData["email"] user.EmailVerified = true - ruser, err := a.CreateUser(user) + var ruser *model.User + if token.Type == TOKEN_TYPE_TEAM_INVITATION { + ruser, err = a.CreateUser(user) + } else { + ruser, err = a.CreateGuest(user) + } if err != nil { return nil, err } @@ -85,6 +91,15 @@ func (a *App) CreateUserWithToken(user *model.User, tokenId string) (*model.User a.AddDirectChannels(team.Id, ruser) + if token.Type == TOKEN_TYPE_GUEST_INVITATION { + for _, channel := range channels { + _, err := a.AddUserToChannel(ruser, channel) + if err != nil { + mlog.Error(err.Error()) + } + } + } + if err := a.DeleteToken(token); err != nil { return nil, err } @@ -233,15 +248,19 @@ func (a *App) CreateGuest(user *model.User) (*model.User, *model.AppError) { } func (a *App) createUserOrGuest(user *model.User, guest bool) (*model.User, *model.AppError) { - if !user.IsLDAPUser() && !user.IsSAMLUser() && !CheckUserDomain(user, *a.Config().TeamSettings.RestrictCreationToDomains) { - return nil, model.NewAppError("CreateUser", "api.user.create_user.accepted_domain.app_error", nil, "", http.StatusBadRequest) - } - user.Roles = model.SYSTEM_USER_ROLE_ID if guest { user.Roles = model.SYSTEM_GUEST_ROLE_ID } + if !user.IsLDAPUser() && !user.IsSAMLUser() && !user.IsGuest() && !CheckUserDomain(user, *a.Config().TeamSettings.RestrictCreationToDomains) { + return nil, model.NewAppError("CreateUser", "api.user.create_user.accepted_domain.app_error", nil, "", http.StatusBadRequest) + } + + if !user.IsLDAPUser() && !user.IsSAMLUser() && user.IsGuest() && !CheckUserDomain(user, *a.Config().GuestAccountsSettings.RestrictCreationToDomains) { + return nil, model.NewAppError("CreateUser", "api.user.create_user.accepted_domain.app_error", nil, "", http.StatusBadRequest) + } + // Below is a special case where the first user in the entire // system is granted the system_admin role count, err := a.Srv.Store.User().Count(model.UserCountOptions{IncludeDeleted: true}) @@ -1056,13 +1075,13 @@ func (a *App) sendUpdatedUserEvent(user model.User) { adminCopyOfUser := user.DeepCopy() a.SanitizeProfile(adminCopyOfUser, true) adminMessage := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_UPDATED, "", "", "", nil) - adminMessage.Add("user", *adminCopyOfUser) + adminMessage.Add("user", &adminCopyOfUser) adminMessage.Broadcast.ContainsSensitiveData = true a.Publish(adminMessage) a.SanitizeProfile(&user, false) message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_UPDATED, "", "", "", nil) - message.Add("user", user) + message.Add("user", &user) message.Broadcast.ContainsSanitizedData = true a.Publish(message) } @@ -2200,6 +2219,61 @@ func (a *App) getListOfAllowedChannelsForTeam(teamId string, viewRestrictions *m return listOfAllowedChannels, nil } +// PromoteGuestToUser Convert user's roles and all his mermbership's roles from +// guest roles to regular user roles. +func (a *App) PromoteGuestToUser(user *model.User, requestorId string) *model.AppError { + err := a.Srv.Store.User().PromoteGuestToUser(user.Id) + if err != nil { + return err + } + userTeams, err := a.Srv.Store.Team().GetTeamsByUserId(user.Id) + if err != nil { + return err + } + + for _, team := range userTeams { + // Soft error if there is an issue joining the default channels + if err = a.JoinDefaultChannels(team.Id, user, false, requestorId); err != nil { + mlog.Error(fmt.Sprintf("Encountered an issue joining default channels err=%v", err), mlog.String("user_id", user.Id), mlog.String("team_id", team.Id), mlog.String("requestor_id", requestorId)) + } + } + + promotedUser, err := a.GetUser(user.Id) + if err != nil { + return err + } + + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_UPDATED, "", "", "", nil) + message.Add("user", promotedUser) + a.Publish(message) + + a.UpdateSessionsIsGuest(promotedUser.Id, promotedUser.IsGuest()) + + return nil +} + +// DemoteUserToGuest Convert user's roles and all his mermbership's roles from +// regular user roles to guest roles. +func (a *App) DemoteUserToGuest(user *model.User) *model.AppError { + err := a.Srv.Store.User().DemoteUserToGuest(user.Id) + if err != nil { + return err + } + + demotedUser, err := a.GetUser(user.Id) + if err != nil { + return err + } + + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_UPDATED, "", "", "", nil) + message.Add("user", demotedUser) + a.Publish(message) + + a.UpdateSessionsIsGuest(demotedUser.Id, demotedUser.IsGuest()) + + return nil +} + // invalidateUserCacheAndPublish Invalidates cache for a user and publishes user updated event func (a *App) invalidateUserCacheAndPublish(userId string) { a.InvalidateCacheForUser(userId) diff --git a/app/user_test.go b/app/user_test.go index ce0006e3b0..9755c95818 100644 --- a/app/user_test.go +++ b/app/user_test.go @@ -561,7 +561,7 @@ func TestCreateUserWithToken(t *testing.T) { user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: "passwd1", AuthService: ""} t.Run("invalid token", func(t *testing.T) { - if _, err := th.App.CreateUserWithToken(&user, "123"); err == nil { + if _, err := th.App.CreateUserWithToken(&user, &model.Token{Token: "123"}); err == nil { t.Fatal("Should fail on unexisting token") } }) @@ -573,7 +573,7 @@ func TestCreateUserWithToken(t *testing.T) { ) require.Nil(t, th.App.Srv.Store.Token().Save(token)) defer th.App.DeleteToken(token) - if _, err := th.App.CreateUserWithToken(&user, token.Token); err == nil { + if _, err := th.App.CreateUserWithToken(&user, token); err == nil { t.Fatal("Should fail on bad token type") } }) @@ -583,10 +583,10 @@ func TestCreateUserWithToken(t *testing.T) { TOKEN_TYPE_TEAM_INVITATION, model.MapToJson(map[string]string{"teamId": th.BasicTeam.Id, "email": user.Email}), ) - token.CreateAt = model.GetMillis() - TEAM_INVITATION_EXPIRY_TIME - 1 + token.CreateAt = model.GetMillis() - INVITATION_EXPIRY_TIME - 1 require.Nil(t, th.App.Srv.Store.Token().Save(token)) defer th.App.DeleteToken(token) - if _, err := th.App.CreateUserWithToken(&user, token.Token); err == nil { + if _, err := th.App.CreateUserWithToken(&user, token); err == nil { t.Fatal("Should fail on expired token") } }) @@ -598,28 +598,60 @@ func TestCreateUserWithToken(t *testing.T) { ) require.Nil(t, th.App.Srv.Store.Token().Save(token)) defer th.App.DeleteToken(token) - if _, err := th.App.CreateUserWithToken(&user, token.Token); err == nil { + if _, err := th.App.CreateUserWithToken(&user, token); err == nil { t.Fatal("Should fail on bad team id") } }) - t.Run("valid request", func(t *testing.T) { + t.Run("valid regular user request", func(t *testing.T) { invitationEmail := model.NewId() + "other-email@test.com" token := model.NewToken( TOKEN_TYPE_TEAM_INVITATION, model.MapToJson(map[string]string{"teamId": th.BasicTeam.Id, "email": invitationEmail}), ) require.Nil(t, th.App.Srv.Store.Token().Save(token)) - newUser, err := th.App.CreateUserWithToken(&user, token.Token) + newUser, err := th.App.CreateUserWithToken(&user, token) if err != nil { t.Log(err) t.Fatal("Should add user to the team") } + assert.False(t, newUser.IsGuest()) if newUser.Email != invitationEmail { t.Fatal("The user email must be the invitation one") } + + _, err = th.App.Srv.Store.Token().GetByToken(token.Token) + require.NotNil(t, err, "The token must be deleted after be used") + + members, err := th.App.GetChannelMembersForUser(th.BasicTeam.Id, newUser.Id) + require.Nil(t, err) + assert.Len(t, *members, 2) + }) + + t.Run("valid guest request", func(t *testing.T) { + invitationEmail := model.NewId() + "other-email@test.com" + token := model.NewToken( + TOKEN_TYPE_GUEST_INVITATION, + model.MapToJson(map[string]string{"teamId": th.BasicTeam.Id, "email": invitationEmail, "channels": th.BasicChannel.Id}), + ) + require.Nil(t, th.App.Srv.Store.Token().Save(token)) + guest := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: "passwd1", AuthService: ""} + newGuest, err := th.App.CreateUserWithToken(&guest, token) + if err != nil { + t.Log(err) + t.Fatal("Should add user to the team") + } + assert.True(t, newGuest.IsGuest()) + if newGuest.Email != invitationEmail { + t.Fatal("The user email must be the invitation one") + } _, err = th.App.Srv.Store.Token().GetByToken(token.Token) require.NotNil(t, err, "The token must be deleted after be used") + + members, err := th.App.GetChannelMembersForUser(th.BasicTeam.Id, newGuest.Id) + require.Nil(t, err) + require.Len(t, *members, 1) + assert.Equal(t, (*members)[0].ChannelId, th.BasicChannel.Id) }) } @@ -870,10 +902,6 @@ func TestGetViewUsersRestrictionsForTeam(t *testing.T) { team2channel1 := th.CreateChannel(team2) th.CreateChannel(team2) // Another channel - team2offtopic, err := th.App.GetChannelByName("off-topic", team2.Id, false) - require.Nil(t, err) - team2townsquare, err := th.App.GetChannelByName("town-square", team2.Id, false) - require.Nil(t, err) th.App.AddUserToChannel(user1, team1channel1) th.App.AddUserToChannel(user1, team1channel2) @@ -929,6 +957,227 @@ func TestGetViewUsersRestrictionsForTeam(t *testing.T) { require.Nil(t, err) assert.NotNil(t, restrictions) - assert.ElementsMatch(t, []string{team1townsquare.Id, team1offtopic.Id, team1channel1.Id, team1channel2.Id, team2townsquare.Id, team2offtopic.Id, team2channel1.Id}, restrictions) + assert.ElementsMatch(t, []string{team1townsquare.Id, team1offtopic.Id, team1channel1.Id, team1channel2.Id}, restrictions) + }) +} + +func TestPromoteGuestToUser(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + t.Run("Must fail with regular user", func(t *testing.T) { + require.Equal(t, "system_user", th.BasicUser.Roles) + err := th.App.PromoteGuestToUser(th.BasicUser, th.BasicUser.Id) + require.Nil(t, err) + + user, err := th.App.GetUser(th.BasicUser.Id) + assert.Nil(t, err) + assert.Equal(t, "system_user", user.Roles) + }) + + t.Run("Must work with guest user without teams or channels", func(t *testing.T) { + guest := th.CreateGuest() + require.Equal(t, "system_guest", guest.Roles) + + err := th.App.PromoteGuestToUser(guest, th.BasicUser.Id) + require.Nil(t, err) + guest, err = th.App.GetUser(guest.Id) + assert.Nil(t, err) + assert.Equal(t, "system_user", guest.Roles) + }) + + t.Run("Must work with guest user with teams but no channels", func(t *testing.T) { + guest := th.CreateGuest() + require.Equal(t, "system_guest", guest.Roles) + th.LinkUserToTeam(guest, th.BasicTeam) + teamMember, err := th.App.GetTeamMember(th.BasicTeam.Id, guest.Id) + require.Nil(t, err) + require.True(t, teamMember.SchemeGuest) + require.False(t, teamMember.SchemeUser) + + err = th.App.PromoteGuestToUser(guest, th.BasicUser.Id) + require.Nil(t, err) + guest, err = th.App.GetUser(guest.Id) + assert.Nil(t, err) + assert.Equal(t, "system_user", guest.Roles) + teamMember, err = th.App.GetTeamMember(th.BasicTeam.Id, guest.Id) + assert.Nil(t, err) + assert.False(t, teamMember.SchemeGuest) + assert.True(t, teamMember.SchemeUser) + }) + + t.Run("Must work with guest user with teams and channels", func(t *testing.T) { + guest := th.CreateGuest() + require.Equal(t, "system_guest", guest.Roles) + th.LinkUserToTeam(guest, th.BasicTeam) + teamMember, err := th.App.GetTeamMember(th.BasicTeam.Id, guest.Id) + require.Nil(t, err) + require.True(t, teamMember.SchemeGuest) + require.False(t, teamMember.SchemeUser) + + channelMember := th.AddUserToChannel(guest, th.BasicChannel) + require.True(t, channelMember.SchemeGuest) + require.False(t, channelMember.SchemeUser) + + err = th.App.PromoteGuestToUser(guest, th.BasicUser.Id) + require.Nil(t, err) + guest, err = th.App.GetUser(guest.Id) + assert.Nil(t, err) + assert.Equal(t, "system_user", guest.Roles) + teamMember, err = th.App.GetTeamMember(th.BasicTeam.Id, guest.Id) + assert.Nil(t, err) + assert.False(t, teamMember.SchemeGuest) + assert.True(t, teamMember.SchemeUser) + channelMember, err = th.App.GetChannelMember(th.BasicChannel.Id, guest.Id) + assert.Nil(t, err) + assert.False(t, teamMember.SchemeGuest) + assert.True(t, teamMember.SchemeUser) + }) + + t.Run("Must add the default channels", func(t *testing.T) { + guest := th.CreateGuest() + require.Equal(t, "system_guest", guest.Roles) + th.LinkUserToTeam(guest, th.BasicTeam) + teamMember, err := th.App.GetTeamMember(th.BasicTeam.Id, guest.Id) + require.Nil(t, err) + require.True(t, teamMember.SchemeGuest) + require.False(t, teamMember.SchemeUser) + + channelMember := th.AddUserToChannel(guest, th.BasicChannel) + require.True(t, channelMember.SchemeGuest) + require.False(t, channelMember.SchemeUser) + + channelMembers, err := th.App.GetChannelMembersForUser(th.BasicTeam.Id, guest.Id) + require.Nil(t, err) + require.Len(t, *channelMembers, 1) + + err = th.App.PromoteGuestToUser(guest, th.BasicUser.Id) + require.Nil(t, err) + guest, err = th.App.GetUser(guest.Id) + assert.Nil(t, err) + assert.Equal(t, "system_user", guest.Roles) + teamMember, err = th.App.GetTeamMember(th.BasicTeam.Id, guest.Id) + assert.Nil(t, err) + assert.False(t, teamMember.SchemeGuest) + assert.True(t, teamMember.SchemeUser) + channelMember, err = th.App.GetChannelMember(th.BasicChannel.Id, guest.Id) + assert.Nil(t, err) + assert.False(t, teamMember.SchemeGuest) + assert.True(t, teamMember.SchemeUser) + + channelMembers, err = th.App.GetChannelMembersForUser(th.BasicTeam.Id, guest.Id) + require.Nil(t, err) + assert.Len(t, *channelMembers, 3) + }) +} + +func TestDemoteUserToGuest(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + t.Run("Must fail with guest user", func(t *testing.T) { + guest := th.CreateGuest() + require.Equal(t, "system_guest", guest.Roles) + err := th.App.DemoteUserToGuest(guest) + require.Nil(t, err) + + user, err := th.App.GetUser(guest.Id) + assert.Nil(t, err) + assert.Equal(t, "system_guest", user.Roles) + }) + + t.Run("Must work with user without teams or channels", func(t *testing.T) { + user := th.CreateUser() + require.Equal(t, "system_user", user.Roles) + + err := th.App.DemoteUserToGuest(user) + require.Nil(t, err) + user, err = th.App.GetUser(user.Id) + assert.Nil(t, err) + assert.Equal(t, "system_guest", user.Roles) + }) + + t.Run("Must work with user with teams but no channels", func(t *testing.T) { + user := th.CreateUser() + require.Equal(t, "system_user", user.Roles) + th.LinkUserToTeam(user, th.BasicTeam) + teamMember, err := th.App.GetTeamMember(th.BasicTeam.Id, user.Id) + require.Nil(t, err) + require.True(t, teamMember.SchemeUser) + require.False(t, teamMember.SchemeGuest) + + err = th.App.DemoteUserToGuest(user) + require.Nil(t, err) + user, err = th.App.GetUser(user.Id) + assert.Nil(t, err) + assert.Equal(t, "system_guest", user.Roles) + teamMember, err = th.App.GetTeamMember(th.BasicTeam.Id, user.Id) + assert.Nil(t, err) + assert.False(t, teamMember.SchemeUser) + assert.True(t, teamMember.SchemeGuest) + }) + + t.Run("Must work with user user with teams and channels", func(t *testing.T) { + user := th.CreateUser() + require.Equal(t, "system_user", user.Roles) + th.LinkUserToTeam(user, th.BasicTeam) + teamMember, err := th.App.GetTeamMember(th.BasicTeam.Id, user.Id) + require.Nil(t, err) + require.True(t, teamMember.SchemeUser) + require.False(t, teamMember.SchemeGuest) + + channelMember := th.AddUserToChannel(user, th.BasicChannel) + require.True(t, channelMember.SchemeUser) + require.False(t, channelMember.SchemeGuest) + + err = th.App.DemoteUserToGuest(user) + require.Nil(t, err) + user, err = th.App.GetUser(user.Id) + assert.Nil(t, err) + assert.Equal(t, "system_guest", user.Roles) + teamMember, err = th.App.GetTeamMember(th.BasicTeam.Id, user.Id) + assert.Nil(t, err) + assert.False(t, teamMember.SchemeUser) + assert.True(t, teamMember.SchemeGuest) + channelMember, err = th.App.GetChannelMember(th.BasicChannel.Id, user.Id) + assert.Nil(t, err) + assert.False(t, teamMember.SchemeUser) + assert.True(t, teamMember.SchemeGuest) + }) + + t.Run("Must respect the current channels not removing defaults", func(t *testing.T) { + user := th.CreateUser() + require.Equal(t, "system_user", user.Roles) + th.LinkUserToTeam(user, th.BasicTeam) + teamMember, err := th.App.GetTeamMember(th.BasicTeam.Id, user.Id) + require.Nil(t, err) + require.True(t, teamMember.SchemeUser) + require.False(t, teamMember.SchemeGuest) + + channelMember := th.AddUserToChannel(user, th.BasicChannel) + require.True(t, channelMember.SchemeUser) + require.False(t, channelMember.SchemeGuest) + + channelMembers, err := th.App.GetChannelMembersForUser(th.BasicTeam.Id, user.Id) + require.Nil(t, err) + require.Len(t, *channelMembers, 3) + + err = th.App.DemoteUserToGuest(user) + require.Nil(t, err) + user, err = th.App.GetUser(user.Id) + assert.Nil(t, err) + assert.Equal(t, "system_guest", user.Roles) + teamMember, err = th.App.GetTeamMember(th.BasicTeam.Id, user.Id) + assert.Nil(t, err) + assert.False(t, teamMember.SchemeUser) + assert.True(t, teamMember.SchemeGuest) + channelMember, err = th.App.GetChannelMember(th.BasicChannel.Id, user.Id) + assert.Nil(t, err) + assert.False(t, teamMember.SchemeUser) + assert.True(t, teamMember.SchemeGuest) + + channelMembers, err = th.App.GetChannelMembersForUser(th.BasicTeam.Id, user.Id) + require.Nil(t, err) + assert.Len(t, *channelMembers, 3) }) } diff --git a/app/web_conn.go b/app/web_conn.go index b2d5d3dc20..bec10c8c6e 100644 --- a/app/web_conn.go +++ b/app/web_conn.go @@ -356,6 +356,15 @@ func (webCon *WebConn) ShouldSendEvent(msg *model.WebSocketEvent) bool { return webCon.IsMemberOfTeam(msg.Broadcast.TeamId) } + if msg.Event == model.WEBSOCKET_EVENT_USER_UPDATED && webCon.GetSession().Props[model.SESSION_PROP_IS_GUEST] == "true" { + canSee, err := webCon.App.UserCanSeeOtherUser(webCon.UserId, msg.Data["user"].(*model.User).Id) + if err != nil { + mlog.Error("webhub.shouldSendEvent: " + err.Error()) + return false + } + return canSee + } + return true } diff --git a/app/web_hub.go b/app/web_hub.go index 69db15f7f4..babf49851b 100644 --- a/app/web_hub.go +++ b/app/web_hub.go @@ -246,6 +246,7 @@ func (a *App) InvalidateCacheForChannelMembers(channelId string) { func (a *App) InvalidateCacheForChannelMembersSkipClusterSend(channelId string) { a.Srv.Store.User().InvalidateProfilesInChannelCache(channelId) a.Srv.Store.Channel().InvalidateMemberCount(channelId) + a.Srv.Store.Channel().InvalidateGuestCount(channelId) } func (a *App) InvalidateCacheForChannelMembersNotifyProps(channelId string) { diff --git a/config/client.go b/config/client.go index bd6d724c0f..12ce56f632 100644 --- a/config/client.go +++ b/config/client.go @@ -273,6 +273,7 @@ func GenerateLimitedClientConfig(c *model.Config, diagnosticId string, license * props["CustomDescriptionText"] = *c.TeamSettings.CustomDescriptionText props["EnableMultifactorAuthentication"] = strconv.FormatBool(*c.ServiceSettings.EnableMultifactorAuthentication) props["EnforceMultifactorAuthentication"] = "false" + props["EnableGuestAccounts"] = strconv.FormatBool(*c.GuestAccountsSettings.Enable) if license != nil { if *license.Features.LDAP { diff --git a/i18n/en.json b/i18n/en.json index f0a638d428..22515d394d 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1862,6 +1862,14 @@ "id": "api.team.add_user_to_team.missing_parameter.app_error", "translation": "Parameter required to add user to team." }, + { + "id": "api.team.demote_user_to_guest.disabled.error", + "translation": "Guest accounts are disabled." + }, + { + "id": "api.team.demote_user_to_guest.license.error", + "translation": "Your license does not support guest accounts" + }, { "id": "api.team.get_invite_info.not_open_team", "translation": "Invite is invalid because this is not an open team." @@ -1906,6 +1914,18 @@ "id": "api.team.invalidate_all_email_invites.app_error", "translation": "Error invalidating email invites." }, + { + "id": "api.team.invate_guests_to_channels.disabled.error", + "translation": "Guest accounts are disabled" + }, + { + "id": "api.team.invate_guests_to_channels.license.error", + "translation": "Your license does not support guest accounts" + }, + { + "id": "api.team.invite_guests.channel_in_invalid_team.app_error", + "translation": "The channels of the invite must be part of the team of the invite." + }, { "id": "api.team.invite_members.disabled.app_error", "translation": "Email invitations are disabled." @@ -1950,6 +1970,14 @@ "id": "api.team.move_channel.success", "translation": "This channel has been moved to this team from %v." }, + { + "id": "api.team.promote_guest_to_user.disabled.error", + "translation": "Guest accounts are disabled." + }, + { + "id": "api.team.promote_guest_to_user.license.error", + "translation": "Your license does not support guest accounts" + }, { "id": "api.team.remove_member.group_constrained.app_error", "translation": "Unable to remove a user from a group-constrained team." @@ -2118,6 +2146,14 @@ "id": "api.templates.invite_body.title", "translation": "You've been invited" }, + { + "id": "api.templates.invite_body_guest.info", + "translation": "The team {{.SenderStatus}} [[{{.SenderName}}]], has invited you to join team [[{{.TeamDisplayName}}]] as a guest." + }, + { + "id": "api.templates.invite_guest_subject", + "translation": "[{{ .SiteName }}] {{ .SenderName }} invited you to join the team {{ .TeamDisplayName }} as a guest" + }, { "id": "api.templates.invite_subject", "translation": "[{{ .SiteName }}] {{ .SenderName }} invited you to join {{ .TeamDisplayName }} Team" @@ -2366,6 +2402,18 @@ "id": "api.user.create_user.disabled.app_error", "translation": "User creation is disabled." }, + { + "id": "api.user.create_user.guest_accounts.disabled.app_error", + "translation": "Guest accounts are disabled" + }, + { + "id": "api.user.create_user.guest_accounts.license.app_error", + "translation": "Your license does not support guest accounts" + }, + { + "id": "api.user.create_user.invalid_invitation_type.app_error", + "translation": "Unable to create the user, invalid invitation." + }, { "id": "api.user.create_user.no_open_server", "translation": "This server does not allow open signups. Please speak with your Administrator to receive an invitation." @@ -2382,6 +2430,10 @@ "id": "api.user.create_user.signup_link_invalid.app_error", "translation": "The signup link does not appear to be valid" }, + { + "id": "api.user.demote_user_to_guest.already_guest.app_error", + "translation": "Unable to convert the user to guest because is already a guest" + }, { "id": "api.user.email_to_ldap.not_available.app_error", "translation": "AD/LDAP not available on this server" @@ -2422,6 +2474,14 @@ "id": "api.user.login.client_side_cert.license.app_error", "translation": "Attempt to use the experimental feature ClientSideCertEnable without a valid enterprise license" }, + { + "id": "api.user.login.guest_accounts.disabled.error", + "translation": "Guest accounts are disabled" + }, + { + "id": "api.user.login.guest_accounts.license.error", + "translation": "Your license does not support guest accounts" + }, { "id": "api.user.login.inactive.app_error", "translation": "Login failed because your account has been deactivated. Please contact an administrator." @@ -2474,6 +2534,10 @@ "id": "api.user.oauth_to_email.not_available.app_error", "translation": "Authentication Transfer not configured or available on this server." }, + { + "id": "api.user.promote_guest_to_user.no_guest.app_error", + "translation": "Unable to convert the guest to regular user because is not a guest" + }, { "id": "api.user.reset_password.broken_token.app_error", "translation": "The reset password token does not appear to be valid." @@ -4802,6 +4866,22 @@ "id": "model.group_syncable.type.app_error", "translation": "invalid type property for group syncable" }, + { + "id": "model.guest.is_valid.channel.app_error", + "translation": "Invalid channel" + }, + { + "id": "model.guest.is_valid.channels.app_error", + "translation": "Invalid channels" + }, + { + "id": "model.guest.is_valid.email.app_error", + "translation": "Invalid email" + }, + { + "id": "model.guest.is_valid.emails.app_error", + "translation": "Invalid emails" + }, { "id": "model.incoming_hook.channel_id.app_error", "translation": "Invalid channel id" @@ -6514,6 +6594,10 @@ "id": "store.sql_session.update_last_activity.app_error", "translation": "Unable to update the last_activity_at" }, + { + "id": "store.sql_session.update_props.app_error", + "translation": "Unable to update session props" + }, { "id": "store.sql_session.update_roles.app_error", "translation": "Unable to update the roles" @@ -6810,6 +6894,26 @@ "id": "store.sql_user.count.app_error", "translation": "UserCountOptions don't make sense" }, + { + "id": "store.sql_user.demote_user_to_guest.channel_members_update.app_error", + "translation": "Failed to update the user channels memberships" + }, + { + "id": "store.sql_user.demote_user_to_guest.commit_transaction.app_error", + "translation": "Failed to commit the database transaction" + }, + { + "id": "store.sql_user.demote_user_to_guest.open_transaction.app_error", + "translation": "Failed to begin the database transaction" + }, + { + "id": "store.sql_user.demote_user_to_guest.team_members_update.app_error", + "translation": "Failed to update the user teams memberships" + }, + { + "id": "store.sql_user.demote_user_to_guest.user_update.app_error", + "translation": "Failed to update the user" + }, { "id": "store.sql_user.get.app_error", "translation": "We encountered an error finding the account" @@ -6890,6 +6994,26 @@ "id": "store.sql_user.permanent_delete.app_error", "translation": "Unable to delete the existing account" }, + { + "id": "store.sql_user.promote_guest.channel_members_update.app_error", + "translation": "Failed to update the user channels memberships" + }, + { + "id": "store.sql_user.promote_guest.commit_transaction.app_error", + "translation": "Failed to commit the database transaction" + }, + { + "id": "store.sql_user.promote_guest.open_transaction.app_error", + "translation": "Failed to begin the database transaction" + }, + { + "id": "store.sql_user.promote_guest.team_members_update.app_error", + "translation": "Failed to update the user teams memberships" + }, + { + "id": "store.sql_user.promote_guest.user_update.app_error", + "translation": "Failed to update the user" + }, { "id": "store.sql_user.save.app_error", "translation": "Unable to save the account." diff --git a/model/channel_stats.go b/model/channel_stats.go index 21af920fc8..5eb26b7e29 100644 --- a/model/channel_stats.go +++ b/model/channel_stats.go @@ -11,6 +11,7 @@ import ( type ChannelStats struct { ChannelId string `json:"channel_id"` MemberCount int64 `json:"member_count"` + GuestCount int64 `json:"guest_count"` } func (o *ChannelStats) ToJson() string { diff --git a/model/client4.go b/model/client4.go index 1f64135802..33e2113225 100644 --- a/model/client4.go +++ b/model/client4.go @@ -1078,6 +1078,26 @@ func (c *Client4) UpdateUserPassword(userId, currentPassword, newPassword string return CheckStatusOK(r), BuildResponse(r) } +// PromoteGuestToUser convert a guest into a regular user +func (c *Client4) PromoteGuestToUser(guestId string) (bool, *Response) { + r, err := c.DoApiPost(c.GetUserRoute(guestId)+"/promote", "") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// DemoteUserToGuest convert a regular user into a guest +func (c *Client4) DemoteUserToGuest(guestId string) (bool, *Response) { + r, err := c.DoApiPost(c.GetUserRoute(guestId)+"/demote", "") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + // UpdateUserRoles updates a user's roles in the system. A user can have "system_user" and "system_admin" roles. func (c *Client4) UpdateUserRoles(userId, roles string) (bool, *Response) { requestBody := map[string]string{"roles": roles} @@ -1904,6 +1924,21 @@ func (c *Client4) InviteUsersToTeam(teamId string, userEmails []string) (bool, * return CheckStatusOK(r), BuildResponse(r) } +// InviteGuestsToTeam invite guest by email to some channels in a team. +func (c *Client4) InviteGuestsToTeam(teamId string, userEmails []string, channels []string, message string) (bool, *Response) { + guestsInvite := GuestsInvite{ + Emails: userEmails, + Channels: channels, + Message: message, + } + r, err := c.DoApiPost(c.GetTeamRoute(teamId)+"/invite-guests/email", guestsInvite.ToJson()) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + // InvalidateEmailInvites will invalidate active email invitations that have not been accepted by the user. func (c *Client4) InvalidateEmailInvites() (bool, *Response) { r, err := c.DoApiDelete(c.GetTeamsRoute() + "/invites/email") diff --git a/model/config.go b/model/config.go index e9e818d053..285ff61f66 100644 --- a/model/config.go +++ b/model/config.go @@ -2312,6 +2312,31 @@ func (s *DisplaySettings) SetDefaults() { } } +type GuestAccountsSettings struct { + Enable *bool + AllowEmailAccounts *bool + EnforceMultifactorAuthentication *bool + RestrictCreationToDomains *string +} + +func (s *GuestAccountsSettings) SetDefaults() { + if s.Enable == nil { + s.Enable = NewBool(false) + } + + if s.AllowEmailAccounts == nil { + s.AllowEmailAccounts = NewBool(true) + } + + if s.EnforceMultifactorAuthentication == nil { + s.EnforceMultifactorAuthentication = NewBool(false) + } + + if s.RestrictCreationToDomains == nil { + s.RestrictCreationToDomains = NewString("") + } +} + type ImageProxySettings struct { Enable *bool ImageProxyType *string @@ -2388,6 +2413,7 @@ type Config struct { JobSettings JobSettings PluginSettings PluginSettings DisplaySettings DisplaySettings + GuestAccountsSettings GuestAccountsSettings ImageProxySettings ImageProxySettings } @@ -2471,6 +2497,7 @@ func (o *Config) SetDefaults() { o.JobSettings.SetDefaults() o.MessageExportSettings.SetDefaults() o.DisplaySettings.SetDefaults() + o.GuestAccountsSettings.SetDefaults() o.ImageProxySettings.SetDefaults(o.ServiceSettings) } diff --git a/model/guest_invite.go b/model/guest_invite.go new file mode 100644 index 0000000000..893c26d33c --- /dev/null +++ b/model/guest_invite.go @@ -0,0 +1,53 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" + "net/http" +) + +type GuestsInvite struct { + Emails []string `json:"emails"` + Channels []string `json:"channels"` + Message string `json:"message"` +} + +// IsValid validates the user and returns an error if it isn't configured +// correctly. +func (i *GuestsInvite) IsValid() *AppError { + if len(i.Emails) == 0 { + return NewAppError("GuestsInvite.IsValid", "model.guest.is_valid.emails.app_error", nil, "", http.StatusBadRequest) + } + + for _, email := range i.Emails { + if len(email) > USER_EMAIL_MAX_LENGTH || len(email) == 0 || !IsValidEmail(email) { + return NewAppError("GuestsInvite.IsValid", "model.guest.is_valid.email.app_error", nil, "email="+email, http.StatusBadRequest) + } + } + + if len(i.Channels) == 0 { + return NewAppError("GuestsInvite.IsValid", "model.guest.is_valid.channels.app_error", nil, "", http.StatusBadRequest) + } + + for _, channel := range i.Channels { + if len(channel) != 26 { + return NewAppError("GuestsInvite.IsValid", "model.guest.is_valid.channel.app_error", nil, "channel="+channel, http.StatusBadRequest) + } + } + return nil +} + +// GuestsInviteFromJson will decode the input and return a GuestsInvite +func GuestsInviteFromJson(data io.Reader) *GuestsInvite { + var invite *GuestsInvite + json.NewDecoder(data).Decode(&invite) + return invite +} + +func (invite *GuestsInvite) ToJson() string { + b, _ := json.Marshal(invite) + return string(b) +} diff --git a/model/license.go b/model/license.go index 7ab21debc0..e9eec517fd 100644 --- a/model/license.go +++ b/model/license.go @@ -59,6 +59,7 @@ type Features struct { MessageExport *bool `json:"message_export"` CustomPermissionsSchemes *bool `json:"custom_permissions_schemes"` CustomTermsOfService *bool `json:"custom_terms_of_service"` + GuestAccountsPermissions *bool `json:"guest_accounts_permissions"` // after we enabled more features we'll need to control them with this FutureFeatures *bool `json:"future_features"` @@ -81,6 +82,7 @@ func (f *Features) ToMap() map[string]interface{} { "data_retention": *f.DataRetention, "message_export": *f.MessageExport, "custom_permissions_schemes": *f.CustomPermissionsSchemes, + "guest_accounts_permissions": *f.GuestAccountsPermissions, "future": *f.FutureFeatures, } } @@ -162,6 +164,10 @@ func (f *Features) SetDefaults() { f.CustomPermissionsSchemes = NewBool(*f.FutureFeatures) } + if f.GuestAccountsPermissions == nil { + f.GuestAccountsPermissions = NewBool(*f.FutureFeatures) + } + if f.CustomTermsOfService == nil { f.CustomTermsOfService = NewBool(*f.FutureFeatures) } diff --git a/model/license_test.go b/model/license_test.go index d550c327f3..c0a206e6c9 100644 --- a/model/license_test.go +++ b/model/license_test.go @@ -52,6 +52,7 @@ func TestLicenseFeaturesSetDefaults(t *testing.T) { CheckTrue(t, *f.DataRetention) CheckTrue(t, *f.MessageExport) CheckTrue(t, *f.CustomPermissionsSchemes) + CheckTrue(t, *f.GuestAccountsPermissions) CheckTrue(t, *f.FutureFeatures) f = Features{} @@ -73,6 +74,7 @@ func TestLicenseFeaturesSetDefaults(t *testing.T) { *f.DataRetention = true *f.MessageExport = true *f.CustomPermissionsSchemes = true + *f.GuestAccountsPermissions = true *f.EmailNotificationContents = true f.SetDefaults() @@ -93,6 +95,7 @@ func TestLicenseFeaturesSetDefaults(t *testing.T) { CheckTrue(t, *f.DataRetention) CheckTrue(t, *f.MessageExport) CheckTrue(t, *f.CustomPermissionsSchemes) + CheckTrue(t, *f.GuestAccountsPermissions) CheckFalse(t, *f.FutureFeatures) } @@ -176,6 +179,7 @@ func TestLicenseToFromJson(t *testing.T) { CheckBool(t, *f1.DataRetention, *f.DataRetention) CheckBool(t, *f1.MessageExport, *f.MessageExport) CheckBool(t, *f1.CustomPermissionsSchemes, *f.CustomPermissionsSchemes) + CheckBool(t, *f1.GuestAccountsPermissions, *f.GuestAccountsPermissions) CheckBool(t, *f1.FutureFeatures, *f.FutureFeatures) invalid := `{"asdf` diff --git a/model/permission.go b/model/permission.go index 09a506c840..72080483b3 100644 --- a/model/permission.go +++ b/model/permission.go @@ -87,6 +87,9 @@ var PERMISSION_READ_OTHERS_BOTS *Permission var PERMISSION_MANAGE_BOTS *Permission var PERMISSION_MANAGE_OTHERS_BOTS *Permission var PERMISSION_VIEW_MEMBERS *Permission +var PERMISSION_INVITE_GUEST *Permission +var PERMISSION_PROMOTE_GUEST *Permission +var PERMISSION_DEMOTE_TO_GUEST *Permission // General permission that encompasses all system admin functions // in the future this could be broken up to allow access to some @@ -533,6 +536,25 @@ func initializePermissions() { "authentication.permisssions.view_members.description", PERMISSION_SCOPE_TEAM, } + PERMISSION_INVITE_GUEST = &Permission{ + "invite_guest", + "authentication.permissions.invite_guest.name", + "authentication.permissions.invite_guest.description", + PERMISSION_SCOPE_TEAM, + } + PERMISSION_PROMOTE_GUEST = &Permission{ + "promote_guest", + "authentication.permissions.promote_guest.name", + "authentication.permissions.promote_guest.description", + PERMISSION_SCOPE_SYSTEM, + } + + PERMISSION_DEMOTE_TO_GUEST = &Permission{ + "demote_to_guest", + "authentication.permissions.demote_to_guest.name", + "authentication.permissions.demote_to_guest.description", + PERMISSION_SCOPE_SYSTEM, + } ALL_PERMISSIONS = []*Permission{ PERMISSION_INVITE_USER, @@ -606,6 +628,9 @@ func initializePermissions() { PERMISSION_MANAGE_OTHERS_BOTS, PERMISSION_MANAGE_SYSTEM, PERMISSION_VIEW_MEMBERS, + PERMISSION_INVITE_GUEST, + PERMISSION_PROMOTE_GUEST, + PERMISSION_DEMOTE_TO_GUEST, } } diff --git a/model/role.go b/model/role.go index 86dcd85499..e246cf9153 100644 --- a/model/role.go +++ b/model/role.go @@ -382,6 +382,9 @@ func MakeDefaultRoles() map[string]*Role { PERMISSION_EDIT_OTHERS_POSTS.Id, PERMISSION_MANAGE_OAUTH.Id, PERMISSION_INVITE_USER.Id, + PERMISSION_INVITE_GUEST.Id, + PERMISSION_PROMOTE_GUEST.Id, + PERMISSION_DEMOTE_TO_GUEST.Id, PERMISSION_DELETE_POST.Id, PERMISSION_DELETE_OTHERS_POSTS.Id, PERMISSION_CREATE_TEAM.Id, diff --git a/model/session.go b/model/session.go index 2e8dba4ebb..dd146531ed 100644 --- a/model/session.go +++ b/model/session.go @@ -22,6 +22,7 @@ const ( SESSION_PROP_IS_BOT = "is_bot" SESSION_PROP_IS_BOT_VALUE = "true" SESSION_TYPE_USER_ACCESS_TOKEN = "UserAccessToken" + SESSION_PROP_IS_GUEST = "is_guest" SESSION_ACTIVITY_TIMEOUT = 1000 * 60 * 5 // 5 minutes SESSION_USER_ACCESS_TOKEN_EXPIRY = 100 * 365 // 100 years ) diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index 0a885f6a6f..942dbf70d7 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -32,6 +32,9 @@ const ( CHANNEL_MEMBERS_COUNTS_CACHE_SIZE = model.CHANNEL_CACHE_SIZE CHANNEL_MEMBERS_COUNTS_CACHE_SEC = 1800 // 30 mins + CHANNEL_GUESTS_COUNTS_CACHE_SIZE = model.CHANNEL_CACHE_SIZE + CHANNEL_GUESTS_COUNTS_CACHE_SEC = 1800 // 30 mins + CHANNEL_CACHE_SEC = 900 // 15 mins ) @@ -278,6 +281,7 @@ type publicChannel struct { } var channelMemberCountsCache = utils.NewLru(CHANNEL_MEMBERS_COUNTS_CACHE_SIZE) +var channelGuestCountsCache = utils.NewLru(CHANNEL_GUESTS_COUNTS_CACHE_SIZE) var allChannelMembersForUserCache = utils.NewLru(ALL_CHANNEL_MEMBERS_FOR_USER_CACHE_SIZE) var allChannelMembersNotifyPropsForChannelCache = utils.NewLru(ALL_CHANNEL_MEMBERS_NOTIFY_PROPS_FOR_CHANNEL_CACHE_SIZE) var channelCache = utils.NewLru(model.CHANNEL_CACHE_SIZE) @@ -285,6 +289,7 @@ var channelByNameCache = utils.NewLru(model.CHANNEL_CACHE_SIZE) func (s SqlChannelStore) ClearCaches() { channelMemberCountsCache.Purge() + channelGuestCountsCache.Purge() allChannelMembersForUserCache.Purge() allChannelMembersNotifyPropsForChannelCache.Purge() channelCache.Purge() @@ -1654,6 +1659,69 @@ func (s SqlChannelStore) GetMemberCount(channelId string, allowFromCache bool) ( return count, nil } +func (s SqlChannelStore) InvalidateGuestCount(channelId string) { + channelGuestCountsCache.Remove(channelId) + if s.metrics != nil { + s.metrics.IncrementMemCacheInvalidationCounter("Channel Guest Counts - Remove by ChannelId") + } +} + +func (s SqlChannelStore) GetGuestCountFromCache(channelId string) int64 { + if cacheItem, ok := channelGuestCountsCache.Get(channelId); ok { + if s.metrics != nil { + s.metrics.IncrementMemCacheHitCounter("Channel Guest Counts") + } + return cacheItem.(int64) + } + + if s.metrics != nil { + s.metrics.IncrementMemCacheMissCounter("Channel Guest Counts") + } + + count, err := s.GetGuestCount(channelId, true) + if err != nil { + return 0 + } + + return count +} + +func (s SqlChannelStore) GetGuestCount(channelId string, allowFromCache bool) (int64, *model.AppError) { + if allowFromCache { + if cacheItem, ok := channelGuestCountsCache.Get(channelId); ok { + if s.metrics != nil { + s.metrics.IncrementMemCacheHitCounter("Channel Guest Counts") + } + return cacheItem.(int64), nil + } + } + + if s.metrics != nil { + s.metrics.IncrementMemCacheMissCounter("Channel Guest Counts") + } + + count, err := s.GetReplica().SelectInt(` + SELECT + count(*) + FROM + ChannelMembers, + Users + WHERE + ChannelMembers.UserId = Users.Id + AND ChannelMembers.ChannelId = :ChannelId + AND ChannelMembers.SchemeGuest = TRUE + AND Users.DeleteAt = 0`, map[string]interface{}{"ChannelId": channelId}) + if err != nil { + return 0, model.NewAppError("SqlChannelStore.GetGuestCount", "store.sql_channel.get_member_count.app_error", nil, "channel_id="+channelId+", "+err.Error(), http.StatusInternalServerError) + } + + if allowFromCache { + channelGuestCountsCache.AddWithExpiresInSecs(channelId, count, CHANNEL_GUESTS_COUNTS_CACHE_SEC) + } + + return count, nil +} + func (s SqlChannelStore) RemoveMember(channelId string, userId string) *model.AppError { _, err := s.GetMaster().Exec("DELETE FROM ChannelMembers WHERE ChannelId = :ChannelId AND UserId = :UserId", map[string]interface{}{"ChannelId": channelId, "UserId": userId}) if err != nil { @@ -1871,8 +1939,7 @@ func (s SqlChannelStore) AnalyticsDeletedTypeCount(teamId string, channelType st func (s SqlChannelStore) GetMembersForUser(teamId string, userId string) (*model.ChannelMembers, *model.AppError) { var dbMembers channelMemberWithSchemeRolesList - _, err := s.GetReplica().Select(&dbMembers, CHANNEL_MEMBERS_WITH_SCHEME_SELECT_QUERY+"WHERE ChannelMembers.UserId = :UserId", map[string]interface{}{"TeamId": teamId, "UserId": userId}) - + _, err := s.GetReplica().Select(&dbMembers, CHANNEL_MEMBERS_WITH_SCHEME_SELECT_QUERY+"WHERE ChannelMembers.UserId = :UserId AND Teams.Id = :TeamId", map[string]interface{}{"TeamId": teamId, "UserId": userId}) if err != nil { return nil, model.NewAppError("SqlChannelStore.GetMembersForUser", "store.sql_channel.get_members.app_error", nil, "teamId="+teamId+", userId="+userId+", err="+err.Error(), http.StatusInternalServerError) } diff --git a/store/sqlstore/session_store.go b/store/sqlstore/session_store.go index e063bd013b..222ef4ba5e 100644 --- a/store/sqlstore/session_store.go +++ b/store/sqlstore/session_store.go @@ -206,6 +206,23 @@ func (me SqlSessionStore) UpdateDeviceId(id string, deviceId string, expiresAt i return deviceId, nil } +func (me SqlSessionStore) UpdateProps(session *model.Session) *model.AppError { + oldSession, appErr := me.Get(session.Id) + if appErr != nil { + return appErr + } + oldSession.Props = session.Props + + count, err := me.GetMaster().Update(oldSession) + if err != nil { + return model.NewAppError("SqlSessionStore.UpdateProps", "store.sql_session.update_props.app_error", nil, err.Error(), http.StatusInternalServerError) + } + if count != 1 { + return model.NewAppError("SqlSessionStore.UpdateProps", "store.sql_session.update_props.app_error", nil, "", http.StatusInternalServerError) + } + return nil +} + func (me SqlSessionStore) AnalyticsSessionCount() (int64, *model.AppError) { query := `SELECT diff --git a/store/sqlstore/tokens_store.go b/store/sqlstore/tokens_store.go index 2a419d2147..603ccc2816 100644 --- a/store/sqlstore/tokens_store.go +++ b/store/sqlstore/tokens_store.go @@ -23,7 +23,7 @@ func NewSqlTokenStore(sqlStore SqlStore) store.TokenStore { table := db.AddTableWithName(model.Token{}, "Tokens").SetKeys(false, "Token") table.ColMap("Token").SetMaxSize(64) table.ColMap("Type").SetMaxSize(64) - table.ColMap("Extra").SetMaxSize(128) + table.ColMap("Extra").SetMaxSize(2048) } return s diff --git a/store/sqlstore/upgrade.go b/store/sqlstore/upgrade.go index fc55ae6234..a7e4edc14d 100644 --- a/store/sqlstore/upgrade.go +++ b/store/sqlstore/upgrade.go @@ -710,6 +710,12 @@ func UpgradeDatabaseToVersion514(sqlStore SqlStore) { // TODO: Uncomment following condition when version 5.14.0 is released // if shouldPerformUpgrade(sqlStore, VERSION_5_13_0, VERSION_5_14_0) { + if sqlStore.DriverName() == model.DATABASE_DRIVER_POSTGRES { + sqlStore.GetMaster().Exec("ALTER TABLE Tokens ALTER COLUMN Extra TYPE varchar(2048)") + } else if sqlStore.DriverName() == model.DATABASE_DRIVER_MYSQL { + sqlStore.GetMaster().Exec("ALTER TABLE Tokens MODIFY Extra text") + } + // saveSchemaVersion(sqlStore, VERSION_5_14_0) // } } diff --git a/store/sqlstore/user_store.go b/store/sqlstore/user_store.go index 96b96f6326..981277ad9e 100644 --- a/store/sqlstore/user_store.go +++ b/store/sqlstore/user_store.go @@ -1066,7 +1066,7 @@ func (us SqlUserStore) PermanentDelete(userId string) *model.AppError { } func (us SqlUserStore) Count(options model.UserCountOptions) (int64, *model.AppError) { - query := sq.Select("COUNT(DISTINCT u.Id)").From("Users AS u") + query := us.getQueryBuilder().Select("COUNT(DISTINCT u.Id)").From("Users AS u") if !options.IncludeDeleted { query = query.Where("u.DeleteAt = 0") @@ -1644,3 +1644,140 @@ func applyViewRestrictionsFilter(query sq.SelectBuilder, restrictions *model.Vie return resultQuery } + +func (us SqlUserStore) PromoteGuestToUser(userId string) *model.AppError { + transaction, err := us.GetMaster().Begin() + if err != nil { + return model.NewAppError("SqlUserStore.PromoteGuestToUser", "store.sql_user.promote_guest.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) + } + defer finalizeTransaction(transaction) + + user, appErr := us.Get(userId) + if appErr != nil { + return appErr + } + + roles := user.GetRoles() + + for idx, role := range roles { + if role == "system_guest" { + roles[idx] = "system_user" + } + } + + query := us.getQueryBuilder().Update("Users"). + Set("Roles", strings.Join(roles, " ")). + Where(sq.Eq{"Id": userId}) + + queryString, args, err := query.ToSql() + if err != nil { + return model.NewAppError("SqlUserStore.PromoteGuestToUser", "store.sql_user.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + if _, err = transaction.Exec(queryString, args...); err != nil { + return model.NewAppError("SqlUserStore.PromoteGuestToUser", "store.sql_user.promote_guest.user_update.app_error", nil, "user_id="+userId, http.StatusInternalServerError) + } + + query = us.getQueryBuilder().Update("ChannelMembers"). + Set("SchemeUser", true). + Set("SchemeGuest", false). + Where(sq.Eq{"UserId": userId}) + + queryString, args, err = query.ToSql() + if err != nil { + return model.NewAppError("SqlUserStore.PromoteGuestToUser", "store.sql_user.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + if _, err = transaction.Exec(queryString, args...); err != nil { + return model.NewAppError("SqlUserStore.PromoteGuestToUser", "store.sql_user.promote_guest.channel_members_update.app_error", nil, "user_id="+userId, http.StatusInternalServerError) + } + + query = us.getQueryBuilder().Update("TeamMembers"). + Set("SchemeUser", true). + Set("SchemeGuest", false). + Where(sq.Eq{"UserId": userId}) + + queryString, args, err = query.ToSql() + if err != nil { + return model.NewAppError("SqlUserStore.PromoteGuestToUser", "store.sql_user.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + if _, err := transaction.Exec(queryString, args...); err != nil { + return model.NewAppError("SqlUserStore.PromoteGuestToUser", "store.sql_user.promote_guest.team_members_update.app_error", nil, "user_id="+userId, http.StatusInternalServerError) + } + + if err := transaction.Commit(); err != nil { + return model.NewAppError("SqlUserStore.PromoteGuestToUser", "store.sql_user.promote_guest.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) + } + return nil +} + +func (us SqlUserStore) DemoteUserToGuest(userId string) *model.AppError { + transaction, err := us.GetMaster().Begin() + if err != nil { + return model.NewAppError("SqlUserStore.DemoteUserToGuest", "store.sql_user.demote_user_to_guest.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) + } + defer finalizeTransaction(transaction) + + user, appErr := us.Get(userId) + if appErr != nil { + return appErr + } + + roles := user.GetRoles() + + newRoles := []string{} + for _, role := range roles { + if role == "system_user" { + newRoles = append(newRoles, "system_guest") + } else if role != "system_admin" { + newRoles = append(newRoles, role) + } + } + + query := us.getQueryBuilder().Update("Users"). + Set("Roles", strings.Join(newRoles, " ")). + Where(sq.Eq{"Id": userId}) + + queryString, args, err := query.ToSql() + if err != nil { + return model.NewAppError("SqlUserStore.DemoteGuestToUser", "store.sql_user.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + if _, err = transaction.Exec(queryString, args...); err != nil { + return model.NewAppError("SqlUserStore.DemoteGuestToUser", "store.sql_user.demote_user_to_guest.user_update.app_error", nil, "user_id="+userId, http.StatusInternalServerError) + } + + query = us.getQueryBuilder().Update("ChannelMembers"). + Set("SchemeUser", false). + Set("SchemeGuest", true). + Where(sq.Eq{"UserId": userId}) + + queryString, args, err = query.ToSql() + if err != nil { + return model.NewAppError("SqlUserStore.DemoteGuestToUser", "store.sql_user.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + if _, err = transaction.Exec(queryString, args...); err != nil { + return model.NewAppError("SqlUserStore.DemoteGuestToUser", "store.sql_user.demote_user_to_guest.channel_members_update.app_error", nil, "user_id="+userId, http.StatusInternalServerError) + } + + query = us.getQueryBuilder().Update("TeamMembers"). + Set("SchemeUser", false). + Set("SchemeGuest", true). + Where(sq.Eq{"UserId": userId}) + + queryString, args, err = query.ToSql() + if err != nil { + return model.NewAppError("SqlUserStore.DemoteGuestToUser", "store.sql_user.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + if _, err := transaction.Exec(queryString, args...); err != nil { + return model.NewAppError("SqlUserStore.DemoteGuestToUser", "store.sql_user.demote_user_to_guest.team_members_update.app_error", nil, "user_id="+userId, http.StatusInternalServerError) + } + + if err := transaction.Commit(); err != nil { + return model.NewAppError("SqlUserStore.DemoteGuestToUser", "store.sql_user.demote_user_to_guest.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) + } + return nil +} diff --git a/store/store.go b/store/store.go index 22543aaf37..3061102b0f 100644 --- a/store/store.go +++ b/store/store.go @@ -172,6 +172,9 @@ type ChannelStore interface { InvalidateMemberCount(channelId string) GetMemberCountFromCache(channelId string) int64 GetMemberCount(channelId string, allowFromCache bool) (int64, *model.AppError) + InvalidateGuestCount(channelId string) + GetGuestCountFromCache(channelId string) int64 + GetGuestCount(channelId string, allowFromCache bool) (int64, *model.AppError) GetPinnedPosts(channelId string) (*model.PostList, *model.AppError) RemoveMember(channelId string, userId string) *model.AppError PermanentDeleteMembersByUser(userId string) *model.AppError @@ -307,6 +310,8 @@ type UserStore interface { Count(options model.UserCountOptions) (int64, *model.AppError) GetTeamGroupUsers(teamID string) ([]*model.User, *model.AppError) GetChannelGroupUsers(channelID string) ([]*model.User, *model.AppError) + PromoteGuestToUser(userID string) *model.AppError + DemoteUserToGuest(userID string) *model.AppError } type BotStore interface { @@ -328,6 +333,7 @@ type SessionStore interface { UpdateLastActivityAt(sessionId string, time int64) *model.AppError UpdateRoles(userId string, roles string) (string, *model.AppError) UpdateDeviceId(id string, deviceId string, expiresAt int64) (string, *model.AppError) + UpdateProps(session *model.Session) *model.AppError AnalyticsSessionCount() (int64, *model.AppError) Cleanup(expiryTime int64, batchSize int64) } diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go index d7163348f4..2ced3babc5 100644 --- a/store/storetest/channel_store.go +++ b/store/storetest/channel_store.go @@ -65,6 +65,7 @@ func TestChannelStore(t *testing.T, ss store.Store, s SqlSupplier) { t.Run("GetMember", func(t *testing.T) { testGetMember(t, ss) }) t.Run("GetMemberForPost", func(t *testing.T) { testChannelStoreGetMemberForPost(t, ss) }) t.Run("GetMemberCount", func(t *testing.T) { testGetMemberCount(t, ss) }) + t.Run("GetGuestCount", func(t *testing.T) { testGetGuestCount(t, ss) }) t.Run("SearchMore", func(t *testing.T) { testChannelStoreSearchMore(t, ss) }) t.Run("SearchInTeam", func(t *testing.T) { testChannelStoreSearchInTeam(t, ss) }) t.Run("SearchAllChannels", func(t *testing.T) { testChannelStoreSearchAllChannels(t, ss) }) @@ -2045,6 +2046,124 @@ func testGetMemberCount(t *testing.T, ss store.Store) { } } +func testGetGuestCount(t *testing.T, ss store.Store) { + teamId := model.NewId() + + c1 := model.Channel{ + TeamId: teamId, + DisplayName: "Channel1", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, + } + _, err := ss.Channel().Save(&c1, -1) + require.Nil(t, err) + + c2 := model.Channel{ + TeamId: teamId, + DisplayName: "Channel2", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, + } + _, err = ss.Channel().Save(&c2, -1) + require.Nil(t, err) + + t.Run("Regular member doesn't count", func(t *testing.T) { + u1 := &model.User{ + Email: MakeEmail(), + DeleteAt: 0, + Roles: model.SYSTEM_USER_ROLE_ID, + } + _, err = ss.User().Save(u1) + require.Nil(t, err) + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)) + + m1 := model.ChannelMember{ + ChannelId: c1.Id, + UserId: u1.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + SchemeGuest: false, + } + _, err = ss.Channel().SaveMember(&m1) + require.Nil(t, err) + + count, channelErr := ss.Channel().GetGuestCount(c1.Id, false) + require.Nil(t, channelErr) + require.Equal(t, count, int64(0)) + }) + + t.Run("Guest member does count", func(t *testing.T) { + u2 := model.User{ + Email: MakeEmail(), + DeleteAt: 0, + Roles: model.SYSTEM_GUEST_ROLE_ID, + } + _, err = ss.User().Save(&u2) + require.Nil(t, err) + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)) + + m2 := model.ChannelMember{ + ChannelId: c1.Id, + UserId: u2.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + SchemeGuest: true, + } + _, err = ss.Channel().SaveMember(&m2) + require.Nil(t, err) + + count, channelErr := ss.Channel().GetGuestCount(c1.Id, false) + require.Nil(t, channelErr) + require.Equal(t, count, int64(1)) + }) + + t.Run("make sure members of other channels aren't counted", func(t *testing.T) { + u3 := model.User{ + Email: MakeEmail(), + DeleteAt: 0, + Roles: model.SYSTEM_GUEST_ROLE_ID, + } + _, err = ss.User().Save(&u3) + require.Nil(t, err) + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)) + + m3 := model.ChannelMember{ + ChannelId: c2.Id, + UserId: u3.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + SchemeGuest: true, + } + _, err = ss.Channel().SaveMember(&m3) + require.Nil(t, err) + + count, channelErr := ss.Channel().GetGuestCount(c1.Id, false) + require.Nil(t, channelErr) + require.Equal(t, count, int64(1)) + }) + + t.Run("make sure inactive users aren't counted", func(t *testing.T) { + u4 := &model.User{ + Email: MakeEmail(), + DeleteAt: 10000, + Roles: model.SYSTEM_GUEST_ROLE_ID, + } + _, err = ss.User().Save(u4) + require.Nil(t, err) + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u4.Id}, -1)) + + m4 := model.ChannelMember{ + ChannelId: c1.Id, + UserId: u4.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + SchemeGuest: true, + } + _, err = ss.Channel().SaveMember(&m4) + require.Nil(t, err) + + count, channelErr := ss.Channel().GetGuestCount(c1.Id, false) + require.Nil(t, channelErr) + require.Equal(t, count, int64(1)) + }) +} + func testChannelStoreSearchMore(t *testing.T, ss store.Store) { teamId := model.NewId() otherTeamId := model.NewId() diff --git a/store/storetest/mocks/ChannelStore.go b/store/storetest/mocks/ChannelStore.go index dad08cbc27..c61206b534 100644 --- a/store/storetest/mocks/ChannelStore.go +++ b/store/storetest/mocks/ChannelStore.go @@ -744,6 +744,43 @@ func (_m *ChannelStore) GetFromMaster(id string) (*model.Channel, *model.AppErro return r0, r1 } +// GetGuestCount provides a mock function with given fields: channelId, allowFromCache +func (_m *ChannelStore) GetGuestCount(channelId string, allowFromCache bool) (int64, *model.AppError) { + ret := _m.Called(channelId, allowFromCache) + + var r0 int64 + if rf, ok := ret.Get(0).(func(string, bool) int64); ok { + r0 = rf(channelId, allowFromCache) + } else { + r0 = ret.Get(0).(int64) + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string, bool) *model.AppError); ok { + r1 = rf(channelId, allowFromCache) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + +// GetGuestCountFromCache provides a mock function with given fields: channelId +func (_m *ChannelStore) GetGuestCountFromCache(channelId string) int64 { + ret := _m.Called(channelId) + + var r0 int64 + if rf, ok := ret.Get(0).(func(string) int64); ok { + r0 = rf(channelId) + } else { + r0 = ret.Get(0).(int64) + } + + return r0 +} + // GetMember provides a mock function with given fields: channelId, userId func (_m *ChannelStore) GetMember(channelId string, userId string) (*model.ChannelMember, *model.AppError) { ret := _m.Called(channelId, userId) @@ -1092,6 +1129,11 @@ func (_m *ChannelStore) InvalidateChannelByName(teamId string, name string) { _m.Called(teamId, name) } +// InvalidateGuestCount provides a mock function with given fields: channelId +func (_m *ChannelStore) InvalidateGuestCount(channelId string) { + _m.Called(channelId) +} + // InvalidateMemberCount provides a mock function with given fields: channelId func (_m *ChannelStore) InvalidateMemberCount(channelId string) { _m.Called(channelId) diff --git a/store/storetest/mocks/SessionStore.go b/store/storetest/mocks/SessionStore.go index 016a58c636..4c0a97ecc9 100644 --- a/store/storetest/mocks/SessionStore.go +++ b/store/storetest/mocks/SessionStore.go @@ -227,6 +227,22 @@ func (_m *SessionStore) UpdateLastActivityAt(sessionId string, time int64) *mode return r0 } +// UpdateProps provides a mock function with given fields: session +func (_m *SessionStore) UpdateProps(session *model.Session) *model.AppError { + ret := _m.Called(session) + + var r0 *model.AppError + if rf, ok := ret.Get(0).(func(*model.Session) *model.AppError); ok { + r0 = rf(session) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.AppError) + } + } + + return r0 +} + // UpdateRoles provides a mock function with given fields: userId, roles func (_m *SessionStore) UpdateRoles(userId string, roles string) (string, *model.AppError) { ret := _m.Called(userId, roles) diff --git a/store/storetest/mocks/UserStore.go b/store/storetest/mocks/UserStore.go index 9ae070dc0e..e308e86241 100644 --- a/store/storetest/mocks/UserStore.go +++ b/store/storetest/mocks/UserStore.go @@ -126,6 +126,22 @@ func (_m *UserStore) Count(options model.UserCountOptions) (int64, *model.AppErr return r0, r1 } +// DemoteUserToGuest provides a mock function with given fields: userID +func (_m *UserStore) DemoteUserToGuest(userID string) *model.AppError { + ret := _m.Called(userID) + + var r0 *model.AppError + if rf, ok := ret.Get(0).(func(string) *model.AppError); ok { + r0 = rf(userID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.AppError) + } + } + + return r0 +} + // Get provides a mock function with given fields: id func (_m *UserStore) Get(id string) (*model.User, *model.AppError) { ret := _m.Called(id) @@ -905,6 +921,22 @@ func (_m *UserStore) PermanentDelete(userId string) *model.AppError { return r0 } +// PromoteGuestToUser provides a mock function with given fields: userID +func (_m *UserStore) PromoteGuestToUser(userID string) *model.AppError { + ret := _m.Called(userID) + + var r0 *model.AppError + if rf, ok := ret.Get(0).(func(string) *model.AppError); ok { + r0 = rf(userID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.AppError) + } + } + + return r0 +} + // ResetLastPictureUpdate provides a mock function with given fields: userId func (_m *UserStore) ResetLastPictureUpdate(userId string) *model.AppError { ret := _m.Called(userId) diff --git a/store/storetest/user_store.go b/store/storetest/user_store.go index f289f39a43..094be9d7fb 100644 --- a/store/storetest/user_store.go +++ b/store/storetest/user_store.go @@ -78,6 +78,8 @@ func TestUserStore(t *testing.T, ss store.Store, s SqlSupplier) { t.Run("GetUsersBatchForIndexing", func(t *testing.T) { testUserStoreGetUsersBatchForIndexing(t, ss) }) t.Run("GetTeamGroupUsers", func(t *testing.T) { testUserStoreGetTeamGroupUsers(t, ss) }) t.Run("GetChannelGroupUsers", func(t *testing.T) { testUserStoreGetChannelGroupUsers(t, ss) }) + t.Run("PromoteGuestToUser", func(t *testing.T) { testUserStorePromoteGuestToUser(t, ss) }) + t.Run("DemoteUserToGuest", func(t *testing.T) { testUserStoreDemoteUserToGuest(t, ss) }) t.Run("ResetLastPictureUpdate", func(t *testing.T) { testUserStoreResetLastPictureUpdate(t, ss) }) } @@ -4154,6 +4156,604 @@ func testUserStoreGetChannelGroupUsers(t *testing.T, ss store.Store) { requireNUsers(2) } +func testUserStorePromoteGuestToUser(t *testing.T, ss store.Store) { + // create users + t.Run("Must do nothing with regular user", func(t *testing.T) { + id := model.NewId() + user, err := ss.User().Save(&model.User{ + Email: id + "@test.com", + Username: "un_" + id, + Nickname: "nn_" + id, + FirstName: "f_" + id, + LastName: "l_" + id, + Password: "Password1", + Roles: "system_user", + }) + require.Nil(t, err) + + teamId := model.NewId() + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id, SchemeGuest: true, SchemeUser: false}, 999)) + + channel, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "Channel name", + Name: "channel-" + model.NewId(), + Type: model.CHANNEL_OPEN, + }, -1) + require.Nil(t, err) + _, err = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user.Id, SchemeGuest: true, SchemeUser: false, NotifyProps: model.GetDefaultChannelNotifyProps()}) + require.Nil(t, err) + + err = ss.User().PromoteGuestToUser(user.Id) + assert.Nil(t, err) + updatedUser, err := ss.User().Get(user.Id) + assert.Nil(t, err) + require.Equal(t, "system_user", updatedUser.Roles) + + updatedTeamMember, err := ss.Team().GetMember(teamId, user.Id) + require.Nil(t, err) + require.False(t, updatedTeamMember.SchemeGuest) + require.True(t, updatedTeamMember.SchemeUser) + + updatedChannelMember, err := ss.Channel().GetMember(channel.Id, user.Id) + require.Nil(t, err) + require.False(t, updatedChannelMember.SchemeGuest) + require.True(t, updatedChannelMember.SchemeUser) + }) + + t.Run("Must do nothing with admin user", func(t *testing.T) { + id := model.NewId() + user, err := ss.User().Save(&model.User{ + Email: id + "@test.com", + Username: "un_" + id, + Nickname: "nn_" + id, + FirstName: "f_" + id, + LastName: "l_" + id, + Password: "Password1", + Roles: "system_user system_admin", + }) + require.Nil(t, err) + + teamId := model.NewId() + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id, SchemeGuest: true, SchemeUser: false}, 999)) + + channel, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "Channel name", + Name: "channel-" + model.NewId(), + Type: model.CHANNEL_OPEN, + }, -1) + require.Nil(t, err) + _, err = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user.Id, SchemeGuest: true, SchemeUser: false, NotifyProps: model.GetDefaultChannelNotifyProps()}) + require.Nil(t, err) + + err = ss.User().PromoteGuestToUser(user.Id) + assert.Nil(t, err) + updatedUser, err := ss.User().Get(user.Id) + assert.Nil(t, err) + require.Equal(t, "system_user system_admin", updatedUser.Roles) + + updatedTeamMember, err := ss.Team().GetMember(teamId, user.Id) + require.Nil(t, err) + require.False(t, updatedTeamMember.SchemeGuest) + require.True(t, updatedTeamMember.SchemeUser) + + updatedChannelMember, err := ss.Channel().GetMember(channel.Id, user.Id) + require.Nil(t, err) + require.False(t, updatedChannelMember.SchemeGuest) + require.True(t, updatedChannelMember.SchemeUser) + }) + + t.Run("Must work with guest user without teams or channels", func(t *testing.T) { + id := model.NewId() + user, err := ss.User().Save(&model.User{ + Email: id + "@test.com", + Username: "un_" + id, + Nickname: "nn_" + id, + FirstName: "f_" + id, + LastName: "l_" + id, + Password: "Password1", + Roles: "system_guest", + }) + require.Nil(t, err) + + err = ss.User().PromoteGuestToUser(user.Id) + assert.Nil(t, err) + updatedUser, err := ss.User().Get(user.Id) + assert.Nil(t, err) + require.Equal(t, "system_user", updatedUser.Roles) + }) + + t.Run("Must work with guest user with teams but no channels", func(t *testing.T) { + id := model.NewId() + user, err := ss.User().Save(&model.User{ + Email: id + "@test.com", + Username: "un_" + id, + Nickname: "nn_" + id, + FirstName: "f_" + id, + LastName: "l_" + id, + Password: "Password1", + Roles: "system_guest", + }) + require.Nil(t, err) + + teamId := model.NewId() + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id, SchemeGuest: true, SchemeUser: false}, 999)) + + err = ss.User().PromoteGuestToUser(user.Id) + assert.Nil(t, err) + updatedUser, err := ss.User().Get(user.Id) + assert.Nil(t, err) + require.Equal(t, "system_user", updatedUser.Roles) + + updatedTeamMember, err := ss.Team().GetMember(teamId, user.Id) + require.Nil(t, err) + require.False(t, updatedTeamMember.SchemeGuest) + require.True(t, updatedTeamMember.SchemeUser) + }) + + t.Run("Must work with guest user with teams and channels", func(t *testing.T) { + id := model.NewId() + user, err := ss.User().Save(&model.User{ + Email: id + "@test.com", + Username: "un_" + id, + Nickname: "nn_" + id, + FirstName: "f_" + id, + LastName: "l_" + id, + Password: "Password1", + Roles: "system_guest", + }) + require.Nil(t, err) + + teamId := model.NewId() + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id, SchemeGuest: true, SchemeUser: false}, 999)) + + channel, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "Channel name", + Name: "channel-" + model.NewId(), + Type: model.CHANNEL_OPEN, + }, -1) + require.Nil(t, err) + _, err = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user.Id, SchemeGuest: true, SchemeUser: false, NotifyProps: model.GetDefaultChannelNotifyProps()}) + require.Nil(t, err) + + err = ss.User().PromoteGuestToUser(user.Id) + assert.Nil(t, err) + updatedUser, err := ss.User().Get(user.Id) + assert.Nil(t, err) + require.Equal(t, "system_user", updatedUser.Roles) + + updatedTeamMember, err := ss.Team().GetMember(teamId, user.Id) + require.Nil(t, err) + require.False(t, updatedTeamMember.SchemeGuest) + require.True(t, updatedTeamMember.SchemeUser) + + updatedChannelMember, err := ss.Channel().GetMember(channel.Id, user.Id) + require.Nil(t, err) + require.False(t, updatedChannelMember.SchemeGuest) + require.True(t, updatedChannelMember.SchemeUser) + }) + + t.Run("Must work with guest user with teams and channels and custom role", func(t *testing.T) { + id := model.NewId() + user, err := ss.User().Save(&model.User{ + Email: id + "@test.com", + Username: "un_" + id, + Nickname: "nn_" + id, + FirstName: "f_" + id, + LastName: "l_" + id, + Password: "Password1", + Roles: "system_guest custom_role", + }) + require.Nil(t, err) + + teamId := model.NewId() + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id, SchemeGuest: true, SchemeUser: false}, 999)) + + channel, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "Channel name", + Name: "channel-" + model.NewId(), + Type: model.CHANNEL_OPEN, + }, -1) + require.Nil(t, err) + _, err = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user.Id, SchemeGuest: true, SchemeUser: false, NotifyProps: model.GetDefaultChannelNotifyProps()}) + require.Nil(t, err) + + err = ss.User().PromoteGuestToUser(user.Id) + assert.Nil(t, err) + updatedUser, err := ss.User().Get(user.Id) + assert.Nil(t, err) + require.Equal(t, "system_user custom_role", updatedUser.Roles) + + updatedTeamMember, err := ss.Team().GetMember(teamId, user.Id) + require.Nil(t, err) + require.False(t, updatedTeamMember.SchemeGuest) + require.True(t, updatedTeamMember.SchemeUser) + + updatedChannelMember, err := ss.Channel().GetMember(channel.Id, user.Id) + require.Nil(t, err) + require.False(t, updatedChannelMember.SchemeGuest) + require.True(t, updatedChannelMember.SchemeUser) + }) + + t.Run("Must no change any other user guest role", func(t *testing.T) { + id := model.NewId() + user1, err := ss.User().Save(&model.User{ + Email: id + "@test.com", + Username: "un_" + id, + Nickname: "nn_" + id, + FirstName: "f_" + id, + LastName: "l_" + id, + Password: "Password1", + Roles: "system_guest", + }) + require.Nil(t, err) + + teamId1 := model.NewId() + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId1, UserId: user1.Id, SchemeGuest: true, SchemeUser: false}, 999)) + + channel, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId1, + DisplayName: "Channel name", + Name: "channel-" + model.NewId(), + Type: model.CHANNEL_OPEN, + }, -1) + require.Nil(t, err) + + _, err = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user1.Id, SchemeGuest: true, SchemeUser: false, NotifyProps: model.GetDefaultChannelNotifyProps()}) + require.Nil(t, err) + + id = model.NewId() + user2, err := ss.User().Save(&model.User{ + Email: id + "@test.com", + Username: "un_" + id, + Nickname: "nn_" + id, + FirstName: "f_" + id, + LastName: "l_" + id, + Password: "Password1", + Roles: "system_guest", + }) + require.Nil(t, err) + + teamId2 := model.NewId() + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId2, UserId: user2.Id, SchemeGuest: true, SchemeUser: false}, 999)) + + _, err = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user2.Id, SchemeGuest: true, SchemeUser: false, NotifyProps: model.GetDefaultChannelNotifyProps()}) + require.Nil(t, err) + + err = ss.User().PromoteGuestToUser(user1.Id) + assert.Nil(t, err) + updatedUser, err := ss.User().Get(user1.Id) + assert.Nil(t, err) + require.Equal(t, "system_user", updatedUser.Roles) + + updatedTeamMember, err := ss.Team().GetMember(teamId1, user1.Id) + require.Nil(t, err) + require.False(t, updatedTeamMember.SchemeGuest) + require.True(t, updatedTeamMember.SchemeUser) + + updatedChannelMember, err := ss.Channel().GetMember(channel.Id, user1.Id) + require.Nil(t, err) + require.False(t, updatedChannelMember.SchemeGuest) + require.True(t, updatedChannelMember.SchemeUser) + + notUpdatedUser, err := ss.User().Get(user2.Id) + assert.Nil(t, err) + require.Equal(t, "system_guest", notUpdatedUser.Roles) + + notUpdatedTeamMember, err := ss.Team().GetMember(teamId2, user2.Id) + require.Nil(t, err) + require.True(t, notUpdatedTeamMember.SchemeGuest) + require.False(t, notUpdatedTeamMember.SchemeUser) + + notUpdatedChannelMember, err := ss.Channel().GetMember(channel.Id, user2.Id) + require.Nil(t, err) + require.True(t, notUpdatedChannelMember.SchemeGuest) + require.False(t, notUpdatedChannelMember.SchemeUser) + }) +} + +func testUserStoreDemoteUserToGuest(t *testing.T, ss store.Store) { + // create users + t.Run("Must do nothing with guest", func(t *testing.T) { + id := model.NewId() + user, err := ss.User().Save(&model.User{ + Email: id + "@test.com", + Username: "un_" + id, + Nickname: "nn_" + id, + FirstName: "f_" + id, + LastName: "l_" + id, + Password: "Password1", + Roles: "system_guest", + }) + require.Nil(t, err) + + teamId := model.NewId() + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id, SchemeGuest: false, SchemeUser: true}, 999)) + + channel, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "Channel name", + Name: "channel-" + model.NewId(), + Type: model.CHANNEL_OPEN, + }, -1) + require.Nil(t, err) + _, err = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user.Id, SchemeGuest: false, SchemeUser: true, NotifyProps: model.GetDefaultChannelNotifyProps()}) + require.Nil(t, err) + + err = ss.User().DemoteUserToGuest(user.Id) + assert.Nil(t, err) + updatedUser, err := ss.User().Get(user.Id) + assert.Nil(t, err) + require.Equal(t, "system_guest", updatedUser.Roles) + + updatedTeamMember, err := ss.Team().GetMember(teamId, user.Id) + require.Nil(t, err) + require.True(t, updatedTeamMember.SchemeGuest) + require.False(t, updatedTeamMember.SchemeUser) + + updatedChannelMember, err := ss.Channel().GetMember(channel.Id, user.Id) + require.Nil(t, err) + require.True(t, updatedChannelMember.SchemeGuest) + require.False(t, updatedChannelMember.SchemeUser) + }) + + t.Run("Must demote properly an admin user", func(t *testing.T) { + id := model.NewId() + user, err := ss.User().Save(&model.User{ + Email: id + "@test.com", + Username: "un_" + id, + Nickname: "nn_" + id, + FirstName: "f_" + id, + LastName: "l_" + id, + Password: "Password1", + Roles: "system_user system_admin", + }) + require.Nil(t, err) + + teamId := model.NewId() + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id, SchemeGuest: true, SchemeUser: false}, 999)) + + channel, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "Channel name", + Name: "channel-" + model.NewId(), + Type: model.CHANNEL_OPEN, + }, -1) + require.Nil(t, err) + _, err = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user.Id, SchemeGuest: true, SchemeUser: false, NotifyProps: model.GetDefaultChannelNotifyProps()}) + require.Nil(t, err) + + err = ss.User().DemoteUserToGuest(user.Id) + assert.Nil(t, err) + updatedUser, err := ss.User().Get(user.Id) + assert.Nil(t, err) + require.Equal(t, "system_guest", updatedUser.Roles) + + updatedTeamMember, err := ss.Team().GetMember(teamId, user.Id) + require.Nil(t, err) + require.True(t, updatedTeamMember.SchemeGuest) + require.False(t, updatedTeamMember.SchemeUser) + + updatedChannelMember, err := ss.Channel().GetMember(channel.Id, user.Id) + require.Nil(t, err) + require.True(t, updatedChannelMember.SchemeGuest) + require.False(t, updatedChannelMember.SchemeUser) + }) + + t.Run("Must work with user without teams or channels", func(t *testing.T) { + id := model.NewId() + user, err := ss.User().Save(&model.User{ + Email: id + "@test.com", + Username: "un_" + id, + Nickname: "nn_" + id, + FirstName: "f_" + id, + LastName: "l_" + id, + Password: "Password1", + Roles: "system_user", + }) + require.Nil(t, err) + + err = ss.User().DemoteUserToGuest(user.Id) + assert.Nil(t, err) + updatedUser, err := ss.User().Get(user.Id) + assert.Nil(t, err) + require.Equal(t, "system_guest", updatedUser.Roles) + }) + + t.Run("Must work with user with teams but no channels", func(t *testing.T) { + id := model.NewId() + user, err := ss.User().Save(&model.User{ + Email: id + "@test.com", + Username: "un_" + id, + Nickname: "nn_" + id, + FirstName: "f_" + id, + LastName: "l_" + id, + Password: "Password1", + Roles: "system_user", + }) + require.Nil(t, err) + + teamId := model.NewId() + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id, SchemeGuest: false, SchemeUser: true}, 999)) + + err = ss.User().DemoteUserToGuest(user.Id) + assert.Nil(t, err) + updatedUser, err := ss.User().Get(user.Id) + assert.Nil(t, err) + require.Equal(t, "system_guest", updatedUser.Roles) + + updatedTeamMember, err := ss.Team().GetMember(teamId, user.Id) + require.Nil(t, err) + require.True(t, updatedTeamMember.SchemeGuest) + require.False(t, updatedTeamMember.SchemeUser) + }) + + t.Run("Must work with user with teams and channels", func(t *testing.T) { + id := model.NewId() + user, err := ss.User().Save(&model.User{ + Email: id + "@test.com", + Username: "un_" + id, + Nickname: "nn_" + id, + FirstName: "f_" + id, + LastName: "l_" + id, + Password: "Password1", + Roles: "system_user", + }) + require.Nil(t, err) + + teamId := model.NewId() + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id, SchemeGuest: false, SchemeUser: true}, 999)) + + channel, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "Channel name", + Name: "channel-" + model.NewId(), + Type: model.CHANNEL_OPEN, + }, -1) + require.Nil(t, err) + _, err = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user.Id, SchemeGuest: false, SchemeUser: true, NotifyProps: model.GetDefaultChannelNotifyProps()}) + require.Nil(t, err) + + err = ss.User().DemoteUserToGuest(user.Id) + assert.Nil(t, err) + updatedUser, err := ss.User().Get(user.Id) + assert.Nil(t, err) + require.Equal(t, "system_guest", updatedUser.Roles) + + updatedTeamMember, err := ss.Team().GetMember(teamId, user.Id) + require.Nil(t, err) + require.True(t, updatedTeamMember.SchemeGuest) + require.False(t, updatedTeamMember.SchemeUser) + + updatedChannelMember, err := ss.Channel().GetMember(channel.Id, user.Id) + assert.Nil(t, err) + require.True(t, updatedChannelMember.SchemeGuest) + require.False(t, updatedChannelMember.SchemeUser) + }) + + t.Run("Must work with user with teams and channels and custom role", func(t *testing.T) { + id := model.NewId() + user, err := ss.User().Save(&model.User{ + Email: id + "@test.com", + Username: "un_" + id, + Nickname: "nn_" + id, + FirstName: "f_" + id, + LastName: "l_" + id, + Password: "Password1", + Roles: "system_user custom_role", + }) + require.Nil(t, err) + + teamId := model.NewId() + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id, SchemeGuest: false, SchemeUser: true}, 999)) + + channel, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "Channel name", + Name: "channel-" + model.NewId(), + Type: model.CHANNEL_OPEN, + }, -1) + require.Nil(t, err) + _, err = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user.Id, SchemeGuest: false, SchemeUser: true, NotifyProps: model.GetDefaultChannelNotifyProps()}) + require.Nil(t, err) + + err = ss.User().DemoteUserToGuest(user.Id) + assert.Nil(t, err) + updatedUser, err := ss.User().Get(user.Id) + assert.Nil(t, err) + require.Equal(t, "system_guest custom_role", updatedUser.Roles) + + updatedTeamMember, err := ss.Team().GetMember(teamId, user.Id) + require.Nil(t, err) + require.True(t, updatedTeamMember.SchemeGuest) + require.False(t, updatedTeamMember.SchemeUser) + + updatedChannelMember, err := ss.Channel().GetMember(channel.Id, user.Id) + assert.Nil(t, err) + require.True(t, updatedChannelMember.SchemeGuest) + require.False(t, updatedChannelMember.SchemeUser) + }) + + t.Run("Must no change any other user role", func(t *testing.T) { + id := model.NewId() + user1, err := ss.User().Save(&model.User{ + Email: id + "@test.com", + Username: "un_" + id, + Nickname: "nn_" + id, + FirstName: "f_" + id, + LastName: "l_" + id, + Password: "Password1", + Roles: "system_user", + }) + require.Nil(t, err) + + teamId1 := model.NewId() + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId1, UserId: user1.Id, SchemeGuest: false, SchemeUser: true}, 999)) + + channel, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId1, + DisplayName: "Channel name", + Name: "channel-" + model.NewId(), + Type: model.CHANNEL_OPEN, + }, -1) + require.Nil(t, err) + + _, err = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user1.Id, SchemeGuest: false, SchemeUser: true, NotifyProps: model.GetDefaultChannelNotifyProps()}) + require.Nil(t, err) + + id = model.NewId() + user2, err := ss.User().Save(&model.User{ + Email: id + "@test.com", + Username: "un_" + id, + Nickname: "nn_" + id, + FirstName: "f_" + id, + LastName: "l_" + id, + Password: "Password1", + Roles: "system_user", + }) + require.Nil(t, err) + + teamId2 := model.NewId() + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId2, UserId: user2.Id, SchemeGuest: false, SchemeUser: true}, 999)) + + _, err = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user2.Id, SchemeGuest: false, SchemeUser: true, NotifyProps: model.GetDefaultChannelNotifyProps()}) + require.Nil(t, err) + + err = ss.User().DemoteUserToGuest(user1.Id) + assert.Nil(t, err) + updatedUser, err := ss.User().Get(user1.Id) + assert.Nil(t, err) + require.Equal(t, "system_guest", updatedUser.Roles) + + updatedTeamMember, err := ss.Team().GetMember(teamId1, user1.Id) + require.Nil(t, err) + require.True(t, updatedTeamMember.SchemeGuest) + require.False(t, updatedTeamMember.SchemeUser) + + updatedChannelMember, err := ss.Channel().GetMember(channel.Id, user1.Id) + assert.Nil(t, err) + require.True(t, updatedChannelMember.SchemeGuest) + require.False(t, updatedChannelMember.SchemeUser) + + notUpdatedUser, err := ss.User().Get(user2.Id) + assert.Nil(t, err) + require.Equal(t, "system_user", notUpdatedUser.Roles) + + notUpdatedTeamMember, err := ss.Team().GetMember(teamId2, user2.Id) + require.Nil(t, err) + require.False(t, notUpdatedTeamMember.SchemeGuest) + require.True(t, notUpdatedTeamMember.SchemeUser) + + notUpdatedChannelMember, err := ss.Channel().GetMember(channel.Id, user2.Id) + assert.Nil(t, err) + require.False(t, notUpdatedChannelMember.SchemeGuest) + require.True(t, notUpdatedChannelMember.SchemeUser) + }) +} + func testUserStoreResetLastPictureUpdate(t *testing.T, ss store.Store) { u1 := &model.User{} u1.Email = MakeEmail() diff --git a/templates/invite_body.html b/templates/invite_body.html index 2352d32454..c226f4ada1 100644 --- a/templates/invite_body.html +++ b/templates/invite_body.html @@ -22,6 +22,7 @@

{{.Props.Button}}

+

{{.Props.Message}}


{{.Html.ExtraInfo}}

{{.Props.TeamURL}}

diff --git a/utils/license.go b/utils/license.go index 338ad17586..d62e90fd40 100644 --- a/utils/license.go +++ b/utils/license.go @@ -155,6 +155,7 @@ func GetClientLicense(l *model.License) map[string]string { props["EmailNotificationContents"] = strconv.FormatBool(*l.Features.EmailNotificationContents) props["MessageExport"] = strconv.FormatBool(*l.Features.MessageExport) props["CustomPermissionsSchemes"] = strconv.FormatBool(*l.Features.CustomPermissionsSchemes) + props["GuestAccountsPermissions"] = strconv.FormatBool(*l.Features.GuestAccountsPermissions) props["CustomTermsOfService"] = strconv.FormatBool(*l.Features.CustomTermsOfService) } diff --git a/web/context.go b/web/context.go index ea243f2616..9201026108 100644 --- a/web/context.go +++ b/web/context.go @@ -115,6 +115,9 @@ func (c *Context) MfaRequired() { c.Err = model.NewAppError("", "api.context.session_expired.app_error", nil, "MfaRequired", http.StatusUnauthorized) return } else { + if user.IsGuest() && !*c.App.Config().GuestAccountsSettings.EnforceMultifactorAuthentication { + return + } // Only required for email and ldap accounts if user.AuthService != "" && user.AuthService != model.USER_AUTH_SERVICE_EMAIL &&