Compare commits

..

40 Commits

Author SHA1 Message Date
aiden
49a981f787 fix: fix that GROUPS is a reserved keyword introduced in MySQL 8.0 (#2458)
Co-authored-by: aidenlu <aiden_lu@wochacha.com>
2023-10-30 10:59:48 +08:00
aiden
34b1945180 feat: fix bugs in custom app sso login with WebAuthn authentication (#2457)
Co-authored-by: aidenlu <aiden_lu@wochacha.com>
2023-10-30 10:54:34 +08:00
Yang Luo
b320cca789 Can disable ldapServerPort by setting to empty string 2023-10-29 23:55:08 +08:00
Yang Luo
b38654a45a Add renderAiAssistant() 2023-10-28 23:58:51 +08:00
Yang Luo
f77fafae24 Fix hidden top navbar item 2023-10-28 17:07:29 +08:00
songjf
8b6b5ffe81 feat: fix go-reddit module checksum mismatch (#2451) 2023-10-28 15:32:36 +08:00
Chao
a147fa3e0b feat: fix bug that tableNamePrefix caused getRolesByUserInternal() to fail (#2450)
If set tableNamePrefix in app.conf, while cause sql error
2023-10-28 09:45:54 +08:00
Yang Luo
9d03665523 Fix FromProviderToIdpInfo() bug 2023-10-27 18:10:22 +08:00
Yang Luo
0106c7f7fa Fix GetIdProvider() bug 2023-10-27 17:03:37 +08:00
Yang Luo
6713dad0af Fix this.props.account null issue 2023-10-27 02:13:23 +08:00
Yang Luo
6ef2b51782 Support fastAutoSignin by backend redirection 2023-10-27 00:44:50 +08:00
Yang Luo
1732cd8538 Fix the bug that sometimes cannot auto login with enableAutoSignin = true 2023-10-27 00:06:17 +08:00
Yang Luo
a10548fe73 Fix org admin's enforcer policy APIs 2023-10-26 23:31:36 +08:00
Yang Luo
f6a7888f83 Deleted user cannot perform actions 2023-10-26 10:41:38 +08:00
Yang Luo
93efaa5459 Fix FileExist() error handling 2023-10-26 10:40:28 +08:00
jump2cn
0bfe683108 feat: change canonicalizer algorithm to xml-exc-c14n# (#2440) 2023-10-24 14:13:09 +08:00
Yang Luo
8a4758c22d Update sync code 2023-10-22 11:56:56 +08:00
Yang Luo
ee3b46e91c Allow permission.Model to be empty 2023-10-22 02:35:51 +08:00
Yang Luo
37744d6cd7 Improve permission error handling 2023-10-22 02:30:29 +08:00
Yang Luo
98defe617b Add providerItem.SignupGroup 2023-10-20 23:10:43 +08:00
Yang Luo
96cbf51ca0 Remove useless alertType field 2023-10-20 23:01:11 +08:00
Yang Luo
22b57fdd23 Add application.EnableSamlC14n10 2023-10-20 22:37:23 +08:00
haiwu
b68e291f37 feat: support SAML Custom provider (#2430)
* 111

* feat: support custom saml provider

* feat: gofumpt code

* feat: gofumpt code

* feat: remove comment

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-10-20 21:11:36 +08:00
aiden
9960b4933b feat: respect isReadOnly in the syncer (#2427)
Co-authored-by: aidenlu <aiden_lu@wochacha.com>
2023-10-19 18:57:12 +08:00
aiden
432a5496f2 fix: skip checking password when the code is provided (#2425)
Co-authored-by: aidenlu <aiden_lu@wochacha.com>
2023-10-19 18:25:25 +08:00
aiden
45db4deb6b feat: support checking permissions for group roles (#2422)
* fix(permission): fix CheckLoginPermission() logic

* style: fix code format

* feat: support settting roles for groups

* fix: fix field name

* style: format codes

---------

Co-authored-by: aidenlu <aiden_lu@wochacha.com>
2023-10-19 15:33:45 +08:00
Yang Luo
3f53591751 Improve verification no provider error message 2023-10-18 15:32:12 +08:00
Yang Luo
d7569684f6 Local admin can edit its org user's other fields now 2023-10-18 12:16:05 +08:00
Yang Luo
a616127909 Add organization.DefaultPassword 2023-10-18 11:58:25 +08:00
Yang Luo
f2e2b960ff Improve downloadImage() error handling 2023-10-18 02:25:22 +08:00
Yang Luo
fbc603876f feat: add originFrontend to app.conf 2023-10-17 21:47:18 +08:00
Yang Luo
9ea77c63d1 Local admin can edit its org users now 2023-10-17 18:23:39 +08:00
songjf
53243a30f3 feat: support tencent cloud SAML SSO authentication with casdoor (#2409)
* feat: Support Tencent Cloud SAML SSO authentication with Casdoor

* feat: support SamlAttributeTable in the frontend

* fix:fixed the error where frontend fields did not match the database fields

* fix:fix lint error

* fix:fixed non-standard naming

* fix:remove if conditional statement

* feat:Add Saml Attribute format select

* fix:fix typo

* fix:fix typo

* fix:fix typo

* Update SamlAttributeTable.js

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-10-17 15:40:41 +08:00
aiden
cbdeb91ee8 feat: support groups in app login permissions (#2413)
* fix(permission): fix CheckLoginPermission() logic

* style: fix code format

---------

Co-authored-by: aidenlu <aiden_lu@wochacha.com>
2023-10-17 14:35:13 +08:00
Yang Luo
2dd1dc582f Add text to app's signup table 2023-10-15 18:17:50 +08:00
Yang Luo
f3d4b45a0f Add label and placeholder to app's signup table 2023-10-15 17:24:38 +08:00
Yang Luo
2ee4aebd96 Fix error handling in GetSamlMeta() 2023-10-15 17:02:40 +08:00
Yang Luo
150e3e30d5 Support app user in API authentication 2023-10-15 15:20:57 +08:00
Yang Luo
1055d7781b Improve error handling in AutoSigninFilter 2023-10-15 12:43:36 +08:00
Yang Luo
1c296e9b6f feat: activate enableGzip by default in app.conf 2023-10-15 01:27:42 +08:00
58 changed files with 995 additions and 253 deletions

View File

@@ -127,8 +127,14 @@ func IsAllowed(subOwner string, subName string, method string, urlPath string, o
return true
}
if user != nil && user.IsAdmin && (subOwner == objOwner || (objOwner == "admin")) {
return true
if user != nil {
if user.IsDeleted {
return false
}
if user.IsAdmin && (subOwner == objOwner || (objOwner == "admin")) {
return true
}
}
res, err := Enforcer.Enforce(subOwner, subName, method, urlPath, objOwner, objName)

View File

@@ -16,9 +16,11 @@ verificationCodeTimeout = 10
initScore = 0
logPostOnly = true
origin =
originFrontend =
staticBaseUrl = "https://cdn.casbin.org"
isDemoMode = false
batchSize = 100
enableGzip = true
ldapServerPort = 389
radiusServerPort = 1812
radiusSecret = "secret"

View File

@@ -477,11 +477,10 @@ func (c *ApiController) Login() {
c.ResponseError(fmt.Sprintf(c.T("auth:The provider: %s is not enabled for the application"), provider.Name))
return
}
userInfo := &idp.UserInfo{}
if provider.Category == "SAML" {
// SAML
userInfo.Id, err = object.ParseSamlResponse(authForm.SamlResponse, provider, c.Ctx.Request.Host)
userInfo, err = object.ParseSamlResponse(authForm.SamlResponse, provider, c.Ctx.Request.Host)
if err != nil {
c.ResponseError(err.Error())
return
@@ -524,7 +523,8 @@ func (c *ApiController) Login() {
if authForm.Method == "signup" {
user := &object.User{}
if provider.Category == "SAML" {
user, err = object.GetUser(util.GetId(application.Organization, userInfo.Id))
// The userInfo.Id is the NameID in SAML response, it could be name / email / phone
user, err = object.GetUserByFields(application.Organization, userInfo.Id)
if err != nil {
c.ResponseError(err.Error())
return
@@ -651,6 +651,15 @@ func (c *ApiController) Login() {
c.ResponseError(fmt.Sprintf(c.T("auth:Failed to create user, user information is invalid: %s"), util.StructToJson(user)))
return
}
if providerItem.SignupGroup != "" {
user.Groups = []string{providerItem.SignupGroup}
_, err = object.UpdateUser(user.GetId(), user, []string{"groups"}, false)
if err != nil {
c.ResponseError(err.Error())
return
}
}
}
// sync info from 3rd-party if possible
@@ -679,6 +688,7 @@ func (c *ApiController) Login() {
record2.User = user.Name
util.SafeGoroutine(func() { object.AddRecord(record2) })
} else if provider.Category == "SAML" {
// TODO: since we get the user info from SAML response, we can try to create the user
resp = &Response{Status: "error", Msg: fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(application.Organization, userInfo.Id))}
}
// resp = &Response{Status: "ok", Msg: "", Data: res}

View File

@@ -33,7 +33,13 @@ func (c *ApiController) GetSamlMeta() {
c.ResponseError(fmt.Sprintf(c.T("saml:Application %s not found"), paramApp))
return
}
metadata, _ := object.GetSamlMeta(application, host)
metadata, err := object.GetSamlMeta(application, host)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["xml"] = metadata
c.ServeXML()
}

View File

@@ -478,7 +478,7 @@ func (c *ApiController) SetPassword() {
return
}
}
} else {
} else if code == "" {
msg := object.CheckPassword(targetUser, oldPassword, c.GetAcceptLanguage())
if msg != "" {
c.ResponseError(msg)

View File

@@ -96,6 +96,13 @@ func (c *ApiController) RequireSignedInUser() (*object.User, bool) {
return nil, false
}
if strings.HasPrefix(userId, "app/") {
tmpUserId := c.Input().Get("userId")
if tmpUserId != "" {
userId = tmpUserId
}
}
user, err := object.GetUser(userId)
if err != nil {
c.ResponseError(err.Error())

View File

@@ -142,6 +142,10 @@ func (c *ApiController) SendVerificationCode() {
c.ResponseError(err.Error())
return
}
if provider == nil {
c.ResponseError(fmt.Sprintf("please add an Email provider to the \"Providers\" list for the application: %s", application.Name))
return
}
sendResp = object.SendVerificationCodeToEmail(organization, user, provider, remoteAddr, vform.Dest)
case object.VerifyTypePhone:
@@ -184,6 +188,10 @@ func (c *ApiController) SendVerificationCode() {
c.ResponseError(err.Error())
return
}
if provider == nil {
c.ResponseError(fmt.Sprintf("please add a SMS provider to the \"Providers\" list for the application: %s", application.Name))
return
}
if phone, ok := util.GetE164Number(vform.Dest, vform.CountryCode); !ok {
c.ResponseError(fmt.Sprintf(c.T("verification:Phone number is invalid in your region %s"), vform.CountryCode))

2
go.mod
View File

@@ -13,7 +13,7 @@ require (
github.com/casbin/casbin/v2 v2.77.2
github.com/casdoor/go-sms-sender v0.15.0
github.com/casdoor/gomail/v2 v2.0.1
github.com/casdoor/notify v0.44.0
github.com/casdoor/notify v0.45.0
github.com/casdoor/oss v1.3.0
github.com/casdoor/xorm-adapter/v3 v3.0.4
github.com/casvisor/casvisor-go-sdk v1.0.3

8
go.sum
View File

@@ -920,12 +920,14 @@ github.com/casbin/casbin/v2 v2.28.3/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRt
github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
github.com/casbin/casbin/v2 v2.77.2 h1:yQinn/w9x8AswiwqwtrXz93VU48R1aYTXdHEx4RI3jM=
github.com/casbin/casbin/v2 v2.77.2/go.mod h1:mzGx0hYW9/ksOSpw3wNjk3NRAroq5VMFYUQ6G43iGPk=
github.com/casdoor/go-reddit/v2 v2.1.0 h1:kIbfdJ7AA7H0uTQ8s0q4GGZqSS5V9wVE74RrXyD9XPs=
github.com/casdoor/go-reddit/v2 v2.1.0/go.mod h1:eagkvwlZ4Hcsuc/uQsLHYEulz5jN65SVSwV/AIE7zsc=
github.com/casdoor/go-sms-sender v0.15.0 h1:9SWj/jd5c7jIteTRUrqbkpWbtIXMDv+t1CEfDhO06m0=
github.com/casdoor/go-sms-sender v0.15.0/go.mod h1:cQs7qqohMJBgIVZebOCB8ko09naG1vzFJEH59VNIscs=
github.com/casdoor/gomail/v2 v2.0.1 h1:J+FG6x80s9e5lBHUn8Sv0Y56mud34KiWih5YdmudR/w=
github.com/casdoor/gomail/v2 v2.0.1/go.mod h1:VnGPslEAtpix5FjHisR/WKB1qvZDBaujbikxDe9d+2Q=
github.com/casdoor/notify v0.44.0 h1:/j2TqO5lXEKYyu2WWtmGh3jh4aeN8m6p+9tWb5j1PWU=
github.com/casdoor/notify v0.44.0/go.mod h1:HgLPFmSmy9+uB72cp2z3Tk5KxpZfStqpLMr+5RddXmw=
github.com/casdoor/notify v0.45.0 h1:OlaFvcQFjGOgA4mRx07M8AH1gvb5xNo21mcqrVGlLgk=
github.com/casdoor/notify v0.45.0/go.mod h1:wNHQu0tiDROMBIvz0j3Om3Lhd5yZ+AIfnFb8MYb8OLQ=
github.com/casdoor/oss v1.3.0 h1:D5pcz65tJRqJrWY11Ks7D9LUsmlhqqMHugjDhSxWTvk=
github.com/casdoor/oss v1.3.0/go.mod h1:YOi6KpG1pZHTkiy9AYaqI0UaPfE7YkaA07d89f1idqY=
github.com/casdoor/xorm-adapter/v3 v3.0.4 h1:vB04Ao8n2jA7aFBI9F+gGXo9+Aa1IQP6mTdo50913DM=
@@ -1829,8 +1831,6 @@ github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljT
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/utahta/go-linenotify v0.5.0 h1:E1tJaB/XhqRY/iz203FD0MaHm10DjQPOq5/Mem2A3Gs=
github.com/utahta/go-linenotify v0.5.0/go.mod h1:KsvBXil2wx+ByaCR0e+IZKTbp4pDesc7yjzRigLf6pE=
github.com/vartanbeno/go-reddit/v2 v2.0.0 h1:fxYMqx5lhbmJ3yYRN1nnQC/gecRB3xpUS2BbG7GLpsk=
github.com/vartanbeno/go-reddit/v2 v2.0.0/go.mod h1:758/S10hwZSLm43NPtwoNQdZFSg3sjB5745Mwjb0ANI=
github.com/volcengine/volc-sdk-golang v1.0.117 h1:ykFVSwsVq9qvIoWP9jeP+VKNAUjrblAdsZl46yVWiH8=
github.com/volcengine/volc-sdk-golang v1.0.117/go.mod h1:ojXSFvj404o2UKnZR9k9LUUWIUU+9XtlRlzk2+UFc/M=
github.com/wendal/errors v0.0.0-20181209125328-7f31f4b264ec/go.mod h1:Q12BUT7DqIlHRmgv3RskH+UCM/4eqVMgI0EMmlSpAXc=

View File

@@ -85,7 +85,7 @@ func GetIdProvider(idpInfo *ProviderInfo, redirectUrl string) IdProvider {
return NewLarkIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
case "GitLab":
return NewGitlabIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
case "Adfs":
case "ADFS":
return NewAdfsIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl)
case "Baidu":
return NewBaiduIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)

View File

@@ -15,6 +15,7 @@
"tags": [],
"languages": ["en", "zh", "es", "fr", "de", "id", "ja", "ko", "ru", "vi", "it", "ms", "tr","ar", "he", "nl", "pl", "fi", "sv", "uk", "kk", "fa"],
"masterPassword": "",
"defaultPassword": "",
"initScore": 2000,
"enableSoftDeletion": false,
"isProfilePublic": true,

View File

@@ -25,6 +25,11 @@ import (
)
func StartLdapServer() {
ldapServerPort := conf.GetConfigString("ldapServerPort")
if ldapServerPort == "" || ldapServerPort == "0" {
return
}
server := ldap.NewServer()
routes := ldap.NewRouteMux()
@@ -32,7 +37,7 @@ func StartLdapServer() {
routes.Search(handleSearch).Label(" SEARCH****")
server.Handle(routes)
err := server.ListenAndServe("0.0.0.0:" + conf.GetConfigString("ldapServerPort"))
err := server.ListenAndServe("0.0.0.0:" + ldapServerPort)
if err != nil {
log.Printf("StartLdapServer() failed, err = %s", err.Error())
}

View File

@@ -22,14 +22,15 @@ config: |
dataSourceName = "file:ent?mode=memory&cache=shared&_fk=1"
dbName = casdoor
redisEndpoint =
defaultStorageProvider =
defaultStorageProvider =
isCloudIntranet = false
authState = "casdoor"
socks5Proxy = ""
verificationCodeTimeout = 10
initScore = 2000
initScore = 0
logPostOnly = true
origin = "https://door.casbin.com"
origin =
enableGzip = true
imagePullSecrets: []
nameOverride: ""

View File

@@ -25,11 +25,19 @@ import (
)
type SignupItem struct {
Name string `json:"name"`
Visible bool `json:"visible"`
Required bool `json:"required"`
Prompted bool `json:"prompted"`
Rule string `json:"rule"`
Name string `json:"name"`
Visible bool `json:"visible"`
Required bool `json:"required"`
Prompted bool `json:"prompted"`
Label string `json:"label"`
Placeholder string `json:"placeholder"`
Rule string `json:"rule"`
}
type SamlItem struct {
Name string `json:"name"`
NameFormat string `json:"nameformat"`
Value string `json:"value"`
}
type Application struct {
@@ -49,17 +57,19 @@ type Application struct {
EnableAutoSignin bool `json:"enableAutoSignin"`
EnableCodeSignin bool `json:"enableCodeSignin"`
EnableSamlCompress bool `json:"enableSamlCompress"`
EnableSamlC14n10 bool `json:"enableSamlC14n10"`
EnableWebAuthn bool `json:"enableWebAuthn"`
EnableLinkWithEmail bool `json:"enableLinkWithEmail"`
OrgChoiceMode string `json:"orgChoiceMode"`
SamlReplyUrl string `xorm:"varchar(100)" json:"samlReplyUrl"`
Providers []*ProviderItem `xorm:"mediumtext" json:"providers"`
SignupItems []*SignupItem `xorm:"varchar(1000)" json:"signupItems"`
SignupItems []*SignupItem `xorm:"varchar(2000)" json:"signupItems"`
GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"`
OrganizationObj *Organization `xorm:"-" json:"organizationObj"`
CertPublicKey string `xorm:"-" json:"certPublicKey"`
Tags []string `xorm:"mediumtext" json:"tags"`
InvitationCodes []string `xorm:"varchar(200)" json:"invitationCodes"`
SamlAttributes []*SamlItem `xorm:"varchar(1000)" json:"samlAttributes"`
ClientId string `xorm:"varchar(100)" json:"clientId"`
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
@@ -306,6 +316,9 @@ func GetMaskedApplication(application *Application, userId string) *Application
if application.OrganizationObj.MasterPassword != "" {
application.OrganizationObj.MasterPassword = "***"
}
if application.OrganizationObj.DefaultPassword != "" {
application.OrganizationObj.DefaultPassword = "***"
}
if application.OrganizationObj.PasswordType != "" {
application.OrganizationObj.PasswordType = "***"
}

View File

@@ -370,7 +370,7 @@ func CheckLoginPermission(userId string, application *Application) (bool, error)
continue
}
if !permission.isUserHit(userId) {
if !permission.isUserHit(userId) && !permission.isRoleHit(userId) {
if permission.Effect == "Allow" {
allowPermissionCount += 1
} else {
@@ -379,7 +379,10 @@ func CheckLoginPermission(userId string, application *Application) (bool, error)
continue
}
enforcer := getPermissionEnforcer(permission)
enforcer, err := getPermissionEnforcer(permission)
if err != nil {
return false, err
}
var isAllowed bool
isAllowed, err = enforcer.Enforce(userId, application.Name, "Read")

View File

@@ -178,7 +178,7 @@ func initBuiltInApplication() {
EnablePassword: true,
EnableSignUp: true,
Providers: []*ProviderItem{
{Name: "provider_captcha_default", CanSignUp: false, CanSignIn: false, CanUnlink: false, Prompted: false, AlertType: "None", Rule: "None", Provider: nil},
{Name: "provider_captcha_default", CanSignUp: false, CanSignIn: false, CanUnlink: false, Prompted: false, SignupGroup: "", Rule: "None", Provider: nil},
},
SignupItems: []*SignupItem{
{Name: "ID", Visible: false, Required: true, Prompted: false, Rule: "Random"},

View File

@@ -59,7 +59,7 @@ func isIpAddress(host string) bool {
return ip != nil
}
func getOriginFromHost(host string) (string, string) {
func getOriginFromHostInternal(host string) (string, string) {
origin := conf.GetConfigString("origin")
if origin != "" {
return origin, origin
@@ -82,6 +82,17 @@ func getOriginFromHost(host string) (string, string) {
}
}
func getOriginFromHost(host string) (string, string) {
originF, originB := getOriginFromHostInternal(host)
originFrontend := conf.GetConfigString("originFrontend")
if originFrontend != "" {
originF = originFrontend
}
return originF, originB
}
func GetOidcDiscovery(host string) OidcDiscovery {
originFrontend, originBackend := getOriginFromHost(host)

View File

@@ -64,6 +64,7 @@ type Organization struct {
Languages []string `xorm:"varchar(255)" json:"languages"`
ThemeData *ThemeData `xorm:"json" json:"themeData"`
MasterPassword string `xorm:"varchar(100)" json:"masterPassword"`
DefaultPassword string `xorm:"varchar(100)" json:"defaultPassword"`
InitScore int `json:"initScore"`
EnableSoftDeletion bool `json:"enableSoftDeletion"`
IsProfilePublic bool `json:"isProfilePublic"`
@@ -155,6 +156,9 @@ func GetMaskedOrganization(organization *Organization, errs ...error) (*Organiza
if organization.MasterPassword != "" {
organization.MasterPassword = "***"
}
if organization.DefaultPassword != "" {
organization.DefaultPassword = "***"
}
return organization, nil
}
@@ -202,9 +206,14 @@ func UpdateOrganization(id string, organization *Organization) (bool, error) {
}
session := ormer.Engine.ID(core.PK{owner, name}).AllCols()
if organization.MasterPassword == "***" {
session.Omit("master_password")
}
if organization.DefaultPassword == "***" {
session.Omit("default_password")
}
affected, err := session.Update(organization)
if err != nil {
return false, err

View File

@@ -113,11 +113,15 @@ func GetPermission(id string) (*Permission, error) {
// checkPermissionValid verifies if the permission is valid
func checkPermissionValid(permission *Permission) error {
enforcer := getPermissionEnforcer(permission)
enforcer, err := getPermissionEnforcer(permission)
if err != nil {
return err
}
enforcer.EnableAutoSave(false)
policies := getPolicies(permission)
_, err := enforcer.AddPolicies(policies)
_, err = enforcer.AddPolicies(policies)
if err != nil {
return err
}
@@ -129,7 +133,7 @@ func checkPermissionValid(permission *Permission) error {
groupingPolicies := getGroupingPolicies(permission)
if len(groupingPolicies) > 0 {
_, err := enforcer.AddGroupingPolicies(groupingPolicies)
_, err = enforcer.AddGroupingPolicies(groupingPolicies)
if err != nil {
return err
}
@@ -150,7 +154,7 @@ func UpdatePermission(id string, permission *Permission) (bool, error) {
return false, nil
}
if permission.ResourceType == "Application" {
if permission.ResourceType == "Application" && permission.Model != "" {
model, err := GetModelEx(util.GetId(owner, permission.Model))
if err != nil {
return false, err
@@ -174,8 +178,16 @@ func UpdatePermission(id string, permission *Permission) (bool, error) {
}
if affected != 0 {
removeGroupingPolicies(oldPermission)
removePolicies(oldPermission)
err = removeGroupingPolicies(oldPermission)
if err != nil {
return false, err
}
err = removePolicies(oldPermission)
if err != nil {
return false, err
}
if oldPermission.Adapter != "" && oldPermission.Adapter != permission.Adapter {
isEmpty, _ := ormer.Engine.IsTableEmpty(oldPermission.Adapter)
if isEmpty {
@@ -185,8 +197,16 @@ func UpdatePermission(id string, permission *Permission) (bool, error) {
}
}
}
addGroupingPolicies(permission)
addPolicies(permission)
err = addGroupingPolicies(permission)
if err != nil {
return false, err
}
err = addPolicies(permission)
if err != nil {
return false, err
}
}
return affected != 0, nil
@@ -199,40 +219,54 @@ func AddPermission(permission *Permission) (bool, error) {
}
if affected != 0 {
addGroupingPolicies(permission)
addPolicies(permission)
err = addGroupingPolicies(permission)
if err != nil {
return false, err
}
err = addPolicies(permission)
if err != nil {
return false, err
}
}
return affected != 0, nil
}
func AddPermissions(permissions []*Permission) bool {
func AddPermissions(permissions []*Permission) (bool, error) {
if len(permissions) == 0 {
return false
return false, nil
}
affected, err := ormer.Engine.Insert(permissions)
if err != nil {
if !strings.Contains(err.Error(), "Duplicate entry") {
panic(err)
return false, err
}
}
for _, permission := range permissions {
// add using for loop
if affected != 0 {
addGroupingPolicies(permission)
addPolicies(permission)
err = addGroupingPolicies(permission)
if err != nil {
return false, err
}
err = addPolicies(permission)
if err != nil {
return false, err
}
}
}
return affected != 0
return affected != 0, nil
}
func AddPermissionsInBatch(permissions []*Permission) bool {
func AddPermissionsInBatch(permissions []*Permission) (bool, error) {
batchSize := conf.GetConfigBatchSize()
if len(permissions) == 0 {
return false
return false, nil
}
affected := false
@@ -245,12 +279,18 @@ func AddPermissionsInBatch(permissions []*Permission) bool {
tmp := permissions[start:end]
fmt.Printf("The syncer adds permissions: [%d - %d]\n", start, end)
if AddPermissions(tmp) {
b, err := AddPermissions(tmp)
if err != nil {
return false, err
}
if b {
affected = true
}
}
return affected
return affected, nil
}
func DeletePermission(permission *Permission) (bool, error) {
@@ -260,8 +300,16 @@ func DeletePermission(permission *Permission) (bool, error) {
}
if affected != 0 {
removeGroupingPolicies(permission)
removePolicies(permission)
err = removeGroupingPolicies(permission)
if err != nil {
return false, err
}
err = removePolicies(permission)
if err != nil {
return false, err
}
if permission.Adapter != "" && permission.Adapter != "permission_rule" {
isEmpty, _ := ormer.Engine.IsTableEmpty(permission.Adapter)
if isEmpty {
@@ -434,6 +482,21 @@ func (p *Permission) isUserHit(name string) bool {
return false
}
func (p *Permission) isRoleHit(userId string) bool {
targetRoles, err := getRolesByUser(userId)
if err != nil {
return false
}
for _, role := range p.Roles {
for _, targetRole := range targetRoles {
if targetRole.GetId() == role {
return true
}
}
}
return false
}
func (p *Permission) isResourceHit(name string) bool {
for _, resource := range p.Resources {
if resource == "*" || resource == name {

View File

@@ -26,23 +26,23 @@ import (
xormadapter "github.com/casdoor/xorm-adapter/v3"
)
func getPermissionEnforcer(p *Permission, permissionIDs ...string) *casbin.Enforcer {
func getPermissionEnforcer(p *Permission, permissionIDs ...string) (*casbin.Enforcer, error) {
// Init an enforcer instance without specifying a model or adapter.
// If you specify an adapter, it will load all policies, which is a
// heavy process that can slow down the application.
enforcer, err := casbin.NewEnforcer(&log.DefaultLogger{}, false)
if err != nil {
panic(err)
return nil, err
}
err = p.setEnforcerModel(enforcer)
if err != nil {
panic(err)
return nil, err
}
err = p.setEnforcerAdapter(enforcer)
if err != nil {
panic(err)
return nil, err
}
policyFilterV5 := []string{p.GetId()}
@@ -60,10 +60,10 @@ func getPermissionEnforcer(p *Permission, permissionIDs ...string) *casbin.Enfor
err = enforcer.LoadFilteredPolicy(policyFilter)
if err != nil {
panic(err)
return nil, err
}
return enforcer
return enforcer, nil
}
func (p *Permission) setEnforcerAdapter(enforcer *casbin.Enforcer) error {
@@ -201,72 +201,96 @@ func getGroupingPolicies(permission *Permission) [][]string {
return groupingPolicies
}
func addPolicies(permission *Permission) {
enforcer := getPermissionEnforcer(permission)
func addPolicies(permission *Permission) error {
enforcer, err := getPermissionEnforcer(permission)
if err != nil {
return err
}
policies := getPolicies(permission)
_, err := enforcer.AddPolicies(policies)
if err != nil {
panic(err)
}
_, err = enforcer.AddPolicies(policies)
return err
}
func addGroupingPolicies(permission *Permission) {
enforcer := getPermissionEnforcer(permission)
func removePolicies(permission *Permission) error {
enforcer, err := getPermissionEnforcer(permission)
if err != nil {
return err
}
policies := getPolicies(permission)
_, err = enforcer.RemovePolicies(policies)
return err
}
func addGroupingPolicies(permission *Permission) error {
enforcer, err := getPermissionEnforcer(permission)
if err != nil {
return err
}
groupingPolicies := getGroupingPolicies(permission)
if len(groupingPolicies) > 0 {
_, err := enforcer.AddGroupingPolicies(groupingPolicies)
_, err = enforcer.AddGroupingPolicies(groupingPolicies)
if err != nil {
panic(err)
return err
}
}
return nil
}
func removeGroupingPolicies(permission *Permission) {
enforcer := getPermissionEnforcer(permission)
func removeGroupingPolicies(permission *Permission) error {
enforcer, err := getPermissionEnforcer(permission)
if err != nil {
return err
}
groupingPolicies := getGroupingPolicies(permission)
if len(groupingPolicies) > 0 {
_, err := enforcer.RemoveGroupingPolicies(groupingPolicies)
_, err = enforcer.RemoveGroupingPolicies(groupingPolicies)
if err != nil {
panic(err)
return err
}
}
}
func removePolicies(permission *Permission) {
enforcer := getPermissionEnforcer(permission)
policies := getPolicies(permission)
_, err := enforcer.RemovePolicies(policies)
if err != nil {
panic(err)
}
return nil
}
type CasbinRequest = []interface{}
func Enforce(permission *Permission, request *CasbinRequest, permissionIds ...string) (bool, error) {
enforcer := getPermissionEnforcer(permission, permissionIds...)
enforcer, err := getPermissionEnforcer(permission, permissionIds...)
if err != nil {
return false, err
}
return enforcer.Enforce(*request...)
}
func BatchEnforce(permission *Permission, requests *[]CasbinRequest, permissionIds ...string) ([]bool, error) {
enforcer := getPermissionEnforcer(permission, permissionIds...)
enforcer, err := getPermissionEnforcer(permission, permissionIds...)
if err != nil {
return nil, err
}
return enforcer.BatchEnforce(*requests)
}
func getAllValues(userId string, fn func(enforcer *casbin.Enforcer) []string) []string {
func getAllValues(userId string, fn func(enforcer *casbin.Enforcer) []string) ([]string, error) {
permissions, _, err := getPermissionsAndRolesByUser(userId)
if err != nil {
panic(err)
return nil, err
}
for _, role := range GetAllRoles(userId) {
permissionsByRole, err := GetPermissionsByRole(role)
if err != nil {
panic(err)
return nil, err
}
permissions = append(permissions, permissionsByRole...)
@@ -274,19 +298,24 @@ func getAllValues(userId string, fn func(enforcer *casbin.Enforcer) []string) []
var values []string
for _, permission := range permissions {
enforcer := getPermissionEnforcer(permission)
enforcer, err := getPermissionEnforcer(permission)
if err != nil {
return nil, err
}
values = append(values, fn(enforcer)...)
}
return values
return values, nil
}
func GetAllObjects(userId string) []string {
func GetAllObjects(userId string) ([]string, error) {
return getAllValues(userId, func(enforcer *casbin.Enforcer) []string {
return enforcer.GetAllObjects()
})
}
func GetAllActions(userId string) []string {
func GetAllActions(userId string) ([]string, error) {
return getAllValues(userId, func(enforcer *casbin.Enforcer) []string {
return enforcer.GetAllActions()
})
@@ -330,17 +359,23 @@ m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act`
// load [policy_definition]
policyDefinition := strings.Split(cfg.String("policy_definition::p"), ",")
fieldsNum := len(policyDefinition)
if fieldsNum > builtInAvailableField {
panic(fmt.Errorf("the maximum policy_definition field number cannot exceed %d, got %d", builtInAvailableField, fieldsNum))
return nil, fmt.Errorf("the maximum policy_definition field number cannot exceed %d, got %d", builtInAvailableField, fieldsNum)
}
// filled empty field with "" and V5 with "permissionId"
for i := builtInAvailableField - fieldsNum; i > 0; i-- {
policyDefinition = append(policyDefinition, "")
}
policyDefinition = append(policyDefinition, "permissionId")
m, _ := model.NewModelFromString(modelText)
m, err := model.NewModelFromString(modelText)
if err != nil {
return nil, err
}
m.AddDef("p", "p", strings.Join(policyDefinition, ","))
return m, err

View File

@@ -83,5 +83,10 @@ func UploadPermissions(owner string, path string) (bool, error) {
return false, nil
}
return AddPermissionsInBatch(newPermissions), nil
affected, err := AddPermissionsInBatch(newPermissions)
if err != nil {
return false, err
}
return affected, nil
}

View File

@@ -415,7 +415,7 @@ func FromProviderToIdpInfo(ctx *context.Context, provider *Provider) *idp.Provid
providerInfo.ClientId = provider.ClientId2
providerInfo.ClientSecret = provider.ClientSecret2
}
} else if provider.Type == "AzureAD" {
} else if provider.Type == "AzureAD" || provider.Type == "ADFS" {
providerInfo.HostUrl = provider.Domain
}

View File

@@ -18,13 +18,13 @@ type ProviderItem struct {
Owner string `json:"owner"`
Name string `json:"name"`
CanSignUp bool `json:"canSignUp"`
CanSignIn bool `json:"canSignIn"`
CanUnlink bool `json:"canUnlink"`
Prompted bool `json:"prompted"`
AlertType string `json:"alertType"`
Rule string `json:"rule"`
Provider *Provider `json:"provider"`
CanSignUp bool `json:"canSignUp"`
CanSignIn bool `json:"canSignIn"`
CanUnlink bool `json:"canUnlink"`
Prompted bool `json:"prompted"`
SignupGroup string `json:"signupGroup"`
Rule string `json:"rule"`
Provider *Provider `json:"provider"`
}
func (application *Application) GetProviderItem(providerName string) *ProviderItem {

View File

@@ -151,8 +151,16 @@ func UpdateRole(id string, role *Role) (bool, error) {
}
for _, permission := range permissions {
addGroupingPolicies(permission)
addPolicies(permission)
err = addGroupingPolicies(permission)
if err != nil {
return false, err
}
err = addPolicies(permission)
if err != nil {
return false, err
}
visited[permission.GetId()] = struct{}{}
}
@@ -166,10 +174,15 @@ func UpdateRole(id string, role *Role) (bool, error) {
if err != nil {
return false, err
}
for _, permission := range permissions {
permissionId := permission.GetId()
if _, ok := visited[permissionId]; !ok {
addGroupingPolicies(permission)
err = addGroupingPolicies(permission)
if err != nil {
return false, err
}
visited[permissionId] = struct{}{}
}
}
@@ -254,14 +267,24 @@ func (role *Role) GetId() string {
func getRolesByUserInternal(userId string) ([]*Role, error) {
roles := []*Role{}
err := ormer.Engine.Where("users like ?", "%"+userId+"\"%").Find(&roles)
user, err := GetUser(userId)
if err != nil {
return roles, err
}
query := ormer.Engine.Alias("r").Where("r.users like ?", fmt.Sprintf("%%%s%%", userId))
for _, group := range user.Groups {
query = query.Or("r.groups like ?", fmt.Sprintf("%%%s%%", group))
}
err = query.Find(&roles)
if err != nil {
return roles, err
}
res := []*Role{}
for _, role := range roles {
if util.InSlice(role.Users, userId) {
if util.InSlice(role.Users, userId) || util.HaveIntersection(role.Groups, user.Groups) {
res = append(res, role)
}
}

View File

@@ -37,7 +37,7 @@ import (
// NewSamlResponse
// returns a saml2 response
func NewSamlResponse(user *User, host string, certificate string, destination string, iss string, requestId string, redirectUri []string) (*etree.Element, error) {
func NewSamlResponse(application *Application, user *User, host string, certificate string, destination string, iss string, requestId string, redirectUri []string) (*etree.Element, error) {
samlResponse := &etree.Element{
Space: "samlp",
Tag: "Response",
@@ -103,6 +103,13 @@ func NewSamlResponse(user *User, host string, certificate string, destination st
displayName.CreateAttr("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic")
displayName.CreateElement("saml:AttributeValue").CreateAttr("xsi:type", "xs:string").Element().SetText(user.DisplayName)
for _, item := range application.SamlAttributes {
role := attributes.CreateElement("saml:Attribute")
role.CreateAttr("Name", item.Name)
role.CreateAttr("NameFormat", item.NameFormat)
role.CreateElement("saml:AttributeValue").CreateAttr("xsi:type", "xs:string").Element().SetText(item.Value)
}
roles := attributes.CreateElement("saml:Attribute")
roles.CreateAttr("Name", "Roles")
roles.CreateAttr("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic")
@@ -184,10 +191,11 @@ type SingleSignOnService struct {
type Attribute struct {
XMLName xml.Name
Name string `xml:"Name,attr"`
NameFormat string `xml:"NameFormat,attr"`
FriendlyName string `xml:"FriendlyName,attr"`
Xmlns string `xml:"xmlns,attr"`
Name string `xml:"Name,attr"`
NameFormat string `xml:"NameFormat,attr"`
FriendlyName string `xml:"FriendlyName,attr"`
Xmlns string `xml:"xmlns,attr"`
Values []string `xml:"AttributeValue"`
}
func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, error) {
@@ -309,13 +317,18 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
_, originBackend := getOriginFromHost(host)
// build signedResponse
samlResponse, _ := NewSamlResponse(user, originBackend, certificate, authnRequest.AssertionConsumerServiceURL, authnRequest.Issuer.Url, authnRequest.ID, application.RedirectUris)
samlResponse, _ := NewSamlResponse(application, user, originBackend, certificate, authnRequest.AssertionConsumerServiceURL, authnRequest.Issuer.Url, authnRequest.ID, application.RedirectUris)
randomKeyStore := &X509Key{
PrivateKey: cert.PrivateKey,
X509Certificate: certificate,
}
ctx := dsig.NewDefaultSigningContext(randomKeyStore)
ctx.Hash = crypto.SHA1
if application.EnableSamlC14n10 {
ctx.Canonicalizer = dsig.MakeC14N10ExclusiveCanonicalizerWithPrefixList("")
}
//signedXML, err := ctx.SignEnvelopedLimix(samlResponse)
//if err != nil {
// return "", "", fmt.Errorf("err: %s", err.Error())

View File

@@ -23,23 +23,49 @@ import (
"regexp"
"strings"
"github.com/casdoor/casdoor/idp"
"github.com/mitchellh/mapstructure"
"github.com/casdoor/casdoor/i18n"
saml2 "github.com/russellhaering/gosaml2"
dsig "github.com/russellhaering/goxmldsig"
)
func ParseSamlResponse(samlResponse string, provider *Provider, host string) (string, error) {
func ParseSamlResponse(samlResponse string, provider *Provider, host string) (*idp.UserInfo, error) {
samlResponse, _ = url.QueryUnescape(samlResponse)
sp, err := buildSp(provider, samlResponse, host)
if err != nil {
return "", err
return nil, err
}
assertionInfo, err := sp.RetrieveAssertionInfo(samlResponse)
if err != nil {
return "", err
return nil, err
}
return assertionInfo.NameID, err
userInfoMap := make(map[string]string)
for spAttr, idpAttr := range provider.UserMapping {
for _, attr := range assertionInfo.Values {
if attr.Name == idpAttr {
userInfoMap[spAttr] = attr.Values[0].Value
}
}
}
userInfoMap["id"] = assertionInfo.NameID
customUserInfo := &idp.CustomUserInfo{}
err = mapstructure.Decode(userInfoMap, customUserInfo)
if err != nil {
return nil, err
}
userInfo := &idp.UserInfo{
Id: customUserInfo.Id,
Username: customUserInfo.Username,
DisplayName: customUserInfo.DisplayName,
Email: customUserInfo.Email,
AvatarUrl: customUserInfo.AvatarUrl,
}
return userInfo, err
}
func GenerateSamlRequest(id, relayState, host, lang string) (auth string, method string, err error) {
@@ -146,14 +172,24 @@ func getCertificateFromSamlResponse(samlResponse string, providerType string) (s
if err != nil {
return "", err
}
deStr := strings.Replace(string(de), "\n", "", -1)
tagMap := map[string]string{
"Aliyun IDaaS": "ds",
"Keycloak": "dsig",
}
var (
expression string
deStr = strings.Replace(string(de), "\n", "", -1)
tagMap = map[string]string{
"Aliyun IDaaS": "ds",
"Keycloak": "dsig",
}
)
tag := tagMap[providerType]
expression := fmt.Sprintf("<%s:X509Certificate>([\\s\\S]*?)</%s:X509Certificate>", tag, tag)
if tag == "" {
// <ds:X509Certificate>...</ds:X509Certificate>
// <dsig:X509Certificate>...</dsig:X509Certificate>
// <X509Certificate>...</X509Certificate>
// ...
expression = "<[^>]*:?X509Certificate>([\\s\\S]*?)<[^>]*:?X509Certificate>"
} else {
expression = fmt.Sprintf("<%s:X509Certificate>([\\s\\S]*?)</%s:X509Certificate>", tag, tag)
}
res := regexp.MustCompile(expression).FindStringSubmatch(deStr)
return res[1], nil
}

View File

@@ -59,6 +59,10 @@ func AddUserToOriginalDatabase(user *User) error {
return nil
}
if syncer.IsReadOnly {
return nil
}
updatedOUser := syncer.createOriginalUserFromUser(user)
_, err = syncer.addUser(updatedOUser)
if err != nil {
@@ -78,6 +82,10 @@ func UpdateUserToOriginalDatabase(user *User) error {
return nil
}
if syncer.IsReadOnly {
return nil
}
newUser, err := GetUser(user.GetId())
if err != nil {
return err

View File

@@ -696,6 +696,10 @@ func AddUser(user *User) (bool, error) {
return false, nil
}
if organization.DefaultPassword != "" && user.Password == "123" {
user.Password = organization.DefaultPassword
}
if user.PasswordType == "" || user.PasswordType == "plain" {
user.UpdateUserPassword(organization)
}

View File

@@ -35,7 +35,7 @@ func downloadImage(client *http.Client, url string) (*bytes.Buffer, string, erro
resp, err := client.Do(req)
if err != nil {
fmt.Printf("downloadImage() error for url [%s]: %s\n", url, err.Error())
if strings.Contains(err.Error(), "EOF") || strings.Contains(err.Error(), "no such host") || strings.Contains(err.Error(), "did not properly respond after a period of time") {
if strings.Contains(err.Error(), "EOF") || strings.Contains(err.Error(), "no such host") || strings.Contains(err.Error(), "did not properly respond after a period of time") || strings.Contains(err.Error(), "unrecognized name") {
return nil, "", nil
} else {
return nil, "", err

View File

@@ -80,10 +80,6 @@ func IsAllowSend(user *User, remoteAddr, recordType string) error {
}
func SendVerificationCodeToEmail(organization *Organization, user *User, provider *Provider, remoteAddr string, dest string) error {
if provider == nil {
return fmt.Errorf("please set an Email provider first")
}
sender := organization.DisplayName
title := provider.Title
code := getRandomCode(6)
@@ -106,10 +102,6 @@ func SendVerificationCodeToEmail(organization *Organization, user *User, provide
}
func SendVerificationCodeToPhone(organization *Organization, user *User, provider *Provider, remoteAddr string, dest string) error {
if provider == nil {
return errors.New("please set a SMS provider first")
}
if err := IsAllowSend(user, remoteAddr, provider.Category); err != nil {
return err
}

View File

@@ -35,14 +35,14 @@ type Object struct {
func getUsername(ctx *context.Context) (username string) {
defer func() {
if r := recover(); r != nil {
username = getUsernameByClientIdSecret(ctx)
username, _ = getUsernameByClientIdSecret(ctx)
}
}()
username = ctx.Input.Session("username").(string)
if username == "" {
username = getUsernameByClientIdSecret(ctx)
username, _ = getUsernameByClientIdSecret(ctx)
}
if username == "" {
@@ -66,6 +66,13 @@ func getObject(ctx *context.Context) (string, string) {
path := ctx.Request.URL.Path
if method == http.MethodGet {
if ctx.Request.URL.Path == "/api/get-policies" && ctx.Input.Query("id") == "/" {
adapterId := ctx.Input.Query("adapterId")
if adapterId != "" {
return util.GetOwnerAndNameFromIdNoCheck(adapterId)
}
}
// query == "?id=built-in/admin"
id := ctx.Input.Query("id")
if id != "" {
@@ -79,8 +86,14 @@ func getObject(ctx *context.Context) (string, string) {
return "", ""
} else {
body := ctx.Input.RequestBody
if path == "/api/add-policy" || path == "/api/remove-policy" || path == "/api/update-policy" {
id := ctx.Input.Query("id")
if id != "" {
return util.GetOwnerAndNameFromIdNoCheck(id)
}
}
body := ctx.Input.RequestBody
if len(body) == 0 {
return ctx.Request.Form.Get("owner"), ctx.Request.Form.Get("name")
}

View File

@@ -45,19 +45,21 @@ func AutoSigninFilter(ctx *context.Context) {
}
if token == nil {
responseError(ctx, "Access token doesn't exist")
responseError(ctx, "Access token doesn't exist in database")
return
}
if util.IsTokenExpired(token.CreatedTime, token.ExpiresIn) {
responseError(ctx, "Access token has expired")
isExpired, expireTime := util.IsTokenExpired(token.CreatedTime, token.ExpiresIn)
if isExpired {
responseError(ctx, fmt.Sprintf("Access token has expired, expireTime = %s", expireTime))
return
}
userId := util.GetId(token.Organization, token.User)
application, err := object.GetApplicationByUserId(fmt.Sprintf("app/%s", token.Application))
if err != nil {
panic(err)
responseError(ctx, err.Error())
return
}
setSessionUser(ctx, userId)
@@ -66,7 +68,11 @@ func AutoSigninFilter(ctx *context.Context) {
}
// "/page?clientId=123&clientSecret=456"
userId := getUsernameByClientIdSecret(ctx)
userId, err := getUsernameByClientIdSecret(ctx)
if err != nil {
responseError(ctx, err.Error())
return
}
if userId != "" {
setSessionUser(ctx, userId)
return

View File

@@ -66,7 +66,7 @@ func denyRequest(ctx *context.Context) {
responseError(ctx, T(ctx, "auth:Unauthorized operation"))
}
func getUsernameByClientIdSecret(ctx *context.Context) string {
func getUsernameByClientIdSecret(ctx *context.Context) (string, error) {
clientId, clientSecret, ok := ctx.Request.BasicAuth()
if !ok {
clientId = ctx.Input.Query("clientId")
@@ -74,19 +74,22 @@ func getUsernameByClientIdSecret(ctx *context.Context) string {
}
if clientId == "" || clientSecret == "" {
return ""
return "", nil
}
application, err := object.GetApplicationByClientId(clientId)
if err != nil {
panic(err)
return "", err
}
if application == nil {
return "", fmt.Errorf("Application not found for client ID: %s", clientId)
}
if application == nil || application.ClientSecret != clientSecret {
return ""
if application.ClientSecret != clientSecret {
return "", fmt.Errorf("Incorrect client secret for application: %s", application.Name)
}
return fmt.Sprintf("app/%s", application.Name)
return fmt.Sprintf("app/%s", application.Name), nil
}
func getUsernameByKeys(ctx *context.Context) string {

View File

@@ -26,6 +26,7 @@ import (
"github.com/beego/beego/context"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
@@ -46,6 +47,46 @@ func getWebBuildFolder() string {
return path
}
func fastAutoSignin(ctx *context.Context) (string, error) {
userId := getSessionUser(ctx)
if userId == "" {
return "", nil
}
clientId := ctx.Input.Query("client_id")
responseType := ctx.Input.Query("response_type")
redirectUri := ctx.Input.Query("redirect_uri")
scope := ctx.Input.Query("scope")
state := ctx.Input.Query("state")
nonce := ""
codeChallenge := ""
if clientId == "" || responseType != "code" || redirectUri == "" {
return "", nil
}
application, err := object.GetApplicationByClientId(clientId)
if err != nil {
return "", err
}
if application == nil {
return "", nil
}
if !application.EnableAutoSignin {
return "", nil
}
code, err := object.GetOAuthCode(userId, clientId, responseType, redirectUri, scope, state, nonce, codeChallenge, ctx.Request.Host, getAcceptLanguage(ctx))
if err != nil {
return "", err
} else if code.Message != "" {
return "", fmt.Errorf(code.Message)
}
res := fmt.Sprintf("%s?code=%s&state=%s", redirectUri, code.Code, state)
return res, nil
}
func StaticFilter(ctx *context.Context) {
urlPath := ctx.Request.URL.Path
@@ -63,6 +104,19 @@ func StaticFilter(ctx *context.Context) {
return
}
if urlPath == "/login/oauth/authorize" {
redirectUrl, err := fastAutoSignin(ctx)
if err != nil {
responseError(ctx, err.Error())
return
}
if redirectUrl != "" {
http.Redirect(ctx.ResponseWriter, ctx.Request, redirectUrl, http.StatusFound)
return
}
}
webBuildFolder := getWebBuildFolder()
path := webBuildFolder
if urlPath == "/" {

View File

@@ -36,7 +36,12 @@ func (db *Database) onDDL(header *replication.EventHeader, nextPos mysql.Positio
}
func (db *Database) OnRow(e *canal.RowsEvent) error {
log.Info("serverId: ", e.Header.ServerID)
if e.Header != nil {
log.Info("serverId: ", e.Header.ServerID)
} else {
log.Info("serverId: e.Header == nil")
}
if strings.Contains(db.Gtid, db.serverUuid) {
return nil
}
@@ -87,11 +92,13 @@ func (db *Database) OnRow(e *canal.RowsEvent) error {
pkColumnValue := getPkColumnValues(oldColumnValue, e.Table.PKColumns)
updateSql, args, err := getUpdateSql(e.Table.Schema, e.Table.Name, columnNames, newColumnValue, pkColumnNames, pkColumnValue)
if err != nil {
log.Error(err)
return err
}
res, err := db.engine.DB().Exec(updateSql, args...)
if err != nil {
log.Error(err)
return err
}
log.Info(updateSql, args, res)
@@ -113,11 +120,13 @@ func (db *Database) OnRow(e *canal.RowsEvent) error {
pkColumnValue := getPkColumnValues(oldColumnValue, e.Table.PKColumns)
deleteSql, args, err := getDeleteSql(e.Table.Schema, e.Table.Name, pkColumnNames, pkColumnValue)
if err != nil {
log.Error(err)
return err
}
res, err := db.engine.DB().Exec(deleteSql, args...)
if err != nil {
log.Error(err)
return err
}
log.Info(deleteSql, args, res)
@@ -141,11 +150,13 @@ func (db *Database) OnRow(e *canal.RowsEvent) error {
insertSql, args, err := getInsertSql(e.Table.Schema, e.Table.Name, columnNames, newColumnValue)
if err != nil {
log.Error(err)
return err
}
res, err := db.engine.DB().Exec(insertSql, args...)
if err != nil {
log.Error(err)
return err
}
log.Info(insertSql, args, res)

View File

@@ -20,11 +20,21 @@ func startSyncJob(db1 *Database, db2 *Database) error {
var wg sync.WaitGroup
// start canal1 replication
go db1.startCanal(db2)
go func(db1 *Database, db2 *Database) {
err := db1.startCanal(db2)
if err != nil {
panic(err)
}
}(db1, db2)
wg.Add(1)
// start canal2 replication
go db2.startCanal(db1)
go func(db1 *Database, db2 *Database) {
err := db2.startCanal(db1)
if err != nil {
panic(err)
}
}(db1, db2)
wg.Add(1)
wg.Wait()

View File

@@ -24,7 +24,10 @@ import (
)
func TestStartSyncJob(t *testing.T) {
db1 := newDatabase("127.0.0.1", 3306, "casdoor", "root", "123456")
db2 := newDatabase("127.0.0.1", 3306, "casdoor2", "root", "123456")
startSyncJob(db1, db2)
db1 := newDatabase("localhost", 3306, "casdoor", "root", "123456")
db2 := newDatabase("localhost", 3306, "casdoor2", "root", "123456")
err := startSyncJob(db1, db2)
if err != nil {
panic(err)
}
}

View File

@@ -15,9 +15,7 @@
package sync
import (
"fmt"
"log"
"strconv"
"github.com/Masterminds/squirrel"
"github.com/xorm-io/xorm"
@@ -74,21 +72,23 @@ func createEngine(dataSourceName string) (*xorm.Engine, error) {
}
func getServerId(engin *xorm.Engine) (uint32, error) {
res, err := engin.QueryInterface("SELECT @@server_id")
record, err := engin.QueryInterface("SELECT @@server_id")
if err != nil {
return 0, err
}
serverId, _ := strconv.ParseUint(fmt.Sprintf("%s", res[0]["@@server_id"]), 10, 32)
return uint32(serverId), nil
res := uint32(record[0]["@@server_id"].(int64))
return res, nil
}
func getServerUuid(engin *xorm.Engine) (string, error) {
res, err := engin.QueryString("show variables like 'server_uuid'")
record, err := engin.QueryString("show variables like 'server_uuid'")
if err != nil {
return "", err
}
serverUuid := fmt.Sprintf("%s", res[0]["Value"])
return serverUuid, err
res := record[0]["Value"]
return res, err
}
func getPkColumnNames(columnNames []string, PKColumns []int) []string {

View File

@@ -24,7 +24,10 @@ import (
)
func FileExist(path string) bool {
if _, err := os.Stat(path); os.IsNotExist(err) {
_, err := os.Stat(path)
if os.IsNotExist(err) {
return false
} else if err != nil {
return false
}
return true

View File

@@ -60,3 +60,19 @@ func ReturnAnyNotEmpty(strs ...string) string {
}
return ""
}
func HaveIntersection(arr1 []string, arr2 []string) bool {
elements := make(map[string]bool)
for _, str := range arr1 {
elements[str] = true
}
for _, str := range arr2 {
if elements[str] {
return true
}
}
return false
}

View File

@@ -58,8 +58,10 @@ func Time2String(timestamp time.Time) string {
return timestamp.Format(time.RFC3339)
}
func IsTokenExpired(createdTime string, expiresIn int) bool {
func IsTokenExpired(createdTime string, expiresIn int) (bool, string) {
createdTimeObj, _ := time.Parse(time.RFC3339, createdTime)
expiresAtObj := createdTimeObj.Add(time.Duration(expiresIn) * time.Second)
return time.Now().After(expiresAtObj)
isExpired := time.Now().After(expiresAtObj)
expireTime := expiresAtObj.Local().Format(time.RFC3339)
return isExpired, expireTime
}

View File

@@ -102,7 +102,7 @@ func Test_IsTokenExpired(t *testing.T) {
},
} {
t.Run(scenario.description, func(t *testing.T) {
result := IsTokenExpired(scenario.input.createdTime, scenario.input.expiresIn)
result, _ := IsTokenExpired(scenario.input.createdTime, scenario.input.expiresIn)
assert.Equal(t, scenario.expected, result, fmt.Sprintf("Expected %t, but was founded %t", scenario.expected, result))
})
}

View File

@@ -17,11 +17,10 @@ import "./App.less";
import {Helmet} from "react-helmet";
import Dashboard from "./basic/Dashboard";
import ShortcutsPage from "./basic/ShortcutsPage";
import {MfaRuleRequired} from "./Setting";
import * as Setting from "./Setting";
import {StyleProvider, legacyLogicalPropertiesTransformer} from "@ant-design/cssinjs";
import {AppstoreTwoTone, BarsOutlined, DollarTwoTone, DownOutlined, HomeTwoTone, InfoCircleFilled, LockTwoTone, LogoutOutlined, SafetyCertificateTwoTone, SettingOutlined, SettingTwoTone, WalletTwoTone} from "@ant-design/icons";
import {Alert, Avatar, Button, Card, ConfigProvider, Drawer, Dropdown, FloatButton, Layout, Menu, Result} from "antd";
import {AppstoreTwoTone, BarsOutlined, DeploymentUnitOutlined, DollarTwoTone, DownOutlined, GithubOutlined, HomeTwoTone, InfoCircleFilled, LockTwoTone, LogoutOutlined, SafetyCertificateTwoTone, SettingOutlined, SettingTwoTone, ShareAltOutlined, WalletTwoTone} from "@ant-design/icons";
import {Alert, Avatar, Button, Card, ConfigProvider, Drawer, Dropdown, FloatButton, Layout, Menu, Result, Tooltip} from "antd";
import {Link, Redirect, Route, Switch, withRouter} from "react-router-dom";
import OrganizationListPage from "./OrganizationListPage";
import OrganizationEditPage from "./OrganizationEditPage";
@@ -110,6 +109,7 @@ class App extends Component {
themeData: Conf.ThemeDefault,
logo: this.getLogo(Setting.getAlgorithmNames(Conf.ThemeDefault)),
requiredEnableMfa: false,
isAiAssistantOpen: false,
};
Setting.initServerUrl();
@@ -137,8 +137,8 @@ class App extends Component {
});
if (requiredEnableMfa === true) {
const mfaType = Setting.getMfaItemsByRules(this.state.account, this.state.account?.organization, [MfaRuleRequired])
.find((item) => item.rule === MfaRuleRequired)?.name;
const mfaType = Setting.getMfaItemsByRules(this.state.account, this.state.account?.organization, [Setting.MfaRuleRequired])
.find((item) => item.rule === Setting.MfaRuleRequired)?.name;
if (mfaType !== undefined) {
this.props.history.push(`/mfa/setup?mfaType=${mfaType}`, {from: "/login"});
}
@@ -380,6 +380,15 @@ class App extends Component {
});
}} />
<LanguageSelect languages={this.state.account.organization.languages} />
<Tooltip title="Click to open AI assitant">
<div className="select-box" onClick={() => {
this.setState({
isAiAssistantOpen: true,
});
}}>
<DeploymentUnitOutlined style={{fontSize: "24px", color: "rgb(77,77,77)"}} />
</div>
</Tooltip>
<OpenTour />
{Setting.isAdminUser(this.state.account) && !Setting.isMobile() &&
<OrganizationSelect
@@ -579,7 +588,7 @@ class App extends Component {
}
}
};
const menuStyleRight = Setting.isAdminUser(this.state.account) && !Setting.isMobile() ? "calc(180px + 260px)" : "260px";
const menuStyleRight = Setting.isAdminUser(this.state.account) && !Setting.isMobile() ? "calc(180px + 280px)" : "280px";
return (
<Layout id="parent-area">
<EnableMfaNotification account={this.state.account} />
@@ -625,7 +634,12 @@ class App extends Component {
</Card>
}
</Content>
{this.renderFooter()}
{
this.renderFooter()
}
{
this.renderAiAssistant()
}
</Layout>
);
}
@@ -651,6 +665,40 @@ class App extends Component {
);
}
renderAiAssistant() {
return (
<Drawer
title={
<React.Fragment>
<Tooltip title="Want to deploy your own AI assistant? Click to learn more!">
<a target="_blank" rel="noreferrer" href={"https://casdoor.com"}>
<img style={{width: "20px", marginRight: "10px", marginBottom: "2px"}} alt="help" src="https://casbin.org/img/casbin.svg" />
AI Assistant
</a>
</Tooltip>
<a className="custom-link" style={{float: "right", marginTop: "2px"}} target="_blank" rel="noreferrer" href={"https://ai.casbin.com"}>
<ShareAltOutlined className="custom-link" style={{fontSize: "20px", color: "rgb(140,140,140)"}} />
</a>
<a className="custom-link" style={{float: "right", marginRight: "30px", marginTop: "2px"}} target="_blank" rel="noreferrer" href={"https://github.com/casibase/casibase"}>
<GithubOutlined className="custom-link" style={{fontSize: "20px", color: "rgb(140,140,140)"}} />
</a>
</React.Fragment>
}
placement="right"
width={500}
mask={false}
onClose={() => {
this.setState({
isAiAssistantOpen: false,
});
}}
visible={this.state.isAiAssistantOpen}
>
<iframe id="iframeHelper" title={"iframeHelper"} src={"https://ai.casbin.com/?isRaw=1"} width="100%" height="100%" scrolling="no" frameBorder="no" />
</Drawer>
);
}
isDoorPages() {
return this.isEntryPages() || window.location.pathname.startsWith("/callback");
}
@@ -696,6 +744,9 @@ class App extends Component {
{
this.renderFooter()
}
{
this.renderAiAssistant()
}
</Layout>
);
}

View File

@@ -28,12 +28,13 @@ import i18next from "i18next";
import UrlTable from "./table/UrlTable";
import ProviderTable from "./table/ProviderTable";
import SignupTable from "./table/SignupTable";
import SamlAttributeTable from "./table/SamlAttributeTable";
import PromptPage from "./auth/PromptPage";
import copy from "copy-to-clipboard";
import ThemeEditor from "./common/theme/ThemeEditor";
import {Controlled as CodeMirror} from "react-codemirror2";
import "codemirror/lib/codemirror.css";
import ThemeEditor from "./common/theme/ThemeEditor";
require("codemirror/theme/material-darker.css");
require("codemirror/mode/htmlmixed/htmlmixed");
@@ -104,6 +105,7 @@ class ApplicationEditPage extends React.Component {
providers: [],
uploading: false,
mode: props.location.mode !== undefined ? props.location.mode : "edit",
samlAttributes: [],
samlMetadata: null,
isAuthorized: true,
};
@@ -638,6 +640,29 @@ class ApplicationEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("application:Enable SAML C14N10"), i18next.t("application:Enable SAML C14N10 - Tooltip"))} :
</Col>
<Col span={1} >
<Switch checked={this.state.application.enableSamlC14n10} onChange={checked => {
this.updateApplicationField("enableSamlC14n10", checked);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:SAML attributes"), i18next.t("general:SAML attributes - Tooltip"))} :
</Col>
<Col span={22} >
<SamlAttributeTable
title={i18next.t("general:SAML attributes")}
table={this.state.application.samlAttributes}
application={this.state.application}
onUpdateTable={(value) => {this.updateApplicationField("samlAttributes", value);}}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:SAML metadata"), i18next.t("application:SAML metadata - Tooltip"))} :

View File

@@ -44,7 +44,7 @@ class ApplicationListPage extends BaseListPage {
enableCodeSignin: false,
enableSamlCompress: false,
providers: [
{name: "provider_captcha_default", canSignUp: false, canSignIn: false, canUnlink: false, prompted: false, alertType: "None"},
{name: "provider_captcha_default", canSignUp: false, canSignIn: false, canUnlink: false, prompted: false, signupGroup: "", rule: ""},
],
signupItems: [
{name: "ID", visible: false, required: true, rule: "Random"},

View File

@@ -313,6 +313,16 @@ class OrganizationEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Default password"), i18next.t("general:Default password - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.organization.defaultPassword} onChange={e => {
this.updateOrganizationField("defaultPassword", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("organization:Init score"), i18next.t("organization:Init score - Tooltip"))} :

View File

@@ -41,6 +41,7 @@ class OrganizationListPage extends BaseListPage {
tags: [],
languages: Setting.Countries.map(item => item.key),
masterPassword: "",
defaultPassword: "",
enableSoftDeletion: false,
isProfilePublic: true,
accountItems: [

View File

@@ -379,10 +379,11 @@ class ProviderEditPage extends React.Component {
loadSamlConfiguration() {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(this.state.provider.metadata, "text/xml");
const cert = xmlDoc.getElementsByTagName("ds:X509Certificate")[0].childNodes[0].nodeValue;
const endpoint = xmlDoc.getElementsByTagName("md:SingleSignOnService")[0].getAttribute("Location");
const issuerUrl = xmlDoc.getElementsByTagName("md:EntityDescriptor")[0].getAttribute("entityID");
const rawXml = this.state.provider.metadata.replace("\n", "");
const xmlDoc = parser.parseFromString(rawXml, "text/xml");
const cert = xmlDoc.querySelector("X509Certificate").childNodes[0].nodeValue.replace(" ", "");
const endpoint = xmlDoc.querySelector("SingleSignOnService").getAttribute("Location");
const issuerUrl = xmlDoc.querySelector("EntityDescriptor").getAttribute("entityID");
this.updateProviderField("idP", cert);
this.updateProviderField("endpoint", endpoint);
this.updateProviderField("issuerUrl", issuerUrl);
@@ -491,7 +492,7 @@ class ProviderEditPage extends React.Component {
this.updateProviderField("type", value);
if (value === "Local File System") {
this.updateProviderField("domain", Setting.getFullServerUrl());
} else if (value === "Custom") {
} else if (value === "Custom" && this.state.provider.category === "OAuth") {
this.updateProviderField("customAuthUrl", "https://door.casdoor.com/login/oauth/authorize");
this.updateProviderField("scopes", "openid profile email");
this.updateProviderField("customTokenUrl", "https://door.casdoor.com/api/login/oauth/access_token");
@@ -553,48 +554,54 @@ class ProviderEditPage extends React.Component {
)
}
{
this.state.provider.type !== "Custom" ? null : (
this.state.provider.type === "Custom" ? (
<React.Fragment>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Auth URL"), i18next.t("provider:Auth URL - Tooltip"))}
</Col>
<Col span={22} >
<Input value={this.state.provider.customAuthUrl} onChange={e => {
this.updateProviderField("customAuthUrl", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Token URL"), i18next.t("provider:Token URL - Tooltip"))}
</Col>
<Col span={22} >
<Input value={this.state.provider.customTokenUrl} onChange={e => {
this.updateProviderField("customTokenUrl", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Scope"), i18next.t("provider:Scope - Tooltip"))}
</Col>
<Col span={22} >
<Input value={this.state.provider.scopes} onChange={e => {
this.updateProviderField("scopes", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:UserInfo URL"), i18next.t("provider:UserInfo URL - Tooltip"))}
</Col>
<Col span={22} >
<Input value={this.state.provider.customUserInfoUrl} onChange={e => {
this.updateProviderField("customUserInfoUrl", e.target.value);
}} />
</Col>
</Row>
{
this.state.provider.category === "OAuth" ? (
<Col>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Auth URL"), i18next.t("provider:Auth URL - Tooltip"))}
</Col>
<Col span={22} >
<Input value={this.state.provider.customAuthUrl} onChange={e => {
this.updateProviderField("customAuthUrl", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Token URL"), i18next.t("provider:Token URL - Tooltip"))}
</Col>
<Col span={22} >
<Input value={this.state.provider.customTokenUrl} onChange={e => {
this.updateProviderField("customTokenUrl", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Scope"), i18next.t("provider:Scope - Tooltip"))}
</Col>
<Col span={22} >
<Input value={this.state.provider.scopes} onChange={e => {
this.updateProviderField("scopes", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:UserInfo URL"), i18next.t("provider:UserInfo URL - Tooltip"))}
</Col>
<Col span={22} >
<Input value={this.state.provider.customUserInfoUrl} onChange={e => {
this.updateProviderField("customUserInfoUrl", e.target.value);
}} />
</Col>
</Row>
</Col>
) : null
}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:User mapping"), i18next.t("provider:User mapping - Tooltip"))} :
@@ -631,7 +638,7 @@ class ProviderEditPage extends React.Component {
</Col>
</Row>
</React.Fragment>
)
) : null
}
{
(this.state.provider.category === "Captcha" && this.state.provider.type === "Default") ||

View File

@@ -209,6 +209,10 @@ export const OtherProviderInfo = {
logo: `${StaticBaseUrl}/img/social_keycloak.png`,
url: "https://www.keycloak.org/",
},
"Custom": {
logo: `${StaticBaseUrl}/img/social_custom.png`,
url: "https://door.casdoor.com/",
},
},
Payment: {
"Dummy": {
@@ -866,10 +870,10 @@ export function getClickable(text) {
}
export function getProviderLogoURL(provider) {
if (provider.type === "Custom" && provider.customLogo) {
return provider.customLogo;
}
if (provider.category === "OAuth") {
if (provider.type === "Custom" && provider.customLogo) {
return provider.customLogo;
}
return `${StaticBaseUrl}/img/social_${provider.type.toLowerCase()}.png`;
} else {
const info = OtherProviderInfo[provider.category][provider.type];
@@ -1014,6 +1018,7 @@ export function getProviderTypeOptions(category) {
return ([
{id: "Aliyun IDaaS", name: "Aliyun IDaaS"},
{id: "Keycloak", name: "Keycloak"},
{id: "Custom", name: "Custom"},
]);
} else if (category === "Payment") {
return ([

View File

@@ -205,7 +205,7 @@ class UserEditPage extends React.Component {
}
isSelfOrAdmin() {
return this.isSelf() || Setting.isAdminUser(this.props.account);
return this.isSelf() || Setting.isLocalAdminUser(this.props.account);
}
getCountryCode() {
@@ -241,7 +241,7 @@ class UserEditPage extends React.Component {
return null;
}
const isAdmin = Setting.isAdminUser(this.props.account);
const isAdmin = Setting.isLocalAdminUser(this.props.account);
if (accountItem.viewRule === "Self") {
if (!this.isSelfOrAdmin()) {

View File

@@ -99,7 +99,9 @@ class LoginPage extends React.Component {
this.setState({enableCaptchaModal: CaptchaRule.Never});
}
}
}
if (prevProps.account !== this.props.account && this.props.account !== undefined) {
if (this.props.account && this.props.account.owner === this.props.application?.organization) {
const params = new URLSearchParams(this.props.location.search);
const silentSignin = params.get("silentSignin");
@@ -764,7 +766,11 @@ class LoginPage extends React.Component {
const rawId = assertion.rawId;
const sig = assertion.response.signature;
const userHandle = assertion.response.userHandle;
return fetch(`${Setting.ServerUrl}/api/webauthn/signin/finish?responseType=${values["type"]}`, {
let finishUrl = `${Setting.ServerUrl}/api/webauthn/signin/finish?responseType=${values["type"]}`;
if (values["type"] === "code") {
finishUrl = `${Setting.ServerUrl}/api/webauthn/signin/finish?responseType=${values["type"]}&clientId=${oAuthParams.clientId}&scope=${oAuthParams.scope}&redirectUri=${oAuthParams.redirectUri}&nonce=${oAuthParams.nonce}&state=${oAuthParams.state}&codeChallenge=${oAuthParams.codeChallenge}&challengeMethod=${oAuthParams.challengeMethod}`;
}
return fetch(finishUrl, {
method: "POST",
credentials: "include",
body: JSON.stringify({

View File

@@ -226,7 +226,7 @@ class SignupPage extends React.Component {
return (
<Form.Item
name="username"
label={i18next.t("signup:Username")}
label={signupItem.label ? signupItem.label : i18next.t("signup:Username")}
rules={[
{
required: required,
@@ -235,7 +235,7 @@ class SignupPage extends React.Component {
},
]}
>
<Input />
<Input placeholder={signupItem.placeholder} />
</Form.Item>
);
} else if (signupItem.name === "Display name") {
@@ -244,7 +244,7 @@ class SignupPage extends React.Component {
<React.Fragment>
<Form.Item
name="firstName"
label={i18next.t("general:First name")}
label={signupItem.label ? signupItem.label : i18next.t("general:First name")}
rules={[
{
required: required,
@@ -253,11 +253,11 @@ class SignupPage extends React.Component {
},
]}
>
<Input />
<Input placeholder={signupItem.placeholder} />
</Form.Item>
<Form.Item
name="lastName"
label={i18next.t("general:Last name")}
label={signupItem.label ? signupItem.label : i18next.t("general:Last name")}
rules={[
{
required: required,
@@ -266,7 +266,7 @@ class SignupPage extends React.Component {
},
]}
>
<Input />
<Input placeholder={signupItem.placeholder} />
</Form.Item>
</React.Fragment>
);
@@ -275,7 +275,7 @@ class SignupPage extends React.Component {
return (
<Form.Item
name="name"
label={(signupItem.rule === "Real name" || signupItem.rule === "First, last") ? i18next.t("general:Real name") : i18next.t("general:Display name")}
label={(signupItem.label ? signupItem.label : (signupItem.rule === "Real name" || signupItem.rule === "First, last") ? i18next.t("general:Real name") : i18next.t("general:Display name"))}
rules={[
{
required: required,
@@ -284,14 +284,14 @@ class SignupPage extends React.Component {
},
]}
>
<Input />
<Input placeholder={signupItem.placeholder} />
</Form.Item>
);
} else if (signupItem.name === "Affiliation") {
return (
<Form.Item
name="affiliation"
label={i18next.t("user:Affiliation")}
label={signupItem.label ? signupItem.label : i18next.t("user:Affiliation")}
rules={[
{
required: required,
@@ -300,14 +300,14 @@ class SignupPage extends React.Component {
},
]}
>
<Input />
<Input placeholder={signupItem.placeholder} />
</Form.Item>
);
} else if (signupItem.name === "ID card") {
return (
<Form.Item
name="idCard"
label={i18next.t("user:ID card")}
label={signupItem.label ? signupItem.label : i18next.t("user:ID card")}
rules={[
{
required: required,
@@ -321,14 +321,14 @@ class SignupPage extends React.Component {
},
]}
>
<Input />
<Input placeholder={signupItem.placeholder} />
</Form.Item>
);
} else if (signupItem.name === "Country/Region") {
return (
<Form.Item
name="country_region"
label={i18next.t("user:Country/Region")}
label={signupItem.label ? signupItem.label : i18next.t("user:Country/Region")}
rules={[
{
required: required,
@@ -344,7 +344,7 @@ class SignupPage extends React.Component {
<React.Fragment>
<Form.Item
name="email"
label={i18next.t("general:Email")}
label={signupItem.label ? signupItem.label : i18next.t("general:Email")}
rules={[
{
required: required,
@@ -363,13 +363,13 @@ class SignupPage extends React.Component {
},
]}
>
<Input onChange={e => this.setState({email: e.target.value})} />
<Input placeholder={signupItem.placeholder} onChange={e => this.setState({email: e.target.value})} />
</Form.Item>
{
signupItem.rule !== "No verification" &&
<Form.Item
name="emailCode"
label={i18next.t("code:Email code")}
label={signupItem.label ? signupItem.label : i18next.t("code:Email code")}
rules={[{
required: required,
message: i18next.t("code:Please input your verification code!"),
@@ -388,7 +388,7 @@ class SignupPage extends React.Component {
} else if (signupItem.name === "Phone") {
return (
<React.Fragment>
<Form.Item label={i18next.t("general:Phone")} required={required}>
<Form.Item label={signupItem.label ? signupItem.label : i18next.t("general:Phone")} required={required}>
<Input.Group compact>
<Form.Item
name="countryCode"
@@ -432,6 +432,7 @@ class SignupPage extends React.Component {
]}
>
<Input
placeholder={signupItem.placeholder}
style={{width: "65%"}}
onChange={e => this.setState({phone: e.target.value})}
/>
@@ -442,7 +443,7 @@ class SignupPage extends React.Component {
signupItem.rule !== "No verification" &&
<Form.Item
name="phoneCode"
label={i18next.t("code:Phone code")}
label={signupItem.label ? signupItem.label : i18next.t("code:Phone code")}
rules={[
{
required: required,
@@ -465,7 +466,7 @@ class SignupPage extends React.Component {
return (
<Form.Item
name="password"
label={i18next.t("general:Password")}
label={signupItem.label ? signupItem.label : i18next.t("general:Password")}
rules={[
{
required: required,
@@ -482,14 +483,14 @@ class SignupPage extends React.Component {
]}
hasFeedback
>
<Input.Password />
<Input.Password placeholder={signupItem.placeholder} />
</Form.Item>
);
} else if (signupItem.name === "Confirm password") {
return (
<Form.Item
name="confirm"
label={i18next.t("signup:Confirm")}
label={signupItem.label ? signupItem.label : i18next.t("signup:Confirm")}
dependencies={["password"]}
hasFeedback
rules={[
@@ -508,14 +509,14 @@ class SignupPage extends React.Component {
}),
]}
>
<Input.Password />
<Input.Password placeholder={signupItem.placeholder} />
</Form.Item>
);
} else if (signupItem.name === "Invitation code") {
return (
<Form.Item
name="invitationCode"
label={i18next.t("application:Invitation code")}
label={signupItem.label ? signupItem.label : i18next.t("application:Invitation code")}
rules={[
{
required: required,
@@ -523,11 +524,15 @@ class SignupPage extends React.Component {
},
]}
>
<Input />
<Input placeholder={signupItem.placeholder} />
</Form.Item>
);
} else if (signupItem.name === "Agreement") {
return AgreementModal.renderAgreementFormItem(application, required, tailFormItemLayout, this);
} else if (signupItem.name.startsWith("Text ")) {
return (
<div dangerouslySetInnerHTML={{__html: signupItem.label}} />
);
}
}

View File

@@ -34,7 +34,7 @@ class OpenTour extends React.Component {
render() {
return (
this.canTour() ?
<Tooltip title="Click to enable the help wizard.">
<Tooltip title="Click to open tour">
<div className="select-box" style={{display: Setting.isMobile() ? "none" : null, ...this.props.style}} onClick={() => TourConfig.setIsTourVisible(true)} >
<QuestionCircleOutlined style={{fontSize: "24px", color: "#4d4d4d"}} />
</div>

View File

@@ -45,3 +45,12 @@ code {
.ant-list-sm .ant-list-item {
padding: 2px !important;
}
.ant-drawer-body {
padding: 0 !important;
overflow: hidden !important;
}
.custom-link:hover {
color: rgb(64 64 64) !important;
}

View File

@@ -804,7 +804,9 @@
"Sub roles": "包含角色",
"Sub roles - Tooltip": "当前角色所包含的子角色",
"Sub users": "包含用户",
"Sub users - Tooltip": "当前角色所包含的用户"
"Sub users - Tooltip": "当前角色所包含的用户",
"Sub groups": "包含群组",
"Sub groups - Tooltip": "当前角色所包含的群组"
},
"signup": {
"Accept": "阅读并接受",
@@ -1030,4 +1032,4 @@
"New Webhook": "添加Webhook",
"Value": "值"
}
}
}

View File

@@ -14,7 +14,7 @@
import React from "react";
import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
import {Button, Col, Row, Select, Switch, Table, Tooltip} from "antd";
import {Button, Col, Input, Row, Select, Switch, Table, Tooltip} from "antd";
import * as Setting from "../Setting";
import i18next from "i18next";
import * as Provider from "../auth/Provider";
@@ -39,7 +39,7 @@ class ProviderTable extends React.Component {
}
addRow(table) {
const row = {name: Setting.getNewRowNameForTable(table, "Please select a provider"), canSignUp: true, canSignIn: true, canUnlink: true, alertType: "None", rule: "None"};
const row = {name: Setting.getNewRowNameForTable(table, "Please select a provider"), canSignUp: true, canSignIn: true, canUnlink: true, prompted: false, signupGroup: "", rule: "None"};
if (table === undefined) {
table = [];
}
@@ -172,6 +172,23 @@ class ProviderTable extends React.Component {
);
},
},
{
title: i18next.t("provider:Signup group"),
dataIndex: "signupGroup",
key: "signupGroup",
width: "120px",
render: (text, record, index) => {
if (!["OAuth", "Web3"].includes(record.provider?.category)) {
return null;
}
return (
<Input value={text} onChange={e => {
this.updateField(table, index, "signupGroup", e.target.value);
}} />
);
},
},
{
title: i18next.t("application:Rule"),
dataIndex: "rule",

View File

@@ -0,0 +1,162 @@
// Copyright 2023 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.
import React from "react";
import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
import {Button, Col, Input, Row, Select, Table, Tooltip} from "antd";
import * as Setting from "../Setting";
import i18next from "i18next";
const {Option} = Select;
class SamlAttributeTable extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
};
}
updateTable(table) {
this.props.onUpdateTable(table);
}
updateField(table, index, key, value) {
table[index][key] = value;
this.updateTable(table);
}
addRow(table) {
const row = {Name: "", nameformat: "", value: ""};
if (table === undefined || table === null) {
table = [];
}
table = Setting.addRow(table, row);
this.updateTable(table);
}
deleteRow(table, i) {
table = Setting.deleteRow(table, i);
this.updateTable(table);
}
upRow(table, i) {
table = Setting.swapRow(table, i - 1, i);
this.updateTable(table);
}
downRow(table, i) {
table = Setting.swapRow(table, i, i + 1);
this.updateTable(table);
}
renderTable(table) {
const columns = [
{
title: i18next.t("user:Name"),
dataIndex: "name",
key: "name",
width: "200px",
render: (text, record, index) => {
return (
<Input value={text} onChange={e => {
this.updateField(table, index, "name", e.target.value);
}} />
);
},
},
{
title: i18next.t("user:Name format"),
dataIndex: "nameformat",
key: "nameformat",
width: "200px",
render: (text, record, index) => {
return (
<Select virtual={false} style={{width: "100%"}}
value={text}
defaultValue="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified"
onChange={value => {
this.updateField(table, index, "nameformat", value);
}} >
<Option key="Unspecified" value="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">{i18next.t("general:Unspecified")}</Option>
<Option key="Basic" value="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">{i18next.t("application:Basic")}</Option>
<Option key="UriReference" value="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">{i18next.t("application:UriReference")}</Option>
<Option key="x500AttributeName" value="urn:oasis:names:tc:SAML:2.0:attrname-format:X500">{i18next.t("application:x500AttributeName")}</Option>
</Select>
);
},
},
{
title: i18next.t("user:Value"),
dataIndex: "value",
key: "value",
width: "200px",
render: (text, record, index) => {
return (
<Input value={text} onChange={e => {
this.updateField(table, index, "value", e.target.value);
}} />
);
},
},
{
title: i18next.t("general:Action"),
dataIndex: "action",
key: "action",
width: "20px",
render: (text, record, index) => {
return (
<div>
<Tooltip placement="bottomLeft" title={i18next.t("general:Up")}>
<Button style={{marginRight: "5px"}} disabled={index === 0} icon={<UpOutlined />} size="small" onClick={() => this.upRow(table, index)} />
</Tooltip>
<Tooltip placement="topLeft" title={i18next.t("general:Down")}>
<Button style={{marginRight: "5px"}} disabled={index === table.length - 1} icon={<DownOutlined />} size="small" onClick={() => this.downRow(table, index)} />
</Tooltip>
<Tooltip placement="topLeft" title={i18next.t("general:Delete")}>
<Button icon={<DeleteOutlined />} size="small" onClick={() => this.deleteRow(table, index)} />
</Tooltip>
</div>
);
},
},
];
return (
<Table title={() => (
<div>
<Button style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button>
</div>
)}
columns={columns} dataSource={table} rowKey="key" size="middle" bordered
/>
);
}
render() {
return (
<div>
<Row style={{marginTop: "20px"}} >
<Col span={24}>
{
this.renderTable(this.props.table)
}
</Col>
</Row>
</div>
);
}
}
export default SamlAttributeTable;

View File

@@ -14,10 +14,16 @@
import React from "react";
import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
import {Button, Col, Row, Select, Switch, Table, Tooltip} from "antd";
import {Button, Col, Input, Popover, Row, Select, Switch, Table, Tooltip} from "antd";
import * as Setting from "../Setting";
import i18next from "i18next";
import {Controlled as CodeMirror} from "react-codemirror2";
import "codemirror/lib/codemirror.css";
require("codemirror/theme/material-darker.css");
require("codemirror/mode/htmlmixed/htmlmixed");
const {Option} = Select;
class SignupTable extends React.Component {
@@ -81,6 +87,11 @@ class SignupTable extends React.Component {
{name: "Phone", displayName: i18next.t("general:Phone")},
{name: "Invitation code", displayName: i18next.t("application:Invitation code")},
{name: "Agreement", displayName: i18next.t("signup:Agreement")},
{name: "Text 1", displayName: i18next.t("signup:Text 1")},
{name: "Text 2", displayName: i18next.t("signup:Text 2")},
{name: "Text 3", displayName: i18next.t("signup:Text 3")},
{name: "Text 4", displayName: i18next.t("signup:Text 4")},
{name: "Text 5", displayName: i18next.t("signup:Text 5")},
];
const getItemDisplayName = (text) => {
@@ -164,6 +175,55 @@ class SignupTable extends React.Component {
);
},
},
{
title: i18next.t("signup:Label"),
dataIndex: "label",
key: "label",
width: "200px",
render: (text, record, index) => {
if (record.name.startsWith("Text ")) {
return (
<Popover placement="right" content={
<div style={{width: "900px", height: "300px"}} >
<CodeMirror value={text}
options={{mode: "htmlmixed", theme: "material-darker"}}
onBeforeChange={(editor, data, value) => {
this.updateField(table, index, "label", value);
}}
/>
</div>
} title={i18next.t("signup:Label HTML")} trigger="click">
<Input value={text} style={{marginBottom: "10px"}} onChange={e => {
this.updateField(table, index, "label", e.target.value);
}} />
</Popover>
);
}
return (
<Input value={text} onChange={e => {
this.updateField(table, index, "label", e.target.value);
}} />
);
},
},
{
title: i18next.t("signup:Placeholder"),
dataIndex: "placeholder",
key: "placeholder",
width: "200px",
render: (text, record, index) => {
if (record.name.startsWith("Text ")) {
return null;
}
return (
<Input value={text} onChange={e => {
this.updateField(table, index, "placeholder", e.target.value);
}} />
);
},
},
{
title: i18next.t("application:Rule"),
dataIndex: "rule",