Compare commits

...

29 Commits

Author SHA1 Message Date
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
haiwu
3d80ec721f fix: use user.UpdatedTime as scim.Meta.Version instead of user.Id (#2406)
* 111

* fix: use user.UpdatedTime as scim.Meta.Version instead of user.Id

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-10-14 11:03:58 +08:00
Yang Luo
43d849086f Fix 127.0.0.1 bug in isHostIntranet() 2023-10-13 23:29:37 +08:00
Yang Luo
69b144d80f feat: change back to running RecordMessage() filter before API handling, because the logged-out user info is missing after session is cleared. Revert: https://github.com/casdoor/casdoor/pull/2369 2023-10-13 16:53:30 +08:00
Yang Luo
52a66ef044 Fix webhook not triggered issue in SendWebhooks() 2023-10-13 16:47:09 +08:00
Yang Luo
ec0a8e16f7 feat: fix CheckLoginPermission() logic 2023-10-13 15:41:23 +08:00
Yang Luo
80a8000057 Add GetModelEx() 2023-10-13 13:45:13 +08:00
Yang Luo
77091a3ae5 Fix null model issue in UpdatePermission() 2023-10-13 12:55:11 +08:00
Pedro Padron
983da685a2 feat: support calling get-user API by only email, phone or userId without owner (#2398) 2023-10-13 02:48:55 +08:00
UsherFall
3d567c3d45 feat: update go-sms-sender to fix Twilio template error (#2395) 2023-10-12 01:53:31 +08:00
haiwu
440d87d70c feat: support SCIM protocol (#2393)
* 111

* feat: support scim/Users GET and POST request

* feat: support scim/Users DELETE/PATCH/PUT request

* feat: better support scim/Users PATCH request

* feat: fix scim/Users logic

* feat: gofumpt

* feat: fix bug in scim/Users

* feat: fix typo

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-10-12 00:13:16 +08:00
Yaodong Yu
e4208d7fd9 feat: restrict the model of application type resource permission (#2394) 2023-10-12 00:05:53 +08:00
Yang Luo
4de716fef3 Improve UploadResource() 2023-10-11 01:27:29 +08:00
Yang Luo
070aa8a65f Show 404 error for index.html not found 2023-10-10 22:57:39 +08:00
48 changed files with 1407 additions and 146 deletions

View File

@@ -81,6 +81,7 @@ p, *, *, GET, /api/get-saml-login, *, *
p, *, *, POST, /api/acs, *, *
p, *, *, GET, /api/saml/metadata, *, *
p, *, *, *, /cas, *, *
p, *, *, *, /scim, *, *
p, *, *, *, /api/webauthn, *, *
p, *, *, GET, /api/get-release, *, *
p, *, *, GET, /api/get-default-application, *, *

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

@@ -37,6 +37,11 @@ func (c *ApiController) Enforce() {
resourceId := c.Input().Get("resourceId")
enforcerId := c.Input().Get("enforcerId")
if len(c.Ctx.Input.RequestBody) == 0 {
c.ResponseError("The request body should not be empty")
return
}
var request object.CasbinRequest
err := json.Unmarshal(c.Ctx.Input.RequestBody, &request)
if err != nil {

View File

@@ -272,6 +272,11 @@ func (c *ApiController) UploadResource() {
return
}
if username == "Built-in-Untracked" {
c.ResponseOk(fileUrl, objectKey)
return
}
if createdTime == "" {
createdTime = util.GetCurrentTime()
}

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

27
controllers/scim.go Normal file
View File

@@ -0,0 +1,27 @@
// 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.
package controllers
import (
"strings"
"github.com/casdoor/casdoor/scim"
)
func (c *RootController) HandleScim() {
path := c.Ctx.Request.URL.Path
c.Ctx.Request.URL.Path = strings.TrimPrefix(path, "/scim")
scim.Server.ServeHTTP(c.Ctx.ResponseWriter, c.Ctx.Request)
}

View File

@@ -160,35 +160,47 @@ func (c *ApiController) GetUser() {
id = util.GetId(userFromUserId.Owner, userFromUserId.Name)
}
if owner == "" {
owner = util.GetOwnerFromId(id)
}
var user *object.User
organization, err := object.GetOrganization(util.GetId("admin", owner))
if err != nil {
c.ResponseError(err.Error())
return
}
if id == "" && owner == "" {
switch {
case email != "":
user, err = object.GetUserByEmailOnly(email)
case phone != "":
user, err = object.GetUserByPhoneOnly(phone)
case userId != "":
user, err = object.GetUserByUserIdOnly(userId)
}
} else {
if owner == "" {
owner = util.GetOwnerFromId(id)
}
if !organization.IsProfilePublic {
requestUserId := c.GetSessionUsername()
hasPermission, err := object.CheckUserPermission(requestUserId, id, false, c.GetAcceptLanguage())
if !hasPermission {
organization, err := object.GetOrganization(util.GetId("admin", owner))
if err != nil {
c.ResponseError(err.Error())
return
}
}
var user *object.User
switch {
case email != "":
user, err = object.GetUserByEmail(owner, email)
case phone != "":
user, err = object.GetUserByPhone(owner, phone)
case userId != "":
user = userFromUserId
default:
user, err = object.GetUser(id)
if !organization.IsProfilePublic {
requestUserId := c.GetSessionUsername()
hasPermission, err := object.CheckUserPermission(requestUserId, id, false, c.GetAcceptLanguage())
if !hasPermission {
c.ResponseError(err.Error())
return
}
}
switch {
case email != "":
user, err = object.GetUserByEmail(owner, email)
case phone != "":
user, err = object.GetUserByPhone(owner, phone)
case userId != "":
user = userFromUserId
default:
user, err = object.GetUser(id)
}
}
if err != nil {
@@ -466,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))

6
go.mod
View File

@@ -11,7 +11,7 @@ require (
github.com/beevik/etree v1.1.0
github.com/casbin/casbin v1.9.1 // indirect
github.com/casbin/casbin/v2 v2.77.2
github.com/casdoor/go-sms-sender v0.14.0
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/oss v1.3.0
@@ -20,6 +20,7 @@ require (
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
github.com/denisenkom/go-mssqldb v0.9.0
github.com/elazarl/go-bindata-assetfs v1.0.1 // indirect
github.com/elimity-com/scim v0.0.0-20230426070224-941a5eac92f3
github.com/fogleman/gg v1.3.0
github.com/forestmgy/ldapserver v1.1.0
github.com/go-git/go-git/v5 v5.6.0
@@ -63,10 +64,11 @@ require (
golang.org/x/crypto v0.12.0
golang.org/x/net v0.14.0
golang.org/x/oauth2 v0.11.0
golang.org/x/text v0.13.0 // indirect
google.golang.org/api v0.138.0
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0
layeh.com/radius v0.0.0-20221205141417-e7fbddd11d68 // indirect
layeh.com/radius v0.0.0-20221205141417-e7fbddd11d68
maunium.net/go/mautrix v0.16.0
modernc.org/sqlite v1.18.2
)

17
go.sum
View File

@@ -917,12 +917,11 @@ github.com/casbin/casbin v1.9.1 h1:ucjbS5zTrmSLtH4XogqOG920Poe6QatdXtz1FEbApeM=
github.com/casbin/casbin v1.9.1/go.mod h1:z8uPsfBJGUsnkagrt3G8QvjgTKFMBJ32UP8HpZllfog=
github.com/casbin/casbin/v2 v2.1.0/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/casbin/casbin/v2 v2.28.3/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
github.com/casbin/casbin/v2 v2.37.0 h1:/poEwPSovi4bTOcP752/CsTQiRz2xycyVKFG7GUhbDw=
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-sms-sender v0.14.0 h1:yqrzWIHUg64OYPynzF5Fr0XDuCWIWxtXIjOQAAkRKuw=
github.com/casdoor/go-sms-sender v0.14.0/go.mod h1:cQs7qqohMJBgIVZebOCB8ko09naG1vzFJEH59VNIscs=
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=
@@ -1011,6 +1010,10 @@ github.com/dghubble/sling v1.4.0/go.mod h1:0r40aNsU9EdDUVBNhfCstAtFgutjgJGYbO1oN
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/di-wu/parser v0.2.2 h1:I9oHJ8spBXOeL7Wps0ffkFFFiXJf/pk7NX9lcAMqRMU=
github.com/di-wu/parser v0.2.2/go.mod h1:SLp58pW6WamdmznrVRrw2NTyn4wAvT9rrEFynKX7nYo=
github.com/di-wu/xsd-datetime v1.0.0 h1:vZoGNkbzpBNoc+JyfVLEbutNDNydYV8XwHeV7eUJoxI=
github.com/di-wu/xsd-datetime v1.0.0/go.mod h1:i3iEhrP3WchwseOBeIdW/zxeoleXTOzx1WyDXgdmOww=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/drswork/go-twitter v0.0.0-20221107160839-dea1b6ed53d7 h1:uh1GSejOhVPRQmoXZxY82TiewZB8QXiaP1skL7Nun3Y=
github.com/drswork/go-twitter v0.0.0-20221107160839-dea1b6ed53d7/go.mod h1:ncTaGuXc5v7AuiVekeJ0Nwh8Bf4cudukoj0qM/15UZE=
@@ -1027,6 +1030,8 @@ github.com/elastic/go-elasticsearch/v6 v6.8.5/go.mod h1:UwaDJsD3rWLM5rKNFzv9hgox
github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
github.com/elimity-com/scim v0.0.0-20230426070224-941a5eac92f3 h1:+zrUtdBUJpY9qptMaaY3CA3T/lBI2+QqfUbzM2uxJss=
github.com/elimity-com/scim v0.0.0-20230426070224-941a5eac92f3/go.mod h1:JkjcmqbLW+khwt2fmBPJFBhx2zGZ8XobRZ+O0VhlwWo=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -1694,6 +1699,8 @@ github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZ
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/scim2/filter-parser/v2 v2.2.0 h1:QGadEcsmypxg8gYChRSM2j1edLyE/2j72j+hdmI4BJM=
github.com/scim2/filter-parser/v2 v2.2.0/go.mod h1:jWnkDToqX/Y0ugz0P5VvpVEUKcWcyHHj+X+je9ce5JA=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
github.com/sendgrid/sendgrid-go v3.13.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
@@ -1795,7 +1802,6 @@ github.com/tidwall/gjson v1.16.0 h1:SyXa+dsSPpUlcwEDuKuEBJEz5vzTvOea+9rjyYodQFg=
github.com/tidwall/gjson v1.16.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
@@ -2301,8 +2307,9 @@ golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

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

@@ -59,7 +59,7 @@ func main() {
beego.InsertFilter("*", beego.BeforeRouter, routers.CorsFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.ApiFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.PrometheusFilter)
beego.InsertFilter("*", beego.AfterExec, routers.RecordMessage, false)
beego.InsertFilter("*", beego.BeforeRouter, routers.RecordMessage)
beego.BConfig.WebConfig.Session.SessionOn = true
beego.BConfig.WebConfig.Session.SessionName = "casdoor_session_id"

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 {
@@ -54,12 +62,13 @@ type Application struct {
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 +315,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

@@ -361,6 +361,8 @@ func CheckLoginPermission(userId string, application *Application) (bool, error)
return false, err
}
allowPermissionCount := 0
denyPermissionCount := 0
allowCount := 0
denyCount := 0
for _, permission := range permissions {
@@ -368,8 +370,13 @@ func CheckLoginPermission(userId string, application *Application) (bool, error)
continue
}
if permission.isUserHit(userId) {
allowCount += 1
if !permission.isUserHit(userId) && !permission.isRoleHit(userId) {
if permission.Effect == "Allow" {
allowPermissionCount += 1
} else {
denyPermissionCount += 1
}
continue
}
enforcer := getPermissionEnforcer(permission)
@@ -391,8 +398,18 @@ func CheckLoginPermission(userId string, application *Application) (bool, error)
}
}
// Deny-override, if one deny is found, then deny
if denyCount > 0 {
return false, nil
} else if allowCount > 0 {
return true, nil
}
// For no-allow and no-deny condition
// If only allow permissions exist, we suppose it's Deny-by-default, aka no-allow means deny
// Otherwise, it's Allow-by-default, aka no-deny means allow
if allowPermissionCount > 0 && denyPermissionCount == 0 {
return false, nil
}
return true, nil
}

View File

@@ -18,7 +18,6 @@ import (
"fmt"
"github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v2/config"
"github.com/casdoor/casdoor/util"
xormadapter "github.com/casdoor/xorm-adapter/v3"
"github.com/xorm-io/core"
@@ -247,23 +246,17 @@ func (enforcer *Enforcer) LoadModelCfg() error {
return nil
}
model, err := GetModel(enforcer.Model)
model, err := GetModelEx(enforcer.Model)
if err != nil {
return err
} else if model == nil {
return fmt.Errorf("the model: %s for enforcer: %s is not found", enforcer.Model, enforcer.GetId())
}
cfg, err := config.NewConfigFromText(model.ModelText)
enforcer.ModelCfg, err = getModelCfg(model)
if err != nil {
return err
}
enforcer.ModelCfg = make(map[string]string)
enforcer.ModelCfg["p"] = cfg.String("policy_definition::p")
if cfg.String("role_definition::g") != "" {
enforcer.ModelCfg["g"] = cfg.String("role_definition::g")
}
return nil
}

View File

@@ -17,6 +17,7 @@ package object
import (
"fmt"
"github.com/casbin/casbin/v2/config"
"github.com/casbin/casbin/v2/model"
"github.com/casdoor/casdoor/util"
"github.com/xorm-io/core"
@@ -83,6 +84,19 @@ func GetModel(id string) (*Model, error) {
return getModel(owner, name)
}
func GetModelEx(id string) (*Model, error) {
owner, name := util.GetOwnerAndNameFromId(id)
model, err := getModel(owner, name)
if err != nil {
return nil, err
}
if model != nil {
return model, nil
}
return getModel("built-in", name)
}
func UpdateModelWithCheck(id string, modelObj *Model) error {
// check model grammar
_, err := model.NewModelFromString(modelObj.ModelText)
@@ -188,3 +202,17 @@ func (m *Model) initModel() error {
return nil
}
func getModelCfg(m *Model) (map[string]string, error) {
cfg, err := config.NewConfigFromText(m.ModelText)
if err != nil {
return nil, err
}
modelCfg := make(map[string]string)
modelCfg["p"] = cfg.String("policy_definition::p")
if cfg.String("role_definition::g") != "" {
modelCfg["g"] = cfg.String("role_definition::g")
}
return modelCfg, nil
}

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

@@ -150,6 +150,24 @@ func UpdatePermission(id string, permission *Permission) (bool, error) {
return false, nil
}
if permission.ResourceType == "Application" {
model, err := GetModelEx(util.GetId(owner, permission.Model))
if err != nil {
return false, err
} else if model == nil {
return false, fmt.Errorf("the model: %s for permission: %s is not found", permission.Model, permission.GetId())
}
modelCfg, err := getModelCfg(model)
if err != nil {
return false, err
}
if len(strings.Split(modelCfg["p"], ",")) != 3 {
return false, fmt.Errorf("the model: %s for permission: %s is not valid, Casbin model's [policy_defination] section should have 3 elements", permission.Model, permission.GetId())
}
}
affected, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(permission)
if err != nil {
return false, err
@@ -406,19 +424,34 @@ func (p *Permission) GetId() string {
}
func (p *Permission) isUserHit(name string) bool {
targetOrg, _ := util.GetOwnerAndNameFromId(name)
targetOrg, targetName := util.GetOwnerAndNameFromId(name)
for _, user := range p.Users {
userOrg, userName := util.GetOwnerAndNameFromId(user)
if userOrg == targetOrg && userName == "*" {
if userOrg == targetOrg && (userName == "*" || userName == targetName) {
return true
}
}
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 name == resource {
if resource == "*" || resource == name {
return true
}
}

View File

@@ -93,12 +93,8 @@ func AddRecord(record *casvisorsdk.Record) bool {
return affected
}
func SendWebhooks(record *casvisorsdk.Record) error {
webhooks, err := getWebhooksByOrganization(record.Organization)
if err != nil {
return err
}
func getFilteredWebhooks(webhooks []*Webhook, action string) []*Webhook {
res := []*Webhook{}
for _, webhook := range webhooks {
if !webhook.IsEnabled {
continue
@@ -106,28 +102,56 @@ func SendWebhooks(record *casvisorsdk.Record) error {
matched := false
for _, event := range webhook.Events {
if record.Action == event {
if action == event {
matched = true
break
}
}
if matched {
var user *User
if webhook.IsUserExtended {
user, err = getUser(record.Organization, record.User)
user, err = GetMaskedUser(user, false, err)
if err != nil {
return err
}
res = append(res, webhook)
}
}
return res
}
func SendWebhooks(record *casvisorsdk.Record) error {
webhooks, err := getWebhooksByOrganization(record.Organization)
if err != nil {
return err
}
errs := []error{}
webhooks = getFilteredWebhooks(webhooks, record.Action)
for _, webhook := range webhooks {
var user *User
if webhook.IsUserExtended {
user, err = getUser(record.Organization, record.User)
if err != nil {
errs = append(errs, err)
continue
}
err = sendWebhook(webhook, record, user)
user, err = GetMaskedUser(user, false, err)
if err != nil {
return err
errs = append(errs, err)
continue
}
}
err = sendWebhook(webhook, record, user)
if err != nil {
errs = append(errs, err)
continue
}
}
if len(errs) > 0 {
errStrings := []string{}
for _, err := range errs {
errStrings = append(errStrings, err.Error())
}
return fmt.Errorf(strings.Join(errStrings, " | "))
}
return nil
}

View File

@@ -254,14 +254,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.Where("role.users like ?", fmt.Sprintf("%%%s%%", userId))
for _, group := range user.Groups {
query = query.Or("role.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,7 +317,7 @@ 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,

View File

@@ -50,6 +50,7 @@ type User struct {
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
Id string `xorm:"varchar(100) index" json:"id"`
ExternalId string `xorm:"varchar(100) index" json:"externalId"`
Type string `xorm:"varchar(100)" json:"type"`
Password string `xorm:"varchar(100)" json:"password"`
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
@@ -371,6 +372,24 @@ func GetUserByEmail(owner string, email string) (*User, error) {
}
}
func GetUserByEmailOnly(email string) (*User, error) {
if email == "" {
return nil, nil
}
user := User{Email: email}
existed, err := ormer.Engine.Get(&user)
if err != nil {
return nil, err
}
if existed {
return &user, nil
} else {
return nil, nil
}
}
func GetUserByPhone(owner string, phone string) (*User, error) {
if owner == "" || phone == "" {
return nil, nil
@@ -389,6 +408,24 @@ func GetUserByPhone(owner string, phone string) (*User, error) {
}
}
func GetUserByPhoneOnly(phone string) (*User, error) {
if phone == "" {
return nil, nil
}
user := User{Phone: phone}
existed, err := ormer.Engine.Get(&user)
if err != nil {
return nil, err
}
if existed {
return &user, nil
} else {
return nil, nil
}
}
func GetUserByUserId(owner string, userId string) (*User, error) {
if owner == "" || userId == "" {
return nil, nil
@@ -407,6 +444,24 @@ func GetUserByUserId(owner string, userId string) (*User, error) {
}
}
func GetUserByUserIdOnly(userId string) (*User, error) {
if userId == "" {
return nil, nil
}
user := User{Id: userId}
existed, err := ormer.Engine.Get(&user)
if err != nil {
return nil, err
}
if existed {
return &user, nil
} else {
return nil, nil
}
}
func GetUserByAccessKey(accessKey string) (*User, error) {
if accessKey == "" {
return nil, nil
@@ -529,7 +584,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
if len(columns) == 0 {
columns = []string{
"owner", "display_name", "avatar",
"owner", "display_name", "avatar", "first_name", "last_name",
"location", "address", "country_code", "region", "language", "affiliation", "title", "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",
"signin_wrong_times", "last_signin_wrong_time", "groups", "access_key", "access_secret",
@@ -546,6 +601,9 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
columns = append(columns, "name", "email", "phone", "country_code", "type")
}
columns = append(columns, "updated_time")
user.UpdatedTime = util.GetCurrentTime()
if util.ContainsString(columns, "groups") {
_, err := userEnforcer.UpdateGroupsForUser(user.GetId(), user.Groups)
if err != nil {
@@ -638,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 == "" {
@@ -139,6 +139,10 @@ func getUrlPath(urlPath string) string {
return "/cas"
}
if strings.HasPrefix(urlPath, "/scim") {
return "/scim"
}
if strings.HasPrefix(urlPath, "/api/login/oauth") {
return "/api/login/oauth"
}

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 {
@@ -190,5 +193,5 @@ func isHostIntranet(s string) bool {
return false
}
return ip.IsPrivate()
return ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast()
}

View File

@@ -277,4 +277,6 @@ func initAPI() {
beego.Router("/cas/:organization/:application/p3/serviceValidate", &controllers.RootController{}, "GET:CasP3ServiceValidate")
beego.Router("/cas/:organization/:application/p3/proxyValidate", &controllers.RootController{}, "GET:CasP3ProxyValidate")
beego.Router("/cas/:organization/:application/samlValidate", &controllers.RootController{}, "POST:SamlValidate")
beego.Router("/scim/*", &controllers.RootController{}, "*:HandleScim")
}

View File

@@ -59,6 +59,9 @@ func StaticFilter(ctx *context.Context) {
if strings.HasPrefix(urlPath, "/cas") && (strings.HasSuffix(urlPath, "/serviceValidate") || strings.HasSuffix(urlPath, "/proxy") || strings.HasSuffix(urlPath, "/proxyValidate") || strings.HasSuffix(urlPath, "/validate") || strings.HasSuffix(urlPath, "/p3/serviceValidate") || strings.HasSuffix(urlPath, "/p3/proxyValidate") || strings.HasSuffix(urlPath, "/samlValidate")) {
return
}
if strings.HasPrefix(urlPath, "/scim") {
return
}
webBuildFolder := getWebBuildFolder()
path := webBuildFolder
@@ -77,6 +80,7 @@ func StaticFilter(ctx *context.Context) {
panic(err)
}
dir = strings.ReplaceAll(dir, "\\", "/")
ctx.ResponseWriter.WriteHeader(http.StatusNotFound)
errorText := fmt.Sprintf("The Casdoor frontend HTML file: \"index.html\" was not found, it should be placed at: \"%s/web/build/index.html\". For more information, see: https://casdoor.org/docs/basic/server-installation/#frontend-1", dir)
http.ServeContent(ctx.ResponseWriter, ctx.Request, "Casdoor frontend has encountered error...", time.Now(), strings.NewReader(errorText))
return

154
scim/server.go Normal file
View File

@@ -0,0 +1,154 @@
// 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.
package scim
import (
"github.com/elimity-com/scim"
"github.com/elimity-com/scim/optional"
"github.com/elimity-com/scim/schema"
)
/*
Example JSON user resource
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
],
"addresses": [
{
"country": "US",
"locality": "San Fransisco",
"region": "US West"
}
],
"displayName": "Hello, Scim",
"name": {
"familyName": "Bob",
"givenName": "Alice"
},
"phoneNumbers": [
{
"value": "46407568879"
}
],
"photos": [
{
"value": "https://cdn.casbin.org/img/casbin.svg"
}
],
"emails": [
{
"value": "cbvdho@example.com"
}
],
"profileUrl": "https://door.casdoor.com/users/build-in/scim_test_user2",
"userName": "scim_test_user2",
"userType": "normal-user",
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
"organization": "built-in"
}
}
*/
const (
UserExtensionKey = "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
)
var (
UserStringField = []schema.SimpleParams{
newStringParams("externalId", false, true),
newStringParams("userName", true, true),
newStringParams("password", false, false),
newStringParams("displayName", false, false),
newStringParams("profileUrl", false, false),
newStringParams("userType", false, false),
}
UserComplexField = []schema.ComplexParams{
newComplexParams("name", false, false, []schema.SimpleParams{
newStringParams("givenName", false, false),
newStringParams("familyName", false, false),
}),
newComplexParams("emails", false, true, []schema.SimpleParams{
newStringParams("value", true, false),
}),
newComplexParams("phoneNumbers", false, true, []schema.SimpleParams{
newStringParams("value", true, false),
}),
newComplexParams("photos", false, true, []schema.SimpleParams{
newStringParams("value", true, false),
}),
newComplexParams("addresses", false, true, []schema.SimpleParams{
newStringParams("locality", false, false),
newStringParams("region", false, false),
newStringParams("country", false, false),
}),
}
Server = GetScimServer()
)
func GetScimServer() scim.Server {
config := scim.ServiceProviderConfig{
// DocumentationURI: optional.NewString("www.example.com/scim"),
SupportPatch: true,
}
codeAttrs := make([]schema.CoreAttribute, 0, len(UserStringField)+len(UserComplexField))
for _, field := range UserStringField {
codeAttrs = append(codeAttrs, schema.SimpleCoreAttribute(field))
}
for _, field := range UserComplexField {
codeAttrs = append(codeAttrs, schema.ComplexCoreAttribute(field))
}
userSchema := schema.Schema{
ID: schema.UserSchema,
Name: optional.NewString("User"),
Description: optional.NewString("User Account"),
Attributes: codeAttrs,
}
extension := schema.Schema{
ID: UserExtensionKey,
Name: optional.NewString("EnterpriseUser"),
Description: optional.NewString("Enterprise User"),
Attributes: []schema.CoreAttribute{
schema.SimpleCoreAttribute(schema.SimpleStringParams(schema.StringParams{
Name: "organization",
Required: true,
})),
},
}
resourceTypes := []scim.ResourceType{
{
ID: optional.NewString("User"),
Name: "User",
Endpoint: "/Users",
Description: optional.NewString("User Account in Casdoor"),
Schema: userSchema,
SchemaExtensions: []scim.SchemaExtension{
{Schema: extension},
},
Handler: UserResourceHandler{},
},
}
server := scim.Server{
Config: config,
ResourceTypes: resourceTypes,
}
return server
}

260
scim/user_handler.go Normal file
View File

@@ -0,0 +1,260 @@
// 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.
package scim
import (
"fmt"
"net/http"
"github.com/casdoor/casdoor/object"
"github.com/elimity-com/scim"
"github.com/elimity-com/scim/errors"
)
type UserResourceHandler struct{}
// https://github.com/elimity-com/scim/blob/master/resource_handler_test.go Example in-memory resource handler
// https://datatracker.ietf.org/doc/html/rfc7644#section-3.4 How to query/update resources
func (h UserResourceHandler) Create(r *http.Request, attrs scim.ResourceAttributes) (scim.Resource, error) {
resource := &scim.Resource{Attributes: attrs}
err := AddScimUser(resource)
return *resource, err
}
func (h UserResourceHandler) Get(r *http.Request, id string) (scim.Resource, error) {
resource, err := GetScimUser(id)
if err != nil {
return scim.Resource{}, err
}
if resource == nil {
return scim.Resource{}, errors.ScimErrorResourceNotFound(id)
}
return *resource, nil
}
func (h UserResourceHandler) Delete(r *http.Request, id string) error {
user, err := object.GetUserByUserIdOnly(id)
if err != nil {
return err
}
if user == nil {
return errors.ScimErrorResourceNotFound(id)
}
_, err = object.DeleteUser(user)
return err
}
func (h UserResourceHandler) GetAll(r *http.Request, params scim.ListRequestParams) (scim.Page, error) {
if params.Count == 0 {
count, err := object.GetGlobalUserCount("", "")
if err != nil {
return scim.Page{}, err
}
return scim.Page{TotalResults: int(count)}, nil
}
resources := make([]scim.Resource, 0)
// startIndex is 1-based index
users, err := object.GetPaginationGlobalUsers(params.StartIndex-1, params.Count, "", "", "", "")
if err != nil {
return scim.Page{}, err
}
for _, user := range users {
resources = append(resources, *user2resource(user))
}
return scim.Page{
TotalResults: len(resources),
Resources: resources,
}, nil
}
func (h UserResourceHandler) Patch(r *http.Request, id string, operations []scim.PatchOperation) (scim.Resource, error) {
user, err := object.GetUserByUserIdOnly(id)
if err != nil {
return scim.Resource{}, err
}
if user == nil {
return scim.Resource{}, errors.ScimErrorResourceNotFound(id)
}
return UpdateScimUserByPatchOperation(id, operations)
}
func (h UserResourceHandler) Replace(r *http.Request, id string, attrs scim.ResourceAttributes) (scim.Resource, error) {
user, err := object.GetUserByUserIdOnly(id)
if err != nil {
return scim.Resource{}, err
}
if user == nil {
return scim.Resource{}, errors.ScimErrorResourceNotFound(id)
}
resource := &scim.Resource{Attributes: attrs}
err = UpdateScimUser(id, resource)
return *resource, err
}
func GetScimUser(id string) (*scim.Resource, error) {
user, err := object.GetUserByUserIdOnly(id)
if err != nil {
return nil, err
}
if user == nil {
return nil, nil
}
r := user2resource(user)
return r, nil
}
func AddScimUser(r *scim.Resource) error {
newUser, err := resource2user(r.Attributes)
if err != nil {
return err
}
// Check whether the user exists.
oldUser, err := object.GetUser(newUser.GetId())
if err != nil {
return err
}
if oldUser != nil {
return errors.ScimErrorUniqueness
}
affect, err := object.AddUser(newUser)
if err != nil {
return err
}
if !affect {
return fmt.Errorf("add new user failed")
}
r.Attributes = user2resource(newUser).Attributes
r.ID = newUser.Id
r.ExternalID = buildExternalId(newUser)
r.Meta = buildMeta(newUser)
return nil
}
func UpdateScimUser(id string, r *scim.Resource) error {
oldUser, err := object.GetUserByUserIdOnly(id)
if err != nil {
return err
}
if oldUser == nil {
return errors.ScimErrorResourceNotFound(id)
}
newUser, err := resource2user(r.Attributes)
if err != nil {
return err
}
_, err = object.UpdateUser(oldUser.GetId(), newUser, nil, true)
if err != nil {
return err
}
r.ID = newUser.Id
r.ExternalID = buildExternalId(newUser)
r.Meta = buildMeta(newUser)
return nil
}
// https://datatracker.ietf.org/doc/html/rfc7644#section-3.5.2 Modifying with PATCH
func UpdateScimUserByPatchOperation(id string, ops []scim.PatchOperation) (r scim.Resource, err error) {
user, err := object.GetUserByUserIdOnly(id)
if err != nil {
return scim.Resource{}, err
}
if user == nil {
return scim.Resource{}, errors.ScimErrorResourceNotFound(id)
}
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("invalid patch op value: %v", r)
}
}()
old := user.GetId()
for _, op := range ops {
value := op.Value
if op.Op == scim.PatchOperationRemove {
value = nil
}
// PatchOperationAdd and PatchOperationReplace is same in Casdoor, just replace the value
switch op.Path.String() {
case "userName":
user.Name = ToString(value, "")
case "password":
user.Password = ToString(value, "")
case "externalId":
user.ExternalId = ToString(value, "")
case "displayName":
user.DisplayName = ToString(value, "")
case "profileUrl":
user.Homepage = ToString(value, "")
case "userType":
user.Type = ToString(value, "")
case "name.givenName":
user.FirstName = ToString(value, "")
case "name.familyName":
user.LastName = ToString(value, "")
case "name":
defaultV := AnyMap{"givenName": "", "familyName": ""}
v := ToAnyMap(value, defaultV) // e.g. {"givenName": "AA", "familyName": "BB"}
user.FirstName = ToString(v["givenName"], user.FirstName)
user.LastName = ToString(v["familyName"], user.LastName)
case "emails":
defaultV := AnyArray{AnyMap{"value": ""}}
vs := ToAnyArray(value, defaultV) // e.g. [{"value": "test@casdoor"}]
if len(vs) > 0 {
v := ToAnyMap(vs[0])
user.Email = ToString(v["value"], user.Email)
}
case "phoneNumbers":
defaultV := AnyArray{AnyMap{"value": ""}}
vs := ToAnyArray(value, defaultV) // e.g. [{"value": "18750004417"}]
if len(vs) > 0 {
v := ToAnyMap(vs[0])
user.Phone = ToString(v["value"], user.Phone)
}
case "photos":
defaultV := AnyArray{AnyMap{"value": ""}}
vs := ToAnyArray(value, defaultV) // e.g. [{"value": "https://cdn.casbin.org/img/casbin.svg"}]
if len(vs) > 0 {
v := ToAnyMap(vs[0])
user.Avatar = ToString(v["value"], user.Avatar)
}
case "addresses":
defaultV := AnyArray{AnyMap{"locality": "", "region": "", "country": ""}}
vs := ToAnyArray(value, defaultV) // e.g. [{"locality": "Hollywood", "region": "CN", "country": "USA"}]
if len(vs) > 0 {
v := ToAnyMap(vs[0])
user.Location = ToString(v["locality"], user.Location)
user.Region = ToString(v["region"], user.Region)
user.CountryCode = ToString(v["country"], user.CountryCode)
}
case UserExtensionKey:
defaultV := AnyMap{"organization": user.Owner}
v := ToAnyMap(value, defaultV) // e.g. {"organization": "org1"}
user.Owner = ToString(v["organization"], user.Owner)
case fmt.Sprintf("%v.%v", UserExtensionKey, "organization"):
user.Owner = ToString(value, user.Owner)
}
}
_, err = object.UpdateUser(old, user, nil, true)
if err != nil {
return scim.Resource{}, err
}
r = *user2resource(user)
return r, nil
}

238
scim/util.go Normal file
View File

@@ -0,0 +1,238 @@
// 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.
package scim
import (
"fmt"
"log"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
"github.com/elimity-com/scim"
"github.com/elimity-com/scim/optional"
"github.com/elimity-com/scim/schema"
)
type AnyMap map[string]interface{}
type AnyArray []interface{}
func ToString(v interface{}, defaultV ...interface{}) string {
if v == nil {
if len(defaultV) > 0 {
v = defaultV[0]
}
}
return v.(string)
}
func ToAnyMap(v interface{}, defaultV ...interface{}) AnyMap {
if v == nil {
if len(defaultV) > 0 {
v = defaultV[0]
}
}
m, ok := v.(map[string]interface{})
if !ok {
m = v.(AnyMap)
}
return m
}
func ToAnyArray(v interface{}, defaultV ...interface{}) AnyArray {
if v == nil {
if len(defaultV) > 0 {
v = defaultV[0]
}
}
m, ok := v.([]interface{})
if !ok {
m = v.(AnyArray)
}
return m
}
func newStringParams(name string, required, unique bool) schema.SimpleParams {
uniqueness := schema.AttributeUniquenessNone()
if unique {
uniqueness = schema.AttributeUniquenessServer()
}
return schema.SimpleStringParams(schema.StringParams{
Name: name,
Required: required,
Uniqueness: uniqueness,
})
}
func newComplexParams(name string, required bool, multi bool, subAttributes []schema.SimpleParams) schema.ComplexParams {
return schema.ComplexParams{
Name: name,
Required: required,
MultiValued: multi,
SubAttributes: subAttributes,
}
}
func buildExternalId(user *object.User) optional.String {
if user.ExternalId != "" {
return optional.NewString(user.ExternalId)
} else {
return optional.String{}
}
}
func buildMeta(user *object.User) scim.Meta {
createdTime := util.String2Time(user.CreatedTime)
updatedTime := util.String2Time(user.UpdatedTime)
if user.UpdatedTime == "" {
updatedTime = createdTime
}
return scim.Meta{
Created: &createdTime,
LastModified: &updatedTime,
Version: util.Time2String(updatedTime),
}
}
func getAttrString(attrs scim.ResourceAttributes, key string) string {
if attrs[key] == nil {
return ""
} else {
return attrs[key].(string)
}
}
func getAttrJson(attrs scim.ResourceAttributes, key string) scim.ResourceAttributes {
if attrs[key] == nil {
return nil
} else {
if v, ok := attrs[key].(map[string]interface{}); ok {
return v
} else if v, ok := attrs[key].([]interface{}); ok {
if len(v) > 0 {
return v[0].(map[string]interface{})
} else {
return nil
}
} else {
panic("invalid attribute type")
}
}
}
func getAttrJsonValue(attrs scim.ResourceAttributes, key1 string, key2 string) string {
attr := getAttrJson(attrs, key1)
if attr == nil {
return ""
} else {
return getAttrString(attr, key2)
}
}
func user2resource(user *object.User) *scim.Resource {
attrs := make(map[string]interface{})
// Singular attributes
attrs["userName"] = user.Name
// The cleartext value or the hashed value of a password SHALL NOT be returnable by a service provider.
// attrs["password"] = user.Password
formatted := fmt.Sprintf("%s %s", user.FirstName, user.LastName)
if user.FirstName == "" {
formatted = user.LastName
}
if user.LastName == "" {
formatted = user.FirstName
}
attrs["name"] = scim.ResourceAttributes{
"formatted": formatted,
"familyName": user.LastName,
"givenName": user.FirstName,
}
attrs["displayName"] = user.DisplayName
attrs["nickName"] = user.DisplayName
attrs["userType"] = user.Type
attrs["profileUrl"] = user.Homepage
attrs["active"] = !user.IsForbidden && !user.IsDeleted
// Multi-Valued attributes
attrs["emails"] = []scim.ResourceAttributes{
{
"value": user.Email,
},
}
attrs["phoneNumbers"] = []scim.ResourceAttributes{
{
"value": user.Phone,
},
}
attrs["photos"] = []scim.ResourceAttributes{
{
"value": user.Avatar,
},
}
attrs["addresses"] = []scim.ResourceAttributes{
{
"locality": user.Location, // e.g. Hollywood
"region": user.Region, // e.g. CN
"country": user.CountryCode, // e.g. USA
},
}
// Enterprise user schema extension
attrs[UserExtensionKey] = scim.ResourceAttributes{
"organization": user.Owner,
}
return &scim.Resource{
ID: user.Id,
ExternalID: buildExternalId(user),
Attributes: attrs,
Meta: buildMeta(user),
}
}
func resource2user(attrs scim.ResourceAttributes) (user *object.User, err error) {
defer func() {
if r := recover(); r != nil {
log.Printf("failed to parse attrs: %v", r)
err = fmt.Errorf("%v", r)
}
}()
user = &object.User{
ExternalId: getAttrString(attrs, "externalId"),
Name: getAttrString(attrs, "userName"),
Password: getAttrString(attrs, "password"),
DisplayName: getAttrString(attrs, "displayName"),
Homepage: getAttrString(attrs, "profileUrl"),
Type: getAttrString(attrs, "userType"),
Owner: getAttrJsonValue(attrs, UserExtensionKey, "organization"),
FirstName: getAttrJsonValue(attrs, "name", "givenName"),
LastName: getAttrJsonValue(attrs, "name", "familyName"),
Email: getAttrJsonValue(attrs, "emails", "value"),
Phone: getAttrJsonValue(attrs, "phoneNumbers", "value"),
Avatar: getAttrJsonValue(attrs, "photos", "value"),
Location: getAttrJsonValue(attrs, "addresses", "locality"),
Region: getAttrJsonValue(attrs, "addresses", "region"),
CountryCode: getAttrJsonValue(attrs, "addresses", "country"),
CreatedTime: util.GetCurrentTime(),
UpdatedTime: util.GetCurrentTime(),
}
if user.Owner == "" {
err = fmt.Errorf("organization in %s is required", UserExtensionKey)
}
return
}

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

@@ -43,8 +43,25 @@ func GetCurrentUnixTime() string {
return strconv.FormatInt(time.Now().UnixNano(), 10)
}
func IsTokenExpired(createdTime string, expiresIn int) bool {
func String2Time(timestamp string) time.Time {
if timestamp == "" {
return time.Now()
}
parseTime, err := time.Parse(time.RFC3339, timestamp)
if err != nil {
panic(err)
}
return parseTime
}
func Time2String(timestamp time.Time) string {
return timestamp.Format(time.RFC3339)
}
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

@@ -35,6 +35,10 @@ module.exports = {
target: "http://localhost:8000",
changeOrigin: true,
},
"/scim": {
target: "http://localhost:8000",
changeOrigin: true,
}
},
},
plugins: [

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,19 @@ class ApplicationEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:SAML Attribute"), i18next.t("general:SAML Attribute - Tooltip"))} :
</Col>
<Col span={22} >
<SamlAttributeTable
title={i18next.t("general:SAML Attribute")}
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

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

@@ -277,7 +277,10 @@ class PermissionEditPage extends React.Component {
<Col span={22} >
<Select virtual={false} mode="multiple" style={{width: "100%"}} value={this.state.permission.users}
onChange={(value => {this.updatePermissionField("users", value);})}
options={this.state.users.map((user) => Setting.getOption(`${user.owner}/${user.name}`, `${user.owner}/${user.name}`))}
options={[
Setting.getOption(i18next.t("organization:All"), "*"),
...this.state.users.map((user) => Setting.getOption(`${user.owner}/${user.name}`, `${user.owner}/${user.name}`)),
]}
/>
</Col>
</Row>
@@ -288,7 +291,10 @@ class PermissionEditPage extends React.Component {
<Col span={22} >
<Select virtual={false} mode="multiple" style={{width: "100%"}} value={this.state.permission.groups}
onChange={(value => {this.updatePermissionField("groups", value);})}
options={this.state.groups.map((group) => Setting.getOption(`${group.owner}/${group.name}`, `${group.owner}/${group.name}`))}
options={[
Setting.getOption(i18next.t("organization:All"), "*"),
...this.state.groups.map((group) => Setting.getOption(`${group.owner}/${group.name}`, `${group.owner}/${group.name}`)),
]}
/>
</Col>
</Row>
@@ -299,8 +305,11 @@ class PermissionEditPage extends React.Component {
<Col span={22} >
<Select disabled={!this.hasRoleDefinition(this.state.model)} virtual={false} mode="multiple" style={{width: "100%"}} value={this.state.permission.roles}
onChange={(value => {this.updatePermissionField("roles", value);})}
options={this.state.roles.filter(roles => (roles.owner !== this.state.roles.owner || roles.name !== this.state.roles.name)).map((permission) => Setting.getOption(`${permission.owner}/${permission.name}`, `${permission.owner}/${permission.name}`))
} />
options={[
Setting.getOption(i18next.t("organization:All"), "*"),
...this.state.roles.filter(roles => (roles.owner !== this.state.roles.owner || roles.name !== this.state.roles.name)).map((permission) => Setting.getOption(`${permission.owner}/${permission.name}`, `${permission.owner}/${permission.name}`)),
]}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
@@ -312,8 +321,11 @@ class PermissionEditPage extends React.Component {
onChange={(value => {
this.updatePermissionField("domains", value);
})}
options={this.state.permission.domains.map((domain) => Setting.getOption(domain, domain))
} />
options={[
Setting.getOption(i18next.t("organization:All"), "*"),
...this.state.permission.domains.map((domain) => Setting.getOption(domain, domain)),
]}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
@@ -340,8 +352,11 @@ class PermissionEditPage extends React.Component {
<Col span={22} >
<Select virtual={false} mode={(this.state.permission.resourceType === "Custom") ? "tags" : "multiple"} style={{width: "100%"}} value={this.state.permission.resources}
onChange={(value => {this.updatePermissionField("resources", value);})}
options={this.state.resources.map((resource) => Setting.getOption(`${resource.name}`, `${resource.name}`))
} />
options={[
Setting.getOption(i18next.t("organization:All"), "*"),
...this.state.resources.map((resource) => Setting.getOption(`${resource.name}`, `${resource.name}`)),
]}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >

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

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

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

@@ -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",