forked from casdoor/casdoor
Compare commits
39 Commits
copilot/ad
...
v2.341.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7df722a103 | ||
|
|
04b1ca1157 | ||
|
|
b0fecefeb7 | ||
|
|
167d24fb1f | ||
|
|
dc58ac0503 | ||
|
|
038d021797 | ||
|
|
7ba660fd7f | ||
|
|
b1c31a4a9d | ||
|
|
90d7add503 | ||
|
|
c961e75ad3 | ||
|
|
547189a034 | ||
|
|
be725eda74 | ||
|
|
0765b352c9 | ||
|
|
a2a8b582d9 | ||
|
|
0973652be4 | ||
|
|
fef75715bf | ||
|
|
4f78d56e31 | ||
|
|
712bc756bc | ||
|
|
1c9952e3d9 | ||
|
|
bbaa28133f | ||
|
|
baef7680ea | ||
|
|
d15b66177c | ||
|
|
5ce6bac529 | ||
|
|
0621f35665 | ||
|
|
1ac2490419 | ||
|
|
8c50ada494 | ||
|
|
22da90576e | ||
|
|
b00404cb3a | ||
|
|
2ed27f4f0a | ||
|
|
bf538d5260 | ||
|
|
13ee5fd150 | ||
|
|
04cdd5a012 | ||
|
|
7b4873734b | ||
|
|
8d2290944a | ||
|
|
6a2bba1627 | ||
|
|
07554bbbe5 | ||
|
|
a050403ee5 | ||
|
|
118eb0af80 | ||
|
|
c16aebe642 |
@@ -1,5 +1,5 @@
|
||||
<h1 align="center" style="border-bottom: none;">📦⚡️ Casdoor</h1>
|
||||
<h3 align="center">An open-source UI-first Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML, CAS, LDAP, SCIM, WebAuthn, TOTP, MFA and RADIUS</h3>
|
||||
<h3 align="center">An open-source AI-first Identity and Access Management (IAM) /AI MCP gateway and auth server with web UI supporting MCP, A2A, OAuth 2.1, OIDC, SAML, CAS, LDAP, SCIM, WebAuthn, TOTP, MFA, Face ID, Google Workspace, Azure AD</h3>
|
||||
<p align="center">
|
||||
<a href="#badge">
|
||||
<img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">
|
||||
|
||||
@@ -68,6 +68,7 @@ p, *, *, POST, /api/upload-users, *, *
|
||||
p, *, *, GET, /api/get-resources, *, *
|
||||
p, *, *, GET, /api/get-records, *, *
|
||||
p, *, *, GET, /api/get-product, *, *
|
||||
p, *, *, GET, /api/get-products, *, *
|
||||
p, *, *, GET, /api/get-order, *, *
|
||||
p, *, *, GET, /api/get-orders, *, *
|
||||
p, *, *, GET, /api/get-user-orders, *, *
|
||||
|
||||
107
certificate/account.go
Normal file
107
certificate/account.go
Normal file
@@ -0,0 +1,107 @@
|
||||
// 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 certificate
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
|
||||
"github.com/casbin/lego/v4/acme"
|
||||
"github.com/casbin/lego/v4/certcrypto"
|
||||
"github.com/casbin/lego/v4/lego"
|
||||
"github.com/casbin/lego/v4/registration"
|
||||
"github.com/casdoor/casdoor/proxy"
|
||||
)
|
||||
|
||||
type Account struct {
|
||||
Email string
|
||||
Registration *registration.Resource
|
||||
Key crypto.PrivateKey
|
||||
}
|
||||
|
||||
/** Implementation of the registration.User interface **/
|
||||
|
||||
// GetEmail returns the email address for the account.
|
||||
func (a *Account) GetEmail() string {
|
||||
return a.Email
|
||||
}
|
||||
|
||||
// GetPrivateKey returns the private RSA account key.
|
||||
func (a *Account) GetPrivateKey() crypto.PrivateKey {
|
||||
return a.Key
|
||||
}
|
||||
|
||||
// GetRegistration returns the server registration.
|
||||
func (a *Account) GetRegistration() *registration.Resource {
|
||||
return a.Registration
|
||||
}
|
||||
|
||||
func getLegoClientAndAccount(email string, privateKey string, devMode bool) (*lego.Client, *Account, error) {
|
||||
key, err := decodeEccKey(privateKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
account := &Account{
|
||||
Email: email,
|
||||
Key: key,
|
||||
}
|
||||
|
||||
config := lego.NewConfig(account)
|
||||
if devMode {
|
||||
config.CADirURL = lego.LEDirectoryStaging
|
||||
} else {
|
||||
config.CADirURL = lego.LEDirectoryProduction
|
||||
}
|
||||
|
||||
config.Certificate.KeyType = certcrypto.RSA2048
|
||||
config.HTTPClient = proxy.ProxyHttpClient
|
||||
|
||||
client, err := lego.NewClient(config)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return client, account, err
|
||||
}
|
||||
|
||||
// GetAcmeClient Incoming an email ,a privatekey and a Boolean value that controls the opening of the test environment
|
||||
// When this function is started for the first time, it will initialize the account-related configuration,
|
||||
// After initializing the configuration, It will try to obtain an account based on the private key,
|
||||
// if it fails, it will create an account based on the private key.
|
||||
// This account will be used during the running of the program
|
||||
func GetAcmeClient(email string, privateKey string, devMode bool) (*lego.Client, error) {
|
||||
// Create a user. New accounts need an email and private key to start.
|
||||
client, account, err := getLegoClientAndAccount(email, privateKey, devMode)
|
||||
|
||||
// try to obtain an account based on the private key
|
||||
account.Registration, err = client.Registration.ResolveAccountByKey()
|
||||
if err != nil {
|
||||
acmeError, ok := err.(*acme.ProblemDetails)
|
||||
if !ok {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if acmeError.Type != "urn:ietf:params:acme:error:accountDoesNotExist" {
|
||||
return nil, acmeError
|
||||
}
|
||||
|
||||
// Failed to get account, so create an account based on the private key.
|
||||
account.Registration, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
47
certificate/account_test.go
Normal file
47
certificate/account_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// 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.
|
||||
|
||||
//go:build !skipCi
|
||||
// +build !skipCi
|
||||
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
"github.com/casdoor/casdoor/proxy"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetClient(t *testing.T) {
|
||||
err := web.LoadAppConfig("ini", "../conf/app.conf")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
proxy.InitHttpClient()
|
||||
|
||||
eccKey := util.ReadStringFromPath("acme_account.key")
|
||||
println(eccKey)
|
||||
|
||||
client, err := GetAcmeClient("acme2@casbin.org", eccKey, false)
|
||||
assert.Nil(t, err)
|
||||
pem, key, err := ObtainCertificateAli(client, "casbin.com", accessKeyId, accessKeySecret)
|
||||
assert.Nil(t, err)
|
||||
println(pem)
|
||||
println()
|
||||
println(key)
|
||||
}
|
||||
20
certificate/conf.go
Normal file
20
certificate/conf.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// 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 certificate
|
||||
|
||||
var (
|
||||
accessKeyId = ""
|
||||
accessKeySecret = ""
|
||||
)
|
||||
151
certificate/dns.go
Normal file
151
certificate/dns.go
Normal file
@@ -0,0 +1,151 @@
|
||||
// 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 certificate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/casbin/lego/v4/certificate"
|
||||
"github.com/casbin/lego/v4/challenge/dns01"
|
||||
"github.com/casbin/lego/v4/cmd"
|
||||
"github.com/casbin/lego/v4/lego"
|
||||
"github.com/casbin/lego/v4/providers/dns/alidns"
|
||||
"github.com/casbin/lego/v4/providers/dns/godaddy"
|
||||
)
|
||||
|
||||
type AliConf struct {
|
||||
Domains []string // The domain names for which you want to apply for a certificate
|
||||
AccessKey string // Aliyun account's AccessKey, if this is not empty, Secret is required.
|
||||
Secret string
|
||||
RAMRole string // Use Ramrole to control aliyun account
|
||||
SecurityToken string // Optional
|
||||
Path string // The path to store cert file
|
||||
Timeout int // Maximum waiting time for certificate application, in minutes
|
||||
}
|
||||
|
||||
type GodaddyConf struct {
|
||||
Domains []string // The domain names for which you want to apply for a certificate
|
||||
APIKey string // GoDaddy account's API Key
|
||||
APISecret string
|
||||
Path string // The path to store cert file
|
||||
Timeout int // Maximum waiting time for certificate application, in minutes
|
||||
}
|
||||
|
||||
// getCert Verify domain ownership, then obtain a certificate, and finally store it locally.
|
||||
// Need to pass in an AliConf struct, some parameters are required, other parameters can be left blank
|
||||
func getAliCert(client *lego.Client, conf AliConf) (string, string, error) {
|
||||
if conf.Timeout <= 0 {
|
||||
conf.Timeout = 3
|
||||
}
|
||||
|
||||
config := alidns.NewDefaultConfig()
|
||||
config.PropagationTimeout = time.Duration(conf.Timeout) * time.Minute
|
||||
config.APIKey = conf.AccessKey
|
||||
config.SecretKey = conf.Secret
|
||||
config.RAMRole = conf.RAMRole
|
||||
config.SecurityToken = conf.SecurityToken
|
||||
|
||||
dnsProvider, err := alidns.NewDNSProvider(config)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// Choose a local DNS service provider to increase the authentication speed
|
||||
servers := []string{"223.5.5.5:53"}
|
||||
err = client.Challenge.SetDNS01Provider(dnsProvider, dns01.CondOption(len(servers) > 0, dns01.AddRecursiveNameservers(dns01.ParseNameservers(servers))), dns01.DisableCompletePropagationRequirement())
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// Obtain the certificate
|
||||
request := certificate.ObtainRequest{
|
||||
Domains: conf.Domains,
|
||||
Bundle: true,
|
||||
}
|
||||
cert, err := client.Certificate.Obtain(request)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return string(cert.Certificate), string(cert.PrivateKey), nil
|
||||
}
|
||||
|
||||
func getGoDaddyCert(client *lego.Client, conf GodaddyConf) (string, string, error) {
|
||||
if conf.Timeout <= 0 {
|
||||
conf.Timeout = 3
|
||||
}
|
||||
|
||||
config := godaddy.NewDefaultConfig()
|
||||
config.PropagationTimeout = time.Duration(conf.Timeout) * time.Minute
|
||||
config.PollingInterval = time.Duration(conf.Timeout) * time.Minute / 9
|
||||
config.APIKey = conf.APIKey
|
||||
config.APISecret = conf.APISecret
|
||||
|
||||
dnsProvider, err := godaddy.NewDNSProvider(config)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// Choose a local DNS service provider to increase the authentication speed
|
||||
servers := []string{"223.5.5.5:53"}
|
||||
err = client.Challenge.SetDNS01Provider(dnsProvider, dns01.CondOption(len(servers) > 0, dns01.AddRecursiveNameservers(dns01.ParseNameservers(servers))), dns01.DisableCompletePropagationRequirement())
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// Obtain the certificate
|
||||
request := certificate.ObtainRequest{
|
||||
Domains: conf.Domains,
|
||||
Bundle: true,
|
||||
}
|
||||
cert, err := client.Certificate.Obtain(request)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return string(cert.Certificate), string(cert.PrivateKey), nil
|
||||
}
|
||||
|
||||
func ObtainCertificateAli(client *lego.Client, domain string, accessKey string, accessSecret string) (string, string, error) {
|
||||
conf := AliConf{
|
||||
Domains: []string{fmt.Sprintf("*.%s", domain), domain},
|
||||
AccessKey: accessKey,
|
||||
Secret: accessSecret,
|
||||
RAMRole: "",
|
||||
SecurityToken: "",
|
||||
Path: "",
|
||||
Timeout: 3,
|
||||
}
|
||||
return getAliCert(client, conf)
|
||||
}
|
||||
|
||||
func ObtainCertificateGoDaddy(client *lego.Client, domain string, accessKey string, accessSecret string) (string, string, error) {
|
||||
conf := GodaddyConf{
|
||||
Domains: []string{fmt.Sprintf("*.%s", domain), domain},
|
||||
APIKey: accessKey,
|
||||
APISecret: accessSecret,
|
||||
Path: "",
|
||||
Timeout: 3,
|
||||
}
|
||||
return getGoDaddyCert(client, conf)
|
||||
}
|
||||
|
||||
func SaveCert(path, filename string, cert *certificate.Resource) {
|
||||
// Store the certificate file locally
|
||||
certsStorage := cmd.NewCertificatesStorageLib(path, filename, true)
|
||||
certsStorage.CreateRootFolder()
|
||||
certsStorage.SaveResource(cert)
|
||||
}
|
||||
55
certificate/ecc.go
Normal file
55
certificate/ecc.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// 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 certificate
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// generateEccKey generates a public and private key pair.(NIST P-256)
|
||||
func generateEccKey() (*ecdsa.PrivateKey, error) {
|
||||
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
}
|
||||
|
||||
// encodeEccKey Return the input private key object as string type private key
|
||||
func encodeEccKey(privateKey *ecdsa.PrivateKey) (string, error) {
|
||||
x509Encoded, err := x509.MarshalECPrivateKey(privateKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pemEncoded := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: x509Encoded})
|
||||
return string(pemEncoded), nil
|
||||
}
|
||||
|
||||
// decodeEccKey Return the entered private key string as a private key object that can be used
|
||||
func decodeEccKey(pemEncoded string) (*ecdsa.PrivateKey, error) {
|
||||
block, _ := pem.Decode([]byte(pemEncoded))
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("invalid PEM-encoded EC private key")
|
||||
}
|
||||
x509Encoded := block.Bytes
|
||||
privateKey, err := x509.ParseECPrivateKey(x509Encoded)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return privateKey, nil
|
||||
}
|
||||
34
certificate/ecc_test.go
Normal file
34
certificate/ecc_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// 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.
|
||||
|
||||
//go:build !skipCi
|
||||
// +build !skipCi
|
||||
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGenerateEccKey(t *testing.T) {
|
||||
eccKey, err := generateEccKey()
|
||||
assert.Nil(t, err)
|
||||
eccKeyStr, err := encodeEccKey(eccKey)
|
||||
assert.Nil(t, err)
|
||||
println(eccKeyStr)
|
||||
util.WriteStringToPath(eccKeyStr, "acme_account.key")
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
@@ -24,6 +25,9 @@ import (
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
//go:embed waf.conf
|
||||
var WafConf string
|
||||
|
||||
func init() {
|
||||
// this array contains the beego configuration items that may be modified via env
|
||||
presetConfigItems := []string{"httpport", "appname"}
|
||||
|
||||
246
conf/waf.conf
Normal file
246
conf/waf.conf
Normal file
@@ -0,0 +1,246 @@
|
||||
# -- Rule engine initialization ----------------------------------------------
|
||||
|
||||
# Enable Coraza, attaching it to every transaction. Use detection
|
||||
# only to start with, because that minimises the chances of post-installation
|
||||
# disruption.
|
||||
#
|
||||
SecRuleEngine DetectionOnly
|
||||
|
||||
|
||||
# -- Request body handling ---------------------------------------------------
|
||||
|
||||
# Allow Coraza to access request bodies. If you don't, Coraza
|
||||
# won't be able to see any POST parameters, which opens a large security
|
||||
# hole for attackers to exploit.
|
||||
#
|
||||
SecRequestBodyAccess On
|
||||
|
||||
# Enable XML request body parser.
|
||||
# Initiate XML Processor in case of xml content-type
|
||||
#
|
||||
SecRule REQUEST_HEADERS:Content-Type "^(?:application(?:/soap\+|/)|text/)xml" \
|
||||
"id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML"
|
||||
|
||||
# Enable JSON request body parser.
|
||||
# Initiate JSON Processor in case of JSON content-type; change accordingly
|
||||
# if your application does not use 'application/json'
|
||||
#
|
||||
SecRule REQUEST_HEADERS:Content-Type "^application/json" \
|
||||
"id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON"
|
||||
|
||||
# Sample rule to enable JSON request body parser for more subtypes.
|
||||
# Uncomment or adapt this rule if you want to engage the JSON
|
||||
# Processor for "+json" subtypes
|
||||
#
|
||||
#SecRule REQUEST_HEADERS:Content-Type "^application/[a-z0-9.-]+[+]json" \
|
||||
# "id:'200006',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON"
|
||||
|
||||
# Maximum request body size we will accept for buffering. If you support
|
||||
# file uploads then the value given on the first line has to be as large
|
||||
# as the largest file you are willing to accept. The second value refers
|
||||
# to the size of data, with files excluded. You want to keep that value as
|
||||
# low as practical.
|
||||
#
|
||||
SecRequestBodyLimit 13107200
|
||||
|
||||
SecRequestBodyInMemoryLimit 131072
|
||||
|
||||
# SecRequestBodyNoFilesLimit is currently not supported by Coraza
|
||||
# SecRequestBodyNoFilesLimit 131072
|
||||
|
||||
# What to do if the request body size is above our configured limit.
|
||||
# Keep in mind that this setting will automatically be set to ProcessPartial
|
||||
# when SecRuleEngine is set to DetectionOnly mode in order to minimize
|
||||
# disruptions when initially deploying Coraza.
|
||||
#
|
||||
SecRequestBodyLimitAction Reject
|
||||
|
||||
# Verify that we've correctly processed the request body.
|
||||
# As a rule of thumb, when failing to process a request body
|
||||
# you should reject the request (when deployed in blocking mode)
|
||||
# or log a high-severity alert (when deployed in detection-only mode).
|
||||
#
|
||||
SecRule REQBODY_ERROR "!@eq 0" \
|
||||
"id:'200002', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2"
|
||||
|
||||
# By default be strict with what we accept in the multipart/form-data
|
||||
# request body. If the rule below proves to be too strict for your
|
||||
# environment consider changing it to detection-only. You are encouraged
|
||||
# _not_ to remove it altogether.
|
||||
#
|
||||
SecRule MULTIPART_STRICT_ERROR "!@eq 0" \
|
||||
"id:'200003',phase:2,t:none,log,deny,status:400, \
|
||||
msg:'Multipart request body failed strict validation: \
|
||||
PE %{REQBODY_PROCESSOR_ERROR}, \
|
||||
BQ %{MULTIPART_BOUNDARY_QUOTED}, \
|
||||
BW %{MULTIPART_BOUNDARY_WHITESPACE}, \
|
||||
DB %{MULTIPART_DATA_BEFORE}, \
|
||||
DA %{MULTIPART_DATA_AFTER}, \
|
||||
HF %{MULTIPART_HEADER_FOLDING}, \
|
||||
LF %{MULTIPART_LF_LINE}, \
|
||||
SM %{MULTIPART_MISSING_SEMICOLON}, \
|
||||
IQ %{MULTIPART_INVALID_QUOTING}, \
|
||||
IP %{MULTIPART_INVALID_PART}, \
|
||||
IH %{MULTIPART_INVALID_HEADER_FOLDING}, \
|
||||
FL %{MULTIPART_FILE_LIMIT_EXCEEDED}'"
|
||||
|
||||
# Did we see anything that might be a boundary?
|
||||
#
|
||||
# Here is a short description about the Coraza Multipart parser: the
|
||||
# parser returns with value 0, if all "boundary-like" line matches with
|
||||
# the boundary string which given in MIME header. In any other cases it returns
|
||||
# with different value, eg. 1 or 2.
|
||||
#
|
||||
# The RFC 1341 descript the multipart content-type and its syntax must contains
|
||||
# only three mandatory lines (above the content):
|
||||
# * Content-Type: multipart/mixed; boundary=BOUNDARY_STRING
|
||||
# * --BOUNDARY_STRING
|
||||
# * --BOUNDARY_STRING--
|
||||
#
|
||||
# First line indicates, that this is a multipart content, second shows that
|
||||
# here starts a part of the multipart content, third shows the end of content.
|
||||
#
|
||||
# If there are any other lines, which starts with "--", then it should be
|
||||
# another boundary id - or not.
|
||||
#
|
||||
# After 3.0.3, there are two kinds of types of boundary errors: strict and permissive.
|
||||
#
|
||||
# If multipart content contains the three necessary lines with correct order, but
|
||||
# there are one or more lines with "--", then parser returns with value 2 (non-zero).
|
||||
#
|
||||
# If some of the necessary lines (usually the start or end) misses, or the order
|
||||
# is wrong, then parser returns with value 1 (also a non-zero).
|
||||
#
|
||||
# You can choose, which one is what you need. The example below contains the
|
||||
# 'strict' mode, which means if there are any lines with start of "--", then
|
||||
# Coraza blocked the content. But the next, commented example contains
|
||||
# the 'permissive' mode, then you check only if the necessary lines exists in
|
||||
# correct order. Whit this, you can enable to upload PEM files (eg "----BEGIN.."),
|
||||
# or other text files, which contains eg. HTTP headers.
|
||||
#
|
||||
# The difference is only the operator - in strict mode (first) the content blocked
|
||||
# in case of any non-zero value. In permissive mode (second, commented) the
|
||||
# content blocked only if the value is explicit 1. If it 0 or 2, the content will
|
||||
# allowed.
|
||||
#
|
||||
|
||||
#
|
||||
# See #1747 and #1924 for further information on the possible values for
|
||||
# MULTIPART_UNMATCHED_BOUNDARY.
|
||||
#
|
||||
SecRule MULTIPART_UNMATCHED_BOUNDARY "@eq 1" \
|
||||
"id:'200004',phase:2,t:none,log,deny,msg:'Multipart parser detected a possible unmatched boundary.'"
|
||||
|
||||
# Some internal errors will set flags in TX and we will need to look for these.
|
||||
# All of these are prefixed with "MSC_". The following flags currently exist:
|
||||
#
|
||||
# COR_PCRE_LIMITS_EXCEEDED: PCRE match limits were exceeded.
|
||||
#
|
||||
SecRule TX:/^COR_/ "!@streq 0" \
|
||||
"id:'200005',phase:2,t:none,deny,msg:'Coraza internal error flagged: %{MATCHED_VAR_NAME}'"
|
||||
|
||||
|
||||
# -- Response body handling --------------------------------------------------
|
||||
|
||||
# Allow Coraza to access response bodies.
|
||||
# You should have this directive enabled in order to identify errors
|
||||
# and data leakage issues.
|
||||
#
|
||||
# Do keep in mind that enabling this directive does increases both
|
||||
# memory consumption and response latency.
|
||||
#
|
||||
SecResponseBodyAccess On
|
||||
|
||||
# Which response MIME types do you want to inspect? You should adjust the
|
||||
# configuration below to catch documents but avoid static files
|
||||
# (e.g., images and archives).
|
||||
#
|
||||
SecResponseBodyMimeType text/plain text/html text/xml
|
||||
|
||||
# Buffer response bodies of up to 512 KB in length.
|
||||
SecResponseBodyLimit 524288
|
||||
|
||||
# What happens when we encounter a response body larger than the configured
|
||||
# limit? By default, we process what we have and let the rest through.
|
||||
# That's somewhat less secure, but does not break any legitimate pages.
|
||||
#
|
||||
SecResponseBodyLimitAction ProcessPartial
|
||||
|
||||
|
||||
# -- Filesystem configuration ------------------------------------------------
|
||||
|
||||
# The location where Coraza will keep its persistent data. This default setting
|
||||
# is chosen due to all systems have /tmp available however, it
|
||||
# too should be updated to a place that other users can't access.
|
||||
#
|
||||
SecDataDir /tmp/
|
||||
|
||||
|
||||
# -- File uploads handling configuration -------------------------------------
|
||||
|
||||
# The location where Coraza stores intercepted uploaded files. This
|
||||
# location must be private to Coraza. You don't want other users on
|
||||
# the server to access the files, do you?
|
||||
#
|
||||
#SecUploadDir /opt/coraza/var/upload/
|
||||
|
||||
# By default, only keep the files that were determined to be unusual
|
||||
# in some way (by an external inspection script). For this to work you
|
||||
# will also need at least one file inspection rule.
|
||||
#
|
||||
#SecUploadKeepFiles RelevantOnly
|
||||
|
||||
# Uploaded files are by default created with permissions that do not allow
|
||||
# any other user to access them. You may need to relax that if you want to
|
||||
# interface Coraza to an external program (e.g., an anti-virus).
|
||||
#
|
||||
#SecUploadFileMode 0600
|
||||
|
||||
|
||||
# -- Debug log configuration -------------------------------------------------
|
||||
|
||||
# Default debug log path
|
||||
# Debug levels:
|
||||
# 0: No logging (least verbose)
|
||||
# 1: Error
|
||||
# 2: Warn
|
||||
# 3: Info
|
||||
# 4-8: Debug
|
||||
# 9: Trace (most verbose)
|
||||
# Most logging has not been implemented because it will be replaced with
|
||||
# advanced rule profiling options
|
||||
#SecDebugLog /opt/coraza/var/log/debug.log
|
||||
#SecDebugLogLevel 3
|
||||
|
||||
|
||||
# -- Audit log configuration -------------------------------------------------
|
||||
|
||||
# Log the transactions that are marked by a rule, as well as those that
|
||||
# trigger a server error (determined by a 5xx or 4xx, excluding 404,
|
||||
# level response status codes).
|
||||
#
|
||||
SecAuditEngine RelevantOnly
|
||||
SecAuditLogRelevantStatus "^(?:(5|4)(0|1)[0-9])$"
|
||||
|
||||
# Log everything we know about a transaction.
|
||||
SecAuditLogParts ABIJDEFHZ
|
||||
|
||||
# Use a single file for logging. This is much easier to look at, but
|
||||
# assumes that you will use the audit log only occasionally.
|
||||
#
|
||||
SecAuditLogType Serial
|
||||
|
||||
|
||||
# -- Miscellaneous -----------------------------------------------------------
|
||||
|
||||
# Use the most commonly used application/x-www-form-urlencoded parameter
|
||||
# separator. There's probably only one application somewhere that uses
|
||||
# something else so don't expect to change this value.
|
||||
#
|
||||
SecArgumentSeparator &
|
||||
|
||||
# Settle on version 0 (zero) cookies, as that is what most applications
|
||||
# use. Using an incorrect cookie version may open your installation to
|
||||
# evasion attacks (against the rules that examine named cookies).
|
||||
#
|
||||
SecCookieFormat 0
|
||||
@@ -323,6 +323,17 @@ func (c *ApiController) Signup() {
|
||||
|
||||
// If OAuth parameters are present, generate OAuth code and return it
|
||||
if clientId != "" && responseType == ResponseTypeCode {
|
||||
consentRequired, err := object.CheckConsentRequired(user, application, scope)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if consentRequired {
|
||||
c.ResponseOk(map[string]bool{"required": true})
|
||||
return
|
||||
}
|
||||
|
||||
code, err := object.GetOAuthCode(userId, clientId, "", "password", responseType, redirectUri, scope, state, nonce, codeChallenge, "", c.Ctx.Request.Host, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
@@ -364,18 +375,11 @@ func (c *ApiController) Logout() {
|
||||
|
||||
c.ClearUserSession()
|
||||
c.ClearTokenSession()
|
||||
owner, username, err := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
_, err = object.DeleteSessionId(util.GetSessionId(owner, username, object.CasdoorApplication), c.Ctx.Input.CruSession.SessionID(context.Background()))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
util.LogInfo(c.Ctx, "API: [%s] logged out", user)
|
||||
if err := c.deleteUserSession(user); err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
application := c.GetSessionApplication()
|
||||
if application == nil || application.Name == "app-built-in" || application.HomepageUrl == "" {
|
||||
@@ -415,21 +419,13 @@ func (c *ApiController) Logout() {
|
||||
|
||||
c.ClearUserSession()
|
||||
c.ClearTokenSession()
|
||||
|
||||
// TODO https://github.com/casdoor/casdoor/pull/1494#discussion_r1095675265
|
||||
owner, username, err := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if err != nil {
|
||||
if err := c.deleteUserSession(user); err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
_, err = object.DeleteSessionId(util.GetSessionId(owner, username, object.CasdoorApplication), c.Ctx.Input.CruSession.SessionID(context.Background()))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
util.LogInfo(c.Ctx, "API: [%s] logged out", user)
|
||||
|
||||
if redirectUri == "" {
|
||||
c.ResponseOk()
|
||||
return
|
||||
@@ -766,3 +762,24 @@ func (c *ApiController) GetCaptcha() {
|
||||
|
||||
c.ResponseOk(Captcha{Type: "none"})
|
||||
}
|
||||
|
||||
func (c *ApiController) deleteUserSession(user string) error {
|
||||
owner, username, err := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Casdoor session ID derived from owner, username, and application
|
||||
sessionId := util.GetSessionId(owner, username, object.CasdoorApplication)
|
||||
|
||||
// Explicitly get the Beego session ID from the context
|
||||
beegoSessionId := c.Ctx.Input.CruSession.SessionID(context.Background())
|
||||
|
||||
_, err = object.DeleteSessionId(sessionId, beegoSessionId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
util.LogInfo(c.Ctx, "API: [%s] logged out", user)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -167,6 +167,19 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
c.ResponseError(c.T("auth:Challenge method should be S256"))
|
||||
return
|
||||
}
|
||||
|
||||
consentRequired, err := object.CheckConsentRequired(user, application, scope)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if consentRequired {
|
||||
resp = &Response{Status: "ok", Data: map[string]bool{"required": true}}
|
||||
resp.Data3 = user.NeedUpdatePassword
|
||||
return
|
||||
}
|
||||
|
||||
code, err := object.GetOAuthCode(userId, clientId, form.Provider, form.SigninMethod, responseType, redirectUri, scope, state, nonce, codeChallenge, resource, c.Ctx.Request.Host, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
@@ -185,10 +198,14 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
} else {
|
||||
scope := c.Ctx.Input.Query("scope")
|
||||
nonce := c.Ctx.Input.Query("nonce")
|
||||
token, _ := object.GetTokenByUser(application, user, scope, nonce, c.Ctx.Request.Host)
|
||||
resp = tokenToResponse(token)
|
||||
if !object.IsScopeValid(scope, application) {
|
||||
resp = &Response{Status: "error", Msg: "error: invalid_scope", Data: ""}
|
||||
} else {
|
||||
token, _ := object.GetTokenByUser(application, user, scope, nonce, c.Ctx.Request.Host)
|
||||
resp = tokenToResponse(token)
|
||||
|
||||
resp.Data3 = user.NeedUpdatePassword
|
||||
resp.Data3 = user.NeedUpdatePassword
|
||||
}
|
||||
}
|
||||
} else if form.Type == ResponseTypeDevice {
|
||||
authCache, ok := object.DeviceAuthMap.LoadAndDelete(form.UserCode)
|
||||
@@ -739,7 +756,11 @@ func (c *ApiController) Login() {
|
||||
}
|
||||
} else if provider.Category == "OAuth" || provider.Category == "Web3" {
|
||||
// OAuth
|
||||
idpInfo := object.FromProviderToIdpInfo(c.Ctx, provider)
|
||||
idpInfo, err := object.FromProviderToIdpInfo(c.Ctx, provider)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
idpInfo.CodeVerifier = authForm.CodeVerifier
|
||||
var idProvider idp.IdProvider
|
||||
idProvider, err = idp.GetIdProvider(idpInfo, authForm.RedirectUri)
|
||||
@@ -950,11 +971,13 @@ func (c *ApiController) Login() {
|
||||
RegisterSource: fmt.Sprintf("%s/%s", application.Organization, application.Name),
|
||||
}
|
||||
|
||||
// Set group from invitation code if available, otherwise use provider's signup group
|
||||
// Set group from invitation code if available, otherwise use provider's signup group or application's default group
|
||||
if invitation != nil && invitation.SignupGroup != "" {
|
||||
user.Groups = []string{invitation.SignupGroup}
|
||||
} else if providerItem.SignupGroup != "" {
|
||||
user.Groups = []string{providerItem.SignupGroup}
|
||||
} else if application.DefaultGroup != "" {
|
||||
user.Groups = []string{application.DefaultGroup}
|
||||
}
|
||||
|
||||
var affected bool
|
||||
|
||||
@@ -183,3 +183,40 @@ func (c *ApiController) DeleteCert() {
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteCert(&cert))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// UpdateCertDomainExpire
|
||||
// @Title UpdateCertDomainExpire
|
||||
// @Tag Cert API
|
||||
// @Description update cert domain expire time
|
||||
// @Param id query string true "The ID of the cert"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-cert-domain-expire [post]
|
||||
func (c *ApiController) UpdateCertDomainExpire() {
|
||||
if _, ok := c.RequireSignedIn(); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
id := c.Ctx.Input.Query("id")
|
||||
cert, err := object.GetCert(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
domainExpireTime, err := object.GetDomainExpireTime(cert.Name)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if domainExpireTime == "" {
|
||||
c.ResponseError("Failed to determine domain expiration time for domain " + cert.Name +
|
||||
". Please verify that the domain is valid, publicly resolvable, and has a retrievable expiration date, " +
|
||||
"or update the domain expiration time manually.")
|
||||
return
|
||||
}
|
||||
cert.DomainExpireTime = domainExpireTime
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateCert(id, cert))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
226
controllers/consent.go
Normal file
226
controllers/consent.go
Normal file
@@ -0,0 +1,226 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
)
|
||||
|
||||
// RevokeConsent revokes a consent record
|
||||
// @Title RevokeConsent
|
||||
// @Tag Consent API
|
||||
// @Description revoke a consent record
|
||||
// @Param body body object.ConsentRecord true "The consent object"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /revoke-consent [post]
|
||||
func (c *ApiController) RevokeConsent() {
|
||||
userId := c.GetSessionUsername()
|
||||
if userId == "" {
|
||||
c.ResponseError(c.T("general:Please login first"))
|
||||
return
|
||||
}
|
||||
|
||||
var consent object.ConsentRecord
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &consent)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Validate that consent.Application is not empty
|
||||
if consent.Application == "" {
|
||||
c.ResponseError(c.T("general:Application cannot be empty"))
|
||||
return
|
||||
}
|
||||
|
||||
// Validate that GrantedScopes is not empty when scope-specific revoke is requested
|
||||
if len(consent.GrantedScopes) == 0 {
|
||||
c.ResponseError(c.T("general:Granted scopes cannot be empty"))
|
||||
return
|
||||
}
|
||||
|
||||
userObj, err := object.GetUser(userId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if userObj == nil {
|
||||
c.ResponseError(c.T("general:The user doesn't exist"))
|
||||
return
|
||||
}
|
||||
|
||||
newScopes := []object.ConsentRecord{}
|
||||
for _, record := range userObj.ApplicationScopes {
|
||||
if record.Application != consent.Application {
|
||||
// skip other applications
|
||||
newScopes = append(newScopes, record)
|
||||
continue
|
||||
}
|
||||
// revoke specified scopes
|
||||
revokeSet := make(map[string]bool)
|
||||
for _, s := range consent.GrantedScopes {
|
||||
revokeSet[s] = true
|
||||
}
|
||||
remaining := []string{}
|
||||
for _, s := range record.GrantedScopes {
|
||||
if !revokeSet[s] {
|
||||
remaining = append(remaining, s)
|
||||
}
|
||||
}
|
||||
if len(remaining) > 0 {
|
||||
// still have remaining scopes, keep the record and update
|
||||
record.GrantedScopes = remaining
|
||||
newScopes = append(newScopes, record)
|
||||
}
|
||||
// otherwise the application authorization is revoked, delete the whole record
|
||||
}
|
||||
userObj.ApplicationScopes = newScopes
|
||||
success, err := object.UpdateUser(userObj.GetId(), userObj, nil, false)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.ResponseOk(success)
|
||||
}
|
||||
|
||||
// GrantConsent grants consent for an OAuth application and returns authorization code
|
||||
// @Title GrantConsent
|
||||
// @Tag Consent API
|
||||
// @Description grant consent for an OAuth application and get authorization code
|
||||
// @Param body body object.ConsentRecord true "The consent object with OAuth parameters"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /grant-consent [post]
|
||||
func (c *ApiController) GrantConsent() {
|
||||
userId := c.GetSessionUsername()
|
||||
if userId == "" {
|
||||
c.ResponseError(c.T("general:Please login first"))
|
||||
return
|
||||
}
|
||||
|
||||
var request struct {
|
||||
Application string `json:"application"`
|
||||
Scopes []string `json:"grantedScopes"`
|
||||
ClientId string `json:"clientId"`
|
||||
Provider string `json:"provider"`
|
||||
SigninMethod string `json:"signinMethod"`
|
||||
ResponseType string `json:"responseType"`
|
||||
RedirectUri string `json:"redirectUri"`
|
||||
Scope string `json:"scope"`
|
||||
State string `json:"state"`
|
||||
Nonce string `json:"nonce"`
|
||||
Challenge string `json:"challenge"`
|
||||
Resource string `json:"resource"`
|
||||
}
|
||||
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &request)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Validate application by clientId
|
||||
application, err := object.GetApplicationByClientId(request.ClientId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if application == nil {
|
||||
c.ResponseError(c.T("general:Invalid client_id"))
|
||||
return
|
||||
}
|
||||
|
||||
// Verify that request.Application matches the application's actual ID
|
||||
if request.Application != application.GetId() {
|
||||
c.ResponseError(c.T("general:Invalid application"))
|
||||
return
|
||||
}
|
||||
|
||||
// Update user's ApplicationScopes
|
||||
userObj, err := object.GetUser(userId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if userObj == nil {
|
||||
c.ResponseError(c.T("general:User not found"))
|
||||
return
|
||||
}
|
||||
|
||||
appId := application.GetId()
|
||||
found := false
|
||||
// Insert new scope into existing applicationScopes
|
||||
for i, record := range userObj.ApplicationScopes {
|
||||
if record.Application == appId {
|
||||
existing := make(map[string]bool)
|
||||
for _, s := range userObj.ApplicationScopes[i].GrantedScopes {
|
||||
existing[s] = true
|
||||
}
|
||||
for _, s := range request.Scopes {
|
||||
if !existing[s] {
|
||||
userObj.ApplicationScopes[i].GrantedScopes = append(userObj.ApplicationScopes[i].GrantedScopes, s)
|
||||
existing[s] = true
|
||||
}
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// create a new applicationScopes if not found
|
||||
if !found {
|
||||
uniqueScopes := []string{}
|
||||
existing := make(map[string]bool)
|
||||
for _, s := range request.Scopes {
|
||||
if !existing[s] {
|
||||
uniqueScopes = append(uniqueScopes, s)
|
||||
existing[s] = true
|
||||
}
|
||||
}
|
||||
userObj.ApplicationScopes = append(userObj.ApplicationScopes, object.ConsentRecord{
|
||||
Application: appId,
|
||||
GrantedScopes: uniqueScopes,
|
||||
})
|
||||
}
|
||||
|
||||
_, err = object.UpdateUser(userObj.GetId(), userObj, []string{"application_scopes"}, false)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Now get the OAuth code
|
||||
code, err := object.GetOAuthCode(
|
||||
userId,
|
||||
request.ClientId,
|
||||
request.Provider,
|
||||
request.SigninMethod,
|
||||
request.ResponseType,
|
||||
request.RedirectUri,
|
||||
request.Scope,
|
||||
request.State,
|
||||
request.Nonce,
|
||||
request.Challenge,
|
||||
request.Resource,
|
||||
c.Ctx.Request.Host,
|
||||
c.GetAcceptLanguage(),
|
||||
)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(code.Code)
|
||||
}
|
||||
193
controllers/rule.go
Normal file
193
controllers/rule.go
Normal file
@@ -0,0 +1,193 @@
|
||||
// Copyright 2023 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"
|
||||
"errors"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego/v2/server/web/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/hsluoyz/modsecurity-go/seclang/parser"
|
||||
)
|
||||
|
||||
func (c *ApiController) GetRules() {
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
if owner == "admin" {
|
||||
owner = ""
|
||||
}
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
rules, err := object.GetRules(owner)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(rules)
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
count, err := object.GetRuleCount(owner, field, value)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
rules, err := object.GetPaginationRules(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(rules, paginator.Nums())
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ApiController) GetRule() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
rule, err := object.GetRule(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(rule)
|
||||
}
|
||||
|
||||
func (c *ApiController) AddRule() {
|
||||
currentTime := util.GetCurrentTime()
|
||||
rule := object.Rule{
|
||||
CreatedTime: currentTime,
|
||||
UpdatedTime: currentTime,
|
||||
}
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &rule)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
err = checkExpressions(rule.Expressions, rule.Type)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = wrapActionResponse(object.AddRule(&rule))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *ApiController) UpdateRule() {
|
||||
var rule object.Rule
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &rule)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = checkExpressions(rule.Expressions, rule.Type)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
id := c.Ctx.Input.Query("id")
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateRule(id, &rule))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *ApiController) DeleteRule() {
|
||||
var rule object.Rule
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &rule)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteRule(&rule))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
func checkExpressions(expressions []*object.Expression, ruleType string) error {
|
||||
values := make([]string, len(expressions))
|
||||
for i, expression := range expressions {
|
||||
values[i] = expression.Value
|
||||
}
|
||||
switch ruleType {
|
||||
case "WAF":
|
||||
return checkWafRule(values)
|
||||
case "IP":
|
||||
return checkIpRule(values)
|
||||
case "IP Rate Limiting":
|
||||
return checkIpRateRule(expressions)
|
||||
case "Compound":
|
||||
return checkCompoundRules(values)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkWafRule(rules []string) error {
|
||||
for _, rule := range rules {
|
||||
scanner := parser.NewSecLangScannerFromString(rule)
|
||||
_, err := scanner.AllDirective()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkIpRule(ipLists []string) error {
|
||||
for _, ipList := range ipLists {
|
||||
for _, ip := range strings.Split(ipList, ",") {
|
||||
_, _, err := net.ParseCIDR(ip)
|
||||
if net.ParseIP(ip) == nil && err != nil {
|
||||
return errors.New("Invalid IP address: " + ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkIpRateRule(expressions []*object.Expression) error {
|
||||
if len(expressions) != 1 {
|
||||
return errors.New("IP Rate Limiting rule must have exactly one expression")
|
||||
}
|
||||
expression := expressions[0]
|
||||
_, err := util.ParseIntWithError(expression.Operator)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = util.ParseIntWithError(expression.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkCompoundRules(rules []string) error {
|
||||
_, err := object.GetRulesByRuleIds(rules)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
123
controllers/site.go
Normal file
123
controllers/site.go
Normal file
@@ -0,0 +1,123 @@
|
||||
// Copyright 2023 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/beego/beego/v2/server/web/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
func (c *ApiController) GetGlobalSites() {
|
||||
sites, err := object.GetGlobalSites()
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(object.GetMaskedSites(sites, util.GetHostname()))
|
||||
}
|
||||
|
||||
func (c *ApiController) GetSites() {
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
if owner == "admin" {
|
||||
owner = ""
|
||||
}
|
||||
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
sites, err := object.GetSites(owner)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.ResponseOk(object.GetMaskedSites(sites, util.GetHostname()))
|
||||
return
|
||||
}
|
||||
|
||||
limitInt := util.ParseInt(limit)
|
||||
count, err := object.GetSiteCount(owner, field, value)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limitInt, count)
|
||||
sites, err := object.GetPaginationSites(owner, paginator.Offset(), limitInt, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(object.GetMaskedSites(sites, util.GetHostname()), paginator.Nums())
|
||||
}
|
||||
|
||||
func (c *ApiController) GetSite() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
site, err := object.GetSite(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(object.GetMaskedSite(site, util.GetHostname()))
|
||||
}
|
||||
|
||||
func (c *ApiController) UpdateSite() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var site object.Site
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &site)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateSite(id, &site))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *ApiController) AddSite() {
|
||||
var site object.Site
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &site)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddSite(&site))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *ApiController) DeleteSite() {
|
||||
var site object.Site
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &site)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteSite(&site))
|
||||
c.ServeJSON()
|
||||
}
|
||||
@@ -162,6 +162,9 @@ func (c *ApiController) DeleteToken() {
|
||||
func (c *ApiController) GetOAuthToken() {
|
||||
clientId := c.Ctx.Input.Query("client_id")
|
||||
clientSecret := c.Ctx.Input.Query("client_secret")
|
||||
assertion := c.Ctx.Input.Query("assertion")
|
||||
clientAssertion := c.Ctx.Input.Query("client_assertion")
|
||||
clientAssertionType := c.Ctx.Input.Query("client_assertion_type")
|
||||
grantType := c.Ctx.Input.Query("grant_type")
|
||||
code := c.Ctx.Input.Query("code")
|
||||
verifier := c.Ctx.Input.Query("code_verifier")
|
||||
@@ -193,6 +196,12 @@ func (c *ApiController) GetOAuthToken() {
|
||||
if clientSecret == "" {
|
||||
clientSecret = tokenRequest.ClientSecret
|
||||
}
|
||||
if clientAssertion == "" {
|
||||
clientAssertion = tokenRequest.ClientAssertion
|
||||
}
|
||||
if clientAssertionType == "" {
|
||||
clientAssertionType = tokenRequest.ClientAssertionType
|
||||
}
|
||||
if grantType == "" {
|
||||
grantType = tokenRequest.GrantType
|
||||
}
|
||||
@@ -235,9 +244,13 @@ func (c *ApiController) GetOAuthToken() {
|
||||
if resource == "" {
|
||||
resource = tokenRequest.Resource
|
||||
}
|
||||
if assertion == "" {
|
||||
assertion = tokenRequest.Assertion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
host := c.Ctx.Request.Host
|
||||
if deviceCode != "" {
|
||||
deviceAuthCache, ok := object.DeviceAuthMap.Load(deviceCode)
|
||||
if !ok {
|
||||
@@ -278,8 +291,7 @@ func (c *ApiController) GetOAuthToken() {
|
||||
username = deviceAuthCacheCast.UserName
|
||||
}
|
||||
|
||||
host := c.Ctx.Request.Host
|
||||
token, err := object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, nonce, username, password, host, refreshToken, tag, avatar, c.GetAcceptLanguage(), subjectToken, subjectTokenType, audience, resource)
|
||||
token, err := object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, nonce, username, password, host, refreshToken, tag, avatar, c.GetAcceptLanguage(), subjectToken, subjectTokenType, assertion, clientAssertion, clientAssertionType, audience, resource)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -323,7 +335,12 @@ func (c *ApiController) RefreshToken() {
|
||||
}
|
||||
}
|
||||
|
||||
refreshToken2, err := object.RefreshToken(grantType, refreshToken, scope, clientId, clientSecret, host)
|
||||
ok, application, clientId, _, err := c.ValidateOAuth(true)
|
||||
if err != nil || !ok {
|
||||
return
|
||||
}
|
||||
|
||||
refreshToken2, err := object.RefreshToken(application, grantType, refreshToken, scope, clientId, clientSecret, host)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -334,14 +351,79 @@ func (c *ApiController) RefreshToken() {
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *ApiController) ResponseTokenError(errorMsg string) {
|
||||
func (c *ApiController) ResponseTokenError(errorMsg string, errorDescription string) {
|
||||
c.Data["json"] = &object.TokenError{
|
||||
Error: errorMsg,
|
||||
Error: errorMsg,
|
||||
ErrorDescription: errorDescription,
|
||||
}
|
||||
c.SetTokenErrorHttpStatus()
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *ApiController) ValidateOAuth(ignoreValidSecret bool) (ok bool, application *object.Application, clientId, clientSecret string, err error) {
|
||||
reqClientId := c.Ctx.Input.Query("client_id")
|
||||
reqClientSecret := c.Ctx.Input.Query("client_secret")
|
||||
clientAssertion := c.Ctx.Input.Query("client_assertion")
|
||||
clientAssertionType := c.Ctx.Input.Query("client_assertion_type")
|
||||
|
||||
if reqClientId == "" && clientAssertionType == "" {
|
||||
var tokenRequest TokenRequest
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &tokenRequest); err == nil {
|
||||
reqClientId = tokenRequest.ClientId
|
||||
reqClientSecret = tokenRequest.ClientSecret
|
||||
clientAssertion = tokenRequest.ClientAssertion
|
||||
clientAssertionType = tokenRequest.ClientAssertionType
|
||||
}
|
||||
}
|
||||
|
||||
if clientAssertionType == "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" {
|
||||
ok, application, err = object.ValidateClientAssertion(clientAssertion, c.Ctx.Request.Host)
|
||||
if err != nil {
|
||||
c.ResponseTokenError(object.InvalidClient, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !ok || application == nil {
|
||||
c.ResponseTokenError(object.InvalidClient, "client_assertion is invalid")
|
||||
return
|
||||
}
|
||||
|
||||
clientSecret = application.ClientSecret
|
||||
clientId = application.ClientId
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
if reqClientId == "" && reqClientSecret == "" {
|
||||
clientId, clientSecret, ok = c.Ctx.Request.BasicAuth()
|
||||
if !ok {
|
||||
clientId = c.Ctx.Input.Query("client_id")
|
||||
clientSecret = c.Ctx.Input.Query("client_secret")
|
||||
if clientId == "" || clientSecret == "" {
|
||||
c.ResponseTokenError(object.InvalidRequest, "")
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
clientId = reqClientId
|
||||
clientSecret = reqClientSecret
|
||||
}
|
||||
|
||||
application, err = object.GetApplicationByClientId(clientId)
|
||||
if err != nil {
|
||||
c.ResponseTokenError(object.InvalidClient, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if application == nil || (application.ClientSecret != clientSecret && !ignoreValidSecret) {
|
||||
c.ResponseTokenError(object.InvalidClient, c.T("token:Invalid application or wrong clientSecret"))
|
||||
return
|
||||
}
|
||||
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
// IntrospectToken
|
||||
// @Title IntrospectToken
|
||||
// @Tag Login API
|
||||
@@ -349,7 +431,7 @@ func (c *ApiController) ResponseTokenError(errorMsg string) {
|
||||
// parameter representing an OAuth 2.0 token and returns a JSON document
|
||||
// representing the meta information surrounding the
|
||||
// token, including whether this token is currently active.
|
||||
// This endpoint only support Basic Authorization.
|
||||
// This endpoint support Basic Authorization and authorization defined in RFC 7523.
|
||||
//
|
||||
// @Param token formData string true "access_token's value or refresh_token's value"
|
||||
// @Param token_type_hint formData string true "the token type access_token or refresh_token"
|
||||
@@ -359,24 +441,9 @@ func (c *ApiController) ResponseTokenError(errorMsg string) {
|
||||
// @router /login/oauth/introspect [post]
|
||||
func (c *ApiController) IntrospectToken() {
|
||||
tokenValue := c.Ctx.Input.Query("token")
|
||||
clientId, clientSecret, ok := c.Ctx.Request.BasicAuth()
|
||||
if !ok {
|
||||
clientId = c.Ctx.Input.Query("client_id")
|
||||
clientSecret = c.Ctx.Input.Query("client_secret")
|
||||
if clientId == "" || clientSecret == "" {
|
||||
c.ResponseTokenError(object.InvalidRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
application, err := object.GetApplicationByClientId(clientId)
|
||||
if err != nil {
|
||||
c.ResponseTokenError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if application == nil || application.ClientSecret != clientSecret {
|
||||
c.ResponseTokenError(c.T("token:Invalid application or wrong clientSecret"))
|
||||
ok, application, clientId, _, err := c.ValidateOAuth(false)
|
||||
if err != nil || !ok {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -390,7 +457,7 @@ func (c *ApiController) IntrospectToken() {
|
||||
if tokenTypeHint != "" {
|
||||
token, err = object.GetTokenByTokenValue(tokenValue, tokenTypeHint)
|
||||
if err != nil {
|
||||
c.ResponseTokenError(err.Error())
|
||||
c.ResponseTokenError(object.InvalidRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if token == nil || token.ExpiresIn <= 0 {
|
||||
@@ -467,7 +534,7 @@ func (c *ApiController) IntrospectToken() {
|
||||
if tokenTypeHint == "" {
|
||||
token, err = object.GetTokenByTokenValue(tokenValue, introspectionResponse.TokenType)
|
||||
if err != nil {
|
||||
c.ResponseTokenError(err.Error())
|
||||
c.ResponseTokenError(object.InvalidRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if token == nil || token.ExpiresIn <= 0 {
|
||||
@@ -479,7 +546,7 @@ func (c *ApiController) IntrospectToken() {
|
||||
if token != nil {
|
||||
application, err = object.GetApplication(fmt.Sprintf("%s/%s", token.Owner, token.Application))
|
||||
if err != nil {
|
||||
c.ResponseTokenError(err.Error())
|
||||
c.ResponseTokenError(object.InvalidClient, err.Error())
|
||||
return
|
||||
}
|
||||
if application == nil {
|
||||
|
||||
@@ -15,20 +15,23 @@
|
||||
package controllers
|
||||
|
||||
type TokenRequest struct {
|
||||
ClientId string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
GrantType string `json:"grant_type"`
|
||||
Code string `json:"code"`
|
||||
Verifier string `json:"code_verifier"`
|
||||
Scope string `json:"scope"`
|
||||
Nonce string `json:"nonce"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Tag string `json:"tag"`
|
||||
Avatar string `json:"avatar"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
SubjectToken string `json:"subject_token"`
|
||||
SubjectTokenType string `json:"subject_token_type"`
|
||||
Audience string `json:"audience"`
|
||||
Resource string `json:"resource"` // RFC 8707 Resource Indicator
|
||||
Assertion string `json:"assertion"`
|
||||
ClientId string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
ClientAssertion string `json:"client_assertion"`
|
||||
ClientAssertionType string `json:"client_assertion_type"`
|
||||
GrantType string `json:"grant_type"`
|
||||
Code string `json:"code"`
|
||||
Verifier string `json:"code_verifier"`
|
||||
Scope string `json:"scope"`
|
||||
Nonce string `json:"nonce"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Tag string `json:"tag"`
|
||||
Avatar string `json:"avatar"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
SubjectToken string `json:"subject_token"`
|
||||
SubjectTokenType string `json:"subject_token_type"`
|
||||
Audience string `json:"audience"`
|
||||
Resource string `json:"resource"` // RFC 8707 Resource Indicator
|
||||
}
|
||||
|
||||
@@ -19,13 +19,16 @@ type EmailProvider interface {
|
||||
}
|
||||
|
||||
func GetEmailProvider(typ string, clientId string, clientSecret string, host string, port int, sslMode string, endpoint string, method string, httpHeaders map[string]string, bodyMapping map[string]string, contentType string, enableProxy bool) EmailProvider {
|
||||
if typ == "Azure ACS" {
|
||||
switch typ {
|
||||
case "Azure ACS":
|
||||
return NewAzureACSEmailProvider(clientSecret, host)
|
||||
} else if typ == "Custom HTTP Email" {
|
||||
case "Custom HTTP Email":
|
||||
return NewHttpEmailProvider(endpoint, method, httpHeaders, bodyMapping, contentType)
|
||||
} else if typ == "SendGrid" {
|
||||
case "SendGrid":
|
||||
return NewSendgridEmailProvider(clientSecret, host, endpoint)
|
||||
} else {
|
||||
case "Resend":
|
||||
return NewResendEmailProvider(clientSecret)
|
||||
default:
|
||||
return NewSmtpEmailProvider(clientId, clientSecret, host, port, typ, sslMode, enableProxy)
|
||||
}
|
||||
}
|
||||
|
||||
48
email/resend.go
Normal file
48
email/resend.go
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/resend/resend-go/v3"
|
||||
)
|
||||
|
||||
type ResendEmailProvider struct {
|
||||
Client *resend.Client
|
||||
}
|
||||
|
||||
func NewResendEmailProvider(apiKey string) *ResendEmailProvider {
|
||||
client := resend.NewClient(apiKey)
|
||||
client.UserAgent += " Casdoor"
|
||||
return &ResendEmailProvider{Client: client}
|
||||
}
|
||||
|
||||
func (s *ResendEmailProvider) Send(fromAddress string, fromName string, toAddresses []string, subject string, content string) error {
|
||||
from := fromAddress
|
||||
if fromName != "" {
|
||||
from = fmt.Sprintf("%s <%s>", fromName, fromAddress)
|
||||
}
|
||||
params := &resend.SendEmailRequest{
|
||||
From: from,
|
||||
To: toAddresses,
|
||||
Subject: subject,
|
||||
Html: content,
|
||||
}
|
||||
if _, err := s.Client.Emails.Send(params); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
27
go.mod
27
go.mod
@@ -22,6 +22,8 @@ require (
|
||||
github.com/beego/beego/v2 v2.3.8
|
||||
github.com/beevik/etree v1.1.0
|
||||
github.com/casbin/casbin/v2 v2.77.2
|
||||
github.com/casbin/lego/v4 v4.5.4
|
||||
github.com/casdoor/casdoor-go-sdk v0.50.0
|
||||
github.com/casdoor/go-sms-sender v0.25.0
|
||||
github.com/casdoor/gomail/v2 v2.2.0
|
||||
github.com/casdoor/ldapserver v1.2.0
|
||||
@@ -29,6 +31,7 @@ require (
|
||||
github.com/casdoor/oss v1.8.0
|
||||
github.com/casdoor/xorm-adapter/v3 v3.1.0
|
||||
github.com/casvisor/casvisor-go-sdk v1.4.0
|
||||
github.com/corazawaf/coraza/v3 v3.3.3
|
||||
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
|
||||
github.com/elimity-com/scim v0.0.0-20230426070224-941a5eac92f3
|
||||
github.com/fogleman/gg v1.3.0
|
||||
@@ -44,9 +47,12 @@ require (
|
||||
github.com/go-webauthn/webauthn v0.10.2
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hsluoyz/modsecurity-go v0.0.7
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/lestrrat-go/jwx v1.2.29
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/likexian/whois v1.15.1
|
||||
github.com/likexian/whois-parser v1.24.9
|
||||
github.com/lor00x/goldap v0.0.0-20180618054307-a546dffdd1a3
|
||||
github.com/markbates/goth v1.82.0
|
||||
github.com/microsoft/go-mssqldb v1.9.0
|
||||
@@ -57,6 +63,7 @@ require (
|
||||
github.com/prometheus/client_golang v1.19.0
|
||||
github.com/prometheus/client_model v0.6.0
|
||||
github.com/qiangmzsx/string-adapter/v2 v2.1.0
|
||||
github.com/resend/resend-go/v3 v3.1.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/russellhaering/gosaml2 v0.9.0
|
||||
github.com/russellhaering/goxmldsig v1.2.0
|
||||
@@ -74,6 +81,7 @@ require (
|
||||
golang.org/x/net v0.41.0
|
||||
golang.org/x/oauth2 v0.27.0
|
||||
golang.org/x/text v0.27.0
|
||||
golang.org/x/time v0.8.0
|
||||
google.golang.org/api v0.215.0
|
||||
layeh.com/radius v0.0.0-20231213012653-1006025d24f8
|
||||
maunium.net/go/mautrix v0.22.1
|
||||
@@ -122,16 +130,17 @@ require (
|
||||
github.com/boombuler/barcode v1.0.1 // indirect
|
||||
github.com/bwmarrin/discordgo v0.28.1 // indirect
|
||||
github.com/caarlos0/go-reddit/v3 v3.0.1 // indirect
|
||||
github.com/casdoor/casdoor-go-sdk v0.50.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect
|
||||
github.com/corazawaf/libinjection-go v0.2.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||
github.com/cschomburg/go-pushbullet v0.0.0-20171206132031-67759df45fbb // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/dghubble/oauth1 v0.7.3 // indirect
|
||||
github.com/dghubble/sling v1.4.2 // indirect
|
||||
@@ -188,37 +197,44 @@ require (
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/likexian/gokit v0.25.13 // indirect
|
||||
github.com/line/line-bot-sdk-go v7.8.0+incompatible // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/magefile/mage v1.15.1-0.20241126214340-bdc92f694516 // indirect
|
||||
github.com/markbates/going v1.0.0 // indirect
|
||||
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-ieproxy v0.0.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/miekg/dns v1.1.57 // indirect
|
||||
github.com/mileusna/viber v1.0.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||
github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4 // indirect
|
||||
github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 // indirect
|
||||
github.com/pingcap/log v0.0.0-20210625125904-98ed8e2eb1c7 // indirect
|
||||
github.com/pingcap/tidb/parser v0.0.0-20221126021158-6b02a5d8ba7d // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/qiniu/go-sdk/v7 v7.12.1 // indirect
|
||||
github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2 // indirect
|
||||
github.com/redis/go-redis/v9 v9.5.5 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
github.com/rs/zerolog v1.33.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.0.1 // indirect
|
||||
github.com/scim2/filter-parser/v2 v2.2.0 // indirect
|
||||
github.com/sendgrid/rest v2.6.9+incompatible // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 // indirect
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
@@ -238,7 +254,9 @@ require (
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
github.com/twilio/twilio-go v1.13.0 // indirect
|
||||
github.com/ucloud/ucloud-sdk-go v0.22.5 // indirect
|
||||
github.com/urfave/cli v1.22.5 // indirect
|
||||
github.com/utahta/go-linenotify v0.5.0 // indirect
|
||||
github.com/valllabh/ocsf-schema-golang v1.0.3 // indirect
|
||||
github.com/volcengine/volc-sdk-golang v1.0.117 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
@@ -261,7 +279,6 @@ require (
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/time v0.8.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
|
||||
@@ -272,6 +289,7 @@ require (
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
@@ -283,4 +301,5 @@ require (
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/token v1.0.1 // indirect
|
||||
rsc.io/binaryregexp v0.2.0 // indirect
|
||||
)
|
||||
|
||||
51
go.sum
51
go.sum
@@ -778,6 +778,7 @@ github.com/alibabacloud-go/tea-xml v1.1.1/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCE
|
||||
github.com/alibabacloud-go/tea-xml v1.1.2/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3 h1:7LYnm+JbOq2B+T/B0fHC4Ies4/FofC4zHzYtqw7dgt0=
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1183/go.mod h1:pUKYbK5JQ+1Dfxk80P0qxGqe5dkxDoabbZS7zOcouyA=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 h1:qagvUyrgOnBIlVRQWOyCZGVKUIYbMBdGdJ104vBpRFU=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible h1:9gWa46nstkJ9miBReJcN8Gq34cBFbzSpQZVVT9N09TM=
|
||||
@@ -850,6 +851,8 @@ github.com/casbin/casbin/v2 v2.28.3/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRt
|
||||
github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
|
||||
github.com/casbin/casbin/v2 v2.77.2 h1:yQinn/w9x8AswiwqwtrXz93VU48R1aYTXdHEx4RI3jM=
|
||||
github.com/casbin/casbin/v2 v2.77.2/go.mod h1:mzGx0hYW9/ksOSpw3wNjk3NRAroq5VMFYUQ6G43iGPk=
|
||||
github.com/casbin/lego/v4 v4.5.4 h1:WdVEj1A5KmKZheNuFNLF/5+UUkpXLt9mEOrLX3E81Vo=
|
||||
github.com/casbin/lego/v4 v4.5.4/go.mod h1:JjTyJgN5pyrDPcg3+aAM1NtFQIXl8zDgsoSS1TnVpJ8=
|
||||
github.com/casdoor/casdoor-go-sdk v0.50.0 h1:bUYbz/MzJuWfLKJbJM0+U0YpYewAur+THp5TKnufWZM=
|
||||
github.com/casdoor/casdoor-go-sdk v0.50.0/go.mod h1:cMnkCQJgMYpgAlgEx8reSt1AVaDIQLcJ1zk5pzBaz+4=
|
||||
github.com/casdoor/go-sms-sender v0.25.0 h1:eF4cOCSbjVg7+0uLlJQnna/FQ0BWW+Fp/x4cXhzQu1Y=
|
||||
@@ -907,10 +910,16 @@ github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWH
|
||||
github.com/cncf/xds/go v0.0.0-20230310173818-32f1caf87195/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI=
|
||||
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||
github.com/corazawaf/coraza/v3 v3.3.3 h1:kqjStHAgWqwP5dh7n0vhTOF0a3t+VikNS/EaMiG0Fhk=
|
||||
github.com/corazawaf/coraza/v3 v3.3.3/go.mod h1:xSaXWOhFMSbrV8qOOfBKAyw3aOqfwaSaOy5BgSF8XlA=
|
||||
github.com/corazawaf/libinjection-go v0.2.2 h1:Chzodvb6+NXh6wew5/yhD0Ggioif9ACrQGR4qjTCs1g=
|
||||
github.com/corazawaf/libinjection-go v0.2.2/go.mod h1:OP4TM7xdJ2skyXqNX1AN1wN5nNZEmJNuWbNPOItn7aw=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cschomburg/go-pushbullet v0.0.0-20171206132031-67759df45fbb h1:7X9nrm+LNWdxzQOiCjy0G51rNUxbH35IDHCjAMvogyM=
|
||||
github.com/cschomburg/go-pushbullet v0.0.0-20171206132031-67759df45fbb/go.mod h1:RfQ9wji3fjcSEsQ+uFCtIh3+BXgcZum8Kt3JxvzYzlk=
|
||||
@@ -922,6 +931,8 @@ github.com/cznic/strutil v0.0.0-20171016134553-529a34b1c186/go.mod h1:AHHPPPXTw0
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f h1:q/DpyjJjZs94bziQ7YkBmIlpqbVP7yw179rnzoNVX1M=
|
||||
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f/go.mod h1:QGrK8vMWWHQYQ3QU9bw9Y9OPNfxccGzfb41qjvVeXtY=
|
||||
github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ=
|
||||
@@ -1291,6 +1302,8 @@ github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg
|
||||
github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
|
||||
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/hsluoyz/modsecurity-go v0.0.7 h1:W5ChaDrm4kM/UhHxoD2zyxQ+6s5kSj6cVftDFgdFzBM=
|
||||
github.com/hsluoyz/modsecurity-go v0.0.7/go.mod h1:hi81ySzwvlQFd5pip9c3uwXHDAW9ayxwLbt8ufxRkdY=
|
||||
github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo=
|
||||
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
@@ -1308,6 +1321,7 @@ github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aW
|
||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
github.com/jinzhu/configor v1.2.1 h1:OKk9dsR8i6HPOCZR8BcMtcEImAFjIhbJFZNyn5GCZko=
|
||||
github.com/jinzhu/configor v1.2.1/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
@@ -1321,7 +1335,9 @@ github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUB
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
@@ -1390,6 +1406,12 @@ github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/likexian/gokit v0.25.13 h1:p2Uw3+6fGG53CwdU2Dz0T6bOycdb2+bAFAa3ymwWVkM=
|
||||
github.com/likexian/gokit v0.25.13/go.mod h1:qQhEWFBEfqLCO3/vOEo2EDKd+EycekVtUK4tex+l2H4=
|
||||
github.com/likexian/whois v1.15.1 h1:6vTMI8n9s1eJdmcO4R9h1x99aQWIZZX1CD3am68gApU=
|
||||
github.com/likexian/whois v1.15.1/go.mod h1:/nxmQ6YXvLz+qTxC/QFtEJNAt0zLuRxJrKiWpBJX8X0=
|
||||
github.com/likexian/whois-parser v1.24.9 h1:BT6fzO3lj3F07yzVv0YXoaj+K4Ush0/cF+Yp6tvJJgk=
|
||||
github.com/likexian/whois-parser v1.24.9/go.mod h1:b6STMHHDaSKbd4PzGrP50wWE5NzeBUETa/hT9gI0G9I=
|
||||
github.com/line/line-bot-sdk-go v7.8.0+incompatible h1:Uf9/OxV0zCVfqyvwZPH8CrdiHXXmMRa/L91G3btQblQ=
|
||||
github.com/line/line-bot-sdk-go v7.8.0+incompatible/go.mod h1:0RjLjJEAU/3GIcHkC3av6O4jInAbt25nnZVmOFUgDBg=
|
||||
github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275 h1:IZycmTpoUtQK3PD60UYBwjaCUHUP7cML494ao9/O8+Q=
|
||||
@@ -1401,6 +1423,8 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2
|
||||
github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
|
||||
github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
|
||||
github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o=
|
||||
github.com/magefile/mage v1.15.1-0.20241126214340-bdc92f694516 h1:aAO0L0ulox6m/CLRYvJff+jWXYYCKGpEm3os7dM/Z+M=
|
||||
github.com/magefile/mage v1.15.1-0.20241126214340-bdc92f694516/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/markbates/going v1.0.0 h1:DQw0ZP7NbNlFGcKbcE/IVSOAFzScxRtLpd0rLMzLhq0=
|
||||
github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA=
|
||||
github.com/markbates/goth v1.82.0 h1:8j/c34AjBSTNzO7zTsOyP5IYCQCMBTRBHAbBt/PI0bQ=
|
||||
@@ -1434,7 +1458,10 @@ github.com/microsoft/go-mssqldb v1.9.0 h1:5Vq+u2f4LDujJNeZn62Z4kBDEC9MjLv0ukRzOu
|
||||
github.com/microsoft/go-mssqldb v1.9.0/go.mod h1:GBbW9ASTiDC+mpgWDGKdm3FnFLTUsLYN3iFL90lQ+PA=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||
github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg=
|
||||
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
|
||||
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
|
||||
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
|
||||
github.com/mileusna/viber v1.0.1 h1:gWB6/lKoWYVxkH0Jb8jRnGIRZ/9DEM7RBZRJHRfdYWs=
|
||||
github.com/mileusna/viber v1.0.1/go.mod h1:Pxu/iPMnYjnHgu+bEp3SiKWHWmlf/kDp/yOX8XUdYrQ=
|
||||
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
|
||||
@@ -1497,6 +1524,8 @@ github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/performancecopilot/speed/v4 v4.0.0/go.mod h1:qxrSyuDGrTOWfV+uKRFhfxw6h/4HXRGUiZiufxo49BM=
|
||||
github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4 h1:1Kw2vDBXmjop+LclnzCb/fFy+sgb3gYARwfmoUcQe6o=
|
||||
github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw=
|
||||
github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=
|
||||
github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||
github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||
@@ -1529,6 +1558,8 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgm
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/polarsource/polar-go v0.12.0 h1:um+6ftOPUMg2TQq9Kv/6fKGBOAl7dOc2YiDdx4Bb0y8=
|
||||
github.com/polarsource/polar-go v0.12.0/go.mod h1:FB11Q4m2n3wIk6l/POOkz0MVOUx1o0Yt4Y97MnQfe0c=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
@@ -1572,12 +1603,16 @@ github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdk
|
||||
github.com/qiniu/go-sdk/v7 v7.12.1 h1:FZG5dhs2MZBV/mHVhmHnsgsQ+j1gSE0RqIoA2WwEDwY=
|
||||
github.com/qiniu/go-sdk/v7 v7.12.1/go.mod h1:btsaOc8CA3hdVloULfFdDgDc+g4f3TDZEFsDY0BLE+w=
|
||||
github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
|
||||
github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2 h1:dq90+d51/hQRaHEqRAsQ1rE/pC1GUS4sc2rCbbFsAIY=
|
||||
github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2/go.mod h1:7tZKcyumwBO6qip7RNQ5r77yrssm9bfCowcLEBcU5IA=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/redis/go-redis/v9 v9.5.5 h1:51VEyMF8eOO+NUHFm8fpg+IOc1xFuFOhxs3R+kPu1FM=
|
||||
github.com/redis/go-redis/v9 v9.5.5/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/resend/resend-go/v3 v3.1.0 h1:bJpU5gYCDcczLdhCo37oy9mOmdtSVlOzM6IfWX9zhMw=
|
||||
github.com/resend/resend-go/v3 v3.1.0/go.mod h1:iI7VA0NoGjWvsNii5iNC5Dy0llsI3HncXPejhniYzwE=
|
||||
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/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
@@ -1595,6 +1630,7 @@ github.com/russellhaering/gosaml2 v0.9.0 h1:CNMnH42z/GirrKjdmNrSS6bAAs47F9bPdl4P
|
||||
github.com/russellhaering/gosaml2 v0.9.0/go.mod h1:byViER/1YPUa0Puj9ROZblpoq2jsE7h/CJmitzX0geU=
|
||||
github.com/russellhaering/goxmldsig v1.2.0 h1:Y6GTTc9Un5hCxSzVz4UIWQ/zuVwDvzJk80guqzwx6Vg=
|
||||
github.com/russellhaering/goxmldsig v1.2.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
|
||||
github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk=
|
||||
@@ -1616,6 +1652,7 @@ github.com/shirou/gopsutil/v4 v4.25.9 h1:JImNpf6gCVhKgZhtaAHJ0serfFGtlfIlSC08eaK
|
||||
github.com/shirou/gopsutil/v4 v4.25.9/go.mod h1:gxIxoC+7nQRwUl/xNhutXlD8lq+jxTgpIkEf3rADHL8=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 h1:xT+JlYxNGqyT+XcU8iUrN18JYed2TvG9yN5ULG2jATM=
|
||||
github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw=
|
||||
@@ -1636,7 +1673,9 @@ github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQ
|
||||
github.com/slack-go/slack v0.15.0 h1:LE2lj2y9vqqiOf+qIIy0GvEoxgF1N5yLGZffmEZykt0=
|
||||
github.com/slack-go/slack v0.15.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
|
||||
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||
github.com/sony/sonyflake v1.0.0 h1:MpU6Ro7tfXwgn2l5eluf9xQvQJDROTBImNCfRXn/YeM=
|
||||
@@ -1653,6 +1692,7 @@ github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e/go.mod h1:qNTQ5P5J
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
@@ -1696,6 +1736,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/timtadh/data-structures v0.5.3/go.mod h1:9R4XODhJ8JdWFEI8P/HJKqxuJctfBQw6fDibMQny2oU=
|
||||
github.com/timtadh/lexmachine v0.2.2/go.mod h1:GBJvD5OAfRn/gnp92zb9KTgHLB7akKyxmVivoYCcjQI=
|
||||
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
|
||||
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
||||
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
|
||||
@@ -1712,9 +1754,13 @@ github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVK
|
||||
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
|
||||
github.com/ucloud/ucloud-sdk-go v0.22.5 h1:GIltVwMDUqQj4iPL/emsZAMhEYWjLTwZqpOxdkdDrM8=
|
||||
github.com/ucloud/ucloud-sdk-go v0.22.5/go.mod h1:dyLmFHmUfgb4RZKYQP9IArlvQ2pxzFthfhwxRzOEPIw=
|
||||
github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU=
|
||||
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/utahta/go-linenotify v0.5.0 h1:E1tJaB/XhqRY/iz203FD0MaHm10DjQPOq5/Mem2A3Gs=
|
||||
github.com/utahta/go-linenotify v0.5.0/go.mod h1:KsvBXil2wx+ByaCR0e+IZKTbp4pDesc7yjzRigLf6pE=
|
||||
github.com/valllabh/ocsf-schema-golang v1.0.3 h1:eR8k/3jP/OOqB8LRCtdJ4U+vlgd/gk5y3KMXoodrsrw=
|
||||
github.com/valllabh/ocsf-schema-golang v1.0.3/go.mod h1:sZ3as9xqm1SSK5feFWIR2CuGeGRhsM7TR1MbpBctzPk=
|
||||
github.com/volcengine/volc-sdk-golang v1.0.117 h1:ykFVSwsVq9qvIoWP9jeP+VKNAUjrblAdsZl46yVWiH8=
|
||||
github.com/volcengine/volc-sdk-golang v1.0.117/go.mod h1:ojXSFvj404o2UKnZR9k9LUUWIUU+9XtlRlzk2+UFc/M=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
@@ -2632,13 +2678,17 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
|
||||
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
|
||||
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
@@ -2745,6 +2795,7 @@ modernc.org/z v1.0.1-0.20210308123920-1f282aa71362/go.mod h1:8/SRk5C/HgiQWCgXdfp
|
||||
modernc.org/z v1.0.1/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA=
|
||||
modernc.org/z v1.5.1 h1:RTNHdsrOpeoSeOF4FbzTo8gBYByaJ5xT7NgZ9ZqRiJM=
|
||||
modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=
|
||||
rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
|
||||
19
i18n/util.go
19
i18n/util.go
@@ -98,15 +98,22 @@ func Translate(language string, errorText string) string {
|
||||
if langMap[language] == nil {
|
||||
file, err := f.ReadFile(fmt.Sprintf("locales/%s/data.json", language))
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Translate error: the language \"%s\" is not supported, err = %s", language, err.Error())
|
||||
originalLanguage := language
|
||||
language = "en"
|
||||
file, err = f.ReadFile(fmt.Sprintf("locales/%s/data.json", language))
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Translate error: the language \"%s\" is not supported, err = %s", originalLanguage, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
data := I18nData{}
|
||||
err = util.JsonToStruct(string(file), &data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
if langMap[language] == nil {
|
||||
data := I18nData{}
|
||||
err = util.JsonToStruct(string(file), &data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
langMap[language] = data
|
||||
}
|
||||
langMap[language] = data
|
||||
}
|
||||
|
||||
res := langMap[language][tokens[0]][tokens[1]]
|
||||
|
||||
@@ -264,27 +264,31 @@ func rsaSignWithRSA256(signContent string, privateKey string) (string, error) {
|
||||
|
||||
// privateKey in database is a string, format it to PEM style
|
||||
func formatPrivateKey(privateKey string) string {
|
||||
// each line length is 64
|
||||
preFmtPrivateKey := ""
|
||||
for i := 0; ; {
|
||||
if i+64 <= len(privateKey) {
|
||||
preFmtPrivateKey = preFmtPrivateKey + privateKey[i:i+64] + "\n"
|
||||
i += 64
|
||||
} else {
|
||||
preFmtPrivateKey = preFmtPrivateKey + privateKey[i:]
|
||||
break
|
||||
// Check if the key is already in PEM format
|
||||
if strings.HasPrefix(privateKey, "-----BEGIN PRIVATE KEY-----") ||
|
||||
strings.HasPrefix(privateKey, "-----BEGIN RSA PRIVATE KEY-----") {
|
||||
// Key is already in PEM format, return as is
|
||||
return privateKey
|
||||
}
|
||||
|
||||
// Remove any whitespace from the key
|
||||
privateKey = strings.ReplaceAll(privateKey, "\n", "")
|
||||
privateKey = strings.ReplaceAll(privateKey, "\r", "")
|
||||
privateKey = strings.ReplaceAll(privateKey, " ", "")
|
||||
|
||||
// Format the key with line breaks every 64 characters using strings.Builder
|
||||
var builder strings.Builder
|
||||
for i := 0; i < len(privateKey); i += 64 {
|
||||
end := i + 64
|
||||
if end > len(privateKey) {
|
||||
end = len(privateKey)
|
||||
}
|
||||
builder.WriteString(privateKey[i:end])
|
||||
if end < len(privateKey) {
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
}
|
||||
privateKey = strings.Trim(preFmtPrivateKey, "\n")
|
||||
|
||||
// add pkcs#8 BEGIN and END
|
||||
PemBegin := "-----BEGIN PRIVATE KEY-----\n"
|
||||
PemEnd := "\n-----END PRIVATE KEY-----"
|
||||
if !strings.HasPrefix(privateKey, PemBegin) {
|
||||
privateKey = PemBegin + privateKey
|
||||
}
|
||||
if !strings.HasSuffix(privateKey, PemEnd) {
|
||||
privateKey = privateKey + PemEnd
|
||||
}
|
||||
return privateKey
|
||||
return "-----BEGIN PRIVATE KEY-----\n" + builder.String() + "\n-----END PRIVATE KEY-----"
|
||||
}
|
||||
|
||||
@@ -67,6 +67,8 @@
|
||||
{"name": "ID", "visible": true, "viewRule": "Public", "modifyRule": "Immutable"},
|
||||
{"name": "Name", "visible": true, "viewRule": "Public", "modifyRule": "Admin"},
|
||||
{"name": "Display name", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
|
||||
{"name": "First name", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
|
||||
{"name": "Last name", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
|
||||
{"name": "Avatar", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
|
||||
{"name": "User type", "visible": true, "viewRule": "Public", "modifyRule": "Admin"},
|
||||
{"name": "Password", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
|
||||
@@ -81,6 +83,7 @@
|
||||
{"name": "Title", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
|
||||
{"name": "ID card type", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
|
||||
{"name": "ID card", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
|
||||
{"name": "ID card info", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
|
||||
{"name": "Real name", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
|
||||
{"name": "ID verification", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
|
||||
{"name": "Homepage", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
|
||||
@@ -101,6 +104,7 @@
|
||||
{"name": "Signup application", "visible": true, "viewRule": "Public", "modifyRule": "Admin"},
|
||||
{"name": "Register type", "visible": true, "viewRule": "Public", "modifyRule": "Admin"},
|
||||
{"name": "Register source", "visible": true, "viewRule": "Public", "modifyRule": "Admin"},
|
||||
{"name": "API key", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
|
||||
{"name": "Roles", "visible": true, "viewRule": "Public", "modifyRule": "Immutable"},
|
||||
{"name": "Permissions", "visible": true, "viewRule": "Public", "modifyRule": "Immutable"},
|
||||
{"name": "Groups", "visible": true, "viewRule": "Public", "modifyRule": "Admin"},
|
||||
@@ -110,9 +114,14 @@
|
||||
{"name": "Is forbidden", "visible": true, "viewRule": "Admin", "modifyRule": "Admin"},
|
||||
{"name": "Is deleted", "visible": true, "viewRule": "Admin", "modifyRule": "Admin"},
|
||||
{"name": "Multi-factor authentication", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
|
||||
{"name": "MFA items", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
|
||||
{"name": "WebAuthn credentials", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
|
||||
{"name": "Last change password time", "visible": true, "viewRule": "Admin", "modifyRule": "Admin"},
|
||||
{"name": "Managed accounts", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
|
||||
{"name": "MFA accounts", "visible": true, "viewRule": "Self", "modifyRule": "Self"}
|
||||
{"name": "Face ID", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
|
||||
{"name": "MFA accounts", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
|
||||
{"name": "Need update password", "visible": true, "viewRule": "Admin", "modifyRule": "Admin"},
|
||||
{"name": "IP whitelist", "visible": true, "viewRule": "Admin", "modifyRule": "Admin"}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
43
ip/ip.go
Normal file
43
ip/ip.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright 2024 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 ip
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
func InitIpDb() {
|
||||
err := Init("ip/17monipdb.dat")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func IsAbroadIp(ip string) bool {
|
||||
// If it's an intranet IP, it's not abroad
|
||||
if util.IsIntranetIp(ip) {
|
||||
return false
|
||||
}
|
||||
|
||||
info, err := Find(ip)
|
||||
if err != nil {
|
||||
fmt.Printf("error: ip = %s, error = %s\n", ip, err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
return info.Country != "中国"
|
||||
}
|
||||
199
ip/ip17mon.go
Normal file
199
ip/ip17mon.go
Normal file
@@ -0,0 +1,199 @@
|
||||
// Copyright 2022 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 ip
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
)
|
||||
|
||||
const Null = "N/A"
|
||||
|
||||
var (
|
||||
ErrInvalidIp = errors.New("invalid ip format")
|
||||
std *Locator
|
||||
)
|
||||
|
||||
// Init default locator with dataFile
|
||||
func Init(dataFile string) (err error) {
|
||||
if std != nil {
|
||||
return
|
||||
}
|
||||
std, err = NewLocator(dataFile)
|
||||
return
|
||||
}
|
||||
|
||||
// Init default locator with data
|
||||
func InitWithData(data []byte) {
|
||||
if std != nil {
|
||||
return
|
||||
}
|
||||
std = NewLocatorWithData(data)
|
||||
return
|
||||
}
|
||||
|
||||
// Find locationInfo by ip string
|
||||
// It will return err when ipstr is not a valid format
|
||||
func Find(ipstr string) (*LocationInfo, error) {
|
||||
return std.Find(ipstr)
|
||||
}
|
||||
|
||||
// Find locationInfo by uint32
|
||||
func FindByUint(ip uint32) *LocationInfo {
|
||||
return std.FindByUint(ip)
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
// New locator with dataFile
|
||||
func NewLocator(dataFile string) (loc *Locator, err error) {
|
||||
data, err := ioutil.ReadFile(dataFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
loc = NewLocatorWithData(data)
|
||||
return
|
||||
}
|
||||
|
||||
// New locator with data
|
||||
func NewLocatorWithData(data []byte) (loc *Locator) {
|
||||
loc = new(Locator)
|
||||
loc.init(data)
|
||||
return
|
||||
}
|
||||
|
||||
type Locator struct {
|
||||
textData []byte
|
||||
indexData1 []uint32
|
||||
indexData2 []int
|
||||
indexData3 []int
|
||||
index []int
|
||||
}
|
||||
|
||||
type LocationInfo struct {
|
||||
Country string
|
||||
Region string
|
||||
City string
|
||||
Isp string
|
||||
}
|
||||
|
||||
// Find locationInfo by ip string
|
||||
// It will return err when ipstr is not a valid format
|
||||
func (loc *Locator) Find(ipstr string) (info *LocationInfo, err error) {
|
||||
ip := net.ParseIP(ipstr).To4()
|
||||
if ip == nil || ip.To4() == nil {
|
||||
err = ErrInvalidIp
|
||||
return
|
||||
}
|
||||
info = loc.FindByUint(binary.BigEndian.Uint32([]byte(ip)))
|
||||
return
|
||||
}
|
||||
|
||||
// Find locationInfo by uint32
|
||||
func (loc *Locator) FindByUint(ip uint32) (info *LocationInfo) {
|
||||
end := len(loc.indexData1) - 1
|
||||
if ip>>24 != 0xff {
|
||||
end = loc.index[(ip>>24)+1]
|
||||
}
|
||||
idx := loc.findIndexOffset(ip, loc.index[ip>>24], end)
|
||||
off := loc.indexData2[idx]
|
||||
return newLocationInfo(loc.textData[off : off+loc.indexData3[idx]])
|
||||
}
|
||||
|
||||
// binary search
|
||||
func (loc *Locator) findIndexOffset(ip uint32, start, end int) int {
|
||||
for start < end {
|
||||
mid := (start + end) / 2
|
||||
if ip > loc.indexData1[mid] {
|
||||
start = mid + 1
|
||||
} else {
|
||||
end = mid
|
||||
}
|
||||
}
|
||||
|
||||
if loc.indexData1[end] >= ip {
|
||||
return end
|
||||
}
|
||||
|
||||
return start
|
||||
}
|
||||
|
||||
func (loc *Locator) init(data []byte) {
|
||||
textoff := int(binary.BigEndian.Uint32(data[:4]))
|
||||
|
||||
loc.textData = data[textoff-1024:]
|
||||
|
||||
loc.index = make([]int, 256)
|
||||
for i := 0; i < 256; i++ {
|
||||
off := 4 + i*4
|
||||
loc.index[i] = int(binary.LittleEndian.Uint32(data[off : off+4]))
|
||||
}
|
||||
|
||||
nidx := (textoff - 4 - 1024 - 1024) / 8
|
||||
|
||||
loc.indexData1 = make([]uint32, nidx)
|
||||
loc.indexData2 = make([]int, nidx)
|
||||
loc.indexData3 = make([]int, nidx)
|
||||
|
||||
for i := 0; i < nidx; i++ {
|
||||
off := 4 + 1024 + i*8
|
||||
loc.indexData1[i] = binary.BigEndian.Uint32(data[off : off+4])
|
||||
loc.indexData2[i] = int(uint32(data[off+4]) | uint32(data[off+5])<<8 | uint32(data[off+6])<<16)
|
||||
loc.indexData3[i] = int(data[off+7])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func newLocationInfo(str []byte) *LocationInfo {
|
||||
var info *LocationInfo
|
||||
|
||||
fields := bytes.Split(str, []byte("\t"))
|
||||
switch len(fields) {
|
||||
case 4:
|
||||
// free version
|
||||
info = &LocationInfo{
|
||||
Country: string(fields[0]),
|
||||
Region: string(fields[1]),
|
||||
City: string(fields[2]),
|
||||
}
|
||||
case 5:
|
||||
// pay version
|
||||
info = &LocationInfo{
|
||||
Country: string(fields[0]),
|
||||
Region: string(fields[1]),
|
||||
City: string(fields[2]),
|
||||
Isp: string(fields[4]),
|
||||
}
|
||||
default:
|
||||
panic("unexpected ip info:" + string(str))
|
||||
}
|
||||
|
||||
if len(info.Country) == 0 {
|
||||
info.Country = Null
|
||||
}
|
||||
if len(info.Region) == 0 {
|
||||
info.Region = Null
|
||||
}
|
||||
if len(info.City) == 0 {
|
||||
info.City = Null
|
||||
}
|
||||
if len(info.Isp) == 0 {
|
||||
info.Isp = Null
|
||||
}
|
||||
return info
|
||||
}
|
||||
9
main.go
9
main.go
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/casdoor/casdoor/proxy"
|
||||
"github.com/casdoor/casdoor/radius"
|
||||
"github.com/casdoor/casdoor/routers"
|
||||
"github.com/casdoor/casdoor/service"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
@@ -73,6 +74,10 @@ func main() {
|
||||
object.InitCasvisorConfig()
|
||||
object.InitCleanupTokens()
|
||||
|
||||
object.InitSiteMap()
|
||||
object.InitRuleMap()
|
||||
object.StartMonitorSitesLoop()
|
||||
|
||||
util.SafeGoroutine(func() { object.RunSyncUsersJob() })
|
||||
util.SafeGoroutine(func() { controllers.InitCLIDownloader() })
|
||||
|
||||
@@ -126,5 +131,9 @@ func main() {
|
||||
go radius.StartRadiusServer()
|
||||
go object.ClearThroughputPerSecond()
|
||||
|
||||
if len(object.SiteMap) != 0 {
|
||||
service.Start()
|
||||
}
|
||||
|
||||
web.Run(fmt.Sprintf(":%v", port))
|
||||
}
|
||||
|
||||
@@ -125,6 +125,7 @@ type Application struct {
|
||||
|
||||
ClientId string `xorm:"varchar(100)" json:"clientId"`
|
||||
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
|
||||
ClientCert string `xorm:"varchar(100)" json:"clientCert"`
|
||||
RedirectUris []string `xorm:"varchar(1000)" json:"redirectUris"`
|
||||
ForcedRedirectOrigin string `xorm:"varchar(100)" json:"forcedRedirectOrigin"`
|
||||
TokenFormat string `xorm:"varchar(100)" json:"tokenFormat"`
|
||||
@@ -155,12 +156,16 @@ type Application struct {
|
||||
FailedSigninFrozenTime int `json:"failedSigninFrozenTime"`
|
||||
CodeResendTimeout int `json:"codeResendTimeout"`
|
||||
|
||||
CustomScopes []*ScopeDescription `xorm:"mediumtext" json:"customScopes"`
|
||||
|
||||
// Reverse proxy fields
|
||||
Domain string `xorm:"varchar(100)" json:"domain"`
|
||||
OtherDomains []string `xorm:"varchar(1000)" json:"otherDomains"`
|
||||
UpstreamHost string `xorm:"varchar(100)" json:"upstreamHost"`
|
||||
SslMode string `xorm:"varchar(100)" json:"sslMode"`
|
||||
SslCert string `xorm:"varchar(100)" json:"sslCert"`
|
||||
|
||||
CertObj *Cert `xorm:"-"`
|
||||
}
|
||||
|
||||
func GetApplicationCount(owner, field, value string) (int64, error) {
|
||||
@@ -745,6 +750,11 @@ func UpdateApplication(id string, application *Application, isGlobalAdmin bool,
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = validateCustomScopes(application.CustomScopes, lang)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, providerItem := range application.Providers {
|
||||
providerItem.Provider = nil
|
||||
}
|
||||
@@ -800,6 +810,11 @@ func AddApplication(application *Application) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = validateCustomScopes(application.CustomScopes, "en")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, providerItem := range application.Providers {
|
||||
providerItem.Provider = nil
|
||||
}
|
||||
|
||||
121
object/cert.go
121
object/cert.go
@@ -16,9 +16,12 @@ package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/certificate"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/xorm-io/core"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
)
|
||||
|
||||
type Cert struct {
|
||||
@@ -33,6 +36,13 @@ type Cert struct {
|
||||
BitSize int `json:"bitSize"`
|
||||
ExpireInYears int `json:"expireInYears"`
|
||||
|
||||
ExpireTime string `xorm:"varchar(100)" json:"expireTime"`
|
||||
DomainExpireTime string `xorm:"varchar(100)" json:"domainExpireTime"`
|
||||
Provider string `xorm:"varchar(100)" json:"provider"`
|
||||
Account string `xorm:"varchar(100)" json:"account"`
|
||||
AccessKey string `xorm:"varchar(100)" json:"accessKey"`
|
||||
AccessSecret string `xorm:"varchar(100)" json:"accessSecret"`
|
||||
|
||||
Certificate string `xorm:"mediumtext" json:"certificate"`
|
||||
PrivateKey string `xorm:"mediumtext" json:"privateKey"`
|
||||
}
|
||||
@@ -224,6 +234,20 @@ func (p *Cert) populateContent() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if p.Type == "SSL" {
|
||||
if p.Certificate != "" {
|
||||
expireTime, err := util.GetCertExpireTime(p.Certificate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.ExpireTime = expireTime
|
||||
} else {
|
||||
p.ExpireTime = ""
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(p.CryptoAlgorithm) < 3 {
|
||||
err := fmt.Errorf("populateContent() error, unsupported crypto algorithm: %s", p.CryptoAlgorithm)
|
||||
return err
|
||||
@@ -258,6 +282,42 @@ func (p *Cert) populateContent() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func RenewCert(cert *Cert) (bool, error) {
|
||||
useProxy := false
|
||||
if cert.Provider == "GoDaddy" {
|
||||
useProxy = true
|
||||
}
|
||||
|
||||
client, err := GetAcmeClient(useProxy)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
var certStr, privateKey string
|
||||
if cert.Provider == "Aliyun" {
|
||||
certStr, privateKey, err = certificate.ObtainCertificateAli(client, cert.Name, cert.AccessKey, cert.AccessSecret)
|
||||
} else if cert.Provider == "GoDaddy" {
|
||||
certStr, privateKey, err = certificate.ObtainCertificateGoDaddy(client, cert.Name, cert.AccessKey, cert.AccessSecret)
|
||||
} else {
|
||||
return false, fmt.Errorf("unknown provider: %s", cert.Provider)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
expireTime, err := util.GetCertExpireTime(certStr)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
cert.ExpireTime = expireTime
|
||||
cert.Certificate = certStr
|
||||
cert.PrivateKey = privateKey
|
||||
|
||||
return UpdateCert(cert.GetId(), cert)
|
||||
}
|
||||
|
||||
func getCertByApplication(application *Application) (*Cert, error) {
|
||||
if application.Cert != "" {
|
||||
return getCertByName(application.Cert)
|
||||
@@ -288,3 +348,64 @@ func certChangeTrigger(oldName string, newName string) error {
|
||||
|
||||
return session.Commit()
|
||||
}
|
||||
|
||||
func getBaseDomain(domain string) (string, error) {
|
||||
// abc.com -> abc.com
|
||||
// abc.com.it -> abc.com.it
|
||||
// subdomain.abc.io -> abc.io
|
||||
// subdomain.abc.org.us -> abc.org.us
|
||||
return publicsuffix.EffectiveTLDPlusOne(domain)
|
||||
}
|
||||
|
||||
func GetCertByDomain(domain string) (*Cert, error) {
|
||||
if domain == "" {
|
||||
return nil, fmt.Errorf("GetCertByDomain() error: domain should not be empty")
|
||||
}
|
||||
|
||||
cert, ok := certMap[domain]
|
||||
if ok {
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
baseDomain, err := getBaseDomain(domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cert, ok = certMap[baseDomain]
|
||||
if ok {
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func getCertMap() (map[string]*Cert, error) {
|
||||
certs, err := GetGlobalCerts()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := map[string]*Cert{}
|
||||
for _, cert := range certs {
|
||||
res[cert.Name] = cert
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (p *Cert) isCertNearExpire() (bool, error) {
|
||||
if p.ExpireTime == "" {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
expireTime, err := time.Parse(time.RFC3339, p.ExpireTime)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
duration := expireTime.Sub(now)
|
||||
res := duration <= 7*24*time.Hour
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
75
object/cert_whois.go
Normal file
75
object/cert_whois.go
Normal file
@@ -0,0 +1,75 @@
|
||||
// Copyright 2023 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 (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/likexian/whois"
|
||||
whoisparser "github.com/likexian/whois-parser"
|
||||
)
|
||||
|
||||
func getDomainExpireTime(domainName string) (string, error) {
|
||||
domainName, err := util.GetBaseDomain(domainName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
server := ""
|
||||
if strings.HasSuffix(domainName, ".com") || strings.HasSuffix(domainName, ".net") {
|
||||
server = "whois.verisign-grs.com"
|
||||
} else if strings.HasSuffix(domainName, ".org") {
|
||||
server = "whois.pir.org"
|
||||
} else if strings.HasSuffix(domainName, ".io") {
|
||||
server = "whois.nic.io"
|
||||
} else if strings.HasSuffix(domainName, ".co") {
|
||||
server = "whois.nic.co"
|
||||
} else if strings.HasSuffix(domainName, ".cn") {
|
||||
server = "whois.cnnic.cn"
|
||||
} else if strings.HasSuffix(domainName, ".run") {
|
||||
server = "whois.nic.run"
|
||||
} else {
|
||||
server = "grs-whois.hichina.com" // com, net, cc, tv
|
||||
}
|
||||
|
||||
client := whois.NewClient()
|
||||
//if server != "whois.cnnic.cn" && server != "grs-whois.hichina.com" {
|
||||
// dialer := proxy.GetProxyDialer()
|
||||
// if dialer != nil {
|
||||
// client.SetDialer(dialer)
|
||||
// }
|
||||
//}
|
||||
|
||||
data, err := client.Whois(domainName, server)
|
||||
if err != nil {
|
||||
if !strings.HasSuffix(domainName, ".run") || data == "" {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
whoisInfo, err := whoisparser.Parse(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
res := whoisInfo.Domain.ExpirationDateInTime.Local().Format(time.RFC3339)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func GetDomainExpireTime(domainName string) (string, error) {
|
||||
return getDomainExpireTime(domainName)
|
||||
}
|
||||
@@ -53,6 +53,8 @@ func getBuiltInAccountItems() []*AccountItem {
|
||||
{Name: "ID", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
|
||||
{Name: "Name", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
|
||||
{Name: "Display name", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
|
||||
{Name: "First name", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
|
||||
{Name: "Last name", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
|
||||
{Name: "Avatar", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
|
||||
{Name: "User type", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
|
||||
{Name: "Password", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
@@ -67,6 +69,7 @@ func getBuiltInAccountItems() []*AccountItem {
|
||||
{Name: "Title", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
|
||||
{Name: "ID card type", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
|
||||
{Name: "ID card", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
|
||||
{Name: "ID card info", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "Real name", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
|
||||
{Name: "ID verification", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "Homepage", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
|
||||
@@ -87,18 +90,25 @@ func getBuiltInAccountItems() []*AccountItem {
|
||||
{Name: "Signup application", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
|
||||
{Name: "Register type", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
|
||||
{Name: "Register source", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
|
||||
{Name: "API key", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "Roles", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
|
||||
{Name: "Permissions", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
|
||||
{Name: "Groups", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
|
||||
{Name: "Consents", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "3rd-party logins", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "Properties", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
|
||||
{Name: "Is admin", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
|
||||
{Name: "Is forbidden", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
|
||||
{Name: "Is deleted", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
|
||||
{Name: "Multi-factor authentication", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "MFA items", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "WebAuthn credentials", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "Last change password time", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
|
||||
{Name: "Managed accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "Face ID", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "MFA accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "Need update password", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
|
||||
{Name: "IP whitelist", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,8 @@ type InitData struct {
|
||||
Sessions []*Session `json:"sessions"`
|
||||
Subscriptions []*Subscription `json:"subscriptions"`
|
||||
Transactions []*Transaction `json:"transactions"`
|
||||
Sites []*Site `json:"sites"`
|
||||
Rules []*Rule `json:"rules"`
|
||||
|
||||
EnforcerPolicies map[string][][]string `json:"enforcerPolicies"`
|
||||
}
|
||||
@@ -142,6 +144,12 @@ func InitFromFile() {
|
||||
for _, transaction := range initData.Transactions {
|
||||
initDefinedTransaction(transaction)
|
||||
}
|
||||
for _, rule := range initData.Rules {
|
||||
initDefinedRule(rule)
|
||||
}
|
||||
for _, site := range initData.Sites {
|
||||
initDefinedSite(site)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,6 +186,8 @@ func readInitDataFromFile(filePath string) (*InitData, error) {
|
||||
Sessions: []*Session{},
|
||||
Subscriptions: []*Subscription{},
|
||||
Transactions: []*Transaction{},
|
||||
Sites: []*Site{},
|
||||
Rules: []*Rule{},
|
||||
|
||||
EnforcerPolicies: map[string][][]string{},
|
||||
}
|
||||
@@ -877,3 +887,51 @@ func initDefinedTransaction(transaction *Transaction) {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func initDefinedSite(site *Site) {
|
||||
existed, err := getSite(site.Owner, site.Name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if existed != nil {
|
||||
if initDataNewOnly {
|
||||
return
|
||||
}
|
||||
affected, err := DeleteSite(site)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if !affected {
|
||||
panic("Fail to delete site")
|
||||
}
|
||||
}
|
||||
site.CreatedTime = util.GetCurrentTime()
|
||||
_, err = AddSite(site)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func initDefinedRule(rule *Rule) {
|
||||
existed, err := getRule(rule.Owner, rule.Name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if existed != nil {
|
||||
if initDataNewOnly {
|
||||
return
|
||||
}
|
||||
affected, err := DeleteRule(rule)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if !affected {
|
||||
panic("Fail to delete rule")
|
||||
}
|
||||
}
|
||||
rule.CreatedTime = util.GetCurrentTime()
|
||||
_, err = AddRule(rule)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,12 @@ func InitFlag() {
|
||||
configPath = *configPathPtr
|
||||
exportData = *exportDataPtr
|
||||
exportFilePath = *exportFilePathPtr
|
||||
|
||||
// Load beego config from the specified config path
|
||||
err := web.LoadAppConfig("ini", configPath)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to load config from %s: %v", configPath, err))
|
||||
}
|
||||
}
|
||||
|
||||
func ShouldExportData() bool {
|
||||
@@ -453,4 +459,14 @@ func (a *Ormer) createTable() {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = a.Engine.Sync2(new(Site))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = a.Engine.Sync2(new(Rule))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ type Payment struct {
|
||||
// Product Info
|
||||
Products []string `xorm:"varchar(1000)" json:"products"`
|
||||
ProductsDisplayName string `xorm:"varchar(1000)" json:"productsDisplayName"`
|
||||
ProductName string `xorm:"varchar(1000)" json:"productName"`
|
||||
ProductDisplayName string `xorm:"varchar(1000)" json:"productDisplayName"`
|
||||
Detail string `xorm:"varchar(255)" json:"detail"`
|
||||
Currency string `xorm:"varchar(100)" json:"currency"`
|
||||
Price float64 `json:"price"`
|
||||
|
||||
@@ -564,7 +564,7 @@ func providerChangeTrigger(oldName string, newName string) error {
|
||||
return session.Commit()
|
||||
}
|
||||
|
||||
func FromProviderToIdpInfo(ctx *context.Context, provider *Provider) *idp.ProviderInfo {
|
||||
func FromProviderToIdpInfo(ctx *context.Context, provider *Provider) (*idp.ProviderInfo, error) {
|
||||
providerInfo := &idp.ProviderInfo{
|
||||
Type: provider.Type,
|
||||
SubType: provider.SubType,
|
||||
@@ -588,9 +588,19 @@ func FromProviderToIdpInfo(ctx *context.Context, provider *Provider) *idp.Provid
|
||||
}
|
||||
} else if provider.Type == "ADFS" || provider.Type == "AzureAD" || provider.Type == "AzureADB2C" || provider.Type == "Casdoor" || provider.Type == "Okta" {
|
||||
providerInfo.HostUrl = provider.Domain
|
||||
} else if provider.Type == "Alipay" && provider.Cert != "" {
|
||||
// For Alipay with certificate mode, load private key from certificate
|
||||
cert, err := GetCert(util.GetId(provider.Owner, provider.Cert))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load certificate for Alipay provider %s: %w", provider.Name, err)
|
||||
}
|
||||
if cert == nil {
|
||||
return nil, fmt.Errorf("certificate not found for Alipay provider %s", provider.Name)
|
||||
}
|
||||
providerInfo.ClientSecret = cert.PrivateKey
|
||||
}
|
||||
|
||||
return providerInfo
|
||||
return providerInfo, nil
|
||||
}
|
||||
|
||||
func GetIdvProviderFromProvider(provider *Provider) idv.IdvProvider {
|
||||
|
||||
139
object/rule.go
Normal file
139
object/rule.go
Normal file
@@ -0,0 +1,139 @@
|
||||
// Copyright 2023 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"
|
||||
"github.com/xorm-io/core"
|
||||
)
|
||||
|
||||
type Expression struct {
|
||||
Name string `json:"name"`
|
||||
Operator string `json:"operator"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type Rule struct {
|
||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
CreatedTime string `xorm:"varchar(100) notnull" json:"createdTime"`
|
||||
UpdatedTime string `xorm:"varchar(100) notnull" json:"updatedTime"`
|
||||
|
||||
Type string `xorm:"varchar(100) notnull" json:"type"`
|
||||
Expressions []*Expression `xorm:"mediumtext" json:"expressions"`
|
||||
Action string `xorm:"varchar(100) notnull" json:"action"`
|
||||
StatusCode int `xorm:"int notnull" json:"statusCode"`
|
||||
Reason string `xorm:"varchar(100) notnull" json:"reason"`
|
||||
IsVerbose bool `xorm:"bool" json:"isVerbose"`
|
||||
}
|
||||
|
||||
func GetGlobalRules() ([]*Rule, error) {
|
||||
rules := []*Rule{}
|
||||
err := ormer.Engine.Asc("owner").Desc("created_time").Find(&rules)
|
||||
return rules, err
|
||||
}
|
||||
|
||||
func GetRules(owner string) ([]*Rule, error) {
|
||||
rules := []*Rule{}
|
||||
err := ormer.Engine.Desc("updated_time").Find(&rules, &Rule{Owner: owner})
|
||||
return rules, err
|
||||
}
|
||||
|
||||
func getRule(owner string, name string) (*Rule, error) {
|
||||
rule := Rule{Owner: owner, Name: name}
|
||||
existed, err := ormer.Engine.Get(&rule)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existed {
|
||||
return &rule, nil
|
||||
} else {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetRule(id string) (*Rule, error) {
|
||||
owner, name := util.GetOwnerAndNameFromIdNoCheck(id)
|
||||
return getRule(owner, name)
|
||||
}
|
||||
|
||||
func UpdateRule(id string, rule *Rule) (bool, error) {
|
||||
owner, name := util.GetOwnerAndNameFromIdNoCheck(id)
|
||||
if s, err := getRule(owner, name); err != nil {
|
||||
return false, err
|
||||
} else if s == nil {
|
||||
return false, nil
|
||||
}
|
||||
rule.UpdatedTime = util.GetCurrentTime()
|
||||
_, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(rule)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
err = refreshRuleMap()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func AddRule(rule *Rule) (bool, error) {
|
||||
affected, err := ormer.Engine.Insert(rule)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if affected != 0 {
|
||||
err = refreshRuleMap()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func DeleteRule(rule *Rule) (bool, error) {
|
||||
affected, err := ormer.Engine.ID(core.PK{rule.Owner, rule.Name}).Delete(&Rule{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if affected != 0 {
|
||||
err = refreshRuleMap()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func (rule *Rule) GetId() string {
|
||||
return fmt.Sprintf("%s/%s", rule.Owner, rule.Name)
|
||||
}
|
||||
|
||||
func GetRuleCount(owner, field, value string) (int64, error) {
|
||||
session := GetSession(owner, -1, -1, field, value, "", "")
|
||||
return session.Count(&Rule{})
|
||||
}
|
||||
|
||||
func GetPaginationRules(owner string, offset, limit int, field, value, sortField, sortOrder string) ([]*Rule, error) {
|
||||
rules := []*Rule{}
|
||||
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
|
||||
err := session.Where("owner = ? or owner = ?", "admin", owner).Find(&rules)
|
||||
if err != nil {
|
||||
return rules, err
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
57
object/rule_cache.go
Normal file
57
object/rule_cache.go
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright 2023 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"
|
||||
)
|
||||
|
||||
var ruleMap = map[string]*Rule{}
|
||||
|
||||
func InitRuleMap() {
|
||||
err := refreshRuleMap()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func refreshRuleMap() error {
|
||||
newRuleMap := map[string]*Rule{}
|
||||
rules, err := GetGlobalRules()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, rule := range rules {
|
||||
newRuleMap[util.GetId(rule.Owner, rule.Name)] = rule
|
||||
}
|
||||
|
||||
ruleMap = newRuleMap
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetRulesByRuleIds(ids []string) ([]*Rule, error) {
|
||||
var res []*Rule
|
||||
for _, id := range ids {
|
||||
rule, ok := ruleMap[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("rule: %s not found", id)
|
||||
}
|
||||
res = append(res, rule)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
276
object/site.go
Normal file
276
object/site.go
Normal file
@@ -0,0 +1,276 @@
|
||||
// Copyright 2023 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"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/xorm-io/core"
|
||||
)
|
||||
|
||||
type NodeItem struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Diff string `json:"diff"`
|
||||
Pid int `json:"pid"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Provider string `json:"provider"`
|
||||
}
|
||||
|
||||
type Site struct {
|
||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
|
||||
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
|
||||
Tag string `xorm:"varchar(100)" json:"tag"`
|
||||
Domain string `xorm:"varchar(100)" json:"domain"`
|
||||
OtherDomains []string `xorm:"varchar(500)" json:"otherDomains"`
|
||||
NeedRedirect bool `json:"needRedirect"`
|
||||
DisableVerbose bool `json:"disableVerbose"`
|
||||
Rules []string `xorm:"varchar(500)" json:"rules"`
|
||||
EnableAlert bool `json:"enableAlert"`
|
||||
AlertInterval int `json:"alertInterval"`
|
||||
AlertTryTimes int `json:"alertTryTimes"`
|
||||
AlertProviders []string `xorm:"varchar(500)" json:"alertProviders"`
|
||||
Challenges []string `xorm:"mediumtext" json:"challenges"`
|
||||
Host string `xorm:"varchar(100)" json:"host"`
|
||||
Port int `json:"port"`
|
||||
Hosts []string `xorm:"varchar(1000)" json:"hosts"`
|
||||
SslMode string `xorm:"varchar(100)" json:"sslMode"`
|
||||
SslCert string `xorm:"-" json:"sslCert"`
|
||||
PublicIp string `xorm:"varchar(100)" json:"publicIp"`
|
||||
Node string `xorm:"varchar(100)" json:"node"`
|
||||
IsSelf bool `json:"isSelf"`
|
||||
Status string `xorm:"varchar(100)" json:"status"`
|
||||
Nodes []*NodeItem `xorm:"mediumtext" json:"nodes"`
|
||||
|
||||
CasdoorApplication string `xorm:"varchar(100)" json:"casdoorApplication"`
|
||||
ApplicationObj *Application `xorm:"-" json:"applicationObj"`
|
||||
}
|
||||
|
||||
func GetGlobalSites() ([]*Site, error) {
|
||||
sites := []*Site{}
|
||||
err := ormer.Engine.Desc("created_time").Find(&sites)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sites, nil
|
||||
}
|
||||
|
||||
func GetSites(owner string) ([]*Site, error) {
|
||||
sites := []*Site{}
|
||||
err := ormer.Engine.Asc("tag").Asc("port").Desc("created_time").Find(&sites, &Site{Owner: owner})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, site := range sites {
|
||||
err = site.populateCert()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return sites, nil
|
||||
}
|
||||
|
||||
func getSite(owner string, name string) (*Site, error) {
|
||||
site := Site{Owner: owner, Name: name}
|
||||
existed, err := ormer.Engine.Get(&site)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if existed {
|
||||
return &site, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func GetSite(id string) (*Site, error) {
|
||||
owner, name := util.GetOwnerAndNameFromIdNoCheck(id)
|
||||
site, err := getSite(owner, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if site != nil {
|
||||
err = site.populateCert()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return site, nil
|
||||
}
|
||||
|
||||
func GetMaskedSite(site *Site, node string) *Site {
|
||||
if site == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if site.PublicIp == "(empty)" {
|
||||
site.PublicIp = ""
|
||||
}
|
||||
|
||||
site.IsSelf = false
|
||||
if site.Node == node {
|
||||
site.IsSelf = true
|
||||
}
|
||||
|
||||
return site
|
||||
}
|
||||
|
||||
func GetMaskedSites(sites []*Site, node string) []*Site {
|
||||
for _, site := range sites {
|
||||
site = GetMaskedSite(site, node)
|
||||
}
|
||||
return sites
|
||||
}
|
||||
|
||||
func UpdateSite(id string, site *Site) (bool, error) {
|
||||
owner, name := util.GetOwnerAndNameFromIdNoCheck(id)
|
||||
if s, err := getSite(owner, name); err != nil {
|
||||
return false, err
|
||||
} else if s == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
site.UpdatedTime = util.GetCurrentTime()
|
||||
|
||||
_, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(site)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = refreshSiteMap()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func UpdateSiteNoRefresh(id string, site *Site) (bool, error) {
|
||||
owner, name := util.GetOwnerAndNameFromIdNoCheck(id)
|
||||
if s, err := getSite(owner, name); err != nil {
|
||||
return false, err
|
||||
} else if s == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
_, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(site)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func AddSite(site *Site) (bool, error) {
|
||||
affected, err := ormer.Engine.Insert(site)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if affected != 0 {
|
||||
err = refreshSiteMap()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func DeleteSite(site *Site) (bool, error) {
|
||||
affected, err := ormer.Engine.ID(core.PK{site.Owner, site.Name}).Delete(&Site{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if affected != 0 {
|
||||
err = refreshSiteMap()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func (site *Site) GetId() string {
|
||||
return fmt.Sprintf("%s/%s", site.Owner, site.Name)
|
||||
}
|
||||
|
||||
func (site *Site) GetChallengeMap() map[string]string {
|
||||
m := map[string]string{}
|
||||
for _, challenge := range site.Challenges {
|
||||
tokens := strings.Split(challenge, ":")
|
||||
m[tokens[0]] = tokens[1]
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (site *Site) GetHost() string {
|
||||
if len(site.Hosts) != 0 {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
return site.Hosts[rand.Intn(len(site.Hosts))]
|
||||
}
|
||||
|
||||
if site.Host != "" {
|
||||
return site.Host
|
||||
}
|
||||
|
||||
if site.Port == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
res := fmt.Sprintf("http://localhost:%d", site.Port)
|
||||
return res
|
||||
}
|
||||
|
||||
func addErrorToMsg(msg string, function string, err error) string {
|
||||
fmt.Printf("%s(): %s\n", function, err.Error())
|
||||
if msg == "" {
|
||||
return fmt.Sprintf("%s(): %s", function, err.Error())
|
||||
} else {
|
||||
return fmt.Sprintf("%s || %s(): %s", msg, function, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func GetSiteCount(owner, field, value string) (int64, error) {
|
||||
session := GetSession(owner, -1, -1, field, value, "", "")
|
||||
return session.Count(&Site{})
|
||||
}
|
||||
|
||||
func GetPaginationSites(owner string, offset, limit int, field, value, sortField, sortOrder string) ([]*Site, error) {
|
||||
sites := []*Site{}
|
||||
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
|
||||
err := session.Where("owner = ? or owner = ?", "admin", owner).Find(&sites)
|
||||
if err != nil {
|
||||
return sites, err
|
||||
}
|
||||
|
||||
return sites, nil
|
||||
}
|
||||
133
object/site_cache.go
Normal file
133
object/site_cache.go
Normal file
@@ -0,0 +1,133 @@
|
||||
// Copyright 2023 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"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
var (
|
||||
SiteMap = map[string]*Site{}
|
||||
certMap = map[string]*Cert{}
|
||||
healthCheckNeededDomains []string
|
||||
)
|
||||
|
||||
func InitSiteMap() {
|
||||
err := refreshSiteMap()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func getCasdoorCertMap() (map[string]*Cert, error) {
|
||||
certs, err := GetCerts("")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetCerts() error: %s", err.Error())
|
||||
}
|
||||
|
||||
res := map[string]*Cert{}
|
||||
for _, cert := range certs {
|
||||
res[cert.Name] = cert
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func getCasdoorApplicationMap() (map[string]*Application, error) {
|
||||
casdoorCertMap, err := getCasdoorCertMap()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
applications, err := GetApplications("")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetOrganizationApplications() error: %s", err.Error())
|
||||
}
|
||||
|
||||
res := map[string]*Application{}
|
||||
for _, application := range applications {
|
||||
if application.Cert != "" {
|
||||
if cert, ok := casdoorCertMap[application.Cert]; ok {
|
||||
application.CertObj = cert
|
||||
}
|
||||
}
|
||||
|
||||
res[application.Name] = application
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func refreshSiteMap() error {
|
||||
applicationMap, err := getCasdoorApplicationMap()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
newSiteMap := map[string]*Site{}
|
||||
newHealthCheckNeededDomains := make([]string, 0)
|
||||
sites, err := GetGlobalSites()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
certMap, err = getCertMap()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, site := range sites {
|
||||
if applicationMap != nil {
|
||||
if site.CasdoorApplication != "" && site.ApplicationObj == nil {
|
||||
if v, ok2 := applicationMap[site.CasdoorApplication]; ok2 {
|
||||
site.ApplicationObj = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if site.Domain != "" && site.PublicIp == "" {
|
||||
go func(site *Site) {
|
||||
site.PublicIp = util.ResolveDomainToIp(site.Domain)
|
||||
_, err2 := UpdateSiteNoRefresh(site.GetId(), site)
|
||||
if err2 != nil {
|
||||
fmt.Printf("UpdateSiteNoRefresh() error: %v\n", err2)
|
||||
}
|
||||
}(site)
|
||||
}
|
||||
|
||||
newSiteMap[strings.ToLower(site.Domain)] = site
|
||||
if !shouldStopHealthCheck(site) {
|
||||
newHealthCheckNeededDomains = append(newHealthCheckNeededDomains, strings.ToLower(site.Domain))
|
||||
}
|
||||
for _, domain := range site.OtherDomains {
|
||||
if domain != "" {
|
||||
newSiteMap[strings.ToLower(domain)] = site
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SiteMap = newSiteMap
|
||||
healthCheckNeededDomains = newHealthCheckNeededDomains
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetSiteByDomain(domain string) *Site {
|
||||
if site, ok := SiteMap[strings.ToLower(domain)]; ok {
|
||||
return site
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
215
object/site_cert.go
Normal file
215
object/site_cert.go
Normal file
@@ -0,0 +1,215 @@
|
||||
// Copyright 2023 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"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
func (site *Site) populateCert() error {
|
||||
if site.Domain == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
cert, err := GetCertByDomain(site.Domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cert == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
site.SslCert = cert.Name
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkUrlToken(url string, keyAuth string) (bool, error) {
|
||||
client := &http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if strings.TrimSpace(string(body)) == keyAuth {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("checkUrlToken() error, response mismatch: expected %q, got %q", keyAuth, body)
|
||||
}
|
||||
|
||||
func (site *Site) preCheckCertForDomain(domain string) (bool, error) {
|
||||
token, keyAuth, err := util.GenerateTwoUniqueRandomStrings()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
site.Challenges = []string{fmt.Sprintf("%s:%s", token, keyAuth)}
|
||||
|
||||
_, err = UpdateSiteNoRefresh(site.GetId(), site)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = refreshSiteMap()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("http://%s/.well-known/acme-challenge/%s", domain, token)
|
||||
var ok bool
|
||||
for i := 0; i < 10; i++ {
|
||||
fmt.Printf("checkUrlToken(): try time: %d\n", i+1)
|
||||
ok, err = checkUrlToken(url, keyAuth)
|
||||
if err != nil {
|
||||
fmt.Printf("preCheckCertForDomain() error: %v\n", err)
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
if ok {
|
||||
fmt.Printf("checkUrlToken(): try time: %d, succeed!\n", i+1)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
site.Challenges = []string{}
|
||||
_, err = UpdateSiteNoRefresh(site.GetId(), site)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = refreshSiteMap()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
func (site *Site) updateCertForDomain(domain string) error {
|
||||
ok, err := site.preCheckCertForDomain(domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
fmt.Printf("preCheckCertForDomain(): not ok for domain: %s\n", domain)
|
||||
return nil
|
||||
}
|
||||
|
||||
certificate, privateKey, err := getHttp01Cert(site.GetId(), domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expireTime, err := util.GetCertExpireTime(certificate)
|
||||
if err != nil {
|
||||
fmt.Printf("getCertExpireTime() error: %v\n", err)
|
||||
}
|
||||
|
||||
domainExpireTime, err := getDomainExpireTime(domain)
|
||||
if err != nil {
|
||||
fmt.Printf("getDomainExpireTime() error: %v\n", err)
|
||||
}
|
||||
|
||||
cert := Cert{
|
||||
Owner: site.Owner,
|
||||
Name: domain,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
DisplayName: domain,
|
||||
Type: "SSL",
|
||||
CryptoAlgorithm: "RSA",
|
||||
ExpireTime: expireTime,
|
||||
DomainExpireTime: domainExpireTime,
|
||||
Provider: "",
|
||||
Account: "",
|
||||
AccessKey: "",
|
||||
AccessSecret: "",
|
||||
Certificate: certificate,
|
||||
PrivateKey: privateKey,
|
||||
}
|
||||
|
||||
_, err = DeleteCert(&cert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = AddCert(&cert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = refreshSiteMap()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (site *Site) checkCerts() error {
|
||||
domains := []string{}
|
||||
if site.Domain != "" {
|
||||
domains = append(domains, site.Domain)
|
||||
}
|
||||
|
||||
for _, domain := range site.OtherDomains {
|
||||
domains = append(domains, domain)
|
||||
}
|
||||
|
||||
for _, domain := range domains {
|
||||
if site.Owner == "admin" || strings.HasSuffix(domain, ".casdoor.com") {
|
||||
continue
|
||||
}
|
||||
|
||||
cert, err := GetCertByDomain(domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cert != nil {
|
||||
var nearExpire bool
|
||||
nearExpire, err = cert.isCertNearExpire()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !nearExpire {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
err = site.updateCertForDomain(domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
103
object/site_cert_account.go
Normal file
103
object/site_cert_account.go
Normal file
@@ -0,0 +1,103 @@
|
||||
// Copyright 2023 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/casbin/lego/v4/acme"
|
||||
"github.com/casbin/lego/v4/certcrypto"
|
||||
"github.com/casbin/lego/v4/lego"
|
||||
"github.com/casbin/lego/v4/registration"
|
||||
"github.com/casdoor/casdoor/certificate"
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/proxy"
|
||||
)
|
||||
|
||||
func getLegoClientAndAccount(email string, privateKey string, devMode bool, useProxy bool) (*lego.Client, *certificate.Account, error) {
|
||||
eccKey, err := decodeEccKey(privateKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
account := &certificate.Account{
|
||||
Email: email,
|
||||
Key: eccKey,
|
||||
}
|
||||
|
||||
config := lego.NewConfig(account)
|
||||
if devMode {
|
||||
config.CADirURL = lego.LEDirectoryStaging
|
||||
} else {
|
||||
config.CADirURL = lego.LEDirectoryProduction
|
||||
}
|
||||
|
||||
config.Certificate.KeyType = certcrypto.RSA2048
|
||||
|
||||
if useProxy {
|
||||
config.HTTPClient = proxy.ProxyHttpClient
|
||||
} else {
|
||||
config.HTTPClient = proxy.DefaultHttpClient
|
||||
}
|
||||
|
||||
client, err := lego.NewClient(config)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return client, account, nil
|
||||
}
|
||||
|
||||
func getAcmeClient(email string, privateKey string, devMode bool, useProxy bool) (*lego.Client, error) {
|
||||
// Create a user. New accounts need an email and private key to start.
|
||||
client, account, err := getLegoClientAndAccount(email, privateKey, devMode, useProxy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// try to obtain an account based on the private key
|
||||
account.Registration, err = client.Registration.ResolveAccountByKey()
|
||||
if err != nil {
|
||||
acmeError, ok := err.(*acme.ProblemDetails)
|
||||
if !ok {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if acmeError.Type != "urn:ietf:params:acme:error:accountDoesNotExist" {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Failed to get account, so create an account based on the private key.
|
||||
account.Registration, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func GetAcmeClient(useProxy bool) (*lego.Client, error) {
|
||||
acmeEmail := conf.GetConfigString("acmeEmail")
|
||||
acmePrivateKey := conf.GetConfigString("acmePrivateKey")
|
||||
if acmeEmail == "" {
|
||||
return nil, fmt.Errorf("acmeEmail should not be empty")
|
||||
}
|
||||
if acmePrivateKey == "" {
|
||||
return nil, fmt.Errorf("acmePrivateKey should not be empty")
|
||||
}
|
||||
|
||||
return getAcmeClient(acmeEmail, acmePrivateKey, false, useProxy)
|
||||
}
|
||||
59
object/site_cert_ecc.go
Normal file
59
object/site_cert_ecc.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright 2023 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 (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// generateEccKey generates a public and private key pair.(NIST P-256)
|
||||
func generateEccKey() *ecdsa.PrivateKey {
|
||||
privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
return privateKey
|
||||
}
|
||||
|
||||
// encodeEccKey Return the input private key object as string type private key
|
||||
func encodeEccKey(privateKey *ecdsa.PrivateKey) string {
|
||||
x509Encoded, err := x509.MarshalECPrivateKey(privateKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
pemEncoded := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: x509Encoded})
|
||||
return string(pemEncoded)
|
||||
}
|
||||
|
||||
// decodeEccKey Return the entered private key string as a private key object that can be used
|
||||
func decodeEccKey(pemEncoded string) (*ecdsa.PrivateKey, error) {
|
||||
pemEncoded = strings.ReplaceAll(pemEncoded, "\\n", "\n")
|
||||
block, _ := pem.Decode([]byte(pemEncoded))
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("decodeEccKey() error, block should not be nil")
|
||||
}
|
||||
|
||||
x509Encoded := block.Bytes
|
||||
privateKey, err := x509.ParseECPrivateKey(x509Encoded)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return privateKey, nil
|
||||
}
|
||||
90
object/site_cert_http.go
Normal file
90
object/site_cert_http.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// Copyright 2023 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/casbin/lego/v4/certificate"
|
||||
)
|
||||
|
||||
type HttpProvider struct {
|
||||
siteId string
|
||||
}
|
||||
|
||||
func (p *HttpProvider) Present(domain string, token string, keyAuth string) error {
|
||||
site, err := GetSite(p.siteId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
site.Challenges = []string{fmt.Sprintf("%s:%s", token, keyAuth)}
|
||||
_, err = UpdateSiteNoRefresh(site.GetId(), site)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = refreshSiteMap()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *HttpProvider) CleanUp(domain string, token string, keyAuth string) error {
|
||||
site, err := GetSite(p.siteId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
site.Challenges = []string{}
|
||||
_, err = UpdateSiteNoRefresh(site.GetId(), site)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = refreshSiteMap()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getHttp01Cert(siteId string, domain string) (string, string, error) {
|
||||
client, err := GetAcmeClient(false)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
provider := HttpProvider{siteId: siteId}
|
||||
err = client.Challenge.SetHTTP01Provider(&provider)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
request := certificate.ObtainRequest{
|
||||
Domains: []string{domain},
|
||||
Bundle: true,
|
||||
}
|
||||
|
||||
resource, err := client.Certificate.Obtain(request)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return string(resource.Certificate), string(resource.PrivateKey), nil
|
||||
}
|
||||
87
object/site_timer.go
Normal file
87
object/site_timer.go
Normal file
@@ -0,0 +1,87 @@
|
||||
// Copyright 2023 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"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
var (
|
||||
siteUpdateMap = map[string]string{}
|
||||
lock = &sync.Mutex{}
|
||||
)
|
||||
|
||||
func monitorSiteCerts() error {
|
||||
sites, err := GetGlobalSites()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, site := range sites {
|
||||
//updatedTime, ok := siteUpdateMap[site.GetId()]
|
||||
//if ok && updatedTime != "" && updatedTime == site.UpdatedTime {
|
||||
// continue
|
||||
//}
|
||||
|
||||
lock.Lock()
|
||||
err = site.checkCerts()
|
||||
lock.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
siteUpdateMap[site.GetId()] = site.UpdatedTime
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func StartMonitorSitesLoop() {
|
||||
fmt.Printf("StartMonitorSitesLoop() Start!\n\n")
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Printf("[%s] Recovered from StartMonitorSitesLoop() panic: %v\n", util.GetCurrentTime(), r)
|
||||
StartMonitorSitesLoop()
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
err := refreshSiteMap()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
|
||||
err = refreshRuleMap()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
|
||||
err = monitorSiteCerts()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
}()
|
||||
}
|
||||
21
object/site_timer_health.go
Normal file
21
object/site_timer_health.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright 2023 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
|
||||
|
||||
var healthCheckTryTimesMap = map[string]int{}
|
||||
|
||||
func shouldStopHealthCheck(site *Site) bool {
|
||||
return site == nil || !site.EnableAlert || site.Domain == "" || site.Status == "Inactive"
|
||||
}
|
||||
@@ -15,8 +15,10 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
sender "github.com/casdoor/go-sms-sender"
|
||||
)
|
||||
|
||||
@@ -61,6 +63,13 @@ func SendSms(provider *Provider, content string, phoneNumbers ...string) error {
|
||||
params["0"] = content
|
||||
} else {
|
||||
params["code"] = content
|
||||
if provider.Type == "Alibaba Cloud PNVS SMS" {
|
||||
timeoutInMinutes, err := conf.GetConfigInt64("verificationCodeTimeout")
|
||||
if err != nil || timeoutInMinutes <= 0 {
|
||||
timeoutInMinutes = 10
|
||||
}
|
||||
params["min"] = strconv.FormatInt(timeoutInMinutes, 10)
|
||||
}
|
||||
}
|
||||
|
||||
err = client.SendMessage(params, phoneNumbers...)
|
||||
|
||||
@@ -406,27 +406,61 @@ func (p *DingtalkSyncerProvider) getDingtalkUsers() ([]*OriginalUser, error) {
|
||||
return originalUsers, nil
|
||||
}
|
||||
|
||||
// getDingtalkUserFieldValue extracts a field value from DingtalkUser by field name
|
||||
func (p *DingtalkSyncerProvider) getDingtalkUserFieldValue(dingtalkUser *DingtalkUser, fieldName string) string {
|
||||
switch fieldName {
|
||||
case "userid":
|
||||
return dingtalkUser.UserId
|
||||
case "unionid":
|
||||
return dingtalkUser.UnionId
|
||||
case "name":
|
||||
return dingtalkUser.Name
|
||||
case "email":
|
||||
return dingtalkUser.Email
|
||||
case "mobile":
|
||||
return dingtalkUser.Mobile
|
||||
case "avatar":
|
||||
return dingtalkUser.Avatar
|
||||
case "title":
|
||||
return dingtalkUser.Position
|
||||
case "job_number":
|
||||
return dingtalkUser.JobNumber
|
||||
case "active":
|
||||
// Invert the boolean because active=true means NOT forbidden
|
||||
return util.BoolToString(!dingtalkUser.Active)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// dingtalkUserToOriginalUser converts DingTalk user to Casdoor OriginalUser
|
||||
func (p *DingtalkSyncerProvider) dingtalkUserToOriginalUser(dingtalkUser *DingtalkUser) *OriginalUser {
|
||||
// Use unionid as name to be consistent with OAuth provider
|
||||
// Fallback to userId if unionid is not available
|
||||
userName := dingtalkUser.UserId
|
||||
if dingtalkUser.UnionId != "" {
|
||||
userName = dingtalkUser.UnionId
|
||||
user := &OriginalUser{
|
||||
Address: []string{},
|
||||
Properties: map[string]string{},
|
||||
Groups: []string{},
|
||||
DingTalk: dingtalkUser.UserId, // Link DingTalk provider account
|
||||
}
|
||||
|
||||
user := &OriginalUser{
|
||||
Id: dingtalkUser.UserId,
|
||||
Name: userName,
|
||||
DisplayName: dingtalkUser.Name,
|
||||
Email: dingtalkUser.Email,
|
||||
Phone: dingtalkUser.Mobile,
|
||||
Avatar: dingtalkUser.Avatar,
|
||||
Title: dingtalkUser.Position,
|
||||
Address: []string{},
|
||||
Properties: map[string]string{},
|
||||
Groups: []string{},
|
||||
DingTalk: dingtalkUser.UserId, // Link DingTalk provider account
|
||||
// Apply TableColumns mapping if configured
|
||||
if len(p.Syncer.TableColumns) > 0 {
|
||||
for _, tableColumn := range p.Syncer.TableColumns {
|
||||
value := p.getDingtalkUserFieldValue(dingtalkUser, tableColumn.Name)
|
||||
p.Syncer.setUserByKeyValue(user, tableColumn.CasdoorName, value)
|
||||
}
|
||||
} else {
|
||||
// Fallback to default mapping for backward compatibility
|
||||
user.Id = dingtalkUser.UserId
|
||||
user.Name = dingtalkUser.UserId
|
||||
if dingtalkUser.UnionId != "" {
|
||||
user.Name = dingtalkUser.UnionId
|
||||
}
|
||||
user.DisplayName = dingtalkUser.Name
|
||||
user.Email = dingtalkUser.Email
|
||||
user.Phone = dingtalkUser.Mobile
|
||||
user.Avatar = dingtalkUser.Avatar
|
||||
user.Title = dingtalkUser.Position
|
||||
user.IsForbidden = !dingtalkUser.Active
|
||||
}
|
||||
|
||||
// Add department IDs to Groups field
|
||||
@@ -434,9 +468,6 @@ func (p *DingtalkSyncerProvider) dingtalkUserToOriginalUser(dingtalkUser *Dingta
|
||||
user.Groups = append(user.Groups, fmt.Sprintf("%d", deptId))
|
||||
}
|
||||
|
||||
// Set IsForbidden based on active status (active=false means user is forbidden)
|
||||
user.IsForbidden = !dingtalkUser.Active
|
||||
|
||||
// Set CreatedTime to current time if not set
|
||||
if user.CreatedTime == "" {
|
||||
user.CreatedTime = util.GetCurrentTime()
|
||||
|
||||
@@ -71,6 +71,19 @@ func (syncer *Syncer) updateUserForOriginalFields(user *User, key string) (bool,
|
||||
columns := syncer.getCasdoorColumns()
|
||||
columns = append(columns, "affiliation", "hash", "pre_hash")
|
||||
|
||||
// Skip password-related columns when the incoming user has no password data.
|
||||
// API-based syncers (DingTalk, WeCom, Lark, etc.) do not provide passwords,
|
||||
// so updating these columns would wipe out locally set passwords.
|
||||
if user.Password == "" {
|
||||
filtered := make([]string, 0, len(columns))
|
||||
for _, col := range columns {
|
||||
if col != "password" && col != "password_salt" && col != "password_type" {
|
||||
filtered = append(filtered, col)
|
||||
}
|
||||
}
|
||||
columns = filtered
|
||||
}
|
||||
|
||||
// Add provider-specific field for API-based syncers to enable login binding
|
||||
// This allows synced users to login via their provider accounts
|
||||
switch syncer.Type {
|
||||
|
||||
@@ -660,6 +660,15 @@ func generateJwtToken(application *Application, user *User, provider string, sig
|
||||
return tokenString, refreshTokenString, name, err
|
||||
}
|
||||
|
||||
func ParseJwtTokenWithoutValidation(token string) (*jwt.Token, error) {
|
||||
t, _, err := jwt.NewParser().ParseUnverified(token, &Claims{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func ParseJwtToken(token string, cert *Cert) (*Claims, error) {
|
||||
t, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
var (
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -154,6 +155,10 @@ func CheckOAuthLogin(clientId string, responseType string, redirectUri string, s
|
||||
return fmt.Sprintf(i18n.Translate(lang, "token:Redirect URI: %s doesn't exist in the allowed Redirect URI list"), redirectUri), application, nil
|
||||
}
|
||||
|
||||
if !IsScopeValid(scope, application) {
|
||||
return i18n.Translate(lang, "token:Invalid scope"), application, nil
|
||||
}
|
||||
|
||||
// Mask application for /api/get-app-login
|
||||
application.ClientSecret = ""
|
||||
return "", application, nil
|
||||
@@ -240,10 +245,33 @@ func GetOAuthCode(userId string, clientId string, provider string, signinMethod
|
||||
}, nil
|
||||
}
|
||||
|
||||
func GetOAuthToken(grantType string, clientId string, clientSecret string, code string, verifier string, scope string, nonce string, username string, password string, host string, refreshToken string, tag string, avatar string, lang string, subjectToken string, subjectTokenType string, audience string, resource string) (interface{}, error) {
|
||||
application, err := GetApplicationByClientId(clientId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func GetOAuthToken(grantType string, clientId string, clientSecret string, code string, verifier string, scope string, nonce string, username string, password string, host string, refreshToken string, tag string, avatar string, lang string, subjectToken string, subjectTokenType string, assertion string, clientAssertion string, clientAssertionType string, audience string, resource string) (interface{}, error) {
|
||||
var (
|
||||
application *Application
|
||||
err error
|
||||
ok bool
|
||||
)
|
||||
|
||||
if clientAssertionType == "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" {
|
||||
ok, application, err = ValidateClientAssertion(clientAssertion, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !ok || application == nil {
|
||||
return &TokenError{
|
||||
Error: InvalidClient,
|
||||
ErrorDescription: "client_assertion is invalid",
|
||||
}, nil
|
||||
}
|
||||
|
||||
clientSecret = application.ClientSecret
|
||||
clientId = application.ClientId
|
||||
} else {
|
||||
application, err = GetApplicationByClientId(clientId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if application == nil {
|
||||
@@ -273,12 +301,14 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
|
||||
token, tokenError, err = GetClientCredentialsToken(application, clientSecret, scope, host)
|
||||
case "token", "id_token": // Implicit Grant
|
||||
token, tokenError, err = GetImplicitToken(application, username, scope, nonce, host)
|
||||
case "urn:ietf:params:oauth:grant-type:jwt-bearer":
|
||||
token, tokenError, err = GetJwtBearerToken(application, assertion, scope, nonce, host)
|
||||
case "urn:ietf:params:oauth:grant-type:device_code":
|
||||
token, tokenError, err = GetImplicitToken(application, username, scope, nonce, host)
|
||||
case "urn:ietf:params:oauth:grant-type:token-exchange": // Token Exchange Grant (RFC 8693)
|
||||
token, tokenError, err = GetTokenExchangeToken(application, clientSecret, subjectToken, subjectTokenType, audience, scope, host)
|
||||
case "refresh_token":
|
||||
refreshToken2, err := RefreshToken(grantType, refreshToken, scope, clientId, clientSecret, host)
|
||||
refreshToken2, err := RefreshToken(application, grantType, refreshToken, scope, clientId, clientSecret, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -320,7 +350,7 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
|
||||
return tokenWrapper, nil
|
||||
}
|
||||
|
||||
func RefreshToken(grantType string, refreshToken string, scope string, clientId string, clientSecret string, host string) (interface{}, error) {
|
||||
func RefreshToken(application *Application, grantType string, refreshToken string, scope string, clientId string, clientSecret string, host string) (interface{}, error) {
|
||||
// check parameters
|
||||
if grantType != "refresh_token" {
|
||||
return &TokenError{
|
||||
@@ -328,16 +358,20 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
|
||||
ErrorDescription: "grant_type should be refresh_token",
|
||||
}, nil
|
||||
}
|
||||
application, err := GetApplicationByClientId(clientId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var err error
|
||||
if application == nil {
|
||||
return &TokenError{
|
||||
Error: InvalidClient,
|
||||
ErrorDescription: "client_id is invalid",
|
||||
}, nil
|
||||
application, err = GetApplicationByClientId(clientId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if application == nil {
|
||||
return &TokenError{
|
||||
Error: InvalidClient,
|
||||
ErrorDescription: "client_id is invalid",
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
if clientSecret != "" && application.ClientSecret != clientSecret {
|
||||
@@ -486,6 +520,28 @@ func IsGrantTypeValid(method string, grantTypes []string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsScopeValid checks whether all space-separated scopes in the scope string
|
||||
// are defined in the application's Scopes list.
|
||||
// If the application has no defined scopes, every scope is considered valid
|
||||
// (backward-compatible behaviour).
|
||||
func IsScopeValid(scope string, application *Application) bool {
|
||||
if len(application.Scopes) == 0 || scope == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
allowed := make(map[string]bool, len(application.Scopes))
|
||||
for _, s := range application.Scopes {
|
||||
allowed[s.Name] = true
|
||||
}
|
||||
|
||||
for _, s := range strings.Fields(scope) {
|
||||
if !allowed[s] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// createGuestUserToken creates a new guest user and returns a token for them
|
||||
func createGuestUserToken(application *Application, clientSecret string, verifier string) (*Token, *TokenError, error) {
|
||||
// Verify client secret if provided
|
||||
@@ -526,12 +582,19 @@ func createGuestUserToken(application *Application, clientSecret string, verifie
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Generate a unique user ID within the confines of the application
|
||||
newUserId, idErr := GenerateIdForNewUser(application)
|
||||
if idErr != nil {
|
||||
// If we fail to generate a unique user ID, we can fallback to a random ID
|
||||
newUserId = util.GenerateId()
|
||||
}
|
||||
|
||||
// Create the guest user
|
||||
guestUser := &User{
|
||||
Owner: application.Organization,
|
||||
Name: guestUsername,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
Id: util.GenerateId(),
|
||||
Id: newUserId,
|
||||
Type: "normal-user",
|
||||
Password: guestPassword,
|
||||
Tag: "guest-user",
|
||||
@@ -715,6 +778,13 @@ func GetAuthorizationCodeToken(application *Application, clientSecret string, co
|
||||
// GetPasswordToken
|
||||
// Resource Owner Password Credentials flow
|
||||
func GetPasswordToken(application *Application, username string, password string, scope string, host string) (*Token, *TokenError, error) {
|
||||
if !IsScopeValid(scope, application) {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidScope,
|
||||
ErrorDescription: "the requested scope is invalid or not defined in the application",
|
||||
}, nil
|
||||
}
|
||||
|
||||
user, err := GetUserByFields(application.Organization, username)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -796,6 +866,12 @@ func GetClientCredentialsToken(application *Application, clientSecret string, sc
|
||||
ErrorDescription: "client_secret is invalid",
|
||||
}, nil
|
||||
}
|
||||
if !IsScopeValid(scope, application) {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidScope,
|
||||
ErrorDescription: "the requested scope is invalid or not defined in the application",
|
||||
}, nil
|
||||
}
|
||||
nullUser := &User{
|
||||
Owner: application.Owner,
|
||||
Id: application.GetId(),
|
||||
@@ -835,6 +911,13 @@ func GetClientCredentialsToken(application *Application, clientSecret string, sc
|
||||
// GetImplicitToken
|
||||
// Implicit flow
|
||||
func GetImplicitToken(application *Application, username string, scope string, nonce string, host string) (*Token, *TokenError, error) {
|
||||
if !IsScopeValid(scope, application) {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidScope,
|
||||
ErrorDescription: "the requested scope is invalid or not defined in the application",
|
||||
}, nil
|
||||
}
|
||||
|
||||
user, err := GetUserByFields(application.Organization, username)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -859,6 +942,84 @@ func GetImplicitToken(application *Application, username string, scope string, n
|
||||
return token, nil, nil
|
||||
}
|
||||
|
||||
// GetJwtBearerToken
|
||||
// RFC 7523
|
||||
func GetJwtBearerToken(application *Application, assertion string, scope string, nonce string, host string) (*Token, *TokenError, error) {
|
||||
ok, claims, err := ValidateJwtAssertion(assertion, application, host)
|
||||
if err != nil || !ok {
|
||||
if err != nil {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: err.Error(),
|
||||
}, err
|
||||
}
|
||||
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: fmt.Sprintf("assertion (JWT) is invalid for application: [%s]", application.GetId()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return GetImplicitToken(application, claims.Subject, scope, nonce, host)
|
||||
}
|
||||
|
||||
func ValidateJwtAssertion(clientAssertion string, application *Application, host string) (bool, *Claims, error) {
|
||||
_, originBackend := getOriginFromHost(host)
|
||||
|
||||
clientCert, err := getCert(application.Owner, application.ClientCert)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
if clientCert == nil {
|
||||
return false, nil, fmt.Errorf("client certificate is not configured for application: [%s]", application.GetId())
|
||||
}
|
||||
|
||||
claims, err := ParseJwtToken(clientAssertion, clientCert)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
if !slices.Contains(application.RedirectUris, claims.Issuer) {
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
if !slices.Contains(claims.Audience, fmt.Sprintf("%s/api/login/oauth/access_token", originBackend)) {
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
return true, claims, nil
|
||||
}
|
||||
|
||||
func ValidateClientAssertion(clientAssertion string, host string) (bool, *Application, error) {
|
||||
token, err := ParseJwtTokenWithoutValidation(clientAssertion)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
clientId, err := token.Claims.GetSubject()
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
application, err := GetApplicationByClientId(clientId)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
if application == nil {
|
||||
return false, nil, fmt.Errorf("application not found for client: [%s]", clientId)
|
||||
}
|
||||
|
||||
ok, _, err := ValidateJwtAssertion(clientAssertion, application, host)
|
||||
if err != nil {
|
||||
return false, application, err
|
||||
}
|
||||
if !ok {
|
||||
return false, application, nil
|
||||
}
|
||||
|
||||
return true, application, nil
|
||||
}
|
||||
|
||||
// GetTokenByUser
|
||||
// Implicit flow
|
||||
func GetTokenByUser(application *Application, user *User, scope string, nonce string, host string) (*Token, error) {
|
||||
@@ -946,9 +1107,16 @@ func GetWechatMiniProgramToken(application *Application, code string, host strin
|
||||
name = fmt.Sprintf("wechat-%s", openId)
|
||||
}
|
||||
|
||||
// Generate a unique user ID within the confines of the application
|
||||
newUserId, idErr := GenerateIdForNewUser(application)
|
||||
if idErr != nil {
|
||||
// If we fail to generate a unique user ID, we can fallback to a random ID
|
||||
newUserId = util.GenerateId()
|
||||
}
|
||||
|
||||
user = &User{
|
||||
Owner: application.Organization,
|
||||
Id: util.GenerateId(),
|
||||
Id: newUserId,
|
||||
Name: name,
|
||||
Avatar: avatar,
|
||||
SignupApplication: application.Name,
|
||||
|
||||
@@ -180,6 +180,7 @@ type User struct {
|
||||
Spotify string `xorm:"spotify varchar(100)" json:"spotify"`
|
||||
Strava string `xorm:"strava varchar(100)" json:"strava"`
|
||||
Stripe string `xorm:"stripe varchar(100)" json:"stripe"`
|
||||
Telegram string `xorm:"telegram varchar(100)" json:"telegram"`
|
||||
TikTok string `xorm:"tiktok varchar(100)" json:"tiktok"`
|
||||
Tumblr string `xorm:"tumblr varchar(100)" json:"tumblr"`
|
||||
Twitch string `xorm:"twitch varchar(100)" json:"twitch"`
|
||||
@@ -241,6 +242,7 @@ type User struct {
|
||||
MfaRememberDeadline string `xorm:"varchar(100)" json:"mfaRememberDeadline"`
|
||||
NeedUpdatePassword bool `json:"needUpdatePassword"`
|
||||
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
|
||||
ApplicationScopes []ConsentRecord `xorm:"mediumtext" json:"applicationScopes"`
|
||||
}
|
||||
|
||||
type Userinfo struct {
|
||||
@@ -860,17 +862,17 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
|
||||
if len(columns) == 0 {
|
||||
columns = []string{
|
||||
"owner", "display_name", "avatar", "first_name", "last_name",
|
||||
"location", "address", "country_code", "region", "language", "affiliation", "title", "id_card_type", "id_card", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application",
|
||||
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts", "face_ids", "mfaAccounts",
|
||||
"signin_wrong_times", "last_change_password_time", "last_signin_wrong_time", "groups", "access_key", "access_secret", "mfa_phone_enabled", "mfa_email_enabled", "email_verified",
|
||||
"location", "address", "addresses", "country_code", "region", "language", "affiliation", "title", "id_card_type", "id_card", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application", "register_type", "register_source",
|
||||
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "mfa_items", "last_change_password_time", "managedAccounts", "face_ids", "mfaAccounts",
|
||||
"signin_wrong_times", "last_signin_wrong_time", "groups", "access_key", "access_secret", "mfa_phone_enabled", "mfa_email_enabled", "email_verified",
|
||||
"github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs",
|
||||
"baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "kwai", "line", "amazon",
|
||||
"auth0", "battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox",
|
||||
"eveonline", "fitbit", "gitea", "heroku", "influxcloud", "instagram", "intercom", "kakao", "lastfm", "mailru", "meetup",
|
||||
"microsoftonline", "naver", "nextcloud", "onedrive", "oura", "patreon", "paypal", "salesforce", "shopify", "soundcloud",
|
||||
"spotify", "strava", "stripe", "type", "tiktok", "tumblr", "twitch", "twitter", "typetalk", "uber", "vk", "wepay", "xero", "yahoo",
|
||||
"yammer", "yandex", "zoom", "custom", "need_update_password", "ip_whitelist", "mfa_items", "mfa_remember_deadline",
|
||||
"cart",
|
||||
"spotify", "strava", "stripe", "type", "telegram", "tiktok", "tumblr", "twitch", "twitter", "typetalk", "uber", "vk", "wepay", "xero", "yahoo",
|
||||
"yammer", "yandex", "zoom", "custom", "need_update_password", "ip_whitelist", "mfa_remember_deadline",
|
||||
"cart", "application_scopes",
|
||||
}
|
||||
}
|
||||
if isAdmin {
|
||||
@@ -954,6 +956,13 @@ func UpdateUserForAllFields(id string, user *User) (bool, error) {
|
||||
|
||||
user.UpdatedTime = util.GetCurrentTime()
|
||||
|
||||
if len(user.Groups) > 0 {
|
||||
_, err = userEnforcer.UpdateGroupsForUser(user.GetId(), user.Groups)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
affected, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(user)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
||||
119
object/user_scope.go
Normal file
119
object/user_scope.go
Normal file
@@ -0,0 +1,119 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
)
|
||||
|
||||
// ConsentRecord represents the data for OAuth consent API requests/responses
|
||||
type ConsentRecord struct {
|
||||
// owner/name
|
||||
Application string `json:"application"`
|
||||
GrantedScopes []string `json:"grantedScopes"`
|
||||
}
|
||||
|
||||
// ScopeDescription represents a human-readable description of an OAuth scope
|
||||
type ScopeDescription struct {
|
||||
Scope string `json:"scope"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// parseScopes converts a space-separated scope string to a slice
|
||||
func parseScopes(scopeStr string) []string {
|
||||
if scopeStr == "" {
|
||||
return []string{}
|
||||
}
|
||||
scopes := strings.Split(scopeStr, " ")
|
||||
var result []string
|
||||
for _, scope := range scopes {
|
||||
trimmed := strings.TrimSpace(scope)
|
||||
if trimmed != "" {
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// CheckConsentRequired checks if user consent is required for the OAuth flow
|
||||
func CheckConsentRequired(userObj *User, application *Application, scopeStr string) (bool, error) {
|
||||
// Skip consent when no custom scopes are configured
|
||||
if len(application.CustomScopes) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Once policy: check if consent already granted
|
||||
requestedScopes := parseScopes(scopeStr)
|
||||
appId := application.GetId()
|
||||
|
||||
// Filter requestedScopes to only include scopes defined in application.CustomScopes
|
||||
customScopesMap := make(map[string]bool)
|
||||
for _, customScope := range application.CustomScopes {
|
||||
if customScope.Scope != "" {
|
||||
customScopesMap[customScope.Scope] = true
|
||||
}
|
||||
}
|
||||
|
||||
validRequestedScopes := []string{}
|
||||
for _, scope := range requestedScopes {
|
||||
if customScopesMap[scope] {
|
||||
validRequestedScopes = append(validRequestedScopes, scope)
|
||||
}
|
||||
}
|
||||
|
||||
// If no valid requested scopes, no consent required
|
||||
if len(validRequestedScopes) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
for _, record := range userObj.ApplicationScopes {
|
||||
if record.Application == appId {
|
||||
// Check if grantedScopes contains all validRequestedScopes
|
||||
grantedMap := make(map[string]bool)
|
||||
for _, scope := range record.GrantedScopes {
|
||||
grantedMap[scope] = true
|
||||
}
|
||||
|
||||
allGranted := true
|
||||
for _, scope := range validRequestedScopes {
|
||||
if !grantedMap[scope] {
|
||||
allGranted = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if allGranted {
|
||||
// Consent already granted for all valid requested scopes
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Consent required
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func validateCustomScopes(customScopes []*ScopeDescription, lang string) error {
|
||||
for _, scope := range customScopes {
|
||||
if scope == nil || strings.TrimSpace(scope.Scope) == "" {
|
||||
return fmt.Errorf("%s: custom scope name", i18n.Translate(lang, "general:Missing parameter"))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -126,12 +126,26 @@ func InitAPI() {
|
||||
web.Router("/api/delete-resource", &controllers.ApiController{}, "POST:DeleteResource")
|
||||
web.Router("/api/upload-resource", &controllers.ApiController{}, "POST:UploadResource")
|
||||
|
||||
web.Router("/api/get-global-sites", &controllers.ApiController{}, "GET:GetGlobalSites")
|
||||
web.Router("/api/get-sites", &controllers.ApiController{}, "GET:GetSites")
|
||||
web.Router("/api/get-site", &controllers.ApiController{}, "GET:GetSite")
|
||||
web.Router("/api/update-site", &controllers.ApiController{}, "POST:UpdateSite")
|
||||
web.Router("/api/add-site", &controllers.ApiController{}, "POST:AddSite")
|
||||
web.Router("/api/delete-site", &controllers.ApiController{}, "POST:DeleteSite")
|
||||
|
||||
web.Router("/api/get-rules", &controllers.ApiController{}, "GET:GetRules")
|
||||
web.Router("/api/get-rule", &controllers.ApiController{}, "GET:GetRule")
|
||||
web.Router("/api/add-rule", &controllers.ApiController{}, "POST:AddRule")
|
||||
web.Router("/api/update-rule", &controllers.ApiController{}, "POST:UpdateRule")
|
||||
web.Router("/api/delete-rule", &controllers.ApiController{}, "POST:DeleteRule")
|
||||
|
||||
web.Router("/api/get-certs", &controllers.ApiController{}, "GET:GetCerts")
|
||||
web.Router("/api/get-global-certs", &controllers.ApiController{}, "GET:GetGlobalCerts")
|
||||
web.Router("/api/get-cert", &controllers.ApiController{}, "GET:GetCert")
|
||||
web.Router("/api/update-cert", &controllers.ApiController{}, "POST:UpdateCert")
|
||||
web.Router("/api/add-cert", &controllers.ApiController{}, "POST:AddCert")
|
||||
web.Router("/api/delete-cert", &controllers.ApiController{}, "POST:DeleteCert")
|
||||
web.Router("/api/update-cert-domain-expire", &controllers.ApiController{}, "POST:UpdateCertDomainExpire")
|
||||
|
||||
web.Router("/api/get-roles", &controllers.ApiController{}, "GET:GetRoles")
|
||||
web.Router("/api/get-role", &controllers.ApiController{}, "GET:GetRole")
|
||||
@@ -319,6 +333,9 @@ func InitAPI() {
|
||||
web.Router("/api/delete-mfa", &controllers.ApiController{}, "POST:DeleteMfa")
|
||||
web.Router("/api/set-preferred-mfa", &controllers.ApiController{}, "POST:SetPreferredMfa")
|
||||
|
||||
web.Router("/api/grant-consent", &controllers.ApiController{}, "POST:GrantConsent")
|
||||
web.Router("/api/revoke-consent", &controllers.ApiController{}, "POST:RevokeConsent")
|
||||
|
||||
web.Router("/.well-known/openid-configuration", &controllers.RootController{}, "GET:GetOidcDiscovery")
|
||||
web.Router("/.well-known/:application/openid-configuration", &controllers.RootController{}, "GET:GetOidcDiscoveryByApplication")
|
||||
web.Router("/.well-known/oauth-authorization-server", &controllers.RootController{}, "GET:GetOAuthServerMetadata")
|
||||
|
||||
@@ -89,6 +89,23 @@ func fastAutoSignin(ctx *context.Context) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
user, err := object.GetUser(userId)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if user == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
consentRequired, err := object.CheckConsentRequired(user, application, scope)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if consentRequired {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
code, err := object.GetOAuthCode(userId, clientId, "", "autoSignin", responseType, redirectUri, scope, state, nonce, codeChallenge, "", ctx.Request.Host, getAcceptLanguage(ctx))
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
112
rule/rule.go
Normal file
112
rule/rule.go
Normal file
@@ -0,0 +1,112 @@
|
||||
// Copyright 2024 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 rule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
type Rule interface {
|
||||
checkRule(expressions []*object.Expression, req *http.Request) (*RuleResult, error)
|
||||
}
|
||||
|
||||
type RuleResult struct {
|
||||
Action string
|
||||
StatusCode int
|
||||
Reason string
|
||||
}
|
||||
|
||||
func CheckRules(ruleIds []string, r *http.Request) (*RuleResult, error) {
|
||||
rules, err := object.GetRulesByRuleIds(ruleIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, rule := range rules {
|
||||
var ruleObj Rule
|
||||
switch rule.Type {
|
||||
case "User-Agent":
|
||||
ruleObj = &UaRule{}
|
||||
case "IP":
|
||||
ruleObj = &IpRule{}
|
||||
case "WAF":
|
||||
ruleObj = &WafRule{}
|
||||
case "IP Rate Limiting":
|
||||
ruleObj = &IpRateRule{
|
||||
ruleName: rule.GetId(),
|
||||
}
|
||||
case "Compound":
|
||||
ruleObj = &CompoundRule{}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown rule type: %s for rule: %s", rule.Type, rule.GetId())
|
||||
}
|
||||
|
||||
result, err := ruleObj.checkRule(rule.Expressions, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if result != nil {
|
||||
// Use rule's action if no action specified by the rule check
|
||||
if result.Action == "" {
|
||||
result.Action = rule.Action
|
||||
}
|
||||
|
||||
// Determine status code
|
||||
if result.StatusCode == 0 {
|
||||
if rule.StatusCode != 0 {
|
||||
result.StatusCode = rule.StatusCode
|
||||
} else {
|
||||
// Set default status codes if not specified
|
||||
switch result.Action {
|
||||
case "Block":
|
||||
result.StatusCode = 403
|
||||
case "Drop":
|
||||
result.StatusCode = 400
|
||||
case "Allow":
|
||||
result.StatusCode = 200
|
||||
case "CAPTCHA":
|
||||
result.StatusCode = 302
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown rule action: %s for rule: %s", result.Action, rule.GetId())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update reason if rule has custom reason
|
||||
if result.Action == "Block" || result.Action == "Drop" {
|
||||
if rule.IsVerbose {
|
||||
// Add verbose debug info with rule name and triggered expression
|
||||
result.Reason = util.GenerateVerboseReason(rule.GetId(), result.Reason, rule.Reason)
|
||||
} else if rule.Reason != "" {
|
||||
result.Reason = rule.Reason
|
||||
} else if result.Reason != "" {
|
||||
result.Reason = fmt.Sprintf("hit rule %s: %s", ruleIds[i], result.Reason)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Default action if no rule matched
|
||||
return &RuleResult{
|
||||
Action: "Allow",
|
||||
StatusCode: 200,
|
||||
}, nil
|
||||
}
|
||||
60
rule/rule_compound.go
Normal file
60
rule/rule_compound.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright 2024 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 rule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
type CompoundRule struct{}
|
||||
|
||||
func (r *CompoundRule) checkRule(expressions []*object.Expression, req *http.Request) (*RuleResult, error) {
|
||||
operators := util.NewStack()
|
||||
res := true
|
||||
for _, expression := range expressions {
|
||||
isHit := true
|
||||
result, err := CheckRules([]string{expression.Value}, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result == nil || result.Action == "" {
|
||||
isHit = false
|
||||
}
|
||||
switch expression.Operator {
|
||||
case "and", "begin":
|
||||
res = res && isHit
|
||||
case "or":
|
||||
operators.Push(res)
|
||||
res = isHit
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown operator: %s", expression.Operator)
|
||||
}
|
||||
if operators.Size() > 0 {
|
||||
last, ok := operators.Pop()
|
||||
for ok {
|
||||
res = last.(bool) || res
|
||||
last, ok = operators.Pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
if res {
|
||||
return &RuleResult{}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
94
rule/rule_ip.go
Normal file
94
rule/rule_ip.go
Normal file
@@ -0,0 +1,94 @@
|
||||
// Copyright 2024 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 rule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/ip"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
type IpRule struct{}
|
||||
|
||||
func (r *IpRule) checkRule(expressions []*object.Expression, req *http.Request) (*RuleResult, error) {
|
||||
clientIp := util.GetClientIp(req)
|
||||
netIp, err := parseIp(clientIp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, expression := range expressions {
|
||||
reason := fmt.Sprintf("expression matched: \"%s %s %s\"", clientIp, expression.Operator, expression.Value)
|
||||
|
||||
// Handle "is abroad" operator
|
||||
if expression.Operator == "is abroad" {
|
||||
if ip.IsAbroadIp(clientIp) {
|
||||
return &RuleResult{Reason: reason}, nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
ips := strings.Split(expression.Value, ",")
|
||||
for _, ipStr := range ips {
|
||||
if strings.Contains(ipStr, "/") {
|
||||
_, ipNet, err := net.ParseCIDR(ipStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch expression.Operator {
|
||||
case "is in":
|
||||
if ipNet.Contains(netIp) {
|
||||
return &RuleResult{Reason: reason}, nil
|
||||
}
|
||||
case "is not in":
|
||||
if !ipNet.Contains(netIp) {
|
||||
return &RuleResult{Reason: reason}, nil
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown operator: %s", expression.Operator)
|
||||
}
|
||||
} else if strings.ContainsAny(ipStr, ".:") {
|
||||
switch expression.Operator {
|
||||
case "is in":
|
||||
if ipStr == clientIp {
|
||||
return &RuleResult{Reason: reason}, nil
|
||||
}
|
||||
case "is not in":
|
||||
if ipStr != clientIp {
|
||||
return &RuleResult{Reason: reason}, nil
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown operator: %s", expression.Operator)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("unknown IP or CIDR format: %s", ipStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func parseIp(ipStr string) (net.IP, error) {
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
return nil, fmt.Errorf("unknown IP or CIDR format: %s", ipStr)
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
134
rule/rule_ip_rate.go
Normal file
134
rule/rule_ip_rate.go
Normal file
@@ -0,0 +1,134 @@
|
||||
// Copyright 2024 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 rule
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
type IpRateRule struct {
|
||||
ruleName string
|
||||
}
|
||||
|
||||
type IpRateLimiter struct {
|
||||
ips map[string]*rate.Limiter
|
||||
mu *sync.RWMutex
|
||||
r rate.Limit
|
||||
b int
|
||||
}
|
||||
|
||||
var blackList = map[string]map[string]time.Time{}
|
||||
|
||||
var ipRateLimiters = map[string]*IpRateLimiter{}
|
||||
|
||||
// NewIpRateLimiter .
|
||||
func NewIpRateLimiter(r rate.Limit, b int) *IpRateLimiter {
|
||||
i := &IpRateLimiter{
|
||||
ips: make(map[string]*rate.Limiter),
|
||||
mu: &sync.RWMutex{},
|
||||
r: r,
|
||||
b: b,
|
||||
}
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
// AddIP creates a new rate limiter and adds it to the ips map,
|
||||
// using the IP address as the key
|
||||
func (i *IpRateLimiter) AddIP(ip string) *rate.Limiter {
|
||||
i.mu.Lock()
|
||||
defer i.mu.Unlock()
|
||||
|
||||
limiter := rate.NewLimiter(i.r, i.b)
|
||||
|
||||
i.ips[ip] = limiter
|
||||
|
||||
return limiter
|
||||
}
|
||||
|
||||
// GetLimiter returns the rate limiter for the provided IP address if it exists.
|
||||
// Otherwise, calls AddIP to add IP address to the map
|
||||
func (i *IpRateLimiter) GetLimiter(ip string) *rate.Limiter {
|
||||
i.mu.Lock()
|
||||
limiter, exists := i.ips[ip]
|
||||
|
||||
if !exists {
|
||||
i.mu.Unlock()
|
||||
return i.AddIP(ip)
|
||||
}
|
||||
|
||||
i.mu.Unlock()
|
||||
|
||||
return limiter
|
||||
}
|
||||
|
||||
func (r *IpRateRule) checkRule(expressions []*object.Expression, req *http.Request) (*RuleResult, error) {
|
||||
expression := expressions[0] // IpRate rule should have only one expression
|
||||
clientIp := util.GetClientIp(req)
|
||||
|
||||
// If the client IP is in the blacklist, check the block time
|
||||
createAt, ok := blackList[r.ruleName][clientIp]
|
||||
if ok {
|
||||
blockTime := util.ParseInt(expression.Value)
|
||||
if time.Now().Sub(createAt) < time.Duration(blockTime)*time.Second {
|
||||
return &RuleResult{
|
||||
Action: "Block",
|
||||
Reason: "Rate limit exceeded",
|
||||
}, nil
|
||||
} else {
|
||||
delete(blackList[r.ruleName], clientIp)
|
||||
}
|
||||
}
|
||||
|
||||
// If the client IP is not in the blacklist, check the rate limit
|
||||
ipRateLimiter := ipRateLimiters[r.ruleName]
|
||||
parseInt := util.ParseInt(expression.Operator)
|
||||
if ipRateLimiter == nil {
|
||||
ipRateLimiter = NewIpRateLimiter(rate.Limit(parseInt), parseInt)
|
||||
ipRateLimiters[r.ruleName] = ipRateLimiter
|
||||
}
|
||||
|
||||
// If the rate limit has changed, update the rate limiter
|
||||
limiter := ipRateLimiter.GetLimiter(clientIp)
|
||||
if ipRateLimiter.r != rate.Limit(parseInt) {
|
||||
ipRateLimiter.r = rate.Limit(parseInt)
|
||||
ipRateLimiter.b = parseInt
|
||||
limiter.SetLimit(ipRateLimiter.r)
|
||||
limiter.SetBurst(ipRateLimiter.b)
|
||||
err := limiter.Wait(req.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// If the rate limit is exceeded, add the client IP to the blacklist
|
||||
allow := limiter.Allow()
|
||||
if !allow {
|
||||
blackList[r.ruleName] = map[string]time.Time{}
|
||||
blackList[r.ruleName][clientIp] = time.Now()
|
||||
return &RuleResult{
|
||||
Action: "Block",
|
||||
Reason: "Rate limit exceeded",
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
154
rule/rule_ip_rate_test.go
Normal file
154
rule/rule_ip_rate_test.go
Normal file
@@ -0,0 +1,154 @@
|
||||
// Copyright 2023 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 rule
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
)
|
||||
|
||||
func TestIpRateRule_checkRule(t *testing.T) {
|
||||
type fields struct {
|
||||
ruleName string
|
||||
}
|
||||
type args struct {
|
||||
args []struct {
|
||||
expressions []*object.Expression
|
||||
req *http.Request
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want []bool
|
||||
want1 []string
|
||||
want2 []string
|
||||
wantErr []bool
|
||||
}{
|
||||
{
|
||||
name: "Test 1",
|
||||
fields: fields{
|
||||
ruleName: "rule1",
|
||||
},
|
||||
args: args{
|
||||
args: []struct {
|
||||
expressions []*object.Expression
|
||||
req *http.Request
|
||||
}{
|
||||
{
|
||||
expressions: []*object.Expression{
|
||||
{
|
||||
Operator: "1",
|
||||
Value: "1",
|
||||
},
|
||||
},
|
||||
req: &http.Request{
|
||||
RemoteAddr: "127.0.0.1",
|
||||
},
|
||||
},
|
||||
{
|
||||
expressions: []*object.Expression{
|
||||
{
|
||||
Operator: "1",
|
||||
Value: "1",
|
||||
},
|
||||
},
|
||||
req: &http.Request{
|
||||
RemoteAddr: "127.0.0.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []bool{false, true},
|
||||
want1: []string{"", "Block"},
|
||||
want2: []string{"", "Rate limit exceeded"},
|
||||
wantErr: []bool{false, false},
|
||||
},
|
||||
{
|
||||
name: "Test 2",
|
||||
fields: fields{
|
||||
ruleName: "rule2",
|
||||
},
|
||||
args: args{
|
||||
args: []struct {
|
||||
expressions []*object.Expression
|
||||
req *http.Request
|
||||
}{
|
||||
{
|
||||
expressions: []*object.Expression{
|
||||
{
|
||||
Operator: "1",
|
||||
Value: "1",
|
||||
},
|
||||
},
|
||||
req: &http.Request{
|
||||
RemoteAddr: "127.0.0.1",
|
||||
},
|
||||
},
|
||||
{
|
||||
expressions: []*object.Expression{
|
||||
{
|
||||
Operator: "10",
|
||||
Value: "1",
|
||||
},
|
||||
},
|
||||
req: &http.Request{
|
||||
RemoteAddr: "127.0.0.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []bool{false, false},
|
||||
want1: []string{"", ""},
|
||||
want2: []string{"", ""},
|
||||
wantErr: []bool{false, false},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &IpRateRule{
|
||||
ruleName: tt.fields.ruleName,
|
||||
}
|
||||
for i, arg := range tt.args.args {
|
||||
result, err := r.checkRule(arg.expressions, arg.req)
|
||||
if (err != nil) != tt.wantErr[i] {
|
||||
t.Errorf("checkRule() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
got := result != nil
|
||||
got1 := ""
|
||||
got2 := ""
|
||||
if result != nil {
|
||||
got1 = result.Action
|
||||
got2 = result.Reason
|
||||
}
|
||||
if got != tt.want[i] {
|
||||
t.Errorf("checkRule() got = %v, want %v", got, tt.want[i])
|
||||
}
|
||||
if got1 != tt.want1[i] {
|
||||
t.Errorf("checkRule() got1 = %v, want %v", got1, tt.want1[i])
|
||||
}
|
||||
if got2 != tt.want2[i] {
|
||||
t.Errorf("checkRule() got2 = %v, want %v", got2, tt.want2[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
63
rule/rule_ua.go
Normal file
63
rule/rule_ua.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright 2024 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 rule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
)
|
||||
|
||||
type UaRule struct{}
|
||||
|
||||
func (r *UaRule) checkRule(expressions []*object.Expression, req *http.Request) (*RuleResult, error) {
|
||||
userAgent := req.UserAgent()
|
||||
for _, expression := range expressions {
|
||||
ua := expression.Value
|
||||
reason := fmt.Sprintf("expression matched: \"%s %s %s\"", userAgent, expression.Operator, expression.Value)
|
||||
switch expression.Operator {
|
||||
case "contains":
|
||||
if strings.Contains(userAgent, ua) {
|
||||
return &RuleResult{Reason: reason}, nil
|
||||
}
|
||||
case "does not contain":
|
||||
if !strings.Contains(userAgent, ua) {
|
||||
return &RuleResult{Reason: reason}, nil
|
||||
}
|
||||
case "equals":
|
||||
if userAgent == ua {
|
||||
return &RuleResult{Reason: reason}, nil
|
||||
}
|
||||
case "does not equal":
|
||||
if strings.Compare(userAgent, ua) != 0 {
|
||||
return &RuleResult{Reason: reason}, nil
|
||||
}
|
||||
case "match":
|
||||
// regex match
|
||||
isHit, err := regexp.MatchString(ua, userAgent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isHit {
|
||||
return &RuleResult{Reason: reason}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
105
rule/rule_waf.go
Normal file
105
rule/rule_waf.go
Normal file
@@ -0,0 +1,105 @@
|
||||
// Copyright 2024 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 rule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/corazawaf/coraza/v3"
|
||||
"github.com/corazawaf/coraza/v3/types"
|
||||
"github.com/hsluoyz/modsecurity-go/seclang/parser"
|
||||
)
|
||||
|
||||
type WafRule struct{}
|
||||
|
||||
func (r *WafRule) checkRule(expressions []*object.Expression, req *http.Request) (*RuleResult, error) {
|
||||
var ruleStr string
|
||||
for _, expression := range expressions {
|
||||
ruleStr += expression.Value
|
||||
}
|
||||
waf, err := coraza.NewWAF(
|
||||
coraza.NewWAFConfig().
|
||||
WithErrorCallback(logError).
|
||||
WithDirectives(conf.WafConf).
|
||||
WithDirectives(ruleStr),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create WAF failed")
|
||||
}
|
||||
tx := waf.NewTransaction()
|
||||
processRequest(tx, req)
|
||||
matchedRules := tx.MatchedRules()
|
||||
for _, matchedRule := range matchedRules {
|
||||
rule := matchedRule.Rule()
|
||||
directive, err := parser.NewSecLangScannerFromString(rule.Raw()).AllDirective()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, d := range directive {
|
||||
ruleDirective := d.(*parser.RuleDirective)
|
||||
for _, action := range ruleDirective.Actions.Action {
|
||||
switch action.Tk {
|
||||
case parser.TkActionBlock, parser.TkActionDeny:
|
||||
return &RuleResult{
|
||||
Action: "Block",
|
||||
Reason: fmt.Sprintf("blocked by WAF rule: %d", rule.ID()),
|
||||
}, nil
|
||||
case parser.TkActionAllow:
|
||||
return &RuleResult{
|
||||
Action: "Allow",
|
||||
}, nil
|
||||
case parser.TkActionDrop:
|
||||
return &RuleResult{
|
||||
Action: "Drop",
|
||||
Reason: fmt.Sprintf("dropped by WAF rule: %d", rule.ID()),
|
||||
}, nil
|
||||
default:
|
||||
// skip other actions
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func processRequest(tx types.Transaction, req *http.Request) {
|
||||
// Process URI and method
|
||||
tx.ProcessURI(req.URL.String(), req.Method, req.Proto)
|
||||
|
||||
// Process request headers
|
||||
for key, values := range req.Header {
|
||||
for _, value := range values {
|
||||
tx.AddRequestHeader(key, value)
|
||||
}
|
||||
}
|
||||
tx.ProcessRequestHeaders()
|
||||
|
||||
// Process request body (if any)
|
||||
if req.Body != nil {
|
||||
_, err := tx.ProcessRequestBody()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func logError(error types.MatchedRule) {
|
||||
msg := error.ErrorLog()
|
||||
fmt.Printf("[WAFlogError][%s] %s\n", error.Rule().Severity(), msg)
|
||||
}
|
||||
90
service/oauth.go
Normal file
90
service/oauth.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// Copyright 2023 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 service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
func getSigninUrl(casdoorClient *casdoorsdk.Client, callbackUrl string, originalPath string) string {
|
||||
scope := "read"
|
||||
return fmt.Sprintf("%s/login/oauth/authorize?client_id=%s&response_type=code&redirect_uri=%s&scope=%s&state=%s",
|
||||
casdoorClient.Endpoint, casdoorClient.ClientId, url.QueryEscape(callbackUrl), scope, url.QueryEscape(originalPath))
|
||||
}
|
||||
|
||||
func redirectToCasdoor(casdoorClient *casdoorsdk.Client, w http.ResponseWriter, r *http.Request) {
|
||||
scheme := getScheme(r)
|
||||
|
||||
callbackUrl := fmt.Sprintf("%s://%s/caswaf-handler", scheme, r.Host)
|
||||
originalPath := r.RequestURI
|
||||
signinUrl := getSigninUrl(casdoorClient, callbackUrl, originalPath)
|
||||
http.Redirect(w, r, signinUrl, http.StatusFound)
|
||||
}
|
||||
|
||||
func handleAuthCallback(w http.ResponseWriter, r *http.Request) {
|
||||
site := getSiteByDomainWithWww(r.Host)
|
||||
if site == nil {
|
||||
responseError(w, "CasWAF error: site not found for host: %s", r.Host)
|
||||
return
|
||||
}
|
||||
|
||||
code := r.URL.Query().Get("code")
|
||||
state := r.URL.Query().Get("state")
|
||||
if code == "" {
|
||||
responseError(w, "CasWAF error: the code should not be empty")
|
||||
return
|
||||
} else if state == "" {
|
||||
responseError(w, "CasWAF error: the state should not be empty")
|
||||
return
|
||||
}
|
||||
|
||||
application, err := object.GetApplication(util.GetId(site.Owner, site.CasdoorApplication))
|
||||
if err != nil {
|
||||
responseError(w, "CasWAF error: casdoorClient.GetOAuthToken() error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//casdoorClient, err := getCasdoorClientFromSite(site)
|
||||
//if err != nil {
|
||||
// responseError(w, "CasWAF error: getCasdoorClientFromSite() error: %s", err.Error())
|
||||
// return
|
||||
//}
|
||||
|
||||
token, tokenError, err := object.GetAuthorizationCodeToken(application, application.ClientSecret, code, "", "")
|
||||
if tokenError != nil {
|
||||
responseError(w, "CasWAF error: casdoorClient.GetOAuthToken() error: %s", tokenError.Error)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
responseError(w, "CasWAF error: casdoorClient.GetOAuthToken() error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
cookie := &http.Cookie{
|
||||
Name: "casdoor_access_token",
|
||||
Value: token.AccessToken,
|
||||
Path: "/",
|
||||
}
|
||||
http.SetCookie(w, cookie)
|
||||
|
||||
originalPath := state
|
||||
http.Redirect(w, r, originalPath, http.StatusFound)
|
||||
}
|
||||
372
service/proxy.go
Normal file
372
service/proxy.go
Normal file
@@ -0,0 +1,372 @@
|
||||
// Copyright 2023 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 service
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/rule"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/casvisor/casvisor-go-sdk/casvisorsdk"
|
||||
)
|
||||
|
||||
func forwardHandler(targetUrl string, writer http.ResponseWriter, request *http.Request) {
|
||||
target, err := url.Parse(targetUrl)
|
||||
|
||||
if nil != err {
|
||||
panic(err)
|
||||
return
|
||||
}
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(target)
|
||||
proxy.Director = func(r *http.Request) {
|
||||
r.URL = target
|
||||
|
||||
if clientIP, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
|
||||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" && xff != clientIP {
|
||||
newXff := fmt.Sprintf("%s, %s", xff, clientIP)
|
||||
// r.Header.Set("X-Forwarded-For", newXff)
|
||||
r.Header.Set("X-Real-Ip", newXff)
|
||||
} else {
|
||||
// r.Header.Set("X-Forwarded-For", clientIP)
|
||||
r.Header.Set("X-Real-Ip", clientIP)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
proxy.ModifyResponse = func(resp *http.Response) error {
|
||||
// Add Secure flag to all Set-Cookie headers in HTTPS responses
|
||||
if request.TLS != nil {
|
||||
// Add HSTS header for HTTPS responses if not already set by backend
|
||||
if resp.Header.Get("Strict-Transport-Security") == "" {
|
||||
resp.Header.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
|
||||
}
|
||||
|
||||
cookies := resp.Header["Set-Cookie"]
|
||||
if len(cookies) > 0 {
|
||||
// Clear existing Set-Cookie headers
|
||||
resp.Header.Del("Set-Cookie")
|
||||
// Add them back with Secure flag if not already present
|
||||
for _, cookie := range cookies {
|
||||
// Check if Secure attribute is already present (case-insensitive)
|
||||
cookieLower := strings.ToLower(cookie)
|
||||
hasSecure := strings.Contains(cookieLower, ";secure;") ||
|
||||
strings.Contains(cookieLower, "; secure;") ||
|
||||
strings.HasSuffix(cookieLower, ";secure") ||
|
||||
strings.HasSuffix(cookieLower, "; secure")
|
||||
if !hasSecure {
|
||||
cookie = cookie + "; Secure"
|
||||
}
|
||||
resp.Header.Add("Set-Cookie", cookie)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fix CORS issue: Remove CORS header combinations that allow credential theft from any origin
|
||||
allowOrigin := resp.Header.Get("Access-Control-Allow-Origin")
|
||||
allowCredentials := resp.Header.Get("Access-Control-Allow-Credentials")
|
||||
|
||||
// Remove CORS headers when the combination is present:
|
||||
// 1. Access-Control-Allow-Credentials: true with Access-Control-Allow-Origin: *
|
||||
// This is actually blocked by browsers but we sanitize it anyway
|
||||
// 2. Access-Control-Allow-Credentials: true with any origin
|
||||
// Without a configured allowlist, we cannot safely validate if the origin
|
||||
// is trusted or if it's being reflected from the request, so we remove all
|
||||
// CORS headers for credential-bearing responses to prevent theft
|
||||
if strings.EqualFold(allowCredentials, "true") && allowOrigin != "" {
|
||||
// Remove CORS headers to prevent credential theft
|
||||
resp.Header.Del("Access-Control-Allow-Origin")
|
||||
resp.Header.Del("Access-Control-Allow-Credentials")
|
||||
resp.Header.Del("Access-Control-Allow-Methods")
|
||||
resp.Header.Del("Access-Control-Allow-Headers")
|
||||
resp.Header.Del("Access-Control-Expose-Headers")
|
||||
resp.Header.Del("Access-Control-Max-Age")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
proxy.ServeHTTP(writer, request)
|
||||
}
|
||||
|
||||
func getHostNonWww(host string) string {
|
||||
res := ""
|
||||
tokens := strings.Split(host, ".")
|
||||
if len(tokens) > 2 && tokens[0] == "www" {
|
||||
res = strings.Join(tokens[1:], ".")
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func logRequest(clientIp string, r *http.Request) {
|
||||
if !strings.Contains(r.UserAgent(), "Uptime-Kuma") {
|
||||
fmt.Printf("handleRequest: %s\t%s\t%s\t%s\t%s\t%s\n", clientIp, r.Method, r.Host, r.RequestURI, r.UserAgent(), r.RemoteAddr)
|
||||
record := casvisorsdk.Record{
|
||||
Owner: "admin",
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
Method: r.Method,
|
||||
RequestUri: r.RequestURI,
|
||||
ClientIp: clientIp,
|
||||
}
|
||||
object.AddRecord(&record)
|
||||
}
|
||||
}
|
||||
|
||||
func redirectToHttps(w http.ResponseWriter, r *http.Request) {
|
||||
targetUrl := fmt.Sprintf("https://%s", joinPath(r.Host, r.RequestURI))
|
||||
http.Redirect(w, r, targetUrl, http.StatusMovedPermanently)
|
||||
}
|
||||
|
||||
func redirectToHost(w http.ResponseWriter, r *http.Request, host string) {
|
||||
protocol := "https"
|
||||
if r.TLS == nil {
|
||||
protocol = "http"
|
||||
}
|
||||
|
||||
targetUrl := fmt.Sprintf("%s://%s", protocol, joinPath(host, r.RequestURI))
|
||||
http.Redirect(w, r, targetUrl, http.StatusMovedPermanently)
|
||||
}
|
||||
|
||||
func handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||
clientIp := util.GetClientIp(r)
|
||||
logRequest(clientIp, r)
|
||||
|
||||
site := getSiteByDomainWithWww(r.Host)
|
||||
if site == nil {
|
||||
if isHostIp(r.Host) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasSuffix(r.Host, ".casdoor.com") && r.RequestURI == "/health-ping" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, err := fmt.Fprintf(w, "OK")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
responseError(w, "CasWAF error: site not found for host: %s", r.Host)
|
||||
return
|
||||
}
|
||||
|
||||
hostNonWww := getHostNonWww(r.Host)
|
||||
if hostNonWww != "" {
|
||||
redirectToHost(w, r, hostNonWww)
|
||||
return
|
||||
}
|
||||
|
||||
if site.Domain != r.Host && site.NeedRedirect {
|
||||
redirectToHost(w, r, site.Domain)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(r.RequestURI, "/.well-known/acme-challenge/") {
|
||||
challengeMap := site.GetChallengeMap()
|
||||
for token, keyAuth := range challengeMap {
|
||||
if r.RequestURI == fmt.Sprintf("/.well-known/acme-challenge/%s", token) {
|
||||
responseOk(w, keyAuth)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
responseError(w, fmt.Sprintf("CasWAF error: ACME HTTP-01 challenge failed, requestUri cannot match with challengeMap, requestUri = %s, challengeMap = %v", r.RequestURI, challengeMap))
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(r.RequestURI, "/MP_verify_") {
|
||||
challengeMap := site.GetChallengeMap()
|
||||
for path, value := range challengeMap {
|
||||
if r.RequestURI == fmt.Sprintf("/%s", path) {
|
||||
responseOk(w, value)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if site.SslMode == "HTTPS Only" {
|
||||
// This domain only supports https but receive http request, redirect to https
|
||||
if r.TLS == nil {
|
||||
redirectToHttps(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// oAuth proxy
|
||||
if site.CasdoorApplication != "" {
|
||||
// handle oAuth proxy
|
||||
cookie, err := r.Cookie("casdoor_access_token")
|
||||
if err != nil && err.Error() != "http: named cookie not present" {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
casdoorClient, err := getCasdoorClientFromSite(site)
|
||||
if err != nil {
|
||||
responseError(w, "CasWAF error: getCasdoorClientFromSite() error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if cookie == nil {
|
||||
// not logged in
|
||||
redirectToCasdoor(casdoorClient, w, r)
|
||||
return
|
||||
} else {
|
||||
_, err = casdoorClient.ParseJwtToken(cookie.Value)
|
||||
if err != nil {
|
||||
responseError(w, "CasWAF error: casdoorClient.ParseJwtToken() error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
host := site.GetHost()
|
||||
if host == "" {
|
||||
responseError(w, "CasWAF error: targetUrl should not be empty for host: %s, site = %v", r.Host, site)
|
||||
return
|
||||
}
|
||||
|
||||
if len(site.Rules) == 0 {
|
||||
nextHandle(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := rule.CheckRules(site.Rules, r)
|
||||
if err != nil {
|
||||
responseError(w, "Internal Server Error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
reason := result.Reason
|
||||
if reason != "" && site.DisableVerbose {
|
||||
reason = "the rule has been hit"
|
||||
}
|
||||
|
||||
switch result.Action {
|
||||
case "", "Allow":
|
||||
// Do not write header for Allow action, let the proxy handle it
|
||||
case "Block":
|
||||
w.WriteHeader(result.StatusCode)
|
||||
responseErrorWithoutCode(w, "Blocked by CasWAF: %s", reason)
|
||||
return
|
||||
case "Drop":
|
||||
w.WriteHeader(result.StatusCode)
|
||||
responseErrorWithoutCode(w, "Dropped by CasWAF: %s", reason)
|
||||
return
|
||||
default:
|
||||
responseError(w, "Error in CasWAF: %s", reason)
|
||||
}
|
||||
nextHandle(w, r)
|
||||
}
|
||||
|
||||
func nextHandle(w http.ResponseWriter, r *http.Request) {
|
||||
site := getSiteByDomainWithWww(r.Host)
|
||||
host := site.GetHost()
|
||||
if site.SslMode == "Static Folder" {
|
||||
var path string
|
||||
if r.RequestURI != "/" {
|
||||
path = filepath.Join(host, r.RequestURI)
|
||||
} else {
|
||||
path = filepath.Join(host, "/index.htm")
|
||||
if !util.FileExist(path) {
|
||||
path = filepath.Join(host, "/index.html")
|
||||
if !util.FileExist(path) {
|
||||
path = filepath.Join(host, r.RequestURI)
|
||||
}
|
||||
}
|
||||
}
|
||||
http.ServeFile(w, r, path)
|
||||
} else {
|
||||
targetUrl := joinPath(site.GetHost(), r.RequestURI)
|
||||
forwardHandler(targetUrl, w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func Start() {
|
||||
serverMux := http.NewServeMux()
|
||||
serverMux.HandleFunc("/", handleRequest)
|
||||
serverMux.HandleFunc("/caswaf-handler", handleAuthCallback)
|
||||
|
||||
gatewayHttpPort, err := conf.GetConfigInt64("gatewayHttpPort")
|
||||
if err != nil {
|
||||
gatewayHttpPort = 80
|
||||
}
|
||||
|
||||
gatewayHttpsPort, err := conf.GetConfigInt64("gatewayHttpsPort")
|
||||
if err != nil {
|
||||
gatewayHttpsPort = 443
|
||||
}
|
||||
|
||||
go func() {
|
||||
fmt.Printf("CasWAF gateway running on: http://127.0.0.1:%d\n", gatewayHttpPort)
|
||||
err := http.ListenAndServe(fmt.Sprintf(":%d", gatewayHttpPort), serverMux)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
fmt.Printf("CasWAF gateway running on: https://127.0.0.1:%d\n", gatewayHttpsPort)
|
||||
server := &http.Server{
|
||||
Handler: serverMux,
|
||||
Addr: fmt.Sprintf(":%d", gatewayHttpsPort),
|
||||
TLSConfig: &tls.Config{
|
||||
// Minimum TLS version 1.2, TLS 1.3 is automatically supported
|
||||
MinVersion: tls.VersionTLS12,
|
||||
// Secure cipher suites for TLS 1.2 (excluding 3DES to prevent Sweet32 attack)
|
||||
// TLS 1.3 cipher suites are automatically configured by Go
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
},
|
||||
// Prefer strong elliptic curves
|
||||
CurvePreferences: []tls.CurveID{
|
||||
tls.X25519,
|
||||
tls.CurveP256,
|
||||
tls.CurveP384,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// start https server and set how to get certificate
|
||||
server.TLSConfig.GetCertificate = func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
domain := info.ServerName
|
||||
cert, err := getX509CertByDomain(domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
err := server.ListenAndServeTLS("", "")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
142
service/util.go
Normal file
142
service/util.go
Normal file
@@ -0,0 +1,142 @@
|
||||
// Copyright 2023 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 service
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
)
|
||||
|
||||
func joinPath(a string, b string) string {
|
||||
if strings.HasSuffix(a, "/") && strings.HasPrefix(b, "/") {
|
||||
b = b[1:]
|
||||
} else if !strings.HasSuffix(a, "/") && !strings.HasPrefix(b, "/") {
|
||||
b = "/" + b
|
||||
}
|
||||
res := a + b
|
||||
return res
|
||||
}
|
||||
|
||||
func isHostIp(host string) bool {
|
||||
hostWithoutPort := strings.Split(host, ":")[0]
|
||||
ip := net.ParseIP(hostWithoutPort)
|
||||
return ip != nil
|
||||
}
|
||||
|
||||
func responseOk(w http.ResponseWriter, format string, a ...interface{}) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
msg := fmt.Sprintf(format, a...)
|
||||
fmt.Println(msg)
|
||||
_, err := fmt.Fprintf(w, msg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func responseError(w http.ResponseWriter, format string, a ...interface{}) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
msg := fmt.Sprintf(format, a...)
|
||||
fmt.Println(msg)
|
||||
_, err := fmt.Fprintf(w, msg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func responseErrorWithoutCode(w http.ResponseWriter, format string, a ...interface{}) {
|
||||
msg := fmt.Sprintf(format, a...)
|
||||
fmt.Println(msg)
|
||||
_, err := fmt.Fprintf(w, msg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func getDomainWithoutPort(domain string) string {
|
||||
if !strings.Contains(domain, ":") {
|
||||
return domain
|
||||
}
|
||||
|
||||
tokens := strings.SplitN(domain, ":", 2)
|
||||
if len(tokens) > 1 {
|
||||
return tokens[0]
|
||||
}
|
||||
return domain
|
||||
}
|
||||
|
||||
func getSiteByDomainWithWww(domain string) *object.Site {
|
||||
hostNonWww := getHostNonWww(domain)
|
||||
if hostNonWww != "" {
|
||||
domain = hostNonWww
|
||||
}
|
||||
|
||||
domainWithoutPort := getDomainWithoutPort(domain)
|
||||
|
||||
site := object.GetSiteByDomain(domainWithoutPort)
|
||||
return site
|
||||
}
|
||||
|
||||
func getX509CertByDomain(domain string) (*tls.Certificate, error) {
|
||||
cert, err := object.GetCertByDomain(domain)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getX509CertByDomain() error: %v, domain: [%s]", err, domain)
|
||||
}
|
||||
if cert == nil {
|
||||
return nil, fmt.Errorf("getX509CertByDomain() error: cert not found for domain: [%s]", domain)
|
||||
}
|
||||
|
||||
tlsCert, certErr := tls.X509KeyPair([]byte(cert.Certificate), []byte(cert.PrivateKey))
|
||||
|
||||
return &tlsCert, certErr
|
||||
}
|
||||
|
||||
func getCasdoorClientFromSite(site *object.Site) (*casdoorsdk.Client, error) {
|
||||
if site.ApplicationObj == nil {
|
||||
return nil, fmt.Errorf("site.ApplicationObj is empty")
|
||||
}
|
||||
|
||||
casdoorEndpoint := conf.GetConfigString("origin")
|
||||
if casdoorEndpoint == "" {
|
||||
casdoorEndpoint = "http://localhost:8000"
|
||||
}
|
||||
|
||||
clientId := site.ApplicationObj.ClientId
|
||||
clientSecret := site.ApplicationObj.ClientSecret
|
||||
|
||||
certificate := ""
|
||||
if site.ApplicationObj.CertObj != nil {
|
||||
certificate = site.ApplicationObj.CertObj.Certificate
|
||||
}
|
||||
|
||||
res := casdoorsdk.NewClient(casdoorEndpoint, clientId, clientSecret, certificate, site.ApplicationObj.Organization, site.CasdoorApplication)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func getScheme(r *http.Request) string {
|
||||
scheme := r.URL.Scheme
|
||||
if scheme == "" {
|
||||
scheme = "http"
|
||||
}
|
||||
return scheme
|
||||
}
|
||||
50
util/cert.go
Normal file
50
util/cert.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright 2026 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 util
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/publicsuffix"
|
||||
)
|
||||
|
||||
func GetCertExpireTime(s string) (string, error) {
|
||||
block, _ := pem.Decode([]byte(s))
|
||||
if block == nil {
|
||||
return "", errors.New("getCertExpireTime() error, block should not be nil")
|
||||
} else if block.Type != "CERTIFICATE" {
|
||||
return "", fmt.Errorf("getCertExpireTime() error, block.Type should be \"CERTIFICATE\" instead of %s", block.Type)
|
||||
}
|
||||
|
||||
certificate, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
t := certificate.NotAfter
|
||||
return t.Local().Format(time.RFC3339), nil
|
||||
}
|
||||
|
||||
func GetBaseDomain(domain string) (string, error) {
|
||||
// abc.com -> abc.com
|
||||
// abc.com.it -> abc.com.it
|
||||
// subdomain.abc.io -> abc.io
|
||||
// subdomain.abc.org.us -> abc.org.us
|
||||
return publicsuffix.EffectiveTLDPlusOne(domain)
|
||||
}
|
||||
@@ -15,9 +15,23 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GetHostname() string {
|
||||
name, err := os.Hostname()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
func IsInternetIp(ip string) bool {
|
||||
ipStr, _, err := net.SplitHostPort(ip)
|
||||
if err != nil {
|
||||
@@ -45,3 +59,55 @@ func IsHostIntranet(ip string) bool {
|
||||
|
||||
return parsedIP.IsPrivate() || parsedIP.IsLoopback() || parsedIP.IsLinkLocalUnicast() || parsedIP.IsLinkLocalMulticast()
|
||||
}
|
||||
|
||||
func ResolveDomainToIp(domain string) string {
|
||||
ips, err := net.LookupIP(domain)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no such host") {
|
||||
return "(empty)"
|
||||
}
|
||||
|
||||
fmt.Printf("resolveDomainToIp() error: %s\n", err.Error())
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
if ipv4 := ip.To4(); ipv4 != nil {
|
||||
return ipv4.String()
|
||||
}
|
||||
}
|
||||
return "(empty)"
|
||||
}
|
||||
|
||||
func PingUrl(url string) (bool, string) {
|
||||
client := http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 200 && resp.StatusCode <= 299 {
|
||||
return true, ""
|
||||
}
|
||||
return false, fmt.Sprintf("Status: %s", resp.Status)
|
||||
}
|
||||
|
||||
func IsIntranetIp(ip string) bool {
|
||||
ipStr, _, err := net.SplitHostPort(ip)
|
||||
if err != nil {
|
||||
ipStr = ip
|
||||
}
|
||||
|
||||
parsedIP := net.ParseIP(ipStr)
|
||||
if parsedIP == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return parsedIP.IsPrivate() ||
|
||||
parsedIP.IsLoopback() ||
|
||||
parsedIP.IsLinkLocalUnicast() ||
|
||||
parsedIP.IsLinkLocalMulticast()
|
||||
}
|
||||
|
||||
40
util/request.go
Normal file
40
util/request.go
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright 2023 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 (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetClientIp(r *http.Request) string {
|
||||
forwarded := r.Header.Get("X-Forwarded-For")
|
||||
if forwarded != "" {
|
||||
clientIP := strings.Split(forwarded, ",")[0]
|
||||
return strings.TrimSpace(clientIP)
|
||||
}
|
||||
|
||||
realIP := r.Header.Get("X-Real-IP")
|
||||
if realIP != "" {
|
||||
return realIP
|
||||
}
|
||||
|
||||
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return r.RemoteAddr
|
||||
}
|
||||
return ip
|
||||
}
|
||||
29
util/rule.go
Normal file
29
util/rule.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright 2024 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"
|
||||
|
||||
// GenerateVerboseReason creates a detailed reason message for verbose mode
|
||||
func GenerateVerboseReason(ruleId string, expressionReason string, customReason string) string {
|
||||
verboseReason := fmt.Sprintf("Rule [%s] triggered", ruleId)
|
||||
if expressionReason != "" {
|
||||
verboseReason += fmt.Sprintf(" - %s", expressionReason)
|
||||
}
|
||||
if customReason != "" {
|
||||
verboseReason += fmt.Sprintf(" - Custom reason: %s", customReason)
|
||||
}
|
||||
return verboseReason
|
||||
}
|
||||
59
util/stacks.go
Normal file
59
util/stacks.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright 2023 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
|
||||
|
||||
// Stack is a stack data structure implemented using a slice
|
||||
type Stack struct {
|
||||
items []interface{}
|
||||
}
|
||||
|
||||
// Push adds an item to the stack
|
||||
func (s *Stack) Push(item interface{}) {
|
||||
s.items = append(s.items, item)
|
||||
}
|
||||
|
||||
// Pop removes and returns the last item from the stack
|
||||
func (s *Stack) Pop() (interface{}, bool) {
|
||||
if len(s.items) == 0 {
|
||||
return nil, false // Return a sentinel value or you could handle this more gracefully
|
||||
}
|
||||
lastIndex := len(s.items) - 1
|
||||
item := s.items[lastIndex]
|
||||
s.items = s.items[:lastIndex]
|
||||
return item, true
|
||||
}
|
||||
|
||||
// Peek returns the last item from the stack without removing it
|
||||
func (s *Stack) Peek() interface{} {
|
||||
if len(s.items) == 0 {
|
||||
return -1
|
||||
}
|
||||
return s.items[len(s.items)-1]
|
||||
}
|
||||
|
||||
// IsEmpty checks if the stack is empty
|
||||
func (s *Stack) IsEmpty() bool {
|
||||
return len(s.items) == 0
|
||||
}
|
||||
|
||||
// Size returns the number of items in the stack
|
||||
func (s *Stack) Size() int {
|
||||
return len(s.items)
|
||||
}
|
||||
|
||||
// NewStack creates a new stack
|
||||
func NewStack() *Stack {
|
||||
return &Stack{}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -393,3 +394,37 @@ func StringToInterfaceArray2d(arrays [][]string) [][]interface{} {
|
||||
}
|
||||
return interfaceArrays
|
||||
}
|
||||
|
||||
func generateRandomString(length int) (string, error) {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
var c byte
|
||||
index := rand.Intn(len(charset))
|
||||
c = charset[index]
|
||||
b[i] = c
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func GenerateTwoUniqueRandomStrings() (string, string, error) {
|
||||
len1 := 16 + int(big.NewInt(17).Int64())
|
||||
len2 := 16 + int(big.NewInt(17).Int64())
|
||||
|
||||
str1, err := generateRandomString(len1)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
str2, err := generateRandomString(len2)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
for str1 == str2 {
|
||||
str2, err = generateRandomString(len2)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
return str1, str2, nil
|
||||
}
|
||||
|
||||
@@ -487,7 +487,7 @@ class App extends Component {
|
||||
: (
|
||||
Conf.CustomFooter !== null ? Conf.CustomFooter : (
|
||||
<React.Fragment>
|
||||
Powered by <a target="_blank" href="https://casdoor.org" rel="noreferrer"><img style={{paddingBottom: "3px"}} height={"20px"} alt={"Casdoor"} src={logo} /></a>
|
||||
Powered by <a target="_blank" href="https://casdoor.org" rel="noreferrer"><img style={{paddingBottom: "3px"}} height={"20px"} alt={"Casdoor"} src={logo} /></a>
|
||||
</React.Fragment>
|
||||
)
|
||||
)
|
||||
@@ -539,15 +539,16 @@ class App extends Component {
|
||||
|
||||
isEntryPages() {
|
||||
return window.location.pathname.startsWith("/signup") ||
|
||||
window.location.pathname.startsWith("/login") ||
|
||||
window.location.pathname.startsWith("/forget") ||
|
||||
window.location.pathname.startsWith("/prompt") ||
|
||||
window.location.pathname.startsWith("/result") ||
|
||||
window.location.pathname.startsWith("/cas") ||
|
||||
window.location.pathname.startsWith("/select-plan") ||
|
||||
window.location.pathname.startsWith("/buy-plan") ||
|
||||
window.location.pathname.startsWith("/qrcode") ||
|
||||
window.location.pathname.startsWith("/captcha");
|
||||
window.location.pathname.startsWith("/login") ||
|
||||
window.location.pathname.startsWith("/forget") ||
|
||||
window.location.pathname.startsWith("/prompt") ||
|
||||
window.location.pathname.startsWith("/result") ||
|
||||
window.location.pathname.startsWith("/cas") ||
|
||||
window.location.pathname.startsWith("/select-plan") ||
|
||||
window.location.pathname.startsWith("/buy-plan") ||
|
||||
window.location.pathname.startsWith("/qrcode") ||
|
||||
window.location.pathname.startsWith("/consent") ||
|
||||
window.location.pathname.startsWith("/captcha");
|
||||
}
|
||||
|
||||
onClick = ({key}) => {
|
||||
@@ -656,7 +657,7 @@ class App extends Component {
|
||||
menuVisible={this.state.menuVisible}
|
||||
logo={this.state.logo}
|
||||
onChangeTheme={this.setTheme}
|
||||
onClick = {this.onClick}
|
||||
onClick={this.onClick}
|
||||
onfinish={() => {
|
||||
this.setState({requiredEnableMfa: false});
|
||||
}}
|
||||
|
||||
@@ -248,6 +248,33 @@ class ApplicationEditPage extends React.Component {
|
||||
return value;
|
||||
}
|
||||
|
||||
trimCustomScopes(customScopes) {
|
||||
if (!Array.isArray(customScopes)) {
|
||||
return [];
|
||||
}
|
||||
return customScopes.map((item) => {
|
||||
const scope = (item?.scope || "").trim();
|
||||
const displayName = (item?.displayName || "").trim();
|
||||
const description = (item?.description || "").trim();
|
||||
return {
|
||||
...item,
|
||||
scope: scope,
|
||||
displayName: displayName,
|
||||
description: description,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
validateCustomScopes(customScopes) {
|
||||
const trimmed = this.trimCustomScopes(customScopes);
|
||||
for (const item of trimmed) {
|
||||
if (!item || !item.scope || item.scope === "") {
|
||||
return {ok: false, scopes: trimmed};
|
||||
}
|
||||
}
|
||||
return {ok: true, scopes: trimmed};
|
||||
}
|
||||
|
||||
updateApplicationField(key, value) {
|
||||
value = this.parseApplicationField(key, value);
|
||||
const application = this.state.application;
|
||||
@@ -506,157 +533,6 @@ class ApplicationEditPage extends React.Component {
|
||||
)}
|
||||
{this.state.activeMenuKey === "authentication" && (
|
||||
<React.Fragment>
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("provider:Client ID"), i18next.t("provider:Client ID - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<Input value={this.state.application.clientId} onChange={e => {
|
||||
this.updateApplicationField("clientId", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:Client secret - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<Input value={this.state.application.clientSecret} onChange={e => {
|
||||
this.updateApplicationField("clientSecret", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("application:Redirect URLs"), i18next.t("application:Redirect URLs - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<UrlTable
|
||||
title={i18next.t("application:Redirect URLs")}
|
||||
table={this.state.application.redirectUris}
|
||||
onUpdateTable={(value) => {this.updateApplicationField("redirectUris", value);}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("application:Forced redirect origin"), i18next.t("general:Forced redirect origin - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<Input prefix={<LinkOutlined />} value={this.state.application.forcedRedirectOrigin} onChange={e => {
|
||||
this.updateApplicationField("forcedRedirectOrigin", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("application:Grant types"), i18next.t("application:Grant types - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<Select virtual={false} mode="multiple" style={{width: "100%"}}
|
||||
value={this.state.application.grantTypes}
|
||||
onChange={(value => {
|
||||
this.updateApplicationField("grantTypes", value);
|
||||
})} >
|
||||
{
|
||||
[
|
||||
{id: "authorization_code", name: "Authorization Code"},
|
||||
{id: "password", name: "Password"},
|
||||
{id: "client_credentials", name: "Client Credentials"},
|
||||
{id: "token", name: "Token"},
|
||||
{id: "id_token", name: "ID Token"},
|
||||
{id: "refresh_token", name: "Refresh Token"},
|
||||
{id: "urn:ietf:params:oauth:grant-type:device_code", name: "Device Code"},
|
||||
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
(this.state.application.category === "Agent") ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("general:Scopes"), i18next.t("general:Scopes - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<ScopeTable
|
||||
title={i18next.t("general:Scopes")}
|
||||
table={this.state.application.scopes}
|
||||
onUpdateTable={(value) => {this.updateApplicationField("scopes", value);}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
) : null
|
||||
}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("application:Token format"), i18next.t("application:Token format - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.application.tokenFormat} onChange={(value => {this.updateApplicationField("tokenFormat", value);})}
|
||||
options={["JWT", "JWT-Empty", "JWT-Custom", "JWT-Standard"].map((item) => Setting.getOption(item, item))}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("application:Token signing method"), i18next.t("application:Token signing method - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.application.tokenSigningMethod === "" ? "RS256" : this.state.application.tokenSigningMethod} onChange={(value => {this.updateApplicationField("tokenSigningMethod", value);})}
|
||||
options={["RS256", "RS512", "ES256", "ES512", "ES384"].map((item) => Setting.getOption(item, item))}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("application:Token fields"), i18next.t("application:Token fields - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<Select virtual={false} disabled={this.state.application.tokenFormat !== "JWT-Custom"} mode="tags" showSearch style={{width: "100%"}} value={this.state.application.tokenFields} onChange={(value => {this.updateApplicationField("tokenFields", value);})}>
|
||||
<Option key={"signinMethod"} value={"signinMethod"}>{"SigninMethod"}</Option>
|
||||
<Option key={"provider"} value={"provider"}>{"Provider"}</Option>
|
||||
{
|
||||
[...Setting.getUserCommonFields(), "permissionNames"].map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
this.state.application.tokenFormat === "JWT-Custom" ? (<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Token attributes"), i18next.t("general:Token attributes - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<TokenAttributeTable
|
||||
title={i18next.t("general:Token attributes")}
|
||||
table={this.state.application.tokenAttributes}
|
||||
application={this.state.application}
|
||||
onUpdateTable={(value) => {this.updateApplicationField("tokenAttributes", value);}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>) : null
|
||||
}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("application:Token expire"), i18next.t("application:Token expire - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<InputNumber style={{width: "150px"}} value={this.state.application.expireInHours} min={0.01} step={1} precision={2} addonAfter="Hours" onChange={value => {
|
||||
this.updateApplicationField("expireInHours", value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("application:Refresh token expire"), i18next.t("application:Refresh token expire - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<InputNumber style={{width: "150px"}} value={this.state.application.refreshExpireInHours} min={0.01} step={1} precision={2} addonAfter="Hours" onChange={value => {
|
||||
this.updateApplicationField("refreshExpireInHours", value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("application:Cookie expire"), i18next.t("application:Cookie expire - Tooltip"))} :
|
||||
@@ -807,7 +683,167 @@ class ApplicationEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{this.state.activeMenuKey === "oidc-oauth" && (
|
||||
<React.Fragment>
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("provider:Client ID"), i18next.t("provider:Client ID - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<Input value={this.state.application.clientId} onChange={e => {
|
||||
this.updateApplicationField("clientId", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:Client secret - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<Input value={this.state.application.clientSecret} onChange={e => {
|
||||
this.updateApplicationField("clientSecret", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("application:Redirect URLs"), i18next.t("application:Redirect URLs - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<UrlTable
|
||||
title={i18next.t("application:Redirect URLs")}
|
||||
table={this.state.application.redirectUris}
|
||||
onUpdateTable={(value) => {this.updateApplicationField("redirectUris", value);}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("application:Forced redirect origin"), i18next.t("general:Forced redirect origin - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<Input prefix={<LinkOutlined />} value={this.state.application.forcedRedirectOrigin} onChange={e => {
|
||||
this.updateApplicationField("forcedRedirectOrigin", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("application:Grant types"), i18next.t("application:Grant types - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<Select virtual={false} mode="multiple" style={{width: "100%"}}
|
||||
value={this.state.application.grantTypes}
|
||||
onChange={(value => {
|
||||
this.updateApplicationField("grantTypes", value);
|
||||
})} >
|
||||
{
|
||||
[
|
||||
{id: "authorization_code", name: "Authorization Code"},
|
||||
{id: "password", name: "Password"},
|
||||
{id: "client_credentials", name: "Client Credentials"},
|
||||
{id: "token", name: "Token"},
|
||||
{id: "id_token", name: "ID Token"},
|
||||
{id: "refresh_token", name: "Refresh Token"},
|
||||
{id: "urn:ietf:params:oauth:grant-type:device_code", name: "Device Code"},
|
||||
{id: "urn:ietf:params:oauth:grant-type:jwt-bearer", name: "JWT Bearer"},
|
||||
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
(this.state.application.category === "Agent") ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("general:Scopes"), i18next.t("general:Scopes - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<ScopeTable
|
||||
title={i18next.t("general:Scopes")}
|
||||
table={this.state.application.scopes}
|
||||
onUpdateTable={(value) => {this.updateApplicationField("scopes", value);}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
) : null
|
||||
}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("application:Token format"), i18next.t("application:Token format - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.application.tokenFormat} onChange={(value => {this.updateApplicationField("tokenFormat", value);})}
|
||||
options={["JWT", "JWT-Empty", "JWT-Custom", "JWT-Standard"].map((item) => Setting.getOption(item, item))}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("application:Token signing method"), i18next.t("application:Token signing method - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.application.tokenSigningMethod === "" ? "RS256" : this.state.application.tokenSigningMethod} onChange={(value => {this.updateApplicationField("tokenSigningMethod", value);})}
|
||||
options={["RS256", "RS512", "ES256", "ES512", "ES384"].map((item) => Setting.getOption(item, item))}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("application:Token fields"), i18next.t("application:Token fields - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<Select virtual={false} disabled={this.state.application.tokenFormat !== "JWT-Custom"} mode="tags" showSearch style={{width: "100%"}} value={this.state.application.tokenFields} onChange={(value => {this.updateApplicationField("tokenFields", value);})}>
|
||||
<Option key={"signinMethod"} value={"signinMethod"}>{"SigninMethod"}</Option>
|
||||
<Option key={"provider"} value={"provider"}>{"Provider"}</Option>
|
||||
{
|
||||
[...Setting.getUserCommonFields(), "permissionNames"].map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
this.state.application.tokenFormat === "JWT-Custom" ? (<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Token attributes"), i18next.t("general:Token attributes - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<TokenAttributeTable
|
||||
title={i18next.t("general:Token attributes")}
|
||||
table={this.state.application.tokenAttributes}
|
||||
application={this.state.application}
|
||||
onUpdateTable={(value) => {this.updateApplicationField("tokenAttributes", value);}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>) : null
|
||||
}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("application:Token expire"), i18next.t("application:Token expire - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<InputNumber style={{width: "150px"}} value={this.state.application.expireInHours} min={0.01} step={1} precision={2} addonAfter="Hours" onChange={value => {
|
||||
this.updateApplicationField("expireInHours", value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("application:Refresh token expire"), i18next.t("application:Refresh token expire - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<InputNumber style={{width: "150px"}} value={this.state.application.refreshExpireInHours} min={0.01} step={1} precision={2} addonAfter="Hours" onChange={value => {
|
||||
this.updateApplicationField("refreshExpireInHours", value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{this.state.activeMenuKey === "saml" && (
|
||||
<React.Fragment>
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("application:SAML reply URL"), i18next.t("application:Redirect URL (Assertion Consumer Service POST Binding URL) - Tooltip"))} :
|
||||
</Col>
|
||||
@@ -1308,7 +1344,7 @@ class ApplicationEditPage extends React.Component {
|
||||
<React.Fragment>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("general:Cert"), i18next.t("general:Cert - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("application:Token cert"), i18next.t("application:Token cert - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.application.cert} onChange={(value => {this.updateApplicationField("cert", value);})}>
|
||||
@@ -1318,6 +1354,18 @@ class ApplicationEditPage extends React.Component {
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("application:Client cert"), i18next.t("application:Client cert - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.application.clientCert} onChange={(value => {this.updateApplicationField("clientCert", value);})}>
|
||||
{
|
||||
this.state.certs.map((cert, index) => <Option key={index} value={cert.name}>{cert.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("application:Failed signin limit"), i18next.t("application:Failed signin limit - Tooltip"))} :
|
||||
@@ -1353,7 +1401,7 @@ class ApplicationEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("general:IP whitelist"), i18next.t("general:IP whitelist - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<Input placeholder = {this.state.application.organizationObj?.ipWhitelist} value={this.state.application.ipWhitelist} onChange={e => {
|
||||
<Input placeholder={this.state.application.organizationObj?.ipWhitelist} value={this.state.application.ipWhitelist} onChange={e => {
|
||||
this.updateApplicationField("ipWhitelist", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
@@ -1452,7 +1500,7 @@ class ApplicationEditPage extends React.Component {
|
||||
<Layout style={{background: "inherit", height: "100%", overflow: "auto"}}>
|
||||
{
|
||||
this.state.menuMode === "horizontal" || !this.state.menuMode ? (
|
||||
<Header style={{background: "inherit", padding: "0px", position: "sticky", top: 0}}>
|
||||
<Header style={{background: "inherit", padding: "0px", position: "sticky", top: 0, height: 38, minHeight: 38}}>
|
||||
<div className="demo-logo" />
|
||||
<Tabs
|
||||
onChange={(key) => {
|
||||
@@ -1461,9 +1509,12 @@ class ApplicationEditPage extends React.Component {
|
||||
}}
|
||||
type="card"
|
||||
activeKey={this.state.activeMenuKey}
|
||||
tabBarStyle={{marginBottom: 0}}
|
||||
items={[
|
||||
{label: i18next.t("application:Basic"), key: "basic"},
|
||||
{label: i18next.t("application:Authentication"), key: "authentication"},
|
||||
{label: "OIDC/OAuth", key: "oidc-oauth"},
|
||||
{label: "SAML", key: "saml"},
|
||||
{label: i18next.t("application:Providers"), key: "providers"},
|
||||
{label: i18next.t("application:UI Customization"), key: "ui-customization"},
|
||||
{label: i18next.t("application:Security"), key: "security"},
|
||||
@@ -1488,6 +1539,8 @@ class ApplicationEditPage extends React.Component {
|
||||
>
|
||||
<Menu.Item key="basic">{i18next.t("application:Basic")}</Menu.Item>
|
||||
<Menu.Item key="authentication">{i18next.t("application:Authentication")}</Menu.Item>
|
||||
<Menu.Item key="oidc-oauth">OIDC/OAuth</Menu.Item>
|
||||
<Menu.Item key="saml">SAML</Menu.Item>
|
||||
<Menu.Item key="providers">{i18next.t("application:Providers")}</Menu.Item>
|
||||
<Menu.Item key="ui-customization">{i18next.t("application:UI Customization")}</Menu.Item>
|
||||
<Menu.Item key="security">{i18next.t("application:Security")}</Menu.Item>
|
||||
@@ -1547,11 +1600,11 @@ class ApplicationEditPage extends React.Component {
|
||||
{
|
||||
Setting.isPasswordEnabled(this.state.application) ? (
|
||||
<div className="loginBackground" style={{backgroundImage: `url(${this.state.application?.formBackgroundUrl})`, overflow: "auto"}}>
|
||||
<SignupPage application={this.state.application} preview = "auto" />
|
||||
<SignupPage application={this.state.application} preview="auto" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="loginBackground" style={{backgroundImage: `url(${this.state.application?.formBackgroundUrl})`, overflow: "auto"}}>
|
||||
<LoginPage type={"login"} mode={"signup"} application={this.state.application} preview = "auto" />
|
||||
<LoginPage type={"login"} mode={"signup"} application={this.state.application} preview="auto" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1577,7 +1630,7 @@ class ApplicationEditPage extends React.Component {
|
||||
}}>
|
||||
<div style={{position: "relative", width: previewWidth, border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", overflow: "auto"}}>
|
||||
<div className="loginBackground" style={{backgroundImage: `url(${this.state.application?.formBackgroundUrl})`, overflow: "auto"}}>
|
||||
<LoginPage type={"login"} mode={"signin"} application={this.state.application} preview = "auto" />
|
||||
<LoginPage type={"login"} mode={"signin"} application={this.state.application} preview="auto" />
|
||||
</div>
|
||||
<div style={{overflow: "auto", ...maskStyle}} />
|
||||
</div>
|
||||
@@ -1621,6 +1674,12 @@ class ApplicationEditPage extends React.Component {
|
||||
const application = Setting.deepCopy(this.state.application);
|
||||
application.providers = application.providers?.filter(provider => this.state.providers.map(provider => provider.name).includes(provider.name));
|
||||
application.signinMethods = application.signinMethods?.filter(signinMethod => ["Password", "Verification code", "WebAuthn", "LDAP", "Face ID", "WeChat"].includes(signinMethod.name));
|
||||
const customScopeValidation = this.validateCustomScopes(application.customScopes);
|
||||
application.customScopes = customScopeValidation.scopes;
|
||||
if (!customScopeValidation.ok) {
|
||||
Setting.showMessage("error", `${i18next.t("general:Name")}: ${i18next.t("provider:This field is required")}`);
|
||||
return;
|
||||
}
|
||||
|
||||
ApplicationBackend.updateApplication("admin", this.state.applicationName, application)
|
||||
.then((res) => {
|
||||
|
||||
@@ -81,6 +81,24 @@ class CertEditPage extends React.Component {
|
||||
value = this.parseCertField(key, value);
|
||||
|
||||
const cert = this.state.cert;
|
||||
const previousType = cert.type;
|
||||
if (key === "type") {
|
||||
if (value === "SSL") {
|
||||
cert.cryptoAlgorithm = "RSA";
|
||||
cert.certificate = "";
|
||||
cert.privateKey = "";
|
||||
} else if (previousType === "SSL" && value !== "SSL") {
|
||||
// Clear SSL-specific sensitive and derived fields when leaving SSL type
|
||||
cert.provider = "";
|
||||
cert.account = "";
|
||||
cert.accessKey = "";
|
||||
cert.accessSecret = "";
|
||||
cert.certificate = "";
|
||||
cert.privateKey = "";
|
||||
cert.expireTime = "";
|
||||
cert.domainExpireTime = "";
|
||||
}
|
||||
}
|
||||
cert[key] = value;
|
||||
this.setState({
|
||||
cert: cert,
|
||||
@@ -157,6 +175,7 @@ class CertEditPage extends React.Component {
|
||||
})}>
|
||||
{
|
||||
[
|
||||
{id: "SSL", name: "SSL"},
|
||||
{id: "x509", name: "x509"},
|
||||
{id: "Payment", name: "Payment"},
|
||||
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
||||
@@ -184,7 +203,10 @@ class CertEditPage extends React.Component {
|
||||
this.updateCertField("privateKey", "");
|
||||
})}>
|
||||
{
|
||||
[
|
||||
(this.state.cert.type === "SSL" ? [
|
||||
{id: "RSA", name: "RSA"},
|
||||
{id: "ECC", name: "ECC"},
|
||||
] : [
|
||||
{id: "RS256", name: "RS256 (RSA + SHA256)"},
|
||||
{id: "RS384", name: "RS384 (RSA + SHA384)"},
|
||||
{id: "RS512", name: "RS512 (RSA + SHA512)"},
|
||||
@@ -194,13 +216,13 @@ class CertEditPage extends React.Component {
|
||||
{id: "PS256", name: "PS256 (RSASSA-PSS using SHA256 and MGF1 with SHA256)"},
|
||||
{id: "PS384", name: "PS384 (RSASSA-PSS using SHA384 and MGF1 with SHA384)"},
|
||||
{id: "PS512", name: "PS512 (RSASSA-PSS using SHA512 and MGF1 with SHA512)"},
|
||||
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
||||
]).map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
this.state.cert.cryptoAlgorithm.startsWith("ES") ? null : (
|
||||
this.state.cert.cryptoAlgorithm.startsWith("ES") || this.state.cert.type === "SSL" ? null : (
|
||||
<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"))} :
|
||||
@@ -219,16 +241,91 @@ class CertEditPage extends React.Component {
|
||||
</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>
|
||||
{
|
||||
this.state.cert.type === "SSL" ? null : (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.cert.type === "SSL" ? (
|
||||
<React.Fragment>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("cert:Expire time")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={Setting.getFormattedDate(this.state.cert.expireTime)} onChange={e => {
|
||||
this.updateCertField("expireTime", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("cert:Domain expire")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={Setting.getFormattedDate(this.state.cert.domainExpireTime)} onChange={e => {
|
||||
this.updateCertField("domainExpireTime", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("cert:Provider")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.cert.provider} onChange={(value => {this.updateCertField("provider", value);})}>
|
||||
{
|
||||
[
|
||||
{id: "GoDaddy", name: "GoDaddy"},
|
||||
{id: "Aliyun", name: "Aliyun"},
|
||||
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("cert:Account")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.cert.account} onChange={e => {
|
||||
this.updateCertField("account", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("cert:Access key")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.cert.accessKey} onChange={e => {
|
||||
this.updateCertField("accessKey", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("cert:Access secret")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input.Password value={this.state.cert.accessSecret} onChange={e => {
|
||||
this.updateCertField("accessSecret", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
) : null
|
||||
}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("cert:Certificate"), i18next.t("cert:Certificate - Tooltip"))} :
|
||||
|
||||
@@ -88,6 +88,28 @@ class CertListPage extends BaseListPage {
|
||||
});
|
||||
}
|
||||
|
||||
refreshCert(i) {
|
||||
const cert = this.state.data[i];
|
||||
CertBackend.refreshDomainExpire(cert.owner, cert.name)
|
||||
.then((res) => {
|
||||
if (res.status === "error") {
|
||||
Setting.showMessage("error", `Failed to refresh domain expire: ${res.msg}`);
|
||||
} else {
|
||||
Setting.showMessage("success", "Domain expire refreshed successfully");
|
||||
this.fetch({
|
||||
pagination: {
|
||||
...this.state.pagination,
|
||||
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
)
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `Domain expire failed to refresh: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
renderTable(certs) {
|
||||
const columns = [
|
||||
{
|
||||
@@ -194,6 +216,12 @@ class CertListPage extends BaseListPage {
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
record.type === "SSL" ? (
|
||||
<Button disabled={!Setting.isAdminUser(this.props.account) && (record.owner !== this.props.account.owner)} style={{margin: "10px 10px 10px 0"}} type="default" onClick={() => this.refreshCert(index)}>{i18next.t("general:Refresh")}
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
<Button disabled={!Setting.isAdminUser(this.props.account) && (record.owner !== this.props.account.owner)} style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/certs/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<PopconfirmModal
|
||||
disabled={!Setting.isAdminUser(this.props.account) && (record.owner !== this.props.account.owner)}
|
||||
|
||||
@@ -26,6 +26,7 @@ import LoginPage from "./auth/LoginPage";
|
||||
import SelfForgetPage from "./auth/SelfForgetPage";
|
||||
import ForgetPage from "./auth/ForgetPage";
|
||||
import PromptPage from "./auth/PromptPage";
|
||||
import ConsentPage from "./auth/ConsentPage";
|
||||
import ResultPage from "./auth/ResultPage";
|
||||
import CasLogout from "./auth/CasLogout";
|
||||
import {authConfig} from "./auth/Auth";
|
||||
@@ -125,6 +126,7 @@ class EntryPage extends React.Component {
|
||||
<Route exact path="/forget/:applicationName" render={(props) => <ForgetPage {...this.props} account={this.props.account} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />} />
|
||||
<Route exact path="/prompt" render={(props) => this.renderLoginIfNotLoggedIn(<PromptPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
|
||||
<Route exact path="/prompt/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<PromptPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
|
||||
<Route exact path="/consent/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<ConsentPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
|
||||
<Route exact path="/result" render={(props) => this.renderHomeIfLoggedIn(<ResultPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
|
||||
<Route exact path="/result/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<ResultPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
|
||||
<Route exact path="/cas/:owner/:casApplicationName/logout" render={(props) => this.renderHomeIfLoggedIn(<CasLogout {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
|
||||
|
||||
@@ -20,7 +20,7 @@ import React, {useState} from "react";
|
||||
import i18next from "i18next";
|
||||
import {
|
||||
AppstoreTwoTone,
|
||||
BarsOutlined, DeploymentUnitOutlined, DollarTwoTone, DownOutlined,
|
||||
BarsOutlined, CheckCircleTwoTone, DeploymentUnitOutlined, DollarTwoTone, DownOutlined,
|
||||
HomeTwoTone,
|
||||
LockTwoTone, LogoutOutlined,
|
||||
SafetyCertificateTwoTone, SettingOutlined, SettingTwoTone,
|
||||
@@ -104,6 +104,10 @@ import TicketListPage from "./TicketListPage";
|
||||
import TicketEditPage from "./TicketEditPage";
|
||||
import * as Cookie from "cookie";
|
||||
import * as UserBackend from "./backend/UserBackend";
|
||||
import SiteListPage from "./SiteListPage";
|
||||
import SiteEditPage from "./SiteEditPage";
|
||||
import RuleEditPage from "./RuleEditPage";
|
||||
import RuleListPage from "./RuleListPage";
|
||||
|
||||
function ManagementPage(props) {
|
||||
const [menuVisible, setMenuVisible] = useState(false);
|
||||
@@ -193,12 +197,12 @@ function ManagementPage(props) {
|
||||
{
|
||||
renderAvatar()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
{Setting.isMobile() ? null : Setting.getShortText(Setting.getNameAtLeast(props.account.displayName), 30)} <DownOutlined />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</Dropdown>
|
||||
);
|
||||
@@ -252,15 +256,15 @@ function ManagementPage(props) {
|
||||
{renderRightDropdown()}
|
||||
{renderWidgets()}
|
||||
{Setting.isAdminUser(props.account) && (props.uri.indexOf("/trees") === -1) &&
|
||||
<OrganizationSelect
|
||||
initValue={Setting.getOrganization()}
|
||||
withAll={true}
|
||||
className="org-select"
|
||||
style={{display: Setting.isMobile() ? "none" : "flex"}}
|
||||
onChange={(value) => {
|
||||
Setting.setOrganization(value);
|
||||
}}
|
||||
/>
|
||||
<OrganizationSelect
|
||||
initValue={Setting.getOrganization()}
|
||||
withAll={true}
|
||||
className="org-select"
|
||||
style={{display: Setting.isMobile() ? "none" : "flex"}}
|
||||
onChange={(value) => {
|
||||
Setting.setOrganization(value);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
@@ -287,9 +291,9 @@ function ManagementPage(props) {
|
||||
|
||||
!Setting.isMobile() ? res.push({
|
||||
label:
|
||||
<Link to="/">
|
||||
<img className="logo" src={logo ?? props.logo} alt="logo" />
|
||||
</Link>,
|
||||
<Link to="/">
|
||||
<img className="logo" src={logo ?? props.logo} alt="logo" />
|
||||
</Link>,
|
||||
disabled: true, key: "logo",
|
||||
style: {
|
||||
padding: 0,
|
||||
@@ -323,6 +327,8 @@ function ManagementPage(props) {
|
||||
Setting.getItem(<Link to="/providers">{i18next.t("application:Providers")}</Link>, "/providers"),
|
||||
Setting.getItem(<Link to="/resources">{i18next.t("general:Resources")}</Link>, "/resources"),
|
||||
Setting.getItem(<Link to="/certs">{i18next.t("general:Certs")}</Link>, "/certs"),
|
||||
Setting.getItem(<Link to="/sites">{i18next.t("general:Sites")}</Link>, "/sites"),
|
||||
Setting.getItem(<Link to="/rules">{i18next.t("general:Rules")}</Link>, "/rules"),
|
||||
]));
|
||||
|
||||
res.push(Setting.getItem(<Link style={{color: textColor}} to="/roles">{i18next.t("general:Authorization")}</Link>, "/auth", <SafetyCertificateTwoTone twoToneColor={twoToneColor} />, [
|
||||
@@ -339,6 +345,12 @@ function ManagementPage(props) {
|
||||
}
|
||||
})));
|
||||
|
||||
res.push(Setting.getItem(<Link style={{color: textColor}} to="/sites">{i18next.t("general:Gateway")}</Link>, "/gateway", <CheckCircleTwoTone twoToneColor={twoToneColor} />, [
|
||||
Setting.getItem(<Link to="/sites">{i18next.t("general:Sites")}</Link>, "/sites"),
|
||||
Setting.getItem(<Link to="/certs">{i18next.t("general:Certs")}</Link>, "/certs"),
|
||||
Setting.getItem(<Link to="/rules">{i18next.t("general:Rules")}</Link>, "/rules"),
|
||||
]));
|
||||
|
||||
res.push(Setting.getItem(<Link style={{color: textColor}} to="/sessions">{i18next.t("general:Logging & Auditing")}</Link>, "/logs", <WalletTwoTone twoToneColor={twoToneColor} />, [
|
||||
Setting.getItem(<Link to="/sessions">{i18next.t("general:Sessions")}</Link>, "/sessions"),
|
||||
Conf.CasvisorUrl ? Setting.getItem(<a target="_blank" rel="noreferrer" href={Conf.CasvisorUrl}>{i18next.t("general:Records")}</a>, "/records")
|
||||
@@ -473,6 +485,10 @@ function ManagementPage(props) {
|
||||
<Route exact path="/resources" render={(props) => renderLoginIfNotLoggedIn(<ResourceListPage account={account} {...props} />)} />
|
||||
<Route exact path="/certs" render={(props) => renderLoginIfNotLoggedIn(<CertListPage account={account} {...props} />)} />
|
||||
<Route exact path="/certs/:organizationName/:certName" render={(props) => renderLoginIfNotLoggedIn(<CertEditPage account={account} {...props} />)} />
|
||||
<Route exact path="/sites" render={(props) => renderLoginIfNotLoggedIn(<SiteListPage account={account} {...props} />)} />
|
||||
<Route exact path="/sites/:organizationName/:siteName" render={(props) => renderLoginIfNotLoggedIn(<SiteEditPage account={account} {...props} />)} />
|
||||
<Route exact path="/rules" render={(props) => renderLoginIfNotLoggedIn(<RuleListPage account={account} {...props} />)} />
|
||||
<Route exact path="/rules/:organizationName/:ruleName" render={(props) => renderLoginIfNotLoggedIn(<RuleEditPage account={account} {...props} />)} />
|
||||
<Route exact path="/verifications" render={(props) => renderLoginIfNotLoggedIn(<VerificationListPage account={account} {...props} />)} />
|
||||
<Route exact path="/roles" render={(props) => renderLoginIfNotLoggedIn(<RoleListPage account={account} {...props} />)} />
|
||||
<Route exact path="/roles/:organizationName/:roleName" render={(props) => renderLoginIfNotLoggedIn(<RoleEditPage account={account} {...props} />)} />
|
||||
|
||||
@@ -564,7 +564,7 @@ class OrganizationEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("organization:Balance credit"), i18next.t("organization:Balance credit - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={4} >
|
||||
<InputNumber value={this.state.organization.balanceCredit ?? 0} onChange={value => {
|
||||
<InputNumber value={this.state.organization.balanceCredit ?? 0} max={0} onChange={value => {
|
||||
this.updateOrganizationField("balanceCredit", value);
|
||||
}} />
|
||||
</Col>
|
||||
|
||||
@@ -57,6 +57,8 @@ class OrganizationListPage extends BaseListPage {
|
||||
{name: "ID", visible: true, viewRule: "Public", modifyRule: "Immutable"},
|
||||
{name: "Name", visible: true, viewRule: "Public", modifyRule: "Admin"},
|
||||
{name: "Display name", visible: true, viewRule: "Public", modifyRule: "Self"},
|
||||
{name: "First name", visible: true, viewRule: "Public", modifyRule: "Self"},
|
||||
{name: "Last name", visible: true, viewRule: "Public", modifyRule: "Self"},
|
||||
{name: "Avatar", visible: true, viewRule: "Public", modifyRule: "Self"},
|
||||
{name: "User type", visible: true, viewRule: "Public", modifyRule: "Admin"},
|
||||
{name: "Password", visible: true, viewRule: "Self", modifyRule: "Self"},
|
||||
@@ -66,6 +68,7 @@ class OrganizationListPage extends BaseListPage {
|
||||
{name: "Country/Region", visible: true, viewRule: "Public", modifyRule: "Self"},
|
||||
{name: "Location", visible: true, viewRule: "Public", modifyRule: "Self"},
|
||||
{name: "Address", visible: true, viewRule: "Public", modifyRule: "Self"},
|
||||
{name: "Addresses", visible: true, viewRule: "Public", modifyRule: "Self"},
|
||||
{name: "Affiliation", visible: true, viewRule: "Public", modifyRule: "Self"},
|
||||
{name: "Title", visible: true, viewRule: "Public", modifyRule: "Self"},
|
||||
{name: "ID card type", visible: true, viewRule: "Public", modifyRule: "Self"},
|
||||
@@ -86,6 +89,8 @@ class OrganizationListPage extends BaseListPage {
|
||||
{name: "Balance", visible: true, viewRule: "Public", modifyRule: "Admin"},
|
||||
{name: "Balance credit", visible: true, viewRule: "Public", modifyRule: "Admin"},
|
||||
{name: "Balance currency", visible: true, viewRule: "Public", modifyRule: "Admin"},
|
||||
{name: "Cart", visible: true, viewRule: "Self", modifyRule: "Self"},
|
||||
{name: "Transactions", visible: true, viewRule: "Self", modifyRule: "Self"},
|
||||
{name: "Signup application", visible: true, viewRule: "Public", modifyRule: "Admin"},
|
||||
{name: "Register type", visible: true, viewRule: "Public", modifyRule: "Admin"},
|
||||
{name: "Register source", visible: true, viewRule: "Public", modifyRule: "Admin"},
|
||||
@@ -93,16 +98,22 @@ class OrganizationListPage extends BaseListPage {
|
||||
{name: "Groups", visible: true, viewRule: "Public", modifyRule: "Admin"},
|
||||
{name: "Roles", visible: true, viewRule: "Public", modifyRule: "Immutable"},
|
||||
{name: "Permissions", visible: true, viewRule: "Public", modifyRule: "Immutable"},
|
||||
{name: "Consents", visible: true, viewRule: "Self", modifyRule: "Self"},
|
||||
{name: "3rd-party logins", visible: true, viewRule: "Self", modifyRule: "Self"},
|
||||
{name: "Properties", visible: false, viewRule: "Admin", modifyRule: "Admin"},
|
||||
{name: "Is online", visible: true, viewRule: "Admin", modifyRule: "Admin"},
|
||||
{name: "Is admin", visible: true, viewRule: "Admin", modifyRule: "Admin"},
|
||||
{name: "Is forbidden", visible: true, viewRule: "Admin", modifyRule: "Admin"},
|
||||
{name: "Is deleted", visible: true, viewRule: "Admin", modifyRule: "Admin"},
|
||||
{Name: "Multi-factor authentication", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "WebAuthn credentials", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "Managed accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "MFA accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{name: "Multi-factor authentication", visible: true, viewRule: "Self", modifyRule: "Self"},
|
||||
{name: "MFA items", visible: true, viewRule: "Self", modifyRule: "Self"},
|
||||
{name: "WebAuthn credentials", visible: true, viewRule: "Self", modifyRule: "Self"},
|
||||
{name: "Last change password time", visible: true, viewRule: "Admin", modifyRule: "Admin"},
|
||||
{name: "Managed accounts", visible: true, viewRule: "Self", modifyRule: "Self"},
|
||||
{name: "Face ID", visible: true, viewRule: "Self", modifyRule: "Self"},
|
||||
{name: "MFA accounts", visible: true, viewRule: "Self", modifyRule: "Self"},
|
||||
{name: "Need update password", visible: true, viewRule: "Admin", modifyRule: "Admin"},
|
||||
{name: "IP whitelist", visible: true, viewRule: "Admin", modifyRule: "Admin"},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import {InfoCircleTwoTone} from "@ant-design/icons";
|
||||
import * as PaymentBackend from "./backend/PaymentBackend";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
import * as ProductBackend from "./backend/ProductBackend";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
@@ -30,7 +29,6 @@ class PaymentEditPage extends React.Component {
|
||||
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
|
||||
paymentName: props.match.params.paymentName,
|
||||
payment: null,
|
||||
products: [],
|
||||
isModalVisible: false,
|
||||
isInvoiceLoading: false,
|
||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||
@@ -39,7 +37,6 @@ class PaymentEditPage extends React.Component {
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getPayment();
|
||||
this.getProducts();
|
||||
}
|
||||
|
||||
getPayment() {
|
||||
@@ -58,19 +55,6 @@ class PaymentEditPage extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
getProducts() {
|
||||
ProductBackend.getProducts(this.state.organizationName)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
products: res.data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `Failed to get products: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
goToViewOrder() {
|
||||
const payment = this.state.payment;
|
||||
if (payment && payment.order) {
|
||||
@@ -240,29 +224,6 @@ class PaymentEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Products"), i18next.t("payment:Products - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select
|
||||
mode="multiple"
|
||||
style={{width: "100%"}}
|
||||
value={this.state.payment?.products || []}
|
||||
disabled={isViewMode}
|
||||
allowClear
|
||||
options={(this.state.products || [])
|
||||
.map((p) => ({
|
||||
label: Setting.getLanguageText(p?.displayName) || p?.name,
|
||||
value: p?.name,
|
||||
}))
|
||||
.filter((o) => o.value)}
|
||||
onChange={(value) => {
|
||||
this.updatePaymentField("products", value);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("order:Price"), i18next.t("plan:Price - Tooltip"))} :
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
import React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {Button, List, Table, Tooltip} from "antd";
|
||||
import {Button, Col, List, Row, Table, Tooltip} from "antd";
|
||||
import moment from "moment";
|
||||
import * as Setting from "./Setting";
|
||||
import * as PaymentBackend from "./backend/PaymentBackend";
|
||||
@@ -195,21 +195,31 @@ class PaymentListPage extends BaseListPage {
|
||||
paddingBottom: 8,
|
||||
}}
|
||||
renderItem={(productInfo, i) => {
|
||||
const price = productInfo.price * (productInfo.quantity || 1);
|
||||
const price = productInfo.price || 0;
|
||||
const number = productInfo.quantity || 1;
|
||||
const currency = record.currency || "USD";
|
||||
const productName = productInfo.displayName || productInfo.name;
|
||||
return (
|
||||
<List.Item>
|
||||
<div style={{display: "inline"}}>
|
||||
<Tooltip placement="topLeft" title={i18next.t("general:Edit")}>
|
||||
<Button style={{marginRight: "5px"}} icon={<EditOutlined />} size="small" onClick={() => Setting.goToLinkSoft(this, `/products/${record.owner}/${productInfo.name}`)} />
|
||||
</Tooltip>
|
||||
<Link to={`/products/${record.owner}/${productInfo.name}`}>
|
||||
{productInfo.displayName || productInfo.name}
|
||||
</Link>
|
||||
<span style={{marginLeft: "8px", color: "#666"}}>
|
||||
{Setting.getPriceDisplay(price, currency)}
|
||||
</span>
|
||||
</div>
|
||||
<Row style={{width: "100%"}} wrap={false} gutter={[12, 0]}>
|
||||
<Col flex="auto" style={{minWidth: 0}}>
|
||||
<div style={{display: "flex", alignItems: "center", minWidth: 0}}>
|
||||
<Tooltip placement="topLeft" title={i18next.t("general:Edit")}>
|
||||
<Button style={{marginRight: "5px"}} icon={<EditOutlined />} size="small" onClick={() => Setting.goToLinkSoft(this, `/products/${record.owner}/${productInfo.name}`)} />
|
||||
</Tooltip>
|
||||
<Tooltip placement="topLeft" title={productName}>
|
||||
<Link to={`/products/${record.owner}/${productInfo.name}`} style={{display: "inline-block", maxWidth: "100%", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap"}}>
|
||||
{productName}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Col>
|
||||
<Col flex="none" style={{whiteSpace: "nowrap"}}>
|
||||
<span style={{color: "#666"}}>
|
||||
{Setting.getCurrencySymbol(currency)}{price} ({Setting.getCurrencyText(currency)}) × {number}
|
||||
</span>
|
||||
</Col>
|
||||
</Row>
|
||||
</List.Item>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -307,16 +307,6 @@ class ProductEditPage extends React.Component {
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("product:Return URL"), i18next.t("product:Return URL - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input prefix={<LinkOutlined />} value={this.state.product.returnUrl} disabled={isViewMode} onChange={e => {
|
||||
this.updateProductField("returnUrl", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("product:Success URL"), i18next.t("product:Success URL - Tooltip"))} :
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -137,7 +137,7 @@ class RecordListPage extends BaseListPage {
|
||||
title: i18next.t("record:Status code"),
|
||||
dataIndex: "statusCode",
|
||||
key: "statusCode",
|
||||
width: "120px",
|
||||
width: "140px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("statusCode"),
|
||||
},
|
||||
|
||||
309
web/src/RuleEditPage.js
Normal file
309
web/src/RuleEditPage.js
Normal file
@@ -0,0 +1,309 @@
|
||||
// Copyright 2023 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, Switch} from "antd";
|
||||
import * as Setting from "./Setting";
|
||||
import * as RuleBackend from "./backend/RuleBackend";
|
||||
import i18next from "i18next";
|
||||
import WafRuleTable from "./table/WafRuleTable";
|
||||
import IpRuleTable from "./table/IpRuleTable";
|
||||
import UaRuleTable from "./table/UaRuleTable";
|
||||
import IpRateRuleTable from "./table/IpRateRuleTable";
|
||||
import CompoundRule from "./common/CompoundRule";
|
||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
class RuleEditPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
owner: props.match.params.organizationName,
|
||||
ruleName: props.match.params.ruleName,
|
||||
rule: null,
|
||||
organizations: [],
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getRule();
|
||||
this.getOrganizations();
|
||||
}
|
||||
|
||||
getRule() {
|
||||
RuleBackend.getRule(this.state.owner, this.state.ruleName).then((res) => {
|
||||
this.setState({
|
||||
rule: res.data,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateRuleField(key, value) {
|
||||
const rule = Setting.deepCopy(this.state.rule);
|
||||
rule[key] = value;
|
||||
if (key === "type") {
|
||||
rule.expressions = [];
|
||||
}
|
||||
this.setState({
|
||||
rule: rule,
|
||||
});
|
||||
}
|
||||
|
||||
updateRuleFieldInExpressions(index, key, value) {
|
||||
const rule = Setting.deepCopy(this.state.rule);
|
||||
rule.expressions[index][key] = value;
|
||||
this.updateRuleField("expressions", rule.expressions);
|
||||
this.setState({
|
||||
rule: rule,
|
||||
});
|
||||
}
|
||||
|
||||
getOrganizations() {
|
||||
if (Setting.isAdminUser(this.props.account)) {
|
||||
OrganizationBackend.getOrganizations("admin")
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
organizations: res.data || [],
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderRule() {
|
||||
return (
|
||||
<Card size="small" title={
|
||||
<div>
|
||||
{i18next.t("rule:Edit Rule")}
|
||||
<Button type="primary" onClick={this.submitRuleEdit.bind(this)}>{i18next.t("general:Save")}</Button>
|
||||
</div>
|
||||
} style={{marginTop: 10}} type="inner">
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col span={2} style={{marginTop: "5px"}}>
|
||||
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} disabled={!Setting.isAdminUser(this.props.account)} value={this.state.rule.owner} onChange={(value => {
|
||||
this.updateRuleField("owner", value);
|
||||
})}>
|
||||
{
|
||||
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col span={2} style={{marginTop: "5px"}}>
|
||||
{i18next.t("general:Name")}:
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<Input value={this.state.rule.name} onChange={e => {
|
||||
this.updateRuleField("name", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col span={2} style={{marginTop: "5px"}}>
|
||||
{i18next.t("rule:Type")}:
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<Select virtual={false} value={this.state.rule.type} style={{width: "100%"}} onChange={value => {
|
||||
this.updateRuleField("type", value);
|
||||
}}>
|
||||
{
|
||||
[
|
||||
{value: "WAF", text: "WAF"},
|
||||
{value: "IP", text: "IP"},
|
||||
{value: "User-Agent", text: "User-Agent"},
|
||||
{value: "IP Rate Limiting", text: i18next.t("rule:IP Rate Limiting")},
|
||||
{value: "Compound", text: i18next.t("rule:Compound")},
|
||||
].map((item, index) => <Option key={index} value={item.value}>{item.text}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("rule:Expressions")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
{
|
||||
this.state.rule.type === "WAF" ? (
|
||||
<WafRuleTable
|
||||
title={"Seclang"}
|
||||
table={this.state.rule.expressions}
|
||||
ruleName={this.state.rule.name}
|
||||
account={this.props.account}
|
||||
onUpdateTable={(value) => {this.updateRuleField("expressions", value);}}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
this.state.rule.type === "IP" ? (
|
||||
<IpRuleTable
|
||||
title={"IPs"}
|
||||
table={this.state.rule.expressions}
|
||||
ruleName={this.state.rule.name}
|
||||
account={this.props.account}
|
||||
onUpdateTable={(value) => {this.updateRuleField("expressions", value);}}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
this.state.rule.type === "User-Agent" ? (
|
||||
<UaRuleTable
|
||||
title={"User-Agents"}
|
||||
table={this.state.rule.expressions}
|
||||
ruleName={this.state.rule.name}
|
||||
account={this.props.account}
|
||||
onUpdateTable={(value) => {this.updateRuleField("expressions", value);}}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
this.state.rule.type === "IP Rate Limiting" ? (
|
||||
<IpRateRuleTable
|
||||
title={i18next.t("rule:IP Rate Limiting")}
|
||||
table={this.state.rule.expressions}
|
||||
ruleName={this.state.rule.name}
|
||||
account={this.props.account}
|
||||
onUpdateTable={(value) => {this.updateRuleField("expressions", value);}}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
this.state.rule.type === "Compound" ? (
|
||||
<CompoundRule
|
||||
title={i18next.t("rule:Compound")}
|
||||
table={this.state.rule.expressions}
|
||||
ruleName={this.state.rule.name}
|
||||
owner={this.state.owner}
|
||||
onUpdateTable={(value) => {this.updateRuleField("expressions", value);}} />
|
||||
) : null
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
this.state.rule.type !== "WAF" && (
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col span={2} style={{marginTop: "5px"}}>
|
||||
{i18next.t("general:Action")}:
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<Select virtual={false} value={this.state.rule.action} defaultValue={"Block"} style={{width: "100%"}} onChange={(value) => {
|
||||
this.updateRuleField("action", value);
|
||||
}}>
|
||||
{
|
||||
[
|
||||
{value: "Allow", text: i18next.t("rule:Allow")},
|
||||
{value: "Block", text: i18next.t("rule:Block")},
|
||||
].map((item, index) => <Option key={index} value={item.value}>{item.text}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.rule.type !== "WAF" && (this.state.rule.action === "Allow" || this.state.rule.action === "Block") && (
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col span={2} style={{marginTop: "5px"}}>
|
||||
{i18next.t("rule:Status code")}:
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<InputNumber value={this.state.rule.statusCode} min={100} max={599} onChange={e => {
|
||||
this.updateRuleField("statusCode", e);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
{
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col span={2} style={{marginTop: "5px"}}>
|
||||
{i18next.t("rule:Reason")}:
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<Input value={this.state.rule.reason}
|
||||
onChange={e => {
|
||||
this.updateRuleField("reason", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
}
|
||||
{
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col span={2} style={{marginTop: "5px"}}>
|
||||
{i18next.t("rule:Verbose mode")}:
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<Switch checked={this.state.rule.isVerbose}
|
||||
onChange={checked => {
|
||||
this.updateRuleField("isVerbose", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Row style={{width: "100%"}}>
|
||||
<Col span={1}>
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
{
|
||||
this.state.rule !== null ? this.renderRule() : null
|
||||
}
|
||||
</Col>
|
||||
<Col span={1}>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{margin: 10}}>
|
||||
<Col span={2}>
|
||||
</Col>
|
||||
<Col span={18}>
|
||||
<Button type="primary" size="large" onClick={this.submitRuleEdit.bind(this)}>{i18next.t("general:Save")}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
submitRuleEdit() {
|
||||
const rule = Setting.deepCopy(this.state.rule);
|
||||
RuleBackend.updateRule(this.state.owner, this.state.ruleName, rule)
|
||||
.then((res) => {
|
||||
if (res.status !== "error") {
|
||||
Setting.showMessage("success", "Rule updated successfully");
|
||||
this.setState({
|
||||
rule: rule,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `Rule failed to update: ${res.msg}`);
|
||||
this.setState({
|
||||
ruleName: this.state.rule.name,
|
||||
});
|
||||
this.props.history.push(`/rules/${this.state.rule.owner}/${this.state.rule.name}`);
|
||||
this.getRule();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default RuleEditPage;
|
||||
236
web/src/RuleListPage.js
Normal file
236
web/src/RuleListPage.js
Normal file
@@ -0,0 +1,236 @@
|
||||
// Copyright 2021 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.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Popconfirm, Table, Tag} from "antd";
|
||||
import moment from "moment";
|
||||
import * as Setting from "./Setting";
|
||||
import * as RuleBackend from "./backend/RuleBackend";
|
||||
import i18next from "i18next";
|
||||
import BaseListPage from "./BaseListPage";
|
||||
|
||||
class RuleListPage extends BaseListPage {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.setState({
|
||||
pagination: {
|
||||
...this.state.pagination,
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
});
|
||||
this.fetch({pagination: this.state.pagination});
|
||||
}
|
||||
|
||||
fetch = (params = {}) => {
|
||||
const sortField = params.sortField, sortOrder = params.sortOrder;
|
||||
if (!params.pagination) {
|
||||
params.pagination = {current: 1, pageSize: 10};
|
||||
}
|
||||
this.setState({
|
||||
loading: true,
|
||||
});
|
||||
RuleBackend.getRules(this.props.account.owner, params.pagination.current, params.pagination.pageSize, sortField, sortOrder).then((res) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
data: res.data,
|
||||
pagination: {
|
||||
...params.pagination,
|
||||
total: res.data2,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.setState({loading: false});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
addRule() {
|
||||
const newRule = this.newRule();
|
||||
RuleBackend.addRule(newRule).then((res) => {
|
||||
if (res.status === "error") {
|
||||
Setting.showMessage("error", `Failed to add: ${res.msg}`);
|
||||
} else {
|
||||
Setting.showMessage("success", "Rule added successfully");
|
||||
this.setState({
|
||||
data: Setting.prependRow(this.state.data, newRule),
|
||||
});
|
||||
this.fetch();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteRule(i) {
|
||||
RuleBackend.deleteRule(this.state.data[i]).then((res) => {
|
||||
if (res.status === "error") {
|
||||
Setting.showMessage("error", `Failed to delete: ${res.msg}`);
|
||||
} else {
|
||||
Setting.showMessage("success", "Deleted successfully");
|
||||
this.fetch({
|
||||
pagination: {
|
||||
...this.state.pagination,
|
||||
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
newRule() {
|
||||
const randomName = Setting.getRandomName();
|
||||
const owner = Setting.getRequestOrganization(this.props.account);
|
||||
return {
|
||||
owner: owner,
|
||||
name: `rule_${randomName}`,
|
||||
createdTime: moment().format(),
|
||||
type: "User-Agent",
|
||||
expressions: [],
|
||||
action: "Block",
|
||||
reason: "Your request is blocked.",
|
||||
};
|
||||
}
|
||||
|
||||
renderTable(data) {
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("general:Owner"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
width: "150px",
|
||||
sorter: (a, b) => a.owner.localeCompare(b.owner),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: "200px",
|
||||
sorter: (a, b) => a.name.localeCompare(b.name),
|
||||
render: (text, rule, index) => {
|
||||
return <a href={`/rules/${rule.owner}/${text}`}>{text}</a>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Create time"),
|
||||
dataIndex: "createdTime",
|
||||
key: "createdTime",
|
||||
width: "200px",
|
||||
sorter: (a, b) => a.createdTime.localeCompare(b.createdTime),
|
||||
render: (text, rule, index) => {
|
||||
return Setting.getFormattedDate(text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Update time"),
|
||||
dataIndex: "updatedTime",
|
||||
key: "updatedTime",
|
||||
width: "200px",
|
||||
sorter: (a, b) => a.updatedTime.localeCompare(b.updatedTime),
|
||||
render: (text, rule, index) => {
|
||||
return Setting.getFormattedDate(text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("rule:Type"),
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
width: "100px",
|
||||
sorter: (a, b) => a.type.localeCompare(b.type),
|
||||
render: (text, rule, index) => {
|
||||
return (
|
||||
<Tag color="blue">
|
||||
{i18next.t(`rule:${text}`)}
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("rule:Expressions"),
|
||||
dataIndex: "expressions",
|
||||
key: "expressions",
|
||||
sorter: (a, b) => a.expressions.localeCompare(b.expressions),
|
||||
render: (text, rule, index) => {
|
||||
return rule.expressions.map((expression, i) => {
|
||||
return (
|
||||
<Tag key={expression} color={"success"}>
|
||||
{expression.operator + " " + expression.value.slice(0, 20)}
|
||||
</Tag>
|
||||
);
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
dataIndex: "action",
|
||||
key: "action",
|
||||
width: "100px",
|
||||
sorter: (a, b) => a.action.localeCompare(b.action),
|
||||
},
|
||||
{
|
||||
title: i18next.t("rule:Status code"),
|
||||
dataIndex: "statusCode",
|
||||
key: "statusCode",
|
||||
width: "120px",
|
||||
sorter: (a, b) => a.statusCode.localeCompare(b.statusCode),
|
||||
},
|
||||
{
|
||||
title: i18next.t("rule:Reason"),
|
||||
dataIndex: "reason",
|
||||
key: "reason",
|
||||
width: "300px",
|
||||
sorter: (a, b) => a.reason.localeCompare(b.reason),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
dataIndex: "",
|
||||
key: "op",
|
||||
render: (text, rule, index) => {
|
||||
return (
|
||||
<div>
|
||||
<Popconfirm
|
||||
title={`Sure to delete rule: ${rule.name} ?`}
|
||||
onConfirm={() => this.deleteRule(index)}
|
||||
>
|
||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/rules/${rule.owner}/${rule.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<Button type="danger">{i18next.t("general:Delete")}</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Table
|
||||
dataSource={data}
|
||||
columns={columns}
|
||||
rowKey="name"
|
||||
pagination={this.state.pagination}
|
||||
loading={this.state.loading}
|
||||
onChange={this.handleTableChange}
|
||||
size="middle"
|
||||
bordered
|
||||
title={() => (
|
||||
<div>
|
||||
{i18next.t("general:Rules")}
|
||||
<Button type="primary" size="small" onClick={() => this.addRule()}>{i18next.t("general:Add")}</Button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default RuleListPage;
|
||||
@@ -182,6 +182,10 @@ export const OtherProviderInfo = {
|
||||
logo: `${StaticBaseUrl}/img/social_default.png`,
|
||||
url: "https://casdoor.org/docs/provider/email/overview",
|
||||
},
|
||||
"Resend": {
|
||||
logo: `${StaticBaseUrl}/img/email_resend.png`,
|
||||
url: "https://resend.com/",
|
||||
},
|
||||
},
|
||||
Storage: {
|
||||
"Local File System": {
|
||||
@@ -457,8 +461,8 @@ export const UserFields = ["owner", "name", "password", "display_name", "id", "t
|
||||
"is_admin", "homepage", "birthday", "gender", "password_type", "password_salt", "external_id", "avatar", "first_name", "last_name",
|
||||
"avatar_type", "permanent_avatar", "email_verified", "region", "location", "address",
|
||||
"affiliation", "title", "id_card_type", "id_card", "real_name", "is_verified", "bio", "tag", "language",
|
||||
"education", "score", "karma", "ranking", "balance", "currency", "is_default_avatar", "is_online",
|
||||
"is_forbidden", "is_deleted", "signup_application", "hash", "pre_hash", "access_key", "access_secret", "access_token",
|
||||
"education", "score", "karma", "ranking", "balance", "balance_credit", "balance_currency", "currency", "is_default_avatar", "is_online",
|
||||
"is_forbidden", "is_deleted", "signup_application", "register_type", "register_source", "hash", "pre_hash", "access_key", "access_secret", "access_token",
|
||||
"created_ip", "last_signin_time", "last_signin_ip", "github", "google", "qq", "wechat", "facebook", "dingtalk",
|
||||
"weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs", "baidu", "alipay", "casdoor", "infoflow", "apple",
|
||||
"azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "kwai", "line", "amazon", "auth0",
|
||||
@@ -469,7 +473,7 @@ export const UserFields = ["owner", "name", "password", "display_name", "id", "t
|
||||
"wepay", "xero", "yahoo", "yammer", "yandex", "zoom", "metamask", "web3onboard", "custom", "webauthnCredentials",
|
||||
"preferred_mfa_type", "recovery_codes", "totp_secret", "mfa_phone_enabled", "mfa_email_enabled", "invitation",
|
||||
"invitation_code", "face_ids", "ldap", "properties", "roles", "permissions", "groups", "last_change_password_time",
|
||||
"last_signin_wrong_time", "signin_wrong_times", "managedAccounts", "mfaAccounts", "need_update_password",
|
||||
"last_signin_wrong_time", "signin_wrong_times", "managedAccounts", "mfaAccounts", "mfaItems", "need_update_password",
|
||||
"created_time", "updated_time", "deleted_time",
|
||||
"ip_whitelist"];
|
||||
|
||||
@@ -500,6 +504,7 @@ export const GetTranslatedUserItems = () => {
|
||||
{name: "Country/Region", label: i18next.t("user:Country/Region")},
|
||||
{name: "Location", label: i18next.t("user:Location")},
|
||||
{name: "Address", label: i18next.t("user:Address")},
|
||||
{name: "Addresses", label: i18next.t("user:Addresses")},
|
||||
{name: "Affiliation", label: i18next.t("user:Affiliation")},
|
||||
{name: "Title", label: i18next.t("general:Title")},
|
||||
{name: "ID card type", label: i18next.t("user:ID card type")},
|
||||
@@ -523,6 +528,8 @@ export const GetTranslatedUserItems = () => {
|
||||
{name: "Karma", label: i18next.t("user:Karma")},
|
||||
{name: "Ranking", label: i18next.t("user:Ranking")},
|
||||
{name: "Signup application", label: i18next.t("general:Signup application")},
|
||||
{name: "Register type", label: i18next.t("user:Register type")},
|
||||
{name: "Register source", label: i18next.t("user:Register source")},
|
||||
{name: "API key", label: i18next.t("general:API key")},
|
||||
{name: "Groups", label: i18next.t("general:Groups")},
|
||||
{name: "Roles", label: i18next.t("general:Roles")},
|
||||
@@ -537,6 +544,7 @@ export const GetTranslatedUserItems = () => {
|
||||
{name: "IP whitelist", label: i18next.t("general:IP whitelist")},
|
||||
{name: "Multi-factor authentication", label: i18next.t("mfa:Multi-factor authentication")},
|
||||
{name: "WebAuthn credentials", label: i18next.t("user:WebAuthn credentials")},
|
||||
{name: "Last change password time", label: i18next.t("user:Last change password time")},
|
||||
{name: "Managed accounts", label: i18next.t("user:Managed accounts")},
|
||||
{name: "Face ID", label: i18next.t("login:Face ID")},
|
||||
{name: "MFA accounts", label: i18next.t("user:MFA accounts")},
|
||||
@@ -554,6 +562,8 @@ export function getUserColumns() {
|
||||
transField = "Country/Region";
|
||||
} else if (field === "mfaAccounts") {
|
||||
transField = "MFA accounts";
|
||||
} else if (field === "mfaItems") {
|
||||
transField = "MFA items";
|
||||
} else if (field === "face_ids") {
|
||||
transField = "Face ID";
|
||||
} else if (field === "managedAccounts") {
|
||||
@@ -1291,6 +1301,7 @@ export function getProviderTypeOptions(category) {
|
||||
{id: "Azure ACS", name: "Azure ACS"},
|
||||
{id: "SendGrid", name: "SendGrid"},
|
||||
{id: "Custom HTTP Email", name: "Custom HTTP Email"},
|
||||
{id: "Resend", name: "Resend"},
|
||||
]
|
||||
);
|
||||
} else if (category === "SMS") {
|
||||
@@ -2416,3 +2427,48 @@ export function getApiPaths() {
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export function getItemId(item) {
|
||||
return item.owner + "/" + item.name;
|
||||
}
|
||||
|
||||
export function getVersionInfo(text, siteName) {
|
||||
if (text === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const versionInfo = JSON.parse(text);
|
||||
const link = versionInfo?.version !== "" ? `${getRepoUrl(siteName)}/releases/tag/${versionInfo?.version}` : "";
|
||||
let versionText = versionInfo?.version !== "" ? versionInfo?.version : "Unknown version";
|
||||
if (versionInfo?.commitOffset > 0) {
|
||||
versionText += ` (ahead+${versionInfo?.commitOffset})`;
|
||||
}
|
||||
|
||||
return {text: versionText, link: link};
|
||||
} catch (e) {
|
||||
return {text: "", link: ""};
|
||||
}
|
||||
}
|
||||
|
||||
export function prependRow(array, row) {
|
||||
return [row, ...array];
|
||||
}
|
||||
|
||||
function getOriginalName(name) {
|
||||
const tokens = name.split("_");
|
||||
if (tokens.length > 0) {
|
||||
return tokens[0];
|
||||
} else {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
export function getRepoUrl(name) {
|
||||
name = getOriginalName(name);
|
||||
if (name === "casdoor") {
|
||||
return "https://github.com/casdoor/casdoor";
|
||||
} else {
|
||||
return `https://github.com/casbin/${name}`;
|
||||
}
|
||||
}
|
||||
|
||||
483
web/src/SiteEditPage.js
Normal file
483
web/src/SiteEditPage.js
Normal file
@@ -0,0 +1,483 @@
|
||||
// Copyright 2023 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, Switch} from "antd";
|
||||
import {LinkOutlined} from "@ant-design/icons";
|
||||
import * as ProviderBackend from "./backend/ProviderBackend";
|
||||
import * as SiteBackend from "./backend/SiteBackend";
|
||||
import * as CertBackend from "./backend/CertBackend";
|
||||
import * as RuleBackend from "./backend/RuleBackend";
|
||||
import * as ApplicationBackend from "./backend/ApplicationBackend";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
import RuleTable from "./table/RuleTable";
|
||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
class SiteEditPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
owner: props.match.params.organizationName,
|
||||
siteName: props.match.params.siteName,
|
||||
rules: [],
|
||||
providers: [],
|
||||
site: null,
|
||||
certs: null,
|
||||
applications: null,
|
||||
organizations: [],
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getOrganizations();
|
||||
this.getSite();
|
||||
this.getCerts();
|
||||
this.getRules();
|
||||
this.getApplications();
|
||||
this.getAlertProviders();
|
||||
}
|
||||
|
||||
getOrganizations() {
|
||||
if (Setting.isAdminUser(this.props.account)) {
|
||||
OrganizationBackend.getOrganizations("admin")
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
organizations: res.data || [],
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getSite() {
|
||||
SiteBackend.getSite(this.state.owner, this.state.siteName)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
site: res.data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `Failed to get site: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getCerts() {
|
||||
CertBackend.getCerts(this.state.owner)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
certs: res.data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `Failed to get certs: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getRules() {
|
||||
RuleBackend.getRules(this.state.owner)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
rules: res.data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `Failed to get rules: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getApplications(owner) {
|
||||
ApplicationBackend.getApplicationsByOrganization("admin", owner || this.state.owner)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
applications: res.data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `Failed to get applications: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getAlertProviders() {
|
||||
ProviderBackend.getProviders()
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
const data = [];
|
||||
for (let i = 0; i < res.data.length; i++) {
|
||||
const provider = res.data[i];
|
||||
if (provider.category === "SMS" || provider.category === "Email") {
|
||||
data.push(provider.category + "/" + provider.name);
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
providers: data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `Failed to get providers: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
parseSiteField(key, value) {
|
||||
if (["score"].includes(key)) {
|
||||
value = Setting.myParseInt(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
updateSiteField(key, value) {
|
||||
value = this.parseSiteField(key, value);
|
||||
|
||||
const site = this.state.site;
|
||||
site[key] = value;
|
||||
this.setState({
|
||||
site: site,
|
||||
});
|
||||
}
|
||||
|
||||
renderSite() {
|
||||
return (
|
||||
<Card size="small" title={
|
||||
<div>
|
||||
{i18next.t("site:Edit Site")}
|
||||
<Button type="primary" onClick={this.submitSiteEdit.bind(this)}>{i18next.t("general:Save")}</Button>
|
||||
</div>
|
||||
} style={{marginLeft: "5px"}} type="inner">
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} disabled={!Setting.isAdminUser(this.props.account)} value={this.state.site.owner} onChange={(value => {
|
||||
this.updateSiteField("owner", value);
|
||||
this.getApplications(value);
|
||||
})}>
|
||||
{
|
||||
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("general:Name")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.site.name} onChange={e => {
|
||||
this.updateSiteField("name", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("general:Display name")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.site.displayName} onChange={e => {
|
||||
this.updateSiteField("displayName", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("general:Tag")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.site.tag} onChange={e => {
|
||||
this.updateSiteField("tag", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Domain")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.site.domain} onChange={e => {
|
||||
this.updateSiteField("domain", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Other domains")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} mode="tags" style={{width: "100%"}} value={this.state.site.otherDomains} onChange={(value => {this.updateSiteField("otherDomains", value);})}>
|
||||
{
|
||||
this.state.site.otherDomains?.map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Need redirect")}:
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.site.needRedirect} onChange={checked => {
|
||||
this.updateSiteField("needRedirect", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Disable verbose")}:
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.site.disableVerbose} onChange={checked => {
|
||||
this.updateSiteField("disableVerbose", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Rules")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<RuleTable
|
||||
title={"Rules"}
|
||||
account={this.props.account}
|
||||
sources={this.state.rules}
|
||||
rules={this.state.site.rules}
|
||||
onUpdateRules={(value) => this.updateSiteField("rules", value)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col span={2} style={{marginTop: "5px"}}>
|
||||
{i18next.t("site:Enable alert")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Switch checked={this.state.site.enableAlert} onChange={checked => {
|
||||
this.updateSiteField("enableAlert", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
this.state.site.enableAlert ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col span={2} style={{marginTop: "5px"}}>
|
||||
{i18next.t("site:Alert interval")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<InputNumber min={1} value={this.state.site.alertInterval} addonAfter={i18next.t("usage:seconds")} onChange={value => {
|
||||
this.updateSiteField("alertInterval", value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
this.state.site.enableAlert ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col span={2} style={{marginTop: "5px"}}>
|
||||
{i18next.t("site:Alert try times")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<InputNumber min={1} value={this.state.site.alertTryTimes} onChange={value => {
|
||||
this.updateSiteField("alertTryTimes", value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
this.state.site.enableAlert ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Alert providers")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} mode="tags" style={{width: "100%"}} value={this.state.site.alertProviders} onChange={(value => {this.updateSiteField("alertProviders", value);})}>
|
||||
{
|
||||
this.state.providers.map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
) : null
|
||||
}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Challenges")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} mode="tags" style={{width: "100%"}} value={this.state.site.challenges} onChange={(value => {this.updateSiteField("challenges", value);})}>
|
||||
{
|
||||
this.state.site.challenges?.map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Host")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input prefix={<LinkOutlined />} value={this.state.site.host} onChange={e => {
|
||||
this.updateSiteField("host", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Port")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<InputNumber min={0} max={65535} value={this.state.site.port} onChange={value => {
|
||||
this.updateSiteField("port", value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Hosts")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} mode="tags" style={{width: "100%"}} value={this.state.site.hosts} onChange={(value => {this.updateSiteField("hosts", value);})}>
|
||||
{
|
||||
this.state.site.hosts?.map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Public IP")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.site.publicIp} onChange={e => {
|
||||
this.updateSiteField("publicIp", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Mode")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.site.sslMode} onChange={(value => {this.updateSiteField("sslMode", value);})}>
|
||||
{
|
||||
[
|
||||
{id: "HTTP", name: "HTTP"},
|
||||
{id: "HTTPS and HTTP", name: "HTTPS and HTTP"},
|
||||
{id: "HTTPS Only", name: "HTTPS Only"},
|
||||
{id: "Static Folder", name: "Static Folder"},
|
||||
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:SSL cert")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select disabled={true} virtual={false} style={{width: "100%"}} showSearch value={this.state.site.sslCert} onChange={(value => {
|
||||
this.updateSiteField("sslCert", value);
|
||||
})}>
|
||||
{
|
||||
this.state.certs?.map((cert, index) => <Option key={index} value={cert.name}>{cert.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Casdoor app")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} showSearch value={this.state.site.casdoorApplication} onChange={(value => {
|
||||
this.updateSiteField("casdoorApplication", value);
|
||||
})}>
|
||||
{
|
||||
this.state.applications?.map((application, index) => <Option key={index} value={application.name}>{application.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Status")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.site.status} onChange={(value => {this.updateSiteField("status", value);})}>
|
||||
{
|
||||
[
|
||||
{id: "Active", name: "Active"},
|
||||
{id: "Inactive", name: "Inactive"},
|
||||
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
submitSiteEdit() {
|
||||
const site = Setting.deepCopy(this.state.site);
|
||||
SiteBackend.updateSite(this.state.site.owner, this.state.siteName, site)
|
||||
.then((res) => {
|
||||
if (res.status === "error") {
|
||||
Setting.showMessage("error", `Failed to save: ${res.msg}`);
|
||||
this.updateSiteField("name", this.state.siteName);
|
||||
} else {
|
||||
Setting.showMessage("success", "Successfully saved");
|
||||
this.setState({
|
||||
siteName: this.state.site.name,
|
||||
});
|
||||
this.props.history.push(`/sites/${this.state.site.owner}/${this.state.site.name}`);
|
||||
this.getSite();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `failed to save: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Row style={{width: "100%"}}>
|
||||
<Col span={1}>
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
{
|
||||
this.state.site !== null ? this.renderSite() : null
|
||||
}
|
||||
</Col>
|
||||
<Col span={1}>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{margin: 10}}>
|
||||
<Col span={2}>
|
||||
</Col>
|
||||
<Col span={18}>
|
||||
<Button type="primary" size="large" onClick={this.submitSiteEdit.bind(this)}>{i18next.t("general:Save")}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SiteEditPage;
|
||||
459
web/src/SiteListPage.js
Normal file
459
web/src/SiteListPage.js
Normal file
@@ -0,0 +1,459 @@
|
||||
// Copyright 2023 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, Tag, Tooltip} from "antd";
|
||||
import moment from "moment";
|
||||
import * as Setting from "./Setting";
|
||||
import * as SiteBackend from "./backend/SiteBackend";
|
||||
import i18next from "i18next";
|
||||
import BaseListPage from "./BaseListPage";
|
||||
|
||||
class SiteListPage extends BaseListPage {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.setState({
|
||||
pagination: {
|
||||
...this.state.pagination,
|
||||
current: 1,
|
||||
pageSize: 1000,
|
||||
},
|
||||
});
|
||||
this.fetch({pagination: this.state.pagination});
|
||||
}
|
||||
|
||||
newSite() {
|
||||
const randomName = Setting.getRandomName();
|
||||
const owner = Setting.getRequestOrganization(this.props.account);
|
||||
return {
|
||||
owner: owner,
|
||||
name: `site_${randomName}`,
|
||||
createdTime: moment().format(),
|
||||
displayName: `New Site - ${randomName}`,
|
||||
domain: "door.casdoor.com",
|
||||
otherDomains: [],
|
||||
needRedirect: false,
|
||||
disableVerbose: false,
|
||||
rules: [],
|
||||
enableAlert: false,
|
||||
alertInterval: 60,
|
||||
alertTryTimes: 3,
|
||||
alertProviders: [],
|
||||
challenges: [],
|
||||
host: "",
|
||||
port: 8000,
|
||||
hosts: [],
|
||||
sslMode: "HTTPS Only",
|
||||
sslCert: "",
|
||||
publicIp: "8.131.81.162",
|
||||
node: "",
|
||||
isSelf: false,
|
||||
nodes: [],
|
||||
casdoorApplication: "",
|
||||
organizations: [],
|
||||
};
|
||||
}
|
||||
|
||||
addSite() {
|
||||
const newSite = this.newSite();
|
||||
SiteBackend.addSite(newSite)
|
||||
.then((res) => {
|
||||
if (res.status === "error") {
|
||||
Setting.showMessage("error", `Failed to add: ${res.msg}`);
|
||||
} else {
|
||||
Setting.showMessage("success", "Site added successfully");
|
||||
this.setState({
|
||||
data: Setting.prependRow(this.state.data, newSite),
|
||||
});
|
||||
this.fetch();
|
||||
}
|
||||
}
|
||||
)
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `Site failed to add: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
deleteSite(i) {
|
||||
SiteBackend.deleteSite(this.state.data[i])
|
||||
.then((res) => {
|
||||
if (res.status === "error") {
|
||||
Setting.showMessage("error", `Failed to delete: ${res.msg}`);
|
||||
} else {
|
||||
Setting.showMessage("success", "Site deleted successfully");
|
||||
this.fetch({
|
||||
pagination: {
|
||||
...this.state.pagination,
|
||||
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
)
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `Site failed to delete: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
renderTable(data) {
|
||||
// const renderExternalLink = () => {
|
||||
// return (
|
||||
// <svg style={{marginLeft: "5px"}} width="13.5" height="13.5" aria-hidden="true" viewBox="0 0 24 24" className="iconExternalLink_nPIU">
|
||||
// <path fill="currentColor" d="M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"></path>
|
||||
// </svg>
|
||||
// );
|
||||
// };
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("general:Owner"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
width: "90px",
|
||||
sorter: (a, b) => a.owner.localeCompare(b.owner),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Tag"),
|
||||
dataIndex: "tag",
|
||||
key: "tag",
|
||||
width: "140px",
|
||||
sorter: (a, b) => a.tag.localeCompare(b.tag),
|
||||
render: (text, record, index) => {
|
||||
if (text === "") {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Link to={`/nodes/${record.owner}/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: "120px",
|
||||
sorter: (a, b) => a.name.localeCompare(b.name),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/sites/${record.owner}/${record.name}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
// {
|
||||
// title: i18next.t("general:Create time"),
|
||||
// dataIndex: "createdTime",
|
||||
// key: "createdTime",
|
||||
// width: "180px",
|
||||
// sorter: (a, b) => a.createdTime.localeCompare(b.createdTime),
|
||||
// render: (text, record, index) => {
|
||||
// return Setting.getFormattedDate(text);
|
||||
// },
|
||||
// },
|
||||
{
|
||||
title: i18next.t("general:Display name"),
|
||||
dataIndex: "displayName",
|
||||
key: "displayName",
|
||||
// width: "200px",
|
||||
sorter: (a, b) => a.displayName.localeCompare(b.displayName),
|
||||
},
|
||||
{
|
||||
title: i18next.t("site:Domain"),
|
||||
dataIndex: "domain",
|
||||
key: "domain",
|
||||
width: "150px",
|
||||
sorter: (a, b) => a.domain.localeCompare(b.domain),
|
||||
render: (text, record, index) => {
|
||||
if (record.publicIp === "") {
|
||||
return text;
|
||||
}
|
||||
|
||||
return (
|
||||
<a target="_blank" rel="noreferrer" href={`https://${text}`}>
|
||||
{text}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("site:Other domains"),
|
||||
dataIndex: "otherDomains",
|
||||
key: "otherDomains",
|
||||
width: "120px",
|
||||
sorter: (a, b) => a.otherDomains.localeCompare(b.otherDomains),
|
||||
render: (text, record, index) => {
|
||||
return record.otherDomains.map(domain => {
|
||||
return (
|
||||
<a key={domain} target="_blank" rel="noreferrer" href={`https://${domain}`}>
|
||||
<Tag color={record.needRedirect ? "default" : "processing"}>
|
||||
{domain}
|
||||
</Tag>
|
||||
</a>
|
||||
);
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Rules"),
|
||||
dataIndex: "rules",
|
||||
key: "rules",
|
||||
width: "120px",
|
||||
sorter: (a, b) => a.rules.localeCompare(b.rules),
|
||||
render: (text, record, index) => {
|
||||
if (!record.rules) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return record.rules.map(rule => {
|
||||
return (
|
||||
<a key={rule} target="_blank" rel="noreferrer" href={`/rules/${rule}`}>
|
||||
<Tag color={"processing"}>
|
||||
{rule}
|
||||
</Tag>
|
||||
</a>
|
||||
);
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("site:Host"),
|
||||
dataIndex: "host",
|
||||
key: "host",
|
||||
width: "80px",
|
||||
sorter: (a, b) => a.host.localeCompare(b.host),
|
||||
render: (text, record, index) => {
|
||||
let host = record.port;
|
||||
if (record.host !== "") {
|
||||
host = `${record.host}:${record.port}`;
|
||||
}
|
||||
|
||||
if (record.status === "Active") {
|
||||
return host;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tag color={"warning"}>
|
||||
{host}
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("site:Hosts"),
|
||||
dataIndex: "hosts",
|
||||
key: "hosts",
|
||||
width: "200px",
|
||||
sorter: (a, b) => a.hosts.length - b.hosts.length,
|
||||
render: (hosts) => {
|
||||
if (!Array.isArray(hosts)) {
|
||||
return null;
|
||||
}
|
||||
return hosts.map((host, index) => (
|
||||
<Tag color="blue" key={index}>
|
||||
{host}
|
||||
</Tag>
|
||||
));
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("site:Nodes"),
|
||||
dataIndex: "nodes",
|
||||
key: "nodes",
|
||||
width: "180px",
|
||||
sorter: (a, b) => a.nodes.length - b.nodes.length,
|
||||
render: (text, record, index) => {
|
||||
return record.nodes.map(node => {
|
||||
const versionInfo = Setting.getVersionInfo(node.version, record.name);
|
||||
let color = node.message === "" ? "processing" : "error";
|
||||
if (color === "processing" && node.provider !== "") {
|
||||
if (node.version === "") {
|
||||
color = "warning";
|
||||
} else if (node.provider !== "") {
|
||||
color = "success";
|
||||
}
|
||||
}
|
||||
|
||||
const getTag = () => {
|
||||
if (versionInfo === null) {
|
||||
return (
|
||||
<Tag key={node.name} color={color}>
|
||||
{node.name}
|
||||
</Tag>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<a key={node.name} target="_blank" rel="noreferrer" href={versionInfo.link}>
|
||||
<Tag color={color}>
|
||||
{`${node.name} (${versionInfo.text})`}
|
||||
</Tag>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (node.message === "") {
|
||||
return getTag();
|
||||
} else {
|
||||
return (
|
||||
<Tooltip key={node.name} title={node.message}>
|
||||
{getTag()}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
// {
|
||||
// title: i18next.t("site:Public IP"),
|
||||
// dataIndex: "publicIp",
|
||||
// key: "publicIp",
|
||||
// width: "120px",
|
||||
// sorter: (a, b) => a.publicIp.localeCompare(b.publicIp),
|
||||
// },
|
||||
// {
|
||||
// title: i18next.t("site:Node"),
|
||||
// dataIndex: "node",
|
||||
// key: "node",
|
||||
// width: "180px",
|
||||
// sorter: (a, b) => a.node.localeCompare(b.node),
|
||||
// render: (text, record, index) => {
|
||||
// return (
|
||||
// <div>
|
||||
// {text}
|
||||
// {
|
||||
// !record.isSelf ? null : (
|
||||
// <Tag style={{marginLeft: "10px"}} icon={<CheckCircleOutlined />} color="success">
|
||||
// {i18next.t("general:Self")}
|
||||
// </Tag>
|
||||
// )
|
||||
// }
|
||||
// </div>
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// title: i18next.t("site:Mode"),
|
||||
// dataIndex: "sslMode",
|
||||
// key: "sslMode",
|
||||
// width: "100px",
|
||||
// sorter: (a, b) => a.sslMode.localeCompare(b.sslMode),
|
||||
// },
|
||||
{
|
||||
title: i18next.t("site:SSL cert"),
|
||||
dataIndex: "sslCert",
|
||||
key: "sslCert",
|
||||
width: "130px",
|
||||
sorter: (a, b) => a.sslCert.localeCompare(b.sslCert),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/certs/admin/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
// {
|
||||
// title: i18next.t("site:Casdoor app"),
|
||||
// dataIndex: "casdoorApplication",
|
||||
// key: "casdoorApplication",
|
||||
// width: "140px",
|
||||
// sorter: (a, b) => a.casdoorApplication.localeCompare(b.casdoorApplication),
|
||||
// render: (text, record, index) => {
|
||||
// if (text === "") {
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// return (
|
||||
// <a target="_blank" rel="noreferrer" href={Setting.getMyProfileUrl(this.state.account).replace("/account", `/applications/${this.props.account.owner}/${text}`)}>
|
||||
// {text}
|
||||
// {renderExternalLink()}
|
||||
// </a>
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
dataIndex: "action",
|
||||
key: "action",
|
||||
width: "180px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/sites/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<Popconfirm
|
||||
title={`Sure to delete site: ${record.name} ?`}
|
||||
onConfirm={() => this.deleteSite(index)}
|
||||
okText="OK"
|
||||
cancelText="Cancel"
|
||||
>
|
||||
<Button style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table columns={columns} dataSource={data} rowKey="name" size="middle" bordered pagination={this.state.pagination}
|
||||
title={() => (
|
||||
<div>
|
||||
{i18next.t("general:Sites")}
|
||||
<Button type="primary" size="small" onClick={this.addSite.bind(this)}>{i18next.t("general:Add")}</Button>
|
||||
</div>
|
||||
)}
|
||||
loading={data === null}
|
||||
onChange={this.handleTableChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
fetch = (params = {}) => {
|
||||
const field = params.searchedColumn, value = params.searchText;
|
||||
const sortField = params.sortField, sortOrder = params.sortOrder;
|
||||
if (!params.pagination) {
|
||||
params.pagination = {current: 1, pageSize: 10};
|
||||
}
|
||||
this.setState({loading: true});
|
||||
// SiteBackend.getSites(this.props.account.name, params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
|
||||
SiteBackend.getSites(this.props.account.owner, "", "", field, value, sortField, sortOrder)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
data: res.data,
|
||||
pagination: {
|
||||
...params.pagination,
|
||||
total: res.data2,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `Failed to get sites: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default SiteListPage;
|
||||
@@ -389,6 +389,13 @@ class SyncerEditPage extends React.Component {
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "unionid",
|
||||
"type": "string",
|
||||
"casdoorName": "Name",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string",
|
||||
@@ -424,13 +431,6 @@ class SyncerEditPage extends React.Component {
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "job_number",
|
||||
"type": "string",
|
||||
"casdoorName": "Name",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "active",
|
||||
"type": "boolean",
|
||||
|
||||
@@ -50,6 +50,7 @@ import MfaTable from "./table/MfaTable";
|
||||
import TransactionTable from "./table/TransactionTable";
|
||||
import CartTable from "./table/CartTable";
|
||||
import * as TransactionBackend from "./backend/TransactionBackend";
|
||||
import ConsentTable from "./table/ConsentTable";
|
||||
import {Content, Header} from "antd/es/layout/layout";
|
||||
import Sider from "antd/es/layout/Sider";
|
||||
|
||||
@@ -73,6 +74,7 @@ class UserEditPage extends React.Component {
|
||||
idCardInfo: ["ID card front", "ID card back", "ID card with person"],
|
||||
openFaceRecognitionModal: false,
|
||||
transactions: [],
|
||||
consents: [],
|
||||
activeMenuKey: window.location.hash?.slice(1) || "",
|
||||
menuMode: "Horizontal",
|
||||
};
|
||||
@@ -110,6 +112,7 @@ class UserEditPage extends React.Component {
|
||||
this.setState({
|
||||
user: res.data,
|
||||
multiFactorAuths: res.data?.multiFactorAuths ?? [],
|
||||
consents: res.data?.applicationScopes ?? [],
|
||||
loading: false,
|
||||
});
|
||||
|
||||
@@ -274,7 +277,7 @@ class UserEditPage extends React.Component {
|
||||
|
||||
// Fallback to comparing by owner and name
|
||||
return (this.state.user.owner === this.props.account.owner &&
|
||||
this.state.user.name === this.props.account.name);
|
||||
this.state.user.name === this.props.account.name);
|
||||
}
|
||||
|
||||
isSelfOrAdmin() {
|
||||
@@ -609,13 +612,20 @@ class UserEditPage extends React.Component {
|
||||
);
|
||||
} else if (accountItem.name === "Addresses") {
|
||||
return (
|
||||
<AddressTable
|
||||
title={i18next.t("user:Addresses")}
|
||||
table={this.state.user.addresses}
|
||||
onUpdateTable={(value) => {
|
||||
this.updateUserField("addresses", value);
|
||||
}}
|
||||
/>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("user:Addresses"), i18next.t("user:Addresses"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<AddressTable
|
||||
title={i18next.t("user:Addresses")}
|
||||
table={this.state.user.addresses}
|
||||
onUpdateTable={(value) => {
|
||||
this.updateUserField("addresses", value);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
} else if (accountItem.name === "Affiliation") {
|
||||
return (
|
||||
@@ -880,7 +890,7 @@ class UserEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("general:Transactions"), i18next.t("general:Transactions"))} :
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<TransactionTable transactions={this.state.transactions} hideTag={true} />
|
||||
<TransactionTable title={i18next.t("general:Transactions")} transactions={this.state.transactions} hideTag={true} />
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
@@ -1122,6 +1132,21 @@ class UserEditPage extends React.Component {
|
||||
/>
|
||||
</Col>
|
||||
</Row>);
|
||||
} else if (accountItem.name === "Consents") {
|
||||
return (
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col style={{marginTop: "5px"}} span={Setting.isMobile() ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("consent:Consents"), i18next.t("consent:Consents - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<ConsentTable
|
||||
title={i18next.t("consent:Consents")}
|
||||
table={this.state.consents}
|
||||
onUpdateTable={() => this.getUser()}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
} else if (accountItem.name === "Multi-factor authentication") {
|
||||
return (
|
||||
!this.isSelfOrAdmin() ? null : (
|
||||
@@ -1130,15 +1155,21 @@ class UserEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("mfa:Multi-factor authentication"), i18next.t("mfa:Multi-factor authentication - Tooltip "))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Card size="small" title={i18next.t("mfa:Multi-factor methods")}
|
||||
extra={this.state.multiFactorAuths?.some(mfaProps => mfaProps.enabled) ?
|
||||
<PopconfirmModal
|
||||
text={i18next.t("general:Disable")}
|
||||
title={i18next.t("general:Sure to disable") + "?"}
|
||||
onConfirm={() => this.deleteMfa()}
|
||||
/> : null
|
||||
}>
|
||||
<Card size="small" title={
|
||||
<div>
|
||||
{i18next.t("mfa:Multi-factor methods")}
|
||||
{this.state.multiFactorAuths?.some(mfaProps => mfaProps.enabled) ?
|
||||
<PopconfirmModal
|
||||
text={i18next.t("general:Disable")}
|
||||
title={i18next.t("general:Sure to disable") + "?"}
|
||||
onConfirm={() => this.deleteMfa()}
|
||||
size="small"
|
||||
/> : null
|
||||
}
|
||||
</div>
|
||||
}>
|
||||
<List
|
||||
size="small"
|
||||
rowKey="mfaType"
|
||||
itemLayout="horizontal"
|
||||
dataSource={this.state.multiFactorAuths}
|
||||
|
||||
260
web/src/auth/ConsentPage.js
Normal file
260
web/src/auth/ConsentPage.js
Normal file
@@ -0,0 +1,260 @@
|
||||
// Copyright 2026 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.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Card, List, Result, Space} from "antd";
|
||||
import {CheckOutlined, LockOutlined} from "@ant-design/icons";
|
||||
import * as ApplicationBackend from "../backend/ApplicationBackend";
|
||||
import * as ConsentBackend from "../backend/ConsentBackend";
|
||||
import * as Setting from "../Setting";
|
||||
import i18next from "i18next";
|
||||
import {withRouter} from "react-router-dom";
|
||||
import * as Util from "./Util";
|
||||
|
||||
class ConsentPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
this.state = {
|
||||
applicationName: props.match?.params?.applicationName || params.get("application"),
|
||||
scopeDescriptions: [],
|
||||
granting: false,
|
||||
oAuthParams: Util.getOAuthGetParameters(),
|
||||
};
|
||||
}
|
||||
|
||||
getApplicationObj() {
|
||||
return this.props.application;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getApplication();
|
||||
this.loadScopeDescriptions();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.application !== prevProps.application) {
|
||||
this.loadScopeDescriptions();
|
||||
}
|
||||
}
|
||||
|
||||
getApplication() {
|
||||
if (!this.state.applicationName) {
|
||||
return;
|
||||
}
|
||||
|
||||
ApplicationBackend.getApplication("admin", this.state.applicationName)
|
||||
.then((res) => {
|
||||
if (res.status === "error") {
|
||||
Setting.showMessage("error", res.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onUpdateApplication(res.data);
|
||||
});
|
||||
}
|
||||
|
||||
loadScopeDescriptions() {
|
||||
const {oAuthParams} = this.state;
|
||||
const application = this.getApplicationObj();
|
||||
if (!oAuthParams?.scope || !application) {
|
||||
return;
|
||||
}
|
||||
// Check if urlPar scope is within application scopes
|
||||
const scopes = oAuthParams.scope.split(" ").map(s => s.trim()).filter(Boolean);
|
||||
const customScopes = application.customScopes || [];
|
||||
const customScopesMap = {};
|
||||
customScopes.forEach(s => {
|
||||
if (s?.scope) {
|
||||
customScopesMap[s.scope] = s;
|
||||
}
|
||||
});
|
||||
|
||||
const scopeDescriptions = scopes
|
||||
.map(scope => {
|
||||
const item = customScopesMap[scope];
|
||||
if (item) {
|
||||
return {
|
||||
...item,
|
||||
displayName: item.displayName || item.scope,
|
||||
};
|
||||
}
|
||||
return {
|
||||
scope: scope,
|
||||
displayName: scope,
|
||||
description: i18next.t("consent:This scope is not defined in the application"),
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
this.setState({
|
||||
scopeDescriptions: scopeDescriptions,
|
||||
});
|
||||
}
|
||||
|
||||
handleGrant() {
|
||||
const {oAuthParams, scopeDescriptions} = this.state;
|
||||
const application = this.getApplicationObj();
|
||||
|
||||
this.setState({granting: true});
|
||||
|
||||
const consent = {
|
||||
owner: application.owner,
|
||||
application: application.owner + "/" + application.name,
|
||||
grantedScopes: scopeDescriptions.map(s => s.scope),
|
||||
};
|
||||
|
||||
ConsentBackend.grantConsent(consent, oAuthParams)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
// res.data contains the authorization code
|
||||
const code = res.data;
|
||||
const concatChar = oAuthParams?.redirectUri?.includes("?") ? "&" : "?";
|
||||
const redirectUrl = `${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`;
|
||||
Setting.goToLink(redirectUrl);
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
this.setState({granting: false});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleDeny() {
|
||||
const {oAuthParams} = this.state;
|
||||
const concatChar = oAuthParams?.redirectUri?.includes("?") ? "&" : "?";
|
||||
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}error=access_denied&error_description=User denied consent&state=${oAuthParams.state}`);
|
||||
}
|
||||
|
||||
render() {
|
||||
const application = this.getApplicationObj();
|
||||
|
||||
if (application === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!application) {
|
||||
return (
|
||||
<Result
|
||||
status="error"
|
||||
title={i18next.t("general:Invalid application")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const {scopeDescriptions, granting} = this.state;
|
||||
const isScopeEmpty = scopeDescriptions.length === 0;
|
||||
|
||||
return (
|
||||
<div className="login-content">
|
||||
<div className={Setting.isDarkTheme(this.props.themeAlgorithm) ? "login-panel-dark" : "login-panel"}>
|
||||
<div className="login-form">
|
||||
<Card
|
||||
style={{
|
||||
padding: "32px",
|
||||
width: 450,
|
||||
borderRadius: "12px",
|
||||
boxShadow: "0 10px 25px rgba(0, 0, 0, 0.05)",
|
||||
border: "1px solid #f0f0f0",
|
||||
}}
|
||||
>
|
||||
<div style={{textAlign: "center", marginBottom: 24}}>
|
||||
{application.logo && (
|
||||
<div style={{marginBottom: 16}}>
|
||||
<img
|
||||
src={application.logo}
|
||||
alt={application.displayName || application.name}
|
||||
style={{height: 56, objectFit: "contain"}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<h2 style={{margin: 0, fontWeight: 600, fontSize: "24px"}}>
|
||||
{i18next.t("consent:Authorization Request")}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div style={{marginBottom: 32}}>
|
||||
<p style={{fontSize: 15, color: "#666", textAlign: "center", lineHeight: "1.6"}}>
|
||||
<span style={{fontWeight: 600, color: "#000"}}>{application.displayName || application.name}</span>
|
||||
{" "}{i18next.t("consent:wants to access your account")}
|
||||
</p>
|
||||
{application.homepageUrl && (
|
||||
<div style={{textAlign: "center", marginTop: 4}}>
|
||||
<a href={application.homepageUrl} target="_blank" rel="noopener noreferrer" style={{fontSize: 13, color: "#1890ff"}}>
|
||||
{application.homepageUrl}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{marginBottom: 32}}>
|
||||
<div style={{fontSize: 14, color: "#8c8c8c", marginBottom: 16}}>
|
||||
<LockOutlined style={{marginRight: 8}} /> {i18next.t("consent:This application is requesting")}
|
||||
</div>
|
||||
<div style={{display: "flex", justifyContent: "center"}}>
|
||||
<List
|
||||
size="small"
|
||||
dataSource={scopeDescriptions}
|
||||
style={{width: "100%"}}
|
||||
renderItem={item => (
|
||||
<List.Item style={{borderBottom: "none", width: "100%"}}>
|
||||
<div style={{display: "inline-grid", gridTemplateColumns: "16px auto", columnGap: 8, alignItems: "start"}}>
|
||||
<CheckOutlined style={{color: "#52c41a", fontSize: "14px", marginTop: "4px", justifySelf: "center"}} />
|
||||
<div style={{fontWeight: 500, fontSize: "14px", lineHeight: "22px"}}>{item.displayName || item.scope}</div>
|
||||
</div>
|
||||
<div style={{fontSize: "12px", color: "#8c8c8c", marginTop: 2}}>{item.description}</div>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{textAlign: "center", marginBottom: 24}}>
|
||||
<Space size={16}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
shape="round"
|
||||
onClick={() => this.handleGrant()}
|
||||
loading={granting}
|
||||
disabled={granting || isScopeEmpty}
|
||||
style={{minWidth: 120, height: 44, fontWeight: 500}}
|
||||
>
|
||||
{i18next.t("consent:Allow")}
|
||||
</Button>
|
||||
<Button
|
||||
size="large"
|
||||
shape="round"
|
||||
onClick={() => this.handleDeny()}
|
||||
disabled={granting || isScopeEmpty}
|
||||
style={{minWidth: 120, height: 44, fontWeight: 500}}
|
||||
>
|
||||
{i18next.t("consent:Deny")}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div style={{padding: "16px", backgroundColor: "#fafafa", borderRadius: "8px", border: "1px solid #f0f0f0"}}>
|
||||
<p style={{margin: 0, fontSize: 12, color: "#8c8c8c", textAlign: "center", lineHeight: "1.5"}}>
|
||||
{i18next.t("consent:By clicking Allow, you allow this app to use your information")}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(ConsentPage);
|
||||
@@ -216,7 +216,7 @@ class LoginPage extends React.Component {
|
||||
this.setState({
|
||||
msg: res.msg,
|
||||
});
|
||||
return ;
|
||||
return;
|
||||
}
|
||||
this.onUpdateApplication(res.data);
|
||||
});
|
||||
@@ -369,6 +369,13 @@ class LoginPage extends React.Component {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if consent is required
|
||||
if (resp.data?.required === true) {
|
||||
// Consent required, redirect to consent page
|
||||
Setting.goToLinkSoft(ths, `/consent/${application.name}?${window.location.search.substring(1)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Setting.hasPromptPage(application)) {
|
||||
AuthBackend.getAccount()
|
||||
.then((res) => {
|
||||
@@ -1141,9 +1148,11 @@ class LoginPage extends React.Component {
|
||||
visible={this.state.openCaptchaModal}
|
||||
noModal={noModal}
|
||||
onUpdateToken={(captchaType, captchaToken, clientSecret) => {
|
||||
this.setState({captchaValues: {
|
||||
captchaType, captchaToken, clientSecret,
|
||||
}});
|
||||
this.setState({
|
||||
captchaValues: {
|
||||
captchaType, captchaToken, clientSecret,
|
||||
},
|
||||
});
|
||||
}}
|
||||
onOk={(captchaType, captchaToken, clientSecret) => {
|
||||
const values = this.state.values;
|
||||
|
||||
@@ -44,20 +44,15 @@ function generateCodeChallenge(verifier) {
|
||||
}
|
||||
|
||||
function storeCodeVerifier(state, verifier) {
|
||||
localStorage.setItem("pkce_verifier", `${state}#${verifier}`);
|
||||
localStorage.setItem(`pkce_verifier_${state}`, verifier);
|
||||
}
|
||||
|
||||
export function getCodeVerifier(state) {
|
||||
const verifierStore = localStorage.getItem("pkce_verifier");
|
||||
const [storedState, verifier] = verifierStore ? verifierStore.split("#") : [null, null];
|
||||
if (storedState !== state) {
|
||||
return null;
|
||||
}
|
||||
return verifier;
|
||||
return localStorage.getItem(`pkce_verifier_${state}`);
|
||||
}
|
||||
|
||||
export function clearCodeVerifier(state) {
|
||||
localStorage.removeItem("pkce_verifier");
|
||||
localStorage.removeItem(`pkce_verifier_${state}`);
|
||||
}
|
||||
|
||||
const authInfo = {
|
||||
@@ -407,24 +402,27 @@ export function getProviderUrl(provider) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getProviderLogoWidget(provider) {
|
||||
export function getProviderLogoWidget(provider, options = {}) {
|
||||
if (provider === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const url = getProviderUrl(provider);
|
||||
if (url !== "") {
|
||||
const disableLink = options.disableLink === true;
|
||||
const imgEl = <img width={36} height={36} src={Setting.getProviderLogoURL(provider)} alt={provider.displayName} />;
|
||||
|
||||
if (url !== "" && !disableLink) {
|
||||
return (
|
||||
<Tooltip title={provider.type}>
|
||||
<a target="_blank" rel="noreferrer" href={getProviderUrl(provider)}>
|
||||
<img width={36} height={36} src={Setting.getProviderLogoURL(provider)} alt={provider.displayName} />
|
||||
{imgEl}
|
||||
</a>
|
||||
</Tooltip>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Tooltip title={provider.type}>
|
||||
<img width={36} height={36} src={Setting.getProviderLogoURL(provider)} alt={provider.displayName} />
|
||||
{imgEl}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -293,8 +293,17 @@ class SignupPage extends React.Component {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if consent is required
|
||||
if (oAuthParams && res.data && typeof res.data === "object" && res.data.required === true) {
|
||||
// Consent required, redirect to consent page
|
||||
Setting.goToLink(`/consent/${application.name}?${window.location.search.substring(1)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// the user's id will be returned by `signup()`, if user signup by phone, the `username` in `values` is undefined.
|
||||
values.username = res.data.split("/")[1];
|
||||
if (typeof res.data === "string") {
|
||||
values.username = res.data.split("/")[1];
|
||||
}
|
||||
if (Setting.hasPromptPage(application) && (!values.plan || !values.pricing)) {
|
||||
AuthBackend.getAccount("")
|
||||
.then((res) => {
|
||||
|
||||
@@ -130,7 +130,7 @@ export function getOAuthGetParameters(params) {
|
||||
}
|
||||
|
||||
let state = getRefinedValue(queries.get("state"));
|
||||
if (state.startsWith("/auth/oauth2/login.php?wantsurl=")) {
|
||||
if (state.startsWith("/auth/oauth2/login.php?wantsurl")) {
|
||||
// state contains URL param encoding for Moodle, URLSearchParams automatically decoded it, so here encode it again
|
||||
state = encodeURIComponent(state);
|
||||
}
|
||||
@@ -213,17 +213,19 @@ export async function WechatOfficialAccountModal(application, provider, method)
|
||||
}
|
||||
|
||||
const t1 = setInterval(await getEvent, 1000, application, provider, res.data2, method);
|
||||
{Modal.info({
|
||||
title: i18next.t("provider:Please use WeChat to scan the QR code and follow the official account for sign in"),
|
||||
content: (
|
||||
<div style={{marginRight: "34px"}}>
|
||||
<QRCode style={{padding: "20px", margin: "auto"}} bordered={false} value={res.data} size={230} />
|
||||
</div>
|
||||
),
|
||||
onOk() {
|
||||
window.clearInterval(t1);
|
||||
},
|
||||
});}
|
||||
{
|
||||
Modal.info({
|
||||
title: i18next.t("provider:Please use WeChat to scan the QR code and follow the official account for sign in"),
|
||||
content: (
|
||||
<div style={{marginRight: "34px"}}>
|
||||
<QRCode style={{padding: "20px", margin: "auto"}} bordered={false} value={res.data} size={230} />
|
||||
</div>
|
||||
),
|
||||
onOk() {
|
||||
window.clearInterval(t1);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,3 +79,10 @@ export function deleteCert(cert) {
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function refreshDomainExpire(owner, name) {
|
||||
return fetch(`${Setting.ServerUrl}/api/update-cert-domain-expire?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
50
web/src/backend/ConsentBackend.js
Normal file
50
web/src/backend/ConsentBackend.js
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright 2026 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.
|
||||
|
||||
import * as Setting from "../Setting";
|
||||
|
||||
export function grantConsent(consent, oAuthParams) {
|
||||
const request = {
|
||||
...consent,
|
||||
clientId: oAuthParams.clientId,
|
||||
provider: "",
|
||||
signinMethod: "",
|
||||
responseType: oAuthParams.responseType || "code",
|
||||
redirectUri: oAuthParams.redirectUri,
|
||||
scope: oAuthParams.scope,
|
||||
state: oAuthParams.state,
|
||||
nonce: oAuthParams.nonce || "",
|
||||
challenge: oAuthParams.codeChallenge || "",
|
||||
resource: "",
|
||||
};
|
||||
return fetch(`${Setting.ServerUrl}/api/grant-consent`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(request),
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function revokeConsent(consent) {
|
||||
return fetch(`${Setting.ServerUrl}/api/revoke-consent`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(consent),
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
53
web/src/backend/RuleBackend.js
Normal file
53
web/src/backend/RuleBackend.js
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright 2023 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 * as Setting from "../Setting";
|
||||
|
||||
export function getRules(owner, page = "", pageSize = "", sortField = "", sortOrder = "") {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-rules?owner=${owner}&p=${page}&pageSize=${pageSize}&sortField=${sortField}&sortOrder=${sortOrder}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getRule(owner, name) {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-rule?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function addRule(rule) {
|
||||
return fetch(`${Setting.ServerUrl}/api/add-rule`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(rule),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function updateRule(owner, name, rule) {
|
||||
return fetch(`${Setting.ServerUrl}/api/update-rule?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(rule),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function deleteRule(rule) {
|
||||
return fetch(`${Setting.ServerUrl}/api/delete-rule`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(rule),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
63
web/src/backend/SiteBackend.js
Normal file
63
web/src/backend/SiteBackend.js
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright 2023 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 * as Setting from "../Setting";
|
||||
|
||||
export function getGlobalSites() {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-global-sites`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getSites(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-sites?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getSite(owner, name) {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-site?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function updateSite(owner, name, site) {
|
||||
const newSite = Setting.deepCopy(site);
|
||||
return fetch(`${Setting.ServerUrl}/api/update-site?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newSite),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function addSite(site) {
|
||||
const newSite = Setting.deepCopy(site);
|
||||
return fetch(`${Setting.ServerUrl}/api/add-site`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newSite),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function deleteSite(site) {
|
||||
const newSite = Setting.deepCopy(site);
|
||||
return fetch(`${Setting.ServerUrl}/api/delete-site`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newSite),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user