PLT-4767 Implement MFA Enforcement (#4662)
* Create MFA setup page and remove MFA setup from account settings modal * Add enforce MFA to system console and force redirect * Lockdown mfa required API routes, add localization, other changes * Minor fixes * Fix typo * Fix some unit tests * Fix more unit tests * Minor fix * Updating UI for MFA screen (#4670) * Updating UI for MFA screen * Updating styles for MFA page * Add the ability to switch between email/sso with MFA enabled * Added mfa change email * Minor UI updates for MFA enforcement * Fix unit test * Fix client unit test * Allow switching email to ldap and back when MFA is enabled * Fix unit test * Revert config.json
Этот коммит содержится в:
коммит произвёл
enahum
родитель
f0d71d8789
Коммит
30a10d35a8
@@ -95,18 +95,22 @@ func TestCreateChannel(t *testing.T) {
|
||||
}
|
||||
|
||||
isLicensed := utils.IsLicensed
|
||||
license := utils.License
|
||||
restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement
|
||||
restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement
|
||||
defer func() {
|
||||
*utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel
|
||||
*utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel
|
||||
utils.IsLicensed = isLicensed
|
||||
utils.License = license
|
||||
utils.SetDefaultRolesBasedOnConfig()
|
||||
}()
|
||||
*utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL
|
||||
*utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL
|
||||
utils.SetDefaultRolesBasedOnConfig()
|
||||
utils.IsLicensed = true
|
||||
utils.License = &model.License{Features: &model.Features{}}
|
||||
utils.License.Features.SetDefaults()
|
||||
|
||||
channel2 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
|
||||
channel3 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id}
|
||||
@@ -288,17 +292,21 @@ func TestUpdateChannel(t *testing.T) {
|
||||
}
|
||||
|
||||
isLicensed := utils.IsLicensed
|
||||
license := utils.License
|
||||
restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement
|
||||
restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement
|
||||
defer func() {
|
||||
*utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel
|
||||
*utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel
|
||||
utils.IsLicensed = isLicensed
|
||||
utils.License = license
|
||||
utils.SetDefaultRolesBasedOnConfig()
|
||||
}()
|
||||
*utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL
|
||||
*utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL
|
||||
utils.IsLicensed = true
|
||||
utils.License = &model.License{Features: &model.Features{}}
|
||||
utils.License.Features.SetDefaults()
|
||||
utils.SetDefaultRolesBasedOnConfig()
|
||||
|
||||
channel2 := th.CreateChannel(Client, team)
|
||||
@@ -436,17 +444,21 @@ func TestUpdateChannelHeader(t *testing.T) {
|
||||
}
|
||||
|
||||
isLicensed := utils.IsLicensed
|
||||
license := utils.License
|
||||
restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement
|
||||
restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement
|
||||
defer func() {
|
||||
*utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel
|
||||
*utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel
|
||||
utils.IsLicensed = isLicensed
|
||||
utils.License = license
|
||||
utils.SetDefaultRolesBasedOnConfig()
|
||||
}()
|
||||
*utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL
|
||||
*utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL
|
||||
utils.IsLicensed = true
|
||||
utils.License = &model.License{Features: &model.Features{}}
|
||||
utils.License.Features.SetDefaults()
|
||||
utils.SetDefaultRolesBasedOnConfig()
|
||||
|
||||
th.LoginBasic()
|
||||
@@ -566,17 +578,21 @@ func TestUpdateChannelPurpose(t *testing.T) {
|
||||
}
|
||||
|
||||
isLicensed := utils.IsLicensed
|
||||
license := utils.License
|
||||
restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement
|
||||
restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement
|
||||
defer func() {
|
||||
*utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel
|
||||
*utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel
|
||||
utils.IsLicensed = isLicensed
|
||||
utils.License = license
|
||||
utils.SetDefaultRolesBasedOnConfig()
|
||||
}()
|
||||
*utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL
|
||||
*utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL
|
||||
utils.IsLicensed = true
|
||||
utils.License = &model.License{Features: &model.Features{}}
|
||||
utils.License.Features.SetDefaults()
|
||||
utils.SetDefaultRolesBasedOnConfig()
|
||||
|
||||
th.LoginBasic()
|
||||
@@ -1071,17 +1087,21 @@ func TestDeleteChannel(t *testing.T) {
|
||||
}
|
||||
|
||||
isLicensed := utils.IsLicensed
|
||||
license := utils.License
|
||||
restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement
|
||||
restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement
|
||||
defer func() {
|
||||
*utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel
|
||||
*utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel
|
||||
utils.IsLicensed = isLicensed
|
||||
utils.License = license
|
||||
utils.SetDefaultRolesBasedOnConfig()
|
||||
}()
|
||||
*utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL
|
||||
*utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL
|
||||
utils.IsLicensed = true
|
||||
utils.License = &model.License{Features: &model.Features{}}
|
||||
utils.License.Features.SetDefaults()
|
||||
utils.SetDefaultRolesBasedOnConfig()
|
||||
|
||||
th.LoginSystemAdmin()
|
||||
|
||||
@@ -47,51 +47,55 @@ type Context struct {
|
||||
}
|
||||
|
||||
func ApiAppHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
|
||||
return &handler{h, false, false, true, false, false, false}
|
||||
return &handler{h, false, false, true, false, false, false, false}
|
||||
}
|
||||
|
||||
func AppHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
|
||||
return &handler{h, false, false, false, false, false, false}
|
||||
return &handler{h, false, false, false, false, false, false, false}
|
||||
}
|
||||
|
||||
func AppHandlerIndependent(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
|
||||
return &handler{h, false, false, false, false, true, false}
|
||||
return &handler{h, false, false, false, false, true, false, false}
|
||||
}
|
||||
|
||||
func ApiUserRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
|
||||
return &handler{h, true, false, true, false, false, false}
|
||||
return &handler{h, true, false, true, false, false, false, true}
|
||||
}
|
||||
|
||||
func ApiUserRequiredActivity(h func(*Context, http.ResponseWriter, *http.Request), isUserActivity bool) http.Handler {
|
||||
return &handler{h, true, false, true, isUserActivity, false, false}
|
||||
return &handler{h, true, false, true, isUserActivity, false, false, true}
|
||||
}
|
||||
|
||||
func ApiUserRequiredMfa(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
|
||||
return &handler{h, true, false, true, false, false, false, false}
|
||||
}
|
||||
|
||||
func UserRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
|
||||
return &handler{h, true, false, false, false, false, false}
|
||||
return &handler{h, true, false, false, false, false, false, true}
|
||||
}
|
||||
|
||||
func AppHandlerTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
|
||||
return &handler{h, false, false, false, false, false, true}
|
||||
return &handler{h, false, false, false, false, false, true, false}
|
||||
}
|
||||
|
||||
func ApiAdminSystemRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
|
||||
return &handler{h, true, true, true, false, false, false}
|
||||
return &handler{h, true, true, true, false, false, false, true}
|
||||
}
|
||||
|
||||
func ApiAdminSystemRequiredTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
|
||||
return &handler{h, true, true, true, false, false, true}
|
||||
return &handler{h, true, true, true, false, false, true, true}
|
||||
}
|
||||
|
||||
func ApiAppHandlerTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
|
||||
return &handler{h, false, false, true, false, false, true}
|
||||
return &handler{h, false, false, true, false, false, true, false}
|
||||
}
|
||||
|
||||
func ApiUserRequiredTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
|
||||
return &handler{h, true, false, true, false, false, true}
|
||||
return &handler{h, true, false, true, false, false, true, true}
|
||||
}
|
||||
|
||||
func ApiAppHandlerTrustRequesterIndependent(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
|
||||
return &handler{h, false, false, true, false, true, true}
|
||||
return &handler{h, false, false, true, false, true, true, false}
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
@@ -102,6 +106,7 @@ type handler struct {
|
||||
isUserActivity bool
|
||||
isTeamIndependent bool
|
||||
trustRequester bool
|
||||
requireMfa bool
|
||||
}
|
||||
|
||||
func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -204,6 +209,10 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
c.UserRequired()
|
||||
}
|
||||
|
||||
if c.Err == nil && h.requireMfa {
|
||||
c.MfaRequired()
|
||||
}
|
||||
|
||||
if c.Err == nil && h.requireSystemAdmin {
|
||||
c.SystemAdminRequired()
|
||||
}
|
||||
@@ -331,6 +340,39 @@ func (c *Context) UserRequired() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Context) MfaRequired() {
|
||||
// Must be licensed for MFA and have it configured for enforcement
|
||||
if !utils.IsLicensed || !*utils.License.Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication || !*utils.Cfg.ServiceSettings.EnforceMultifactorAuthentication {
|
||||
return
|
||||
}
|
||||
|
||||
// OAuth integrations are excepted
|
||||
if c.Session.IsOAuth {
|
||||
return
|
||||
}
|
||||
|
||||
if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil {
|
||||
c.Err = model.NewLocAppError("", "api.context.session_expired.app_error", nil, "MfaRequired")
|
||||
c.Err.StatusCode = http.StatusUnauthorized
|
||||
return
|
||||
} else {
|
||||
user := result.Data.(*model.User)
|
||||
|
||||
// Only required for email and ldap accounts
|
||||
if user.AuthService != "" &&
|
||||
user.AuthService != model.USER_AUTH_SERVICE_EMAIL &&
|
||||
user.AuthService != model.USER_AUTH_SERVICE_LDAP {
|
||||
return
|
||||
}
|
||||
|
||||
if !user.MfaActive {
|
||||
c.Err = model.NewLocAppError("", "api.context.mfa_required.app_error", nil, "MfaRequired")
|
||||
c.Err.StatusCode = http.StatusUnauthorized
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Context) SystemAdminRequired() {
|
||||
if len(c.Session.UserId) == 0 {
|
||||
c.Err = model.NewLocAppError("", "api.context.session_expired.app_error", nil, "SystemAdminRequired")
|
||||
|
||||
@@ -856,7 +856,7 @@ func CompleteSwitchWithOAuth(c *Context, w http.ResponseWriter, r *http.Request,
|
||||
return
|
||||
}
|
||||
|
||||
if result := <-Srv.Store.User().UpdateAuthData(user.Id, service, &authData, ssoEmail); result.Err != nil {
|
||||
if result := <-Srv.Store.User().UpdateAuthData(user.Id, service, &authData, ssoEmail, true); result.Err != nil {
|
||||
c.Err = result.Err
|
||||
return
|
||||
}
|
||||
|
||||
@@ -426,10 +426,14 @@ func TestInviteMembers(t *testing.T) {
|
||||
}
|
||||
|
||||
isLicensed := utils.IsLicensed
|
||||
license := utils.License
|
||||
defer func() {
|
||||
utils.IsLicensed = isLicensed
|
||||
utils.License = license
|
||||
}()
|
||||
utils.IsLicensed = true
|
||||
utils.License = &model.License{Features: &model.Features{}}
|
||||
utils.License.Features.SetDefaults()
|
||||
|
||||
if _, err := Client.InviteMembers(invites); err == nil {
|
||||
t.Fatal("should have errored not team admin and licensed")
|
||||
|
||||
59
api/user.go
59
api/user.go
@@ -65,8 +65,8 @@ func InitUser() {
|
||||
BaseRoutes.NeedChannel.Handle("/users/autocomplete", ApiUserRequired(autocompleteUsersInChannel)).Methods("GET")
|
||||
|
||||
BaseRoutes.Users.Handle("/mfa", ApiAppHandler(checkMfa)).Methods("POST")
|
||||
BaseRoutes.Users.Handle("/generate_mfa_secret", ApiUserRequiredTrustRequester(generateMfaSecret)).Methods("GET")
|
||||
BaseRoutes.Users.Handle("/update_mfa", ApiUserRequired(updateMfa)).Methods("POST")
|
||||
BaseRoutes.Users.Handle("/generate_mfa_secret", ApiUserRequiredMfa(generateMfaSecret)).Methods("GET")
|
||||
BaseRoutes.Users.Handle("/update_mfa", ApiUserRequiredMfa(updateMfa)).Methods("POST")
|
||||
|
||||
BaseRoutes.Users.Handle("/claim/email_to_oauth", ApiAppHandler(emailToOAuth)).Methods("POST")
|
||||
BaseRoutes.Users.Handle("/claim/oauth_to_email", ApiUserRequired(oauthToEmail)).Methods("POST")
|
||||
@@ -1875,6 +1875,30 @@ func sendPasswordChangeEmail(c *Context, email, siteURL, method string) {
|
||||
}
|
||||
}
|
||||
|
||||
func sendMfaChangeEmail(c *Context, email string, siteURL string, activated bool) {
|
||||
subject := c.T("api.templates.mfa_change_subject",
|
||||
map[string]interface{}{"SiteName": utils.Cfg.TeamSettings.SiteName})
|
||||
|
||||
bodyPage := utils.NewHTMLTemplate("mfa_change_body", c.Locale)
|
||||
bodyPage.Props["SiteURL"] = siteURL
|
||||
|
||||
bodyText := ""
|
||||
if activated {
|
||||
bodyText = "api.templates.mfa_activated_body.info"
|
||||
bodyPage.Props["Title"] = c.T("api.templates.mfa_activated_body.title")
|
||||
} else {
|
||||
bodyText = "api.templates.mfa_deactivated_body.info"
|
||||
bodyPage.Props["Title"] = c.T("api.templates.mfa_deactivated_body.title")
|
||||
}
|
||||
|
||||
bodyPage.Html["Info"] = template.HTML(c.T(bodyText,
|
||||
map[string]interface{}{"SiteURL": siteURL}))
|
||||
|
||||
if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil {
|
||||
l4g.Error(utils.T("api.user.send_mfa_change_email.error"), err)
|
||||
}
|
||||
}
|
||||
|
||||
func sendEmailChangeEmail(c *Context, oldEmail, newEmail, siteURL string) {
|
||||
subject := fmt.Sprintf("[%v] %v", utils.Cfg.TeamSettings.SiteName, c.T("api.templates.email_change_subject",
|
||||
map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName}))
|
||||
@@ -2016,6 +2040,8 @@ func emailToOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
mfaToken := props["token"]
|
||||
|
||||
service := props["service"]
|
||||
if len(service) == 0 {
|
||||
c.SetInvalidParam("emailToOAuth", "service")
|
||||
@@ -2039,7 +2065,7 @@ func emailToOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
user = result.Data.(*model.User)
|
||||
}
|
||||
|
||||
if err := checkPasswordAndAllCriteria(user, password, ""); err != nil {
|
||||
if err := checkPasswordAndAllCriteria(user, password, mfaToken); err != nil {
|
||||
c.LogAuditWithUserId(user.Id, "failed - bad authentication")
|
||||
c.Err = err
|
||||
return
|
||||
@@ -2147,6 +2173,8 @@ func emailToLdap(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
token := props["token"]
|
||||
|
||||
c.LogAudit("attempt")
|
||||
|
||||
var user *model.User
|
||||
@@ -2158,7 +2186,7 @@ func emailToLdap(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
user = result.Data.(*model.User)
|
||||
}
|
||||
|
||||
if err := checkPasswordAndAllCriteria(user, emailPassword, ""); err != nil {
|
||||
if err := checkPasswordAndAllCriteria(user, emailPassword, token); err != nil {
|
||||
c.LogAuditWithUserId(user.Id, "failed - bad authentication")
|
||||
c.Err = err
|
||||
return
|
||||
@@ -2213,6 +2241,8 @@ func ldapToEmail(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
token := props["token"]
|
||||
|
||||
c.LogAudit("attempt")
|
||||
|
||||
var user *model.User
|
||||
@@ -2242,6 +2272,12 @@ func ldapToEmail(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := checkUserMfa(user, token); err != nil {
|
||||
c.LogAuditWithUserId(user.Id, "fail - mfa token failed")
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if result := <-Srv.Store.User().UpdatePassword(user.Id, model.HashPassword(emailPassword)); result.Err != nil {
|
||||
c.LogAudit("fail - database issue")
|
||||
c.Err = result.Err
|
||||
@@ -2379,18 +2415,33 @@ func updateMfa(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
c.LogAudit("attempt")
|
||||
|
||||
if activate {
|
||||
if err := ActivateMfa(c.Session.UserId, token); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
c.LogAudit("success - activated")
|
||||
} else {
|
||||
if err := DeactivateMfa(c.Session.UserId); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
c.LogAudit("success - deactivated")
|
||||
}
|
||||
|
||||
go func() {
|
||||
var user *model.User
|
||||
if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil {
|
||||
l4g.Warn(result.Err)
|
||||
} else {
|
||||
user = result.Data.(*model.User)
|
||||
}
|
||||
|
||||
sendMfaChangeEmail(c, user.Email, c.GetSiteURL(), activate)
|
||||
}()
|
||||
|
||||
rdata := map[string]string{}
|
||||
rdata["status"] = "ok"
|
||||
w.Write([]byte(model.MapToJson(rdata)))
|
||||
|
||||
@@ -1341,7 +1341,7 @@ func TestResetPassword(t *testing.T) {
|
||||
}
|
||||
|
||||
authData := model.NewId()
|
||||
if result := <-Srv.Store.User().UpdateAuthData(user.Id, "random", &authData, ""); result.Err != nil {
|
||||
if result := <-Srv.Store.User().UpdateAuthData(user.Id, "random", &authData, "", true); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
}
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user