Mono repo -> Master (#22553)
Combines the following repositories into one: https://github.com/mattermost/mattermost-server https://github.com/mattermost/mattermost-webapp https://github.com/mattermost/focalboard https://github.com/mattermost/mattermost-plugin-playbooks
Этот коммит содержится в:
16
server/boards/services/auth/email.go
Обычный файл
16
server/boards/services/auth/email.go
Обычный файл
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package auth
|
||||
|
||||
import "regexp"
|
||||
|
||||
var emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
|
||||
|
||||
// IsEmailValid checks if the email provided passes the required structure and length.
|
||||
func IsEmailValid(e string) bool {
|
||||
if len(e) < 3 || len(e) > 254 {
|
||||
return false
|
||||
}
|
||||
return emailRegex.MatchString(e)
|
||||
}
|
||||
109
server/boards/services/auth/password.go
Обычный файл
109
server/boards/services/auth/password.go
Обычный файл
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
PasswordMaximumLength = 64
|
||||
PasswordSpecialChars = "!\"\\#$%&'()*+,-./:;<=>?@[]^_`|~" //nolint:gosec
|
||||
PasswordNumbers = "0123456789"
|
||||
PasswordUpperCaseLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
PasswordLowerCaseLetters = "abcdefghijklmnopqrstuvwxyz"
|
||||
PasswordAllChars = PasswordSpecialChars + PasswordNumbers + PasswordUpperCaseLetters + PasswordLowerCaseLetters
|
||||
|
||||
InvalidLowercasePassword = "lowercase"
|
||||
InvalidMinLengthPassword = "min-length"
|
||||
InvalidMaxLengthPassword = "max-length"
|
||||
InvalidNumberPassword = "number"
|
||||
InvalidUppercasePassword = "uppercase"
|
||||
InvalidSymbolPassword = "symbol"
|
||||
)
|
||||
|
||||
var PasswordHashStrength = 10
|
||||
|
||||
// HashPassword generates a hash using the bcrypt.GenerateFromPassword.
|
||||
func HashPassword(password string) string {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), PasswordHashStrength)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return string(hash)
|
||||
}
|
||||
|
||||
// ComparePassword compares the hash.
|
||||
func ComparePassword(hash, password string) bool {
|
||||
if password == "" || hash == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
type InvalidPasswordError struct {
|
||||
FailingCriterias []string
|
||||
}
|
||||
|
||||
func (ipe *InvalidPasswordError) Error() string {
|
||||
return fmt.Sprintf("invalid password, failing criteria: %s", strings.Join(ipe.FailingCriterias, ", "))
|
||||
}
|
||||
|
||||
type PasswordSettings struct {
|
||||
MinimumLength int
|
||||
Lowercase bool
|
||||
Number bool
|
||||
Uppercase bool
|
||||
Symbol bool
|
||||
}
|
||||
|
||||
func IsPasswordValid(password string, settings PasswordSettings) error {
|
||||
err := &InvalidPasswordError{
|
||||
FailingCriterias: []string{},
|
||||
}
|
||||
|
||||
if len(password) < settings.MinimumLength {
|
||||
err.FailingCriterias = append(err.FailingCriterias, InvalidMinLengthPassword)
|
||||
}
|
||||
|
||||
if len(password) > PasswordMaximumLength {
|
||||
err.FailingCriterias = append(err.FailingCriterias, InvalidMaxLengthPassword)
|
||||
}
|
||||
|
||||
if settings.Lowercase {
|
||||
if !strings.ContainsAny(password, PasswordLowerCaseLetters) {
|
||||
err.FailingCriterias = append(err.FailingCriterias, InvalidLowercasePassword)
|
||||
}
|
||||
}
|
||||
|
||||
if settings.Uppercase {
|
||||
if !strings.ContainsAny(password, PasswordUpperCaseLetters) {
|
||||
err.FailingCriterias = append(err.FailingCriterias, InvalidUppercasePassword)
|
||||
}
|
||||
}
|
||||
|
||||
if settings.Number {
|
||||
if !strings.ContainsAny(password, PasswordNumbers) {
|
||||
err.FailingCriterias = append(err.FailingCriterias, InvalidNumberPassword)
|
||||
}
|
||||
}
|
||||
|
||||
if settings.Symbol {
|
||||
if !strings.ContainsAny(password, PasswordSpecialChars) {
|
||||
err.FailingCriterias = append(err.FailingCriterias, InvalidSymbolPassword)
|
||||
}
|
||||
}
|
||||
|
||||
if len(err.FailingCriterias) > 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
148
server/boards/services/auth/password_test.go
Обычный файл
148
server/boards/services/auth/password_test.go
Обычный файл
@@ -0,0 +1,148 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPasswordHash(t *testing.T) {
|
||||
hash := HashPassword("Test")
|
||||
|
||||
assert.True(t, ComparePassword(hash, "Test"), "Passwords don't match")
|
||||
assert.False(t, ComparePassword(hash, "Test2"), "Passwords should not have matched")
|
||||
}
|
||||
|
||||
func TestIsPasswordValidWithSettings(t *testing.T) {
|
||||
for name, tc := range map[string]struct {
|
||||
Password string
|
||||
Settings PasswordSettings
|
||||
ExpectedFailingCriterias []string
|
||||
}{
|
||||
"Short": {
|
||||
Password: strings.Repeat("x", 3),
|
||||
Settings: PasswordSettings{
|
||||
MinimumLength: 3,
|
||||
Lowercase: false,
|
||||
Uppercase: false,
|
||||
Number: false,
|
||||
Symbol: false,
|
||||
},
|
||||
},
|
||||
"Long": {
|
||||
Password: strings.Repeat("x", PasswordMaximumLength),
|
||||
Settings: PasswordSettings{
|
||||
MinimumLength: 3,
|
||||
Lowercase: false,
|
||||
Uppercase: false,
|
||||
Number: false,
|
||||
Symbol: false,
|
||||
},
|
||||
},
|
||||
"TooShort": {
|
||||
Password: strings.Repeat("x", 2),
|
||||
Settings: PasswordSettings{
|
||||
MinimumLength: 3,
|
||||
Lowercase: false,
|
||||
Uppercase: false,
|
||||
Number: false,
|
||||
Symbol: false,
|
||||
},
|
||||
ExpectedFailingCriterias: []string{"min-length"},
|
||||
},
|
||||
"TooLong": {
|
||||
Password: strings.Repeat("x", PasswordMaximumLength+1),
|
||||
Settings: PasswordSettings{
|
||||
MinimumLength: 3,
|
||||
Lowercase: false,
|
||||
Uppercase: false,
|
||||
Number: false,
|
||||
Symbol: false,
|
||||
},
|
||||
ExpectedFailingCriterias: []string{"max-length"},
|
||||
},
|
||||
"MissingLower": {
|
||||
Password: "AAAAAAAAAAASD123!@#",
|
||||
Settings: PasswordSettings{
|
||||
MinimumLength: 3,
|
||||
Lowercase: true,
|
||||
Uppercase: false,
|
||||
Number: false,
|
||||
Symbol: false,
|
||||
},
|
||||
ExpectedFailingCriterias: []string{"lowercase"},
|
||||
},
|
||||
"MissingUpper": {
|
||||
Password: "aaaaaaaaaaaaasd123!@#",
|
||||
Settings: PasswordSettings{
|
||||
MinimumLength: 3,
|
||||
Uppercase: true,
|
||||
Lowercase: false,
|
||||
Number: false,
|
||||
Symbol: false,
|
||||
},
|
||||
ExpectedFailingCriterias: []string{"uppercase"},
|
||||
},
|
||||
"MissingNumber": {
|
||||
Password: "asasdasdsadASD!@#",
|
||||
Settings: PasswordSettings{
|
||||
MinimumLength: 3,
|
||||
Number: true,
|
||||
Lowercase: false,
|
||||
Uppercase: false,
|
||||
Symbol: false,
|
||||
},
|
||||
ExpectedFailingCriterias: []string{"number"},
|
||||
},
|
||||
"MissingSymbol": {
|
||||
Password: "asdasdasdasdasdASD123",
|
||||
Settings: PasswordSettings{
|
||||
MinimumLength: 3,
|
||||
Symbol: true,
|
||||
Lowercase: false,
|
||||
Uppercase: false,
|
||||
Number: false,
|
||||
},
|
||||
ExpectedFailingCriterias: []string{"symbol"},
|
||||
},
|
||||
"MissingMultiple": {
|
||||
Password: "asdasdasdasdasdasd",
|
||||
Settings: PasswordSettings{
|
||||
MinimumLength: 3,
|
||||
Lowercase: true,
|
||||
Uppercase: true,
|
||||
Number: true,
|
||||
Symbol: true,
|
||||
},
|
||||
ExpectedFailingCriterias: []string{"uppercase", "number", "symbol"},
|
||||
},
|
||||
"Everything": {
|
||||
Password: "asdASD!@#123",
|
||||
Settings: PasswordSettings{
|
||||
MinimumLength: 3,
|
||||
Lowercase: true,
|
||||
Uppercase: true,
|
||||
Number: true,
|
||||
Symbol: true,
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
err := IsPasswordValid(tc.Password, tc.Settings)
|
||||
if len(tc.ExpectedFailingCriterias) == 0 {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
var errFC *InvalidPasswordError
|
||||
if assert.ErrorAs(t, err, &errFC) {
|
||||
assert.Equal(t, tc.ExpectedFailingCriterias, errFC.FailingCriterias)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
67
server/boards/services/auth/request_parser.go
Обычный файл
67
server/boards/services/auth/request_parser.go
Обычный файл
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
HeaderToken = "token"
|
||||
HeaderAuth = "Authorization"
|
||||
HeaderBearer = "BEARER"
|
||||
SessionCookieToken = "FOCALBOARDAUTHTOKEN"
|
||||
)
|
||||
|
||||
type TokenLocation int
|
||||
|
||||
const (
|
||||
TokenLocationNotFound TokenLocation = iota
|
||||
TokenLocationHeader
|
||||
TokenLocationCookie
|
||||
TokenLocationQueryString
|
||||
)
|
||||
|
||||
func (tl TokenLocation) String() string {
|
||||
switch tl {
|
||||
case TokenLocationNotFound:
|
||||
return "Not Found"
|
||||
case TokenLocationHeader:
|
||||
return "Header"
|
||||
case TokenLocationCookie:
|
||||
return "Cookie"
|
||||
case TokenLocationQueryString:
|
||||
return "QueryString"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func ParseAuthTokenFromRequest(r *http.Request) (string, TokenLocation) {
|
||||
authHeader := r.Header.Get(HeaderAuth)
|
||||
|
||||
// Attempt to parse the token from the cookie
|
||||
if cookie, err := r.Cookie(SessionCookieToken); err == nil {
|
||||
return cookie.Value, TokenLocationCookie
|
||||
}
|
||||
|
||||
// Parse the token from the header
|
||||
if len(authHeader) > 6 && strings.ToUpper(authHeader[0:6]) == HeaderBearer {
|
||||
// Default session token
|
||||
return authHeader[7:], TokenLocationHeader
|
||||
}
|
||||
|
||||
if len(authHeader) > 5 && strings.ToLower(authHeader[0:5]) == HeaderToken {
|
||||
// OAuth token
|
||||
return authHeader[6:], TokenLocationHeader
|
||||
}
|
||||
|
||||
// Attempt to parse token out of the query string
|
||||
if token := r.URL.Query().Get("access_token"); token != "" {
|
||||
return token, TokenLocationQueryString
|
||||
}
|
||||
|
||||
return "", TokenLocationNotFound
|
||||
}
|
||||
51
server/boards/services/auth/request_parser_test.go
Обычный файл
51
server/boards/services/auth/request_parser_test.go
Обычный файл
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseAuthTokenFromRequest(t *testing.T) {
|
||||
cases := []struct {
|
||||
header string
|
||||
cookie string
|
||||
query string
|
||||
expectedToken string
|
||||
expectedLocation TokenLocation
|
||||
}{
|
||||
{"", "", "", "", TokenLocationNotFound},
|
||||
{"token mytoken", "", "", "mytoken", TokenLocationHeader},
|
||||
{"BEARER mytoken", "", "", "mytoken", TokenLocationHeader},
|
||||
{"", "mytoken", "", "mytoken", TokenLocationCookie},
|
||||
{"", "", "mytoken", "mytoken", TokenLocationQueryString},
|
||||
}
|
||||
|
||||
for testnum, tc := range cases {
|
||||
pathname := "/test/here"
|
||||
if tc.query != "" {
|
||||
pathname += "?access_token=" + tc.query
|
||||
}
|
||||
req := httptest.NewRequest("GET", pathname, nil)
|
||||
if tc.header != "" {
|
||||
req.Header.Add(HeaderAuth, tc.header)
|
||||
}
|
||||
if tc.cookie != "" {
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: "FOCALBOARDAUTHTOKEN",
|
||||
Value: tc.cookie,
|
||||
})
|
||||
}
|
||||
|
||||
token, location := ParseAuthTokenFromRequest(req)
|
||||
|
||||
require.Equal(t, tc.expectedToken, token, "Wrong token on test "+strconv.Itoa(testnum))
|
||||
require.Equal(t, tc.expectedLocation, location, "Wrong location on test "+strconv.Itoa(testnum))
|
||||
}
|
||||
}
|
||||
Ссылка в новой задаче
Block a user