Compare commits

...

57 Commits

Author SHA1 Message Date
Yang Luo
2895c72d32 fix: improve Actions field UI in permission list page 2026-03-11 21:43:32 +08:00
ANormalDD
f6129b09c8 feat: implement minimal HTML+JS OAuth callback and provider_hint flows (#5238) 2026-03-10 19:04:55 +08:00
Yang Luo
0bbbb48af1 feat: upgrade to golang:1.24.13 and node:20.20.1 (LTS), update Dockerfile base images (#5246) 2026-03-10 18:12:12 +08:00
gaël Prudhomme
34a8b252d5 feat: fix bug in site's owner/organization (#5239) 2026-03-09 23:55:41 +08:00
DacongDA
c756e56f74 feat: fix proxy server panic issue if port has been used (#5240) 2026-03-09 22:16:58 +08:00
DacongDA
dbc2a676ba feat: allow user to set binding rule in provider items (#5224) 2026-03-07 22:20:48 +08:00
Yang Luo
74e6b73e7b feat: fix empty "Binding providers" prompt step appearing after signup (#5221) 2026-03-07 17:41:24 +08:00
Yang Luo
07de8a40d6 feat: fix invitation code lost during signup when form field is not visible (#5231) 2026-03-07 15:35:47 +08:00
ANormalDD
c6a6ec8869 feat: fix bug that invitation links fail with external OAuth providers (#5229) 2026-03-07 14:45:22 +08:00
Yang Luo
394b3e1372 feat: add Kerberos/SPNEGO authentication (#5225) 2026-03-07 09:46:45 +08:00
Yang Luo
fa93d4eb8b feat: add LDAP server attribute filtering per organization (#5222) 2026-03-07 00:53:20 +08:00
Yang Luo
47a5fc8b09 feat: support regex/wildcard scopes in OAuth authorization requests (#5223) 2026-03-07 00:52:27 +08:00
Yang Luo
c1acb7a432 fix: fix go.sum 2026-03-07 00:02:33 +08:00
Yang Luo
c10b2c162f feat: fix Twilio SMS sending verification code bug (#5205) 2026-03-06 22:32:09 +08:00
ANormalDD
41ec8ba44f feat(web): add AutoComplete for SAML attributes value (#5215) 2026-03-06 20:27:40 +08:00
Yang Luo
7df722a103 fix: set organization.balanceCredit's max to 0 2026-03-06 14:10:07 +08:00
Yang Luo
04b1ca1157 fix: revert "feat: fix BalanceCredit to act as overdraft limit, not minimum balance floor" (#5214) 2026-03-06 13:36:21 +08:00
DacongDA
b0fecefeb7 feat: add Site and Rule to Casdoor (#5194) 2026-03-06 01:02:16 +08:00
Yang Luo
167d24fb1f fix: fix getOAuthGetParameters bug in Moodle 2026-03-05 23:05:20 +08:00
Yang Luo
dc58ac0503 feat: fix BalanceCredit to act as overdraft limit, not minimum balance floor (#5210) 2026-03-05 22:56:46 +08:00
Br1an
038d021797 fix: skip password columns in syncer when remote has no password data (#5183) 2026-03-05 22:35:27 +08:00
Yang Luo
7ba660fd7f feat: fix normal users blocked from /product-store (#5195) 2026-03-05 22:24:36 +08:00
Tomáš Karela Procházka
b1c31a4a9d feat: add Resend email provider (#5200) 2026-03-05 20:55:23 +08:00
Yang Luo
90d7add503 fix: remove useless returnUrl field from ProductEditPage (#5190) 2026-03-04 21:48:47 +08:00
Yang Luo
c961e75ad3 feat: fall back to English when unsupported Accept-Language locale is requested (#5177) 2026-03-04 21:41:10 +08:00
Br1an
547189a034 feat: add missing "min" param for Cloud PNVS SMS provider (#5180) 2026-03-03 09:08:31 +08:00
DacongDA
be725eda74 feat: merge CasWAF's cert related code into Casdoor's cert code (#5171) 2026-02-27 01:36:07 +08:00
Ke Wang
0765b352c9 fix: respect application's ID signup rule in WeChat Mini Program login (#5168) 2026-02-24 21:21:18 +08:00
Yang Luo
a2a8b582d9 feat: make DingTalk syncer respect TableColumns field mapping configuration (#5073) 2026-02-24 12:55:40 +08:00
Sriram-B-Srivatsa
0973652be4 fix: reduce code duplication in Logout logic (#5163) 2026-02-24 12:53:31 +08:00
Yang Luo
fef75715bf fix(web): prevent dashboard graph overlap when y-axis values increase 2026-02-23 15:24:05 +08:00
hikarukimi
4f78d56e31 feat: add OAuth consent page 2026-02-23 15:16:04 +08:00
hikarukimi
712bc756bc fix: improve code format 2026-02-23 15:09:57 +08:00
DacongDA
1c9952e3d9 feat: support JWT Profile for OAuth 2.0 Client Grants (RFC 7523) (#5124) 2026-02-23 14:44:34 +08:00
Yang Luo
bbaa28133f feat: apply application.DefaultGroup for OAuth signups (#5157) 2026-02-22 01:06:18 +08:00
Yang Luo
baef7680ea feat: validate OAuth scopes against Application config; return invalid_scope per RFC 6749 (#5153) 2026-02-21 17:44:26 +08:00
Yang Luo
d15b66177c feat: add missing Telegram field to User struct (#5151) 2026-02-21 17:21:31 +08:00
Yang Luo
5ce6bac529 fix: improve provider table links 2026-02-21 01:36:00 +08:00
Yang Luo
0621f35665 fix: improve tabs height UI in app edit page 2026-02-21 01:16:36 +08:00
Yang Luo
1ac2490419 fix: add OIDC and SAML tabs in application edit page 2026-02-21 01:13:54 +08:00
DacongDA
8c50ada494 feat: refactor provider edit page into different JS files (#5141) 2026-02-21 00:57:38 +08:00
Yang Luo
22da90576e feat: can free input in "Tag" in Addresses table 2026-02-20 16:49:50 +08:00
Yang Luo
b00404cb3a fix: fix RegionSelect cannot save value bug in Addresses table 2026-02-20 16:45:43 +08:00
Yang Luo
2ed27f4f0a fix: improve tables UI in my account page 2026-02-20 16:35:29 +08:00
Yang Luo
bf538d5260 fix: update UpdateUser() columns for missing User fields 2026-02-20 11:02:52 +08:00
Yang Luo
13ee5fd150 feat: sync newOrganization() accountItems with getBuiltInAccountItems() (#5146) 2026-02-20 10:47:02 +08:00
Yang Luo
04cdd5a012 feat: add missing user fields to GetTranslatedUserItems, getBuiltInAccountItems, init_data template, and UserFields (#5144) 2026-02-20 10:37:51 +08:00
Yang Luo
7b4873734b feat: fix "--config" flag to actually load specified configuration file (#5139) 2026-02-19 02:13:29 +08:00
Yang Luo
8d2290944a fix: add back Payment.ProductName and ProductDisplayName fields for backward compatibility 2026-02-18 19:28:14 +08:00
Yang Luo
6a2bba1627 feat: fix field visibility logic for provider types in ProviderEditPage (#5134) 2026-02-18 15:22:28 +08:00
Yang Luo
07554bbbe5 feat: fix Alipay OAuth provider by loading private key from cert object (#5119) 2026-02-17 14:42:21 +08:00
karatekaneen
a050403ee5 feat: fix bug that PKCE fails when multiple custom OAuth providers are configured (#5117) 2026-02-16 23:32:07 +08:00
IsAurora6
118eb0af80 feat: Optimize the display of payment products. (#5115) 2026-02-16 16:32:02 +08:00
Yang Luo
c16aebe642 fix: update README slogan 2026-02-16 02:33:45 +08:00
Yang Luo
3b8e7c9da2 fix: extend application with reverse proxy fields (#5113) 2026-02-16 02:23:47 +08:00
Yang Luo
4d5de767b0 fix: sync frontend i18n strings 2026-02-16 02:01:48 +08:00
Yang Luo
54bf8eae5c fix: improve category column UI in app list page 2026-02-16 01:46:06 +08:00
182 changed files with 13204 additions and 2008 deletions

View File

@@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM node:18.19.0 AS FRONT
FROM --platform=$BUILDPLATFORM node:20.20.1 AS FRONT
WORKDIR /web
# Copy only dependency files first for better caching
@@ -9,7 +9,7 @@ RUN yarn install --frozen-lockfile --network-timeout 1000000
COPY ./web .
RUN NODE_OPTIONS="--max-old-space-size=4096" yarn run build
FROM --platform=$BUILDPLATFORM golang:1.23.12 AS BACK
FROM --platform=$BUILDPLATFORM golang:1.24.13 AS BACK
WORKDIR /go/src/casdoor
# Copy only go.mod and go.sum first for dependency caching

View File

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

View File

@@ -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, *, *
@@ -117,6 +118,7 @@ p, *, *, GET, /api/run-casbin-command, *, *
p, *, *, POST, /api/refresh-engines, *, *
p, *, *, GET, /api/get-invitation-info, *, *
p, *, *, GET, /api/faceid-signin-begin, *, *
p, *, *, GET, /api/kerberos-login, *, *
`
sa := stringadapter.NewAdapter(ruleText)

107
certificate/account.go Normal file
View 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
}

View 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
View 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
View 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
View 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
View 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")
}

View File

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

View File

@@ -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 == "" {
@@ -405,7 +409,7 @@ func (c *ApiController) Logout() {
return
}
if application == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist")), token.Application)
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), token.Application))
return
}
@@ -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
}

View File

@@ -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,15 @@ 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)
expandedScope, valid := object.IsScopeValidAndExpand(scope, application)
if !valid {
resp = &Response{Status: "error", Msg: "error: invalid_scope", Data: ""}
} else {
token, _ := object.GetTokenByUser(application, user, expandedScope, 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)
@@ -438,6 +456,55 @@ func checkMfaEnable(c *ApiController, user *object.User, organization *object.Or
return false
}
func getExistUserByBindingRule(providerItem *object.ProviderItem, application *object.Application, userInfo *idp.UserInfo) (user *object.User, err error) {
if providerItem.BindingRule == nil {
providerItem.BindingRule = &[]string{"Email", "Phone", "Name"}
}
if len(*providerItem.BindingRule) == 0 {
return nil, nil
}
for _, rule := range *providerItem.BindingRule {
// Find existing user with Email
if rule == "Email" {
user, err = object.GetUserByField(application.Organization, "email", userInfo.Email)
if err != nil {
return nil, err
}
if user != nil {
return user, nil
}
}
// Find existing user with phone number
if rule == "Phone" {
user, err = object.GetUserByField(application.Organization, "phone", userInfo.Phone)
if err != nil {
return nil, err
}
if user != nil {
return user, nil
}
}
// Try to find existing user by username (case-insensitive)
// This allows OAuth providers (e.g., Wecom) to automatically associate with
// existing users when usernames match, particularly useful for enterprise
// scenarios where signup is disabled and users already exist in Casdoor
if rule == "Name" {
user, err = object.GetUserByFields(application.Organization, userInfo.Username)
if err != nil {
return nil, err
}
if user != nil {
return user, nil
}
}
}
return user, nil
}
// Login ...
// @Title Login
// @Tag Login API
@@ -739,7 +806,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)
@@ -785,7 +856,7 @@ func (c *ApiController) Login() {
return
}
if !reg.MatchString(userInfo.Email) {
c.ResponseError(fmt.Sprintf(c.T("check:Email is invalid")))
c.ResponseError(c.T("check:Email is invalid"))
}
}
}
@@ -825,36 +896,10 @@ func (c *ApiController) Login() {
c.Ctx.Input.SetParam("recordUserId", user.GetId())
} else if provider.Category == "OAuth" || provider.Category == "Web3" || provider.Category == "SAML" {
// Sign up via OAuth
if application.EnableLinkWithEmail {
if userInfo.Email != "" {
// Find existing user with Email
user, err = object.GetUserByField(application.Organization, "email", userInfo.Email)
if err != nil {
c.ResponseError(err.Error())
return
}
}
if user == nil && userInfo.Phone != "" {
// Find existing user with phone number
user, err = object.GetUserByField(application.Organization, "phone", userInfo.Phone)
if err != nil {
c.ResponseError(err.Error())
return
}
}
}
// Try to find existing user by username (case-insensitive)
// This allows OAuth providers (e.g., Wecom) to automatically associate with
// existing users when usernames match, particularly useful for enterprise
// scenarios where signup is disabled and users already exist in Casdoor
if user == nil && userInfo.Username != "" {
user, err = object.GetUserByFields(application.Organization, userInfo.Username)
if err != nil {
c.ResponseError(err.Error())
return
}
user, err = getExistUserByBindingRule(providerItem, application, userInfo)
if err != nil {
c.ResponseError(err.Error())
return
}
if user == nil {
@@ -950,11 +995,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

View File

@@ -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
View 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)
}

105
controllers/kerberos.go Normal file
View File

@@ -0,0 +1,105 @@
// 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 (
"fmt"
"strings"
"github.com/casdoor/casdoor/form"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// KerberosLogin
// @Title KerberosLogin
// @Tag Login API
// @Description Kerberos/SPNEGO login via Integrated Windows Authentication
// @Param application query string true "application name"
// @Success 200 {object} controllers.Response The Response object
// @router /kerberos-login [get]
func (c *ApiController) KerberosLogin() {
applicationName := c.Ctx.Input.Query("application")
if applicationName == "" {
c.ResponseError(c.T("general:Missing parameter") + ": application")
return
}
application, err := object.GetApplication(fmt.Sprintf("admin/%s", applicationName))
if err != nil {
c.ResponseError(err.Error())
return
}
if application == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), applicationName))
return
}
organization, err := object.GetOrganization(util.GetId("admin", application.Organization))
if err != nil {
c.ResponseError(err.Error())
return
}
if organization == nil {
c.ResponseError(fmt.Sprintf("The organization: %s does not exist", application.Organization))
return
}
if organization.KerberosRealm == "" || organization.KerberosKeytab == "" {
c.ResponseError("Kerberos is not configured for this organization")
return
}
authHeader := c.Ctx.Input.Header("Authorization")
if authHeader == "" || !strings.HasPrefix(authHeader, "Negotiate ") {
c.Ctx.Output.Header("WWW-Authenticate", "Negotiate")
c.Ctx.Output.SetStatus(401)
c.Ctx.Output.Body([]byte("Kerberos authentication required"))
return
}
spnegoToken := strings.TrimPrefix(authHeader, "Negotiate ")
kerberosUsername, err := object.ValidateKerberosToken(organization, spnegoToken)
if err != nil {
c.Ctx.Output.Header("WWW-Authenticate", "Negotiate")
c.ResponseError(fmt.Sprintf("Kerberos authentication failed: %s", err.Error()))
return
}
user, err := object.GetUserByKerberosName(organization.Name, kerberosUsername)
if err != nil {
c.ResponseError(err.Error())
return
}
if user == nil {
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), kerberosUsername))
return
}
application.OrganizationObj = organization
authForm := &form.AuthForm{
Type: "code",
Application: applicationName,
Organization: organization.Name,
}
resp := c.HandleLoggedIn(application, user, authForm)
if resp != nil {
c.Data["json"] = resp
c.ServeJSON()
}
}

193
controllers/rule.go Normal file
View 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
View 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()
}

View File

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

View File

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

View File

@@ -15,6 +15,7 @@
package controllers
import (
"errors"
"fmt"
"strings"
@@ -230,7 +231,7 @@ func (c *ApiController) GetProviderFromContext(category string) (*object.Provide
userId, ok := c.RequireSignedIn()
if !ok {
return nil, fmt.Errorf(c.T("general:Please login first"))
return nil, errors.New(c.T("general:Please login first"))
}
application, err := object.GetApplicationByUserId(userId)

View File

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

102
go.mod
View File

@@ -1,6 +1,8 @@
module github.com/casdoor/casdoor
go 1.23.0
go 1.24.0
toolchain go1.24.13
require (
github.com/Masterminds/squirrel v1.5.3
@@ -22,6 +24,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,12 +33,13 @@ 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
github.com/go-asn1-ber/asn1-ber v1.5.5
github.com/go-git/go-git/v5 v5.16.3
github.com/go-jose/go-jose/v4 v4.1.2
github.com/go-jose/go-jose/v4 v4.1.3
github.com/go-ldap/ldap/v3 v3.4.6
github.com/go-mysql-org/go-mysql v1.7.0
github.com/go-pay/gopay v1.5.115
@@ -44,9 +49,13 @@ 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/jcmturner/gokrb5/v8 v8.4.4
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
@@ -55,8 +64,9 @@ require (
github.com/polarsource/polar-go v0.12.0
github.com/pquerna/otp v1.4.0
github.com/prometheus/client_golang v1.19.0
github.com/prometheus/client_model v0.6.0
github.com/prometheus/client_model v0.6.2
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
@@ -70,10 +80,11 @@ require (
github.com/xorm-io/builder v0.3.13
github.com/xorm-io/core v0.7.4
github.com/xorm-io/xorm v1.1.6
golang.org/x/crypto v0.40.0
golang.org/x/net v0.41.0
golang.org/x/oauth2 v0.27.0
golang.org/x/text v0.27.0
golang.org/x/crypto v0.47.0
golang.org/x/net v0.49.0
golang.org/x/oauth2 v0.32.0
golang.org/x/text v0.33.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
@@ -81,11 +92,11 @@ require (
)
require (
cel.dev/expr v0.18.0 // indirect
cel.dev/expr v0.24.0 // indirect
cloud.google.com/go v0.116.0 // indirect
cloud.google.com/go/auth v0.13.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
cloud.google.com/go/compute/metadata v0.6.0 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/iam v1.2.2 // indirect
cloud.google.com/go/monitoring v1.21.2 // indirect
cloud.google.com/go/storage v1.47.0 // indirect
@@ -95,7 +106,7 @@ require (
github.com/Azure/azure-storage-blob-go v0.15.0 // indirect
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.49.0 // indirect
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect
@@ -122,16 +133,16 @@ 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/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f // 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
@@ -141,8 +152,8 @@ require (
github.com/drswork/go-twitter v0.0.0-20221107160839-dea1b6ed53d7 // indirect
github.com/ebitengine/purego v0.9.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/envoyproxy/go-control-plane v0.13.1 // indirect
github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.35.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.6.0 // indirect
github.com/ggicci/httpin v0.19.0 // indirect
@@ -150,7 +161,7 @@ require (
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-lark/lark v1.15.1 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-pay/crypto v0.0.1 // indirect
@@ -175,8 +186,14 @@ require (
github.com/gorilla/websocket v1.5.3 // indirect
github.com/gregdel/pushover v1.3.1 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
github.com/jcmturner/gofork v1.7.6 // indirect
github.com/jcmturner/goidentity/v6 v6.0.1 // indirect
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
@@ -188,41 +205,49 @@ 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
github.com/slack-go/slack v0.15.0 // indirect
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
github.com/spyzhov/ajson v0.8.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/syndtr/goleveldb v1.0.0 // indirect
@@ -238,40 +263,42 @@ 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
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.mau.fi/util v0.8.3 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.32.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.57.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 // indirect
go.opentelemetry.io/otel v1.32.0 // indirect
go.opentelemetry.io/otel/metric v1.32.0 // indirect
go.opentelemetry.io/otel/sdk v1.32.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect
go.opentelemetry.io/otel/trace v1.32.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.40.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
go.opentelemetry.io/otel v1.40.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.7.0 // indirect
go.uber.org/zap v1.19.1 // indirect
golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e // indirect
golang.org/x/image v0.0.0-20220302094943-723b81ca9867 // indirect
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
golang.org/x/image v0.18.0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/tools v0.40.0 // indirect
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
google.golang.org/grpc v1.68.0 // indirect
google.golang.org/grpc/stats/opentelemetry v0.0.0-20241028142157-ada6787961b3 // indirect
google.golang.org/protobuf v1.36.1 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/grpc v1.78.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
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 +310,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
)

207
go.sum
View File

@@ -1,5 +1,5 @@
cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo=
cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
@@ -186,8 +186,8 @@ cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZ
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY=
cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck=
cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w=
@@ -658,8 +658,8 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 h1:3c8yed4lgqTt+oTQ+JNMDo+F4xprBf+O/il4ZC0nRLw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0 h1:o90wcURuxekmXrtxmYWTyNla0+ZEHhud6DI1ZTxd1vI=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0/go.mod h1:6fTWu4m3jocfUZLYF5KsZC1TUfRvEjs7lM4crme/irw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.49.0 h1:jJKWl98inONJAr/IZrdFQUWcwUO95DLY1XMD1ZIut+g=
@@ -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=
@@ -872,7 +875,6 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g=
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -905,12 +907,20 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
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/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0=
github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4=
github.com/corazawaf/coraza-coreruleset v0.0.0-20240226094324-415b1017abdc h1:OlJhrgI3I+FLUCTI3JJW8MoqyM78WbqJjecqMnqG+wc=
github.com/corazawaf/coraza-coreruleset v0.0.0-20240226094324-415b1017abdc/go.mod h1:7rsocqNDkTCira5T0M7buoKR2ehh7YZiPkzxRuAgvVU=
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=
@@ -920,8 +930,9 @@ github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs
github.com/cznic/sortutil v0.0.0-20181122101858-f5f958428db8/go.mod h1:q2w6Bg5jeox1B+QkJ6Wp/+Vn0G/bo3f1uY7Fn3vivIQ=
github.com/cznic/strutil v0.0.0-20171016134553-529a34b1c186/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc=
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=
@@ -971,14 +982,18 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34=
github.com/envoyproxy/go-control-plane v0.11.0/go.mod h1:VnHyVMpzcLvCFt9yUz1UnCwHLhwx1WguiVDV7pTG/tI=
github.com/envoyproxy/go-control-plane v0.13.1 h1:vPfJZCkob6yTMEgS+0TwfTUfbHjfy/6vOJ8hUWX/uXE=
github.com/envoyproxy/go-control-plane v0.13.1/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw=
github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM=
github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs=
github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo=
github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo=
github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w=
github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM=
github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
@@ -990,6 +1005,8 @@ github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzP
github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=
github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2/go.mod h1:VzmDKDJVZI3aJmnRI9VjAn9nJ8qPPsN1fqzr9dqInIo=
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
@@ -1023,8 +1040,8 @@ github.com/go-git/go-git/v5 v5.16.3/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lo
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=
github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs=
@@ -1041,8 +1058,8 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-mysql-org/go-mysql v1.7.0 h1:qE5FTRb3ZeTQmlk3pjE+/m2ravGxxRDrVDTyDe9tvqI=
@@ -1250,7 +1267,9 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1 h1:LqbZZ9sNMWVjeXS4NN5oVvhMjDyLhmA1LG86oSo+IqY=
github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1/go.mod h1:YeAe0gNeiNT5hoiZRI4yiOky6jVdNvfO2N6Kav/HmxY=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
@@ -1282,6 +1301,8 @@ github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdv
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
@@ -1291,6 +1312,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=
@@ -1300,14 +1323,25 @@ github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da h1:FjHUJJ7oBW4G/9
github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jcchavezs/mergefs v0.1.0 h1:7oteO7Ocl/fnfFMkoVLJxTveCjrsd//UB0j89xmnpec=
github.com/jcchavezs/mergefs v0.1.0/go.mod h1:eRLTrsA+vFwQZ48hj8p8gki/5v9C2bFtHH5Mnn4bcGk=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc=
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
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 +1355,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 +1426,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 +1443,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=
@@ -1435,6 +1479,8 @@ github.com/microsoft/go-mssqldb v1.9.0/go.mod h1:GBbW9ASTiDC+mpgWDGKdm3FnFLTUsLY
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/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 +1543,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=
@@ -1527,8 +1575,9 @@ github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZ
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
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=
@@ -1549,8 +1598,8 @@ github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
@@ -1572,12 +1621,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 +1648,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 +1670,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 +1691,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=
@@ -1645,6 +1702,8 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO
github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/spyzhov/ajson v0.8.0 h1:sFXyMbi4Y/BKjrsfkUZHSjA2JM1184enheSjjoT/zCc=
github.com/spyzhov/ajson v0.8.0/go.mod h1:63V+CGM6f1Bu/p4nLIN8885ojBdt88TbLoSFzyqMuVA=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
@@ -1653,6 +1712,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 +1756,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 +1774,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=
@@ -1758,24 +1824,26 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/detectors/gcp v1.32.0 h1:P78qWqkLSShicHmAzfECaTgvslqHxblNE9j62Ws1NK8=
go.opentelemetry.io/contrib/detectors/gcp v1.32.0/go.mod h1:TVqo0Sda4Cv8gCIixd7LuLwW4EylumVWfhjZJjDD4DU=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.57.0 h1:qtFISDHKolvIxzSs0gIaiPUPR0Cucb0F2coHC7ZLdps=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.57.0/go.mod h1:Y+Pop1Q6hCOnETWTW4NROK/q1hv50hM7yDaUTjG8lp8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.40.0 h1:Awaf8gmW99tZTOWqkLCOl6aw1/rxAWVlHsHIZ3fT2sA=
go.opentelemetry.io/contrib/detectors/gcp v1.40.0/go.mod h1:99OY9ZCqyLkzJLTh5XhECpLRSxcZl+ZDKBEO+jMBFR4=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 h1:XmiuHzgJt067+a6kwyAzkhXooYVv3/TOw9cM2VfJgUM=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0/go.mod h1:KDgtbWKTQs4bM+VPUr6WlL9m/WXcmkCcBlIzqxPGzmI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
@@ -1827,6 +1895,7 @@ golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
@@ -1841,8 +1910,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20181106170214-d68db9428509/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1873,8 +1942,9 @@ golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+o
golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/image v0.0.0-20220302094943-723b81ca9867 h1:TcHcE0vrmgzNH1v3ppjcMGbhG5+9fMuvOmUYwNEF4q4=
golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -1907,8 +1977,8 @@ golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20171115151908-9dfe39835686/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1996,8 +2066,8 @@ golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -2029,8 +2099,8 @@ golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk=
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20171101214715-fd80eb99c8f6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -2053,8 +2123,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -2180,8 +2250,8 @@ golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -2205,8 +2275,8 @@ golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -2231,8 +2301,8 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -2315,8 +2385,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -2329,6 +2399,8 @@ gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJ
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0=
gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY=
@@ -2545,15 +2617,15 @@ google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go.
google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
google.golang.org/genproto/googleapis/api v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda h1:+2XxjfsAu6vqFxwGBRcHiMaDCuZiqXGDUDVWVtrFAnE=
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234015-3fc162c6f38a/go.mod h1:xURIpW9ES5+/GZhnV6beoEtxQrnkRGIfP5VQG2tCBLc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -2595,11 +2667,9 @@ google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/grpc/stats/opentelemetry v0.0.0-20241028142157-ada6787961b3 h1:hUfOButuEtpc0UvYiaYRbNwxVYr0mQQOWq6X8beJ9Gc=
google.golang.org/grpc/stats/opentelemetry v0.0.0-20241028142157-ada6787961b3/go.mod h1:jzYlkSMbKypzuu6xoAEijsNVo9ZeDF1u/zCfFgsx7jg=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -2618,8 +2688,8 @@ google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
@@ -2632,13 +2702,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 +2819,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=

View File

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

View File

@@ -17,6 +17,7 @@ package idp
import (
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@@ -102,7 +103,7 @@ func (idp *AdfsIdProvider) GetToken(code string) (*oauth2.Token, error) {
return nil, err
}
if pToken.ErrMsg != "" {
return nil, fmt.Errorf(pToken.ErrMsg)
return nil, errors.New(pToken.ErrMsg)
}
token := &oauth2.Token{

View File

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

View File

@@ -16,6 +16,7 @@ package idp
import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
@@ -158,7 +159,7 @@ func (idp *DingTalkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
}
if dtUserInfo.OpenId == "" || dtUserInfo.UnionId == "" {
return nil, fmt.Errorf(string(data))
return nil, errors.New(string(data))
}
countryCode, err := util.GetCountryCode(dtUserInfo.StateCode, dtUserInfo.Mobile)
@@ -267,7 +268,7 @@ func (idp *DingTalkIdProvider) getUserId(unionId string, accessToken string) (st
if data.ErrCode == 60121 {
return "", fmt.Errorf("该应用只允许本企业内部用户登录,您不属于该企业,无法登录")
} else if data.ErrCode != 0 {
return "", fmt.Errorf(data.ErrMessage)
return "", errors.New(data.ErrMessage)
}
return data.Result.UserId, nil
}
@@ -294,7 +295,7 @@ func (idp *DingTalkIdProvider) getUserCorpEmail(userId string, accessToken strin
return "", "", "", err
}
if data.ErrMessage != "ok" {
return "", "", "", fmt.Errorf(data.ErrMessage)
return "", "", "", errors.New(data.ErrMessage)
}
return data.Result.Mobile, data.Result.Email, data.Result.UnionId, nil
}

View File

@@ -19,6 +19,7 @@ import (
"crypto/sha1"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@@ -124,7 +125,7 @@ func (idp *WeChatIdProvider) GetToken(code string) (*oauth2.Token, error) {
// {"errcode":40163,"errmsg":"code been used, rid: 6206378a-793424c0-2e4091cc"}
if strings.Contains(buf.String(), "errcode") {
return nil, fmt.Errorf(buf.String())
return nil, errors.New(buf.String())
}
var wechatAccessToken WechatAccessToken

View File

@@ -17,6 +17,7 @@ package idp
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@@ -93,7 +94,7 @@ func (idp *WeChatMobileIdProvider) GetToken(code string) (*oauth2.Token, error)
// Check for error response
if bytes.Contains(buf.Bytes(), []byte("errcode")) {
return nil, fmt.Errorf(buf.String())
return nil, errors.New(buf.String())
}
var wechatAccessToken WechatAccessToken

View File

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

View File

@@ -203,49 +203,101 @@ func handleSearch(w ldap.ResponseWriter, m *ldap.Message) {
return
}
orgCache := make(map[string]*object.Organization)
for _, user := range users {
dn := fmt.Sprintf("uid=%s,cn=%s,%s", user.Id, user.Name, string(r.BaseObject()))
e := ldap.NewSearchResultEntry(dn)
uidNumberStr := fmt.Sprintf("%v", hash(user.Name))
if _, ok := orgCache[user.Owner]; !ok {
org, err := object.GetOrganizationByUser(user)
if err != nil {
log.Printf("handleSearch: failed to get organization for user %s: %v", user.Name, err)
}
orgCache[user.Owner] = org
}
org := orgCache[user.Owner]
e := buildUserSearchEntry(user, string(r.BaseObject()), resolveRequestAttributes(r.Attributes()), org)
w.Write(e)
}
w.Write(res)
}
// resolveRequestAttributes expands the "*" wildcard to the full list of additional LDAP attributes.
func resolveRequestAttributes(attrs message.AttributeSelection) []string {
result := make([]string, 0, len(attrs))
for _, attr := range attrs {
if string(attr) == "*" {
result = make([]string, 0, len(AdditionalLdapAttributes))
for _, a := range AdditionalLdapAttributes {
result = append(result, string(a))
}
return result
}
result = append(result, string(attr))
}
return result
}
// buildUserSearchEntry constructs an LDAP search result entry for the given user,
// respecting the organization's LdapAttributes filter.
func buildUserSearchEntry(user *object.User, baseDN string, attrs []string, org *object.Organization) message.SearchResultEntry {
dn := fmt.Sprintf("uid=%s,cn=%s,%s", user.Id, user.Name, baseDN)
e := ldap.NewSearchResultEntry(dn)
uidNumberStr := fmt.Sprintf("%v", hash(user.Name))
if IsLdapAttrAllowed(org, "uidNumber") {
e.AddAttribute("uidNumber", message.AttributeValue(uidNumberStr))
}
if IsLdapAttrAllowed(org, "gidNumber") {
e.AddAttribute("gidNumber", message.AttributeValue(uidNumberStr))
}
if IsLdapAttrAllowed(org, "homeDirectory") {
e.AddAttribute("homeDirectory", message.AttributeValue("/home/"+user.Name))
}
if IsLdapAttrAllowed(org, "cn") {
e.AddAttribute("cn", message.AttributeValue(user.Name))
}
if IsLdapAttrAllowed(org, "uid") {
e.AddAttribute("uid", message.AttributeValue(user.Id))
}
if IsLdapAttrAllowed(org, "mail") {
e.AddAttribute("mail", message.AttributeValue(user.Email))
}
if IsLdapAttrAllowed(org, "mobile") {
e.AddAttribute("mobile", message.AttributeValue(user.Phone))
}
if IsLdapAttrAllowed(org, "sn") {
e.AddAttribute("sn", message.AttributeValue(user.LastName))
}
if IsLdapAttrAllowed(org, "givenName") {
e.AddAttribute("givenName", message.AttributeValue(user.FirstName))
// Add POSIX attributes for Linux machine login support
}
// Add POSIX attributes for Linux machine login support
if IsLdapAttrAllowed(org, "loginShell") {
e.AddAttribute("loginShell", getAttribute("loginShell", user))
}
if IsLdapAttrAllowed(org, "gecos") {
e.AddAttribute("gecos", getAttribute("gecos", user))
// Add SSH public key if available
}
// Add SSH public key if available
if IsLdapAttrAllowed(org, "sshPublicKey") {
sshKey := getAttribute("sshPublicKey", user)
if sshKey != "" {
e.AddAttribute("sshPublicKey", sshKey)
}
// Add objectClass for posixAccount
e.AddAttribute("objectClass", "posixAccount")
}
// Add objectClass for posixAccount
e.AddAttribute("objectClass", "posixAccount")
if IsLdapAttrAllowed(org, ldapMemberOfAttr) {
for _, group := range user.Groups {
e.AddAttribute(ldapMemberOfAttr, message.AttributeValue(group))
}
attrs := r.Attributes()
for _, attr := range attrs {
if string(attr) == "*" {
attrs = AdditionalLdapAttributes
break
}
}
for _, attr := range attrs {
e.AddAttribute(message.AttributeDescription(attr), getAttribute(string(attr), user))
if string(attr) == "title" {
e.AddAttribute(message.AttributeDescription(attr), getAttribute("title", user))
}
}
w.Write(e)
}
w.Write(res)
for _, attr := range attrs {
if !IsLdapAttrAllowed(org, attr) {
continue
}
e.AddAttribute(message.AttributeDescription(attr), getAttribute(attr, user))
}
return e
}
func handleRootSearch(w ldap.ResponseWriter, r *message.SearchRequest, res *message.SearchResultDone, m *ldap.Message) {

View File

@@ -198,6 +198,20 @@ func stringInSlice(value string, list []string) bool {
return false
}
// IsLdapAttrAllowed checks whether the given LDAP attribute is allowed for the organization.
// An empty filter or a filter containing "All" means all attributes are allowed.
func IsLdapAttrAllowed(org *object.Organization, attr string) bool {
if org == nil || len(org.LdapAttributes) == 0 {
return true
}
for _, f := range org.LdapAttributes {
if strings.EqualFold(f, "All") || strings.EqualFold(f, attr) {
return true
}
}
return false
}
func buildUserFilterCondition(filter interface{}) (builder.Cond, error) {
switch f := filter.(type) {
case message.FilterAnd:

View File

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

View File

@@ -15,6 +15,7 @@
package object
import (
"errors"
"fmt"
"regexp"
"strings"
@@ -125,6 +126,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"`
@@ -154,6 +156,17 @@ type Application struct {
FailedSigninLimit int `json:"failedSigninLimit"`
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) {
@@ -646,7 +659,7 @@ func GetMaskedApplications(applications []*Application, userId string) []*Applic
func GetAllowedApplications(applications []*Application, userId string, lang string) ([]*Application, error) {
if userId == "" {
return nil, fmt.Errorf(i18n.Translate(lang, "auth:Unauthorized operation"))
return nil, errors.New(i18n.Translate(lang, "auth:Unauthorized operation"))
}
if isUserIdGlobalAdmin(userId) {
@@ -658,7 +671,7 @@ func GetAllowedApplications(applications []*Application, userId string, lang str
return nil, err
}
if user == nil {
return nil, fmt.Errorf(i18n.Translate(lang, "auth:Unauthorized operation"))
return nil, errors.New(i18n.Translate(lang, "auth:Unauthorized operation"))
}
if user.IsAdmin {
@@ -706,7 +719,7 @@ func UpdateApplication(id string, application *Application, isGlobalAdmin bool,
}
if !isGlobalAdmin && oldApplication.Organization != application.Organization {
return false, fmt.Errorf(i18n.Translate(lang, "auth:Unauthorized operation"))
return false, errors.New(i18n.Translate(lang, "auth:Unauthorized operation"))
}
if name == "app-built-in" {
@@ -738,6 +751,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
}
@@ -793,6 +811,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
}

View File

@@ -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
View 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)
}

View File

@@ -15,6 +15,7 @@
package object
import (
"errors"
"fmt"
"regexp"
"strings"
@@ -192,7 +193,7 @@ func CheckInvitationDefaultCode(code string, defaultCode string, lang string) er
if matched, err := util.IsInvitationCodeMatch(code, defaultCode); err != nil {
return err
} else if !matched {
return fmt.Errorf(i18n.Translate(lang, "check:Default code does not match the code's matching rules"))
return errors.New(i18n.Translate(lang, "check:Default code does not match the code's matching rules"))
}
return nil
}
@@ -225,7 +226,7 @@ func checkSigninErrorTimes(user *User, lang string) error {
func CheckPassword(user *User, password string, lang string, options ...bool) error {
if password == "" {
return fmt.Errorf(i18n.Translate(lang, "check:Password cannot be empty"))
return errors.New(i18n.Translate(lang, "check:Password cannot be empty"))
}
enableCaptcha := false
@@ -246,7 +247,7 @@ func CheckPassword(user *User, password string, lang string, options ...bool) er
return err
}
if organization == nil {
return fmt.Errorf(i18n.Translate(lang, "check:Organization does not exist"))
return errors.New(i18n.Translate(lang, "check:Organization does not exist"))
}
passwordType := user.PasswordType
@@ -335,7 +336,7 @@ func CheckLdapUserPassword(user *User, password string, lang string, options ...
}
if len(searchResult.Entries) > 1 {
conn.Close()
return fmt.Errorf(i18n.Translate(lang, "check:Multiple accounts with same uid, please check your ldap server"))
return errors.New(i18n.Translate(lang, "check:Multiple accounts with same uid, please check your ldap server"))
}
hit = true
@@ -377,12 +378,12 @@ func CheckUserPassword(organization string, username string, password string, la
}
if user.IsForbidden {
return nil, fmt.Errorf(i18n.Translate(lang, "check:The user is forbidden to sign in, please contact the administrator"))
return nil, errors.New(i18n.Translate(lang, "check:The user is forbidden to sign in, please contact the administrator"))
}
// Prevent direct login for guest users without upgrading
if user.Tag == "guest-user" {
return nil, fmt.Errorf(i18n.Translate(lang, "check:Guest users must upgrade their account by setting a username and password before they can sign in directly"))
return nil, errors.New(i18n.Translate(lang, "check:Guest users must upgrade their account by setting a username and password before they can sign in directly"))
}
if isSigninViaLdap {
@@ -393,7 +394,7 @@ func CheckUserPassword(organization string, username string, password string, la
if user.Ldap != "" {
if !isSigninViaLdap && !isPasswordWithLdapEnabled {
return nil, fmt.Errorf(i18n.Translate(lang, "check:password or code is incorrect"))
return nil, errors.New(i18n.Translate(lang, "check:password or code is incorrect"))
}
// only for LDAP users
@@ -422,7 +423,7 @@ func CheckUserPassword(organization string, username string, password string, la
func CheckUserPermission(requestUserId, userId string, strict bool, lang string) (bool, error) {
if requestUserId == "" {
return false, fmt.Errorf(i18n.Translate(lang, "general:Please login first"))
return false, errors.New(i18n.Translate(lang, "general:Please login first"))
}
userOwner := util.GetOwnerFromId(userId)
@@ -454,7 +455,7 @@ func CheckUserPermission(requestUserId, userId string, strict bool, lang string)
}
if requestUser == nil {
return false, fmt.Errorf(i18n.Translate(lang, "check:Session outdated, please login again"))
return false, errors.New(i18n.Translate(lang, "check:Session outdated, please login again"))
}
if requestUser.IsGlobalAdmin() {
hasPermission = true
@@ -469,7 +470,7 @@ func CheckUserPermission(requestUserId, userId string, strict bool, lang string)
}
}
return hasPermission, fmt.Errorf(i18n.Translate(lang, "auth:Unauthorized operation"))
return hasPermission, errors.New(i18n.Translate(lang, "auth:Unauthorized operation"))
}
func CheckApiPermission(userId string, organization string, path string, method string) (bool, error) {

View File

@@ -15,6 +15,7 @@
package object
import (
"errors"
"fmt"
"net"
"strings"
@@ -34,7 +35,7 @@ func CheckEntryIp(clientIp string, user *User, application *Application, organiz
if user != nil {
err = isEntryIpAllowd(user.IpWhitelist, entryIp, lang)
if err != nil {
return fmt.Errorf(err.Error() + user.Name)
return errors.New(err.Error() + user.Name)
}
}
@@ -42,7 +43,7 @@ func CheckEntryIp(clientIp string, user *User, application *Application, organiz
err = isEntryIpAllowd(application.IpWhitelist, entryIp, lang)
if err != nil {
application.IpRestriction = err.Error() + application.Name
return fmt.Errorf(err.Error() + application.Name)
return errors.New(err.Error() + application.Name)
} else {
application.IpRestriction = ""
}
@@ -56,7 +57,7 @@ func CheckEntryIp(clientIp string, user *User, application *Application, organiz
err = isEntryIpAllowd(organization.IpWhitelist, entryIp, lang)
if err != nil {
organization.IpRestriction = err.Error() + organization.Name
return fmt.Errorf(err.Error() + organization.Name)
return errors.New(err.Error() + organization.Name)
} else {
organization.IpRestriction = ""
}

View File

@@ -15,7 +15,7 @@
package object
import (
"fmt"
"errors"
"time"
"github.com/casdoor/casdoor/i18n"
@@ -28,7 +28,7 @@ func checkPasswordExpired(user *User, lang string) error {
return err
}
if organization == nil {
return fmt.Errorf(i18n.Translate(lang, "check:Organization does not exist"))
return errors.New(i18n.Translate(lang, "check:Organization does not exist"))
}
passwordExpireDays := organization.PasswordExpireDays
@@ -39,7 +39,7 @@ func checkPasswordExpired(user *User, lang string) error {
lastChangePasswordTime := user.LastChangePasswordTime
if lastChangePasswordTime == "" {
if user.CreatedTime == "" {
return fmt.Errorf(i18n.Translate(lang, "check:Your password has expired. Please reset your password by clicking \"Forgot password\""))
return errors.New(i18n.Translate(lang, "check:Your password has expired. Please reset your password by clicking \"Forgot password\""))
}
lastChangePasswordTime = user.CreatedTime
}
@@ -47,7 +47,7 @@ func checkPasswordExpired(user *User, lang string) error {
lastTime := util.String2Time(lastChangePasswordTime)
expireTime := lastTime.AddDate(0, 0, passwordExpireDays)
if time.Now().After(expireTime) {
return fmt.Errorf(i18n.Translate(lang, "check:Your password has expired. Please reset your password by clicking \"Forgot password\""))
return errors.New(i18n.Translate(lang, "check:Your password has expired. Please reset your password by clicking \"Forgot password\""))
}
return nil
}

View File

@@ -15,6 +15,7 @@
package object
import (
"errors"
"fmt"
"regexp"
"strconv"
@@ -99,7 +100,7 @@ func recordSigninErrorInfo(user *User, lang string, options ...bool) error {
leftChances := failedSigninLimit - user.SigninWrongTimes
if leftChances == 0 && enableCaptcha {
return fmt.Errorf(i18n.Translate(lang, "check:password or code is incorrect"))
return errors.New(i18n.Translate(lang, "check:password or code is incorrect"))
} else if leftChances >= 0 {
return fmt.Errorf(i18n.Translate(lang, "check:password or code is incorrect, you have %s remaining chances"), strconv.Itoa(leftChances))
}

View File

@@ -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"},
}
}

View File

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

109
object/kerberos.go Normal file
View File

@@ -0,0 +1,109 @@
// 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 (
"encoding/base64"
"fmt"
"strings"
"github.com/jcmturner/gokrb5/v8/credentials"
"github.com/jcmturner/gokrb5/v8/gssapi"
"github.com/jcmturner/gokrb5/v8/keytab"
"github.com/jcmturner/gokrb5/v8/service"
"github.com/jcmturner/gokrb5/v8/spnego"
)
// ctxCredentials is the SPNEGO context key holding the Kerberos credentials.
// This must match the value used internally by gokrb5's spnego package.
// If the gokrb5 library changes this internal constant in a future version,
// this value will need to be updated accordingly.
const ctxCredentials = "github.com/jcmturner/gokrb5/v8/ctxCredentials"
// ValidateKerberosToken validates a base64-encoded SPNEGO token from the
// Authorization header and returns the authenticated Kerberos username.
func ValidateKerberosToken(organization *Organization, spnegoTokenBase64 string) (string, error) {
if organization.KerberosRealm == "" || organization.KerberosKdcHost == "" || organization.KerberosKeytab == "" {
return "", fmt.Errorf("kerberos configuration is incomplete for organization: %s", organization.Name)
}
keytabData, err := base64.StdEncoding.DecodeString(organization.KerberosKeytab)
if err != nil {
return "", fmt.Errorf("failed to decode keytab: %w", err)
}
kt := keytab.New()
err = kt.Unmarshal(keytabData)
if err != nil {
return "", fmt.Errorf("failed to parse keytab: %w", err)
}
servicePrincipal := organization.KerberosServiceName
if servicePrincipal == "" {
servicePrincipal = "HTTP"
}
spnegoSvc := spnego.SPNEGOService(kt, service.KeytabPrincipal(servicePrincipal))
tokenBytes, err := base64.StdEncoding.DecodeString(spnegoTokenBase64)
if err != nil {
return "", fmt.Errorf("failed to decode SPNEGO token: %w", err)
}
var st spnego.SPNEGOToken
err = st.Unmarshal(tokenBytes)
if err != nil {
return "", fmt.Errorf("failed to unmarshal SPNEGO token: %w", err)
}
authed, ctx, status := spnegoSvc.AcceptSecContext(&st)
if status.Code != gssapi.StatusComplete && status.Code != gssapi.StatusContinueNeeded {
return "", fmt.Errorf("SPNEGO validation error: %s", status.Message)
}
if status.Code == gssapi.StatusContinueNeeded {
return "", fmt.Errorf("SPNEGO negotiation requires continuation, which is not supported")
}
if !authed {
return "", fmt.Errorf("SPNEGO token validation failed")
}
creds, ok := ctx.Value(ctxCredentials).(*credentials.Credentials)
if !ok || creds == nil {
return "", fmt.Errorf("no credentials found in SPNEGO context")
}
username := creds.UserName()
if username == "" {
return "", fmt.Errorf("no username found in Kerberos ticket")
}
return username, nil
}
// GetUserByKerberosName looks up a Casdoor user by their Kerberos principal name.
// It strips the realm part (e.g., "user@REALM.COM" -> "user") and searches by username.
func GetUserByKerberosName(organizationName string, kerberosUsername string) (*User, error) {
username := kerberosUsername
if idx := strings.Index(username, "@"); idx >= 0 {
username = username[:idx]
}
user, err := GetUserByFields(organizationName, username)
if err != nil {
return nil, err
}
return user, nil
}

View File

@@ -750,7 +750,7 @@ func ResetLdapPassword(user *User, oldPassword string, newPassword string, lang
}
if len(searchResult.Entries) > 1 {
conn.Close()
return fmt.Errorf(i18n.Translate(lang, "check:Multiple accounts with same uid, please check your ldap server"))
return errors.New(i18n.Translate(lang, "check:Multiple accounts with same uid, please check your ldap server"))
}
userDn := searchResult.Entries[0].DN

View File

@@ -94,6 +94,13 @@ type Organization struct {
DcrPolicy string `xorm:"varchar(100)" json:"dcrPolicy"`
LdapAttributes []string `xorm:"mediumtext" json:"ldapAttributes"`
KerberosRealm string `xorm:"varchar(200)" json:"kerberosRealm"`
KerberosKdcHost string `xorm:"varchar(200)" json:"kerberosKdcHost"`
KerberosKeytab string `xorm:"mediumtext" json:"kerberosKeytab"`
KerberosServiceName string `xorm:"varchar(100)" json:"kerberosServiceName"`
OrgBalance float64 `json:"orgBalance"`
UserBalance float64 `json:"userBalance"`
BalanceCredit float64 `json:"balanceCredit"`

View File

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

View File

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

View File

@@ -15,6 +15,7 @@
package object
import (
"errors"
"fmt"
"regexp"
"strings"
@@ -425,7 +426,7 @@ func GetCaptchaProviderByApplication(applicationId, isCurrentProvider, lang stri
}
if application == nil || len(application.Providers) == 0 {
return nil, fmt.Errorf(i18n.Translate(lang, "provider:Invalid application id"))
return nil, errors.New(i18n.Translate(lang, "provider:Invalid application id"))
}
for _, provider := range application.Providers {
if provider.Provider == nil {
@@ -472,7 +473,7 @@ func GetFaceIdProviderByApplication(applicationId, isCurrentProvider, lang strin
}
if application == nil || len(application.Providers) == 0 {
return nil, fmt.Errorf(i18n.Translate(lang, "provider:Invalid application id"))
return nil, errors.New(i18n.Translate(lang, "provider:Invalid application id"))
}
for _, provider := range application.Providers {
if provider.Provider == nil {
@@ -513,7 +514,7 @@ func GetIdvProviderByApplication(applicationId, isCurrentProvider, lang string)
}
if application == nil || len(application.Providers) == 0 {
return nil, fmt.Errorf(i18n.Translate(lang, "provider:Invalid application id"))
return nil, errors.New(i18n.Translate(lang, "provider:Invalid application id"))
}
for _, provider := range application.Providers {
if provider.Provider == nil {
@@ -564,7 +565,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 +589,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 {

View File

@@ -21,6 +21,7 @@ type ProviderItem struct {
CanSignUp bool `json:"canSignUp"`
CanSignIn bool `json:"canSignIn"`
CanUnlink bool `json:"canUnlink"`
BindingRule *[]string `json:"bindingRule"`
CountryCodes []string `json:"countryCodes"`
Prompted bool `json:"prompted"`
SignupGroup string `json:"signupGroup"`

View File

@@ -16,6 +16,7 @@ package object
import (
"encoding/json"
"errors"
"fmt"
"regexp"
"strings"
@@ -351,7 +352,7 @@ func SendWebhooks(record *casvisorsdk.Record) error {
for _, err := range errs {
errStrings = append(errStrings, err.Error())
}
return fmt.Errorf(strings.Join(errStrings, " | "))
return errors.New(strings.Join(errStrings, " | "))
}
return nil
}

139
object/rule.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}
}()
}

View 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"
}

View File

@@ -15,8 +15,10 @@
package object
import (
"strconv"
"strings"
"github.com/casdoor/casdoor/conf"
sender "github.com/casdoor/go-sms-sender"
)
@@ -30,6 +32,10 @@ func getSmsClient(provider *Provider) (sender.SmsClient, error) {
client, err = newHttpSmsClient(provider.Endpoint, provider.Method, provider.Title, provider.TemplateCode, provider.HttpHeaders, provider.UserMapping, provider.IssuerUrl, provider.EnableProxy)
} else if provider.Type == "Alibaba Cloud PNVS SMS" {
client, err = newPnvsSmsClient(provider.ClientId, provider.ClientSecret, provider.SignName, provider.TemplateCode, provider.RegionId)
} else if provider.Type == sender.Twilio {
// For Twilio, the message body is pre-formatted in SendSms using the template.
// Pass "%s" as the template so go-sms-sender's fmt.Sprintf passes the content through unchanged.
client, err = sender.NewSmsClient(provider.Type, provider.ClientId, provider.ClientSecret, provider.SignName, "%s", provider.AppId)
} else {
client, err = sender.NewSmsClient(provider.Type, provider.ClientId, provider.ClientSecret, provider.SignName, provider.TemplateCode, provider.AppId)
}
@@ -50,6 +56,11 @@ func SendSms(provider *Provider, content string, phoneNumbers ...string) error {
if provider.AppId != "" {
phoneNumbers = append([]string{provider.AppId}, phoneNumbers...)
}
// Pre-format the message body using the provider's template.
// If the template contains "%s", substitute the code; otherwise use the content (code) directly.
if strings.Contains(provider.TemplateCode, "%s") {
content = strings.Replace(provider.TemplateCode, "%s", content, 1)
}
} else if provider.Type == sender.Aliyun || provider.Type == "Alibaba Cloud PNVS SMS" {
for i, number := range phoneNumbers {
phoneNumbers[i] = strings.TrimPrefix(number, "+86")
@@ -61,6 +72,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...)

View File

@@ -16,6 +16,7 @@ package object
import (
"encoding/json"
"errors"
"fmt"
"github.com/aliyun/alibaba-cloud-sdk-go/services/dypnsapi"
@@ -77,7 +78,7 @@ func (c *PnvsSmsClient) SendMessage(param map[string]string, targetPhoneNumber .
if response.Code != "OK" {
if response.Message != "" {
return fmt.Errorf(response.Message)
return errors.New(response.Message)
}
return fmt.Errorf("PNVS SMS send failed with code: %s", response.Code)
}

View File

@@ -15,6 +15,7 @@
package object
import (
"errors"
"fmt"
"github.com/casdoor/casdoor/i18n"
@@ -170,7 +171,7 @@ func UpdateSyncer(id string, syncer *Syncer, isGlobalAdmin bool, lang string) (b
} else if s == nil {
return false, nil
} else if !isGlobalAdmin && s.Organization != syncer.Organization {
return false, fmt.Errorf(i18n.Translate(lang, "auth:Unauthorized operation"))
return false, errors.New(i18n.Translate(lang, "auth:Unauthorized operation"))
}
// Close old syncer connections before updating

View File

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

View File

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

View File

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

View File

@@ -19,6 +19,8 @@ import (
"encoding/base64"
"fmt"
"net/url"
"regexp"
"slices"
"strings"
"sync"
"time"
@@ -154,6 +156,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
@@ -190,6 +196,16 @@ func GetOAuthCode(userId string, clientId string, provider string, signinMethod
}, nil
}
// Expand regex/wildcard scopes to concrete scope names.
expandedScope, ok := IsScopeValidAndExpand(scope, application)
if !ok {
return &Code{
Message: i18n.Translate(lang, "token:Invalid scope"),
Code: "",
}, nil
}
scope = expandedScope
// Validate resource parameter (RFC 8707)
if err := validateResourceURI(resource); err != nil {
return &Code{
@@ -240,10 +256,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 +312,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 +361,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 +369,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 +531,81 @@ func IsGrantTypeValid(method string, grantTypes []string) bool {
return false
}
// isRegexScope returns true if the scope string contains regex metacharacters.
func isRegexScope(scope string) bool {
return strings.ContainsAny(scope, ".*+?^${}()|[]\\")
}
// IsScopeValidAndExpand expands any regex patterns in the space-separated scope string
// against the application's configured scopes. Literal scopes are kept as-is
// after verifying they exist in the allowed list. Regex scopes are matched
// against every allowed scope name; all matches replace the pattern.
// If the application has no defined scopes, the original scope string is
// returned unchanged (backward-compatible behaviour).
// Returns the expanded scope string and whether the scope is valid.
func IsScopeValidAndExpand(scope string, application *Application) (string, bool) {
if len(application.Scopes) == 0 || scope == "" {
return scope, true
}
allowedNames := make([]string, 0, len(application.Scopes))
allowedSet := make(map[string]bool, len(application.Scopes))
for _, s := range application.Scopes {
allowedNames = append(allowedNames, s.Name)
allowedSet[s.Name] = true
}
seen := make(map[string]bool)
var expanded []string
for _, s := range strings.Fields(scope) {
// Try exact match first.
if allowedSet[s] {
if !seen[s] {
seen[s] = true
expanded = append(expanded, s)
}
continue
}
// Not an exact match if it looks like a regex, try pattern matching.
if !isRegexScope(s) {
return "", false
}
// Treat as regex pattern must be a valid regex and match ≥ 1 scope.
re, err := regexp.Compile("^" + s + "$")
if err != nil {
return "", false
}
matched := false
for _, name := range allowedNames {
if re.MatchString(name) {
matched = true
if !seen[name] {
seen[name] = true
expanded = append(expanded, name)
}
}
}
if !matched {
return "", false
}
}
return strings.Join(expanded, " "), true
}
// IsScopeValid checks whether all space-separated scopes in the scope string
// are defined in the application's Scopes list (including regex expansion).
// If the application has no defined scopes, every scope is considered valid
// (backward-compatible behaviour).
func IsScopeValid(scope string, application *Application) bool {
_, ok := IsScopeValidAndExpand(scope, application)
return ok
}
// 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 +646,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 +842,15 @@ 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) {
expandedScope, ok := IsScopeValidAndExpand(scope, application)
if !ok {
return nil, &TokenError{
Error: InvalidScope,
ErrorDescription: "the requested scope is invalid or not defined in the application",
}, nil
}
scope = expandedScope
user, err := GetUserByFields(application.Organization, username)
if err != nil {
return nil, nil, err
@@ -796,6 +932,14 @@ func GetClientCredentialsToken(application *Application, clientSecret string, sc
ErrorDescription: "client_secret is invalid",
}, nil
}
expandedScope, ok := IsScopeValidAndExpand(scope, application)
if !ok {
return nil, &TokenError{
Error: InvalidScope,
ErrorDescription: "the requested scope is invalid or not defined in the application",
}, nil
}
scope = expandedScope
nullUser := &User{
Owner: application.Owner,
Id: application.GetId(),
@@ -835,6 +979,15 @@ 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) {
expandedScope, ok := IsScopeValidAndExpand(scope, application)
if !ok {
return nil, &TokenError{
Error: InvalidScope,
ErrorDescription: "the requested scope is invalid or not defined in the application",
}, nil
}
scope = expandedScope
user, err := GetUserByFields(application.Organization, username)
if err != nil {
return nil, nil, err
@@ -859,6 +1012,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 +1177,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,

View File

@@ -15,6 +15,7 @@
package object
import (
"errors"
"fmt"
"strings"
@@ -249,7 +250,7 @@ func updateBalanceForTransaction(transaction *Transaction, amount float64, lang
} else if transaction.Tag == "User" {
// Update user's balance
if transaction.User == "" {
return fmt.Errorf(i18n.Translate(lang, "general:User is required for User category transaction"))
return errors.New(i18n.Translate(lang, "general:User is required for User category transaction"))
}
if err := UpdateUserBalance(transaction.Owner, transaction.User, amount, currency, lang); err != nil {
return err

View File

@@ -15,6 +15,7 @@
package object
import (
"errors"
"fmt"
"github.com/casdoor/casdoor/i18n"
@@ -32,7 +33,7 @@ func validateBalanceForTransaction(transaction *Transaction, amount float64, lan
} else if transaction.Tag == "User" {
// Validate user balance change
if transaction.User == "" {
return fmt.Errorf(i18n.Translate(lang, "general:User is required for User category transaction"))
return errors.New(i18n.Translate(lang, "general:User is required for User category transaction"))
}
if err := validateUserBalance(transaction.Owner, transaction.User, amount, currency, lang); err != nil {
return err

View File

@@ -17,6 +17,7 @@ package object
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"reflect"
@@ -180,6 +181,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 +243,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 +863,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 +957,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
@@ -978,7 +988,7 @@ func AddUser(user *User, lang string) (bool, error) {
}
if user.Owner == "" || user.Name == "" {
return false, fmt.Errorf(i18n.Translate(lang, "user:the user's owner and name should not be empty"))
return false, errors.New(i18n.Translate(lang, "user:the user's owner and name should not be empty"))
}
if CheckUsernameWithEmail(user.Name, "en") != "" {
@@ -1004,7 +1014,7 @@ func AddUser(user *User, lang string) (bool, error) {
}
if organization.Name == "built-in" && !organization.HasPrivilegeConsent && user.Name != "admin" {
return false, fmt.Errorf(i18n.Translate(lang, "organization:adding a new user to the 'built-in' organization is currently disabled. Please note: all users in the 'built-in' organization are global administrators in Casdoor. Refer to the docs: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. If you still wish to create a user for the 'built-in' organization, go to the organization's settings page and enable the 'Has privilege consent' option."))
return false, errors.New(i18n.Translate(lang, "organization:adding a new user to the 'built-in' organization is currently disabled. Please note: all users in the 'built-in' organization are global administrators in Casdoor. Refer to the docs: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. If you still wish to create a user for the 'built-in' organization, go to the organization's settings page and enable the 'Has privilege consent' option."))
}
if user.BalanceCurrency == "" {

119
object/user_scope.go Normal file
View 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
}

View File

@@ -32,7 +32,7 @@ func GetWebAuthnObject(host string) (*webauthn.WebAuthn, error) {
localUrl, err := url.Parse(originBackend)
if err != nil {
return nil, fmt.Errorf("error when parsing origin:" + err.Error())
return nil, fmt.Errorf("error when parsing origin: %w", err)
}
webAuthn, err := webauthn.New(&webauthn.Config{

View File

@@ -15,6 +15,7 @@
package object
import (
"errors"
"fmt"
"math"
"math/rand"
@@ -325,13 +326,13 @@ func CheckSigninCode(user *User, dest, code, lang string) error {
case wrongCodeError:
return recordSigninErrorInfo(user, lang)
default:
return fmt.Errorf(result.Msg)
return errors.New(result.Msg)
}
}
func CheckFaceId(user *User, faceId []float64, lang string) error {
if len(user.FaceIds) == 0 {
return fmt.Errorf(i18n.Translate(lang, "check:Face data does not exist, cannot log in"))
return errors.New(i18n.Translate(lang, "check:Face data does not exist, cannot log in"))
}
for _, userFaceId := range user.FaceIds {
@@ -348,7 +349,7 @@ func CheckFaceId(user *User, faceId []float64, lang string) error {
}
}
return fmt.Errorf(i18n.Translate(lang, "check:Face data mismatch"))
return errors.New(i18n.Translate(lang, "check:Face data mismatch"))
}
func GetVerifyType(username string) (verificationCodeType string) {

View File

@@ -15,6 +15,7 @@
package object
import (
"errors"
"fmt"
"github.com/casdoor/casdoor/i18n"
@@ -118,7 +119,7 @@ func UpdateWebhook(id string, webhook *Webhook, isGlobalAdmin bool, lang string)
} else if w == nil {
return false, nil
} else if !isGlobalAdmin && w.Organization != webhook.Organization {
return false, fmt.Errorf(i18n.Translate(lang, "auth:Unauthorized operation"))
return false, errors.New(i18n.Translate(lang, "auth:Unauthorized operation"))
}
affected, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(webhook)

View File

@@ -17,7 +17,6 @@ package pp
import (
"context"
"errors"
"fmt"
"strconv"
"github.com/casdoor/casdoor/conf"
@@ -108,7 +107,7 @@ func (pp *PaypalPaymentProvider) Notify(body []byte, orderId string) (*NotifyRes
notifyResult.NotifyMessage = errDetail.Description
return notifyResult, nil
default:
err = fmt.Errorf(errDetail.Description)
err = errors.New(errDetail.Description)
return nil, err
}
}
@@ -125,7 +124,7 @@ func (pp *PaypalPaymentProvider) Notify(body []byte, orderId string) (*NotifyRes
notifyResult.NotifyMessage = errDetail.Description
return notifyResult, nil
default:
err = fmt.Errorf(errDetail.Description)
err = errors.New(errDetail.Description)
return nil, err
}
}

View File

@@ -0,0 +1,228 @@
// 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 routers
import (
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/beego/beego/v2/server/web/context"
"github.com/casdoor/casdoor/util"
)
const (
providerHintRedirectScriptName = "ProviderHintRedirect.js"
authCallbackHandlerScriptName = "AuthCallbackHandler.js"
)
func getLightweightAuthScriptPath(scriptName string) string {
candidates := []string{
filepath.Join(getWebBuildFolder(), scriptName),
}
if frontendBaseDir != "" {
candidates = append(candidates,
filepath.Join(frontendBaseDir, "public", scriptName),
filepath.Join(filepath.Dir(frontendBaseDir), "public", scriptName),
)
}
candidates = append(candidates, filepath.Join("web", "public", scriptName))
for _, candidate := range candidates {
if util.FileExist(candidate) {
return candidate
}
}
return ""
}
func serveLightweightAuthScript(ctx *context.Context, requestPath string, scriptName string) bool {
if ctx.Request.URL.Path != requestPath {
return false
}
scriptPath := getLightweightAuthScriptPath(scriptName)
if scriptPath == "" {
ctx.ResponseWriter.WriteHeader(http.StatusNotFound)
http.ServeContent(ctx.ResponseWriter, ctx.Request, scriptName, time.Now(), strings.NewReader("window.location.replace('/');"))
return true
}
f, err := os.Open(filepath.Clean(scriptPath))
if err != nil {
ctx.ResponseWriter.WriteHeader(http.StatusInternalServerError)
http.ServeContent(ctx.ResponseWriter, ctx.Request, scriptName, time.Now(), strings.NewReader("window.location.replace('/');"))
return true
}
defer f.Close()
fileInfo, err := f.Stat()
if err != nil {
ctx.ResponseWriter.WriteHeader(http.StatusInternalServerError)
http.ServeContent(ctx.ResponseWriter, ctx.Request, scriptName, time.Now(), strings.NewReader("window.location.replace('/');"))
return true
}
ctx.Output.Header("Content-Type", "application/javascript; charset=utf-8")
ctx.Output.Header("Cache-Control", "no-store")
http.ServeContent(ctx.ResponseWriter, ctx.Request, fileInfo.Name(), fileInfo.ModTime(), f)
return true
}
func serveProviderHintRedirectScript(ctx *context.Context) bool {
return serveLightweightAuthScript(ctx, "/"+providerHintRedirectScriptName, providerHintRedirectScriptName)
}
func serveAuthCallbackHandlerScript(ctx *context.Context) bool {
return serveLightweightAuthScript(ctx, "/"+authCallbackHandlerScriptName, authCallbackHandlerScriptName)
}
func serveProviderHintRedirectPage(ctx *context.Context) bool {
if ctx.Request.URL.Path != "/login/oauth/authorize" {
return false
}
providerHint := ctx.Input.Query("provider_hint")
if providerHint == "" {
return false
}
const providerHintRedirectHtml = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Redirecting...</title>
<style>
html, body {
width: 100%;
height: 100%;
margin: 0;
background: #ffffff;
color: #1f2937;
font-family: sans-serif;
}
body {
display: flex;
align-items: center;
justify-content: center;
}
.redirecting {
font-size: 14px;
opacity: 0.72;
}
</style>
</head>
<body>
<div class="redirecting">Redirecting...</div>
<script src="/ProviderHintRedirect.js"></script>
<script>
(function() {
function redirectToFallback() {
var url = new URL(window.location.href);
url.searchParams.delete("provider_hint");
window.location.replace(url.pathname + url.search + url.hash);
}
if (!window.CasdoorProviderHintRedirect || typeof window.CasdoorProviderHintRedirect.run !== "function") {
redirectToFallback();
return;
}
window.CasdoorProviderHintRedirect.run();
})();
</script>
</body>
</html>
`
ctx.Output.Header("Content-Type", "text/html; charset=utf-8")
ctx.Output.Header("Cache-Control", "no-store")
http.ServeContent(ctx.ResponseWriter, ctx.Request, "provider-hint-redirect.html", time.Now(), strings.NewReader(providerHintRedirectHtml))
return true
}
func serveAuthCallbackPage(ctx *context.Context) bool {
if ctx.Request.URL.Path != "/callback" {
return false
}
if ctx.Input.Query("__casdoor_callback_react") == "1" {
return false
}
if ctx.Input.Query("state") == "" {
return false
}
const authCallbackHtml = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Signing in...</title>
<style>
html, body {
width: 100%;
height: 100%;
margin: 0;
background: #ffffff;
color: #1f2937;
font-family: sans-serif;
}
body {
display: flex;
align-items: center;
justify-content: center;
}
.callback-status {
font-size: 14px;
opacity: 0.82;
padding: 0 24px;
text-align: center;
}
</style>
</head>
<body>
<div id="callback-status" class="callback-status">Signing in...</div>
<script src="/AuthCallbackHandler.js"></script>
<script>
(function() {
if (!window.CasdoorAuthCallback || typeof window.CasdoorAuthCallback.run !== "function") {
document.getElementById("callback-status").textContent = "Failed to load callback handler.";
return;
}
window.CasdoorAuthCallback.run();
})();
</script>
</body>
</html>
`
ctx.Output.Header("Content-Type", "text/html; charset=utf-8")
ctx.Output.Header("Cache-Control", "no-store")
http.ServeContent(ctx.ResponseWriter, ctx.Request, "auth-callback.html", time.Now(), strings.NewReader(authCallbackHtml))
return true
}

View File

@@ -64,6 +64,7 @@ func InitAPI() {
web.Router("/api/get-captcha-status", &controllers.ApiController{}, "GET:GetCaptchaStatus")
web.Router("/api/callback", &controllers.ApiController{}, "POST:Callback")
web.Router("/api/device-auth", &controllers.ApiController{}, "POST:DeviceAuth")
web.Router("/api/kerberos-login", &controllers.ApiController{}, "GET:KerberosLogin")
web.Router("/api/get-organizations", &controllers.ApiController{}, "GET:GetOrganizations")
web.Router("/api/get-organization", &controllers.ApiController{}, "GET:GetOrganization")
@@ -126,12 +127,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 +334,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")

View File

@@ -16,6 +16,7 @@ package routers
import (
"compress/gzip"
"errors"
"fmt"
"io"
"net/http"
@@ -89,11 +90,28 @@ 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
} else if code.Message != "" {
return "", fmt.Errorf(code.Message)
return "", errors.New(code.Message)
}
sep := "?"
@@ -114,6 +132,12 @@ func StaticFilter(ctx *context.Context) {
if strings.HasPrefix(urlPath, "/api/") || strings.HasPrefix(urlPath, "/.well-known/") {
return
}
if serveAuthCallbackHandlerScript(ctx) {
return
}
if serveProviderHintRedirectScript(ctx) {
return
}
if strings.HasPrefix(urlPath, "/cas") && (strings.HasSuffix(urlPath, "/serviceValidate") || strings.HasSuffix(urlPath, "/proxy") || strings.HasSuffix(urlPath, "/proxyValidate") || strings.HasSuffix(urlPath, "/validate") || strings.HasSuffix(urlPath, "/p3/serviceValidate") || strings.HasSuffix(urlPath, "/p3/proxyValidate") || strings.HasSuffix(urlPath, "/samlValidate")) {
return
}
@@ -132,6 +156,14 @@ func StaticFilter(ctx *context.Context) {
http.Redirect(ctx.ResponseWriter, ctx.Request, redirectUrl, http.StatusFound)
return
}
if serveProviderHintRedirectPage(ctx) {
return
}
}
if serveAuthCallbackPage(ctx) {
return
}
webBuildFolder := getWebBuildFolder()

112
rule/rule.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}

373
service/proxy.go Normal file
View File

@@ -0,0 +1,373 @@
// 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/beego/beego/v2/core/logs"
"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, "%s", keyAuth)
return
}
}
responseError(w, "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, "%s", 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 {
logs.Error(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 {
logs.Error(err)
}
}()
}

142
service/util.go Normal file
View 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.Fprint(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.Fprint(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.Fprint(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
View 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)
}

View File

@@ -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
View 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
View 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
View 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{}
}

View File

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

View File

@@ -0,0 +1,430 @@
// 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.
(function() {
"use strict";
var reactFallbackKey = "__casdoor_callback_react";
var reactFallbackPayloadKey = "casdoor_callback_react_fallback";
function setStatus(message, isError) {
var statusNode = document.getElementById("callback-status");
if (!statusNode) {
return;
}
statusNode.textContent = message;
statusNode.style.color = isError ? "#b42318" : "#1f2937";
}
function getReactCallbackOrigin() {
if (window.location.port === "8000" && ["localhost", "127.0.0.1"].indexOf(window.location.hostname) !== -1) {
return window.location.protocol + "//" + window.location.hostname + ":7001";
}
return window.location.origin;
}
function goToReactFallback() {
var url = new URL(window.location.href);
url.protocol = window.location.protocol;
url.host = new URL(getReactCallbackOrigin()).host;
url.searchParams.set(reactFallbackKey, "1");
window.location.replace(url.toString());
}
function storeReactFallbackPayload(payload) {
sessionStorage.setItem(reactFallbackPayloadKey, JSON.stringify(payload));
}
function getQueryParamsFromState(state) {
var query = sessionStorage.getItem(state);
if (query === null) {
return atob(state);
}
return query;
}
function getInnerParams() {
var params = new URLSearchParams(window.location.search);
var state = params.get("state");
if (!state) {
return null;
}
var queryString = getQueryParamsFromState(state);
return new URLSearchParams(queryString);
}
function getResponseType(innerParams) {
var method = innerParams.get("method");
if (method === "signup") {
var realRedirectUri = innerParams.get("redirect_uri");
if (realRedirectUri === null) {
var samlRequest = innerParams.get("SAMLRequest");
var casService = innerParams.get("service");
if (samlRequest) {
return "saml";
}
if (casService) {
return "cas";
}
return "login";
}
var realRedirectUrl = new URL(realRedirectUri).origin;
if (window.location.origin === realRedirectUrl) {
return "login";
}
return innerParams.get("response_type") || "code";
}
if (method === "link") {
return "link";
}
return "unknown";
}
function getCodeVerifier(state) {
return localStorage.getItem("pkce_verifier_" + state);
}
function clearCodeVerifier(state) {
localStorage.removeItem("pkce_verifier_" + state);
}
function getRefinedValue(value) {
return value || "";
}
function getRawGetParameter(key, source) {
var token = source.split(key + "=")[1];
if (!token) {
return "";
}
var result = token.split("&")[0];
if (!result) {
return "";
}
return decodeURIComponent(result);
}
function getOAuthGetParameters(innerParams, queryString) {
var lowercaseQueries = {};
innerParams.forEach(function(value, key) {
lowercaseQueries[key.toLowerCase()] = value;
});
var clientId = getRefinedValue(innerParams.get("client_id"));
var responseType = getRefinedValue(innerParams.get("response_type"));
var redirectUri = getRawGetParameter("redirect_uri", queryString);
if (redirectUri === "") {
redirectUri = getRefinedValue(innerParams.get("redirect_uri"));
}
var scope = getRefinedValue(innerParams.get("scope"));
if (redirectUri.indexOf("#") !== -1 && scope === "") {
scope = getRawGetParameter("scope", queryString);
}
var state = getRefinedValue(innerParams.get("state"));
if (state.indexOf("/auth/oauth2/login.php?wantsurl") === 0) {
state = encodeURIComponent(state);
}
if (redirectUri.indexOf("#") !== -1 && state === "") {
state = getRawGetParameter("state", queryString);
}
return {
clientId: clientId,
responseType: responseType,
redirectUri: redirectUri,
scope: scope,
state: state,
nonce: getRefinedValue(innerParams.get("nonce")),
challengeMethod: getRefinedValue(innerParams.get("code_challenge_method")),
codeChallenge: getRefinedValue(innerParams.get("code_challenge")),
responseMode: getRefinedValue(innerParams.get("response_mode")),
relayState: getRefinedValue(lowercaseQueries["relaystate"]),
type: "code"
};
}
function oAuthParamsToQuery(oAuthParams) {
if (!oAuthParams) {
return "";
}
return "?clientId=" + oAuthParams.clientId +
"&responseType=" + oAuthParams.responseType +
"&redirectUri=" + encodeURIComponent(oAuthParams.redirectUri) +
"&type=" + oAuthParams.type +
"&scope=" + oAuthParams.scope +
"&state=" + oAuthParams.state +
"&nonce=" + oAuthParams.nonce +
"&code_challenge_method=" + oAuthParams.challengeMethod +
"&code_challenge=" + oAuthParams.codeChallenge;
}
function createFormAndSubmit(action, params) {
var form = document.createElement("form");
form.method = "post";
form.action = action;
Object.keys(params).forEach(function(key) {
if (params[key] === null || params[key] === undefined) {
return;
}
var input = document.createElement("input");
input.type = "hidden";
input.name = key;
input.value = params[key];
form.appendChild(input);
});
document.body.appendChild(form);
form.submit();
}
function extractCallbackCode(params) {
var isSteam = params.get("openid.mode");
var code = params.get("code") || params.get("auth_code") || params.get("authCode");
if (code === null) {
var web3AuthTokenKey = params.get("web3AuthTokenKey");
if (web3AuthTokenKey !== null) {
code = localStorage.getItem(web3AuthTokenKey);
}
}
if (isSteam !== null && code === null) {
code = window.location.search;
}
var telegramId = params.get("id");
if (telegramId !== null && (code === null || code === "")) {
var telegramAuthData = {
id: parseInt(telegramId, 10)
};
var hash = params.get("hash");
var authDate = params.get("auth_date");
if (hash) {
telegramAuthData.hash = hash;
}
if (authDate) {
telegramAuthData.auth_date = authDate;
}
["first_name", "last_name", "username", "photo_url"].forEach(function(field) {
var value = params.get(field);
if (value !== null && value !== "") {
telegramAuthData[field] = value;
}
});
code = JSON.stringify(telegramAuthData);
}
return code;
}
function shouldFallbackToReact(res) {
return res.data === "RequiredMfa" || res.data === "NextMfa" || res.data === "SelectPlan" || res.data === "BuyPlanResult" || res.data3;
}
function getFromLink() {
return sessionStorage.getItem("from") || "/";
}
async function loginCas(body, casService) {
return fetch(window.location.origin + "/api/login?service=" + encodeURIComponent(casService || ""), {
method: "POST",
credentials: "include",
body: JSON.stringify(body),
headers: {
"Accept-Language": localStorage.getItem("language") || navigator.language || "en"
}
}).then(function(res) {
return res.json();
});
}
async function run() {
setStatus("Signing in...", false);
var params = new URLSearchParams(window.location.search);
var innerParams = getInnerParams();
if (!innerParams) {
setStatus("Missing callback state.", true);
return;
}
var queryString = getQueryParamsFromState(params.get("state"));
var applicationName = innerParams.get("application");
var providerName = innerParams.get("provider");
var method = innerParams.get("method");
var samlRequest = innerParams.get("SAMLRequest");
var code = extractCallbackCode(params);
var responseType = getResponseType(innerParams);
var redirectUri = window.location.origin + "/callback";
var codeVerifier = getCodeVerifier(params.get("state"));
var body = {
type: responseType,
application: applicationName,
provider: providerName,
code: code,
samlRequest: samlRequest,
state: applicationName,
invitationCode: innerParams.get("invitationCode") || "",
redirectUri: redirectUri,
method: method,
codeVerifier: codeVerifier
};
if (codeVerifier) {
clearCodeVerifier(params.get("state"));
}
if (responseType === "cas") {
var casService = innerParams.get("service") || "";
var casRes = await loginCas(body, casService);
if (casRes.status !== "ok") {
setStatus(casRes.msg || "Failed to sign in.", true);
return;
}
if (shouldFallbackToReact(casRes)) {
storeReactFallbackPayload({
search: window.location.search,
body: body,
res: casRes,
flow: "cas",
casService: casService
});
goToReactFallback();
return;
}
if (casService === "") {
setStatus("Logged in successfully. Now you can visit apps protected by Casdoor.", false);
return;
}
var serviceUrl = new URL(casService);
serviceUrl.searchParams.append("ticket", casRes.data);
window.location.replace(serviceUrl.toString());
return;
}
var oAuthParams = getOAuthGetParameters(innerParams, queryString);
var response = await fetch(window.location.origin + "/api/login" + oAuthParamsToQuery(oAuthParams), {
method: "POST",
credentials: "include",
body: JSON.stringify(body),
headers: {
"Accept-Language": localStorage.getItem("language") || navigator.language || "en"
}
});
var res = await response.json();
if (res.status !== "ok") {
setStatus(res.msg || "Failed to sign in.", true);
return;
}
if (shouldFallbackToReact(res)) {
storeReactFallbackPayload({
search: window.location.search,
body: body,
res: res,
flow: "oauth",
responseType: responseType,
queryString: queryString,
innerParams: queryString,
oAuthParams: oAuthParams
});
goToReactFallback();
return;
}
var concatChar = oAuthParams.redirectUri.indexOf("?") !== -1 ? "&" : "?";
var responseMode = oAuthParams.responseMode || "query";
var responseTypes = responseType.split(" ");
if (responseType === "login") {
window.location.replace(getFromLink());
return;
}
if (responseType === "code") {
if (responseMode === "form_post") {
createFormAndSubmit(oAuthParams.redirectUri, {code: res.data, state: oAuthParams.state});
} else {
window.location.replace(oAuthParams.redirectUri + concatChar + "code=" + res.data + "&state=" + oAuthParams.state);
}
return;
}
if (responseTypes.indexOf("token") !== -1 || responseTypes.indexOf("id_token") !== -1) {
if (responseMode === "form_post") {
createFormAndSubmit(oAuthParams.redirectUri, {
token: responseTypes.indexOf("token") !== -1 ? res.data : null,
id_token: responseTypes.indexOf("id_token") !== -1 ? res.data : null,
token_type: "bearer",
state: oAuthParams.state
});
} else {
window.location.replace(oAuthParams.redirectUri + concatChar + responseType + "=" + res.data + "&state=" + oAuthParams.state + "&token_type=bearer");
}
return;
}
if (responseType === "link") {
var from = innerParams.get("from") || "/";
var oauth = innerParams.get("oauth");
if (oauth) {
from += "?oauth=" + oauth;
}
window.location.replace(from);
return;
}
if (responseType === "saml") {
if (res.data2 && res.data2.method === "POST") {
createFormAndSubmit(res.data2.redirectUrl, {
SAMLResponse: res.data,
RelayState: oAuthParams.relayState
});
} else if (res.data2) {
var samlRedirectUri = res.data2.redirectUrl;
window.location.replace(samlRedirectUri + (samlRedirectUri.indexOf("?") !== -1 ? "&" : "?") + "SAMLResponse=" + encodeURIComponent(res.data) + "&RelayState=" + oAuthParams.relayState);
} else {
setStatus("Unsupported SAML callback response.", true);
}
return;
}
goToReactFallback();
}
window.CasdoorAuthCallback = {
run: function() {
return run().catch(function(error) {
setStatus(error && error.message ? error.message : "Failed to complete callback.", true);
});
}
};
})();

View File

@@ -0,0 +1,409 @@
// 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.
(function() {
"use strict";
function getFallbackUrl() {
var url = new URL(window.location.href);
url.searchParams.delete("provider_hint");
return url.pathname + url.search + url.hash;
}
function redirectToFallback() {
window.location.replace(getFallbackUrl());
}
function getAcceptLanguage() {
return localStorage.getItem("language") || navigator.language || "en";
}
function isProviderVisible(providerItem) {
if (!providerItem || !providerItem.provider) {
return false;
}
if (["OAuth", "SAML", "Web3"].indexOf(providerItem.provider.category) === -1) {
return false;
}
if (providerItem.provider.type === "WeChatMiniProgram") {
return false;
}
return true;
}
function isProviderVisibleForSignIn(providerItem) {
if (providerItem.canSignIn === false) {
return false;
}
return isProviderVisible(providerItem);
}
function base64UrlEncode(buffer) {
var binary = "";
for (var index = 0; index < buffer.length; index++) {
binary += String.fromCharCode(buffer[index]);
}
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}
function generateCodeVerifier() {
var array = new Uint8Array(32);
window.crypto.getRandomValues(array);
return base64UrlEncode(array);
}
async function generateCodeChallenge(verifier) {
var data = new TextEncoder().encode(verifier);
var digest = await window.crypto.subtle.digest("SHA-256", data);
return base64UrlEncode(new Uint8Array(digest));
}
function storeCodeVerifier(state, verifier) {
localStorage.setItem("pkce_verifier_" + state, verifier);
}
var authInfo = {
Google: {scope: "profile+email", endpoint: "https://accounts.google.com/signin/oauth"},
GitHub: {scope: "user:email+read:user", endpoint: "https://github.com/login/oauth/authorize"},
QQ: {scope: "get_user_info", endpoint: "https://graph.qq.com/oauth2.0/authorize"},
WeChat: {scope: "snsapi_login", endpoint: "https://open.weixin.qq.com/connect/qrconnect", mpScope: "snsapi_userinfo", mpEndpoint: "https://open.weixin.qq.com/connect/oauth2/authorize"},
WeChatMiniProgram: {endpoint: "https://mp.weixin.qq.com/"},
Facebook: {scope: "email,public_profile", endpoint: "https://www.facebook.com/dialog/oauth"},
DingTalk: {scope: "openid", endpoint: "https://login.dingtalk.com/oauth2/auth"},
Weibo: {scope: "email", endpoint: "https://api.weibo.com/oauth2/authorize"},
Gitee: {scope: "user_info%20emails", endpoint: "https://gitee.com/oauth/authorize"},
LinkedIn: {scope: "r_liteprofile%20r_emailaddress", endpoint: "https://www.linkedin.com/oauth/v2/authorization"},
WeCom: {scope: "snsapi_userinfo", endpoint: "https://login.work.weixin.qq.com/wwlogin/sso/login", silentEndpoint: "https://open.weixin.qq.com/connect/oauth2/authorize", internalEndpoint: "https://login.work.weixin.qq.com/wwlogin/sso/login"},
Lark: {endpoint: "https://open.feishu.cn/open-apis/authen/v1/index", endpoint2: "https://accounts.larksuite.com/open-apis/authen/v1/authorize"},
GitLab: {scope: "read_user+profile", endpoint: "https://gitlab.com/oauth/authorize"},
ADFS: {scope: "openid", endpoint: "http://example.com"},
Baidu: {scope: "basic", endpoint: "http://openapi.baidu.com/oauth/2.0/authorize"},
Alipay: {scope: "basic", endpoint: "https://openauth.alipay.com/oauth2/publicAppAuthorize.htm"},
Casdoor: {scope: "openid%20profile%20email", endpoint: "http://example.com"},
Infoflow: {endpoint: "https://xpc.im.baidu.com/oauth2/authorize"},
Apple: {scope: "name%20email", endpoint: "https://appleid.apple.com/auth/authorize"},
AzureAD: {scope: "user.read", endpoint: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"},
AzureADB2C: {scope: "openid", endpoint: "https://tenant.b2clogin.com/tenant.onmicrosoft.com/userflow/oauth2/v2.0/authorize"},
Slack: {scope: "users:read", endpoint: "https://slack.com/oauth/authorize"},
Steam: {endpoint: "https://steamcommunity.com/openid/login"},
Okta: {scope: "openid%20profile%20email", endpoint: "http://example.com"},
Douyin: {scope: "user_info", endpoint: "https://open.douyin.com/platform/oauth/connect"},
Kwai: {scope: "user_info", endpoint: "https://open.kuaishou.com/oauth2/connect"},
Custom: {endpoint: "https://example.com/"},
Bilibili: {endpoint: "https://passport.bilibili.com/register/pc_oauth2.html"},
Line: {scope: "profile%20openid%20email", endpoint: "https://access.line.me/oauth2/v2.1/authorize"},
Amazon: {scope: "profile", endpoint: "https://www.amazon.com/ap/oa"},
Auth0: {scope: "openid%20profile%20email", endpoint: "http://auth0.com/authorize"},
BattleNet: {scope: "openid", endpoint: "https://oauth.battlenet.com.cn/authorize"},
Bitbucket: {scope: "account", endpoint: "https://bitbucket.org/site/oauth2/authorize"},
Box: {scope: "root_readwrite", endpoint: "https://account.box.com/api/oauth2/authorize"},
CloudFoundry: {scope: "cloud_controller.read", endpoint: "https://login.cloudfoundry.org/oauth/authorize"},
Dailymotion: {scope: "userinfo", endpoint: "https://api.dailymotion.com/oauth/authorize"},
Deezer: {scope: "basic_access", endpoint: "https://connect.deezer.com/oauth/auth.php"},
DigitalOcean: {scope: "read", endpoint: "https://cloud.digitalocean.com/v1/oauth/authorize"},
Discord: {scope: "identify%20email", endpoint: "https://discord.com/api/oauth2/authorize"},
Dropbox: {scope: "account_info.read", endpoint: "https://www.dropbox.com/oauth2/authorize"},
EveOnline: {scope: "publicData", endpoint: "https://login.eveonline.com/oauth/authorize"},
Fitbit: {scope: "activity%20heartrate%20location%20nutrition%20profile%20settings%20sleep%20social%20weight", endpoint: "https://www.fitbit.com/oauth2/authorize"},
Gitea: {scope: "user:email", endpoint: "https://gitea.com/login/oauth/authorize"},
Heroku: {scope: "global", endpoint: "https://id.heroku.com/oauth/authorize"},
InfluxCloud: {scope: "read:org", endpoint: "https://cloud2.influxdata.com/oauth/authorize"},
Instagram: {scope: "user_profile", endpoint: "https://api.instagram.com/oauth/authorize"},
Intercom: {scope: "user.read", endpoint: "https://app.intercom.com/oauth"},
Kakao: {scope: "account_email", endpoint: "https://kauth.kakao.com/oauth/authorize"},
Lastfm: {scope: "user_read", endpoint: "https://www.last.fm/api/auth"},
Mailru: {scope: "userinfo", endpoint: "https://oauth.mail.ru/login"},
MailRu: {scope: "userinfo", endpoint: "https://oauth.mail.ru/login"},
Meetup: {scope: "basic", endpoint: "https://secure.meetup.com/oauth2/authorize"},
MicrosoftOnline: {scope: "openid%20profile%20email", endpoint: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"},
Naver: {scope: "profile", endpoint: "https://nid.naver.com/oauth2.0/authorize"},
Nextcloud: {scope: "openid%20profile%20email", endpoint: "https://cloud.example.org/apps/oauth2/authorize"},
OneDrive: {scope: "offline_access%20onedrive.readonly", endpoint: "https://login.live.com/oauth20_authorize.srf"},
Oura: {scope: "personal", endpoint: "https://cloud.ouraring.com/oauth/authorize"},
Patreon: {scope: "identity", endpoint: "https://www.patreon.com/oauth2/authorize"},
PayPal: {scope: "openid%20profile%20email", endpoint: "https://www.sandbox.paypal.com/connect"},
SalesForce: {scope: "openid%20profile%20email", endpoint: "https://login.salesforce.com/services/oauth2/authorize"},
Shopify: {scope: "read_products", endpoint: "https://myshopify.com/admin/oauth/authorize"},
Soundcloud: {scope: "non-expiring", endpoint: "https://api.soundcloud.com/connect"},
SoundCloud: {scope: "non-expiring", endpoint: "https://api.soundcloud.com/connect"},
Spotify: {scope: "user-read-email", endpoint: "https://accounts.spotify.com/authorize"},
Strava: {scope: "read", endpoint: "https://www.strava.com/oauth/authorize"},
Stripe: {scope: "read_only", endpoint: "https://connect.stripe.com/oauth/authorize"},
TikTok: {scope: "user.info.basic", endpoint: "https://www.tiktok.com/auth/authorize/"},
Tumblr: {scope: "basic", endpoint: "https://www.tumblr.com/oauth2/authorize"},
Twitch: {scope: "user_read", endpoint: "https://id.twitch.tv/oauth2/authorize"},
Twitter: {scope: "users.read%20tweet.read", endpoint: "https://twitter.com/i/oauth2/authorize"},
Telegram: {scope: "", endpoint: "https://core.telegram.org/widgets/login"},
Typetalk: {scope: "my", endpoint: "https://typetalk.com/oauth2/authorize"},
Uber: {scope: "profile", endpoint: "https://login.uber.com/oauth/v2/authorize"},
VK: {scope: "email", endpoint: "https://oauth.vk.com/authorize"},
Wepay: {scope: "manage_accounts%20view_user", endpoint: "https://www.wepay.com/v2/oauth2/authorize"},
Xero: {scope: "openid%20profile%20email", endpoint: "https://login.xero.com/identity/connect/authorize"},
Yahoo: {scope: "openid%20profile%20email", endpoint: "https://api.login.yahoo.com/oauth2/request_auth"},
Yammer: {scope: "user", endpoint: "https://www.yammer.com/oauth2/authorize"},
Yandex: {scope: "login:email", endpoint: "https://oauth.yandex.com/authorize"},
Zoom: {scope: "user:read", endpoint: "https://zoom.us/oauth/authorize"},
MetaMask: {scope: "", endpoint: ""},
Web3Onboard: {scope: "", endpoint: ""}
};
function getStateFromQueryParams(applicationName, providerName, method, isShortState) {
var query = window.location.search;
query = query + "&application=" + encodeURIComponent(applicationName) + "&provider=" + encodeURIComponent(providerName) + "&method=" + method;
if (method === "link") {
query = query + "&from=" + window.location.pathname;
}
if (!isShortState) {
return btoa(query);
}
var state = providerName;
sessionStorage.setItem(state, query);
return state;
}
async function getAuthUrl(application, provider, method, code) {
if (!application || !provider) {
return "";
}
var normalizedType = provider.type.indexOf("Custom") === 0 ? "Custom" : provider.type;
var info = authInfo[normalizedType];
if (!info) {
return "";
}
var endpoint = info.endpoint;
var redirectOrigin = application.forcedRedirectOrigin ? application.forcedRedirectOrigin : window.location.origin;
var redirectUri = redirectOrigin + "/callback";
var scope = info.scope;
if (provider.scopes && provider.scopes.trim() !== "") {
scope = provider.scopes;
}
var isShortState = (provider.type === "WeChat" && navigator.userAgent.indexOf("MicroMessenger") !== -1) || provider.type === "Twitter";
var applicationName = application.name;
if (application.isShared) {
applicationName = application.name + "-org-" + application.organization;
}
var state = getStateFromQueryParams(applicationName, provider.name, method, isShortState);
var codeVerifier = generateCodeVerifier();
var codeChallenge = await generateCodeChallenge(codeVerifier);
storeCodeVerifier(state, codeVerifier);
if (provider.type === "AzureAD") {
if (provider.domain !== "") {
endpoint = endpoint.replace("common", provider.domain);
}
} else if (provider.type === "Apple") {
redirectUri = redirectOrigin + "/api/callback";
} else if (provider.type === "Google" && provider.disableSsl) {
scope += "+https://www.googleapis.com/auth/user.phonenumbers.read";
} else if (provider.type === "Nextcloud") {
if (provider.domain) {
endpoint = provider.domain + "/apps/oauth2/authorize";
}
} else if (provider.type === "Lark" && provider.disableSsl) {
endpoint = authInfo[provider.type].endpoint2;
}
if (["Google", "GitHub", "Facebook", "Weibo", "Gitee", "LinkedIn", "GitLab", "AzureAD", "Slack", "Line", "Amazon", "Auth0", "BattleNet", "Bitbucket", "Box", "CloudFoundry", "Dailymotion", "DigitalOcean", "Discord", "Dropbox", "EveOnline", "Gitea", "Heroku", "InfluxCloud", "Instagram", "Intercom", "Kakao", "MailRu", "Mailru", "Meetup", "MicrosoftOnline", "Naver", "Nextcloud", "OneDrive", "Oura", "Patreon", "PayPal", "SalesForce", "SoundCloud", "Soundcloud", "Spotify", "Strava", "Stripe", "Tumblr", "Twitch", "Typetalk", "Uber", "VK", "Wepay", "Xero", "Yahoo", "Yammer", "Yandex", "Zoom"].indexOf(provider.type) !== -1) {
return endpoint + "?client_id=" + provider.clientId + "&redirect_uri=" + redirectUri + "&scope=" + scope + "&response_type=code&state=" + state;
} else if (provider.type === "QQ") {
return endpoint + "?response_type=code&client_id=" + provider.clientId + "&redirect_uri=" + encodeURIComponent(redirectUri) + "&state=" + encodeURIComponent(state) + "&scope=" + encodeURIComponent(scope);
} else if (provider.type === "AzureADB2C") {
return "https://" + provider.domain + ".b2clogin.com/" + provider.domain + ".onmicrosoft.com/" + provider.appId + "/oauth2/v2.0/authorize?client_id=" + provider.clientId + "&nonce=defaultNonce&redirect_uri=" + encodeURIComponent(redirectUri) + "&scope=" + scope + "&response_type=code&state=" + state + "&prompt=login";
} else if (provider.type === "DingTalk") {
return endpoint + "?client_id=" + provider.clientId + "&redirect_uri=" + redirectUri + "&scope=" + scope + "&response_type=code&prompt=login%20consent&state=" + state;
} else if (provider.type === "WeChat") {
if (navigator.userAgent.indexOf("MicroMessenger") !== -1) {
return authInfo[provider.type].mpEndpoint + "?appid=" + provider.clientId2 + "&redirect_uri=" + redirectUri + "&state=" + state + "&scope=" + authInfo[provider.type].mpScope + "&response_type=code#wechat_redirect";
}
if (provider.clientId2 && provider.disableSsl && provider.signName === "media") {
return redirectOrigin + "/callback?state=" + state + "&code=wechat_oa:" + code;
}
return endpoint + "?appid=" + provider.clientId + "&redirect_uri=" + redirectUri + "&scope=" + scope + "&response_type=code&state=" + state + "#wechat_redirect";
} else if (provider.type === "WeCom") {
if (provider.subType === "Internal") {
if (provider.method === "Silent") {
endpoint = authInfo[provider.type].silentEndpoint;
return endpoint + "?appid=" + provider.clientId + "&redirect_uri=" + redirectUri + "&state=" + state + "&scope=" + scope + "&response_type=code#wechat_redirect";
}
if (provider.method === "Normal") {
endpoint = authInfo[provider.type].internalEndpoint;
return endpoint + "?login_type=CorpApp&appid=" + provider.clientId + "&agentid=" + provider.appId + "&redirect_uri=" + redirectUri + "&state=" + state;
}
return "https://error:not-supported-provider-method:" + provider.method;
}
if (provider.subType === "Third-party") {
if (provider.method === "Silent") {
endpoint = authInfo[provider.type].silentEndpoint;
return endpoint + "?appid=" + provider.clientId + "&redirect_uri=" + redirectUri + "&state=" + state + "&scope=" + scope + "&response_type=code#wechat_redirect";
}
if (provider.method === "Normal") {
endpoint = authInfo[provider.type].endpoint;
return endpoint + "?login_type=ServiceApp&appid=" + provider.clientId + "&redirect_uri=" + redirectUri + "&state=" + state;
}
return "https://error:not-supported-provider-method:" + provider.method;
}
return "https://error:not-supported-provider-sub-type:" + provider.subType;
} else if (provider.type === "Lark") {
if (provider.disableSsl) {
redirectUri = encodeURIComponent(redirectUri);
}
return endpoint + "?app_id=" + provider.clientId + "&redirect_uri=" + redirectUri + "&state=" + state;
} else if (provider.type === "ADFS") {
return provider.domain + "/adfs/oauth2/authorize?client_id=" + provider.clientId + "&redirect_uri=" + redirectUri + "&state=" + state + "&response_type=code&nonce=casdoor&scope=openid";
} else if (provider.type === "Baidu") {
return endpoint + "?client_id=" + provider.clientId + "&redirect_uri=" + redirectUri + "&state=" + state + "&response_type=code&scope=" + scope + "&display=popup";
} else if (provider.type === "Alipay") {
return endpoint + "?app_id=" + provider.clientId + "&scope=auth_user&redirect_uri=" + redirectUri + "&state=" + state + "&response_type=code&scope=" + scope + "&display=popup";
} else if (provider.type === "Casdoor") {
return provider.domain + "/login/oauth/authorize?client_id=" + provider.clientId + "&redirect_uri=" + redirectUri + "&state=" + state + "&response_type=code&scope=" + scope;
} else if (provider.type === "Infoflow") {
return endpoint + "?appid=" + provider.clientId + "&redirect_uri=" + redirectUri + "?state=" + state;
} else if (provider.type === "Apple") {
return endpoint + "?client_id=" + provider.clientId + "&redirect_uri=" + redirectUri + "&state=" + state + "&response_type=code%20id_token&scope=" + scope + "&response_mode=form_post";
} else if (provider.type === "Steam") {
return endpoint + "?openid.claimed_id=http://specs.openid.net/auth/2.0/identifier_select&openid.identity=http://specs.openid.net/auth/2.0/identifier_select&openid.mode=checkid_setup&openid.ns=http://specs.openid.net/auth/2.0&openid.realm=" + redirectOrigin + "&openid.return_to=" + redirectUri + "?state=" + state;
} else if (provider.type === "Okta") {
return provider.domain + "/v1/authorize?client_id=" + provider.clientId + "&redirect_uri=" + redirectUri + "&state=" + state + "&response_type=code&scope=" + scope;
} else if (provider.type === "Douyin" || provider.type === "TikTok") {
return endpoint + "?client_key=" + provider.clientId + "&redirect_uri=" + redirectUri + "&state=" + state + "&response_type=code&scope=" + scope;
} else if (provider.type === "Kwai") {
return endpoint + "?app_id=" + provider.clientId + "&redirect_uri=" + redirectUri + "&state=" + state + "&response_type=code&scope=" + scope;
} else if (normalizedType === "Custom") {
var authUrl = provider.customAuthUrl + "?client_id=" + provider.clientId + "&redirect_uri=" + redirectUri + "&scope=" + provider.scopes + "&response_type=code&state=" + state;
if (provider.enablePkce) {
authUrl += "&code_challenge=" + codeChallenge + "&code_challenge_method=S256";
}
return authUrl;
} else if (provider.type === "Bilibili") {
return endpoint + "#/?client_id=" + provider.clientId + "&return_url=" + redirectUri + "&state=" + state + "&response_type=code";
} else if (provider.type === "Deezer") {
return endpoint + "?app_id=" + provider.clientId + "&redirect_uri=" + redirectUri + "&perms=" + scope;
} else if (provider.type === "Lastfm") {
return endpoint + "?api_key=" + provider.clientId + "&cb=" + redirectUri;
} else if (provider.type === "Shopify") {
return endpoint + "?client_id=" + provider.clientId + "&redirect_uri=" + redirectUri + "&scope=" + scope + "&state=" + state + "&grant_options[]=per-user";
} else if (provider.type === "Twitter" || provider.type === "Fitbit") {
return endpoint + "?client_id=" + provider.clientId + "&redirect_uri=" + redirectUri + "&state=" + state + "&response_type=code&scope=" + scope + "&code_challenge=" + codeChallenge + "&code_challenge_method=S256";
} else if (provider.type === "Telegram") {
return redirectOrigin + "/telegram-login?state=" + state;
} else if (provider.type === "MetaMask" || provider.type === "Web3Onboard") {
return redirectUri + "?state=" + state;
}
return "";
}
async function fetchJson(url, options) {
var response = await fetch(url, options);
if (!response.ok) {
throw new Error("Request failed with status " + response.status);
}
return response.json();
}
async function getApplication(applicationName) {
return fetchJson("/api/get-application?id=" + encodeURIComponent(applicationName), {
credentials: "include"
});
}
async function getProviders(applicationName, language) {
return fetchJson("/api/get-login-providers?application=" + encodeURIComponent(applicationName) + "&acceptLanguage=" + encodeURIComponent(language), {
credentials: "include"
});
}
async function run() {
localStorage.setItem("signinUrl", window.location.pathname + window.location.search);
var providerHint = new URLSearchParams(window.location.search).get("provider_hint");
if (!providerHint) {
redirectToFallback();
return;
}
var searchParams = new URLSearchParams();
var currentSearchParams = new URLSearchParams(window.location.search);
searchParams.set("clientId", currentSearchParams.get("client_id") || "");
searchParams.set("responseType", currentSearchParams.get("response_type") || "");
searchParams.set("redirectUri", currentSearchParams.get("redirect_uri") || "");
searchParams.set("type", "code");
searchParams.set("scope", currentSearchParams.get("scope") || "");
searchParams.set("state", currentSearchParams.get("state") || "");
searchParams.set("nonce", currentSearchParams.get("nonce") || "");
searchParams.set("code_challenge_method", currentSearchParams.get("code_challenge_method") || "");
searchParams.set("code_challenge", currentSearchParams.get("code_challenge") || "");
var response = await fetch("/api/get-app-login?" + searchParams.toString(), {
method: "GET",
credentials: "include",
headers: {
"Accept-Language": getAcceptLanguage()
}
});
var payload = await response.json();
if (payload.status !== "ok" || !payload.data) {
redirectToFallback();
return;
}
var application = payload.data;
localStorage.setItem("applicationName", application.name || "");
var providerItem = (application.providers || []).find(function(item) {
return item && item.provider && item.provider.name === providerHint && isProviderVisibleForSignIn(item);
});
if (!providerItem) {
redirectToFallback();
return;
}
var authUrl = await getAuthUrl(application, providerItem.provider, "signup");
if (!authUrl) {
redirectToFallback();
return;
}
window.location.replace(authUrl);
}
window.CasdoorProviderHintRedirect = {
run: function() {
return run().catch(function() {
redirectToFallback();
});
}
};
})();

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