Compare commits

...

132 Commits

Author SHA1 Message Date
374928e719 feat: add custom HTTP Email provider (#2542)
* feat: implement Custom HTTP Email provider

* Update Setting.js

* Update ProviderEditPage.js

* Update http.go

* Update provider.go

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-12-14 22:35:25 +08:00
5c103e8cd3 Improve error handling in GenerateIdForNewUser() 2023-12-14 10:12:00 +08:00
85b86e8831 fix: dropped object group errors (#2545) 2023-12-14 09:00:25 +08:00
08864686f3 feat: fix Google cloud storage provider bug 2023-12-14 00:25:50 +08:00
dc06eb9948 feat: fix secret information issue in the CAPTCHA provider code (#2531) 2023-12-11 18:01:56 +08:00
b068202e74 Improve Radius username handling 2023-12-11 18:01:28 +08:00
cb16567c7b feat: helm support extra containers (#2530) 2023-12-10 14:41:56 +08:00
4eb725d47a Improve image upload UI 2023-12-08 19:42:20 +08:00
ce72a172b0 feat: add back Custom HTTP SMS provider 2023-12-07 16:59:41 +08:00
5521962e0c feat: update go-sms-sender to v0.17.0 to improve error handling 2023-12-07 14:25:21 +08:00
37b8b09cc0 feat: update go-sms-sender to v0.16.0 to fix first number missing bug in AmazonSNSClient.SendMessage 2023-12-06 20:05:48 +08:00
482eb61168 feat: improve StaticFilter() 2023-12-05 18:33:06 +08:00
8819a8697b feat: fix dropped error in stripe.go (#2525) 2023-12-05 16:02:33 +08:00
85cb68eb66 feat: unbind LDAP clients if not used any more 2023-12-02 17:51:25 +08:00
b25b5f0249 Support original accessToken in token APIs 2023-12-02 16:56:18 +08:00
947dcf6e75 Fix "All" roles bug in permission edit page 2023-12-02 15:26:52 +08:00
113c27db73 Improve logout's id_token_hint logic 2023-12-02 02:13:34 +08:00
badfe34755 feat: add "nonce" into the OAuth and OIDC tokens, for some apps require "nonce" to integrate (#2522) 2023-12-01 18:29:39 +08:00
a5f9f61381 feat: add token hash to improve performance 2023-11-30 18:05:30 +08:00
2ce8c93ead feat: Improve LDAP filter support (#2519) 2023-11-26 23:11:49 +08:00
da41ac7275 Improve error handling in getFaviconFileBuffer() 2023-11-25 18:31:33 +08:00
fd0c70a827 feat: Revert "feat: fix login page path after logout" (#2516)
This reverts commit 23d4488b64.
2023-11-24 15:52:59 +08:00
c4a6f07672 Allow app user in demo mode 2023-11-24 01:04:23 +08:00
a67f541171 feat: in LDAP, search '*' should return all properties (#2511) 2023-11-22 23:52:40 +08:00
192968bac8 Improve permission.State 2023-11-22 00:03:33 +08:00
23d4488b64 feat: fix login page path after logout (#2493)
Co-authored-by: aidenlu <aiden_lu@wochacha.com>
2023-11-21 23:37:35 +08:00
23f4684e1d feat: make MFA works for CAS login (#2506)
* feat: make MFA works for CAS login

* fix: Reduced code redundancy

* fix: Modified the format of the code.

* fix: fix an error with the 'res' variable

* Update LoginPage.js

* Update LoginPage.js

* Update LoginPage.js

* Update MfaAuthVerifyForm.js

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-11-21 21:35:19 +08:00
1a91e7b0f9 feat: support LDAP in Linux (#2508) 2023-11-21 14:01:27 +08:00
811999b6cc feat: fix error handling in CheckPassword() related functions 2023-11-20 21:49:19 +08:00
7786018051 feat: use short state for OAuth provider (#2504)
* fix: use fixed length of state

* fix: use short state
2023-11-19 07:30:29 +08:00
6c72f86d03 fix: support LDAP in linux (#2500)
Co-authored-by: Xiang Zhen Gan <m1353825@163.com>
2023-11-16 23:58:09 +08:00
5b151f4ec4 feat: improve cert edit page UI 2023-11-13 15:57:46 +08:00
e9b7d1266f Fix API typo: /get-global-certs 2023-11-13 14:22:40 +08:00
2d4998228c Add organization.MasterVerificationCode 2023-11-13 13:53:41 +08:00
d3ed6c348b Improve GetOAuthToken() API's parameter handling 2023-11-13 02:30:32 +08:00
a22e05dcc1 feat: fix the UI and navigation errors on the prompt page (#2486) 2023-11-12 15:54:38 +08:00
0ac2b69f5a feat: support WeChat Pay via JSAPI (#2488)
* feat: support wechat jsapi payment

* feat: add log

* feat: update sign

* feat: process wechat pay result

* feat: process wechat pay result

* feat: save wechat openid for different app

* feat: save wechat openid for different app

* feat: add SetUserOAuthProperties for signup

* feat: fix openid for wechat

* feat: get user extra property in buyproduct

* feat: remove log

* feat: remove log

* feat: gofumpt code

* feat: change lr->crlf

* feat: change crlf->lf

* feat: improve code
2023-11-11 17:16:57 +08:00
d090e9c860 Improve downloadImage() 2023-11-10 08:35:21 +08:00
8ebb158765 feat: improve README 2023-11-09 21:52:52 +08:00
ea2f053630 feat: add fields like Email to user profile in JWT-Empty mode 2023-11-09 20:20:42 +08:00
988b14c6b5 Fix user's UpdatedTime in other APIs 2023-11-08 20:22:28 +08:00
a9e72ac3cb feat: fix bug in GetAllowedApplications() 2023-11-08 10:31:24 +08:00
498cd02d49 feat: add GetAllowedApplications() in user's app homepage 2023-11-08 09:48:31 +08:00
a389842f59 Improve Product fields 2023-11-06 19:44:21 +08:00
6c69daa666 feat: fix search for ldap users' name within an organization (#2476)
* fix: #2304

* fix: when logging in with OAuth2 and authenticating via WebAuthn, retrieve the application from the clientId.

* fix: search for ldap users' name within an organization

---------

Co-authored-by: aidenlu <aiden_lu@wochacha.com>
2023-11-06 11:48:23 +08:00
53c89bbe89 feat: upgrade xorm-adapter to add id to CasbinRule 2023-11-03 02:48:01 +08:00
9442aa9f7a Remove useless PermissionRule 2023-11-03 00:39:16 +08:00
8a195715d0 Remove migrator code 2023-11-03 00:25:09 +08:00
b985bab3f3 fix: fix dropped errors in GetUser() (#2470)
* controllers: fix dropped errors

* Update user.go

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-11-01 23:07:24 +08:00
477a090aa0 feat: when logging in with OAuth2 and authenticating via WebAuthn, retrieve the application from the clientId (#2469)
* fix: #2304

* fix: when logging in with OAuth2 and authenticating via WebAuthn, retrieve the application from the clientId.

---------

Co-authored-by: aidenlu <aiden_lu@wochacha.com>
2023-11-01 18:40:05 +08:00
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
187 changed files with 5554 additions and 2306 deletions

View File

@ -127,7 +127,7 @@ jobs:
release-and-push:
name: Release And Push
runs-on: ubuntu-latest
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push'
if: github.repository == 'casbin/casdoor' && github.event_name == 'push'
needs: [ frontend, backend, linter, e2e ]
steps:
- name: Checkout
@ -184,27 +184,27 @@ jobs:
- name: Log in to Docker Hub
uses: docker/login-action@v1
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' && steps.should_push.outputs.push=='true'
if: github.repository == 'casbin/casdoor' && github.event_name == 'push' && steps.should_push.outputs.push=='true'
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Push to Docker Hub
uses: docker/build-push-action@v3
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' && steps.should_push.outputs.push=='true'
if: github.repository == 'casbin/casdoor' && github.event_name == 'push' && steps.should_push.outputs.push=='true'
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
- name: Push All In One Version to Docker Hub
uses: docker/build-push-action@v3
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' && steps.should_push.outputs.push=='true'
if: github.repository == 'casbin/casdoor' && github.event_name == 'push' && steps.should_push.outputs.push=='true'
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: ./

View File

@ -7,7 +7,7 @@ on:
jobs:
synchronize-with-crowdin:
runs-on: ubuntu-latest
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push'
if: github.repository == 'casbin/casdoor' && github.event_name == 'push'
steps:
- name: Checkout

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

@ -1,5 +1,5 @@
<h1 align="center" style="border-bottom: none;">📦⚡️ Casdoor</h1>
<h3 align="center">A UI-first centralized authentication / Single-Sign-On (SSO) platform based on OAuth 2.0 / OIDC.</h3>
<h3 align="center">An open-source UI-first Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML, CAS, LDAP, SCIM, WebAuthn, TOTP, MFA and RADIUS</h3>
<p align="center">
<a href="#badge">
<img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">

View File

@ -81,6 +81,7 @@ p, *, *, GET, /api/get-saml-login, *, *
p, *, *, POST, /api/acs, *, *
p, *, *, GET, /api/saml/metadata, *, *
p, *, *, *, /cas, *, *
p, *, *, *, /scim, *, *
p, *, *, *, /api/webauthn, *, *
p, *, *, GET, /api/get-release, *, *
p, *, *, GET, /api/get-default-application, *, *
@ -95,7 +96,7 @@ p, *, *, GET, /api/get-organization-names, *, *
sa := stringadapter.NewAdapter(ruleText)
// load all rules from string adapter to enforcer's memory
err := sa.LoadPolicy(Enforcer.GetModel())
err = sa.LoadPolicy(Enforcer.GetModel())
if err != nil {
panic(err)
}
@ -126,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)
@ -140,11 +147,11 @@ 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
if subOwner == objOwner && subName == objName && !(subOwner == "built-in" && subName == "admin") {
if (subOwner == objOwner && subName == objName || subOwner == "app") && !(subOwner == "built-in" && subName == "admin") {
return true
}
return false

View File

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

View File

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

View File

@ -282,17 +282,15 @@ func (c *ApiController) Logout() {
return
}
affected, application, token, err := object.ExpireTokenByAccessToken(accessToken)
_, application, token, err := object.ExpireTokenByAccessToken(accessToken)
if err != nil {
c.ResponseError(err.Error())
return
}
if !affected {
if token == nil {
c.ResponseError(c.T("token:Token not found, invalid accessToken"))
return
}
if application == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist")), token.Application)
return
@ -319,7 +317,15 @@ func (c *ApiController) Logout() {
return
} else {
if application.IsRedirectUriValid(redirectUri) {
c.Ctx.Redirect(http.StatusFound, fmt.Sprintf("%s?state=%s", strings.TrimRight(redirectUri, "/"), state))
redirectUrl := redirectUri
if state != "" {
if strings.Contains(redirectUri, "?") {
redirectUrl = fmt.Sprintf("%s&state=%s", strings.TrimSuffix(redirectUri, "/"), state)
} else {
redirectUrl = fmt.Sprintf("%s?state=%s", strings.TrimSuffix(redirectUri, "/"), state)
}
}
c.Ctx.Redirect(http.StatusFound, redirectUrl)
} else {
c.ResponseError(fmt.Sprintf(c.T("token:Redirect URI: %s doesn't exist in the allowed Redirect URI list"), redirectUri))
return
@ -473,7 +479,7 @@ func (c *ApiController) GetCaptcha() {
Type: captchaProvider.Type,
SubType: captchaProvider.SubType,
ClientId: captchaProvider.ClientId,
ClientSecret: captchaProvider.ClientSecret,
ClientSecret: "***",
ClientId2: captchaProvider.ClientId2,
ClientSecret2: captchaProvider.ClientSecret2,
})

View File

@ -173,6 +173,12 @@ func (c *ApiController) GetOrganizationApplications() {
return
}
applications, err = object.GetAllowedApplications(applications, userId)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(object.GetMaskedApplications(applications, userId))
} else {
limit := util.ParseInt(limit)

View File

@ -34,6 +34,7 @@ import (
"github.com/casdoor/casdoor/proxy"
"github.com/casdoor/casdoor/util"
"github.com/google/uuid"
"golang.org/x/oauth2"
)
var (
@ -154,7 +155,8 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
resp = &Response{Status: "error", Msg: fmt.Sprintf("error: grant_type: %s is not supported in this application", form.Type), Data: ""}
} else {
scope := c.Input().Get("scope")
token, _ := object.GetTokenByUser(application, user, scope, c.Ctx.Request.Host)
nonce := c.Input().Get("nonce")
token, _ := object.GetTokenByUser(application, user, scope, nonce, c.Ctx.Request.Host)
resp = tokenToResponse(token)
}
} else if form.Type == ResponseTypeSaml { // saml flow
@ -331,8 +333,6 @@ func (c *ApiController) Login() {
}
var user *object.User
var msg string
if authForm.Password == "" {
if user, err = object.GetUserByFields(authForm.Organization, authForm.Username); err != nil {
c.ResponseError(err.Error(), nil)
@ -354,20 +354,21 @@ func (c *ApiController) Login() {
}
// check result through Email or Phone
checkResult := object.CheckSigninCode(user, checkDest, authForm.Code, c.GetAcceptLanguage())
if len(checkResult) != 0 {
c.ResponseError(fmt.Sprintf("%s - %s", verificationCodeType, checkResult))
err = object.CheckSigninCode(user, checkDest, authForm.Code, c.GetAcceptLanguage())
if err != nil {
c.ResponseError(fmt.Sprintf("%s - %s", verificationCodeType, err.Error()))
return
}
// disable the verification code
err := object.DisableVerificationCode(checkDest)
err = object.DisableVerificationCode(checkDest)
if err != nil {
c.ResponseError(err.Error(), nil)
return
}
} else {
application, err := object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
var application *object.Application
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
if err != nil {
c.ResponseError(err.Error(), nil)
return
@ -386,7 +387,18 @@ func (c *ApiController) Login() {
c.ResponseError(err.Error())
return
} else if enableCaptcha {
isHuman, err := captcha.VerifyCaptchaByCaptchaType(authForm.CaptchaType, authForm.CaptchaToken, authForm.ClientSecret)
captchaProvider, err := object.GetCaptchaProviderByApplication(util.GetId(application.Owner, application.Name), "false", c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error())
return
}
if captchaProvider.Type != "Default" {
authForm.ClientSecret = captchaProvider.ClientSecret
}
var isHuman bool
isHuman, err = captcha.VerifyCaptchaByCaptchaType(authForm.CaptchaType, authForm.CaptchaToken, authForm.ClientSecret)
if err != nil {
c.ResponseError(err.Error())
return
@ -399,13 +411,15 @@ func (c *ApiController) Login() {
}
password := authForm.Password
user, msg = object.CheckUserPassword(authForm.Organization, authForm.Username, password, c.GetAcceptLanguage(), enableCaptcha)
user, err = object.CheckUserPassword(authForm.Organization, authForm.Username, password, c.GetAcceptLanguage(), enableCaptcha)
}
if msg != "" {
resp = &Response{Status: "error", Msg: msg}
if err != nil {
c.ResponseError(err.Error())
return
} else {
application, err := object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
var application *object.Application
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
if err != nil {
c.ResponseError(err.Error())
return
@ -416,7 +430,8 @@ func (c *ApiController) Login() {
return
}
organization, err := object.GetOrganizationByUser(user)
var organization *object.Organization
organization, err = object.GetOrganizationByUser(user)
if err != nil {
c.ResponseError(err.Error())
}
@ -461,12 +476,15 @@ func (c *ApiController) Login() {
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), authForm.Application))
return
}
organization, err := object.GetOrganization(util.GetId("admin", application.Organization))
var organization *object.Organization
organization, err = object.GetOrganization(util.GetId("admin", application.Organization))
if err != nil {
c.ResponseError(c.T(err.Error()))
}
provider, err := object.GetProvider(util.GetId("admin", authForm.Provider))
var provider *object.Provider
provider, err = object.GetProvider(util.GetId("admin", authForm.Provider))
if err != nil {
c.ResponseError(err.Error())
return
@ -477,11 +495,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
@ -489,7 +506,12 @@ 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)
var idProvider idp.IdProvider
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
@ -503,7 +525,8 @@ func (c *ApiController) Login() {
}
// https://github.com/golang/oauth2/issues/123#issuecomment-103715338
token, err := idProvider.GetToken(authForm.Code)
var token *oauth2.Token
token, err = idProvider.GetToken(authForm.Code)
if err != nil {
c.ResponseError(err.Error())
return
@ -524,7 +547,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
@ -543,7 +567,12 @@ func (c *ApiController) Login() {
if user.IsForbidden {
c.ResponseError(c.T("check:The user is forbidden to sign in, please contact the administrator"))
}
// sync info from 3rd-party if possible
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo)
if err != nil {
c.ResponseError(err.Error())
return
}
resp = c.HandleLoggedIn(application, user, &authForm)
record := object.NewRecord(c.Ctx)
@ -584,14 +613,16 @@ func (c *ApiController) Login() {
}
// Handle username conflicts
tmpUser, err := object.GetUser(util.GetId(application.Organization, userInfo.Username))
var tmpUser *object.User
tmpUser, err = object.GetUser(util.GetId(application.Organization, userInfo.Username))
if err != nil {
c.ResponseError(err.Error())
return
}
if tmpUser != nil {
uid, err := uuid.NewRandom()
var uid uuid.UUID
uid, err = uuid.NewRandom()
if err != nil {
c.ResponseError(err.Error())
return
@ -602,14 +633,16 @@ func (c *ApiController) Login() {
}
properties := map[string]string{}
count, err := object.GetUserCount(application.Organization, "", "", "")
var count int64
count, err = object.GetUserCount(application.Organization, "", "", "")
if err != nil {
c.ResponseError(err.Error())
return
}
properties["no"] = strconv.Itoa(int(count + 2))
initScore, err := organization.GetInitScore()
var initScore int
initScore, err = organization.GetInitScore()
if err != nil {
c.ResponseError(fmt.Errorf(c.T("account:Get init score failed, error: %w"), err).Error())
return
@ -641,7 +674,8 @@ func (c *ApiController) Login() {
Properties: properties,
}
affected, err := object.AddUser(user)
var affected bool
affected, err = object.AddUser(user)
if err != nil {
c.ResponseError(err.Error())
return
@ -651,10 +685,19 @@ 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
_, err := object.SetUserOAuthProperties(organization, user, provider.Type, userInfo)
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo)
if err != nil {
c.ResponseError(err.Error())
return
@ -679,6 +722,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}
@ -689,7 +733,8 @@ func (c *ApiController) Login() {
return
}
oldUser, err := object.GetUserByField(application.Organization, provider.Type, userInfo.Id)
var oldUser *object.User
oldUser, err = object.GetUserByField(application.Organization, provider.Type, userInfo.Id)
if err != nil {
c.ResponseError(err.Error())
return
@ -700,7 +745,8 @@ func (c *ApiController) Login() {
return
}
user, err := object.GetUser(userId)
var user *object.User
user, err = object.GetUser(userId)
if err != nil {
c.ResponseError(err.Error())
return
@ -713,7 +759,8 @@ func (c *ApiController) Login() {
return
}
isLinked, err := object.LinkUserAccount(user, provider.Type, userInfo.Id)
var isLinked bool
isLinked, err = object.LinkUserAccount(user, provider.Type, userInfo.Id)
if err != nil {
c.ResponseError(err.Error())
return
@ -726,7 +773,8 @@ func (c *ApiController) Login() {
}
}
} else if c.getMfaUserSession() != "" {
user, err := object.GetUser(c.getMfaUserSession())
var user *object.User
user, err = object.GetUser(c.getMfaUserSession())
if err != nil {
c.ResponseError(err.Error())
return
@ -759,7 +807,8 @@ func (c *ApiController) Login() {
return
}
application, err := object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
var application *object.Application
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
if err != nil {
c.ResponseError(err.Error())
return
@ -780,7 +829,8 @@ func (c *ApiController) Login() {
} else {
if c.GetSessionUsername() != "" {
// user already signed in to Casdoor, so let the user click the avatar button to do the quick sign-in
application, err := object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
var application *object.Application
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
if err != nil {
c.ResponseError(err.Error())
return

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 {
@ -238,7 +243,13 @@ func (c *ApiController) GetAllObjects() {
return
}
c.ResponseOk(object.GetAllObjects(userId))
objects, err := object.GetAllObjects(userId)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(objects)
}
func (c *ApiController) GetAllActions() {
@ -248,7 +259,13 @@ func (c *ApiController) GetAllActions() {
return
}
c.ResponseOk(object.GetAllActions(userId))
actions, err := object.GetAllActions(userId)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(actions)
}
func (c *ApiController) GetAllRoles() {
@ -258,5 +275,11 @@ func (c *ApiController) GetAllRoles() {
return
}
c.ResponseOk(object.GetAllRoles(userId))
roles, err := object.GetAllRoles(userId)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(roles)
}

View File

@ -65,13 +65,13 @@ func (c *ApiController) GetCerts() {
}
}
// GetGlobleCerts
// @Title GetGlobleCerts
// GetGlobalCerts
// @Title GetGlobalCerts
// @Tag Cert API
// @Description get globle certs
// @Success 200 {array} object.Cert The Response object
// @router /get-globle-certs [get]
func (c *ApiController) GetGlobleCerts() {
// @router /get-global-certs [get]
func (c *ApiController) GetGlobalCerts() {
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
@ -80,7 +80,7 @@ func (c *ApiController) GetGlobleCerts() {
sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" {
maskedCerts, err := object.GetMaskedCerts(object.GetGlobleCerts())
maskedCerts, err := object.GetMaskedCerts(object.GetGlobalCerts())
if err != nil {
c.ResponseError(err.Error())
return

View File

@ -59,6 +59,7 @@ func (c *ApiController) GetLdapUsers() {
c.ResponseError(err.Error())
return
}
defer conn.Close()
//groupsMap, err := conn.GetLdapGroups(ldapServer.BaseDn)
//if err != nil {

View File

@ -163,6 +163,8 @@ func (c *ApiController) BuyProduct() {
id := c.Input().Get("id")
host := c.Ctx.Request.Host
providerName := c.Input().Get("providerName")
paymentEnv := c.Input().Get("paymentEnv")
// buy `pricingName/planName` for `paidUserName`
pricingName := c.Input().Get("pricingName")
planName := c.Input().Get("planName")
@ -187,11 +189,11 @@ func (c *ApiController) BuyProduct() {
return
}
payment, err := object.BuyProduct(id, user, providerName, pricingName, planName, host)
payment, attachInfo, err := object.BuyProduct(id, user, providerName, pricingName, planName, host, paymentEnv)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(payment)
c.ResponseOk(payment, attachInfo)
}

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

@ -158,10 +158,9 @@ func (c *ApiController) DeleteToken() {
// @Success 401 {object} object.TokenError The Response object
// @router api/login/oauth/access_token [post]
func (c *ApiController) GetOAuthToken() {
grantType := c.Input().Get("grant_type")
refreshToken := c.Input().Get("refresh_token")
clientId := c.Input().Get("client_id")
clientSecret := c.Input().Get("client_secret")
grantType := c.Input().Get("grant_type")
code := c.Input().Get("code")
verifier := c.Input().Get("code_verifier")
scope := c.Input().Get("scope")
@ -169,35 +168,61 @@ func (c *ApiController) GetOAuthToken() {
password := c.Input().Get("password")
tag := c.Input().Get("tag")
avatar := c.Input().Get("avatar")
refreshToken := c.Input().Get("refresh_token")
if clientId == "" && clientSecret == "" {
clientId, clientSecret, _ = c.Ctx.Request.BasicAuth()
}
if clientId == "" {
// If clientID is empty, try to read data from RequestBody
if len(c.Ctx.Input.RequestBody) != 0 {
// If clientId is empty, try to read data from RequestBody
var tokenRequest TokenRequest
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &tokenRequest); err == nil {
clientId = tokenRequest.ClientId
clientSecret = tokenRequest.ClientSecret
grantType = tokenRequest.GrantType
refreshToken = tokenRequest.RefreshToken
code = tokenRequest.Code
verifier = tokenRequest.Verifier
scope = tokenRequest.Scope
username = tokenRequest.Username
password = tokenRequest.Password
tag = tokenRequest.Tag
avatar = tokenRequest.Avatar
err := json.Unmarshal(c.Ctx.Input.RequestBody, &tokenRequest)
if err == nil {
if clientId == "" {
clientId = tokenRequest.ClientId
}
if clientSecret == "" {
clientSecret = tokenRequest.ClientSecret
}
if grantType == "" {
grantType = tokenRequest.GrantType
}
if code == "" {
code = tokenRequest.Code
}
if verifier == "" {
verifier = tokenRequest.Verifier
}
if scope == "" {
scope = tokenRequest.Scope
}
if username == "" {
username = tokenRequest.Username
}
if password == "" {
password = tokenRequest.Password
}
if tag == "" {
tag = tokenRequest.Tag
}
if avatar == "" {
avatar = tokenRequest.Avatar
}
if refreshToken == "" {
refreshToken = tokenRequest.RefreshToken
}
}
}
host := c.Ctx.Request.Host
oAuthtoken, err := object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, username, password, host, refreshToken, tag, avatar, c.GetAcceptLanguage())
token, err := object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, username, password, host, refreshToken, tag, avatar, c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = oAuthtoken
c.Data["json"] = token
c.SetTokenErrorHttpStatus()
c.ServeJSON()
}

View File

@ -15,10 +15,10 @@
package controllers
type TokenRequest struct {
GrantType string `json:"grant_type"`
Code string `json:"code"`
ClientId string `json:"client_id"`
ClientSecret string `json:"client_secret"`
GrantType string `json:"grant_type"`
Code string `json:"code"`
Verifier string `json:"code_verifier"`
Scope string `json:"scope"`
Username string `json:"username"`

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
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)
}
organization, err := object.GetOrganization(util.GetId("admin", owner))
if err != nil {
c.ResponseError(err.Error())
return
}
if !organization.IsProfilePublic {
requestUserId := c.GetSessionUsername()
hasPermission, err := object.CheckUserPermission(requestUserId, id, false, c.GetAcceptLanguage())
if !hasPermission {
var organization *object.Organization
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 {
@ -460,16 +476,16 @@ func (c *ApiController) SetPassword() {
isAdmin := c.IsAdmin()
if isAdmin {
if oldPassword != "" {
msg := object.CheckPassword(targetUser, oldPassword, c.GetAcceptLanguage())
if msg != "" {
c.ResponseError(msg)
err = object.CheckPassword(targetUser, oldPassword, c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error())
return
}
}
} else {
msg := object.CheckPassword(targetUser, oldPassword, c.GetAcceptLanguage())
if msg != "" {
c.ResponseError(msg)
} else if code == "" {
err = object.CheckPassword(targetUser, oldPassword, c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error())
return
}
}
@ -502,11 +518,11 @@ func (c *ApiController) CheckUserPassword() {
return
}
_, msg := object.CheckUserPassword(user.Owner, user.Name, user.Password, c.GetAcceptLanguage())
if msg == "" {
c.ResponseOk()
_, err = object.CheckUserPassword(user.Owner, user.Name, user.Password, c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error())
} else {
c.ResponseError(msg)
c.ResponseOk()
}
}
@ -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

@ -53,17 +53,34 @@ func (c *ApiController) SendVerificationCode() {
return
}
if vform.CaptchaType != "none" {
if captchaProvider := captcha.GetCaptchaProvider(vform.CaptchaType); captchaProvider == nil {
c.ResponseError(c.T("general:don't support captchaProvider: ") + vform.CaptchaType)
return
} else if isHuman, err := captchaProvider.VerifyCaptcha(vform.CaptchaToken, vform.ClientSecret); err != nil {
c.ResponseError(err.Error())
return
} else if !isHuman {
provider, err := object.GetCaptchaProviderByApplication(vform.ApplicationId, "false", c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error())
return
}
if provider != nil {
if vform.CaptchaType != provider.Type {
c.ResponseError(c.T("verification:Turing test failed."))
return
}
if provider.Type != "Default" {
vform.ClientSecret = provider.ClientSecret
}
if vform.CaptchaType != "none" {
if captchaProvider := captcha.GetCaptchaProvider(vform.CaptchaType); captchaProvider == nil {
c.ResponseError(c.T("general:don't support captchaProvider: ") + vform.CaptchaType)
return
} else if isHuman, err := captchaProvider.VerifyCaptcha(vform.CaptchaToken, vform.ClientSecret); err != nil {
c.ResponseError(err.Error())
return
} else if !isHuman {
c.ResponseError(c.T("verification:Turing test failed."))
return
}
}
}
application, err := object.GetApplication(vform.ApplicationId)
@ -142,6 +159,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 +205,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))
@ -217,6 +242,16 @@ func (c *ApiController) VerifyCaptcha() {
return
}
captchaProvider, err := object.GetCaptchaProviderByOwnerName(vform.ApplicationId, c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error())
return
}
if captchaProvider.Type != "Default" {
vform.ClientSecret = captchaProvider.ClientSecret
}
provider := captcha.GetCaptchaProvider(vform.CaptchaType)
if provider == nil {
c.ResponseError(c.T("verification:Invalid captcha provider."))

View File

@ -154,6 +154,7 @@ func (c *ApiController) WebAuthnSigninBegin() {
// @router /webauthn/signin/finish [post]
func (c *ApiController) WebAuthnSigninFinish() {
responseType := c.Input().Get("responseType")
clientId := c.Input().Get("clientId")
webauthnObj, err := object.GetWebAuthnObject(c.Ctx.Request.Host)
if err != nil {
c.ResponseError(err.Error())
@ -182,7 +183,13 @@ func (c *ApiController) WebAuthnSigninFinish() {
c.SetSessionUsername(userId)
util.LogInfo(c.Ctx, "API: [%s] signed in", userId)
application, err := object.GetApplicationByUser(user)
var application *object.Application
if clientId != "" && (responseType == ResponseTypeCode) {
application, err = object.GetApplicationByClientId(clientId)
} else {
application, err = object.GetApplicationByUser(user)
}
if err != nil {
c.ResponseError(err.Error())
return

75
email/http.go Normal file
View File

@ -0,0 +1,75 @@
// 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 email
import (
"bytes"
"fmt"
"net/http"
"github.com/casdoor/casdoor/proxy"
)
type HttpEmailProvider struct {
endpoint string
method string
}
func NewHttpEmailProvider(endpoint string, method string) *HttpEmailProvider {
client := &HttpEmailProvider{
endpoint: endpoint,
method: method,
}
return client
}
func (c *HttpEmailProvider) Send(fromAddress string, fromName string, toAddress string, subject string, content string) error {
req, err := http.NewRequest(c.method, c.endpoint, bytes.NewBufferString(content))
if err != nil {
return err
}
if c.method == "POST" {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.PostForm = map[string][]string{
"fromName": {fromName},
"toAddress": {toAddress},
"subject": {subject},
"content": {content},
}
} else if c.method == "GET" {
q := req.URL.Query()
q.Add("fromName", fromName)
q.Add("toAddress", toAddress)
q.Add("subject", subject)
q.Add("content", content)
req.URL.RawQuery = q.Encode()
} else {
return fmt.Errorf("HttpEmailProvider's Send() error, unsupported method: %s", c.method)
}
httpClient := proxy.DefaultHttpClient
resp, err := httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("HttpEmailProvider's Send() error, custom HTTP Email request failed with status: %s", resp.Status)
}
return err
}

View File

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

28
go.mod
View File

@ -9,28 +9,30 @@ require (
github.com/aws/aws-sdk-go v1.45.5
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.77.2
github.com/casdoor/go-sms-sender v0.14.0
github.com/casdoor/go-sms-sender v0.17.0
github.com/casdoor/gomail/v2 v2.0.1
github.com/casdoor/notify v0.43.0
github.com/casdoor/oss v1.3.0
github.com/casdoor/xorm-adapter/v3 v3.0.4
github.com/casdoor/notify v0.45.0
github.com/casdoor/oss v1.4.1
github.com/casdoor/xorm-adapter/v3 v3.1.0
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-asn1-ber/asn1-ber v1.5.5
github.com/go-git/go-git/v5 v5.6.0
github.com/go-ldap/ldap/v3 v3.3.0
github.com/go-ldap/ldap/v3 v3.4.6
github.com/go-mysql-org/go-mysql v1.7.0
github.com/go-pay/gopay v1.5.72
github.com/go-sql-driver/mysql v1.6.0
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
github.com/go-webauthn/webauthn v0.6.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/google/uuid v1.3.1
github.com/google/uuid v1.4.0
github.com/json-iterator/go v1.1.12
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
github.com/lestrrat-go/jwx v1.2.21
github.com/lib/pq v1.10.9
@ -41,7 +43,7 @@ require (
github.com/nyaruka/phonenumbers v1.1.5
github.com/pquerna/otp v1.4.0
github.com/prometheus/client_golang v1.11.1
github.com/prometheus/client_model v0.3.0
github.com/prometheus/client_model v0.4.0
github.com/qiangmzsx/string-adapter/v2 v2.1.0
github.com/robfig/cron/v3 v3.0.1
github.com/russellhaering/gosaml2 v0.9.0
@ -60,13 +62,13 @@ require (
github.com/xorm-io/core v0.7.4
github.com/xorm-io/xorm v1.1.6
github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/crypto v0.12.0
golang.org/x/net v0.14.0
golang.org/x/oauth2 v0.11.0
google.golang.org/api v0.138.0
golang.org/x/crypto v0.14.0
golang.org/x/net v0.17.0
golang.org/x/oauth2 v0.13.0
google.golang.org/api v0.150.0
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0
layeh.com/radius v0.0.0-20221205141417-e7fbddd11d68 // indirect
layeh.com/radius v0.0.0-20221205141417-e7fbddd11d68
maunium.net/go/mautrix v0.16.0
modernc.org/sqlite v1.18.2
)

320
go.sum

File diff suppressed because it is too large Load Diff

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

@ -89,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":
@ -101,8 +101,24 @@ func NewGothIdProvider(providerType string, clientId string, clientSecret string
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":
@ -386,10 +402,10 @@ 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

View File

@ -15,6 +15,7 @@
package idp
import (
"fmt"
"net/http"
"strings"
@ -30,16 +31,19 @@ type UserInfo struct {
Phone string
CountryCode string
AvatarUrl string
Extra map[string]string
}
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 +57,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

@ -186,15 +186,24 @@ func (idp *WeChatIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error)
id = wechatUserInfo.Openid
}
extra := make(map[string]string)
extra["wechat_unionid"] = wechatUserInfo.Openid
// For WeChat, different appId corresponds to different openId
extra[BuildWechatOpenIdKey(idp.Config.ClientID)] = wechatUserInfo.Openid
userInfo := UserInfo{
Id: id,
Username: wechatUserInfo.Nickname,
DisplayName: wechatUserInfo.Nickname,
AvatarUrl: wechatUserInfo.Headimgurl,
Extra: extra,
}
return &userInfo, nil
}
func BuildWechatOpenIdKey(appId string) string {
return fmt.Sprintf("wechat_openid_%s", appId)
}
func GetWechatOfficialAccountAccessToken(clientId string, clientSecret string) (string, error) {
accessTokenUrl := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", clientId, clientSecret)
request, err := http.NewRequest("GET", accessTokenUrl, nil)

View File

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

View File

@ -16,6 +16,7 @@ package ldap
import (
"fmt"
"hash/fnv"
"log"
"github.com/casdoor/casdoor/conf"
@ -25,6 +26,11 @@ import (
)
func StartLdapServer() {
ldapServerPort := conf.GetConfigString("ldapServerPort")
if ldapServerPort == "" || ldapServerPort == "0" {
return
}
server := ldap.NewServer()
routes := ldap.NewRouteMux()
@ -32,7 +38,7 @@ func StartLdapServer() {
routes.Search(handleSearch).Label(" SEARCH****")
server.Handle(routes)
err := server.ListenAndServe("0.0.0.0:" + conf.GetConfigString("ldapServerPort"))
err := server.ListenAndServe("0.0.0.0:" + ldapServerPort)
if err != nil {
log.Printf("StartLdapServer() failed, err = %s", err.Error())
}
@ -44,20 +50,20 @@ func handleBind(w ldap.ResponseWriter, m *ldap.Message) {
if r.AuthenticationChoice() == "simple" {
bindUsername, bindOrg, err := getNameAndOrgFromDN(string(r.Name()))
if err != "" {
log.Printf("Bind failed ,ErrMsg=%s", err)
if err != nil {
log.Printf("getNameAndOrgFromDN() error: %s", err.Error())
res.SetResultCode(ldap.LDAPResultInvalidDNSyntax)
res.SetDiagnosticMessage("bind failed ErrMsg: " + err)
res.SetDiagnosticMessage(fmt.Sprintf("getNameAndOrgFromDN() error: %s", err.Error()))
w.Write(res)
return
}
bindPassword := string(r.AuthenticationSimple())
bindUser, err := object.CheckUserPassword(bindOrg, bindUsername, bindPassword, "en")
if err != "" {
if err != nil {
log.Printf("Bind failed User=%s, Pass=%#v, ErrMsg=%s", string(r.Name()), r.Authentication(), err)
res.SetResultCode(ldap.LDAPResultInvalidCredentials)
res.SetDiagnosticMessage("invalid credentials ErrMsg: " + err)
res.SetDiagnosticMessage("invalid credentials ErrMsg: " + err.Error())
w.Write(res)
return
}
@ -73,7 +79,7 @@ func handleBind(w ldap.ResponseWriter, m *ldap.Message) {
m.Client.OrgName = bindOrg
} else {
res.SetResultCode(ldap.LDAPResultAuthMethodNotSupported)
res.SetDiagnosticMessage("Authentication method not supported,Please use Simple Authentication")
res.SetDiagnosticMessage("Authentication method not supported, please use Simple Authentication")
}
w.Write(res)
}
@ -108,10 +114,22 @@ func handleSearch(w ldap.ResponseWriter, m *ldap.Message) {
}
for _, user := range users {
dn := fmt.Sprintf("cn=%s,%s", user.Name, string(r.BaseObject()))
dn := fmt.Sprintf("uid=%s,cn=%s,%s", user.Id, user.Name, string(r.BaseObject()))
e := ldap.NewSearchResultEntry(dn)
for _, attr := range r.Attributes() {
uidNumberStr := fmt.Sprintf("%v", hash(user.Name))
e.AddAttribute("uidNumber", message.AttributeValue(uidNumberStr))
e.AddAttribute("gidNumber", message.AttributeValue(uidNumberStr))
e.AddAttribute("homeDirectory", message.AttributeValue("/home/"+user.Name))
e.AddAttribute("cn", message.AttributeValue(user.Name))
e.AddAttribute("uid", message.AttributeValue(user.Id))
attrs := r.Attributes()
for _, attr := range attrs {
if string(attr) == "*" {
attrs = AdditionalLdapAttributes
break
}
}
for _, attr := range attrs {
e.AddAttribute(message.AttributeDescription(attr), getAttribute(string(attr), user))
if string(attr) == "cn" {
e.AddAttribute(message.AttributeDescription(attr), getAttribute("title", user))
@ -122,3 +140,9 @@ func handleSearch(w ldap.ResponseWriter, m *ldap.Message) {
}
w.Write(res)
}
func hash(s string) uint32 {
h := fnv.New32a()
h.Write([]byte(s))
return h.Sum32()
}

View File

@ -24,9 +24,73 @@ import (
"github.com/lor00x/goldap/message"
ldap "github.com/forestmgy/ldapserver"
"github.com/xorm-io/builder"
)
func getNameAndOrgFromDN(DN string) (string, string, string) {
type AttributeMapper func(user *object.User) message.AttributeValue
type FieldRelation struct {
userField string
notSearchable bool
hideOnStarOp bool
fieldMapper AttributeMapper
}
func (rel FieldRelation) GetField() (string, error) {
if rel.notSearchable {
return "", fmt.Errorf("attribute %s not supported", rel.userField)
}
return rel.userField, nil
}
func (rel FieldRelation) GetAttributeValue(user *object.User) message.AttributeValue {
return rel.fieldMapper(user)
}
var ldapAttributesMapping = map[string]FieldRelation{
"cn": {userField: "name", hideOnStarOp: true, fieldMapper: func(user *object.User) message.AttributeValue {
return message.AttributeValue(user.Name)
}},
"uid": {userField: "name", hideOnStarOp: true, fieldMapper: func(user *object.User) message.AttributeValue {
return message.AttributeValue(user.Name)
}},
"displayname": {userField: "displayName", fieldMapper: func(user *object.User) message.AttributeValue {
return message.AttributeValue(user.DisplayName)
}},
"email": {userField: "email", fieldMapper: func(user *object.User) message.AttributeValue {
return message.AttributeValue(user.Email)
}},
"mail": {userField: "email", fieldMapper: func(user *object.User) message.AttributeValue {
return message.AttributeValue(user.Email)
}},
"mobile": {userField: "phone", fieldMapper: func(user *object.User) message.AttributeValue {
return message.AttributeValue(user.Phone)
}},
"title": {userField: "tag", fieldMapper: func(user *object.User) message.AttributeValue {
return message.AttributeValue(user.Tag)
}},
"userPassword": {
userField: "userPassword",
notSearchable: true,
fieldMapper: func(user *object.User) message.AttributeValue {
return message.AttributeValue(getUserPasswordWithType(user))
},
},
}
var AdditionalLdapAttributes []message.LDAPString
func init() {
for k, v := range ldapAttributesMapping {
if v.hideOnStarOp {
continue
}
AdditionalLdapAttributes = append(AdditionalLdapAttributes, message.LDAPString(k))
}
}
func getNameAndOrgFromDN(DN string) (string, string, error) {
DNFields := strings.Split(DN, ",")
params := make(map[string]string, len(DNFields))
for _, field := range DNFields {
@ -37,12 +101,12 @@ func getNameAndOrgFromDN(DN string) (string, string, string) {
}
if params["cn"] == "" {
return "", "", "please use Admin Name format like cn=xxx,ou=xxx,dc=example,dc=com"
return "", "", fmt.Errorf("please use Admin Name format like cn=xxx,ou=xxx,dc=example,dc=com")
}
if params["ou"] == "" {
return params["cn"], object.CasdoorOrganization, ""
return params["cn"], object.CasdoorOrganization, nil
}
return params["cn"], params["ou"], ""
return params["cn"], params["ou"], nil
}
func getNameAndOrgFromFilter(baseDN, filter string) (string, string, int) {
@ -50,7 +114,11 @@ func getNameAndOrgFromFilter(baseDN, filter string) (string, string, int) {
return "", "", ldap.LDAPResultInvalidDNSyntax
}
name, org, _ := getNameAndOrgFromDN(fmt.Sprintf("cn=%s,", getUsername(filter)) + baseDN)
name, org, err := getNameAndOrgFromDN(fmt.Sprintf("cn=%s,", getUsername(filter)) + baseDN)
if err != nil {
panic(err)
}
return name, org, ldap.LDAPResultSuccess
}
@ -83,6 +151,92 @@ func stringInSlice(value string, list []string) bool {
return false
}
func buildUserFilterCondition(filter interface{}) (builder.Cond, error) {
switch f := filter.(type) {
case message.FilterAnd:
conditions := make([]builder.Cond, len(f))
for i, v := range f {
cond, err := buildUserFilterCondition(v)
if err != nil {
return nil, err
}
conditions[i] = cond
}
return builder.And(conditions...), nil
case message.FilterOr:
conditions := make([]builder.Cond, len(f))
for i, v := range f {
cond, err := buildUserFilterCondition(v)
if err != nil {
return nil, err
}
conditions[i] = cond
}
return builder.Or(conditions...), nil
case message.FilterNot:
cond, err := buildUserFilterCondition(f.Filter)
if err != nil {
return nil, err
}
return builder.Not{cond}, nil
case message.FilterEqualityMatch:
field, err := getUserFieldFromAttribute(string(f.AttributeDesc()))
if err != nil {
return nil, err
}
return builder.Eq{field: string(f.AssertionValue())}, nil
case message.FilterPresent:
field, err := getUserFieldFromAttribute(string(f))
if err != nil {
return nil, err
}
return builder.NotNull{field}, nil
case message.FilterGreaterOrEqual:
field, err := getUserFieldFromAttribute(string(f.AttributeDesc()))
if err != nil {
return nil, err
}
return builder.Gte{field: string(f.AssertionValue())}, nil
case message.FilterLessOrEqual:
field, err := getUserFieldFromAttribute(string(f.AttributeDesc()))
if err != nil {
return nil, err
}
return builder.Lte{field: string(f.AssertionValue())}, nil
case message.FilterSubstrings:
field, err := getUserFieldFromAttribute(string(f.Type_()))
if err != nil {
return nil, err
}
var expr string
for _, substring := range f.Substrings() {
switch s := substring.(type) {
case message.SubstringInitial:
expr += string(s) + "%"
continue
case message.SubstringAny:
expr += string(s) + "%"
continue
case message.SubstringFinal:
expr += string(s)
continue
}
}
return builder.Expr(field+" LIKE ?", expr), nil
default:
return nil, fmt.Errorf("LDAP filter operation %#v not supported", f)
}
}
func buildSafeCondition(filter interface{}) builder.Cond {
condition, err := buildUserFilterCondition(filter)
if err != nil {
log.Printf("err = %v", err.Error())
return nil
}
return condition
}
func GetFilteredUsers(m *ldap.Message) (filteredUsers []*object.User, code int) {
var err error
r := m.GetSearchRequest()
@ -94,15 +248,14 @@ func GetFilteredUsers(m *ldap.Message) (filteredUsers []*object.User, code int)
if name == "*" && m.Client.IsOrgAdmin { // get all users from organization 'org'
if m.Client.IsGlobalAdmin && org == "*" {
filteredUsers, err = object.GetGlobalUsers()
filteredUsers, err = object.GetGlobalUsersWithFilter(buildSafeCondition(r.Filter()))
if err != nil {
panic(err)
}
return filteredUsers, ldap.LDAPResultSuccess
}
if m.Client.IsGlobalAdmin || org == m.Client.OrgName {
filteredUsers, err = object.GetUsers(org)
filteredUsers, err = object.GetUsersWithFilter(org, buildSafeCondition(r.Filter()))
if err != nil {
panic(err)
}
@ -144,7 +297,7 @@ func GetFilteredUsers(m *ldap.Message) (filteredUsers []*object.User, code int)
return nil, ldap.LDAPResultNoSuchObject
}
users, err := object.GetUsersByTag(org, name)
users, err := object.GetUsersByTagWithFilter(org, name, buildSafeCondition(r.Filter()))
if err != nil {
panic(err)
}
@ -178,24 +331,17 @@ func getUserPasswordWithType(user *object.User) string {
}
func getAttribute(attributeName string, user *object.User) message.AttributeValue {
switch attributeName {
case "cn":
return message.AttributeValue(user.Name)
case "uid":
return message.AttributeValue(user.Name)
case "displayname":
return message.AttributeValue(user.DisplayName)
case "email":
return message.AttributeValue(user.Email)
case "mail":
return message.AttributeValue(user.Email)
case "mobile":
return message.AttributeValue(user.Phone)
case "title":
return message.AttributeValue(user.Tag)
case "userPassword":
return message.AttributeValue(getUserPasswordWithType(user))
default:
v, ok := ldapAttributesMapping[attributeName]
if !ok {
return ""
}
return v.GetAttributeValue(user)
}
func getUserFieldFromAttribute(attributeName string) (string, error) {
v, ok := ldapAttributesMapping[attributeName]
if !ok {
return "", fmt.Errorf("attribute %s not supported", attributeName)
}
return v.GetField()
}

87
ldap/util_test.go Normal file
View File

@ -0,0 +1,87 @@
package ldap
import (
"testing"
"github.com/stretchr/testify/assert"
ber "github.com/go-asn1-ber/asn1-ber"
goldap "github.com/go-ldap/ldap/v3"
"github.com/lor00x/goldap/message"
"github.com/xorm-io/builder"
)
func args(exp ...interface{}) []interface{} {
return exp
}
func TestLdapFilterAsQuery(t *testing.T) {
scenarios := []struct {
description string
input string
expectedExpr string
expectedArgs []interface{}
}{
{"Should be SQL for FilterAnd", "(&(mail=2)(email=1))", "email=? AND email=?", args("2", "1")},
{"Should be SQL for FilterOr", "(|(mail=2)(email=1))", "email=? OR email=?", args("2", "1")},
{"Should be SQL for FilterNot", "(!(mail=2))", "NOT email=?", args("2")},
{"Should be SQL for FilterEqualityMatch", "(mail=2)", "email=?", args("2")},
{"Should be SQL for FilterPresent", "(mail=*)", "email IS NOT NULL", nil},
{"Should be SQL for FilterGreaterOrEqual", "(mail>=admin)", "email>=?", args("admin")},
{"Should be SQL for FilterLessOrEqual", "(mail<=admin)", "email<=?", args("admin")},
{"Should be SQL for FilterSubstrings", "(mail=admin*ex*c*m)", "email LIKE ?", args("admin%ex%c%m")},
}
for _, scenery := range scenarios {
t.Run(scenery.description, func(t *testing.T) {
searchRequest, err := buildLdapSearchRequest(scenery.input)
if err != nil {
assert.FailNow(t, "Unable to create searchRequest", err)
}
m, err := message.ReadLDAPMessage(message.NewBytes(0, searchRequest.Bytes()))
if err != nil {
assert.FailNow(t, "Unable to create searchRequest", err)
}
req := m.ProtocolOp().(message.SearchRequest)
cond, err := buildUserFilterCondition(req.Filter())
if err != nil {
assert.FailNow(t, "Unable to build condition", err)
}
expr, args, err := builder.ToSQL(cond)
if err != nil {
assert.FailNow(t, "Unable to build sql", err)
}
assert.Equal(t, scenery.expectedExpr, expr)
assert.Equal(t, scenery.expectedArgs, args)
})
}
}
func buildLdapSearchRequest(filter string) (*ber.Packet, error) {
packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request")
packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 1, "MessageID"))
pkt := ber.Encode(ber.ClassApplication, ber.TypeConstructed, goldap.ApplicationSearchRequest, nil, "Search Request")
pkt.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "Base DN"))
pkt.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, 0, "Scope"))
pkt.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, 0, "Deref Aliases"))
pkt.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 0, "Size Limit"))
pkt.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 0, "Time Limit"))
pkt.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, false, "Types Only"))
// compile and encode filter
filterPacket, err := goldap.CompileFilter(filter)
if err != nil {
return nil, err
}
pkt.AppendChild(filterPacket)
// encode attributes
attributesPacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attributes")
attributesPacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "*", "Attribute"))
pkt.AppendChild(attributesPacket)
packet.AppendChild(pkt)
return packet, nil
}

View File

@ -34,7 +34,6 @@ func main() {
object.InitFlag()
object.InitAdapter()
object.CreateTables()
object.DoMigration()
object.InitDb()
object.InitFromFile()
@ -59,7 +58,7 @@ func main() {
beego.InsertFilter("*", beego.BeforeRouter, routers.CorsFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.ApiFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.PrometheusFilter)
beego.InsertFilter("*", beego.AfterExec, routers.RecordMessage, false)
beego.InsertFilter("*", beego.BeforeRouter, routers.RecordMessage)
beego.BConfig.WebConfig.Session.SessionOn = true
beego.BConfig.WebConfig.Session.SessionName = "casdoor_session_id"

View File

@ -15,10 +15,10 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.0
version: 0.2.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "1.16.0"
appVersion: "1.17.0"

View File

@ -59,6 +59,9 @@ spec:
volumeMounts:
- name: config-volume
mountPath: /conf
{{ if .Values.extraContainersEnabled }}
{{- .Values.extraContainers | nindent 8 }}
{{- end }}
volumes:
- name: config-volume
projected:

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: ""
@ -107,3 +108,10 @@ nodeSelector: {}
tolerations: []
affinity: {}
# -- Optionally add extra sidecar containers.
extraContainersEnabled: false
extraContainers: ""
# extraContainers: |
# - name: ...
# image: ...

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,12 @@ func GetMaskedApplication(application *Application, userId string) *Application
if application.OrganizationObj.MasterPassword != "" {
application.OrganizationObj.MasterPassword = "***"
}
if application.OrganizationObj.DefaultPassword != "" {
application.OrganizationObj.DefaultPassword = "***"
}
if application.OrganizationObj.MasterVerificationCode != "" {
application.OrganizationObj.MasterVerificationCode = "***"
}
if application.OrganizationObj.PasswordType != "" {
application.OrganizationObj.PasswordType = "***"
}
@ -332,6 +348,34 @@ func GetMaskedApplications(applications []*Application, userId string) []*Applic
return applications
}
func GetAllowedApplications(applications []*Application, userId string) ([]*Application, error) {
if userId == "" || isUserIdGlobalAdmin(userId) {
return applications, nil
}
user, err := GetUser(userId)
if err != nil {
return nil, err
}
if user != nil && user.IsAdmin {
return applications, nil
}
res := []*Application{}
for _, application := range applications {
var allowed bool
allowed, err = CheckLoginPermission(userId, application)
if err != nil {
return nil, err
}
if allowed {
res = append(res, application)
}
}
return res, nil
}
func UpdateApplication(id string, application *Application) (bool, error) {
owner, name := util.GetOwnerAndNameFromId(id)
oldApplication, err := getApplication(owner, name)
@ -428,7 +472,7 @@ func (application *Application) GetId() string {
}
func (application *Application) IsRedirectUriValid(redirectUri string) bool {
redirectUris := append([]string{"http://localhost:", "http://127.0.0.1:", "http://casdoor-app"}, application.RedirectUris...)
redirectUris := append([]string{"http://localhost:", "https://localhost:", "http://127.0.0.1:", "http://casdoor-app"}, application.RedirectUris...)
for _, targetUri := range redirectUris {
targetUriRegex := regexp.MustCompile(targetUri)
if targetUriRegex.MatchString(redirectUri) || strings.Contains(redirectUri, targetUri) {

View File

@ -87,7 +87,7 @@ func GetGlobalCertsCount(field, value string) (int64, error) {
return session.Count(&Cert{})
}
func GetGlobleCerts() ([]*Cert, error) {
func GetGlobalCerts() ([]*Cert, error) {
certs := []*Cert{}
err := ormer.Engine.Desc("created_time").Find(&certs)
if err != nil {
@ -163,6 +163,12 @@ func UpdateCert(id string, cert *Cert) (bool, error) {
return false, err
}
}
err := cert.populateContent()
if err != nil {
return false, err
}
affected, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(cert)
if err != nil {
return false, err
@ -172,10 +178,9 @@ func UpdateCert(id string, cert *Cert) (bool, error) {
}
func AddCert(cert *Cert) (bool, error) {
if cert.Certificate == "" || cert.PrivateKey == "" {
certificate, privateKey := generateRsaKeys(cert.BitSize, cert.ExpireInYears, cert.Name, cert.Owner)
cert.Certificate = certificate
cert.PrivateKey = privateKey
err := cert.populateContent()
if err != nil {
return false, err
}
affected, err := ormer.Engine.Insert(cert)
@ -199,6 +204,20 @@ func (p *Cert) GetId() string {
return fmt.Sprintf("%s/%s", p.Owner, p.Name)
}
func (p *Cert) populateContent() error {
if p.Certificate == "" || p.PrivateKey == "" {
certificate, privateKey, err := generateRsaKeys(p.BitSize, p.ExpireInYears, p.Name, p.Owner)
if err != nil {
return err
}
p.Certificate = certificate
p.PrivateKey = privateKey
}
return nil
}
func getCertByApplication(application *Application) (*Cert, error) {
if application.Cert != "" {
return getCertByName(application.Cert)

View File

@ -142,7 +142,7 @@ func CheckUserSignup(application *Application, organization *Organization, form
return ""
}
func checkSigninErrorTimes(user *User, lang string) string {
func checkSigninErrorTimes(user *User, lang string) error {
if user.SigninWrongTimes >= SigninWrongTimesLimit {
lastSignWrongTime, _ := time.Parse(time.RFC3339, user.LastSigninWrongTime)
passedTime := time.Now().UTC().Sub(lastSignWrongTime)
@ -150,37 +150,39 @@ func checkSigninErrorTimes(user *User, lang string) string {
// deny the login if the error times is greater than the limit and the last login time is less than the duration
if minutes > 0 {
return fmt.Sprintf(i18n.Translate(lang, "check:You have entered the wrong password or code too many times, please wait for %d minutes and try again"), minutes)
return fmt.Errorf(i18n.Translate(lang, "check:You have entered the wrong password or code too many times, please wait for %d minutes and try again"), minutes)
}
// reset the error times
user.SigninWrongTimes = 0
UpdateUser(user.GetId(), user, []string{"signin_wrong_times"}, false)
_, err := UpdateUser(user.GetId(), user, []string{"signin_wrong_times"}, false)
return err
}
return ""
return nil
}
func CheckPassword(user *User, password string, lang string, options ...bool) string {
func CheckPassword(user *User, password string, lang string, options ...bool) error {
enableCaptcha := false
if len(options) > 0 {
enableCaptcha = options[0]
}
// check the login error times
if !enableCaptcha {
if msg := checkSigninErrorTimes(user, lang); msg != "" {
return msg
err := checkSigninErrorTimes(user, lang)
if err != nil {
return err
}
}
organization, err := GetOrganizationByUser(user)
if err != nil {
panic(err)
return err
}
if organization == nil {
return i18n.Translate(lang, "check:Organization does not exist")
return fmt.Errorf(i18n.Translate(lang, "check:Organization does not exist"))
}
passwordType := user.PasswordType
@ -191,19 +193,17 @@ func CheckPassword(user *User, password string, lang string, options ...bool) st
if credManager != nil {
if organization.MasterPassword != "" {
if credManager.IsPasswordCorrect(password, organization.MasterPassword, "", organization.PasswordSalt) {
resetUserSigninErrorTimes(user)
return ""
return resetUserSigninErrorTimes(user)
}
}
if credManager.IsPasswordCorrect(password, user.Password, user.PasswordSalt, organization.PasswordSalt) {
resetUserSigninErrorTimes(user)
return ""
return resetUserSigninErrorTimes(user)
}
return recordSigninErrorInfo(user, lang, enableCaptcha)
} else {
return fmt.Sprintf(i18n.Translate(lang, "check:unsupported password type: %s"), organization.PasswordType)
return fmt.Errorf(i18n.Translate(lang, "check:unsupported password type: %s"), organization.PasswordType)
}
}
@ -217,10 +217,10 @@ func CheckPasswordComplexity(user *User, password string) string {
return CheckPasswordComplexityByOrg(organization, password)
}
func checkLdapUserPassword(user *User, password string, lang string) string {
func checkLdapUserPassword(user *User, password string, lang string) error {
ldaps, err := GetLdaps(user.Owner)
if err != nil {
return err.Error()
return err
}
ldapLoginSuccess := false
@ -237,65 +237,73 @@ func checkLdapUserPassword(user *User, password string, lang string) string {
searchResult, err := conn.Conn.Search(searchReq)
if err != nil {
return err.Error()
conn.Close()
return err
}
if len(searchResult.Entries) == 0 {
conn.Close()
continue
}
if len(searchResult.Entries) > 1 {
return i18n.Translate(lang, "check:Multiple accounts with same uid, please check your ldap server")
conn.Close()
return fmt.Errorf(i18n.Translate(lang, "check:Multiple accounts with same uid, please check your ldap server"))
}
hit = true
dn := searchResult.Entries[0].DN
if err := conn.Conn.Bind(dn, password); err == nil {
if err = conn.Conn.Bind(dn, password); err == nil {
ldapLoginSuccess = true
conn.Close()
break
}
conn.Close()
}
if !ldapLoginSuccess {
if !hit {
return "user not exist"
return fmt.Errorf("user not exist")
}
return i18n.Translate(lang, "check:LDAP user name or password incorrect")
return fmt.Errorf(i18n.Translate(lang, "check:LDAP user name or password incorrect"))
}
return ""
return nil
}
func CheckUserPassword(organization string, username string, password string, lang string, options ...bool) (*User, string) {
func CheckUserPassword(organization string, username string, password string, lang string, options ...bool) (*User, error) {
enableCaptcha := false
if len(options) > 0 {
enableCaptcha = options[0]
}
user, err := GetUserByFields(organization, username)
if err != nil {
panic(err)
return nil, err
}
if user == nil || user.IsDeleted {
return nil, fmt.Sprintf(i18n.Translate(lang, "general:The user: %s doesn't exist"), util.GetId(organization, username))
return nil, fmt.Errorf(i18n.Translate(lang, "general:The user: %s doesn't exist"), util.GetId(organization, username))
}
if user.IsForbidden {
return nil, i18n.Translate(lang, "check:The user is forbidden to sign in, please contact the administrator")
return nil, fmt.Errorf(i18n.Translate(lang, "check:The user is forbidden to sign in, please contact the administrator"))
}
if user.Ldap != "" {
// ONLY for ldap users
if msg := checkLdapUserPassword(user, password, lang); msg != "" {
if msg == "user not exist" {
return nil, fmt.Sprintf(i18n.Translate(lang, "check:The user: %s doesn't exist in LDAP server"), username)
// only for LDAP users
err = checkLdapUserPassword(user, password, lang)
if err != nil {
if err.Error() == "user not exist" {
return nil, fmt.Errorf(i18n.Translate(lang, "check:The user: %s doesn't exist in LDAP server"), username)
}
return nil, msg
return nil, err
}
} else {
if msg := CheckPassword(user, password, lang, enableCaptcha); msg != "" {
return nil, msg
err = CheckPassword(user, password, lang, enableCaptcha)
if err != nil {
return nil, err
}
}
return user, ""
return user, nil
}
func CheckUserPermission(requestUserId, userId string, strict bool, lang string) (bool, error) {
@ -308,7 +316,7 @@ func CheckUserPermission(requestUserId, userId string, strict bool, lang string)
if userId != "" {
targetUser, err := GetUser(userId)
if err != nil {
panic(err)
return false, err
}
if targetUser == nil {
@ -351,8 +359,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,18 +369,28 @@ func CheckLoginPermission(userId string, application *Application) (bool, error)
return false, err
}
allowPermissionCount := 0
denyPermissionCount := 0
allowCount := 0
denyCount := 0
for _, permission := range permissions {
if !permission.IsEnabled || permission.ResourceType != "Application" || !permission.isResourceHit(application.Name) {
if !permission.IsEnabled || permission.State != "Approved" || permission.ResourceType != "Application" || !permission.isResourceHit(application.Name) {
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 +409,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,20 +36,23 @@ func isValidRealName(s string) bool {
return reRealName.MatchString(s)
}
func resetUserSigninErrorTimes(user *User) {
func resetUserSigninErrorTimes(user *User) error {
// if the password is correct and wrong times is not zero, reset the error times
if user.SigninWrongTimes == 0 {
return
return nil
}
user.SigninWrongTimes = 0
UpdateUser(user.GetId(), user, []string{"signin_wrong_times", "last_signin_wrong_time"}, false)
_, err := UpdateUser(user.GetId(), user, []string{"signin_wrong_times", "last_signin_wrong_time"}, false)
return err
}
func recordSigninErrorInfo(user *User, lang string, options ...bool) string {
func recordSigninErrorInfo(user *User, lang string, options ...bool) error {
enableCaptcha := false
if len(options) > 0 {
enableCaptcha = options[0]
}
// increase failed login count
if user.SigninWrongTimes < SigninWrongTimesLimit {
user.SigninWrongTimes++
@ -61,13 +64,18 @@ func recordSigninErrorInfo(user *User, lang string, options ...bool) string {
}
// update user
UpdateUser(user.GetId(), user, []string{"signin_wrong_times", "last_signin_wrong_time"}, false)
_, err := UpdateUser(user.GetId(), user, []string{"signin_wrong_times", "last_signin_wrong_time"}, false)
if err != nil {
return err
}
leftChances := SigninWrongTimesLimit - user.SigninWrongTimes
if leftChances == 0 && enableCaptcha {
return fmt.Sprint(i18n.Translate(lang, "check:password or code is incorrect"))
return fmt.Errorf(i18n.Translate(lang, "check:password or code is incorrect"))
} else if leftChances >= 0 {
return fmt.Sprintf(i18n.Translate(lang, "check:password or code is incorrect, you have %d remaining chances"), leftChances)
return fmt.Errorf(i18n.Translate(lang, "check:password or code is incorrect, you have %d remaining chances"), leftChances)
}
// don't show the chance error message if the user has no chance left
return fmt.Sprintf(i18n.Translate(lang, "check:You have entered the wrong password or code too many times, please wait for %d minutes and try again"), int(LastSignWrongTimeDuration.Minutes()))
return fmt.Errorf(i18n.Translate(lang, "check:You have entered the wrong password or code too many times, please wait for %d minutes and try again"), int(LastSignWrongTimeDuration.Minutes()))
}

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.Host, provider.Port, provider.DisableSsl)
emailProvider := email.GetEmailProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, provider.DisableSsl, provider.Endpoint, provider.Method)
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"
@ -247,23 +246,17 @@ func (enforcer *Enforcer) LoadModelCfg() error {
return nil
}
model, err := GetModel(enforcer.Model)
model, err := GetModelEx(enforcer.Model)
if err != nil {
return err
} else if model == nil {
return fmt.Errorf("the model: %s for enforcer: %s is not found", enforcer.Model, enforcer.GetId())
}
cfg, err := config.NewConfigFromText(model.ModelText)
enforcer.ModelCfg, err = getModelCfg(model)
if err != nil {
return err
}
enforcer.ModelCfg = make(map[string]string)
enforcer.ModelCfg["p"] = cfg.String("policy_definition::p")
if cfg.String("role_definition::g") != "" {
enforcer.ModelCfg["g"] = cfg.String("role_definition::g")
}
return nil
}

View File

@ -271,7 +271,9 @@ func GetGroupUsers(groupId string) ([]*User, error) {
users := []*User{}
owner, _ := util.GetOwnerAndNameFromId(groupId)
names, err := userEnforcer.GetUserNamesByGroupName(groupId)
if err != nil {
return nil, err
}
err = ormer.Engine.Where("owner = ?", owner).In("name", names).Find(&users)
if err != nil {
return nil, err
@ -303,6 +305,9 @@ func GroupChangeTrigger(oldName, newName string) error {
groups := []*Group{}
err = session.Where("parent_id = ?", oldName).Find(&groups)
if err != nil {
return err
}
for _, group := range groups {
group.ParentId = newName
_, err := session.ID(core.PK{group.Owner, group.Name}).Cols("parent_id").Update(group)

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"},
@ -396,15 +396,22 @@ func initBuiltInPermission() {
Name: "permission-built-in",
CreatedTime: util.GetCurrentTime(),
DisplayName: "Built-in Permission",
Description: "Built-in Permission",
Users: []string{"built-in/*"},
Groups: []string{},
Roles: []string{},
Domains: []string{},
Model: "model-built-in",
Adapter: "",
ResourceType: "Application",
Resources: []string{"app-built-in"},
Actions: []string{"Read", "Write", "Admin"},
Effect: "Allow",
IsEnabled: true,
Submitter: "admin",
Approver: "admin",
ApproveTime: util.GetCurrentTime(),
State: "Approved",
}
_, err = AddPermission(permission)
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

@ -100,6 +100,7 @@ func (l *LdapAutoSynchronizer) syncRoutine(ldap *Ldap, stopChan chan struct{}) e
users, err := conn.GetLdapUsers(ldap)
if err != nil {
conn.Close()
logs.Warning(fmt.Sprintf("autoSync failed for %s, error %s", ldap.Id, err))
continue
}
@ -111,6 +112,8 @@ func (l *LdapAutoSynchronizer) syncRoutine(ldap *Ldap, stopChan chan struct{}) e
} else {
logs.Info(fmt.Sprintf("ldap autosync success, %d new users, %d existing users", len(users)-len(existed), len(existed)))
}
conn.Close()
}
}

View File

@ -81,6 +81,17 @@ func (ldap *Ldap) GetLdapConn() (c *LdapConn, err error) {
return &LdapConn{Conn: conn, IsAD: isAD}, nil
}
func (l *LdapConn) Close() {
if l.Conn == nil {
return
}
err := l.Conn.Unbind()
if err != nil {
panic(err)
}
}
func isMicrosoftAD(Conn *goldap.Conn) (bool, error) {
SearchFilter := "(objectClass=*)"
SearchAttributes := []string{"vendorname", "vendorversion", "isGlobalCatalogReady", "forestFunctionality"}
@ -305,7 +316,7 @@ func SyncLdapUsers(owner string, syncUsers []LdapUser, ldapId string) (existUser
return nil, nil, err
}
name, err := syncUser.buildLdapUserName()
name, err := syncUser.buildLdapUserName(owner)
if err != nil {
return nil, nil, err
}
@ -354,10 +365,10 @@ func GetExistUuids(owner string, uuids []string) ([]string, error) {
return existUuids, nil
}
func (ldapUser *LdapUser) buildLdapUserName() (string, error) {
func (ldapUser *LdapUser) buildLdapUserName(owner string) (string, error) {
user := User{}
uidWithNumber := fmt.Sprintf("%s_%s", ldapUser.Uid, ldapUser.UidNumber)
has, err := ormer.Engine.Where("name = ? or name = ?", ldapUser.Uid, uidWithNumber).Get(&user)
has, err := ormer.Engine.Where("owner = ? and (name = ? or name = ?)", owner, ldapUser.Uid, uidWithNumber).Get(&user)
if err != nil {
return "", err
}

View File

@ -1,51 +0,0 @@
// 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/xorm-io/xorm/migrate"
type Migrator interface {
IsMigrationNeeded() bool
DoMigration() *migrate.Migration
}
func DoMigration() {
migrators := []Migrator{
&Migrator_1_101_0_PR_1083{},
&Migrator_1_235_0_PR_1530{},
&Migrator_1_240_0_PR_1539{},
&Migrator_1_314_0_PR_1841{},
// more migrators add here in chronological order...
}
migrations := []*migrate.Migration{}
for _, migrator := range migrators {
if migrator.IsMigrationNeeded() {
migrations = append(migrations, migrator.DoMigration())
}
}
options := &migrate.Options{
TableName: "migration",
IDColumnName: "id",
}
m := migrate.New(ormer.Engine, options, migrations)
err := m.Migrate()
if err != nil {
panic(err)
}
}

View File

@ -1,70 +0,0 @@
// 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 (
"strings"
"github.com/xorm-io/xorm"
"github.com/xorm-io/xorm/migrate"
)
type Migrator_1_101_0_PR_1083 struct{}
func (*Migrator_1_101_0_PR_1083) IsMigrationNeeded() bool {
exist1, _ := ormer.Engine.IsTableExist("model")
exist2, _ := ormer.Engine.IsTableExist("permission")
exist3, _ := ormer.Engine.IsTableExist("permission_rule")
if exist1 && exist2 && exist3 {
return true
}
return false
}
func (*Migrator_1_101_0_PR_1083) DoMigration() *migrate.Migration {
migration := migrate.Migration{
ID: "20230209MigratePermissionRule--Use V5 instead of V1 to store permissionID",
Migrate: func(engine *xorm.Engine) error {
models := []*Model{}
err := engine.Table("model").Find(&models, &Model{})
if err != nil {
panic(err)
}
isHit := false
for _, model := range models {
if strings.Contains(model.ModelText, "permission") {
// update model table
model.ModelText = strings.Replace(model.ModelText, "permission,", "", -1)
UpdateModel(model.GetId(), model)
isHit = true
}
}
if isHit {
// update permission_rule table
sql := "UPDATE `permission_rule`SET V0 = V1, V1 = V2, V2 = V3, V3 = V4, V4 = V5 WHERE V0 IN (SELECT CONCAT(owner, '/', name) AS permission_id FROM `permission`)"
_, err = engine.Exec(sql)
if err != nil {
return err
}
}
return err
},
}
return &migration
}

View File

@ -1,46 +0,0 @@
// 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 (
xormadapter "github.com/casdoor/xorm-adapter/v3"
"github.com/xorm-io/xorm"
"github.com/xorm-io/xorm/migrate"
)
type Migrator_1_235_0_PR_1530 struct{}
func (*Migrator_1_235_0_PR_1530) IsMigrationNeeded() bool {
exist, _ := ormer.Engine.IsTableExist("casbin_rule")
return exist
}
func (*Migrator_1_235_0_PR_1530) DoMigration() *migrate.Migration {
migration := migrate.Migration{
ID: "20221015CasbinRule--fill ptype field with p",
Migrate: func(engine *xorm.Engine) error {
_, err := engine.Cols("ptype").Update(&xormadapter.CasbinRule{
Ptype: "p",
})
return err
},
Rollback: func(engine *xorm.Engine) error {
return engine.DropTables(&xormadapter.CasbinRule{})
},
}
return &migration
}

View File

@ -1,141 +0,0 @@
// 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 (
"errors"
"github.com/xorm-io/xorm"
"github.com/xorm-io/xorm/migrate"
)
type Migrator_1_240_0_PR_1539 struct{}
func (*Migrator_1_240_0_PR_1539) IsMigrationNeeded() bool {
exist, _ := ormer.Engine.IsTableExist("session")
err := ormer.Engine.Table("session").Find(&[]*Session{})
if exist && err != nil {
return true
}
return false
}
func (*Migrator_1_240_0_PR_1539) DoMigration() *migrate.Migration {
migration := migrate.Migration{
ID: "20230211MigrateSession--Create a new field 'application' for table `session`",
Migrate: func(engine *xorm.Engine) error {
if alreadyCreated, _ := engine.IsTableExist("session_tmp"); alreadyCreated {
return errors.New("there is already a table called 'session_tmp', please rename or delete it for casdoor version migration and restart")
}
type oldSession 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"`
SessionId []string `json:"sessionId"`
}
tx := engine.NewSession()
defer tx.Close()
err := tx.Begin()
if err != nil {
return err
}
err = tx.Table("session_tmp").CreateTable(&Session{})
if err != nil {
return err
}
oldSessions := []*oldSession{}
newSessions := []*Session{}
err = tx.Table("session").Find(&oldSessions)
if err != nil {
return err
}
for _, oldSession := range oldSessions {
newApplication := "null"
if oldSession.Owner == "built-in" {
newApplication = "app-built-in"
}
newSessions = append(newSessions, &Session{
Owner: oldSession.Owner,
Name: oldSession.Name,
Application: newApplication,
CreatedTime: oldSession.CreatedTime,
SessionId: oldSession.SessionId,
})
}
rollbackFlag := false
_, err = tx.Table("session_tmp").Insert(newSessions)
count1, _ := tx.Table("session_tmp").Count()
count2, _ := tx.Table("session").Count()
if err != nil || count1 != count2 {
rollbackFlag = true
}
delete := &Session{
Application: "null",
}
_, err = tx.Table("session_tmp").Delete(*delete)
if err != nil {
rollbackFlag = true
}
if rollbackFlag {
tx.DropTable("session_tmp")
return errors.New("there is something wrong with data migration for table `session`, if there is a table called `session_tmp` not created by you in casdoor, please drop it, then restart anyhow")
}
err = tx.DropTable("session")
if err != nil {
return errors.New("fail to drop table `session` for casdoor, please drop it and rename the table `session_tmp` to `session` manually and restart")
}
// Already drop table `session`
// Can't find an api from xorm for altering table name
err = tx.Table("session").CreateTable(&Session{})
if err != nil {
return errors.New("there is something wrong with data migration for table `session`, please restart")
}
sessions := []*Session{}
tx.Table("session_tmp").Find(&sessions)
_, err = tx.Table("session").Insert(sessions)
if err != nil {
return errors.New("there is something wrong with data migration for table `session`, please drop table `session` and rename table `session_tmp` to `session` and restart")
}
err = tx.DropTable("session_tmp")
if err != nil {
return errors.New("fail to drop table `session_tmp` for casdoor, please drop it manually and restart")
}
tx.Commit()
return nil
},
}
return &migration
}

View File

@ -1,68 +0,0 @@
// 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/xorm-io/xorm"
"github.com/xorm-io/xorm/migrate"
)
type Migrator_1_314_0_PR_1841 struct{}
func (*Migrator_1_314_0_PR_1841) IsMigrationNeeded() bool {
count, err := ormer.Engine.Where("password_type=?", "").Count(&User{})
if err != nil {
// table doesn't exist
return false
}
return count > 100
}
func (*Migrator_1_314_0_PR_1841) DoMigration() *migrate.Migration {
migration := migrate.Migration{
ID: "20230515MigrateUser--Create a new field 'passwordType' for table `user`",
Migrate: func(engine *xorm.Engine) error {
tx := engine.NewSession()
defer tx.Close()
err := tx.Begin()
if err != nil {
return err
}
organizations := []*Organization{}
err = tx.Table("organization").Find(&organizations)
if err != nil {
return err
}
for _, organization := range organizations {
user := &User{PasswordType: organization.PasswordType}
_, err = tx.Where("owner = ?", organization.Name).Cols("password_type").Update(user)
if err != nil {
return err
}
}
tx.Commit()
return nil
},
}
return &migration
}

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

@ -51,22 +51,24 @@ type Organization struct {
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
WebsiteUrl string `xorm:"varchar(100)" json:"websiteUrl"`
Favicon string `xorm:"varchar(100)" json:"favicon"`
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
PasswordOptions []string `xorm:"varchar(100)" json:"passwordOptions"`
CountryCodes []string `xorm:"varchar(200)" json:"countryCodes"`
DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"`
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
Tags []string `xorm:"mediumtext" json:"tags"`
Languages []string `xorm:"varchar(255)" json:"languages"`
ThemeData *ThemeData `xorm:"json" json:"themeData"`
MasterPassword string `xorm:"varchar(100)" json:"masterPassword"`
InitScore int `json:"initScore"`
EnableSoftDeletion bool `json:"enableSoftDeletion"`
IsProfilePublic bool `json:"isProfilePublic"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
WebsiteUrl string `xorm:"varchar(100)" json:"websiteUrl"`
Favicon string `xorm:"varchar(100)" json:"favicon"`
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
PasswordOptions []string `xorm:"varchar(100)" json:"passwordOptions"`
CountryCodes []string `xorm:"varchar(200)" json:"countryCodes"`
DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"`
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
Tags []string `xorm:"mediumtext" json:"tags"`
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"`
MasterVerificationCode string `xorm:"varchar(100)" json:"masterVerificationCode"`
InitScore int `json:"initScore"`
EnableSoftDeletion bool `json:"enableSoftDeletion"`
IsProfilePublic bool `json:"isProfilePublic"`
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"`
@ -155,6 +157,12 @@ func GetMaskedOrganization(organization *Organization, errs ...error) (*Organiza
if organization.MasterPassword != "" {
organization.MasterPassword = "***"
}
if organization.DefaultPassword != "" {
organization.DefaultPassword = "***"
}
if organization.MasterVerificationCode != "" {
organization.MasterVerificationCode = "***"
}
return organization, nil
}
@ -202,9 +210,17 @@ 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")
}
if organization.MasterVerificationCode == "***" {
session.Omit("master_verification_code")
}
affected, err := session.Update(organization)
if err != nil {
return false, err

View File

@ -64,7 +64,6 @@ func InitConfig() {
InitAdapter()
CreateTables()
DoMigration()
}
func InitAdapter() {
@ -330,11 +329,6 @@ func (a *Ormer) createTable() {
panic(err)
}
err = a.Engine.Sync2(new(PermissionRule))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(xormadapter.CasbinRule))
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"
@ -48,17 +49,6 @@ type Permission struct {
State string `xorm:"varchar(100)" json:"state"`
}
type PermissionRule struct {
Ptype string `xorm:"varchar(100) index not null default ''" json:"ptype"`
V0 string `xorm:"varchar(100) index not null default ''" json:"v0"`
V1 string `xorm:"varchar(100) index not null default ''" json:"v1"`
V2 string `xorm:"varchar(100) index not null default ''" json:"v2"`
V3 string `xorm:"varchar(100) index not null default ''" json:"v3"`
V4 string `xorm:"varchar(100) index not null default ''" json:"v4"`
V5 string `xorm:"varchar(100) index not null default ''" json:"v5"`
Id string `xorm:"varchar(100) index not null default ''" json:"id"`
}
const builtInAvailableField = 5 // Casdoor built-in adapter, use V5 to filter permission, so has 5 available field
func GetPermissionCount(owner, field, value string) (int64, error) {
@ -112,11 +102,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
}
@ -126,9 +120,13 @@ func checkPermissionValid(permission *Permission) error {
return nil
}
groupingPolicies := getGroupingPolicies(permission)
groupingPolicies, err := getGroupingPolicies(permission)
if err != nil {
return err
}
if len(groupingPolicies) > 0 {
_, err := enforcer.AddGroupingPolicies(groupingPolicies)
_, err = enforcer.AddGroupingPolicies(groupingPolicies)
if err != nil {
return err
}
@ -149,14 +147,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 +190,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 +212,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 +293,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 {
@ -406,19 +465,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

@ -23,26 +23,27 @@ import (
"github.com/casbin/casbin/v2/log"
"github.com/casbin/casbin/v2/model"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util"
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 +61,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 {
@ -137,6 +138,16 @@ func getPolicies(permission *Permission) [][]string {
}
func getRolesInRole(roleId string, visited map[string]struct{}) ([]*Role, error) {
roleOwner, roleName := util.GetOwnerAndNameFromId(roleId)
if roleName == "*" {
roles, err := GetRoles(roleOwner)
if err != nil {
return []*Role{}, err
}
return roles, nil
}
role, err := GetRole(roleId)
if err != nil {
return []*Role{}, err
@ -162,7 +173,7 @@ func getRolesInRole(roleId string, visited map[string]struct{}) ([]*Role, error)
return roles, nil
}
func getGroupingPolicies(permission *Permission) [][]string {
func getGroupingPolicies(permission *Permission) ([][]string, error) {
var groupingPolicies [][]string
domainExist := len(permission.Domains) > 0
@ -170,12 +181,18 @@ func getGroupingPolicies(permission *Permission) [][]string {
for _, roleId := range permission.Roles {
visited := map[string]struct{}{}
if roleId == "*" {
roleId = util.GetId(permission.Owner, "*")
}
rolesInRole, err := getRolesInRole(roleId, visited)
if err != nil {
panic(err)
return nil, err
}
for _, role := range rolesInRole {
roleId := role.GetId()
roleId = role.GetId()
for _, subUser := range role.Users {
if domainExist {
for _, domain := range permission.Domains {
@ -198,75 +215,110 @@ func getGroupingPolicies(permission *Permission) [][]string {
}
}
return groupingPolicies
return groupingPolicies, nil
}
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)
_, err = enforcer.AddPolicies(policies)
return err
}
func removePolicies(permission *Permission) error {
enforcer, err := getPermissionEnforcer(permission)
if err != nil {
panic(err)
return err
}
}
func addGroupingPolicies(permission *Permission) {
enforcer := getPermissionEnforcer(permission)
groupingPolicies := getGroupingPolicies(permission)
if len(groupingPolicies) > 0 {
_, err := enforcer.AddGroupingPolicies(groupingPolicies)
if err != nil {
panic(err)
}
}
}
func removeGroupingPolicies(permission *Permission) {
enforcer := getPermissionEnforcer(permission)
groupingPolicies := getGroupingPolicies(permission)
if len(groupingPolicies) > 0 {
_, err := enforcer.RemoveGroupingPolicies(groupingPolicies)
if err != nil {
panic(err)
}
}
}
func removePolicies(permission *Permission) {
enforcer := getPermissionEnforcer(permission)
policies := getPolicies(permission)
_, err := enforcer.RemovePolicies(policies)
_, err = enforcer.RemovePolicies(policies)
return err
}
func addGroupingPolicies(permission *Permission) error {
enforcer, err := getPermissionEnforcer(permission)
if err != nil {
panic(err)
return err
}
groupingPolicies, err := getGroupingPolicies(permission)
if err != nil {
return err
}
if len(groupingPolicies) > 0 {
_, err = enforcer.AddGroupingPolicies(groupingPolicies)
if err != nil {
return err
}
}
return nil
}
func removeGroupingPolicies(permission *Permission) error {
enforcer, err := getPermissionEnforcer(permission)
if err != nil {
return err
}
groupingPolicies, err := getGroupingPolicies(permission)
if err != nil {
return err
}
if len(groupingPolicies) > 0 {
_, err = enforcer.RemoveGroupingPolicies(groupingPolicies)
if err != nil {
return err
}
}
return nil
}
type CasbinRequest = []interface{}
func Enforce(permission *Permission, request *CasbinRequest, permissionIds ...string) (bool, error) {
enforcer := getPermissionEnforcer(permission, permissionIds...)
enforcer, err := getPermissionEnforcer(permission, permissionIds...)
if err != nil {
return false, err
}
return enforcer.Enforce(*request...)
}
func BatchEnforce(permission *Permission, requests *[]CasbinRequest, permissionIds ...string) ([]bool, error) {
enforcer := getPermissionEnforcer(permission, permissionIds...)
enforcer, err := getPermissionEnforcer(permission, permissionIds...)
if err != nil {
return nil, err
}
return enforcer.BatchEnforce(*requests)
}
func getAllValues(userId string, fn func(enforcer *casbin.Enforcer) []string) []string {
func getAllValues(userId string, fn func(enforcer *casbin.Enforcer) []string) ([]string, error) {
permissions, _, err := getPermissionsAndRolesByUser(userId)
if err != nil {
panic(err)
return nil, err
}
for _, role := range GetAllRoles(userId) {
allRoles, err := GetAllRoles(userId)
if err != nil {
return nil, err
}
for _, role := range allRoles {
permissionsByRole, err := GetPermissionsByRole(role)
if err != nil {
panic(err)
return nil, err
}
permissions = append(permissions, permissionsByRole...)
@ -274,35 +326,40 @@ 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 {
func GetAllRoles(userId string) ([]string, error) {
roles, err := getRolesByUser(userId)
if err != nil {
panic(err)
return nil, err
}
var res []string
res := []string{}
for _, role := range roles {
res = append(res, role.Name)
}
return res
return res, nil
}
func GetBuiltInModel(modelText string) (model.Model, error) {
@ -330,17 +387,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

@ -17,6 +17,8 @@ package object
import (
"fmt"
"github.com/casdoor/casdoor/idp"
"github.com/casdoor/casdoor/pp"
"github.com/casdoor/casdoor/util"
@ -30,8 +32,8 @@ type Product struct {
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Image string `xorm:"varchar(100)" json:"image"`
Detail string `xorm:"varchar(255)" json:"detail"`
Description string `xorm:"varchar(100)" json:"description"`
Detail string `xorm:"varchar(1000)" json:"detail"`
Description string `xorm:"varchar(200)" json:"description"`
Tag string `xorm:"varchar(100)" json:"tag"`
Currency string `xorm:"varchar(100)" json:"currency"`
Price float64 `json:"price"`
@ -158,30 +160,28 @@ func (product *Product) getProvider(providerName string) (*Provider, error) {
return provider, nil
}
func BuyProduct(id string, user *User, providerName, pricingName, planName, host string) (*Payment, error) {
func BuyProduct(id string, user *User, providerName, pricingName, planName, host, paymentEnv string) (payment *Payment, attachInfo map[string]interface{}, err error) {
product, err := GetProduct(id)
if err != nil {
return nil, err
return nil, nil, err
}
if product == nil {
return nil, fmt.Errorf("the product: %s does not exist", id)
return nil, nil, fmt.Errorf("the product: %s does not exist", id)
}
provider, err := product.getProvider(providerName)
if err != nil {
return nil, err
return nil, nil, err
}
pProvider, err := GetPaymentProvider(provider)
if err != nil {
return nil, err
return nil, nil, err
}
owner := product.Owner
productName := product.Name
payerName := fmt.Sprintf("%s | %s", user.Name, user.DisplayName)
paymentName := fmt.Sprintf("payment_%v", util.GenerateTimeId())
productDisplayName := product.DisplayName
originFrontend, originBackend := getOriginFromHost(host)
returnUrl := fmt.Sprintf("%s/payments/%s/%s/result", originFrontend, owner, paymentName)
@ -191,26 +191,46 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
if pricingName != "" && planName != "" {
plan, err := GetPlan(util.GetId(owner, planName))
if err != nil {
return nil, err
return nil, nil, err
}
if plan == nil {
return nil, fmt.Errorf("the plan: %s does not exist", planName)
return nil, nil, fmt.Errorf("the plan: %s does not exist", planName)
}
sub := NewSubscription(owner, user.Name, plan.Name, paymentName, plan.Period)
_, err = AddSubscription(sub)
if err != nil {
return nil, err
return nil, nil, err
}
returnUrl = fmt.Sprintf("%s/buy-plan/%s/%s/result?subscription=%s", originFrontend, owner, pricingName, sub.Name)
}
}
// Create an OrderId and get the payUrl
payUrl, orderId, err := pProvider.Pay(providerName, productName, payerName, paymentName, productDisplayName, product.Price, product.Currency, returnUrl, notifyUrl)
// Create an order
payReq := &pp.PayReq{
ProviderName: providerName,
ProductName: product.Name,
PayerName: payerName,
PayerId: user.Id,
PaymentName: paymentName,
ProductDisplayName: product.DisplayName,
Price: product.Price,
Currency: product.Currency,
ReturnUrl: returnUrl,
NotifyUrl: notifyUrl,
PaymentEnv: paymentEnv,
}
// custom process for WeChat & WeChat Pay
if provider.Type == "WeChat Pay" {
payReq.PayerId, err = getUserExtraProperty(user, "WeChat", idp.BuildWechatOpenIdKey(provider.ClientId2))
if err != nil {
return nil, nil, err
}
}
payResp, err := pProvider.Pay(payReq)
if err != nil {
return nil, err
return nil, nil, err
}
// Create a Payment linked with Product and Order
payment := &Payment{
payment = &Payment{
Owner: product.Owner,
Name: paymentName,
CreatedTime: util.GetCurrentTime(),
@ -219,8 +239,8 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
Provider: provider.Name,
Type: provider.Type,
ProductName: productName,
ProductDisplayName: productDisplayName,
ProductName: product.Name,
ProductDisplayName: product.DisplayName,
Detail: product.Detail,
Tag: product.Tag,
Currency: product.Currency,
@ -228,10 +248,10 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
ReturnUrl: product.ReturnUrl,
User: user.Name,
PayUrl: payUrl,
PayUrl: payResp.PayUrl,
SuccessUrl: returnUrl,
State: pp.PaymentStateCreated,
OutOrderId: orderId,
OutOrderId: payResp.OrderId,
}
if provider.Type == "Dummy" {
@ -240,13 +260,13 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
affected, err := AddPayment(payment)
if err != nil {
return nil, err
return nil, nil, err
}
if !affected {
return nil, fmt.Errorf("failed to add payment: %s", util.StructToJson(payment))
return nil, nil, fmt.Errorf("failed to add payment: %s", util.StructToJson(payment))
}
return payment, err
return payment, payResp.AttachInfo, nil
}
func ExtendProductWithProviders(product *Product) error {

View File

@ -37,9 +37,9 @@ type Provider struct {
SubType string `xorm:"varchar(100)" json:"subType"`
Method string `xorm:"varchar(100)" json:"method"`
ClientId string `xorm:"varchar(200)" json:"clientId"`
ClientSecret string `xorm:"varchar(2000)" json:"clientSecret"`
ClientSecret string `xorm:"varchar(3000)" 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,7 +417,7 @@ func FromProviderToIdpInfo(ctx *context.Context, provider *Provider) *idp.Provid
providerInfo.ClientId = provider.ClientId2
providerInfo.ClientSecret = provider.ClientSecret2
}
} else if provider.Type == "AzureAD" {
} else if provider.Type == "AzureAD" || provider.Type == "ADFS" || provider.Type == "Okta" {
providerInfo.HostUrl = provider.Domain
}

View File

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

View File

@ -9,7 +9,6 @@ import (
)
// 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"`

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

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

View File

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

@ -26,6 +26,8 @@ func getSmsClient(provider *Provider) (sender.SmsClient, error) {
if provider.Type == sender.HuaweiCloud || provider.Type == sender.AzureACS {
client, err = sender.NewSmsClient(provider.Type, provider.ClientId, provider.ClientSecret, provider.SignName, provider.TemplateCode, provider.ProviderUrl, provider.AppId)
} else if provider.Type == "Custom HTTP SMS" {
client, err = newHttpSmsClient(provider.Endpoint, provider.Method, provider.Title)
} else {
client, err = sender.NewSmsClient(provider.Type, provider.ClientId, provider.ClientSecret, provider.SignName, provider.TemplateCode, provider.AppId)
}

76
object/sms_custom.go Normal file
View File

@ -0,0 +1,76 @@
// 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 (
"bytes"
"fmt"
"net/http"
"github.com/casdoor/casdoor/proxy"
)
type HttpSmsClient struct {
endpoint string
method string
paramName string
}
func newHttpSmsClient(endpoint string, method string, paramName string) (*HttpSmsClient, error) {
client := &HttpSmsClient{
endpoint: endpoint,
method: method,
paramName: paramName,
}
return client, nil
}
func (c *HttpSmsClient) SendMessage(param map[string]string, targetPhoneNumber ...string) error {
phoneNumber := targetPhoneNumber[0]
content := param["code"]
req, err := http.NewRequest(c.method, c.endpoint, bytes.NewBufferString(content))
if err != nil {
return err
}
if c.method == "POST" {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.PostForm = map[string][]string{
"phoneNumber": targetPhoneNumber,
c.paramName: {content},
}
} else if c.method == "GET" {
q := req.URL.Query()
q.Add("phoneNumber", phoneNumber)
q.Add(c.paramName, content)
req.URL.RawQuery = q.Encode()
} else {
return fmt.Errorf("HttpSmsClient's SendMessage() error, unsupported method: %s", c.method)
}
httpClient := proxy.DefaultHttpClient
resp, err := httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("HttpSmsClient's SendMessage() error, custom HTTP SMS request failed with status: %s", resp.Status)
}
return err
}

View File

@ -112,7 +112,10 @@ func getStorageProvider(provider *Provider, lang string) (oss.StorageInterface,
if provider.Domain == "" {
provider.Domain = storageProvider.GetEndpoint()
UpdateProvider(provider.GetId(), provider)
_, err := UpdateProvider(provider.GetId(), provider)
if err != nil {
return nil, err
}
}
return storageProvider, nil
@ -126,7 +129,12 @@ func uploadFile(provider *Provider, fullFilePath string, fileBuffer *bytes.Buffe
fileUrl, objectKey := GetUploadFileUrl(provider, fullFilePath, true)
_, err = storageProvider.Put(objectKey, fileBuffer)
objectKeyRefined := objectKey
if provider.Type == "Google Cloud Storage" {
objectKeyRefined = strings.TrimPrefix(objectKeyRefined, "/")
}
_, err = storageProvider.Put(objectKeyRefined, fileBuffer)
if err != nil {
return "", "", err
}

View File

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

View File

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

View File

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

View File

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

View File

@ -17,6 +17,7 @@ package object
import (
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"time"
@ -51,15 +52,17 @@ type Token struct {
Organization string `xorm:"varchar(100)" json:"organization"`
User string `xorm:"varchar(100)" json:"user"`
Code string `xorm:"varchar(100) index" json:"code"`
AccessToken string `xorm:"mediumtext" json:"accessToken"`
RefreshToken string `xorm:"mediumtext" json:"refreshToken"`
ExpiresIn int `json:"expiresIn"`
Scope string `xorm:"varchar(100)" json:"scope"`
TokenType string `xorm:"varchar(100)" json:"tokenType"`
CodeChallenge string `xorm:"varchar(100)" json:"codeChallenge"`
CodeIsUsed bool `json:"codeIsUsed"`
CodeExpireIn int64 `json:"codeExpireIn"`
Code string `xorm:"varchar(100) index" json:"code"`
AccessToken string `xorm:"mediumtext" json:"accessToken"`
RefreshToken string `xorm:"mediumtext" json:"refreshToken"`
AccessTokenHash string `xorm:"varchar(100) index" json:"accessTokenHash"`
RefreshTokenHash string `xorm:"varchar(100) index" json:"refreshTokenHash"`
ExpiresIn int `json:"expiresIn"`
Scope string `xorm:"varchar(100)" json:"scope"`
TokenType string `xorm:"varchar(100)" json:"tokenType"`
CodeChallenge string `xorm:"varchar(100)" json:"codeChallenge"`
CodeIsUsed bool `json:"codeIsUsed"`
CodeExpireIn int64 `json:"codeExpireIn"`
}
type TokenWrapper struct {
@ -141,6 +144,48 @@ func getTokenByCode(code string) (*Token, error) {
return nil, nil
}
func GetTokenByAccessToken(accessToken string) (*Token, error) {
token := Token{AccessTokenHash: getTokenHash(accessToken)}
existed, err := ormer.Engine.Get(&token)
if err != nil {
return nil, err
}
if !existed {
token = Token{AccessToken: accessToken}
existed, err = ormer.Engine.Get(&token)
if err != nil {
return nil, err
}
}
if !existed {
return nil, nil
}
return &token, nil
}
func GetTokenByRefreshToken(refreshToken string) (*Token, error) {
token := Token{RefreshTokenHash: getTokenHash(refreshToken)}
existed, err := ormer.Engine.Get(&token)
if err != nil {
return nil, err
}
if !existed {
token = Token{RefreshToken: refreshToken}
existed, err = ormer.Engine.Get(&token)
if err != nil {
return nil, err
}
}
if !existed {
return nil, nil
}
return &token, nil
}
func updateUsedByCode(token *Token) bool {
affected, err := ormer.Engine.Where("code=?", token.Code).Cols("code_is_used").Update(token)
if err != nil {
@ -159,6 +204,24 @@ func (token *Token) GetId() string {
return fmt.Sprintf("%s/%s", token.Owner, token.Name)
}
func getTokenHash(input string) string {
hash := sha256.Sum256([]byte(input))
res := hex.EncodeToString(hash[:])
if len(res) > 64 {
return res[:64]
}
return res
}
func (token *Token) popularHashes() {
if token.AccessTokenHash == "" && token.AccessToken != "" {
token.AccessTokenHash = getTokenHash(token.AccessToken)
}
if token.RefreshTokenHash == "" && token.RefreshToken != "" {
token.RefreshTokenHash = getTokenHash(token.RefreshToken)
}
}
func UpdateToken(id string, token *Token) (bool, error) {
owner, name := util.GetOwnerAndNameFromId(id)
if t, err := getToken(owner, name); err != nil {
@ -167,6 +230,8 @@ func UpdateToken(id string, token *Token) (bool, error) {
return false, nil
}
token.popularHashes()
affected, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(token)
if err != nil {
return false, err
@ -176,6 +241,8 @@ func UpdateToken(id string, token *Token) (bool, error) {
}
func AddToken(token *Token) (bool, error) {
token.popularHashes()
affected, err := ormer.Engine.Insert(token)
if err != nil {
return false, err
@ -194,18 +261,16 @@ func DeleteToken(token *Token) (bool, error) {
}
func ExpireTokenByAccessToken(accessToken string) (bool, *Application, *Token, error) {
token := Token{AccessToken: accessToken}
existed, err := ormer.Engine.Get(&token)
token, err := GetTokenByAccessToken(accessToken)
if err != nil {
return false, nil, nil, err
}
if !existed {
if token == nil {
return false, nil, nil, nil
}
token.ExpiresIn = 0
affected, err := ormer.Engine.ID(core.PK{token.Owner, token.Name}).Cols("expires_in").Update(&token)
affected, err := ormer.Engine.ID(core.PK{token.Owner, token.Name}).Cols("expires_in").Update(token)
if err != nil {
return false, nil, nil, err
}
@ -215,22 +280,7 @@ func ExpireTokenByAccessToken(accessToken string) (bool, *Application, *Token, e
return false, nil, nil, err
}
return affected != 0, application, &token, nil
}
func GetTokenByAccessToken(accessToken string) (*Token, error) {
// Check if the accessToken is in the database
token := Token{AccessToken: accessToken}
existed, err := ormer.Engine.Get(&token)
if err != nil {
return nil, err
}
if !existed {
return nil, nil
}
return &token, nil
return affected != 0, application, token, nil
}
func GetTokenByTokenAndApplication(token string, application string) (*Token, error) {
@ -432,16 +482,17 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
ErrorDescription: "client_id is invalid",
}, nil
}
if clientSecret != "" && application.ClientSecret != clientSecret {
return &TokenError{
Error: InvalidClient,
ErrorDescription: "client_secret is invalid",
}, nil
}
// check whether the refresh token is valid, and has not expired.
token := Token{RefreshToken: refreshToken}
existed, err := ormer.Engine.Get(&token)
if err != nil || !existed {
token, err := GetTokenByRefreshToken(refreshToken)
if err != nil || token == nil {
return &TokenError{
Error: InvalidGrant,
ErrorDescription: "refresh token is invalid, expired or revoked",
@ -452,6 +503,12 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
if err != nil {
return nil, err
}
if cert == nil {
return &TokenError{
Error: InvalidGrant,
ErrorDescription: fmt.Sprintf("cert: %s cannot be found", application.Cert),
}, nil
}
_, err = ParseJwtToken(refreshToken, cert)
if err != nil {
@ -460,6 +517,7 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
ErrorDescription: fmt.Sprintf("parse refresh token error: %s", err.Error()),
}, nil
}
// generate a new token
user, err := getUser(application.Organization, token.User)
if err != nil {
@ -477,6 +535,7 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
if err != nil {
return nil, err
}
newAccessToken, newRefreshToken, tokenName, err := generateJwtToken(application, user, "", scope, host)
if err != nil {
return &TokenError{
@ -504,7 +563,7 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
return nil, err
}
_, err = DeleteToken(&token)
_, err = DeleteToken(token)
if err != nil {
return nil, err
}
@ -517,7 +576,6 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
ExpiresIn: newToken.ExpiresIn,
Scope: newToken.Scope,
}
return tokenWrapper, nil
}
@ -621,25 +679,25 @@ func GetPasswordToken(application *Application, username string, password string
if err != nil {
return nil, nil, err
}
if user == nil {
return nil, &TokenError{
Error: InvalidGrant,
ErrorDescription: "the user does not exist",
}, nil
}
var msg string
if user.Ldap != "" {
msg = checkLdapUserPassword(user, password, "en")
err = checkLdapUserPassword(user, password, "en")
} else {
msg = CheckPassword(user, password, "en")
err = CheckPassword(user, password, "en")
}
if msg != "" {
if err != nil {
return nil, &TokenError{
Error: InvalidGrant,
ErrorDescription: "invalid username or password",
ErrorDescription: fmt.Sprintf("invalid username or password: %s", err.Error()),
}, nil
}
if user.IsForbidden {
return nil, &TokenError{
Error: InvalidGrant,
@ -729,13 +787,13 @@ func GetClientCredentialsToken(application *Application, clientSecret string, sc
// GetTokenByUser
// Implicit flow
func GetTokenByUser(application *Application, user *User, scope string, host string) (*Token, error) {
func GetTokenByUser(application *Application, user *User, scope string, nonce string, host string) (*Token, error) {
err := ExtendUserWithRolesAndPermissions(user)
if err != nil {
return nil, err
}
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", scope, host)
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, nonce, scope, host)
if err != nil {
return nil, err
}

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{
@ -286,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

@ -34,6 +34,12 @@ type Claims struct {
type UserShort struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
Id string `xorm:"varchar(100) index" json:"id"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Avatar string `xorm:"varchar(500)" json:"avatar"`
Email string `xorm:"varchar(100) index" json:"email"`
Phone string `xorm:"varchar(20) index" json:"phone"`
}
type UserWithoutThirdIdp struct {
@ -144,6 +150,12 @@ func getShortUser(user *User) *UserShort {
res := &UserShort{
Owner: user.Owner,
Name: user.Name,
Id: user.Id,
DisplayName: user.DisplayName,
Avatar: user.Avatar,
Email: user.Email,
Phone: user.Phone,
}
return res
}
@ -368,6 +380,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

@ -24,14 +24,14 @@ import (
"time"
)
func generateRsaKeys(bitSize int, expireInYears int, commonName string, organization string) (string, string) {
func generateRsaKeys(bitSize int, expireInYears int, commonName string, organization string) (string, string, error) {
// https://stackoverflow.com/questions/64104586/use-golang-to-get-rsa-key-the-same-way-openssl-genrsa
// https://stackoverflow.com/questions/43822945/golang-can-i-create-x509keypair-using-rsa-key
// Generate RSA key.
key, err := rsa.GenerateKey(rand.Reader, bitSize)
if err != nil {
panic(err)
return "", "", err
}
// Encode private key to PKCS#1 ASN.1 PEM.
@ -54,9 +54,10 @@ func generateRsaKeys(bitSize int, expireInYears int, commonName string, organiza
},
BasicConstraintsValid: true,
}
cert, err := x509.CreateCertificate(rand.Reader, &tml, &tml, &key.PublicKey, key)
if err != nil {
panic(err)
return "", "", err
}
// Generate a pem block with the certificate
@ -65,5 +66,5 @@ func generateRsaKeys(bitSize int, expireInYears int, commonName string, organiza
Bytes: cert,
})
return string(certPem), string(privateKeyPem)
return string(certPem), string(privateKeyPem), nil
}

View File

@ -23,7 +23,10 @@ import (
func TestGenerateRsaKeys(t *testing.T) {
fileId := "token_jwt_key"
certificate, privateKey := generateRsaKeys(4096, 20, "Casdoor Cert", "Casdoor Organization")
certificate, privateKey, err := generateRsaKeys(4096, 20, "Casdoor Cert", "Casdoor Organization")
if err != nil {
panic(err)
}
// Write certificate (aka certificate) to file.
util.WriteStringToPath(certificate, fmt.Sprintf("%s.pem", fileId))

View File

@ -22,6 +22,7 @@ import (
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/xorm-io/builder"
"github.com/xorm-io/core"
)
@ -50,6 +51,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"`
@ -230,6 +232,20 @@ func GetGlobalUsers() ([]*User, error) {
return users, nil
}
func GetGlobalUsersWithFilter(cond builder.Cond) ([]*User, error) {
users := []*User{}
session := ormer.Engine.Desc("created_time")
if cond != nil {
session = session.Where(cond)
}
err := session.Find(&users)
if err != nil {
return nil, err
}
return users, nil
}
func GetPaginationGlobalUsers(offset, limit int, field, value, sortField, sortOrder string) ([]*User, error) {
users := []*User{}
session := GetSessionForUser("", offset, limit, field, value, sortField, sortOrder)
@ -265,9 +281,27 @@ func GetUsers(owner string) ([]*User, error) {
return users, nil
}
func GetUsersByTag(owner string, tag string) ([]*User, error) {
func GetUsersWithFilter(owner string, cond builder.Cond) ([]*User, error) {
users := []*User{}
err := ormer.Engine.Desc("created_time").Find(&users, &User{Owner: owner, Tag: tag})
session := ormer.Engine.Desc("created_time")
if cond != nil {
session = session.Where(cond)
}
err := session.Find(&users, &User{Owner: owner})
if err != nil {
return nil, err
}
return users, nil
}
func GetUsersByTagWithFilter(owner string, tag string, cond builder.Cond) ([]*User, error) {
users := []*User{}
session := ormer.Engine.Desc("created_time")
if cond != nil {
session = session.Where(cond)
}
err := session.Find(&users, &User{Owner: owner, Tag: tag})
if err != nil {
return nil, err
}
@ -371,6 +405,24 @@ func GetUserByEmail(owner string, email string) (*User, error) {
}
}
func GetUserByEmailOnly(email string) (*User, error) {
if email == "" {
return nil, nil
}
user := User{Email: email}
existed, err := ormer.Engine.Get(&user)
if err != nil {
return nil, err
}
if existed {
return &user, nil
} else {
return nil, nil
}
}
func GetUserByPhone(owner string, phone string) (*User, error) {
if owner == "" || phone == "" {
return nil, nil
@ -389,6 +441,24 @@ func GetUserByPhone(owner string, phone string) (*User, error) {
}
}
func GetUserByPhoneOnly(phone string) (*User, error) {
if phone == "" {
return nil, nil
}
user := User{Phone: phone}
existed, err := ormer.Engine.Get(&user)
if err != nil {
return nil, err
}
if existed {
return &user, nil
} else {
return nil, nil
}
}
func GetUserByUserId(owner string, userId string) (*User, error) {
if owner == "" || userId == "" {
return nil, nil
@ -407,6 +477,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
@ -506,7 +594,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 {
@ -529,7 +617,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
if len(columns) == 0 {
columns = []string{
"owner", "display_name", "avatar",
"owner", "display_name", "avatar", "first_name", "last_name",
"location", "address", "country_code", "region", "language", "affiliation", "title", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application",
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts",
"signin_wrong_times", "last_signin_wrong_time", "groups", "access_key", "access_secret",
@ -546,6 +634,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 {
@ -584,7 +675,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 {
@ -606,6 +697,8 @@ func UpdateUserForAllFields(id string, user *User) (bool, error) {
}
}
user.UpdatedTime = util.GetCurrentTime()
affected, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(user)
if err != nil {
return false, err
@ -630,19 +723,26 @@ func AddUser(user *User) (bool, error) {
}
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" {
user.UpdateUserPassword(organization)
}
err := user.UpdateUserHash()
err = user.UpdateUserHash()
if err != nil {
return false, err
}
@ -676,9 +776,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])
@ -686,7 +785,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
}
@ -710,23 +809,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 {
@ -788,7 +886,7 @@ func (user *User) GetId() string {
}
func isUserIdGlobalAdmin(userId string) bool {
return strings.HasPrefix(userId, "built-in/")
return strings.HasPrefix(userId, "built-in/") || strings.HasPrefix(userId, "app/")
}
func ExtendUserWithRolesAndPermissions(user *User) (err error) {
@ -884,9 +982,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()
@ -923,7 +1021,10 @@ func GenerateIdForNewUser(application *Application) (string, error) {
lastUserId := -1
if lastUser != nil {
lastUserId = util.ParseInt(lastUser.Id)
lastUserId, err = util.ParseIntWithError(lastUser.Id)
if err != nil {
return util.GenerateId(), nil
}
}
res := strconv.Itoa(lastUserId + 1)

View File

@ -35,11 +35,7 @@ func downloadImage(client *http.Client, url string) (*bytes.Buffer, string, erro
resp, err := client.Do(req)
if err != nil {
fmt.Printf("downloadImage() error for url [%s]: %s\n", url, err.Error())
if strings.Contains(err.Error(), "EOF") || strings.Contains(err.Error(), "no such host") || strings.Contains(err.Error(), "did not properly respond after a period of time") {
return nil, "", nil
} else {
return nil, "", err
}
return nil, "", nil
}
defer resp.Body.Close()
@ -58,6 +54,8 @@ func downloadImage(client *http.Client, url string) (*bytes.Buffer, string, erro
if strings.Contains(contentType, "text/html") {
fileExtension = ".html"
} else if contentType == "image/vnd.microsoft.icon" {
fileExtension = ".ico"
} else {
fileExtensions, err := mime.ExtensionsByType(contentType)
if err != nil {

View File

@ -186,10 +186,47 @@ func parseSize(sizes string) []int {
return nil
}
var publicEmailDomains = map[string]int{
"gmail.com": 1,
"163.com": 1,
"qq.com": 1,
"yahoo.com": 1,
"hotmail.com": 1,
"outlook.com": 1,
"icloud.com": 1,
"mail.com": 1,
"aol.com": 1,
"live.com": 1,
"yandex.com": 1,
"yahoo.co.jp": 1,
"yahoo.co.in": 1,
"yahoo.co.uk": 1,
"me.com": 1,
"msn.com": 1,
"comcast.net": 1,
"sbcglobal.net": 1,
"verizon.net": 1,
"earthlink.net": 1,
"cox.net": 1,
"rediffmail.com": 1,
"in.com": 1,
"hotmail.co.uk": 1,
"hotmail.fr": 1,
"zoho.com": 1,
"gmx.com": 1,
"gmx.de": 1,
"gmx.net": 1,
}
func isPublicEmailDomain(domain string) bool {
_, exists := publicEmailDomains[domain]
return exists
}
func getFaviconFileBuffer(client *http.Client, email string) (*bytes.Buffer, string, error) {
tokens := strings.Split(email, "@")
domain := tokens[1]
if domain == "gmail.com" || domain == "163.com" || domain == "qq.com" {
if isPublicEmailDomain(domain) {
return nil, "", nil
}
@ -203,11 +240,11 @@ func getFaviconFileBuffer(client *http.Client, email string) (*bytes.Buffer, str
if buffer != nil {
faviconUrl, err = GetFaviconUrl(buffer.String())
if err != nil {
return nil, "", err
}
if !strings.HasPrefix(faviconUrl, "http") {
faviconUrl = util.UrlJoin(htmlUrl, faviconUrl)
fmt.Printf("getFaviconFileBuffer() error, faviconUrl is empty, error = %s\n", err.Error())
} else {
if !strings.HasPrefix(faviconUrl, "http") {
faviconUrl = util.UrlJoin(htmlUrl, faviconUrl)
}
}
}

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

@ -20,7 +20,10 @@ import (
"reflect"
"strings"
jsoniter "github.com/json-iterator/go"
"github.com/casdoor/casdoor/idp"
"github.com/casdoor/casdoor/util"
"github.com/xorm-io/core"
)
@ -110,6 +113,10 @@ func SetUserField(user *User, field string, value string) (bool, error) {
return false, err
}
if user != nil {
user.UpdatedTime = util.GetCurrentTime()
}
_, err = ormer.Engine.ID(core.PK{user.Owner, user.Name}).Cols("hash").Update(user)
if err != nil {
return false, err
@ -137,6 +144,25 @@ func setUserProperty(user *User, field string, value string) {
}
}
func getUserProperty(user *User, field string) string {
if user.Properties == nil {
return ""
}
return user.Properties[field]
}
func getUserExtraProperty(user *User, providerType, key string) (string, error) {
extraJson := getUserProperty(user, fmt.Sprintf("oauth_%s_extra", providerType))
if extraJson == "" {
return "", nil
}
extra := make(map[string]string)
if err := jsoniter.Unmarshal([]byte(extraJson), &extra); err != nil {
return "", err
}
return extra[key], nil
}
func SetUserOAuthProperties(organization *Organization, user *User, providerType string, userInfo *idp.UserInfo) (bool, error) {
if userInfo.Id != "" {
propertyName := fmt.Sprintf("oauth_%s_id", providerType)
@ -180,6 +206,27 @@ func SetUserOAuthProperties(organization *Organization, user *User, providerType
}
}
if userInfo.Extra != nil {
// Save extra info as json string
propertyName := fmt.Sprintf("oauth_%s_extra", providerType)
oldExtraJson := getUserProperty(user, propertyName)
extra := make(map[string]string)
if oldExtraJson != "" {
if err := jsoniter.Unmarshal([]byte(oldExtraJson), &extra); err != nil {
return false, err
}
}
for k, v := range userInfo.Extra {
extra[k] = v
}
newExtraJson, err := jsoniter.Marshal(extra)
if err != nil {
return false, err
}
setUserProperty(user, propertyName, string(newExtraJson))
}
return UpdateUserForAllFields(user.GetId(), user)
}

View File

@ -80,13 +80,14 @@ 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)
if organization.MasterVerificationCode != "" {
code = organization.MasterVerificationCode
}
// "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes."
content := fmt.Sprintf(provider.Content, code)
@ -106,15 +107,15 @@ 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
}
code := getRandomCode(6)
if organization.MasterVerificationCode != "" {
code = organization.MasterVerificationCode
}
if err := SendSms(provider, code, dest); err != nil {
return err
}
@ -164,7 +165,7 @@ func getVerificationRecord(dest string) (*VerificationRecord, error) {
return &record, nil
}
func CheckVerificationCode(dest, code, lang string) *VerifyResult {
func CheckVerificationCode(dest string, code string, lang string) *VerifyResult {
record, err := getVerificationRecord(dest)
if err != nil {
panic(err)
@ -191,32 +192,32 @@ func CheckVerificationCode(dest, code, lang string) *VerifyResult {
return &VerifyResult{VerificationSuccess, ""}
}
func DisableVerificationCode(dest string) (err error) {
func DisableVerificationCode(dest string) error {
record, err := getVerificationRecord(dest)
if record == nil || err != nil {
return
return nil
}
record.IsUsed = true
_, err = ormer.Engine.ID(core.PK{record.Owner, record.Name}).AllCols().Update(record)
return
return err
}
func CheckSigninCode(user *User, dest, code, lang string) string {
func CheckSigninCode(user *User, dest, code, lang string) error {
// check the login error times
if msg := checkSigninErrorTimes(user, lang); msg != "" {
return msg
err := checkSigninErrorTimes(user, lang)
if err != nil {
return err
}
result := CheckVerificationCode(dest, code, lang)
switch result.Code {
case VerificationSuccess:
resetUserSigninErrorTimes(user)
return ""
return resetUserSigninErrorTimes(user)
case wrongCodeError:
return recordSigninErrorInfo(user, lang)
default:
return result.Msg
return fmt.Errorf(result.Msg)
}
}

View File

@ -49,20 +49,24 @@ func NewAlipayPaymentProvider(appId string, appCertificate string, appPrivateKey
return pp, nil
}
func (pp *AlipayPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) {
func (pp *AlipayPaymentProvider) Pay(r *PayReq) (*PayResp, error) {
// pp.Client.DebugSwitch = gopay.DebugOn
bm := gopay.BodyMap{}
pp.Client.SetReturnUrl(returnUrl)
pp.Client.SetNotifyUrl(notifyUrl)
bm.Set("subject", joinAttachString([]string{productName, productDisplayName, providerName}))
bm.Set("out_trade_no", paymentName)
bm.Set("total_amount", priceFloat64ToString(price))
pp.Client.SetReturnUrl(r.ReturnUrl)
pp.Client.SetNotifyUrl(r.NotifyUrl)
bm.Set("subject", joinAttachString([]string{r.ProductName, r.ProductDisplayName, r.ProviderName}))
bm.Set("out_trade_no", r.PaymentName)
bm.Set("total_amount", priceFloat64ToString(r.Price))
payUrl, err := pp.Client.TradePagePay(context.Background(), bm)
if err != nil {
return "", "", err
return nil, err
}
return payUrl, paymentName, nil
payResp := &PayResp{
PayUrl: payUrl,
OrderId: r.PaymentName,
}
return payResp, nil
}
func (pp *AlipayPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {

View File

@ -21,8 +21,10 @@ func NewDummyPaymentProvider() (*DummyPaymentProvider, error) {
return pp, nil
}
func (pp *DummyPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) {
return returnUrl, "", nil
func (pp *DummyPaymentProvider) Pay(r *PayReq) (*PayResp, error) {
return &PayResp{
PayUrl: r.ReturnUrl,
}, nil
}
func (pp *DummyPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {

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