Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions api/v4/source/system.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,9 @@
is_gov_sku:
type: boolean
description: Whether this is a government SKU license
is_non_production:
type: boolean
description: Whether the license is a non-production (developer) key
customer:
type: object
properties:
Expand Down
16 changes: 13 additions & 3 deletions server/channels/api4/team_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,12 @@ func localInviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request)
if !isEmailAddressAllowed(emailAddress, allowedDomains) {
invite.Error = model.NewAppError("localInviteUsersToTeam", "api.team.invite_members.invalid_email.app_error", map[string]any{"Addresses": emailAddress}, "", http.StatusBadRequest)
errList = append(errList, model.EmailInviteWithErrorToString(invite))
} else if deactivated, userErr := c.App.IsDeactivatedUserEmail(emailAddress); userErr != nil {
invite.Error = userErr
errList = append(errList, model.EmailInviteWithErrorToString(invite))
} else if deactivated {
invite.Error = model.NewAppError("localInviteUsersToTeam", "api.team.invite_members.account_deactivated.app_error", map[string]any{"Addresses": emailAddress}, "", http.StatusBadRequest)
errList = append(errList, model.EmailInviteWithErrorToString(invite))
} else {
goodEmails = append(goodEmails, emailAddress)
}
Expand Down Expand Up @@ -218,16 +224,20 @@ func localInviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request)
} else {
var invalidEmailList []string

for _, email := range emailList {
if !isEmailAddressAllowed(email, allowedDomains) {
invalidEmailList = append(invalidEmailList, email)
for _, emailAddr := range emailList {
if !isEmailAddressAllowed(emailAddr, allowedDomains) {
invalidEmailList = append(invalidEmailList, emailAddr)
}
}
if len(invalidEmailList) > 0 {
s := strings.Join(invalidEmailList, ", ")
c.Err = model.NewAppError("localInviteUsersToTeam", "api.team.invite_members.invalid_email.app_error", map[string]any{"Addresses": s}, "", http.StatusBadRequest)
return
}
if c.Err = c.App.CheckForDeactivatedInvites("localInviteUsersToTeam", emailList); c.Err != nil {
return
}

err := c.App.Srv().EmailService.SendInviteEmails(c.AppContext, email.InviteEmailData{
Team: team,
SenderName: "Administrator",
Expand Down
29 changes: 29 additions & 0 deletions server/channels/api4/team_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4285,6 +4285,35 @@ func TestInviteUsersToTeam(t *testing.T) {
}, "rate limits")
}

func TestLocalInviteUsersToTeamDeactivatedUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)

th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableEmailInvitations = true
})

_, appErr := th.App.UpdateActive(th.Context, th.BasicUser2, false)
require.Nil(t, appErr)
t.Cleanup(func() {
_, _ = th.App.UpdateActive(th.Context, th.BasicUser2, true)
})

t.Run("non-graceful local invite returns error for deactivated user", func(t *testing.T) {
_, err := th.LocalClient.InviteUsersToTeam(context.Background(), th.BasicTeam.Id, []string{th.BasicUser2.Email})
require.Error(t, err)
CheckErrorID(t, err, "api.team.invite_members.account_deactivated.app_error")
})

t.Run("graceful local invite returns error for deactivated user", func(t *testing.T) {
invitesWithErrors, _, err := th.LocalClient.InviteUsersToTeamGracefully(context.Background(), th.BasicTeam.Id, []string{th.BasicUser2.Email})
require.NoError(t, err)
require.Len(t, invitesWithErrors, 1)
require.NotNil(t, invitesWithErrors[0].Error)
CheckErrorID(t, invitesWithErrors[0].Error, "api.team.invite_members.account_deactivated.app_error")
})
}

func TestInviteUsersToTeamWithProfiles(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
Expand Down
6 changes: 6 additions & 0 deletions server/channels/api4/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -2938,6 +2938,12 @@ func switchAccountType(c *Context, w http.ResponseWriter, r *http.Request) {
return
}

if c.AppContext.Session().IsOAuth {
c.SetPermissionError(model.PermissionEditOtherUsers)
c.Err.DetailedError += ", attempted access by oauth app"
return
}

link, err = c.App.SwitchOAuthToEmail(c.AppContext, switchRequest.Email, switchRequest.NewPassword, c.AppContext.Session().UserId)
} else if switchRequest.EmailToLdap() {
link, err = c.App.SwitchEmailToLdap(c.AppContext, switchRequest.Email, switchRequest.Password, switchRequest.MfaCode, switchRequest.LdapLoginId, switchRequest.NewPassword)
Expand Down
29 changes: 29 additions & 0 deletions server/channels/api4/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5695,6 +5695,35 @@ func TestSwitchAccount(t *testing.T) {
require.Equal(t, "/login?extra=signin_change", link)
})

t.Run("OAuth app session cannot switch to email", func(t *testing.T) {
setupUserAuth(t, model.UserAuthServiceGitlab, true)

session, appErr := th.App.GetSession(th.Client.AuthToken)
require.Nil(t, appErr)
session.IsOAuth = true
th.App.AddSessionToCache(session)
t.Cleanup(func() {
th.Server.Platform().ClearUserSessionCacheLocal(th.BasicUser.Id)
})

sr := &model.SwitchRequest{
CurrentService: model.UserAuthServiceGitlab,
NewService: model.UserAuthServiceEmail,
Email: th.BasicUser.Email,
NewPassword: model.NewTestPassword(),
}

_, resp, err := th.Client.SwitchAccountType(context.Background(), sr)
require.Error(t, err)
CheckForbiddenStatus(t, resp)

// The account must remain attached to its login provider
th.App.InvalidateCacheForUser(th.BasicUser.Id)
user, appErr := th.App.GetUser(th.BasicUser.Id)
require.Nil(t, appErr)
require.Equal(t, model.UserAuthServiceGitlab, user.AuthService)
})

t.Run("Disabled if EnableSignUpWithEmail is false", func(t *testing.T) {
setupUserAuth(t, model.UserAuthServiceGitlab, true)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.EmailSettings.EnableSignUpWithEmail = false })
Expand Down
4 changes: 4 additions & 0 deletions server/channels/app/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -1211,6 +1211,10 @@ func (a *App) SwitchOAuthToEmail(rctx request.CTX, email, password, requesterId
return "", model.NewAppError("oauthToEmail", "api.user.oauth_to_email.not_available.app_error", nil, "", http.StatusForbidden)
}

if rctx.Session().IsOAuth {
return "", model.NewAppError("SwitchOAuthToEmail", "api.user.oauth_to_email.integration_session.app_error", nil, "", http.StatusForbidden)
}

if !*a.Config().EmailSettings.EnableSignUpWithEmail {
return "", model.NewAppError("SwitchOAuthToEmail", "api.user.auth_switch.not_available.email_signup_disabled.app_error", nil, "", http.StatusForbidden)
}
Expand Down
49 changes: 49 additions & 0 deletions server/channels/app/oauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1621,6 +1621,55 @@ func TestOAuthImplicitGrantRejectsDeactivatedUser(t *testing.T) {
require.Empty(t, accessData, "no access data may be persisted for an inactive user")
}

func TestSwitchOAuthToEmail(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)

setupOAuthUser := func(t *testing.T) *model.User {
t.Helper()

authData := model.NewId()
_, err := th.App.Srv().Store().User().UpdateAuthData(th.BasicUser.Id, model.UserAuthServiceGitlab, &authData, th.BasicUser.Email, true)
require.NoError(t, err)
th.App.InvalidateCacheForUser(th.BasicUser.Id)

user, appErr := th.App.GetUser(th.BasicUser.Id)
require.Nil(t, appErr)
require.Equal(t, model.UserAuthServiceGitlab, user.AuthService)

return user
}

t.Run("rejects integration session", func(t *testing.T) {
user := setupOAuthUser(t)

rctx := th.Context.WithSession(&model.Session{UserId: user.Id, Id: model.NewId(), IsOAuth: true})

_, appErr := th.App.SwitchOAuthToEmail(rctx, user.Email, model.NewTestPassword(), user.Id)
require.NotNil(t, appErr)
require.Equal(t, "api.user.oauth_to_email.integration_session.app_error", appErr.Id)
require.Equal(t, http.StatusForbidden, appErr.StatusCode)

user, appErr = th.App.GetUser(user.Id)
require.Nil(t, appErr)
require.Equal(t, model.UserAuthServiceGitlab, user.AuthService)
})

t.Run("allows regular session", func(t *testing.T) {
user := setupOAuthUser(t)

rctx := th.Context.WithSession(&model.Session{UserId: user.Id, Id: model.NewId()})

link, appErr := th.App.SwitchOAuthToEmail(rctx, user.Email, model.NewTestPassword(), user.Id)
require.Nil(t, appErr)
require.Equal(t, "/login?extra=signin_change", link)

user, appErr = th.App.GetUser(user.Id)
require.Nil(t, appErr)
require.Empty(t, user.AuthService)
})
}

func TestParseOAuthStateTokenExtra(t *testing.T) {
t.Run("valid token with normal values", func(t *testing.T) {
email, action, cookie, err := parseOAuthStateTokenExtra("user@example.com:email_to_sso:randomcookie123")
Expand Down
1 change: 1 addition & 0 deletions server/channels/app/platform/license.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@ func (ps *PlatformService) logLicense(message string, license *model.License) {
mlog.String("sku_short_name", license.SkuShortName),
mlog.Bool("is_trial", license.IsTrial),
mlog.Bool("is_gov_sku", license.IsGovSku),
mlog.Bool("is_non_production", license.IsNonProduction),
)

if license.Customer != nil {
Expand Down
1 change: 1 addition & 0 deletions server/channels/app/platform/support_packet.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ func (ps *PlatformService) getSupportPacketDiagnostics(rctx request.CTX) (*model
d.License.SkuShortName = license.SkuShortName
d.License.IsTrial = license.IsTrial
d.License.IsGovSKU = license.IsGovSku
d.License.IsNonProduction = license.IsNonProduction
}

/* Server */
Expand Down
49 changes: 49 additions & 0 deletions server/channels/app/team.go
Original file line number Diff line number Diff line change
Expand Up @@ -1560,6 +1560,39 @@ func (a *App) prepareInviteNewUsersToTeam(teamID, senderId string, channelIds []
return user, team, channels, nil
}

func (a *App) IsDeactivatedUserEmail(email string) (bool, *model.AppError) {
existingUser, appErr := a.GetUserByEmail(email)
if appErr != nil {
if appErr.Id == MissingAccountError {
return false, nil
}
return false, appErr
}
return existingUser.DeleteAt != 0, nil
}

// CheckForDeactivatedInvites returns an error if any email belongs to a
// deactivated account. where identifies the caller in the returned AppError.
func (a *App) CheckForDeactivatedInvites(where string, emailList []string) *model.AppError {
var deactivatedEmailList []string
for _, email := range emailList {
deactivated, userErr := a.IsDeactivatedUserEmail(email)
if userErr != nil {
return userErr
}
if deactivated {
deactivatedEmailList = append(deactivatedEmailList, email)
}
}

if len(deactivatedEmailList) > 0 {
s := strings.Join(deactivatedEmailList, ", ")
return model.NewAppError(where, "api.team.invite_members.account_deactivated.app_error", map[string]any{"Addresses": s}, "", http.StatusBadRequest)
}

return nil
}

// isPreSetUsernameAvailable reports whether a username pre-set on an invite is not
// already taken by an existing user or group.
func (a *App) isPreSetUsernameAvailable(username string) bool {
Expand Down Expand Up @@ -1668,6 +1701,10 @@ func (a *App) sendInviteNewUsersToTeamGracefully(rctx request.CTX, memberInvite
}
if !teams.IsEmailAddressAllowed(invitedEmail, allowedDomains) {
invite.Error = model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.invalid_email.app_error", map[string]any{"Addresses": invitedEmail}, "", http.StatusBadRequest)
} else if deactivated, userErr := a.IsDeactivatedUserEmail(invitedEmail); userErr != nil {
invite.Error = userErr
} else if deactivated {
invite.Error = model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.account_deactivated.app_error", map[string]any{"Addresses": invitedEmail}, "", http.StatusBadRequest)
} else if profile := profilesByEmail[invitedEmail]; profile != nil && !a.isPreSetUsernameAvailable(profile.Username) {
// Catch taken usernames at invite time so the invitee doesn't dead-end at signup.
invite.Error = model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.username_taken.app_error", map[string]any{"Username": profile.Username}, "", http.StatusBadRequest)
Expand Down Expand Up @@ -1817,6 +1854,10 @@ func (a *App) InviteGuestsToChannelsGracefully(rctx request.CTX, teamID string,
}
if !users.CheckEmailDomain(email, *a.Config().GuestAccountsSettings.RestrictCreationToDomains) {
invite.Error = model.NewAppError("InviteGuestsToChannelsGracefully", "api.team.invite_members.invalid_email.app_error", map[string]any{"Addresses": email}, "", http.StatusBadRequest)
} else if deactivated, userErr := a.IsDeactivatedUserEmail(email); userErr != nil {
invite.Error = userErr
} else if deactivated {
invite.Error = model.NewAppError("InviteGuestsToChannelsGracefully", "api.team.invite_members.account_deactivated.app_error", map[string]any{"Addresses": email}, "", http.StatusBadRequest)
} else {
goodEmails = append(goodEmails, email)
}
Expand Down Expand Up @@ -1885,6 +1926,10 @@ func (a *App) InviteNewUsersToTeam(rctx request.CTX, emailList []string, teamID,
return model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.invalid_email.app_error", map[string]any{"Addresses": s}, "", http.StatusBadRequest)
}

if err = a.CheckForDeactivatedInvites("InviteNewUsersToTeam", emailList); err != nil {
return err
}

nameFormat := *a.Config().TeamSettings.TeammateNameDisplay
eErr := a.Srv().EmailService.SendInviteEmails(rctx, email.InviteEmailData{
Team: team,
Expand Down Expand Up @@ -1931,6 +1976,10 @@ func (a *App) InviteGuestsToChannels(rctx request.CTX, teamID string, guestsInvi
return model.NewAppError("InviteGuestsToChannels", "api.team.invite_members.invalid_email.app_error", map[string]any{"Addresses": s}, "", http.StatusBadRequest)
}

if err = a.CheckForDeactivatedInvites("InviteGuestsToChannels", guestsInvite.Emails); err != nil {
return err
}

nameFormat := *a.Config().TeamSettings.TeammateNameDisplay
senderProfileImage, _, err := a.GetProfileImage(user)
if err != nil {
Expand Down
Loading
Loading