Compare commits

...

116 Commits

Author SHA1 Message Date
e082cf10e0 fix: fix Okta provider no host issue (#2467) 2023-11-01 18:14:39 +08:00
3215b88eae fix: ADFS GetToken() and GetUserInfo() bug (#2468)
* fix adfs bug

* Update adfs.go

---------

Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2023-11-01 17:58:17 +08:00
9703f3f712 Support Apple OAuth login now 2023-10-31 23:10:36 +08:00
140737b2f6 Fix some bugs in Apple OAuth login path 2023-10-31 23:10:36 +08:00
b285144a64 ci: support MySQL data sync (#2443)
* feat: support tool for mysql master-slave sync

* feat: support mysql master-master sync

* feat: improve log

* feat: improve code

* fix: fix bug when len(res) ==0

* fix: fix bug when len(res) ==0

* feat: support master-slave sync

* feat: add deleteSlaveUser for TestStopMasterSlaveSync

* feat: add deleteSlaveUser for TestStopMasterSlaveSync
2023-10-31 21:00:09 +08:00
49c6ce2221 refactor: New Crowdin translations (#1667)
* refactor: New Crowdin translations by Github Action

* refactor: New Crowdin Backend translations by Github Action

---------

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2023-10-31 18:11:05 +08:00
2398e69012 Improve fastAutoSignin() 2023-10-31 16:54:30 +08:00
ade9de8256 Add DumpToFile() to export init_data.json 2023-10-31 14:39:50 +08:00
1bf5497d08 Improve error handling for GetUser() 2023-10-31 14:01:37 +08:00
cf10738f45 Fix typo in AddUserKeys() 2023-10-31 13:31:12 +08:00
ac00713c20 Improve error handling for object/user.go 2023-10-31 13:20:44 +08:00
febb27f765 Remove useless fields in GenerateCasToken() 2023-10-30 18:45:34 +08:00
49a981f787 fix: fix that GROUPS is a reserved keyword introduced in MySQL 8.0 (#2458)
Co-authored-by: aidenlu <aiden_lu@wochacha.com>
2023-10-30 10:59:48 +08:00
34b1945180 feat: fix bugs in custom app sso login with WebAuthn authentication (#2457)
Co-authored-by: aidenlu <aiden_lu@wochacha.com>
2023-10-30 10:54:34 +08:00
b320cca789 Can disable ldapServerPort by setting to empty string 2023-10-29 23:55:08 +08:00
b38654a45a Add renderAiAssistant() 2023-10-28 23:58:51 +08:00
f77fafae24 Fix hidden top navbar item 2023-10-28 17:07:29 +08:00
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
3b2820cbe3 feat: make redirect_uri really optional in logout route (#2342) 2023-09-18 21:47:49 +08:00
764e88f603 Change MFA issuer 2023-09-18 17:40:11 +08:00
7f298efebc feat: fix Apple OAuth issue (#2338)
* feat: fix sign in with apple bug

* fix username
2023-09-18 17:04:03 +08:00
0fc48bb6cd Remove escapePath() to fix Unicode resource filenames 2023-09-17 21:31:22 +08:00
c3b3840994 fix: fix update score permission check (#2335)
* fix: Fixed the missing permission check when updating the score field.
* Update object/user_util.go
2023-09-16 21:06:20 +08:00
eacc3fae5a fix: handle more errors in downloadImage() 2023-09-15 22:53:09 +08:00
ce7a2e924b feat: fix XML format issue in GenerateCasToken() 2023-09-15 22:38:04 +08:00
152 changed files with 4737 additions and 1858 deletions

View File

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

View File

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

3
.gitignore vendored
View File

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

View File

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

View File

@ -46,6 +46,7 @@ p, *, *, POST, /api/login, *, *
p, *, *, GET, /api/get-app-login, *, *
p, *, *, POST, /api/logout, *, *
p, *, *, GET, /api/logout, *, *
p, *, *, POST, /api/callback, *, *
p, *, *, GET, /api/get-account, *, *
p, *, *, GET, /api/userinfo, *, *
p, *, *, GET, /api/user, *, *
@ -80,6 +81,7 @@ p, *, *, GET, /api/get-saml-login, *, *
p, *, *, POST, /api/acs, *, *
p, *, *, GET, /api/saml/metadata, *, *
p, *, *, *, /cas, *, *
p, *, *, *, /scim, *, *
p, *, *, *, /api/webauthn, *, *
p, *, *, GET, /api/get-release, *, *
p, *, *, GET, /api/get-default-application, *, *
@ -125,8 +127,14 @@ func IsAllowed(subOwner string, subName string, method string, urlPath string, o
return true
}
if user != nil && user.IsAdmin && (subOwner == objOwner || (objOwner == "admin")) {
return true
if user != nil {
if user.IsDeleted {
return false
}
if user.IsAdmin && (subOwner == objOwner || (objOwner == "admin")) {
return true
}
}
res, err := Enforcer.Enforce(subOwner, subName, method, urlPath, objOwner, objName)
@ -139,7 +147,7 @@ func IsAllowed(subOwner string, subName string, method string, urlPath string, o
func isAllowedInDemoMode(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool {
if method == "POST" {
if strings.HasPrefix(urlPath, "/api/login") || urlPath == "/api/logout" || urlPath == "/api/signup" || urlPath == "/api/send-verification-code" || urlPath == "/api/send-email" || urlPath == "/api/verify-captcha" {
if strings.HasPrefix(urlPath, "/api/login") || urlPath == "/api/logout" || urlPath == "/api/signup" || urlPath == "/api/callback" || urlPath == "/api/send-verification-code" || urlPath == "/api/send-email" || urlPath == "/api/verify-captcha" {
return true
} else if urlPath == "/api/update-user" {
// Allow ordinary users to update their own information

View File

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

View File

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

View File

@ -18,7 +18,6 @@ import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/casdoor/casdoor/form"
@ -119,20 +118,10 @@ func (c *ApiController) Signup() {
}
}
id := util.GenerateId()
if application.GetSignupItemRule("ID") == "Incremental" {
lastUser, err := object.GetLastUser(authForm.Organization)
if err != nil {
c.ResponseError(err.Error())
return
}
lastIdInt := -1
if lastUser != nil {
lastIdInt = util.ParseInt(lastUser.Id)
}
id = strconv.Itoa(lastIdInt + 1)
id, err := object.GenerateIdForNewUser(application)
if err != nil {
c.ResponseError(err.Error())
return
}
username := authForm.Username
@ -309,27 +298,32 @@ func (c *ApiController) Logout() {
return
}
if application.IsRedirectUriValid(redirectUri) {
if user == "" {
user = util.GetId(token.Organization, token.User)
}
if user == "" {
user = util.GetId(token.Organization, token.User)
}
c.ClearUserSession()
// TODO https://github.com/casdoor/casdoor/pull/1494#discussion_r1095675265
owner, username := util.GetOwnerAndNameFromId(user)
c.ClearUserSession()
// TODO https://github.com/casdoor/casdoor/pull/1494#discussion_r1095675265
owner, username := util.GetOwnerAndNameFromId(user)
_, err := object.DeleteSessionId(util.GetSessionId(owner, username, object.CasdoorApplication), c.Ctx.Input.CruSession.SessionID())
if err != nil {
c.ResponseError(err.Error())
_, err = object.DeleteSessionId(util.GetSessionId(owner, username, object.CasdoorApplication), c.Ctx.Input.CruSession.SessionID())
if err != nil {
c.ResponseError(err.Error())
return
}
util.LogInfo(c.Ctx, "API: [%s] logged out", user)
if redirectUri == "" {
c.ResponseOk()
return
} else {
if application.IsRedirectUriValid(redirectUri) {
c.Ctx.Redirect(http.StatusFound, fmt.Sprintf("%s?state=%s", strings.TrimRight(redirectUri, "/"), state))
} else {
c.ResponseError(fmt.Sprintf(c.T("token:Redirect URI: %s doesn't exist in the allowed Redirect URI list"), redirectUri))
return
}
util.LogInfo(c.Ctx, "API: [%s] logged out", user)
c.Ctx.Redirect(http.StatusFound, fmt.Sprintf("%s?state=%s", strings.TrimRight(redirectUri, "/"), state))
} else {
c.ResponseError(fmt.Sprintf(c.T("token:Redirect URI: %s doesn't exist in the allowed Redirect URI list"), redirectUri))
return
}
}
}

View File

@ -20,6 +20,7 @@ import (
"encoding/xml"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
@ -476,11 +477,10 @@ func (c *ApiController) Login() {
c.ResponseError(fmt.Sprintf(c.T("auth:The provider: %s is not enabled for the application"), provider.Name))
return
}
userInfo := &idp.UserInfo{}
if provider.Category == "SAML" {
// SAML
userInfo.Id, err = object.ParseSamlResponse(authForm.SamlResponse, provider, c.Ctx.Request.Host)
userInfo, err = object.ParseSamlResponse(authForm.SamlResponse, provider, c.Ctx.Request.Host)
if err != nil {
c.ResponseError(err.Error())
return
@ -488,7 +488,11 @@ func (c *ApiController) Login() {
} else if provider.Category == "OAuth" || provider.Category == "Web3" {
// OAuth
idpInfo := object.FromProviderToIdpInfo(c.Ctx, provider)
idProvider := idp.GetIdProvider(idpInfo, authForm.RedirectUri)
idProvider, err := idp.GetIdProvider(idpInfo, authForm.RedirectUri)
if err != nil {
c.ResponseError(err.Error())
return
}
if idProvider == nil {
c.ResponseError(fmt.Sprintf(c.T("storage:The provider type: %s is not supported"), provider.Type))
return
@ -523,7 +527,8 @@ func (c *ApiController) Login() {
if authForm.Method == "signup" {
user := &object.User{}
if provider.Category == "SAML" {
user, err = object.GetUser(util.GetId(application.Organization, userInfo.Id))
// The userInfo.Id is the NameID in SAML response, it could be name / email / phone
user, err = object.GetUserByFields(application.Organization, userInfo.Id)
if err != nil {
c.ResponseError(err.Error())
return
@ -614,11 +619,16 @@ func (c *ApiController) Login() {
return
}
userId := userInfo.Id
if userId == "" {
userId = util.GenerateId()
}
user = &object.User{
Owner: application.Organization,
Name: userInfo.Username,
CreatedTime: util.GetCurrentTime(),
Id: util.GenerateId(),
Id: userId,
Type: "normal-user",
DisplayName: userInfo.DisplayName,
Avatar: userInfo.AvatarUrl,
@ -645,6 +655,15 @@ func (c *ApiController) Login() {
c.ResponseError(fmt.Sprintf(c.T("auth:Failed to create user, user information is invalid: %s"), util.StructToJson(user)))
return
}
if providerItem.SignupGroup != "" {
user.Groups = []string{providerItem.SignupGroup}
_, err = object.UpdateUser(user.GetId(), user, []string{"groups"}, false)
if err != nil {
c.ResponseError(err.Error())
return
}
}
}
// sync info from 3rd-party if possible
@ -673,6 +692,7 @@ func (c *ApiController) Login() {
record2.User = user.Name
util.SafeGoroutine(func() { object.AddRecord(record2) })
} else if provider.Category == "SAML" {
// TODO: since we get the user info from SAML response, we can try to create the user
resp = &Response{Status: "error", Msg: fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(application.Organization, userInfo.Id))}
}
// resp = &Response{Status: "ok", Msg: "", Data: res}
@ -896,3 +916,16 @@ func (c *ApiController) GetCaptchaStatus() {
}
c.ResponseOk(captchaEnabled)
}
// Callback
// @Title Callback
// @Tag Callback API
// @Description Get Login Error Counts
// @router /api/Callback [post]
func (c *ApiController) Callback() {
code := c.GetString("code")
state := c.GetString("state")
frontendCallbackUrl := fmt.Sprintf("/callback?code=%s&state=%s", code, state)
c.Ctx.Redirect(http.StatusFound, frontendCallbackUrl)
}

View File

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

View File

@ -191,7 +191,7 @@ func (c *ApiController) UpdatePolicy() {
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 {
c.ResponseError(err.Error())
return
@ -210,7 +210,7 @@ func (c *ApiController) AddPolicy() {
return
}
affected, err := object.AddPolicy(id, util.CasbinToSlice(policy))
affected, err := object.AddPolicy(id, policy.Ptype, util.CasbinToSlice(policy))
if err != nil {
c.ResponseError(err.Error())
return
@ -229,7 +229,7 @@ func (c *ApiController) RemovePolicy() {
return
}
affected, err := object.RemovePolicy(id, util.CasbinToSlice(policy))
affected, err := object.RemovePolicy(id, policy.Ptype, util.CasbinToSlice(policy))
if err != nil {
c.ResponseError(err.Error())
return

View File

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

View File

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

View File

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

View File

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

27
controllers/scim.go Normal file
View File

@ -0,0 +1,27 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"strings"
"github.com/casdoor/casdoor/scim"
)
func (c *RootController) HandleScim() {
path := c.Ctx.Request.URL.Path
c.Ctx.Request.URL.Path = strings.TrimPrefix(path, "/scim")
scim.Server.ServeHTTP(c.Ctx.ResponseWriter, c.Ctx.Request)
}

View File

@ -160,35 +160,51 @@ func (c *ApiController) GetUser() {
id = util.GetId(userFromUserId.Owner, userFromUserId.Name)
}
if owner == "" {
owner = util.GetOwnerFromId(id)
}
var user *object.User
organization, err := object.GetOrganization(util.GetId("admin", owner))
if err != nil {
c.ResponseError(err.Error())
return
}
if id == "" && owner == "" {
switch {
case email != "":
user, err = object.GetUserByEmailOnly(email)
case phone != "":
user, err = object.GetUserByPhoneOnly(phone)
case userId != "":
user, err = object.GetUserByUserIdOnly(userId)
}
} else {
if owner == "" {
owner = util.GetOwnerFromId(id)
}
if !organization.IsProfilePublic {
requestUserId := c.GetSessionUsername()
hasPermission, err := object.CheckUserPermission(requestUserId, id, false, c.GetAcceptLanguage())
if !hasPermission {
organization, err := object.GetOrganization(util.GetId("admin", owner))
if err != nil {
c.ResponseError(err.Error())
return
}
}
if organization == nil {
c.ResponseError(fmt.Sprintf("the organization: %s is not found", owner))
return
}
var user *object.User
switch {
case email != "":
user, err = object.GetUserByEmail(owner, email)
case phone != "":
user, err = object.GetUserByPhone(owner, phone)
case userId != "":
user = userFromUserId
default:
user, err = object.GetUser(id)
if !organization.IsProfilePublic {
requestUserId := c.GetSessionUsername()
hasPermission, err := object.CheckUserPermission(requestUserId, id, false, c.GetAcceptLanguage())
if !hasPermission {
c.ResponseError(err.Error())
return
}
}
switch {
case email != "":
user, err = object.GetUserByEmail(owner, email)
case phone != "":
user, err = object.GetUserByPhone(owner, phone)
case userId != "":
user = userFromUserId
default:
user, err = object.GetUser(id)
}
}
if err != nil {
@ -466,7 +482,7 @@ func (c *ApiController) SetPassword() {
return
}
}
} else {
} else if code == "" {
msg := object.CheckPassword(targetUser, oldPassword, c.GetAcceptLanguage())
if msg != "" {
c.ResponseError(msg)
@ -560,11 +576,11 @@ func (c *ApiController) GetUserCount() {
c.ResponseOk(count)
}
// AddUserkeys
// @Title AddUserkeys
// AddUserKeys
// @Title AddUserKeys
// @router /add-user-keys [post]
// @Tag User API
func (c *ApiController) AddUserkeys() {
func (c *ApiController) AddUserKeys() {
var user object.User
err := json.Unmarshal(c.Ctx.Input.RequestBody, &user)
if err != nil {
@ -573,7 +589,7 @@ func (c *ApiController) AddUserkeys() {
}
isAdmin := c.IsAdmin()
affected, err := object.AddUserkeys(&user, isAdmin)
affected, err := object.AddUserKeys(&user, isAdmin)
if err != nil {
c.ResponseError(err.Error())
return

View File

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

View File

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

View File

@ -123,7 +123,9 @@ func (a *AzureACSEmailProvider) sendEmail(e *Email) error {
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 {
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()
// Response error Handling
if resp.StatusCode == http.StatusBadRequest {
if resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusUnauthorized {
commError := ErrorResponse{}
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
}
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" {
return NewAzureACSEmailProvider(appId, host)
return NewAzureACSEmailProvider(clientSecret, host)
} else {
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/beevik/etree v1.1.0
github.com/casbin/casbin v1.9.1 // indirect
github.com/casbin/casbin/v2 v2.37.0
github.com/casdoor/go-sms-sender v0.14.0
github.com/casbin/casbin/v2 v2.77.2
github.com/casdoor/go-sms-sender v0.15.0
github.com/casdoor/gomail/v2 v2.0.1
github.com/casdoor/notify v0.43.0
github.com/casdoor/notify v0.45.0
github.com/casdoor/oss v1.3.0
github.com/casdoor/xorm-adapter/v3 v3.0.4
github.com/casvisor/casvisor-go-sdk v1.0.3
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
github.com/denisenkom/go-mssqldb v0.9.0
github.com/elazarl/go-bindata-assetfs v1.0.1 // indirect
github.com/elimity-com/scim v0.0.0-20230426070224-941a5eac92f3
github.com/fogleman/gg v1.3.0
github.com/forestmgy/ldapserver v1.1.0
github.com/go-git/go-git/v5 v5.6.0
@ -54,6 +55,7 @@ require (
github.com/stripe/stripe-go/v74 v74.29.0
github.com/tealeg/xlsx v1.0.5
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/xorm-io/builder v0.3.13
github.com/xorm-io/core v0.7.4
@ -62,9 +64,11 @@ require (
golang.org/x/crypto v0.12.0
golang.org/x/net v0.14.0
golang.org/x/oauth2 v0.11.0
golang.org/x/text v0.13.0 // indirect
google.golang.org/api v0.138.0
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0
layeh.com/radius v0.0.0-20221205141417-e7fbddd11d68
maunium.net/go/mautrix v0.16.0
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/v2 v2.1.0/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/casbin/casbin/v2 v2.28.3/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
github.com/casbin/casbin/v2 v2.37.0 h1:/poEwPSovi4bTOcP752/CsTQiRz2xycyVKFG7GUhbDw=
github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
github.com/casdoor/go-sms-sender v0.14.0 h1:yqrzWIHUg64OYPynzF5Fr0XDuCWIWxtXIjOQAAkRKuw=
github.com/casdoor/go-sms-sender v0.14.0/go.mod h1:cQs7qqohMJBgIVZebOCB8ko09naG1vzFJEH59VNIscs=
github.com/casbin/casbin/v2 v2.77.2 h1:yQinn/w9x8AswiwqwtrXz93VU48R1aYTXdHEx4RI3jM=
github.com/casbin/casbin/v2 v2.77.2/go.mod h1:mzGx0hYW9/ksOSpw3wNjk3NRAroq5VMFYUQ6G43iGPk=
github.com/casdoor/go-reddit/v2 v2.1.0 h1:kIbfdJ7AA7H0uTQ8s0q4GGZqSS5V9wVE74RrXyD9XPs=
github.com/casdoor/go-reddit/v2 v2.1.0/go.mod h1:eagkvwlZ4Hcsuc/uQsLHYEulz5jN65SVSwV/AIE7zsc=
github.com/casdoor/go-sms-sender v0.15.0 h1:9SWj/jd5c7jIteTRUrqbkpWbtIXMDv+t1CEfDhO06m0=
github.com/casdoor/go-sms-sender v0.15.0/go.mod h1:cQs7qqohMJBgIVZebOCB8ko09naG1vzFJEH59VNIscs=
github.com/casdoor/gomail/v2 v2.0.1 h1:J+FG6x80s9e5lBHUn8Sv0Y56mud34KiWih5YdmudR/w=
github.com/casdoor/gomail/v2 v2.0.1/go.mod h1:VnGPslEAtpix5FjHisR/WKB1qvZDBaujbikxDe9d+2Q=
github.com/casdoor/notify v0.43.0 h1:NukyVZ9l7d2TSlB5YWKJyDsPmHCvwKQVi9rWDprtcU4=
github.com/casdoor/notify v0.43.0/go.mod h1:qDmQM5vr2uU01BEuDC6pY6ryahSU11cXPqlHFW232Do=
github.com/casdoor/notify v0.45.0 h1:OlaFvcQFjGOgA4mRx07M8AH1gvb5xNo21mcqrVGlLgk=
github.com/casdoor/notify v0.45.0/go.mod h1:wNHQu0tiDROMBIvz0j3Om3Lhd5yZ+AIfnFb8MYb8OLQ=
github.com/casdoor/oss v1.3.0 h1:D5pcz65tJRqJrWY11Ks7D9LUsmlhqqMHugjDhSxWTvk=
github.com/casdoor/oss v1.3.0/go.mod h1:YOi6KpG1pZHTkiy9AYaqI0UaPfE7YkaA07d89f1idqY=
github.com/casdoor/xorm-adapter/v3 v3.0.4 h1:vB04Ao8n2jA7aFBI9F+gGXo9+Aa1IQP6mTdo50913DM=
@ -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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/di-wu/parser v0.2.2 h1:I9oHJ8spBXOeL7Wps0ffkFFFiXJf/pk7NX9lcAMqRMU=
github.com/di-wu/parser v0.2.2/go.mod h1:SLp58pW6WamdmznrVRrw2NTyn4wAvT9rrEFynKX7nYo=
github.com/di-wu/xsd-datetime v1.0.0 h1:vZoGNkbzpBNoc+JyfVLEbutNDNydYV8XwHeV7eUJoxI=
github.com/di-wu/xsd-datetime v1.0.0/go.mod h1:i3iEhrP3WchwseOBeIdW/zxeoleXTOzx1WyDXgdmOww=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/drswork/go-twitter v0.0.0-20221107160839-dea1b6ed53d7 h1:uh1GSejOhVPRQmoXZxY82TiewZB8QXiaP1skL7Nun3Y=
github.com/drswork/go-twitter v0.0.0-20221107160839-dea1b6ed53d7/go.mod h1:ncTaGuXc5v7AuiVekeJ0Nwh8Bf4cudukoj0qM/15UZE=
@ -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.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
github.com/elimity-com/scim v0.0.0-20230426070224-941a5eac92f3 h1:+zrUtdBUJpY9qptMaaY3CA3T/lBI2+QqfUbzM2uxJss=
github.com/elimity-com/scim v0.0.0-20230426070224-941a5eac92f3/go.mod h1:JkjcmqbLW+khwt2fmBPJFBhx2zGZ8XobRZ+O0VhlwWo=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@ -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/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/scim2/filter-parser/v2 v2.2.0 h1:QGadEcsmypxg8gYChRSM2j1edLyE/2j72j+hdmI4BJM=
github.com/scim2/filter-parser/v2 v2.2.0/go.mod h1:jWnkDToqX/Y0ugz0P5VvpVEUKcWcyHHj+X+je9ce5JA=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
github.com/sendgrid/sendgrid-go v3.13.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
@ -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/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
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/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/utahta/go-linenotify v0.5.0 h1:E1tJaB/XhqRY/iz203FD0MaHm10DjQPOq5/Mem2A3Gs=
github.com/utahta/go-linenotify v0.5.0/go.mod h1:KsvBXil2wx+ByaCR0e+IZKTbp4pDesc7yjzRigLf6pE=
github.com/vartanbeno/go-reddit/v2 v2.0.1 h1:P6ITpf5YHjdy7DHZIbUIDn/iNAoGcEoDQnMa+L4vutw=
github.com/vartanbeno/go-reddit/v2 v2.0.1/go.mod h1:758/S10hwZSLm43NPtwoNQdZFSg3sjB5745Mwjb0ANI=
github.com/volcengine/volc-sdk-golang v1.0.117 h1:ykFVSwsVq9qvIoWP9jeP+VKNAUjrblAdsZl46yVWiH8=
github.com/volcengine/volc-sdk-golang v1.0.117/go.mod h1:ojXSFvj404o2UKnZR9k9LUUWIUU+9XtlRlzk2+UFc/M=
github.com/wendal/errors v0.0.0-20181209125328-7f31f4b264ec/go.mod h1:Q12BUT7DqIlHRmgv3RskH+UCM/4eqVMgI0EMmlSpAXc=
@ -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-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-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-20201016220609-9e8e0b390897/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.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -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.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
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.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=

View File

@ -19,7 +19,7 @@
"The provider: %s is not enabled for the application": "Le fournisseur :%s n'est pas activé pour l'application",
"Unauthorized operation": "Opération non autorisée",
"Unknown authentication type (not password or provider), form = %s": "Type d'authentification inconnu (pas de mot de passe ou de fournisseur), formulaire = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags"
"User's tag: %s is not listed in the application's tags": "Le tag de lutilisateur %s nest pas répertorié dans les tags de lapplication"
},
"cas": {
"Service %s and %s do not match": "Les services %s et %s ne correspondent pas"
@ -43,7 +43,7 @@
"Phone number is invalid": "Le numéro de téléphone est invalide",
"Session outdated, please login again": "Session expirée, veuillez vous connecter à nouveau",
"The user is forbidden to sign in, please contact the administrator": "L'utilisateur est interdit de se connecter, veuillez contacter l'administrateur",
"The user: %s doesn't exist in LDAP server": "The user: %s doesn't exist in LDAP server",
"The user: %s doesn't exist in LDAP server": "L'utilisateur %s n'existe pas sur le serveur LDAP",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "Le nom d'utilisateur ne peut contenir que des caractères alphanumériques, des traits soulignés ou des tirets, ne peut pas avoir de tirets ou de traits soulignés consécutifs et ne peut pas commencer ou se terminer par un tiret ou un trait souligné.",
"Username already exists": "Nom d'utilisateur existe déjà",
"Username cannot be an email address": "Nom d'utilisateur ne peut pas être une adresse e-mail",
@ -53,7 +53,7 @@
"Username must have at least 2 characters": "Le nom d'utilisateur doit comporter au moins 2 caractères",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Vous avez entré le mauvais mot de passe ou code plusieurs fois, veuillez attendre %d minutes et réessayer",
"Your region is not allow to signup by phone": "Votre région n'est pas autorisée à s'inscrire par téléphone",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect": "mot de passe ou code invalide",
"password or code is incorrect, you have %d remaining chances": "Le mot de passe ou le code est incorrect, il vous reste %d chances",
"unsupported password type: %s": "Type de mot de passe non pris en charge : %s"
},
@ -61,8 +61,8 @@
"Missing parameter": "Paramètre manquant",
"Please login first": "Veuillez d'abord vous connecter",
"The user: %s doesn't exist": "L'utilisateur : %s n'existe pas",
"don't support captchaProvider: ": "Ne pas prendre en charge la captchaProvider",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode"
"don't support captchaProvider: ": "ne prend pas en charge captchaProvider: ",
"this operation is not allowed in demo mode": "cette opération nest pas autorisée en mode démo"
},
"ldap": {
"Ldap server exist": "Le serveur LDAP existe"

View File

@ -24,14 +24,6 @@
"cas": {
"Service %s and %s do not match": "Service %s and %s do not match"
},
"chat": {
"The chat type must be \\\"AI\\\"": "The chat type must be \\\"AI\\\"",
"The chat: %s is not found": "The chat: %s is not found",
"The message is invalid": "The message is invalid",
"The message: %s is not found": "The message: %s is not found",
"The provider: %s is invalid": "The provider: %s is invalid",
"The provider: %s is not found": "The provider: %s is not found"
},
"check": {
"Affiliation cannot be blank": "Affiliation cannot be blank",
"DisplayName cannot be blank": "DisplayName cannot be blank",

View File

@ -24,14 +24,6 @@
"cas": {
"Service %s and %s do not match": "Service %s and %s do not match"
},
"chat": {
"The chat type must be \\\"AI\\\"": "The chat type must be \\\"AI\\\"",
"The chat: %s is not found": "The chat: %s is not found",
"The message is invalid": "The message is invalid",
"The message: %s is not found": "The message: %s is not found",
"The provider: %s is invalid": "The provider: %s is invalid",
"The provider: %s is not found": "The provider: %s is not found"
},
"check": {
"Affiliation cannot be blank": "Affiliation cannot be blank",
"DisplayName cannot be blank": "DisplayName cannot be blank",

View File

@ -24,14 +24,6 @@
"cas": {
"Service %s and %s do not match": "Service %s and %s do not match"
},
"chat": {
"The chat type must be \\\"AI\\\"": "The chat type must be \\\"AI\\\"",
"The chat: %s is not found": "The chat: %s is not found",
"The message is invalid": "The message is invalid",
"The message: %s is not found": "The message: %s is not found",
"The provider: %s is invalid": "The provider: %s is invalid",
"The provider: %s is not found": "The provider: %s is not found"
},
"check": {
"Affiliation cannot be blank": "Affiliation cannot be blank",
"DisplayName cannot be blank": "DisplayName cannot be blank",

View File

@ -43,7 +43,7 @@
"Phone number is invalid": "无效手机号",
"Session outdated, please login again": "会话已过期,请重新登录",
"The user is forbidden to sign in, please contact the administrator": "该用户被禁止登录,请联系管理员",
"The user: %s doesn't exist in LDAP server": "The user: %s doesn't exist in LDAP server",
"The user: %s doesn't exist in LDAP server": "用户: %s 在LDAP服务器中未找到",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "用户名只能包含字母数字字符、下划线或连字符,不能有连续的连字符或下划线,也不能以连字符或下划线开头或结尾",
"Username already exists": "用户名已存在",
"Username cannot be an email address": "用户名不可以是邮箱地址",
@ -62,7 +62,7 @@
"Please login first": "请先登录",
"The user: %s doesn't exist": "用户: %s不存在",
"don't support captchaProvider: ": "不支持验证码提供商: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode"
"this operation is not allowed in demo mode": "demo模式下不允许该操作"
},
"ldap": {
"Ldap server exist": "LDAP服务器已存在"

View File

@ -19,7 +19,6 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"time"
@ -84,6 +83,7 @@ func (idp *AdfsIdProvider) GetToken(code string) (*oauth2.Token, error) {
payload.Set("code", code)
payload.Set("grant_type", "authorization_code")
payload.Set("client_id", idp.Config.ClientID)
payload.Set("client_secret", idp.Config.ClientSecret)
payload.Set("redirect_uri", idp.Config.RedirectURL)
resp, err := idp.Client.PostForm(idp.Config.Endpoint.TokenURL, payload)
if err != nil {
@ -118,11 +118,25 @@ func (idp *AdfsIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
if err != nil {
return nil, err
}
body, err := ioutil.ReadAll(resp.Body)
keyset, err := jwk.ParseKey(body)
body, err := io.ReadAll(resp.Body)
var respKeys struct {
Keys []interface{} `json:"keys"`
}
if err := json.Unmarshal(body, &respKeys); err != nil {
return nil, err
}
respKey, err := json.Marshal(&(respKeys.Keys[0]))
if err != nil {
return nil, err
}
keyset, err := jwk.ParseKey(respKey)
if err != nil {
return nil, err
}
tokenSrc := []byte(token.AccessToken)
publicKey, _ := keyset.PublicKey()
idToken, _ := jwt.Parse(tokenSrc, jwt.WithVerify(jwa.RS256, publicKey))

View File

@ -19,6 +19,7 @@ import (
"net/http"
"net/url"
"reflect"
"strings"
"time"
"github.com/casdoor/casdoor/util"
@ -88,7 +89,7 @@ type GothIdProvider struct {
Session goth.Session
}
func NewGothIdProvider(providerType string, clientId string, clientSecret string, redirectUrl string, hostUrl string) *GothIdProvider {
func NewGothIdProvider(providerType string, clientId string, clientSecret string, clientId2 string, clientSecret2 string, redirectUrl string, hostUrl string) (*GothIdProvider, error) {
var idp GothIdProvider
switch providerType {
case "Amazon":
@ -97,8 +98,27 @@ func NewGothIdProvider(providerType string, clientId string, clientSecret string
Session: &amazon.Session{},
}
case "Apple":
if !strings.Contains(redirectUrl, "/api/callback") {
redirectUrl = strings.Replace(redirectUrl, "/callback", "/api/callback", 1)
}
iat := time.Now().Unix()
exp := iat + 60*60
sp := apple.SecretParams{
ClientId: clientId,
TeamId: clientSecret,
KeyId: clientId2,
PKCS8PrivateKey: clientSecret2,
Iat: int(iat),
Exp: int(exp),
}
secret, err := apple.MakeSecret(sp)
if err != nil {
return nil, err
}
idp = GothIdProvider{
Provider: apple.New(clientId, clientSecret, redirectUrl, nil),
Provider: apple.New(clientId, *secret, redirectUrl, nil),
Session: &apple.Session{},
}
case "AzureAD":
@ -382,17 +402,19 @@ func NewGothIdProvider(providerType string, clientId string, clientSecret string
Session: &zoom.Session{},
}
default:
return nil
return nil, fmt.Errorf("OAuth Goth provider type: %s is not supported", providerType)
}
return &idp
return &idp, nil
}
// SetHttpClient
// Goth's idp all implement the Client method, but since the goth.Provider interface does not provide to modify idp's client method, reflection is required
func (idp *GothIdProvider) SetHttpClient(client *http.Client) {
idpClient := reflect.ValueOf(idp.Provider).Elem().FieldByName("HTTPClient")
idpClient.Set(reflect.ValueOf(client))
if idpClient.IsValid() {
idpClient.Set(reflect.ValueOf(client))
}
}
func (idp *GothIdProvider) GetToken(code string) (*oauth2.Token, error) {
@ -468,6 +490,8 @@ func getUser(gothUser goth.User, provider string) *UserInfo {
if provider == "steam" {
user.Username = user.Id
user.Email = ""
} else if provider == "apple" {
user.Username = util.GetUsernameFromEmail(user.Email)
}
return &user
}

View File

@ -15,6 +15,7 @@
package idp
import (
"fmt"
"net/http"
"strings"
@ -33,13 +34,15 @@ type UserInfo struct {
}
type ProviderInfo struct {
Type string
SubType string
ClientId string
ClientSecret string
AppId string
HostUrl string
RedirectUrl string
Type string
SubType string
ClientId string
ClientSecret string
ClientId2 string
ClientSecret2 string
AppId string
HostUrl string
RedirectUrl string
TokenURL string
AuthURL string
@ -53,71 +56,71 @@ type IdProvider interface {
GetUserInfo(token *oauth2.Token) (*UserInfo, error)
}
func GetIdProvider(idpInfo *ProviderInfo, redirectUrl string) IdProvider {
func GetIdProvider(idpInfo *ProviderInfo, redirectUrl string) (IdProvider, error) {
switch idpInfo.Type {
case "GitHub":
return NewGithubIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewGithubIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "Google":
return NewGoogleIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewGoogleIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "QQ":
return NewQqIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewQqIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "WeChat":
return NewWeChatIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewWeChatIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "Facebook":
return NewFacebookIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewFacebookIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "DingTalk":
return NewDingTalkIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewDingTalkIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "Weibo":
return NewWeiBoIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewWeiBoIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "Gitee":
return NewGiteeIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewGiteeIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "LinkedIn":
return NewLinkedInIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewLinkedInIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "WeCom":
if idpInfo.SubType == "Internal" {
return NewWeComInternalIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewWeComInternalIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
} else if idpInfo.SubType == "Third-party" {
return NewWeComIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewWeComIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
} else {
return nil
return nil, fmt.Errorf("WeCom provider subType: %s is not supported", idpInfo.SubType)
}
case "Lark":
return NewLarkIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewLarkIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "GitLab":
return NewGitlabIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
case "Adfs":
return NewAdfsIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl)
return NewGitlabIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "ADFS":
return NewAdfsIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl), nil
case "Baidu":
return NewBaiduIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewBaiduIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "Alipay":
return NewAlipayIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewAlipayIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "Custom":
return NewCustomIdProvider(idpInfo, redirectUrl)
return NewCustomIdProvider(idpInfo, redirectUrl), nil
case "Infoflow":
if idpInfo.SubType == "Internal" {
return NewInfoflowInternalIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, idpInfo.AppId, redirectUrl)
return NewInfoflowInternalIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, idpInfo.AppId, redirectUrl), nil
} else if idpInfo.SubType == "Third-party" {
return NewInfoflowIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, idpInfo.AppId, redirectUrl)
return NewInfoflowIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, idpInfo.AppId, redirectUrl), nil
} else {
return nil
return nil, fmt.Errorf("Infoflow provider subType: %s is not supported", idpInfo.SubType)
}
case "Casdoor":
return NewCasdoorIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl)
return NewCasdoorIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl), nil
case "Okta":
return NewOktaIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl)
return NewOktaIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl), nil
case "Douyin":
return NewDouyinIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewDouyinIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "Bilibili":
return NewBilibiliIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewBilibiliIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "MetaMask":
return NewMetaMaskIdProvider()
return NewMetaMaskIdProvider(), nil
case "Web3Onboard":
return NewWeb3OnboardIdProvider()
return NewWeb3OnboardIdProvider(), nil
default:
if isGothSupport(idpInfo.Type) {
return NewGothIdProvider(idpInfo.Type, idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl)
return NewGothIdProvider(idpInfo.Type, idpInfo.ClientId, idpInfo.ClientSecret, idpInfo.ClientId2, idpInfo.ClientSecret2, redirectUrl, idpInfo.HostUrl)
}
return nil
return nil, fmt.Errorf("OAuth provider type: %s is not supported", idpInfo.Type)
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -21,7 +21,7 @@ import (
"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)
if err != nil {
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) {
if typ == "Telegram" {
return NewTelegramProvider(appId, receiver)
return NewTelegramProvider(clientSecret, receiver)
} else if typ == "Custom HTTP" {
return NewCustomHttpProvider(receiver, method, title)
} else if typ == "DingTalk" {
return NewDingTalkProvider(appId, receiver)
return NewDingTalkProvider(clientId, clientSecret)
} else if typ == "Lark" {
return NewLarkProvider(receiver)
return NewLarkProvider(clientSecret)
} else if typ == "Microsoft Teams" {
return NewMicrosoftTeamsProvider(receiver)
return NewMicrosoftTeamsProvider(clientSecret)
} else if typ == "Bark" {
return NewBarkProvider(receiver)
return NewBarkProvider(clientSecret)
} else if typ == "Pushover" {
return NewPushoverProvider(appId, receiver)
return NewPushoverProvider(clientSecret, receiver)
} else if typ == "Pushbullet" {
return NewPushbulletProvider(appId, receiver)
return NewPushbulletProvider(clientSecret, receiver)
} else if typ == "Slack" {
return NewSlackProvider(appId, receiver)
return NewSlackProvider(clientSecret, receiver)
} else if typ == "Webpush" {
return NewWebpushProvider(clientId, clientSecret, receiver)
} else if typ == "Discord" {
return NewDiscordProvider(appId, receiver)
return NewDiscordProvider(clientSecret, receiver)
} else if typ == "Google Chat" {
return NewGoogleChatProvider(metaData)
} else if typ == "Line" {

View File

@ -30,15 +30,15 @@ type Adapter struct {
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
Type string `xorm:"varchar(100)" json:"type"`
DatabaseType string `xorm:"varchar(100)" json:"databaseType"`
Host string `xorm:"varchar(100)" json:"host"`
Port int `json:"port"`
User string `xorm:"varchar(100)" json:"user"`
Password string `xorm:"varchar(100)" json:"password"`
Database string `xorm:"varchar(100)" json:"database"`
Table string `xorm:"varchar(100)" json:"table"`
TableNamePrefix string `xorm:"varchar(100)" json:"tableNamePrefix"`
Table string `xorm:"varchar(100)" json:"table"`
UseSameDb bool `json:"useSameDb"`
Type string `xorm:"varchar(100)" json:"type"`
DatabaseType string `xorm:"varchar(100)" json:"databaseType"`
Host string `xorm:"varchar(100)" json:"host"`
Port int `json:"port"`
User string `xorm:"varchar(100)" json:"user"`
Password string `xorm:"varchar(100)" json:"password"`
Database string `xorm:"varchar(100)" json:"database"`
*xormadapter.Adapter `xorm:"-" json:"-"`
}
@ -139,63 +139,69 @@ func (adapter *Adapter) GetId() string {
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 {
if adapter.Adapter == nil {
var dataSourceName string
if adapter.Adapter != nil {
return nil
}
if adapter.isBuiltIn() {
dataSourceName = conf.GetConfigString("dataSourceName")
if adapter.DatabaseType == "mysql" {
dataSourceName = dataSourceName + adapter.Database
}
} else {
switch adapter.DatabaseType {
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)
}
var driverName string
var dataSourceName string
if adapter.UseSameDb || adapter.isBuiltIn() {
driverName = conf.GetConfigString("driverName")
dataSourceName = conf.GetConfigString("dataSourceName")
if conf.GetConfigString("driverName") == "mysql" {
dataSourceName = dataSourceName + conf.GetConfigString("dbName")
}
if !isCloudIntranet {
dataSourceName = strings.ReplaceAll(dataSourceName, "dbi.", "db.")
}
var err error
engine, err := xorm.NewEngine(adapter.DatabaseType, dataSourceName)
if adapter.isBuiltIn() && adapter.DatabaseType == "postgres" {
schema := util.GetValueFromDataSourceName("search_path", dataSourceName)
if schema != "" {
engine.SetSchema(schema)
}
}
adapter.Adapter, err = xormadapter.NewAdapterByEngineWithTableName(engine, adapter.getTable(), adapter.TableNamePrefix)
if err != nil {
return err
} else {
driverName = adapter.DatabaseType
switch driverName {
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)
}
}
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
}

View File

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

View File

@ -351,8 +351,8 @@ func CheckUserPermission(requestUserId, userId string, strict bool, lang string)
}
func CheckLoginPermission(userId string, application *Application) (bool, error) {
var err error
if userId == "built-in/admin" {
owner, _ := util.GetOwnerAndNameFromId(userId)
if owner == "built-in" {
return true, nil
}
@ -361,6 +361,8 @@ func CheckLoginPermission(userId string, application *Application) (bool, error)
return false, err
}
allowPermissionCount := 0
denyPermissionCount := 0
allowCount := 0
denyCount := 0
for _, permission := range permissions {
@ -368,11 +370,19 @@ func CheckLoginPermission(userId string, application *Application) (bool, error)
continue
}
if permission.isUserHit(userId) {
allowCount += 1
if !permission.isUserHit(userId) && !permission.isRoleHit(userId) {
if permission.Effect == "Allow" {
allowPermissionCount += 1
} else {
denyPermissionCount += 1
}
continue
}
enforcer := getPermissionEnforcer(permission)
enforcer, err := getPermissionEnforcer(permission)
if err != nil {
return false, err
}
var isAllowed bool
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 {
return false, nil
} else if allowCount > 0 {
return true, nil
}
// For no-allow and no-deny condition
// If only allow permissions exist, we suppose it's Deny-by-default, aka no-allow means deny
// Otherwise, it's Allow-by-default, aka no-deny means allow
if allowPermissionCount > 0 && denyPermissionCount == 0 {
return false, nil
}
return true, nil
}

View File

@ -36,7 +36,7 @@ func getDialer(provider *Provider) *gomail.Dialer {
}
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
if fromAddress == "" {

View File

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

View File

@ -226,7 +226,7 @@ func GetGroupUserCount(groupId string, field, value string) (int64, error) {
} else {
return ormer.Engine.Table("user").
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()
}
}
@ -247,7 +247,7 @@ func GetPaginationGroupUsers(groupId string, offset, limit int, field, value, so
}
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 == "" {

View File

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

121
object/init_data_dump.go Normal file
View File

@ -0,0 +1,121 @@
// 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 object
import "github.com/casdoor/casdoor/util"
func DumpToFile(filePath string) error {
return writeInitDataToFile(filePath)
}
func writeInitDataToFile(filePath string) error {
organizations, err := GetOrganizations("admin")
if err != nil {
return err
}
applications, err := GetApplications("admin")
if err != nil {
return err
}
users, err := GetGlobalUsers()
if err != nil {
return err
}
certs, err := GetCerts("")
if err != nil {
return err
}
providers, err := GetGlobalProviders()
if err != nil {
return err
}
ldaps, err := GetLdaps("")
if err != nil {
return err
}
models, err := GetModels("")
if err != nil {
return err
}
permissions, err := GetPermissions("")
if err != nil {
return err
}
payments, err := GetPayments("")
if err != nil {
return err
}
products, err := GetProducts("")
if err != nil {
return err
}
resources, err := GetResources("", "")
if err != nil {
return err
}
roles, err := GetRoles("")
if err != nil {
return err
}
syncers, err := GetSyncers("")
if err != nil {
return err
}
tokens, err := GetTokens("", "")
if err != nil {
return err
}
webhooks, err := GetWebhooks("", "")
if err != nil {
return err
}
data := &InitData{
Organizations: organizations,
Applications: applications,
Users: users,
Certs: certs,
Providers: providers,
Ldaps: ldaps,
Models: models,
Permissions: permissions,
Payments: payments,
Products: products,
Resources: resources,
Roles: roles,
Syncers: syncers,
Tokens: tokens,
Webhooks: webhooks,
}
text := util.StructToJsonFormatted(data)
util.WriteStringToPath(text, filePath)
return nil
}

View File

@ -1,4 +1,4 @@
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -12,26 +12,18 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !skipCi
// +build !skipCi
package object
func (syncer *Syncer) getUsers() []*User {
users, err := GetUsers(syncer.Organization)
import "testing"
func TestDumpToFile(t *testing.T) {
InitConfig()
err := DumpToFile("./init_data_dump.json")
if err != nil {
panic(err)
}
return users
}
func (syncer *Syncer) getUserMap() ([]*User, map[string]*User, map[string]*User) {
users := syncer.getUsers()
m1 := map[string]*User{}
m2 := map[string]*User{}
for _, user := range users {
m1[user.Id] = user
m2[user.Name] = user
}
return users, m1, m2
}

View File

@ -42,7 +42,7 @@ func (mfa *TotpMfa) Initiate(ctx *context.Context, userId string) (*MfaProps, er
//if issuer == "" {
// issuer = "casdoor"
//}
issuer := "casdoor"
issuer := "Casdoor"
key, err := totp.Generate(totp.GenerateOpts{
Issuer: issuer,

View File

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

View File

@ -59,7 +59,7 @@ func isIpAddress(host string) bool {
return ip != nil
}
func getOriginFromHost(host string) (string, string) {
func getOriginFromHostInternal(host string) (string, string) {
origin := conf.GetConfigString("origin")
if origin != "" {
return origin, origin
@ -82,6 +82,17 @@ func getOriginFromHost(host string) (string, string) {
}
}
func getOriginFromHost(host string) (string, string) {
originF, originB := getOriginFromHostInternal(host)
originFrontend := conf.GetConfigString("originFrontend")
if originFrontend != "" {
originF = originFrontend
}
return originF, originB
}
func GetOidcDiscovery(host string) OidcDiscovery {
originFrontend, originBackend := getOriginFromHost(host)
@ -127,9 +138,16 @@ func GetJsonWebKeySet() (jose.JSONWebKeySet, error) {
continue
}
if cert.Certificate == "" {
return jwks, fmt.Errorf("the certificate field should not be empty for the cert: %v", cert)
}
certPemBlock := []byte(cert.Certificate)
certDerBlock, _ := pem.Decode(certPemBlock)
x509Cert, _ := x509.ParseCertificate(certDerBlock.Bytes)
x509Cert, err := x509.ParseCertificate(certDerBlock.Bytes)
if err != nil {
return jwks, err
}
var jwk jose.JSONWebKey
jwk.Key = x509Cert.PublicKey

View File

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

View File

@ -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")
tbMapper := core.NewPrefixMapper(core.SnakeMapper{}, tableNamePrefix)
@ -121,19 +125,22 @@ func finalizer(a *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.driverName = driverName
a.dataSourceName = dataSourceName
a.dbName = dbName
// 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.
runtime.SetFinalizer(a, finalizer)
return a
return a, nil
}
func refineDataSourceNameForPostgres(dataSourceName string) string {
@ -192,7 +199,7 @@ func (a *Ormer) CreateDatabase() error {
return err
}
func (a *Ormer) open() {
func (a *Ormer) open() error {
dataSourceName := a.dataSourceName + a.dbName
if a.driverName != "mysql" {
dataSourceName = a.dataSourceName
@ -200,8 +207,9 @@ func (a *Ormer) open() {
engine, err := xorm.NewEngine(a.driverName, dataSourceName)
if err != nil {
panic(err)
return err
}
if a.driverName == "postgres" {
schema := util.GetValueFromDataSourceName("search_path", dataSourceName)
if schema != "" {
@ -210,6 +218,7 @@ func (a *Ormer) open() {
}
a.Engine = engine
return nil
}
func (a *Ormer) close() {
@ -316,6 +325,11 @@ func (a *Ormer) createTable() {
panic(err)
}
err = a.Engine.Sync2(new(RadiusAccounting))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(PermissionRule))
if err != nil {
panic(err)

View File

@ -54,7 +54,7 @@ type Payment struct {
// Order Info
OutOrderId string `xorm:"varchar(100)" json:"outOrderId"`
PayUrl string `xorm:"varchar(2000)" json:"payUrl"`
SuccessUrl string `xorm:"varchar(2000)" json:"successUrl""` // `successUrl` is redirected from `payUrl` after pay success
SuccessUrl string `xorm:"varchar(2000)" json:"successUrl"` // `successUrl` is redirected from `payUrl` after pay success
State pp.PaymentState `xorm:"varchar(100)" json:"state"`
Message string `xorm:"varchar(2000)" json:"message"`
}

View File

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

View File

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

View File

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

View File

@ -39,7 +39,7 @@ type Provider struct {
ClientId string `xorm:"varchar(200)" json:"clientId"`
ClientSecret string `xorm:"varchar(2000)" json:"clientSecret"`
ClientId2 string `xorm:"varchar(100)" json:"clientId2"`
ClientSecret2 string `xorm:"varchar(100)" json:"clientSecret2"`
ClientSecret2 string `xorm:"varchar(500)" json:"clientSecret2"`
Cert string `xorm:"varchar(100)" json:"cert"`
CustomAuthUrl string `xorm:"varchar(200)" json:"customAuthUrl"`
CustomTokenUrl string `xorm:"varchar(200)" json:"customTokenUrl"`
@ -398,16 +398,18 @@ func providerChangeTrigger(oldName string, newName string) error {
func FromProviderToIdpInfo(ctx *context.Context, provider *Provider) *idp.ProviderInfo {
providerInfo := &idp.ProviderInfo{
Type: provider.Type,
SubType: provider.SubType,
ClientId: provider.ClientId,
ClientSecret: provider.ClientSecret,
AppId: provider.AppId,
HostUrl: provider.Host,
TokenURL: provider.CustomTokenUrl,
AuthURL: provider.CustomAuthUrl,
UserInfoURL: provider.CustomUserInfoUrl,
UserMapping: provider.UserMapping,
Type: provider.Type,
SubType: provider.SubType,
ClientId: provider.ClientId,
ClientSecret: provider.ClientSecret,
ClientId2: provider.ClientId2,
ClientSecret2: provider.ClientSecret2,
AppId: provider.AppId,
HostUrl: provider.Host,
TokenURL: provider.CustomTokenUrl,
AuthURL: provider.CustomAuthUrl,
UserInfoURL: provider.CustomUserInfoUrl,
UserMapping: provider.UserMapping,
}
if provider.Type == "WeChat" {
@ -415,6 +417,8 @@ func FromProviderToIdpInfo(ctx *context.Context, provider *Provider) *idp.Provid
providerInfo.ClientId = provider.ClientId2
providerInfo.ClientSecret = provider.ClientSecret2
}
} else if provider.Type == "AzureAD" || provider.Type == "ADFS" || provider.Type == "Okta" {
providerInfo.HostUrl = provider.Domain
}
return providerInfo

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -72,7 +72,7 @@ func GetTruncatedPath(provider *Provider, fullFilePath string, limit int) string
}
func GetUploadFileUrl(provider *Provider, fullFilePath string, hasTimestamp bool) (string, string) {
escapedPath := util.UrlJoin(provider.PathPrefix, escapePath(fullFilePath))
escapedPath := util.UrlJoin(provider.PathPrefix, fullFilePath)
objectKey := util.UrlJoin(util.GetUrlPath(provider.Domain), escapedPath)
host := ""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -195,6 +195,9 @@ func GenerateCasToken(userId string, service string) (string, error) {
user, _ = GetMaskedUser(user, false)
user.WebauthnCredentials = nil
user.Properties = nil
authenticationSuccess := CasAuthenticationSuccess{
User: user.Name,
Attributes: &CasAttributes{
@ -216,10 +219,15 @@ func GenerateCasToken(userId string, service string) (string, error) {
}
for k, v := range tmp {
if v != "" {
value := fmt.Sprintf("%v", v)
if value == "<nil>" || value == "[]" || value == "map[]" {
value = ""
}
if value != "" {
authenticationSuccess.Attributes.UserAttributes.Attributes = append(authenticationSuccess.Attributes.UserAttributes.Attributes, &CasNamedAttribute{
Name: k,
Value: fmt.Sprintf("%v", v),
Value: value,
})
}
}
@ -281,6 +289,10 @@ func GetValidationBySaml(samlRequest string, host string) (string, string, error
return "", "", err
}
if cert.Certificate == "" {
return "", "", fmt.Errorf("the certificate field should not be empty for the cert: %v", cert)
}
block, _ := pem.Decode([]byte(cert.Certificate))
certificate := base64.StdEncoding.EncodeToString(block.Bytes)
randomKeyStore := &X509Key{

View File

@ -26,7 +26,7 @@ type Claims struct {
*User
TokenType string `json:"tokenType,omitempty"`
Nonce string `json:"nonce,omitempty"`
Tag string `json:"tag,omitempty"`
Tag string `json:"tag"`
Scope string `json:"scope,omitempty"`
jwt.RegisteredClaims
}
@ -37,56 +37,90 @@ type UserShort struct {
}
type UserWithoutThirdIdp struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
Id string `xorm:"varchar(100) index" json:"id"`
Type string `xorm:"varchar(100)" json:"type"`
Password string `xorm:"varchar(100)" json:"password"`
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
FirstName string `xorm:"varchar(100)" json:"firstName"`
LastName string `xorm:"varchar(100)" json:"lastName"`
Avatar string `xorm:"varchar(500)" json:"avatar"`
PermanentAvatar string `xorm:"varchar(500)" json:"permanentAvatar"`
Email string `xorm:"varchar(100) index" json:"email"`
EmailVerified bool `json:"emailVerified"`
Phone string `xorm:"varchar(100) index" json:"phone"`
Location string `xorm:"varchar(100)" json:"location"`
Address []string `json:"address"`
Affiliation string `xorm:"varchar(100)" json:"affiliation"`
Title string `xorm:"varchar(100)" json:"title"`
IdCardType string `xorm:"varchar(100)" json:"idCardType"`
IdCard string `xorm:"varchar(100) index" json:"idCard"`
Homepage string `xorm:"varchar(100)" json:"homepage"`
Bio string `xorm:"varchar(100)" json:"bio"`
Tag string `xorm:"varchar(100)" json:"tag"`
Region string `xorm:"varchar(100)" json:"region"`
Language string `xorm:"varchar(100)" json:"language"`
Gender string `xorm:"varchar(100)" json:"gender"`
Birthday string `xorm:"varchar(100)" json:"birthday"`
Education string `xorm:"varchar(100)" json:"education"`
Score int `json:"score"`
Karma int `json:"karma"`
Ranking int `json:"ranking"`
IsDefaultAvatar bool `json:"isDefaultAvatar"`
IsOnline bool `json:"isOnline"`
IsAdmin bool `json:"isAdmin"`
IsForbidden bool `json:"isForbidden"`
IsDeleted bool `json:"isDeleted"`
SignupApplication string `xorm:"varchar(100)" json:"signupApplication"`
Hash string `xorm:"varchar(100)" json:"hash"`
PreHash string `xorm:"varchar(100)" json:"preHash"`
CreatedIp string `xorm:"varchar(100)" json:"createdIp"`
LastSigninTime string `xorm:"varchar(100)" json:"lastSigninTime"`
LastSigninIp string `xorm:"varchar(100)" json:"lastSigninIp"`
Ldap string `xorm:"ldap varchar(100)" json:"ldap"`
Properties map[string]string `json:"properties"`
Roles []*Role `xorm:"-" json:"roles"`
Permissions []*Permission `xorm:"-" json:"permissions"`
LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"`
SigninWrongTimes int `json:"signinWrongTimes"`
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100) index" json:"createdTime"`
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
Id string `xorm:"varchar(100) index" json:"id"`
Type string `xorm:"varchar(100)" json:"type"`
Password string `xorm:"varchar(100)" json:"password"`
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
FirstName string `xorm:"varchar(100)" json:"firstName"`
LastName string `xorm:"varchar(100)" json:"lastName"`
Avatar string `xorm:"varchar(500)" json:"avatar"`
AvatarType string `xorm:"varchar(100)" json:"avatarType"`
PermanentAvatar string `xorm:"varchar(500)" json:"permanentAvatar"`
Email string `xorm:"varchar(100) index" json:"email"`
EmailVerified bool `json:"emailVerified"`
Phone string `xorm:"varchar(20) index" json:"phone"`
CountryCode string `xorm:"varchar(6)" json:"countryCode"`
Region string `xorm:"varchar(100)" json:"region"`
Location string `xorm:"varchar(100)" json:"location"`
Address []string `json:"address"`
Affiliation string `xorm:"varchar(100)" json:"affiliation"`
Title string `xorm:"varchar(100)" json:"title"`
IdCardType string `xorm:"varchar(100)" json:"idCardType"`
IdCard string `xorm:"varchar(100) index" json:"idCard"`
Homepage string `xorm:"varchar(100)" json:"homepage"`
Bio string `xorm:"varchar(100)" json:"bio"`
Tag string `xorm:"varchar(100)" json:"tag"`
Language string `xorm:"varchar(100)" json:"language"`
Gender string `xorm:"varchar(100)" json:"gender"`
Birthday string `xorm:"varchar(100)" json:"birthday"`
Education string `xorm:"varchar(100)" json:"education"`
Score int `json:"score"`
Karma int `json:"karma"`
Ranking int `json:"ranking"`
IsDefaultAvatar bool `json:"isDefaultAvatar"`
IsOnline bool `json:"isOnline"`
IsAdmin bool `json:"isAdmin"`
IsForbidden bool `json:"isForbidden"`
IsDeleted bool `json:"isDeleted"`
SignupApplication string `xorm:"varchar(100)" json:"signupApplication"`
Hash string `xorm:"varchar(100)" json:"hash"`
PreHash string `xorm:"varchar(100)" json:"preHash"`
AccessKey string `xorm:"varchar(100)" json:"accessKey"`
AccessSecret string `xorm:"varchar(100)" json:"accessSecret"`
GitHub string `xorm:"github varchar(100)" json:"github"`
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 {
@ -101,7 +135,7 @@ type ClaimsWithoutThirdIdp struct {
*UserWithoutThirdIdp
TokenType string `json:"tokenType,omitempty"`
Nonce string `json:"nonce,omitempty"`
Tag string `json:"tag,omitempty"`
Tag string `json:"tag"`
Scope string `json:"scope,omitempty"`
jwt.RegisteredClaims
}
@ -125,14 +159,18 @@ func getUserWithoutThirdIdp(user *User) *UserWithoutThirdIdp {
Type: user.Type,
Password: user.Password,
PasswordSalt: user.PasswordSalt,
PasswordType: user.PasswordType,
DisplayName: user.DisplayName,
FirstName: user.FirstName,
LastName: user.LastName,
Avatar: user.Avatar,
AvatarType: user.AvatarType,
PermanentAvatar: user.PermanentAvatar,
Email: user.Email,
EmailVerified: user.EmailVerified,
Phone: user.Phone,
CountryCode: user.CountryCode,
Region: user.Region,
Location: user.Location,
Address: user.Address,
Affiliation: user.Affiliation,
@ -142,7 +180,6 @@ func getUserWithoutThirdIdp(user *User) *UserWithoutThirdIdp {
Homepage: user.Homepage,
Bio: user.Bio,
Tag: user.Tag,
Region: user.Region,
Language: user.Language,
Gender: user.Gender,
Birthday: user.Birthday,
@ -158,16 +195,38 @@ func getUserWithoutThirdIdp(user *User) *UserWithoutThirdIdp {
SignupApplication: user.SignupApplication,
Hash: user.Hash,
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,
LastSigninTime: user.LastSigninTime,
LastSigninIp: user.LastSigninIp,
PreferredMfaType: user.PreferredMfaType,
RecoveryCodes: user.RecoveryCodes,
TotpSecret: user.TotpSecret,
MfaPhoneEnabled: user.MfaPhoneEnabled,
MfaEmailEnabled: user.MfaEmailEnabled,
Ldap: user.Ldap,
Properties: user.Properties,
Roles: user.Roles,
Permissions: user.Permissions,
Groups: user.Groups,
LastSigninWrongTime: user.LastSigninWrongTime,
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"])
}
if cert.Certificate == "" {
return nil, fmt.Errorf("the certificate field should not be empty for the cert: %v", cert)
}
// RSA certificate
certificate, err := jwt.ParseRSAPublicKeyFromPEM([]byte(cert.Certificate))
if err != nil {

View File

@ -16,6 +16,7 @@ package object
import (
"fmt"
"strconv"
"strings"
"github.com/casdoor/casdoor/conf"
@ -49,6 +50,7 @@ type User struct {
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
Id string `xorm:"varchar(100) index" json:"id"`
ExternalId string `xorm:"varchar(100) index" json:"externalId"`
Type string `xorm:"varchar(100)" json:"type"`
Password string `xorm:"varchar(100)" json:"password"`
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
@ -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) {
if owner == "" || phone == "" {
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) {
if owner == "" || userId == "" {
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) {
if accessKey == "" {
return nil, nil
@ -483,7 +539,7 @@ func GetMaskedUsers(users []*User, errs ...error) ([]*User, error) {
return users, nil
}
func GetLastUser(owner string) (*User, error) {
func getLastUser(owner string) (*User, error) {
user := User{Owner: owner}
existed, err := ormer.Engine.Desc("created_time", "id").Get(&user)
if err != nil {
@ -505,7 +561,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
return false, err
}
if oldUser == nil {
return false, nil
return false, fmt.Errorf("the user: %s is not found", id)
}
if name != user.Name {
@ -528,7 +584,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
if len(columns) == 0 {
columns = []string{
"owner", "display_name", "avatar",
"owner", "display_name", "avatar", "first_name", "last_name",
"location", "address", "country_code", "region", "language", "affiliation", "title", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application",
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts",
"signin_wrong_times", "last_signin_wrong_time", "groups", "access_key", "access_secret",
@ -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, "updated_time")
user.UpdatedTime = util.GetCurrentTime()
if util.ContainsString(columns, "groups") {
_, err := userEnforcer.UpdateGroupsForUser(user.GetId(), user.Groups)
if err != nil {
@ -583,7 +642,7 @@ func UpdateUserForAllFields(id string, user *User) (bool, error) {
}
if oldUser == nil {
return false, nil
return false, fmt.Errorf("the user: %s is not found", id)
}
if name != user.Name {
@ -614,18 +673,34 @@ func UpdateUserForAllFields(id string, user *User) (bool, error) {
}
func AddUser(user *User) (bool, error) {
var err error
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 == "" {
return false, nil
return false, fmt.Errorf("the user's owner and name should not be empty")
}
organization, _ := GetOrganizationByUser(user)
organization, err := GetOrganizationByUser(user)
if err != nil {
return false, err
}
if organization == nil {
return false, nil
return false, fmt.Errorf("the organization: %s is not found", user.Owner)
}
if organization.DefaultPassword != "" && user.Password == "123" {
user.Password = organization.DefaultPassword
}
if user.PasswordType == "" || user.PasswordType == "plain" {
@ -666,9 +741,8 @@ func AddUser(user *User) (bool, error) {
}
func AddUsers(users []*User) (bool, error) {
var err error
if len(users) == 0 {
return false, nil
return false, fmt.Errorf("no users are provided")
}
// organization := GetOrganizationByUser(users[0])
@ -676,7 +750,7 @@ func AddUsers(users []*User) (bool, error) {
// this function is only used for syncer or batch upload, so no need to encrypt the password
// user.UpdateUserPassword(organization)
err = user.UpdateUserHash()
err := user.UpdateUserHash()
if err != nil {
return false, err
}
@ -700,23 +774,22 @@ func AddUsers(users []*User) (bool, error) {
}
func AddUsersInBatch(users []*User) (bool, error) {
batchSize := conf.GetConfigBatchSize()
if len(users) == 0 {
return false, nil
return false, fmt.Errorf("no users are provided")
}
batchSize := conf.GetConfigBatchSize()
affected := false
for i := 0; i < (len(users)-1)/batchSize+1; i++ {
start := i * batchSize
end := (i + 1) * batchSize
for i := 0; i < len(users); i += batchSize {
start := i
end := i + batchSize
if end > len(users) {
end = len(users)
}
tmp := users[start:end]
// TODO: save to log instead of standard output
// fmt.Printf("Add users: [%d - %d].\n", start, end)
fmt.Printf("The syncer adds users: [%d - %d]\n", start, end)
if ok, err := AddUsers(tmp); err != nil {
return false, err
} else if ok {
@ -786,7 +859,7 @@ func ExtendUserWithRolesAndPermissions(user *User) (err error) {
return
}
user.Permissions, user.Roles, err = GetPermissionsAndRolesByUser(user.GetId())
user.Permissions, user.Roles, err = getPermissionsAndRolesByUser(user.GetId())
if err != nil {
return err
}
@ -874,9 +947,9 @@ func (user *User) GetPreferredMfaProps(masked bool) *MfaProps {
return user.GetMfaProps(user.PreferredMfaType, masked)
}
func AddUserkeys(user *User, isAdmin bool) (bool, error) {
func AddUserKeys(user *User, isAdmin bool) (bool, error) {
if user == nil {
return false, nil
return false, fmt.Errorf("the user is not found")
}
user.AccessKey = util.GenerateId()
@ -900,3 +973,22 @@ func (user *User) IsGlobalAdmin() bool {
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)
if err != nil {
fmt.Printf("downloadImage() error for url [%s]: %s\n", url, err.Error())
if strings.Contains(err.Error(), "EOF") || strings.Contains(err.Error(), "no such host") {
if strings.Contains(err.Error(), "EOF") || strings.Contains(err.Error(), "no such host") || strings.Contains(err.Error(), "did not properly respond after a period of time") || strings.Contains(err.Error(), "unrecognized name") {
return nil, "", nil
} else {
return nil, "", err

View File

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

View File

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

View File

@ -320,6 +320,11 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
itemsChanged = append(itemsChanged, item)
}
if oldUser.Score != newUser.Score {
item := GetAccountItemByName("Score", organization)
itemsChanged = append(itemsChanged, item)
}
for i := range itemsChanged {
if pass, err := CheckAccountItemModifyRule(itemsChanged[i], isAdmin, lang); !pass {
return pass, err

View File

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

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

View File

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

View File

@ -16,6 +16,9 @@ package routers
import (
"fmt"
"net"
"net/http"
"net/url"
"strings"
"github.com/beego/beego/context"
@ -33,6 +36,8 @@ type Response struct {
}
func responseError(ctx *context.Context, error string, data ...interface{}) {
ctx.ResponseWriter.WriteHeader(http.StatusForbidden)
resp := Response{Status: "error", Msg: error}
switch len(data) {
case 2:
@ -61,7 +66,7 @@ func denyRequest(ctx *context.Context) {
responseError(ctx, T(ctx, "auth:Unauthorized operation"))
}
func getUsernameByClientIdSecret(ctx *context.Context) string {
func getUsernameByClientIdSecret(ctx *context.Context) (string, error) {
clientId, clientSecret, ok := ctx.Request.BasicAuth()
if !ok {
clientId = ctx.Input.Query("clientId")
@ -69,19 +74,22 @@ func getUsernameByClientIdSecret(ctx *context.Context) string {
}
if clientId == "" || clientSecret == "" {
return ""
return "", nil
}
application, err := object.GetApplicationByClientId(clientId)
if err != nil {
panic(err)
return "", err
}
if application == nil {
return "", fmt.Errorf("Application not found for client ID: %s", clientId)
}
if application == nil || application.ClientSecret != clientSecret {
return ""
if application.ClientSecret != clientSecret {
return "", fmt.Errorf("Incorrect client secret for application: %s", application.Name)
}
return fmt.Sprintf("app/%s", application.Name)
return fmt.Sprintf("app/%s", application.Name), nil
}
func getUsernameByKeys(ctx *context.Context) string {
@ -151,3 +159,39 @@ func parseBearerToken(ctx *context.Context) string {
return tokens[1]
}
func getHostname(s string) string {
if s == "" {
return ""
}
l, err := url.Parse(s)
if err != nil {
panic(err)
}
res := l.Hostname()
return res
}
func removePort(s string) string {
ipStr, _, err := net.SplitHostPort(s)
if err != nil {
ipStr = s
}
return ipStr
}
func isHostIntranet(s string) bool {
ipStr, _, err := net.SplitHostPort(s)
if err != nil {
ipStr = s
}
ip := net.ParseIP(ipStr)
if ip == nil {
return false
}
return ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast()
}

View File

@ -16,6 +16,7 @@ package routers
import (
"net/http"
"strings"
"github.com/beego/beego/context"
"github.com/casdoor/casdoor/conf"
@ -29,42 +30,61 @@ const (
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) {
origin := ctx.Input.Header(headerOrigin)
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 originHostname == "appleid.apple.com" {
setCorsHeaders(ctx, origin)
return
}
if ctx.Request.Method == "POST" && ctx.Request.RequestURI == "/api/login/oauth/access_token" {
ctx.Output.Header(headerAllowOrigin, origin)
ctx.Output.Header(headerAllowMethods, "POST, GET, OPTIONS, DELETE")
ctx.Output.Header(headerAllowHeaders, "Content-Type, Authorization")
setCorsHeaders(ctx, origin)
return
}
if ctx.Request.RequestURI == "/api/userinfo" {
ctx.Output.Header(headerAllowOrigin, origin)
ctx.Output.Header(headerAllowMethods, "POST, GET, OPTIONS, DELETE")
ctx.Output.Header(headerAllowHeaders, "Content-Type, Authorization")
setCorsHeaders(ctx, origin)
return
}
if origin != "" && originConf != "" && origin != originConf {
ok, err := object.IsOriginAllowed(origin)
if err != nil {
panic(err)
}
if ok {
ctx.Output.Header(headerAllowOrigin, origin)
ctx.Output.Header(headerAllowMethods, "POST, GET, OPTIONS, DELETE")
ctx.Output.Header(headerAllowHeaders, "Content-Type, Authorization")
if origin != "" {
if origin == originConf {
setCorsHeaders(ctx, origin)
} else if originHostname == host {
setCorsHeaders(ctx, origin)
} else if isHostIntranet(host) {
setCorsHeaders(ctx, origin)
} else {
ctx.ResponseWriter.WriteHeader(http.StatusForbidden)
return
}
ok, err := object.IsOriginAllowed(origin)
if err != nil {
panic(err)
}
if ctx.Input.Method() == "OPTIONS" {
ctx.ResponseWriter.WriteHeader(http.StatusOK)
return
if ok {
setCorsHeaders(ctx, origin)
} else {
ctx.ResponseWriter.WriteHeader(http.StatusForbidden)
return
}
}
}

View File

@ -63,6 +63,7 @@ func initAPI() {
beego.Router("/api/webhook", &controllers.ApiController{}, "POST:HandleOfficialAccountEvent")
beego.Router("/api/get-webhook-event", &controllers.ApiController{}, "GET:GetWebhookEventType")
beego.Router("/api/get-captcha-status", &controllers.ApiController{}, "GET:GetCaptchaStatus")
beego.Router("/api/callback", &controllers.ApiController{}, "POST:Callback")
beego.Router("/api/get-organizations", &controllers.ApiController{}, "GET:GetOrganizations")
beego.Router("/api/get-organization", &controllers.ApiController{}, "GET:GetOrganization")
@ -78,7 +79,7 @@ func initAPI() {
beego.Router("/api/get-user-count", &controllers.ApiController{}, "GET:GetUserCount")
beego.Router("/api/get-user", &controllers.ApiController{}, "GET:GetUser")
beego.Router("/api/update-user", &controllers.ApiController{}, "POST:UpdateUser")
beego.Router("/api/add-user-keys", &controllers.ApiController{}, "POST:AddUserkeys")
beego.Router("/api/add-user-keys", &controllers.ApiController{}, "POST:AddUserKeys")
beego.Router("/api/add-user", &controllers.ApiController{}, "POST:AddUser")
beego.Router("/api/delete-user", &controllers.ApiController{}, "POST:DeleteUser")
beego.Router("/api/upload-users", &controllers.ApiController{}, "POST:UploadUsers")
@ -276,4 +277,6 @@ func initAPI() {
beego.Router("/cas/:organization/:application/p3/serviceValidate", &controllers.RootController{}, "GET:CasP3ServiceValidate")
beego.Router("/cas/:organization/:application/p3/proxyValidate", &controllers.RootController{}, "GET:CasP3ProxyValidate")
beego.Router("/cas/:organization/:application/samlValidate", &controllers.RootController{}, "POST:SamlValidate")
beego.Router("/scim/*", &controllers.RootController{}, "*:HandleScim")
}

View File

@ -26,6 +26,7 @@ import (
"github.com/beego/beego/context"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
@ -33,8 +34,63 @@ var (
oldStaticBaseUrl = "https://cdn.casbin.org"
newStaticBaseUrl = conf.GetConfigString("staticBaseUrl")
enableGzip = conf.GetConfigBool("enableGzip")
frontendBaseDir = conf.GetConfigString("frontendBaseDir")
)
func getWebBuildFolder() string {
path := "web/build"
if util.FileExist(filepath.Join(path, "index.html")) || frontendBaseDir == "" {
return path
}
path = filepath.Join(frontendBaseDir, "web/build")
return path
}
func 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)
}
sep := "?"
if strings.Contains(redirectUri, "?") {
sep = "&"
}
res := fmt.Sprintf("%s%scode=%s&state=%s", redirectUri, sep, code.Code, state)
return res, nil
}
func StaticFilter(ctx *context.Context) {
urlPath := ctx.Request.URL.Path
@ -48,8 +104,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")) {
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 == "/" {
path += "/index.html"
} else {
@ -57,7 +130,7 @@ func StaticFilter(ctx *context.Context) {
}
if !util.FileExist(path) {
path = "web/build/index.html"
path = webBuildFolder + "/index.html"
}
if !util.FileExist(path) {
dir, err := os.Getwd()
@ -65,6 +138,7 @@ func StaticFilter(ctx *context.Context) {
panic(err)
}
dir = strings.ReplaceAll(dir, "\\", "/")
ctx.ResponseWriter.WriteHeader(http.StatusNotFound)
errorText := fmt.Sprintf("The Casdoor frontend HTML file: \"index.html\" was not found, it should be placed at: \"%s/web/build/index.html\". For more information, see: https://casdoor.org/docs/basic/server-installation/#frontend-1", dir)
http.ServeContent(ctx.ResponseWriter, ctx.Request, "Casdoor frontend has encountered error...", time.Now(), strings.NewReader(errorText))
return

154
scim/server.go Normal file
View File

@ -0,0 +1,154 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scim
import (
"github.com/elimity-com/scim"
"github.com/elimity-com/scim/optional"
"github.com/elimity-com/scim/schema"
)
/*
Example JSON user resource
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
],
"addresses": [
{
"country": "US",
"locality": "San Fransisco",
"region": "US West"
}
],
"displayName": "Hello, Scim",
"name": {
"familyName": "Bob",
"givenName": "Alice"
},
"phoneNumbers": [
{
"value": "46407568879"
}
],
"photos": [
{
"value": "https://cdn.casbin.org/img/casbin.svg"
}
],
"emails": [
{
"value": "cbvdho@example.com"
}
],
"profileUrl": "https://door.casdoor.com/users/build-in/scim_test_user2",
"userName": "scim_test_user2",
"userType": "normal-user",
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
"organization": "built-in"
}
}
*/
const (
UserExtensionKey = "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
)
var (
UserStringField = []schema.SimpleParams{
newStringParams("externalId", false, true),
newStringParams("userName", true, true),
newStringParams("password", false, false),
newStringParams("displayName", false, false),
newStringParams("profileUrl", false, false),
newStringParams("userType", false, false),
}
UserComplexField = []schema.ComplexParams{
newComplexParams("name", false, false, []schema.SimpleParams{
newStringParams("givenName", false, false),
newStringParams("familyName", false, false),
}),
newComplexParams("emails", false, true, []schema.SimpleParams{
newStringParams("value", true, false),
}),
newComplexParams("phoneNumbers", false, true, []schema.SimpleParams{
newStringParams("value", true, false),
}),
newComplexParams("photos", false, true, []schema.SimpleParams{
newStringParams("value", true, false),
}),
newComplexParams("addresses", false, true, []schema.SimpleParams{
newStringParams("locality", false, false),
newStringParams("region", false, false),
newStringParams("country", false, false),
}),
}
Server = GetScimServer()
)
func GetScimServer() scim.Server {
config := scim.ServiceProviderConfig{
// DocumentationURI: optional.NewString("www.example.com/scim"),
SupportPatch: true,
}
codeAttrs := make([]schema.CoreAttribute, 0, len(UserStringField)+len(UserComplexField))
for _, field := range UserStringField {
codeAttrs = append(codeAttrs, schema.SimpleCoreAttribute(field))
}
for _, field := range UserComplexField {
codeAttrs = append(codeAttrs, schema.ComplexCoreAttribute(field))
}
userSchema := schema.Schema{
ID: schema.UserSchema,
Name: optional.NewString("User"),
Description: optional.NewString("User Account"),
Attributes: codeAttrs,
}
extension := schema.Schema{
ID: UserExtensionKey,
Name: optional.NewString("EnterpriseUser"),
Description: optional.NewString("Enterprise User"),
Attributes: []schema.CoreAttribute{
schema.SimpleCoreAttribute(schema.SimpleStringParams(schema.StringParams{
Name: "organization",
Required: true,
})),
},
}
resourceTypes := []scim.ResourceType{
{
ID: optional.NewString("User"),
Name: "User",
Endpoint: "/Users",
Description: optional.NewString("User Account in Casdoor"),
Schema: userSchema,
SchemaExtensions: []scim.SchemaExtension{
{Schema: extension},
},
Handler: UserResourceHandler{},
},
}
server := scim.Server{
Config: config,
ResourceTypes: resourceTypes,
}
return server
}

260
scim/user_handler.go Normal file
View File

@ -0,0 +1,260 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scim
import (
"fmt"
"net/http"
"github.com/casdoor/casdoor/object"
"github.com/elimity-com/scim"
"github.com/elimity-com/scim/errors"
)
type UserResourceHandler struct{}
// https://github.com/elimity-com/scim/blob/master/resource_handler_test.go Example in-memory resource handler
// https://datatracker.ietf.org/doc/html/rfc7644#section-3.4 How to query/update resources
func (h UserResourceHandler) Create(r *http.Request, attrs scim.ResourceAttributes) (scim.Resource, error) {
resource := &scim.Resource{Attributes: attrs}
err := AddScimUser(resource)
return *resource, err
}
func (h UserResourceHandler) Get(r *http.Request, id string) (scim.Resource, error) {
resource, err := GetScimUser(id)
if err != nil {
return scim.Resource{}, err
}
if resource == nil {
return scim.Resource{}, errors.ScimErrorResourceNotFound(id)
}
return *resource, nil
}
func (h UserResourceHandler) Delete(r *http.Request, id string) error {
user, err := object.GetUserByUserIdOnly(id)
if err != nil {
return err
}
if user == nil {
return errors.ScimErrorResourceNotFound(id)
}
_, err = object.DeleteUser(user)
return err
}
func (h UserResourceHandler) GetAll(r *http.Request, params scim.ListRequestParams) (scim.Page, error) {
if params.Count == 0 {
count, err := object.GetGlobalUserCount("", "")
if err != nil {
return scim.Page{}, err
}
return scim.Page{TotalResults: int(count)}, nil
}
resources := make([]scim.Resource, 0)
// startIndex is 1-based index
users, err := object.GetPaginationGlobalUsers(params.StartIndex-1, params.Count, "", "", "", "")
if err != nil {
return scim.Page{}, err
}
for _, user := range users {
resources = append(resources, *user2resource(user))
}
return scim.Page{
TotalResults: len(resources),
Resources: resources,
}, nil
}
func (h UserResourceHandler) Patch(r *http.Request, id string, operations []scim.PatchOperation) (scim.Resource, error) {
user, err := object.GetUserByUserIdOnly(id)
if err != nil {
return scim.Resource{}, err
}
if user == nil {
return scim.Resource{}, errors.ScimErrorResourceNotFound(id)
}
return UpdateScimUserByPatchOperation(id, operations)
}
func (h UserResourceHandler) Replace(r *http.Request, id string, attrs scim.ResourceAttributes) (scim.Resource, error) {
user, err := object.GetUserByUserIdOnly(id)
if err != nil {
return scim.Resource{}, err
}
if user == nil {
return scim.Resource{}, errors.ScimErrorResourceNotFound(id)
}
resource := &scim.Resource{Attributes: attrs}
err = UpdateScimUser(id, resource)
return *resource, err
}
func GetScimUser(id string) (*scim.Resource, error) {
user, err := object.GetUserByUserIdOnly(id)
if err != nil {
return nil, err
}
if user == nil {
return nil, nil
}
r := user2resource(user)
return r, nil
}
func AddScimUser(r *scim.Resource) error {
newUser, err := resource2user(r.Attributes)
if err != nil {
return err
}
// Check whether the user exists.
oldUser, err := object.GetUser(newUser.GetId())
if err != nil {
return err
}
if oldUser != nil {
return errors.ScimErrorUniqueness
}
affect, err := object.AddUser(newUser)
if err != nil {
return err
}
if !affect {
return fmt.Errorf("add new user failed")
}
r.Attributes = user2resource(newUser).Attributes
r.ID = newUser.Id
r.ExternalID = buildExternalId(newUser)
r.Meta = buildMeta(newUser)
return nil
}
func UpdateScimUser(id string, r *scim.Resource) error {
oldUser, err := object.GetUserByUserIdOnly(id)
if err != nil {
return err
}
if oldUser == nil {
return errors.ScimErrorResourceNotFound(id)
}
newUser, err := resource2user(r.Attributes)
if err != nil {
return err
}
_, err = object.UpdateUser(oldUser.GetId(), newUser, nil, true)
if err != nil {
return err
}
r.ID = newUser.Id
r.ExternalID = buildExternalId(newUser)
r.Meta = buildMeta(newUser)
return nil
}
// https://datatracker.ietf.org/doc/html/rfc7644#section-3.5.2 Modifying with PATCH
func UpdateScimUserByPatchOperation(id string, ops []scim.PatchOperation) (r scim.Resource, err error) {
user, err := object.GetUserByUserIdOnly(id)
if err != nil {
return scim.Resource{}, err
}
if user == nil {
return scim.Resource{}, errors.ScimErrorResourceNotFound(id)
}
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("invalid patch op value: %v", r)
}
}()
old := user.GetId()
for _, op := range ops {
value := op.Value
if op.Op == scim.PatchOperationRemove {
value = nil
}
// PatchOperationAdd and PatchOperationReplace is same in Casdoor, just replace the value
switch op.Path.String() {
case "userName":
user.Name = ToString(value, "")
case "password":
user.Password = ToString(value, "")
case "externalId":
user.ExternalId = ToString(value, "")
case "displayName":
user.DisplayName = ToString(value, "")
case "profileUrl":
user.Homepage = ToString(value, "")
case "userType":
user.Type = ToString(value, "")
case "name.givenName":
user.FirstName = ToString(value, "")
case "name.familyName":
user.LastName = ToString(value, "")
case "name":
defaultV := AnyMap{"givenName": "", "familyName": ""}
v := ToAnyMap(value, defaultV) // e.g. {"givenName": "AA", "familyName": "BB"}
user.FirstName = ToString(v["givenName"], user.FirstName)
user.LastName = ToString(v["familyName"], user.LastName)
case "emails":
defaultV := AnyArray{AnyMap{"value": ""}}
vs := ToAnyArray(value, defaultV) // e.g. [{"value": "test@casdoor"}]
if len(vs) > 0 {
v := ToAnyMap(vs[0])
user.Email = ToString(v["value"], user.Email)
}
case "phoneNumbers":
defaultV := AnyArray{AnyMap{"value": ""}}
vs := ToAnyArray(value, defaultV) // e.g. [{"value": "18750004417"}]
if len(vs) > 0 {
v := ToAnyMap(vs[0])
user.Phone = ToString(v["value"], user.Phone)
}
case "photos":
defaultV := AnyArray{AnyMap{"value": ""}}
vs := ToAnyArray(value, defaultV) // e.g. [{"value": "https://cdn.casbin.org/img/casbin.svg"}]
if len(vs) > 0 {
v := ToAnyMap(vs[0])
user.Avatar = ToString(v["value"], user.Avatar)
}
case "addresses":
defaultV := AnyArray{AnyMap{"locality": "", "region": "", "country": ""}}
vs := ToAnyArray(value, defaultV) // e.g. [{"locality": "Hollywood", "region": "CN", "country": "USA"}]
if len(vs) > 0 {
v := ToAnyMap(vs[0])
user.Location = ToString(v["locality"], user.Location)
user.Region = ToString(v["region"], user.Region)
user.CountryCode = ToString(v["country"], user.CountryCode)
}
case UserExtensionKey:
defaultV := AnyMap{"organization": user.Owner}
v := ToAnyMap(value, defaultV) // e.g. {"organization": "org1"}
user.Owner = ToString(v["organization"], user.Owner)
case fmt.Sprintf("%v.%v", UserExtensionKey, "organization"):
user.Owner = ToString(value, user.Owner)
}
}
_, err = object.UpdateUser(old, user, nil, true)
if err != nil {
return scim.Resource{}, err
}
r = *user2resource(user)
return r, nil
}

238
scim/util.go Normal file
View File

@ -0,0 +1,238 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scim
import (
"fmt"
"log"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
"github.com/elimity-com/scim"
"github.com/elimity-com/scim/optional"
"github.com/elimity-com/scim/schema"
)
type AnyMap map[string]interface{}
type AnyArray []interface{}
func ToString(v interface{}, defaultV ...interface{}) string {
if v == nil {
if len(defaultV) > 0 {
v = defaultV[0]
}
}
return v.(string)
}
func ToAnyMap(v interface{}, defaultV ...interface{}) AnyMap {
if v == nil {
if len(defaultV) > 0 {
v = defaultV[0]
}
}
m, ok := v.(map[string]interface{})
if !ok {
m = v.(AnyMap)
}
return m
}
func ToAnyArray(v interface{}, defaultV ...interface{}) AnyArray {
if v == nil {
if len(defaultV) > 0 {
v = defaultV[0]
}
}
m, ok := v.([]interface{})
if !ok {
m = v.(AnyArray)
}
return m
}
func newStringParams(name string, required, unique bool) schema.SimpleParams {
uniqueness := schema.AttributeUniquenessNone()
if unique {
uniqueness = schema.AttributeUniquenessServer()
}
return schema.SimpleStringParams(schema.StringParams{
Name: name,
Required: required,
Uniqueness: uniqueness,
})
}
func newComplexParams(name string, required bool, multi bool, subAttributes []schema.SimpleParams) schema.ComplexParams {
return schema.ComplexParams{
Name: name,
Required: required,
MultiValued: multi,
SubAttributes: subAttributes,
}
}
func buildExternalId(user *object.User) optional.String {
if user.ExternalId != "" {
return optional.NewString(user.ExternalId)
} else {
return optional.String{}
}
}
func buildMeta(user *object.User) scim.Meta {
createdTime := util.String2Time(user.CreatedTime)
updatedTime := util.String2Time(user.UpdatedTime)
if user.UpdatedTime == "" {
updatedTime = createdTime
}
return scim.Meta{
Created: &createdTime,
LastModified: &updatedTime,
Version: util.Time2String(updatedTime),
}
}
func getAttrString(attrs scim.ResourceAttributes, key string) string {
if attrs[key] == nil {
return ""
} else {
return attrs[key].(string)
}
}
func getAttrJson(attrs scim.ResourceAttributes, key string) scim.ResourceAttributes {
if attrs[key] == nil {
return nil
} else {
if v, ok := attrs[key].(map[string]interface{}); ok {
return v
} else if v, ok := attrs[key].([]interface{}); ok {
if len(v) > 0 {
return v[0].(map[string]interface{})
} else {
return nil
}
} else {
panic("invalid attribute type")
}
}
}
func getAttrJsonValue(attrs scim.ResourceAttributes, key1 string, key2 string) string {
attr := getAttrJson(attrs, key1)
if attr == nil {
return ""
} else {
return getAttrString(attr, key2)
}
}
func user2resource(user *object.User) *scim.Resource {
attrs := make(map[string]interface{})
// Singular attributes
attrs["userName"] = user.Name
// The cleartext value or the hashed value of a password SHALL NOT be returnable by a service provider.
// attrs["password"] = user.Password
formatted := fmt.Sprintf("%s %s", user.FirstName, user.LastName)
if user.FirstName == "" {
formatted = user.LastName
}
if user.LastName == "" {
formatted = user.FirstName
}
attrs["name"] = scim.ResourceAttributes{
"formatted": formatted,
"familyName": user.LastName,
"givenName": user.FirstName,
}
attrs["displayName"] = user.DisplayName
attrs["nickName"] = user.DisplayName
attrs["userType"] = user.Type
attrs["profileUrl"] = user.Homepage
attrs["active"] = !user.IsForbidden && !user.IsDeleted
// Multi-Valued attributes
attrs["emails"] = []scim.ResourceAttributes{
{
"value": user.Email,
},
}
attrs["phoneNumbers"] = []scim.ResourceAttributes{
{
"value": user.Phone,
},
}
attrs["photos"] = []scim.ResourceAttributes{
{
"value": user.Avatar,
},
}
attrs["addresses"] = []scim.ResourceAttributes{
{
"locality": user.Location, // e.g. Hollywood
"region": user.Region, // e.g. CN
"country": user.CountryCode, // e.g. USA
},
}
// Enterprise user schema extension
attrs[UserExtensionKey] = scim.ResourceAttributes{
"organization": user.Owner,
}
return &scim.Resource{
ID: user.Id,
ExternalID: buildExternalId(user),
Attributes: attrs,
Meta: buildMeta(user),
}
}
func resource2user(attrs scim.ResourceAttributes) (user *object.User, err error) {
defer func() {
if r := recover(); r != nil {
log.Printf("failed to parse attrs: %v", r)
err = fmt.Errorf("%v", r)
}
}()
user = &object.User{
ExternalId: getAttrString(attrs, "externalId"),
Name: getAttrString(attrs, "userName"),
Password: getAttrString(attrs, "password"),
DisplayName: getAttrString(attrs, "displayName"),
Homepage: getAttrString(attrs, "profileUrl"),
Type: getAttrString(attrs, "userType"),
Owner: getAttrJsonValue(attrs, UserExtensionKey, "organization"),
FirstName: getAttrJsonValue(attrs, "name", "givenName"),
LastName: getAttrJsonValue(attrs, "name", "familyName"),
Email: getAttrJsonValue(attrs, "emails", "value"),
Phone: getAttrJsonValue(attrs, "phoneNumbers", "value"),
Avatar: getAttrJsonValue(attrs, "photos", "value"),
Location: getAttrJsonValue(attrs, "addresses", "locality"),
Region: getAttrJsonValue(attrs, "addresses", "region"),
CountryCode: getAttrJsonValue(attrs, "addresses", "country"),
CreatedTime: util.GetCurrentTime(),
UpdatedTime: util.GetCurrentTime(),
}
if user.Owner == "" {
err = fmt.Errorf("organization in %s is required", UserExtensionKey)
}
return
}

View File

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

View File

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

View File

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

View File

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

116
sync_v2/cmd_test.go Normal file
View File

@ -0,0 +1,116 @@
// 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 sync_v2
import (
"testing"
_ "github.com/go-sql-driver/mysql"
)
/*
The following config should be added to my.cnf:
gtid_mode=on
enforce_gtid_consistency=on
binlog-format=ROW
server-id = 1 # this should be different for each mysql instance (1,2)
auto_increment_offset = 1 # this is same as server-id
auto_increment_increment = 2 # this is same as the number of mysql instances (2)
log-bin = mysql-bin
replicate-do-db = casdoor # this is the database name
binlog-do-db = casdoor # this is the database name
*/
var Configs = []Database{
{
host: "test-db.v2tl.com",
port: 3306,
username: "root",
password: "password",
database: "casdoor",
// the following two fields are used to create replication user, you don't need to change them
slaveUser: "repl_user",
slavePassword: "repl_user",
},
{
host: "localhost",
port: 3306,
username: "root",
password: "password",
database: "casdoor",
// the following two fields are used to create replication user, you don't need to change them
slaveUser: "repl_user",
slavePassword: "repl_user",
},
}
func TestStartMasterSlaveSync(t *testing.T) {
// for example, this is aliyun rds
db0 := newDatabase(&Configs[0])
// for example, this is local mysql instance
db1 := newDatabase(&Configs[1])
createSlaveUser(db0)
// db0 is master, db1 is slave
startSlave(db0, db1)
}
func TestStopMasterSlaveSync(t *testing.T) {
// for example, this is aliyun rds
db0 := newDatabase(&Configs[0])
// for example, this is local mysql instance
db1 := newDatabase(&Configs[1])
stopSlave(db1)
deleteSlaveUser(db0)
}
func TestStartMasterMasterSync(t *testing.T) {
db0 := newDatabase(&Configs[0])
db1 := newDatabase(&Configs[1])
createSlaveUser(db0)
createSlaveUser(db1)
// db0 is master, db1 is slave
startSlave(db0, db1)
// db1 is master, db0 is slave
startSlave(db1, db0)
}
func TestStopMasterMasterSync(t *testing.T) {
db0 := newDatabase(&Configs[0])
db1 := newDatabase(&Configs[1])
stopSlave(db0)
stopSlave(db1)
deleteSlaveUser(db0)
deleteSlaveUser(db1)
}
func TestShowSlaveStatus(t *testing.T) {
db0 := newDatabase(&Configs[0])
db1 := newDatabase(&Configs[1])
slaveStatus(db0)
slaveStatus(db1)
}
func TestShowMasterStatus(t *testing.T) {
db0 := newDatabase(&Configs[0])
db1 := newDatabase(&Configs[1])
masterStatus(db0)
masterStatus(db1)
}

70
sync_v2/db.go Normal file
View File

@ -0,0 +1,70 @@
// 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 sync_v2
import (
"fmt"
"log"
"github.com/xorm-io/xorm"
)
type Database struct {
host string
port int
database string
username string
password string
slaveUser string
slavePassword string
engine *xorm.Engine
}
func (db *Database) exec(format string, args ...interface{}) []map[string]string {
sql := fmt.Sprintf(format, args...)
res, err := db.engine.QueryString(sql)
if err != nil {
panic(err)
}
return res
}
func createEngine(dataSourceName string) (*xorm.Engine, error) {
engine, err := xorm.NewEngine("mysql", dataSourceName)
if err != nil {
return nil, err
}
// ping mysql
err = engine.Ping()
if err != nil {
return nil, err
}
engine.ShowSQL(true)
log.Println("mysql connection success")
return engine, nil
}
func newDatabase(db *Database) *Database {
dataSourceName := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", db.username, db.password, db.host, db.port, db.database)
engine, err := createEngine(dataSourceName)
if err != nil {
panic(err)
}
db.engine = engine
return db
}

89
sync_v2/master.go Normal file
View File

@ -0,0 +1,89 @@
// 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 sync_v2
import (
"fmt"
"log"
)
func deleteSlaveUser(masterdb *Database) {
defer func() {
if err := recover(); err != nil {
log.Fatalln(err)
}
}()
masterdb.exec("delete from mysql.user where user = '%v'", masterdb.slaveUser)
masterdb.exec("flush privileges")
}
func createSlaveUser(masterdb *Database) {
res := make([]map[string]string, 0)
defer func() {
if err := recover(); err != nil {
log.Fatalln(err)
}
}()
res = masterdb.exec("show databases")
dbNames := make([]string, 0, len(res))
for _, dbInfo := range res {
dbName := dbInfo["Database"]
dbNames = append(dbNames, dbName)
}
log.Println("dbs in mysql: ", dbNames)
res = masterdb.exec("show tables")
tableNames := make([]string, 0, len(res))
for _, table := range res {
tableName := table[fmt.Sprintf("Tables_in_%v", masterdb.database)]
tableNames = append(tableNames, tableName)
}
log.Printf("tables in %v: %v", masterdb.database, tableNames)
// delete user to prevent user already exists
res = masterdb.exec("delete from mysql.user where user = '%v'", masterdb.slaveUser)
res = masterdb.exec("flush privileges")
// create replication user
res = masterdb.exec("create user '%s'@'%s' identified by '%s'", masterdb.slaveUser, "%", masterdb.slavePassword)
res = masterdb.exec("select host, user from mysql.user where user = '%v'", masterdb.slaveUser)
log.Println("user: ", res[0])
res = masterdb.exec("grant replication slave on *.* to '%s'@'%s'", masterdb.slaveUser, "%")
res = masterdb.exec("flush privileges")
res = masterdb.exec("show grants for '%s'@'%s'", masterdb.slaveUser, "%")
log.Println("grants: ", res[0])
// check env
res = masterdb.exec("show variables like 'server_id'")
log.Println("server_id: ", res[0]["Value"])
res = masterdb.exec("show variables like 'log_bin'")
log.Println("log_bin: ", res[0]["Value"])
res = masterdb.exec("show variables like 'binlog_format'")
log.Println("binlog_format: ", res[0]["Value"])
res = masterdb.exec("show variables like 'binlog_row_image'")
}
func masterStatus(masterdb *Database) {
res := masterdb.exec("show master status")
if len(res) == 0 {
log.Printf("no master status for master [%v:%v]\n", masterdb.host, masterdb.port)
return
}
pos := res[0]["Position"]
file := res[0]["File"]
log.Println("*****check master status*****")
log.Println("master:", masterdb.host, ":", masterdb.port)
log.Println("file:", file, ", position:", pos, ", master status:", res)
log.Println("*****************************")
}

84
sync_v2/slave.go Normal file
View File

@ -0,0 +1,84 @@
// 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 sync_v2
import "log"
// slaveStatus shows slave status
func slaveStatus(slavedb *Database) {
res := slavedb.exec("show slave status")
if len(res) == 0 {
log.Printf("no slave status for slave [%v:%v]\n", slavedb.host, slavedb.port)
return
}
log.Println("*****check slave status*****")
log.Println("slave:", slavedb.host, ":", slavedb.port)
masterServerId := res[0]["Master_Server_Id"]
log.Println("master server id:", masterServerId)
lastError := res[0]["Last_Error"]
log.Println("last error:", lastError) // this should be empty
lastIoError := res[0]["Last_IO_Error"]
log.Println("last io error:", lastIoError) // this should be empty
slaveIoState := res[0]["Slave_IO_State"]
log.Println("slave io state:", slaveIoState)
slaveIoRunning := res[0]["Slave_IO_Running"]
log.Println("slave io running:", slaveIoRunning) // this should be Yes
slaveSqlRunning := res[0]["Slave_SQL_Running"]
log.Println("slave sql running:", slaveSqlRunning) // this should be Yes
slaveSqlRunningState := res[0]["Slave_SQL_Running_State"]
log.Println("slave sql running state:", slaveSqlRunningState)
slaveSecondsBehindMaster := res[0]["Seconds_Behind_Master"]
log.Println("seconds behind master:", slaveSecondsBehindMaster) // this should be 0, if not, it means the slave is behind the master
log.Println("slave status:", res)
log.Println("****************************")
}
// stopSlave stops slave
func stopSlave(slavedb *Database) {
defer func() {
if err := recover(); err != nil {
log.Fatalln(err)
}
}()
slavedb.exec("stop slave")
slaveStatus(slavedb)
}
// startSlave starts slave
func startSlave(masterdb *Database, slavedb *Database) {
res := make([]map[string]string, 0)
defer func() {
if err := recover(); err != nil {
log.Fatalln(err)
}
}()
stopSlave(slavedb)
// get the info about master
res = masterdb.exec("show master status")
if len(res) == 0 {
log.Println("no master status")
return
}
pos := res[0]["Position"]
file := res[0]["File"]
log.Println("file:", file, ", position:", pos, ", master status:", res)
res = slavedb.exec("stop slave")
res = slavedb.exec(
"change master to master_host='%v', master_port=%v, master_user='%v', master_password='%v', master_log_file='%v', master_log_pos=%v;",
masterdb.host, masterdb.port, masterdb.slaveUser, masterdb.slavePassword, file, pos,
)
res = slavedb.exec("start slave")
slaveStatus(slavedb)
}

66
sync_v2/table_test.go Normal file
View File

@ -0,0 +1,66 @@
// 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 sync_v2
import (
"log"
"math/rand"
"testing"
"github.com/casdoor/casdoor/util"
)
type TestUser struct {
Id int64 `xorm:"pk autoincr"`
Username string `xorm:"varchar(50)"`
Address string `xorm:"varchar(50)"`
Card string `xorm:"varchar(50)"`
Age int
}
func TestCreateUserTable(t *testing.T) {
db := newDatabase(&Configs[0])
err := db.engine.Sync2(new(TestUser))
if err != nil {
log.Fatalln(err)
}
}
func TestInsertUser(t *testing.T) {
db := newDatabase(&Configs[0])
// random generate user
user := &TestUser{
Username: util.GetRandomName(),
Age: rand.Intn(100) + 10,
}
_, err := db.engine.Insert(user)
if err != nil {
log.Fatalln(err)
}
}
func TestDeleteUser(t *testing.T) {
db := newDatabase(&Configs[0])
user := &TestUser{
Id: 10,
}
_, err := db.engine.Delete(user)
if err != nil {
log.Fatalln(err)
}
}

View File

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

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