Implement APIv4 infrastructure (#5191)
* Implement APIv4 infrastructure * Update parameter requirement functions per feedback
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
3e2f879b77
Коммит
c01d9ad6cf
177
app/authentication.go
Обычный файл
177
app/authentication.go
Обычный файл
@@ -0,0 +1,177 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/mattermost/platform/einterfaces"
|
||||
"github.com/mattermost/platform/model"
|
||||
"github.com/mattermost/platform/utils"
|
||||
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CheckPasswordAndAllCriteria(user *model.User, password string, mfaToken string) *model.AppError {
|
||||
if err := CheckUserAdditionalAuthenticationCriteria(user, mfaToken); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := checkUserPassword(user, password); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// This to be used for places we check the users password when they are already logged in
|
||||
func doubleCheckPassword(user *model.User, password string) *model.AppError {
|
||||
if err := checkUserLoginAttempts(user); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := checkUserPassword(user, password); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkUserPassword(user *model.User, password string) *model.AppError {
|
||||
if !model.ComparePassword(user.Password, password) {
|
||||
if result := <-Srv.Store.User().UpdateFailedPasswordAttempts(user.Id, user.FailedAttempts+1); result.Err != nil {
|
||||
return result.Err
|
||||
}
|
||||
|
||||
return model.NewLocAppError("checkUserPassword", "api.user.check_user_password.invalid.app_error", nil, "user_id="+user.Id)
|
||||
} else {
|
||||
if result := <-Srv.Store.User().UpdateFailedPasswordAttempts(user.Id, 0); result.Err != nil {
|
||||
return result.Err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func checkLdapUserPasswordAndAllCriteria(ldapId *string, password string, mfaToken string) (*model.User, *model.AppError) {
|
||||
ldapInterface := einterfaces.GetLdapInterface()
|
||||
|
||||
if ldapInterface == nil || ldapId == nil {
|
||||
err := model.NewLocAppError("doLdapAuthentication", "api.user.login_ldap.not_available.app_error", nil, "")
|
||||
err.StatusCode = http.StatusNotImplemented
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var user *model.User
|
||||
if ldapUser, err := ldapInterface.DoLogin(*ldapId, password); err != nil {
|
||||
err.StatusCode = http.StatusUnauthorized
|
||||
return nil, err
|
||||
} else {
|
||||
user = ldapUser
|
||||
}
|
||||
|
||||
if err := CheckUserMfa(user, mfaToken); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := checkUserNotDisabled(user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// user successfully authenticated
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func CheckUserAdditionalAuthenticationCriteria(user *model.User, mfaToken string) *model.AppError {
|
||||
if err := CheckUserMfa(user, mfaToken); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := checkEmailVerified(user); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := checkUserNotDisabled(user); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := checkUserLoginAttempts(user); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CheckUserMfa(user *model.User, token string) *model.AppError {
|
||||
if !user.MfaActive || !utils.IsLicensed || !*utils.License.Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication {
|
||||
return nil
|
||||
}
|
||||
|
||||
mfaInterface := einterfaces.GetMfaInterface()
|
||||
if mfaInterface == nil {
|
||||
return model.NewLocAppError("checkUserMfa", "api.user.check_user_mfa.not_available.app_error", nil, "")
|
||||
}
|
||||
|
||||
if ok, err := mfaInterface.ValidateToken(user.MfaSecret, token); err != nil {
|
||||
return err
|
||||
} else if !ok {
|
||||
return model.NewLocAppError("checkUserMfa", "api.user.check_user_mfa.bad_code.app_error", nil, "")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkUserLoginAttempts(user *model.User) *model.AppError {
|
||||
if user.FailedAttempts >= utils.Cfg.ServiceSettings.MaximumLoginAttempts {
|
||||
return model.NewLocAppError("checkUserLoginAttempts", "api.user.check_user_login_attempts.too_many.app_error", nil, "user_id="+user.Id)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkEmailVerified(user *model.User) *model.AppError {
|
||||
if !user.EmailVerified && utils.Cfg.EmailSettings.RequireEmailVerification {
|
||||
return model.NewLocAppError("Login", "api.user.login.not_verified.app_error", nil, "user_id="+user.Id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkUserNotDisabled(user *model.User) *model.AppError {
|
||||
if user.DeleteAt > 0 {
|
||||
return model.NewLocAppError("Login", "api.user.login.inactive.app_error", nil, "user_id="+user.Id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func authenticateUser(user *model.User, password, mfaToken string) (*model.User, *model.AppError) {
|
||||
ldapAvailable := *utils.Cfg.LdapSettings.Enable && einterfaces.GetLdapInterface() != nil && utils.IsLicensed && *utils.License.Features.LDAP
|
||||
|
||||
if user.AuthService == model.USER_AUTH_SERVICE_LDAP {
|
||||
if !ldapAvailable {
|
||||
err := model.NewLocAppError("login", "api.user.login_ldap.not_available.app_error", nil, "")
|
||||
err.StatusCode = http.StatusNotImplemented
|
||||
return user, err
|
||||
} else if ldapUser, err := checkLdapUserPasswordAndAllCriteria(user.AuthData, password, mfaToken); err != nil {
|
||||
err.StatusCode = http.StatusUnauthorized
|
||||
return user, err
|
||||
} else {
|
||||
// slightly redundant to get the user again, but we need to get it from the LDAP server
|
||||
return ldapUser, nil
|
||||
}
|
||||
} else if user.AuthService != "" {
|
||||
authService := user.AuthService
|
||||
if authService == model.USER_AUTH_SERVICE_SAML || authService == model.USER_AUTH_SERVICE_LDAP {
|
||||
authService = strings.ToUpper(authService)
|
||||
}
|
||||
err := model.NewLocAppError("login", "api.user.login.use_auth_service.app_error", map[string]interface{}{"AuthService": authService}, "")
|
||||
err.StatusCode = http.StatusBadRequest
|
||||
return user, err
|
||||
} else {
|
||||
if err := CheckPasswordAndAllCriteria(user, password, mfaToken); err != nil {
|
||||
err.StatusCode = http.StatusUnauthorized
|
||||
return user, err
|
||||
} else {
|
||||
return user, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,13 +5,13 @@ package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "image/gif"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
_ "image/gif"
|
||||
"image/jpeg"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -24,9 +24,9 @@ import (
|
||||
"sync"
|
||||
|
||||
l4g "github.com/alecthomas/log4go"
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/mattermost/platform/model"
|
||||
"github.com/mattermost/platform/utils"
|
||||
"github.com/disintegration/imaging"
|
||||
s3 "github.com/minio/minio-go"
|
||||
"github.com/rwcarlsen/goexif/exif"
|
||||
_ "golang.org/x/image/bmp"
|
||||
@@ -359,7 +359,7 @@ func MigrateFilenamesToFileInfos(post *model.Post) []*model.FileInfo {
|
||||
|
||||
func GeneratePublicLink(siteURL string, info *model.FileInfo) string {
|
||||
hash := GeneratePublicLinkHash(info.Id, *utils.Cfg.FileSettings.PublicLinkSalt)
|
||||
return fmt.Sprintf("%s%s/public/files/%v/get?h=%s", siteURL, model.API_URL_SUFFIX, info.Id, hash)
|
||||
return fmt.Sprintf("%s%s/public/files/%v/get?h=%s", siteURL, model.API_URL_SUFFIX_V3, info.Id, hash)
|
||||
}
|
||||
|
||||
func GeneratePublicLinkHash(fileId, salt string) string {
|
||||
|
||||
136
app/login.go
Обычный файл
136
app/login.go
Обычный файл
@@ -0,0 +1,136 @@
|
||||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/platform/einterfaces"
|
||||
"github.com/mattermost/platform/model"
|
||||
"github.com/mattermost/platform/utils"
|
||||
"github.com/mssola/user_agent"
|
||||
)
|
||||
|
||||
func AuthenticateUserForLogin(id, loginId, password, mfaToken, deviceId string, ldapOnly bool) (*model.User, *model.AppError) {
|
||||
if len(password) == 0 {
|
||||
err := model.NewLocAppError("AuthenticateUserForLogin", "api.user.login.blank_pwd.app_error", nil, "")
|
||||
err.StatusCode = http.StatusBadRequest
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var user *model.User
|
||||
var err *model.AppError
|
||||
|
||||
if len(id) != 0 {
|
||||
if user, err = GetUser(id); err != nil {
|
||||
err.StatusCode = http.StatusBadRequest
|
||||
if einterfaces.GetMetricsInterface() != nil {
|
||||
einterfaces.GetMetricsInterface().IncrementLoginFail()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if user, err = GetUserForLogin(loginId, ldapOnly); err != nil {
|
||||
if einterfaces.GetMetricsInterface() != nil {
|
||||
einterfaces.GetMetricsInterface().IncrementLoginFail()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// and then authenticate them
|
||||
if user, err = authenticateUser(user, password, mfaToken); err != nil {
|
||||
if einterfaces.GetMetricsInterface() != nil {
|
||||
einterfaces.GetMetricsInterface().IncrementLoginFail()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if einterfaces.GetMetricsInterface() != nil {
|
||||
einterfaces.GetMetricsInterface().IncrementLogin()
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func DoLogin(w http.ResponseWriter, r *http.Request, user *model.User, deviceId string) (*model.Session, *model.AppError) {
|
||||
session := &model.Session{UserId: user.Id, Roles: user.GetRawRoles(), DeviceId: deviceId, IsOAuth: false}
|
||||
|
||||
maxAge := *utils.Cfg.ServiceSettings.SessionLengthWebInDays * 60 * 60 * 24
|
||||
|
||||
if len(deviceId) > 0 {
|
||||
session.SetExpireInDays(*utils.Cfg.ServiceSettings.SessionLengthMobileInDays)
|
||||
|
||||
// A special case where we logout of all other sessions with the same Id
|
||||
if err := RevokeSessionsForDeviceId(user.Id, deviceId, ""); err != nil {
|
||||
err.StatusCode = http.StatusInternalServerError
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
session.SetExpireInDays(*utils.Cfg.ServiceSettings.SessionLengthWebInDays)
|
||||
}
|
||||
|
||||
ua := user_agent.New(r.UserAgent())
|
||||
|
||||
plat := ua.Platform()
|
||||
if plat == "" {
|
||||
plat = "unknown"
|
||||
}
|
||||
|
||||
os := ua.OS()
|
||||
if os == "" {
|
||||
os = "unknown"
|
||||
}
|
||||
|
||||
bname, bversion := ua.Browser()
|
||||
if bname == "" {
|
||||
bname = "unknown"
|
||||
}
|
||||
|
||||
if bversion == "" {
|
||||
bversion = "0.0"
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
var err *model.AppError
|
||||
if session, err = CreateSession(session); err != nil {
|
||||
err.StatusCode = http.StatusInternalServerError
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w.Header().Set(model.HEADER_TOKEN, session.Token)
|
||||
|
||||
secure := false
|
||||
if GetProtocol(r) == "https" {
|
||||
secure = true
|
||||
}
|
||||
|
||||
expiresAt := time.Unix(model.GetMillis()/1000+int64(maxAge), 0)
|
||||
sessionCookie := &http.Cookie{
|
||||
Name: model.SESSION_COOKIE_TOKEN,
|
||||
Value: session.Token,
|
||||
Path: "/",
|
||||
MaxAge: maxAge,
|
||||
Expires: expiresAt,
|
||||
HttpOnly: true,
|
||||
Secure: secure,
|
||||
}
|
||||
|
||||
http.SetCookie(w, sessionCookie)
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func GetProtocol(r *http.Request) string {
|
||||
if r.Header.Get(model.HEADER_FORWARDED_PROTO) == "https" {
|
||||
return "https"
|
||||
} else {
|
||||
return "http"
|
||||
}
|
||||
}
|
||||
108
app/user.go
108
app/user.go
@@ -31,6 +31,10 @@ import (
|
||||
)
|
||||
|
||||
func CreateUserWithHash(user *model.User, hash string, data string) (*model.User, *model.AppError) {
|
||||
if err := IsUserSignUpAllowed(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
props := model.MapFromJson(strings.NewReader(data))
|
||||
|
||||
if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
|
||||
@@ -69,6 +73,10 @@ func CreateUserWithHash(user *model.User, hash string, data string) (*model.User
|
||||
}
|
||||
|
||||
func CreateUserWithInviteId(user *model.User, inviteId string, siteURL string) (*model.User, *model.AppError) {
|
||||
if err := IsUserSignUpAllowed(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var team *model.Team
|
||||
if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil {
|
||||
return nil, result.Err
|
||||
@@ -76,6 +84,8 @@ func CreateUserWithInviteId(user *model.User, inviteId string, siteURL string) (
|
||||
team = result.Data.(*model.Team)
|
||||
}
|
||||
|
||||
user.EmailVerified = false
|
||||
|
||||
var ruser *model.User
|
||||
var err *model.AppError
|
||||
if ruser, err = CreateUser(user); err != nil {
|
||||
@@ -95,6 +105,40 @@ func CreateUserWithInviteId(user *model.User, inviteId string, siteURL string) (
|
||||
return ruser, nil
|
||||
}
|
||||
|
||||
func CreateUserFromSignup(user *model.User, siteURL string) (*model.User, *model.AppError) {
|
||||
if err := IsUserSignUpAllowed(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !IsFirstUserAccount() && !*utils.Cfg.TeamSettings.EnableOpenServer {
|
||||
err := model.NewLocAppError("CreateUserFromSignup", "api.user.create_user.no_open_server", nil, "email="+user.Email)
|
||||
err.StatusCode = http.StatusForbidden
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user.EmailVerified = false
|
||||
|
||||
ruser, err := CreateUser(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.Locale, siteURL); err != nil {
|
||||
l4g.Error(err.Error())
|
||||
}
|
||||
|
||||
return ruser, nil
|
||||
}
|
||||
|
||||
func IsUserSignUpAllowed() *model.AppError {
|
||||
if !utils.Cfg.EmailSettings.EnableSignUpWithEmail || !utils.Cfg.TeamSettings.EnableUserCreation {
|
||||
err := model.NewLocAppError("IsUserSignUpAllowed", "api.user.create_user.signup_email_disabled.app_error", nil, "")
|
||||
err.StatusCode = http.StatusNotImplemented
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func IsFirstUserAccount() bool {
|
||||
if SessionCacheLength() == 0 {
|
||||
if cr := <-Srv.Store.User().GetTotalUsersCount(); cr.Err != nil {
|
||||
@@ -575,6 +619,43 @@ func SetProfileImage(userId string, imageData *multipart.FileHeader) *model.AppE
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdatePasswordAsUser(userId, currentPassword, newPassword, siteURL string) *model.AppError {
|
||||
var user *model.User
|
||||
var err *model.AppError
|
||||
|
||||
if user, err = GetUser(userId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
err = model.NewLocAppError("updatePassword", "api.user.update_password.valid_account.app_error", nil, "")
|
||||
err.StatusCode = http.StatusBadRequest
|
||||
return err
|
||||
}
|
||||
|
||||
if user.AuthData != nil && *user.AuthData != "" {
|
||||
err = model.NewLocAppError("updatePassword", "api.user.update_password.oauth.app_error", nil, "auth_service="+user.AuthService)
|
||||
err.StatusCode = http.StatusBadRequest
|
||||
return err
|
||||
}
|
||||
|
||||
if err := doubleCheckPassword(user, currentPassword); err != nil {
|
||||
if err.Id == "api.user.check_user_password.invalid.app_error" {
|
||||
err = model.NewLocAppError("updatePassword", "api.user.update_password.incorrect.app_error", nil, "")
|
||||
}
|
||||
err.StatusCode = http.StatusForbidden
|
||||
return err
|
||||
}
|
||||
|
||||
T := utils.GetUserTranslations(user.Locale)
|
||||
|
||||
if err := UpdatePasswordSendEmail(user, newPassword, T("api.user.update_password.menu"), siteURL); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateActiveNoLdap(userId string, active bool) (*model.User, *model.AppError) {
|
||||
var user *model.User
|
||||
var err *model.AppError
|
||||
@@ -624,6 +705,33 @@ func UpdateActive(user *model.User, active bool) (*model.User, *model.AppError)
|
||||
}
|
||||
}
|
||||
|
||||
func SanitizeProfile(user *model.User, asAdmin bool) {
|
||||
options := utils.Cfg.GetSanitizeOptions()
|
||||
if asAdmin {
|
||||
options["email"] = true
|
||||
options["fullname"] = true
|
||||
options["authservice"] = true
|
||||
}
|
||||
user.SanitizeProfile(options)
|
||||
}
|
||||
|
||||
func UpdateUserAsUser(user *model.User, siteURL string, asAdmin bool) (*model.User, *model.AppError) {
|
||||
updatedUser, err := UpdateUser(user, siteURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
SanitizeProfile(updatedUser, asAdmin)
|
||||
|
||||
omitUsers := make(map[string]bool, 1)
|
||||
omitUsers[updatedUser.Id] = true
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_UPDATED, "", "", "", omitUsers)
|
||||
message.Add("user", updatedUser)
|
||||
go Publish(message)
|
||||
|
||||
return updatedUser, nil
|
||||
}
|
||||
|
||||
func UpdateUser(user *model.User, siteURL string) (*model.User, *model.AppError) {
|
||||
if result := <-Srv.Store.User().Update(user, false); result.Err != nil {
|
||||
return nil, result.Err
|
||||
|
||||
Ссылка в новой задаче
Block a user