Compare commits

...

45 Commits

Author SHA1 Message Date
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
wxy
684cbdb951 fix: replace the wrong param name willExist (#2389) 2023-10-10 21:47:38 +08:00
QingKai Hao
9aec69ef47 feat: stop building docker image of linux/arm64 (#2390) 2023-10-10 21:19:54 +08:00
Yang Luo
98411ef67b feat: remove db migrate CI 2023-10-10 19:22:41 +08:00
Yang Luo
71279f548d Show cert.Certificate empty error 2023-10-10 19:19:20 +08:00
Yang Luo
0096e47351 feat: fix 403 error in CorsFilter 2023-10-10 18:39:25 +08:00
Yang Luo
814d3f749b Fix Syncer.getKey() 2023-10-09 02:47:42 +08:00
Yang Luo
ec0f457c7f Fix syncer.updateUser() bug 2023-10-09 01:14:35 +08:00
Yang Luo
0033ae1ff1 Improve syncer code 2023-10-08 20:50:28 +08:00
Yang Luo
d06d7c5c09 Fix batch methods like AddUsersInBatch() 2023-10-08 19:33:28 +08:00
Yang Luo
23c4fd8183 Fix go-reddit v2.0.1 doesn't exist issue 2023-10-08 19:29:26 +08:00
Yang Luo
e3558894c3 Add isHostIntranet to CORS filter 2023-10-08 19:29:19 +08:00
Yang Luo
2fd2d88d20 Return 403 in filter's responseError() 2023-10-05 00:12:02 +08:00
Yang Luo
d0c424db0a Don't panic in AddRecord() 2023-10-05 00:11:13 +08:00
Yang Luo
6a9d1e0fe5 Add frontendBaseDir 2023-10-04 12:19:56 +08:00
Yang Luo
938e8e2699 Improve code 2023-09-30 10:49:10 +08:00
Yang Luo
620383cf33 Allow CORS for https://localhost 2023-09-30 09:11:47 +08:00
Yang Luo
de6cd380eb Set OPTIONS status in setCorsHeaders() 2023-09-30 01:13:29 +08:00
79 changed files with 1604 additions and 410 deletions

View File

@@ -195,7 +195,7 @@ jobs:
with:
context: .
target: STANDARD
platforms: linux/amd64,linux/arm64
platforms: linux/amd64
push: true
tags: casbin/casdoor:${{steps.get-current-tag.outputs.tag }},casbin/casdoor:latest
@@ -205,6 +205,6 @@ jobs:
with:
context: .
target: ALLINONE
platforms: linux/amd64,linux/arm64
platforms: linux/amd64
push: true
tags: casbin/casdoor-all-in-one:${{steps.get-current-tag.outputs.tag }},casbin/casdoor-all-in-one:latest

View File

@@ -1,61 +0,0 @@
name: Migration Test
on:
push:
paths:
- 'object/migrator**'
pull_request:
paths:
- 'object/migrator**'
jobs:
db-migrator-test:
name: db-migrator-test
runs-on: ubuntu-latest
services:
mysql:
image: mysql:5.7
env:
MYSQL_DATABASE: casdoor
MYSQL_ROOT_PASSWORD: 123456
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: '^1.16.5'
- uses: actions/setup-node@v2
with:
node-version: 16
- name: pull casdoor-master-latest
run: |
sudo apt update
sudo apt install git
sudo apt install net-tools
sudo mkdir tmp
cd tmp
sudo git clone https://github.com/casdoor/casdoor.git
cd ..
working-directory: ./
- name: run casdoor-master-latest
run: |
sudo nohup go run main.go &
sudo sleep 2m
working-directory: ./tmp/casdoor
- name: stop casdoor-master-latest
run: |
sudo kill -9 `sudo netstat -anltp | grep 8000 | awk '{print $7}' | cut -d / -f 1`
working-directory: ./
- name: run casdoor-current-version
run: |
sudo nohup go run ./main.go &
sudo sleep 2m
working-directory: ./
- name: test port-8000
run: |
if [[ `sudo netstat -anltp | grep 8000 | awk '{print $7}'` == "" ]];then echo 'db-migrator-test fail' && exit 1;fi;
echo 'db-migrator-test pass'
working-directory: ./

3
.gitignore vendored
View File

@@ -30,5 +30,4 @@ commentsRouter*.go
# ignore build result
casdoor
server_linux_arm64
server_linux_amd64
server

View File

@@ -1,7 +1,6 @@
FROM node:16.18.0 AS FRONT
WORKDIR /web
COPY ./web .
RUN yarn config set registry https://registry.npmmirror.com
RUN yarn install --frozen-lockfile --network-timeout 1000000 && yarn run build
@@ -14,9 +13,6 @@ RUN go test -v -run TestGetVersionInfo ./util/system_test.go ./util/system.go >
FROM alpine:latest AS STANDARD
LABEL MAINTAINER="https://casdoor.org/"
ARG USER=casdoor
ARG TARGETOS
ARG TARGETARCH
ENV BUILDX_ARCH="${TARGETOS:-linux}_${TARGETARCH:-amd64}"
RUN sed -i 's/https/http/' /etc/apk/repositories
RUN apk add --update sudo
@@ -31,7 +27,7 @@ RUN adduser -D $USER -u 1000 \
USER 1000
WORKDIR /
COPY --from=BACK --chown=$USER:$USER /go/src/casdoor/server_${BUILDX_ARCH} ./server
COPY --from=BACK --chown=$USER:$USER /go/src/casdoor/server ./server
COPY --from=BACK --chown=$USER:$USER /go/src/casdoor/swagger ./swagger
COPY --from=BACK --chown=$USER:$USER /go/src/casdoor/conf/app.conf ./conf/app.conf
COPY --from=BACK --chown=$USER:$USER /go/src/casdoor/version_info.txt ./go/src/casdoor/version_info.txt
@@ -50,15 +46,12 @@ RUN apt update \
FROM db AS ALLINONE
LABEL MAINTAINER="https://casdoor.org/"
ARG TARGETOS
ARG TARGETARCH
ENV BUILDX_ARCH="${TARGETOS:-linux}_${TARGETARCH:-amd64}"
RUN apt update
RUN apt install -y ca-certificates && update-ca-certificates
WORKDIR /
COPY --from=BACK /go/src/casdoor/server_${BUILDX_ARCH} ./server
COPY --from=BACK /go/src/casdoor/server ./server
COPY --from=BACK /go/src/casdoor/swagger ./swagger
COPY --from=BACK /go/src/casdoor/docker-entrypoint.sh /docker-entrypoint.sh
COPY --from=BACK /go/src/casdoor/conf/app.conf ./conf/app.conf

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

@@ -8,5 +8,4 @@ else
echo "Google is blocked, Go proxy is enabled: GOPROXY=https://goproxy.cn,direct"
export GOPROXY="https://goproxy.cn,direct"
fi
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o server_linux_amd64 .
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-w -s" -o server_linux_arm64 .
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o server .

View File

@@ -8,20 +8,23 @@ dbName = casdoor
tableNamePrefix =
showSql = false
redisEndpoint =
defaultStorageProvider =
defaultStorageProvider =
isCloudIntranet = false
authState = "casdoor"
socks5Proxy = "127.0.0.1:10808"
verificationCodeTimeout = 10
initScore = 2000
initScore = 0
logPostOnly = true
origin =
originFrontend =
staticBaseUrl = "https://cdn.casbin.org"
isDemoMode = false
batchSize = 100
enableGzip = true
ldapServerPort = 389
radiusServerPort = 1812
radiusSecret = "secret"
quota = {"organization": -1, "user": -1, "application": -1, "provider": -1}
logConfig = {"filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}
initDataFile = "./init_data.json"
initDataFile = "./init_data.json"
frontendBaseDir = "../casdoor"

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

View File

@@ -1,4 +1,4 @@
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
// 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.
@@ -12,26 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package object
package controllers
func (syncer *Syncer) getUsers() []*User {
users, err := GetUsers(syncer.Organization)
if err != nil {
panic(err)
}
import (
"strings"
return users
}
func (syncer *Syncer) getUserMap() ([]*User, map[string]*User, map[string]*User) {
users := syncer.getUsers()
m1 := map[string]*User{}
m2 := map[string]*User{}
for _, user := range users {
m1[user.Id] = user
m2[user.Name] = user
}
return users, m1, m2
"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 {

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

8
go.mod
View File

@@ -11,15 +11,16 @@ 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.43.0
github.com/casdoor/notify v0.44.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
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
)

25
go.sum
View File

@@ -917,16 +917,15 @@ 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.43.0 h1:NukyVZ9l7d2TSlB5YWKJyDsPmHCvwKQVi9rWDprtcU4=
github.com/casdoor/notify v0.43.0/go.mod h1:qDmQM5vr2uU01BEuDC6pY6ryahSU11cXPqlHFW232Do=
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/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=
@@ -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=
@@ -1823,8 +1829,8 @@ 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.1 h1:P6ITpf5YHjdy7DHZIbUIDn/iNAoGcEoDQnMa+L4vutw=
github.com/vartanbeno/go-reddit/v2 v2.0.1/go.mod h1:758/S10hwZSLm43NPtwoNQdZFSg3sjB5745Mwjb0ANI=
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=
@@ -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 = "***"
}
@@ -428,7 +440,7 @@ func (application *Application) GetId() string {
}
func (application *Application) IsRedirectUriValid(redirectUri string) bool {
redirectUris := append([]string{"http://localhost:", "http://127.0.0.1:", "http://casdoor-app"}, application.RedirectUris...)
redirectUris := append([]string{"http://localhost:", "https://localhost:", "http://127.0.0.1:", "http://casdoor-app"}, application.RedirectUris...)
for _, targetUri := range redirectUris {
targetUriRegex := regexp.MustCompile(targetUri)
if targetUriRegex.MatchString(redirectUri) || strings.Contains(redirectUri, targetUri) {

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)
@@ -127,9 +138,16 @@ func GetJsonWebKeySet() (jose.JSONWebKeySet, error) {
continue
}
if cert.Certificate == "" {
return jwks, fmt.Errorf("the certificate field should not be empty for the cert: %v", cert)
}
certPemBlock := []byte(cert.Certificate)
certDerBlock, _ := pem.Decode(certPemBlock)
x509Cert, _ := x509.ParseCertificate(certDerBlock.Bytes)
x509Cert, err := x509.ParseCertificate(certDerBlock.Bytes)
if err != nil {
return jwks, err
}
var jwk jose.JSONWebKey
jwk.Key = x509Cert.PublicKey

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

@@ -15,6 +15,7 @@
package object
import (
"fmt"
"strings"
"github.com/casdoor/casdoor/conf"
@@ -149,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
@@ -217,16 +236,15 @@ func AddPermissionsInBatch(permissions []*Permission) bool {
}
affected := false
for i := 0; i < (len(permissions)-1)/batchSize+1; i++ {
start := i * batchSize
end := (i + 1) * batchSize
for i := 0; i < len(permissions); i += batchSize {
start := i
end := i + batchSize
if end > len(permissions) {
end = len(permissions)
}
tmp := permissions[start:end]
// TODO: save to log instead of standard output
// fmt.Printf("Add Permissions: [%d - %d].\n", start, end)
fmt.Printf("The syncer adds permissions: [%d - %d]\n", start, end)
if AddPermissions(tmp) {
affected = true
}
@@ -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

@@ -82,5 +82,6 @@ func UploadPermissions(owner string, path string) (bool, error) {
if len(newPermissions) == 0 {
return false, nil
}
return AddPermissionsInBatch(newPermissions), nil
}

View File

@@ -87,47 +87,71 @@ func AddRecord(record *casvisorsdk.Record) bool {
affected, err := casvisorsdk.AddRecord(record)
if err != nil {
panic(err)
fmt.Printf("AddRecord() error: %s", err.Error())
}
return affected
}
func getFilteredWebhooks(webhooks []*Webhook, action string) []*Webhook {
res := []*Webhook{}
for _, webhook := range webhooks {
if !webhook.IsEnabled {
continue
}
matched := false
for _, event := range webhook.Events {
if action == event {
matched = true
break
}
}
if matched {
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 {
if !webhook.IsEnabled {
continue
}
matched := false
for _, event := range webhook.Events {
if record.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
}
}
err = sendWebhook(webhook, record, user)
var user *User
if webhook.IsUserExtended {
user, err = getUser(record.Organization, record.User)
if err != nil {
return err
errs = append(errs, err)
continue
}
user, err = GetMaskedUser(user, false, err)
if err != nil {
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

@@ -208,16 +208,15 @@ func AddRolesInBatch(roles []*Role) bool {
}
affected := false
for i := 0; i < (len(roles)-1)/batchSize+1; i++ {
start := i * batchSize
end := (i + 1) * batchSize
for i := 0; i < len(roles); i += batchSize {
start := i
end := i + batchSize
if end > len(roles) {
end = len(roles)
}
tmp := roles[start:end]
// TODO: save to log instead of standard output
// fmt.Printf("Add users: [%d - %d].\n", start, end)
fmt.Printf("The syncer adds roles: [%d - %d]\n", start, end)
if AddRoles(tmp) {
affected = true
}
@@ -255,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

@@ -68,5 +68,6 @@ func UploadRoles(owner string, path string) (bool, error) {
if len(newRoles) == 0 {
return false, nil
}
return AddRolesInBatch(newRoles), nil
}

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) {
@@ -200,6 +208,10 @@ func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, e
return nil, errors.New("please set a cert for the application first")
}
if cert.Certificate == "" {
return nil, fmt.Errorf("the certificate field should not be empty for the cert: %v", cert)
}
block, _ := pem.Decode([]byte(cert.Certificate))
certificate := base64.StdEncoding.EncodeToString(block.Bytes)
@@ -288,6 +300,10 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
return "", "", "", err
}
if cert.Certificate == "" {
return "", "", "", fmt.Errorf("the certificate field should not be empty for the cert: %v", cert)
}
block, _ := pem.Decode([]byte(cert.Certificate))
certificate := base64.StdEncoding.EncodeToString(block.Bytes)
@@ -301,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

@@ -230,25 +230,32 @@ func (syncer *Syncer) getTable() string {
}
}
func (syncer *Syncer) getKey() string {
key := "id"
hasKey := false
hasId := false
func (syncer *Syncer) getKeyColumn() *TableColumn {
var column *TableColumn
for _, tableColumn := range syncer.TableColumns {
if tableColumn.IsKey {
hasKey = true
key = tableColumn.Name
}
if tableColumn.Name == "id" {
hasId = true
column = tableColumn
}
}
if !hasKey && !hasId {
key = syncer.TableColumns[0].Name
if column == nil {
for _, tableColumn := range syncer.TableColumns {
if tableColumn.Name == "id" {
column = tableColumn
}
}
}
return key
if column == nil {
column = syncer.TableColumns[0]
}
return column
}
func (syncer *Syncer) getKey() string {
column := syncer.getKeyColumn()
return util.CamelToSnakeCase(column.CasdoorName)
}
func RunSyncer(syncer *Syncer) error {

View File

@@ -16,7 +16,8 @@ package object
import (
"fmt"
"time"
"github.com/casdoor/casdoor/util"
)
func (syncer *Syncer) syncUsers() error {
@@ -26,17 +27,26 @@ func (syncer *Syncer) syncUsers() error {
fmt.Printf("Running syncUsers()..\n")
users, _, _ := syncer.getUserMap()
oUsers, _, err := syncer.getOriginalUserMap()
users, err := GetUsers(syncer.Organization)
if err != nil {
fmt.Printf(err.Error())
timestamp := time.Now().Format("2006-01-02 15:04:05")
line := fmt.Sprintf("[%s] %s\n", timestamp, err.Error())
_, err = updateSyncerErrorText(syncer, line)
if err != nil {
return err
line := fmt.Sprintf("[%s] %s\n", util.GetCurrentTime(), err.Error())
_, err2 := updateSyncerErrorText(syncer, line)
if err2 != nil {
panic(err2)
}
return err
}
oUsers, err := syncer.getOriginalUsers()
if err != nil {
line := fmt.Sprintf("[%s] %s\n", util.GetCurrentTime(), err.Error())
_, err2 := updateSyncerErrorText(syncer, line)
if err2 != nil {
panic(err2)
}
return err
}
fmt.Printf("Users: %d, oUsers: %d\n", len(users), len(oUsers))
@@ -76,7 +86,7 @@ func (syncer *Syncer) syncUsers() error {
updatedUser.PreHash = oHash
fmt.Printf("Update from oUser to user: %v\n", updatedUser)
_, err = syncer.updateUserForOriginalByFields(updatedUser, key)
_, err = syncer.updateUserForOriginalFields(updatedUser, key)
if err != nil {
return err
}
@@ -113,7 +123,7 @@ func (syncer *Syncer) syncUsers() error {
updatedUser.PreHash = oHash
fmt.Printf("Update from oUser to user (2nd condition): %v\n", updatedUser)
_, err = syncer.updateUserForOriginalByFields(updatedUser, key)
_, err = syncer.updateUserForOriginalFields(updatedUser, key)
if err != nil {
return err
}
@@ -122,6 +132,7 @@ func (syncer *Syncer) syncUsers() error {
}
}
}
_, err = AddUsersInBatch(newUsers)
if err != nil {
return err

View File

@@ -21,7 +21,6 @@ import (
"time"
"github.com/casdoor/casdoor/util"
"github.com/xorm-io/core"
)
type OriginalUser = User
@@ -50,19 +49,6 @@ func (syncer *Syncer) getOriginalUsers() ([]*OriginalUser, error) {
return users, nil
}
func (syncer *Syncer) getOriginalUserMap() ([]*OriginalUser, map[string]*OriginalUser, error) {
users, err := syncer.getOriginalUsers()
if err != nil {
return users, nil, err
}
m := map[string]*OriginalUser{}
for _, user := range users {
m[user.Id] = user
}
return users, m, nil
}
func (syncer *Syncer) addUser(user *OriginalUser) (bool, error) {
m := syncer.getMapFromOriginalUser(user)
affected, err := syncer.Ormer.Engine.Table(syncer.getTable()).Insert(m)
@@ -89,38 +75,14 @@ func (syncer *Syncer) updateUser(user *OriginalUser) (bool, error) {
pkValue := m[key]
delete(m, key)
affected, err := syncer.Ormer.Engine.Table(syncer.getTable()).ID(pkValue).Update(&m)
affected, err := syncer.Ormer.Engine.Table(syncer.getTable()).Where(fmt.Sprintf("%s = ?", key), pkValue).Update(&m)
if err != nil {
return false, err
}
return affected != 0, nil
}
func (syncer *Syncer) updateUserForOriginalFields(user *User) (bool, error) {
var err error
owner, name := util.GetOwnerAndNameFromId(user.GetId())
oldUser, err := getUserById(owner, name)
if oldUser == nil || err != nil {
return false, err
}
if user.Avatar != oldUser.Avatar && user.Avatar != "" {
user.PermanentAvatar, err = getPermanentAvatarUrl(user.Owner, user.Name, user.Avatar, true)
if err != nil {
return false, err
}
}
columns := syncer.getCasdoorColumns()
columns = append(columns, "affiliation", "hash", "pre_hash")
affected, err := ormer.Engine.ID(core.PK{oldUser.Owner, oldUser.Name}).Cols(columns...).Update(user)
if err != nil {
return false, err
}
return affected != 0, nil
}
func (syncer *Syncer) updateUserForOriginalByFields(user *User, key string) (bool, error) {
func (syncer *Syncer) updateUserForOriginalFields(user *User, key string) (bool, error) {
var err error
oldUser := User{}
@@ -196,7 +158,10 @@ func RunSyncUsersJob() {
}
for _, syncer := range syncers {
addSyncerJob(syncer)
err = addSyncerJob(syncer)
if err != nil {
panic(err)
}
}
time.Sleep(time.Duration(1<<63 - 1))

View File

@@ -286,6 +286,10 @@ func GetValidationBySaml(samlRequest string, host string) (string, string, error
return "", "", err
}
if cert.Certificate == "" {
return "", "", fmt.Errorf("the certificate field should not be empty for the cert: %v", cert)
}
block, _ := pem.Decode([]byte(cert.Certificate))
certificate := base64.StdEncoding.EncodeToString(block.Bytes)
randomKeyStore := &X509Key{

View File

@@ -368,6 +368,10 @@ func ParseJwtToken(token string, cert *Cert) (*Claims, error) {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
if cert.Certificate == "" {
return nil, fmt.Errorf("the certificate field should not be empty for the cert: %v", cert)
}
// RSA certificate
certificate, err := jwt.ParseRSAPublicKeyFromPEM([]byte(cert.Certificate))
if err != nil {

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)
}
@@ -717,16 +779,15 @@ func AddUsersInBatch(users []*User) (bool, error) {
}
affected := false
for i := 0; i < (len(users)-1)/batchSize+1; i++ {
start := i * batchSize
end := (i + 1) * batchSize
for i := 0; i < len(users); i += batchSize {
start := i
end := i + batchSize
if end > len(users) {
end = len(users)
}
tmp := users[start:end]
// TODO: save to log instead of standard output
// fmt.Printf("Add users: [%d - %d].\n", start, end)
fmt.Printf("The syncer adds users: [%d - %d]\n", start, end)
if ok, err := AddUsers(tmp); err != nil {
return false, err
} else if ok {

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

@@ -144,5 +144,6 @@ func UploadUsers(owner string, path string) (bool, error) {
if len(newUsers) == 0 {
return false, nil
}
return AddUsersInBatch(newUsers)
}

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

@@ -16,6 +16,9 @@ package routers
import (
"fmt"
"net"
"net/http"
"net/url"
"strings"
"github.com/beego/beego/context"
@@ -33,6 +36,8 @@ type Response struct {
}
func responseError(ctx *context.Context, error string, data ...interface{}) {
ctx.ResponseWriter.WriteHeader(http.StatusForbidden)
resp := Response{Status: "error", Msg: error}
switch len(data) {
case 2:
@@ -61,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")
@@ -69,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 {
@@ -151,3 +159,39 @@ func parseBearerToken(ctx *context.Context) string {
return tokens[1]
}
func getHostname(s string) string {
if s == "" {
return ""
}
l, err := url.Parse(s)
if err != nil {
panic(err)
}
res := l.Hostname()
return res
}
func removePort(s string) string {
ipStr, _, err := net.SplitHostPort(s)
if err != nil {
ipStr = s
}
return ipStr
}
func isHostIntranet(s string) bool {
ipStr, _, err := net.SplitHostPort(s)
if err != nil {
ipStr = s
}
ip := net.ParseIP(ipStr)
if ip == nil {
return false
}
return ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast()
}

View File

@@ -16,7 +16,6 @@ package routers
import (
"net/http"
"net/url"
"strings"
"github.com/beego/beego/context"
@@ -35,29 +34,19 @@ func setCorsHeaders(ctx *context.Context, origin string) {
ctx.Output.Header(headerAllowOrigin, origin)
ctx.Output.Header(headerAllowMethods, "POST, GET, OPTIONS, DELETE")
ctx.Output.Header(headerAllowHeaders, "Content-Type, Authorization")
}
func getHostname(s string) string {
if s == "" {
return ""
if ctx.Input.Method() == "OPTIONS" {
ctx.ResponseWriter.WriteHeader(http.StatusOK)
}
l, err := url.Parse(s)
if err != nil {
panic(err)
}
res := l.Hostname()
return res
}
func CorsFilter(ctx *context.Context) {
origin := ctx.Input.Header(headerOrigin)
originConf := conf.GetConfigString("origin")
originHostname := getHostname(origin)
host := ctx.Request.Host
host := removePort(ctx.Request.Host)
if strings.HasPrefix(origin, "http://localhost") || strings.HasPrefix(origin, "http://127.0.0.1") || strings.HasPrefix(origin, "http://casdoor-app") {
if strings.HasPrefix(origin, "http://localhost") || strings.HasPrefix(origin, "https://localhost") || strings.HasPrefix(origin, "http://127.0.0.1") || strings.HasPrefix(origin, "http://casdoor-app") {
setCorsHeaders(ctx, origin)
return
}
@@ -77,6 +66,8 @@ func CorsFilter(ctx *context.Context) {
setCorsHeaders(ctx, origin)
} else if originHostname == host {
setCorsHeaders(ctx, origin)
} else if isHostIntranet(host) {
setCorsHeaders(ctx, origin)
} else {
ok, err := object.IsOriginAllowed(origin)
if err != nil {
@@ -89,11 +80,6 @@ func CorsFilter(ctx *context.Context) {
ctx.ResponseWriter.WriteHeader(http.StatusForbidden)
return
}
if ctx.Input.Method() == "OPTIONS" {
ctx.ResponseWriter.WriteHeader(http.StatusOK)
return
}
}
}

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

@@ -33,8 +33,19 @@ var (
oldStaticBaseUrl = "https://cdn.casbin.org"
newStaticBaseUrl = conf.GetConfigString("staticBaseUrl")
enableGzip = conf.GetConfigBool("enableGzip")
frontendBaseDir = conf.GetConfigString("frontendBaseDir")
)
func getWebBuildFolder() string {
path := "web/build"
if util.FileExist(filepath.Join(path, "index.html")) || frontendBaseDir == "" {
return path
}
path = filepath.Join(frontendBaseDir, "web/build")
return path
}
func StaticFilter(ctx *context.Context) {
urlPath := ctx.Request.URL.Path
@@ -48,8 +59,12 @@ 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
}
path := "web/build"
webBuildFolder := getWebBuildFolder()
path := webBuildFolder
if urlPath == "/" {
path += "/index.html"
} else {
@@ -57,7 +72,7 @@ func StaticFilter(ctx *context.Context) {
}
if !util.FileExist(path) {
path = "web/build/index.html"
path = webBuildFolder + "/index.html"
}
if !util.FileExist(path) {
dir, err := os.Getwd()
@@ -65,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

@@ -272,7 +272,7 @@ class AdapterEditPage extends React.Component {
);
}
submitAdapterEdit(willExist) {
submitAdapterEdit(exitAfterSave) {
const adapter = Setting.deepCopy(this.state.adapter);
AdapterBackend.updateAdapter(this.state.organizationName, this.state.adapterName, adapter)
.then((res) => {
@@ -282,7 +282,7 @@ class AdapterEditPage extends React.Component {
adapterName: this.state.adapter.name,
});
if (willExist) {
if (exitAfterSave) {
this.props.history.push("/adapters");
} else {
this.props.history.push(`/adapters/${this.state.organizationName}/${this.state.adapter.name}`);

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"))} :
@@ -1001,7 +1016,7 @@ class ApplicationEditPage extends React.Component {
);
}
submitApplicationEdit(willExist) {
submitApplicationEdit(exitAfterSave) {
const application = Setting.deepCopy(this.state.application);
application.providers = application.providers?.filter(provider => this.state.providers.map(provider => provider.name).includes(provider.name));
@@ -1013,7 +1028,7 @@ class ApplicationEditPage extends React.Component {
applicationName: this.state.application.name,
});
if (willExist) {
if (exitAfterSave) {
this.props.history.push("/applications");
} else {
this.props.history.push(`/applications/${this.state.application.organization}/${this.state.application.name}`);

View File

@@ -251,7 +251,7 @@ class CertEditPage extends React.Component {
);
}
submitCertEdit(willExist) {
submitCertEdit(exitAfterSave) {
const cert = Setting.deepCopy(this.state.cert);
CertBackend.updateCert(this.state.owner, this.state.certName, cert)
.then((res) => {
@@ -261,7 +261,7 @@ class CertEditPage extends React.Component {
certName: this.state.cert.name,
});
if (willExist) {
if (exitAfterSave) {
this.props.history.push("/certs");
} else {
this.props.history.push(`/certs/${this.state.cert.owner}/${this.state.cert.name}`);

View File

@@ -198,7 +198,7 @@ class EnforcerEditPage extends React.Component {
);
}
submitEnforcerEdit(willExist) {
submitEnforcerEdit(exitAfterSave) {
const enforcer = Setting.deepCopy(this.state.enforcer);
EnforcerBackend.updateEnforcer(this.state.organizationName, this.state.enforcerName, enforcer)
.then((res) => {
@@ -208,7 +208,7 @@ class EnforcerEditPage extends React.Component {
enforcerName: this.state.enforcer.name,
});
if (willExist) {
if (exitAfterSave) {
this.props.history.push("/enforcers");
} else {
this.props.history.push(`/enforcers/${this.state.enforcer.owner}/${this.state.enforcer.name}`);

View File

@@ -191,7 +191,7 @@ class GroupEditPage extends React.Component {
);
}
submitGroupEdit(willExist) {
submitGroupEdit(exitAfterSave) {
const group = Setting.deepCopy(this.state.group);
group["isTopGroup"] = this.state.organizations.some((organization) => organization.name === group.parentId);
@@ -203,7 +203,7 @@ class GroupEditPage extends React.Component {
groupName: this.state.group.name,
});
if (willExist) {
if (exitAfterSave) {
const groupTreeUrl = sessionStorage.getItem("groupTreeUrl");
if (groupTreeUrl !== null) {
sessionStorage.removeItem("groupTreeUrl");

View File

@@ -230,7 +230,7 @@ class LdapEditPage extends React.Component {
);
}
submitLdapEdit(willExist) {
submitLdapEdit(exitAfterSave) {
LddpBackend.updateLdap(this.state.ldap)
.then((res) => {
if (res.status === "ok") {
@@ -239,7 +239,7 @@ class LdapEditPage extends React.Component {
organizationName: this.state.ldap.owner,
});
if (willExist) {
if (exitAfterSave) {
this.props.history.push(`/organizations/${this.state.organizationName}`);
}
} else {

View File

@@ -165,7 +165,7 @@ class ModelEditPage extends React.Component {
);
}
submitModelEdit(willExist) {
submitModelEdit(exitAfterSave) {
const model = Setting.deepCopy(this.state.model);
ModelBackend.updateModel(this.state.organizationName, this.state.modelName, model)
.then((res) => {
@@ -175,7 +175,7 @@ class ModelEditPage extends React.Component {
modelName: this.state.model.name,
});
if (willExist) {
if (exitAfterSave) {
this.props.history.push("/models");
} else {
this.props.history.push(`/models/${this.state.model.owner}/${this.state.model.name}`);

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"))} :
@@ -411,7 +421,7 @@ class OrganizationEditPage extends React.Component {
);
}
submitOrganizationEdit(willExist) {
submitOrganizationEdit(exitAfterSave) {
const organization = Setting.deepCopy(this.state.organization);
organization.accountItems = organization.accountItems?.filter(accountItem => accountItem.name !== "Please select an account item");
@@ -429,7 +439,7 @@ class OrganizationEditPage extends React.Component {
});
window.dispatchEvent(new Event("storageOrganizationsChanged"));
if (willExist) {
if (exitAfterSave) {
this.props.history.push("/organizations");
} else {
this.props.history.push(`/organizations/${this.state.organization.name}`);

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

@@ -438,7 +438,7 @@ class PaymentEditPage extends React.Component {
return "";
}
submitPaymentEdit(willExist) {
submitPaymentEdit(exitAfterSave) {
const errorText = this.checkError();
if (errorText !== "") {
Setting.showMessage("error", errorText);
@@ -454,7 +454,7 @@ class PaymentEditPage extends React.Component {
paymentName: this.state.payment.name,
});
if (willExist) {
if (exitAfterSave) {
this.props.history.push("/payments");
} else {
this.props.history.push(`/payments/${this.state.payment.name}`);

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"}} >
@@ -444,7 +459,7 @@ class PermissionEditPage extends React.Component {
);
}
submitPermissionEdit(willExist) {
submitPermissionEdit(exitAfterSave) {
if (this.state.permission.users.length === 0 && this.state.permission.roles.length === 0) {
Setting.showMessage("error", "The users and roles cannot be empty at the same time");
return;
@@ -475,7 +490,7 @@ class PermissionEditPage extends React.Component {
permissionName: this.state.permission.name,
});
if (willExist) {
if (exitAfterSave) {
this.props.history.push("/permissions");
} else {
this.props.history.push(`/permissions/${this.state.permission.owner}/${encodeURIComponent(this.state.permission.name)}`);

View File

@@ -263,7 +263,7 @@ class PlanEditPage extends React.Component {
);
}
submitPlanEdit(willExist) {
submitPlanEdit(exitAfterSave) {
const plan = Setting.deepCopy(this.state.plan);
PlanBackend.updatePlan(this.state.organizationName, this.state.planName, plan)
.then((res) => {
@@ -273,7 +273,7 @@ class PlanEditPage extends React.Component {
planName: this.state.plan.name,
});
if (willExist) {
if (exitAfterSave) {
this.props.history.push("/plans");
} else {
this.props.history.push(`/plans/${this.state.plan.owner}/${this.state.plan.name}`);

View File

@@ -226,7 +226,7 @@ class PricingEditPage extends React.Component {
);
}
submitPricingEdit(willExist) {
submitPricingEdit(exitAfterSave) {
const pricing = Setting.deepCopy(this.state.pricing);
PricingBackend.updatePricing(this.state.organizationName, this.state.pricingName, pricing)
.then((res) => {
@@ -236,7 +236,7 @@ class PricingEditPage extends React.Component {
pricingName: this.state.pricing.name,
});
if (willExist) {
if (exitAfterSave) {
this.props.history.push("/pricings");
} else {
this.props.history.push(`/pricings/${this.state.pricing.owner}/${this.state.pricing.name}`);

View File

@@ -311,7 +311,7 @@ class ProductEditPage extends React.Component {
);
}
submitProductEdit(willExist) {
submitProductEdit(exitAfterSave) {
const product = Setting.deepCopy(this.state.product);
ProductBackend.updateProduct(this.state.organizationName, this.state.productName, product)
.then((res) => {
@@ -321,7 +321,7 @@ class ProductEditPage extends React.Component {
productName: this.state.product.name,
});
if (willExist) {
if (exitAfterSave) {
this.props.history.push("/products");
} else {
this.props.history.push(`/products/${this.state.product.owner}/${this.state.product.name}`);

View File

@@ -1198,7 +1198,7 @@ class ProviderEditPage extends React.Component {
);
}
submitProviderEdit(willExist) {
submitProviderEdit(exitAfterSave) {
const provider = Setting.deepCopy(this.state.provider);
ProviderBackend.updateProvider(this.state.owner, this.state.providerName, provider)
.then((res) => {
@@ -1209,7 +1209,7 @@ class ProviderEditPage extends React.Component {
providerName: this.state.provider.name,
});
if (willExist) {
if (exitAfterSave) {
this.props.history.push("/providers");
} else {
this.props.history.push(`/providers/${this.state.provider.owner}/${this.state.provider.name}`);

View File

@@ -240,7 +240,7 @@ class RoleEditPage extends React.Component {
);
}
submitRoleEdit(willExist) {
submitRoleEdit(exitAfterSave) {
const role = Setting.deepCopy(this.state.role);
RoleBackend.updateRole(this.state.organizationName, this.state.roleName, role)
.then((res) => {
@@ -250,7 +250,7 @@ class RoleEditPage extends React.Component {
roleName: this.state.role.name,
});
if (willExist) {
if (exitAfterSave) {
this.props.history.push("/roles");
} else {
this.props.history.push(`/roles/${this.state.role.owner}/${encodeURIComponent(this.state.role.name)}`);

View File

@@ -294,7 +294,7 @@ class SubscriptionEditPage extends React.Component {
);
}
submitSubscriptionEdit(willExist) {
submitSubscriptionEdit(exitAfterSave) {
const subscription = Setting.deepCopy(this.state.subscription);
SubscriptionBackend.updateSubscription(this.state.organizationName, this.state.subscriptionName, subscription)
.then((res) => {
@@ -304,7 +304,7 @@ class SubscriptionEditPage extends React.Component {
subscriptionName: this.state.subscription.name,
});
if (willExist) {
if (exitAfterSave) {
this.props.history.push("/subscriptions");
} else {
this.props.history.push(`/subscriptions/${this.state.subscription.owner}/${this.state.subscription.name}`);

View File

@@ -425,7 +425,7 @@ class SyncerEditPage extends React.Component {
);
}
submitSyncerEdit(willExist) {
submitSyncerEdit(exitAfterSave) {
const syncer = Setting.deepCopy(this.state.syncer);
SyncerBackend.updateSyncer(this.state.syncer.owner, this.state.syncerName, syncer)
.then((res) => {
@@ -435,7 +435,7 @@ class SyncerEditPage extends React.Component {
syncerName: this.state.syncer.name,
});
if (willExist) {
if (exitAfterSave) {
this.props.history.push("/syncers");
} else {
this.props.history.push(`/syncers/${this.state.syncer.name}`);

View File

@@ -173,7 +173,7 @@ class TokenEditPage extends React.Component {
);
}
submitTokenEdit(willExist) {
submitTokenEdit(exitAfterSave) {
const token = Setting.deepCopy(this.state.token);
TokenBackend.updateToken(this.state.token.owner, this.state.tokenName, token)
.then((res) => {
@@ -183,7 +183,7 @@ class TokenEditPage extends React.Component {
tokenName: this.state.token.name,
});
if (willExist) {
if (exitAfterSave) {
this.props.history.push("/tokens");
} else {
this.props.history.push(`/tokens/${this.state.token.name}`);

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()) {
@@ -1043,7 +1043,7 @@ class UserEditPage extends React.Component {
}
}
submitUserEdit(needExit) {
submitUserEdit(exitAfterSave) {
const user = Setting.deepCopy(this.state.user);
UserBackend.updateUser(this.state.organizationName, this.state.userName, user)
.then((res) => {
@@ -1055,7 +1055,7 @@ class UserEditPage extends React.Component {
});
if (this.props.history !== undefined) {
if (needExit) {
if (exitAfterSave) {
const userListUrl = sessionStorage.getItem("userListUrl");
if (userListUrl !== null) {
this.props.history.push(userListUrl);
@@ -1066,7 +1066,7 @@ class UserEditPage extends React.Component {
this.props.history.push(`/users/${this.state.user.owner}/${this.state.user.name}`);
}
} else {
if (needExit) {
if (exitAfterSave) {
if (this.state.returnUrl) {
window.location.href = this.state.returnUrl;
}

View File

@@ -298,13 +298,6 @@ class UserListPage extends BaseListPage {
sorter: true,
...this.getColumnSearchProps("phone"),
},
// {
// title: 'Phone',
// dataIndex: 'phone',
// key: 'phone',
// width: '120px',
// sorter: (a, b) => a.phone.localeCompare(b.phone),
// },
{
title: i18next.t("user:Affiliation"),
dataIndex: "affiliation",

View File

@@ -322,7 +322,7 @@ class WebhookEditPage extends React.Component {
);
}
submitWebhookEdit(willExist) {
submitWebhookEdit(exitAfterSave) {
const webhook = Setting.deepCopy(this.state.webhook);
WebhookBackend.updateWebhook(this.state.webhook.owner, this.state.webhookName, webhook)
.then((res) => {
@@ -332,7 +332,7 @@ class WebhookEditPage extends React.Component {
webhookName: this.state.webhook.name,
});
if (willExist) {
if (exitAfterSave) {
this.props.history.push("/webhooks");
} else {
this.props.history.push(`/webhooks/${this.state.webhook.name}`);

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