Compare commits

..

76 Commits

Author SHA1 Message Date
80f01074fa fix: 'restart always' instruction is in wrong position (#468) 2022-02-03 21:27:27 +08:00
d943d5cc61 fix: oauth params null value error (#465)
Signed-off-by: Steve0x2a <stevesough@gmail.com>
2022-01-30 17:58:54 +08:00
19ed35f964 Add getOriginFromHost(). 2022-01-29 23:43:25 +08:00
5757021e87 fix: prohibit cross-origin access (#462) 2022-01-29 21:52:04 +08:00
259a4e1307 Fix Docker compose on Apple M1 Chip 2022-01-29 10:42:33 +08:00
034d822dd5 Fix empty UserInfo ID in wecom_internal.go 2022-01-29 10:27:45 +08:00
a8502d1173 Fix GetIdProvider() bug. 2022-01-29 09:52:48 +08:00
3c2f7b7fc8 feat: add protection against attacks (#460)
Signed-off-by: 0x2a <stevesough@gmail.com>
2022-01-29 00:32:57 +08:00
fbc73de3bb Support WeCom Internal sub type. 2022-01-28 23:57:54 +08:00
479daf4fa4 Improve code format. 2022-01-28 17:45:41 +08:00
d129202b95 fix: no database check when using accessToken (#461)
Signed-off-by: 0x2a <stevesough@gmail.com>
2022-01-28 15:07:42 +08:00
c1f553440e feat: add wecom internal support (#452)
Signed-off-by: 0x2a <stevesough@gmail.com>
2022-01-28 12:44:45 +08:00
7dcae2d183 fix: add k8s deployments example (#446) 2022-01-28 09:25:25 +08:00
5ec0c7a890 fix: fix the SQL injection vulnerability in field filter (#442)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-01-26 19:36:36 +08:00
051752340d feat: add userinfo endpoint (#447)
* feat: add userinfo endpoint

Signed-off-by: 0x2a <stevesough@gmail.com>

* feat: add scope support

Signed-off-by: 0x2a <stevesough@gmail.com>

* fix: modify the endpoint of discovery

Signed-off-by: 0x2a <stevesough@gmail.com>
2022-01-26 11:56:01 +08:00
c87c001da3 fix: fix the permission page can not open when initial a new project (#449) 2022-01-25 19:39:04 +08:00
12bc419659 fix: baidu's display name error (#440)
Signed-off-by: Steve0x2a <stevesough@gmail.com>
2022-01-23 20:32:44 +08:00
d5f18f2d64 Support SilentSignin. 2022-01-23 13:02:55 +08:00
02c06bc93c feat: add baidu support as idp (#438)
* feat: add baidu support as idp

Signed-off-by: Steve0x2a <stevesough@gmail.com>

* fix: add license

Signed-off-by: Steve0x2a <stevesough@gmail.com>
2022-01-22 19:36:44 +08:00
40aa9a4693 fix: remove wait-for-it (#436) 2022-01-22 15:50:48 +08:00
630b84f534 feat: add PKCE support (#434)
* feat: add PKCE support

Signed-off-by: Steve0x2a <stevesough@gmail.com>

* fix: error output when challenge is empty

Signed-off-by: Steve0x2a <stevesough@gmail.com>
2022-01-21 09:29:19 +08:00
339a85e4b0 Support tableNamePrefix in authz table. 2022-01-20 14:20:37 +08:00
c22ab44894 Update import path. 2022-01-20 14:11:46 +08:00
c3fb48f473 fix: Add a configuration that can set the table prefix. There is no prefix by default (#432)
* fix: Add a configuration that can set the table prefix. There is no prefix by default

* fix: Add a configuration that can set the table prefix. There is no prefix by default
2022-01-20 13:50:20 +08:00
a111fd672c fix: Add the configuration of whether to print SQL. The default value is false (#429) 2022-01-19 16:58:45 +08:00
9fd175eefd Add ErrorText to syncer. 2022-01-17 21:17:42 +08:00
d9bcce9485 Start syncer dynamically. 2022-01-17 20:09:29 +08:00
d183b9eca9 Change syncer.SyncInterval to second-level. 2022-01-17 19:27:52 +08:00
f24d9ae251 Don't update password in AddUsers(). 2022-01-17 13:26:30 +08:00
030c1caa50 Fix bug in IsGlobalAdmin(). 2022-01-15 23:23:14 +08:00
cee2c608a2 Disable PasswordModal when needed in user edit page. 2022-01-15 21:34:37 +08:00
82d0e895e0 Update users and roles when org is changed in permission edit page. 2022-01-15 21:11:47 +08:00
dee9bac110 Show signupApplication in user edit page. 2022-01-15 18:29:10 +08:00
e7a6986b62 Add index to User.Id 2022-01-14 17:42:11 +08:00
b91b4aec91 Allow global admin to modify username. 2022-01-13 23:20:10 +08:00
fe48c38bc6 feat: support minio (#418)
Signed-off-by: abingcbc <abingcbc626@gmail.com>
2022-01-13 21:48:00 +08:00
1be777c08f Fix GetUserByField()'s bug for idCard. 2022-01-13 12:48:15 +08:00
8d54bfad8a feat: support create database via cmd line (#417) 2022-01-13 11:35:13 +08:00
728fe11a3c Refactor CountDownInput. 2022-01-07 20:34:27 +08:00
69e0f4e40d Add idCard in GetUserByFields(). 2022-01-04 19:52:29 +08:00
ba32a45693 Add ClaimsShort to fix the JWT user's owner and name empty bug. 2022-01-03 22:54:27 +08:00
a4d83af768 refactor: New Crowdin translations by Github Action (#412)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-01-02 23:55:06 +08:00
5b8f6415d9 Add Gitter badge. 2022-01-02 23:34:24 +08:00
5389cb435c Fix Crowdin sync. 2022-01-02 23:16:08 +08:00
9b6131890c Add permission page. 2022-01-01 16:28:33 +08:00
ffc0a0e0d5 fix: refresh_token endpoint does not work (#410)
Signed-off-by: 0x2a <stevesough@gmail.com>
2022-01-01 15:20:49 +08:00
ff22bf507f Add role page. 2022-01-01 15:11:16 +08:00
2d4103d751 Add isUserExtended to webhook. 2022-01-01 11:16:37 +08:00
4611b59b08 Add webhook edit page's preview. 2022-01-01 10:58:39 +08:00
445d3c9d0e feat: support spring security oauth2 (#408)
Signed-off-by: abingcbc <abingcbc626@gmail.com>
2021-12-31 19:55:34 +08:00
dbebd1846f Fix code sign-in link hiding. 2021-12-31 13:36:10 +08:00
2fcc8f5bfe Support app user in SetPassword(). 2021-12-31 13:32:18 +08:00
4b65320a96 Support user uploading via xlsx. 2021-12-31 13:00:35 +08:00
5e8897e41b Make cert work. 2021-12-31 10:02:06 +08:00
ba1646a0c3 Add cert pages. 2021-12-31 00:36:36 +08:00
c1cd187558 Improve UI. 2021-12-29 20:50:49 +08:00
519fd655cf Add GetMaskedApplication() and GetMaskedApplications(). 2021-12-29 20:04:39 +08:00
377ac05928 Don't clear session in SetPassword(). 2021-12-28 23:07:09 +08:00
4f124ff140 fix: refresh token does not return (#401)
Signed-off-by: 0x2a <stevesough@gmail.com>
2021-12-28 19:44:17 +08:00
d5f802ec7d Support IdCard in signup page. 2021-12-28 17:48:24 +08:00
64d3b7e87f Add EnableSigninSession to app. 2021-12-28 17:15:47 +08:00
dfce1bd74c Remove adapter.createDatabase(). 2021-12-27 22:49:54 +08:00
067ae5448f fix: idp using goth shows wrong display name (#398)
* fix: adjust the accessToken field

Signed-off-by: 0x2a <stevesough@gmail.com>

* fix: missing name and owner

Signed-off-by: 0x2a <stevesough@gmail.com>

* fix: get wrong display name

Signed-off-by: 0x2a <stevesough@gmail.com>
2021-12-27 18:55:25 +08:00
9943e3c316 Add isEnabled to webhook. 2021-12-26 21:03:12 +08:00
0c665edcbc Add headers to webhook. 2021-12-26 20:43:32 +08:00
5015bf1c7d Add method to webhook. 2021-12-26 19:56:02 +08:00
2ec947d488 Update project_id. 2021-12-26 09:49:00 +08:00
10a85f2386 feat: add server-side search, filter and sorter for all pages (#388)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2021-12-25 10:55:10 +08:00
0d13512eb1 docs: add docs for all-in-one image (#396) 2021-12-25 00:33:35 +08:00
b60856be5e Fix bug in updateUserForOriginalFields(). 2021-12-25 00:19:17 +08:00
4b4c9be71b Fix other bugs in syncer. 2021-12-25 00:05:54 +08:00
e79e3c36d0 Support more DBs in syncer. 2021-12-24 23:23:06 +08:00
cc8c9b32ef Fix missing options in renderOptions(). 2021-12-24 14:09:12 +08:00
efdcb3279d Fix null address bug in getOriginalUsersFromMap(). 2021-12-24 01:03:50 +08:00
3818492065 Fix updating old DB code. 2021-12-24 00:36:53 +08:00
f4890a6a22 Improve syncer.initAdapter(). 2021-12-23 21:28:40 +08:00
144 changed files with 7503 additions and 2385 deletions

View File

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

View File

@ -20,7 +20,7 @@ COPY --from=FRONT /web/build /web/build
CMD chmod 777 /tmp && service mariadb start&&\
if [ "${MYSQL_ROOT_PASSWORD}" = "" ] ;then MYSQL_ROOT_PASSWORD=123456 ; fi&&\
mysqladmin -u root password ${MYSQL_ROOT_PASSWORD} &&\
./wait-for-it localhost:3306 -- ./server
./wait-for-it localhost:3306 -- ./server --createDatabase=true
FROM alpine:latest
@ -32,5 +32,5 @@ COPY --from=BACK /go/src/casdoor/ ./
COPY --from=BACK /usr/bin/wait-for-it ./
RUN mkdir -p web/build && apk add --no-cache bash coreutils
COPY --from=FRONT /web/build /web/build
CMD ./wait-for-it db:3306 -- ./server
CMD ./server

View File

@ -7,10 +7,10 @@
<a href="https://hub.docker.com/r/casbin/casdoor">
<img alt="docker pull casbin/casdoor" src="https://img.shields.io/docker/pulls/casbin/casdoor.svg">
</a>
<a href="https://github.com/casbin/casdoor/actions/workflows/build.yml">
<a href="https://github.com/casdoor/casdoor/actions/workflows/build.yml">
<img alt="GitHub Workflow Status (branch)" src="https://github.com/casbin/jcasbin/workflows/build/badge.svg?style=flat-square">
</a>
<a href="https://github.com/casbin/casdoor/releases/latest">
<a href="https://github.com/casdoor/casdoor/releases/latest">
<img alt="GitHub Release" src="https://img.shields.io/github/v/release/casbin/casdoor.svg">
</a>
<a href="https://hub.docker.com/repository/docker/casbin/casdoor">
@ -19,21 +19,27 @@
</p>
<p align="center">
<a href="https://goreportcard.com/report/github.com/casbin/casdoor">
<img alt="Go Report Card" src="https://goreportcard.com/badge/github.com/casbin/casdoor?style=flat-square">
<a href="https://goreportcard.com/report/github.com/casdoor/casdoor">
<img alt="Go Report Card" src="https://goreportcard.com/badge/github.com/casdoor/casdoor?style=flat-square">
</a>
<a href="https://github.com/casbin/casdoor/blob/master/LICENSE">
<a href="https://github.com/casdoor/casdoor/blob/master/LICENSE">
<img src="https://img.shields.io/github/license/casbin/casdoor?style=flat-square" alt="license">
</a>
<a href="https://github.com/casbin/casdoor/issues">
<a href="https://github.com/casdoor/casdoor/issues">
<img alt="GitHub issues" src="https://img.shields.io/github/issues/casbin/casdoor?style=flat-square">
</a>
<a href="#">
<img alt="GitHub stars" src="https://img.shields.io/github/stars/casbin/casdoor?style=flat-square">
</a>
<a href="https://github.com/casbin/casdoor/network">
<a href="https://github.com/casdoor/casdoor/network">
<img alt="GitHub forks" src="https://img.shields.io/github/forks/casbin/casdoor?style=flat-square">
</a>
<a href="https://crowdin.com/project/casdoor-site">
<img alt="Crowdin" src="https://badges.crowdin.net/casdoor-site/localized.svg">
</a>
<a href="https://gitter.im/casbin/casdoor">
<img alt="Gitter" src="https://badges.gitter.im/casbin/casdoor.svg">
</a>
</p>
## Online demo
@ -41,21 +47,20 @@
Deployed site: https://door.casbin.com/
## Quick Start
Run your own casdoor program in a few minutes:smiley:
Run your own casdoor program in a few minutes.
### Download
There are two methods, get code via go subcommand `get`:
```shell
go get github.com/casbin/casdoor
go get github.com/casdoor/casdoor
```
or `git`:
```bash
git clone https://github.com/casbin/casdoor
git clone https://github.com/casdoor/casdoor
```
Finally, change directory:
@ -117,9 +122,26 @@ go build main.go && sudo ./main
### Docker
Casdoor provide 2 kinds of image:
- casbin/casdoor-all-in-one, in which casdoor binary, a mysql database and all necessary configurations are packed up. This image is for new user to have a trial on casdoor quickly. **With this image you can start a casdoor immediately with one single command (or two) without any complex configuration**. **Note: we DO NOT recommend you to use this image in productive environment**
- casbin/casdoor: normal & graceful casdoor image with only casdoor and environment installed.
This method requires [docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/) to be installed first.
#### Simple configuration
### Start casdoor with casbin/casdoor-all-in-one
if the image is not pulled, pull it from dockerhub
```shell
docker pull casbin/casdoor-all-in-one
```
Start it with
```shell
docker run -p 8000:8000 casbin/casdoor-all-in-one
```
Now you can visit http://localhost:8000 and have a try. Default account and password is 'admin' and '123'. Go for it!
### Start casdoor with casbin/casdoor
#### modify the configurations
For the convenience of your first attempt, docker-compose.yml contains commands to start a database via docker.
Thus edit `conf/app.conf` to point out the location of database(db:3306), modify `dataSourceName` to the fixed content:
@ -138,14 +160,6 @@ docker-compose up
That's it! Try to visit http://localhost:8000/. :small_airplane:
### Docker Hub
This method requires [docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/) to be installed first.
```bash
docker pull casbin/casdoor
```
## Detailed documentation
We also provide a complete [document](https://casdoor.org/) as a reference.
@ -168,5 +182,5 @@ If you are contributing to casdoor, please note that we use [Crowdin](https://cr
## License
[Apache-2.0](https://github.com/casbin/casdoor/blob/master/LICENSE)
[Apache-2.0](https://github.com/casdoor/casdoor/blob/master/LICENSE)

View File

@ -18,8 +18,8 @@ import (
"github.com/astaxie/beego"
"github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v2/model"
"github.com/casbin/casdoor/conf"
xormadapter "github.com/casbin/xorm-adapter/v2"
"github.com/casdoor/casdoor/conf"
stringadapter "github.com/qiangmzsx/string-adapter/v2"
)
@ -28,7 +28,8 @@ var Enforcer *casbin.Enforcer
func InitAuthz() {
var err error
a, err := xormadapter.NewAdapter(beego.AppConfig.String("driverName"), conf.GetBeegoConfDataSourceName()+beego.AppConfig.String("dbName"), true)
tableNamePrefix := beego.AppConfig.String("tableNamePrefix")
a, err := xormadapter.NewAdapterWithTableName(beego.AppConfig.String("driverName"), conf.GetBeegoConfDataSourceName()+beego.AppConfig.String("dbName"), "casbin_rule", tableNamePrefix, true)
if err != nil {
panic(err)
}
@ -79,7 +80,9 @@ p, *, *, POST, /api/login, *, *
p, *, *, GET, /api/get-app-login, *, *
p, *, *, POST, /api/logout, *, *
p, *, *, GET, /api/get-account, *, *
p, *, *, GET, /api/userinfo, *, *
p, *, *, POST, /api/login/oauth/access_token, *, *
p, *, *, POST, /api/login/oauth/refresh_token, *, *
p, *, *, GET, /api/get-application, *, *
p, *, *, GET, /api/get-users, *, *
p, *, *, GET, /api/get-user, *, *

View File

@ -6,6 +6,8 @@ copyrequestbody = true
driverName = mysql
dataSourceName = root:123456@tcp(localhost:3306)/
dbName = casdoor
tableNamePrefix =
showSql = false
redisEndpoint =
defaultStorageProvider =
isCloudIntranet = false
@ -14,4 +16,4 @@ httpProxy = "127.0.0.1:10808"
verificationCodeTimeout = 10
initScore = 2000
logPostOnly = true
origin = "https://door.casbin.com"
origin =

View File

@ -18,9 +18,11 @@ import (
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/util"
"github.com/astaxie/beego"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
const (
@ -38,6 +40,7 @@ type RequestForm struct {
Email string `json:"email"`
Phone string `json:"phone"`
Affiliation string `json:"affiliation"`
IdCard string `json:"idCard"`
Region string `json:"region"`
Application string `json:"application"`
@ -61,10 +64,23 @@ type Response struct {
Status string `json:"status"`
Msg string `json:"msg"`
Sub string `json:"sub"`
Name string `json:"name"`
Data interface{} `json:"data"`
Data2 interface{} `json:"data2"`
}
type Userinfo struct {
Sub string `json:"sub"`
Iss string `json:"iss"`
Aud string `json:"aud"`
Name string `json:"name,omitempty"`
DisplayName string `json:"preferred_username,omitempty"`
Email string `json:"email,omitempty"`
Avatar string `json:"picture,omitempty"`
Address string `json:"address,omitempty"`
Phone string `json:"phone,omitempty"`
}
type HumanCheck struct {
Type string `json:"type"`
AppKey string `json:"appKey"`
@ -151,6 +167,7 @@ func (c *ApiController) Signup() {
Phone: form.Phone,
Address: []string{},
Affiliation: form.Affiliation,
IdCard: form.IdCard,
Region: form.Region,
Score: getInitScore(),
IsAdmin: false,
@ -220,6 +237,7 @@ func (c *ApiController) GetAccount() {
resp := Response{
Status: "ok",
Sub: user.Id,
Name: user.Name,
Data: user,
Data2: organization,
}
@ -227,6 +245,47 @@ func (c *ApiController) GetAccount() {
c.ServeJSON()
}
// UserInfo
// @Title UserInfo
// @Tag Account API
// @Description return user information according to OIDC standards
// @Success 200 {object} controllers.Userinfo The Response object
// @router /userinfo [get]
func (c *ApiController) GetUserinfo() {
userId, ok := c.RequireSignedIn()
if !ok {
return
}
user := object.GetUser(userId)
if user == nil {
c.ResponseError(fmt.Sprintf("The user: %s doesn't exist", userId))
return
}
scope, aud := c.GetSessionOidc()
iss := beego.AppConfig.String("origin")
resp := Userinfo{
Sub: user.Id,
Iss: iss,
Aud: aud,
}
if strings.Contains(scope, "profile") {
resp.Name = user.Name
resp.DisplayName = user.DisplayName
resp.Avatar = user.Avatar
}
if strings.Contains(scope, "email") {
resp.Email = user.Email
}
if strings.Contains(scope, "address") {
resp.Address = user.Location
}
if strings.Contains(scope, "phone") {
resp.Phone = user.Phone
}
c.Data["json"] = resp
c.ServeJSON()
}
// GetHumanCheck ...
// @Tag Login API
// @Title GetHumancheck

View File

@ -18,8 +18,8 @@ import (
"encoding/json"
"github.com/astaxie/beego/utils/pagination"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// GetApplications
@ -30,16 +30,30 @@ import (
// @Success 200 {array} object.Application The Response object
// @router /get-applications [get]
func (c *ApiController) GetApplications() {
userId := c.GetSessionUsername()
owner := c.Input().Get("owner")
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
value := c.Input().Get("value")
sortField := c.Input().Get("sortField")
sortOrder := c.Input().Get("sortOrder")
organization := c.Input().Get("organization")
if limit == "" || page == "" {
c.Data["json"] = object.GetApplications(owner)
var applications []*object.Application
if organization == "" {
applications = object.GetApplications(owner)
} else {
applications = object.GetApplicationsByOrganizationName(owner, organization)
}
c.Data["json"] = object.GetMaskedApplications(applications, userId)
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetApplicationCount(owner)))
applications := object.GetPaginationApplications(owner, paginator.Offset(), limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetApplicationCount(owner, field, value)))
applications := object.GetMaskedApplications(object.GetPaginationApplications(owner, paginator.Offset(), limit, field, value, sortField, sortOrder), userId)
c.ResponseOk(applications, paginator.Nums())
}
}
@ -52,9 +66,10 @@ func (c *ApiController) GetApplications() {
// @Success 200 {object} object.Application The Response object
// @router /get-application [get]
func (c *ApiController) GetApplication() {
userId := c.GetSessionUsername()
id := c.Input().Get("id")
c.Data["json"] = object.GetApplication(id)
c.Data["json"] = object.GetMaskedApplication(object.GetApplication(id), userId)
c.ServeJSON()
}
@ -66,6 +81,7 @@ func (c *ApiController) GetApplication() {
// @Success 200 {object} object.Application The Response object
// @router /get-user-application [get]
func (c *ApiController) GetUserApplication() {
userId := c.GetSessionUsername()
id := c.Input().Get("id")
user := object.GetUser(id)
if user == nil {
@ -73,7 +89,7 @@ func (c *ApiController) GetUserApplication() {
return
}
c.Data["json"] = object.GetApplicationByUser(user)
c.Data["json"] = object.GetMaskedApplication(object.GetApplicationByUser(user), userId)
c.ServeJSON()
}

View File

@ -24,10 +24,10 @@ import (
"time"
"github.com/astaxie/beego"
"github.com/casbin/casdoor/idp"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/proxy"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/idp"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/proxy"
"github.com/casdoor/casdoor/util"
)
func codeToResponse(code *object.Code) *Response {
@ -52,10 +52,17 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
scope := c.Input().Get("scope")
state := c.Input().Get("state")
nonce := c.Input().Get("nonce")
code := object.GetOAuthCode(userId, clientId, responseType, redirectUri, scope, state, nonce)
challengeMethod := c.Input().Get("code_challenge_method")
codeChallenge := c.Input().Get("code_challenge")
if challengeMethod != "S256" && challengeMethod != "null" && challengeMethod != "" {
c.ResponseError("Challenge method should be S256")
return
}
code := object.GetOAuthCode(userId, clientId, responseType, redirectUri, scope, state, nonce, codeChallenge)
resp = codeToResponse(code)
if application.HasPromptPage() {
if application.EnableSigninSession || application.HasPromptPage() {
// The prompt page needs the user to be signed in
c.SetSessionUsername(userId)
}
@ -214,7 +221,7 @@ func (c *ApiController) Login() {
clientSecret = provider.ClientSecret2
}
idProvider := idp.GetIdProvider(provider.Type, clientId, clientSecret, form.RedirectUri)
idProvider := idp.GetIdProvider(provider.Type, provider.SubType, clientId, clientSecret, form.RedirectUri)
if idProvider == nil {
c.ResponseError(fmt.Sprintf("The provider type: %s is not supported", provider.Type))
return

View File

@ -15,10 +15,12 @@
package controllers
import (
"strings"
"time"
"github.com/astaxie/beego"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// controller for handlers under /api uri
@ -35,6 +37,21 @@ type SessionData struct {
ExpireTime int64
}
func (c *ApiController) IsGlobalAdmin() bool {
username := c.GetSessionUsername()
if strings.HasPrefix(username, "app/") {
// e.g., "app/app-casnode"
return true
}
user := object.GetUser(username)
if user == nil {
return false
}
return user.Owner == "built-in" || user.IsGlobalAdmin
}
// GetSessionUsername ...
func (c *ApiController) GetSessionUsername() string {
// check if user session expired
@ -55,6 +72,28 @@ func (c *ApiController) GetSessionUsername() string {
return user.(string)
}
func (c *ApiController) GetSessionOidc() (string, string) {
sessionData := c.GetSessionData()
if sessionData != nil &&
sessionData.ExpireTime != 0 &&
sessionData.ExpireTime < time.Now().Unix() {
c.SetSessionUsername("")
c.SetSessionData(nil)
return "", ""
}
scopeValue := c.GetSession("scope")
audValue := c.GetSession("aud")
var scope, aud string
var ok bool
if scope, ok = scopeValue.(string); !ok {
scope = ""
}
if aud, ok = audValue.(string); !ok {
aud = ""
}
return scope, aud
}
// SetSessionUsername ...
func (c *ApiController) SetSessionUsername(user string) {
c.SetSession("username", user)

116
controllers/cert.go Normal file
View File

@ -0,0 +1,116 @@
// Copyright 2021 The casbin 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 (
"encoding/json"
"github.com/astaxie/beego/utils/pagination"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// GetCerts
// @Title GetCerts
// @Tag Cert API
// @Description get certs
// @Param owner query string true "The owner of certs"
// @Success 200 {array} object.Cert The Response object
// @router /get-certs [get]
func (c *ApiController) GetCerts() {
owner := c.Input().Get("owner")
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
value := c.Input().Get("value")
sortField := c.Input().Get("sortField")
sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" {
c.Data["json"] = object.GetMaskedCerts(object.GetCerts(owner))
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetCertCount(owner, field, value)))
certs := object.GetMaskedCerts(object.GetPaginationCerts(owner, paginator.Offset(), limit, field, value, sortField, sortOrder))
c.ResponseOk(certs, paginator.Nums())
}
}
// @Title GetCert
// @Tag Cert API
// @Description get cert
// @Param id query string true "The id of the cert"
// @Success 200 {object} object.Cert The Response object
// @router /get-cert [get]
func (c *ApiController) GetCert() {
id := c.Input().Get("id")
c.Data["json"] = object.GetMaskedCert(object.GetCert(id))
c.ServeJSON()
}
// @Title UpdateCert
// @Tag Cert API
// @Description update cert
// @Param id query string true "The id of the cert"
// @Param body body object.Cert true "The details of the cert"
// @Success 200 {object} controllers.Response The Response object
// @router /update-cert [post]
func (c *ApiController) UpdateCert() {
id := c.Input().Get("id")
var cert object.Cert
err := json.Unmarshal(c.Ctx.Input.RequestBody, &cert)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.UpdateCert(id, &cert))
c.ServeJSON()
}
// @Title AddCert
// @Tag Cert API
// @Description add cert
// @Param body body object.Cert true "The details of the cert"
// @Success 200 {object} controllers.Response The Response object
// @router /add-cert [post]
func (c *ApiController) AddCert() {
var cert object.Cert
err := json.Unmarshal(c.Ctx.Input.RequestBody, &cert)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.AddCert(&cert))
c.ServeJSON()
}
// @Title DeleteCert
// @Tag Cert API
// @Description delete cert
// @Param body body object.Cert true "The details of the cert"
// @Success 200 {object} controllers.Response The Response object
// @router /delete-cert [post]
func (c *ApiController) DeleteCert() {
var cert object.Cert
err := json.Unmarshal(c.Ctx.Input.RequestBody, &cert)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.DeleteCert(&cert))
c.ServeJSON()
}

View File

@ -17,8 +17,8 @@ package controllers
import (
"encoding/json"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
type LdapServer struct {

View File

@ -17,7 +17,7 @@ package controllers
import (
"encoding/json"
"github.com/casbin/casdoor/object"
"github.com/casdoor/casdoor/object"
)
type LinkForm struct {

View File

@ -14,13 +14,14 @@
package controllers
import "github.com/casbin/casdoor/object"
import "github.com/casdoor/casdoor/object"
// @Title GetOidcDiscovery
// @Tag OIDC API
// @router /.well-known/openid-configuration [get]
func (c *RootController) GetOidcDiscovery() {
c.Data["json"] = object.GetOidcDiscovery()
host := c.Ctx.Request.Host
c.Data["json"] = object.GetOidcDiscovery(host)
c.ServeJSON()
}
@ -28,7 +29,7 @@ func (c *RootController) GetOidcDiscovery() {
// @Tag OIDC API
// @router /api/certs [get]
func (c *RootController) GetOidcCert() {
jwks, err := object.GetJSONWebKeySet()
jwks, err := object.GetJsonWebKeySet()
if err != nil {
c.ResponseError(err.Error())
return

View File

@ -18,8 +18,8 @@ import (
"encoding/json"
"github.com/astaxie/beego/utils/pagination"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// GetOrganizations ...
@ -33,13 +33,17 @@ func (c *ApiController) GetOrganizations() {
owner := c.Input().Get("owner")
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
value := c.Input().Get("value")
sortField := c.Input().Get("sortField")
sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" {
c.Data["json"] = object.GetMaskedOrganizations(object.GetOrganizations(owner))
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetOrganizationCount(owner)))
organizations := object.GetMaskedOrganizations(object.GetPaginationOrganizations(owner, paginator.Offset(), limit))
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetOrganizationCount(owner, field, value)))
organizations := object.GetMaskedOrganizations(object.GetPaginationOrganizations(owner, paginator.Offset(), limit, field, value, sortField, sortOrder))
c.ResponseOk(organizations, paginator.Nums())
}
}

116
controllers/permission.go Normal file
View File

@ -0,0 +1,116 @@
// Copyright 2021 The casbin 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 (
"encoding/json"
"github.com/astaxie/beego/utils/pagination"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// GetPermissions
// @Title GetPermissions
// @Tag Permission API
// @Description get permissions
// @Param owner query string true "The owner of permissions"
// @Success 200 {array} object.Permission The Response object
// @router /get-permissions [get]
func (c *ApiController) GetPermissions() {
owner := c.Input().Get("owner")
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
value := c.Input().Get("value")
sortField := c.Input().Get("sortField")
sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" {
c.Data["json"] = object.GetPermissions(owner)
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetPermissionCount(owner, field, value)))
permissions := object.GetPaginationPermissions(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
c.ResponseOk(permissions, paginator.Nums())
}
}
// @Title GetPermission
// @Tag Permission API
// @Description get permission
// @Param id query string true "The id of the permission"
// @Success 200 {object} object.Permission The Response object
// @router /get-permission [get]
func (c *ApiController) GetPermission() {
id := c.Input().Get("id")
c.Data["json"] = object.GetPermission(id)
c.ServeJSON()
}
// @Title UpdatePermission
// @Tag Permission API
// @Description update permission
// @Param id query string true "The id of the permission"
// @Param body body object.Permission true "The details of the permission"
// @Success 200 {object} controllers.Response The Response object
// @router /update-permission [post]
func (c *ApiController) UpdatePermission() {
id := c.Input().Get("id")
var permission object.Permission
err := json.Unmarshal(c.Ctx.Input.RequestBody, &permission)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.UpdatePermission(id, &permission))
c.ServeJSON()
}
// @Title AddPermission
// @Tag Permission API
// @Description add permission
// @Param body body object.Permission true "The details of the permission"
// @Success 200 {object} controllers.Response The Response object
// @router /add-permission [post]
func (c *ApiController) AddPermission() {
var permission object.Permission
err := json.Unmarshal(c.Ctx.Input.RequestBody, &permission)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.AddPermission(&permission))
c.ServeJSON()
}
// @Title DeletePermission
// @Tag Permission API
// @Description delete permission
// @Param body body object.Permission true "The details of the permission"
// @Success 200 {object} controllers.Response The Response object
// @router /delete-permission [post]
func (c *ApiController) DeletePermission() {
var permission object.Permission
err := json.Unmarshal(c.Ctx.Input.RequestBody, &permission)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.DeletePermission(&permission))
c.ServeJSON()
}

View File

@ -16,10 +16,9 @@ package controllers
import (
"encoding/json"
"github.com/astaxie/beego/utils/pagination"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// GetProviders
@ -33,13 +32,17 @@ func (c *ApiController) GetProviders() {
owner := c.Input().Get("owner")
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
value := c.Input().Get("value")
sortField := c.Input().Get("sortField")
sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" {
c.Data["json"] = object.GetMaskedProviders(object.GetProviders(owner))
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetProviderCount(owner)))
providers := object.GetMaskedProviders(object.GetPaginationProviders(owner, paginator.Offset(), limit))
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetProviderCount(owner, field, value)))
providers := object.GetMaskedProviders(object.GetPaginationProviders(owner, paginator.Offset(), limit, field, value, sortField, sortOrder))
c.ResponseOk(providers, paginator.Nums())
}
}

View File

@ -16,8 +16,8 @@ package controllers
import (
"github.com/astaxie/beego/utils/pagination"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// GetRecords
@ -31,13 +31,17 @@ import (
func (c *ApiController) GetRecords() {
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
value := c.Input().Get("value")
sortField := c.Input().Get("sortField")
sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" {
c.Data["json"] = object.GetRecords()
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetRecordCount()))
records := object.GetPaginationRecords(paginator.Offset(), limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetRecordCount(field, value)))
records := object.GetPaginationRecords(paginator.Offset(), limit, field, value, sortField, sortOrder)
c.ResponseOk(records, paginator.Nums())
}
}

View File

@ -23,8 +23,8 @@ import (
"path/filepath"
"github.com/astaxie/beego/utils/pagination"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// @router /get-resources [get]
@ -35,13 +35,17 @@ func (c *ApiController) GetResources() {
user := c.Input().Get("user")
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
value := c.Input().Get("value")
sortField := c.Input().Get("sortField")
sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" {
c.Data["json"] = object.GetResources(owner, user)
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetResourceCount(owner, user)))
resources := object.GetPaginationResources(owner, user, paginator.Offset(), limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetResourceCount(owner, user, field, value)))
resources := object.GetPaginationResources(owner, user, paginator.Offset(), limit, field, value, sortField, sortOrder)
c.ResponseOk(resources, paginator.Nums())
}
}
@ -198,7 +202,7 @@ func (c *ApiController) UploadResource() {
}
user.Avatar = fileUrl
object.UpdateUser(user.GetId(), user, []string{"avatar"})
object.UpdateUser(user.GetId(), user, []string{"avatar"}, false)
case "termsOfUse":
applicationId := fmt.Sprintf("admin/%s", parent)
app := object.GetApplication(applicationId)

116
controllers/role.go Normal file
View File

@ -0,0 +1,116 @@
// Copyright 2021 The casbin 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 (
"encoding/json"
"github.com/astaxie/beego/utils/pagination"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// GetRoles
// @Title GetRoles
// @Tag Role API
// @Description get roles
// @Param owner query string true "The owner of roles"
// @Success 200 {array} object.Role The Response object
// @router /get-roles [get]
func (c *ApiController) GetRoles() {
owner := c.Input().Get("owner")
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
value := c.Input().Get("value")
sortField := c.Input().Get("sortField")
sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" {
c.Data["json"] = object.GetRoles(owner)
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetRoleCount(owner, field, value)))
roles := object.GetPaginationRoles(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
c.ResponseOk(roles, paginator.Nums())
}
}
// @Title GetRole
// @Tag Role API
// @Description get role
// @Param id query string true "The id of the role"
// @Success 200 {object} object.Role The Response object
// @router /get-role [get]
func (c *ApiController) GetRole() {
id := c.Input().Get("id")
c.Data["json"] = object.GetRole(id)
c.ServeJSON()
}
// @Title UpdateRole
// @Tag Role API
// @Description update role
// @Param id query string true "The id of the role"
// @Param body body object.Role true "The details of the role"
// @Success 200 {object} controllers.Response The Response object
// @router /update-role [post]
func (c *ApiController) UpdateRole() {
id := c.Input().Get("id")
var role object.Role
err := json.Unmarshal(c.Ctx.Input.RequestBody, &role)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.UpdateRole(id, &role))
c.ServeJSON()
}
// @Title AddRole
// @Tag Role API
// @Description add role
// @Param body body object.Role true "The details of the role"
// @Success 200 {object} controllers.Response The Response object
// @router /add-role [post]
func (c *ApiController) AddRole() {
var role object.Role
err := json.Unmarshal(c.Ctx.Input.RequestBody, &role)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.AddRole(&role))
c.ServeJSON()
}
// @Title DeleteRole
// @Tag Role API
// @Description delete role
// @Param body body object.Role true "The details of the role"
// @Success 200 {object} controllers.Response The Response object
// @router /delete-role [post]
func (c *ApiController) DeleteRole() {
var role object.Role
err := json.Unmarshal(c.Ctx.Input.RequestBody, &role)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.DeleteRole(&role))
c.ServeJSON()
}

View File

@ -21,8 +21,8 @@ import (
"encoding/json"
"fmt"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// SendEmail

View File

@ -18,8 +18,8 @@ import (
"encoding/json"
"github.com/astaxie/beego/utils/pagination"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// GetSyncers
@ -33,13 +33,17 @@ func (c *ApiController) GetSyncers() {
owner := c.Input().Get("owner")
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
value := c.Input().Get("value")
sortField := c.Input().Get("sortField")
sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" {
c.Data["json"] = object.GetSyncers(owner)
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetSyncerCount(owner)))
syncers := object.GetPaginationSyncers(owner, paginator.Offset(), limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetSyncerCount(owner, field, value)))
syncers := object.GetPaginationSyncers(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
c.ResponseOk(syncers, paginator.Nums())
}
}

View File

@ -18,8 +18,8 @@ import (
"encoding/json"
"github.com/astaxie/beego/utils/pagination"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// GetTokens
@ -35,13 +35,17 @@ func (c *ApiController) GetTokens() {
owner := c.Input().Get("owner")
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
value := c.Input().Get("value")
sortField := c.Input().Get("sortField")
sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" {
c.Data["json"] = object.GetTokens(owner)
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetTokenCount(owner)))
tokens := object.GetPaginationTokens(owner, paginator.Offset(), limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetTokenCount(owner, field, value)))
tokens := object.GetPaginationTokens(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
c.ResponseOk(tokens, paginator.Nums())
}
}
@ -138,7 +142,15 @@ func (c *ApiController) GetOAuthCode() {
state := c.Input().Get("state")
nonce := c.Input().Get("nonce")
c.Data["json"] = object.GetOAuthCode(userId, clientId, responseType, redirectUri, scope, state, nonce)
challengeMethod := c.Input().Get("code_challenge_method")
codeChallenge := c.Input().Get("code_challenge")
if challengeMethod != "S256" && challengeMethod != "null" && challengeMethod != "" {
c.ResponseError("Challenge method should be S256")
return
}
c.Data["json"] = object.GetOAuthCode(userId, clientId, responseType, redirectUri, scope, state, nonce, codeChallenge)
c.ServeJSON()
}
@ -157,12 +169,13 @@ func (c *ApiController) GetOAuthToken() {
clientId := c.Input().Get("client_id")
clientSecret := c.Input().Get("client_secret")
code := c.Input().Get("code")
verifier := c.Input().Get("code_verifier")
if clientId == "" && clientSecret == "" {
clientId, clientSecret, _ = c.Ctx.Request.BasicAuth()
}
c.Data["json"] = object.GetOAuthToken(grantType, clientId, clientSecret, code)
c.Data["json"] = object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier)
c.ServeJSON()
}

View File

@ -20,8 +20,8 @@ import (
"strings"
"github.com/astaxie/beego/utils/pagination"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// GetGlobalUsers
@ -33,13 +33,17 @@ import (
func (c *ApiController) GetGlobalUsers() {
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
value := c.Input().Get("value")
sortField := c.Input().Get("sortField")
sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" {
c.Data["json"] = object.GetMaskedUsers(object.GetGlobalUsers())
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetGlobalUserCount()))
users := object.GetPaginationGlobalUsers(paginator.Offset(), limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetGlobalUserCount(field, value)))
users := object.GetPaginationGlobalUsers(paginator.Offset(), limit, field, value, sortField, sortOrder)
c.ResponseOk(users, paginator.Nums())
}
}
@ -55,13 +59,17 @@ func (c *ApiController) GetUsers() {
owner := c.Input().Get("owner")
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
value := c.Input().Get("value")
sortField := c.Input().Get("sortField")
sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" {
c.Data["json"] = object.GetMaskedUsers(object.GetUsers(owner))
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetUserCount(owner)))
users := object.GetPaginationUsers(owner, paginator.Offset(), limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetUserCount(owner, field, value)))
users := object.GetPaginationUsers(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
c.ResponseOk(users, paginator.Nums())
}
}
@ -117,7 +125,8 @@ func (c *ApiController) UpdateUser() {
columns = strings.Split(columnsStr, ",")
}
affected := object.UpdateUser(id, &user, columns)
isGlobalAdmin := c.IsGlobalAdmin()
affected := object.UpdateUser(id, &user, columns, isGlobalAdmin)
if affected {
object.UpdateUserToOriginalDatabase(&user)
}
@ -218,11 +227,6 @@ func (c *ApiController) SetPassword() {
c.ResponseError("Please login first.")
return
}
requestUser := object.GetUser(requestUserId)
if requestUser == nil {
c.ResponseError("Session outdated. Please login again.")
return
}
userId := fmt.Sprintf("%s/%s", userOwner, userName)
targetUser := object.GetUser(userId)
@ -232,7 +236,14 @@ func (c *ApiController) SetPassword() {
}
hasPermission := false
if strings.HasPrefix(requestUserId, "app/") {
hasPermission = true
} else {
requestUser := object.GetUser(requestUserId)
if requestUser == nil {
c.ResponseError("Session outdated. Please login again.")
return
}
if requestUser.IsGlobalAdmin {
hasPermission = true
} else if requestUserId == userId {
@ -240,7 +251,7 @@ func (c *ApiController) SetPassword() {
} else if targetUser.Owner == requestUser.Owner && requestUser.IsAdmin {
hasPermission = true
}
}
if !hasPermission {
c.ResponseError("You don't have the permission to do this.")
return
@ -264,8 +275,6 @@ func (c *ApiController) SetPassword() {
return
}
c.SetSessionUsername("")
targetUser.Password = newPassword
object.SetUserField(targetUser, "password", targetUser.Password)
c.Data["json"] = Response{Status: "ok"}
@ -322,7 +331,7 @@ func (c *ApiController) GetUserCount() {
count := 0
if isOnline == "" {
count = object.GetUserCount(owner)
count = object.GetUserCount(owner, "", "")
} else {
count = object.GetOnlineUserCount(owner, util.ParseInt(isOnline))
}

View File

@ -0,0 +1,60 @@
// Copyright 2021 The casbin 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 (
"fmt"
"io"
"mime/multipart"
"os"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
func saveFile(path string, file *multipart.File) {
f, err := os.Create(path)
if err != nil {
panic(err)
}
defer f.Close()
_, err = io.Copy(f, *file)
if err != nil {
panic(err)
}
}
func (c *ApiController) UploadUsers() {
userId := c.GetSessionUsername()
owner, user := util.GetOwnerAndNameFromId(userId)
file, header, err := c.Ctx.Request.FormFile("file")
if err != nil {
panic(err)
}
fileId := fmt.Sprintf("%s_%s_%s", owner, user, util.RemoveExt(header.Filename))
path := util.GetUploadXlsxPath(fileId)
util.EnsureFileFolderExists(path)
saveFile(path, &file)
affected := object.UploadUsers(owner, fileId)
if affected {
c.ResponseOk()
} else {
c.ResponseError("Failed to import users")
}
}

View File

@ -19,8 +19,8 @@ import (
"strconv"
"github.com/astaxie/beego"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// ResponseOk ...

View File

@ -19,8 +19,8 @@ import (
"fmt"
"strings"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
func (c *ApiController) getCurrentUser() *object.User {

View File

@ -18,8 +18,8 @@ import (
"encoding/json"
"github.com/astaxie/beego/utils/pagination"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// GetWebhooks
@ -33,13 +33,17 @@ func (c *ApiController) GetWebhooks() {
owner := c.Input().Get("owner")
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
value := c.Input().Get("value")
sortField := c.Input().Get("sortField")
sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" {
c.Data["json"] = object.GetWebhooks(owner)
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetWebhookCount(owner)))
webhooks := object.GetPaginationWebhooks(owner, paginator.Offset(), limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetWebhookCount(owner, field, value)))
webhooks := object.GetPaginationWebhooks(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
c.ResponseOk(webhooks, paginator.Nums())
}
}

View File

@ -1,6 +1,7 @@
version: '3.1'
services:
casdoor:
restart: always
build:
context: ./
dockerfile: Dockerfile
@ -15,6 +16,7 @@ services:
db:
restart: always
image: mysql:8.0.25
platform: linux/amd64
ports:
- "3306:3306"
environment:

7
go.mod
View File

@ -1,4 +1,4 @@
module github.com/casbin/casdoor
module github.com/casdoor/casdoor
go 1.16
@ -8,7 +8,7 @@ require (
github.com/aws/aws-sdk-go v1.37.30
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect
github.com/casbin/casbin/v2 v2.30.1
github.com/casbin/xorm-adapter/v2 v2.3.1
github.com/casbin/xorm-adapter/v2 v2.5.1
github.com/casdoor/go-sms-sender v0.0.5
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df
@ -18,13 +18,14 @@ require (
github.com/google/uuid v1.2.0
github.com/jinzhu/configor v1.2.1 // indirect
github.com/markbates/goth v1.68.1-0.20211006204042-9dc8905b41c8
github.com/mileusna/crontab v1.0.1
github.com/qiangmzsx/string-adapter/v2 v2.1.0
github.com/qor/oss v0.0.0-20191031055114-aef9ba66bf76
github.com/robfig/cron/v3 v3.0.1
github.com/russellhaering/gosaml2 v0.6.0
github.com/russellhaering/goxmldsig v1.1.1
github.com/satori/go.uuid v1.2.0 // indirect
github.com/smartystreets/goconvey v1.6.4 // indirect
github.com/tealeg/xlsx v1.0.5
github.com/thanhpk/randstr v1.0.4
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2

12
go.sum
View File

@ -74,11 +74,11 @@ github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6
github.com/casbin/casbin v1.7.0 h1:PuzlE8w0JBg/DhIqnkF1Dewf3z+qmUZMVN07PonvVUQ=
github.com/casbin/casbin v1.7.0/go.mod h1:c67qKN6Oum3UF5Q1+BByfFxkwKvhwW57ITjqwtzR1KE=
github.com/casbin/casbin/v2 v2.1.0/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/casbin/casbin/v2 v2.25.5/go.mod h1:wUgota0cQbTXE6Vd+KWpg41726jFRi7upxio0sR+Xd0=
github.com/casbin/casbin/v2 v2.28.3/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
github.com/casbin/casbin/v2 v2.30.1 h1:P5HWadDL7olwUXNdcuKUBk+x75Y2eitFxYTcLNKeKF0=
github.com/casbin/casbin/v2 v2.30.1/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
github.com/casbin/xorm-adapter/v2 v2.3.1 h1:RVGsM6KYFP9s4OQJXrP/gv56Wmt5P40mzvcyXgv5xeg=
github.com/casbin/xorm-adapter/v2 v2.3.1/go.mod h1:GZ+nlIdasVFunQ71SlvkL/HcQQBvFncphDf+2Yl167c=
github.com/casbin/xorm-adapter/v2 v2.5.1 h1:BkpIxRHKa0s3bSMx173PpuU7oTs+Zw7XmD0BIta0HGM=
github.com/casbin/xorm-adapter/v2 v2.5.1/go.mod h1:AeH4dBKHC9/zYxzdPVHhPDzF8LYLqjDdb767CWJoV54=
github.com/casdoor/go-sms-sender v0.0.5 h1:9qhlMM+UoSOvvY7puUULqSHBBA7fbe02Px/tzchQboo=
github.com/casdoor/go-sms-sender v0.0.5/go.mod h1:TMM/BsZQAa+7JVDXl2KqgxnzZgCjmHEX5MBN662mM5M=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@ -264,8 +264,6 @@ github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJK
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mileusna/crontab v1.0.1 h1:YrDLc7l3xOiznmXq2FtAgg+1YQ3yC6pfFVPe+ywXNtg=
github.com/mileusna/crontab v1.0.1/go.mod h1:dbns64w/u3tUnGZGf8pAa76ZqOfeBX4olW4U1ZwExmc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -313,6 +311,8 @@ github.com/qiangmzsx/string-adapter/v2 v2.1.0 h1:q0y8TPa/sTwtriJPRe8gWL++PuZ+XbO
github.com/qiangmzsx/string-adapter/v2 v2.1.0/go.mod h1:PElPB7b7HnGKTsuADAffFpOQXHqjEGJz1+U1a6yR5wA=
github.com/qor/oss v0.0.0-20191031055114-aef9ba66bf76 h1:J2Xj92efYLxPl3BiibgEDEUiMsCBzwTurE/8JjD8CG4=
github.com/qor/oss v0.0.0-20191031055114-aef9ba66bf76/go.mod h1:JhtPzUhP5KGtCB2yksmxuYAD4hEWw4qGQJpucjsm3U0=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
@ -349,6 +349,8 @@ github.com/syndtr/goleveldb v0.0.0-20160425020131-cfa635847112/go.mod h1:Z4AUp2K
github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/tealeg/xlsx v1.0.5 h1:+f8oFmvY8Gw1iUXzPk+kz+4GpbDZPK1FhPiQRd+ypgE=
github.com/tealeg/xlsx v1.0.5/go.mod h1:btRS8dz54TDnvKNosuAqxrM1QgN1udgk9O34bDCnORM=
github.com/tencentcloud/tencentcloud-sdk-go v1.0.154 h1:THBgwGwUQtsw6L53cSSA2wwL3sLrm+HJ3Dk+ye/lMCI=
github.com/tencentcloud/tencentcloud-sdk-go v1.0.154/go.mod h1:asUz5BPXxgoPGaRgZaVm1iGcUAuHyYUo1nXqKa83cvI=
github.com/thanhpk/randstr v1.0.4 h1:IN78qu/bR+My+gHCvMEXhR/i5oriVHcTB/BJJIRTsNo=

View File

@ -21,7 +21,7 @@ import (
"regexp"
"strings"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/util"
)
type I18nData map[string]map[string]string

View File

@ -18,7 +18,7 @@ import (
"fmt"
"strings"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/util"
)
func getI18nFilePath(language string) string {

116
idp/baidu.go Normal file
View File

@ -0,0 +1,116 @@
// Copyright 2021 The casbin 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 idp
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"golang.org/x/oauth2"
)
type BaiduIdProvider struct {
Client *http.Client
Config *oauth2.Config
}
func NewBaiduIdProvider(clientId string, clientSecret string, redirectUrl string) *BaiduIdProvider {
idp := &BaiduIdProvider{}
config := idp.getConfig()
config.ClientID = clientId
config.ClientSecret = clientSecret
config.RedirectURL = redirectUrl
idp.Config = config
return idp
}
func (idp *BaiduIdProvider) SetHttpClient(client *http.Client) {
idp.Client = client
}
func (idp *BaiduIdProvider) getConfig() *oauth2.Config {
var endpoint = oauth2.Endpoint{
AuthURL: "https://openapi.baidu.com/oauth/2.0/authorize",
TokenURL: "https://openapi.baidu.com/oauth/2.0/token",
}
var config = &oauth2.Config{
Scopes: []string{"email"},
Endpoint: endpoint,
}
return config
}
func (idp *BaiduIdProvider) GetToken(code string) (*oauth2.Token, error) {
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, idp.Client)
return idp.Config.Exchange(ctx, code)
}
/*
{
"userid":"2097322476",
"username":"wl19871011",
"realname":"阳光",
"userdetail":"喜欢自由",
"birthday":"1987-01-01",
"marriage":"恋爱",
"sex":"男",
"blood":"O",
"constellation":"射手",
"figure":"小巧",
"education":"大学/专科",
"trade":"计算机/电子产品",
"job":"未知",
"birthday_year":"1987",
"birthday_month":"01",
"birthday_day":"01",
}
*/
type BaiduUserInfo struct {
OpenId string `json:"openid"`
Username string `json:"username"`
Portrait string `json:"portrait"`
}
func (idp *BaiduIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
resp, err := idp.Client.Get(fmt.Sprintf("https://openapi.baidu.com/rest/2.0/passport/users/getInfo?access_token=%s", token.AccessToken))
if err != nil {
return nil, err
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
baiduUser := BaiduUserInfo{}
if err = json.Unmarshal(data, &baiduUser); err != nil {
return nil, err
}
userInfo := UserInfo{
Id: baiduUser.OpenId,
Username: baiduUser.Username,
DisplayName: baiduUser.Username,
AvatarUrl: fmt.Sprintf("https://himg.bdimg.com/sys/portrait/item/%s", baiduUser.Portrait),
}
return &userInfo, nil
}

View File

@ -21,6 +21,7 @@ import (
"reflect"
"time"
"github.com/casdoor/casdoor/util"
"github.com/markbates/goth"
"github.com/markbates/goth/providers/amazon"
"github.com/markbates/goth/providers/apple"
@ -244,11 +245,27 @@ func getUser(gothUser goth.User) *UserInfo {
//Some idp return an empty Name
//so construct the Name with firstname and lastname or nickname
if user.Username == "" {
user.Username = fmt.Sprintf("%v%v", gothUser.FirstName, gothUser.LastName)
}
if user.Username == "" {
if gothUser.FirstName != "" && gothUser.LastName != "" {
user.Username = getName(gothUser.FirstName, gothUser.LastName)
} else {
user.Username = gothUser.NickName
}
}
if user.DisplayName == "" {
if gothUser.FirstName != "" && gothUser.LastName != "" {
user.DisplayName = getName(gothUser.FirstName, gothUser.LastName)
} else {
user.DisplayName = user.Username
}
}
return &user
}
func getName(firstName, lastName string) string {
if util.IsChinese(firstName) || util.IsChinese(lastName) {
return fmt.Sprintf("%s%s", lastName, firstName)
} else {
return fmt.Sprintf("%s %s", firstName, lastName)
}
}

View File

@ -35,33 +35,41 @@ type IdProvider interface {
GetUserInfo(token *oauth2.Token) (*UserInfo, error)
}
func GetIdProvider(providerType string, clientId string, clientSecret string, redirectUrl string) IdProvider {
if providerType == "GitHub" {
func GetIdProvider(typ string, subType string, clientId string, clientSecret string, redirectUrl string) IdProvider {
if typ == "GitHub" {
return NewGithubIdProvider(clientId, clientSecret, redirectUrl)
} else if providerType == "Google" {
} else if typ == "Google" {
return NewGoogleIdProvider(clientId, clientSecret, redirectUrl)
} else if providerType == "QQ" {
} else if typ == "QQ" {
return NewQqIdProvider(clientId, clientSecret, redirectUrl)
} else if providerType == "WeChat" {
} else if typ == "WeChat" {
return NewWeChatIdProvider(clientId, clientSecret, redirectUrl)
} else if providerType == "Facebook" {
} else if typ == "Facebook" {
return NewFacebookIdProvider(clientId, clientSecret, redirectUrl)
} else if providerType == "DingTalk" {
} else if typ == "DingTalk" {
return NewDingTalkIdProvider(clientId, clientSecret, redirectUrl)
} else if providerType == "Weibo" {
} else if typ == "Weibo" {
return NewWeiBoIdProvider(clientId, clientSecret, redirectUrl)
} else if providerType == "Gitee" {
} else if typ == "Gitee" {
return NewGiteeIdProvider(clientId, clientSecret, redirectUrl)
} else if providerType == "LinkedIn" {
} else if typ == "LinkedIn" {
return NewLinkedInIdProvider(clientId, clientSecret, redirectUrl)
} else if providerType == "WeCom" {
} else if typ == "WeCom" {
if subType == "Internal" {
return NewWeComInternalIdProvider(clientId, clientSecret, redirectUrl)
} else if subType == "Third-party" {
return NewWeComIdProvider(clientId, clientSecret, redirectUrl)
} else if providerType == "Lark" {
} else {
return nil
}
} else if typ == "Lark" {
return NewLarkIdProvider(clientId, clientSecret, redirectUrl)
} else if providerType == "GitLab" {
} else if typ == "GitLab" {
return NewGitlabIdProvider(clientId, clientSecret, redirectUrl)
} else if isGothSupport(providerType) {
return NewGothIdProvider(providerType, clientId, clientSecret, redirectUrl)
} else if typ == "Baidu" {
return NewBaiduIdProvider(clientId, clientSecret, redirectUrl)
} else if isGothSupport(typ) {
return NewGothIdProvider(typ, clientId, clientSecret, redirectUrl)
}
return nil

171
idp/wecom_internal.go Normal file
View File

@ -0,0 +1,171 @@
// Copyright 2021 The casbin 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 idp
import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"golang.org/x/oauth2"
)
//This idp is using wecom internal application api as idp
type WeComInternalIdProvider struct {
Client *http.Client
Config *oauth2.Config
}
func NewWeComInternalIdProvider(clientId string, clientSecret string, redirectUrl string) *WeComInternalIdProvider {
idp := &WeComInternalIdProvider{}
config := idp.getConfig(clientId, clientSecret, redirectUrl)
idp.Config = config
return idp
}
func (idp *WeComInternalIdProvider) SetHttpClient(client *http.Client) {
idp.Client = client
}
func (idp *WeComInternalIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config {
var config = &oauth2.Config{
ClientID: clientId,
ClientSecret: clientSecret,
RedirectURL: redirectUrl,
}
return config
}
type WecomInterToken struct {
Errcode int `json:"errcode"`
Errmsg string `json:"errmsg"`
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
}
// GetToken use code get access_token (*operation of getting code ought to be done in front)
// get more detail via: https://developer.work.weixin.qq.com/document/path/91039
func (idp *WeComInternalIdProvider) GetToken(code string) (*oauth2.Token, error) {
pTokenParams := &struct {
CorpId string `json:"corpid"`
Corpsecret string `json:"corpsecret"`
}{idp.Config.ClientID, idp.Config.ClientSecret}
resp, err := idp.Client.Get(fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s", pTokenParams.CorpId, pTokenParams.Corpsecret))
if err != nil {
return nil, err
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
pToken := &WecomInterToken{}
err = json.Unmarshal(data, pToken)
if err != nil {
return nil, err
}
if pToken.Errcode != 0 {
return nil, fmt.Errorf("pToken.Errcode = %d, pToken.Errmsg = %s", pToken.Errcode, pToken.Errmsg)
}
token := &oauth2.Token{
AccessToken: pToken.AccessToken,
Expiry: time.Unix(time.Now().Unix()+int64(pToken.ExpiresIn), 0),
}
raw := make(map[string]interface{})
raw["code"] = code
token = token.WithExtra(raw)
return token, nil
}
type WecomInternalUserResp struct {
Errcode int `json:"errcode"`
Errmsg string `json:"errmsg"`
UserId string `json:"UserId"`
OpenId string `json:"OpenId"`
}
type WecomInternalUserInfo struct {
Errcode int `json:"errcode"`
Errmsg string `json:"errmsg"`
Name string `json:"name"`
Email string `json:"email"`
Avatar string `json:"avatar"`
OpenId string `json:"open_userid"`
}
func (idp *WeComInternalIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
//Get userid first
accessToken := token.AccessToken
code := token.Extra("code").(string)
resp, err := idp.Client.Get(fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=%s&code=%s", accessToken, code))
if err != nil {
return nil, err
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
userResp := &WecomInternalUserResp{}
err = json.Unmarshal(data, userResp)
if err != nil {
return nil, err
}
if userResp.Errcode != 0 {
return nil, fmt.Errorf("userIdResp.Errcode = %d, userIdResp.Errmsg = %s", userResp.Errcode, userResp.Errmsg)
}
if userResp.OpenId != "" {
return nil, fmt.Errorf("not an internal user")
}
//Use userid and accesstoken to get user information
resp, err = idp.Client.Get(fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=%s&userid=%s", accessToken, userResp.UserId))
if err != nil {
return nil, err
}
data, err = io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
infoResp := &WecomInternalUserInfo{}
err = json.Unmarshal(data, infoResp)
if err != nil {
return nil, err
}
if infoResp.Errcode != 0 {
return nil, fmt.Errorf("userInfoResp.errcode = %d, userInfoResp.errmsg = %s", infoResp.Errcode, infoResp.Errmsg)
}
userInfo := UserInfo{
Id: infoResp.OpenId,
Username: infoResp.Name,
DisplayName: infoResp.Name,
Email: infoResp.Email,
AvatarUrl: infoResp.Avatar,
}
if userInfo.Id == "" {
userInfo.Id = userInfo.Username
}
return &userInfo, nil
}

56
k8s.yaml Normal file
View File

@ -0,0 +1,56 @@
# this is only an EXAMPLE of deploying casddor in kubernetes
# please modify this file according to your requirements
apiVersion: v1
kind: Service
metadata:
#EDIT IT: if you don't want to run casdoor in default namespace, please modify this field
#namespace: casdoor
name: casdoor-svc
labels:
app: casdoor
spec:
#EDIT IT: if you don't want to run casdoor in default namespace, please modify this filed
type: NodePort
ports:
- port: 8000
selector:
app: casdoor
---
apiVersion: apps/v1
kind: Deployment
metadata:
#EDIT IT: if you don't want to run casdoor in default namespace, please modify this field
#namespace: casdoor
name: casdoor-deployment
labels:
app: casdoor
spec:
#EDIT IT: if you don't use redis, casdoor should not have multiple replicas
replicas: 1
selector:
matchLabels:
app: casdoor
template:
metadata:
labels:
app: casdoor
spec:
containers:
- name: casdoor-container
image: casbin/casdoor:latest
imagePullPolicy: Always
ports:
- containerPort: 8000
volumeMounts:
# the mounted directory path in THE CONTAINER
- mountPath: /conf
name: conf
env:
- name: RUNNING_IN_DOCKER
value: "true"
#if you want to deploy this in real prod env, consider the config map
volumes:
- name: conf
hostPath:
#EDIT IT: the mounted directory path in THE HOST
path: /conf

26
main.go
View File

@ -15,20 +15,22 @@
package main
import (
"flag"
"github.com/astaxie/beego"
"github.com/astaxie/beego/logs"
"github.com/astaxie/beego/plugins/cors"
_ "github.com/astaxie/beego/session/redis"
"github.com/casbin/casdoor/authz"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/proxy"
"github.com/casbin/casdoor/routers"
_ "github.com/casbin/casdoor/routers"
"github.com/casdoor/casdoor/authz"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/proxy"
"github.com/casdoor/casdoor/routers"
_ "github.com/casdoor/casdoor/routers"
)
func main() {
object.InitAdapter()
createDatabase := flag.Bool("createDatabase", false, "true if you need casdoor to create database")
flag.Parse()
object.InitAdapter(*createDatabase)
object.InitDb()
object.InitDefaultStorageProvider()
object.InitLdapAutoSynchronizer()
@ -37,14 +39,6 @@ func main() {
go object.RunSyncUsersJob()
beego.InsertFilter("*", beego.BeforeRouter, cors.Allow(&cors.Options{
AllowOrigins: []string{"*"},
AllowMethods: []string{"GET", "PUT", "PATCH"},
AllowHeaders: []string{"Origin"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
}))
//beego.DelStaticPath("/static")
beego.SetStaticPath("/static", "web/build/static")
beego.BConfig.WebConfig.DirectoryIndex = true

View File

@ -17,9 +17,12 @@ package object
import (
"fmt"
"runtime"
"xorm.io/core"
"github.com/astaxie/beego"
"github.com/casbin/casdoor/conf"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util"
//_ "github.com/denisenkom/go-mssqldb" // db = mssql
_ "github.com/go-sql-driver/mysql" // db = mysql
//_ "github.com/lib/pq" // db = postgres
"xorm.io/xorm"
@ -33,11 +36,15 @@ func InitConfig() {
panic(err)
}
InitAdapter()
InitAdapter(true)
}
func InitAdapter() {
func InitAdapter(createDatabase bool) {
adapter = NewAdapter(beego.AppConfig.String("driverName"), conf.GetBeegoConfDataSourceName(), beego.AppConfig.String("dbName"))
if createDatabase {
adapter.CreateDatabase()
}
adapter.createTable()
}
@ -73,7 +80,7 @@ func NewAdapter(driverName string, dataSourceName string, dbName string) *Adapte
return a
}
func (a *Adapter) createDatabase() error {
func (a *Adapter) CreateDatabase() error {
engine, err := xorm.NewEngine(a.driverName, a.dataSourceName)
if err != nil {
return err
@ -85,13 +92,12 @@ func (a *Adapter) createDatabase() error {
}
func (a *Adapter) open() {
if a.driverName != "postgres" {
if err := a.createDatabase(); err != nil {
panic(err)
}
dataSourceName := a.dataSourceName + a.dbName
if a.driverName != "mysql" {
dataSourceName = a.dataSourceName
}
engine, err := xorm.NewEngine(a.driverName, a.dataSourceName+a.dbName)
engine, err := xorm.NewEngine(a.driverName, dataSourceName)
if err != nil {
panic(err)
}
@ -105,6 +111,13 @@ func (a *Adapter) close() {
}
func (a *Adapter) createTable() {
showSql, _ := beego.AppConfig.Bool("showSql")
a.Engine.ShowSQL(showSql)
tableNamePrefix := beego.AppConfig.String("tableNamePrefix")
tbMapper := core.NewPrefixMapper(core.SnakeMapper{}, tableNamePrefix)
a.Engine.SetTableMapper(tbMapper)
err := a.Engine.Sync2(new(Organization))
if err != nil {
panic(err)
@ -115,6 +128,16 @@ func (a *Adapter) createTable() {
panic(err)
}
err = a.Engine.Sync2(new(Role))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Permission))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Provider))
if err != nil {
panic(err)
@ -155,8 +178,37 @@ func (a *Adapter) createTable() {
panic(err)
}
err = a.Engine.Sync2(new(Cert))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Ldap))
if err != nil {
panic(err)
}
}
func GetSession(owner string, offset, limit int, field, value, sortField, sortOrder string) *xorm.Session {
session := adapter.Engine.Prepare()
if offset != -1 && limit != -1 {
session.Limit(limit, offset)
}
if owner != "" {
session = session.And("owner=?", owner)
}
if field != "" && value != "" {
if filterField(field) {
session = session.And(fmt.Sprintf("%s like ?", util.SnakeString(field)), fmt.Sprintf("%%%s%%", value))
}
}
if sortField == "" || sortOrder == "" {
sortField = "created_time"
}
if sortOrder == "ascend" {
session = session.Asc(util.SnakeString(sortField))
} else {
session = session.Desc(util.SnakeString(sortField))
}
return session
}

View File

@ -17,7 +17,7 @@ package object
import (
"fmt"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
)
@ -31,8 +31,10 @@ type Application struct {
HomepageUrl string `xorm:"varchar(100)" json:"homepageUrl"`
Description string `xorm:"varchar(100)" json:"description"`
Organization string `xorm:"varchar(100)" json:"organization"`
Cert string `xorm:"varchar(100)" json:"cert"`
EnablePassword bool `json:"enablePassword"`
EnableSignUp bool `json:"enableSignUp"`
EnableSigninSession bool `json:"enableSigninSession"`
EnableCodeSignin bool `json:"enableCodeSignin"`
Providers []*ProviderItem `xorm:"mediumtext" json:"providers"`
SignupItems []*SignupItem `xorm:"varchar(1000)" json:"signupItems"`
@ -53,8 +55,9 @@ type Application struct {
SigninHtml string `xorm:"mediumtext" json:"signinHtml"`
}
func GetApplicationCount(owner string) int {
count, err := adapter.Engine.Count(&Application{Owner: owner})
func GetApplicationCount(owner, field, value string) int {
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Application{})
if err != nil {
panic(err)
}
@ -72,9 +75,10 @@ func GetApplications(owner string) []*Application {
return applications
}
func GetPaginationApplications(owner string, offset, limit int) []*Application {
func GetPaginationApplications(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Application {
applications := []*Application{}
err := adapter.Engine.Desc("created_time").Limit(limit, offset).Find(&applications, &Application{Owner: owner})
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&applications)
if err != nil {
panic(err)
}
@ -82,7 +86,7 @@ func GetPaginationApplications(owner string, offset, limit int) []*Application {
return applications
}
func getApplicationsByOrganizationName(owner string, organization string) []*Application {
func GetApplicationsByOrganizationName(owner string, organization string) []*Application {
applications := []*Application{}
err := adapter.Engine.Desc("created_time").Find(&applications, &Application{Owner: owner, Organization: organization})
if err != nil {
@ -194,24 +198,37 @@ func GetApplicationByClientId(clientId string) *Application {
}
}
func GetApplicationByClientIdAndSecret(clientId, clientSecret string) *Application {
if util.IsStrsEmpty(clientId, clientSecret) {
return nil
}
app := GetApplicationByClientId(clientId)
if app == nil || app.ClientSecret != clientSecret {
return nil
}
return app
}
func GetApplication(id string) *Application {
owner, name := util.GetOwnerAndNameFromId(id)
return getApplication(owner, name)
}
func GetMaskedApplication(application *Application, userId string) *Application {
if isUserIdGlobalAdmin(userId) {
return application
}
if application == nil {
return nil
}
if application.ClientSecret != "" {
application.ClientSecret = "***"
}
return application
}
func GetMaskedApplications(applications []*Application, userId string) []*Application {
if isUserIdGlobalAdmin(userId) {
return applications
}
for _, application := range applications {
application = GetMaskedApplication(application, userId)
}
return applications
}
func UpdateApplication(id string, application *Application) bool {
owner, name := util.GetOwnerAndNameFromId(id)
if getApplication(owner, name) == nil {

View File

@ -20,7 +20,7 @@ import (
"io"
"github.com/astaxie/beego"
"github.com/casbin/casdoor/proxy"
"github.com/casdoor/casdoor/proxy"
)
var defaultStorageProvider *Provider = nil

View File

@ -18,7 +18,7 @@ import (
"fmt"
"testing"
"github.com/casbin/casdoor/proxy"
"github.com/casdoor/casdoor/proxy"
)
func TestSyncPermanentAvatars(t *testing.T) {

161
object/cert.go Normal file
View File

@ -0,0 +1,161 @@
// Copyright 2021 The casbin 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 (
"fmt"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
)
type Cert 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"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Scope string `xorm:"varchar(100)" json:"scope"`
Type string `xorm:"varchar(100)" json:"type"`
CryptoAlgorithm string `xorm:"varchar(100)" json:"cryptoAlgorithm"`
BitSize int `json:"bitSize"`
ExpireInYears int `json:"expireInYears"`
PublicKey string `xorm:"mediumtext" json:"publicKey"`
PrivateKey string `xorm:"mediumtext" json:"privateKey"`
}
func GetMaskedCert(cert *Cert) *Cert {
if cert == nil {
return nil
}
return cert
}
func GetMaskedCerts(certs []*Cert) []*Cert {
for _, cert := range certs {
cert = GetMaskedCert(cert)
}
return certs
}
func GetCertCount(owner, field, value string) int {
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Cert{})
if err != nil {
panic(err)
}
return int(count)
}
func GetCerts(owner string) []*Cert {
certs := []*Cert{}
err := adapter.Engine.Desc("created_time").Find(&certs, &Cert{Owner: owner})
if err != nil {
panic(err)
}
return certs
}
func GetPaginationCerts(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Cert {
certs := []*Cert{}
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&certs)
if err != nil {
panic(err)
}
return certs
}
func getCert(owner string, name string) *Cert {
if owner == "" || name == "" {
return nil
}
cert := Cert{Owner: owner, Name: name}
existed, err := adapter.Engine.Get(&cert)
if err != nil {
panic(err)
}
if existed {
return &cert
} else {
return nil
}
}
func GetCert(id string) *Cert {
owner, name := util.GetOwnerAndNameFromId(id)
return getCert(owner, name)
}
func UpdateCert(id string, cert *Cert) bool {
owner, name := util.GetOwnerAndNameFromId(id)
if getCert(owner, name) == nil {
return false
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(cert)
if err != nil {
panic(err)
}
return affected != 0
}
func AddCert(cert *Cert) bool {
if cert.PublicKey == "" || cert.PrivateKey == "" {
publicKey, privateKey := generateRsaKeys(cert.BitSize, cert.ExpireInYears, cert.Name, cert.Owner)
cert.PublicKey = publicKey
cert.PrivateKey = privateKey
}
affected, err := adapter.Engine.Insert(cert)
if err != nil {
panic(err)
}
return affected != 0
}
func DeleteCert(cert *Cert) bool {
affected, err := adapter.Engine.ID(core.PK{cert.Owner, cert.Name}).Delete(&Cert{})
if err != nil {
panic(err)
}
return affected != 0
}
func (p *Cert) GetId() string {
return fmt.Sprintf("%s/%s", p.Owner, p.Name)
}
func getCertByApplication(application *Application) *Cert {
if application.Cert != "" {
return getCert("admin", application.Cert)
} else {
return GetDefaultCert()
}
}
func GetDefaultCert() *Cert {
return getCert("admin", "cert-built-in")
}

View File

@ -18,15 +18,19 @@ import (
"fmt"
"regexp"
"github.com/casbin/casdoor/cred"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/cred"
"github.com/casdoor/casdoor/util"
goldap "github.com/go-ldap/ldap/v3"
)
var reWhiteSpace *regexp.Regexp
var (
reWhiteSpace *regexp.Regexp
reFieldWhiteList *regexp.Regexp
)
func init() {
reWhiteSpace, _ = regexp.Compile(`\s`)
reFieldWhiteList, _ = regexp.Compile(`^[A-Za-z0-9]+$`)
}
func CheckUserSignup(application *Application, organization *Organization, username string, password string, displayName string, email string, phone string, affiliation string) string {
@ -179,3 +183,7 @@ func CheckUserPassword(organization string, username string, password string) (*
return user, ""
}
func filterField(field string) bool {
return reFieldWhiteList.MatchString(field)
}

View File

@ -14,12 +14,23 @@
package object
import "github.com/casbin/casdoor/util"
import (
_ "embed"
"github.com/casdoor/casdoor/util"
)
//go:embed token_jwt_key.pem
var tokenJwtPublicKey string
//go:embed token_jwt_key.key
var tokenJwtPrivateKey string
func InitDb() {
initBuiltInOrganization()
initBuiltInUser()
initBuiltInApplication()
initBuiltInCert()
initBuiltInLdap()
}
@ -69,6 +80,8 @@ func initBuiltInUser() {
IsGlobalAdmin: true,
IsForbidden: false,
IsDeleted: false,
SignupApplication: "built-in-app",
CreatedIp: "127.0.0.1",
Properties: make(map[string]string),
}
AddUser(user)
@ -88,6 +101,7 @@ func initBuiltInApplication() {
Logo: "https://cdn.casbin.com/logo/logo_1024x256.png",
HomepageUrl: "https://casdoor.org",
Organization: "built-in",
Cert: "cert-built-in",
EnablePassword: true,
EnableSignUp: true,
Providers: []*ProviderItem{},
@ -107,6 +121,28 @@ func initBuiltInApplication() {
AddApplication(application)
}
func initBuiltInCert() {
cert := getCert("admin", "cert-built-in")
if cert != nil {
return
}
cert = &Cert{
Owner: "admin",
Name: "cert-built-in",
CreatedTime: util.GetCurrentTime(),
DisplayName: "Built-in Cert",
Scope: "JWT",
Type: "x509",
CryptoAlgorithm: "RSA",
BitSize: 4096,
ExpireInYears: 20,
PublicKey: tokenJwtPublicKey,
PrivateKey: tokenJwtPrivateKey,
}
AddCert(cert)
}
func initBuiltInLdap() {
ldap := GetLdap("ldap-built-in")
if ldap != nil {

View File

@ -19,7 +19,7 @@ import (
"fmt"
"strings"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/util"
goldap "github.com/go-ldap/ldap/v3"
"github.com/thanhpk/randstr"
)

View File

@ -18,6 +18,7 @@ import (
"crypto/x509"
"encoding/pem"
"fmt"
"strings"
"github.com/astaxie/beego"
"gopkg.in/square/go-jose.v2"
@ -40,22 +41,39 @@ type OidcDiscovery struct {
RequestObjectSigningAlgValuesSupported []string `json:"request_object_signing_alg_values_supported"`
}
var oidcDiscovery OidcDiscovery
func getOriginFromHost(host string) (string, string) {
protocol := "https://"
if strings.HasPrefix(host, "localhost") {
protocol = "http://"
}
if host == "localhost:8000" {
return fmt.Sprintf("%s%s", protocol, "localhost:7001"), fmt.Sprintf("%s%s", protocol, "localhost:8000")
} else {
return fmt.Sprintf("%s%s", protocol, host), fmt.Sprintf("%s%s", protocol, host)
}
}
func GetOidcDiscovery(host string) OidcDiscovery {
originFrontend, originBackend := getOriginFromHost(host)
func init() {
origin := beego.AppConfig.String("origin")
if origin != "" {
originFrontend = origin
originBackend = origin
}
// Examples:
// https://login.okta.com/.well-known/openid-configuration
// https://auth0.auth0.com/.well-known/openid-configuration
// https://accounts.google.com/.well-known/openid-configuration
// https://access.line.me/.well-known/openid-configuration
oidcDiscovery = OidcDiscovery{
Issuer: origin,
AuthorizationEndpoint: fmt.Sprintf("%s/login/oauth/authorize", origin),
TokenEndpoint: fmt.Sprintf("%s/api/login/oauth/access_token", origin),
UserinfoEndpoint: fmt.Sprintf("%s/api/get-account", origin),
JwksUri: fmt.Sprintf("%s/api/certs", origin),
oidcDiscovery := OidcDiscovery{
Issuer: originFrontend,
AuthorizationEndpoint: fmt.Sprintf("%s/login/oauth/authorize", originFrontend),
TokenEndpoint: fmt.Sprintf("%s/api/login/oauth/access_token", originBackend),
UserinfoEndpoint: fmt.Sprintf("%s/api/userinfo", originBackend),
JwksUri: fmt.Sprintf("%s/api/certs", originBackend),
ResponseTypesSupported: []string{"id_token"},
ResponseModesSupported: []string{"login", "code", "link"},
GrantTypesSupported: []string{"password", "authorization_code"},
@ -66,23 +84,24 @@ func init() {
RequestParameterSupported: true,
RequestObjectSigningAlgValuesSupported: []string{"HS256", "HS384", "HS512"},
}
}
func GetOidcDiscovery() OidcDiscovery {
return oidcDiscovery
}
func GetJSONWebKeySet() (jose.JSONWebKeySet, error) {
func GetJsonWebKeySet() (jose.JSONWebKeySet, error) {
cert := GetDefaultCert()
//follows the protocol rfc 7517(draft)
//link here: https://self-issued.info/docs/draft-ietf-jose-json-web-key.html
//or https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key
certPEMBlock := []byte(tokenJwtPublicKey)
certDERBlock, _ := pem.Decode(certPEMBlock)
x509Cert, _ := x509.ParseCertificate(certDERBlock.Bytes)
certPemBlock := []byte(cert.PublicKey)
certDerBlock, _ := pem.Decode(certPemBlock)
x509Cert, _ := x509.ParseCertificate(certDerBlock.Bytes)
var jwk jose.JSONWebKey
jwk.Key = x509Cert.PublicKey
jwk.Certificates = []*x509.Certificate{x509Cert}
jwk.KeyID = cert.Name
var jwks jose.JSONWebKeySet
jwks.Keys = []jose.JSONWebKey{jwk}

View File

@ -15,8 +15,8 @@
package object
import (
"github.com/casbin/casdoor/cred"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/cred"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
)
@ -36,8 +36,9 @@ type Organization struct {
EnableSoftDeletion bool `json:"enableSoftDeletion"`
}
func GetOrganizationCount(owner string) int {
count, err := adapter.Engine.Count(&Organization{Owner: owner})
func GetOrganizationCount(owner, field, value string) int {
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Organization{})
if err != nil {
panic(err)
}
@ -55,9 +56,10 @@ func GetOrganizations(owner string) []*Organization {
return organizations
}
func GetPaginationOrganizations(owner string, offset, limit int) []*Organization {
func GetPaginationOrganizations(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Organization {
organizations := []*Organization{}
err := adapter.Engine.Desc("created_time").Limit(limit, offset).Find(&organizations, &Provider{Owner: owner})
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&organizations)
if err != nil {
panic(err)
}
@ -117,7 +119,7 @@ func UpdateOrganization(id string, organization *Organization) bool {
}
if name != organization.Name {
applications := getApplicationsByOrganizationName("admin", name)
applications := GetApplicationsByOrganizationName("admin", name)
for _, application := range applications {
application.Organization = organization.Name
UpdateApplication(application.GetId(), application)

129
object/permission.go Normal file
View File

@ -0,0 +1,129 @@
// Copyright 2021 The casbin 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 (
"fmt"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
)
type Permission 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"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Users []string `xorm:"mediumtext" json:"users"`
Roles []string `xorm:"mediumtext" json:"roles"`
ResourceType string `xorm:"varchar(100)" json:"resourceType"`
Resources []string `xorm:"mediumtext" json:"resources"`
Actions []string `xorm:"mediumtext" json:"actions"`
Effect string `xorm:"varchar(100)" json:"effect"`
IsEnabled bool `json:"isEnabled"`
}
func GetPermissionCount(owner, field, value string) int {
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Permission{})
if err != nil {
panic(err)
}
return int(count)
}
func GetPermissions(owner string) []*Permission {
permissions := []*Permission{}
err := adapter.Engine.Desc("created_time").Find(&permissions, &Permission{Owner: owner})
if err != nil {
panic(err)
}
return permissions
}
func GetPaginationPermissions(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Permission {
permissions := []*Permission{}
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&permissions)
if err != nil {
panic(err)
}
return permissions
}
func getPermission(owner string, name string) *Permission {
if owner == "" || name == "" {
return nil
}
permission := Permission{Owner: owner, Name: name}
existed, err := adapter.Engine.Get(&permission)
if err != nil {
panic(err)
}
if existed {
return &permission
} else {
return nil
}
}
func GetPermission(id string) *Permission {
owner, name := util.GetOwnerAndNameFromId(id)
return getPermission(owner, name)
}
func UpdatePermission(id string, permission *Permission) bool {
owner, name := util.GetOwnerAndNameFromId(id)
if getPermission(owner, name) == nil {
return false
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(permission)
if err != nil {
panic(err)
}
return affected != 0
}
func AddPermission(permission *Permission) bool {
affected, err := adapter.Engine.Insert(permission)
if err != nil {
panic(err)
}
return affected != 0
}
func DeletePermission(permission *Permission) bool {
affected, err := adapter.Engine.ID(core.PK{permission.Owner, permission.Name}).Delete(&Permission{})
if err != nil {
panic(err)
}
return affected != 0
}
func (permission *Permission) GetId() string {
return fmt.Sprintf("%s/%s", permission.Owner, permission.Name)
}

View File

@ -17,7 +17,7 @@ package object
import (
"fmt"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
)
@ -29,6 +29,7 @@ type Provider struct {
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Category string `xorm:"varchar(100)" json:"category"`
Type string `xorm:"varchar(100)" json:"type"`
SubType string `xorm:"varchar(100)" json:"subType"`
Method string `xorm:"varchar(100)" json:"method"`
ClientId string `xorm:"varchar(100)" json:"clientId"`
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
@ -80,8 +81,9 @@ func GetMaskedProviders(providers []*Provider) []*Provider {
return providers
}
func GetProviderCount(owner string) int {
count, err := adapter.Engine.Count(&Provider{Owner: owner})
func GetProviderCount(owner, field, value string) int {
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Provider{})
if err != nil {
panic(err)
}
@ -99,9 +101,10 @@ func GetProviders(owner string) []*Provider {
return providers
}
func GetPaginationProviders(owner string, offset, limit int) []*Provider {
func GetPaginationProviders(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Provider {
providers := []*Provider{}
err := adapter.Engine.Desc("created_time").Limit(limit, offset).Find(&providers, &Provider{Owner: owner})
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&providers)
if err != nil {
panic(err)
}

View File

@ -20,7 +20,7 @@ import (
"github.com/astaxie/beego"
"github.com/astaxie/beego/context"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/util"
)
var logPostOnly bool
@ -47,6 +47,8 @@ type Record struct {
RequestUri string `xorm:"varchar(1000)" json:"requestUri"`
Action string `xorm:"varchar(1000)" json:"action"`
ExtendedUser *User `xorm:"-" json:"extendedUser"`
IsTriggered bool `json:"isTriggered"`
}
@ -99,8 +101,9 @@ func AddRecord(record *Record) bool {
return affected != 0
}
func GetRecordCount() int {
count, err := adapter.Engine.Count(&Record{})
func GetRecordCount(field, value string) int {
session := GetSession("", -1, -1, field, value, "", "")
count, err := session.Count(&Record{})
if err != nil {
panic(err)
}
@ -118,9 +121,10 @@ func GetRecords() []*Record {
return records
}
func GetPaginationRecords(offset, limit int) []*Record {
func GetPaginationRecords(offset, limit int, field, value, sortField, sortOrder string) []*Record {
records := []*Record{}
err := adapter.Engine.Desc("id").Limit(limit, offset).Find(&records)
session := GetSession("", offset, limit, field, value, sortField, sortOrder)
err := session.Find(&records)
if err != nil {
panic(err)
}
@ -141,6 +145,10 @@ func GetRecordsByField(record *Record) []*Record {
func SendWebhooks(record *Record) error {
webhooks := getWebhooksByOrganization(record.Organization)
for _, webhook := range webhooks {
if !webhook.IsEnabled {
continue
}
matched := false
for _, event := range webhook.Events {
if record.Action == event {
@ -150,6 +158,11 @@ func SendWebhooks(record *Record) error {
}
if matched {
if webhook.IsUserExtended {
user := getUser(record.Organization, record.User)
record.ExtendedUser = user
}
err := sendWebhook(webhook, record)
if err != nil {
return err

View File

@ -17,7 +17,7 @@ package object
import (
"fmt"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
)
@ -39,8 +39,9 @@ type Resource struct {
Description string `xorm:"varchar(1000)" json:"description"`
}
func GetResourceCount(owner string, user string) int {
count, err := adapter.Engine.Count(&Resource{Owner: owner, User: user})
func GetResourceCount(owner, user, field, value string) int {
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Resource{User: user})
if err != nil {
panic(err)
}
@ -63,14 +64,15 @@ func GetResources(owner string, user string) []*Resource {
return resources
}
func GetPaginationResources(owner, user string, offset, limit int) []*Resource {
func GetPaginationResources(owner, user string, offset, limit int, field, value, sortField, sortOrder string) []*Resource {
if owner == "built-in" {
owner = ""
user = ""
}
resources := []*Resource{}
err := adapter.Engine.Desc("created_time").Limit(limit, offset).Find(&resources, &Resource{Owner: owner, User: user})
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&resources, &Resource{User: user})
if err != nil {
panic(err)
}

123
object/role.go Normal file
View File

@ -0,0 +1,123 @@
// Copyright 2021 The casbin 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 (
"fmt"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
)
type Role 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"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Users []string `xorm:"mediumtext" json:"users"`
Roles []string `xorm:"mediumtext" json:"roles"`
IsEnabled bool `json:"isEnabled"`
}
func GetRoleCount(owner, field, value string) int {
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Role{})
if err != nil {
panic(err)
}
return int(count)
}
func GetRoles(owner string) []*Role {
roles := []*Role{}
err := adapter.Engine.Desc("created_time").Find(&roles, &Role{Owner: owner})
if err != nil {
panic(err)
}
return roles
}
func GetPaginationRoles(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Role {
roles := []*Role{}
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&roles)
if err != nil {
panic(err)
}
return roles
}
func getRole(owner string, name string) *Role {
if owner == "" || name == "" {
return nil
}
role := Role{Owner: owner, Name: name}
existed, err := adapter.Engine.Get(&role)
if err != nil {
panic(err)
}
if existed {
return &role
} else {
return nil
}
}
func GetRole(id string) *Role {
owner, name := util.GetOwnerAndNameFromId(id)
return getRole(owner, name)
}
func UpdateRole(id string, role *Role) bool {
owner, name := util.GetOwnerAndNameFromId(id)
if getRole(owner, name) == nil {
return false
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(role)
if err != nil {
panic(err)
}
return affected != 0
}
func AddRole(role *Role) bool {
affected, err := adapter.Engine.Insert(role)
if err != nil {
panic(err)
}
return affected != 0
}
func DeleteRole(role *Role) bool {
affected, err := adapter.Engine.ID(core.PK{role.Owner, role.Name}).Delete(&Role{})
if err != nil {
panic(err)
}
return affected != 0
}
func (role *Role) GetId() string {
return fmt.Sprintf("%s/%s", role.Owner, role.Name)
}

View File

@ -20,8 +20,8 @@ import (
"strings"
"github.com/astaxie/beego"
"github.com/casbin/casdoor/storage"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/storage"
"github.com/casdoor/casdoor/util"
)
var isCloudIntranet bool

View File

@ -17,7 +17,7 @@ package object
import (
"fmt"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
)
@ -41,20 +41,23 @@ type Syncer struct {
Port int `json:"port"`
User string `xorm:"varchar(100)" json:"user"`
Password string `xorm:"varchar(100)" json:"password"`
DatabaseType string `xorm:"varchar(100)" json:"databaseType"`
Database string `xorm:"varchar(100)" json:"database"`
Table string `xorm:"varchar(100)" json:"table"`
TablePrimaryKey string `xorm:"varchar(100)" json:"tablePrimaryKey"`
TableColumns []*TableColumn `xorm:"mediumtext" json:"tableColumns"`
AffiliationTable string `xorm:"varchar(100)" json:"affiliationTable"`
AvatarBaseUrl string `xorm:"varchar(100)" json:"avatarBaseUrl"`
ErrorText string `xorm:"mediumtext" json:"errorText"`
SyncInterval int `json:"syncInterval"`
IsEnabled bool `json:"isEnabled"`
Adapter *Adapter `xorm:"-" json:"-"`
}
func GetSyncerCount(owner string) int {
count, err := adapter.Engine.Count(&Syncer{Owner: owner})
func GetSyncerCount(owner, field, value string) int {
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Syncer{})
if err != nil {
panic(err)
}
@ -72,9 +75,10 @@ func GetSyncers(owner string) []*Syncer {
return syncers
}
func GetPaginationSyncers(owner string, offset, limit int) []*Syncer {
func GetPaginationSyncers(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Syncer {
syncers := []*Syncer{}
err := adapter.Engine.Desc("created_time").Limit(limit, offset).Find(&syncers, &Syncer{Owner: owner})
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&syncers)
if err != nil {
panic(err)
}
@ -134,6 +138,26 @@ func UpdateSyncer(id string, syncer *Syncer) bool {
panic(err)
}
if affected == 1 {
addSyncerJob(syncer)
}
return affected != 0
}
func updateSyncerErrorText(syncer *Syncer, line string) bool {
s := getSyncer(syncer.Owner, syncer.Name)
if s == nil {
return false
}
s.ErrorText = s.ErrorText + line
affected, err := adapter.Engine.ID(core.PK{s.Owner, s.Name}).Cols("error_text").Update(s)
if err != nil {
panic(err)
}
return affected != 0
}
@ -143,6 +167,10 @@ func AddSyncer(syncer *Syncer) bool {
panic(err)
}
if affected == 1 {
addSyncerJob(syncer)
}
return affected != 0
}
@ -152,9 +180,29 @@ func DeleteSyncer(syncer *Syncer) bool {
panic(err)
}
if affected == 1 {
deleteSyncerJob(syncer)
}
return affected != 0
}
func (syncer *Syncer) GetId() string {
return fmt.Sprintf("%s/%s", syncer.Owner, syncer.Name)
}
func (syncer *Syncer) getTableColumnsTypeMap() map[string]string {
m := map[string]string{}
for _, tableColumn := range syncer.TableColumns {
m[tableColumn.Name] = tableColumn.Type
}
return m
}
func (syncer *Syncer) getTable() string {
if syncer.DatabaseType == "mssql" {
return fmt.Sprintf("[%s]", syncer.Table)
} else {
return syncer.Table
}
}

View File

@ -14,27 +14,56 @@
package object
import "github.com/mileusna/crontab"
import (
"fmt"
var cronMap map[string]*crontab.Crontab
"github.com/robfig/cron/v3"
)
var cronMap map[string]*cron.Cron
func init() {
cronMap = map[string]*crontab.Crontab{}
cronMap = map[string]*cron.Cron{}
}
func getCrontab(name string) *crontab.Crontab {
ctab, ok := cronMap[name]
func getCronMap(name string) *cron.Cron {
m, ok := cronMap[name]
if !ok {
ctab = crontab.New()
cronMap[name] = ctab
m = cron.New()
cronMap[name] = m
}
return ctab
return m
}
func clearCrontab(name string) {
ctab, ok := cronMap[name]
func clearCron(name string) {
cron, ok := cronMap[name]
if ok {
ctab.Clear()
cron.Stop()
delete(cronMap, name)
}
}
func addSyncerJob(syncer *Syncer) {
deleteSyncerJob(syncer)
if !syncer.IsEnabled {
return
}
syncer.initAdapter()
syncer.syncUsers()
schedule := fmt.Sprintf("@every %ds", syncer.SyncInterval)
cron := getCronMap(syncer.Name)
_, err := cron.AddFunc(schedule, syncer.syncUsers)
if err != nil {
panic(err)
}
cron.Start()
}
func deleteSyncerJob(syncer *Syncer) {
clearCron(syncer.Name)
}

View File

@ -14,13 +14,25 @@
package object
import "fmt"
import (
"fmt"
"time"
)
func (syncer *Syncer) syncUsers() {
fmt.Printf("Running syncUsers()..\n")
users, userMap := syncer.getUserMap()
oUsers, oUserMap := syncer.getOriginalUserMap()
oUsers, oUserMap, err := syncer.getOriginalUserMap()
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())
updateSyncerErrorText(syncer, line)
return
}
fmt.Printf("Users: %d, oUsers: %d\n", len(users), len(oUsers))
var affiliationMap map[int]string
@ -77,7 +89,9 @@ func (syncer *Syncer) syncUsers() {
for _, user := range users {
id := user.Id
if _, ok := oUserMap[id]; !ok {
panic(fmt.Sprintf("New original user: cannot create now, user = %v", user))
newOUser := syncer.createOriginalUserFromUser(user)
syncer.addUser(newOUser)
fmt.Printf("New oUser: %v\n", newOUser)
}
}
}

View File

@ -19,44 +19,54 @@ import (
"strings"
"time"
"github.com/astaxie/beego"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
)
type OriginalUser = User
func (syncer *Syncer) getOriginalUsers() []*OriginalUser {
sql := fmt.Sprintf("select * from %s", syncer.Table)
func (syncer *Syncer) getOriginalUsers() ([]*OriginalUser, error) {
sql := fmt.Sprintf("select * from %s", syncer.getTable())
results, err := syncer.Adapter.Engine.QueryString(sql)
if err != nil {
panic(err)
return nil, err
}
return syncer.getOriginalUsersFromMap(results)
return syncer.getOriginalUsersFromMap(results), nil
}
func (syncer *Syncer) getOriginalUserMap() ([]*OriginalUser, map[string]*OriginalUser) {
users := syncer.getOriginalUsers()
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
return users, m, nil
}
func (syncer *Syncer) addUser(user *OriginalUser) bool {
func (syncer *Syncer) addUser(user *OriginalUser) (bool, error) {
m := syncer.getMapFromOriginalUser(user)
affected, err := syncer.Adapter.Engine.Table(syncer.Table).Insert(m)
keyString, valueString := syncer.getSqlKeyValueStringFromMap(m)
sql := fmt.Sprintf("insert into %s (%s) values (%s)", syncer.getTable(), keyString, valueString)
res, err := syncer.Adapter.Engine.Exec(sql)
if err != nil {
panic(err)
return false, err
}
return affected != 0
affected, err := res.RowsAffected()
if err != nil {
return false, err
}
func (syncer *Syncer) getOriginalColumns() []string {
return affected != 0, nil
}
/*func (syncer *Syncer) getOriginalColumns() []string {
res := []string{}
for _, tableColumn := range syncer.TableColumns {
if tableColumn.CasdoorName != "Id" {
@ -64,7 +74,7 @@ func (syncer *Syncer) getOriginalColumns() []string {
}
}
return res
}
}*/
func (syncer *Syncer) getCasdoorColumns() []string {
res := []string{}
@ -77,22 +87,31 @@ func (syncer *Syncer) getCasdoorColumns() []string {
return res
}
func (syncer *Syncer) updateUser(user *OriginalUser) bool {
func (syncer *Syncer) updateUser(user *OriginalUser) (bool, error) {
m := syncer.getMapFromOriginalUser(user)
columns := syncer.getOriginalColumns()
affected, err := syncer.Adapter.Engine.Table(syncer.Table).ID(syncer.TablePrimaryKey).Cols(columns...).Update(m)
pkValue := m[syncer.TablePrimaryKey]
delete(m, syncer.TablePrimaryKey)
setString := syncer.getSqlSetStringFromMap(m)
sql := fmt.Sprintf("update %s set %s where %s = %s", syncer.getTable(), setString, syncer.TablePrimaryKey, pkValue)
res, err := syncer.Adapter.Engine.Exec(sql)
if err != nil {
panic(err)
return false, err
}
return affected != 0
affected, err := res.RowsAffected()
if err != nil {
return false, err
}
func (syncer *Syncer) updateUserForOriginalFields(user *User) bool {
return affected != 0, nil
}
func (syncer *Syncer) updateUserForOriginalFields(user *User) (bool, error) {
owner, name := util.GetOwnerAndNameFromId(user.GetId())
oldUser := getUser(owner, name)
oldUser := getUserById(owner, name)
if oldUser == nil {
return false
return false, nil
}
if user.Avatar != oldUser.Avatar && user.Avatar != "" {
@ -101,12 +120,12 @@ func (syncer *Syncer) updateUserForOriginalFields(user *User) bool {
columns := syncer.getCasdoorColumns()
columns = append(columns, "affiliation", "hash", "pre_hash")
affected, err := adapter.Engine.ID(core.PK{user.Owner, user.Name}).Cols(columns...).Update(user)
affected, err := adapter.Engine.ID(core.PK{oldUser.Owner, oldUser.Name}).Cols(columns...).Update(user)
if err != nil {
panic(err)
return false, err
}
return affected != 0
return affected != 0, nil
}
func (syncer *Syncer) calculateHash(user *OriginalUser) string {
@ -124,30 +143,25 @@ func (syncer *Syncer) calculateHash(user *OriginalUser) string {
func (syncer *Syncer) initAdapter() {
if syncer.Adapter == nil {
dataSourceName := fmt.Sprintf("%s:%s@tcp(%s:%d)/", syncer.User, syncer.Password, syncer.Host, syncer.Port)
syncer.Adapter = NewAdapter(beego.AppConfig.String("driverName"), dataSourceName, syncer.Database)
var dataSourceName string
if syncer.DatabaseType == "mssql" {
dataSourceName = fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s", syncer.User, syncer.Password, syncer.Host, syncer.Port, syncer.Database)
} else {
dataSourceName = fmt.Sprintf("%s:%s@tcp(%s:%d)/", syncer.User, syncer.Password, syncer.Host, syncer.Port)
}
if !isCloudIntranet {
dataSourceName = strings.ReplaceAll(dataSourceName, "dbi.", "db.")
}
syncer.Adapter = NewAdapter(syncer.DatabaseType, dataSourceName, syncer.Database)
}
}
func RunSyncUsersJob() {
syncers := GetSyncers("admin")
for _, syncer := range syncers {
if !syncer.IsEnabled {
continue
}
syncer.initAdapter()
syncer.syncUsers()
// run at every minute
//schedule := fmt.Sprintf("* * * * %d", syncer.SyncInterval)
schedule := "* * * * *"
ctab := getCrontab(syncer.Name)
err := ctab.AddJob(schedule, syncer.syncUsers)
if err != nil {
panic(err)
}
addSyncerJob(syncer)
}
time.Sleep(time.Duration(1<<63 - 1))

View File

@ -19,7 +19,7 @@ import (
"strconv"
"strings"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/util"
)
func (syncer *Syncer) getFullAvatarUrl(avatar string) string {
@ -27,7 +27,7 @@ func (syncer *Syncer) getFullAvatarUrl(avatar string) string {
return avatar
}
if !strings.HasPrefix(avatar, "https://") {
if !strings.HasPrefix(avatar, "http") {
return fmt.Sprintf("%s%s", syncer.AvatarBaseUrl, avatar)
}
return avatar
@ -76,7 +76,9 @@ func (syncer *Syncer) createUserFromOriginalUser(originalUser *OriginalUser, aff
}
func (syncer *Syncer) createOriginalUserFromUser(user *User) *OriginalUser {
return user
originalUser := *user
originalUser.Avatar = syncer.getPartialAvatarUrl(user.Avatar)
return &originalUser
}
func (syncer *Syncer) setUserByKeyValue(user *User, key string, value string) {
@ -157,7 +159,11 @@ func (syncer *Syncer) setUserByKeyValue(user *User, key string, value string) {
func (syncer *Syncer) getOriginalUsersFromMap(results []map[string]string) []*OriginalUser {
users := []*OriginalUser{}
for _, result := range results {
originalUser := &OriginalUser{}
originalUser := &OriginalUser{
Address: []string{},
Properties: map[string]string{},
}
for _, tableColumn := range syncer.TableColumns {
syncer.setUserByKeyValue(originalUser, tableColumn.CasdoorName, result[tableColumn.Name])
}
@ -211,3 +217,34 @@ func (syncer *Syncer) getMapFromOriginalUser(user *OriginalUser) map[string]stri
return m2
}
func (syncer *Syncer) getSqlSetStringFromMap(m map[string]string) string {
typeMap := syncer.getTableColumnsTypeMap()
tokens := []string{}
for k, v := range m {
token := fmt.Sprintf("%s = %s", k, v)
if typeMap[k] == "string" {
token = fmt.Sprintf("%s = '%s'", k, v)
}
tokens = append(tokens, token)
}
return strings.Join(tokens, ", ")
}
func (syncer *Syncer) getSqlKeyValueStringFromMap(m map[string]string) (string, string) {
typeMap := syncer.getTableColumnsTypeMap()
keys := []string{}
values := []string{}
for k, v := range m {
if typeMap[k] == "string" {
v = fmt.Sprintf("'%s'", v)
}
keys = append(keys, k)
values = append(values, v)
}
return strings.Join(keys, ", "), strings.Join(values, ", ")
}

View File

@ -24,7 +24,7 @@ func (syncer *Syncer) getUserMap() ([]*User, map[string]*User) {
m := map[string]*User{}
for _, user := range users {
m[user.Name] = user
m[user.Id] = user
}
return users, m
}

View File

@ -15,11 +15,13 @@
package object
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"strings"
"time"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
)
@ -43,18 +45,23 @@ type Token struct {
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 {
AccessToken string `json:"access_token"`
IdToken string `json:"id_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
Scope string `json:"scope"`
}
func GetTokenCount(owner string) int {
count, err := adapter.Engine.Count(&Token{Owner: owner})
func GetTokenCount(owner, field, value string) int {
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Token{})
if err != nil {
panic(err)
}
@ -72,9 +79,10 @@ func GetTokens(owner string) []*Token {
return tokens
}
func GetPaginationTokens(owner string, offset, limit int) []*Token {
func GetPaginationTokens(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Token {
tokens := []*Token{}
err := adapter.Engine.Desc("created_time").Limit(limit, offset).Find(&tokens, &Token{Owner: owner})
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&tokens)
if err != nil {
panic(err)
}
@ -101,8 +109,8 @@ func getToken(owner string, name string) *Token {
}
func getTokenByCode(code string) *Token {
token := Token{}
existed, err := adapter.Engine.Where("code=?", code).Get(&token)
token := Token{Code: code}
existed, err := adapter.Engine.Get(&token)
if err != nil {
panic(err)
}
@ -114,6 +122,15 @@ func getTokenByCode(code string) *Token {
return nil
}
func updateUsedByCode(token *Token) bool {
affected, err := adapter.Engine.Where("code=?", token.Code).Cols("code_is_used").Update(token)
if err != nil {
panic(err)
}
return affected != 0
}
func GetToken(id string) *Token {
owner, name := util.GetOwnerAndNameFromId(id)
return getToken(owner, name)
@ -151,6 +168,16 @@ func DeleteToken(token *Token) bool {
return affected != 0
}
func GetTokenByAccessToken(accessToken string) *Token {
//Check if the accessToken is in the database
token := Token{AccessToken: accessToken}
existed, err := adapter.Engine.Get(&token)
if err != nil || !existed {
return nil
}
return &token
}
func CheckOAuthLogin(clientId string, responseType string, redirectUri string, scope string, state string) (string, *Application) {
if responseType != "code" {
return "response_type should be \"code\"", nil
@ -177,7 +204,7 @@ func CheckOAuthLogin(clientId string, responseType string, redirectUri string, s
return "", application
}
func GetOAuthCode(userId string, clientId string, responseType string, redirectUri string, scope string, state string, nonce string) *Code {
func GetOAuthCode(userId string, clientId string, responseType string, redirectUri string, scope string, state string, nonce string, challenge string) *Code {
user := GetUser(userId)
if user == nil {
return &Code{
@ -185,6 +212,12 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU
Code: "",
}
}
if user.IsForbidden {
return &Code{
Message: "error: the user is forbidden to sign in, please contact the administrator",
Code: "",
}
}
msg, application := CheckOAuthLogin(clientId, responseType, redirectUri, scope, state)
if msg != "" {
@ -194,11 +227,15 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU
}
}
accessToken, refreshToken, err := generateJwtToken(application, user, nonce)
accessToken, refreshToken, err := generateJwtToken(application, user, nonce, scope)
if err != nil {
panic(err)
}
if challenge == "null" {
challenge = ""
}
token := &Token{
Owner: application.Owner,
Name: util.GenerateId(),
@ -212,6 +249,9 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU
ExpiresIn: application.ExpireInHours * 60,
Scope: scope,
TokenType: "Bearer",
CodeChallenge: challenge,
CodeIsUsed: false,
CodeExpireIn: time.Now().Add(time.Minute * 5).Unix(),
}
AddToken(token)
@ -221,7 +261,7 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU
}
}
func GetOAuthToken(grantType string, clientId string, clientSecret string, code string) *TokenWrapper {
func GetOAuthToken(grantType string, clientId string, clientSecret string, code string, verifier string) *TokenWrapper {
application := GetApplicationByClientId(clientId)
if application == nil {
return &TokenWrapper{
@ -277,10 +317,41 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
Scope: "",
}
}
if token.CodeChallenge != "" && pkceChallenge(verifier) != token.CodeChallenge {
return &TokenWrapper{
AccessToken: "error: incorrect code_verifier",
TokenType: "",
ExpiresIn: 0,
Scope: "",
}
}
if token.CodeIsUsed {
//Resist replay attacks, if the code is reused, the token generated with this code will be deleted
DeleteToken(token)
return &TokenWrapper{
AccessToken: "error: code has been used.",
TokenType: "",
ExpiresIn: 0,
Scope: "",
}
}
if time.Now().Unix() > token.CodeExpireIn {
//can only use the code to generate a token within five minutes
DeleteToken(token)
return &TokenWrapper{
AccessToken: "error: code has expired",
TokenType: "",
ExpiresIn: 0,
Scope: "",
}
}
token.CodeIsUsed = true
updateUsedByCode(token)
tokenWrapper := &TokenWrapper{
AccessToken: token.AccessToken,
IdToken: token.AccessToken,
RefreshToken: token.RefreshToken,
TokenType: token.TokenType,
ExpiresIn: token.ExpiresIn,
Scope: token.Scope,
@ -289,52 +360,66 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
return tokenWrapper
}
func RefreshToken(grantType string, refreshToken string, scope string, clientId string, clientSecret string) *Code {
func RefreshToken(grantType string, refreshToken string, scope string, clientId string, clientSecret string) *TokenWrapper {
// check parameters
if grantType != "refresh_token" {
return &Code{
Message: "error: grant_type should be \"refresh_token\"",
Code: "",
return &TokenWrapper{
AccessToken: "error: grant_type should be \"refresh_token\"",
TokenType: "",
ExpiresIn: 0,
Scope: "",
}
}
application := GetApplicationByClientId(clientId)
if application == nil {
return &Code{
Message: "error: invalid client_id",
Code: "",
return &TokenWrapper{
AccessToken: "error: invalid client_id",
TokenType: "",
ExpiresIn: 0,
Scope: "",
}
}
if application.ClientSecret != clientSecret {
return &Code{
Message: "error: invalid client_secret",
Code: "",
return &TokenWrapper{
AccessToken: "error: invalid client_secret",
TokenType: "",
ExpiresIn: 0,
Scope: "",
}
}
// check whether the refresh token is valid, and has not expired.
token := Token{RefreshToken: refreshToken}
existed, err := adapter.Engine.Get(&token)
if err != nil || !existed {
return &Code{
Message: "error: invalid refresh_token",
Code: "",
return &TokenWrapper{
AccessToken: "error: invalid refresh_token",
TokenType: "",
ExpiresIn: 0,
Scope: "",
}
}
claims, err := ParseJwtToken(refreshToken)
cert := getCertByApplication(application)
_, err = ParseJwtToken(refreshToken, cert)
if err != nil {
return &Code{
Message: "error: invalid refresh_token",
Code: "",
}
}
if time.Now().Unix() > claims.ExpiresAt.Unix() {
return &Code{
Message: "error: expired refresh_token",
Code: "",
return &TokenWrapper{
AccessToken: fmt.Sprintf("error: %s", err.Error()),
TokenType: "",
ExpiresIn: 0,
Scope: "",
}
}
// generate a new token
user := getUser(application.Owner, token.User)
newAccessToken, newRefreshToken, err := generateJwtToken(application, user, "")
user := getUser(application.Organization, token.User)
if user.IsForbidden {
return &TokenWrapper{
AccessToken: "error: the user is forbidden to sign in, please contact the administrator",
TokenType: "",
ExpiresIn: 0,
Scope: "",
}
}
newAccessToken, newRefreshToken, err := generateJwtToken(application, user, "", scope)
if err != nil {
panic(err)
}
@ -355,8 +440,21 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
}
AddToken(newToken)
return &Code{
Message: "",
Code: token.Code,
tokenWrapper := &TokenWrapper{
AccessToken: token.AccessToken,
IdToken: token.AccessToken,
RefreshToken: token.RefreshToken,
TokenType: token.TokenType,
ExpiresIn: token.ExpiresIn,
Scope: token.Scope,
}
return tokenWrapper
}
// PkceChallenge: base64-URL-encoded SHA256 hash of verifier, per rfc 7636
func pkceChallenge(verifier string) string {
sum := sha256.Sum256([]byte(verifier))
challenge := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(sum[:])
return challenge
}

View File

@ -23,21 +23,45 @@ import (
"github.com/golang-jwt/jwt/v4"
)
//go:embed token_jwt_key.pem
var tokenJwtPublicKey string
//go:embed token_jwt_key.key
var tokenJwtPrivateKey string
type Claims struct {
*User
Name string `json:"name,omitempty"`
Owner string `json:"owner,omitempty"`
Nonce string `json:"nonce,omitempty"`
Tag string `json:"tag,omitempty"`
Scope string `json:"scope,omitempty"`
jwt.RegisteredClaims
}
func generateJwtToken(application *Application, user *User, nonce string) (string, string, error) {
type UserShort struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
}
type ClaimsShort struct {
*UserShort
Nonce string `json:"nonce,omitempty"`
Scope string `json:"scope,omitempty"`
jwt.RegisteredClaims
}
func getShortUser(user *User) *UserShort {
res := &UserShort{
Owner: user.Owner,
Name: user.Name,
}
return res
}
func getShortClaims(claims Claims) ClaimsShort {
res := ClaimsShort{
UserShort: getShortUser(claims.User),
Nonce: claims.Nonce,
Scope: claims.Scope,
RegisteredClaims: claims.RegisteredClaims,
}
return res
}
func generateJwtToken(application *Application, user *User, nonce string, scope string) (string, string, error) {
nowTime := time.Now()
expireTime := nowTime.Add(time.Duration(application.ExpireInHours) * time.Hour)
refreshExpireTime := nowTime.Add(time.Duration(application.RefreshExpireInHours) * time.Hour)
@ -47,6 +71,9 @@ func generateJwtToken(application *Application, user *User, nonce string) (strin
claims := Claims{
User: user,
Nonce: nonce,
// FIXME: A workaround for custom claim by reusing `tag` in user info
Tag: user.Tag,
Scope: scope,
RegisteredClaims: jwt.RegisteredClaims{
Issuer: beego.AppConfig.String("origin"),
Subject: user.Id,
@ -57,24 +84,32 @@ func generateJwtToken(application *Application, user *User, nonce string) (strin
ID: "",
},
}
//all fields of the User struct are not added in "JWT-Empty" format
var token *jwt.Token
var refreshToken *jwt.Token
// the JWT token length in "JWT-Empty" mode will be very short, as User object only has two properties: owner and name
if application.TokenFormat == "JWT-Empty" {
claims.User = nil
}
claims.Name = user.Name
claims.Owner = user.Owner
claimsShort := getShortClaims(claims)
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
token = jwt.NewWithClaims(jwt.SigningMethodRS256, claimsShort)
claimsShort.ExpiresAt = jwt.NewNumericDate(refreshExpireTime)
refreshToken = jwt.NewWithClaims(jwt.SigningMethodRS256, claimsShort)
} else {
token = jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
claims.ExpiresAt = jwt.NewNumericDate(refreshExpireTime)
refreshToken := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
refreshToken = jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
}
// Use "token_jwt_key.key" as RSA private key
privateKey := tokenJwtPrivateKey
key, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(privateKey))
cert := getCertByApplication(application)
// RSA private key
key, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(cert.PrivateKey))
if err != nil {
return "", "", err
}
token.Header["kid"] = cert.Name
tokenString, err := token.SignedString(key)
if err != nil {
return "", "", err
@ -84,14 +119,14 @@ func generateJwtToken(application *Application, user *User, nonce string) (strin
return tokenString, refreshTokenString, err
}
func ParseJwtToken(token string) (*Claims, error) {
func ParseJwtToken(token string, cert *Cert) (*Claims, error) {
t, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
// Use "token_jwt_key.pem" as RSA public key
publicKey, err := jwt.ParseRSAPublicKeyFromPEM([]byte(tokenJwtPublicKey))
// RSA public key
publicKey, err := jwt.ParseRSAPublicKeyFromPEM([]byte(cert.PublicKey))
if err != nil {
return nil, err
}

View File

@ -20,19 +20,14 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"time"
"github.com/casbin/casdoor/util"
)
func generateRsaKeys(fileId string) {
func generateRsaKeys(bitSize int, expireInYears int, commonName string, organization string) (string, string) {
// 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
bitSize := 4096
// Generate RSA key.
key, err := rsa.GenerateKey(rand.Reader, bitSize)
if err != nil {
@ -50,12 +45,12 @@ func generateRsaKeys(fileId string) {
tml := x509.Certificate{
// you can add any attr that you need
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(20, 0, 0),
NotAfter: time.Now().AddDate(expireInYears, 0, 0),
// you have to generate a different serial number each execution
SerialNumber: big.NewInt(123456),
Subject: pkix.Name{
CommonName: "Casdoor Cert",
Organization: []string{"Casdoor Organization"},
CommonName: commonName,
Organization: []string{organization},
},
BasicConstraintsValid: true,
}
@ -70,9 +65,5 @@ func generateRsaKeys(fileId string) {
Bytes: cert,
})
// Write private key to file.
util.WriteBytesToPath(privateKeyPem, fmt.Sprintf("%s.key", fileId))
// Write certificate (aka public key) to file.
util.WriteBytesToPath(certPem, fmt.Sprintf("%s.pem", fileId))
return string(certPem), string(privateKeyPem)
}

View File

@ -14,9 +14,20 @@
package object
import "testing"
import (
"fmt"
"testing"
"github.com/casdoor/casdoor/util"
)
func TestGenerateRsaKeys(t *testing.T) {
fileId := "token_jwt_key"
generateRsaKeys(fileId)
publicKey, privateKey := generateRsaKeys(4096, 20, "Casdoor Cert", "Casdoor Organization")
// Write certificate (aka public key) to file.
util.WriteStringToPath(publicKey, fmt.Sprintf("%s.pem", fileId))
// Write private key to file.
util.WriteStringToPath(privateKey, fmt.Sprintf("%s.key", fileId))
}

View File

@ -16,8 +16,9 @@ package object
import (
"fmt"
"strings"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
)
@ -27,21 +28,21 @@ type User struct {
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
Id string `xorm:"varchar(100)" json:"id"`
Id string `xorm:"varchar(100) index" json:"id"`
Type string `xorm:"varchar(100)" json:"type"`
Password string `xorm:"varchar(100)" json:"password"`
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Avatar string `xorm:"varchar(255)" json:"avatar"`
PermanentAvatar string `xorm:"varchar(255)" json:"permanentAvatar"`
Email string `xorm:"varchar(100)" json:"email"`
Phone string `xorm:"varchar(100)" json:"phone"`
Avatar string `xorm:"varchar(500)" json:"avatar"`
PermanentAvatar string `xorm:"varchar(500)" json:"permanentAvatar"`
Email string `xorm:"varchar(100) index" json:"email"`
Phone string `xorm:"varchar(100) index" json:"phone"`
Location string `xorm:"varchar(100)" json:"location"`
Address []string `json:"address"`
Affiliation string `xorm:"varchar(100)" json:"affiliation"`
Title string `xorm:"varchar(100)" json:"title"`
IdCardType string `xorm:"varchar(100)" json:"idCardType"`
IdCard string `xorm:"varchar(100)" json:"idCard"`
IdCard string `xorm:"varchar(100) index" json:"idCard"`
Homepage string `xorm:"varchar(100)" json:"homepage"`
Bio string `xorm:"varchar(100)" json:"bio"`
Tag string `xorm:"varchar(100)" json:"tag"`
@ -78,6 +79,7 @@ type User struct {
Wecom string `xorm:"wecom varchar(100)" json:"wecom"`
Lark string `xorm:"lark varchar(100)" json:"lark"`
Gitlab string `xorm:"gitlab varchar(100)" json:"gitlab"`
Baidu string `xorm:"baidu varchar(100)" json:"baidu"`
Apple string `xorm:"apple varchar(100)" json:"apple"`
AzureAD string `xorm:"azuread varchar(100)" json:"azuread"`
Slack string `xorm:"slack varchar(100)" json:"slack"`
@ -86,8 +88,9 @@ type User struct {
Properties map[string]string `json:"properties"`
}
func GetGlobalUserCount() int {
count, err := adapter.Engine.Count(&User{})
func GetGlobalUserCount(field, value string) int {
session := GetSession("", -1, -1, field, value, "", "")
count, err := session.Count(&User{})
if err != nil {
panic(err)
}
@ -105,9 +108,10 @@ func GetGlobalUsers() []*User {
return users
}
func GetPaginationGlobalUsers(offset, limit int) []*User {
func GetPaginationGlobalUsers(offset, limit int, field, value, sortField, sortOrder string) []*User {
users := []*User{}
err := adapter.Engine.Desc("created_time").Limit(limit, offset).Find(&users)
session := GetSession("", offset, limit, field, value, sortField, sortOrder)
err := session.Find(&users)
if err != nil {
panic(err)
}
@ -115,8 +119,9 @@ func GetPaginationGlobalUsers(offset, limit int) []*User {
return users
}
func GetUserCount(owner string) int {
count, err := adapter.Engine.Count(&User{Owner: owner})
func GetUserCount(owner, field, value string) int {
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&User{})
if err != nil {
panic(err)
}
@ -153,9 +158,10 @@ func GetSortedUsers(owner string, sorter string, limit int) []*User {
return users
}
func GetPaginationUsers(owner string, offset, limit int) []*User {
func GetPaginationUsers(owner string, offset, limit int, field, value, sortField, sortOrder string) []*User {
users := []*User{}
err := adapter.Engine.Desc("created_time").Limit(limit, offset).Find(&users, &User{Owner: owner})
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&users)
if err != nil {
panic(err)
}
@ -181,6 +187,24 @@ func getUser(owner string, name string) *User {
}
}
func getUserById(owner string, id string) *User {
if owner == "" || id == "" {
return nil
}
user := User{Owner: owner, Id: id}
existed, err := adapter.Engine.Get(&user)
if err != nil {
panic(err)
}
if existed {
return &user
} else {
return nil
}
}
func GetUserByEmail(owner string, email string) *User {
if owner == "" || email == "" {
return nil
@ -241,7 +265,7 @@ func GetLastUser(owner string) *User {
return nil
}
func UpdateUser(id string, user *User, columns []string) bool {
func UpdateUser(id string, user *User, columns []string, isGlobalAdmin bool) bool {
owner, name := util.GetOwnerAndNameFromIdNoCheck(id)
oldUser := getUser(owner, name)
if oldUser == nil {
@ -256,9 +280,12 @@ func UpdateUser(id string, user *User, columns []string) bool {
if len(columns) == 0 {
columns = []string{"owner", "display_name", "avatar",
"location", "address", "region", "language", "affiliation", "title", "homepage", "bio", "score", "tag",
"location", "address", "region", "language", "affiliation", "title", "homepage", "bio", "score", "tag", "signup_application",
"is_admin", "is_global_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties"}
}
if isGlobalAdmin {
columns = append(columns, "name")
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).Cols(columns...).Update(user)
if err != nil {
@ -319,9 +346,10 @@ func AddUsers(users []*User) bool {
return false
}
organization := GetOrganizationByUser(users[0])
//organization := GetOrganizationByUser(users[0])
for _, user := range users {
user.UpdateUserPassword(organization)
// this function is only used for syncer or batch upload, so no need to encrypt the password
//user.UpdateUserPassword(organization)
user.UpdateUserHash()
user.PreHash = user.Hash
@ -379,3 +407,7 @@ func LinkUserAccount(user *User, field string, value string) bool {
func (user *User) GetId() string {
return fmt.Sprintf("%s/%s", user.Owner, user.Name)
}
func isUserIdGlobalAdmin(userId string) bool {
return strings.HasPrefix(userId, "built-in/")
}

View File

@ -14,7 +14,7 @@
package object
import "github.com/casbin/casdoor/cred"
import "github.com/casdoor/casdoor/cred"
func calculateHash(user *User) string {
syncer := getDbSyncerForUser(user)

View File

@ -19,7 +19,7 @@ import (
"reflect"
"testing"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
)

114
object/user_upload.go Normal file
View File

@ -0,0 +1,114 @@
// Copyright 2021 The casbin 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"
"github.com/casdoor/casdoor/xlsx"
)
func getUserMap(owner string) map[string]*User {
m := map[string]*User{}
users := GetUsers(owner)
for _, user := range users {
m[user.GetId()] = user
}
return m
}
func parseLineItem(line *[]string, i int) string {
if i >= len(*line) {
return ""
} else {
return (*line)[i]
}
}
func parseLineItemInt(line *[]string, i int) int {
s := parseLineItem(line, i)
return util.ParseInt(s)
}
func parseLineItemBool(line *[]string, i int) bool {
return parseLineItemInt(line, i) != 0
}
func UploadUsers(owner string, fileId string) bool {
table := xlsx.ReadXlsxFile(fileId)
oldUserMap := getUserMap(owner)
newUsers := []*User{}
for _, line := range table {
if parseLineItem(&line, 0) == "" {
continue
}
user := &User{
Owner: parseLineItem(&line, 0),
Name: parseLineItem(&line, 1),
CreatedTime: parseLineItem(&line, 2),
UpdatedTime: parseLineItem(&line, 3),
Id: parseLineItem(&line, 4),
Type: parseLineItem(&line, 5),
Password: parseLineItem(&line, 6),
PasswordSalt: parseLineItem(&line, 7),
DisplayName: parseLineItem(&line, 8),
Avatar: parseLineItem(&line, 9),
PermanentAvatar: "",
Email: parseLineItem(&line, 10),
Phone: parseLineItem(&line, 11),
Location: parseLineItem(&line, 12),
Address: []string{parseLineItem(&line, 13)},
Affiliation: parseLineItem(&line, 14),
Title: parseLineItem(&line, 15),
IdCardType: parseLineItem(&line, 16),
IdCard: parseLineItem(&line, 17),
Homepage: parseLineItem(&line, 18),
Bio: parseLineItem(&line, 19),
Tag: parseLineItem(&line, 20),
Region: parseLineItem(&line, 21),
Language: parseLineItem(&line, 22),
Gender: parseLineItem(&line, 23),
Birthday: parseLineItem(&line, 24),
Education: parseLineItem(&line, 25),
Score: parseLineItemInt(&line, 26),
Ranking: parseLineItemInt(&line, 27),
IsDefaultAvatar: false,
IsOnline: parseLineItemBool(&line, 28),
IsAdmin: parseLineItemBool(&line, 29),
IsGlobalAdmin: parseLineItemBool(&line, 30),
IsForbidden: parseLineItemBool(&line, 31),
IsDeleted: parseLineItemBool(&line, 32),
SignupApplication: parseLineItem(&line, 33),
Hash: "",
PreHash: "",
CreatedIp: parseLineItem(&line, 34),
LastSigninTime: parseLineItem(&line, 35),
LastSigninIp: parseLineItem(&line, 36),
Properties: map[string]string{},
}
if _, ok := oldUserMap[user.GetId()]; !ok {
newUsers = append(newUsers, user)
}
}
if len(newUsers) == 0 {
return false
}
return AddUsersInBatch(newUsers)
}

View File

@ -19,7 +19,7 @@ import (
"reflect"
"strings"
"github.com/casbin/casdoor/idp"
"github.com/casdoor/casdoor/idp"
"xorm.io/core"
)
@ -64,6 +64,12 @@ func GetUserByFields(organization string, field string) *User {
return user
}
// check ID card
user = GetUserByField(organization, "id_card", field)
if user != nil {
return user
}
return nil
}

View File

@ -21,7 +21,7 @@ import (
"time"
"github.com/astaxie/beego"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
)

View File

@ -17,24 +17,34 @@ package object
import (
"fmt"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
)
type Header struct {
Name string `json:"name"`
Value string `json:"value"`
}
type Webhook 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"`
Url string `xorm:"varchar(100)" json:"url"`
ContentType string `xorm:"varchar(100)" json:"contentType"`
Events []string `xorm:"varchar(100)" json:"events"`
Organization string `xorm:"varchar(100) index" json:"organization"`
Url string `xorm:"varchar(100)" json:"url"`
Method string `xorm:"varchar(100)" json:"method"`
ContentType string `xorm:"varchar(100)" json:"contentType"`
Headers []*Header `xorm:"mediumtext" json:"headers"`
Events []string `xorm:"varchar(100)" json:"events"`
IsUserExtended bool `json:"isUserExtended"`
IsEnabled bool `json:"isEnabled"`
}
func GetWebhookCount(owner string) int {
count, err := adapter.Engine.Count(&Webhook{Owner: owner})
func GetWebhookCount(owner, field, value string) int {
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Webhook{})
if err != nil {
panic(err)
}
@ -52,9 +62,10 @@ func GetWebhooks(owner string) []*Webhook {
return webhooks
}
func GetPaginationWebhooks(owner string, offset, limit int) []*Webhook {
func GetPaginationWebhooks(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Webhook {
webhooks := []*Webhook{}
err := adapter.Engine.Desc("created_time").Limit(limit, offset).Find(&webhooks, &Webhook{Owner: owner})
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&webhooks)
if err != nil {
panic(err)
}

View File

@ -18,7 +18,7 @@ import (
"net/http"
"strings"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/util"
)
func sendWebhook(webhook *Webhook, record *Record) error {
@ -26,13 +26,17 @@ func sendWebhook(webhook *Webhook, record *Record) error {
body := strings.NewReader(util.StructToJson(record))
req, err := http.NewRequest("POST", webhook.Url, body)
req, err := http.NewRequest(webhook.Method, webhook.Url, body)
if err != nil {
return err
}
req.Header.Set("Content-Type", webhook.ContentType)
for _, header := range webhook.Headers {
req.Header.Set(header.Name, header.Value)
}
_, err = client.Do(req)
return err
}

View File

@ -20,8 +20,8 @@ import (
"net/http"
"github.com/astaxie/beego/context"
"github.com/casbin/casdoor/authz"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/authz"
"github.com/casdoor/casdoor/util"
)
type Object struct {

View File

@ -16,11 +16,10 @@ package routers
import (
"fmt"
"time"
"github.com/astaxie/beego/context"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
func AutoSigninFilter(ctx *context.Context) {
@ -28,20 +27,28 @@ func AutoSigninFilter(ctx *context.Context) {
// return
//}
// "/page?access_token=123"
// GET parameter like "/page?access_token=123" or
// HTTP Bearer token like "Authorization: Bearer 123"
accessToken := ctx.Input.Query("accessToken")
if accessToken == "" {
accessToken = parseBearerToken(ctx)
}
if accessToken != "" {
claims, err := object.ParseJwtToken(accessToken)
if err != nil {
responseError(ctx, "invalid JWT token")
token := object.GetTokenByAccessToken(accessToken)
if token == nil {
responseError(ctx, "Access token doesn't exist")
return
}
if time.Now().Unix() > claims.ExpiresAt.Unix() {
responseError(ctx, "expired JWT token")
if !util.IsTokenExpired(token.CreatedTime, token.ExpiresIn) {
responseError(ctx, "Access token has expired")
return
}
userId := fmt.Sprintf("%s/%s", claims.User.Owner, claims.User.Name)
userId := fmt.Sprintf("%s/%s", token.Organization, token.User)
application, _ := object.GetApplicationByUserId(fmt.Sprintf("app/%s", token.Application))
setSessionUser(ctx, userId)
setSessionOidc(ctx, token.Scope, application.ClientId)
return
}
@ -67,17 +74,4 @@ func AutoSigninFilter(ctx *context.Context) {
return
}
// HTTP Bearer token
// Authorization: Bearer bearerToken
bearerToken := parseBearerToken(ctx)
if bearerToken != "" {
claims, err := object.ParseJwtToken(bearerToken)
if err != nil {
responseError(ctx, err.Error())
return
}
setSessionUser(ctx, fmt.Sprintf("%s/%s", claims.Owner, claims.Name))
setSessionExpire(ctx, claims.ExpiresAt.Unix())
}
}

View File

@ -19,8 +19,8 @@ import (
"strings"
"github.com/astaxie/beego/context"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
type Response struct {
@ -97,6 +97,18 @@ func setSessionExpire(ctx *context.Context, ExpireTime int64) {
ctx.Input.CruSession.SessionRelease(ctx.ResponseWriter)
}
func setSessionOidc(ctx *context.Context, scope string, aud string) {
err := ctx.Input.CruSession.Set("scope", scope)
if err != nil {
panic(err)
}
err = ctx.Input.CruSession.Set("aud", aud)
if err != nil {
panic(err)
}
ctx.Input.CruSession.SessionRelease(ctx.ResponseWriter)
}
func parseBearerToken(ctx *context.Context) string {
header := ctx.Request.Header.Get("Authorization")
tokens := strings.Split(header, " ")

View File

@ -18,8 +18,8 @@ import (
"fmt"
"github.com/astaxie/beego/context"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
func getUser(ctx *context.Context) (username string) {

View File

@ -22,7 +22,7 @@ package routers
import (
"github.com/astaxie/beego"
"github.com/casbin/casdoor/controllers"
"github.com/casdoor/casdoor/controllers"
)
func init() {
@ -50,6 +50,7 @@ func initAPI() {
beego.Router("/api/get-app-login", &controllers.ApiController{}, "GET:GetApplicationLogin")
beego.Router("/api/logout", &controllers.ApiController{}, "POST:Logout")
beego.Router("/api/get-account", &controllers.ApiController{}, "GET:GetAccount")
beego.Router("/api/userinfo", &controllers.ApiController{}, "GET:GetUserinfo")
beego.Router("/api/unlink", &controllers.ApiController{}, "POST:Unlink")
beego.Router("/api/get-saml-login", &controllers.ApiController{}, "GET:GetSamlLogin")
beego.Router("/api/acs", &controllers.ApiController{}, "POST:HandleSamlLogin")
@ -68,6 +69,19 @@ func initAPI() {
beego.Router("/api/update-user", &controllers.ApiController{}, "POST:UpdateUser")
beego.Router("/api/add-user", &controllers.ApiController{}, "POST:AddUser")
beego.Router("/api/delete-user", &controllers.ApiController{}, "POST:DeleteUser")
beego.Router("/api/upload-users", &controllers.ApiController{}, "POST:UploadUsers")
beego.Router("/api/get-roles", &controllers.ApiController{}, "GET:GetRoles")
beego.Router("/api/get-role", &controllers.ApiController{}, "GET:GetRole")
beego.Router("/api/update-role", &controllers.ApiController{}, "POST:UpdateRole")
beego.Router("/api/add-role", &controllers.ApiController{}, "POST:AddRole")
beego.Router("/api/delete-role", &controllers.ApiController{}, "POST:DeleteRole")
beego.Router("/api/get-permissions", &controllers.ApiController{}, "GET:GetPermissions")
beego.Router("/api/get-permission", &controllers.ApiController{}, "GET:GetPermission")
beego.Router("/api/update-permission", &controllers.ApiController{}, "POST:UpdatePermission")
beego.Router("/api/add-permission", &controllers.ApiController{}, "POST:AddPermission")
beego.Router("/api/delete-permission", &controllers.ApiController{}, "POST:DeletePermission")
beego.Router("/api/set-password", &controllers.ApiController{}, "POST:SetPassword")
beego.Router("/api/check-user-password", &controllers.ApiController{}, "POST:CheckUserPassword")
@ -112,6 +126,7 @@ func initAPI() {
beego.Router("/api/delete-token", &controllers.ApiController{}, "POST:DeleteToken")
beego.Router("/api/login/oauth/code", &controllers.ApiController{}, "POST:GetOAuthCode")
beego.Router("/api/login/oauth/access_token", &controllers.ApiController{}, "POST:GetOAuthToken")
beego.Router("/api/login/oauth/refresh_token", &controllers.ApiController{}, "POST:RefreshToken")
beego.Router("/api/get-records", &controllers.ApiController{}, "GET:GetRecords")
beego.Router("/api/get-records-filter", &controllers.ApiController{}, "POST:GetRecordsByFilter")
@ -128,6 +143,12 @@ func initAPI() {
beego.Router("/api/add-syncer", &controllers.ApiController{}, "POST:AddSyncer")
beego.Router("/api/delete-syncer", &controllers.ApiController{}, "POST:DeleteSyncer")
beego.Router("/api/get-certs", &controllers.ApiController{}, "GET:GetCerts")
beego.Router("/api/get-cert", &controllers.ApiController{}, "GET:GetCert")
beego.Router("/api/update-cert", &controllers.ApiController{}, "POST:UpdateCert")
beego.Router("/api/add-cert", &controllers.ApiController{}, "POST:AddCert")
beego.Router("/api/delete-cert", &controllers.ApiController{}, "POST:DeleteCert")
beego.Router("/api/send-email", &controllers.ApiController{}, "POST:SendEmail")
beego.Router("/api/send-sms", &controllers.ApiController{}, "POST:SendSms")

View File

@ -19,7 +19,7 @@ import (
"strings"
"github.com/astaxie/beego/context"
"github.com/casbin/casdoor/util"
"github.com/casdoor/casdoor/util"
)
func StaticFilter(ctx *context.Context) {

View File

@ -18,6 +18,7 @@ import (
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
)
@ -28,6 +29,24 @@ func FileExist(path string) bool {
return true
}
func GetPath(path string) string {
return filepath.Dir(path)
}
func EnsureFileFolderExists(path string) {
p := GetPath(path)
if !FileExist(p) {
err := os.MkdirAll(p, os.ModePerm)
if err != nil {
panic(err)
}
}
}
func RemoveExt(filename string) string {
return filename[:len(filename)-len(filepath.Ext(filename))]
}
func UrlJoin(base string, path string) string {
res := fmt.Sprintf("%s/%s", strings.TrimRight(base, "/"), strings.TrimLeft(path, "/"))
return res

21
util/setting.go Normal file
View File

@ -0,0 +1,21 @@
// Copyright 2021 The casbin 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 util
import "fmt"
func GetUploadXlsxPath(fileId string) string {
return fmt.Sprintf("tmpFiles/%s.xlsx", fileId)
}

View File

@ -23,6 +23,7 @@ import (
"os"
"strconv"
"strings"
"unicode"
"github.com/google/uuid"
)
@ -162,3 +163,32 @@ func WriteBytesToPath(b []byte, path string) {
panic(err)
}
}
// SnakeString XxYy to xx_yy
func SnakeString(s string) string {
data := make([]byte, 0, len(s)*2)
j := false
num := len(s)
for i := 0; i < num; i++ {
d := s[i]
if i > 0 && d >= 'A' && d <= 'Z' && j {
data = append(data, '_')
}
if d != '_' {
j = true
}
data = append(data, d)
}
return strings.ToLower(string(data[:]))
}
func IsChinese(str string) bool {
var flag bool
for _, v := range str {
if unicode.Is(unicode.Han, v) {
flag = true
break
}
}
return flag
}

View File

@ -28,3 +28,9 @@ func GetCurrentTime() string {
func GetCurrentUnixTime() string {
return strconv.FormatInt(time.Now().UnixNano(), 10)
}
func IsTokenExpired(createdTime string, expiresIn int) bool {
createdTimeObj, _ := time.Parse(time.RFC3339, createdTime)
expiresAtObj := createdTimeObj.Add(time.Duration(expiresIn) * time.Minute)
return time.Now().Before(expiresAtObj)
}

View File

@ -1,6 +1,14 @@
const CracoLessPlugin = require('craco-less');
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
}
},
},
plugins: [
{
plugin: CracoLessPlugin,

View File

@ -1,4 +1,4 @@
project_id: '463556'
project_id: '491513'
api_token_env: 'CROWDIN_PERSONAL_TOKEN'
preserve_hierarchy: true
files: [

View File

@ -13,9 +13,11 @@
"codemirror": "^5.61.1",
"copy-to-clipboard": "^3.3.1",
"craco-less": "^1.17.1",
"file-saver": "^2.0.5",
"i18n-iso-countries": "^7.0.0",
"i18next": "^19.8.9",
"moment": "^2.29.1",
"qs": "^6.10.2",
"react": "^17.0.2",
"react-codemirror2": "^7.2.1",
"react-cropper": "^2.1.7",
@ -23,6 +25,7 @@
"react-dom": "^17.0.2",
"react-github-corner": "^2.5.0",
"react-helmet": "^6.1.0",
"react-highlight-words": "^0.17.0",
"react-i18next": "^11.8.7",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",

View File

@ -23,12 +23,15 @@ import OrganizationListPage from "./OrganizationListPage";
import OrganizationEditPage from "./OrganizationEditPage";
import UserListPage from "./UserListPage";
import UserEditPage from "./UserEditPage";
import RoleListPage from "./RoleListPage";
import RoleEditPage from "./RoleEditPage";
import PermissionListPage from "./PermissionListPage";
import PermissionEditPage from "./PermissionEditPage";
import ProviderListPage from "./ProviderListPage";
import ProviderEditPage from "./ProviderEditPage";
import ApplicationListPage from "./ApplicationListPage";
import ApplicationEditPage from "./ApplicationEditPage";
import ResourceListPage from "./ResourceListPage";
// import ResourceEditPage from "./ResourceEditPage";
import LdapEditPage from "./LdapEditPage";
import LdapSyncPage from "./LdapSyncPage";
import TokenListPage from "./TokenListPage";
@ -38,6 +41,8 @@ import WebhookListPage from "./WebhookListPage";
import WebhookEditPage from "./WebhookEditPage";
import SyncerListPage from "./SyncerListPage";
import SyncerEditPage from "./SyncerEditPage";
import CertListPage from "./CertListPage";
import CertEditPage from "./CertEditPage";
import AccountPage from "./account/AccountPage";
import HomePage from "./basic/HomePage";
import CustomGithubCorner from "./CustomGithubCorner";
@ -101,6 +106,10 @@ class App extends Component {
this.setState({ selectedMenuKey: '/organizations' });
} else if (uri.includes('/users')) {
this.setState({ selectedMenuKey: '/users' });
} else if (uri.includes('/roles')) {
this.setState({ selectedMenuKey: '/roles' });
} else if (uri.includes('/permissions')) {
this.setState({ selectedMenuKey: '/permissions' });
} else if (uri.includes('/providers')) {
this.setState({ selectedMenuKey: '/providers' });
} else if (uri.includes('/applications')) {
@ -115,6 +124,8 @@ class App extends Component {
this.setState({ selectedMenuKey: '/webhooks' });
} else if (uri.includes('/syncers')) {
this.setState({ selectedMenuKey: '/syncers' });
} else if (uri.includes('/certs')) {
this.setState({ selectedMenuKey: '/certs' });
} else if (uri.includes('/signup')) {
this.setState({ selectedMenuKey: '/signup' });
} else if (uri.includes('/login')) {
@ -324,6 +335,20 @@ class App extends Component {
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/roles">
<Link to="/roles">
{i18next.t("general:Roles")}
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/permissions">
<Link to="/permissions">
{i18next.t("general:Permissions")}
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/providers">
<Link to="/providers">
@ -376,6 +401,13 @@ class App extends Component {
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/certs">
<Link to="/certs">
{i18next.t("general:Certs")}
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/swagger">
<a target="_blank" rel="noreferrer" href={Setting.isLocalhost() ? `${Setting.ServerUrl}/swagger` : "/swagger"}>
@ -426,6 +458,10 @@ class App extends Component {
<Route exact path="/organizations/:organizationName/users" render={(props) => this.renderLoginIfNotLoggedIn(<UserListPage account={this.state.account} {...props} />)}/>
<Route exact path="/users" render={(props) => this.renderLoginIfNotLoggedIn(<UserListPage account={this.state.account} {...props} />)}/>
<Route exact path="/users/:organizationName/:userName" render={(props) => <UserEditPage account={this.state.account} {...props} />}/>
<Route exact path="/roles" render={(props) => this.renderLoginIfNotLoggedIn(<RoleListPage account={this.state.account} {...props} />)}/>
<Route exact path="/roles/:organizationName/:roleName" render={(props) => this.renderLoginIfNotLoggedIn(<RoleEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/permissions" render={(props) => this.renderLoginIfNotLoggedIn(<PermissionListPage account={this.state.account} {...props} />)}/>
<Route exact path="/permissions/:organizationName/:permissionName" render={(props) => this.renderLoginIfNotLoggedIn(<PermissionEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/providers" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderListPage account={this.state.account} {...props} />)}/>
<Route exact path="/providers/:providerName" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/applications" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationListPage account={this.state.account} {...props} />)}/>
@ -440,6 +476,8 @@ class App extends Component {
<Route exact path="/webhooks/:webhookName" render={(props) => this.renderLoginIfNotLoggedIn(<WebhookEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/syncers" render={(props) => this.renderLoginIfNotLoggedIn(<SyncerListPage account={this.state.account} {...props} />)}/>
<Route exact path="/syncers/:syncerName" render={(props) => this.renderLoginIfNotLoggedIn(<SyncerEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/certs" render={(props) => this.renderLoginIfNotLoggedIn(<CertListPage account={this.state.account} {...props} />)}/>
<Route exact path="/certs/:certName" render={(props) => this.renderLoginIfNotLoggedIn(<CertEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/records" render={(props) => this.renderLoginIfNotLoggedIn(<RecordListPage account={this.state.account} {...props} />)}/>
<Route exact path="/.well-known/openid-configuration" render={(props) => <OdicDiscoveryPage />}/>
<Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")}

View File

@ -291,6 +291,16 @@ class ApplicationEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("application:Signin session"), i18next.t("application:Enable signin session - Tooltip"))} :
</Col>
<Col span={1} >
<Switch checked={this.state.application.enableSigninSession} onChange={checked => {
this.updateApplicationField('enableSigninSession', checked);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("application:Enable code signin"), i18next.t("application:Enable code signin - Tooltip"))} :

View File

@ -20,32 +20,9 @@ import moment from "moment";
import * as Setting from "./Setting";
import * as ApplicationBackend from "./backend/ApplicationBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
class ApplicationListPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
applications: null,
total: 0,
};
}
UNSAFE_componentWillMount() {
this.getApplications(1, 10);
}
getApplications(page, pageSize) {
ApplicationBackend.getApplications("admin", page, pageSize)
.then((res) => {
if (res.status === "ok") {
this.setState({
applications: res.data,
total: res.data2
});
}
});
}
class ApplicationListPage extends BaseListPage {
newApplication() {
const randomName = Setting.getRandomName();
@ -57,7 +34,8 @@ class ApplicationListPage extends React.Component {
logo: "https://cdn.casbin.com/logo/logo_1024x256.png",
enablePassword: true,
enableSignUp: true,
EnableCodeSignin: false,
enableSigninSession: true,
enableCodeSignin: false,
providers: [],
signupItems: [
{name: "ID", visible: false, required: true, rule: "Random"},
@ -80,10 +58,6 @@ class ApplicationListPage extends React.Component {
ApplicationBackend.addApplication(newApplication)
.then((res) => {
Setting.showMessage("success", `Application added successfully`);
this.setState({
applications: Setting.prependRow(this.state.applications, newApplication),
total: this.state.total + 1
});
this.props.history.push(`/applications/${newApplication.name}`);
}
)
@ -93,12 +67,12 @@ class ApplicationListPage extends React.Component {
}
deleteApplication(i) {
ApplicationBackend.deleteApplication(this.state.applications[i])
ApplicationBackend.deleteApplication(this.state.data[i])
.then((res) => {
Setting.showMessage("success", `Application deleted successfully`);
this.setState({
applications: Setting.deleteRow(this.state.applications, i),
total: this.state.total - 1
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
}
)
@ -115,7 +89,8 @@ class ApplicationListPage extends React.Component {
key: 'name',
width: '150px',
fixed: 'left',
sorter: (a, b) => a.name.localeCompare(b.name),
sorter: true,
...this.getColumnSearchProps('name'),
render: (text, record, index) => {
return (
<Link to={`/applications/${text}`}>
@ -129,7 +104,7 @@ class ApplicationListPage extends React.Component {
dataIndex: 'createdTime',
key: 'createdTime',
width: '160px',
sorter: (a, b) => a.createdTime.localeCompare(b.createdTime),
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
}
@ -139,7 +114,8 @@ class ApplicationListPage extends React.Component {
dataIndex: 'displayName',
key: 'displayName',
// width: '100px',
sorter: (a, b) => a.displayName.localeCompare(b.displayName),
sorter: true,
...this.getColumnSearchProps('displayName'),
},
{
title: 'Logo',
@ -159,7 +135,8 @@ class ApplicationListPage extends React.Component {
dataIndex: 'organization',
key: 'organization',
width: '150px',
sorter: (a, b) => a.organization.localeCompare(b.organization),
sorter: true,
...this.getColumnSearchProps('organization'),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
@ -172,6 +149,7 @@ class ApplicationListPage extends React.Component {
title: i18next.t("general:Providers"),
dataIndex: 'providers',
key: 'providers',
...this.getColumnSearchProps('providers'),
// width: '600px',
render: (text, record, index) => {
const providers = text;
@ -247,12 +225,10 @@ class ApplicationListPage extends React.Component {
];
const paginationProps = {
total: this.state.total,
total: this.state.pagination.total,
showQuickJumper: true,
showSizeChanger: true,
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.total),
onChange: (page, pageSize) => this.getApplications(page, pageSize),
onShowSizeChange: (current, size) => this.getApplications(current, size),
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
};
return (
@ -264,21 +240,33 @@ class ApplicationListPage extends React.Component {
<Button type="primary" size="small" onClick={this.addApplication.bind(this)}>{i18next.t("general:Add")}</Button>
</div>
)}
loading={applications === null}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
</div>
);
}
render() {
return (
<div>
{
this.renderTable(this.state.applications)
}
</div>
);
fetch = (params = {}) => {
let field = params.searchedColumn, value = params.searchText;
let sortField = params.sortField, sortOrder = params.sortOrder;
this.setState({ loading: true });
ApplicationBackend.getApplications("admin", params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
.then((res) => {
if (res.status === "ok") {
this.setState({
loading: false,
data: res.data,
pagination: {
...params.pagination,
total: res.data2,
},
searchText: params.searchText,
searchedColumn: params.searchedColumn,
});
}
});
};
}
export default ApplicationListPage;

138
web/src/BaseListPage.js Normal file
View File

@ -0,0 +1,138 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Input, Space} from "antd";
import {SearchOutlined} from "@ant-design/icons";
import Highlighter from "react-highlight-words";
class BaseListPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
data: [],
pagination: {
current: 1,
pageSize: 10,
},
loading: false,
searchText: '',
searchedColumn: '',
};
}
UNSAFE_componentWillMount() {
const { pagination } = this.state;
this.fetch({ pagination });
}
getColumnSearchProps = dataIndex => ({
filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }) => (
<div style={{ padding: 8 }}>
<Input
ref={node => {
this.searchInput = node;
}}
placeholder={`Search ${dataIndex}`}
value={selectedKeys[0]}
onChange={e => setSelectedKeys(e.target.value ? [e.target.value] : [])}
onPressEnter={() => this.handleSearch(selectedKeys, confirm, dataIndex)}
style={{ marginBottom: 8, display: 'block' }}
/>
<Space>
<Button
type="primary"
onClick={() => this.handleSearch(selectedKeys, confirm, dataIndex)}
icon={<SearchOutlined />}
size="small"
style={{ width: 90 }}
>
Search
</Button>
<Button onClick={() => this.handleReset(clearFilters)} size="small" style={{ width: 90 }}>
Reset
</Button>
<Button
type="link"
size="small"
onClick={() => {
confirm({ closeDropdown: false });
this.setState({
searchText: selectedKeys[0],
searchedColumn: dataIndex,
});
}}
>
Filter
</Button>
</Space>
</div>
),
filterIcon: filtered => <SearchOutlined style={{ color: filtered ? '#1890ff' : undefined }} />,
onFilter: (value, record) =>
record[dataIndex]
? record[dataIndex].toString().toLowerCase().includes(value.toLowerCase())
: '',
onFilterDropdownVisibleChange: visible => {
if (visible) {
setTimeout(() => this.searchInput.select(), 100);
}
},
render: text =>
this.state.searchedColumn === dataIndex ? (
<Highlighter
highlightStyle={{ backgroundColor: '#ffc069', padding: 0 }}
searchWords={[this.state.searchText]}
autoEscape
textToHighlight={text ? text.toString() : ''}
/>
) : (
text
),
});
handleSearch = (selectedKeys, confirm, dataIndex) => {
this.fetch({searchText: selectedKeys[0], searchedColumn: dataIndex, pagination: this.state.pagination});
};
handleReset = clearFilters => {
clearFilters();
const { pagination } = this.state;
this.fetch({ pagination });
};
handleTableChange = (pagination, filters, sorter) => {
this.fetch({
sortField: sorter.field,
sortOrder: sorter.order,
pagination,
...filters,
searchText: this.state.searchText,
searchedColumn: this.state.searchedColumn,
});
};
render() {
return (
<div>
{
this.renderTable(this.state.data)
}
</div>
);
}
}
export default BaseListPage;

254
web/src/CertEditPage.js Normal file
View File

@ -0,0 +1,254 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Card, Col, Input, InputNumber, Row, Select} from 'antd';
import * as CertBackend from "./backend/CertBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
import copy from "copy-to-clipboard";
import FileSaver from "file-saver";
const { Option } = Select;
const { TextArea } = Input;
class CertEditPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
certName: props.match.params.certName,
cert: null,
};
}
UNSAFE_componentWillMount() {
this.getCert();
}
getCert() {
CertBackend.getCert("admin", this.state.certName)
.then((cert) => {
this.setState({
cert: cert,
});
});
}
parseCertField(key, value) {
if (["port"].includes(key)) {
value = Setting.myParseInt(value);
}
return value;
}
updateCertField(key, value) {
value = this.parseCertField(key, value);
let cert = this.state.cert;
cert[key] = value;
this.setState({
cert: cert,
});
}
renderCert() {
return (
<Card size="small" title={
<div>
{i18next.t("cert:Edit Cert")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitCertEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitCertEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
<Row style={{marginTop: '10px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.cert.name} onChange={e => {
this.updateCertField('name', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.cert.displayName} onChange={e => {
this.updateCertField('displayName', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Scope"), i18next.t("cert:Scope - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.cert.scope} onChange={(value => {
this.updateCertField('scope', value);
})}>
{
[
{id: 'JWT', name: 'JWT'},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Type"), i18next.t("cert:Type - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.cert.type} onChange={(value => {
this.updateCertField('type', value);
})}>
{
[
{id: 'x509', name: 'x509'},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Crypto algorithm"), i18next.t("cert:Crypto algorithm - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.cert.cryptoAlgorithm} onChange={(value => {
this.updateCertField('cryptoAlgorithm', value);
})}>
{
[
{id: 'RSA', name: 'RSA'},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Bit size"), i18next.t("cert:Bit size - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.cert.bitSize} onChange={value => {
this.updateCertField('bitSize', value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Expire in years"), i18next.t("cert:Expire in years - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.cert.expireInYears} onChange={value => {
this.updateCertField('expireInYears', value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Public key"), i18next.t("cert:Public key - Tooltip"))} :
</Col>
<Col span={9} >
<Button style={{marginRight: '10px', marginBottom: '10px'}} onClick={() => {
copy(this.state.cert.publicKey);
Setting.showMessage("success", i18next.t("cert:Public key copied to clipboard successfully"));
}}
>
{i18next.t("cert:Copy public key")}
</Button>
<Button type="primary" onClick={() => {
const blob = new Blob([this.state.cert.publicKey], {type: "text/plain;charset=utf-8"});
FileSaver.saveAs(blob, "token_jwt_key.pem");
}}
>
{i18next.t("cert:Download public key")}
</Button>
<TextArea autoSize={{minRows: 30, maxRows: 30}} value={this.state.cert.publicKey} onChange={e => {
this.updateCertField('publicKey', e.target.value);
}} />
</Col>
<Col span={1} />
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Private key"), i18next.t("cert:Private key - Tooltip"))} :
</Col>
<Col span={9} >
<Button style={{marginRight: '10px', marginBottom: '10px'}} onClick={() => {
copy(this.state.cert.privateKey);
Setting.showMessage("success", i18next.t("cert:Private key copied to clipboard successfully"));
}}
>
{i18next.t("cert:Copy private key")}
</Button>
<Button type="primary" onClick={() => {
const blob = new Blob([this.state.cert.privateKey], {type: "text/plain;charset=utf-8"});
FileSaver.saveAs(blob, "token_jwt_key.key");
}}
>
{i18next.t("cert:Download private key")}
</Button>
<TextArea autoSize={{minRows: 30, maxRows: 30}} value={this.state.cert.privateKey} onChange={e => {
this.updateCertField('privateKey', e.target.value);
}} />
</Col>
</Row>
</Card>
)
}
submitCertEdit(willExist) {
let cert = Setting.deepCopy(this.state.cert);
CertBackend.updateCert(this.state.cert.owner, this.state.certName, cert)
.then((res) => {
if (res.msg === "") {
Setting.showMessage("success", `Successfully saved`);
this.setState({
certName: this.state.cert.name,
});
if (willExist) {
this.props.history.push(`/certs`);
} else {
this.props.history.push(`/certs/${this.state.cert.name}`);
}
} else {
Setting.showMessage("error", res.msg);
this.updateCertField('name', this.state.certName);
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
});
}
render() {
return (
<div>
{
this.state.cert !== null ? this.renderCert() : null
}
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitCertEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitCertEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
</div>
</div>
);
}
}
export default CertEditPage;

230
web/src/CertListPage.js Normal file
View File

@ -0,0 +1,230 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Link} from "react-router-dom";
import {Button, Popconfirm, Table} from 'antd';
import moment from "moment";
import * as Setting from "./Setting";
import * as CertBackend from "./backend/CertBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
class CertListPage extends BaseListPage {
newCert() {
const randomName = Setting.getRandomName();
return {
owner: "admin", // this.props.account.certname,
name: `cert_${randomName}`,
createdTime: moment().format(),
displayName: `New Cert - ${randomName}`,
scope: "JWT",
type: "x509",
cryptoAlgorithm: "RSA",
bitSize: 4096,
expireInYears: 20,
publicKey: "",
privateKey: "",
}
}
addCert() {
const newCert = this.newCert();
CertBackend.addCert(newCert)
.then((res) => {
Setting.showMessage("success", `Cert added successfully`);
this.props.history.push(`/certs/${newCert.name}`);
}
)
.catch(error => {
Setting.showMessage("error", `Cert failed to add: ${error}`);
});
}
deleteCert(i) {
CertBackend.deleteCert(this.state.data[i])
.then((res) => {
Setting.showMessage("success", `Cert deleted successfully`);
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
}
)
.catch(error => {
Setting.showMessage("error", `Cert failed to delete: ${error}`);
});
}
renderTable(certs) {
const columns = [
{
title: i18next.t("general:Name"),
dataIndex: 'name',
key: 'name',
width: '120px',
fixed: 'left',
sorter: true,
...this.getColumnSearchProps('name'),
render: (text, record, index) => {
return (
<Link to={`/certs/${text}`}>
{text}
</Link>
)
}
},
{
title: i18next.t("general:Created time"),
dataIndex: 'createdTime',
key: 'createdTime',
width: '180px',
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
}
},
{
title: i18next.t("general:Display name"),
dataIndex: 'displayName',
key: 'displayName',
// width: '100px',
sorter: true,
...this.getColumnSearchProps('displayName'),
},
{
title: i18next.t("cert:Scope"),
dataIndex: 'scope',
key: 'scope',
filterMultiple: false,
filters: [
{text: 'JWT', value: 'JWT'},
],
width: '110px',
sorter: true,
},
{
title: i18next.t("cert:Type"),
dataIndex: 'type',
key: 'type',
filterMultiple: false,
filters: [
{text: 'x509', value: 'x509'},
],
width: '110px',
sorter: true,
},
{
title: i18next.t("cert:Crypto algorithm"),
dataIndex: 'cryptoAlgorithm',
key: 'cryptoAlgorithm',
filterMultiple: false,
filters: [
{text: 'RSA', value: 'RSA'},
],
width: '190px',
sorter: true,
},
{
title: i18next.t("cert:Bit size"),
dataIndex: 'bitSize',
key: 'bitSize',
width: '130px',
sorter: true,
...this.getColumnSearchProps('bitSize'),
},
{
title: i18next.t("cert:Expire in years"),
dataIndex: 'expireInYears',
key: 'expireInYears',
width: '170px',
sorter: true,
...this.getColumnSearchProps('expireInYears'),
},
{
title: i18next.t("general:Action"),
dataIndex: '',
key: 'op',
width: '170px',
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: '10px', marginBottom: '10px', marginRight: '10px'}} type="primary" onClick={() => this.props.history.push(`/certs/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm
title={`Sure to delete cert: ${record.name} ?`}
onConfirm={() => this.deleteCert(index)}
>
<Button style={{marginBottom: '10px'}} type="danger">{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
)
}
},
];
const paginationProps = {
total: this.state.pagination.total,
showQuickJumper: true,
showSizeChanger: true,
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
};
return (
<div>
<Table scroll={{x: 'max-content'}} columns={columns} dataSource={certs} rowKey="name" size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Certs")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small" onClick={this.addCert.bind(this)}>{i18next.t("general:Add")}</Button>
</div>
)}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
</div>
);
}
fetch = (params = {}) => {
let field = params.searchedColumn, value = params.searchText;
let sortField = params.sortField, sortOrder = params.sortOrder;
if (params.category !== undefined && params.category !== null) {
field = "category";
value = params.category;
} else if (params.type !== undefined && params.type !== null) {
field = "type";
value = params.type;
}
this.setState({ loading: true });
CertBackend.getCerts("admin", params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
.then((res) => {
if (res.status === "ok") {
this.setState({
loading: false,
data: res.data,
pagination: {
...params.pagination,
total: res.data2,
},
searchText: params.searchText,
searchedColumn: params.searchedColumn,
});
}
});
};
}
export default CertListPage;

View File

@ -13,7 +13,7 @@
// limitations under the License.
export const ShowGithubCorner = false;
export const GithubRepo = "https://github.com/casbin/casdoor";
export const GithubRepo = "https://github.com/casdoor/casdoor";
export const ForceLanguage = "";
export const DefaultLanguage = "en";

View File

@ -19,32 +19,9 @@ import moment from "moment";
import * as Setting from "./Setting";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
class OrganizationListPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
organizations: null,
total: 0
};
}
UNSAFE_componentWillMount() {
this.getOrganizations(1, 10);
}
getOrganizations(page, pageSize) {
OrganizationBackend.getOrganizations("admin", page, pageSize)
.then((res) => {
if (res.status === "ok") {
this.setState({
organizations: res.data,
total: res.data2
});
}
});
}
class OrganizationListPage extends BaseListPage {
newOrganization() {
const randomName = Setting.getRandomName();
@ -69,10 +46,6 @@ class OrganizationListPage extends React.Component {
OrganizationBackend.addOrganization(newOrganization)
.then((res) => {
Setting.showMessage("success", `Organization added successfully`);
this.setState({
organizations: Setting.prependRow(this.state.organizations, newOrganization),
total: this.state.total + 1
});
this.props.history.push(`/organizations/${newOrganization.name}`);
}
)
@ -82,12 +55,12 @@ class OrganizationListPage extends React.Component {
}
deleteOrganization(i) {
OrganizationBackend.deleteOrganization(this.state.organizations[i])
OrganizationBackend.deleteOrganization(this.state.data[i])
.then((res) => {
Setting.showMessage("success", `Organization deleted successfully`);
this.setState({
organizations: Setting.deleteRow(this.state.organizations, i),
total: this.state.total - 1
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
}
)
@ -104,7 +77,8 @@ class OrganizationListPage extends React.Component {
key: 'name',
width: '120px',
fixed: 'left',
sorter: (a, b) => a.name.localeCompare(b.name),
sorter: true,
...this.getColumnSearchProps('name'),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
@ -118,7 +92,7 @@ class OrganizationListPage extends React.Component {
dataIndex: 'createdTime',
key: 'createdTime',
width: '160px',
sorter: (a, b) => a.createdTime.localeCompare(b.createdTime),
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
}
@ -128,7 +102,8 @@ class OrganizationListPage extends React.Component {
dataIndex: 'displayName',
key: 'displayName',
// width: '100px',
sorter: (a, b) => a.displayName.localeCompare(b.displayName),
sorter: true,
...this.getColumnSearchProps('displayName'),
},
{
title: i18next.t("organization:Favicon"),
@ -148,7 +123,8 @@ class OrganizationListPage extends React.Component {
dataIndex: 'websiteUrl',
key: 'websiteUrl',
width: '300px',
sorter: (a, b) => a.websiteUrl.localeCompare(b.websiteUrl),
sorter: true,
...this.getColumnSearchProps('websiteUrl'),
render: (text, record, index) => {
return (
<a target="_blank" rel="noreferrer" href={text}>
@ -162,14 +138,21 @@ class OrganizationListPage extends React.Component {
dataIndex: 'passwordType',
key: 'passwordType',
width: '150px',
sorter: (a, b) => a.passwordType.localeCompare(b.passwordType),
sorter: true,
filterMultiple: false,
filters: [
{text: 'plain', value: 'plain'},
{text: 'salt', value: 'salt'},
{text: 'md5-salt', value: 'md5-salt'},
],
},
{
title: i18next.t("general:Password salt"),
dataIndex: 'passwordSalt',
key: 'passwordSalt',
width: '150px',
sorter: (a, b) => a.passwordSalt.localeCompare(b.passwordSalt),
sorter: true,
...this.getColumnSearchProps('passwordSalt'),
},
{
title: i18next.t("organization:Default avatar"),
@ -189,7 +172,7 @@ class OrganizationListPage extends React.Component {
dataIndex: 'enableSoftDeletion',
key: 'enableSoftDeletion',
width: '140px',
sorter: (a, b) => a.enableSoftDeletion - b.enableSoftDeletion,
sorter: true,
render: (text, record, index) => {
return (
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
@ -221,12 +204,10 @@ class OrganizationListPage extends React.Component {
];
const paginationProps = {
total: this.state.total,
total: this.state.pagination.total,
showQuickJumper: true,
showSizeChanger: true,
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.total),
onChange: (page, pageSize) => this.getOrganizations(page, pageSize),
onShowSizeChange: (current, size) => this.getOrganizations(current, size),
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
};
return (
@ -238,21 +219,37 @@ class OrganizationListPage extends React.Component {
<Button type="primary" size="small" onClick={this.addOrganization.bind(this)}>{i18next.t("general:Add")}</Button>
</div>
)}
loading={organizations === null}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
</div>
);
}
render() {
return (
<div>
{
this.renderTable(this.state.organizations)
fetch = (params = {}) => {
let field = params.searchedColumn, value = params.searchText;
let sortField = params.sortField, sortOrder = params.sortOrder;
if (params.passwordType !== undefined && params.passwordType !== null) {
field = "passwordType";
value = params.passwordType;
}
</div>
);
this.setState({ loading: true });
OrganizationBackend.getOrganizations("admin", params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
.then((res) => {
if (res.status === "ok") {
this.setState({
loading: false,
data: res.data,
pagination: {
...params.pagination,
total: res.data2,
},
searchText: params.searchText,
searchedColumn: params.searchedColumn,
});
}
});
};
}
export default OrganizationListPage;

View File

@ -59,7 +59,7 @@ export const PasswordModal = (props) => {
return (
<Row>
<Button type="default" onClick={showModal}>
<Button type="default" disabled={props.disabled} onClick={showModal}>
{ hasOldPassword ? i18next.t("user:Modify password...") : i18next.t("user:Set password...")}
</Button>
<Modal

View File

@ -0,0 +1,276 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Card, Col, Input, Row, Select, Switch} from 'antd';
import * as PermissionBackend from "./backend/PermissionBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as UserBackend from "./backend/UserBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
import * as RoleBackend from "./backend/RoleBackend";
const { Option } = Select;
class PermissionEditPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
permissionName: props.match.params.permissionName,
permission: null,
organizations: [],
users: [],
roles: [],
};
}
UNSAFE_componentWillMount() {
this.getPermission();
this.getOrganizations();
}
getPermission() {
PermissionBackend.getPermission(this.state.organizationName, this.state.permissionName)
.then((permission) => {
this.setState({
permission: permission,
});
this.getUsers(permission.owner);
this.getRoles(permission.owner);
});
}
getOrganizations() {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: (res.msg === undefined) ? res : [],
});
});
}
getUsers(organizationName) {
UserBackend.getUsers(organizationName)
.then((res) => {
this.setState({
users: res,
});
});
}
getRoles(organizationName) {
RoleBackend.getRoles(organizationName)
.then((res) => {
this.setState({
roles: res,
});
});
}
parsePermissionField(key, value) {
if ([""].includes(key)) {
value = Setting.myParseInt(value);
}
return value;
}
updatePermissionField(key, value) {
value = this.parsePermissionField(key, value);
let permission = this.state.permission;
permission[key] = value;
this.setState({
permission: permission,
});
}
renderPermission() {
return (
<Card size="small" title={
<div>
{i18next.t("permission:Edit Permission")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitPermissionEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitPermissionEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
<Row style={{marginTop: '10px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.permission.owner} onChange={(owner => {
this.updatePermissionField('owner', owner);
this.getUsers(owner);
this.getRoles(owner);
})}>
{
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.permission.name} onChange={e => {
this.updatePermissionField('name', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.permission.displayName} onChange={e => {
this.updatePermissionField('displayName', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("role:Sub users"), i18next.t("role:Sub users - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} mode="tags" style={{width: '100%'}} value={this.state.permission.users} onChange={(value => {this.updatePermissionField('users', value);})}>
{
this.state.users.map((user, index) => <Option key={index} value={`${user.owner}/${user.name}`}>{`${user.owner}/${user.name}`}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("role:Sub roles"), i18next.t("role:Sub roles - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} mode="tags" style={{width: '100%'}} value={this.state.permission.roles} onChange={(value => {this.updatePermissionField('roles', value);})}>
{
this.state.roles.filter(roles => (roles.owner !== this.state.roles.owner || roles.name !== this.state.roles.name)).map((permission, index) => <Option key={index} value={`${permission.owner}/${permission.name}`}>{`${permission.owner}/${permission.name}`}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("permission:Resource type"), i18next.t("permission:Resource type - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.permission.resourceType} onChange={(value => {
this.updatePermissionField('resourceType', value);
})}>
{
[
{id: 'Application', name: 'Application'},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("permission:Actions"), i18next.t("permission:Actions - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} mode="tags" style={{width: '100%'}} value={this.state.permission.actions} onChange={(value => {
this.updatePermissionField('actions', value);
})}>
{
[
{id: 'Read', name: 'Read'},
{id: 'Write', name: 'Write'},
{id: 'Admin', name: 'Admin'},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("permission:Effect"), i18next.t("permission:Effect - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.permission.effect} onChange={(value => {
this.updatePermissionField('effect', value);
})}>
{
[
{id: 'Allow', name: 'Allow'},
{id: 'Deny', name: 'Deny'},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} :
</Col>
<Col span={1} >
<Switch checked={this.state.permission.isEnabled} onChange={checked => {
this.updatePermissionField('isEnabled', checked);
}} />
</Col>
</Row>
</Card>
)
}
submitPermissionEdit(willExist) {
let permission = Setting.deepCopy(this.state.permission);
PermissionBackend.updatePermission(this.state.organizationName, this.state.permissionName, permission)
.then((res) => {
if (res.msg === "") {
Setting.showMessage("success", `Successfully saved`);
this.setState({
permissionName: this.state.permission.name,
});
if (willExist) {
this.props.history.push(`/permissions`);
} else {
this.props.history.push(`/permissions/${this.state.permission.owner}/${this.state.permission.name}`);
}
} else {
Setting.showMessage("error", res.msg);
this.updatePermissionField('name', this.state.permissionName);
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
});
}
render() {
return (
<div>
{
this.state.permission !== null ? this.renderPermission() : null
}
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitPermissionEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitPermissionEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
</div>
</div>
);
}
}
export default PermissionEditPage;

View File

@ -0,0 +1,271 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Link} from "react-router-dom";
import {Button, Popconfirm, Switch, Table} from 'antd';
import moment from "moment";
import * as Setting from "./Setting";
import * as PermissionBackend from "./backend/PermissionBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
class PermissionListPage extends BaseListPage {
newPermission() {
const randomName = Setting.getRandomName();
return {
owner: "built-in",
name: `permission_${randomName}`,
createdTime: moment().format(),
displayName: `New Permission - ${randomName}`,
users: [],
roles: [],
resourceType: "Application",
resources: ["app-built-in"],
action: "Read",
effect: "Allow",
isEnabled: true,
}
}
addPermission() {
const newPermission = this.newPermission();
PermissionBackend.addPermission(newPermission)
.then((res) => {
Setting.showMessage("success", `Permission added successfully`);
this.props.history.push(`/permissions/${newPermission.owner}/${newPermission.name}`);
}
)
.catch(error => {
Setting.showMessage("error", `Permission failed to add: ${error}`);
});
}
deletePermission(i) {
PermissionBackend.deletePermission(this.state.data[i])
.then((res) => {
Setting.showMessage("success", `Permission deleted successfully`);
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
}
)
.catch(error => {
Setting.showMessage("error", `Permission failed to delete: ${error}`);
});
}
renderTable(permissions) {
const columns = [
{
title: i18next.t("general:Organization"),
dataIndex: 'owner',
key: 'owner',
width: '120px',
sorter: true,
...this.getColumnSearchProps('owner'),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
)
}
},
{
title: i18next.t("general:Name"),
dataIndex: 'name',
key: 'name',
width: '150px',
fixed: 'left',
sorter: true,
...this.getColumnSearchProps('name'),
render: (text, record, index) => {
return (
<Link to={`/permissions/${text}`}>
{text}
</Link>
)
}
},
{
title: i18next.t("general:Created time"),
dataIndex: 'createdTime',
key: 'createdTime',
width: '160px',
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
}
},
{
title: i18next.t("general:Display name"),
dataIndex: 'displayName',
key: 'displayName',
width: '160px',
sorter: true,
...this.getColumnSearchProps('displayName'),
},
{
title: i18next.t("role:Sub users"),
dataIndex: 'users',
key: 'users',
// width: '100px',
sorter: true,
...this.getColumnSearchProps('users'),
render: (text, record, index) => {
return Setting.getTags(text);
}
},
{
title: i18next.t("role:Sub roles"),
dataIndex: 'roles',
key: 'roles',
// width: '100px',
sorter: true,
...this.getColumnSearchProps('roles'),
render: (text, record, index) => {
return Setting.getTags(text);
}
},
{
title: i18next.t("permission:Resource type"),
dataIndex: 'resourceType',
key: 'resourceType',
filterMultiple: false,
filters: [
{text: 'Application', value: 'Application'},
],
width: '170px',
sorter: true,
},
{
title: i18next.t("permission:Resources"),
dataIndex: 'resources',
key: 'resources',
// width: '100px',
sorter: true,
...this.getColumnSearchProps('resources'),
render: (text, record, index) => {
return Setting.getTags(text);
}
},
{
title: i18next.t("permission:Actions"),
dataIndex: 'actions',
key: 'actions',
// width: '100px',
sorter: true,
...this.getColumnSearchProps('actions'),
render: (text, record, index) => {
return Setting.getTags(text);
}
},
{
title: i18next.t("permission:Effect"),
dataIndex: 'effect',
key: 'effect',
filterMultiple: false,
filters: [
{text: 'Allow', value: 'Allow'},
{text: 'Deny', value: 'Deny'},
],
width: '120px',
sorter: true,
},
{
title: i18next.t("general:Is enabled"),
dataIndex: 'isEnabled',
key: 'isEnabled',
width: '120px',
sorter: true,
render: (text, record, index) => {
return (
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
)
}
},
{
title: i18next.t("general:Action"),
dataIndex: '',
key: 'op',
width: '170px',
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: '10px', marginBottom: '10px', marginRight: '10px'}} type="primary" onClick={() => this.props.history.push(`/permissions/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm
title={`Sure to delete permission: ${record.name} ?`}
onConfirm={() => this.deletePermission(index)}
>
<Button style={{marginBottom: '10px'}} type="danger">{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
)
}
},
];
const paginationProps = {
total: this.state.pagination.total,
showQuickJumper: true,
showSizeChanger: true,
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
};
return (
<div>
<Table scroll={{x: 'max-content'}} columns={columns} dataSource={permissions} rowKey="name" size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Permissions")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small" onClick={this.addPermission.bind(this)}>{i18next.t("general:Add")}</Button>
</div>
)}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
</div>
);
}
fetch = (params = {}) => {
let field = params.searchedColumn, value = params.searchText;
let sortField = params.sortField, sortOrder = params.sortOrder;
if (params.type !== undefined && params.type !== null) {
field = "type";
value = params.type;
}
this.setState({ loading: true });
PermissionBackend.getPermissions("", params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
.then((res) => {
if (res.status === "ok") {
this.setState({
loading: false,
data: res.data,
pagination: {
...params.pagination,
total: res.data2,
},
searchText: params.searchText,
searchedColumn: params.searchedColumn,
});
}
});
};
}
export default PermissionListPage;

View File

@ -64,60 +64,6 @@ class ProviderEditPage extends React.Component {
});
}
getProviderTypeOptions(provider) {
if (provider.category === "OAuth") {
return (
[
{id: 'Google', name: 'Google'},
{id: 'GitHub', name: 'GitHub'},
{id: 'QQ', name: 'QQ'},
{id: 'WeChat', name: 'WeChat'},
{id: 'Facebook', name: 'Facebook'},
{id: 'DingTalk', name: 'DingTalk'},
{id: 'Weibo', name: 'Weibo'},
{id: 'Gitee', name: 'Gitee'},
{id: 'LinkedIn', name: 'LinkedIn'},
{id: 'WeCom', name: 'WeCom'},
{id: 'Lark', name: 'Lark'},
{id: 'GitLab', name: 'GitLab'},
{id: 'Apple', name: 'Apple'},
{id: 'AzureAD', name: 'AzureAD'},
{id: 'Slack', name: 'Slack'},
]
);
} else if (provider.category === "Email") {
return (
[
{id: 'Default', name: 'Default'},
]
);
} else if (provider.category === "SMS") {
return (
[
{id: 'Aliyun SMS', name: 'Aliyun SMS'},
{id: 'Tencent Cloud SMS', name: 'Tencent Cloud SMS'},
{id: 'Volc Engine SMS', name: 'Volc Engine SMS'},
]
);
} else if (provider.category === "Storage") {
return (
[
{id: 'Local File System', name: 'Local File System'},
{id: 'AWS S3', name: 'AWS S3'},
{id: 'Aliyun OSS', name: 'Aliyun OSS'},
{id: 'Tencent Cloud COS', name: 'Tencent Cloud COS'},
]
);
} else if (provider.category === "SAML") {
return ([
{id: 'Aliyun IDaaS', name: 'Aliyun IDaaS'},
{id: 'Keycloak', name: 'Keycloak'},
]);
} else {
return [];
}
}
getClientIdLabel() {
switch (this.state.provider.category) {
case "Email":
@ -145,18 +91,21 @@ class ProviderEditPage extends React.Component {
getAppIdRow() {
let text, tooltip;
if (this.state.provider.category === "SMS" && this.state.provider.type === "Tencent Cloud SMS") {
text = "provider:App ID";
tooltip = "provider:App ID - Tooltip";
text = i18next.t("provider:App ID");
tooltip = i18next.t("provider:App ID - Tooltip");
} else if (this.state.provider.type === "WeCom" && this.state.provider.subType === "Internal") {
text = i18next.t("provider:Agent ID");
tooltip = i18next.t("provider:Agent ID - Tooltip");
} else if (this.state.provider.category === "SMS" && this.state.provider.type === "Volc Engine SMS") {
text = "provider:SMS account";
tooltip = "provider:SMS account - Tooltip";
text = i18next.t("provider:SMS account");
tooltip = i18next.t("provider:SMS account - Tooltip");
} else {
return null;
}
return <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t(text), i18next.t(tooltip))} :
{Setting.getLabel(text, tooltip)} :
</Col>
<Col span={22} >
<Input value={this.state.provider.appId} onChange={e => {
@ -252,13 +201,28 @@ class ProviderEditPage extends React.Component {
}
})}>
{
this.getProviderTypeOptions(this.state.provider).map((providerType, index) => <Option key={index} value={providerType.id}>{providerType.name}</Option>)
Setting.getProviderTypeOptions(this.state.provider.category).map((providerType, index) => <Option key={index} value={providerType.id}>{providerType.name}</Option>)
}
</Select>
</Col>
</Row>
{
this.state.provider.type !== "WeCom" ? null : (
<React.Fragment>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={2}>
{Setting.getLabel(i18next.t("provider:Sub type"), i18next.t("provider:Sub type - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.provider.subType} onChange={value => {
this.updateProviderField('subType', value);
}}>
{
Setting.getProviderSubTypeOptions(this.state.provider.type).map((providerSubType, index) => <Option key={index} value={providerSubType.id}>{providerSubType.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={2}>
{Setting.getLabel(i18next.t("provider:Method"), i18next.t("provider:Method - Tooltip"))} :
@ -273,6 +237,7 @@ class ProviderEditPage extends React.Component {
</Select>
</Col>
</Row>
</React.Fragment>
)
}
<Row style={{marginTop: '20px'}} >

View File

@ -20,32 +20,9 @@ import * as Setting from "./Setting";
import * as ProviderBackend from "./backend/ProviderBackend";
import * as Provider from "./auth/Provider";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
class ProviderListPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
providers: null,
total: 0,
};
}
UNSAFE_componentWillMount() {
this.getProviders(1, 10);
}
getProviders(page, pageSize) {
ProviderBackend.getProviders("admin", page, pageSize)
.then((res) => {
if (res.status === "ok") {
this.setState({
providers: res.data,
total: res.data2
});
}
});
}
class ProviderListPage extends BaseListPage {
newProvider() {
const randomName = Setting.getRandomName();
@ -71,10 +48,6 @@ class ProviderListPage extends React.Component {
ProviderBackend.addProvider(newProvider)
.then((res) => {
Setting.showMessage("success", `Provider added successfully`);
this.setState({
providers: Setting.prependRow(this.state.providers, newProvider),
total: this.state.total + 1
});
this.props.history.push(`/providers/${newProvider.name}`);
}
)
@ -84,12 +57,12 @@ class ProviderListPage extends React.Component {
}
deleteProvider(i) {
ProviderBackend.deleteProvider(this.state.providers[i])
ProviderBackend.deleteProvider(this.state.data[i])
.then((res) => {
Setting.showMessage("success", `Provider deleted successfully`);
this.setState({
providers: Setting.deleteRow(this.state.providers, i),
total: this.state.total - 1
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
}
)
@ -106,7 +79,8 @@ class ProviderListPage extends React.Component {
key: 'name',
width: '120px',
fixed: 'left',
sorter: (a, b) => a.name.localeCompare(b.name),
sorter: true,
...this.getColumnSearchProps('name'),
render: (text, record, index) => {
return (
<Link to={`/providers/${text}`}>
@ -120,7 +94,7 @@ class ProviderListPage extends React.Component {
dataIndex: 'createdTime',
key: 'createdTime',
width: '180px',
sorter: (a, b) => a.createdTime.localeCompare(b.createdTime),
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
}
@ -130,22 +104,39 @@ class ProviderListPage extends React.Component {
dataIndex: 'displayName',
key: 'displayName',
// width: '100px',
sorter: (a, b) => a.displayName.localeCompare(b.displayName),
sorter: true,
...this.getColumnSearchProps('displayName'),
},
{
title: i18next.t("provider:Category"),
dataIndex: 'category',
key: 'category',
width: '100px',
sorter: (a, b) => a.category.localeCompare(b.category),
filterMultiple: false,
filters: [
{text: 'OAuth', value: 'OAuth'},
{text: 'Email', value: 'Email'},
{text: 'SMS', value: 'SMS'},
{text: 'Storage', value: 'Storage'},
{text: 'SAML', value: 'SAML'},
],
width: '110px',
sorter: true,
},
{
title: i18next.t("provider:Type"),
dataIndex: 'type',
key: 'type',
width: '80px',
width: '110px',
align: 'center',
sorter: (a, b) => a.type.localeCompare(b.type),
filterMultiple: false,
filters: [
{text: 'OAuth', value: 'OAuth', children: Setting.getProviderTypeOptions('OAuth').map((o) => {return {text:o.id, value:o.name}})},
{text: 'Email', value: 'Email', children: Setting.getProviderTypeOptions('Email').map((o) => {return {text:o.id, value:o.name}})},
{text: 'SMS', value: 'SMS', children: Setting.getProviderTypeOptions('SMS').map((o) => {return {text:o.id, value:o.name}})},
{text: 'Storage', value: 'Storage', children: Setting.getProviderTypeOptions('Storage').map((o) => {return {text:o.id, value:o.name}})},
{text: 'SAML', value: 'SAML', children: Setting.getProviderTypeOptions('SAML').map((o) => {return {text:o.id, value:o.name}})},
],
sorter: true,
render: (text, record, index) => {
return Provider.getProviderLogoWidget(record);
}
@ -155,24 +146,19 @@ class ProviderListPage extends React.Component {
dataIndex: 'clientId',
key: 'clientId',
width: '100px',
sorter: (a, b) => a.clientId.localeCompare(b.clientId),
sorter: true,
...this.getColumnSearchProps('clientId'),
render: (text, record, index) => {
return Setting.getShortText(text);
}
},
// {
// title: 'Client secret',
// dataIndex: 'clientSecret',
// key: 'clientSecret',
// width: '150px',
// sorter: (a, b) => a.clientSecret.localeCompare(b.clientSecret),
// },
{
title: i18next.t("provider:Provider URL"),
dataIndex: 'providerUrl',
key: 'providerUrl',
width: '150px',
sorter: (a, b) => a.providerUrl.localeCompare(b.providerUrl),
sorter: true,
...this.getColumnSearchProps('providerUrl'),
render: (text, record, index) => {
return (
<a target="_blank" rel="noreferrer" href={text}>
@ -206,12 +192,10 @@ class ProviderListPage extends React.Component {
];
const paginationProps = {
total: this.state.total,
total: this.state.pagination.total,
showQuickJumper: true,
showSizeChanger: true,
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.total),
onChange: (page, pageSize) => this.getProviders(page, pageSize),
onShowSizeChange: (current, size) => this.getProviders(current, size),
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
};
return (
@ -223,21 +207,40 @@ class ProviderListPage extends React.Component {
<Button type="primary" size="small" onClick={this.addProvider.bind(this)}>{i18next.t("general:Add")}</Button>
</div>
)}
loading={providers === null}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
</div>
);
}
render() {
return (
<div>
{
this.renderTable(this.state.providers)
fetch = (params = {}) => {
let field = params.searchedColumn, value = params.searchText;
let sortField = params.sortField, sortOrder = params.sortOrder;
if (params.category !== undefined && params.category !== null) {
field = "category";
value = params.category;
} else if (params.type !== undefined && params.type !== null) {
field = "type";
value = params.type;
}
</div>
);
this.setState({ loading: true });
ProviderBackend.getProviders("admin", params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
.then((res) => {
if (res.status === "ok") {
this.setState({
loading: false,
data: res.data,
pagination: {
...params.pagination,
total: res.data2,
},
searchText: params.searchText,
searchedColumn: params.searchedColumn,
});
}
});
};
}
export default ProviderListPage;

View File

@ -19,31 +19,14 @@ import * as Setting from "./Setting";
import * as RecordBackend from "./backend/RecordBackend";
import i18next from "i18next";
import moment from "moment";
import BaseListPage from "./BaseListPage";
class RecordListPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
records: null,
total: 0,
};
}
class RecordListPage extends BaseListPage {
UNSAFE_componentWillMount() {
this.getRecords(1, 20);
}
getRecords(page, pageSize) {
RecordBackend.getRecords(page, pageSize)
.then((res) => {
if (res.status === "ok") {
this.setState({
records: res.data,
total: res.data2
});
}
});
this.state.pagination.pageSize = 20;
const { pagination } = this.state;
this.fetch({ pagination });
}
newRecord() {
@ -68,21 +51,24 @@ class RecordListPage extends React.Component {
dataIndex: 'name',
key: 'name',
width: '320px',
sorter: (a, b) => a.name.localeCompare(b.name),
sorter: true,
...this.getColumnSearchProps('name'),
},
{
title: i18next.t("general:ID"),
dataIndex: 'id',
key: 'id',
width: '90px',
sorter: (a, b) => a.id - b.id,
sorter: true,
...this.getColumnSearchProps('id'),
},
{
title: i18next.t("general:Client IP"),
dataIndex: 'clientIp',
key: 'clientIp',
width: '150px',
sorter: (a, b) => a.clientIp.localeCompare(b.clientIp),
sorter: true,
...this.getColumnSearchProps('clientIp'),
render: (text, record, index) => {
return (
<a target="_blank" rel="noreferrer" href={`https://db-ip.com/${text}`}>
@ -96,7 +82,7 @@ class RecordListPage extends React.Component {
dataIndex: 'createdTime',
key: 'createdTime',
width: '180px',
sorter: (a, b) => a.createdTime.localeCompare(b.createdTime),
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
}
@ -105,8 +91,9 @@ class RecordListPage extends React.Component {
title: i18next.t("general:Organization"),
dataIndex: 'organization',
key: 'organization',
width: '80px',
sorter: (a, b) => a.organization.localeCompare(b.organization),
width: '110px',
sorter: true,
...this.getColumnSearchProps('organization'),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
@ -120,7 +107,8 @@ class RecordListPage extends React.Component {
dataIndex: 'user',
key: 'user',
width: '120px',
sorter: (a, b) => a.user.localeCompare(b.user),
sorter: true,
...this.getColumnSearchProps('user'),
render: (text, record, index) => {
return (
<Link to={`/users/${record.organization}/${record.user}`}>
@ -133,22 +121,36 @@ class RecordListPage extends React.Component {
title: i18next.t("general:Method"),
dataIndex: 'method',
key: 'method',
width: '100px',
sorter: (a, b) => a.method.localeCompare(b.method),
width: '110px',
sorter: true,
filterMultiple: false,
filters: [
{text: 'GET', value: 'GET'},
{text: 'HEAD', value: 'HEAD'},
{text: 'POST', value: 'POST'},
{text: 'PUT', value: 'PUT'},
{text: 'DELETE', value: 'DELETE'},
{text: 'CONNECT', value: 'CONNECT'},
{text: 'OPTIONS', value: 'OPTIONS'},
{text: 'TRACE', value: 'TRACE'},
{text: 'PATCH', value: 'PATCH'},
],
},
{
title: i18next.t("general:Request URI"),
dataIndex: 'requestUri',
key: 'requestUri',
// width: '300px',
sorter: (a, b) => a.requestUri.localeCompare(b.requestUri),
sorter: true,
...this.getColumnSearchProps('requestUri'),
},
{
title: i18next.t("general:Action"),
dataIndex: 'action',
key: 'action',
width: '200px',
sorter: (a, b) => a.action.localeCompare(b.action),
sorter: true,
...this.getColumnSearchProps('action'),
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
return text;
@ -159,7 +161,7 @@ class RecordListPage extends React.Component {
dataIndex: 'isTriggered',
key: 'isTriggered',
width: '140px',
sorter: (a, b) => a.isTriggered - b.isTriggered,
sorter: true,
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
if (!["signup", "login", "logout", "update-user"].includes(record.action)) {
@ -174,13 +176,11 @@ class RecordListPage extends React.Component {
];
const paginationProps = {
pageSize: 20,
total: this.state.total,
total: this.state.pagination.total,
pageSize: this.state.pagination.pageSize,
showQuickJumper: true,
showSizeChanger: true,
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.total),
onChange: (page, pageSize) => this.getRecords(page, pageSize),
onShowSizeChange: (current, size) => this.getRecords(current, size),
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
};
return (
@ -191,21 +191,37 @@ class RecordListPage extends React.Component {
{i18next.t("general:Records")}&nbsp;&nbsp;&nbsp;&nbsp;
</div>
)}
loading={records === null}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
</div>
);
}
render() {
return (
<div>
{
this.renderTable(this.state.records)
fetch = (params = {}) => {
let field = params.searchedColumn, value = params.searchText;
let sortField = params.sortField, sortOrder = params.sortOrder;
if (params.method !== undefined && params.method !== null) {
field = "method";
value = params.method;
}
</div>
);
this.setState({ loading: true });
RecordBackend.getRecords(params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
.then((res) => {
if (res.status === "ok") {
this.setState({
loading: false,
data: res.data,
pagination: {
...params.pagination,
total: res.data2,
},
searchText: params.searchText,
searchedColumn: params.searchedColumn,
});
}
});
};
}
export default RecordListPage;

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