Compare commits

...

28 Commits

Author SHA1 Message Date
a399184cfc fix: correct edit URL in model list (#1108)
Co-authored-by: Mario Fischer <mario.fischer@inmanet.de>
2022-09-07 00:54:27 +08:00
2f9f946c87 feat: fix GOPROXY bug by exporting environment variable (#1106) 2022-09-05 23:17:39 +08:00
d8b60f838e fix: fix bugs about 3rd-party login in cas flow (#1096) 2022-09-05 23:02:25 +08:00
7599e2715a feat: add demo mode (#1097)
* feat: add demo mode

* feat: add demo mode

* Update app.conf

* Update authz.go

* Update authz.go

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-09-04 21:20:19 +08:00
35676455bc chore(style): add keyword spacing rule (#1098) 2022-09-04 19:40:30 +08:00
8128671c8c Improve email code 2022-09-04 12:15:07 +08:00
ee54dec3b3 feat: add support for mysubmail (#1095)
* feat: add support for mysubmail

* Update email.go

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-09-04 12:09:50 +08:00
d278bc9651 Add receiver for Email provider 2022-09-04 11:37:36 +08:00
b23bd0b189 Support SUBMAIL email provider 2022-09-04 11:21:20 +08:00
409be85264 Fix placeholder typo 2022-09-03 18:52:35 +08:00
0395b7e1a9 feat: migrate permission data (#1083)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-09-03 16:45:58 +08:00
4536fd0636 Use app.conf value in getOriginFromHost() 2022-09-03 15:12:34 +08:00
af9ae7dbb7 feat: buildx failed with: EROR: failed to solve: executor failed running [/bin/sh -c ./build.sh]: exit code: 127 (#1089) 2022-09-02 14:50:27 +08:00
e266696b32 feat: add default permission to built-in group (#1087)
* fix: add default permission

* fix: add default permission

* fix: add default permission
2022-09-02 12:03:13 +08:00
e108d26ec7 fix: recover header logo && add styleint check (#1084)
* fix: fix header logo not show

* feat: update lint-staged

feat: add stylelint
2022-08-31 23:26:58 +08:00
349ce7f1d4 fix: refactor build.sh #1081 (#1082)
* fix: Add default access permission for new built-in group users

* fix: Add default access permission for new built-in group users

* fix: File is not `gofumpt`-ed (gofumpt)

* fix: refactor build.sh #1081

* fix: rollback

* fix: newline

* fix: refactor build.sh rename var #1081
2022-08-31 16:08:10 +08:00
8da50b7893 feat: extend managed accounts for get-account api (#1068)
* feat: add get-extend-account api

* feat: extend managed accounts for get-account api

* fix go-linter err

* Use GetApplicationsByOrganizationName
2022-08-30 00:57:27 +08:00
2394c8e2b4 Make sure newStaticBaseUrl is not empty 2022-08-29 21:27:47 +08:00
c62983d734 Use conf.GetConfigString() 2022-08-29 21:26:00 +08:00
5948782cdd fix: fix eslint error in webstorm (#1073) 2022-08-29 15:23:51 +08:00
674d1619dd fix: fix hot update error #1071 (#1072) 2022-08-29 13:45:31 +08:00
11b8b65ca0 feat: update antd and react to latest (#1069) 2022-08-28 23:14:04 +08:00
411d76798d fix: fix upload file security issue (#1063)
* fix: fix upload file security issue

* fix: fix
2022-08-25 11:34:09 +08:00
7b0b426a76 feat: check model grammar when saving and provide a ACL model as init data (#1062)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-08-24 17:21:05 +08:00
a383af0ebc feat: fix token info not contains roles and permissions (#1060)
* fix: fix token info not contains roles and permissions

feat: remove repeated code for obtaining roles and permissions in user controller

* Update user.go

* Update user.go

* Update token.go

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-08-24 01:41:26 +08:00
f02875e1b1 fix: enable captcha in the application which is not built-in (#1061) 2022-08-23 23:30:45 +08:00
e2921419b9 Add TestDeployStaticFiles() 2022-08-23 21:17:58 +08:00
42864700ec chore: update badges (#1058) 2022-08-23 13:11:42 +08:00
47 changed files with 4461 additions and 5567 deletions

View File

@ -8,7 +8,7 @@
<img alt="docker pull casbin/casdoor" src="https://img.shields.io/docker/pulls/casbin/casdoor.svg"> <img alt="docker pull casbin/casdoor" src="https://img.shields.io/docker/pulls/casbin/casdoor.svg">
</a> </a>
<a href="https://github.com/casdoor/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"> <img alt="GitHub Workflow Status (branch)" src="https://github.com/casdoor/casdoor/workflows/Build/badge.svg?style=flat-square">
</a> </a>
<a href="https://github.com/casdoor/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"> <img alt="GitHub Release" src="https://img.shields.io/github/v/release/casbin/casdoor.svg">
@ -42,65 +42,48 @@
</a> </a>
</p> </p>
## Online demo ## Online demo
- International: https://door.casdoor.org (read-only) - International: https://door.casdoor.org (read-only)
- Asian mirror: https://door.casdoor.com (read-only) - Asian mirror: https://door.casdoor.com (read-only)
- Asian mirror: https://demo.casdoor.com (read-write, will restore for every 5 minutes) - Asian mirror: https://demo.casdoor.com (read-write, will restore for every 5 minutes)
## Documentation ## Documentation
- International: https://casdoor.org - International: https://casdoor.org
- Asian mirror: https://docs.casdoor.cn - Asian mirror: https://docs.casdoor.cn
## Install ## Install
- By source code: https://casdoor.org/docs/basic/server-installation - By source code: https://casdoor.org/docs/basic/server-installation
- By Docker: https://casdoor.org/docs/basic/try-with-docker - By Docker: https://casdoor.org/docs/basic/try-with-docker
## How to connect to Casdoor? ## How to connect to Casdoor?
https://casdoor.org/docs/how-to-connect/overview https://casdoor.org/docs/how-to-connect/overview
## Casdoor Public API ## Casdoor Public API
- Docs: https://casdoor.org/docs/basic/public-api - Docs: https://casdoor.org/docs/basic/public-api
- Swagger: https://door.casdoor.com/swagger - Swagger: https://door.casdoor.com/swagger
## Integrations ## Integrations
https://casdoor.org/docs/integration/apisix https://casdoor.org/docs/integration/apisix
## How to contact? ## How to contact?
- Gitter: https://gitter.im/casbin/casdoor - Gitter: https://gitter.im/casbin/casdoor
- Forum: https://forum.casbin.com - Forum: https://forum.casbin.com
- Contact: https://tawk.to/chat/623352fea34c2456412b8c51/1fuc7od6e - Contact: https://tawk.to/chat/623352fea34c2456412b8c51/1fuc7od6e
## Contribute ## Contribute
For casdoor, if you have any questions, you can give Issues, or you can also directly start Pull Requests(but we recommend giving issues first to communicate with the community). For casdoor, if you have any questions, you can give Issues, or you can also directly start Pull Requests(but we recommend giving issues first to communicate with the community).
### I18n translation ### I18n translation
If you are contributing to casdoor, please note that we use [Crowdin](https://crowdin.com/project/casdoor-site) as translating platform and i18next as translating tool. When you add some words using i18next in the ```web/``` directory, please remember to add what you have added to the ```web/src/locales/en/data.json``` file. If you are contributing to casdoor, please note that we use [Crowdin](https://crowdin.com/project/casdoor-site) as translating platform and i18next as translating tool. When you add some words using i18next in the `web/` directory, please remember to add what you have added to the `web/src/locales/en/data.json` file.
## License ## License

View File

@ -128,6 +128,12 @@ p, *, *, GET, /api/get-release, *, *
} }
func IsAllowed(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool { func IsAllowed(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool {
if conf.IsDemoMode() {
if !isAllowedInDemoMode(subOwner, subName, method, urlPath, objOwner, objName) {
return false
}
}
res, err := Enforcer.Enforce(subOwner, subName, method, urlPath, objOwner, objName) res, err := Enforcer.Enforce(subOwner, subName, method, urlPath, objOwner, objName)
if err != nil { if err != nil {
panic(err) panic(err)
@ -135,3 +141,22 @@ func IsAllowed(subOwner string, subName string, method string, urlPath string, o
return res return res
} }
func isAllowedInDemoMode(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool {
if method == "POST" {
if urlPath == "/api/login" || urlPath == "/api/logout" || urlPath == "/api/signup" || urlPath == "/api/send-verification-code" {
return true
} else if urlPath == "/api/update-user" {
// Allow ordinary users to update their own information
if subOwner == objOwner && subName == objName && !(subOwner == "built-in" && subName == "admin") {
return true
}
return false
} else {
return false
}
}
// If method equals GET
return true
}

View File

@ -4,8 +4,8 @@ curl www.google.com -o /dev/null --connect-timeout 5 2 > /dev/null
if [ $? == 0 ] if [ $? == 0 ]
then then
echo "Successfully connected to Google, no need to use Go proxy" echo "Successfully connected to Google, no need to use Go proxy"
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o server .
else else
echo "Google is blocked, Go proxy is enabled: GOPROXY=https://goproxy.cn,direct" echo "Google is blocked, Go proxy is enabled: GOPROXY=https://goproxy.cn,direct"
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOPROXY=https://goproxy.cn,direct go build -ldflags="-w -s" -o server . export GOPROXY="https://goproxy.cn,direct"
fi fi
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o server .

View File

@ -16,4 +16,5 @@ verificationCodeTimeout = 10
initScore = 2000 initScore = 2000
logPostOnly = true logPostOnly = true
origin = origin =
staticBaseUrl = "https://cdn.casbin.org" staticBaseUrl = "https://cdn.casbin.org"
isDemoMode = false

View File

@ -28,7 +28,15 @@ func GetConfigString(key string) string {
if value, ok := os.LookupEnv(key); ok { if value, ok := os.LookupEnv(key); ok {
return value return value
} }
return beego.AppConfig.String(key)
res := beego.AppConfig.String(key)
if res == "" {
if key == "staticBaseUrl" {
res = "https://cdn.casbin.org"
}
}
return res
} }
func GetConfigBool(key string) (bool, error) { func GetConfigBool(key string) (bool, error) {
@ -72,3 +80,7 @@ func GetBeegoConfDataSourceName() string {
return dataSourceName return dataSourceName
} }
func IsDemoMode() bool {
return strings.ToLower(GetConfigString("isDemoMode")) == "true"
}

View File

@ -269,6 +269,11 @@ func (c *ApiController) GetAccount() {
return return
} }
managedAccounts := c.Input().Get("managedAccounts")
if managedAccounts == "1" {
user = object.ExtendManagedAccountsWithUser(user)
}
organization := object.GetMaskedOrganization(object.GetOrganizationByUser(user)) organization := object.GetMaskedOrganization(object.GetOrganizationByUser(user))
resp := Response{ resp := Response{
Status: "ok", Status: "ok",

View File

@ -119,12 +119,7 @@ func (c *ApiController) GetUser() {
user = object.GetUser(id) user = object.GetUser(id)
} }
if user != nil { object.ExtendUserWithRolesAndPermissions(user)
roles := object.GetRolesByUser(user.GetId())
user.Roles = roles
permissions := object.GetPermissionsByUser(user.GetId())
user.Permissions = permissions
}
c.Data["json"] = object.GetMaskedUser(user) c.Data["json"] = object.GetMaskedUser(user)
c.ServeJSON() c.ServeJSON()

70
deployment/deploy.go Normal file
View File

@ -0,0 +1,70 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package deployment
import (
"fmt"
"os"
"strings"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/storage"
"github.com/casdoor/casdoor/util"
"github.com/casdoor/oss"
)
func deployStaticFiles(provider *object.Provider) {
storageProvider := storage.GetStorageProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.RegionId, provider.Bucket, provider.Endpoint)
if storageProvider == nil {
panic(fmt.Sprintf("the provider type: %s is not supported", provider.Type))
}
uploadFolder(storageProvider, "js")
uploadFolder(storageProvider, "css")
updateHtml(provider.Domain)
}
func uploadFolder(storageProvider oss.StorageInterface, folder string) {
path := fmt.Sprintf("../web/build/static/%s/", folder)
filenames := util.ListFiles(path)
for _, filename := range filenames {
if !strings.HasSuffix(filename, folder) {
continue
}
file, err := os.Open(path + filename)
if err != nil {
panic(err)
}
objectKey := fmt.Sprintf("static/%s/%s", folder, filename)
_, err = storageProvider.Put(objectKey, file)
if err != nil {
panic(err)
}
fmt.Printf("Uploaded [%s] to [%s]\n", path, objectKey)
}
}
func updateHtml(domainPath string) {
htmlPath := "../web/build/index.html"
html := util.ReadStringFromPath(htmlPath)
html = strings.Replace(html, "\"/static/", fmt.Sprintf("\"%s", domainPath), -1)
util.WriteStringToPath(html, htmlPath)
fmt.Printf("Updated HTML to [%s]\n", html)
}

29
deployment/deploy_test.go Normal file
View File

@ -0,0 +1,29 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !skipCi
// +build !skipCi
package deployment
import (
"testing"
"github.com/casdoor/casdoor/object"
)
func TestDeployStaticFiles(t *testing.T) {
provider := object.GetProvider("admin/provider_storage_aliyun_oss")
deployStaticFiles(provider)
}

View File

@ -362,3 +362,34 @@ func IsAllowOrigin(origin string) bool {
return allowOrigin return allowOrigin
} }
func getApplicationMap(organization string) map[string]*Application {
applications := GetApplicationsByOrganizationName("admin", organization)
applicationMap := make(map[string]*Application)
for _, application := range applications {
applicationMap[application.Name] = application
}
return applicationMap
}
func ExtendManagedAccountsWithUser(user *User) *User {
if user.ManagedAccounts == nil || len(user.ManagedAccounts) == 0 {
return user
}
applicationMap := getApplicationMap(user.Owner)
var managedAccounts []ManagedAccount
for _, managedAccount := range user.ManagedAccounts {
application := applicationMap[managedAccount.Application]
if application != nil {
managedAccount.SigninUrl = application.SigninUrl
managedAccounts = append(managedAccounts, managedAccount)
}
}
user.ManagedAccounts = managedAccounts
return user
}

View File

@ -302,6 +302,10 @@ func CheckAccessPermission(userId string, application *Application) (bool, error
} }
if isHit { if isHit {
containsAsterisk := ContainsAsterisk(userId, permission.Users)
if containsAsterisk {
return true, err
}
enforcer := getEnforcer(permission) enforcer := getEnforcer(permission)
allowed, err = enforcer.Enforce(userId, application.Name, "read") allowed, err = enforcer.Enforce(userId, application.Name, "read")
break break

View File

@ -16,10 +16,28 @@
package object package object
import "github.com/go-gomail/gomail" import (
"crypto/tls"
"github.com/go-gomail/gomail"
)
func getDialer(provider *Provider) *gomail.Dialer {
dialer := &gomail.Dialer{}
if provider.Type == "SUBMAIL" {
dialer = gomail.NewDialer(provider.Host, provider.Port, provider.AppId, provider.ClientSecret)
dialer.TLSConfig = &tls.Config{InsecureSkipVerify: true}
} else {
dialer = gomail.NewDialer(provider.Host, provider.Port, provider.ClientId, provider.ClientSecret)
}
dialer.SSL = !provider.DisableSsl
return dialer
}
func SendEmail(provider *Provider, title string, content string, dest string, sender string) error { func SendEmail(provider *Provider, title string, content string, dest string, sender string) error {
dialer := gomail.NewDialer(provider.Host, provider.Port, provider.ClientId, provider.ClientSecret) dialer := getDialer(provider)
message := gomail.NewMessage() message := gomail.NewMessage()
message.SetAddressHeader("From", provider.ClientId, sender) message.SetAddressHeader("From", provider.ClientId, sender)
@ -32,8 +50,7 @@ func SendEmail(provider *Provider, title string, content string, dest string, se
// DailSmtpServer Dail Smtp server // DailSmtpServer Dail Smtp server
func DailSmtpServer(provider *Provider) error { func DailSmtpServer(provider *Provider) error {
dialer := gomail.NewDialer(provider.Host, provider.Port, provider.ClientId, provider.ClientSecret) dialer := getDialer(provider)
dialer.SSL = !provider.DisableSsl
sender, err := dialer.Dial() sender, err := dialer.Dial()
if err != nil { if err != nil {

View File

@ -19,14 +19,17 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/astaxie/beego" "github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/duo-labs/webauthn/webauthn" "github.com/duo-labs/webauthn/webauthn"
) )
func InitDb() { func InitDb() {
MigratePermissionRule()
existed := initBuiltInOrganization() existed := initBuiltInOrganization()
if !existed { if !existed {
initBuiltInModel()
initBuiltInPermission() initBuiltInPermission()
initBuiltInProvider() initBuiltInProvider()
initBuiltInUser() initBuiltInUser()
@ -38,8 +41,6 @@ func InitDb() {
initWebAuthn() initWebAuthn()
} }
var staticBaseUrl = beego.AppConfig.String("staticBaseUrl")
func initBuiltInOrganization() bool { func initBuiltInOrganization() bool {
organization := getOrganization("admin", "built-in") organization := getOrganization("admin", "built-in")
if organization != nil { if organization != nil {
@ -52,10 +53,10 @@ func initBuiltInOrganization() bool {
CreatedTime: util.GetCurrentTime(), CreatedTime: util.GetCurrentTime(),
DisplayName: "Built-in Organization", DisplayName: "Built-in Organization",
WebsiteUrl: "https://example.com", WebsiteUrl: "https://example.com",
Favicon: fmt.Sprintf("%s/img/casbin/favicon.ico", staticBaseUrl), Favicon: fmt.Sprintf("%s/img/casbin/favicon.ico", conf.GetConfigString("staticBaseUrl")),
PasswordType: "plain", PasswordType: "plain",
PhonePrefix: "86", PhonePrefix: "86",
DefaultAvatar: fmt.Sprintf("%s/img/casbin.svg", staticBaseUrl), DefaultAvatar: fmt.Sprintf("%s/img/casbin.svg", conf.GetConfigString("staticBaseUrl")),
Tags: []string{}, Tags: []string{},
AccountItems: []*AccountItem{ AccountItems: []*AccountItem{
{Name: "Organization", Visible: true, ViewRule: "Public", ModifyRule: "Admin"}, {Name: "Organization", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
@ -105,7 +106,7 @@ func initBuiltInUser() {
Type: "normal-user", Type: "normal-user",
Password: "123", Password: "123",
DisplayName: "Admin", DisplayName: "Admin",
Avatar: fmt.Sprintf("%s/img/casbin.svg", staticBaseUrl), Avatar: fmt.Sprintf("%s/img/casbin.svg", conf.GetConfigString("staticBaseUrl")),
Email: "admin@example.com", Email: "admin@example.com",
Phone: "12345678910", Phone: "12345678910",
Address: []string{}, Address: []string{},
@ -135,7 +136,7 @@ func initBuiltInApplication() {
Name: "app-built-in", Name: "app-built-in",
CreatedTime: util.GetCurrentTime(), CreatedTime: util.GetCurrentTime(),
DisplayName: "Casdoor", DisplayName: "Casdoor",
Logo: fmt.Sprintf("%s/img/casdoor-logo_1185x256.png", staticBaseUrl), Logo: fmt.Sprintf("%s/img/casdoor-logo_1185x256.png", conf.GetConfigString("staticBaseUrl")),
HomepageUrl: "https://casdoor.org", HomepageUrl: "https://casdoor.org",
Organization: "built-in", Organization: "built-in",
Cert: "cert-built-in", Cert: "cert-built-in",
@ -239,6 +240,33 @@ func initWebAuthn() {
gob.Register(webauthn.SessionData{}) gob.Register(webauthn.SessionData{})
} }
func initBuiltInModel() {
model := GetModel("built-in/model-built-in")
if model != nil {
return
}
model = &Model{
Owner: "built-in",
Name: "model-built-in",
CreatedTime: util.GetCurrentTime(),
DisplayName: "Built-in Model",
IsEnabled: true,
ModelText: `[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act`,
}
AddModel(model)
}
func initBuiltInPermission() { func initBuiltInPermission() {
permission := GetPermission("built-in/permission-built-in") permission := GetPermission("built-in/permission-built-in")
if permission != nil { if permission != nil {
@ -250,9 +278,10 @@ func initBuiltInPermission() {
Name: "permission-built-in", Name: "permission-built-in",
CreatedTime: util.GetCurrentTime(), CreatedTime: util.GetCurrentTime(),
DisplayName: "Built-in Permission", DisplayName: "Built-in Permission",
Users: []string{"built-in/admin"}, Users: []string{"built-in/*"},
Roles: []string{}, Roles: []string{},
Domains: []string{}, Domains: []string{},
Model: "model-built-in",
ResourceType: "Application", ResourceType: "Application",
Resources: []string{"app-built-in"}, Resources: []string{"app-built-in"},
Actions: []string{"Read", "Write", "Admin"}, Actions: []string{"Read", "Write", "Admin"},

View File

@ -17,6 +17,7 @@ package object
import ( import (
"fmt" "fmt"
"github.com/casbin/casbin/v2/model"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"xorm.io/core" "xorm.io/core"
) )
@ -85,13 +86,19 @@ func GetModel(id string) *Model {
return getModel(owner, name) return getModel(owner, name)
} }
func UpdateModel(id string, model *Model) bool { func UpdateModel(id string, modelObj *Model) bool {
owner, name := util.GetOwnerAndNameFromId(id) owner, name := util.GetOwnerAndNameFromId(id)
if getModel(owner, name) == nil { if getModel(owner, name) == nil {
return false return false
} }
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(model) // check model grammar
_, err := model.NewModelFromString(modelObj.ModelText)
if err != nil {
panic(err)
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(modelObj)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -43,6 +43,11 @@ type OidcDiscovery struct {
} }
func getOriginFromHost(host string) (string, string) { func getOriginFromHost(host string) (string, string) {
origin := conf.GetConfigString("origin")
if origin != "" {
return origin, origin
}
protocol := "https://" protocol := "https://"
if strings.HasPrefix(host, "localhost") { if strings.HasPrefix(host, "localhost") {
protocol = "http://" protocol = "http://"
@ -58,12 +63,6 @@ func getOriginFromHost(host string) (string, string) {
func GetOidcDiscovery(host string) OidcDiscovery { func GetOidcDiscovery(host string) OidcDiscovery {
originFrontend, originBackend := getOriginFromHost(host) originFrontend, originBackend := getOriginFromHost(host)
origin := conf.GetConfigString("origin")
if origin != "" {
originFrontend = origin
originBackend = origin
}
// Examples: // Examples:
// https://login.okta.com/.well-known/openid-configuration // https://login.okta.com/.well-known/openid-configuration
// https://auth0.auth0.com/.well-known/openid-configuration // https://auth0.auth0.com/.well-known/openid-configuration

View File

@ -16,6 +16,7 @@ package object
import ( import (
"fmt" "fmt"
"strings"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"xorm.io/core" "xorm.io/core"
@ -207,3 +208,44 @@ func GetPermissionsBySubmitter(owner string, submitter string) []*Permission {
return permissions return permissions
} }
func MigratePermissionRule() {
models := []*Model{}
err := adapter.Engine.Find(&models, &Model{})
if err != nil {
panic(err)
}
isHit := false
for _, model := range models {
if strings.Contains(model.ModelText, "permission") {
// update model table
model.ModelText = strings.Replace(model.ModelText, "permission,", "", -1)
UpdateModel(model.GetId(), model)
isHit = true
}
}
if isHit {
// update permission_rule table
sql := "UPDATE `permission_rule`SET V0 = V1, V1 = V2, V2 = V3, V3 = V4, V4 = V5 WHERE V0 IN (SELECT CONCAT(owner, '/', name) AS permission_id FROM `permission`)"
_, err = adapter.Engine.Exec(sql)
if err != nil {
return
}
}
}
func ContainsAsterisk(userId string, users []string) bool {
containsAsterisk := false
group, _ := util.GetOwnerAndNameFromId(userId)
for _, user := range users {
permissionGroup, permissionUserName := util.GetOwnerAndNameFromId(user)
if permissionGroup == group && permissionUserName == "*" {
containsAsterisk = true
break
}
}
return containsAsterisk
}

View File

@ -48,6 +48,7 @@ type Provider struct {
DisableSsl bool `json:"disableSsl"` DisableSsl bool `json:"disableSsl"`
Title string `xorm:"varchar(100)" json:"title"` Title string `xorm:"varchar(100)" json:"title"`
Content string `xorm:"varchar(1000)" json:"content"` Content string `xorm:"varchar(1000)" json:"content"`
Receiver string `xorm:"varchar(100)" json:"receiver"`
RegionId string `xorm:"varchar(100)" json:"regionId"` RegionId string `xorm:"varchar(100)" json:"regionId"`
SignName string `xorm:"varchar(100)" json:"signName"` SignName string `xorm:"varchar(100)" json:"signName"`

View File

@ -28,7 +28,6 @@ import (
"time" "time"
"github.com/RobotsAndPencils/go-saml" "github.com/RobotsAndPencils/go-saml"
"github.com/astaxie/beego"
"github.com/beevik/etree" "github.com/beevik/etree"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v4"
dsig "github.com/russellhaering/goxmldsig" dsig "github.com/russellhaering/goxmldsig"
@ -176,16 +175,12 @@ type Attribute struct {
} }
func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, error) { func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, error) {
//_, originBackend := getOriginFromHost(host)
cert := getCertByApplication(application) cert := getCertByApplication(application)
block, _ := pem.Decode([]byte(cert.Certificate)) block, _ := pem.Decode([]byte(cert.Certificate))
certificate := base64.StdEncoding.EncodeToString(block.Bytes) certificate := base64.StdEncoding.EncodeToString(block.Bytes)
origin := beego.AppConfig.String("origin")
originFrontend, originBackend := getOriginFromHost(host) originFrontend, originBackend := getOriginFromHost(host)
if origin != "" {
originBackend = origin
}
d := IdpEntityDescriptor{ d := IdpEntityDescriptor{
XMLName: xml.Name{ XMLName: xml.Name{
Local: "md:EntityDescriptor", Local: "md:EntityDescriptor",

View File

@ -70,10 +70,12 @@ func GenerateSamlLoginUrl(id, relayState string) (string, string, error) {
} }
func buildSp(provider *Provider, samlResponse string) (*saml2.SAMLServiceProvider, error) { func buildSp(provider *Provider, samlResponse string) (*saml2.SAMLServiceProvider, error) {
origin := conf.GetConfigString("origin")
certStore := dsig.MemoryX509CertificateStore{ certStore := dsig.MemoryX509CertificateStore{
Roots: []*x509.Certificate{}, Roots: []*x509.Certificate{},
} }
origin := conf.GetConfigString("origin")
certEncodedData := "" certEncodedData := ""
if samlResponse != "" { if samlResponse != "" {
certEncodedData = parseSamlResponse(samlResponse, provider.Type) certEncodedData = parseSamlResponse(samlResponse, provider.Type)

View File

@ -103,6 +103,11 @@ func uploadFile(provider *Provider, fullFilePath string, fileBuffer *bytes.Buffe
} }
func UploadFileSafe(provider *Provider, fullFilePath string, fileBuffer *bytes.Buffer) (string, string, error) { func UploadFileSafe(provider *Provider, fullFilePath string, fileBuffer *bytes.Buffer) (string, string, error) {
// check fullFilePath is there security issue
if strings.Contains(fullFilePath, "..") {
return "", "", fmt.Errorf("the fullFilePath: %s is not allowed", fullFilePath)
}
var fileUrl string var fileUrl string
var objectKey string var objectKey string
var err error var err error

View File

@ -287,6 +287,7 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU
} }
} }
ExtendUserWithRolesAndPermissions(user)
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, nonce, scope, host) accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, nonce, scope, host)
if err != nil { if err != nil {
panic(err) panic(err)
@ -421,6 +422,7 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
} }
} }
ExtendUserWithRolesAndPermissions(user)
newAccessToken, newRefreshToken, tokenName, err := generateJwtToken(application, user, "", scope, host) newAccessToken, newRefreshToken, tokenName, err := generateJwtToken(application, user, "", scope, host)
if err != nil { if err != nil {
return &TokenError{ return &TokenError{
@ -571,6 +573,7 @@ func GetPasswordToken(application *Application, username string, password string
} }
} }
ExtendUserWithRolesAndPermissions(user)
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", scope, host) accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", scope, host)
if err != nil { if err != nil {
return nil, &TokenError{ return nil, &TokenError{
@ -640,6 +643,7 @@ func GetClientCredentialsToken(application *Application, clientSecret string, sc
// GetTokenByUser // GetTokenByUser
// Implicit flow // Implicit flow
func GetTokenByUser(application *Application, user *User, scope string, host string) (*Token, error) { func GetTokenByUser(application *Application, user *User, scope string, host string) (*Token, error) {
ExtendUserWithRolesAndPermissions(user)
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", scope, host) accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", scope, host)
if err != nil { if err != nil {
return nil, err return nil, err
@ -726,6 +730,7 @@ func GetWechatMiniProgramToken(application *Application, code string, host strin
AddUser(user) AddUser(user)
} }
ExtendUserWithRolesAndPermissions(user)
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", host) accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", host)
if err != nil { if err != nil {
return nil, &TokenError{ return nil, &TokenError{

View File

@ -18,7 +18,6 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v4"
) )
@ -67,11 +66,7 @@ func generateJwtToken(application *Application, user *User, nonce string, scope
refreshExpireTime := nowTime.Add(time.Duration(application.RefreshExpireInHours) * time.Hour) refreshExpireTime := nowTime.Add(time.Duration(application.RefreshExpireInHours) * time.Hour)
user.Password = "" user.Password = ""
origin := conf.GetConfigString("origin")
_, originBackend := getOriginFromHost(host) _, originBackend := getOriginFromHost(host)
if origin != "" {
originBackend = origin
}
name := util.GenerateId() name := util.GenerateId()
jti := fmt.Sprintf("%s/%s", application.Owner, name) jti := fmt.Sprintf("%s/%s", application.Owner, name)

View File

@ -18,7 +18,6 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/duo-labs/webauthn/webauthn" "github.com/duo-labs/webauthn/webauthn"
"xorm.io/core" "xorm.io/core"
@ -527,11 +526,8 @@ func GetUserInfo(userId string, scope string, aud string, host string) (*Userinf
if user == nil { if user == nil {
return nil, fmt.Errorf("the user: %s doesn't exist", userId) return nil, fmt.Errorf("the user: %s doesn't exist", userId)
} }
origin := conf.GetConfigString("origin")
_, originBackend := getOriginFromHost(host) _, originBackend := getOriginFromHost(host)
if origin != "" {
originBackend = origin
}
resp := Userinfo{ resp := Userinfo{
Sub: user.Id, Sub: user.Id,
@ -566,3 +562,12 @@ func (user *User) GetId() string {
func isUserIdGlobalAdmin(userId string) bool { func isUserIdGlobalAdmin(userId string) bool {
return strings.HasPrefix(userId, "built-in/") return strings.HasPrefix(userId, "built-in/")
} }
func ExtendUserWithRolesAndPermissions(user *User) {
if user == nil {
return
}
user.Roles = GetRolesByUser(user.GetId())
user.Permissions = GetPermissionsByUser(user.GetId())
}

View File

@ -19,7 +19,7 @@ import (
"net/url" "net/url"
"strings" "strings"
"github.com/astaxie/beego" "github.com/casdoor/casdoor/conf"
"github.com/duo-labs/webauthn/protocol" "github.com/duo-labs/webauthn/protocol"
"github.com/duo-labs/webauthn/webauthn" "github.com/duo-labs/webauthn/webauthn"
) )
@ -27,20 +27,17 @@ import (
func GetWebAuthnObject(host string) *webauthn.WebAuthn { func GetWebAuthnObject(host string) *webauthn.WebAuthn {
var err error var err error
origin := beego.AppConfig.String("origin") _, originBackend := getOriginFromHost(host)
if origin == "" {
_, origin = getOriginFromHost(host)
}
localUrl, err := url.Parse(origin) localUrl, err := url.Parse(originBackend)
if err != nil { if err != nil {
panic("error when parsing origin:" + err.Error()) panic("error when parsing origin:" + err.Error())
} }
webAuthn, err := webauthn.New(&webauthn.Config{ webAuthn, err := webauthn.New(&webauthn.Config{
RPDisplayName: beego.AppConfig.String("appname"), // Display Name for your site RPDisplayName: conf.GetConfigString("appname"), // Display Name for your site
RPID: strings.Split(localUrl.Host, ":")[0], // Generally the domain name for your site, it's ok because splits cannot return empty array RPID: strings.Split(localUrl.Host, ":")[0], // Generally the domain name for your site, it's ok because splits cannot return empty array
RPOrigin: origin, // The origin URL for WebAuthn requests RPOrigin: originBackend, // The origin URL for WebAuthn requests
// RPIcon: "https://duo.com/logo.png", // Optional icon URL for your site // RPIcon: "https://duo.com/logo.png", // Optional icon URL for your site
}) })
if err != nil { if err != nil {

View File

@ -47,7 +47,7 @@ func SendVerificationCodeToEmail(organization *Organization, user *User, provide
sender := organization.DisplayName sender := organization.DisplayName
title := provider.Title title := provider.Title
code := getRandomCode(5) code := getRandomCode(6)
// "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes." // "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes."
content := fmt.Sprintf(provider.Content, code) content := fmt.Sprintf(provider.Content, code)
@ -63,7 +63,7 @@ func SendVerificationCodeToPhone(organization *Organization, user *User, provide
return errors.New("please set a SMS provider first") return errors.New("please set a SMS provider first")
} }
code := getRandomCode(5) code := getRandomCode(6)
if err := SendSms(provider, code, dest); err != nil { if err := SendSms(provider, code, dest); err != nil {
return err return err
} }

View File

@ -19,14 +19,14 @@ import (
"os" "os"
"strings" "strings"
"github.com/astaxie/beego"
"github.com/astaxie/beego/context" "github.com/astaxie/beego/context"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
) )
var ( var (
oldStaticBaseUrl = "https://cdn.casbin.org" oldStaticBaseUrl = "https://cdn.casbin.org"
newStaticBaseUrl = beego.AppConfig.String("staticBaseUrl") newStaticBaseUrl = conf.GetConfigString("staticBaseUrl")
) )
func StaticFilter(ctx *context.Context) { func StaticFilter(ctx *context.Context) {

View File

@ -16,6 +16,7 @@ package util
import ( import (
"fmt" "fmt"
"io/ioutil"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
@ -43,6 +44,23 @@ func EnsureFileFolderExists(path string) {
} }
} }
func ListFiles(path string) []string {
res := []string{}
files, err := ioutil.ReadDir(path)
if err != nil {
panic(err)
}
for _, f := range files {
if !f.IsDir() {
res = append(res, f.Name())
}
}
return res
}
func RemoveExt(filename string) string { func RemoveExt(filename string) string {
return filename[:len(filename)-len(filepath.Ext(filename))] return filename[:len(filename)-len(filepath.Ext(filename))]
} }

View File

@ -4,12 +4,18 @@
"es6": true, "es6": true,
"node": true "node": true
}, },
"parser": "babel-eslint", "parser": "@babel/eslint-parser",
"parserOptions": { "parserOptions": {
"ecmaVersion": 12, "ecmaVersion": 12,
"sourceType": "module", "sourceType": "module",
"ecmaFeatures": { "ecmaFeatures": {
"jsx": true "jsx": true
},
"requireConfigFile": false,
"babelOptions": {
"babelrc": false,
"configFile": false,
"presets": ["@babel/preset-react"]
} }
}, },
"settings": { "settings": {
@ -91,6 +97,7 @@
"react/jsx-key": "error", "react/jsx-key": "error",
"no-console": "error", "no-console": "error",
"eqeqeq": "error", "eqeqeq": "error",
"keyword-spacing": "error",
"react/prop-types": "off", "react/prop-types": "off",
"react/display-name": "off", "react/display-name": "off",

6
web/.stylelintrc.json Normal file
View File

@ -0,0 +1,6 @@
{
"extends": [
"stylelint-config-standard",
"stylelint-config-recommended-less"
]
}

17
web/babel.config.json Normal file
View File

@ -0,0 +1,17 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"edge": "17",
"firefox": "60",
"chrome": "67",
"safari": "11.1"
},
"useBuiltIns": "usage",
"corejs": "3.6.5"
}
]
]
}

View File

@ -3,35 +3,35 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@ant-design/icons": "^4.6.2", "@ant-design/icons": "^4.7.0",
"@craco/craco": "^6.1.1", "@craco/craco": "^6.4.5",
"@crowdin/cli": "^3.6.4", "@crowdin/cli": "^3.7.10",
"@testing-library/jest-dom": "^4.2.4", "@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2", "@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2", "@testing-library/user-event": "^7.1.2",
"antd": "^4.15.5", "antd": "^4.22.8",
"codemirror": "^5.61.1", "codemirror": "^5.61.1",
"copy-to-clipboard": "^3.3.1", "copy-to-clipboard": "^3.3.1",
"core-js": "^3.21.1", "core-js": "^3.25.0",
"craco-less": "^1.17.1", "craco-less": "^2.0.0",
"eslint-plugin-unused-imports": "^2.0.0", "eslint-plugin-unused-imports": "^2.0.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"i18n-iso-countries": "^7.0.0", "i18n-iso-countries": "^7.0.0",
"i18next": "^19.8.9", "i18next": "^19.8.9",
"moment": "^2.29.1", "moment": "^2.29.1",
"qs": "^6.10.2", "qs": "^6.10.2",
"react": "^17.0.2", "react": "^18.2.0",
"react-app-polyfill": "^3.0.0", "react-app-polyfill": "^3.0.0",
"react-codemirror2": "^7.2.1", "react-codemirror2": "^7.2.1",
"react-cropper": "^2.1.7", "react-cropper": "^2.1.7",
"react-device-detect": "^1.14.0", "react-device-detect": "^2.2.2",
"react-dom": "^17.0.2", "react-dom": "^18.2.0",
"react-github-corner": "^2.5.0", "react-github-corner": "^2.5.0",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-highlight-words": "^0.17.0", "react-highlight-words": "^0.18.0",
"react-i18next": "^11.8.7", "react-i18next": "^11.8.7",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.3.3",
"react-scripts": "4.0.3", "react-scripts": "5.0.1",
"react-social-login-buttons": "^3.4.0" "react-social-login-buttons": "^3.4.0"
}, },
"scripts": { "scripts": {
@ -41,7 +41,8 @@
"eject": "craco eject", "eject": "craco eject",
"crowdin:sync": "crowdin upload && crowdin download", "crowdin:sync": "crowdin upload && crowdin download",
"preinstall": "node -e \"if (process.env.npm_execpath.indexOf('yarn') === -1) throw new Error('Use yarn for installing: https://yarnpkg.com/en/docs/install')\"", "preinstall": "node -e \"if (process.env.npm_execpath.indexOf('yarn') === -1) throw new Error('Use yarn for installing: https://yarnpkg.com/en/docs/install')\"",
"fix": "eslint --fix ." "fix": "eslint --fix src/**/*.{js,jsx,ts,tsx}",
"lint:css": "stylelint src/**/*.{css,less} --fix"
}, },
"eslintConfig": { "eslintConfig": {
"extends": "react-app" "extends": "react-app"
@ -61,15 +62,24 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.18.13",
"@babel/eslint-parser": "^7.18.9",
"@babel/preset-react": "^7.18.6",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^7.11.0", "eslint": "8.22.0",
"eslint-plugin-react": "^7.30.1", "eslint-plugin-react": "^7.31.1",
"husky": "^4.3.8", "husky": "^4.3.8",
"lint-staged": "^13.0.3" "lint-staged": "^13.0.3",
"stylelint": "^14.11.0",
"stylelint-config-recommended-less": "^1.0.4",
"stylelint-config-standard": "^28.0.0"
}, },
"lint-staged": { "lint-staged": {
"src/**/*.{js,jsx,css,sass,ts,tsx}": [ "src/**/*.{css,less}": [
"yarn fix" "stylelint --fix"
],
"src/**/*.{js,jsx,ts,tsx}": [
"eslint --fix"
] ]
}, },
"husky": { "husky": {

View File

@ -527,7 +527,7 @@ class App extends Component {
} }
renderRouter() { renderRouter() {
return( return (
<div> <div>
<Switch> <Switch>
<Route exact path="/result" render={(props) => this.renderHomeIfLoggedIn(<ResultPage {...props} />)} /> <Route exact path="/result" render={(props) => this.renderHomeIfLoggedIn(<ResultPage {...props} />)} />
@ -595,7 +595,7 @@ class App extends Component {
// theme="dark" // theme="dark"
mode={(Setting.isMobile() && this.isStartPages()) ? "inline" : "horizontal"} mode={(Setting.isMobile() && this.isStartPages()) ? "inline" : "horizontal"}
selectedKeys={[`${this.state.selectedMenuKey}`]} selectedKeys={[`${this.state.selectedMenuKey}`]}
style={{lineHeight: "64px", width: "80%", position: "absolute"}} style={{lineHeight: "64px", width: "80%", position: "absolute", left: "145px"}}
> >
{ {
this.renderMenu() this.renderMenu()
@ -618,7 +618,7 @@ class App extends Component {
</div> </div>
); );
} else { } else {
return( return (
<div> <div>
<Header style={{padding: "0", marginBottom: "3px"}}> <Header style={{padding: "0", marginBottom: "3px"}}>
{ {

View File

@ -1,6 +1,8 @@
@import '~antd/dist/antd.less'; /* stylelint-disable at-rule-name-case */
/* stylelint-disable selector-class-pattern */
@import "~antd/dist/antd.less";
@StaticBaseUrl:"https://cdn.casbin.org"; @StaticBaseUrl: "https://cdn.casbin.org";
.App { .App {
text-align: center; text-align: center;
@ -69,8 +71,8 @@
} }
.content-warp-card { .content-warp-card {
box-shadow: 0 1px 5px 0 rgba(51, 51, 51, 0.14); box-shadow: 0 1px 5px 0 rgb(51 51 51 / 14%);
margin: 5px 5px 5px 5px; margin: 5px;
flex: 1; flex: 1;
align-items: stretch; align-items: stretch;
} }

View File

@ -39,101 +39,102 @@ class BaseListPage extends React.Component {
this.fetch({pagination}); this.fetch({pagination});
} }
getColumnSearchProps = dataIndex => ({ getColumnSearchProps = dataIndex => ({
filterDropdown: ({setSelectedKeys, selectedKeys, confirm, clearFilters}) => ( filterDropdown: ({setSelectedKeys, selectedKeys, confirm, clearFilters}) => (
<div style={{padding: 8}}> <div style={{padding: 8}}>
<Input <Input
ref={node => { ref={node => {
this.searchInput = node; this.searchInput = node;
}} }}
placeholder={`Search ${dataIndex}`} placeholder={`Search ${dataIndex}`}
value={selectedKeys[0]} value={selectedKeys[0]}
onChange={e => setSelectedKeys(e.target.value ? [e.target.value] : [])} onChange={e => setSelectedKeys(e.target.value ? [e.target.value] : [])}
onPressEnter={() => this.handleSearch(selectedKeys, confirm, dataIndex)} onPressEnter={() => this.handleSearch(selectedKeys, confirm, dataIndex)}
style={{marginBottom: 8, display: "block"}} style={{marginBottom: 8, display: "block"}}
/> />
<Space>
<Button <Space>
type="primary" <Button
onClick={() => this.handleSearch(selectedKeys, confirm, dataIndex)} type="primary"
icon={<SearchOutlined />} onClick={() => this.handleSearch(selectedKeys, confirm, dataIndex)}
size="small" icon={<SearchOutlined />}
style={{width: 90}} size="small"
> style={{width: 90}}
>
Search Search
</Button> </Button>
<Button onClick={() => this.handleReset(clearFilters)} size="small" style={{width: 90}}> <Button onClick={() => this.handleReset(clearFilters)} size="small" style={{width: 90}}>
Reset Reset
</Button> </Button>
<Button <Button
type="link" type="link"
size="small" size="small"
onClick={() => { onClick={() => {
confirm({closeDropdown: false}); confirm({closeDropdown: false});
this.setState({ this.setState({
searchText: selectedKeys[0], searchText: selectedKeys[0],
searchedColumn: dataIndex, searchedColumn: dataIndex,
}); });
}} }}
> >
Filter Filter
</Button> </Button>
</Space> </Space>
</div> </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
), ),
filterIcon: filtered => <SearchOutlined style={{color: filtered ? "#1890ff" : undefined}} />, });
onFilter: (value, record) =>
record[dataIndex] handleSearch = (selectedKeys, confirm, dataIndex) => {
? record[dataIndex].toString().toLowerCase().includes(value.toLowerCase()) this.fetch({searchText: selectedKeys[0], searchedColumn: dataIndex, pagination: this.state.pagination});
: "", };
onFilterDropdownVisibleChange: visible => {
if (visible) { handleReset = clearFilters => {
setTimeout(() => this.searchInput.select(), 100); clearFilters();
} const {pagination} = this.state;
}, this.fetch({pagination});
render: text => };
this.state.searchedColumn === dataIndex ? (
<Highlighter handleTableChange = (pagination, filters, sorter) => {
highlightStyle={{backgroundColor: "#ffc069", padding: 0}} this.fetch({
searchWords={[this.state.searchText]} sortField: sorter.field,
autoEscape sortOrder: sorter.order,
textToHighlight={text ? text.toString() : ""} pagination,
/> ...filters,
) : ( searchText: this.state.searchText,
text searchedColumn: this.state.searchedColumn,
),
}); });
};
handleSearch = (selectedKeys, confirm, dataIndex) => { render() {
this.fetch({searchText: selectedKeys[0], searchedColumn: dataIndex, pagination: this.state.pagination}); return (
}; <div>
{
handleReset = clearFilters => { this.renderTable(this.state.data)
clearFilters(); }
const {pagination} = this.state; </div>
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; export default BaseListPage;

View File

@ -38,7 +38,7 @@ class ManagedAccountTable extends React.Component {
} }
addRow(table) { addRow(table) {
const row = {application: "", username: "", password: "", signinUrl: ""}; const row = {application: "", username: "", password: ""};
if (table === undefined || table === null) { if (table === undefined || table === null) {
table = []; table = [];
} }
@ -69,16 +69,11 @@ class ManagedAccountTable extends React.Component {
key: "application", key: "application",
render: (text, record, index) => { render: (text, record, index) => {
const items = this.props.applications; const items = this.props.applications;
const signinUrlMap = new Map();
for (const application of items) {
signinUrlMap.set(application.name, application.signinUrl);
}
return ( return (
<Select virtual={false} style={{width: "100%"}} <Select virtual={false} style={{width: "100%"}}
value={text} value={text}
onChange={value => { onChange={value => {
this.updateField(table, index, "application", value); this.updateField(table, index, "application", value);
this.updateField(table, index, "signinUrl", signinUrlMap.get(value));
}} > }} >
{ {
items.map((item, index) => <Option key={index} value={item.name}>{item.name}</Option>) items.map((item, index) => <Option key={index} value={item.name}>{item.name}</Option>)

View File

@ -88,7 +88,7 @@ class ModelListPage extends BaseListPage {
...this.getColumnSearchProps("name"), ...this.getColumnSearchProps("name"),
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<Link to={`/models/${text}`}> <Link to={`/models/${record.owner}/${text}`}>
{text} {text}
</Link> </Link>
); );

View File

@ -34,7 +34,6 @@ class ProviderEditPage extends React.Component {
providerName: props.match.params.providerName, providerName: props.match.params.providerName,
provider: null, provider: null,
mode: props.location.mode !== undefined ? props.location.mode : "edit", mode: props.location.mode !== undefined ? props.location.mode : "edit",
testEmail: this.props.account["email"] !== undefined ? this.props.account["email"] : "",
}; };
} }
@ -131,6 +130,9 @@ class ProviderEditPage extends React.Component {
} else if (this.state.provider.category === "SMS" && this.state.provider.type === "Huawei Cloud SMS") { } else if (this.state.provider.category === "SMS" && this.state.provider.type === "Huawei Cloud SMS") {
text = i18next.t("provider:Channel No."); text = i18next.t("provider:Channel No.");
tooltip = i18next.t("provider:Channel No. - Tooltip"); tooltip = i18next.t("provider:Channel No. - Tooltip");
} else if (this.state.provider.category === "Email" && this.state.provider.type === "SUBMAIL") {
text = i18next.t("provider:App ID");
tooltip = i18next.t("provider:App ID - Tooltip");
} else { } else {
return null; return null;
} }
@ -199,9 +201,12 @@ class ProviderEditPage extends React.Component {
this.updateProviderField("type", "GitHub"); this.updateProviderField("type", "GitHub");
} else if (value === "Email") { } else if (value === "Email") {
this.updateProviderField("type", "Default"); this.updateProviderField("type", "Default");
this.updateProviderField("host", "smtp.example.com");
this.updateProviderField("port", 465);
this.updateProviderField("disableSsl", false); this.updateProviderField("disableSsl", false);
this.updateProviderField("title", "Casdoor Verification Code"); this.updateProviderField("title", "Casdoor Verification Code");
this.updateProviderField("content", "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes."); this.updateProviderField("content", "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes.");
this.updateProviderField("receiver", this.props.account.email);
} else if (value === "SMS") { } else if (value === "SMS") {
this.updateProviderField("type", "Aliyun SMS"); this.updateProviderField("type", "Aliyun SMS");
} else if (value === "Storage") { } else if (value === "Storage") {
@ -536,7 +541,7 @@ class ProviderEditPage extends React.Component {
{Setting.getLabel(i18next.t("provider:Email Content"), i18next.t("provider:Email Content - Tooltip"))} : {Setting.getLabel(i18next.t("provider:Email Content"), i18next.t("provider:Email Content - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<TextArea autoSize={{minRows: 1, maxRows: 100}} value={this.state.provider.content} onChange={e => { <TextArea autoSize={{minRows: 3, maxRows: 100}} value={this.state.provider.content} onChange={e => {
this.updateProviderField("content", e.target.value); this.updateProviderField("content", e.target.value);
}} /> }} />
</Col> </Col>
@ -546,19 +551,16 @@ class ProviderEditPage extends React.Component {
{Setting.getLabel(i18next.t("provider:Test Email"), i18next.t("provider:Test Email - Tooltip"))} : {Setting.getLabel(i18next.t("provider:Test Email"), i18next.t("provider:Test Email - Tooltip"))} :
</Col> </Col>
<Col span={4} > <Col span={4} >
<Input value={this.state.testEmail} <Input value={this.state.provider.receiver} placeholder = {i18next.t("user:Input your email")} onChange={e => {
placeHolder = {i18next.t("user:Input your email")} this.updateProviderField("receiver", e.target.value);
onChange={e => { }} />
this.setState({testEmail: e.target.value});
}} />
</Col> </Col>
<Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary" <Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary" onClick={() => ProviderEditTestEmail.connectSmtpServer(this.state.provider)} >
onClick={() => ProviderEditTestEmail.connectSmtpServer(this.state.provider)} >
{i18next.t("provider:Test Connection")} {i18next.t("provider:Test Connection")}
</Button> </Button>
<Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary" <Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary"
disabled={!Setting.isValidEmail(this.state.testEmail)} disabled={!Setting.isValidEmail(this.state.provider.receiver)}
onClick={() => ProviderEditTestEmail.sendTestEmail(this.state.provider, this.state.testEmail)} > onClick={() => ProviderEditTestEmail.sendTestEmail(this.state.provider, this.state.provider.receiver)} >
{i18next.t("provider:Send Test Email")} {i18next.t("provider:Send Test Email")}
</Button> </Button>
</Row> </Row>

View File

@ -56,8 +56,12 @@ export const ResetModal = (props) => {
}); });
}; };
let placeHolder = ""; let placeholder = "";
if (destType === "email") {placeHolder = i18next.t("user:Input your email");} else if (destType === "phone") {placeHolder = i18next.t("user:Input your phone number");} if (destType === "email") {
placeholder = i18next.t("user:Input your email");
} else if (destType === "phone") {
placeholder = i18next.t("user:Input your phone number");
}
return ( return (
<Row> <Row>
@ -80,7 +84,7 @@ export const ResetModal = (props) => {
<Input <Input
addonBefore={destType === "email" ? i18next.t("user:New Email") : i18next.t("user:New phone")} addonBefore={destType === "email" ? i18next.t("user:New Email") : i18next.t("user:New phone")}
prefix={destType === "email" ? <MailOutlined /> : <PhoneOutlined />} prefix={destType === "email" ? <MailOutlined /> : <PhoneOutlined />}
placeholder={placeHolder} placeholder={placeholder}
onChange={e => setDest(e.target.value)} onChange={e => setDest(e.target.value)}
/> />
</Row> </Row>
@ -89,6 +93,7 @@ export const ResetModal = (props) => {
textBefore={i18next.t("code:Code You Received")} textBefore={i18next.t("code:Code You Received")}
onChange={setCode} onChange={setCode}
onButtonClickArgs={[dest, destType, Setting.getApplicationName(application)]} onButtonClickArgs={[dest, destType, Setting.getApplicationName(application)]}
application={application}
/> />
</Row> </Row>
</Col> </Col>

View File

@ -450,9 +450,9 @@ export function trim(str, ch) {
let start = 0; let start = 0;
let end = str.length; let end = str.length;
while(start < end && str[start] === ch) {++start;} while (start < end && str[start] === ch) {++start;}
while(end > start && str[end - 1] === ch) {--end;} while (end > start && str[end - 1] === ch) {--end;}
return (start > 0 || end < str.length) ? str.substring(start, end) : str; return (start > 0 || end < str.length) ? str.substring(start, end) : str;
} }
@ -633,6 +633,7 @@ export function getProviderTypeOptions(category) {
return ( return (
[ [
{id: "Default", name: "Default"}, {id: "Default", name: "Default"},
{id: "SUBMAIL", name: "SUBMAIL"},
] ]
); );
} else if (category === "SMS") { } else if (category === "SMS") {

View File

@ -50,8 +50,12 @@ class AuthCallback extends React.Component {
// Casdoor's own login page, so "code" is not necessary // Casdoor's own login page, so "code" is not necessary
if (realRedirectUri === null) { if (realRedirectUri === null) {
const samlRequest = innerParams.get("SAMLRequest"); const samlRequest = innerParams.get("SAMLRequest");
// cas don't use 'redirect_url', it is called 'service'
const casService = innerParams.get("service");
if (samlRequest !== null && samlRequest !== undefined && samlRequest !== "") { if (samlRequest !== null && samlRequest !== undefined && samlRequest !== "") {
return "saml"; return "saml";
} else if (casService !== null && casService !== undefined && casService !== "") {
return "cas";
} }
return "login"; return "login";
} }
@ -97,6 +101,7 @@ class AuthCallback extends React.Component {
const providerName = innerParams.get("provider"); const providerName = innerParams.get("provider");
const method = innerParams.get("method"); const method = innerParams.get("method");
const samlRequest = innerParams.get("SAMLRequest"); const samlRequest = innerParams.get("SAMLRequest");
const casService = innerParams.get("service");
const redirectUri = `${window.location.origin}/callback`; const redirectUri = `${window.location.origin}/callback`;
@ -111,6 +116,31 @@ class AuthCallback extends React.Component {
redirectUri: redirectUri, redirectUri: redirectUri,
method: method, method: method,
}; };
if (this.getResponseType() === "cas") {
// user is using casdoor as cas sso server, and wants the ticket to be acquired
AuthBackend.loginCas(body, {"service": casService}).then((res) => {
if (res.status === "ok") {
let msg = "Logged in successfully.";
if (casService === "") {
// If service was not specified, Casdoor must display a message notifying the client that it has successfully initiated a single sign-on session.
msg += "Now you can visit apps protected by Casdoor.";
}
Util.showMessage("success", msg);
if (casService !== "") {
const st = res.data;
const newUrl = new URL(casService);
newUrl.searchParams.append("ticket", st);
window.location.href = newUrl.toString();
}
} else {
Util.showMessage("error", `Failed to log in: ${res.msg}`);
}
});
return;
}
// OAuth
const oAuthParams = Util.getOAuthGetParameters(innerParams); const oAuthParams = Util.getOAuthGetParameters(innerParams);
const concatChar = oAuthParams?.redirectUri?.includes("?") ? "&" : "?"; const concatChar = oAuthParams?.redirectUri?.includes("?") ? "&" : "?";
AuthBackend.login(body, oAuthParams) AuthBackend.login(body, oAuthParams)

View File

@ -353,11 +353,13 @@ class ForgetPage extends React.Component {
<CountDownInput <CountDownInput
disabled={this.state.username === "" || this.state.verifyType === ""} disabled={this.state.username === "" || this.state.verifyType === ""}
onButtonClickArgs={[this.state.email, "email", Setting.getApplicationName(this.state.application), this.state.name]} onButtonClickArgs={[this.state.email, "email", Setting.getApplicationName(this.state.application), this.state.name]}
application={application}
/> />
) : ( ) : (
<CountDownInput <CountDownInput
disabled={this.state.username === "" || this.state.verifyType === ""} disabled={this.state.username === "" || this.state.verifyType === ""}
onButtonClickArgs={[this.state.phone, "phone", Setting.getApplicationName(this.state.application), this.state.name]} onButtonClickArgs={[this.state.phone, "phone", Setting.getApplicationName(this.state.application), this.state.name]}
application={application}
/> />
)} )}
</Form.Item> </Form.Item>

View File

@ -625,6 +625,7 @@ class LoginPage extends React.Component {
<CountDownInput <CountDownInput
disabled={this.state.username?.length === 0 || !this.state.validEmailOrPhone} disabled={this.state.username?.length === 0 || !this.state.validEmailOrPhone}
onButtonClickArgs={[this.state.username, this.state.validEmail ? "email" : "phone", Setting.getApplicationName(application)]} onButtonClickArgs={[this.state.username, this.state.validEmail ? "email" : "phone", Setting.getApplicationName(application)]}
application={application}
/> />
</Form.Item> </Form.Item>
) : ( ) : (

View File

@ -360,6 +360,7 @@ class SignupPage extends React.Component {
<CountDownInput <CountDownInput
disabled={!this.state.validEmail} disabled={!this.state.validEmail}
onButtonClickArgs={[this.state.email, "email", Setting.getApplicationName(application)]} onButtonClickArgs={[this.state.email, "email", Setting.getApplicationName(application)]}
application={application}
/> />
</Form.Item> </Form.Item>
} }
@ -412,6 +413,7 @@ class SignupPage extends React.Component {
<CountDownInput <CountDownInput
disabled={!this.state.validPhone} disabled={!this.state.validPhone}
onButtonClickArgs={[this.state.phone, "phone", Setting.getApplicationName(application)]} onButtonClickArgs={[this.state.phone, "phone", Setting.getApplicationName(application)]}
application={application}
/> />
</Form.Item> </Form.Item>
</React.Fragment> </React.Fragment>
@ -582,9 +584,9 @@ class SignupPage extends React.Component {
&nbsp;&nbsp;{i18next.t("signup:Have account?")}&nbsp; &nbsp;&nbsp;{i18next.t("signup:Have account?")}&nbsp;
<a onClick={() => { <a onClick={() => {
const linkInStorage = sessionStorage.getItem("signinUrl"); const linkInStorage = sessionStorage.getItem("signinUrl");
if(linkInStorage !== null && linkInStorage !== "") { if (linkInStorage !== null && linkInStorage !== "") {
Setting.goToLink(linkInStorage); Setting.goToLink(linkInStorage);
}else{ } else {
Setting.goToLogin(this, application); Setting.goToLogin(this, application);
} }
}}> }}>

View File

@ -17,13 +17,12 @@ import React from "react";
import i18next from "i18next"; import i18next from "i18next";
import * as UserBackend from "../backend/UserBackend"; import * as UserBackend from "../backend/UserBackend";
import {SafetyOutlined} from "@ant-design/icons"; import {SafetyOutlined} from "@ant-design/icons";
import {authConfig} from "../auth/Auth";
import {CaptchaWidget} from "./CaptchaWidget"; import {CaptchaWidget} from "./CaptchaWidget";
const {Search} = Input; const {Search} = Input;
export const CountDownInput = (props) => { export const CountDownInput = (props) => {
const {disabled, textBefore, onChange, onButtonClickArgs} = props; const {disabled, textBefore, onChange, onButtonClickArgs, application} = props;
const [visible, setVisible] = React.useState(false); const [visible, setVisible] = React.useState(false);
const [key, setKey] = React.useState(""); const [key, setKey] = React.useState("");
const [captchaImg, setCaptchaImg] = React.useState(""); const [captchaImg, setCaptchaImg] = React.useState("");
@ -69,7 +68,7 @@ export const CountDownInput = (props) => {
}; };
const loadCaptcha = () => { const loadCaptcha = () => {
UserBackend.getCaptcha("admin", authConfig.appName, false).then(res => { UserBackend.getCaptcha(application.owner, application.name, false).then(res => {
if (res.type === "none") { if (res.type === "none") {
UserBackend.sendCode("none", "", "", ...onButtonClickArgs).then(res => { UserBackend.sendCode("none", "", "", ...onButtonClickArgs).then(res => {
if (res) { if (res) {

View File

@ -1,14 +1,28 @@
body { body {
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', font-family:
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', -apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
Oxygen,
Ubuntu,
Cantarell,
"Fira Sans",
"Droid Sans",
"Helvetica Neue",
sans-serif; sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
code { code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', font-family:
source-code-pro,
Menlo,
Monaco,
Consolas,
"Courier New",
monospace; monospace;
} }
@ -17,12 +31,14 @@ code {
background-size: 130px, 27px; background-size: 130px, 27px;
width: 130px; width: 130px;
height: 27px; height: 27px;
/*background: rgba(0, 0, 0, 0.2);*/ margin: 17px 0 16px 15px;
margin: 17px 10px 16px 20px;
float: left; float: left;
} }
.ant-table.ant-table-middle .ant-table-title, .ant-table.ant-table-middle .ant-table-footer, .ant-table.ant-table-middle thead > tr > th, .ant-table.ant-table-middle tbody > tr > td { .ant-table.ant-table-middle .ant-table-title,
.ant-table.ant-table-middle .ant-table-footer,
.ant-table.ant-table-middle thead > tr > th,
.ant-table.ant-table-middle tbody > tr > td {
padding: 1px 8px !important; padding: 1px 8px !important;
} }

View File

@ -16,18 +16,19 @@ import "core-js/es";
import "react-app-polyfill/ie9"; import "react-app-polyfill/ie9";
import "react-app-polyfill/stable"; import "react-app-polyfill/stable";
import React from "react"; import React from "react";
import ReactDOM from "react-dom"; import {createRoot} from "react-dom/client";
import "./index.css"; import "./index.css";
import App from "./App"; import App from "./App";
import * as serviceWorker from "./serviceWorker"; import * as serviceWorker from "./serviceWorker";
import {BrowserRouter} from "react-router-dom"; import {BrowserRouter} from "react-router-dom";
ReactDOM.render( const container = document.getElementById("root");
<BrowserRouter>
<App /> const app = createRoot(container);
</BrowserRouter>,
document.getElementById("root") app.render(<BrowserRouter>
); <App />
</BrowserRouter>);
// If you want your app to work offline and load faster, you can change // If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls. // unregister() to register() below. Note this comes with some pitfalls.

File diff suppressed because it is too large Load Diff