casdoor/object/check.go
Mr Forest 1bb3d2dea9
feat: refactor backend i18n (#1373)
* fix: handle the dataSourceName when DB changes

* reduce duplication of code

* feat: refactor translation error message

* feat: use json intsead of ini file

* remove useless translation

* fix translate problems

* remove useless addition

* fix pr problems

* fix pr problems

* fix split problem

* use gofumpt to fmt code

* use crowdin to execute backend translation

* fix pr problems

* refactor: change translation file structure same as frontend

* delete useless output

* update go.mod
2022-12-07 13:13:23 +08:00

360 lines
11 KiB
Go

// Copyright 2021 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"regexp"
"strings"
"time"
"unicode"
"github.com/casdoor/casdoor/cred"
"github.com/casdoor/casdoor/i18n"
"github.com/casdoor/casdoor/util"
goldap "github.com/go-ldap/ldap/v3"
)
var (
reWhiteSpace *regexp.Regexp
reFieldWhiteList *regexp.Regexp
)
const (
SigninWrongTimesLimit = 5
LastSignWrongTimeDuration = time.Minute * 15
)
func init() {
reWhiteSpace, _ = regexp.Compile(`\s`)
reFieldWhiteList, _ = regexp.Compile(`^[A-Za-z0-9]+$`)
}
func CheckUserSignup(application *Application, organization *Organization, username string, password string, displayName string, firstName string, lastName string, email string, phone string, affiliation string, lang string) string {
if organization == nil {
return i18n.Translate(lang, "check:Organization does not exist")
}
if application.IsSignupItemVisible("Username") {
if len(username) <= 1 {
return i18n.Translate(lang, "check:Username must have at least 2 characters")
}
if unicode.IsDigit(rune(username[0])) {
return i18n.Translate(lang, "check:Username cannot start with a digit")
}
if util.IsEmailValid(username) {
return i18n.Translate(lang, "check:Username cannot be an email address")
}
if reWhiteSpace.MatchString(username) {
return i18n.Translate(lang, "check:Username cannot contain white spaces")
}
msg := CheckUsername(username, lang)
if msg != "" {
return msg
}
if HasUserByField(organization.Name, "name", username) {
return i18n.Translate(lang, "check:Username already exists")
}
if HasUserByField(organization.Name, "email", email) {
return i18n.Translate(lang, "check:Email already exists")
}
if HasUserByField(organization.Name, "phone", phone) {
return i18n.Translate(lang, "check:Phone already exists")
}
}
if len(password) <= 5 {
return i18n.Translate(lang, "check:Password must have at least 6 characters")
}
if application.IsSignupItemVisible("Email") {
if email == "" {
if application.IsSignupItemRequired("Email") {
return i18n.Translate(lang, "check:Email cannot be empty")
} else {
return ""
}
}
if HasUserByField(organization.Name, "email", email) {
return i18n.Translate(lang, "check:Email already exists")
} else if !util.IsEmailValid(email) {
return i18n.Translate(lang, "check:Email is invalid")
}
}
if application.IsSignupItemVisible("Phone") {
if phone == "" {
if application.IsSignupItemRequired("Phone") {
return i18n.Translate(lang, "check:Phone cannot be empty")
} else {
return ""
}
}
if HasUserByField(organization.Name, "phone", phone) {
return i18n.Translate(lang, "check:Phone already exists")
} else if organization.PhonePrefix == "86" && !util.IsPhoneCnValid(phone) {
return i18n.Translate(lang, "check:Phone number is invalid")
}
}
if application.IsSignupItemVisible("Display name") {
if application.GetSignupItemRule("Display name") == "First, last" && (firstName != "" || lastName != "") {
if firstName == "" {
return i18n.Translate(lang, "check:FirstName cannot be blank")
} else if lastName == "" {
return i18n.Translate(lang, "check:LastName cannot be blank")
}
} else {
if displayName == "" {
return i18n.Translate(lang, "check:DisplayName cannot be blank")
} else if application.GetSignupItemRule("Display name") == "Real name" {
if !isValidRealName(displayName) {
return i18n.Translate(lang, "check:DisplayName is not valid real name")
}
}
}
}
if application.IsSignupItemVisible("Affiliation") {
if affiliation == "" {
return i18n.Translate(lang, "check:Affiliation cannot be blank")
}
}
return ""
}
func checkSigninErrorTimes(user *User, lang string) string {
if user.SigninWrongTimes >= SigninWrongTimesLimit {
lastSignWrongTime, _ := time.Parse(time.RFC3339, user.LastSigninWrongTime)
passedTime := time.Now().UTC().Sub(lastSignWrongTime)
seconds := int(LastSignWrongTimeDuration.Seconds() - passedTime.Seconds())
// deny the login if the error times is greater than the limit and the last login time is less than the duration
if seconds > 0 {
return fmt.Sprintf(i18n.Translate(lang, "check:You have entered the wrong password too many times, please wait for %d minutes %d seconds and try again"), seconds/60, seconds%60)
}
// reset the error times
user.SigninWrongTimes = 0
UpdateUser(user.GetId(), user, []string{"signin_wrong_times"}, user.IsGlobalAdmin)
}
return ""
}
func CheckPassword(user *User, password string, lang string) string {
// check the login error times
if msg := checkSigninErrorTimes(user, lang); msg != "" {
return msg
}
organization := GetOrganizationByUser(user)
if organization == nil {
return i18n.Translate(lang, "check:Organization does not exist")
}
credManager := cred.GetCredManager(organization.PasswordType)
if credManager != nil {
if organization.MasterPassword != "" {
if credManager.IsPasswordCorrect(password, organization.MasterPassword, "", organization.PasswordSalt) {
resetUserSigninErrorTimes(user)
return ""
}
}
if credManager.IsPasswordCorrect(password, user.Password, user.PasswordSalt, organization.PasswordSalt) {
resetUserSigninErrorTimes(user)
return ""
}
return recordSigninErrorInfo(user, lang)
} else {
return fmt.Sprintf(i18n.Translate(lang, "check:unsupported password type: %s"), organization.PasswordType)
}
}
func checkLdapUserPassword(user *User, password string, lang string) (*User, string) {
ldaps := GetLdaps(user.Owner)
ldapLoginSuccess := false
for _, ldapServer := range ldaps {
conn, err := GetLdapConn(ldapServer.Host, ldapServer.Port, ldapServer.Admin, ldapServer.Passwd)
if err != nil {
continue
}
SearchFilter := fmt.Sprintf("(&(objectClass=posixAccount)(uid=%s))", user.Name)
searchReq := goldap.NewSearchRequest(ldapServer.BaseDn,
goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 0, 0, false,
SearchFilter, []string{}, nil)
searchResult, err := conn.Conn.Search(searchReq)
if err != nil {
return nil, err.Error()
}
if len(searchResult.Entries) == 0 {
continue
} else if len(searchResult.Entries) > 1 {
return nil, i18n.Translate(lang, "check:Multiple accounts with same uid, please check your ldap server")
}
dn := searchResult.Entries[0].DN
if err := conn.Conn.Bind(dn, password); err == nil {
ldapLoginSuccess = true
break
}
}
if !ldapLoginSuccess {
return nil, i18n.Translate(lang, "check:Ldap user name or password incorrect")
}
return user, ""
}
func CheckUserPassword(organization string, username string, password string, lang string) (*User, string) {
user := GetUserByFields(organization, username)
if user == nil || user.IsDeleted == true {
return nil, i18n.Translate(lang, "check:The user doesn't exist")
}
if user.IsForbidden {
return nil, i18n.Translate(lang, "check:The user is forbidden to sign in, please contact the administrator")
}
if user.Ldap != "" {
// ONLY for ldap users
return checkLdapUserPassword(user, password, lang)
} else {
msg := CheckPassword(user, password, lang)
if msg != "" {
return nil, msg
}
}
return user, ""
}
func filterField(field string) bool {
return reFieldWhiteList.MatchString(field)
}
func CheckUserPermission(requestUserId, userId, userOwner string, strict bool, lang string) (bool, error) {
if requestUserId == "" {
return false, fmt.Errorf(i18n.Translate(lang, "check:Please login first"))
}
if userId != "" {
targetUser := GetUser(userId)
if targetUser == nil {
return false, fmt.Errorf(i18n.Translate(lang, "check:The user: %s doesn't exist"), userId)
}
userOwner = targetUser.Owner
}
hasPermission := false
if strings.HasPrefix(requestUserId, "app/") {
hasPermission = true
} else {
requestUser := GetUser(requestUserId)
if requestUser == nil {
return false, fmt.Errorf(i18n.Translate(lang, "check:Session outdated, please login again"))
}
if requestUser.IsGlobalAdmin {
hasPermission = true
} else if requestUserId == userId {
hasPermission = true
} else if userOwner == requestUser.Owner {
if strict {
hasPermission = requestUser.IsAdmin
} else {
hasPermission = true
}
}
}
return hasPermission, fmt.Errorf(i18n.Translate(lang, "check:You don't have the permission to do this"))
}
func CheckAccessPermission(userId string, application *Application) (bool, error) {
permissions := GetPermissions(application.Organization)
allowed := true
var err error
for _, permission := range permissions {
if !permission.IsEnabled || len(permission.Users) == 0 {
continue
}
isHit := false
for _, resource := range permission.Resources {
if application.Name == resource {
isHit = true
break
}
}
if isHit {
containsAsterisk := ContainsAsterisk(userId, permission.Users)
if containsAsterisk {
return true, err
}
enforcer := getEnforcer(permission)
allowed, err = enforcer.Enforce(userId, application.Name, "read")
break
}
}
return allowed, err
}
func CheckUsername(username string, lang string) string {
if username == "" {
return i18n.Translate(lang, "check:Empty username.")
} else if len(username) > 39 {
return i18n.Translate(lang, "check:Username is too long (maximum is 39 characters).")
}
exclude, _ := regexp.Compile("^[\u0021-\u007E]+$")
if !exclude.MatchString(username) {
return ""
}
// https://stackoverflow.com/questions/58726546/github-username-convention-using-regex
re, _ := regexp.Compile("^[a-zA-Z0-9]+((?:-[a-zA-Z0-9]+)|(?:_[a-zA-Z0-9]+))*$")
if !re.MatchString(username) {
return i18n.Translate(lang, "check:The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.")
}
return ""
}
func CheckToEnableCaptcha(application *Application) bool {
if len(application.Providers) == 0 {
return false
}
for _, providerItem := range application.Providers {
if providerItem.Provider == nil {
continue
}
if providerItem.Provider.Category == "Captcha" && providerItem.Provider.Type == "Default" {
return providerItem.Rule == "Always"
}
}
return false
}