Compare commits

...

92 Commits

Author SHA1 Message Date
8b6b5ffe81 feat: fix go-reddit module checksum mismatch (#2451) 2023-10-28 15:32:36 +08:00
a147fa3e0b feat: fix bug that tableNamePrefix caused getRolesByUserInternal() to fail (#2450)
If set tableNamePrefix in app.conf, while cause sql error
2023-10-28 09:45:54 +08:00
9d03665523 Fix FromProviderToIdpInfo() bug 2023-10-27 18:10:22 +08:00
0106c7f7fa Fix GetIdProvider() bug 2023-10-27 17:03:37 +08:00
6713dad0af Fix this.props.account null issue 2023-10-27 02:13:23 +08:00
6ef2b51782 Support fastAutoSignin by backend redirection 2023-10-27 00:44:50 +08:00
1732cd8538 Fix the bug that sometimes cannot auto login with enableAutoSignin = true 2023-10-27 00:06:17 +08:00
a10548fe73 Fix org admin's enforcer policy APIs 2023-10-26 23:31:36 +08:00
f6a7888f83 Deleted user cannot perform actions 2023-10-26 10:41:38 +08:00
93efaa5459 Fix FileExist() error handling 2023-10-26 10:40:28 +08:00
0bfe683108 feat: change canonicalizer algorithm to xml-exc-c14n# (#2440) 2023-10-24 14:13:09 +08:00
8a4758c22d Update sync code 2023-10-22 11:56:56 +08:00
ee3b46e91c Allow permission.Model to be empty 2023-10-22 02:35:51 +08:00
37744d6cd7 Improve permission error handling 2023-10-22 02:30:29 +08:00
98defe617b Add providerItem.SignupGroup 2023-10-20 23:10:43 +08:00
96cbf51ca0 Remove useless alertType field 2023-10-20 23:01:11 +08:00
22b57fdd23 Add application.EnableSamlC14n10 2023-10-20 22:37:23 +08:00
b68e291f37 feat: support SAML Custom provider (#2430)
* 111

* feat: support custom saml provider

* feat: gofumpt code

* feat: gofumpt code

* feat: remove comment

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-10-20 21:11:36 +08:00
9960b4933b feat: respect isReadOnly in the syncer (#2427)
Co-authored-by: aidenlu <aiden_lu@wochacha.com>
2023-10-19 18:57:12 +08:00
432a5496f2 fix: skip checking password when the code is provided (#2425)
Co-authored-by: aidenlu <aiden_lu@wochacha.com>
2023-10-19 18:25:25 +08:00
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
3f53591751 Improve verification no provider error message 2023-10-18 15:32:12 +08:00
d7569684f6 Local admin can edit its org user's other fields now 2023-10-18 12:16:05 +08:00
a616127909 Add organization.DefaultPassword 2023-10-18 11:58:25 +08:00
f2e2b960ff Improve downloadImage() error handling 2023-10-18 02:25:22 +08:00
fbc603876f feat: add originFrontend to app.conf 2023-10-17 21:47:18 +08:00
9ea77c63d1 Local admin can edit its org users now 2023-10-17 18:23:39 +08:00
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
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
2dd1dc582f Add text to app's signup table 2023-10-15 18:17:50 +08:00
f3d4b45a0f Add label and placeholder to app's signup table 2023-10-15 17:24:38 +08:00
2ee4aebd96 Fix error handling in GetSamlMeta() 2023-10-15 17:02:40 +08:00
150e3e30d5 Support app user in API authentication 2023-10-15 15:20:57 +08:00
1055d7781b Improve error handling in AutoSigninFilter 2023-10-15 12:43:36 +08:00
1c296e9b6f feat: activate enableGzip by default in app.conf 2023-10-15 01:27:42 +08:00
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
43d849086f Fix 127.0.0.1 bug in isHostIntranet() 2023-10-13 23:29:37 +08:00
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
52a66ef044 Fix webhook not triggered issue in SendWebhooks() 2023-10-13 16:47:09 +08:00
ec0a8e16f7 feat: fix CheckLoginPermission() logic 2023-10-13 15:41:23 +08:00
80a8000057 Add GetModelEx() 2023-10-13 13:45:13 +08:00
77091a3ae5 Fix null model issue in UpdatePermission() 2023-10-13 12:55:11 +08:00
983da685a2 feat: support calling get-user API by only email, phone or userId without owner (#2398) 2023-10-13 02:48:55 +08:00
3d567c3d45 feat: update go-sms-sender to fix Twilio template error (#2395) 2023-10-12 01:53:31 +08:00
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
e4208d7fd9 feat: restrict the model of application type resource permission (#2394) 2023-10-12 00:05:53 +08:00
4de716fef3 Improve UploadResource() 2023-10-11 01:27:29 +08:00
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
9aec69ef47 feat: stop building docker image of linux/arm64 (#2390) 2023-10-10 21:19:54 +08:00
98411ef67b feat: remove db migrate CI 2023-10-10 19:22:41 +08:00
71279f548d Show cert.Certificate empty error 2023-10-10 19:19:20 +08:00
0096e47351 feat: fix 403 error in CorsFilter 2023-10-10 18:39:25 +08:00
814d3f749b Fix Syncer.getKey() 2023-10-09 02:47:42 +08:00
ec0f457c7f Fix syncer.updateUser() bug 2023-10-09 01:14:35 +08:00
0033ae1ff1 Improve syncer code 2023-10-08 20:50:28 +08:00
d06d7c5c09 Fix batch methods like AddUsersInBatch() 2023-10-08 19:33:28 +08:00
23c4fd8183 Fix go-reddit v2.0.1 doesn't exist issue 2023-10-08 19:29:26 +08:00
e3558894c3 Add isHostIntranet to CORS filter 2023-10-08 19:29:19 +08:00
2fd2d88d20 Return 403 in filter's responseError() 2023-10-05 00:12:02 +08:00
d0c424db0a Don't panic in AddRecord() 2023-10-05 00:11:13 +08:00
6a9d1e0fe5 Add frontendBaseDir 2023-10-04 12:19:56 +08:00
938e8e2699 Improve code 2023-09-30 10:49:10 +08:00
620383cf33 Allow CORS for https://localhost 2023-09-30 09:11:47 +08:00
de6cd380eb Set OPTIONS status in setCorsHeaders() 2023-09-30 01:13:29 +08:00
7e0bce2d0f feat: run RecordMessage() filter after API handling (#2369)
* feat: write records after exec (#2368)

* add returnOnOutput params
2023-09-29 10:12:00 +08:00
1461268a51 Allow redirect URL for casdoor-app 2023-09-27 22:37:57 +08:00
5ec49dc883 feat: fix claims.tag and UserWithoutThirdIdp missing fields, fix for Rust SDK 2023-09-27 18:07:57 +08:00
5c89705d9e feat: allow CORS for 127.0.0.1 2023-09-27 14:10:59 +08:00
06e3b8481f Improve adapter error handling 2023-09-27 01:11:58 +08:00
81a8b91e3f Fix enforcer policy add and delete 2023-09-27 00:18:21 +08:00
56787fab90 Improve adapter.UseSameDb 2023-09-26 23:41:09 +08:00
1319216625 Add adapter.UseSameDb 2023-09-26 23:41:08 +08:00
6fe5c44c1c feat: support radius accounting request (#2362)
* feat: add radius server

* feat: parse org from packet

* feat: add comment

* feat: support radius accounting

* feat: change log

* feat: add copyright
2023-09-26 22:48:00 +08:00
981908b0b6 Fix crash in LDAP's sync: GenerateIdForNewUser() 2023-09-26 19:12:28 +08:00
03a281cb5d Improve CorsFilter code 2023-09-26 14:51:38 +08:00
a8e541159b Allow localhost in CorsFilter 2023-09-26 00:03:26 +08:00
577bf91d25 Refactor out setCorsHeaders() 2023-09-26 00:02:31 +08:00
329a6a8132 Fix get-pricing and get-plan API null error handling 2023-09-25 22:11:08 +08:00
fba0866cd6 Fix error handling in StartRadiusServer() 2023-09-25 20:55:02 +08:00
aab6a799fe fix: use client secret field for providers (#2355)
* feat: fix key exposure problem

* fix display bug
2023-09-24 18:35:58 +08:00
b94d06fb07 feat: add some Radius protocol code (#2351)
* feat: add radius server

* feat: parse org from packet

* feat: add comment

* Update main.go

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-09-24 16:50:31 +08:00
f9cc6ed064 Add groups to role 2023-09-24 10:17:18 +08:00
4cc9137637 Improve permission, adapter page UI 2023-09-24 09:56:06 +08:00
d145ab780c feat: fix wrong elements in getPermissionsByUser() related functions 2023-09-24 09:13:54 +08:00
687830697e Refactor getPermissionsAndRolesByUser() related code 2023-09-24 08:08:32 +08:00
111d1a5786 Use UserInfo's ID in OAuth login 2023-09-23 00:13:13 +08:00
775dd9eb57 Improve email provider error handling and fix bug 2023-09-21 23:11:58 +08:00
8f6c295c40 fix: empty AzureAD tenant id (#2349) 2023-09-21 08:34:23 +08:00
2f31e35315 feat: update casbin to 2.77.2 (#2345)
* fix: make redirect_uri really optional in logout route

* feat: update casbin to 2.77.2
2023-09-20 23:37:55 +08:00
b6d6aa9d04 Use GenerateIdForNewUser() in add-user API 2023-09-20 22:50:17 +08:00
f40d44fa1c Refactor out GenerateIdForNewUser() 2023-09-20 22:45:00 +08:00
120 changed files with 3146 additions and 1016 deletions

View File

@ -195,7 +195,7 @@ jobs:
with: with:
context: . context: .
target: STANDARD target: STANDARD
platforms: linux/amd64,linux/arm64 platforms: linux/amd64
push: true push: true
tags: casbin/casdoor:${{steps.get-current-tag.outputs.tag }},casbin/casdoor:latest tags: casbin/casdoor:${{steps.get-current-tag.outputs.tag }},casbin/casdoor:latest
@ -205,6 +205,6 @@ jobs:
with: with:
context: . context: .
target: ALLINONE target: ALLINONE
platforms: linux/amd64,linux/arm64 platforms: linux/amd64
push: true push: true
tags: casbin/casdoor-all-in-one:${{steps.get-current-tag.outputs.tag }},casbin/casdoor-all-in-one:latest 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 # ignore build result
casdoor casdoor
server_linux_arm64 server
server_linux_amd64

View File

@ -1,7 +1,6 @@
FROM node:16.18.0 AS FRONT FROM node:16.18.0 AS FRONT
WORKDIR /web WORKDIR /web
COPY ./web . COPY ./web .
RUN yarn config set registry https://registry.npmmirror.com
RUN yarn install --frozen-lockfile --network-timeout 1000000 && yarn run build 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 FROM alpine:latest AS STANDARD
LABEL MAINTAINER="https://casdoor.org/" LABEL MAINTAINER="https://casdoor.org/"
ARG USER=casdoor ARG USER=casdoor
ARG TARGETOS
ARG TARGETARCH
ENV BUILDX_ARCH="${TARGETOS:-linux}_${TARGETARCH:-amd64}"
RUN sed -i 's/https/http/' /etc/apk/repositories RUN sed -i 's/https/http/' /etc/apk/repositories
RUN apk add --update sudo RUN apk add --update sudo
@ -31,7 +27,7 @@ RUN adduser -D $USER -u 1000 \
USER 1000 USER 1000
WORKDIR / 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/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/conf/app.conf ./conf/app.conf
COPY --from=BACK --chown=$USER:$USER /go/src/casdoor/version_info.txt ./go/src/casdoor/version_info.txt 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 FROM db AS ALLINONE
LABEL MAINTAINER="https://casdoor.org/" LABEL MAINTAINER="https://casdoor.org/"
ARG TARGETOS
ARG TARGETARCH
ENV BUILDX_ARCH="${TARGETOS:-linux}_${TARGETARCH:-amd64}"
RUN apt update RUN apt update
RUN apt install -y ca-certificates && update-ca-certificates RUN apt install -y ca-certificates && update-ca-certificates
WORKDIR / 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/swagger ./swagger
COPY --from=BACK /go/src/casdoor/docker-entrypoint.sh /docker-entrypoint.sh COPY --from=BACK /go/src/casdoor/docker-entrypoint.sh /docker-entrypoint.sh
COPY --from=BACK /go/src/casdoor/conf/app.conf ./conf/app.conf 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, *, *, POST, /api/acs, *, *
p, *, *, GET, /api/saml/metadata, *, * p, *, *, GET, /api/saml/metadata, *, *
p, *, *, *, /cas, *, * p, *, *, *, /cas, *, *
p, *, *, *, /scim, *, *
p, *, *, *, /api/webauthn, *, * p, *, *, *, /api/webauthn, *, *
p, *, *, GET, /api/get-release, *, * p, *, *, GET, /api/get-release, *, *
p, *, *, GET, /api/get-default-application, *, * p, *, *, GET, /api/get-default-application, *, *
@ -126,8 +127,14 @@ func IsAllowed(subOwner string, subName string, method string, urlPath string, o
return true return true
} }
if user != nil && user.IsAdmin && (subOwner == objOwner || (objOwner == "admin")) { if user != nil {
return true if user.IsDeleted {
return false
}
if user.IsAdmin && (subOwner == objOwner || (objOwner == "admin")) {
return true
}
} }
res, err := Enforcer.Enforce(subOwner, subName, method, urlPath, objOwner, objName) res, err := Enforcer.Enforce(subOwner, subName, method, urlPath, objOwner, objName)

View File

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

View File

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

View File

@ -18,7 +18,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"strconv"
"strings" "strings"
"github.com/casdoor/casdoor/form" "github.com/casdoor/casdoor/form"
@ -119,20 +118,10 @@ func (c *ApiController) Signup() {
} }
} }
id := util.GenerateId() id, err := object.GenerateIdForNewUser(application)
if application.GetSignupItemRule("ID") == "Incremental" { if err != nil {
lastUser, err := object.GetLastUser(authForm.Organization) c.ResponseError(err.Error())
if err != nil { return
c.ResponseError(err.Error())
return
}
lastIdInt := -1
if lastUser != nil {
lastIdInt = util.ParseInt(lastUser.Id)
}
id = strconv.Itoa(lastIdInt + 1)
} }
username := authForm.Username username := authForm.Username

View File

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

View File

@ -37,6 +37,11 @@ func (c *ApiController) Enforce() {
resourceId := c.Input().Get("resourceId") resourceId := c.Input().Get("resourceId")
enforcerId := c.Input().Get("enforcerId") 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 var request object.CasbinRequest
err := json.Unmarshal(c.Ctx.Input.RequestBody, &request) err := json.Unmarshal(c.Ctx.Input.RequestBody, &request)
if err != nil { if err != nil {

View File

@ -191,7 +191,7 @@ func (c *ApiController) UpdatePolicy() {
return return
} }
affected, err := object.UpdatePolicy(id, util.CasbinToSlice(policies[0]), util.CasbinToSlice(policies[1])) affected, err := object.UpdatePolicy(id, policies[0].Ptype, util.CasbinToSlice(policies[0]), util.CasbinToSlice(policies[1]))
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
@ -210,7 +210,7 @@ func (c *ApiController) AddPolicy() {
return return
} }
affected, err := object.AddPolicy(id, util.CasbinToSlice(policy)) affected, err := object.AddPolicy(id, policy.Ptype, util.CasbinToSlice(policy))
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
@ -229,7 +229,7 @@ func (c *ApiController) RemovePolicy() {
return return
} }
affected, err := object.RemovePolicy(id, util.CasbinToSlice(policy)) affected, err := object.RemovePolicy(id, policy.Ptype, util.CasbinToSlice(policy))
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return

View File

@ -16,7 +16,6 @@ package controllers
import ( import (
"encoding/json" "encoding/json"
"fmt"
"github.com/beego/beego/utils/pagination" "github.com/beego/beego/utils/pagination"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
@ -83,11 +82,8 @@ func (c *ApiController) GetPlan() {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
if plan == nil {
c.ResponseError(fmt.Sprintf(c.T("plan:The plan: %s does not exist"), id)) if plan != nil && includeOption {
return
}
if includeOption {
options, err := object.GetPermissionsByRole(plan.Role) options, err := object.GetPermissionsByRole(plan.Role)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
@ -97,11 +93,9 @@ func (c *ApiController) GetPlan() {
for _, option := range options { for _, option := range options {
plan.Options = append(plan.Options, option.DisplayName) plan.Options = append(plan.Options, option.DisplayName)
} }
c.ResponseOk(plan)
} else {
c.ResponseOk(plan)
} }
c.ResponseOk(plan)
} }
// UpdatePlan // UpdatePlan

View File

@ -16,7 +16,6 @@ package controllers
import ( import (
"encoding/json" "encoding/json"
"fmt"
"github.com/beego/beego/utils/pagination" "github.com/beego/beego/utils/pagination"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
@ -81,10 +80,7 @@ func (c *ApiController) GetPricing() {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
if pricing == nil {
c.ResponseError(fmt.Sprintf(c.T("pricing:The pricing: %s does not exist"), id))
return
}
c.ResponseOk(pricing) c.ResponseOk(pricing)
} }

View File

@ -272,6 +272,11 @@ func (c *ApiController) UploadResource() {
return return
} }
if username == "Built-in-Untracked" {
c.ResponseOk(fileUrl, objectKey)
return
}
if createdTime == "" { if createdTime == "" {
createdTime = util.GetCurrentTime() 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)) c.ResponseError(fmt.Sprintf(c.T("saml:Application %s not found"), paramApp))
return 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.Data["xml"] = metadata
c.ServeXML() 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"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with 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 // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package object package controllers
func (syncer *Syncer) getUsers() []*User { import (
users, err := GetUsers(syncer.Organization) "strings"
if err != nil {
panic(err)
}
return users "github.com/casdoor/casdoor/scim"
} )
func (syncer *Syncer) getUserMap() ([]*User, map[string]*User, map[string]*User) { func (c *RootController) HandleScim() {
users := syncer.getUsers() path := c.Ctx.Request.URL.Path
c.Ctx.Request.URL.Path = strings.TrimPrefix(path, "/scim")
m1 := map[string]*User{} scim.Server.ServeHTTP(c.Ctx.ResponseWriter, c.Ctx.Request)
m2 := map[string]*User{}
for _, user := range users {
m1[user.Id] = user
m2[user.Name] = user
}
return users, m1, m2
} }

View File

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

View File

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

View File

@ -142,6 +142,10 @@ func (c *ApiController) SendVerificationCode() {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return 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) sendResp = object.SendVerificationCodeToEmail(organization, user, provider, remoteAddr, vform.Dest)
case object.VerifyTypePhone: case object.VerifyTypePhone:
@ -184,6 +188,10 @@ func (c *ApiController) SendVerificationCode() {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return 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 { 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)) c.ResponseError(fmt.Sprintf(c.T("verification:Phone number is invalid in your region %s"), vform.CountryCode))

View File

@ -123,7 +123,9 @@ func (a *AzureACSEmailProvider) sendEmail(e *Email) error {
bodyBuffer := bytes.NewBuffer(postBody) bodyBuffer := bytes.NewBuffer(postBody)
req, err := http.NewRequest("POST", a.Endpoint+sendEmailEndpoint+"?api-version="+apiVersion, bodyBuffer) endpoint := strings.TrimSuffix(a.Endpoint, "/")
url := fmt.Sprintf("%s/emails:send?api-version=2023-03-31", endpoint)
req, err := http.NewRequest("POST", url, bodyBuffer)
if err != nil { if err != nil {
return fmt.Errorf("error creating AzureACS API request: %s", err) return fmt.Errorf("error creating AzureACS API request: %s", err)
} }
@ -149,7 +151,7 @@ func (a *AzureACSEmailProvider) sendEmail(e *Email) error {
defer resp.Body.Close() defer resp.Body.Close()
// Response error Handling // Response error Handling
if resp.StatusCode == http.StatusBadRequest { if resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusUnauthorized {
commError := ErrorResponse{} commError := ErrorResponse{}
err = json.NewDecoder(resp.Body).Decode(&commError) err = json.NewDecoder(resp.Body).Decode(&commError)

View File

@ -18,9 +18,9 @@ type EmailProvider interface {
Send(fromAddress string, fromName, toAddress string, subject string, content string) error Send(fromAddress string, fromName, toAddress string, subject string, content string) error
} }
func GetEmailProvider(typ string, clientId string, clientSecret string, appId string, host string, port int, disableSsl bool) EmailProvider { func GetEmailProvider(typ string, clientId string, clientSecret string, host string, port int, disableSsl bool) EmailProvider {
if typ == "Azure ACS" { if typ == "Azure ACS" {
return NewAzureACSEmailProvider(appId, host) return NewAzureACSEmailProvider(clientSecret, host)
} else { } else {
return NewSmtpEmailProvider(clientId, clientSecret, host, port, typ, disableSsl) return NewSmtpEmailProvider(clientId, clientSecret, host, port, typ, disableSsl)
} }

10
go.mod
View File

@ -10,16 +10,17 @@ require (
github.com/beego/beego v1.12.12 github.com/beego/beego v1.12.12
github.com/beevik/etree v1.1.0 github.com/beevik/etree v1.1.0
github.com/casbin/casbin v1.9.1 // indirect github.com/casbin/casbin v1.9.1 // indirect
github.com/casbin/casbin/v2 v2.37.0 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/gomail/v2 v2.0.1
github.com/casdoor/notify v0.43.0 github.com/casdoor/notify v0.45.0
github.com/casdoor/oss v1.3.0 github.com/casdoor/oss v1.3.0
github.com/casdoor/xorm-adapter/v3 v3.0.4 github.com/casdoor/xorm-adapter/v3 v3.0.4
github.com/casvisor/casvisor-go-sdk v1.0.3 github.com/casvisor/casvisor-go-sdk v1.0.3
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
github.com/denisenkom/go-mssqldb v0.9.0 github.com/denisenkom/go-mssqldb v0.9.0
github.com/elazarl/go-bindata-assetfs v1.0.1 // indirect 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/fogleman/gg v1.3.0
github.com/forestmgy/ldapserver v1.1.0 github.com/forestmgy/ldapserver v1.1.0
github.com/go-git/go-git/v5 v5.6.0 github.com/go-git/go-git/v5 v5.6.0
@ -54,6 +55,7 @@ require (
github.com/stripe/stripe-go/v74 v74.29.0 github.com/stripe/stripe-go/v74 v74.29.0
github.com/tealeg/xlsx v1.0.5 github.com/tealeg/xlsx v1.0.5
github.com/thanhpk/randstr v1.0.4 github.com/thanhpk/randstr v1.0.4
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tklauser/go-sysconf v0.3.10 // indirect github.com/tklauser/go-sysconf v0.3.10 // indirect
github.com/xorm-io/builder v0.3.13 github.com/xorm-io/builder v0.3.13
github.com/xorm-io/core v0.7.4 github.com/xorm-io/core v0.7.4
@ -62,9 +64,11 @@ require (
golang.org/x/crypto v0.12.0 golang.org/x/crypto v0.12.0
golang.org/x/net v0.14.0 golang.org/x/net v0.14.0
golang.org/x/oauth2 v0.11.0 golang.org/x/oauth2 v0.11.0
golang.org/x/text v0.13.0 // indirect
google.golang.org/api v0.138.0 google.golang.org/api v0.138.0
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 gopkg.in/square/go-jose.v2 v2.6.0
layeh.com/radius v0.0.0-20221205141417-e7fbddd11d68
maunium.net/go/mautrix v0.16.0 maunium.net/go/mautrix v0.16.0
modernc.org/sqlite v1.18.2 modernc.org/sqlite v1.18.2
) )

32
go.sum
View File

@ -917,14 +917,17 @@ github.com/casbin/casbin v1.9.1 h1:ucjbS5zTrmSLtH4XogqOG920Poe6QatdXtz1FEbApeM=
github.com/casbin/casbin v1.9.1/go.mod h1:z8uPsfBJGUsnkagrt3G8QvjgTKFMBJ32UP8HpZllfog= 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.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.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.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
github.com/casdoor/go-sms-sender v0.14.0 h1:yqrzWIHUg64OYPynzF5Fr0XDuCWIWxtXIjOQAAkRKuw= github.com/casbin/casbin/v2 v2.77.2 h1:yQinn/w9x8AswiwqwtrXz93VU48R1aYTXdHEx4RI3jM=
github.com/casdoor/go-sms-sender v0.14.0/go.mod h1:cQs7qqohMJBgIVZebOCB8ko09naG1vzFJEH59VNIscs= github.com/casbin/casbin/v2 v2.77.2/go.mod h1:mzGx0hYW9/ksOSpw3wNjk3NRAroq5VMFYUQ6G43iGPk=
github.com/casdoor/go-reddit/v2 v2.1.0 h1:kIbfdJ7AA7H0uTQ8s0q4GGZqSS5V9wVE74RrXyD9XPs=
github.com/casdoor/go-reddit/v2 v2.1.0/go.mod h1:eagkvwlZ4Hcsuc/uQsLHYEulz5jN65SVSwV/AIE7zsc=
github.com/casdoor/go-sms-sender v0.15.0 h1:9SWj/jd5c7jIteTRUrqbkpWbtIXMDv+t1CEfDhO06m0=
github.com/casdoor/go-sms-sender v0.15.0/go.mod h1:cQs7qqohMJBgIVZebOCB8ko09naG1vzFJEH59VNIscs=
github.com/casdoor/gomail/v2 v2.0.1 h1:J+FG6x80s9e5lBHUn8Sv0Y56mud34KiWih5YdmudR/w= github.com/casdoor/gomail/v2 v2.0.1 h1:J+FG6x80s9e5lBHUn8Sv0Y56mud34KiWih5YdmudR/w=
github.com/casdoor/gomail/v2 v2.0.1/go.mod h1:VnGPslEAtpix5FjHisR/WKB1qvZDBaujbikxDe9d+2Q= 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.45.0 h1:OlaFvcQFjGOgA4mRx07M8AH1gvb5xNo21mcqrVGlLgk=
github.com/casdoor/notify v0.43.0/go.mod h1:qDmQM5vr2uU01BEuDC6pY6ryahSU11cXPqlHFW232Do= github.com/casdoor/notify v0.45.0/go.mod h1:wNHQu0tiDROMBIvz0j3Om3Lhd5yZ+AIfnFb8MYb8OLQ=
github.com/casdoor/oss v1.3.0 h1:D5pcz65tJRqJrWY11Ks7D9LUsmlhqqMHugjDhSxWTvk= github.com/casdoor/oss v1.3.0 h1:D5pcz65tJRqJrWY11Ks7D9LUsmlhqqMHugjDhSxWTvk=
github.com/casdoor/oss v1.3.0/go.mod h1:YOi6KpG1pZHTkiy9AYaqI0UaPfE7YkaA07d89f1idqY= github.com/casdoor/oss v1.3.0/go.mod h1:YOi6KpG1pZHTkiy9AYaqI0UaPfE7YkaA07d89f1idqY=
github.com/casdoor/xorm-adapter/v3 v3.0.4 h1:vB04Ao8n2jA7aFBI9F+gGXo9+Aa1IQP6mTdo50913DM= github.com/casdoor/xorm-adapter/v3 v3.0.4 h1:vB04Ao8n2jA7aFBI9F+gGXo9+Aa1IQP6mTdo50913DM=
@ -1009,6 +1012,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/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-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/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/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 h1:uh1GSejOhVPRQmoXZxY82TiewZB8QXiaP1skL7Nun3Y=
github.com/drswork/go-twitter v0.0.0-20221107160839-dea1b6ed53d7/go.mod h1:ncTaGuXc5v7AuiVekeJ0Nwh8Bf4cudukoj0qM/15UZE= github.com/drswork/go-twitter v0.0.0-20221107160839-dea1b6ed53d7/go.mod h1:ncTaGuXc5v7AuiVekeJ0Nwh8Bf4cudukoj0qM/15UZE=
@ -1025,6 +1032,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.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 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= 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 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 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= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@ -1692,6 +1701,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/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 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 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/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/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= github.com/sendgrid/sendgrid-go v3.13.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
@ -1793,8 +1804,9 @@ 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/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 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 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.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=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw= github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw=
@ -1819,8 +1831,6 @@ github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljT
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/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 h1:E1tJaB/XhqRY/iz203FD0MaHm10DjQPOq5/Mem2A3Gs=
github.com/utahta/go-linenotify v0.5.0/go.mod h1:KsvBXil2wx+ByaCR0e+IZKTbp4pDesc7yjzRigLf6pE= 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/volcengine/volc-sdk-golang v1.0.117 h1:ykFVSwsVq9qvIoWP9jeP+VKNAUjrblAdsZl46yVWiH8= 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/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= github.com/wendal/errors v0.0.0-20181209125328-7f31f4b264ec/go.mod h1:Q12BUT7DqIlHRmgv3RskH+UCM/4eqVMgI0EMmlSpAXc=
@ -1908,6 +1918,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@ -2296,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.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.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.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.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-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-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -2764,6 +2776,8 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
layeh.com/radius v0.0.0-20221205141417-e7fbddd11d68 h1:2NDro2Jzkrqfngy/sA5GVnChs7fx8EzcQKFi/lI2cfg=
layeh.com/radius v0.0.0-20221205141417-e7fbddd11d68/go.mod h1:pFWM9De99EY9TPVyHIyA56QmoRViVck/x41WFkUlc9A=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=

View File

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

View File

@ -15,6 +15,7 @@
"tags": [], "tags": [],
"languages": ["en", "zh", "es", "fr", "de", "id", "ja", "ko", "ru", "vi", "it", "ms", "tr","ar", "he", "nl", "pl", "fi", "sv", "uk", "kk", "fa"], "languages": ["en", "zh", "es", "fr", "de", "id", "ja", "ko", "ru", "vi", "it", "ms", "tr","ar", "he", "nl", "pl", "fi", "sv", "uk", "kk", "fa"],
"masterPassword": "", "masterPassword": "",
"defaultPassword": "",
"initScore": 2000, "initScore": 2000,
"enableSoftDeletion": false, "enableSoftDeletion": false,
"isProfilePublic": true, "isProfilePublic": true,
@ -176,9 +177,7 @@
], ],
"permissions": [ "permissions": [
{ {
"actions": [ "actions": [],
""
],
"displayName": "", "displayName": "",
"effect": "", "effect": "",
"isEnabled": true, "isEnabled": true,
@ -186,15 +185,9 @@
"name": "", "name": "",
"owner": "", "owner": "",
"resourceType": "", "resourceType": "",
"resources": [ "resources": [],
"" "roles": [],
], "users": []
"roles": [
""
],
"users": [
""
]
} }
], ],
"payments": [ "payments": [
@ -236,9 +229,7 @@
"name": "", "name": "",
"owner": "", "owner": "",
"price": 0, "price": 0,
"providers": [ "providers": [],
""
],
"quantity": 0, "quantity": 0,
"returnUrl": "", "returnUrl": "",
"sold": 0, "sold": 0,
@ -268,12 +259,8 @@
"isEnabled": true, "isEnabled": true,
"name": "", "name": "",
"owner": "", "owner": "",
"roles": [ "roles": [],
"" "users": []
],
"users": [
""
]
} }
], ],
"syncers": [ "syncers": [
@ -284,7 +271,7 @@
"databaseType": "", "databaseType": "",
"errorText": "", "errorText": "",
"host": "", "host": "",
"isEnabled": true, "isEnabled": false,
"name": "", "name": "",
"organization": "", "organization": "",
"owner": "", "owner": "",
@ -298,9 +285,7 @@
"isHashed": true, "isHashed": true,
"name": "", "name": "",
"type": "", "type": "",
"values": [ "values": []
""
]
} }
], ],
"tablePrimaryKey": "", "tablePrimaryKey": "",
@ -330,9 +315,7 @@
"webhooks": [ "webhooks": [
{ {
"contentType": "", "contentType": "",
"events": [ "events": [],
""
],
"headers": [ "headers": [
{ {
"name": "", "name": "",

View File

@ -34,7 +34,7 @@ func StartLdapServer() {
server.Handle(routes) server.Handle(routes)
err := server.ListenAndServe("0.0.0.0:" + conf.GetConfigString("ldapServerPort")) err := server.ListenAndServe("0.0.0.0:" + conf.GetConfigString("ldapServerPort"))
if err != nil { if err != nil {
log.Printf("StartLdapServer() failed, ErrMsg = %s", err.Error()) log.Printf("StartLdapServer() failed, err = %s", err.Error())
} }
} }

View File

@ -117,7 +117,7 @@ func GetFilteredUsers(m *ldap.Message) (filteredUsers []*object.User, code int)
hasPermission, err := object.CheckUserPermission(requestUserId, userId, true, "en") hasPermission, err := object.CheckUserPermission(requestUserId, userId, true, "en")
if !hasPermission { if !hasPermission {
log.Printf("ErrMsg = %v", err.Error()) log.Printf("err = %v", err.Error())
return nil, ldap.LDAPResultInsufficientAccessRights return nil, ldap.LDAPResultInsufficientAccessRights
} }

View File

@ -25,6 +25,7 @@ import (
"github.com/casdoor/casdoor/ldap" "github.com/casdoor/casdoor/ldap"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/proxy" "github.com/casdoor/casdoor/proxy"
"github.com/casdoor/casdoor/radius"
"github.com/casdoor/casdoor/routers" "github.com/casdoor/casdoor/routers"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
) )
@ -81,6 +82,7 @@ func main() {
logs.SetLogFuncCall(false) logs.SetLogFuncCall(false)
go ldap.StartLdapServer() go ldap.StartLdapServer()
go radius.StartRadiusServer()
go object.ClearThroughputPerSecond() go object.ClearThroughputPerSecond()
beego.Run(fmt.Sprintf(":%v", port)) beego.Run(fmt.Sprintf(":%v", port))

View File

@ -27,9 +27,10 @@ config: |
authState = "casdoor" authState = "casdoor"
socks5Proxy = "" socks5Proxy = ""
verificationCodeTimeout = 10 verificationCodeTimeout = 10
initScore = 2000 initScore = 0
logPostOnly = true logPostOnly = true
origin = "https://door.casbin.com" origin =
enableGzip = true
imagePullSecrets: [] imagePullSecrets: []
nameOverride: "" nameOverride: ""

View File

@ -21,7 +21,7 @@ import (
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
) )
func NewMatrixProvider(userId string, roomId string, accessToken string, homeServer string) (*notify.Notify, error) { func NewMatrixProvider(userId string, accessToken string, roomId string, homeServer string) (*notify.Notify, error) {
matrixSrv, err := matrix.New(id.UserID(userId), id.RoomID(roomId), homeServer, accessToken) matrixSrv, err := matrix.New(id.UserID(userId), id.RoomID(roomId), homeServer, accessToken)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -18,27 +18,27 @@ import "github.com/casdoor/notify"
func GetNotificationProvider(typ string, clientId string, clientSecret string, clientId2 string, clientSecret2 string, appId string, receiver string, method string, title string, metaData string) (notify.Notifier, error) { func GetNotificationProvider(typ string, clientId string, clientSecret string, clientId2 string, clientSecret2 string, appId string, receiver string, method string, title string, metaData string) (notify.Notifier, error) {
if typ == "Telegram" { if typ == "Telegram" {
return NewTelegramProvider(appId, receiver) return NewTelegramProvider(clientSecret, receiver)
} else if typ == "Custom HTTP" { } else if typ == "Custom HTTP" {
return NewCustomHttpProvider(receiver, method, title) return NewCustomHttpProvider(receiver, method, title)
} else if typ == "DingTalk" { } else if typ == "DingTalk" {
return NewDingTalkProvider(appId, receiver) return NewDingTalkProvider(clientId, clientSecret)
} else if typ == "Lark" { } else if typ == "Lark" {
return NewLarkProvider(receiver) return NewLarkProvider(clientSecret)
} else if typ == "Microsoft Teams" { } else if typ == "Microsoft Teams" {
return NewMicrosoftTeamsProvider(receiver) return NewMicrosoftTeamsProvider(clientSecret)
} else if typ == "Bark" { } else if typ == "Bark" {
return NewBarkProvider(receiver) return NewBarkProvider(clientSecret)
} else if typ == "Pushover" { } else if typ == "Pushover" {
return NewPushoverProvider(appId, receiver) return NewPushoverProvider(clientSecret, receiver)
} else if typ == "Pushbullet" { } else if typ == "Pushbullet" {
return NewPushbulletProvider(appId, receiver) return NewPushbulletProvider(clientSecret, receiver)
} else if typ == "Slack" { } else if typ == "Slack" {
return NewSlackProvider(appId, receiver) return NewSlackProvider(clientSecret, receiver)
} else if typ == "Webpush" { } else if typ == "Webpush" {
return NewWebpushProvider(clientId, clientSecret, receiver) return NewWebpushProvider(clientId, clientSecret, receiver)
} else if typ == "Discord" { } else if typ == "Discord" {
return NewDiscordProvider(appId, receiver) return NewDiscordProvider(clientSecret, receiver)
} else if typ == "Google Chat" { } else if typ == "Google Chat" {
return NewGoogleChatProvider(metaData) return NewGoogleChatProvider(metaData)
} else if typ == "Line" { } else if typ == "Line" {

View File

@ -30,15 +30,15 @@ type Adapter struct {
Name string `xorm:"varchar(100) notnull pk" json:"name"` Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"` CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
Type string `xorm:"varchar(100)" json:"type"` Table string `xorm:"varchar(100)" json:"table"`
DatabaseType string `xorm:"varchar(100)" json:"databaseType"` UseSameDb bool `json:"useSameDb"`
Host string `xorm:"varchar(100)" json:"host"` Type string `xorm:"varchar(100)" json:"type"`
Port int `json:"port"` DatabaseType string `xorm:"varchar(100)" json:"databaseType"`
User string `xorm:"varchar(100)" json:"user"` Host string `xorm:"varchar(100)" json:"host"`
Password string `xorm:"varchar(100)" json:"password"` Port int `json:"port"`
Database string `xorm:"varchar(100)" json:"database"` User string `xorm:"varchar(100)" json:"user"`
Table string `xorm:"varchar(100)" json:"table"` Password string `xorm:"varchar(100)" json:"password"`
TableNamePrefix string `xorm:"varchar(100)" json:"tableNamePrefix"` Database string `xorm:"varchar(100)" json:"database"`
*xormadapter.Adapter `xorm:"-" json:"-"` *xormadapter.Adapter `xorm:"-" json:"-"`
} }
@ -139,63 +139,69 @@ func (adapter *Adapter) GetId() string {
return fmt.Sprintf("%s/%s", adapter.Owner, adapter.Name) return fmt.Sprintf("%s/%s", adapter.Owner, adapter.Name)
} }
func (adapter *Adapter) getTable() string {
if adapter.DatabaseType == "mssql" {
return fmt.Sprintf("[%s]", adapter.Table)
} else {
return adapter.Table
}
}
func (adapter *Adapter) InitAdapter() error { func (adapter *Adapter) InitAdapter() error {
if adapter.Adapter == nil { if adapter.Adapter != nil {
var dataSourceName string return nil
}
if adapter.isBuiltIn() { var driverName string
dataSourceName = conf.GetConfigString("dataSourceName") var dataSourceName string
if adapter.DatabaseType == "mysql" { if adapter.UseSameDb || adapter.isBuiltIn() {
dataSourceName = dataSourceName + adapter.Database driverName = conf.GetConfigString("driverName")
} dataSourceName = conf.GetConfigString("dataSourceName")
} else { if conf.GetConfigString("driverName") == "mysql" {
switch adapter.DatabaseType { dataSourceName = dataSourceName + conf.GetConfigString("dbName")
case "mssql":
dataSourceName = fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s", adapter.User,
adapter.Password, adapter.Host, adapter.Port, adapter.Database)
case "mysql":
dataSourceName = fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", adapter.User,
adapter.Password, adapter.Host, adapter.Port, adapter.Database)
case "postgres":
dataSourceName = fmt.Sprintf("user=%s password=%s host=%s port=%d sslmode=disable dbname=%s", adapter.User,
adapter.Password, adapter.Host, adapter.Port, adapter.Database)
case "CockroachDB":
dataSourceName = fmt.Sprintf("user=%s password=%s host=%s port=%d sslmode=disable dbname=%s serial_normalization=virtual_sequence",
adapter.User, adapter.Password, adapter.Host, adapter.Port, adapter.Database)
case "sqlite3":
dataSourceName = fmt.Sprintf("file:%s", adapter.Host)
default:
return fmt.Errorf("unsupported database type: %s", adapter.DatabaseType)
}
} }
} else {
if !isCloudIntranet { driverName = adapter.DatabaseType
dataSourceName = strings.ReplaceAll(dataSourceName, "dbi.", "db.") switch driverName {
} case "mssql":
dataSourceName = fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s", adapter.User,
var err error adapter.Password, adapter.Host, adapter.Port, adapter.Database)
engine, err := xorm.NewEngine(adapter.DatabaseType, dataSourceName) case "mysql":
dataSourceName = fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", adapter.User,
if adapter.isBuiltIn() && adapter.DatabaseType == "postgres" { adapter.Password, adapter.Host, adapter.Port, adapter.Database)
schema := util.GetValueFromDataSourceName("search_path", dataSourceName) case "postgres":
if schema != "" { dataSourceName = fmt.Sprintf("user=%s password=%s host=%s port=%d sslmode=disable dbname=%s", adapter.User,
engine.SetSchema(schema) adapter.Password, adapter.Host, adapter.Port, adapter.Database)
} case "CockroachDB":
} dataSourceName = fmt.Sprintf("user=%s password=%s host=%s port=%d sslmode=disable dbname=%s serial_normalization=virtual_sequence",
adapter.User, adapter.Password, adapter.Host, adapter.Port, adapter.Database)
adapter.Adapter, err = xormadapter.NewAdapterByEngineWithTableName(engine, adapter.getTable(), adapter.TableNamePrefix) case "sqlite3":
if err != nil { dataSourceName = fmt.Sprintf("file:%s", adapter.Host)
return err default:
return fmt.Errorf("unsupported database type: %s", adapter.DatabaseType)
} }
} }
if !isCloudIntranet {
dataSourceName = strings.ReplaceAll(dataSourceName, "dbi.", "db.")
}
engine, err := xorm.NewEngine(driverName, dataSourceName)
if err != nil {
return err
}
if (adapter.UseSameDb || adapter.isBuiltIn()) && driverName == "postgres" {
schema := util.GetValueFromDataSourceName("search_path", dataSourceName)
if schema != "" {
engine.SetSchema(schema)
}
}
var tableName string
if driverName == "mssql" {
tableName = fmt.Sprintf("[%s]", adapter.Table)
} else {
tableName = adapter.Table
}
adapter.Adapter, err = xormadapter.NewAdapterByEngineWithTableName(engine, tableName, "")
if err != nil {
return err
}
return nil return nil
} }

View File

@ -25,11 +25,19 @@ import (
) )
type SignupItem struct { type SignupItem struct {
Name string `json:"name"` Name string `json:"name"`
Visible bool `json:"visible"` Visible bool `json:"visible"`
Required bool `json:"required"` Required bool `json:"required"`
Prompted bool `json:"prompted"` Prompted bool `json:"prompted"`
Rule string `json:"rule"` 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 { type Application struct {
@ -49,17 +57,19 @@ type Application struct {
EnableAutoSignin bool `json:"enableAutoSignin"` EnableAutoSignin bool `json:"enableAutoSignin"`
EnableCodeSignin bool `json:"enableCodeSignin"` EnableCodeSignin bool `json:"enableCodeSignin"`
EnableSamlCompress bool `json:"enableSamlCompress"` EnableSamlCompress bool `json:"enableSamlCompress"`
EnableSamlC14n10 bool `json:"enableSamlC14n10"`
EnableWebAuthn bool `json:"enableWebAuthn"` EnableWebAuthn bool `json:"enableWebAuthn"`
EnableLinkWithEmail bool `json:"enableLinkWithEmail"` EnableLinkWithEmail bool `json:"enableLinkWithEmail"`
OrgChoiceMode string `json:"orgChoiceMode"` OrgChoiceMode string `json:"orgChoiceMode"`
SamlReplyUrl string `xorm:"varchar(100)" json:"samlReplyUrl"` SamlReplyUrl string `xorm:"varchar(100)" json:"samlReplyUrl"`
Providers []*ProviderItem `xorm:"mediumtext" json:"providers"` 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"` GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"`
OrganizationObj *Organization `xorm:"-" json:"organizationObj"` OrganizationObj *Organization `xorm:"-" json:"organizationObj"`
CertPublicKey string `xorm:"-" json:"certPublicKey"` CertPublicKey string `xorm:"-" json:"certPublicKey"`
Tags []string `xorm:"mediumtext" json:"tags"` Tags []string `xorm:"mediumtext" json:"tags"`
InvitationCodes []string `xorm:"varchar(200)" json:"invitationCodes"` InvitationCodes []string `xorm:"varchar(200)" json:"invitationCodes"`
SamlAttributes []*SamlItem `xorm:"varchar(1000)" json:"samlAttributes"`
ClientId string `xorm:"varchar(100)" json:"clientId"` ClientId string `xorm:"varchar(100)" json:"clientId"`
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"` ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
@ -306,6 +316,9 @@ func GetMaskedApplication(application *Application, userId string) *Application
if application.OrganizationObj.MasterPassword != "" { if application.OrganizationObj.MasterPassword != "" {
application.OrganizationObj.MasterPassword = "***" application.OrganizationObj.MasterPassword = "***"
} }
if application.OrganizationObj.DefaultPassword != "" {
application.OrganizationObj.DefaultPassword = "***"
}
if application.OrganizationObj.PasswordType != "" { if application.OrganizationObj.PasswordType != "" {
application.OrganizationObj.PasswordType = "***" application.OrganizationObj.PasswordType = "***"
} }
@ -428,7 +441,7 @@ func (application *Application) GetId() string {
} }
func (application *Application) IsRedirectUriValid(redirectUri string) bool { func (application *Application) IsRedirectUriValid(redirectUri string) bool {
redirectUris := append([]string{"http://localhost:"}, application.RedirectUris...) redirectUris := append([]string{"http://localhost:", "https://localhost:", "http://127.0.0.1:", "http://casdoor-app"}, application.RedirectUris...)
for _, targetUri := range redirectUris { for _, targetUri := range redirectUris {
targetUriRegex := regexp.MustCompile(targetUri) targetUriRegex := regexp.MustCompile(targetUri)
if targetUriRegex.MatchString(redirectUri) || strings.Contains(redirectUri, 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 return false, err
} }
allowPermissionCount := 0
denyPermissionCount := 0
allowCount := 0 allowCount := 0
denyCount := 0 denyCount := 0
for _, permission := range permissions { for _, permission := range permissions {
@ -368,11 +370,19 @@ func CheckLoginPermission(userId string, application *Application) (bool, error)
continue continue
} }
if permission.isUserHit(userId) { if !permission.isUserHit(userId) && !permission.isRoleHit(userId) {
allowCount += 1 if permission.Effect == "Allow" {
allowPermissionCount += 1
} else {
denyPermissionCount += 1
}
continue
} }
enforcer := getPermissionEnforcer(permission) enforcer, err := getPermissionEnforcer(permission)
if err != nil {
return false, err
}
var isAllowed bool var isAllowed bool
isAllowed, err = enforcer.Enforce(userId, application.Name, "Read") isAllowed, err = enforcer.Enforce(userId, application.Name, "Read")
@ -391,8 +401,18 @@ func CheckLoginPermission(userId string, application *Application) (bool, error)
} }
} }
// Deny-override, if one deny is found, then deny
if denyCount > 0 { if denyCount > 0 {
return false, nil 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 return true, nil
} }

View File

@ -36,7 +36,7 @@ func getDialer(provider *Provider) *gomail.Dialer {
} }
func SendEmail(provider *Provider, title string, content string, dest string, sender string) error { func SendEmail(provider *Provider, title string, content string, dest string, sender string) error {
emailProvider := email.GetEmailProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.AppId, provider.Host, provider.Port, provider.DisableSsl) emailProvider := email.GetEmailProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, provider.DisableSsl)
fromAddress := provider.ClientId2 fromAddress := provider.ClientId2
if fromAddress == "" { if fromAddress == "" {

View File

@ -18,7 +18,6 @@ import (
"fmt" "fmt"
"github.com/casbin/casbin/v2" "github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v2/config"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
xormadapter "github.com/casdoor/xorm-adapter/v3" xormadapter "github.com/casdoor/xorm-adapter/v3"
"github.com/xorm-io/core" "github.com/xorm-io/core"
@ -191,39 +190,55 @@ func GetPolicies(id string) ([]*xormadapter.CasbinRule, error) {
return nil, err return nil, err
} }
policies := util.MatrixToCasbinRules("p", enforcer.GetPolicy()) pRules := enforcer.GetPolicy()
res := util.MatrixToCasbinRules("p", pRules)
if enforcer.GetModel()["g"] != nil { if enforcer.GetModel()["g"] != nil {
policies = append(policies, util.MatrixToCasbinRules("g", enforcer.GetGroupingPolicy())...) gRules := enforcer.GetGroupingPolicy()
res2 := util.MatrixToCasbinRules("g", gRules)
res = append(res, res2...)
} }
return policies, nil return res, nil
} }
func UpdatePolicy(id string, oldPolicy, newPolicy []string) (bool, error) { func UpdatePolicy(id string, ptype string, oldPolicy []string, newPolicy []string) (bool, error) {
enforcer, err := GetInitializedEnforcer(id) enforcer, err := GetInitializedEnforcer(id)
if err != nil { if err != nil {
return false, err return false, err
} }
return enforcer.UpdatePolicy(oldPolicy, newPolicy) if ptype == "p" {
return enforcer.UpdatePolicy(oldPolicy, newPolicy)
} else {
return enforcer.UpdateGroupingPolicy(oldPolicy, newPolicy)
}
} }
func AddPolicy(id string, policy []string) (bool, error) { func AddPolicy(id string, ptype string, policy []string) (bool, error) {
enforcer, err := GetInitializedEnforcer(id) enforcer, err := GetInitializedEnforcer(id)
if err != nil { if err != nil {
return false, err return false, err
} }
return enforcer.AddPolicy(policy) if ptype == "p" {
return enforcer.AddPolicy(policy)
} else {
return enforcer.AddGroupingPolicy(policy)
}
} }
func RemovePolicy(id string, policy []string) (bool, error) { func RemovePolicy(id string, ptype string, policy []string) (bool, error) {
enforcer, err := GetInitializedEnforcer(id) enforcer, err := GetInitializedEnforcer(id)
if err != nil { if err != nil {
return false, err return false, err
} }
return enforcer.RemovePolicy(policy) if ptype == "p" {
return enforcer.RemovePolicy(policy)
} else {
return enforcer.RemoveGroupingPolicy(policy)
}
} }
func (enforcer *Enforcer) LoadModelCfg() error { func (enforcer *Enforcer) LoadModelCfg() error {
@ -231,23 +246,17 @@ func (enforcer *Enforcer) LoadModelCfg() error {
return nil return nil
} }
model, err := GetModel(enforcer.Model) model, err := GetModelEx(enforcer.Model)
if err != nil { if err != nil {
return err return err
} else if model == nil { } else if model == nil {
return fmt.Errorf("the model: %s for enforcer: %s is not found", enforcer.Model, enforcer.GetId()) 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 { if err != nil {
return err 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 return nil
} }

View File

@ -226,7 +226,7 @@ func GetGroupUserCount(groupId string, field, value string) (int64, error) {
} else { } else {
return ormer.Engine.Table("user"). return ormer.Engine.Table("user").
Where("owner = ?", owner).In("name", names). Where("owner = ?", owner).In("name", names).
And(fmt.Sprintf("user.%s LIKE ?", util.CamelToSnakeCase(field)), "%"+value+"%"). And(fmt.Sprintf("user.%s like ?", util.CamelToSnakeCase(field)), "%"+value+"%").
Count() Count()
} }
} }
@ -247,7 +247,7 @@ func GetPaginationGroupUsers(groupId string, offset, limit int, field, value, so
} }
if field != "" && value != "" { if field != "" && value != "" {
session = session.And(fmt.Sprintf("user.%s LIKE ?", util.CamelToSnakeCase(field)), "%"+value+"%") session = session.And(fmt.Sprintf("user.%s like ?", util.CamelToSnakeCase(field)), "%"+value+"%")
} }
if sortField == "" || sortOrder == "" { if sortField == "" || sortOrder == "" {

View File

@ -178,7 +178,7 @@ func initBuiltInApplication() {
EnablePassword: true, EnablePassword: true,
EnableSignUp: true, EnableSignUp: true,
Providers: []*ProviderItem{ Providers: []*ProviderItem{
{Name: "provider_captcha_default", CanSignUp: false, CanSignIn: false, CanUnlink: false, Prompted: false, AlertType: "None", Rule: "None", Provider: nil}, {Name: "provider_captcha_default", CanSignUp: false, CanSignIn: false, CanUnlink: false, Prompted: false, SignupGroup: "", Rule: "None", Provider: nil},
}, },
SignupItems: []*SignupItem{ SignupItems: []*SignupItem{
{Name: "ID", Visible: false, Required: true, Prompted: false, Rule: "Random"}, {Name: "ID", Visible: false, Required: true, Prompted: false, Rule: "Random"},
@ -423,14 +423,11 @@ func initBuiltInUserAdapter() {
} }
adapter = &Adapter{ adapter = &Adapter{
Owner: "built-in", Owner: "built-in",
Name: "user-adapter-built-in", Name: "user-adapter-built-in",
CreatedTime: util.GetCurrentTime(), CreatedTime: util.GetCurrentTime(),
Type: "Database", Table: "casbin_user_rule",
DatabaseType: conf.GetConfigString("driverName"), UseSameDb: true,
TableNamePrefix: conf.GetConfigString("tableNamePrefix"),
Database: conf.GetConfigString("dbName"),
Table: "casbin_user_rule",
} }
_, err = AddAdapter(adapter) _, err = AddAdapter(adapter)
if err != nil { if err != nil {
@ -449,14 +446,11 @@ func initBuiltInApiAdapter() {
} }
adapter = &Adapter{ adapter = &Adapter{
Owner: "built-in", Owner: "built-in",
Name: "api-adapter-built-in", Name: "api-adapter-built-in",
CreatedTime: util.GetCurrentTime(), CreatedTime: util.GetCurrentTime(),
Type: "Database", Table: "casbin_api_rule",
DatabaseType: conf.GetConfigString("driverName"), UseSameDb: true,
TableNamePrefix: conf.GetConfigString("tableNamePrefix"),
Database: conf.GetConfigString("dbName"),
Table: "casbin_api_rule",
} }
_, err = AddAdapter(adapter) _, err = AddAdapter(adapter)
if err != nil { if err != nil {

View File

@ -17,6 +17,7 @@ package object
import ( import (
"fmt" "fmt"
"github.com/casbin/casbin/v2/config"
"github.com/casbin/casbin/v2/model" "github.com/casbin/casbin/v2/model"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/xorm-io/core" "github.com/xorm-io/core"
@ -83,6 +84,19 @@ func GetModel(id string) (*Model, error) {
return getModel(owner, name) 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 { func UpdateModelWithCheck(id string, modelObj *Model) error {
// check model grammar // check model grammar
_, err := model.NewModelFromString(modelObj.ModelText) _, err := model.NewModelFromString(modelObj.ModelText)
@ -188,3 +202,17 @@ func (m *Model) initModel() error {
return nil 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 return ip != nil
} }
func getOriginFromHost(host string) (string, string) { func getOriginFromHostInternal(host string) (string, string) {
origin := conf.GetConfigString("origin") origin := conf.GetConfigString("origin")
if origin != "" { if origin != "" {
return origin, 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 { func GetOidcDiscovery(host string) OidcDiscovery {
originFrontend, originBackend := getOriginFromHost(host) originFrontend, originBackend := getOriginFromHost(host)
@ -127,9 +138,16 @@ func GetJsonWebKeySet() (jose.JSONWebKeySet, error) {
continue continue
} }
if cert.Certificate == "" {
return jwks, fmt.Errorf("the certificate field should not be empty for the cert: %v", cert)
}
certPemBlock := []byte(cert.Certificate) certPemBlock := []byte(cert.Certificate)
certDerBlock, _ := pem.Decode(certPemBlock) 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 var jwk jose.JSONWebKey
jwk.Key = x509Cert.PublicKey jwk.Key = x509Cert.PublicKey

View File

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

View File

@ -86,7 +86,11 @@ func InitAdapter() {
} }
} }
ormer = NewAdapter(conf.GetConfigString("driverName"), conf.GetConfigDataSourceName(), conf.GetConfigString("dbName")) var err error
ormer, err = NewAdapter(conf.GetConfigString("driverName"), conf.GetConfigDataSourceName(), conf.GetConfigString("dbName"))
if err != nil {
panic(err)
}
tableNamePrefix := conf.GetConfigString("tableNamePrefix") tableNamePrefix := conf.GetConfigString("tableNamePrefix")
tbMapper := core.NewPrefixMapper(core.SnakeMapper{}, tableNamePrefix) tbMapper := core.NewPrefixMapper(core.SnakeMapper{}, tableNamePrefix)
@ -121,19 +125,22 @@ func finalizer(a *Ormer) {
} }
// NewAdapter is the constructor for Ormer. // NewAdapter is the constructor for Ormer.
func NewAdapter(driverName string, dataSourceName string, dbName string) *Ormer { func NewAdapter(driverName string, dataSourceName string, dbName string) (*Ormer, error) {
a := &Ormer{} a := &Ormer{}
a.driverName = driverName a.driverName = driverName
a.dataSourceName = dataSourceName a.dataSourceName = dataSourceName
a.dbName = dbName a.dbName = dbName
// Open the DB, create it if not existed. // Open the DB, create it if not existed.
a.open() err := a.open()
if err != nil {
return nil, err
}
// Call the destructor when the object is released. // Call the destructor when the object is released.
runtime.SetFinalizer(a, finalizer) runtime.SetFinalizer(a, finalizer)
return a return a, nil
} }
func refineDataSourceNameForPostgres(dataSourceName string) string { func refineDataSourceNameForPostgres(dataSourceName string) string {
@ -192,7 +199,7 @@ func (a *Ormer) CreateDatabase() error {
return err return err
} }
func (a *Ormer) open() { func (a *Ormer) open() error {
dataSourceName := a.dataSourceName + a.dbName dataSourceName := a.dataSourceName + a.dbName
if a.driverName != "mysql" { if a.driverName != "mysql" {
dataSourceName = a.dataSourceName dataSourceName = a.dataSourceName
@ -200,8 +207,9 @@ func (a *Ormer) open() {
engine, err := xorm.NewEngine(a.driverName, dataSourceName) engine, err := xorm.NewEngine(a.driverName, dataSourceName)
if err != nil { if err != nil {
panic(err) return err
} }
if a.driverName == "postgres" { if a.driverName == "postgres" {
schema := util.GetValueFromDataSourceName("search_path", dataSourceName) schema := util.GetValueFromDataSourceName("search_path", dataSourceName)
if schema != "" { if schema != "" {
@ -210,6 +218,7 @@ func (a *Ormer) open() {
} }
a.Engine = engine a.Engine = engine
return nil
} }
func (a *Ormer) close() { func (a *Ormer) close() {
@ -316,6 +325,11 @@ func (a *Ormer) createTable() {
panic(err) panic(err)
} }
err = a.Engine.Sync2(new(RadiusAccounting))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(PermissionRule)) err = a.Engine.Sync2(new(PermissionRule))
if err != nil { if err != nil {
panic(err) panic(err)

View File

@ -15,6 +15,7 @@
package object package object
import ( import (
"fmt"
"strings" "strings"
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
@ -112,11 +113,15 @@ func GetPermission(id string) (*Permission, error) {
// checkPermissionValid verifies if the permission is valid // checkPermissionValid verifies if the permission is valid
func checkPermissionValid(permission *Permission) error { func checkPermissionValid(permission *Permission) error {
enforcer := getPermissionEnforcer(permission) enforcer, err := getPermissionEnforcer(permission)
if err != nil {
return err
}
enforcer.EnableAutoSave(false) enforcer.EnableAutoSave(false)
policies := getPolicies(permission) policies := getPolicies(permission)
_, err := enforcer.AddPolicies(policies) _, err = enforcer.AddPolicies(policies)
if err != nil { if err != nil {
return err return err
} }
@ -128,7 +133,7 @@ func checkPermissionValid(permission *Permission) error {
groupingPolicies := getGroupingPolicies(permission) groupingPolicies := getGroupingPolicies(permission)
if len(groupingPolicies) > 0 { if len(groupingPolicies) > 0 {
_, err := enforcer.AddGroupingPolicies(groupingPolicies) _, err = enforcer.AddGroupingPolicies(groupingPolicies)
if err != nil { if err != nil {
return err return err
} }
@ -149,14 +154,40 @@ func UpdatePermission(id string, permission *Permission) (bool, error) {
return false, nil return false, nil
} }
if permission.ResourceType == "Application" && permission.Model != "" {
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) affected, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(permission)
if err != nil { if err != nil {
return false, err return false, err
} }
if affected != 0 { if affected != 0 {
removeGroupingPolicies(oldPermission) err = removeGroupingPolicies(oldPermission)
removePolicies(oldPermission) if err != nil {
return false, err
}
err = removePolicies(oldPermission)
if err != nil {
return false, err
}
if oldPermission.Adapter != "" && oldPermission.Adapter != permission.Adapter { if oldPermission.Adapter != "" && oldPermission.Adapter != permission.Adapter {
isEmpty, _ := ormer.Engine.IsTableEmpty(oldPermission.Adapter) isEmpty, _ := ormer.Engine.IsTableEmpty(oldPermission.Adapter)
if isEmpty { if isEmpty {
@ -166,8 +197,16 @@ func UpdatePermission(id string, permission *Permission) (bool, error) {
} }
} }
} }
addGroupingPolicies(permission)
addPolicies(permission) err = addGroupingPolicies(permission)
if err != nil {
return false, err
}
err = addPolicies(permission)
if err != nil {
return false, err
}
} }
return affected != 0, nil return affected != 0, nil
@ -180,59 +219,78 @@ func AddPermission(permission *Permission) (bool, error) {
} }
if affected != 0 { if affected != 0 {
addGroupingPolicies(permission) err = addGroupingPolicies(permission)
addPolicies(permission) if err != nil {
return false, err
}
err = addPolicies(permission)
if err != nil {
return false, err
}
} }
return affected != 0, nil return affected != 0, nil
} }
func AddPermissions(permissions []*Permission) bool { func AddPermissions(permissions []*Permission) (bool, error) {
if len(permissions) == 0 { if len(permissions) == 0 {
return false return false, nil
} }
affected, err := ormer.Engine.Insert(permissions) affected, err := ormer.Engine.Insert(permissions)
if err != nil { if err != nil {
if !strings.Contains(err.Error(), "Duplicate entry") { if !strings.Contains(err.Error(), "Duplicate entry") {
panic(err) return false, err
} }
} }
for _, permission := range permissions { for _, permission := range permissions {
// add using for loop // add using for loop
if affected != 0 { if affected != 0 {
addGroupingPolicies(permission) err = addGroupingPolicies(permission)
addPolicies(permission) if err != nil {
return false, err
}
err = addPolicies(permission)
if err != nil {
return false, err
}
} }
} }
return affected != 0 return affected != 0, nil
} }
func AddPermissionsInBatch(permissions []*Permission) bool { func AddPermissionsInBatch(permissions []*Permission) (bool, error) {
batchSize := conf.GetConfigBatchSize() batchSize := conf.GetConfigBatchSize()
if len(permissions) == 0 { if len(permissions) == 0 {
return false return false, nil
} }
affected := false affected := false
for i := 0; i < (len(permissions)-1)/batchSize+1; i++ { for i := 0; i < len(permissions); i += batchSize {
start := i * batchSize start := i
end := (i + 1) * batchSize end := i + batchSize
if end > len(permissions) { if end > len(permissions) {
end = len(permissions) end = len(permissions)
} }
tmp := permissions[start:end] tmp := permissions[start:end]
// TODO: save to log instead of standard output fmt.Printf("The syncer adds permissions: [%d - %d]\n", start, end)
// fmt.Printf("Add Permissions: [%d - %d].\n", start, end)
if AddPermissions(tmp) { b, err := AddPermissions(tmp)
if err != nil {
return false, err
}
if b {
affected = true affected = true
} }
} }
return affected return affected, nil
} }
func DeletePermission(permission *Permission) (bool, error) { func DeletePermission(permission *Permission) (bool, error) {
@ -242,8 +300,16 @@ func DeletePermission(permission *Permission) (bool, error) {
} }
if affected != 0 { if affected != 0 {
removeGroupingPolicies(permission) err = removeGroupingPolicies(permission)
removePolicies(permission) if err != nil {
return false, err
}
err = removePolicies(permission)
if err != nil {
return false, err
}
if permission.Adapter != "" && permission.Adapter != "permission_rule" { if permission.Adapter != "" && permission.Adapter != "permission_rule" {
isEmpty, _ := ormer.Engine.IsTableEmpty(permission.Adapter) isEmpty, _ := ormer.Engine.IsTableEmpty(permission.Adapter)
if isEmpty { if isEmpty {
@ -258,9 +324,59 @@ func DeletePermission(permission *Permission) (bool, error) {
return affected != 0, nil return affected != 0, nil
} }
func GetPermissionsAndRolesByUser(userId string) ([]*Permission, []*Role, error) { func getPermissionsByUser(userId string) ([]*Permission, error) {
permissions := []*Permission{} permissions := []*Permission{}
err := ormer.Engine.Where("users like ?", "%"+userId+"\"%").Find(&permissions) err := ormer.Engine.Where("users like ?", "%"+userId+"\"%").Find(&permissions)
if err != nil {
return permissions, err
}
res := []*Permission{}
for _, permission := range permissions {
if util.InSlice(permission.Users, userId) {
res = append(res, permission)
}
}
return res, nil
}
func GetPermissionsByRole(roleId string) ([]*Permission, error) {
permissions := []*Permission{}
err := ormer.Engine.Where("roles like ?", "%"+roleId+"\"%").Find(&permissions)
if err != nil {
return permissions, err
}
res := []*Permission{}
for _, permission := range permissions {
if util.InSlice(permission.Roles, roleId) {
res = append(res, permission)
}
}
return res, nil
}
func GetPermissionsByResource(resourceId string) ([]*Permission, error) {
permissions := []*Permission{}
err := ormer.Engine.Where("resources like ?", "%"+resourceId+"\"%").Find(&permissions)
if err != nil {
return permissions, err
}
res := []*Permission{}
for _, permission := range permissions {
if util.InSlice(permission.Resources, resourceId) {
res = append(res, permission)
}
}
return res, nil
}
func getPermissionsAndRolesByUser(userId string) ([]*Permission, []*Role, error) {
permissions, err := getPermissionsByUser(userId)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -277,14 +393,13 @@ func GetPermissionsAndRolesByUser(userId string) ([]*Permission, []*Role, error)
permFromRoles := []*Permission{} permFromRoles := []*Permission{}
roles, err := GetRolesByUser(userId) roles, err := getRolesByUser(userId)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
for _, role := range roles { for _, role := range roles {
perms := []*Permission{} perms, err := GetPermissionsByRole(role.GetId())
err := ormer.Engine.Where("roles like ?", "%"+role.GetId()+"\"%").Find(&perms)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -302,26 +417,6 @@ func GetPermissionsAndRolesByUser(userId string) ([]*Permission, []*Role, error)
return permissions, roles, nil return permissions, roles, nil
} }
func GetPermissionsByRole(roleId string) ([]*Permission, error) {
permissions := []*Permission{}
err := ormer.Engine.Where("roles like ?", "%"+roleId+"\"%").Find(&permissions)
if err != nil {
return permissions, err
}
return permissions, nil
}
func GetPermissionsByResource(resourceId string) ([]*Permission, error) {
permissions := []*Permission{}
err := ormer.Engine.Where("resources like ?", "%"+resourceId+"\"%").Find(&permissions)
if err != nil {
return permissions, err
}
return permissions, nil
}
func GetPermissionsBySubmitter(owner string, submitter string) ([]*Permission, error) { func GetPermissionsBySubmitter(owner string, submitter string) ([]*Permission, error) {
permissions := []*Permission{} permissions := []*Permission{}
err := ormer.Engine.Desc("created_time").Find(&permissions, &Permission{Owner: owner, Submitter: submitter}) err := ormer.Engine.Desc("created_time").Find(&permissions, &Permission{Owner: owner, Submitter: submitter})
@ -377,19 +472,34 @@ func (p *Permission) GetId() string {
} }
func (p *Permission) isUserHit(name string) bool { func (p *Permission) isUserHit(name string) bool {
targetOrg, _ := util.GetOwnerAndNameFromId(name) targetOrg, targetName := util.GetOwnerAndNameFromId(name)
for _, user := range p.Users { for _, user := range p.Users {
userOrg, userName := util.GetOwnerAndNameFromId(user) userOrg, userName := util.GetOwnerAndNameFromId(user)
if userOrg == targetOrg && userName == "*" { if userOrg == targetOrg && (userName == "*" || userName == targetName) {
return true return true
} }
} }
return false 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 { func (p *Permission) isResourceHit(name string) bool {
for _, resource := range p.Resources { for _, resource := range p.Resources {
if name == resource { if resource == "*" || resource == name {
return true return true
} }
} }

View File

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

View File

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

View File

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

View File

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

124
object/radius.go Normal file
View File

@ -0,0 +1,124 @@
package object
import (
"fmt"
"time"
"github.com/casdoor/casdoor/util"
"github.com/xorm-io/core"
)
// https://www.cisco.com/c/en/us/td/docs/ios-xml/ios/sec_usr_radatt/configuration/xe-16/sec-usr-radatt-xe-16-book/sec-rad-ov-ietf-attr.html
// https://support.huawei.com/enterprise/zh/doc/EDOC1000178159/35071f9a
type RadiusAccounting struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime time.Time `json:"createdTime"`
Username string `xorm:"index" json:"username"`
ServiceType int64 `json:"serviceType"` // e.g. LoginUser (1)
NasId string `json:"nasId"` // String identifying the network access server originating the Access-Request.
NasIpAddr string `json:"nasIpAddr"` // e.g. "192.168.0.10"
NasPortId string `json:"nasPortId"` // Contains a text string which identifies the port of the NAS that is authenticating the user. e.g."eth.0"
NasPortType int64 `json:"nasPortType"` // Indicates the type of physical port the network access server is using to authenticate the user. e.g.Ethernet15
NasPort int64 `json:"nasPort"` // Indicates the physical port number of the network access server that is authenticating the user. e.g. 233
FramedIpAddr string `json:"framedIpAddr"` // Indicates the IP address to be configured for the user by sending the IP address of a user to the RADIUS server.
FramedIpNetmask string `json:"framedIpNetmask"` // Indicates the IP netmask to be configured for the user when the user is using a device on a network.
AcctSessionId string `xorm:"index" json:"acctSessionId"`
AcctSessionTime int64 `json:"acctSessionTime"` // Indicates how long (in seconds) the user has received service.
AcctInputTotal int64 `json:"acctInputTotal"`
AcctOutputTotal int64 `json:"acctOutputTotal"`
AcctInputPackets int64 `json:"acctInputPackets"` // Indicates how many packets have been received from the port over the course of this service being provided to a framed user.
AcctOutputPackets int64 `json:"acctOutputPackets"` // Indicates how many packets have been sent to the port in the course of delivering this service to a framed user.
AcctTerminateCause int64 `json:"acctTerminateCause"` // e.g. Lost-Carrier (2)
LastUpdate time.Time `json:"lastUpdate"`
AcctStartTime time.Time `xorm:"index" json:"acctStartTime"`
AcctStopTime time.Time `xorm:"index" json:"acctStopTime"`
}
func (ra *RadiusAccounting) GetId() string {
return util.GetId(ra.Owner, ra.Name)
}
func getRadiusAccounting(owner, name string) (*RadiusAccounting, error) {
if owner == "" || name == "" {
return nil, nil
}
ra := RadiusAccounting{Owner: owner, Name: name}
existed, err := ormer.Engine.Get(&ra)
if err != nil {
return nil, err
}
if existed {
return &ra, nil
} else {
return nil, nil
}
}
func getPaginationRadiusAccounting(owner, field, value, sortField, sortOrder string, offset, limit int) ([]*RadiusAccounting, error) {
ras := []*RadiusAccounting{}
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&ras)
if err != nil {
return ras, err
}
return ras, nil
}
func GetRadiusAccounting(id string) (*RadiusAccounting, error) {
owner, name := util.GetOwnerAndNameFromId(id)
return getRadiusAccounting(owner, name)
}
func GetRadiusAccountingBySessionId(sessionId string) (*RadiusAccounting, error) {
ras, err := getPaginationRadiusAccounting("", "acct_session_id", sessionId, "created_time", "desc", 0, 1)
if err != nil {
return nil, err
}
if len(ras) == 0 {
return nil, nil
}
return ras[0], nil
}
func AddRadiusAccounting(ra *RadiusAccounting) error {
_, err := ormer.Engine.Insert(ra)
return err
}
func DeleteRadiusAccounting(ra *RadiusAccounting) error {
_, err := ormer.Engine.ID(core.PK{ra.Owner, ra.Name}).Delete(&RadiusAccounting{})
return err
}
func UpdateRadiusAccounting(id string, ra *RadiusAccounting) error {
owner, name := util.GetOwnerAndNameFromId(id)
_, err := ormer.Engine.ID(core.PK{owner, name}).Update(ra)
return err
}
func InterimUpdateRadiusAccounting(oldRa *RadiusAccounting, newRa *RadiusAccounting, stop bool) error {
if oldRa.AcctSessionId != newRa.AcctSessionId {
return fmt.Errorf("AcctSessionId is not equal, newRa = %s, oldRa = %s", newRa.AcctSessionId, oldRa.AcctSessionId)
}
oldRa.AcctInputTotal = newRa.AcctInputTotal
oldRa.AcctOutputTotal = newRa.AcctOutputTotal
oldRa.AcctInputPackets = newRa.AcctInputPackets
oldRa.AcctOutputPackets = newRa.AcctOutputPackets
oldRa.AcctSessionTime = newRa.AcctSessionTime
if stop {
oldRa.AcctStopTime = newRa.AcctStopTime
if oldRa.AcctStopTime.IsZero() {
oldRa.AcctStopTime = time.Now()
}
oldRa.AcctTerminateCause = newRa.AcctTerminateCause
} else {
oldRa.LastUpdate = time.Now()
}
return UpdateRadiusAccounting(oldRa.GetId(), oldRa)
}

View File

@ -87,47 +87,71 @@ func AddRecord(record *casvisorsdk.Record) bool {
affected, err := casvisorsdk.AddRecord(record) affected, err := casvisorsdk.AddRecord(record)
if err != nil { if err != nil {
panic(err) fmt.Printf("AddRecord() error: %s", err.Error())
} }
return affected 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 { func SendWebhooks(record *casvisorsdk.Record) error {
webhooks, err := getWebhooksByOrganization(record.Organization) webhooks, err := getWebhooksByOrganization(record.Organization)
if err != nil { if err != nil {
return err return err
} }
errs := []error{}
webhooks = getFilteredWebhooks(webhooks, record.Action)
for _, webhook := range webhooks { for _, webhook := range webhooks {
if !webhook.IsEnabled { var user *User
continue if webhook.IsUserExtended {
} user, err = getUser(record.Organization, record.User)
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)
if err != nil { 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 return nil
} }

View File

@ -32,6 +32,7 @@ type Role struct {
Description string `xorm:"varchar(100)" json:"description"` Description string `xorm:"varchar(100)" json:"description"`
Users []string `xorm:"mediumtext" json:"users"` Users []string `xorm:"mediumtext" json:"users"`
Groups []string `xorm:"mediumtext" json:"groups"`
Roles []string `xorm:"mediumtext" json:"roles"` Roles []string `xorm:"mediumtext" json:"roles"`
Domains []string `xorm:"mediumtext" json:"domains"` Domains []string `xorm:"mediumtext" json:"domains"`
IsEnabled bool `json:"isEnabled"` IsEnabled bool `json:"isEnabled"`
@ -150,8 +151,16 @@ func UpdateRole(id string, role *Role) (bool, error) {
} }
for _, permission := range permissions { for _, permission := range permissions {
addGroupingPolicies(permission) err = addGroupingPolicies(permission)
addPolicies(permission) if err != nil {
return false, err
}
err = addPolicies(permission)
if err != nil {
return false, err
}
visited[permission.GetId()] = struct{}{} visited[permission.GetId()] = struct{}{}
} }
@ -165,10 +174,15 @@ func UpdateRole(id string, role *Role) (bool, error) {
if err != nil { if err != nil {
return false, err return false, err
} }
for _, permission := range permissions { for _, permission := range permissions {
permissionId := permission.GetId() permissionId := permission.GetId()
if _, ok := visited[permissionId]; !ok { if _, ok := visited[permissionId]; !ok {
addGroupingPolicies(permission) err = addGroupingPolicies(permission)
if err != nil {
return false, err
}
visited[permissionId] = struct{}{} visited[permissionId] = struct{}{}
} }
} }
@ -207,16 +221,15 @@ func AddRolesInBatch(roles []*Role) bool {
} }
affected := false affected := false
for i := 0; i < (len(roles)-1)/batchSize+1; i++ { for i := 0; i < len(roles); i += batchSize {
start := i * batchSize start := i
end := (i + 1) * batchSize end := i + batchSize
if end > len(roles) { if end > len(roles) {
end = len(roles) end = len(roles)
} }
tmp := roles[start:end] tmp := roles[start:end]
// TODO: save to log instead of standard output fmt.Printf("The syncer adds roles: [%d - %d]\n", start, end)
// fmt.Printf("Add users: [%d - %d].\n", start, end)
if AddRoles(tmp) { if AddRoles(tmp) {
affected = true affected = true
} }
@ -252,15 +265,40 @@ func (role *Role) GetId() string {
return fmt.Sprintf("%s/%s", role.Owner, role.Name) return fmt.Sprintf("%s/%s", role.Owner, role.Name)
} }
func GetRolesByUser(userId string) ([]*Role, error) { func getRolesByUserInternal(userId string) ([]*Role, error) {
roles := []*Role{} roles := []*Role{}
err := ormer.Engine.Where("users like ?", "%"+userId+"\"%").Find(&roles) user, err := GetUser(userId)
if err != nil { if err != nil {
return roles, err return roles, err
} }
allRolesIds := make([]string, 0, len(roles)) query := ormer.Engine.Where("users like ?", fmt.Sprintf("%%%s%%", userId))
for _, group := range user.Groups {
query = query.Or("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) || util.HaveIntersection(role.Groups, user.Groups) {
res = append(res, role)
}
}
return res, nil
}
func getRolesByUser(userId string) ([]*Role, error) {
roles, err := getRolesByUserInternal(userId)
if err != nil {
return roles, err
}
allRolesIds := []string{}
for _, role := range roles { for _, role := range roles {
allRolesIds = append(allRolesIds, role.GetId()) allRolesIds = append(allRolesIds, role.GetId())
} }
@ -336,16 +374,6 @@ func GetMaskedRoles(roles []*Role) []*Role {
return roles return roles
} }
func GetRolesByNamePrefix(owner string, prefix string) ([]*Role, error) {
roles := []*Role{}
err := ormer.Engine.Where("owner=? and name like ?", owner, prefix+"%").Find(&roles)
if err != nil {
return roles, err
}
return roles, nil
}
// GetAncestorRoles returns a list of roles that contain the given roleIds // GetAncestorRoles returns a list of roles that contain the given roleIds
func GetAncestorRoles(roleIds ...string) ([]*Role, error) { func GetAncestorRoles(roleIds ...string) ([]*Role, error) {
var ( var (

View File

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

View File

@ -37,7 +37,7 @@ import (
// NewSamlResponse // NewSamlResponse
// returns a saml2 response // 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{ samlResponse := &etree.Element{
Space: "samlp", Space: "samlp",
Tag: "Response", 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.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) 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 := attributes.CreateElement("saml:Attribute")
roles.CreateAttr("Name", "Roles") roles.CreateAttr("Name", "Roles")
roles.CreateAttr("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic") roles.CreateAttr("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic")
@ -184,10 +191,11 @@ type SingleSignOnService struct {
type Attribute struct { type Attribute struct {
XMLName xml.Name XMLName xml.Name
Name string `xml:"Name,attr"` Name string `xml:"Name,attr"`
NameFormat string `xml:"NameFormat,attr"` NameFormat string `xml:"NameFormat,attr"`
FriendlyName string `xml:"FriendlyName,attr"` FriendlyName string `xml:"FriendlyName,attr"`
Xmlns string `xml:"xmlns,attr"` Xmlns string `xml:"xmlns,attr"`
Values []string `xml:"AttributeValue"`
} }
func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, error) { 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") 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)) block, _ := pem.Decode([]byte(cert.Certificate))
certificate := base64.StdEncoding.EncodeToString(block.Bytes) certificate := base64.StdEncoding.EncodeToString(block.Bytes)
@ -288,6 +300,10 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
return "", "", "", err 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)) block, _ := pem.Decode([]byte(cert.Certificate))
certificate := base64.StdEncoding.EncodeToString(block.Bytes) certificate := base64.StdEncoding.EncodeToString(block.Bytes)
@ -301,13 +317,18 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
_, originBackend := getOriginFromHost(host) _, originBackend := getOriginFromHost(host)
// build signedResponse // 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{ randomKeyStore := &X509Key{
PrivateKey: cert.PrivateKey, PrivateKey: cert.PrivateKey,
X509Certificate: certificate, X509Certificate: certificate,
} }
ctx := dsig.NewDefaultSigningContext(randomKeyStore) ctx := dsig.NewDefaultSigningContext(randomKeyStore)
ctx.Hash = crypto.SHA1 ctx.Hash = crypto.SHA1
if application.EnableSamlC14n10 {
ctx.Canonicalizer = dsig.MakeC14N10ExclusiveCanonicalizerWithPrefixList("")
}
//signedXML, err := ctx.SignEnvelopedLimix(samlResponse) //signedXML, err := ctx.SignEnvelopedLimix(samlResponse)
//if err != nil { //if err != nil {
// return "", "", fmt.Errorf("err: %s", err.Error()) // return "", "", fmt.Errorf("err: %s", err.Error())

View File

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

View File

@ -230,28 +230,39 @@ func (syncer *Syncer) getTable() string {
} }
} }
func (syncer *Syncer) getKey() string { func (syncer *Syncer) getKeyColumn() *TableColumn {
key := "id" var column *TableColumn
hasKey := false
hasId := false
for _, tableColumn := range syncer.TableColumns { for _, tableColumn := range syncer.TableColumns {
if tableColumn.IsKey { if tableColumn.IsKey {
hasKey = true column = tableColumn
key = tableColumn.Name
}
if tableColumn.Name == "id" {
hasId = true
} }
} }
if !hasKey && !hasId { if column == nil {
key = syncer.TableColumns[0].Name 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 { func RunSyncer(syncer *Syncer) error {
syncer.initAdapter() err := syncer.initAdapter()
if err != nil {
return err
}
return syncer.syncUsers() return syncer.syncUsers()
} }

View File

@ -50,9 +50,12 @@ func addSyncerJob(syncer *Syncer) error {
return nil return nil
} }
syncer.initAdapter() err := syncer.initAdapter()
if err != nil {
return err
}
err := syncer.syncUsers() err = syncer.syncUsers()
if err != nil { if err != nil {
return err return err
} }

View File

@ -38,7 +38,11 @@ func getEnabledSyncerForOrganization(organization string) (*Syncer, error) {
for _, syncer := range syncers { for _, syncer := range syncers {
if syncer.Organization == organization && syncer.IsEnabled { if syncer.Organization == organization && syncer.IsEnabled {
syncer.initAdapter() err = syncer.initAdapter()
if err != nil {
return nil, err
}
return syncer, nil return syncer, nil
} }
} }
@ -55,6 +59,10 @@ func AddUserToOriginalDatabase(user *User) error {
return nil return nil
} }
if syncer.IsReadOnly {
return nil
}
updatedOUser := syncer.createOriginalUserFromUser(user) updatedOUser := syncer.createOriginalUserFromUser(user)
_, err = syncer.addUser(updatedOUser) _, err = syncer.addUser(updatedOUser)
if err != nil { if err != nil {
@ -74,6 +82,10 @@ func UpdateUserToOriginalDatabase(user *User) error {
return nil return nil
} }
if syncer.IsReadOnly {
return nil
}
newUser, err := GetUser(user.GetId()) newUser, err := GetUser(user.GetId())
if err != nil { if err != nil {
return err return err

View File

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

View File

@ -21,7 +21,6 @@ import (
"time" "time"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/xorm-io/core"
) )
type OriginalUser = User type OriginalUser = User
@ -50,19 +49,6 @@ func (syncer *Syncer) getOriginalUsers() ([]*OriginalUser, error) {
return users, nil 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) { func (syncer *Syncer) addUser(user *OriginalUser) (bool, error) {
m := syncer.getMapFromOriginalUser(user) m := syncer.getMapFromOriginalUser(user)
affected, err := syncer.Ormer.Engine.Table(syncer.getTable()).Insert(m) 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] pkValue := m[key]
delete(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 { if err != nil {
return false, err return false, err
} }
return affected != 0, nil return affected != 0, nil
} }
func (syncer *Syncer) updateUserForOriginalFields(user *User) (bool, error) { func (syncer *Syncer) updateUserForOriginalFields(user *User, key string) (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) {
var err error var err error
oldUser := User{} oldUser := User{}
@ -162,27 +124,31 @@ func (syncer *Syncer) calculateHash(user *OriginalUser) string {
return util.GetMd5Hash(s) return util.GetMd5Hash(s)
} }
func (syncer *Syncer) initAdapter() { func (syncer *Syncer) initAdapter() error {
if syncer.Ormer == nil { if syncer.Ormer != nil {
var dataSourceName string return nil
if syncer.DatabaseType == "mssql" {
dataSourceName = fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s", syncer.User, syncer.Password, syncer.Host, syncer.Port, syncer.Database)
} else if syncer.DatabaseType == "postgres" {
sslMode := "disable"
if syncer.SslMode != "" {
sslMode = syncer.SslMode
}
dataSourceName = fmt.Sprintf("user=%s password=%s host=%s port=%d sslmode=%s dbname=%s", syncer.User, syncer.Password, syncer.Host, syncer.Port, sslMode, syncer.Database)
} else {
dataSourceName = fmt.Sprintf("%s:%s@tcp(%s:%d)/", syncer.User, syncer.Password, syncer.Host, syncer.Port)
}
if !isCloudIntranet {
dataSourceName = strings.ReplaceAll(dataSourceName, "dbi.", "db.")
}
syncer.Ormer = NewAdapter(syncer.DatabaseType, dataSourceName, syncer.Database)
} }
var dataSourceName string
if syncer.DatabaseType == "mssql" {
dataSourceName = fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s", syncer.User, syncer.Password, syncer.Host, syncer.Port, syncer.Database)
} else if syncer.DatabaseType == "postgres" {
sslMode := "disable"
if syncer.SslMode != "" {
sslMode = syncer.SslMode
}
dataSourceName = fmt.Sprintf("user=%s password=%s host=%s port=%d sslmode=%s dbname=%s", syncer.User, syncer.Password, syncer.Host, syncer.Port, sslMode, syncer.Database)
} else {
dataSourceName = fmt.Sprintf("%s:%s@tcp(%s:%d)/", syncer.User, syncer.Password, syncer.Host, syncer.Port)
}
if !isCloudIntranet {
dataSourceName = strings.ReplaceAll(dataSourceName, "dbi.", "db.")
}
var err error
syncer.Ormer, err = NewAdapter(syncer.DatabaseType, dataSourceName, syncer.Database)
return err
} }
func RunSyncUsersJob() { func RunSyncUsersJob() {
@ -192,7 +158,10 @@ func RunSyncUsersJob() {
} }
for _, syncer := range syncers { for _, syncer := range syncers {
addSyncerJob(syncer) err = addSyncerJob(syncer)
if err != nil {
panic(err)
}
} }
time.Sleep(time.Duration(1<<63 - 1)) time.Sleep(time.Duration(1<<63 - 1))

View File

@ -286,6 +286,10 @@ func GetValidationBySaml(samlRequest string, host string) (string, string, error
return "", "", err 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)) block, _ := pem.Decode([]byte(cert.Certificate))
certificate := base64.StdEncoding.EncodeToString(block.Bytes) certificate := base64.StdEncoding.EncodeToString(block.Bytes)
randomKeyStore := &X509Key{ randomKeyStore := &X509Key{

View File

@ -26,7 +26,7 @@ type Claims struct {
*User *User
TokenType string `json:"tokenType,omitempty"` TokenType string `json:"tokenType,omitempty"`
Nonce string `json:"nonce,omitempty"` Nonce string `json:"nonce,omitempty"`
Tag string `json:"tag,omitempty"` Tag string `json:"tag"`
Scope string `json:"scope,omitempty"` Scope string `json:"scope,omitempty"`
jwt.RegisteredClaims jwt.RegisteredClaims
} }
@ -37,56 +37,90 @@ type UserShort struct {
} }
type UserWithoutThirdIdp struct { type UserWithoutThirdIdp struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"` Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"` Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"` CreatedTime string `xorm:"varchar(100) index" json:"createdTime"`
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"` UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
Id string `xorm:"varchar(100) index" json:"id"`
Type string `xorm:"varchar(100)" json:"type"` Id string `xorm:"varchar(100) index" json:"id"`
Password string `xorm:"varchar(100)" json:"password"` Type string `xorm:"varchar(100)" json:"type"`
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"` Password string `xorm:"varchar(100)" json:"password"`
DisplayName string `xorm:"varchar(100)" json:"displayName"` PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
FirstName string `xorm:"varchar(100)" json:"firstName"` PasswordType string `xorm:"varchar(100)" json:"passwordType"`
LastName string `xorm:"varchar(100)" json:"lastName"` DisplayName string `xorm:"varchar(100)" json:"displayName"`
Avatar string `xorm:"varchar(500)" json:"avatar"` FirstName string `xorm:"varchar(100)" json:"firstName"`
PermanentAvatar string `xorm:"varchar(500)" json:"permanentAvatar"` LastName string `xorm:"varchar(100)" json:"lastName"`
Email string `xorm:"varchar(100) index" json:"email"` Avatar string `xorm:"varchar(500)" json:"avatar"`
EmailVerified bool `json:"emailVerified"` AvatarType string `xorm:"varchar(100)" json:"avatarType"`
Phone string `xorm:"varchar(100) index" json:"phone"` PermanentAvatar string `xorm:"varchar(500)" json:"permanentAvatar"`
Location string `xorm:"varchar(100)" json:"location"` Email string `xorm:"varchar(100) index" json:"email"`
Address []string `json:"address"` EmailVerified bool `json:"emailVerified"`
Affiliation string `xorm:"varchar(100)" json:"affiliation"` Phone string `xorm:"varchar(20) index" json:"phone"`
Title string `xorm:"varchar(100)" json:"title"` CountryCode string `xorm:"varchar(6)" json:"countryCode"`
IdCardType string `xorm:"varchar(100)" json:"idCardType"` Region string `xorm:"varchar(100)" json:"region"`
IdCard string `xorm:"varchar(100) index" json:"idCard"` Location string `xorm:"varchar(100)" json:"location"`
Homepage string `xorm:"varchar(100)" json:"homepage"` Address []string `json:"address"`
Bio string `xorm:"varchar(100)" json:"bio"` Affiliation string `xorm:"varchar(100)" json:"affiliation"`
Tag string `xorm:"varchar(100)" json:"tag"` Title string `xorm:"varchar(100)" json:"title"`
Region string `xorm:"varchar(100)" json:"region"` IdCardType string `xorm:"varchar(100)" json:"idCardType"`
Language string `xorm:"varchar(100)" json:"language"` IdCard string `xorm:"varchar(100) index" json:"idCard"`
Gender string `xorm:"varchar(100)" json:"gender"` Homepage string `xorm:"varchar(100)" json:"homepage"`
Birthday string `xorm:"varchar(100)" json:"birthday"` Bio string `xorm:"varchar(100)" json:"bio"`
Education string `xorm:"varchar(100)" json:"education"` Tag string `xorm:"varchar(100)" json:"tag"`
Score int `json:"score"` Language string `xorm:"varchar(100)" json:"language"`
Karma int `json:"karma"` Gender string `xorm:"varchar(100)" json:"gender"`
Ranking int `json:"ranking"` Birthday string `xorm:"varchar(100)" json:"birthday"`
IsDefaultAvatar bool `json:"isDefaultAvatar"` Education string `xorm:"varchar(100)" json:"education"`
IsOnline bool `json:"isOnline"` Score int `json:"score"`
IsAdmin bool `json:"isAdmin"` Karma int `json:"karma"`
IsForbidden bool `json:"isForbidden"` Ranking int `json:"ranking"`
IsDeleted bool `json:"isDeleted"` IsDefaultAvatar bool `json:"isDefaultAvatar"`
SignupApplication string `xorm:"varchar(100)" json:"signupApplication"` IsOnline bool `json:"isOnline"`
Hash string `xorm:"varchar(100)" json:"hash"` IsAdmin bool `json:"isAdmin"`
PreHash string `xorm:"varchar(100)" json:"preHash"` IsForbidden bool `json:"isForbidden"`
CreatedIp string `xorm:"varchar(100)" json:"createdIp"` IsDeleted bool `json:"isDeleted"`
LastSigninTime string `xorm:"varchar(100)" json:"lastSigninTime"` SignupApplication string `xorm:"varchar(100)" json:"signupApplication"`
LastSigninIp string `xorm:"varchar(100)" json:"lastSigninIp"` Hash string `xorm:"varchar(100)" json:"hash"`
Ldap string `xorm:"ldap varchar(100)" json:"ldap"` PreHash string `xorm:"varchar(100)" json:"preHash"`
Properties map[string]string `json:"properties"` AccessKey string `xorm:"varchar(100)" json:"accessKey"`
Roles []*Role `xorm:"-" json:"roles"` AccessSecret string `xorm:"varchar(100)" json:"accessSecret"`
Permissions []*Permission `xorm:"-" json:"permissions"`
LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"` GitHub string `xorm:"github varchar(100)" json:"github"`
SigninWrongTimes int `json:"signinWrongTimes"` Google string `xorm:"varchar(100)" json:"google"`
QQ string `xorm:"qq varchar(100)" json:"qq"`
WeChat string `xorm:"wechat varchar(100)" json:"wechat"`
Facebook string `xorm:"facebook varchar(100)" json:"facebook"`
DingTalk string `xorm:"dingtalk varchar(100)" json:"dingtalk"`
Weibo string `xorm:"weibo varchar(100)" json:"weibo"`
Gitee string `xorm:"gitee varchar(100)" json:"gitee"`
LinkedIn string `xorm:"linkedin varchar(100)" json:"linkedin"`
Wecom string `xorm:"wecom varchar(100)" json:"wecom"`
Lark string `xorm:"lark varchar(100)" json:"lark"`
Gitlab string `xorm:"gitlab varchar(100)" json:"gitlab"`
CreatedIp string `xorm:"varchar(100)" json:"createdIp"`
LastSigninTime string `xorm:"varchar(100)" json:"lastSigninTime"`
LastSigninIp string `xorm:"varchar(100)" json:"lastSigninIp"`
// WebauthnCredentials []webauthn.Credential `xorm:"webauthnCredentials blob" json:"webauthnCredentials"`
PreferredMfaType string `xorm:"varchar(100)" json:"preferredMfaType"`
RecoveryCodes []string `xorm:"varchar(1000)" json:"recoveryCodes"`
TotpSecret string `xorm:"varchar(100)" json:"totpSecret"`
MfaPhoneEnabled bool `json:"mfaPhoneEnabled"`
MfaEmailEnabled bool `json:"mfaEmailEnabled"`
// MultiFactorAuths []*MfaProps `xorm:"-" json:"multiFactorAuths,omitempty"`
Ldap string `xorm:"ldap varchar(100)" json:"ldap"`
Properties map[string]string `json:"properties"`
Roles []*Role `json:"roles"`
Permissions []*Permission `json:"permissions"`
Groups []string `xorm:"groups varchar(1000)" json:"groups"`
LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"`
SigninWrongTimes int `json:"signinWrongTimes"`
// ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"`
} }
type ClaimsShort struct { type ClaimsShort struct {
@ -101,7 +135,7 @@ type ClaimsWithoutThirdIdp struct {
*UserWithoutThirdIdp *UserWithoutThirdIdp
TokenType string `json:"tokenType,omitempty"` TokenType string `json:"tokenType,omitempty"`
Nonce string `json:"nonce,omitempty"` Nonce string `json:"nonce,omitempty"`
Tag string `json:"tag,omitempty"` Tag string `json:"tag"`
Scope string `json:"scope,omitempty"` Scope string `json:"scope,omitempty"`
jwt.RegisteredClaims jwt.RegisteredClaims
} }
@ -125,14 +159,18 @@ func getUserWithoutThirdIdp(user *User) *UserWithoutThirdIdp {
Type: user.Type, Type: user.Type,
Password: user.Password, Password: user.Password,
PasswordSalt: user.PasswordSalt, PasswordSalt: user.PasswordSalt,
PasswordType: user.PasswordType,
DisplayName: user.DisplayName, DisplayName: user.DisplayName,
FirstName: user.FirstName, FirstName: user.FirstName,
LastName: user.LastName, LastName: user.LastName,
Avatar: user.Avatar, Avatar: user.Avatar,
AvatarType: user.AvatarType,
PermanentAvatar: user.PermanentAvatar, PermanentAvatar: user.PermanentAvatar,
Email: user.Email, Email: user.Email,
EmailVerified: user.EmailVerified, EmailVerified: user.EmailVerified,
Phone: user.Phone, Phone: user.Phone,
CountryCode: user.CountryCode,
Region: user.Region,
Location: user.Location, Location: user.Location,
Address: user.Address, Address: user.Address,
Affiliation: user.Affiliation, Affiliation: user.Affiliation,
@ -142,7 +180,6 @@ func getUserWithoutThirdIdp(user *User) *UserWithoutThirdIdp {
Homepage: user.Homepage, Homepage: user.Homepage,
Bio: user.Bio, Bio: user.Bio,
Tag: user.Tag, Tag: user.Tag,
Region: user.Region,
Language: user.Language, Language: user.Language,
Gender: user.Gender, Gender: user.Gender,
Birthday: user.Birthday, Birthday: user.Birthday,
@ -158,16 +195,38 @@ func getUserWithoutThirdIdp(user *User) *UserWithoutThirdIdp {
SignupApplication: user.SignupApplication, SignupApplication: user.SignupApplication,
Hash: user.Hash, Hash: user.Hash,
PreHash: user.PreHash, PreHash: user.PreHash,
AccessKey: user.AccessKey,
AccessSecret: user.AccessSecret,
GitHub: user.GitHub,
Google: user.Google,
QQ: user.QQ,
WeChat: user.WeChat,
Facebook: user.Facebook,
DingTalk: user.DingTalk,
Weibo: user.Weibo,
Gitee: user.Gitee,
LinkedIn: user.LinkedIn,
Wecom: user.Wecom,
Lark: user.Lark,
Gitlab: user.Gitlab,
CreatedIp: user.CreatedIp, CreatedIp: user.CreatedIp,
LastSigninTime: user.LastSigninTime, LastSigninTime: user.LastSigninTime,
LastSigninIp: user.LastSigninIp, LastSigninIp: user.LastSigninIp,
PreferredMfaType: user.PreferredMfaType,
RecoveryCodes: user.RecoveryCodes,
TotpSecret: user.TotpSecret,
MfaPhoneEnabled: user.MfaPhoneEnabled,
MfaEmailEnabled: user.MfaEmailEnabled,
Ldap: user.Ldap, Ldap: user.Ldap,
Properties: user.Properties, Properties: user.Properties,
Roles: user.Roles, Roles: user.Roles,
Permissions: user.Permissions, Permissions: user.Permissions,
Groups: user.Groups,
LastSigninWrongTime: user.LastSigninWrongTime, LastSigninWrongTime: user.LastSigninWrongTime,
SigninWrongTimes: user.SigninWrongTimes, SigninWrongTimes: user.SigninWrongTimes,
@ -309,6 +368,10 @@ func ParseJwtToken(token string, cert *Cert) (*Claims, error) {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 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 // RSA certificate
certificate, err := jwt.ParseRSAPublicKeyFromPEM([]byte(cert.Certificate)) certificate, err := jwt.ParseRSAPublicKeyFromPEM([]byte(cert.Certificate))
if err != nil { if err != nil {

View File

@ -16,6 +16,7 @@ package object
import ( import (
"fmt" "fmt"
"strconv"
"strings" "strings"
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
@ -49,6 +50,7 @@ type User struct {
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"` UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
Id string `xorm:"varchar(100) index" json:"id"` Id string `xorm:"varchar(100) index" json:"id"`
ExternalId string `xorm:"varchar(100) index" json:"externalId"`
Type string `xorm:"varchar(100)" json:"type"` Type string `xorm:"varchar(100)" json:"type"`
Password string `xorm:"varchar(100)" json:"password"` Password string `xorm:"varchar(100)" json:"password"`
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"` PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
@ -370,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) { func GetUserByPhone(owner string, phone string) (*User, error) {
if owner == "" || phone == "" { if owner == "" || phone == "" {
return nil, nil return nil, nil
@ -388,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) { func GetUserByUserId(owner string, userId string) (*User, error) {
if owner == "" || userId == "" { if owner == "" || userId == "" {
return nil, nil return nil, nil
@ -406,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) { func GetUserByAccessKey(accessKey string) (*User, error) {
if accessKey == "" { if accessKey == "" {
return nil, nil return nil, nil
@ -483,7 +539,7 @@ func GetMaskedUsers(users []*User, errs ...error) ([]*User, error) {
return users, nil return users, nil
} }
func GetLastUser(owner string) (*User, error) { func getLastUser(owner string) (*User, error) {
user := User{Owner: owner} user := User{Owner: owner}
existed, err := ormer.Engine.Desc("created_time", "id").Get(&user) existed, err := ormer.Engine.Desc("created_time", "id").Get(&user)
if err != nil { if err != nil {
@ -528,7 +584,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
if len(columns) == 0 { if len(columns) == 0 {
columns = []string{ 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", "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", "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", "signin_wrong_times", "last_signin_wrong_time", "groups", "access_key", "access_secret",
@ -545,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, "name", "email", "phone", "country_code", "type")
} }
columns = append(columns, "updated_time")
user.UpdatedTime = util.GetCurrentTime()
if util.ContainsString(columns, "groups") { if util.ContainsString(columns, "groups") {
_, err := userEnforcer.UpdateGroupsForUser(user.GetId(), user.Groups) _, err := userEnforcer.UpdateGroupsForUser(user.GetId(), user.Groups)
if err != nil { if err != nil {
@ -614,9 +673,18 @@ func UpdateUserForAllFields(id string, user *User) (bool, error) {
} }
func AddUser(user *User) (bool, error) { func AddUser(user *User) (bool, error) {
var err error
if user.Id == "" { if user.Id == "" {
user.Id = util.GenerateId() application, err := GetApplicationByUser(user)
if err != nil {
return false, err
}
id, err := GenerateIdForNewUser(application)
if err != nil {
return false, err
}
user.Id = id
} }
if user.Owner == "" || user.Name == "" { if user.Owner == "" || user.Name == "" {
@ -628,11 +696,15 @@ func AddUser(user *User) (bool, error) {
return false, nil return false, nil
} }
if organization.DefaultPassword != "" && user.Password == "123" {
user.Password = organization.DefaultPassword
}
if user.PasswordType == "" || user.PasswordType == "plain" { if user.PasswordType == "" || user.PasswordType == "plain" {
user.UpdateUserPassword(organization) user.UpdateUserPassword(organization)
} }
err = user.UpdateUserHash() err := user.UpdateUserHash()
if err != nil { if err != nil {
return false, err return false, err
} }
@ -707,16 +779,15 @@ func AddUsersInBatch(users []*User) (bool, error) {
} }
affected := false affected := false
for i := 0; i < (len(users)-1)/batchSize+1; i++ { for i := 0; i < len(users); i += batchSize {
start := i * batchSize start := i
end := (i + 1) * batchSize end := i + batchSize
if end > len(users) { if end > len(users) {
end = len(users) end = len(users)
} }
tmp := users[start:end] tmp := users[start:end]
// TODO: save to log instead of standard output fmt.Printf("The syncer adds users: [%d - %d]\n", start, end)
// fmt.Printf("Add users: [%d - %d].\n", start, end)
if ok, err := AddUsers(tmp); err != nil { if ok, err := AddUsers(tmp); err != nil {
return false, err return false, err
} else if ok { } else if ok {
@ -786,7 +857,7 @@ func ExtendUserWithRolesAndPermissions(user *User) (err error) {
return return
} }
user.Permissions, user.Roles, err = GetPermissionsAndRolesByUser(user.GetId()) user.Permissions, user.Roles, err = getPermissionsAndRolesByUser(user.GetId())
if err != nil { if err != nil {
return err return err
} }
@ -900,3 +971,22 @@ func (user *User) IsGlobalAdmin() bool {
return user.Owner == "built-in" return user.Owner == "built-in"
} }
func GenerateIdForNewUser(application *Application) (string, error) {
if application == nil || application.GetSignupItemRule("ID") != "Incremental" {
return util.GenerateId(), nil
}
lastUser, err := getLastUser(application.Organization)
if err != nil {
return "", err
}
lastUserId := -1
if lastUser != nil {
lastUserId = util.ParseInt(lastUser.Id)
}
res := strconv.Itoa(lastUserId + 1)
return res, nil
}

View File

@ -35,7 +35,7 @@ func downloadImage(client *http.Client, url string) (*bytes.Buffer, string, erro
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
fmt.Printf("downloadImage() error for url [%s]: %s\n", url, err.Error()) 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 return nil, "", nil
} else { } else {
return nil, "", err return nil, "", err

View File

@ -87,7 +87,7 @@ func (e *UserGroupEnforcer) GetAllUsersByGroup(group string) ([]string, error) {
users, err := e.enforcer.GetUsersForRole(GetGroupWithPrefix(group)) users, err := e.enforcer.GetUsersForRole(GetGroupWithPrefix(group))
if err != nil { if err != nil {
if err == errors.ERR_NAME_NOT_FOUND { if err == errors.ErrNameNotFound {
return []string{}, nil return []string{}, nil
} }
return nil, err return nil, err

View File

@ -144,5 +144,6 @@ func UploadUsers(owner string, path string) (bool, error) {
if len(newUsers) == 0 { if len(newUsers) == 0 {
return false, nil return false, nil
} }
return AddUsersInBatch(newUsers) 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 { 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 sender := organization.DisplayName
title := provider.Title title := provider.Title
code := getRandomCode(6) 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 { 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 { if err := IsAllowSend(user, remoteAddr, provider.Category); err != nil {
return err return err
} }

109
radius/server.go Normal file
View File

@ -0,0 +1,109 @@
// 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 radius
import (
"fmt"
"log"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/object"
"layeh.com/radius"
"layeh.com/radius/rfc2865"
"layeh.com/radius/rfc2866"
)
// https://support.huawei.com/enterprise/zh/doc/EDOC1000178159/35071f9a#tab_3
func StartRadiusServer() {
secret := conf.GetConfigString("radiusSecret")
server := radius.PacketServer{
Addr: "0.0.0.0:" + conf.GetConfigString("radiusServerPort"),
Handler: radius.HandlerFunc(handlerRadius),
SecretSource: radius.StaticSecretSource([]byte(secret)),
}
log.Printf("Starting Radius server on %s", server.Addr)
if err := server.ListenAndServe(); err != nil {
log.Printf("StartRadiusServer() failed, err = %v", err)
}
}
func handlerRadius(w radius.ResponseWriter, r *radius.Request) {
switch r.Code {
case radius.CodeAccessRequest:
handleAccessRequest(w, r)
case radius.CodeAccountingRequest:
handleAccountingRequest(w, r)
default:
log.Printf("radius message, code = %d", r.Code)
}
}
func handleAccessRequest(w radius.ResponseWriter, r *radius.Request) {
username := rfc2865.UserName_GetString(r.Packet)
password := rfc2865.UserPassword_GetString(r.Packet)
organization := rfc2865.Class_GetString(r.Packet)
log.Printf("handleAccessRequest() username=%v, org=%v, password=%v", username, organization, password)
if organization == "" {
w.Write(r.Response(radius.CodeAccessReject))
return
}
_, msg := object.CheckUserPassword(organization, username, password, "en")
if msg != "" {
w.Write(r.Response(radius.CodeAccessReject))
return
}
w.Write(r.Response(radius.CodeAccessAccept))
}
func handleAccountingRequest(w radius.ResponseWriter, r *radius.Request) {
statusType := rfc2866.AcctStatusType_Get(r.Packet)
username := rfc2865.UserName_GetString(r.Packet)
organization := rfc2865.Class_GetString(r.Packet)
log.Printf("handleAccountingRequest() username=%v, org=%v, statusType=%v", username, organization, statusType)
w.Write(r.Response(radius.CodeAccountingResponse))
var err error
defer func() {
if err != nil {
log.Printf("handleAccountingRequest() failed, err = %v", err)
}
}()
switch statusType {
case rfc2866.AcctStatusType_Value_Start:
// Start an accounting session
ra := GetAccountingFromRequest(r)
err = object.AddRadiusAccounting(ra)
case rfc2866.AcctStatusType_Value_InterimUpdate, rfc2866.AcctStatusType_Value_Stop:
// Interim update to an accounting session | Stop an accounting session
var (
newRa = GetAccountingFromRequest(r)
oldRa *object.RadiusAccounting
)
oldRa, err = object.GetRadiusAccountingBySessionId(newRa.AcctSessionId)
if err != nil {
return
}
if oldRa == nil {
if err = object.AddRadiusAccounting(newRa); err != nil {
return
}
}
stop := statusType == rfc2866.AcctStatusType_Value_Stop
err = object.InterimUpdateRadiusAccounting(oldRa, newRa, stop)
case rfc2866.AcctStatusType_Value_AccountingOn, rfc2866.AcctStatusType_Value_AccountingOff:
// By default, no Accounting-On or Accounting-Off messages are sent (no acct-on-off).
default:
err = fmt.Errorf("unsupport statusType = %v", statusType)
}
}

54
radius/server_test.go Normal file
View File

@ -0,0 +1,54 @@
// 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.
//go:build !skipCi
// +build !skipCi
package radius
import (
"context"
"testing"
"layeh.com/radius"
"layeh.com/radius/rfc2865"
)
func TestAccessRequestRejected(t *testing.T) {
packet := radius.New(radius.CodeAccessRequest, []byte(`secret`))
rfc2865.UserName_SetString(packet, "admin")
rfc2865.UserPassword_SetString(packet, "12345")
rfc2865.Class_SetString(packet, "built-in")
response, err := radius.Exchange(context.Background(), packet, "localhost:1812")
if err != nil {
t.Fatal(err)
}
if response.Code != radius.CodeAccessReject {
t.Fatalf("Expected %v, got %v", radius.CodeAccessReject, response.Code)
}
}
func TestAccessRequestAccepted(t *testing.T) {
packet := radius.New(radius.CodeAccessRequest, []byte(`secret`))
rfc2865.UserName_SetString(packet, "admin")
rfc2865.UserPassword_SetString(packet, "123")
rfc2865.Class_SetString(packet, "built-in")
response, err := radius.Exchange(context.Background(), packet, "localhost:1812")
if err != nil {
t.Fatal(err)
}
if response.Code != radius.CodeAccessAccept {
t.Fatalf("Expected %v, got %v", radius.CodeAccessAccept, response.Code)
}
}

67
radius/util.go Normal file
View File

@ -0,0 +1,67 @@
// 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 radius
import (
"fmt"
"time"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
"layeh.com/radius"
"layeh.com/radius/rfc2865"
"layeh.com/radius/rfc2866"
"layeh.com/radius/rfc2869"
)
func GetAccountingFromRequest(r *radius.Request) *object.RadiusAccounting {
acctInputOctets := int(rfc2866.AcctInputOctets_Get(r.Packet))
acctInputGigawords := int(rfc2869.AcctInputGigawords_Get(r.Packet))
acctOutputOctets := int(rfc2866.AcctOutputOctets_Get(r.Packet))
acctOutputGigawords := int(rfc2869.AcctOutputGigawords_Get(r.Packet))
organization := rfc2865.Class_GetString(r.Packet)
getAcctStartTime := func(sessionTime int) time.Time {
m, _ := time.ParseDuration(fmt.Sprintf("-%ds", sessionTime))
return time.Now().Add(m)
}
ra := &object.RadiusAccounting{
Owner: organization,
Name: "ra_" + util.GenerateId()[:6],
CreatedTime: time.Now(),
Username: rfc2865.UserName_GetString(r.Packet),
ServiceType: int64(rfc2865.ServiceType_Get(r.Packet)),
NasId: rfc2865.NASIdentifier_GetString(r.Packet),
NasIpAddr: rfc2865.NASIPAddress_Get(r.Packet).String(),
NasPortId: rfc2869.NASPortID_GetString(r.Packet),
NasPortType: int64(rfc2865.NASPortType_Get(r.Packet)),
NasPort: int64(rfc2865.NASPort_Get(r.Packet)),
FramedIpAddr: rfc2865.FramedIPAddress_Get(r.Packet).String(),
FramedIpNetmask: rfc2865.FramedIPNetmask_Get(r.Packet).String(),
AcctSessionId: rfc2866.AcctSessionID_GetString(r.Packet),
AcctSessionTime: int64(rfc2866.AcctSessionTime_Get(r.Packet)),
AcctInputTotal: int64(acctInputOctets) + int64(acctInputGigawords)*4*1024*1024*1024,
AcctOutputTotal: int64(acctOutputOctets) + int64(acctOutputGigawords)*4*1024*1024*1024,
AcctInputPackets: int64(rfc2866.AcctInputPackets_Get(r.Packet)),
AcctOutputPackets: int64(rfc2866.AcctInputPackets_Get(r.Packet)),
AcctStartTime: getAcctStartTime(int(rfc2866.AcctSessionTime_Get(r.Packet))),
AcctTerminateCause: int64(rfc2866.AcctTerminateCause_Get(r.Packet)),
LastUpdate: time.Now(),
}
return ra
}

View File

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

View File

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

View File

@ -16,6 +16,9 @@ package routers
import ( import (
"fmt" "fmt"
"net"
"net/http"
"net/url"
"strings" "strings"
"github.com/beego/beego/context" "github.com/beego/beego/context"
@ -33,6 +36,8 @@ type Response struct {
} }
func responseError(ctx *context.Context, error string, data ...interface{}) { func responseError(ctx *context.Context, error string, data ...interface{}) {
ctx.ResponseWriter.WriteHeader(http.StatusForbidden)
resp := Response{Status: "error", Msg: error} resp := Response{Status: "error", Msg: error}
switch len(data) { switch len(data) {
case 2: case 2:
@ -61,7 +66,7 @@ func denyRequest(ctx *context.Context) {
responseError(ctx, T(ctx, "auth:Unauthorized operation")) 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() clientId, clientSecret, ok := ctx.Request.BasicAuth()
if !ok { if !ok {
clientId = ctx.Input.Query("clientId") clientId = ctx.Input.Query("clientId")
@ -69,19 +74,22 @@ func getUsernameByClientIdSecret(ctx *context.Context) string {
} }
if clientId == "" || clientSecret == "" { if clientId == "" || clientSecret == "" {
return "" return "", nil
} }
application, err := object.GetApplicationByClientId(clientId) application, err := object.GetApplicationByClientId(clientId)
if err != nil { 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 { if application.ClientSecret != clientSecret {
return "" 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 { func getUsernameByKeys(ctx *context.Context) string {
@ -151,3 +159,39 @@ func parseBearerToken(ctx *context.Context) string {
return tokens[1] 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,6 +16,7 @@ package routers
import ( import (
"net/http" "net/http"
"strings"
"github.com/beego/beego/context" "github.com/beego/beego/context"
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
@ -29,42 +30,56 @@ const (
headerAllowHeaders = "Access-Control-Allow-Headers" headerAllowHeaders = "Access-Control-Allow-Headers"
) )
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")
if ctx.Input.Method() == "OPTIONS" {
ctx.ResponseWriter.WriteHeader(http.StatusOK)
}
}
func CorsFilter(ctx *context.Context) { func CorsFilter(ctx *context.Context) {
origin := ctx.Input.Header(headerOrigin) origin := ctx.Input.Header(headerOrigin)
originConf := conf.GetConfigString("origin") originConf := conf.GetConfigString("origin")
originHostname := getHostname(origin)
host := removePort(ctx.Request.Host)
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
}
if ctx.Request.Method == "POST" && ctx.Request.RequestURI == "/api/login/oauth/access_token" { if ctx.Request.Method == "POST" && ctx.Request.RequestURI == "/api/login/oauth/access_token" {
ctx.Output.Header(headerAllowOrigin, origin) setCorsHeaders(ctx, origin)
ctx.Output.Header(headerAllowMethods, "POST, GET, OPTIONS, DELETE")
ctx.Output.Header(headerAllowHeaders, "Content-Type, Authorization")
return return
} }
if ctx.Request.RequestURI == "/api/userinfo" { if ctx.Request.RequestURI == "/api/userinfo" {
ctx.Output.Header(headerAllowOrigin, origin) setCorsHeaders(ctx, origin)
ctx.Output.Header(headerAllowMethods, "POST, GET, OPTIONS, DELETE")
ctx.Output.Header(headerAllowHeaders, "Content-Type, Authorization")
return return
} }
if origin != "" && originConf != "" && origin != originConf { if origin != "" {
ok, err := object.IsOriginAllowed(origin) if origin == originConf {
if err != nil { setCorsHeaders(ctx, origin)
panic(err) } else if originHostname == host {
} setCorsHeaders(ctx, origin)
} else if isHostIntranet(host) {
if ok { setCorsHeaders(ctx, origin)
ctx.Output.Header(headerAllowOrigin, origin)
ctx.Output.Header(headerAllowMethods, "POST, GET, OPTIONS, DELETE")
ctx.Output.Header(headerAllowHeaders, "Content-Type, Authorization")
} else { } else {
ctx.ResponseWriter.WriteHeader(http.StatusForbidden) ok, err := object.IsOriginAllowed(origin)
return if err != nil {
} panic(err)
}
if ctx.Input.Method() == "OPTIONS" { if ok {
ctx.ResponseWriter.WriteHeader(http.StatusOK) setCorsHeaders(ctx, origin)
return } else {
ctx.ResponseWriter.WriteHeader(http.StatusForbidden)
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/serviceValidate", &controllers.RootController{}, "GET:CasP3ServiceValidate")
beego.Router("/cas/:organization/:application/p3/proxyValidate", &controllers.RootController{}, "GET:CasP3ProxyValidate") beego.Router("/cas/:organization/:application/p3/proxyValidate", &controllers.RootController{}, "GET:CasP3ProxyValidate")
beego.Router("/cas/:organization/:application/samlValidate", &controllers.RootController{}, "POST:SamlValidate") beego.Router("/cas/:organization/:application/samlValidate", &controllers.RootController{}, "POST:SamlValidate")
beego.Router("/scim/*", &controllers.RootController{}, "*:HandleScim")
} }

View File

@ -26,6 +26,7 @@ import (
"github.com/beego/beego/context" "github.com/beego/beego/context"
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
) )
@ -33,8 +34,59 @@ var (
oldStaticBaseUrl = "https://cdn.casbin.org" oldStaticBaseUrl = "https://cdn.casbin.org"
newStaticBaseUrl = conf.GetConfigString("staticBaseUrl") newStaticBaseUrl = conf.GetConfigString("staticBaseUrl")
enableGzip = conf.GetConfigBool("enableGzip") 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 fastAutoSignin(ctx *context.Context) (string, error) {
userId := getSessionUser(ctx)
if userId == "" {
return "", nil
}
clientId := ctx.Input.Query("client_id")
responseType := ctx.Input.Query("response_type")
redirectUri := ctx.Input.Query("redirect_uri")
scope := ctx.Input.Query("scope")
state := ctx.Input.Query("state")
nonce := ""
codeChallenge := ""
if clientId == "" || responseType != "code" || redirectUri == "" {
return "", nil
}
application, err := object.GetApplicationByClientId(clientId)
if err != nil {
return "", err
}
if application == nil {
return "", nil
}
if !application.EnableAutoSignin {
return "", nil
}
code, err := object.GetOAuthCode(userId, clientId, responseType, redirectUri, scope, state, nonce, codeChallenge, ctx.Request.Host, getAcceptLanguage(ctx))
if err != nil {
return "", err
} else if code.Message != "" {
return "", fmt.Errorf(code.Message)
}
res := fmt.Sprintf("%s?code=%s&state=%s", redirectUri, code.Code, state)
return res, nil
}
func StaticFilter(ctx *context.Context) { func StaticFilter(ctx *context.Context) {
urlPath := ctx.Request.URL.Path urlPath := ctx.Request.URL.Path
@ -48,8 +100,25 @@ 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")) { 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 return
} }
if strings.HasPrefix(urlPath, "/scim") {
return
}
path := "web/build" if urlPath == "/login/oauth/authorize" {
redirectUrl, err := fastAutoSignin(ctx)
if err != nil {
responseError(ctx, err.Error())
return
}
if redirectUrl != "" {
http.Redirect(ctx.ResponseWriter, ctx.Request, redirectUrl, http.StatusFound)
return
}
}
webBuildFolder := getWebBuildFolder()
path := webBuildFolder
if urlPath == "/" { if urlPath == "/" {
path += "/index.html" path += "/index.html"
} else { } else {
@ -57,7 +126,7 @@ func StaticFilter(ctx *context.Context) {
} }
if !util.FileExist(path) { if !util.FileExist(path) {
path = "web/build/index.html" path = webBuildFolder + "/index.html"
} }
if !util.FileExist(path) { if !util.FileExist(path) {
dir, err := os.Getwd() dir, err := os.Getwd()
@ -65,6 +134,7 @@ func StaticFilter(ctx *context.Context) {
panic(err) panic(err)
} }
dir = strings.ReplaceAll(dir, "\\", "/") 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) 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)) http.ServeContent(ctx.ResponseWriter, ctx.Request, "Casdoor frontend has encountered error...", time.Now(), strings.NewReader(errorText))
return 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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -60,3 +60,19 @@ func ReturnAnyNotEmpty(strs ...string) string {
} }
return "" 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) 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) createdTimeObj, _ := time.Parse(time.RFC3339, createdTime)
expiresAtObj := createdTimeObj.Add(time.Duration(expiresIn) * time.Second) 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) { 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)) 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", target: "http://localhost:8000",
changeOrigin: true, changeOrigin: true,
}, },
"/scim": {
target: "http://localhost:8000",
changeOrigin: true,
}
}, },
}, },
plugins: [ plugins: [

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Button, Card, Col, Input, InputNumber, Row, Select} from "antd"; import {Button, Card, Col, Input, InputNumber, Row, Select, Switch} from "antd";
import * as AdapterBackend from "./backend/AdapterBackend"; import * as AdapterBackend from "./backend/AdapterBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend"; import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
@ -81,56 +81,6 @@ class AdapterEditPage extends React.Component {
}); });
} }
renderDataSourceNameConfig() {
if (Setting.builtInObject(this.state.adapter)) {
return null;
}
return (
<React.Fragment>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.adapter.host} onChange={e => {
this.updateAdapterField("host", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Port"), i18next.t("provider:Port - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.adapter.port} min={0} max={65535} onChange={value => {
this.updateAdapterField("port", value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:User"), i18next.t("general:User - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.adapter.user} onChange={e => {
this.updateAdapterField("user", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Password"), i18next.t("general:Password - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.adapter.password} onChange={e => {
this.updateAdapterField("password", e.target.value);
}} />
</Col>
</Row>
</React.Fragment>
);
}
renderAdapter() { renderAdapter() {
return ( return (
<Card size="small" title={ <Card size="small" title={
@ -165,55 +115,6 @@ class AdapterEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Type"), i18next.t("provider:Type - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} disabled={Setting.builtInObject(this.state.adapter)} style={{width: "100%"}} value={this.state.adapter.type} onChange={(value => {
this.updateAdapterField("type", value);
const adapter = this.state.adapter;
// adapter["tableColumns"] = Setting.getAdapterTableColumns(this.state.adapter);
this.setState({
adapter: adapter,
});
})}>
{
["Database"]
.map((item, index) => <Option key={index} value={item}>{item}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("syncer:Database type"), i18next.t("syncer:Database type - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} disabled={Setting.builtInObject(this.state.adapter)} style={{width: "100%"}} value={this.state.adapter.databaseType} onChange={(value => {this.updateAdapterField("databaseType", value);})}>
{
[
{id: "mysql", name: "MySQL"},
{id: "postgres", name: "PostgreSQL"},
{id: "mssql", name: "SQL Server"},
{id: "oracle", name: "Oracle"},
{id: "sqlite3", name: "Sqlite 3"},
].map((databaseType, index) => <Option key={index} value={databaseType.id}>{databaseType.name}</Option>)
}
</Select>
</Col>
</Row>
{this.state.adapter.type === "Database" ? this.renderDataSourceNameConfig() : null}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("syncer:Database"), i18next.t("syncer:Database - Tooltip"))} :
</Col>
<Col span={22} >
<Input disabled={Setting.builtInObject(this.state.adapter)} value={this.state.adapter.database} onChange={e => {
this.updateAdapterField("database", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("syncer:Table"), i18next.t("syncer:Table - Tooltip"))} : {Setting.getLabel(i18next.t("syncer:Table"), i18next.t("syncer:Table - Tooltip"))} :
@ -225,9 +126,130 @@ class AdapterEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("adapter:Use same DB"), i18next.t("adapter:Use same DB - Tooltip"))} :
</Col>
<Col span={1} >
<Switch disabled={Setting.builtInObject(this.state.adapter)} checked={this.state.adapter.useSameDb || Setting.builtInObject(this.state.adapter)} onChange={checked => {
this.updateAdapterField("useSameDb", checked);
if (checked) {
this.updateAdapterField("type", "");
this.updateAdapterField("databaseType", "");
this.updateAdapterField("host", "");
this.updateAdapterField("port", 0);
this.updateAdapterField("user", "");
this.updateAdapterField("password", "");
this.updateAdapterField("database", "");
} else {
this.updateAdapterField("type", "Database");
this.updateAdapterField("databaseType", "mysql");
this.updateAdapterField("host", "localhost");
this.updateAdapterField("port", 3306);
this.updateAdapterField("user", "root");
this.updateAdapterField("password", "123456");
this.updateAdapterField("database", "dbName");
}
}} />
</Col>
</Row>
{
(this.state.adapter.useSameDb || Setting.builtInObject(this.state.adapter)) ? null : (
<React.Fragment>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Type"), i18next.t("provider:Type - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} disabled={Setting.builtInObject(this.state.adapter)} style={{width: "100%"}} value={this.state.adapter.type} onChange={(value => {
this.updateAdapterField("type", value);
const adapter = this.state.adapter;
// adapter["tableColumns"] = Setting.getAdapterTableColumns(this.state.adapter);
this.setState({
adapter: adapter,
});
})}>
{
["Database"]
.map((item, index) => <Option key={index} value={item}>{item}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("syncer:Database type"), i18next.t("syncer:Database type - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} disabled={Setting.builtInObject(this.state.adapter)} style={{width: "100%"}} value={this.state.adapter.databaseType} onChange={(value => {this.updateAdapterField("databaseType", value);})}>
{
[
{id: "mysql", name: "MySQL"},
{id: "postgres", name: "PostgreSQL"},
{id: "mssql", name: "SQL Server"},
{id: "oracle", name: "Oracle"},
{id: "sqlite3", name: "Sqlite 3"},
].map((databaseType, index) => <Option key={index} value={databaseType.id}>{databaseType.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.adapter.host} onChange={e => {
this.updateAdapterField("host", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Port"), i18next.t("provider:Port - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.adapter.port} min={0} max={65535} onChange={value => {
this.updateAdapterField("port", value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:User"), i18next.t("general:User - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.adapter.user} onChange={e => {
this.updateAdapterField("user", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Password"), i18next.t("general:Password - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.adapter.password} onChange={e => {
this.updateAdapterField("password", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("syncer:Database"), i18next.t("syncer:Database - Tooltip"))} :
</Col>
<Col span={22} >
<Input disabled={Setting.builtInObject(this.state.adapter)} value={this.state.adapter.database} onChange={e => {
this.updateAdapterField("database", e.target.value);
}} />
</Col>
</Row>
</React.Fragment>
)
}
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:DB Test"), i18next.t("provider:DB Test - Tooltip"))} : {Setting.getLabel(i18next.t("provider:DB test"), i18next.t("provider:DB test - Tooltip"))} :
</Col> </Col>
<Col span={2} > <Col span={2} >
<Button type={"primary"} onClick={() => { <Button type={"primary"} onClick={() => {
@ -250,7 +272,7 @@ class AdapterEditPage extends React.Component {
); );
} }
submitAdapterEdit(willExist) { submitAdapterEdit(exitAfterSave) {
const adapter = Setting.deepCopy(this.state.adapter); const adapter = Setting.deepCopy(this.state.adapter);
AdapterBackend.updateAdapter(this.state.organizationName, this.state.adapterName, adapter) AdapterBackend.updateAdapter(this.state.organizationName, this.state.adapterName, adapter)
.then((res) => { .then((res) => {
@ -260,7 +282,7 @@ class AdapterEditPage extends React.Component {
adapterName: this.state.adapter.name, adapterName: this.state.adapter.name,
}); });
if (willExist) { if (exitAfterSave) {
this.props.history.push("/adapters"); this.props.history.push("/adapters");
} else { } else {
this.props.history.push(`/adapters/${this.state.organizationName}/${this.state.adapter.name}`); this.props.history.push(`/adapters/${this.state.organizationName}/${this.state.adapter.name}`);

View File

@ -14,7 +14,7 @@
import React from "react"; import React from "react";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import {Button, Table} from "antd"; import {Button, Switch, Table} from "antd";
import moment from "moment"; import moment from "moment";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import * as AdapterBackend from "./backend/AdapterBackend"; import * as AdapterBackend from "./backend/AdapterBackend";
@ -30,14 +30,8 @@ class AdapterListPage extends BaseListPage {
owner: owner, owner: owner,
name: `adapter_${randomName}`, name: `adapter_${randomName}`,
createdTime: moment().format(), createdTime: moment().format(),
type: "Database", table: "table_name",
host: "localhost", useSameDb: true,
port: 3306,
user: "root",
password: "123456",
databaseType: "mysql",
database: "dbName",
table: "tableName",
}; };
} }
@ -118,6 +112,25 @@ class AdapterListPage extends BaseListPage {
return Setting.getFormattedDate(text); return Setting.getFormattedDate(text);
}, },
}, },
{
title: i18next.t("syncer:Table"),
dataIndex: "table",
key: "table",
width: "120px",
sorter: true,
},
{
title: i18next.t("adapter:Use same DB"),
dataIndex: "useSameDb",
key: "useSameDb",
width: "120px",
sorter: true,
render: (text, record, index) => {
return (
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
);
},
},
{ {
title: i18next.t("provider:Type"), title: i18next.t("provider:Type"),
dataIndex: "type", dataIndex: "type",
@ -129,6 +142,13 @@ class AdapterListPage extends BaseListPage {
{text: "Database", value: "Database"}, {text: "Database", value: "Database"},
], ],
}, },
{
title: i18next.t("syncer:Database type"),
dataIndex: "databaseType",
key: "databaseType",
width: "120px",
sorter: (a, b) => a.databaseType.localeCompare(b.databaseType),
},
{ {
title: i18next.t("provider:Host"), title: i18next.t("provider:Host"),
dataIndex: "host", dataIndex: "host",
@ -144,6 +164,12 @@ class AdapterListPage extends BaseListPage {
width: "100px", width: "100px",
sorter: true, sorter: true,
...this.getColumnSearchProps("port"), ...this.getColumnSearchProps("port"),
render: (text, record, index) => {
if (text === 0) {
return "";
}
return text;
},
}, },
{ {
title: i18next.t("general:User"), title: i18next.t("general:User"),
@ -161,13 +187,6 @@ class AdapterListPage extends BaseListPage {
sorter: true, sorter: true,
...this.getColumnSearchProps("password"), ...this.getColumnSearchProps("password"),
}, },
{
title: i18next.t("syncer:Database type"),
dataIndex: "databaseType",
key: "databaseType",
width: "120px",
sorter: (a, b) => a.databaseType.localeCompare(b.databaseType),
},
{ {
title: i18next.t("syncer:Database"), title: i18next.t("syncer:Database"),
dataIndex: "database", dataIndex: "database",
@ -175,13 +194,6 @@ class AdapterListPage extends BaseListPage {
width: "120px", width: "120px",
sorter: true, sorter: true,
}, },
{
title: i18next.t("syncer:Table"),
dataIndex: "table",
key: "table",
width: "120px",
sorter: true,
},
{ {
title: i18next.t("general:Action"), title: i18next.t("general:Action"),
dataIndex: "", dataIndex: "",

View File

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

View File

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

View File

@ -251,7 +251,7 @@ class CertEditPage extends React.Component {
); );
} }
submitCertEdit(willExist) { submitCertEdit(exitAfterSave) {
const cert = Setting.deepCopy(this.state.cert); const cert = Setting.deepCopy(this.state.cert);
CertBackend.updateCert(this.state.owner, this.state.certName, cert) CertBackend.updateCert(this.state.owner, this.state.certName, cert)
.then((res) => { .then((res) => {
@ -261,7 +261,7 @@ class CertEditPage extends React.Component {
certName: this.state.cert.name, certName: this.state.cert.name,
}); });
if (willExist) { if (exitAfterSave) {
this.props.history.push("/certs"); this.props.history.push("/certs");
} else { } else {
this.props.history.push(`/certs/${this.state.cert.owner}/${this.state.cert.name}`); 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); const enforcer = Setting.deepCopy(this.state.enforcer);
EnforcerBackend.updateEnforcer(this.state.organizationName, this.state.enforcerName, enforcer) EnforcerBackend.updateEnforcer(this.state.organizationName, this.state.enforcerName, enforcer)
.then((res) => { .then((res) => {
@ -208,7 +208,7 @@ class EnforcerEditPage extends React.Component {
enforcerName: this.state.enforcer.name, enforcerName: this.state.enforcer.name,
}); });
if (willExist) { if (exitAfterSave) {
this.props.history.push("/enforcers"); this.props.history.push("/enforcers");
} else { } else {
this.props.history.push(`/enforcers/${this.state.enforcer.owner}/${this.state.enforcer.name}`); 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); const group = Setting.deepCopy(this.state.group);
group["isTopGroup"] = this.state.organizations.some((organization) => organization.name === group.parentId); 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, groupName: this.state.group.name,
}); });
if (willExist) { if (exitAfterSave) {
const groupTreeUrl = sessionStorage.getItem("groupTreeUrl"); const groupTreeUrl = sessionStorage.getItem("groupTreeUrl");
if (groupTreeUrl !== null) { if (groupTreeUrl !== null) {
sessionStorage.removeItem("groupTreeUrl"); sessionStorage.removeItem("groupTreeUrl");

View File

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

View File

@ -165,7 +165,7 @@ class ModelEditPage extends React.Component {
); );
} }
submitModelEdit(willExist) { submitModelEdit(exitAfterSave) {
const model = Setting.deepCopy(this.state.model); const model = Setting.deepCopy(this.state.model);
ModelBackend.updateModel(this.state.organizationName, this.state.modelName, model) ModelBackend.updateModel(this.state.organizationName, this.state.modelName, model)
.then((res) => { .then((res) => {
@ -175,7 +175,7 @@ class ModelEditPage extends React.Component {
modelName: this.state.model.name, modelName: this.state.model.name,
}); });
if (willExist) { if (exitAfterSave) {
this.props.history.push("/models"); this.props.history.push("/models");
} else { } else {
this.props.history.push(`/models/${this.state.model.owner}/${this.state.model.name}`); 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> </Col>
</Row> </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"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("organization:Init score"), i18next.t("organization:Init score - Tooltip"))} : {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); const organization = Setting.deepCopy(this.state.organization);
organization.accountItems = organization.accountItems?.filter(accountItem => accountItem.name !== "Please select an account item"); 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")); window.dispatchEvent(new Event("storageOrganizationsChanged"));
if (willExist) { if (exitAfterSave) {
this.props.history.push("/organizations"); this.props.history.push("/organizations");
} else { } else {
this.props.history.push(`/organizations/${this.state.organization.name}`); this.props.history.push(`/organizations/${this.state.organization.name}`);

View File

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

View File

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

View File

@ -277,7 +277,10 @@ class PermissionEditPage extends React.Component {
<Col span={22} > <Col span={22} >
<Select virtual={false} mode="multiple" style={{width: "100%"}} value={this.state.permission.users} <Select virtual={false} mode="multiple" style={{width: "100%"}} value={this.state.permission.users}
onChange={(value => {this.updatePermissionField("users", value);})} 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> </Col>
</Row> </Row>
@ -288,7 +291,10 @@ class PermissionEditPage extends React.Component {
<Col span={22} > <Col span={22} >
<Select virtual={false} mode="multiple" style={{width: "100%"}} value={this.state.permission.groups} <Select virtual={false} mode="multiple" style={{width: "100%"}} value={this.state.permission.groups}
onChange={(value => {this.updatePermissionField("groups", value);})} 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> </Col>
</Row> </Row>
@ -299,8 +305,11 @@ class PermissionEditPage extends React.Component {
<Col span={22} > <Col span={22} >
<Select disabled={!this.hasRoleDefinition(this.state.model)} virtual={false} mode="multiple" style={{width: "100%"}} value={this.state.permission.roles} <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);})} 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> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
@ -312,8 +321,11 @@ class PermissionEditPage extends React.Component {
onChange={(value => { onChange={(value => {
this.updatePermissionField("domains", 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> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
@ -340,8 +352,11 @@ class PermissionEditPage extends React.Component {
<Col span={22} > <Col span={22} >
<Select virtual={false} mode={(this.state.permission.resourceType === "Custom") ? "tags" : "multiple"} style={{width: "100%"}} value={this.state.permission.resources} <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);})} 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> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} > <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) { 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"); Setting.showMessage("error", "The users and roles cannot be empty at the same time");
return; return;
@ -475,7 +490,7 @@ class PermissionEditPage extends React.Component {
permissionName: this.state.permission.name, permissionName: this.state.permission.name,
}); });
if (willExist) { if (exitAfterSave) {
this.props.history.push("/permissions"); this.props.history.push("/permissions");
} else { } else {
this.props.history.push(`/permissions/${this.state.permission.owner}/${encodeURIComponent(this.state.permission.name)}`); this.props.history.push(`/permissions/${this.state.permission.owner}/${encodeURIComponent(this.state.permission.name)}`);

View File

@ -298,6 +298,13 @@ class PermissionListPage extends BaseListPage {
filterMultiple: false, filterMultiple: false,
width: "120px", width: "120px",
sorter: true, sorter: true,
render: (text, record, index) => {
return (
<Link to={`/users/${record.owner}/${encodeURIComponent(text)}`}>
{text}
</Link>
);
},
}, },
{ {
title: i18next.t("permission:Approver"), title: i18next.t("permission:Approver"),
@ -306,6 +313,13 @@ class PermissionListPage extends BaseListPage {
filterMultiple: false, filterMultiple: false,
width: "120px", width: "120px",
sorter: true, sorter: true,
render: (text, record, index) => {
return (
<Link to={`/users/${record.owner}/${encodeURIComponent(text)}`}>
{text}
</Link>
);
},
}, },
{ {
title: i18next.t("permission:Approve time"), title: i18next.t("permission:Approve time"),

Some files were not shown because too many files have changed in this diff Show More