Compare commits

...

16 Commits

Author SHA1 Message Date
fe40910e3b feat: support stateless MFA setup (#3382) 2024-11-29 19:50:10 +08:00
2d1736f13a feat: Add more data to the dashboard page chart #3365 (#3375)
* test

* feat: #3365 add more dada to the dashboard page chart

* feat: #3365 Add more data to the dashboard page chart
2024-11-26 09:16:35 +08:00
12b4d1c7cd feat: change LDAP attribute from cn to title for correct username mapping (#3378) 2024-11-26 09:13:05 +08:00
a45d2b87c1 feat: Add translations for Persian (#3372) 2024-11-23 16:24:07 +08:00
8484465d09 feat: fix SAML failed to redirect issue when login api returns RequiredMfa (#3364) 2024-11-21 20:31:56 +08:00
dff65eee20 feat: Force users to change their passwords after 3/6/12 months (#3352)
* feat: Force users to change their passwords after 3/6/12 months

* feat: Check if the password has expired by using the last_change_password_time field added to the user table

* feat: Use the created_time field of the user table to aid password expiration checking

* feat: Rename variable
2024-11-19 21:06:52 +08:00
596016456c feat: update CI's upload-artifact and download-artifact actions to v4 (#3361)
v3 of `actions/upload-artifact` and `actions/download-artifact` will be
fully deprecated by 5 December 2024. Jobs that are scheduled to run
during the brownout periods will also fail. See [1][2].

[1]: https://github.blog/changelog/2024-04-16-deprecation-notice-v3-of-the-artifact-actions/
[2]: https://github.blog/changelog/2024-11-05-notice-of-breaking-changes-for-github-actions/

Signed-off-by: Eng Zer Jun <engzerjun@gmail.com>
2024-11-19 00:07:59 +08:00
673261c258 feat: fix placeholder bug in signin page (#3359) 2024-11-17 00:14:26 +08:00
3c5985a3c0 fix: fix several bugs in samlRequest (#3358) 2024-11-17 00:14:04 +08:00
4f3d62520a feat: fix the dashboard page shows zero data in mobile phone (#3356) 2024-11-16 22:02:49 +08:00
96f8b3d937 feat: fix SAML metadata URL and XML generation issue when enablePostBinding is enabled (#3354) 2024-11-16 15:35:30 +08:00
7ab5a5ade1 feat: add processArgsToTempFiles() to RunCasbinCommand() 2024-11-15 20:25:48 +08:00
5cbd0a96ca Use json format for argString in RunCasbinCommand() 2024-11-15 18:27:25 +08:00
7ccd8c4d4f feat: add RunCasbinCommand() API 2024-11-15 17:44:57 +08:00
b0fa3fc484 feat: add Casbin CLI API to Casdoor (#3351) 2024-11-15 16:10:22 +08:00
af01c4226a feat: add Organization.PasswordExpireDays field 2024-11-15 11:33:28 +08:00
29 changed files with 1730 additions and 1394 deletions

View File

@ -114,12 +114,12 @@ jobs:
wait-on-timeout: 210
working-directory: ./web
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
if: failure()
with:
name: cypress-screenshots
path: ./web/cypress/screenshots
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
if: always()
with:
name: cypress-videos

View File

@ -98,6 +98,7 @@ p, *, *, GET, /api/get-organization-names, *, *
p, *, *, GET, /api/get-all-objects, *, *
p, *, *, GET, /api/get-all-actions, *, *
p, *, *, GET, /api/get-all-roles, *, *
p, *, *, GET, /api/run-casbin-command, *, *
p, *, *, GET, /api/get-invitation-info, *, *
p, *, *, GET, /api/faceid-signin-begin, *, *
`

View File

@ -0,0 +1,114 @@
// Copyright 2024 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 controllers
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
)
func processArgsToTempFiles(args []string) ([]string, []string, error) {
tempFiles := []string{}
newArgs := []string{}
for i := 0; i < len(args); i++ {
if (args[i] == "-m" || args[i] == "-p") && i+1 < len(args) {
pattern := fmt.Sprintf("casbin_temp_%s_*.conf", args[i])
tempFile, err := os.CreateTemp("", pattern)
if err != nil {
return nil, nil, fmt.Errorf("failed to create temp file: %v", err)
}
_, err = tempFile.WriteString(args[i+1])
if err != nil {
tempFile.Close()
return nil, nil, fmt.Errorf("failed to write to temp file: %v", err)
}
tempFile.Close()
tempFiles = append(tempFiles, tempFile.Name())
newArgs = append(newArgs, args[i], tempFile.Name())
i++
} else {
newArgs = append(newArgs, args[i])
}
}
return tempFiles, newArgs, nil
}
// RunCasbinCommand
// @Title RunCasbinCommand
// @Tag Enforcer API
// @Description Call Casbin CLI commands
// @Success 200 {object} controllers.Response The Response object
// @router /run-casbin-command [get]
func (c *ApiController) RunCasbinCommand() {
language := c.Input().Get("language")
argString := c.Input().Get("args")
if language == "" {
language = "go"
}
// use "casbin-go-cli" by default, can be also "casbin-java-cli", "casbin-node-cli", etc.
// the pre-built binary of "casbin-go-cli" can be found at: https://github.com/casbin/casbin-go-cli/releases
binaryName := fmt.Sprintf("casbin-%s-cli", language)
_, err := exec.LookPath(binaryName)
if err != nil {
c.ResponseError(fmt.Sprintf("executable file: %s not found in PATH", binaryName))
return
}
// RBAC model & policy example:
// https://door.casdoor.com/api/run-casbin-command?language=go&args=["enforce", "-m", "[request_definition]\nr = sub, obj, act\n\n[policy_definition]\np = sub, obj, act\n\n[role_definition]\ng = _, _\n\n[policy_effect]\ne = some(where (p.eft == allow))\n\n[matchers]\nm = g(r.sub, p.sub) %26%26 r.obj == p.obj %26%26 r.act == p.act", "-p", "p, alice, data1, read\np, bob, data2, write\np, data2_admin, data2, read\np, data2_admin, data2, write\ng, alice, data2_admin", "alice", "data1", "read"]
// Casbin CLI usage:
// https://github.com/jcasbin/casbin-java-cli?tab=readme-ov-file#get-started
var args []string
err = json.Unmarshal([]byte(argString), &args)
if err != nil {
c.ResponseError(err.Error())
return
}
tempFiles, processedArgs, err := processArgsToTempFiles(args)
defer func() {
for _, file := range tempFiles {
os.Remove(file)
}
}()
if err != nil {
c.ResponseError(err.Error())
return
}
command := exec.Command(binaryName, processedArgs...)
outputBytes, err := command.CombinedOutput()
if err != nil {
errorString := err.Error()
if outputBytes != nil {
output := string(outputBytes)
errorString = fmt.Sprintf("%s, error: %s", output, err.Error())
}
c.ResponseError(errorString)
return
}
output := string(outputBytes)
output = strings.TrimSuffix(output, "\n")
c.ResponseOk(output)
}

View File

@ -22,13 +22,6 @@ import (
"github.com/google/uuid"
)
const (
MfaRecoveryCodesSession = "mfa_recovery_codes"
MfaCountryCodeSession = "mfa_country_code"
MfaDestSession = "mfa_dest"
MfaTotpSecretSession = "mfa_totp_secret"
)
// MfaSetupInitiate
// @Title MfaSetupInitiate
// @Tag MFA API
@ -72,11 +65,6 @@ func (c *ApiController) MfaSetupInitiate() {
}
recoveryCode := uuid.NewString()
c.SetSession(MfaRecoveryCodesSession, recoveryCode)
if mfaType == object.TotpType {
c.SetSession(MfaTotpSecretSession, mfaProps.Secret)
}
mfaProps.RecoveryCodes = []string{recoveryCode}
resp := mfaProps
@ -94,6 +82,9 @@ func (c *ApiController) MfaSetupInitiate() {
func (c *ApiController) MfaSetupVerify() {
mfaType := c.Ctx.Request.Form.Get("mfaType")
passcode := c.Ctx.Request.Form.Get("passcode")
secret := c.Ctx.Request.Form.Get("secret")
dest := c.Ctx.Request.Form.Get("dest")
countryCode := c.Ctx.Request.Form.Get("secret")
if mfaType == "" || passcode == "" {
c.ResponseError("missing auth type or passcode")
@ -104,32 +95,28 @@ func (c *ApiController) MfaSetupVerify() {
MfaType: mfaType,
}
if mfaType == object.TotpType {
secret := c.GetSession(MfaTotpSecretSession)
if secret == nil {
if secret == "" {
c.ResponseError("totp secret is missing")
return
}
config.Secret = secret.(string)
config.Secret = secret
} else if mfaType == object.SmsType {
dest := c.GetSession(MfaDestSession)
if dest == nil {
if dest == "" {
c.ResponseError("destination is missing")
return
}
config.Secret = dest.(string)
countryCode := c.GetSession(MfaCountryCodeSession)
if countryCode == nil {
config.Secret = dest
if countryCode == "" {
c.ResponseError("country code is missing")
return
}
config.CountryCode = countryCode.(string)
config.CountryCode = countryCode
} else if mfaType == object.EmailType {
dest := c.GetSession(MfaDestSession)
if dest == nil {
if dest == "" {
c.ResponseError("destination is missing")
return
}
config.Secret = dest.(string)
config.Secret = dest
}
mfaUtil := object.GetMfaUtil(mfaType, config)
@ -159,6 +146,10 @@ func (c *ApiController) MfaSetupEnable() {
owner := c.Ctx.Request.Form.Get("owner")
name := c.Ctx.Request.Form.Get("name")
mfaType := c.Ctx.Request.Form.Get("mfaType")
secret := c.Ctx.Request.Form.Get("secret")
dest := c.Ctx.Request.Form.Get("dest")
countryCode := c.Ctx.Request.Form.Get("secret")
recoveryCodes := c.Ctx.Request.Form.Get("recoveryCodes")
user, err := object.GetUser(util.GetId(owner, name))
if err != nil {
@ -176,43 +167,39 @@ func (c *ApiController) MfaSetupEnable() {
}
if mfaType == object.TotpType {
secret := c.GetSession(MfaTotpSecretSession)
if secret == nil {
if secret == "" {
c.ResponseError("totp secret is missing")
return
}
config.Secret = secret.(string)
config.Secret = secret
} else if mfaType == object.EmailType {
if user.Email == "" {
dest := c.GetSession(MfaDestSession)
if dest == nil {
if dest == "" {
c.ResponseError("destination is missing")
return
}
user.Email = dest.(string)
user.Email = dest
}
} else if mfaType == object.SmsType {
if user.Phone == "" {
dest := c.GetSession(MfaDestSession)
if dest == nil {
if dest == "" {
c.ResponseError("destination is missing")
return
}
user.Phone = dest.(string)
countryCode := c.GetSession(MfaCountryCodeSession)
if countryCode == nil {
user.Phone = dest
if countryCode == "" {
c.ResponseError("country code is missing")
return
}
user.CountryCode = countryCode.(string)
user.CountryCode = countryCode
}
}
recoveryCodes := c.GetSession(MfaRecoveryCodesSession)
if recoveryCodes == nil {
if recoveryCodes == "" {
c.ResponseError("recovery codes is missing")
return
}
config.RecoveryCodes = []string{recoveryCodes.(string)}
config.RecoveryCodes = []string{recoveryCodes}
mfaUtil := object.GetMfaUtil(mfaType, config)
if mfaUtil == nil {
@ -226,14 +213,6 @@ func (c *ApiController) MfaSetupEnable() {
return
}
c.DelSession(MfaRecoveryCodesSession)
if mfaType == object.TotpType {
c.DelSession(MfaTotpSecretSession)
} else {
c.DelSession(MfaCountryCodeSession)
c.DelSession(MfaDestSession)
}
c.ResponseOk(http.StatusText(http.StatusOK))
}

View File

@ -561,8 +561,9 @@ func (c *ApiController) SetPassword() {
targetUser.Password = newPassword
targetUser.UpdateUserPassword(organization)
targetUser.NeedUpdatePassword = false
targetUser.LastChangePasswordTime = util.GetCurrentTime()
_, err = object.UpdateUser(userId, targetUser, []string{"password", "need_update_password", "password_type"}, false)
_, err = object.UpdateUser(userId, targetUser, []string{"password", "need_update_password", "password_type", "last_change_password_time"}, false)
if err != nil {
c.ResponseError(err.Error())
return

View File

@ -246,8 +246,6 @@ func (c *ApiController) SendVerificationCode() {
if user != nil && util.GetMaskedEmail(mfaProps.Secret) == vform.Dest {
vform.Dest = mfaProps.Secret
}
} else if vform.Method == MfaSetupVerification {
c.SetSession(MfaDestSession, vform.Dest)
}
provider, err = application.GetEmailProvider(vform.Method)
@ -282,11 +280,6 @@ func (c *ApiController) SendVerificationCode() {
vform.CountryCode = user.GetCountryCode(vform.CountryCode)
}
}
if vform.Method == MfaSetupVerification {
c.SetSession(MfaCountryCodeSession, vform.CountryCode)
c.SetSession(MfaDestSession, vform.Dest)
}
} else if vform.Method == MfaAuthVerification {
mfaProps := user.GetPreferredMfaProps(false)
if user != nil && util.GetMaskedPhone(mfaProps.Secret) == vform.Dest {

View File

@ -1,167 +1,167 @@
{
"account": {
"Failed to add user": "Failed to add user",
"Get init score failed, error: %w": "Get init score failed, error: %w",
"Please sign out first": "Please sign out first",
"The application does not allow to sign up new account": "The application does not allow to sign up new account"
"Failed to add user": "عدم موفقیت در افزودن کاربر",
"Get init score failed, error: %w": "عدم موفقیت در دریافت امتیاز اولیه، خطا: %w",
"Please sign out first": "لطفاً ابتدا خارج شوید",
"The application does not allow to sign up new account": "برنامه اجازه ثبت‌نام حساب جدید را نمی‌دهد"
},
"auth": {
"Challenge method should be S256": "Challenge method should be S256",
"Failed to create user, user information is invalid: %s": "Failed to create user, user information is invalid: %s",
"Failed to login in: %s": "Failed to login in: %s",
"Invalid token": "Invalid token",
"State expected: %s, but got: %s": "State expected: %s, but got: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)",
"The application: %s does not exist": "The application: %s does not exist",
"The login method: login with LDAP is not enabled for the application": "The login method: login with LDAP is not enabled for the application",
"The login method: login with SMS is not enabled for the application": "The login method: login with SMS is not enabled for the application",
"The login method: login with email is not enabled for the application": "The login method: login with email is not enabled for the application",
"The login method: login with face is not enabled for the application": "The login method: login with face is not enabled for the application",
"The login method: login with password is not enabled for the application": "The login method: login with password is not enabled for the application",
"The organization: %s does not exist": "The organization: %s does not exist",
"The provider: %s is not enabled for the application": "The provider: %s is not enabled for the application",
"Unauthorized operation": "Unauthorized operation",
"Unknown authentication type (not password or provider), form = %s": "Unknown authentication type (not password or provider), form = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"
"Challenge method should be S256": "روش چالش باید S256 باشد",
"Failed to create user, user information is invalid: %s": "عدم موفقیت در ایجاد کاربر، اطلاعات کاربر نامعتبر است: %s",
"Failed to login in: %s": "عدم موفقیت در ورود: %s",
"Invalid token": "توکن نامعتبر",
"State expected: %s, but got: %s": "وضعیت مورد انتظار: %s، اما دریافت شد: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "حساب برای ارائه‌دهنده: %s و نام کاربری: %s (%s) وجود ندارد و مجاز به ثبت‌نام به‌عنوان حساب جدید از طریق %%s نیست، لطفاً از روش دیگری برای ثبت‌نام استفاده کنید",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "حساب برای ارائه‌دهنده: %s و نام کاربری: %s (%s) وجود ندارد و مجاز به ثبت‌نام به‌عنوان حساب جدید نیست، لطفاً با پشتیبانی IT خود تماس بگیرید",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "حساب برای ارائه‌دهنده: %s و نام کاربری: %s (%s) در حال حاضر به حساب دیگری مرتبط است: %s (%s)",
"The application: %s does not exist": "برنامه: %s وجود ندارد",
"The login method: login with LDAP is not enabled for the application": "روش ورود: ورود با LDAP برای برنامه فعال نیست",
"The login method: login with SMS is not enabled for the application": "روش ورود: ورود با پیامک برای برنامه فعال نیست",
"The login method: login with email is not enabled for the application": "روش ورود: ورود با ایمیل برای برنامه فعال نیست",
"The login method: login with face is not enabled for the application": "روش ورود: ورود با چهره برای برنامه فعال نیست",
"The login method: login with password is not enabled for the application": "روش ورود: ورود با رمز عبور برای برنامه فعال نیست",
"The organization: %s does not exist": "سازمان: %s وجود ندارد",
"The provider: %s is not enabled for the application": "ارائه‌دهنده: %s برای برنامه فعال نیست",
"Unauthorized operation": "عملیات غیرمجاز",
"Unknown authentication type (not password or provider), form = %s": "نوع احراز هویت ناشناخته (نه رمز عبور و نه ارائه‌دهنده)، فرم = %s",
"User's tag: %s is not listed in the application's tags": "برچسب کاربر: %s در برچسب‌های برنامه فهرست نشده است",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "کاربر پرداختی %s اشتراک فعال یا در انتظار ندارد و برنامه: %s قیمت‌گذاری پیش‌فرض ندارد"
},
"cas": {
"Service %s and %s do not match": "Service %s and %s do not match"
"Service %s and %s do not match": "سرویس %s و %s مطابقت ندارند"
},
"check": {
"Affiliation cannot be blank": "Affiliation cannot be blank",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists",
"Email cannot be empty": "Email cannot be empty",
"Email is invalid": "Email is invalid",
"Empty username.": "Empty username.",
"Face data does not exist, cannot log in": "Face data does not exist, cannot log in",
"Face data mismatch": "Face data mismatch",
"FirstName cannot be blank": "FirstName cannot be blank",
"Invitation code cannot be blank": "Invitation code cannot be blank",
"Invitation code exhausted": "Invitation code exhausted",
"Invitation code is invalid": "Invitation code is invalid",
"Invitation code suspended": "Invitation code suspended",
"LDAP user name or password incorrect": "LDAP user name or password incorrect",
"LastName cannot be blank": "LastName cannot be blank",
"Multiple accounts with same uid, please check your ldap server": "Multiple accounts with same uid, please check your ldap server",
"Organization does not exist": "Organization does not exist",
"Phone already exists": "Phone already exists",
"Phone cannot be empty": "Phone cannot be empty",
"Phone number is invalid": "Phone number is invalid",
"Please register using the email corresponding to the invitation code": "Please register using the email corresponding to the invitation code",
"Please register using the phone corresponding to the invitation code": "Please register using the phone corresponding to the invitation code",
"Please register using the username corresponding to the invitation code": "Please register using the username corresponding to the invitation code",
"Session outdated, please login again": "Session outdated, please login again",
"The invitation code has already been used": "The invitation code has already been used",
"The user is forbidden to sign in, please contact the administrator": "The user is forbidden to sign in, please contact the administrator",
"The user: %s doesn't exist in LDAP server": "The user: %s doesn't exist in LDAP server",
"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.": "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.",
"The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex": "The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex",
"The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"": "The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"",
"Username already exists": "Username already exists",
"Username cannot be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "Username cannot start with a digit",
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
"Username must have at least 2 characters": "Username must have at least 2 characters",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
"Affiliation cannot be blank": "وابستگی نمی‌تواند خالی باشد",
"Default code does not match the code's matching rules": "کد پیش‌فرض با قوانین تطبیق کد مطابقت ندارد",
"DisplayName cannot be blank": "نام نمایشی نمی‌تواند خالی باشد",
"DisplayName is not valid real name": "نام نمایشی یک نام واقعی معتبر نیست",
"Email already exists": "ایمیل قبلاً وجود دارد",
"Email cannot be empty": "ایمیل نمی‌تواند خالی باشد",
"Email is invalid": "ایمیل نامعتبر است",
"Empty username.": "نام کاربری خالی است.",
"Face data does not exist, cannot log in": "داده‌های چهره وجود ندارد، نمی‌توان وارد شد",
"Face data mismatch": "عدم تطابق داده‌های چهره",
"FirstName cannot be blank": "نام نمی‌تواند خالی باشد",
"Invitation code cannot be blank": "کد دعوت نمی‌تواند خالی باشد",
"Invitation code exhausted": "کد دعوت استفاده شده است",
"Invitation code is invalid": "کد دعوت نامعتبر است",
"Invitation code suspended": "کد دعوت معلق است",
"LDAP user name or password incorrect": "نام کاربری یا رمز عبور LDAP نادرست است",
"LastName cannot be blank": "نام خانوادگی نمی‌تواند خالی باشد",
"Multiple accounts with same uid, please check your ldap server": "چندین حساب با uid یکسان، لطفاً سرور LDAP خود را بررسی کنید",
"Organization does not exist": "سازمان وجود ندارد",
"Phone already exists": "تلفن قبلاً وجود دارد",
"Phone cannot be empty": "تلفن نمی‌تواند خالی باشد",
"Phone number is invalid": "شماره تلفن نامعتبر است",
"Please register using the email corresponding to the invitation code": "لطفاً با استفاده از ایمیل مربوط به کد دعوت ثبت‌نام کنید",
"Please register using the phone corresponding to the invitation code": "لطفاً با استفاده از تلفن مربوط به کد دعوت ثبت‌نام کنید",
"Please register using the username corresponding to the invitation code": "لطفاً با استفاده از نام کاربری مربوط به کد دعوت ثبت‌نام کنید",
"Session outdated, please login again": "جلسه منقضی شده است، لطفاً دوباره وارد شوید",
"The invitation code has already been used": "کد دعوت قبلاً استفاده شده است",
"The user is forbidden to sign in, please contact the administrator": "ورود کاربر ممنوع است، لطفاً با مدیر تماس بگیرید",
"The user: %s doesn't exist in LDAP server": "کاربر: %s در سرور LDAP وجود ندارد",
"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.": "نام کاربری فقط می‌تواند حاوی کاراکترهای الفبایی عددی، زیرخط یا خط تیره باشد، نمی‌تواند خط تیره یا زیرخط متوالی داشته باشد، و نمی‌تواند با خط تیره یا زیرخط شروع یا پایان یابد.",
"The value \"%s\" for account field \"%s\" doesn't match the account item regex": "مقدار \"%s\" برای فیلد حساب \"%s\" با عبارت منظم مورد حساب مطابقت ندارد",
"The value \"%s\" for signup field \"%s\" doesn't match the signup item regex of the application \"%s\"": "مقدار \"%s\" برای فیلد ثبت‌نام \"%s\" با عبارت منظم مورد ثبت‌نام برنامه \"%s\" مطابقت ندارد",
"Username already exists": "نام کاربری قبلاً وجود دارد",
"Username cannot be an email address": "نام کاربری نمی‌تواند یک آدرس ایمیل باشد",
"Username cannot contain white spaces": "نام کاربری نمی‌تواند حاوی فاصله باشد",
"Username cannot start with a digit": "نام کاربری نمی‌تواند با یک رقم شروع شود",
"Username is too long (maximum is 39 characters).": "نام کاربری بیش از حد طولانی است (حداکثر ۳۹ کاراکتر).",
"Username must have at least 2 characters": "نام کاربری باید حداقل ۲ کاراکتر داشته باشد",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "شما رمز عبور یا کد اشتباه را بیش از حد وارد کرده‌اید، لطفاً %d دقیقه صبر کنید و دوباره تلاش کنید",
"Your region is not allow to signup by phone": "منطقه شما اجازه ثبت‌نام با تلفن را ندارد",
"password or code is incorrect": "رمز عبور یا کد نادرست است",
"password or code is incorrect, you have %d remaining chances": "رمز عبور یا کد نادرست است، شما %d فرصت باقی‌مانده دارید",
"unsupported password type: %s": "نوع رمز عبور پشتیبانی نشده: %s"
},
"general": {
"Missing parameter": "Missing parameter",
"Please login first": "Please login first",
"The organization: %s should have one application at least": "The organization: %s should have one application at least",
"The user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode",
"this operation requires administrator to perform": "this operation requires administrator to perform"
"Missing parameter": "پارامتر گمشده",
"Please login first": "لطفاً ابتدا وارد شوید",
"The organization: %s should have one application at least": "سازمان: %s باید حداقل یک برنامه داشته باشد",
"The user: %s doesn't exist": "کاربر: %s وجود ندارد",
"don't support captchaProvider: ": "از captchaProvider پشتیبانی نمی‌شود: ",
"this operation is not allowed in demo mode": "این عملیات در حالت دمو مجاز نیست",
"this operation requires administrator to perform": "این عملیات نیاز به مدیر برای انجام دارد"
},
"ldap": {
"Ldap server exist": "Ldap server exist"
"Ldap server exist": "سرور LDAP وجود دارد"
},
"link": {
"Please link first": "Please link first",
"This application has no providers": "This application has no providers",
"This application has no providers of type": "This application has no providers of type",
"This provider can't be unlinked": "This provider can't be unlinked",
"You are not the global admin, you can't unlink other users": "You are not the global admin, you can't unlink other users",
"You can't unlink yourself, you are not a member of any application": "You can't unlink yourself, you are not a member of any application"
"Please link first": "لطفاً ابتدا پیوند دهید",
"This application has no providers": "این برنامه ارائه‌دهنده‌ای ندارد",
"This application has no providers of type": "این برنامه ارائه‌دهنده‌ای از نوع ندارد",
"This provider can't be unlinked": "این ارائه‌دهنده نمی‌تواند لغو پیوند شود",
"You are not the global admin, you can't unlink other users": "شما مدیر جهانی نیستید، نمی‌توانید کاربران دیگر را لغو پیوند کنید",
"You can't unlink yourself, you are not a member of any application": "شما نمی‌توانید خودتان را لغو پیوند کنید، شما عضو هیچ برنامه‌ای نیستید"
},
"organization": {
"Only admin can modify the %s.": "Only admin can modify the %s.",
"The %s is immutable.": "The %s is immutable.",
"Unknown modify rule %s.": "Unknown modify rule %s."
"Only admin can modify the %s.": "فقط مدیر می‌تواند %s را تغییر دهد.",
"The %s is immutable.": "%s غیرقابل تغییر است.",
"Unknown modify rule %s.": "قانون تغییر ناشناخته %s."
},
"permission": {
"The permission: \\\"%s\\\" doesn't exist": "The permission: \\\"%s\\\" doesn't exist"
"The permission: \"%s\" doesn't exist": "مجوز: \"%s\" وجود ندارد"
},
"provider": {
"Invalid application id": "Invalid application id",
"the provider: %s does not exist": "the provider: %s does not exist"
"Invalid application id": "شناسه برنامه نامعتبر",
"the provider: %s does not exist": "ارائه‌دهنده: %s وجود ندارد"
},
"resource": {
"User is nil for tag: avatar": "User is nil for tag: avatar",
"Username or fullFilePath is empty: username = %s, fullFilePath = %s": "Username or fullFilePath is empty: username = %s, fullFilePath = %s"
"User is nil for tag: avatar": "کاربر برای برچسب: آواتار تهی است",
"Username or fullFilePath is empty: username = %s, fullFilePath = %s": "نام کاربری یا مسیر کامل فایل خالی است: نام کاربری = %s، مسیر کامل فایل = %s"
},
"saml": {
"Application %s not found": "Application %s not found"
"Application %s not found": "برنامه %s یافت نشد"
},
"saml_sp": {
"provider %s's category is not SAML": "provider %s's category is not SAML"
"provider %s's category is not SAML": "دسته‌بندی ارائه‌دهنده %s SAML نیست"
},
"service": {
"Empty parameters for emailForm: %v": "Empty parameters for emailForm: %v",
"Invalid Email receivers: %s": "Invalid Email receivers: %s",
"Invalid phone receivers: %s": "Invalid phone receivers: %s"
"Empty parameters for emailForm: %v": "پارامترهای خالی برای emailForm: %v",
"Invalid Email receivers: %s": "گیرندگان ایمیل نامعتبر: %s",
"Invalid phone receivers: %s": "گیرندگان تلفن نامعتبر: %s"
},
"storage": {
"The objectKey: %s is not allowed": "The objectKey: %s is not allowed",
"The provider type: %s is not supported": "The provider type: %s is not supported"
"The objectKey: %s is not allowed": "objectKey: %s مجاز نیست",
"The provider type: %s is not supported": "نوع ارائه‌دهنده: %s پشتیبانی نمی‌شود"
},
"token": {
"Grant_type: %s is not supported in this application": "Grant_type: %s is not supported in this application",
"Invalid application or wrong clientSecret": "Invalid application or wrong clientSecret",
"Invalid client_id": "Invalid client_id",
"Redirect URI: %s doesn't exist in the allowed Redirect URI list": "Redirect URI: %s doesn't exist in the allowed Redirect URI list",
"Token not found, invalid accessToken": "Token not found, invalid accessToken"
"Grant_type: %s is not supported in this application": "grant_type: %s در این برنامه پشتیبانی نمی‌شود",
"Invalid application or wrong clientSecret": "برنامه نامعتبر یا clientSecret نادرست",
"Invalid client_id": "client_id نامعتبر",
"Redirect URI: %s doesn't exist in the allowed Redirect URI list": "آدرس بازگشت: %s در لیست آدرس‌های بازگشت مجاز وجود ندارد",
"Token not found, invalid accessToken": "توکن یافت نشد، accessToken نامعتبر"
},
"user": {
"Display name cannot be empty": "Display name cannot be empty",
"New password cannot contain blank space.": "New password cannot contain blank space."
"Display name cannot be empty": "نام نمایشی نمی‌تواند خالی باشد",
"New password cannot contain blank space.": "رمز عبور جدید نمی‌تواند حاوی فاصله خالی باشد."
},
"user_upload": {
"Failed to import users": "Failed to import users"
"Failed to import users": "عدم موفقیت در وارد کردن کاربران"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",
"The provider: %s is not found": "The provider: %s is not found"
"No application is found for userId: %s": "هیچ برنامه‌ای برای userId: %s یافت نشد",
"No provider for category: %s is found for application: %s": "هیچ ارائه‌دهنده‌ای برای دسته‌بندی: %s برای برنامه: %s یافت نشد",
"The provider: %s is not found": "ارائه‌دهنده: %s یافت نشد"
},
"verification": {
"Invalid captcha provider.": "Invalid captcha provider.",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
"The verification code has not been sent yet!": "The verification code has not been sent yet!",
"The verification code has not been sent yet, or has already been used!": "The verification code has not been sent yet, or has already been used!",
"Turing test failed.": "Turing test failed.",
"Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.",
"Unknown type": "Unknown type",
"Wrong verification code!": "Wrong verification code!",
"You should verify your code in %d min!": "You should verify your code in %d min!",
"please add a SMS provider to the \\\"Providers\\\" list for the application: %s": "please add a SMS provider to the \\\"Providers\\\" list for the application: %s",
"please add an Email provider to the \\\"Providers\\\" list for the application: %s": "please add an Email provider to the \\\"Providers\\\" list for the application: %s",
"the user does not exist, please sign up first": "the user does not exist, please sign up first"
"Invalid captcha provider.": "ارائه‌دهنده کپچا نامعتبر.",
"Phone number is invalid in your region %s": "شماره تلفن در منطقه شما نامعتبر است %s",
"The verification code has not been sent yet!": "کد تأیید هنوز ارسال نشده است!",
"The verification code has not been sent yet, or has already been used!": "کد تأیید هنوز ارسال نشده است، یا قبلاً استفاده شده است!",
"Turing test failed.": "تست تورینگ ناموفق بود.",
"Unable to get the email modify rule.": "عدم توانایی در دریافت قانون تغییر ایمیل.",
"Unable to get the phone modify rule.": "عدم توانایی در دریافت قانون تغییر تلفن.",
"Unknown type": "نوع ناشناخته",
"Wrong verification code!": "کد تأیید اشتباه!",
"You should verify your code in %d min!": "شما باید کد خود را در %d دقیقه تأیید کنید!",
"please add a SMS provider to the \"Providers\" list for the application: %s": "لطفاً یک ارائه‌دهنده پیامک به لیست \"ارائه‌دهندگان\" برای برنامه: %s اضافه کنید",
"please add an Email provider to the \"Providers\" list for the application: %s": "لطفاً یک ارائه‌دهنده ایمیل به لیست \"ارائه‌دهندگان\" برای برنامه: %s اضافه کنید",
"the user does not exist, please sign up first": "کاربر وجود ندارد، لطفاً ابتدا ثبت‌نام کنید"
},
"webauthn": {
"Found no credentials for this user": "Found no credentials for this user",
"Please call WebAuthnSigninBegin first": "Please call WebAuthnSigninBegin first"
"Found no credentials for this user": "هیچ اعتباری برای این کاربر یافت نشد",
"Please call WebAuthnSigninBegin first": "لطفاً ابتدا WebAuthnSigninBegin را فراخوانی کنید"
}
}

View File

@ -142,7 +142,7 @@ func handleSearch(w ldap.ResponseWriter, m *ldap.Message) {
}
for _, attr := range attrs {
e.AddAttribute(message.AttributeDescription(attr), getAttribute(string(attr), user))
if string(attr) == "cn" {
if string(attr) == "title" {
e.AddAttribute(message.AttributeDescription(attr), getAttribute("title", user))
}
}

View File

@ -381,7 +381,13 @@ func CheckUserPassword(organization string, username string, password string, la
if err != nil {
return nil, err
}
err = checkPasswordExpired(user, lang)
if err != nil {
return nil, err
}
}
return user, nil
}

View File

@ -0,0 +1,53 @@
// Copyright 2024 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"
"time"
"github.com/casdoor/casdoor/i18n"
"github.com/casdoor/casdoor/util"
)
func checkPasswordExpired(user *User, lang string) error {
organization, err := GetOrganizationByUser(user)
if err != nil {
return err
}
if organization == nil {
return fmt.Errorf(i18n.Translate(lang, "check:Organization does not exist"))
}
passwordExpireDays := organization.PasswordExpireDays
if passwordExpireDays <= 0 {
return nil
}
lastChangePasswordTime := user.LastChangePasswordTime
if lastChangePasswordTime == "" {
if user.CreatedTime == "" {
return fmt.Errorf(i18n.Translate(lang, "check:Your password has expired. Please reset your password by clicking \"Forgot password\""))
}
lastChangePasswordTime = user.CreatedTime
}
lastTime := util.String2Time(lastChangePasswordTime)
expireTime := lastTime.AddDate(0, 0, passwordExpireDays)
if time.Now().After(expireTime) {
return fmt.Errorf(i18n.Translate(lang, "check:Your password has expired. Please reset your password by clicking \"Forgot password\""))
}
return nil
}

View File

@ -25,6 +25,12 @@ type Dashboard struct {
ProviderCounts []int `json:"providerCounts"`
ApplicationCounts []int `json:"applicationCounts"`
SubscriptionCounts []int `json:"subscriptionCounts"`
RoleCounts []int `json:"roleCounts"`
GroupCounts []int `json:"groupCounts"`
ResourceCounts []int `json:"resourceCounts"`
CertCounts []int `json:"certCounts"`
PermissionCounts []int `json:"permissionCounts"`
TransactionCounts []int `json:"transactionCounts"`
}
func GetDashboard(owner string) (*Dashboard, error) {
@ -38,6 +44,12 @@ func GetDashboard(owner string) (*Dashboard, error) {
ProviderCounts: make([]int, 31),
ApplicationCounts: make([]int, 31),
SubscriptionCounts: make([]int, 31),
RoleCounts: make([]int, 31),
GroupCounts: make([]int, 31),
ResourceCounts: make([]int, 31),
CertCounts: make([]int, 31),
PermissionCounts: make([]int, 31),
TransactionCounts: make([]int, 31),
}
organizations := []Organization{}
@ -45,9 +57,15 @@ func GetDashboard(owner string) (*Dashboard, error) {
providers := []Provider{}
applications := []Application{}
subscriptions := []Subscription{}
roles := []Role{}
groups := []Group{}
resources := []Resource{}
certs := []Cert{}
permissions := []Permission{}
transactions := []Transaction{}
var wg sync.WaitGroup
wg.Add(5)
wg.Add(11)
go func() {
defer wg.Done()
if err := ormer.Engine.Find(&organizations, &Organization{Owner: owner}); err != nil {
@ -86,6 +104,50 @@ func GetDashboard(owner string) (*Dashboard, error) {
panic(err)
}
}()
go func() {
defer wg.Done()
if err := ormer.Engine.Find(&roles, &Role{Owner: owner}); err != nil {
panic(err)
}
}()
go func() {
defer wg.Done()
if err := ormer.Engine.Find(&groups, &Group{Owner: owner}); err != nil {
panic(err)
}
}()
go func() {
defer wg.Done()
if err := ormer.Engine.Find(&resources, &Resource{Owner: owner}); err != nil {
panic(err)
}
}()
go func() {
defer wg.Done()
if err := ormer.Engine.Find(&certs, &Cert{Owner: owner}); err != nil {
panic(err)
}
}()
go func() {
defer wg.Done()
if err := ormer.Engine.Find(&permissions, &Permission{Owner: owner}); err != nil {
panic(err)
}
}()
go func() {
defer wg.Done()
if err := ormer.Engine.Find(&transactions, &Transaction{Owner: owner}); err != nil {
panic(err)
}
}()
wg.Wait()
nowTime := time.Now()
@ -96,6 +158,12 @@ func GetDashboard(owner string) (*Dashboard, error) {
dashboard.ProviderCounts[30-i] = countCreatedBefore(providers, cutTime)
dashboard.ApplicationCounts[30-i] = countCreatedBefore(applications, cutTime)
dashboard.SubscriptionCounts[30-i] = countCreatedBefore(subscriptions, cutTime)
dashboard.RoleCounts[30-i] = countCreatedBefore(roles, cutTime)
dashboard.GroupCounts[30-i] = countCreatedBefore(groups, cutTime)
dashboard.ResourceCounts[30-i] = countCreatedBefore(resources, cutTime)
dashboard.CertCounts[30-i] = countCreatedBefore(certs, cutTime)
dashboard.PermissionCounts[30-i] = countCreatedBefore(permissions, cutTime)
dashboard.TransactionCounts[30-i] = countCreatedBefore(transactions, cutTime)
}
return dashboard, nil
}
@ -138,6 +206,48 @@ func countCreatedBefore(objects interface{}, before time.Time) int {
count++
}
}
case []Role:
for _, r := range obj {
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", r.CreatedTime)
if createdTime.Before(before) {
count++
}
}
case []Group:
for _, g := range obj {
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", g.CreatedTime)
if createdTime.Before(before) {
count++
}
}
case []Resource:
for _, r := range obj {
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", r.CreatedTime)
if createdTime.Before(before) {
count++
}
}
case []Cert:
for _, c := range obj {
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", c.CreatedTime)
if createdTime.Before(before) {
count++
}
}
case []Permission:
for _, p := range obj {
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", p.CreatedTime)
if createdTime.Before(before) {
count++
}
}
case []Transaction:
for _, t := range obj {
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", t.CreatedTime)
if createdTime.Before(before) {
count++
}
}
}
return count
}

View File

@ -62,6 +62,7 @@ type Organization struct {
PasswordOptions []string `xorm:"varchar(100)" json:"passwordOptions"`
PasswordObfuscatorType string `xorm:"varchar(100)" json:"passwordObfuscatorType"`
PasswordObfuscatorKey string `xorm:"varchar(100)" json:"passwordObfuscatorKey"`
PasswordExpireDays int `json:"passwordExpireDays"`
CountryCodes []string `xorm:"varchar(200)" json:"countryCodes"`
DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"`
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`

View File

@ -26,6 +26,7 @@ import (
"errors"
"fmt"
"io"
"strings"
"time"
"github.com/beevik/etree"
@ -222,10 +223,13 @@ func GetSamlMeta(application *Application, host string, enablePostBinding bool)
originFrontend, originBackend := getOriginFromHost(host)
idpLocation := ""
idpBinding := ""
if enablePostBinding {
idpLocation = fmt.Sprintf("%s/api/saml/redirect/%s/%s", originBackend, application.Owner, application.Name)
idpBinding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
} else {
idpLocation = fmt.Sprintf("%s/login/saml/authorize/%s/%s", originFrontend, application.Owner, application.Name)
idpBinding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
}
d := IdpEntityDescriptor{
@ -258,7 +262,7 @@ func GetSamlMeta(application *Application, host string, enablePostBinding bool)
{Xmlns: "urn:oasis:names:tc:SAML:2.0:assertion", Name: "Name", NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic", FriendlyName: "Name"},
},
SingleSignOnService: SingleSignOnService{
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
Binding: idpBinding,
Location: idpLocation,
},
ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol",
@ -273,29 +277,38 @@ func GetSamlMeta(application *Application, host string, enablePostBinding bool)
func GetSamlResponse(application *Application, user *User, samlRequest string, host string) (string, string, string, error) {
// request type
method := "GET"
samlRequest = strings.ReplaceAll(samlRequest, " ", "+")
// base64 decode
defated, err := base64.StdEncoding.DecodeString(samlRequest)
if err != nil {
return "", "", "", fmt.Errorf("err: Failed to decode SAML request, %s", err.Error())
}
// decompress
var buffer bytes.Buffer
rdr := flate.NewReader(bytes.NewReader(defated))
var requestByte []byte
for {
_, err = io.CopyN(&buffer, rdr, 1024)
if err != nil {
if err == io.EOF {
break
if strings.Contains(string(defated), "xmlns:") {
requestByte = defated
} else {
// decompress
var buffer bytes.Buffer
rdr := flate.NewReader(bytes.NewReader(defated))
for {
_, err = io.CopyN(&buffer, rdr, 1024)
if err != nil {
if err == io.EOF {
break
}
return "", "", "", err
}
return "", "", "", err
}
requestByte = buffer.Bytes()
}
var authnRequest saml.AuthNRequest
err = xml.Unmarshal(buffer.Bytes(), &authnRequest)
err = xml.Unmarshal(requestByte, &authnRequest)
if err != nil {
return "", "", "", fmt.Errorf("err: Failed to unmarshal AuthnRequest, please check the SAML request, %s", err.Error())
}

View File

@ -200,8 +200,9 @@ type User struct {
Permissions []*Permission `json:"permissions"`
Groups []string `xorm:"groups varchar(1000)" json:"groups"`
LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"`
SigninWrongTimes int `json:"signinWrongTimes"`
LastChangePasswordTime string `xorm:"varchar(100)" json:"lastChangePasswordTime"`
LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"`
SigninWrongTimes int `json:"signinWrongTimes"`
ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"`
MfaAccounts []MfaAccount `xorm:"mfaAccounts blob" json:"mfaAccounts"`
@ -690,7 +691,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
"owner", "display_name", "avatar", "first_name", "last_name",
"location", "address", "country_code", "region", "language", "affiliation", "title", "id_card_type", "id_card", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application",
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts", "face_ids", "mfaAccounts",
"signin_wrong_times", "last_signin_wrong_time", "groups", "access_key", "access_secret", "mfa_phone_enabled", "mfa_email_enabled",
"signin_wrong_times", "last_change_password_time", "last_signin_wrong_time", "groups", "access_key", "access_secret", "mfa_phone_enabled", "mfa_email_enabled",
"github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs",
"baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "line", "amazon",
"auth0", "battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox",

View File

@ -174,6 +174,8 @@ func initAPI() {
beego.Router("/api/get-all-actions", &controllers.ApiController{}, "GET:GetAllActions")
beego.Router("/api/get-all-roles", &controllers.ApiController{}, "GET:GetAllRoles")
beego.Router("/api/run-casbin-command", &controllers.ApiController{}, "GET:RunCasbinCommand")
beego.Router("/api/get-sessions", &controllers.ApiController{}, "GET:GetSessions")
beego.Router("/api/get-session", &controllers.ApiController{}, "GET:GetSingleSession")
beego.Router("/api/update-session", &controllers.ApiController{}, "POST:UpdateSession")

View File

@ -765,7 +765,7 @@ class ApplicationEditPage extends React.Component {
/>
<br />
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
copy(`${window.location.origin}/api/saml/metadata?application=admin/${encodeURIComponent(this.state.applicationName)}&post=${this.state.application.enableSamlPostBinding}`);
copy(`${window.location.origin}/api/saml/metadata?application=admin/${encodeURIComponent(this.state.applicationName)}&enablePostBinding=${this.state.application.enableSamlPostBinding}`);
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}}
>

View File

@ -198,11 +198,11 @@ function ManagementPage(props) {
</div>
</Tooltip>
<OpenTour />
{Setting.isAdminUser(props.account) && !Setting.isMobile() && (props.uri.indexOf("/trees") === -1) &&
{Setting.isAdminUser(props.account) && (props.uri.indexOf("/trees") === -1) &&
<OrganizationSelect
initValue={Setting.getOrganization()}
withAll={true}
style={{marginRight: "20px", width: "180px", display: "flex"}}
style={{marginRight: "20px", width: "180px", display: !Setting.isMobile() ? "flex" : "none"}}
onChange={(value) => {
Setting.setOrganization(value);
}}

View File

@ -339,6 +339,16 @@ class OrganizationEditPage extends React.Component {
</Col>
</Row>)
}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("organization:Password expire days"), i18next.t("organization:Password expire days - Tooltip"))} :
</Col>
<Col span={4} >
<InputNumber value={this.state.organization.passwordExpireDays} onChange={value => {
this.updateOrganizationField("passwordExpireDays", value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Supported country codes"), i18next.t("general:Supported country codes - Tooltip"))} :

View File

@ -37,6 +37,7 @@ class OrganizationListPage extends BaseListPage {
passwordOptions: [],
passwordObfuscatorType: "Plain",
passwordObfuscatorKey: "",
passwordExpireDays: 0,
countryCodes: ["US"],
defaultAvatar: `${Setting.StaticBaseUrl}/img/casbin.svg`,
defaultApplication: "",

View File

@ -1009,6 +1009,19 @@ class UserEditPage extends React.Component {
</Col>
</Row>
);
} else if (accountItem.name === "Last change password time") {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Last change password time"), i18next.t("user:Last change password time"))} :
</Col>
<Col span={22}>
<Input value={this.state.user.lastChangePasswordTime} onChange={e => {
this.updateUserField("lastChangePasswordTime", e.target.value);
}} />
</Col>
</Row>
);
} else if (accountItem.name === "Managed accounts") {
return (
<Row style={{marginTop: "20px"}} >

View File

@ -243,7 +243,10 @@ class LoginPage extends React.Component {
}
}
getPlaceholder() {
getPlaceholder(defaultPlaceholder = null) {
if (defaultPlaceholder) {
return defaultPlaceholder;
}
switch (this.state.loginMethod) {
case "verificationCode": return i18next.t("login:Email or phone");
case "verificationCodeEmail": return i18next.t("login:Email");
@ -485,6 +488,10 @@ class LoginPage extends React.Component {
const accessToken = res.data;
Setting.goToLink(`${oAuthParams.redirectUri}#${amendatoryResponseType}=${accessToken}&state=${oAuthParams.state}&token_type=bearer`);
} else if (responseType === "saml") {
if (res.data === RequiredMfa) {
this.props.onLoginSuccess(window.location.href);
return;
}
if (res.data2.needUpdatePassword) {
sessionStorage.setItem("signinUrl", window.location.href);
Setting.goToLink(this, `/forget/${this.state.applicationName}`);
@ -679,7 +686,7 @@ class LoginPage extends React.Component {
id="input"
className="login-username-input"
prefix={<UserOutlined className="site-form-item-icon" />}
placeholder={this.getPlaceholder()}
placeholder={this.getPlaceholder(signinItem.placeholder)}
onChange={e => {
this.setState({
username: e.target.value,
@ -1093,7 +1100,7 @@ class LoginPage extends React.Component {
className="login-password-input"
prefix={<LockOutlined className="site-form-item-icon" />}
type="password"
placeholder={i18next.t("general:Password")}
placeholder={signinItem.placeholder ? signinItem.placeholder : i18next.t("general:Password")}
disabled={this.state.loginMethod === "password" ? !Setting.isPasswordEnabled(application) : !Setting.isLdapEnabled(application)}
/>
</Form.Item>

View File

@ -179,8 +179,10 @@ class MfaSetupPage extends React.Component {
mfaProps={this.state.mfaProps}
application={this.state.application}
user={this.props.account}
onSuccess={() => {
onSuccess={(res) => {
this.setState({
dest: res.dest,
countryCode: res.countryCode,
current: this.state.current + 1,
});
}}
@ -195,7 +197,7 @@ class MfaSetupPage extends React.Component {
);
case 2:
return (
<MfaEnableForm user={this.getUser()} mfaType={this.state.mfaType} recoveryCodes={this.state.mfaProps.recoveryCodes}
<MfaEnableForm user={this.getUser()} mfaType={this.state.mfaType} secret={this.state.mfaProps.secret} recoveryCodes={this.state.mfaProps.recoveryCodes} dest={this.state.dest} countryCode={this.state.countryCode}
onSuccess={() => {
Setting.showMessage("success", i18next.t("general:Enabled successfully"));
this.props.onfinish();

View File

@ -113,6 +113,9 @@ export function getCasLoginParameters(owner, name) {
export function getOAuthGetParameters(params) {
const queries = (params !== undefined) ? params : new URLSearchParams(window.location.search);
const lowercaseQueries = {};
queries.forEach((val, key) => {lowercaseQueries[key.toLowerCase()] = val;});
const clientId = getRefinedValue(queries.get("client_id"));
const responseType = getRefinedValue(queries.get("response_type"));
@ -138,9 +141,9 @@ export function getOAuthGetParameters(params) {
const nonce = getRefinedValue(queries.get("nonce"));
const challengeMethod = getRefinedValue(queries.get("code_challenge_method"));
const codeChallenge = getRefinedValue(queries.get("code_challenge"));
const samlRequest = getRefinedValue(queries.get("SAMLRequest"));
const relayState = getRefinedValue(queries.get("RelayState"));
const noRedirect = getRefinedValue(queries.get("noRedirect"));
const samlRequest = getRefinedValue(lowercaseQueries["samlRequest".toLowerCase()]);
const relayState = getRefinedValue(lowercaseQueries["RelayState".toLowerCase()]);
const noRedirect = getRefinedValue(lowercaseQueries["noRedirect".toLowerCase()]);
if (clientId === "" && samlRequest === "") {
// login

View File

@ -3,11 +3,15 @@ import i18next from "i18next";
import React, {useState} from "react";
import * as MfaBackend from "../../backend/MfaBackend";
export function MfaEnableForm({user, mfaType, recoveryCodes, onSuccess, onFail}) {
export function MfaEnableForm({user, mfaType, secret, recoveryCodes, dest, countryCode, onSuccess, onFail}) {
const [loading, setLoading] = useState(false);
const requestEnableMfa = () => {
const data = {
mfaType,
secret,
recoveryCodes,
dest,
countryCode,
...user,
};
setLoading(true);

View File

@ -26,11 +26,13 @@ export const mfaSetup = "mfaSetup";
export function MfaVerifyForm({mfaProps, application, user, onSuccess, onFail}) {
const [form] = Form.useForm();
const onFinish = ({passcode}) => {
const data = {passcode, mfaType: mfaProps.mfaType, ...user};
const onFinish = ({passcode, countryCode, dest}) => {
const data = {passcode, mfaType: mfaProps.mfaType, secret: mfaProps.secret, dest: dest, countryCode: countryCode, ...user};
MfaBackend.MfaSetupVerify(data)
.then((res) => {
if (res.status === "ok") {
res.dest = dest;
res.countryCode = countryCode;
onSuccess(res);
} else {
onFail(res);

View File

@ -1,5 +1,5 @@
import {UserOutlined} from "@ant-design/icons";
import {Button, Form, Input} from "antd";
import {Button, Form, Input, Space} from "antd";
import i18next from "i18next";
import React, {useEffect} from "react";
import {CountryCodeSelect} from "../../common/select/CountryCodeSelect";
@ -19,11 +19,13 @@ export const MfaVerifySmsForm = ({mfaProps, application, onFinish, method, user}
}
if (mfaProps.mfaType === SmsMfaType) {
setDest(user.phone);
form.setFieldValue("dest", user.phone);
return;
}
if (mfaProps.mfaType === EmailMfaType) {
setDest(user.email);
form.setFieldValue("dest", user.email);
}
}, [mfaProps.mfaType]);
@ -57,45 +59,44 @@ export const MfaVerifySmsForm = ({mfaProps, application, onFinish, method, user}
<div style={{marginBottom: 20, textAlign: "left", gap: 8}}>
{isEmail() ? i18next.t("mfa:Your email is") : i18next.t("mfa:Your phone is")} {dest}
</div> :
(<React.Fragment>
(
<p>{isEmail() ? i18next.t("mfa:Please bind your email first, the system will automatically uses the mail for multi-factor authentication") :
i18next.t("mfa:Please bind your phone first, the system automatically uses the phone for multi-factor authentication")}
</p>
<Input.Group compact style={{width: "300Px", marginBottom: "30px"}}>
{isEmail() ? null :
<Form.Item
name="countryCode"
noStyle
rules={[
{
required: false,
message: i18next.t("signup:Please select your country code!"),
},
]}
>
<CountryCodeSelect
initValue={mfaProps.countryCode}
style={{width: "30%"}}
countryCodes={application.organizationObj.countryCodes}
/>
</Form.Item>
}
<Form.Item
name="dest"
noStyle
rules={[{required: true, message: i18next.t("login:Please input your Email or Phone!")}]}
>
<Input
style={{width: isEmail() ? "100% " : "70%"}}
onChange={(e) => {setDest(e.target.value);}}
prefix={<UserOutlined />}
placeholder={isEmail() ? i18next.t("general:Email") : i18next.t("general:Phone")}
/>
</Form.Item>
</Input.Group>
</React.Fragment>
)
}
<Space.Compact style={{width: "300Px", marginBottom: "30px", display: isShowText() ? "none" : ""}}>
{isEmail() || isShowText() ? null :
<Form.Item
name="countryCode"
noStyle
rules={[
{
required: false,
message: i18next.t("signup:Please select your country code!"),
},
]}
>
<CountryCodeSelect
initValue={mfaProps.countryCode}
style={{width: "30%"}}
countryCodes={application.organizationObj.countryCodes}
/>
</Form.Item>
}
<Form.Item
name="dest"
noStyle
rules={[{required: true, message: i18next.t("login:Please input your Email or Phone!")}]}
>
<Input
style={{width: isEmail() ? "100% " : "70%"}}
onChange={(e) => {setDest(e.target.value);}}
prefix={<UserOutlined />}
placeholder={isEmail() ? i18next.t("general:Email") : i18next.t("general:Phone")}
/>
</Form.Item>
</Space.Compact>
<Form.Item
name="passcode"
rules={[{required: true, message: i18next.t("login:Please input your code!")}]}

View File

@ -32,6 +32,9 @@ export function MfaSetupVerify(values) {
formData.append("name", values.name);
formData.append("mfaType", values.mfaType);
formData.append("passcode", values.passcode);
formData.append("secret", values.secret);
formData.append("dest", values.dest);
formData.append("countryCode", values.countryCode);
return fetch(`${Setting.ServerUrl}/api/mfa/setup/verify`, {
method: "POST",
credentials: "include",
@ -44,6 +47,10 @@ export function MfaSetupEnable(values) {
formData.append("mfaType", values.mfaType);
formData.append("owner", values.owner);
formData.append("name", values.name);
formData.append("secret", values.secret);
formData.append("recoveryCodes", values.recoveryCodes);
formData.append("dest", values.dest);
formData.append("countryCode", values.countryCode);
return fetch(`${Setting.ServerUrl}/api/mfa/setup/enable`, {
method: "POST",
credentials: "include",

View File

@ -135,6 +135,12 @@ const Dashboard = (props) => {
i18next.t("general:Applications"),
i18next.t("general:Organizations"),
i18next.t("general:Subscriptions"),
i18next.t("general:Roles"),
i18next.t("general:Groups"),
i18next.t("general:Resources"),
i18next.t("general:Certs"),
i18next.t("general:Permissions"),
i18next.t("general:Transactions"),
], top: "10%"},
grid: {left: "3%", right: "4%", bottom: "0", top: "25%", containLabel: true},
xAxis: {type: "category", boundaryGap: false, data: dateArray},
@ -145,6 +151,12 @@ const Dashboard = (props) => {
{name: i18next.t("general:Providers"), type: "line", data: dashboardData.providerCounts},
{name: i18next.t("general:Applications"), type: "line", data: dashboardData.applicationCounts},
{name: i18next.t("general:Subscriptions"), type: "line", data: dashboardData.subscriptionCounts},
{name: i18next.t("general:Roles"), type: "line", data: dashboardData.roleCounts},
{name: i18next.t("general:Groups"), type: "line", data: dashboardData.groupCounts},
{name: i18next.t("general:Resources"), type: "line", data: dashboardData.resourceCounts},
{name: i18next.t("general:Certs"), type: "line", data: dashboardData.certCounts},
{name: i18next.t("general:Permissions"), type: "line", data: dashboardData.permissionCounts},
{name: i18next.t("general:Transactions"), type: "line", data: dashboardData.transactionCounts},
],
};
myChart.setOption(option);

File diff suppressed because it is too large Load Diff