Doug Lauder
2023-03-22 17:22:27 -04:00
коммит произвёл GitHub
родитель b61c096497
Коммит c943ed6859
13276 изменённых файлов: 1695615 добавлений и 223189 удалений

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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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)
}
}
})
}
}

Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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))
}
}