forked from casdoor/casdoor
Compare commits
28 Commits
v2.339.0
...
copilot/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbd7f09859 | ||
|
|
9643dc2ee0 | ||
|
|
899c2546cf | ||
|
|
95defad3b1 | ||
|
|
6a263cb5cb | ||
|
|
54d6a59cb6 | ||
|
|
2693c07b3c | ||
|
|
2895c72d32 | ||
|
|
f6129b09c8 | ||
|
|
0bbbb48af1 | ||
|
|
34a8b252d5 | ||
|
|
c756e56f74 | ||
|
|
dbc2a676ba | ||
|
|
74e6b73e7b | ||
|
|
07de8a40d6 | ||
|
|
c6a6ec8869 | ||
|
|
394b3e1372 | ||
|
|
fa93d4eb8b | ||
|
|
47a5fc8b09 | ||
|
|
c1acb7a432 | ||
|
|
c10b2c162f | ||
|
|
41ec8ba44f | ||
|
|
7df722a103 | ||
|
|
04b1ca1157 | ||
|
|
b0fecefeb7 | ||
|
|
167d24fb1f | ||
|
|
dc58ac0503 | ||
|
|
038d021797 |
@@ -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
|
||||
|
||||
@@ -118,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)
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
@@ -24,6 +25,9 @@ import (
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
//go:embed waf.conf
|
||||
var WafConf string
|
||||
|
||||
func init() {
|
||||
// this array contains the beego configuration items that may be modified via env
|
||||
presetConfigItems := []string{"httpport", "appname"}
|
||||
|
||||
246
conf/waf.conf
Normal file
246
conf/waf.conf
Normal file
@@ -0,0 +1,246 @@
|
||||
# -- Rule engine initialization ----------------------------------------------
|
||||
|
||||
# Enable Coraza, attaching it to every transaction. Use detection
|
||||
# only to start with, because that minimises the chances of post-installation
|
||||
# disruption.
|
||||
#
|
||||
SecRuleEngine DetectionOnly
|
||||
|
||||
|
||||
# -- Request body handling ---------------------------------------------------
|
||||
|
||||
# Allow Coraza to access request bodies. If you don't, Coraza
|
||||
# won't be able to see any POST parameters, which opens a large security
|
||||
# hole for attackers to exploit.
|
||||
#
|
||||
SecRequestBodyAccess On
|
||||
|
||||
# Enable XML request body parser.
|
||||
# Initiate XML Processor in case of xml content-type
|
||||
#
|
||||
SecRule REQUEST_HEADERS:Content-Type "^(?:application(?:/soap\+|/)|text/)xml" \
|
||||
"id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML"
|
||||
|
||||
# Enable JSON request body parser.
|
||||
# Initiate JSON Processor in case of JSON content-type; change accordingly
|
||||
# if your application does not use 'application/json'
|
||||
#
|
||||
SecRule REQUEST_HEADERS:Content-Type "^application/json" \
|
||||
"id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON"
|
||||
|
||||
# Sample rule to enable JSON request body parser for more subtypes.
|
||||
# Uncomment or adapt this rule if you want to engage the JSON
|
||||
# Processor for "+json" subtypes
|
||||
#
|
||||
#SecRule REQUEST_HEADERS:Content-Type "^application/[a-z0-9.-]+[+]json" \
|
||||
# "id:'200006',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON"
|
||||
|
||||
# Maximum request body size we will accept for buffering. If you support
|
||||
# file uploads then the value given on the first line has to be as large
|
||||
# as the largest file you are willing to accept. The second value refers
|
||||
# to the size of data, with files excluded. You want to keep that value as
|
||||
# low as practical.
|
||||
#
|
||||
SecRequestBodyLimit 13107200
|
||||
|
||||
SecRequestBodyInMemoryLimit 131072
|
||||
|
||||
# SecRequestBodyNoFilesLimit is currently not supported by Coraza
|
||||
# SecRequestBodyNoFilesLimit 131072
|
||||
|
||||
# What to do if the request body size is above our configured limit.
|
||||
# Keep in mind that this setting will automatically be set to ProcessPartial
|
||||
# when SecRuleEngine is set to DetectionOnly mode in order to minimize
|
||||
# disruptions when initially deploying Coraza.
|
||||
#
|
||||
SecRequestBodyLimitAction Reject
|
||||
|
||||
# Verify that we've correctly processed the request body.
|
||||
# As a rule of thumb, when failing to process a request body
|
||||
# you should reject the request (when deployed in blocking mode)
|
||||
# or log a high-severity alert (when deployed in detection-only mode).
|
||||
#
|
||||
SecRule REQBODY_ERROR "!@eq 0" \
|
||||
"id:'200002', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2"
|
||||
|
||||
# By default be strict with what we accept in the multipart/form-data
|
||||
# request body. If the rule below proves to be too strict for your
|
||||
# environment consider changing it to detection-only. You are encouraged
|
||||
# _not_ to remove it altogether.
|
||||
#
|
||||
SecRule MULTIPART_STRICT_ERROR "!@eq 0" \
|
||||
"id:'200003',phase:2,t:none,log,deny,status:400, \
|
||||
msg:'Multipart request body failed strict validation: \
|
||||
PE %{REQBODY_PROCESSOR_ERROR}, \
|
||||
BQ %{MULTIPART_BOUNDARY_QUOTED}, \
|
||||
BW %{MULTIPART_BOUNDARY_WHITESPACE}, \
|
||||
DB %{MULTIPART_DATA_BEFORE}, \
|
||||
DA %{MULTIPART_DATA_AFTER}, \
|
||||
HF %{MULTIPART_HEADER_FOLDING}, \
|
||||
LF %{MULTIPART_LF_LINE}, \
|
||||
SM %{MULTIPART_MISSING_SEMICOLON}, \
|
||||
IQ %{MULTIPART_INVALID_QUOTING}, \
|
||||
IP %{MULTIPART_INVALID_PART}, \
|
||||
IH %{MULTIPART_INVALID_HEADER_FOLDING}, \
|
||||
FL %{MULTIPART_FILE_LIMIT_EXCEEDED}'"
|
||||
|
||||
# Did we see anything that might be a boundary?
|
||||
#
|
||||
# Here is a short description about the Coraza Multipart parser: the
|
||||
# parser returns with value 0, if all "boundary-like" line matches with
|
||||
# the boundary string which given in MIME header. In any other cases it returns
|
||||
# with different value, eg. 1 or 2.
|
||||
#
|
||||
# The RFC 1341 descript the multipart content-type and its syntax must contains
|
||||
# only three mandatory lines (above the content):
|
||||
# * Content-Type: multipart/mixed; boundary=BOUNDARY_STRING
|
||||
# * --BOUNDARY_STRING
|
||||
# * --BOUNDARY_STRING--
|
||||
#
|
||||
# First line indicates, that this is a multipart content, second shows that
|
||||
# here starts a part of the multipart content, third shows the end of content.
|
||||
#
|
||||
# If there are any other lines, which starts with "--", then it should be
|
||||
# another boundary id - or not.
|
||||
#
|
||||
# After 3.0.3, there are two kinds of types of boundary errors: strict and permissive.
|
||||
#
|
||||
# If multipart content contains the three necessary lines with correct order, but
|
||||
# there are one or more lines with "--", then parser returns with value 2 (non-zero).
|
||||
#
|
||||
# If some of the necessary lines (usually the start or end) misses, or the order
|
||||
# is wrong, then parser returns with value 1 (also a non-zero).
|
||||
#
|
||||
# You can choose, which one is what you need. The example below contains the
|
||||
# 'strict' mode, which means if there are any lines with start of "--", then
|
||||
# Coraza blocked the content. But the next, commented example contains
|
||||
# the 'permissive' mode, then you check only if the necessary lines exists in
|
||||
# correct order. Whit this, you can enable to upload PEM files (eg "----BEGIN.."),
|
||||
# or other text files, which contains eg. HTTP headers.
|
||||
#
|
||||
# The difference is only the operator - in strict mode (first) the content blocked
|
||||
# in case of any non-zero value. In permissive mode (second, commented) the
|
||||
# content blocked only if the value is explicit 1. If it 0 or 2, the content will
|
||||
# allowed.
|
||||
#
|
||||
|
||||
#
|
||||
# See #1747 and #1924 for further information on the possible values for
|
||||
# MULTIPART_UNMATCHED_BOUNDARY.
|
||||
#
|
||||
SecRule MULTIPART_UNMATCHED_BOUNDARY "@eq 1" \
|
||||
"id:'200004',phase:2,t:none,log,deny,msg:'Multipart parser detected a possible unmatched boundary.'"
|
||||
|
||||
# Some internal errors will set flags in TX and we will need to look for these.
|
||||
# All of these are prefixed with "MSC_". The following flags currently exist:
|
||||
#
|
||||
# COR_PCRE_LIMITS_EXCEEDED: PCRE match limits were exceeded.
|
||||
#
|
||||
SecRule TX:/^COR_/ "!@streq 0" \
|
||||
"id:'200005',phase:2,t:none,deny,msg:'Coraza internal error flagged: %{MATCHED_VAR_NAME}'"
|
||||
|
||||
|
||||
# -- Response body handling --------------------------------------------------
|
||||
|
||||
# Allow Coraza to access response bodies.
|
||||
# You should have this directive enabled in order to identify errors
|
||||
# and data leakage issues.
|
||||
#
|
||||
# Do keep in mind that enabling this directive does increases both
|
||||
# memory consumption and response latency.
|
||||
#
|
||||
SecResponseBodyAccess On
|
||||
|
||||
# Which response MIME types do you want to inspect? You should adjust the
|
||||
# configuration below to catch documents but avoid static files
|
||||
# (e.g., images and archives).
|
||||
#
|
||||
SecResponseBodyMimeType text/plain text/html text/xml
|
||||
|
||||
# Buffer response bodies of up to 512 KB in length.
|
||||
SecResponseBodyLimit 524288
|
||||
|
||||
# What happens when we encounter a response body larger than the configured
|
||||
# limit? By default, we process what we have and let the rest through.
|
||||
# That's somewhat less secure, but does not break any legitimate pages.
|
||||
#
|
||||
SecResponseBodyLimitAction ProcessPartial
|
||||
|
||||
|
||||
# -- Filesystem configuration ------------------------------------------------
|
||||
|
||||
# The location where Coraza will keep its persistent data. This default setting
|
||||
# is chosen due to all systems have /tmp available however, it
|
||||
# too should be updated to a place that other users can't access.
|
||||
#
|
||||
SecDataDir /tmp/
|
||||
|
||||
|
||||
# -- File uploads handling configuration -------------------------------------
|
||||
|
||||
# The location where Coraza stores intercepted uploaded files. This
|
||||
# location must be private to Coraza. You don't want other users on
|
||||
# the server to access the files, do you?
|
||||
#
|
||||
#SecUploadDir /opt/coraza/var/upload/
|
||||
|
||||
# By default, only keep the files that were determined to be unusual
|
||||
# in some way (by an external inspection script). For this to work you
|
||||
# will also need at least one file inspection rule.
|
||||
#
|
||||
#SecUploadKeepFiles RelevantOnly
|
||||
|
||||
# Uploaded files are by default created with permissions that do not allow
|
||||
# any other user to access them. You may need to relax that if you want to
|
||||
# interface Coraza to an external program (e.g., an anti-virus).
|
||||
#
|
||||
#SecUploadFileMode 0600
|
||||
|
||||
|
||||
# -- Debug log configuration -------------------------------------------------
|
||||
|
||||
# Default debug log path
|
||||
# Debug levels:
|
||||
# 0: No logging (least verbose)
|
||||
# 1: Error
|
||||
# 2: Warn
|
||||
# 3: Info
|
||||
# 4-8: Debug
|
||||
# 9: Trace (most verbose)
|
||||
# Most logging has not been implemented because it will be replaced with
|
||||
# advanced rule profiling options
|
||||
#SecDebugLog /opt/coraza/var/log/debug.log
|
||||
#SecDebugLogLevel 3
|
||||
|
||||
|
||||
# -- Audit log configuration -------------------------------------------------
|
||||
|
||||
# Log the transactions that are marked by a rule, as well as those that
|
||||
# trigger a server error (determined by a 5xx or 4xx, excluding 404,
|
||||
# level response status codes).
|
||||
#
|
||||
SecAuditEngine RelevantOnly
|
||||
SecAuditLogRelevantStatus "^(?:(5|4)(0|1)[0-9])$"
|
||||
|
||||
# Log everything we know about a transaction.
|
||||
SecAuditLogParts ABIJDEFHZ
|
||||
|
||||
# Use a single file for logging. This is much easier to look at, but
|
||||
# assumes that you will use the audit log only occasionally.
|
||||
#
|
||||
SecAuditLogType Serial
|
||||
|
||||
|
||||
# -- Miscellaneous -----------------------------------------------------------
|
||||
|
||||
# Use the most commonly used application/x-www-form-urlencoded parameter
|
||||
# separator. There's probably only one application somewhere that uses
|
||||
# something else so don't expect to change this value.
|
||||
#
|
||||
SecArgumentSeparator &
|
||||
|
||||
# Settle on version 0 (zero) cookies, as that is what most applications
|
||||
# use. Using an incorrect cookie version may open your installation to
|
||||
# evasion attacks (against the rules that examine named cookies).
|
||||
#
|
||||
SecCookieFormat 0
|
||||
@@ -409,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
|
||||
}
|
||||
|
||||
|
||||
@@ -198,10 +198,11 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
} else {
|
||||
scope := c.Ctx.Input.Query("scope")
|
||||
nonce := c.Ctx.Input.Query("nonce")
|
||||
if !object.IsScopeValid(scope, application) {
|
||||
expandedScope, valid := object.IsScopeValidAndExpand(scope, application)
|
||||
if !valid {
|
||||
resp = &Response{Status: "error", Msg: "error: invalid_scope", Data: ""}
|
||||
} else {
|
||||
token, _ := object.GetTokenByUser(application, user, scope, nonce, c.Ctx.Request.Host)
|
||||
token, _ := object.GetTokenByUser(application, user, expandedScope, nonce, c.Ctx.Request.Host)
|
||||
resp = tokenToResponse(token)
|
||||
|
||||
resp.Data3 = user.NeedUpdatePassword
|
||||
@@ -455,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
|
||||
@@ -806,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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -846,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 {
|
||||
@@ -1390,7 +1414,7 @@ func (c *ApiController) Callback() {
|
||||
code := c.GetString("code")
|
||||
state := c.GetString("state")
|
||||
|
||||
frontendCallbackUrl := fmt.Sprintf("/callback?code=%s&state=%s", code, state)
|
||||
frontendCallbackUrl := fmt.Sprintf("/callback?code=%s&state=%s", url.QueryEscape(code), url.QueryEscape(state))
|
||||
c.Ctx.Redirect(http.StatusFound, frontendCallbackUrl)
|
||||
}
|
||||
|
||||
|
||||
105
controllers/kerberos.go
Normal file
105
controllers/kerberos.go
Normal 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()
|
||||
}
|
||||
}
|
||||
229
controllers/rule.go
Normal file
229
controllers/rule.go
Normal file
@@ -0,0 +1,229 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
// GetRules
|
||||
// @Title GetRules
|
||||
// @Tag Rule API
|
||||
// @Description get rules
|
||||
// @Param owner query string true "The owner of rules"
|
||||
// @Success 200 {array} object.Rule The Response object
|
||||
// @router /get-rules [get]
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
// GetRule
|
||||
// @Title GetRule
|
||||
// @Tag Rule API
|
||||
// @Description get rule
|
||||
// @Param id query string true "The id ( owner/name ) of the rule"
|
||||
// @Success 200 {object} object.Rule The Response object
|
||||
// @router /get-rule [get]
|
||||
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)
|
||||
}
|
||||
|
||||
// AddRule
|
||||
// @Title AddRule
|
||||
// @Tag Rule API
|
||||
// @Description add rule
|
||||
// @Param body body object.Rule true "The details of the rule"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /add-rule [post]
|
||||
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()
|
||||
}
|
||||
|
||||
// UpdateRule
|
||||
// @Title UpdateRule
|
||||
// @Tag Rule API
|
||||
// @Description update rule
|
||||
// @Param id query string true "The id ( owner/name ) of the rule"
|
||||
// @Param body body object.Rule true "The details of the rule"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-rule [post]
|
||||
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()
|
||||
}
|
||||
|
||||
// DeleteRule
|
||||
// @Title DeleteRule
|
||||
// @Tag Rule API
|
||||
// @Description delete rule
|
||||
// @Param body body object.Rule true "The details of the rule"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /delete-rule [post]
|
||||
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
|
||||
}
|
||||
165
controllers/site.go
Normal file
165
controllers/site.go
Normal file
@@ -0,0 +1,165 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
// GetGlobalSites
|
||||
// @Title GetGlobalSites
|
||||
// @Tag Site API
|
||||
// @Description get global sites
|
||||
// @Success 200 {array} object.Site The Response object
|
||||
// @router /get-global-sites [get]
|
||||
func (c *ApiController) GetGlobalSites() {
|
||||
sites, err := object.GetGlobalSites()
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(object.GetMaskedSites(sites, util.GetHostname()))
|
||||
}
|
||||
|
||||
// GetSites
|
||||
// @Title GetSites
|
||||
// @Tag Site API
|
||||
// @Description get sites
|
||||
// @Param owner query string true "The owner of sites"
|
||||
// @Success 200 {array} object.Site The Response object
|
||||
// @router /get-sites [get]
|
||||
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())
|
||||
}
|
||||
|
||||
// GetSite
|
||||
// @Title GetSite
|
||||
// @Tag Site API
|
||||
// @Description get site
|
||||
// @Param id query string true "The id ( owner/name ) of the site"
|
||||
// @Success 200 {object} object.Site The Response object
|
||||
// @router /get-site [get]
|
||||
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()))
|
||||
}
|
||||
|
||||
// UpdateSite
|
||||
// @Title UpdateSite
|
||||
// @Tag Site API
|
||||
// @Description update site
|
||||
// @Param id query string true "The id ( owner/name ) of the site"
|
||||
// @Param body body object.Site true "The details of the site"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-site [post]
|
||||
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()
|
||||
}
|
||||
|
||||
// AddSite
|
||||
// @Title AddSite
|
||||
// @Tag Site API
|
||||
// @Description add site
|
||||
// @Param body body object.Site true "The details of the site"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /add-site [post]
|
||||
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()
|
||||
}
|
||||
|
||||
// DeleteSite
|
||||
// @Title DeleteSite
|
||||
// @Tag Site API
|
||||
// @Description delete site
|
||||
// @Param body body object.Site true "The details of the site"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /delete-site [post]
|
||||
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()
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -598,15 +598,11 @@ func (c *ApiController) VerifyCode() {
|
||||
}
|
||||
|
||||
if !passed {
|
||||
result, err := object.CheckVerificationCode(checkDest, authForm.Code, c.GetAcceptLanguage())
|
||||
err = object.CheckVerifyCodeWithLimit(user, checkDest, authForm.Code, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if result.Code != object.VerificationSuccess {
|
||||
c.ResponseError(result.Msg)
|
||||
return
|
||||
}
|
||||
|
||||
err = object.DisableVerificationCode(checkDest)
|
||||
if err != nil {
|
||||
|
||||
92
go.mod
92
go.mod
@@ -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
|
||||
@@ -23,6 +25,7 @@ require (
|
||||
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
|
||||
@@ -30,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
|
||||
@@ -45,6 +49,8 @@ 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
|
||||
@@ -58,7 +64,7 @@ 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
|
||||
@@ -74,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
|
||||
@@ -85,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
|
||||
@@ -99,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
|
||||
@@ -126,17 +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
|
||||
@@ -146,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
|
||||
@@ -155,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
|
||||
@@ -180,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
|
||||
@@ -196,24 +208,26 @@ require (
|
||||
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.43 // 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
|
||||
@@ -233,6 +247,7 @@ require (
|
||||
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
|
||||
@@ -250,36 +265,36 @@ require (
|
||||
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
|
||||
@@ -295,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
|
||||
)
|
||||
|
||||
179
go.sum
179
go.sum
@@ -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=
|
||||
@@ -875,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=
|
||||
@@ -908,8 +907,14 @@ 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=
|
||||
@@ -925,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=
|
||||
@@ -976,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=
|
||||
@@ -995,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=
|
||||
@@ -1028,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=
|
||||
@@ -1046,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=
|
||||
@@ -1255,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=
|
||||
@@ -1287,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=
|
||||
@@ -1296,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=
|
||||
@@ -1305,11 +1323,21 @@ 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=
|
||||
@@ -1415,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=
|
||||
@@ -1448,8 +1478,9 @@ github.com/microsoft/go-mssqldb v1.9.0 h1:5Vq+u2f4LDujJNeZn62Z4kBDEC9MjLv0ukRzOu
|
||||
github.com/microsoft/go-mssqldb v1.9.0/go.mod h1:GBbW9ASTiDC+mpgWDGKdm3FnFLTUsLYN3iFL90lQ+PA=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||
github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg=
|
||||
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
|
||||
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
|
||||
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
|
||||
github.com/mileusna/viber v1.0.1 h1:gWB6/lKoWYVxkH0Jb8jRnGIRZ/9DEM7RBZRJHRfdYWs=
|
||||
github.com/mileusna/viber v1.0.1/go.mod h1:Pxu/iPMnYjnHgu+bEp3SiKWHWmlf/kDp/yOX8XUdYrQ=
|
||||
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
|
||||
@@ -1512,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=
|
||||
@@ -1542,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=
|
||||
@@ -1564,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=
|
||||
@@ -1668,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=
|
||||
@@ -1720,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=
|
||||
@@ -1741,6 +1779,8 @@ github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtX
|
||||
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=
|
||||
@@ -1784,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=
|
||||
@@ -1853,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=
|
||||
@@ -1867,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=
|
||||
@@ -1899,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=
|
||||
@@ -1933,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=
|
||||
@@ -2022,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=
|
||||
@@ -2055,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=
|
||||
@@ -2079,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=
|
||||
@@ -2206,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=
|
||||
@@ -2231,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=
|
||||
@@ -2257,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=
|
||||
@@ -2341,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=
|
||||
@@ -2355,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=
|
||||
@@ -2571,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=
|
||||
@@ -2621,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=
|
||||
@@ -2644,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=
|
||||
@@ -2775,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=
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
43
ip/ip.go
Normal file
43
ip/ip.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright 2024 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ip
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
func InitIpDb() {
|
||||
err := Init("ip/17monipdb.dat")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func IsAbroadIp(ip string) bool {
|
||||
// If it's an intranet IP, it's not abroad
|
||||
if util.IsIntranetIp(ip) {
|
||||
return false
|
||||
}
|
||||
|
||||
info, err := Find(ip)
|
||||
if err != nil {
|
||||
fmt.Printf("error: ip = %s, error = %s\n", ip, err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
return info.Country != "中国"
|
||||
}
|
||||
199
ip/ip17mon.go
Normal file
199
ip/ip17mon.go
Normal file
@@ -0,0 +1,199 @@
|
||||
// Copyright 2022 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ip
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
)
|
||||
|
||||
const Null = "N/A"
|
||||
|
||||
var (
|
||||
ErrInvalidIp = errors.New("invalid ip format")
|
||||
std *Locator
|
||||
)
|
||||
|
||||
// Init default locator with dataFile
|
||||
func Init(dataFile string) (err error) {
|
||||
if std != nil {
|
||||
return
|
||||
}
|
||||
std, err = NewLocator(dataFile)
|
||||
return
|
||||
}
|
||||
|
||||
// Init default locator with data
|
||||
func InitWithData(data []byte) {
|
||||
if std != nil {
|
||||
return
|
||||
}
|
||||
std = NewLocatorWithData(data)
|
||||
return
|
||||
}
|
||||
|
||||
// Find locationInfo by ip string
|
||||
// It will return err when ipstr is not a valid format
|
||||
func Find(ipstr string) (*LocationInfo, error) {
|
||||
return std.Find(ipstr)
|
||||
}
|
||||
|
||||
// Find locationInfo by uint32
|
||||
func FindByUint(ip uint32) *LocationInfo {
|
||||
return std.FindByUint(ip)
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
// New locator with dataFile
|
||||
func NewLocator(dataFile string) (loc *Locator, err error) {
|
||||
data, err := ioutil.ReadFile(dataFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
loc = NewLocatorWithData(data)
|
||||
return
|
||||
}
|
||||
|
||||
// New locator with data
|
||||
func NewLocatorWithData(data []byte) (loc *Locator) {
|
||||
loc = new(Locator)
|
||||
loc.init(data)
|
||||
return
|
||||
}
|
||||
|
||||
type Locator struct {
|
||||
textData []byte
|
||||
indexData1 []uint32
|
||||
indexData2 []int
|
||||
indexData3 []int
|
||||
index []int
|
||||
}
|
||||
|
||||
type LocationInfo struct {
|
||||
Country string
|
||||
Region string
|
||||
City string
|
||||
Isp string
|
||||
}
|
||||
|
||||
// Find locationInfo by ip string
|
||||
// It will return err when ipstr is not a valid format
|
||||
func (loc *Locator) Find(ipstr string) (info *LocationInfo, err error) {
|
||||
ip := net.ParseIP(ipstr).To4()
|
||||
if ip == nil || ip.To4() == nil {
|
||||
err = ErrInvalidIp
|
||||
return
|
||||
}
|
||||
info = loc.FindByUint(binary.BigEndian.Uint32([]byte(ip)))
|
||||
return
|
||||
}
|
||||
|
||||
// Find locationInfo by uint32
|
||||
func (loc *Locator) FindByUint(ip uint32) (info *LocationInfo) {
|
||||
end := len(loc.indexData1) - 1
|
||||
if ip>>24 != 0xff {
|
||||
end = loc.index[(ip>>24)+1]
|
||||
}
|
||||
idx := loc.findIndexOffset(ip, loc.index[ip>>24], end)
|
||||
off := loc.indexData2[idx]
|
||||
return newLocationInfo(loc.textData[off : off+loc.indexData3[idx]])
|
||||
}
|
||||
|
||||
// binary search
|
||||
func (loc *Locator) findIndexOffset(ip uint32, start, end int) int {
|
||||
for start < end {
|
||||
mid := (start + end) / 2
|
||||
if ip > loc.indexData1[mid] {
|
||||
start = mid + 1
|
||||
} else {
|
||||
end = mid
|
||||
}
|
||||
}
|
||||
|
||||
if loc.indexData1[end] >= ip {
|
||||
return end
|
||||
}
|
||||
|
||||
return start
|
||||
}
|
||||
|
||||
func (loc *Locator) init(data []byte) {
|
||||
textoff := int(binary.BigEndian.Uint32(data[:4]))
|
||||
|
||||
loc.textData = data[textoff-1024:]
|
||||
|
||||
loc.index = make([]int, 256)
|
||||
for i := 0; i < 256; i++ {
|
||||
off := 4 + i*4
|
||||
loc.index[i] = int(binary.LittleEndian.Uint32(data[off : off+4]))
|
||||
}
|
||||
|
||||
nidx := (textoff - 4 - 1024 - 1024) / 8
|
||||
|
||||
loc.indexData1 = make([]uint32, nidx)
|
||||
loc.indexData2 = make([]int, nidx)
|
||||
loc.indexData3 = make([]int, nidx)
|
||||
|
||||
for i := 0; i < nidx; i++ {
|
||||
off := 4 + 1024 + i*8
|
||||
loc.indexData1[i] = binary.BigEndian.Uint32(data[off : off+4])
|
||||
loc.indexData2[i] = int(uint32(data[off+4]) | uint32(data[off+5])<<8 | uint32(data[off+6])<<16)
|
||||
loc.indexData3[i] = int(data[off+7])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func newLocationInfo(str []byte) *LocationInfo {
|
||||
var info *LocationInfo
|
||||
|
||||
fields := bytes.Split(str, []byte("\t"))
|
||||
switch len(fields) {
|
||||
case 4:
|
||||
// free version
|
||||
info = &LocationInfo{
|
||||
Country: string(fields[0]),
|
||||
Region: string(fields[1]),
|
||||
City: string(fields[2]),
|
||||
}
|
||||
case 5:
|
||||
// pay version
|
||||
info = &LocationInfo{
|
||||
Country: string(fields[0]),
|
||||
Region: string(fields[1]),
|
||||
City: string(fields[2]),
|
||||
Isp: string(fields[4]),
|
||||
}
|
||||
default:
|
||||
panic("unexpected ip info:" + string(str))
|
||||
}
|
||||
|
||||
if len(info.Country) == 0 {
|
||||
info.Country = Null
|
||||
}
|
||||
if len(info.Region) == 0 {
|
||||
info.Region = Null
|
||||
}
|
||||
if len(info.City) == 0 {
|
||||
info.City = Null
|
||||
}
|
||||
if len(info.Isp) == 0 {
|
||||
info.Isp = Null
|
||||
}
|
||||
return info
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
14
ldap/util.go
14
ldap/util.go
@@ -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:
|
||||
|
||||
11
main.go
11
main.go
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/casdoor/casdoor/proxy"
|
||||
"github.com/casdoor/casdoor/radius"
|
||||
"github.com/casdoor/casdoor/routers"
|
||||
"github.com/casdoor/casdoor/service"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
@@ -73,6 +74,12 @@ func main() {
|
||||
object.InitCasvisorConfig()
|
||||
object.InitCleanupTokens()
|
||||
|
||||
object.InitSiteMap()
|
||||
if len(object.SiteMap) != 0 {
|
||||
object.InitRuleMap()
|
||||
object.StartMonitorSitesLoop()
|
||||
}
|
||||
|
||||
util.SafeGoroutine(func() { object.RunSyncUsersJob() })
|
||||
util.SafeGoroutine(func() { controllers.InitCLIDownloader() })
|
||||
|
||||
@@ -126,5 +133,9 @@ func main() {
|
||||
go radius.StartRadiusServer()
|
||||
go object.ClearThroughputPerSecond()
|
||||
|
||||
if len(object.SiteMap) != 0 {
|
||||
service.Start()
|
||||
}
|
||||
|
||||
web.Run(fmt.Sprintf(":%v", port))
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -164,6 +165,8 @@ type Application struct {
|
||||
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) {
|
||||
@@ -656,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) {
|
||||
@@ -668,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 {
|
||||
@@ -716,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" {
|
||||
|
||||
@@ -16,10 +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 {
|
||||
@@ -346,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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 = ""
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
109
object/kerberos.go
Normal 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
|
||||
}
|
||||
@@ -453,20 +453,20 @@ func SyncLdapUsers(owner string, syncUsers []LdapUser, ldapId string) (existUser
|
||||
}
|
||||
tag := strings.Join(ou, ".")
|
||||
|
||||
for _, syncUser := range syncUsers {
|
||||
existUuids, err := GetExistUuids(owner, uuids)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
existUuids, err := GetExistUuids(owner, uuids)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
found := false
|
||||
if len(existUuids) > 0 {
|
||||
for _, existUuid := range existUuids {
|
||||
if syncUser.Uuid == existUuid {
|
||||
existUsers = append(existUsers, syncUser)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
existUuidSet := make(map[string]struct{}, len(existUuids))
|
||||
for _, uuid := range existUuids {
|
||||
existUuidSet[uuid] = struct{}{}
|
||||
}
|
||||
|
||||
for _, syncUser := range syncUsers {
|
||||
_, found := existUuidSet[syncUser.Uuid]
|
||||
if found {
|
||||
existUsers = append(existUsers, syncUser)
|
||||
}
|
||||
|
||||
if !found {
|
||||
@@ -713,11 +713,23 @@ func dnToGroupName(owner, dn string) string {
|
||||
func GetExistUuids(owner string, uuids []string) ([]string, error) {
|
||||
var existUuids []string
|
||||
|
||||
// PostgreSQL only supports up to 65535 parameters per query, so we batch the uuids
|
||||
const batchSize = 100
|
||||
tableNamePrefix := conf.GetConfigString("tableNamePrefix")
|
||||
err := ormer.Engine.Table(tableNamePrefix+"user").Where("owner = ?", owner).Cols("ldap").
|
||||
In("ldap", uuids).Select("DISTINCT ldap").Find(&existUuids)
|
||||
if err != nil {
|
||||
return existUuids, err
|
||||
for i := 0; i < len(uuids); i += batchSize {
|
||||
end := i + batchSize
|
||||
if end > len(uuids) {
|
||||
end = len(uuids)
|
||||
}
|
||||
batch := uuids[i:end]
|
||||
|
||||
var batchUuids []string
|
||||
err := ormer.Engine.Table(tableNamePrefix+"user").Where("owner = ?", owner).Cols("ldap").
|
||||
In("ldap", batch).Select("DISTINCT ldap").Find(&batchUuids)
|
||||
if err != nil {
|
||||
return existUuids, err
|
||||
}
|
||||
existUuids = append(existUuids, batchUuids...)
|
||||
}
|
||||
|
||||
return existUuids, nil
|
||||
@@ -750,7 +762,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
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -459,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
139
object/rule.go
Normal file
@@ -0,0 +1,139 @@
|
||||
// Copyright 2023 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/xorm-io/core"
|
||||
)
|
||||
|
||||
type Expression struct {
|
||||
Name string `json:"name"`
|
||||
Operator string `json:"operator"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type Rule struct {
|
||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
CreatedTime string `xorm:"varchar(100) notnull" json:"createdTime"`
|
||||
UpdatedTime string `xorm:"varchar(100) notnull" json:"updatedTime"`
|
||||
|
||||
Type string `xorm:"varchar(100) notnull" json:"type"`
|
||||
Expressions []*Expression `xorm:"mediumtext" json:"expressions"`
|
||||
Action string `xorm:"varchar(100) notnull" json:"action"`
|
||||
StatusCode int `xorm:"int notnull" json:"statusCode"`
|
||||
Reason string `xorm:"varchar(100) notnull" json:"reason"`
|
||||
IsVerbose bool `xorm:"bool" json:"isVerbose"`
|
||||
}
|
||||
|
||||
func GetGlobalRules() ([]*Rule, error) {
|
||||
rules := []*Rule{}
|
||||
err := ormer.Engine.Asc("owner").Desc("created_time").Find(&rules)
|
||||
return rules, err
|
||||
}
|
||||
|
||||
func GetRules(owner string) ([]*Rule, error) {
|
||||
rules := []*Rule{}
|
||||
err := ormer.Engine.Desc("updated_time").Find(&rules, &Rule{Owner: owner})
|
||||
return rules, err
|
||||
}
|
||||
|
||||
func getRule(owner string, name string) (*Rule, error) {
|
||||
rule := Rule{Owner: owner, Name: name}
|
||||
existed, err := ormer.Engine.Get(&rule)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existed {
|
||||
return &rule, nil
|
||||
} else {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetRule(id string) (*Rule, error) {
|
||||
owner, name := util.GetOwnerAndNameFromIdNoCheck(id)
|
||||
return getRule(owner, name)
|
||||
}
|
||||
|
||||
func UpdateRule(id string, rule *Rule) (bool, error) {
|
||||
owner, name := util.GetOwnerAndNameFromIdNoCheck(id)
|
||||
if s, err := getRule(owner, name); err != nil {
|
||||
return false, err
|
||||
} else if s == nil {
|
||||
return false, nil
|
||||
}
|
||||
rule.UpdatedTime = util.GetCurrentTime()
|
||||
_, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(rule)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
err = refreshRuleMap()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func AddRule(rule *Rule) (bool, error) {
|
||||
affected, err := ormer.Engine.Insert(rule)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if affected != 0 {
|
||||
err = refreshRuleMap()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func DeleteRule(rule *Rule) (bool, error) {
|
||||
affected, err := ormer.Engine.ID(core.PK{rule.Owner, rule.Name}).Delete(&Rule{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if affected != 0 {
|
||||
err = refreshRuleMap()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func (rule *Rule) GetId() string {
|
||||
return fmt.Sprintf("%s/%s", rule.Owner, rule.Name)
|
||||
}
|
||||
|
||||
func GetRuleCount(owner, field, value string) (int64, error) {
|
||||
session := GetSession(owner, -1, -1, field, value, "", "")
|
||||
return session.Count(&Rule{})
|
||||
}
|
||||
|
||||
func GetPaginationRules(owner string, offset, limit int, field, value, sortField, sortOrder string) ([]*Rule, error) {
|
||||
rules := []*Rule{}
|
||||
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
|
||||
err := session.Where("owner = ? or owner = ?", "admin", owner).Find(&rules)
|
||||
if err != nil {
|
||||
return rules, err
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
57
object/rule_cache.go
Normal file
57
object/rule_cache.go
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright 2023 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
var ruleMap = map[string]*Rule{}
|
||||
|
||||
func InitRuleMap() {
|
||||
err := refreshRuleMap()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func refreshRuleMap() error {
|
||||
newRuleMap := map[string]*Rule{}
|
||||
rules, err := GetGlobalRules()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, rule := range rules {
|
||||
newRuleMap[util.GetId(rule.Owner, rule.Name)] = rule
|
||||
}
|
||||
|
||||
ruleMap = newRuleMap
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetRulesByRuleIds(ids []string) ([]*Rule, error) {
|
||||
var res []*Rule
|
||||
for _, id := range ids {
|
||||
rule, ok := ruleMap[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("rule: %s not found", id)
|
||||
}
|
||||
res = append(res, rule)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
276
object/site.go
Normal file
276
object/site.go
Normal file
@@ -0,0 +1,276 @@
|
||||
// Copyright 2023 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/xorm-io/core"
|
||||
)
|
||||
|
||||
type NodeItem struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Diff string `json:"diff"`
|
||||
Pid int `json:"pid"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Provider string `json:"provider"`
|
||||
}
|
||||
|
||||
type Site struct {
|
||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
|
||||
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
|
||||
Tag string `xorm:"varchar(100)" json:"tag"`
|
||||
Domain string `xorm:"varchar(100)" json:"domain"`
|
||||
OtherDomains []string `xorm:"varchar(500)" json:"otherDomains"`
|
||||
NeedRedirect bool `json:"needRedirect"`
|
||||
DisableVerbose bool `json:"disableVerbose"`
|
||||
Rules []string `xorm:"varchar(500)" json:"rules"`
|
||||
EnableAlert bool `json:"enableAlert"`
|
||||
AlertInterval int `json:"alertInterval"`
|
||||
AlertTryTimes int `json:"alertTryTimes"`
|
||||
AlertProviders []string `xorm:"varchar(500)" json:"alertProviders"`
|
||||
Challenges []string `xorm:"mediumtext" json:"challenges"`
|
||||
Host string `xorm:"varchar(100)" json:"host"`
|
||||
Port int `json:"port"`
|
||||
Hosts []string `xorm:"varchar(1000)" json:"hosts"`
|
||||
SslMode string `xorm:"varchar(100)" json:"sslMode"`
|
||||
SslCert string `xorm:"-" json:"sslCert"`
|
||||
PublicIp string `xorm:"varchar(100)" json:"publicIp"`
|
||||
Node string `xorm:"varchar(100)" json:"node"`
|
||||
IsSelf bool `json:"isSelf"`
|
||||
Status string `xorm:"varchar(100)" json:"status"`
|
||||
Nodes []*NodeItem `xorm:"mediumtext" json:"nodes"`
|
||||
|
||||
CasdoorApplication string `xorm:"varchar(100)" json:"casdoorApplication"`
|
||||
ApplicationObj *Application `xorm:"-" json:"applicationObj"`
|
||||
}
|
||||
|
||||
func GetGlobalSites() ([]*Site, error) {
|
||||
sites := []*Site{}
|
||||
err := ormer.Engine.Desc("created_time").Find(&sites)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sites, nil
|
||||
}
|
||||
|
||||
func GetSites(owner string) ([]*Site, error) {
|
||||
sites := []*Site{}
|
||||
err := ormer.Engine.Asc("tag").Asc("port").Desc("created_time").Find(&sites, &Site{Owner: owner})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, site := range sites {
|
||||
err = site.populateCert()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return sites, nil
|
||||
}
|
||||
|
||||
func getSite(owner string, name string) (*Site, error) {
|
||||
site := Site{Owner: owner, Name: name}
|
||||
existed, err := ormer.Engine.Get(&site)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if existed {
|
||||
return &site, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func GetSite(id string) (*Site, error) {
|
||||
owner, name := util.GetOwnerAndNameFromIdNoCheck(id)
|
||||
site, err := getSite(owner, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if site != nil {
|
||||
err = site.populateCert()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return site, nil
|
||||
}
|
||||
|
||||
func GetMaskedSite(site *Site, node string) *Site {
|
||||
if site == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if site.PublicIp == "(empty)" {
|
||||
site.PublicIp = ""
|
||||
}
|
||||
|
||||
site.IsSelf = false
|
||||
if site.Node == node {
|
||||
site.IsSelf = true
|
||||
}
|
||||
|
||||
return site
|
||||
}
|
||||
|
||||
func GetMaskedSites(sites []*Site, node string) []*Site {
|
||||
for _, site := range sites {
|
||||
site = GetMaskedSite(site, node)
|
||||
}
|
||||
return sites
|
||||
}
|
||||
|
||||
func UpdateSite(id string, site *Site) (bool, error) {
|
||||
owner, name := util.GetOwnerAndNameFromIdNoCheck(id)
|
||||
if s, err := getSite(owner, name); err != nil {
|
||||
return false, err
|
||||
} else if s == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
site.UpdatedTime = util.GetCurrentTime()
|
||||
|
||||
_, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(site)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = refreshSiteMap()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func UpdateSiteNoRefresh(id string, site *Site) (bool, error) {
|
||||
owner, name := util.GetOwnerAndNameFromIdNoCheck(id)
|
||||
if s, err := getSite(owner, name); err != nil {
|
||||
return false, err
|
||||
} else if s == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
_, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(site)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func AddSite(site *Site) (bool, error) {
|
||||
affected, err := ormer.Engine.Insert(site)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if affected != 0 {
|
||||
err = refreshSiteMap()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func DeleteSite(site *Site) (bool, error) {
|
||||
affected, err := ormer.Engine.ID(core.PK{site.Owner, site.Name}).Delete(&Site{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if affected != 0 {
|
||||
err = refreshSiteMap()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func (site *Site) GetId() string {
|
||||
return fmt.Sprintf("%s/%s", site.Owner, site.Name)
|
||||
}
|
||||
|
||||
func (site *Site) GetChallengeMap() map[string]string {
|
||||
m := map[string]string{}
|
||||
for _, challenge := range site.Challenges {
|
||||
tokens := strings.Split(challenge, ":")
|
||||
m[tokens[0]] = tokens[1]
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (site *Site) GetHost() string {
|
||||
if len(site.Hosts) != 0 {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
return site.Hosts[rand.Intn(len(site.Hosts))]
|
||||
}
|
||||
|
||||
if site.Host != "" {
|
||||
return site.Host
|
||||
}
|
||||
|
||||
if site.Port == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
res := fmt.Sprintf("http://localhost:%d", site.Port)
|
||||
return res
|
||||
}
|
||||
|
||||
func addErrorToMsg(msg string, function string, err error) string {
|
||||
fmt.Printf("%s(): %s\n", function, err.Error())
|
||||
if msg == "" {
|
||||
return fmt.Sprintf("%s(): %s", function, err.Error())
|
||||
} else {
|
||||
return fmt.Sprintf("%s || %s(): %s", msg, function, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func GetSiteCount(owner, field, value string) (int64, error) {
|
||||
session := GetSession(owner, -1, -1, field, value, "", "")
|
||||
return session.Count(&Site{})
|
||||
}
|
||||
|
||||
func GetPaginationSites(owner string, offset, limit int, field, value, sortField, sortOrder string) ([]*Site, error) {
|
||||
sites := []*Site{}
|
||||
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
|
||||
err := session.Where("owner = ? or owner = ?", "admin", owner).Find(&sites)
|
||||
if err != nil {
|
||||
return sites, err
|
||||
}
|
||||
|
||||
return sites, nil
|
||||
}
|
||||
133
object/site_cache.go
Normal file
133
object/site_cache.go
Normal file
@@ -0,0 +1,133 @@
|
||||
// Copyright 2023 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
var (
|
||||
SiteMap = map[string]*Site{}
|
||||
certMap = map[string]*Cert{}
|
||||
healthCheckNeededDomains []string
|
||||
)
|
||||
|
||||
func InitSiteMap() {
|
||||
err := refreshSiteMap()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func getCasdoorCertMap() (map[string]*Cert, error) {
|
||||
certs, err := GetCerts("")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetCerts() error: %s", err.Error())
|
||||
}
|
||||
|
||||
res := map[string]*Cert{}
|
||||
for _, cert := range certs {
|
||||
res[cert.Name] = cert
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func getCasdoorApplicationMap() (map[string]*Application, error) {
|
||||
casdoorCertMap, err := getCasdoorCertMap()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
applications, err := GetApplications("")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetOrganizationApplications() error: %s", err.Error())
|
||||
}
|
||||
|
||||
res := map[string]*Application{}
|
||||
for _, application := range applications {
|
||||
if application.Cert != "" {
|
||||
if cert, ok := casdoorCertMap[application.Cert]; ok {
|
||||
application.CertObj = cert
|
||||
}
|
||||
}
|
||||
|
||||
res[application.Name] = application
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func refreshSiteMap() error {
|
||||
applicationMap, err := getCasdoorApplicationMap()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
newSiteMap := map[string]*Site{}
|
||||
newHealthCheckNeededDomains := make([]string, 0)
|
||||
sites, err := GetGlobalSites()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
certMap, err = getCertMap()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, site := range sites {
|
||||
if applicationMap != nil {
|
||||
if site.CasdoorApplication != "" && site.ApplicationObj == nil {
|
||||
if v, ok2 := applicationMap[site.CasdoorApplication]; ok2 {
|
||||
site.ApplicationObj = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if site.Domain != "" && site.PublicIp == "" {
|
||||
go func(site *Site) {
|
||||
site.PublicIp = util.ResolveDomainToIp(site.Domain)
|
||||
_, err2 := UpdateSiteNoRefresh(site.GetId(), site)
|
||||
if err2 != nil {
|
||||
fmt.Printf("UpdateSiteNoRefresh() error: %v\n", err2)
|
||||
}
|
||||
}(site)
|
||||
}
|
||||
|
||||
newSiteMap[strings.ToLower(site.Domain)] = site
|
||||
if !shouldStopHealthCheck(site) {
|
||||
newHealthCheckNeededDomains = append(newHealthCheckNeededDomains, strings.ToLower(site.Domain))
|
||||
}
|
||||
for _, domain := range site.OtherDomains {
|
||||
if domain != "" {
|
||||
newSiteMap[strings.ToLower(domain)] = site
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SiteMap = newSiteMap
|
||||
healthCheckNeededDomains = newHealthCheckNeededDomains
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetSiteByDomain(domain string) *Site {
|
||||
if site, ok := SiteMap[strings.ToLower(domain)]; ok {
|
||||
return site
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
215
object/site_cert.go
Normal file
215
object/site_cert.go
Normal file
@@ -0,0 +1,215 @@
|
||||
// Copyright 2023 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
func (site *Site) populateCert() error {
|
||||
if site.Domain == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
cert, err := GetCertByDomain(site.Domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cert == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
site.SslCert = cert.Name
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkUrlToken(url string, keyAuth string) (bool, error) {
|
||||
client := &http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if strings.TrimSpace(string(body)) == keyAuth {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("checkUrlToken() error, response mismatch: expected %q, got %q", keyAuth, body)
|
||||
}
|
||||
|
||||
func (site *Site) preCheckCertForDomain(domain string) (bool, error) {
|
||||
token, keyAuth, err := util.GenerateTwoUniqueRandomStrings()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
site.Challenges = []string{fmt.Sprintf("%s:%s", token, keyAuth)}
|
||||
|
||||
_, err = UpdateSiteNoRefresh(site.GetId(), site)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = refreshSiteMap()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("http://%s/.well-known/acme-challenge/%s", domain, token)
|
||||
var ok bool
|
||||
for i := 0; i < 10; i++ {
|
||||
fmt.Printf("checkUrlToken(): try time: %d\n", i+1)
|
||||
ok, err = checkUrlToken(url, keyAuth)
|
||||
if err != nil {
|
||||
fmt.Printf("preCheckCertForDomain() error: %v\n", err)
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
if ok {
|
||||
fmt.Printf("checkUrlToken(): try time: %d, succeed!\n", i+1)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
site.Challenges = []string{}
|
||||
_, err = UpdateSiteNoRefresh(site.GetId(), site)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = refreshSiteMap()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
func (site *Site) updateCertForDomain(domain string) error {
|
||||
ok, err := site.preCheckCertForDomain(domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
fmt.Printf("preCheckCertForDomain(): not ok for domain: %s\n", domain)
|
||||
return nil
|
||||
}
|
||||
|
||||
certificate, privateKey, err := getHttp01Cert(site.GetId(), domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expireTime, err := util.GetCertExpireTime(certificate)
|
||||
if err != nil {
|
||||
fmt.Printf("getCertExpireTime() error: %v\n", err)
|
||||
}
|
||||
|
||||
domainExpireTime, err := getDomainExpireTime(domain)
|
||||
if err != nil {
|
||||
fmt.Printf("getDomainExpireTime() error: %v\n", err)
|
||||
}
|
||||
|
||||
cert := Cert{
|
||||
Owner: site.Owner,
|
||||
Name: domain,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
DisplayName: domain,
|
||||
Type: "SSL",
|
||||
CryptoAlgorithm: "RSA",
|
||||
ExpireTime: expireTime,
|
||||
DomainExpireTime: domainExpireTime,
|
||||
Provider: "",
|
||||
Account: "",
|
||||
AccessKey: "",
|
||||
AccessSecret: "",
|
||||
Certificate: certificate,
|
||||
PrivateKey: privateKey,
|
||||
}
|
||||
|
||||
_, err = DeleteCert(&cert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = AddCert(&cert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = refreshSiteMap()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (site *Site) checkCerts() error {
|
||||
domains := []string{}
|
||||
if site.Domain != "" {
|
||||
domains = append(domains, site.Domain)
|
||||
}
|
||||
|
||||
for _, domain := range site.OtherDomains {
|
||||
domains = append(domains, domain)
|
||||
}
|
||||
|
||||
for _, domain := range domains {
|
||||
if site.Owner == "admin" || strings.HasSuffix(domain, ".casdoor.com") {
|
||||
continue
|
||||
}
|
||||
|
||||
cert, err := GetCertByDomain(domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cert != nil {
|
||||
var nearExpire bool
|
||||
nearExpire, err = cert.isCertNearExpire()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !nearExpire {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
err = site.updateCertForDomain(domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
90
object/site_cert_http.go
Normal file
90
object/site_cert_http.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// Copyright 2023 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/casbin/lego/v4/certificate"
|
||||
)
|
||||
|
||||
type HttpProvider struct {
|
||||
siteId string
|
||||
}
|
||||
|
||||
func (p *HttpProvider) Present(domain string, token string, keyAuth string) error {
|
||||
site, err := GetSite(p.siteId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
site.Challenges = []string{fmt.Sprintf("%s:%s", token, keyAuth)}
|
||||
_, err = UpdateSiteNoRefresh(site.GetId(), site)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = refreshSiteMap()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *HttpProvider) CleanUp(domain string, token string, keyAuth string) error {
|
||||
site, err := GetSite(p.siteId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
site.Challenges = []string{}
|
||||
_, err = UpdateSiteNoRefresh(site.GetId(), site)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = refreshSiteMap()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getHttp01Cert(siteId string, domain string) (string, string, error) {
|
||||
client, err := GetAcmeClient(false)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
provider := HttpProvider{siteId: siteId}
|
||||
err = client.Challenge.SetHTTP01Provider(&provider)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
request := certificate.ObtainRequest{
|
||||
Domains: []string{domain},
|
||||
Bundle: true,
|
||||
}
|
||||
|
||||
resource, err := client.Certificate.Obtain(request)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return string(resource.Certificate), string(resource.PrivateKey), nil
|
||||
}
|
||||
87
object/site_timer.go
Normal file
87
object/site_timer.go
Normal file
@@ -0,0 +1,87 @@
|
||||
// Copyright 2023 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
var (
|
||||
siteUpdateMap = map[string]string{}
|
||||
lock = &sync.Mutex{}
|
||||
)
|
||||
|
||||
func monitorSiteCerts() error {
|
||||
sites, err := GetGlobalSites()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, site := range sites {
|
||||
//updatedTime, ok := siteUpdateMap[site.GetId()]
|
||||
//if ok && updatedTime != "" && updatedTime == site.UpdatedTime {
|
||||
// continue
|
||||
//}
|
||||
|
||||
lock.Lock()
|
||||
err = site.checkCerts()
|
||||
lock.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
siteUpdateMap[site.GetId()] = site.UpdatedTime
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func StartMonitorSitesLoop() {
|
||||
fmt.Printf("StartMonitorSitesLoop() Start!\n\n")
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Printf("[%s] Recovered from StartMonitorSitesLoop() panic: %v\n", util.GetCurrentTime(), r)
|
||||
StartMonitorSitesLoop()
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
err := refreshSiteMap()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
|
||||
err = refreshRuleMap()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
|
||||
err = monitorSiteCerts()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
}()
|
||||
}
|
||||
21
object/site_timer_health.go
Normal file
21
object/site_timer_health.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright 2023 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
var healthCheckTryTimesMap = map[string]int{}
|
||||
|
||||
func shouldStopHealthCheck(site *Site) bool {
|
||||
return site == nil || !site.EnableAlert || site.Domain == "" || site.Status == "Inactive"
|
||||
}
|
||||
@@ -32,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)
|
||||
}
|
||||
@@ -52,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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -195,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{
|
||||
@@ -520,26 +531,79 @@ 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.
|
||||
// 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 {
|
||||
if len(application.Scopes) == 0 || scope == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
allowed := make(map[string]bool, len(application.Scopes))
|
||||
for _, s := range application.Scopes {
|
||||
allowed[s.Name] = true
|
||||
}
|
||||
|
||||
for _, s := range strings.Fields(scope) {
|
||||
if !allowed[s] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
_, ok := IsScopeValidAndExpand(scope, application)
|
||||
return ok
|
||||
}
|
||||
|
||||
// createGuestUserToken creates a new guest user and returns a token for them
|
||||
@@ -778,12 +842,14 @@ func GetAuthorizationCodeToken(application *Application, clientSecret string, co
|
||||
// GetPasswordToken
|
||||
// Resource Owner Password Credentials flow
|
||||
func GetPasswordToken(application *Application, username string, password string, scope string, host string) (*Token, *TokenError, error) {
|
||||
if !IsScopeValid(scope, application) {
|
||||
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 {
|
||||
@@ -866,12 +932,14 @@ func GetClientCredentialsToken(application *Application, clientSecret string, sc
|
||||
ErrorDescription: "client_secret is invalid",
|
||||
}, nil
|
||||
}
|
||||
if !IsScopeValid(scope, application) {
|
||||
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(),
|
||||
@@ -911,12 +979,14 @@ func GetClientCredentialsToken(application *Application, clientSecret string, sc
|
||||
// GetImplicitToken
|
||||
// Implicit flow
|
||||
func GetImplicitToken(application *Application, username string, scope string, nonce string, host string) (*Token, *TokenError, error) {
|
||||
if !IsScopeValid(scope, application) {
|
||||
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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -17,6 +17,7 @@ package object
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
@@ -987,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") != "" {
|
||||
@@ -1013,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 == "" {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -15,12 +15,15 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
@@ -34,7 +37,16 @@ type VerifyResult struct {
|
||||
Msg string
|
||||
}
|
||||
|
||||
var ResetLinkReg *regexp.Regexp
|
||||
type verifyCodeErrorInfo struct {
|
||||
wrongTimes int
|
||||
lastWrongTime time.Time
|
||||
}
|
||||
|
||||
var (
|
||||
ResetLinkReg *regexp.Regexp
|
||||
verifyCodeErrorMap = map[string]*verifyCodeErrorInfo{}
|
||||
verifyCodeErrorMapLock sync.Mutex
|
||||
)
|
||||
|
||||
const (
|
||||
VerificationSuccess = iota
|
||||
@@ -325,13 +337,114 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
// getVerifyCodeErrorKey builds the in-memory key for verify-code failed attempt tracking
|
||||
func getVerifyCodeErrorKey(user *User, dest string) string {
|
||||
if user == nil {
|
||||
return dest
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s:%s", user.GetId(), dest)
|
||||
}
|
||||
|
||||
func checkVerifyCodeErrorTimes(user *User, dest, lang string) error {
|
||||
failedSigninLimit, failedSigninFrozenTime, err := GetFailedSigninConfigByUser(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key := getVerifyCodeErrorKey(user, dest)
|
||||
|
||||
verifyCodeErrorMapLock.Lock()
|
||||
defer verifyCodeErrorMapLock.Unlock()
|
||||
|
||||
errorInfo, ok := verifyCodeErrorMap[key]
|
||||
if !ok || errorInfo == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if errorInfo.wrongTimes < failedSigninLimit {
|
||||
return nil
|
||||
}
|
||||
|
||||
minutes := failedSigninFrozenTime - int(time.Now().UTC().Sub(errorInfo.lastWrongTime).Minutes())
|
||||
if minutes > 0 {
|
||||
return fmt.Errorf(i18n.Translate(lang, "check:You have entered the wrong password or code too many times, please wait for %d minutes and try again"), minutes)
|
||||
}
|
||||
|
||||
delete(verifyCodeErrorMap, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func recordVerifyCodeErrorInfo(user *User, dest, lang string) error {
|
||||
failedSigninLimit, failedSigninFrozenTime, err := GetFailedSigninConfigByUser(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key := getVerifyCodeErrorKey(user, dest)
|
||||
|
||||
verifyCodeErrorMapLock.Lock()
|
||||
defer verifyCodeErrorMapLock.Unlock()
|
||||
|
||||
errorInfo, ok := verifyCodeErrorMap[key]
|
||||
if !ok || errorInfo == nil {
|
||||
errorInfo = &verifyCodeErrorInfo{}
|
||||
verifyCodeErrorMap[key] = errorInfo
|
||||
}
|
||||
|
||||
if errorInfo.wrongTimes < failedSigninLimit {
|
||||
errorInfo.wrongTimes++
|
||||
}
|
||||
|
||||
if errorInfo.wrongTimes >= failedSigninLimit {
|
||||
errorInfo.lastWrongTime = time.Now().UTC()
|
||||
}
|
||||
|
||||
leftChances := failedSigninLimit - errorInfo.wrongTimes
|
||||
if leftChances >= 0 {
|
||||
return fmt.Errorf(i18n.Translate(lang, "check:password or code is incorrect, you have %s remaining chances"), strconv.Itoa(leftChances))
|
||||
}
|
||||
|
||||
return fmt.Errorf(i18n.Translate(lang, "check:You have entered the wrong password or code too many times, please wait for %d minutes and try again"), failedSigninFrozenTime)
|
||||
}
|
||||
|
||||
func resetVerifyCodeErrorTimes(user *User, dest string) {
|
||||
key := getVerifyCodeErrorKey(user, dest)
|
||||
|
||||
verifyCodeErrorMapLock.Lock()
|
||||
defer verifyCodeErrorMapLock.Unlock()
|
||||
|
||||
delete(verifyCodeErrorMap, key)
|
||||
}
|
||||
|
||||
func CheckVerifyCodeWithLimit(user *User, dest, code, lang string) error {
|
||||
if err := checkVerifyCodeErrorTimes(user, dest, lang); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := CheckVerificationCode(dest, code, lang)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch result.Code {
|
||||
case VerificationSuccess:
|
||||
resetVerifyCodeErrorTimes(user, dest)
|
||||
return nil
|
||||
case wrongCodeError:
|
||||
return recordVerifyCodeErrorInfo(user, dest, lang)
|
||||
default:
|
||||
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 +461,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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
228
routers/lightweight_auth_filter.go
Normal file
228
routers/lightweight_auth_filter.go
Normal 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
|
||||
}
|
||||
@@ -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,6 +127,19 @@ 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")
|
||||
|
||||
@@ -16,6 +16,7 @@ package routers
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -110,7 +111,7 @@ func fastAutoSignin(ctx *context.Context) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if code.Message != "" {
|
||||
return "", fmt.Errorf(code.Message)
|
||||
return "", errors.New(code.Message)
|
||||
}
|
||||
|
||||
sep := "?"
|
||||
@@ -131,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
|
||||
}
|
||||
@@ -149,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
112
rule/rule.go
Normal file
@@ -0,0 +1,112 @@
|
||||
// Copyright 2024 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
type Rule interface {
|
||||
checkRule(expressions []*object.Expression, req *http.Request) (*RuleResult, error)
|
||||
}
|
||||
|
||||
type RuleResult struct {
|
||||
Action string
|
||||
StatusCode int
|
||||
Reason string
|
||||
}
|
||||
|
||||
func CheckRules(ruleIds []string, r *http.Request) (*RuleResult, error) {
|
||||
rules, err := object.GetRulesByRuleIds(ruleIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, rule := range rules {
|
||||
var ruleObj Rule
|
||||
switch rule.Type {
|
||||
case "User-Agent":
|
||||
ruleObj = &UaRule{}
|
||||
case "IP":
|
||||
ruleObj = &IpRule{}
|
||||
case "WAF":
|
||||
ruleObj = &WafRule{}
|
||||
case "IP Rate Limiting":
|
||||
ruleObj = &IpRateRule{
|
||||
ruleName: rule.GetId(),
|
||||
}
|
||||
case "Compound":
|
||||
ruleObj = &CompoundRule{}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown rule type: %s for rule: %s", rule.Type, rule.GetId())
|
||||
}
|
||||
|
||||
result, err := ruleObj.checkRule(rule.Expressions, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if result != nil {
|
||||
// Use rule's action if no action specified by the rule check
|
||||
if result.Action == "" {
|
||||
result.Action = rule.Action
|
||||
}
|
||||
|
||||
// Determine status code
|
||||
if result.StatusCode == 0 {
|
||||
if rule.StatusCode != 0 {
|
||||
result.StatusCode = rule.StatusCode
|
||||
} else {
|
||||
// Set default status codes if not specified
|
||||
switch result.Action {
|
||||
case "Block":
|
||||
result.StatusCode = 403
|
||||
case "Drop":
|
||||
result.StatusCode = 400
|
||||
case "Allow":
|
||||
result.StatusCode = 200
|
||||
case "CAPTCHA":
|
||||
result.StatusCode = 302
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown rule action: %s for rule: %s", result.Action, rule.GetId())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update reason if rule has custom reason
|
||||
if result.Action == "Block" || result.Action == "Drop" {
|
||||
if rule.IsVerbose {
|
||||
// Add verbose debug info with rule name and triggered expression
|
||||
result.Reason = util.GenerateVerboseReason(rule.GetId(), result.Reason, rule.Reason)
|
||||
} else if rule.Reason != "" {
|
||||
result.Reason = rule.Reason
|
||||
} else if result.Reason != "" {
|
||||
result.Reason = fmt.Sprintf("hit rule %s: %s", ruleIds[i], result.Reason)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Default action if no rule matched
|
||||
return &RuleResult{
|
||||
Action: "Allow",
|
||||
StatusCode: 200,
|
||||
}, nil
|
||||
}
|
||||
60
rule/rule_compound.go
Normal file
60
rule/rule_compound.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright 2024 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
type CompoundRule struct{}
|
||||
|
||||
func (r *CompoundRule) checkRule(expressions []*object.Expression, req *http.Request) (*RuleResult, error) {
|
||||
operators := util.NewStack()
|
||||
res := true
|
||||
for _, expression := range expressions {
|
||||
isHit := true
|
||||
result, err := CheckRules([]string{expression.Value}, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result == nil || result.Action == "" {
|
||||
isHit = false
|
||||
}
|
||||
switch expression.Operator {
|
||||
case "and", "begin":
|
||||
res = res && isHit
|
||||
case "or":
|
||||
operators.Push(res)
|
||||
res = isHit
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown operator: %s", expression.Operator)
|
||||
}
|
||||
if operators.Size() > 0 {
|
||||
last, ok := operators.Pop()
|
||||
for ok {
|
||||
res = last.(bool) || res
|
||||
last, ok = operators.Pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
if res {
|
||||
return &RuleResult{}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
94
rule/rule_ip.go
Normal file
94
rule/rule_ip.go
Normal file
@@ -0,0 +1,94 @@
|
||||
// Copyright 2024 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/ip"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
type IpRule struct{}
|
||||
|
||||
func (r *IpRule) checkRule(expressions []*object.Expression, req *http.Request) (*RuleResult, error) {
|
||||
clientIp := util.GetClientIp(req)
|
||||
netIp, err := parseIp(clientIp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, expression := range expressions {
|
||||
reason := fmt.Sprintf("expression matched: \"%s %s %s\"", clientIp, expression.Operator, expression.Value)
|
||||
|
||||
// Handle "is abroad" operator
|
||||
if expression.Operator == "is abroad" {
|
||||
if ip.IsAbroadIp(clientIp) {
|
||||
return &RuleResult{Reason: reason}, nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
ips := strings.Split(expression.Value, ",")
|
||||
for _, ipStr := range ips {
|
||||
if strings.Contains(ipStr, "/") {
|
||||
_, ipNet, err := net.ParseCIDR(ipStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch expression.Operator {
|
||||
case "is in":
|
||||
if ipNet.Contains(netIp) {
|
||||
return &RuleResult{Reason: reason}, nil
|
||||
}
|
||||
case "is not in":
|
||||
if !ipNet.Contains(netIp) {
|
||||
return &RuleResult{Reason: reason}, nil
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown operator: %s", expression.Operator)
|
||||
}
|
||||
} else if strings.ContainsAny(ipStr, ".:") {
|
||||
switch expression.Operator {
|
||||
case "is in":
|
||||
if ipStr == clientIp {
|
||||
return &RuleResult{Reason: reason}, nil
|
||||
}
|
||||
case "is not in":
|
||||
if ipStr != clientIp {
|
||||
return &RuleResult{Reason: reason}, nil
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown operator: %s", expression.Operator)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("unknown IP or CIDR format: %s", ipStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func parseIp(ipStr string) (net.IP, error) {
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
return nil, fmt.Errorf("unknown IP or CIDR format: %s", ipStr)
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
134
rule/rule_ip_rate.go
Normal file
134
rule/rule_ip_rate.go
Normal file
@@ -0,0 +1,134 @@
|
||||
// Copyright 2024 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rule
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
type IpRateRule struct {
|
||||
ruleName string
|
||||
}
|
||||
|
||||
type IpRateLimiter struct {
|
||||
ips map[string]*rate.Limiter
|
||||
mu *sync.RWMutex
|
||||
r rate.Limit
|
||||
b int
|
||||
}
|
||||
|
||||
var blackList = map[string]map[string]time.Time{}
|
||||
|
||||
var ipRateLimiters = map[string]*IpRateLimiter{}
|
||||
|
||||
// NewIpRateLimiter .
|
||||
func NewIpRateLimiter(r rate.Limit, b int) *IpRateLimiter {
|
||||
i := &IpRateLimiter{
|
||||
ips: make(map[string]*rate.Limiter),
|
||||
mu: &sync.RWMutex{},
|
||||
r: r,
|
||||
b: b,
|
||||
}
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
// AddIP creates a new rate limiter and adds it to the ips map,
|
||||
// using the IP address as the key
|
||||
func (i *IpRateLimiter) AddIP(ip string) *rate.Limiter {
|
||||
i.mu.Lock()
|
||||
defer i.mu.Unlock()
|
||||
|
||||
limiter := rate.NewLimiter(i.r, i.b)
|
||||
|
||||
i.ips[ip] = limiter
|
||||
|
||||
return limiter
|
||||
}
|
||||
|
||||
// GetLimiter returns the rate limiter for the provided IP address if it exists.
|
||||
// Otherwise, calls AddIP to add IP address to the map
|
||||
func (i *IpRateLimiter) GetLimiter(ip string) *rate.Limiter {
|
||||
i.mu.Lock()
|
||||
limiter, exists := i.ips[ip]
|
||||
|
||||
if !exists {
|
||||
i.mu.Unlock()
|
||||
return i.AddIP(ip)
|
||||
}
|
||||
|
||||
i.mu.Unlock()
|
||||
|
||||
return limiter
|
||||
}
|
||||
|
||||
func (r *IpRateRule) checkRule(expressions []*object.Expression, req *http.Request) (*RuleResult, error) {
|
||||
expression := expressions[0] // IpRate rule should have only one expression
|
||||
clientIp := util.GetClientIp(req)
|
||||
|
||||
// If the client IP is in the blacklist, check the block time
|
||||
createAt, ok := blackList[r.ruleName][clientIp]
|
||||
if ok {
|
||||
blockTime := util.ParseInt(expression.Value)
|
||||
if time.Now().Sub(createAt) < time.Duration(blockTime)*time.Second {
|
||||
return &RuleResult{
|
||||
Action: "Block",
|
||||
Reason: "Rate limit exceeded",
|
||||
}, nil
|
||||
} else {
|
||||
delete(blackList[r.ruleName], clientIp)
|
||||
}
|
||||
}
|
||||
|
||||
// If the client IP is not in the blacklist, check the rate limit
|
||||
ipRateLimiter := ipRateLimiters[r.ruleName]
|
||||
parseInt := util.ParseInt(expression.Operator)
|
||||
if ipRateLimiter == nil {
|
||||
ipRateLimiter = NewIpRateLimiter(rate.Limit(parseInt), parseInt)
|
||||
ipRateLimiters[r.ruleName] = ipRateLimiter
|
||||
}
|
||||
|
||||
// If the rate limit has changed, update the rate limiter
|
||||
limiter := ipRateLimiter.GetLimiter(clientIp)
|
||||
if ipRateLimiter.r != rate.Limit(parseInt) {
|
||||
ipRateLimiter.r = rate.Limit(parseInt)
|
||||
ipRateLimiter.b = parseInt
|
||||
limiter.SetLimit(ipRateLimiter.r)
|
||||
limiter.SetBurst(ipRateLimiter.b)
|
||||
err := limiter.Wait(req.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// If the rate limit is exceeded, add the client IP to the blacklist
|
||||
allow := limiter.Allow()
|
||||
if !allow {
|
||||
blackList[r.ruleName] = map[string]time.Time{}
|
||||
blackList[r.ruleName][clientIp] = time.Now()
|
||||
return &RuleResult{
|
||||
Action: "Block",
|
||||
Reason: "Rate limit exceeded",
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
154
rule/rule_ip_rate_test.go
Normal file
154
rule/rule_ip_rate_test.go
Normal file
@@ -0,0 +1,154 @@
|
||||
// Copyright 2023 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rule
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
)
|
||||
|
||||
func TestIpRateRule_checkRule(t *testing.T) {
|
||||
type fields struct {
|
||||
ruleName string
|
||||
}
|
||||
type args struct {
|
||||
args []struct {
|
||||
expressions []*object.Expression
|
||||
req *http.Request
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want []bool
|
||||
want1 []string
|
||||
want2 []string
|
||||
wantErr []bool
|
||||
}{
|
||||
{
|
||||
name: "Test 1",
|
||||
fields: fields{
|
||||
ruleName: "rule1",
|
||||
},
|
||||
args: args{
|
||||
args: []struct {
|
||||
expressions []*object.Expression
|
||||
req *http.Request
|
||||
}{
|
||||
{
|
||||
expressions: []*object.Expression{
|
||||
{
|
||||
Operator: "1",
|
||||
Value: "1",
|
||||
},
|
||||
},
|
||||
req: &http.Request{
|
||||
RemoteAddr: "127.0.0.1",
|
||||
},
|
||||
},
|
||||
{
|
||||
expressions: []*object.Expression{
|
||||
{
|
||||
Operator: "1",
|
||||
Value: "1",
|
||||
},
|
||||
},
|
||||
req: &http.Request{
|
||||
RemoteAddr: "127.0.0.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []bool{false, true},
|
||||
want1: []string{"", "Block"},
|
||||
want2: []string{"", "Rate limit exceeded"},
|
||||
wantErr: []bool{false, false},
|
||||
},
|
||||
{
|
||||
name: "Test 2",
|
||||
fields: fields{
|
||||
ruleName: "rule2",
|
||||
},
|
||||
args: args{
|
||||
args: []struct {
|
||||
expressions []*object.Expression
|
||||
req *http.Request
|
||||
}{
|
||||
{
|
||||
expressions: []*object.Expression{
|
||||
{
|
||||
Operator: "1",
|
||||
Value: "1",
|
||||
},
|
||||
},
|
||||
req: &http.Request{
|
||||
RemoteAddr: "127.0.0.1",
|
||||
},
|
||||
},
|
||||
{
|
||||
expressions: []*object.Expression{
|
||||
{
|
||||
Operator: "10",
|
||||
Value: "1",
|
||||
},
|
||||
},
|
||||
req: &http.Request{
|
||||
RemoteAddr: "127.0.0.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []bool{false, false},
|
||||
want1: []string{"", ""},
|
||||
want2: []string{"", ""},
|
||||
wantErr: []bool{false, false},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &IpRateRule{
|
||||
ruleName: tt.fields.ruleName,
|
||||
}
|
||||
for i, arg := range tt.args.args {
|
||||
result, err := r.checkRule(arg.expressions, arg.req)
|
||||
if (err != nil) != tt.wantErr[i] {
|
||||
t.Errorf("checkRule() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
got := result != nil
|
||||
got1 := ""
|
||||
got2 := ""
|
||||
if result != nil {
|
||||
got1 = result.Action
|
||||
got2 = result.Reason
|
||||
}
|
||||
if got != tt.want[i] {
|
||||
t.Errorf("checkRule() got = %v, want %v", got, tt.want[i])
|
||||
}
|
||||
if got1 != tt.want1[i] {
|
||||
t.Errorf("checkRule() got1 = %v, want %v", got1, tt.want1[i])
|
||||
}
|
||||
if got2 != tt.want2[i] {
|
||||
t.Errorf("checkRule() got2 = %v, want %v", got2, tt.want2[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
63
rule/rule_ua.go
Normal file
63
rule/rule_ua.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright 2024 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
)
|
||||
|
||||
type UaRule struct{}
|
||||
|
||||
func (r *UaRule) checkRule(expressions []*object.Expression, req *http.Request) (*RuleResult, error) {
|
||||
userAgent := req.UserAgent()
|
||||
for _, expression := range expressions {
|
||||
ua := expression.Value
|
||||
reason := fmt.Sprintf("expression matched: \"%s %s %s\"", userAgent, expression.Operator, expression.Value)
|
||||
switch expression.Operator {
|
||||
case "contains":
|
||||
if strings.Contains(userAgent, ua) {
|
||||
return &RuleResult{Reason: reason}, nil
|
||||
}
|
||||
case "does not contain":
|
||||
if !strings.Contains(userAgent, ua) {
|
||||
return &RuleResult{Reason: reason}, nil
|
||||
}
|
||||
case "equals":
|
||||
if userAgent == ua {
|
||||
return &RuleResult{Reason: reason}, nil
|
||||
}
|
||||
case "does not equal":
|
||||
if strings.Compare(userAgent, ua) != 0 {
|
||||
return &RuleResult{Reason: reason}, nil
|
||||
}
|
||||
case "match":
|
||||
// regex match
|
||||
isHit, err := regexp.MatchString(ua, userAgent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isHit {
|
||||
return &RuleResult{Reason: reason}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
105
rule/rule_waf.go
Normal file
105
rule/rule_waf.go
Normal file
@@ -0,0 +1,105 @@
|
||||
// Copyright 2024 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/corazawaf/coraza/v3"
|
||||
"github.com/corazawaf/coraza/v3/types"
|
||||
"github.com/hsluoyz/modsecurity-go/seclang/parser"
|
||||
)
|
||||
|
||||
type WafRule struct{}
|
||||
|
||||
func (r *WafRule) checkRule(expressions []*object.Expression, req *http.Request) (*RuleResult, error) {
|
||||
var ruleStr string
|
||||
for _, expression := range expressions {
|
||||
ruleStr += expression.Value
|
||||
}
|
||||
waf, err := coraza.NewWAF(
|
||||
coraza.NewWAFConfig().
|
||||
WithErrorCallback(logError).
|
||||
WithDirectives(conf.WafConf).
|
||||
WithDirectives(ruleStr),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create WAF failed")
|
||||
}
|
||||
tx := waf.NewTransaction()
|
||||
processRequest(tx, req)
|
||||
matchedRules := tx.MatchedRules()
|
||||
for _, matchedRule := range matchedRules {
|
||||
rule := matchedRule.Rule()
|
||||
directive, err := parser.NewSecLangScannerFromString(rule.Raw()).AllDirective()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, d := range directive {
|
||||
ruleDirective := d.(*parser.RuleDirective)
|
||||
for _, action := range ruleDirective.Actions.Action {
|
||||
switch action.Tk {
|
||||
case parser.TkActionBlock, parser.TkActionDeny:
|
||||
return &RuleResult{
|
||||
Action: "Block",
|
||||
Reason: fmt.Sprintf("blocked by WAF rule: %d", rule.ID()),
|
||||
}, nil
|
||||
case parser.TkActionAllow:
|
||||
return &RuleResult{
|
||||
Action: "Allow",
|
||||
}, nil
|
||||
case parser.TkActionDrop:
|
||||
return &RuleResult{
|
||||
Action: "Drop",
|
||||
Reason: fmt.Sprintf("dropped by WAF rule: %d", rule.ID()),
|
||||
}, nil
|
||||
default:
|
||||
// skip other actions
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func processRequest(tx types.Transaction, req *http.Request) {
|
||||
// Process URI and method
|
||||
tx.ProcessURI(req.URL.String(), req.Method, req.Proto)
|
||||
|
||||
// Process request headers
|
||||
for key, values := range req.Header {
|
||||
for _, value := range values {
|
||||
tx.AddRequestHeader(key, value)
|
||||
}
|
||||
}
|
||||
tx.ProcessRequestHeaders()
|
||||
|
||||
// Process request body (if any)
|
||||
if req.Body != nil {
|
||||
_, err := tx.ProcessRequestBody()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func logError(error types.MatchedRule) {
|
||||
msg := error.ErrorLog()
|
||||
fmt.Printf("[WAFlogError][%s] %s\n", error.Rule().Severity(), msg)
|
||||
}
|
||||
90
service/oauth.go
Normal file
90
service/oauth.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// Copyright 2023 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
func getSigninUrl(casdoorClient *casdoorsdk.Client, callbackUrl string, originalPath string) string {
|
||||
scope := "read"
|
||||
return fmt.Sprintf("%s/login/oauth/authorize?client_id=%s&response_type=code&redirect_uri=%s&scope=%s&state=%s",
|
||||
casdoorClient.Endpoint, casdoorClient.ClientId, url.QueryEscape(callbackUrl), scope, url.QueryEscape(originalPath))
|
||||
}
|
||||
|
||||
func redirectToCasdoor(casdoorClient *casdoorsdk.Client, w http.ResponseWriter, r *http.Request) {
|
||||
scheme := getScheme(r)
|
||||
|
||||
callbackUrl := fmt.Sprintf("%s://%s/caswaf-handler", scheme, r.Host)
|
||||
originalPath := r.RequestURI
|
||||
signinUrl := getSigninUrl(casdoorClient, callbackUrl, originalPath)
|
||||
http.Redirect(w, r, signinUrl, http.StatusFound)
|
||||
}
|
||||
|
||||
func handleAuthCallback(w http.ResponseWriter, r *http.Request) {
|
||||
site := getSiteByDomainWithWww(r.Host)
|
||||
if site == nil {
|
||||
responseError(w, "CasWAF error: site not found for host: %s", r.Host)
|
||||
return
|
||||
}
|
||||
|
||||
code := r.URL.Query().Get("code")
|
||||
state := r.URL.Query().Get("state")
|
||||
if code == "" {
|
||||
responseError(w, "CasWAF error: the code should not be empty")
|
||||
return
|
||||
} else if state == "" {
|
||||
responseError(w, "CasWAF error: the state should not be empty")
|
||||
return
|
||||
}
|
||||
|
||||
application, err := object.GetApplication(util.GetId(site.Owner, site.CasdoorApplication))
|
||||
if err != nil {
|
||||
responseError(w, "CasWAF error: casdoorClient.GetOAuthToken() error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//casdoorClient, err := getCasdoorClientFromSite(site)
|
||||
//if err != nil {
|
||||
// responseError(w, "CasWAF error: getCasdoorClientFromSite() error: %s", err.Error())
|
||||
// return
|
||||
//}
|
||||
|
||||
token, tokenError, err := object.GetAuthorizationCodeToken(application, application.ClientSecret, code, "", "")
|
||||
if tokenError != nil {
|
||||
responseError(w, "CasWAF error: casdoorClient.GetOAuthToken() error: %s", tokenError.Error)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
responseError(w, "CasWAF error: casdoorClient.GetOAuthToken() error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
cookie := &http.Cookie{
|
||||
Name: "casdoor_access_token",
|
||||
Value: token.AccessToken,
|
||||
Path: "/",
|
||||
}
|
||||
http.SetCookie(w, cookie)
|
||||
|
||||
originalPath := state
|
||||
http.Redirect(w, r, originalPath, http.StatusFound)
|
||||
}
|
||||
373
service/proxy.go
Normal file
373
service/proxy.go
Normal 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
142
service/util.go
Normal file
@@ -0,0 +1,142 @@
|
||||
// Copyright 2023 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
)
|
||||
|
||||
func joinPath(a string, b string) string {
|
||||
if strings.HasSuffix(a, "/") && strings.HasPrefix(b, "/") {
|
||||
b = b[1:]
|
||||
} else if !strings.HasSuffix(a, "/") && !strings.HasPrefix(b, "/") {
|
||||
b = "/" + b
|
||||
}
|
||||
res := a + b
|
||||
return res
|
||||
}
|
||||
|
||||
func isHostIp(host string) bool {
|
||||
hostWithoutPort := strings.Split(host, ":")[0]
|
||||
ip := net.ParseIP(hostWithoutPort)
|
||||
return ip != nil
|
||||
}
|
||||
|
||||
func responseOk(w http.ResponseWriter, format string, a ...interface{}) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
msg := fmt.Sprintf(format, a...)
|
||||
fmt.Println(msg)
|
||||
_, err := fmt.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
|
||||
}
|
||||
@@ -15,8 +15,12 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GetHostname() string {
|
||||
@@ -55,3 +59,55 @@ func IsHostIntranet(ip string) bool {
|
||||
|
||||
return parsedIP.IsPrivate() || parsedIP.IsLoopback() || parsedIP.IsLinkLocalUnicast() || parsedIP.IsLinkLocalMulticast()
|
||||
}
|
||||
|
||||
func ResolveDomainToIp(domain string) string {
|
||||
ips, err := net.LookupIP(domain)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no such host") {
|
||||
return "(empty)"
|
||||
}
|
||||
|
||||
fmt.Printf("resolveDomainToIp() error: %s\n", err.Error())
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
if ipv4 := ip.To4(); ipv4 != nil {
|
||||
return ipv4.String()
|
||||
}
|
||||
}
|
||||
return "(empty)"
|
||||
}
|
||||
|
||||
func PingUrl(url string) (bool, string) {
|
||||
client := http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 200 && resp.StatusCode <= 299 {
|
||||
return true, ""
|
||||
}
|
||||
return false, fmt.Sprintf("Status: %s", resp.Status)
|
||||
}
|
||||
|
||||
func IsIntranetIp(ip string) bool {
|
||||
ipStr, _, err := net.SplitHostPort(ip)
|
||||
if err != nil {
|
||||
ipStr = ip
|
||||
}
|
||||
|
||||
parsedIP := net.ParseIP(ipStr)
|
||||
if parsedIP == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return parsedIP.IsPrivate() ||
|
||||
parsedIP.IsLoopback() ||
|
||||
parsedIP.IsLinkLocalUnicast() ||
|
||||
parsedIP.IsLinkLocalMulticast()
|
||||
}
|
||||
|
||||
40
util/request.go
Normal file
40
util/request.go
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright 2023 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetClientIp(r *http.Request) string {
|
||||
forwarded := r.Header.Get("X-Forwarded-For")
|
||||
if forwarded != "" {
|
||||
clientIP := strings.Split(forwarded, ",")[0]
|
||||
return strings.TrimSpace(clientIP)
|
||||
}
|
||||
|
||||
realIP := r.Header.Get("X-Real-IP")
|
||||
if realIP != "" {
|
||||
return realIP
|
||||
}
|
||||
|
||||
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return r.RemoteAddr
|
||||
}
|
||||
return ip
|
||||
}
|
||||
29
util/rule.go
Normal file
29
util/rule.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright 2024 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package util
|
||||
|
||||
import "fmt"
|
||||
|
||||
// GenerateVerboseReason creates a detailed reason message for verbose mode
|
||||
func GenerateVerboseReason(ruleId string, expressionReason string, customReason string) string {
|
||||
verboseReason := fmt.Sprintf("Rule [%s] triggered", ruleId)
|
||||
if expressionReason != "" {
|
||||
verboseReason += fmt.Sprintf(" - %s", expressionReason)
|
||||
}
|
||||
if customReason != "" {
|
||||
verboseReason += fmt.Sprintf(" - Custom reason: %s", customReason)
|
||||
}
|
||||
return verboseReason
|
||||
}
|
||||
59
util/stacks.go
Normal file
59
util/stacks.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright 2023 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package util
|
||||
|
||||
// Stack is a stack data structure implemented using a slice
|
||||
type Stack struct {
|
||||
items []interface{}
|
||||
}
|
||||
|
||||
// Push adds an item to the stack
|
||||
func (s *Stack) Push(item interface{}) {
|
||||
s.items = append(s.items, item)
|
||||
}
|
||||
|
||||
// Pop removes and returns the last item from the stack
|
||||
func (s *Stack) Pop() (interface{}, bool) {
|
||||
if len(s.items) == 0 {
|
||||
return nil, false // Return a sentinel value or you could handle this more gracefully
|
||||
}
|
||||
lastIndex := len(s.items) - 1
|
||||
item := s.items[lastIndex]
|
||||
s.items = s.items[:lastIndex]
|
||||
return item, true
|
||||
}
|
||||
|
||||
// Peek returns the last item from the stack without removing it
|
||||
func (s *Stack) Peek() interface{} {
|
||||
if len(s.items) == 0 {
|
||||
return -1
|
||||
}
|
||||
return s.items[len(s.items)-1]
|
||||
}
|
||||
|
||||
// IsEmpty checks if the stack is empty
|
||||
func (s *Stack) IsEmpty() bool {
|
||||
return len(s.items) == 0
|
||||
}
|
||||
|
||||
// Size returns the number of items in the stack
|
||||
func (s *Stack) Size() int {
|
||||
return len(s.items)
|
||||
}
|
||||
|
||||
// NewStack creates a new stack
|
||||
func NewStack() *Stack {
|
||||
return &Stack{}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -393,3 +394,37 @@ func StringToInterfaceArray2d(arrays [][]string) [][]interface{} {
|
||||
}
|
||||
return interfaceArrays
|
||||
}
|
||||
|
||||
func generateRandomString(length int) (string, error) {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
var c byte
|
||||
index := rand.Intn(len(charset))
|
||||
c = charset[index]
|
||||
b[i] = c
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func GenerateTwoUniqueRandomStrings() (string, string, error) {
|
||||
len1 := 16 + int(big.NewInt(17).Int64())
|
||||
len2 := 16 + int(big.NewInt(17).Int64())
|
||||
|
||||
str1, err := generateRandomString(len1)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
str2, err := generateRandomString(len2)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
for str1 == str2 {
|
||||
str2, err = generateRandomString(len2)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
return str1, str2, nil
|
||||
}
|
||||
|
||||
427
web/public/AuthCallbackHandler.js
Normal file
427
web/public/AuthCallbackHandler.js
Normal file
@@ -0,0 +1,427 @@
|
||||
// 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 (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=" + encodeURIComponent(res.data) + "&state=" + encodeURIComponent(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 + "=" + encodeURIComponent(res.data) + "&state=" + encodeURIComponent(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);
|
||||
});
|
||||
}
|
||||
};
|
||||
})();
|
||||
409
web/public/ProviderHintRedirect.js
Normal file
409
web/public/ProviderHintRedirect.js
Normal 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();
|
||||
});
|
||||
}
|
||||
};
|
||||
})();
|
||||
@@ -1497,7 +1497,7 @@ class ApplicationEditPage extends React.Component {
|
||||
</div>
|
||||
} style={{margin: (Setting.isMobile()) ? "5px" : {}, height: "calc(100vh - 145px - 48px)", overflow: "hidden"}}
|
||||
styles={{body: {height: "100%"}}} type="inner">
|
||||
<Layout style={{background: "inherit", height: "100%", overflow: "auto"}}>
|
||||
<Layout style={{background: "inherit", height: "100%"}}>
|
||||
{
|
||||
this.state.menuMode === "horizontal" || !this.state.menuMode ? (
|
||||
<Header style={{background: "inherit", padding: "0px", position: "sticky", top: 0, height: 38, minHeight: 38}}>
|
||||
@@ -1548,7 +1548,10 @@ class ApplicationEditPage extends React.Component {
|
||||
</Menu>
|
||||
</Sider>) : null
|
||||
}
|
||||
<Content style={{padding: "15px"}}>
|
||||
<Content style={{padding: "15px",
|
||||
overflowY: "auto",
|
||||
height: "100%",
|
||||
paddingBottom: "80px"}}>
|
||||
{this.renderApplicationForm()}
|
||||
</Content>
|
||||
</Layout>
|
||||
|
||||
@@ -20,7 +20,7 @@ import React, {useState} from "react";
|
||||
import i18next from "i18next";
|
||||
import {
|
||||
AppstoreTwoTone,
|
||||
BarsOutlined, DeploymentUnitOutlined, DollarTwoTone, DownOutlined,
|
||||
BarsOutlined, CheckCircleTwoTone, DeploymentUnitOutlined, DollarTwoTone, DownOutlined,
|
||||
HomeTwoTone,
|
||||
LockTwoTone, LogoutOutlined,
|
||||
SafetyCertificateTwoTone, SettingOutlined, SettingTwoTone,
|
||||
@@ -104,6 +104,10 @@ import TicketListPage from "./TicketListPage";
|
||||
import TicketEditPage from "./TicketEditPage";
|
||||
import * as Cookie from "cookie";
|
||||
import * as UserBackend from "./backend/UserBackend";
|
||||
import SiteListPage from "./SiteListPage";
|
||||
import SiteEditPage from "./SiteEditPage";
|
||||
import RuleEditPage from "./RuleEditPage";
|
||||
import RuleListPage from "./RuleListPage";
|
||||
|
||||
function ManagementPage(props) {
|
||||
const [menuVisible, setMenuVisible] = useState(false);
|
||||
@@ -323,6 +327,8 @@ function ManagementPage(props) {
|
||||
Setting.getItem(<Link to="/providers">{i18next.t("application:Providers")}</Link>, "/providers"),
|
||||
Setting.getItem(<Link to="/resources">{i18next.t("general:Resources")}</Link>, "/resources"),
|
||||
Setting.getItem(<Link to="/certs">{i18next.t("general:Certs")}</Link>, "/certs"),
|
||||
Setting.getItem(<Link to="/sites">{i18next.t("general:Sites")}</Link>, "/sites"),
|
||||
Setting.getItem(<Link to="/rules">{i18next.t("general:Rules")}</Link>, "/rules"),
|
||||
]));
|
||||
|
||||
res.push(Setting.getItem(<Link style={{color: textColor}} to="/roles">{i18next.t("general:Authorization")}</Link>, "/auth", <SafetyCertificateTwoTone twoToneColor={twoToneColor} />, [
|
||||
@@ -339,6 +345,12 @@ function ManagementPage(props) {
|
||||
}
|
||||
})));
|
||||
|
||||
res.push(Setting.getItem(<Link style={{color: textColor}} to="/sites">{i18next.t("general:Gateway")}</Link>, "/gateway", <CheckCircleTwoTone twoToneColor={twoToneColor} />, [
|
||||
Setting.getItem(<Link to="/sites">{i18next.t("general:Sites")}</Link>, "/sites"),
|
||||
Setting.getItem(<Link to="/certs">{i18next.t("general:Certs")}</Link>, "/certs"),
|
||||
Setting.getItem(<Link to="/rules">{i18next.t("general:Rules")}</Link>, "/rules"),
|
||||
]));
|
||||
|
||||
res.push(Setting.getItem(<Link style={{color: textColor}} to="/sessions">{i18next.t("general:Logging & Auditing")}</Link>, "/logs", <WalletTwoTone twoToneColor={twoToneColor} />, [
|
||||
Setting.getItem(<Link to="/sessions">{i18next.t("general:Sessions")}</Link>, "/sessions"),
|
||||
Conf.CasvisorUrl ? Setting.getItem(<a target="_blank" rel="noreferrer" href={Conf.CasvisorUrl}>{i18next.t("general:Records")}</a>, "/records")
|
||||
@@ -473,6 +485,10 @@ function ManagementPage(props) {
|
||||
<Route exact path="/resources" render={(props) => renderLoginIfNotLoggedIn(<ResourceListPage account={account} {...props} />)} />
|
||||
<Route exact path="/certs" render={(props) => renderLoginIfNotLoggedIn(<CertListPage account={account} {...props} />)} />
|
||||
<Route exact path="/certs/:organizationName/:certName" render={(props) => renderLoginIfNotLoggedIn(<CertEditPage account={account} {...props} />)} />
|
||||
<Route exact path="/sites" render={(props) => renderLoginIfNotLoggedIn(<SiteListPage account={account} {...props} />)} />
|
||||
<Route exact path="/sites/:organizationName/:siteName" render={(props) => renderLoginIfNotLoggedIn(<SiteEditPage account={account} {...props} />)} />
|
||||
<Route exact path="/rules" render={(props) => renderLoginIfNotLoggedIn(<RuleListPage account={account} {...props} />)} />
|
||||
<Route exact path="/rules/:organizationName/:ruleName" render={(props) => renderLoginIfNotLoggedIn(<RuleEditPage account={account} {...props} />)} />
|
||||
<Route exact path="/verifications" render={(props) => renderLoginIfNotLoggedIn(<VerificationListPage account={account} {...props} />)} />
|
||||
<Route exact path="/roles" render={(props) => renderLoginIfNotLoggedIn(<RoleListPage account={account} {...props} />)} />
|
||||
<Route exact path="/roles/:organizationName/:roleName" render={(props) => renderLoginIfNotLoggedIn(<RoleEditPage account={account} {...props} />)} />
|
||||
|
||||
@@ -564,7 +564,7 @@ class OrganizationEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("organization:Balance credit"), i18next.t("organization:Balance credit - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={4} >
|
||||
<InputNumber value={this.state.organization.balanceCredit ?? 0} onChange={value => {
|
||||
<InputNumber value={this.state.organization.balanceCredit ?? 0} max={0} onChange={value => {
|
||||
this.updateOrganizationField("balanceCredit", value);
|
||||
}} />
|
||||
</Col>
|
||||
@@ -747,6 +747,43 @@ class OrganizationEditPage extends React.Component {
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("organization:LDAP attributes"), i18next.t("organization:LDAP attributes - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<Select
|
||||
mode="multiple"
|
||||
allowClear
|
||||
style={{width: "100%"}}
|
||||
value={this.state.organization.ldapAttributes ?? []}
|
||||
onChange={(value) => {
|
||||
this.updateOrganizationField("ldapAttributes", value);
|
||||
}}
|
||||
options={[
|
||||
{value: "uid", label: "uid"},
|
||||
{value: "cn", label: "cn"},
|
||||
{value: "mail", label: "mail"},
|
||||
{value: "email", label: "email"},
|
||||
{value: "mobile", label: "mobile"},
|
||||
{value: "displayName", label: "displayName"},
|
||||
{value: "givenName", label: "givenName"},
|
||||
{value: "sn", label: "sn"},
|
||||
{value: "uidNumber", label: "uidNumber"},
|
||||
{value: "gidNumber", label: "gidNumber"},
|
||||
{value: "homeDirectory", label: "homeDirectory"},
|
||||
{value: "loginShell", label: "loginShell"},
|
||||
{value: "gecos", label: "gecos"},
|
||||
{value: "sshPublicKey", label: "sshPublicKey"},
|
||||
{value: "memberOf", label: "memberOf"},
|
||||
{value: "title", label: "title"},
|
||||
{value: "userPassword", label: "userPassword"},
|
||||
{value: "c", label: "c"},
|
||||
{value: "co", label: "co"},
|
||||
]}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:LDAPs"), i18next.t("general:LDAPs - Tooltip"))} :
|
||||
@@ -762,6 +799,46 @@ class OrganizationEditPage extends React.Component {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("organization:Kerberos realm"), i18next.t("organization:Kerberos realm - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.organization.kerberosRealm} onChange={e => {
|
||||
this.updateOrganizationField("kerberosRealm", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("organization:Kerberos KDC host"), i18next.t("organization:Kerberos KDC host - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.organization.kerberosKdcHost} onChange={e => {
|
||||
this.updateOrganizationField("kerberosKdcHost", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("organization:Kerberos keytab"), i18next.t("organization:Kerberos keytab - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input.TextArea rows={4} value={this.state.organization.kerberosKeytab} onChange={e => {
|
||||
this.updateOrganizationField("kerberosKeytab", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("organization:Kerberos service name"), i18next.t("organization:Kerberos service name - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.organization.kerberosServiceName} placeholder="HTTP" onChange={e => {
|
||||
this.updateOrganizationField("kerberosServiceName", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -339,7 +339,7 @@ class PermissionListPage extends BaseListPage {
|
||||
case "Admin":
|
||||
return i18next.t("general:Admin");
|
||||
default:
|
||||
return null;
|
||||
return tag || null;
|
||||
}
|
||||
});
|
||||
return Setting.getTags(tags);
|
||||
|
||||
309
web/src/RuleEditPage.js
Normal file
309
web/src/RuleEditPage.js
Normal file
@@ -0,0 +1,309 @@
|
||||
// Copyright 2023 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Card, Col, Input, InputNumber, Row, Select, Switch} from "antd";
|
||||
import * as Setting from "./Setting";
|
||||
import * as RuleBackend from "./backend/RuleBackend";
|
||||
import i18next from "i18next";
|
||||
import WafRuleTable from "./table/WafRuleTable";
|
||||
import IpRuleTable from "./table/IpRuleTable";
|
||||
import UaRuleTable from "./table/UaRuleTable";
|
||||
import IpRateRuleTable from "./table/IpRateRuleTable";
|
||||
import CompoundRule from "./common/CompoundRule";
|
||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
class RuleEditPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
owner: props.match.params.organizationName,
|
||||
ruleName: props.match.params.ruleName,
|
||||
rule: null,
|
||||
organizations: [],
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getRule();
|
||||
this.getOrganizations();
|
||||
}
|
||||
|
||||
getRule() {
|
||||
RuleBackend.getRule(this.state.owner, this.state.ruleName).then((res) => {
|
||||
this.setState({
|
||||
rule: res.data,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateRuleField(key, value) {
|
||||
const rule = Setting.deepCopy(this.state.rule);
|
||||
rule[key] = value;
|
||||
if (key === "type") {
|
||||
rule.expressions = [];
|
||||
}
|
||||
this.setState({
|
||||
rule: rule,
|
||||
});
|
||||
}
|
||||
|
||||
updateRuleFieldInExpressions(index, key, value) {
|
||||
const rule = Setting.deepCopy(this.state.rule);
|
||||
rule.expressions[index][key] = value;
|
||||
this.updateRuleField("expressions", rule.expressions);
|
||||
this.setState({
|
||||
rule: rule,
|
||||
});
|
||||
}
|
||||
|
||||
getOrganizations() {
|
||||
if (Setting.isAdminUser(this.props.account)) {
|
||||
OrganizationBackend.getOrganizations("admin")
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
organizations: res.data || [],
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderRule() {
|
||||
return (
|
||||
<Card size="small" title={
|
||||
<div>
|
||||
{i18next.t("rule:Edit Rule")}
|
||||
<Button type="primary" onClick={this.submitRuleEdit.bind(this)}>{i18next.t("general:Save")}</Button>
|
||||
</div>
|
||||
} style={{marginTop: 10}} type="inner">
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col span={2} style={{marginTop: "5px"}}>
|
||||
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} disabled={!Setting.isAdminUser(this.props.account)} value={this.state.rule.owner} onChange={(value => {
|
||||
this.updateRuleField("owner", value);
|
||||
})}>
|
||||
{
|
||||
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col span={2} style={{marginTop: "5px"}}>
|
||||
{i18next.t("general:Name")}:
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<Input value={this.state.rule.name} onChange={e => {
|
||||
this.updateRuleField("name", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col span={2} style={{marginTop: "5px"}}>
|
||||
{i18next.t("rule:Type")}:
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<Select virtual={false} value={this.state.rule.type} style={{width: "100%"}} onChange={value => {
|
||||
this.updateRuleField("type", value);
|
||||
}}>
|
||||
{
|
||||
[
|
||||
{value: "WAF", text: "WAF"},
|
||||
{value: "IP", text: "IP"},
|
||||
{value: "User-Agent", text: "User-Agent"},
|
||||
{value: "IP Rate Limiting", text: i18next.t("rule:IP Rate Limiting")},
|
||||
{value: "Compound", text: i18next.t("rule:Compound")},
|
||||
].map((item, index) => <Option key={index} value={item.value}>{item.text}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("rule:Expressions")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
{
|
||||
this.state.rule.type === "WAF" ? (
|
||||
<WafRuleTable
|
||||
title={"Seclang"}
|
||||
table={this.state.rule.expressions}
|
||||
ruleName={this.state.rule.name}
|
||||
account={this.props.account}
|
||||
onUpdateTable={(value) => {this.updateRuleField("expressions", value);}}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
this.state.rule.type === "IP" ? (
|
||||
<IpRuleTable
|
||||
title={"IPs"}
|
||||
table={this.state.rule.expressions}
|
||||
ruleName={this.state.rule.name}
|
||||
account={this.props.account}
|
||||
onUpdateTable={(value) => {this.updateRuleField("expressions", value);}}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
this.state.rule.type === "User-Agent" ? (
|
||||
<UaRuleTable
|
||||
title={"User-Agents"}
|
||||
table={this.state.rule.expressions}
|
||||
ruleName={this.state.rule.name}
|
||||
account={this.props.account}
|
||||
onUpdateTable={(value) => {this.updateRuleField("expressions", value);}}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
this.state.rule.type === "IP Rate Limiting" ? (
|
||||
<IpRateRuleTable
|
||||
title={i18next.t("rule:IP Rate Limiting")}
|
||||
table={this.state.rule.expressions}
|
||||
ruleName={this.state.rule.name}
|
||||
account={this.props.account}
|
||||
onUpdateTable={(value) => {this.updateRuleField("expressions", value);}}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
this.state.rule.type === "Compound" ? (
|
||||
<CompoundRule
|
||||
title={i18next.t("rule:Compound")}
|
||||
table={this.state.rule.expressions}
|
||||
ruleName={this.state.rule.name}
|
||||
owner={this.state.owner}
|
||||
onUpdateTable={(value) => {this.updateRuleField("expressions", value);}} />
|
||||
) : null
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
this.state.rule.type !== "WAF" && (
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col span={2} style={{marginTop: "5px"}}>
|
||||
{i18next.t("general:Action")}:
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<Select virtual={false} value={this.state.rule.action} defaultValue={"Block"} style={{width: "100%"}} onChange={(value) => {
|
||||
this.updateRuleField("action", value);
|
||||
}}>
|
||||
{
|
||||
[
|
||||
{value: "Allow", text: i18next.t("rule:Allow")},
|
||||
{value: "Block", text: i18next.t("rule:Block")},
|
||||
].map((item, index) => <Option key={index} value={item.value}>{item.text}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.rule.type !== "WAF" && (this.state.rule.action === "Allow" || this.state.rule.action === "Block") && (
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col span={2} style={{marginTop: "5px"}}>
|
||||
{i18next.t("rule:Status code")}:
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<InputNumber value={this.state.rule.statusCode} min={100} max={599} onChange={e => {
|
||||
this.updateRuleField("statusCode", e);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
{
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col span={2} style={{marginTop: "5px"}}>
|
||||
{i18next.t("rule:Reason")}:
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<Input value={this.state.rule.reason}
|
||||
onChange={e => {
|
||||
this.updateRuleField("reason", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
}
|
||||
{
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col span={2} style={{marginTop: "5px"}}>
|
||||
{i18next.t("rule:Verbose mode")}:
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<Switch checked={this.state.rule.isVerbose}
|
||||
onChange={checked => {
|
||||
this.updateRuleField("isVerbose", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Row style={{width: "100%"}}>
|
||||
<Col span={1}>
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
{
|
||||
this.state.rule !== null ? this.renderRule() : null
|
||||
}
|
||||
</Col>
|
||||
<Col span={1}>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{margin: 10}}>
|
||||
<Col span={2}>
|
||||
</Col>
|
||||
<Col span={18}>
|
||||
<Button type="primary" size="large" onClick={this.submitRuleEdit.bind(this)}>{i18next.t("general:Save")}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
submitRuleEdit() {
|
||||
const rule = Setting.deepCopy(this.state.rule);
|
||||
RuleBackend.updateRule(this.state.owner, this.state.ruleName, rule)
|
||||
.then((res) => {
|
||||
if (res.status !== "error") {
|
||||
Setting.showMessage("success", "Rule updated successfully");
|
||||
this.setState({
|
||||
rule: rule,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `Rule failed to update: ${res.msg}`);
|
||||
this.setState({
|
||||
ruleName: this.state.rule.name,
|
||||
});
|
||||
this.props.history.push(`/rules/${this.state.rule.owner}/${this.state.rule.name}`);
|
||||
this.getRule();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default RuleEditPage;
|
||||
236
web/src/RuleListPage.js
Normal file
236
web/src/RuleListPage.js
Normal file
@@ -0,0 +1,236 @@
|
||||
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Popconfirm, Table, Tag} from "antd";
|
||||
import moment from "moment";
|
||||
import * as Setting from "./Setting";
|
||||
import * as RuleBackend from "./backend/RuleBackend";
|
||||
import i18next from "i18next";
|
||||
import BaseListPage from "./BaseListPage";
|
||||
|
||||
class RuleListPage extends BaseListPage {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.setState({
|
||||
pagination: {
|
||||
...this.state.pagination,
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
});
|
||||
this.fetch({pagination: this.state.pagination});
|
||||
}
|
||||
|
||||
fetch = (params = {}) => {
|
||||
const sortField = params.sortField, sortOrder = params.sortOrder;
|
||||
if (!params.pagination) {
|
||||
params.pagination = {current: 1, pageSize: 10};
|
||||
}
|
||||
this.setState({
|
||||
loading: true,
|
||||
});
|
||||
RuleBackend.getRules(this.props.account.owner, params.pagination.current, params.pagination.pageSize, sortField, sortOrder).then((res) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
data: res.data,
|
||||
pagination: {
|
||||
...params.pagination,
|
||||
total: res.data2,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.setState({loading: false});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
addRule() {
|
||||
const newRule = this.newRule();
|
||||
RuleBackend.addRule(newRule).then((res) => {
|
||||
if (res.status === "error") {
|
||||
Setting.showMessage("error", `Failed to add: ${res.msg}`);
|
||||
} else {
|
||||
Setting.showMessage("success", "Rule added successfully");
|
||||
this.setState({
|
||||
data: Setting.prependRow(this.state.data, newRule),
|
||||
});
|
||||
this.fetch();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteRule(i) {
|
||||
RuleBackend.deleteRule(this.state.data[i]).then((res) => {
|
||||
if (res.status === "error") {
|
||||
Setting.showMessage("error", `Failed to delete: ${res.msg}`);
|
||||
} else {
|
||||
Setting.showMessage("success", "Deleted successfully");
|
||||
this.fetch({
|
||||
pagination: {
|
||||
...this.state.pagination,
|
||||
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
newRule() {
|
||||
const randomName = Setting.getRandomName();
|
||||
const owner = Setting.getRequestOrganization(this.props.account);
|
||||
return {
|
||||
owner: owner,
|
||||
name: `rule_${randomName}`,
|
||||
createdTime: moment().format(),
|
||||
type: "User-Agent",
|
||||
expressions: [],
|
||||
action: "Block",
|
||||
reason: "Your request is blocked.",
|
||||
};
|
||||
}
|
||||
|
||||
renderTable(data) {
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("general:Owner"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
width: "150px",
|
||||
sorter: (a, b) => a.owner.localeCompare(b.owner),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: "200px",
|
||||
sorter: (a, b) => a.name.localeCompare(b.name),
|
||||
render: (text, rule, index) => {
|
||||
return <a href={`/rules/${rule.owner}/${text}`}>{text}</a>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Create time"),
|
||||
dataIndex: "createdTime",
|
||||
key: "createdTime",
|
||||
width: "200px",
|
||||
sorter: (a, b) => a.createdTime.localeCompare(b.createdTime),
|
||||
render: (text, rule, index) => {
|
||||
return Setting.getFormattedDate(text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Update time"),
|
||||
dataIndex: "updatedTime",
|
||||
key: "updatedTime",
|
||||
width: "200px",
|
||||
sorter: (a, b) => a.updatedTime.localeCompare(b.updatedTime),
|
||||
render: (text, rule, index) => {
|
||||
return Setting.getFormattedDate(text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("rule:Type"),
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
width: "100px",
|
||||
sorter: (a, b) => a.type.localeCompare(b.type),
|
||||
render: (text, rule, index) => {
|
||||
return (
|
||||
<Tag color="blue">
|
||||
{i18next.t(`rule:${text}`)}
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("rule:Expressions"),
|
||||
dataIndex: "expressions",
|
||||
key: "expressions",
|
||||
sorter: (a, b) => a.expressions.localeCompare(b.expressions),
|
||||
render: (text, rule, index) => {
|
||||
return rule.expressions.map((expression, i) => {
|
||||
return (
|
||||
<Tag key={expression} color={"success"}>
|
||||
{expression.operator + " " + expression.value.slice(0, 20)}
|
||||
</Tag>
|
||||
);
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
dataIndex: "action",
|
||||
key: "action",
|
||||
width: "100px",
|
||||
sorter: (a, b) => a.action.localeCompare(b.action),
|
||||
},
|
||||
{
|
||||
title: i18next.t("rule:Status code"),
|
||||
dataIndex: "statusCode",
|
||||
key: "statusCode",
|
||||
width: "120px",
|
||||
sorter: (a, b) => a.statusCode.localeCompare(b.statusCode),
|
||||
},
|
||||
{
|
||||
title: i18next.t("rule:Reason"),
|
||||
dataIndex: "reason",
|
||||
key: "reason",
|
||||
width: "300px",
|
||||
sorter: (a, b) => a.reason.localeCompare(b.reason),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
dataIndex: "",
|
||||
key: "op",
|
||||
render: (text, rule, index) => {
|
||||
return (
|
||||
<div>
|
||||
<Popconfirm
|
||||
title={`Sure to delete rule: ${rule.name} ?`}
|
||||
onConfirm={() => this.deleteRule(index)}
|
||||
>
|
||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/rules/${rule.owner}/${rule.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<Button type="danger">{i18next.t("general:Delete")}</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Table
|
||||
dataSource={data}
|
||||
columns={columns}
|
||||
rowKey="name"
|
||||
pagination={this.state.pagination}
|
||||
loading={this.state.loading}
|
||||
onChange={this.handleTableChange}
|
||||
size="middle"
|
||||
bordered
|
||||
title={() => (
|
||||
<div>
|
||||
{i18next.t("general:Rules")}
|
||||
<Button type="primary" size="small" onClick={() => this.addRule()}>{i18next.t("general:Add")}</Button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default RuleListPage;
|
||||
@@ -843,7 +843,7 @@ export function hasPromptPage(application) {
|
||||
}
|
||||
|
||||
const signupItems = getAllPromptedSignupItems(application);
|
||||
if (signupItems?.length > 0) {
|
||||
if (signupItems?.filter(item => item.name === "Country/Region").length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2427,3 +2427,48 @@ export function getApiPaths() {
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export function getItemId(item) {
|
||||
return item.owner + "/" + item.name;
|
||||
}
|
||||
|
||||
export function getVersionInfo(text, siteName) {
|
||||
if (text === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const versionInfo = JSON.parse(text);
|
||||
const link = versionInfo?.version !== "" ? `${getRepoUrl(siteName)}/releases/tag/${versionInfo?.version}` : "";
|
||||
let versionText = versionInfo?.version !== "" ? versionInfo?.version : "Unknown version";
|
||||
if (versionInfo?.commitOffset > 0) {
|
||||
versionText += ` (ahead+${versionInfo?.commitOffset})`;
|
||||
}
|
||||
|
||||
return {text: versionText, link: link};
|
||||
} catch (e) {
|
||||
return {text: "", link: ""};
|
||||
}
|
||||
}
|
||||
|
||||
export function prependRow(array, row) {
|
||||
return [row, ...array];
|
||||
}
|
||||
|
||||
function getOriginalName(name) {
|
||||
const tokens = name.split("_");
|
||||
if (tokens.length > 0) {
|
||||
return tokens[0];
|
||||
} else {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
export function getRepoUrl(name) {
|
||||
name = getOriginalName(name);
|
||||
if (name === "casdoor") {
|
||||
return "https://github.com/casdoor/casdoor";
|
||||
} else {
|
||||
return `https://github.com/casbin/${name}`;
|
||||
}
|
||||
}
|
||||
|
||||
484
web/src/SiteEditPage.js
Normal file
484
web/src/SiteEditPage.js
Normal file
@@ -0,0 +1,484 @@
|
||||
// Copyright 2023 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Card, Col, Input, InputNumber, Row, Select, Switch} from "antd";
|
||||
import {LinkOutlined} from "@ant-design/icons";
|
||||
import * as ProviderBackend from "./backend/ProviderBackend";
|
||||
import * as SiteBackend from "./backend/SiteBackend";
|
||||
import * as CertBackend from "./backend/CertBackend";
|
||||
import * as RuleBackend from "./backend/RuleBackend";
|
||||
import * as ApplicationBackend from "./backend/ApplicationBackend";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
import RuleTable from "./table/RuleTable";
|
||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
class SiteEditPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
owner: props.match.params.organizationName,
|
||||
siteName: props.match.params.siteName,
|
||||
rules: [],
|
||||
providers: [],
|
||||
site: null,
|
||||
certs: null,
|
||||
applications: null,
|
||||
organizations: [],
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getOrganizations();
|
||||
this.getSite();
|
||||
this.getCerts();
|
||||
this.getRules();
|
||||
this.getApplications();
|
||||
this.getAlertProviders();
|
||||
}
|
||||
|
||||
getOrganizations() {
|
||||
if (Setting.isAdminUser(this.props.account)) {
|
||||
OrganizationBackend.getOrganizations("admin")
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
organizations: res.data || [],
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getSite() {
|
||||
SiteBackend.getSite(this.state.site?.owner || this.state.owner, this.state.siteName)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
site: res.data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `Failed to get site: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getCerts() {
|
||||
CertBackend.getCerts(this.state.owner)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
certs: res.data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `Failed to get certs: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getRules() {
|
||||
RuleBackend.getRules(this.state.owner)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
rules: res.data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `Failed to get rules: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getApplications(owner) {
|
||||
ApplicationBackend.getApplicationsByOrganization("admin", owner || this.state.owner)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
applications: res.data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `Failed to get applications: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getAlertProviders() {
|
||||
ProviderBackend.getProviders()
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
const data = [];
|
||||
for (let i = 0; i < res.data.length; i++) {
|
||||
const provider = res.data[i];
|
||||
if (provider.category === "SMS" || provider.category === "Email") {
|
||||
data.push(provider.category + "/" + provider.name);
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
providers: data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `Failed to get providers: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
parseSiteField(key, value) {
|
||||
if (["score"].includes(key)) {
|
||||
value = Setting.myParseInt(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
updateSiteField(key, value) {
|
||||
value = this.parseSiteField(key, value);
|
||||
|
||||
const site = this.state.site;
|
||||
site[key] = value;
|
||||
this.setState({
|
||||
site: site,
|
||||
});
|
||||
}
|
||||
|
||||
renderSite() {
|
||||
return (
|
||||
<Card size="small" title={
|
||||
<div>
|
||||
{i18next.t("site:Edit Site")}
|
||||
<Button type="primary" onClick={this.submitSiteEdit.bind(this)}>{i18next.t("general:Save")}</Button>
|
||||
</div>
|
||||
} style={{marginLeft: "5px"}} type="inner">
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} disabled={!Setting.isAdminUser(this.props.account)} value={this.state.site.owner} onChange={(value => {
|
||||
this.updateSiteField("owner", value);
|
||||
this.getApplications(value);
|
||||
})}>
|
||||
{
|
||||
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("general:Name")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.site.name} onChange={e => {
|
||||
this.updateSiteField("name", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("general:Display name")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.site.displayName} onChange={e => {
|
||||
this.updateSiteField("displayName", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("general:Tag")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.site.tag} onChange={e => {
|
||||
this.updateSiteField("tag", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Domain")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.site.domain} onChange={e => {
|
||||
this.updateSiteField("domain", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Other domains")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} mode="tags" style={{width: "100%"}} value={this.state.site.otherDomains} onChange={(value => {this.updateSiteField("otherDomains", value);})}>
|
||||
{
|
||||
this.state.site.otherDomains?.map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Need redirect")}:
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.site.needRedirect} onChange={checked => {
|
||||
this.updateSiteField("needRedirect", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Disable verbose")}:
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.site.disableVerbose} onChange={checked => {
|
||||
this.updateSiteField("disableVerbose", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Rules")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<RuleTable
|
||||
title={"Rules"}
|
||||
account={this.props.account}
|
||||
sources={this.state.rules}
|
||||
rules={this.state.site.rules}
|
||||
onUpdateRules={(value) => this.updateSiteField("rules", value)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col span={2} style={{marginTop: "5px"}}>
|
||||
{i18next.t("site:Enable alert")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Switch checked={this.state.site.enableAlert} onChange={checked => {
|
||||
this.updateSiteField("enableAlert", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
this.state.site.enableAlert ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col span={2} style={{marginTop: "5px"}}>
|
||||
{i18next.t("site:Alert interval")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<InputNumber min={1} value={this.state.site.alertInterval} addonAfter={i18next.t("usage:seconds")} onChange={value => {
|
||||
this.updateSiteField("alertInterval", value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
this.state.site.enableAlert ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col span={2} style={{marginTop: "5px"}}>
|
||||
{i18next.t("site:Alert try times")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<InputNumber min={1} value={this.state.site.alertTryTimes} onChange={value => {
|
||||
this.updateSiteField("alertTryTimes", value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
this.state.site.enableAlert ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Alert providers")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} mode="tags" style={{width: "100%"}} value={this.state.site.alertProviders} onChange={(value => {this.updateSiteField("alertProviders", value);})}>
|
||||
{
|
||||
this.state.providers.map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
) : null
|
||||
}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Challenges")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} mode="tags" style={{width: "100%"}} value={this.state.site.challenges} onChange={(value => {this.updateSiteField("challenges", value);})}>
|
||||
{
|
||||
this.state.site.challenges?.map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Host")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input prefix={<LinkOutlined />} value={this.state.site.host} onChange={e => {
|
||||
this.updateSiteField("host", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Port")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<InputNumber min={0} max={65535} value={this.state.site.port} onChange={value => {
|
||||
this.updateSiteField("port", value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Hosts")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} mode="tags" style={{width: "100%"}} value={this.state.site.hosts} onChange={(value => {this.updateSiteField("hosts", value);})}>
|
||||
{
|
||||
this.state.site.hosts?.map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Public IP")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.site.publicIp} onChange={e => {
|
||||
this.updateSiteField("publicIp", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Mode")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.site.sslMode} onChange={(value => {this.updateSiteField("sslMode", value);})}>
|
||||
{
|
||||
[
|
||||
{id: "HTTP", name: "HTTP"},
|
||||
{id: "HTTPS and HTTP", name: "HTTPS and HTTP"},
|
||||
{id: "HTTPS Only", name: "HTTPS Only"},
|
||||
{id: "Static Folder", name: "Static Folder"},
|
||||
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:SSL cert")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select disabled={true} virtual={false} style={{width: "100%"}} showSearch value={this.state.site.sslCert} onChange={(value => {
|
||||
this.updateSiteField("sslCert", value);
|
||||
})}>
|
||||
{
|
||||
this.state.certs?.map((cert, index) => <Option key={index} value={cert.name}>{cert.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Casdoor app")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} showSearch value={this.state.site.casdoorApplication} onChange={(value => {
|
||||
this.updateSiteField("casdoorApplication", value);
|
||||
})}>
|
||||
{
|
||||
this.state.applications?.map((application, index) => <Option key={index} value={application.name}>{application.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Status")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.site.status} onChange={(value => {this.updateSiteField("status", value);})}>
|
||||
{
|
||||
[
|
||||
{id: "Active", name: "Active"},
|
||||
{id: "Inactive", name: "Inactive"},
|
||||
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
submitSiteEdit() {
|
||||
const site = Setting.deepCopy(this.state.site);
|
||||
SiteBackend.updateSite(this.state.owner, this.state.siteName, site)
|
||||
.then((res) => {
|
||||
if (res.status === "error") {
|
||||
Setting.showMessage("error", `Failed to save: ${res.msg}`);
|
||||
this.updateSiteField("name", this.state.siteName);
|
||||
} else {
|
||||
Setting.showMessage("success", "Successfully saved");
|
||||
this.setState({
|
||||
owner: this.state.site.owner,
|
||||
siteName: this.state.site.name,
|
||||
});
|
||||
this.props.history.push(`/sites/${this.state.site.owner}/${this.state.site.name}`);
|
||||
this.getSite();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `failed to save: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Row style={{width: "100%"}}>
|
||||
<Col span={1}>
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
{
|
||||
this.state.site !== null ? this.renderSite() : null
|
||||
}
|
||||
</Col>
|
||||
<Col span={1}>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{margin: 10}}>
|
||||
<Col span={2}>
|
||||
</Col>
|
||||
<Col span={18}>
|
||||
<Button type="primary" size="large" onClick={this.submitSiteEdit.bind(this)}>{i18next.t("general:Save")}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SiteEditPage;
|
||||
459
web/src/SiteListPage.js
Normal file
459
web/src/SiteListPage.js
Normal file
@@ -0,0 +1,459 @@
|
||||
// Copyright 2023 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {Button, Popconfirm, Table, Tag, Tooltip} from "antd";
|
||||
import moment from "moment";
|
||||
import * as Setting from "./Setting";
|
||||
import * as SiteBackend from "./backend/SiteBackend";
|
||||
import i18next from "i18next";
|
||||
import BaseListPage from "./BaseListPage";
|
||||
|
||||
class SiteListPage extends BaseListPage {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.setState({
|
||||
pagination: {
|
||||
...this.state.pagination,
|
||||
current: 1,
|
||||
pageSize: 1000,
|
||||
},
|
||||
});
|
||||
this.fetch({pagination: this.state.pagination});
|
||||
}
|
||||
|
||||
newSite() {
|
||||
const randomName = Setting.getRandomName();
|
||||
const owner = Setting.getRequestOrganization(this.props.account);
|
||||
return {
|
||||
owner: owner,
|
||||
name: `site_${randomName}`,
|
||||
createdTime: moment().format(),
|
||||
displayName: `New Site - ${randomName}`,
|
||||
domain: "door.casdoor.com",
|
||||
otherDomains: [],
|
||||
needRedirect: false,
|
||||
disableVerbose: false,
|
||||
rules: [],
|
||||
enableAlert: false,
|
||||
alertInterval: 60,
|
||||
alertTryTimes: 3,
|
||||
alertProviders: [],
|
||||
challenges: [],
|
||||
host: "",
|
||||
port: 8000,
|
||||
hosts: [],
|
||||
sslMode: "HTTPS Only",
|
||||
sslCert: "",
|
||||
publicIp: "8.131.81.162",
|
||||
node: "",
|
||||
isSelf: false,
|
||||
nodes: [],
|
||||
casdoorApplication: "",
|
||||
organizations: [],
|
||||
};
|
||||
}
|
||||
|
||||
addSite() {
|
||||
const newSite = this.newSite();
|
||||
SiteBackend.addSite(newSite)
|
||||
.then((res) => {
|
||||
if (res.status === "error") {
|
||||
Setting.showMessage("error", `Failed to add: ${res.msg}`);
|
||||
} else {
|
||||
Setting.showMessage("success", "Site added successfully");
|
||||
this.setState({
|
||||
data: Setting.prependRow(this.state.data, newSite),
|
||||
});
|
||||
this.fetch();
|
||||
}
|
||||
}
|
||||
)
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `Site failed to add: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
deleteSite(i) {
|
||||
SiteBackend.deleteSite(this.state.data[i])
|
||||
.then((res) => {
|
||||
if (res.status === "error") {
|
||||
Setting.showMessage("error", `Failed to delete: ${res.msg}`);
|
||||
} else {
|
||||
Setting.showMessage("success", "Site deleted successfully");
|
||||
this.fetch({
|
||||
pagination: {
|
||||
...this.state.pagination,
|
||||
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
)
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `Site failed to delete: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
renderTable(data) {
|
||||
// const renderExternalLink = () => {
|
||||
// return (
|
||||
// <svg style={{marginLeft: "5px"}} width="13.5" height="13.5" aria-hidden="true" viewBox="0 0 24 24" className="iconExternalLink_nPIU">
|
||||
// <path fill="currentColor" d="M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"></path>
|
||||
// </svg>
|
||||
// );
|
||||
// };
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("general:Owner"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
width: "90px",
|
||||
sorter: (a, b) => a.owner.localeCompare(b.owner),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Tag"),
|
||||
dataIndex: "tag",
|
||||
key: "tag",
|
||||
width: "140px",
|
||||
sorter: (a, b) => a.tag.localeCompare(b.tag),
|
||||
render: (text, record, index) => {
|
||||
if (text === "") {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Link to={`/nodes/${record.owner}/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: "120px",
|
||||
sorter: (a, b) => a.name.localeCompare(b.name),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/sites/${record.owner}/${record.name}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
// {
|
||||
// title: i18next.t("general:Create time"),
|
||||
// dataIndex: "createdTime",
|
||||
// key: "createdTime",
|
||||
// width: "180px",
|
||||
// sorter: (a, b) => a.createdTime.localeCompare(b.createdTime),
|
||||
// render: (text, record, index) => {
|
||||
// return Setting.getFormattedDate(text);
|
||||
// },
|
||||
// },
|
||||
{
|
||||
title: i18next.t("general:Display name"),
|
||||
dataIndex: "displayName",
|
||||
key: "displayName",
|
||||
// width: "200px",
|
||||
sorter: (a, b) => a.displayName.localeCompare(b.displayName),
|
||||
},
|
||||
{
|
||||
title: i18next.t("site:Domain"),
|
||||
dataIndex: "domain",
|
||||
key: "domain",
|
||||
width: "150px",
|
||||
sorter: (a, b) => a.domain.localeCompare(b.domain),
|
||||
render: (text, record, index) => {
|
||||
if (record.publicIp === "") {
|
||||
return text;
|
||||
}
|
||||
|
||||
return (
|
||||
<a target="_blank" rel="noreferrer" href={`https://${text}`}>
|
||||
{text}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("site:Other domains"),
|
||||
dataIndex: "otherDomains",
|
||||
key: "otherDomains",
|
||||
width: "120px",
|
||||
sorter: (a, b) => a.otherDomains.localeCompare(b.otherDomains),
|
||||
render: (text, record, index) => {
|
||||
return record.otherDomains.map(domain => {
|
||||
return (
|
||||
<a key={domain} target="_blank" rel="noreferrer" href={`https://${domain}`}>
|
||||
<Tag color={record.needRedirect ? "default" : "processing"}>
|
||||
{domain}
|
||||
</Tag>
|
||||
</a>
|
||||
);
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Rules"),
|
||||
dataIndex: "rules",
|
||||
key: "rules",
|
||||
width: "120px",
|
||||
sorter: (a, b) => a.rules.localeCompare(b.rules),
|
||||
render: (text, record, index) => {
|
||||
if (!record.rules) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return record.rules.map(rule => {
|
||||
return (
|
||||
<a key={rule} target="_blank" rel="noreferrer" href={`/rules/${rule}`}>
|
||||
<Tag color={"processing"}>
|
||||
{rule}
|
||||
</Tag>
|
||||
</a>
|
||||
);
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("site:Host"),
|
||||
dataIndex: "host",
|
||||
key: "host",
|
||||
width: "80px",
|
||||
sorter: (a, b) => a.host.localeCompare(b.host),
|
||||
render: (text, record, index) => {
|
||||
let host = record.port;
|
||||
if (record.host !== "") {
|
||||
host = `${record.host}:${record.port}`;
|
||||
}
|
||||
|
||||
if (record.status === "Active") {
|
||||
return host;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tag color={"warning"}>
|
||||
{host}
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("site:Hosts"),
|
||||
dataIndex: "hosts",
|
||||
key: "hosts",
|
||||
width: "200px",
|
||||
sorter: (a, b) => a.hosts.length - b.hosts.length,
|
||||
render: (hosts) => {
|
||||
if (!Array.isArray(hosts)) {
|
||||
return null;
|
||||
}
|
||||
return hosts.map((host, index) => (
|
||||
<Tag color="blue" key={index}>
|
||||
{host}
|
||||
</Tag>
|
||||
));
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("site:Nodes"),
|
||||
dataIndex: "nodes",
|
||||
key: "nodes",
|
||||
width: "180px",
|
||||
sorter: (a, b) => a.nodes.length - b.nodes.length,
|
||||
render: (text, record, index) => {
|
||||
return record.nodes.map(node => {
|
||||
const versionInfo = Setting.getVersionInfo(node.version, record.name);
|
||||
let color = node.message === "" ? "processing" : "error";
|
||||
if (color === "processing" && node.provider !== "") {
|
||||
if (node.version === "") {
|
||||
color = "warning";
|
||||
} else if (node.provider !== "") {
|
||||
color = "success";
|
||||
}
|
||||
}
|
||||
|
||||
const getTag = () => {
|
||||
if (versionInfo === null) {
|
||||
return (
|
||||
<Tag key={node.name} color={color}>
|
||||
{node.name}
|
||||
</Tag>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<a key={node.name} target="_blank" rel="noreferrer" href={versionInfo.link}>
|
||||
<Tag color={color}>
|
||||
{`${node.name} (${versionInfo.text})`}
|
||||
</Tag>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (node.message === "") {
|
||||
return getTag();
|
||||
} else {
|
||||
return (
|
||||
<Tooltip key={node.name} title={node.message}>
|
||||
{getTag()}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
// {
|
||||
// title: i18next.t("site:Public IP"),
|
||||
// dataIndex: "publicIp",
|
||||
// key: "publicIp",
|
||||
// width: "120px",
|
||||
// sorter: (a, b) => a.publicIp.localeCompare(b.publicIp),
|
||||
// },
|
||||
// {
|
||||
// title: i18next.t("site:Node"),
|
||||
// dataIndex: "node",
|
||||
// key: "node",
|
||||
// width: "180px",
|
||||
// sorter: (a, b) => a.node.localeCompare(b.node),
|
||||
// render: (text, record, index) => {
|
||||
// return (
|
||||
// <div>
|
||||
// {text}
|
||||
// {
|
||||
// !record.isSelf ? null : (
|
||||
// <Tag style={{marginLeft: "10px"}} icon={<CheckCircleOutlined />} color="success">
|
||||
// {i18next.t("general:Self")}
|
||||
// </Tag>
|
||||
// )
|
||||
// }
|
||||
// </div>
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// title: i18next.t("site:Mode"),
|
||||
// dataIndex: "sslMode",
|
||||
// key: "sslMode",
|
||||
// width: "100px",
|
||||
// sorter: (a, b) => a.sslMode.localeCompare(b.sslMode),
|
||||
// },
|
||||
{
|
||||
title: i18next.t("site:SSL cert"),
|
||||
dataIndex: "sslCert",
|
||||
key: "sslCert",
|
||||
width: "130px",
|
||||
sorter: (a, b) => a.sslCert.localeCompare(b.sslCert),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/certs/admin/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
// {
|
||||
// title: i18next.t("site:Casdoor app"),
|
||||
// dataIndex: "casdoorApplication",
|
||||
// key: "casdoorApplication",
|
||||
// width: "140px",
|
||||
// sorter: (a, b) => a.casdoorApplication.localeCompare(b.casdoorApplication),
|
||||
// render: (text, record, index) => {
|
||||
// if (text === "") {
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// return (
|
||||
// <a target="_blank" rel="noreferrer" href={Setting.getMyProfileUrl(this.state.account).replace("/account", `/applications/${this.props.account.owner}/${text}`)}>
|
||||
// {text}
|
||||
// {renderExternalLink()}
|
||||
// </a>
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
dataIndex: "action",
|
||||
key: "action",
|
||||
width: "180px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/sites/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<Popconfirm
|
||||
title={`Sure to delete site: ${record.name} ?`}
|
||||
onConfirm={() => this.deleteSite(index)}
|
||||
okText="OK"
|
||||
cancelText="Cancel"
|
||||
>
|
||||
<Button style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table columns={columns} dataSource={data} rowKey="name" size="middle" bordered pagination={this.state.pagination}
|
||||
title={() => (
|
||||
<div>
|
||||
{i18next.t("general:Sites")}
|
||||
<Button type="primary" size="small" onClick={this.addSite.bind(this)}>{i18next.t("general:Add")}</Button>
|
||||
</div>
|
||||
)}
|
||||
loading={data === null}
|
||||
onChange={this.handleTableChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
fetch = (params = {}) => {
|
||||
const field = params.searchedColumn, value = params.searchText;
|
||||
const sortField = params.sortField, sortOrder = params.sortOrder;
|
||||
if (!params.pagination) {
|
||||
params.pagination = {current: 1, pageSize: 10};
|
||||
}
|
||||
this.setState({loading: true});
|
||||
// SiteBackend.getSites(this.props.account.name, params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
|
||||
(Setting.isDefaultOrganizationSelected(this.props.account) ? SiteBackend.getGlobalSites() : SiteBackend.getSites(Setting.getRequestOrganization(this.props.account), "", "", field, value, sortField, sortOrder))
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
data: res.data,
|
||||
pagination: {
|
||||
...params.pagination,
|
||||
total: res.data2,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `Failed to get sites: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default SiteListPage;
|
||||
@@ -24,6 +24,9 @@ import i18next from "i18next";
|
||||
import RedirectForm from "../common/RedirectForm";
|
||||
import {createFormAndSubmit, renderLoginPanel} from "../Setting";
|
||||
|
||||
const reactFallbackKey = "__casdoor_callback_react";
|
||||
const reactFallbackPayloadKey = "casdoor_callback_react_fallback";
|
||||
|
||||
class AuthCallback extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -36,6 +39,134 @@ class AuthCallback extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
getNormalizedSearch(search) {
|
||||
const normalizedUrl = new URL(`${window.location.origin}/callback${search || ""}`);
|
||||
normalizedUrl.searchParams.delete(reactFallbackKey);
|
||||
return normalizedUrl.search;
|
||||
}
|
||||
|
||||
consumeReactFallbackPayload() {
|
||||
const payload = sessionStorage.getItem(reactFallbackPayloadKey);
|
||||
if (!payload) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedPayload = JSON.parse(payload);
|
||||
if (this.getNormalizedSearch(parsedPayload.search) !== this.getNormalizedSearch(this.props.location.search)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
sessionStorage.removeItem(reactFallbackPayloadKey);
|
||||
return parsedPayload;
|
||||
} catch {
|
||||
sessionStorage.removeItem(reactFallbackPayloadKey);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
handleCasLoginResult(res, body, casService) {
|
||||
const handleCasLogin = (res) => {
|
||||
let msg = "Logged in successfully.";
|
||||
if (casService === "") {
|
||||
msg += "Now you can visit apps protected by Casdoor.";
|
||||
}
|
||||
Setting.showMessage("success", msg);
|
||||
|
||||
if (casService !== "") {
|
||||
const st = res.data;
|
||||
const newUrl = new URL(casService);
|
||||
newUrl.searchParams.append("ticket", st);
|
||||
window.location.href = newUrl.toString();
|
||||
}
|
||||
};
|
||||
|
||||
Setting.checkLoginMfa(res, body, {"service": casService}, handleCasLogin, this);
|
||||
}
|
||||
|
||||
handleOAuthLoginResult(res, body, innerParams, queryString, applicationName, responseType) {
|
||||
const oAuthParams = Util.getOAuthGetParameters(innerParams);
|
||||
const concatChar = oAuthParams?.redirectUri?.includes("?") ? "&" : "?";
|
||||
const responseMode = oAuthParams?.responseMode || "query";
|
||||
const signinUrl = localStorage.getItem("signinUrl");
|
||||
const responseTypes = responseType.split(" ");
|
||||
|
||||
const handleLogin = (res) => {
|
||||
if (responseType === "login") {
|
||||
if (res.data3) {
|
||||
sessionStorage.setItem("signinUrl", signinUrl);
|
||||
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
|
||||
return;
|
||||
}
|
||||
Setting.showMessage("success", "Logged in successfully");
|
||||
const link = Setting.getFromLink();
|
||||
Setting.goToLink(link);
|
||||
} else if (responseType === "code") {
|
||||
if (res.data3) {
|
||||
sessionStorage.setItem("signinUrl", signinUrl);
|
||||
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (responseMode === "form_post") {
|
||||
const params = {
|
||||
code: res.data,
|
||||
state: oAuthParams?.state,
|
||||
};
|
||||
createFormAndSubmit(oAuthParams?.redirectUri, params);
|
||||
} else {
|
||||
const code = res.data;
|
||||
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${encodeURIComponent(code)}&state=${encodeURIComponent(oAuthParams.state)}`);
|
||||
}
|
||||
} else if (responseTypes.includes("token") || responseTypes.includes("id_token")) {
|
||||
if (res.data3) {
|
||||
sessionStorage.setItem("signinUrl", signinUrl);
|
||||
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (responseMode === "form_post") {
|
||||
const params = {
|
||||
token: responseTypes.includes("token") ? res.data : null,
|
||||
id_token: responseTypes.includes("id_token") ? res.data : null,
|
||||
token_type: "bearer",
|
||||
state: oAuthParams?.state,
|
||||
};
|
||||
createFormAndSubmit(oAuthParams?.redirectUri, params);
|
||||
} else {
|
||||
const token = res.data;
|
||||
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}${responseType}=${encodeURIComponent(token)}&state=${encodeURIComponent(oAuthParams.state)}&token_type=bearer`);
|
||||
}
|
||||
} else if (responseType === "link") {
|
||||
let from = innerParams.get("from");
|
||||
const oauth = innerParams.get("oauth");
|
||||
if (oauth) {
|
||||
from += `?oauth=${oauth}`;
|
||||
}
|
||||
Setting.goToLinkSoftOrJumpSelf(this, from);
|
||||
} else if (responseType === "saml") {
|
||||
if (res.data2.method === "POST") {
|
||||
this.setState({
|
||||
samlResponse: res.data,
|
||||
redirectUrl: res.data2.redirectUrl,
|
||||
relayState: oAuthParams.relayState,
|
||||
});
|
||||
} else {
|
||||
if (res.data3) {
|
||||
sessionStorage.setItem("signinUrl", signinUrl);
|
||||
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
|
||||
return;
|
||||
}
|
||||
const SAMLResponse = res.data;
|
||||
const redirectUri = res.data2.redirectUrl;
|
||||
Setting.goToLink(`${redirectUri}${redirectUri.includes("?") ? "&" : "?"}SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Setting.checkLoginMfa(res, body, oAuthParams, handleLogin, this, window.location.origin);
|
||||
}
|
||||
|
||||
getInnerParams() {
|
||||
// For example, for Casbin-OA, realRedirectUri = "http://localhost:9000/login"
|
||||
// realRedirectUrl = "http://localhost:9000"
|
||||
@@ -87,6 +218,7 @@ class AuthCallback extends React.Component {
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
const params = new URLSearchParams(this.props.location.search);
|
||||
const queryString = Util.getQueryParamsFromState(params.get("state"));
|
||||
const isSteam = params.get("openid.mode");
|
||||
let code = params.get("code");
|
||||
// WeCom returns "auth_code=xxx" instead of "code=xxx"
|
||||
@@ -158,6 +290,7 @@ class AuthCallback extends React.Component {
|
||||
samlRequest: samlRequest,
|
||||
// state: innerParams.get("state"),
|
||||
state: applicationName,
|
||||
invitationCode: innerParams.get("invitationCode") || "",
|
||||
redirectUri: redirectUri,
|
||||
method: method,
|
||||
codeVerifier: codeVerifier, // Include PKCE code verifier
|
||||
@@ -168,27 +301,22 @@ class AuthCallback extends React.Component {
|
||||
Provider.clearCodeVerifier(params.get("state"));
|
||||
}
|
||||
|
||||
const reactFallbackPayload = this.consumeReactFallbackPayload();
|
||||
if (reactFallbackPayload !== null) {
|
||||
if (reactFallbackPayload.flow === "cas") {
|
||||
this.handleCasLoginResult(reactFallbackPayload.res, reactFallbackPayload.body || body, reactFallbackPayload.casService || casService);
|
||||
} else {
|
||||
const fallbackInnerParams = new URLSearchParams(reactFallbackPayload.innerParams || Util.getQueryParamsFromState(params.get("state")));
|
||||
this.handleOAuthLoginResult(reactFallbackPayload.res, reactFallbackPayload.body || body, fallbackInnerParams, reactFallbackPayload.queryString, applicationName, reactFallbackPayload.responseType || this.getResponseType());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.getResponseType() === "cas") {
|
||||
// user is using casdoor as cas sso server, and wants the ticket to be acquired
|
||||
AuthBackend.loginCas(body, {"service": casService}).then((res) => {
|
||||
if (res.status === "ok") {
|
||||
const handleCasLogin = (res) => {
|
||||
let msg = "Logged in successfully.";
|
||||
if (casService === "") {
|
||||
// If service was not specified, Casdoor must display a message notifying the client that it has successfully initiated a single sign-on session.
|
||||
msg += "Now you can visit apps protected by Casdoor.";
|
||||
}
|
||||
Setting.showMessage("success", msg);
|
||||
|
||||
if (casService !== "") {
|
||||
const st = res.data;
|
||||
const newUrl = new URL(casService);
|
||||
newUrl.searchParams.append("ticket", st);
|
||||
window.location.href = newUrl.toString();
|
||||
}
|
||||
};
|
||||
|
||||
Setting.checkLoginMfa(res, body, {"service": casService}, handleCasLogin, this);
|
||||
this.handleCasLoginResult(res, body, casService);
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
|
||||
}
|
||||
@@ -197,91 +325,11 @@ class AuthCallback extends React.Component {
|
||||
}
|
||||
// OAuth
|
||||
const oAuthParams = Util.getOAuthGetParameters(innerParams);
|
||||
const concatChar = oAuthParams?.redirectUri?.includes("?") ? "&" : "?";
|
||||
const responseMode = oAuthParams?.responseMode || "query"; // Default to "query" if not specified
|
||||
const signinUrl = localStorage.getItem("signinUrl");
|
||||
|
||||
AuthBackend.login(body, oAuthParams)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
const responseType = this.getResponseType();
|
||||
const responseTypes = responseType.split(" ");
|
||||
const handleLogin = (res) => {
|
||||
if (responseType === "login") {
|
||||
if (res.data3) {
|
||||
sessionStorage.setItem("signinUrl", signinUrl);
|
||||
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
|
||||
return;
|
||||
}
|
||||
Setting.showMessage("success", "Logged in successfully");
|
||||
// Setting.goToLinkSoft(this, "/");
|
||||
const link = Setting.getFromLink();
|
||||
Setting.goToLink(link);
|
||||
} else if (responseType === "code") {
|
||||
if (res.data3) {
|
||||
sessionStorage.setItem("signinUrl", signinUrl);
|
||||
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (responseMode === "form_post") {
|
||||
const params = {
|
||||
code: res.data,
|
||||
state: oAuthParams?.state,
|
||||
};
|
||||
createFormAndSubmit(oAuthParams?.redirectUri, params);
|
||||
} else {
|
||||
const code = res.data;
|
||||
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
|
||||
}
|
||||
// Setting.showMessage("success", `Authorization code: ${res.data}`);
|
||||
} else if (responseTypes.includes("token") || responseTypes.includes("id_token")) {
|
||||
if (res.data3) {
|
||||
sessionStorage.setItem("signinUrl", signinUrl);
|
||||
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (responseMode === "form_post") {
|
||||
const params = {
|
||||
token: responseTypes.includes("token") ? res.data : null,
|
||||
id_token: responseTypes.includes("id_token") ? res.data : null,
|
||||
token_type: "bearer",
|
||||
state: oAuthParams?.state,
|
||||
};
|
||||
createFormAndSubmit(oAuthParams?.redirectUri, params);
|
||||
} else {
|
||||
const token = res.data;
|
||||
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}${responseType}=${token}&state=${oAuthParams.state}&token_type=bearer`);
|
||||
}
|
||||
} else if (responseType === "link") {
|
||||
let from = innerParams.get("from");
|
||||
const oauth = innerParams.get("oauth");
|
||||
if (oauth) {
|
||||
from += `?oauth=${oauth}`;
|
||||
}
|
||||
Setting.goToLinkSoftOrJumpSelf(this, from);
|
||||
} else if (responseType === "saml") {
|
||||
if (res.data2.method === "POST") {
|
||||
this.setState({
|
||||
samlResponse: res.data,
|
||||
redirectUrl: res.data2.redirectUrl,
|
||||
relayState: oAuthParams.relayState,
|
||||
});
|
||||
} else {
|
||||
if (res.data3) {
|
||||
sessionStorage.setItem("signinUrl", signinUrl);
|
||||
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
|
||||
return;
|
||||
}
|
||||
const SAMLResponse = res.data;
|
||||
const redirectUri = res.data2.redirectUrl;
|
||||
Setting.goToLink(`${redirectUri}${redirectUri.includes("?") ? "&" : "?"}SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Setting.checkLoginMfa(res, body, oAuthParams, handleLogin, this, window.location.origin);
|
||||
this.handleOAuthLoginResult(res, body, innerParams, queryString, applicationName, this.getResponseType());
|
||||
} else {
|
||||
this.setState({
|
||||
msg: res.msg,
|
||||
|
||||
@@ -273,6 +273,10 @@ class SignupPage extends React.Component {
|
||||
values.education = values.education.join(", ");
|
||||
}
|
||||
|
||||
if (this.state.invitationCode && !values.invitationCode) {
|
||||
values.invitationCode = this.state.invitationCode;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
values.plan = params.get("plan");
|
||||
values.pricing = params.get("pricing");
|
||||
|
||||
@@ -130,10 +130,6 @@ export function getOAuthGetParameters(params) {
|
||||
}
|
||||
|
||||
let state = getRefinedValue(queries.get("state"));
|
||||
if (state.startsWith("/auth/oauth2/login.php?wantsurl=")) {
|
||||
// state contains URL param encoding for Moodle, URLSearchParams automatically decoded it, so here encode it again
|
||||
state = encodeURIComponent(state);
|
||||
}
|
||||
if (redirectUri.includes("#") && state === "") {
|
||||
state = getRawGetParameter("state");
|
||||
}
|
||||
|
||||
53
web/src/backend/RuleBackend.js
Normal file
53
web/src/backend/RuleBackend.js
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright 2023 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import * as Setting from "../Setting";
|
||||
|
||||
export function getRules(owner, page = "", pageSize = "", sortField = "", sortOrder = "") {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-rules?owner=${owner}&p=${page}&pageSize=${pageSize}&sortField=${sortField}&sortOrder=${sortOrder}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getRule(owner, name) {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-rule?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function addRule(rule) {
|
||||
return fetch(`${Setting.ServerUrl}/api/add-rule`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(rule),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function updateRule(owner, name, rule) {
|
||||
return fetch(`${Setting.ServerUrl}/api/update-rule?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(rule),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function deleteRule(rule) {
|
||||
return fetch(`${Setting.ServerUrl}/api/delete-rule`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(rule),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
63
web/src/backend/SiteBackend.js
Normal file
63
web/src/backend/SiteBackend.js
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright 2023 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import * as Setting from "../Setting";
|
||||
|
||||
export function getGlobalSites() {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-global-sites`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getSites(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-sites?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getSite(owner, name) {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-site?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function updateSite(owner, name, site) {
|
||||
const newSite = Setting.deepCopy(site);
|
||||
return fetch(`${Setting.ServerUrl}/api/update-site?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newSite),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function addSite(site) {
|
||||
const newSite = Setting.deepCopy(site);
|
||||
return fetch(`${Setting.ServerUrl}/api/add-site`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newSite),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function deleteSite(site) {
|
||||
const newSite = Setting.deepCopy(site);
|
||||
return fetch(`${Setting.ServerUrl}/api/delete-site`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newSite),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
192
web/src/common/CompoundRule.js
Normal file
192
web/src/common/CompoundRule.js
Normal file
@@ -0,0 +1,192 @@
|
||||
// Copyright 2023 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
|
||||
import {Button, Col, Row, Select, Table, Tooltip} from "antd";
|
||||
import {getRules} from "../backend/RuleBackend";
|
||||
import * as Setting from "../Setting";
|
||||
import i18next from "i18next";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
class CompoundRule extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
rules: [],
|
||||
defaultRules: [
|
||||
{
|
||||
name: "Start",
|
||||
operator: "begin",
|
||||
value: "rule1",
|
||||
},
|
||||
{
|
||||
name: "And",
|
||||
operator: "and",
|
||||
value: "rule2",
|
||||
},
|
||||
],
|
||||
};
|
||||
if (this.props.table.length === 0) {
|
||||
this.restore();
|
||||
}
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getRules();
|
||||
}
|
||||
|
||||
getRules() {
|
||||
getRules(this.props.owner).then((res) => {
|
||||
const rules = [];
|
||||
for (let i = 0; i < res.data.length; i++) {
|
||||
if (Setting.getItemId(res.data[i]) === this.props.owner + "/" + this.props.ruleName) {
|
||||
continue;
|
||||
}
|
||||
rules.push(Setting.getItemId(res.data[i]));
|
||||
}
|
||||
this.setState({
|
||||
rules: rules,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateTable(table) {
|
||||
this.props.onUpdateTable(table);
|
||||
}
|
||||
|
||||
updateField(table, index, key, value) {
|
||||
table[index][key] = value;
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
addRow(table) {
|
||||
const row = {name: `New Item - ${table.length}`, operator: "and", value: ""};
|
||||
if (table === undefined) {
|
||||
table = [];
|
||||
}
|
||||
|
||||
table = Setting.addRow(table, row);
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
deleteRow(table, i) {
|
||||
table = Setting.deleteRow(table, i);
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
upRow(table, i) {
|
||||
table = Setting.swapRow(table, i - 1, i);
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
downRow(table, i) {
|
||||
table = Setting.swapRow(table, i, i + 1);
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
restore() {
|
||||
this.updateTable(this.state.defaultRules);
|
||||
}
|
||||
|
||||
renderTable(table) {
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("rule:Logic"),
|
||||
dataIndex: "operator",
|
||||
key: "operator",
|
||||
width: "180px",
|
||||
render: (text, record, index) => {
|
||||
const options = [];
|
||||
if (index !== 0) {
|
||||
options.push({value: "and", text: i18next.t("rule:and")});
|
||||
options.push({value: "or", text: i18next.t("rule:or")});
|
||||
} else {
|
||||
options.push({value: "begin", text: i18next.t("rule:begin")});
|
||||
}
|
||||
return (
|
||||
<Select value={text} virtual={false} style={{width: "100%"}} onChange={value => {
|
||||
this.updateField(table, index, "operator", value);
|
||||
}}>
|
||||
{
|
||||
options.map((item, index) => <Option key={index} value={item.value}>{item.text}</Option>)
|
||||
}
|
||||
</Select>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("rule:Rule"),
|
||||
dataIndex: "value",
|
||||
key: "value",
|
||||
render: (text, record, index) => (
|
||||
<Select value={text} virtual={false} style={{width: "100%"}} onChange={value => {
|
||||
this.updateField(table, index, "value", value);
|
||||
}}>
|
||||
{
|
||||
this.state.rules.map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
}
|
||||
</Select>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
key: "action",
|
||||
width: "100px",
|
||||
render: (text, record, index) => (
|
||||
<div>
|
||||
<Tooltip placement="bottomLeft" title={"Up"}>
|
||||
<Button style={{marginRight: "5px"}} disabled={index === 0} icon={<UpOutlined />} size="small" onClick={() => this.upRow(table, index)} />
|
||||
</Tooltip>
|
||||
<Tooltip placement="topLeft" title={"Down"}>
|
||||
<Button style={{marginRight: "5px"}} disabled={index === table.length - 1} icon={<DownOutlined />} size="small" onClick={() => this.downRow(table, index)} />
|
||||
</Tooltip>
|
||||
<Tooltip placement="topLeft" title={"Delete"}>
|
||||
<Button icon={<DeleteOutlined />} size="small" onClick={() => this.deleteRow(table, index)} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Table rowKey="index" columns={columns} dataSource={table} size="middle" bordered pagination={false}
|
||||
title={() => (
|
||||
<div>
|
||||
{this.props.title}
|
||||
<Button style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button>
|
||||
<Button style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.restore()}>{i18next.t("general:Restore")}</Button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col span={24}>
|
||||
{
|
||||
this.renderTable(this.props.table)
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CompoundRule;
|
||||
@@ -37,6 +37,15 @@ export const NavItemTree = ({disabled, checkedKeys, defaultExpandedKeys, onCheck
|
||||
{title: i18next.t("general:Certs"), key: "/certs"},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Gateway"),
|
||||
key: "/sites-top",
|
||||
children: [
|
||||
{title: i18next.t("general:Certs"), key: "/certs"},
|
||||
{title: i18next.t("general:Rules"), key: "/rules"},
|
||||
{title: i18next.t("general:Sites"), key: "/sites"},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Authorization"),
|
||||
key: "/roles-top",
|
||||
|
||||
@@ -570,7 +570,7 @@
|
||||
"Physical": "Physisch",
|
||||
"Show all": "Alle anzeigen",
|
||||
"Virtual": "Virtuell",
|
||||
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "Sie müssen zuerst alle Untergruppen löschen. Sie können die Untergruppen im linken Gruppenbaum unter [Organisationen] -\u003e [Gruppen] anzeigen."
|
||||
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "Sie müssen zuerst alle Untergruppen löschen. Sie können die Untergruppen im linken Gruppenbaum unter [Organisationen] -> [Gruppen] anzeigen."
|
||||
},
|
||||
"home": {
|
||||
"New users past 30 days": "Neue Benutzer der letzten 30 Tage",
|
||||
@@ -770,6 +770,14 @@
|
||||
"Init score - Tooltip": "Anfangspunkte, die Benutzern bei der Registrierung vergeben werden",
|
||||
"Is profile public": "Ist das Profil öffentlich?",
|
||||
"Is profile public - Tooltip": "Nach der Schließung können nur globale Administratoren oder Benutzer in der gleichen Organisation auf die Profilseite des Benutzers zugreifen",
|
||||
"Kerberos KDC host": "Kerberos KDC host",
|
||||
"Kerberos KDC host - Tooltip": "The hostname of the Kerberos Key Distribution Center (e.g., kdc.example.com)",
|
||||
"Kerberos keytab": "Kerberos keytab",
|
||||
"Kerberos keytab - Tooltip": "The base64-encoded keytab file for the service principal",
|
||||
"Kerberos realm": "Kerberos realm",
|
||||
"Kerberos realm - Tooltip": "The Kerberos realm for IWA/SPNEGO authentication (e.g., EXAMPLE.COM)",
|
||||
"Kerberos service name": "Kerberos service name",
|
||||
"Kerberos service name - Tooltip": "The Kerberos service principal name (defaults to HTTP)",
|
||||
"Modify rule": "Regel ändern",
|
||||
"New Organization": "Neue Organisation",
|
||||
"Optional": "Optional",
|
||||
|
||||
@@ -570,7 +570,7 @@
|
||||
"Physical": "Physical",
|
||||
"Show all": "Show all",
|
||||
"Virtual": "Virtual",
|
||||
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page"
|
||||
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page"
|
||||
},
|
||||
"home": {
|
||||
"New users past 30 days": "New users past 30 days",
|
||||
@@ -770,6 +770,14 @@
|
||||
"Init score - Tooltip": "Initial score points awarded to users upon registration",
|
||||
"Is profile public": "Is profile public",
|
||||
"Is profile public - Tooltip": "After being closed, only global administrators or users in the same organization can access the user's profile page",
|
||||
"Kerberos KDC host": "Kerberos KDC host",
|
||||
"Kerberos KDC host - Tooltip": "The hostname of the Kerberos Key Distribution Center (e.g., kdc.example.com)",
|
||||
"Kerberos keytab": "Kerberos keytab",
|
||||
"Kerberos keytab - Tooltip": "The base64-encoded keytab file for the service principal",
|
||||
"Kerberos realm": "Kerberos realm",
|
||||
"Kerberos realm - Tooltip": "The Kerberos realm for IWA/SPNEGO authentication (e.g., EXAMPLE.COM)",
|
||||
"Kerberos service name": "Kerberos service name",
|
||||
"Kerberos service name - Tooltip": "The Kerberos service principal name (defaults to HTTP)",
|
||||
"Modify rule": "Modify rule",
|
||||
"New Organization": "New Organization",
|
||||
"Optional": "Optional",
|
||||
|
||||
@@ -570,7 +570,7 @@
|
||||
"Physical": "Físico",
|
||||
"Show all": "Mostrar todos",
|
||||
"Virtual": "Virtual",
|
||||
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "Necesitas eliminar todos los subgrupos primero. Puedes ver los subgrupos en el árbol de grupos a la izquierda en la página [Organizaciones] -\u003e [Grupos]"
|
||||
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "Necesitas eliminar todos los subgrupos primero. Puedes ver los subgrupos en el árbol de grupos a la izquierda en la página [Organizaciones] -> [Grupos]"
|
||||
},
|
||||
"home": {
|
||||
"New users past 30 days": "Nuevos usuarios en los últimos 30 días",
|
||||
@@ -770,6 +770,14 @@
|
||||
"Init score - Tooltip": "Puntos de puntuación inicial otorgados a los usuarios al registrarse",
|
||||
"Is profile public": "Es el perfil público",
|
||||
"Is profile public - Tooltip": "Después de estar cerrado, solo los administradores globales o usuarios de la misma organización pueden acceder a la página de perfil del usuario",
|
||||
"Kerberos KDC host": "Kerberos KDC host",
|
||||
"Kerberos KDC host - Tooltip": "The hostname of the Kerberos Key Distribution Center (e.g., kdc.example.com)",
|
||||
"Kerberos keytab": "Kerberos keytab",
|
||||
"Kerberos keytab - Tooltip": "The base64-encoded keytab file for the service principal",
|
||||
"Kerberos realm": "Kerberos realm",
|
||||
"Kerberos realm - Tooltip": "The Kerberos realm for IWA/SPNEGO authentication (e.g., EXAMPLE.COM)",
|
||||
"Kerberos service name": "Kerberos service name",
|
||||
"Kerberos service name - Tooltip": "The Kerberos service principal name (defaults to HTTP)",
|
||||
"Modify rule": "Modificar regla",
|
||||
"New Organization": "Nueva organización",
|
||||
"Optional": "Opcional",
|
||||
|
||||
@@ -570,7 +570,7 @@
|
||||
"Physical": "Physique",
|
||||
"Show all": "Afficher tout",
|
||||
"Virtual": "Virtuel",
|
||||
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "Vous devez d'abord supprimer tous les sous-groupes. Vous pouvez voir les sous-groupes dans l'arborescence des groupes à gauche de la page [Organisations] -\u003e [Groupes]"
|
||||
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "Vous devez d'abord supprimer tous les sous-groupes. Vous pouvez voir les sous-groupes dans l'arborescence des groupes à gauche de la page [Organisations] -> [Groupes]"
|
||||
},
|
||||
"home": {
|
||||
"New users past 30 days": "Nouveaux utilisateurs ces 30 derniers jours",
|
||||
@@ -770,6 +770,14 @@
|
||||
"Init score - Tooltip": "Score initial attribué au compte lors de leur inscription",
|
||||
"Is profile public": "Est-ce que le profil est public ?",
|
||||
"Is profile public - Tooltip": "Après sa fermeture, seuls les administrateurs et administratrices globales ou les comptes de la même organisation peuvent accéder à la page de profil de l'utilisateur",
|
||||
"Kerberos KDC host": "Kerberos KDC host",
|
||||
"Kerberos KDC host - Tooltip": "The hostname of the Kerberos Key Distribution Center (e.g., kdc.example.com)",
|
||||
"Kerberos keytab": "Kerberos keytab",
|
||||
"Kerberos keytab - Tooltip": "The base64-encoded keytab file for the service principal",
|
||||
"Kerberos realm": "Kerberos realm",
|
||||
"Kerberos realm - Tooltip": "The Kerberos realm for IWA/SPNEGO authentication (e.g., EXAMPLE.COM)",
|
||||
"Kerberos service name": "Kerberos service name",
|
||||
"Kerberos service name - Tooltip": "The Kerberos service principal name (defaults to HTTP)",
|
||||
"Modify rule": "Règle de modification",
|
||||
"New Organization": "Nouvelle organisation",
|
||||
"Optional": "Optionnel",
|
||||
|
||||
@@ -570,7 +570,7 @@
|
||||
"Physical": "物理",
|
||||
"Show all": "すべて表示",
|
||||
"Virtual": "仮想",
|
||||
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "最初にすべてのサブグループを削除する必要があります。[組織] -\u003e [グループ]ページの左側のグループツリーでサブグループを確認できます"
|
||||
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "最初にすべてのサブグループを削除する必要があります。[組織] -> [グループ]ページの左側のグループツリーでサブグループを確認できます"
|
||||
},
|
||||
"home": {
|
||||
"New users past 30 days": "過去30日間の新規ユーザー",
|
||||
@@ -770,6 +770,14 @@
|
||||
"Init score - Tooltip": "登録時にユーザーに与えられる初期スコアポイント",
|
||||
"Is profile public": "プロフィールは公開されていますか?",
|
||||
"Is profile public - Tooltip": "閉鎖された後、グローバル管理者または同じ組織のユーザーだけがユーザーのプロファイルページにアクセスできます",
|
||||
"Kerberos KDC host": "Kerberos KDC host",
|
||||
"Kerberos KDC host - Tooltip": "The hostname of the Kerberos Key Distribution Center (e.g., kdc.example.com)",
|
||||
"Kerberos keytab": "Kerberos keytab",
|
||||
"Kerberos keytab - Tooltip": "The base64-encoded keytab file for the service principal",
|
||||
"Kerberos realm": "Kerberos realm",
|
||||
"Kerberos realm - Tooltip": "The Kerberos realm for IWA/SPNEGO authentication (e.g., EXAMPLE.COM)",
|
||||
"Kerberos service name": "Kerberos service name",
|
||||
"Kerberos service name - Tooltip": "The Kerberos service principal name (defaults to HTTP)",
|
||||
"Modify rule": "ルールを変更する",
|
||||
"New Organization": "新しい組織",
|
||||
"Optional": "オプション",
|
||||
|
||||
@@ -570,7 +570,7 @@
|
||||
"Physical": "Fizyczna",
|
||||
"Show all": "Pokaż wszystko",
|
||||
"Virtual": "Wirtualna",
|
||||
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "Musisz najpierw usunąć wszystkie podgrupy. Możesz przeglądać podgrupy w lewym drzewie grup na stronie [Organizacje] -\u003e [Grupy]"
|
||||
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "Musisz najpierw usunąć wszystkie podgrupy. Możesz przeglądać podgrupy w lewym drzewie grup na stronie [Organizacje] -> [Grupy]"
|
||||
},
|
||||
"home": {
|
||||
"New users past 30 days": "Nowi użytkownicy w ciągu ostatnich 30 dni",
|
||||
@@ -770,6 +770,14 @@
|
||||
"Init score - Tooltip": "Początkowe punkty przyznawane użytkownikom po rejestracji",
|
||||
"Is profile public": "Profil publiczny",
|
||||
"Is profile public - Tooltip": "Po wyłączeniu tylko administratorzy globalni lub użytkownicy z tej samej organizacji mogą uzyskać dostęp do strony profilu użytkownika",
|
||||
"Kerberos KDC host": "Kerberos KDC host",
|
||||
"Kerberos KDC host - Tooltip": "The hostname of the Kerberos Key Distribution Center (e.g., kdc.example.com)",
|
||||
"Kerberos keytab": "Kerberos keytab",
|
||||
"Kerberos keytab - Tooltip": "The base64-encoded keytab file for the service principal",
|
||||
"Kerberos realm": "Kerberos realm",
|
||||
"Kerberos realm - Tooltip": "The Kerberos realm for IWA/SPNEGO authentication (e.g., EXAMPLE.COM)",
|
||||
"Kerberos service name": "Kerberos service name",
|
||||
"Kerberos service name - Tooltip": "The Kerberos service principal name (defaults to HTTP)",
|
||||
"Modify rule": "Zasada modyfikacji",
|
||||
"New Organization": "Nowa organizacja",
|
||||
"Optional": "Opcjonalne",
|
||||
|
||||
@@ -570,7 +570,7 @@
|
||||
"Physical": "Físico",
|
||||
"Show all": "Mostrar todos",
|
||||
"Virtual": "Virtual",
|
||||
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "Você precisa excluir todos os subgrupos primeiro. Você pode visualizar os subgrupos na árvore de grupos à esquerda na página [Organizações] -\u003e [Grupos]"
|
||||
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "Você precisa excluir todos os subgrupos primeiro. Você pode visualizar os subgrupos na árvore de grupos à esquerda na página [Organizações] -> [Grupos]"
|
||||
},
|
||||
"home": {
|
||||
"New users past 30 days": "Novos usuários nos últimos 30 dias",
|
||||
@@ -770,6 +770,14 @@
|
||||
"Init score - Tooltip": "Pontos de pontuação inicial concedidos aos usuários no momento do registro",
|
||||
"Is profile public": "Perfil é público",
|
||||
"Is profile public - Tooltip": "Após ser fechado, apenas administradores globais ou usuários na mesma organização podem acessar a página de perfil do usuário",
|
||||
"Kerberos KDC host": "Kerberos KDC host",
|
||||
"Kerberos KDC host - Tooltip": "The hostname of the Kerberos Key Distribution Center (e.g., kdc.example.com)",
|
||||
"Kerberos keytab": "Kerberos keytab",
|
||||
"Kerberos keytab - Tooltip": "The base64-encoded keytab file for the service principal",
|
||||
"Kerberos realm": "Kerberos realm",
|
||||
"Kerberos realm - Tooltip": "The Kerberos realm for IWA/SPNEGO authentication (e.g., EXAMPLE.COM)",
|
||||
"Kerberos service name": "Kerberos service name",
|
||||
"Kerberos service name - Tooltip": "The Kerberos service principal name (defaults to HTTP)",
|
||||
"Modify rule": "Modificar regra",
|
||||
"New Organization": "Nova Organização",
|
||||
"Optional": "Opcional",
|
||||
|
||||
@@ -570,7 +570,7 @@
|
||||
"Physical": "Fiziksel",
|
||||
"Show all": "Tümünü göster",
|
||||
"Virtual": "Sanal",
|
||||
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "Önce tüm alt grupları silmeniz gerekir. Alt grupları [Organizasyonlar] -\u003e [Gruplar] sayfasının sol grup ağacından görüntüleyebilirsiniz."
|
||||
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "Önce tüm alt grupları silmeniz gerekir. Alt grupları [Organizasyonlar] -> [Gruplar] sayfasının sol grup ağacından görüntüleyebilirsiniz."
|
||||
},
|
||||
"home": {
|
||||
"New users past 30 days": "Son 30 gündeki yeni kullanıcılar",
|
||||
@@ -770,6 +770,14 @@
|
||||
"Init score - Tooltip": "Kullanıcılara kayıt sırasında verilen başlangıç puanları",
|
||||
"Is profile public": "Profil genel mi",
|
||||
"Is profile public - Tooltip": "Kapatıldıktan sonra, yalnızca küresel yöneticiler veya aynı organizasyondaki kullanıcılar kullanıcının profil sayfasına erişebilir",
|
||||
"Kerberos KDC host": "Kerberos KDC host",
|
||||
"Kerberos KDC host - Tooltip": "The hostname of the Kerberos Key Distribution Center (e.g., kdc.example.com)",
|
||||
"Kerberos keytab": "Kerberos keytab",
|
||||
"Kerberos keytab - Tooltip": "The base64-encoded keytab file for the service principal",
|
||||
"Kerberos realm": "Kerberos realm",
|
||||
"Kerberos realm - Tooltip": "The Kerberos realm for IWA/SPNEGO authentication (e.g., EXAMPLE.COM)",
|
||||
"Kerberos service name": "Kerberos service name",
|
||||
"Kerberos service name - Tooltip": "The Kerberos service principal name (defaults to HTTP)",
|
||||
"Modify rule": "Kuralı Değiştir",
|
||||
"New Organization": "Yeni Organizasyon",
|
||||
"Optional": "İsteğe bağlı",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user