forked from casdoor/casdoor
feat: add Site and Rule to Casdoor (#5194)
This commit is contained in:
@@ -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
|
||||
193
controllers/rule.go
Normal file
193
controllers/rule.go
Normal file
@@ -0,0 +1,193 @@
|
||||
// Copyright 2023 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego/v2/server/web/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/hsluoyz/modsecurity-go/seclang/parser"
|
||||
)
|
||||
|
||||
func (c *ApiController) GetRules() {
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
if owner == "admin" {
|
||||
owner = ""
|
||||
}
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
rules, err := object.GetRules(owner)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(rules)
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
count, err := object.GetRuleCount(owner, field, value)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
rules, err := object.GetPaginationRules(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(rules, paginator.Nums())
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ApiController) GetRule() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
rule, err := object.GetRule(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(rule)
|
||||
}
|
||||
|
||||
func (c *ApiController) AddRule() {
|
||||
currentTime := util.GetCurrentTime()
|
||||
rule := object.Rule{
|
||||
CreatedTime: currentTime,
|
||||
UpdatedTime: currentTime,
|
||||
}
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &rule)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
err = checkExpressions(rule.Expressions, rule.Type)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = wrapActionResponse(object.AddRule(&rule))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *ApiController) UpdateRule() {
|
||||
var rule object.Rule
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &rule)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = checkExpressions(rule.Expressions, rule.Type)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
id := c.Ctx.Input.Query("id")
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateRule(id, &rule))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *ApiController) DeleteRule() {
|
||||
var rule object.Rule
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &rule)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteRule(&rule))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
func checkExpressions(expressions []*object.Expression, ruleType string) error {
|
||||
values := make([]string, len(expressions))
|
||||
for i, expression := range expressions {
|
||||
values[i] = expression.Value
|
||||
}
|
||||
switch ruleType {
|
||||
case "WAF":
|
||||
return checkWafRule(values)
|
||||
case "IP":
|
||||
return checkIpRule(values)
|
||||
case "IP Rate Limiting":
|
||||
return checkIpRateRule(expressions)
|
||||
case "Compound":
|
||||
return checkCompoundRules(values)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkWafRule(rules []string) error {
|
||||
for _, rule := range rules {
|
||||
scanner := parser.NewSecLangScannerFromString(rule)
|
||||
_, err := scanner.AllDirective()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkIpRule(ipLists []string) error {
|
||||
for _, ipList := range ipLists {
|
||||
for _, ip := range strings.Split(ipList, ",") {
|
||||
_, _, err := net.ParseCIDR(ip)
|
||||
if net.ParseIP(ip) == nil && err != nil {
|
||||
return errors.New("Invalid IP address: " + ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkIpRateRule(expressions []*object.Expression) error {
|
||||
if len(expressions) != 1 {
|
||||
return errors.New("IP Rate Limiting rule must have exactly one expression")
|
||||
}
|
||||
expression := expressions[0]
|
||||
_, err := util.ParseIntWithError(expression.Operator)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = util.ParseIntWithError(expression.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkCompoundRules(rules []string) error {
|
||||
_, err := object.GetRulesByRuleIds(rules)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
123
controllers/site.go
Normal file
123
controllers/site.go
Normal file
@@ -0,0 +1,123 @@
|
||||
// Copyright 2023 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/v2/server/web/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
func (c *ApiController) GetGlobalSites() {
|
||||
sites, err := object.GetGlobalSites()
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(object.GetMaskedSites(sites, util.GetHostname()))
|
||||
}
|
||||
|
||||
func (c *ApiController) GetSites() {
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
if owner == "admin" {
|
||||
owner = ""
|
||||
}
|
||||
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
sites, err := object.GetSites(owner)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.ResponseOk(object.GetMaskedSites(sites, util.GetHostname()))
|
||||
return
|
||||
}
|
||||
|
||||
limitInt := util.ParseInt(limit)
|
||||
count, err := object.GetSiteCount(owner, field, value)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limitInt, count)
|
||||
sites, err := object.GetPaginationSites(owner, paginator.Offset(), limitInt, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(object.GetMaskedSites(sites, util.GetHostname()), paginator.Nums())
|
||||
}
|
||||
|
||||
func (c *ApiController) GetSite() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
site, err := object.GetSite(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(object.GetMaskedSite(site, util.GetHostname()))
|
||||
}
|
||||
|
||||
func (c *ApiController) UpdateSite() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var site object.Site
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &site)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateSite(id, &site))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *ApiController) AddSite() {
|
||||
var site object.Site
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &site)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddSite(&site))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *ApiController) DeleteSite() {
|
||||
var site object.Site
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &site)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteSite(&site))
|
||||
c.ServeJSON()
|
||||
}
|
||||
17
go.mod
17
go.mod
@@ -23,6 +23,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,6 +31,7 @@ require (
|
||||
github.com/casdoor/oss v1.8.0
|
||||
github.com/casdoor/xorm-adapter/v3 v3.1.0
|
||||
github.com/casvisor/casvisor-go-sdk v1.4.0
|
||||
github.com/corazawaf/coraza/v3 v3.3.3
|
||||
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
|
||||
github.com/elimity-com/scim v0.0.0-20230426070224-941a5eac92f3
|
||||
github.com/fogleman/gg v1.3.0
|
||||
@@ -45,6 +47,7 @@ require (
|
||||
github.com/go-webauthn/webauthn v0.10.2
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hsluoyz/modsecurity-go v0.0.7
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/lestrrat-go/jwx v1.2.29
|
||||
github.com/lib/pq v1.10.9
|
||||
@@ -78,6 +81,7 @@ require (
|
||||
golang.org/x/net v0.41.0
|
||||
golang.org/x/oauth2 v0.27.0
|
||||
golang.org/x/text v0.27.0
|
||||
golang.org/x/time v0.8.0
|
||||
google.golang.org/api v0.215.0
|
||||
layeh.com/radius v0.0.0-20231213012653-1006025d24f8
|
||||
maunium.net/go/mautrix v0.22.1
|
||||
@@ -126,17 +130,17 @@ require (
|
||||
github.com/boombuler/barcode v1.0.1 // indirect
|
||||
github.com/bwmarrin/discordgo v0.28.1 // indirect
|
||||
github.com/caarlos0/go-reddit/v3 v3.0.1 // indirect
|
||||
github.com/casdoor/casdoor-go-sdk v0.50.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect
|
||||
github.com/corazawaf/libinjection-go v0.2.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||
github.com/cschomburg/go-pushbullet v0.0.0-20171206132031-67759df45fbb // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/dghubble/oauth1 v0.7.3 // indirect
|
||||
github.com/dghubble/sling v1.4.2 // indirect
|
||||
@@ -196,24 +200,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
|
||||
@@ -250,6 +256,7 @@ 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
|
||||
@@ -272,7 +279,6 @@ require (
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/time v0.8.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
|
||||
@@ -295,4 +301,5 @@ require (
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/token v1.0.1 // indirect
|
||||
rsc.io/binaryregexp v0.2.0 // indirect
|
||||
)
|
||||
|
||||
21
go.sum
21
go.sum
@@ -910,6 +910,10 @@ github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWH
|
||||
github.com/cncf/xds/go v0.0.0-20230310173818-32f1caf87195/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI=
|
||||
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||
github.com/corazawaf/coraza/v3 v3.3.3 h1:kqjStHAgWqwP5dh7n0vhTOF0a3t+VikNS/EaMiG0Fhk=
|
||||
github.com/corazawaf/coraza/v3 v3.3.3/go.mod h1:xSaXWOhFMSbrV8qOOfBKAyw3aOqfwaSaOy5BgSF8XlA=
|
||||
github.com/corazawaf/libinjection-go v0.2.2 h1:Chzodvb6+NXh6wew5/yhD0Ggioif9ACrQGR4qjTCs1g=
|
||||
github.com/corazawaf/libinjection-go v0.2.2/go.mod h1:OP4TM7xdJ2skyXqNX1AN1wN5nNZEmJNuWbNPOItn7aw=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
@@ -927,6 +931,8 @@ github.com/cznic/strutil v0.0.0-20171016134553-529a34b1c186/go.mod h1:AHHPPPXTw0
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f h1:q/DpyjJjZs94bziQ7YkBmIlpqbVP7yw179rnzoNVX1M=
|
||||
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f/go.mod h1:QGrK8vMWWHQYQ3QU9bw9Y9OPNfxccGzfb41qjvVeXtY=
|
||||
github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ=
|
||||
@@ -1296,6 +1302,8 @@ github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg
|
||||
github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
|
||||
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/hsluoyz/modsecurity-go v0.0.7 h1:W5ChaDrm4kM/UhHxoD2zyxQ+6s5kSj6cVftDFgdFzBM=
|
||||
github.com/hsluoyz/modsecurity-go v0.0.7/go.mod h1:hi81ySzwvlQFd5pip9c3uwXHDAW9ayxwLbt8ufxRkdY=
|
||||
github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo=
|
||||
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
@@ -1415,6 +1423,8 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2
|
||||
github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
|
||||
github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
|
||||
github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o=
|
||||
github.com/magefile/mage v1.15.1-0.20241126214340-bdc92f694516 h1:aAO0L0ulox6m/CLRYvJff+jWXYYCKGpEm3os7dM/Z+M=
|
||||
github.com/magefile/mage v1.15.1-0.20241126214340-bdc92f694516/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/markbates/going v1.0.0 h1:DQw0ZP7NbNlFGcKbcE/IVSOAFzScxRtLpd0rLMzLhq0=
|
||||
github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA=
|
||||
github.com/markbates/goth v1.82.0 h1:8j/c34AjBSTNzO7zTsOyP5IYCQCMBTRBHAbBt/PI0bQ=
|
||||
@@ -1450,6 +1460,8 @@ github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3N
|
||||
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 +1524,8 @@ github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/performancecopilot/speed/v4 v4.0.0/go.mod h1:qxrSyuDGrTOWfV+uKRFhfxw6h/4HXRGUiZiufxo49BM=
|
||||
github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4 h1:1Kw2vDBXmjop+LclnzCb/fFy+sgb3gYARwfmoUcQe6o=
|
||||
github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw=
|
||||
github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=
|
||||
github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||
github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||
@@ -1544,6 +1558,8 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgm
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/polarsource/polar-go v0.12.0 h1:um+6ftOPUMg2TQq9Kv/6fKGBOAl7dOc2YiDdx4Bb0y8=
|
||||
github.com/polarsource/polar-go v0.12.0/go.mod h1:FB11Q4m2n3wIk6l/POOkz0MVOUx1o0Yt4Y97MnQfe0c=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
@@ -1720,6 +1736,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/timtadh/data-structures v0.5.3/go.mod h1:9R4XODhJ8JdWFEI8P/HJKqxuJctfBQw6fDibMQny2oU=
|
||||
github.com/timtadh/lexmachine v0.2.2/go.mod h1:GBJvD5OAfRn/gnp92zb9KTgHLB7akKyxmVivoYCcjQI=
|
||||
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
|
||||
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
||||
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
|
||||
@@ -1741,6 +1759,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=
|
||||
@@ -2775,6 +2795,7 @@ modernc.org/z v1.0.1-0.20210308123920-1f282aa71362/go.mod h1:8/SRk5C/HgiQWCgXdfp
|
||||
modernc.org/z v1.0.1/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA=
|
||||
modernc.org/z v1.5.1 h1:RTNHdsrOpeoSeOF4FbzTo8gBYByaJ5xT7NgZ9ZqRiJM=
|
||||
modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=
|
||||
rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
|
||||
43
ip/ip.go
Normal file
43
ip/ip.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright 2024 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ip
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
func InitIpDb() {
|
||||
err := Init("ip/17monipdb.dat")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func IsAbroadIp(ip string) bool {
|
||||
// If it's an intranet IP, it's not abroad
|
||||
if util.IsIntranetIp(ip) {
|
||||
return false
|
||||
}
|
||||
|
||||
info, err := Find(ip)
|
||||
if err != nil {
|
||||
fmt.Printf("error: ip = %s, error = %s\n", ip, err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
return info.Country != "中国"
|
||||
}
|
||||
199
ip/ip17mon.go
Normal file
199
ip/ip17mon.go
Normal file
@@ -0,0 +1,199 @@
|
||||
// Copyright 2022 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ip
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
)
|
||||
|
||||
const Null = "N/A"
|
||||
|
||||
var (
|
||||
ErrInvalidIp = errors.New("invalid ip format")
|
||||
std *Locator
|
||||
)
|
||||
|
||||
// Init default locator with dataFile
|
||||
func Init(dataFile string) (err error) {
|
||||
if std != nil {
|
||||
return
|
||||
}
|
||||
std, err = NewLocator(dataFile)
|
||||
return
|
||||
}
|
||||
|
||||
// Init default locator with data
|
||||
func InitWithData(data []byte) {
|
||||
if std != nil {
|
||||
return
|
||||
}
|
||||
std = NewLocatorWithData(data)
|
||||
return
|
||||
}
|
||||
|
||||
// Find locationInfo by ip string
|
||||
// It will return err when ipstr is not a valid format
|
||||
func Find(ipstr string) (*LocationInfo, error) {
|
||||
return std.Find(ipstr)
|
||||
}
|
||||
|
||||
// Find locationInfo by uint32
|
||||
func FindByUint(ip uint32) *LocationInfo {
|
||||
return std.FindByUint(ip)
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
// New locator with dataFile
|
||||
func NewLocator(dataFile string) (loc *Locator, err error) {
|
||||
data, err := ioutil.ReadFile(dataFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
loc = NewLocatorWithData(data)
|
||||
return
|
||||
}
|
||||
|
||||
// New locator with data
|
||||
func NewLocatorWithData(data []byte) (loc *Locator) {
|
||||
loc = new(Locator)
|
||||
loc.init(data)
|
||||
return
|
||||
}
|
||||
|
||||
type Locator struct {
|
||||
textData []byte
|
||||
indexData1 []uint32
|
||||
indexData2 []int
|
||||
indexData3 []int
|
||||
index []int
|
||||
}
|
||||
|
||||
type LocationInfo struct {
|
||||
Country string
|
||||
Region string
|
||||
City string
|
||||
Isp string
|
||||
}
|
||||
|
||||
// Find locationInfo by ip string
|
||||
// It will return err when ipstr is not a valid format
|
||||
func (loc *Locator) Find(ipstr string) (info *LocationInfo, err error) {
|
||||
ip := net.ParseIP(ipstr).To4()
|
||||
if ip == nil || ip.To4() == nil {
|
||||
err = ErrInvalidIp
|
||||
return
|
||||
}
|
||||
info = loc.FindByUint(binary.BigEndian.Uint32([]byte(ip)))
|
||||
return
|
||||
}
|
||||
|
||||
// Find locationInfo by uint32
|
||||
func (loc *Locator) FindByUint(ip uint32) (info *LocationInfo) {
|
||||
end := len(loc.indexData1) - 1
|
||||
if ip>>24 != 0xff {
|
||||
end = loc.index[(ip>>24)+1]
|
||||
}
|
||||
idx := loc.findIndexOffset(ip, loc.index[ip>>24], end)
|
||||
off := loc.indexData2[idx]
|
||||
return newLocationInfo(loc.textData[off : off+loc.indexData3[idx]])
|
||||
}
|
||||
|
||||
// binary search
|
||||
func (loc *Locator) findIndexOffset(ip uint32, start, end int) int {
|
||||
for start < end {
|
||||
mid := (start + end) / 2
|
||||
if ip > loc.indexData1[mid] {
|
||||
start = mid + 1
|
||||
} else {
|
||||
end = mid
|
||||
}
|
||||
}
|
||||
|
||||
if loc.indexData1[end] >= ip {
|
||||
return end
|
||||
}
|
||||
|
||||
return start
|
||||
}
|
||||
|
||||
func (loc *Locator) init(data []byte) {
|
||||
textoff := int(binary.BigEndian.Uint32(data[:4]))
|
||||
|
||||
loc.textData = data[textoff-1024:]
|
||||
|
||||
loc.index = make([]int, 256)
|
||||
for i := 0; i < 256; i++ {
|
||||
off := 4 + i*4
|
||||
loc.index[i] = int(binary.LittleEndian.Uint32(data[off : off+4]))
|
||||
}
|
||||
|
||||
nidx := (textoff - 4 - 1024 - 1024) / 8
|
||||
|
||||
loc.indexData1 = make([]uint32, nidx)
|
||||
loc.indexData2 = make([]int, nidx)
|
||||
loc.indexData3 = make([]int, nidx)
|
||||
|
||||
for i := 0; i < nidx; i++ {
|
||||
off := 4 + 1024 + i*8
|
||||
loc.indexData1[i] = binary.BigEndian.Uint32(data[off : off+4])
|
||||
loc.indexData2[i] = int(uint32(data[off+4]) | uint32(data[off+5])<<8 | uint32(data[off+6])<<16)
|
||||
loc.indexData3[i] = int(data[off+7])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func newLocationInfo(str []byte) *LocationInfo {
|
||||
var info *LocationInfo
|
||||
|
||||
fields := bytes.Split(str, []byte("\t"))
|
||||
switch len(fields) {
|
||||
case 4:
|
||||
// free version
|
||||
info = &LocationInfo{
|
||||
Country: string(fields[0]),
|
||||
Region: string(fields[1]),
|
||||
City: string(fields[2]),
|
||||
}
|
||||
case 5:
|
||||
// pay version
|
||||
info = &LocationInfo{
|
||||
Country: string(fields[0]),
|
||||
Region: string(fields[1]),
|
||||
City: string(fields[2]),
|
||||
Isp: string(fields[4]),
|
||||
}
|
||||
default:
|
||||
panic("unexpected ip info:" + string(str))
|
||||
}
|
||||
|
||||
if len(info.Country) == 0 {
|
||||
info.Country = Null
|
||||
}
|
||||
if len(info.Region) == 0 {
|
||||
info.Region = Null
|
||||
}
|
||||
if len(info.City) == 0 {
|
||||
info.City = Null
|
||||
}
|
||||
if len(info.Isp) == 0 {
|
||||
info.Isp = Null
|
||||
}
|
||||
return info
|
||||
}
|
||||
9
main.go
9
main.go
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/casdoor/casdoor/proxy"
|
||||
"github.com/casdoor/casdoor/radius"
|
||||
"github.com/casdoor/casdoor/routers"
|
||||
"github.com/casdoor/casdoor/service"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
@@ -73,6 +74,10 @@ func main() {
|
||||
object.InitCasvisorConfig()
|
||||
object.InitCleanupTokens()
|
||||
|
||||
object.InitSiteMap()
|
||||
object.InitRuleMap()
|
||||
object.StartMonitorSitesLoop()
|
||||
|
||||
util.SafeGoroutine(func() { object.RunSyncUsersJob() })
|
||||
util.SafeGoroutine(func() { controllers.InitCLIDownloader() })
|
||||
|
||||
@@ -126,5 +131,9 @@ func main() {
|
||||
go radius.StartRadiusServer()
|
||||
go object.ClearThroughputPerSecond()
|
||||
|
||||
if len(object.SiteMap) != 0 {
|
||||
service.Start()
|
||||
}
|
||||
|
||||
web.Run(fmt.Sprintf(":%v", port))
|
||||
}
|
||||
|
||||
@@ -164,6 +164,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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
@@ -126,6 +126,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")
|
||||
|
||||
112
rule/rule.go
Normal file
112
rule/rule.go
Normal file
@@ -0,0 +1,112 @@
|
||||
// Copyright 2024 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
type Rule interface {
|
||||
checkRule(expressions []*object.Expression, req *http.Request) (*RuleResult, error)
|
||||
}
|
||||
|
||||
type RuleResult struct {
|
||||
Action string
|
||||
StatusCode int
|
||||
Reason string
|
||||
}
|
||||
|
||||
func CheckRules(ruleIds []string, r *http.Request) (*RuleResult, error) {
|
||||
rules, err := object.GetRulesByRuleIds(ruleIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, rule := range rules {
|
||||
var ruleObj Rule
|
||||
switch rule.Type {
|
||||
case "User-Agent":
|
||||
ruleObj = &UaRule{}
|
||||
case "IP":
|
||||
ruleObj = &IpRule{}
|
||||
case "WAF":
|
||||
ruleObj = &WafRule{}
|
||||
case "IP Rate Limiting":
|
||||
ruleObj = &IpRateRule{
|
||||
ruleName: rule.GetId(),
|
||||
}
|
||||
case "Compound":
|
||||
ruleObj = &CompoundRule{}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown rule type: %s for rule: %s", rule.Type, rule.GetId())
|
||||
}
|
||||
|
||||
result, err := ruleObj.checkRule(rule.Expressions, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if result != nil {
|
||||
// Use rule's action if no action specified by the rule check
|
||||
if result.Action == "" {
|
||||
result.Action = rule.Action
|
||||
}
|
||||
|
||||
// Determine status code
|
||||
if result.StatusCode == 0 {
|
||||
if rule.StatusCode != 0 {
|
||||
result.StatusCode = rule.StatusCode
|
||||
} else {
|
||||
// Set default status codes if not specified
|
||||
switch result.Action {
|
||||
case "Block":
|
||||
result.StatusCode = 403
|
||||
case "Drop":
|
||||
result.StatusCode = 400
|
||||
case "Allow":
|
||||
result.StatusCode = 200
|
||||
case "CAPTCHA":
|
||||
result.StatusCode = 302
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown rule action: %s for rule: %s", result.Action, rule.GetId())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update reason if rule has custom reason
|
||||
if result.Action == "Block" || result.Action == "Drop" {
|
||||
if rule.IsVerbose {
|
||||
// Add verbose debug info with rule name and triggered expression
|
||||
result.Reason = util.GenerateVerboseReason(rule.GetId(), result.Reason, rule.Reason)
|
||||
} else if rule.Reason != "" {
|
||||
result.Reason = rule.Reason
|
||||
} else if result.Reason != "" {
|
||||
result.Reason = fmt.Sprintf("hit rule %s: %s", ruleIds[i], result.Reason)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Default action if no rule matched
|
||||
return &RuleResult{
|
||||
Action: "Allow",
|
||||
StatusCode: 200,
|
||||
}, nil
|
||||
}
|
||||
60
rule/rule_compound.go
Normal file
60
rule/rule_compound.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright 2024 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
type CompoundRule struct{}
|
||||
|
||||
func (r *CompoundRule) checkRule(expressions []*object.Expression, req *http.Request) (*RuleResult, error) {
|
||||
operators := util.NewStack()
|
||||
res := true
|
||||
for _, expression := range expressions {
|
||||
isHit := true
|
||||
result, err := CheckRules([]string{expression.Value}, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result == nil || result.Action == "" {
|
||||
isHit = false
|
||||
}
|
||||
switch expression.Operator {
|
||||
case "and", "begin":
|
||||
res = res && isHit
|
||||
case "or":
|
||||
operators.Push(res)
|
||||
res = isHit
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown operator: %s", expression.Operator)
|
||||
}
|
||||
if operators.Size() > 0 {
|
||||
last, ok := operators.Pop()
|
||||
for ok {
|
||||
res = last.(bool) || res
|
||||
last, ok = operators.Pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
if res {
|
||||
return &RuleResult{}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
94
rule/rule_ip.go
Normal file
94
rule/rule_ip.go
Normal file
@@ -0,0 +1,94 @@
|
||||
// Copyright 2024 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/ip"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
type IpRule struct{}
|
||||
|
||||
func (r *IpRule) checkRule(expressions []*object.Expression, req *http.Request) (*RuleResult, error) {
|
||||
clientIp := util.GetClientIp(req)
|
||||
netIp, err := parseIp(clientIp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, expression := range expressions {
|
||||
reason := fmt.Sprintf("expression matched: \"%s %s %s\"", clientIp, expression.Operator, expression.Value)
|
||||
|
||||
// Handle "is abroad" operator
|
||||
if expression.Operator == "is abroad" {
|
||||
if ip.IsAbroadIp(clientIp) {
|
||||
return &RuleResult{Reason: reason}, nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
ips := strings.Split(expression.Value, ",")
|
||||
for _, ipStr := range ips {
|
||||
if strings.Contains(ipStr, "/") {
|
||||
_, ipNet, err := net.ParseCIDR(ipStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch expression.Operator {
|
||||
case "is in":
|
||||
if ipNet.Contains(netIp) {
|
||||
return &RuleResult{Reason: reason}, nil
|
||||
}
|
||||
case "is not in":
|
||||
if !ipNet.Contains(netIp) {
|
||||
return &RuleResult{Reason: reason}, nil
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown operator: %s", expression.Operator)
|
||||
}
|
||||
} else if strings.ContainsAny(ipStr, ".:") {
|
||||
switch expression.Operator {
|
||||
case "is in":
|
||||
if ipStr == clientIp {
|
||||
return &RuleResult{Reason: reason}, nil
|
||||
}
|
||||
case "is not in":
|
||||
if ipStr != clientIp {
|
||||
return &RuleResult{Reason: reason}, nil
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown operator: %s", expression.Operator)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("unknown IP or CIDR format: %s", ipStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func parseIp(ipStr string) (net.IP, error) {
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
return nil, fmt.Errorf("unknown IP or CIDR format: %s", ipStr)
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
134
rule/rule_ip_rate.go
Normal file
134
rule/rule_ip_rate.go
Normal file
@@ -0,0 +1,134 @@
|
||||
// Copyright 2024 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rule
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
type IpRateRule struct {
|
||||
ruleName string
|
||||
}
|
||||
|
||||
type IpRateLimiter struct {
|
||||
ips map[string]*rate.Limiter
|
||||
mu *sync.RWMutex
|
||||
r rate.Limit
|
||||
b int
|
||||
}
|
||||
|
||||
var blackList = map[string]map[string]time.Time{}
|
||||
|
||||
var ipRateLimiters = map[string]*IpRateLimiter{}
|
||||
|
||||
// NewIpRateLimiter .
|
||||
func NewIpRateLimiter(r rate.Limit, b int) *IpRateLimiter {
|
||||
i := &IpRateLimiter{
|
||||
ips: make(map[string]*rate.Limiter),
|
||||
mu: &sync.RWMutex{},
|
||||
r: r,
|
||||
b: b,
|
||||
}
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
// AddIP creates a new rate limiter and adds it to the ips map,
|
||||
// using the IP address as the key
|
||||
func (i *IpRateLimiter) AddIP(ip string) *rate.Limiter {
|
||||
i.mu.Lock()
|
||||
defer i.mu.Unlock()
|
||||
|
||||
limiter := rate.NewLimiter(i.r, i.b)
|
||||
|
||||
i.ips[ip] = limiter
|
||||
|
||||
return limiter
|
||||
}
|
||||
|
||||
// GetLimiter returns the rate limiter for the provided IP address if it exists.
|
||||
// Otherwise, calls AddIP to add IP address to the map
|
||||
func (i *IpRateLimiter) GetLimiter(ip string) *rate.Limiter {
|
||||
i.mu.Lock()
|
||||
limiter, exists := i.ips[ip]
|
||||
|
||||
if !exists {
|
||||
i.mu.Unlock()
|
||||
return i.AddIP(ip)
|
||||
}
|
||||
|
||||
i.mu.Unlock()
|
||||
|
||||
return limiter
|
||||
}
|
||||
|
||||
func (r *IpRateRule) checkRule(expressions []*object.Expression, req *http.Request) (*RuleResult, error) {
|
||||
expression := expressions[0] // IpRate rule should have only one expression
|
||||
clientIp := util.GetClientIp(req)
|
||||
|
||||
// If the client IP is in the blacklist, check the block time
|
||||
createAt, ok := blackList[r.ruleName][clientIp]
|
||||
if ok {
|
||||
blockTime := util.ParseInt(expression.Value)
|
||||
if time.Now().Sub(createAt) < time.Duration(blockTime)*time.Second {
|
||||
return &RuleResult{
|
||||
Action: "Block",
|
||||
Reason: "Rate limit exceeded",
|
||||
}, nil
|
||||
} else {
|
||||
delete(blackList[r.ruleName], clientIp)
|
||||
}
|
||||
}
|
||||
|
||||
// If the client IP is not in the blacklist, check the rate limit
|
||||
ipRateLimiter := ipRateLimiters[r.ruleName]
|
||||
parseInt := util.ParseInt(expression.Operator)
|
||||
if ipRateLimiter == nil {
|
||||
ipRateLimiter = NewIpRateLimiter(rate.Limit(parseInt), parseInt)
|
||||
ipRateLimiters[r.ruleName] = ipRateLimiter
|
||||
}
|
||||
|
||||
// If the rate limit has changed, update the rate limiter
|
||||
limiter := ipRateLimiter.GetLimiter(clientIp)
|
||||
if ipRateLimiter.r != rate.Limit(parseInt) {
|
||||
ipRateLimiter.r = rate.Limit(parseInt)
|
||||
ipRateLimiter.b = parseInt
|
||||
limiter.SetLimit(ipRateLimiter.r)
|
||||
limiter.SetBurst(ipRateLimiter.b)
|
||||
err := limiter.Wait(req.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// If the rate limit is exceeded, add the client IP to the blacklist
|
||||
allow := limiter.Allow()
|
||||
if !allow {
|
||||
blackList[r.ruleName] = map[string]time.Time{}
|
||||
blackList[r.ruleName][clientIp] = time.Now()
|
||||
return &RuleResult{
|
||||
Action: "Block",
|
||||
Reason: "Rate limit exceeded",
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
154
rule/rule_ip_rate_test.go
Normal file
154
rule/rule_ip_rate_test.go
Normal file
@@ -0,0 +1,154 @@
|
||||
// Copyright 2023 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rule
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
)
|
||||
|
||||
func TestIpRateRule_checkRule(t *testing.T) {
|
||||
type fields struct {
|
||||
ruleName string
|
||||
}
|
||||
type args struct {
|
||||
args []struct {
|
||||
expressions []*object.Expression
|
||||
req *http.Request
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want []bool
|
||||
want1 []string
|
||||
want2 []string
|
||||
wantErr []bool
|
||||
}{
|
||||
{
|
||||
name: "Test 1",
|
||||
fields: fields{
|
||||
ruleName: "rule1",
|
||||
},
|
||||
args: args{
|
||||
args: []struct {
|
||||
expressions []*object.Expression
|
||||
req *http.Request
|
||||
}{
|
||||
{
|
||||
expressions: []*object.Expression{
|
||||
{
|
||||
Operator: "1",
|
||||
Value: "1",
|
||||
},
|
||||
},
|
||||
req: &http.Request{
|
||||
RemoteAddr: "127.0.0.1",
|
||||
},
|
||||
},
|
||||
{
|
||||
expressions: []*object.Expression{
|
||||
{
|
||||
Operator: "1",
|
||||
Value: "1",
|
||||
},
|
||||
},
|
||||
req: &http.Request{
|
||||
RemoteAddr: "127.0.0.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []bool{false, true},
|
||||
want1: []string{"", "Block"},
|
||||
want2: []string{"", "Rate limit exceeded"},
|
||||
wantErr: []bool{false, false},
|
||||
},
|
||||
{
|
||||
name: "Test 2",
|
||||
fields: fields{
|
||||
ruleName: "rule2",
|
||||
},
|
||||
args: args{
|
||||
args: []struct {
|
||||
expressions []*object.Expression
|
||||
req *http.Request
|
||||
}{
|
||||
{
|
||||
expressions: []*object.Expression{
|
||||
{
|
||||
Operator: "1",
|
||||
Value: "1",
|
||||
},
|
||||
},
|
||||
req: &http.Request{
|
||||
RemoteAddr: "127.0.0.1",
|
||||
},
|
||||
},
|
||||
{
|
||||
expressions: []*object.Expression{
|
||||
{
|
||||
Operator: "10",
|
||||
Value: "1",
|
||||
},
|
||||
},
|
||||
req: &http.Request{
|
||||
RemoteAddr: "127.0.0.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []bool{false, false},
|
||||
want1: []string{"", ""},
|
||||
want2: []string{"", ""},
|
||||
wantErr: []bool{false, false},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &IpRateRule{
|
||||
ruleName: tt.fields.ruleName,
|
||||
}
|
||||
for i, arg := range tt.args.args {
|
||||
result, err := r.checkRule(arg.expressions, arg.req)
|
||||
if (err != nil) != tt.wantErr[i] {
|
||||
t.Errorf("checkRule() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
got := result != nil
|
||||
got1 := ""
|
||||
got2 := ""
|
||||
if result != nil {
|
||||
got1 = result.Action
|
||||
got2 = result.Reason
|
||||
}
|
||||
if got != tt.want[i] {
|
||||
t.Errorf("checkRule() got = %v, want %v", got, tt.want[i])
|
||||
}
|
||||
if got1 != tt.want1[i] {
|
||||
t.Errorf("checkRule() got1 = %v, want %v", got1, tt.want1[i])
|
||||
}
|
||||
if got2 != tt.want2[i] {
|
||||
t.Errorf("checkRule() got2 = %v, want %v", got2, tt.want2[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
63
rule/rule_ua.go
Normal file
63
rule/rule_ua.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright 2024 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
)
|
||||
|
||||
type UaRule struct{}
|
||||
|
||||
func (r *UaRule) checkRule(expressions []*object.Expression, req *http.Request) (*RuleResult, error) {
|
||||
userAgent := req.UserAgent()
|
||||
for _, expression := range expressions {
|
||||
ua := expression.Value
|
||||
reason := fmt.Sprintf("expression matched: \"%s %s %s\"", userAgent, expression.Operator, expression.Value)
|
||||
switch expression.Operator {
|
||||
case "contains":
|
||||
if strings.Contains(userAgent, ua) {
|
||||
return &RuleResult{Reason: reason}, nil
|
||||
}
|
||||
case "does not contain":
|
||||
if !strings.Contains(userAgent, ua) {
|
||||
return &RuleResult{Reason: reason}, nil
|
||||
}
|
||||
case "equals":
|
||||
if userAgent == ua {
|
||||
return &RuleResult{Reason: reason}, nil
|
||||
}
|
||||
case "does not equal":
|
||||
if strings.Compare(userAgent, ua) != 0 {
|
||||
return &RuleResult{Reason: reason}, nil
|
||||
}
|
||||
case "match":
|
||||
// regex match
|
||||
isHit, err := regexp.MatchString(ua, userAgent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isHit {
|
||||
return &RuleResult{Reason: reason}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
105
rule/rule_waf.go
Normal file
105
rule/rule_waf.go
Normal file
@@ -0,0 +1,105 @@
|
||||
// Copyright 2024 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/corazawaf/coraza/v3"
|
||||
"github.com/corazawaf/coraza/v3/types"
|
||||
"github.com/hsluoyz/modsecurity-go/seclang/parser"
|
||||
)
|
||||
|
||||
type WafRule struct{}
|
||||
|
||||
func (r *WafRule) checkRule(expressions []*object.Expression, req *http.Request) (*RuleResult, error) {
|
||||
var ruleStr string
|
||||
for _, expression := range expressions {
|
||||
ruleStr += expression.Value
|
||||
}
|
||||
waf, err := coraza.NewWAF(
|
||||
coraza.NewWAFConfig().
|
||||
WithErrorCallback(logError).
|
||||
WithDirectives(conf.WafConf).
|
||||
WithDirectives(ruleStr),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create WAF failed")
|
||||
}
|
||||
tx := waf.NewTransaction()
|
||||
processRequest(tx, req)
|
||||
matchedRules := tx.MatchedRules()
|
||||
for _, matchedRule := range matchedRules {
|
||||
rule := matchedRule.Rule()
|
||||
directive, err := parser.NewSecLangScannerFromString(rule.Raw()).AllDirective()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, d := range directive {
|
||||
ruleDirective := d.(*parser.RuleDirective)
|
||||
for _, action := range ruleDirective.Actions.Action {
|
||||
switch action.Tk {
|
||||
case parser.TkActionBlock, parser.TkActionDeny:
|
||||
return &RuleResult{
|
||||
Action: "Block",
|
||||
Reason: fmt.Sprintf("blocked by WAF rule: %d", rule.ID()),
|
||||
}, nil
|
||||
case parser.TkActionAllow:
|
||||
return &RuleResult{
|
||||
Action: "Allow",
|
||||
}, nil
|
||||
case parser.TkActionDrop:
|
||||
return &RuleResult{
|
||||
Action: "Drop",
|
||||
Reason: fmt.Sprintf("dropped by WAF rule: %d", rule.ID()),
|
||||
}, nil
|
||||
default:
|
||||
// skip other actions
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func processRequest(tx types.Transaction, req *http.Request) {
|
||||
// Process URI and method
|
||||
tx.ProcessURI(req.URL.String(), req.Method, req.Proto)
|
||||
|
||||
// Process request headers
|
||||
for key, values := range req.Header {
|
||||
for _, value := range values {
|
||||
tx.AddRequestHeader(key, value)
|
||||
}
|
||||
}
|
||||
tx.ProcessRequestHeaders()
|
||||
|
||||
// Process request body (if any)
|
||||
if req.Body != nil {
|
||||
_, err := tx.ProcessRequestBody()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func logError(error types.MatchedRule) {
|
||||
msg := error.ErrorLog()
|
||||
fmt.Printf("[WAFlogError][%s] %s\n", error.Rule().Severity(), msg)
|
||||
}
|
||||
90
service/oauth.go
Normal file
90
service/oauth.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// Copyright 2023 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
func getSigninUrl(casdoorClient *casdoorsdk.Client, callbackUrl string, originalPath string) string {
|
||||
scope := "read"
|
||||
return fmt.Sprintf("%s/login/oauth/authorize?client_id=%s&response_type=code&redirect_uri=%s&scope=%s&state=%s",
|
||||
casdoorClient.Endpoint, casdoorClient.ClientId, url.QueryEscape(callbackUrl), scope, url.QueryEscape(originalPath))
|
||||
}
|
||||
|
||||
func redirectToCasdoor(casdoorClient *casdoorsdk.Client, w http.ResponseWriter, r *http.Request) {
|
||||
scheme := getScheme(r)
|
||||
|
||||
callbackUrl := fmt.Sprintf("%s://%s/caswaf-handler", scheme, r.Host)
|
||||
originalPath := r.RequestURI
|
||||
signinUrl := getSigninUrl(casdoorClient, callbackUrl, originalPath)
|
||||
http.Redirect(w, r, signinUrl, http.StatusFound)
|
||||
}
|
||||
|
||||
func handleAuthCallback(w http.ResponseWriter, r *http.Request) {
|
||||
site := getSiteByDomainWithWww(r.Host)
|
||||
if site == nil {
|
||||
responseError(w, "CasWAF error: site not found for host: %s", r.Host)
|
||||
return
|
||||
}
|
||||
|
||||
code := r.URL.Query().Get("code")
|
||||
state := r.URL.Query().Get("state")
|
||||
if code == "" {
|
||||
responseError(w, "CasWAF error: the code should not be empty")
|
||||
return
|
||||
} else if state == "" {
|
||||
responseError(w, "CasWAF error: the state should not be empty")
|
||||
return
|
||||
}
|
||||
|
||||
application, err := object.GetApplication(util.GetId(site.Owner, site.CasdoorApplication))
|
||||
if err != nil {
|
||||
responseError(w, "CasWAF error: casdoorClient.GetOAuthToken() error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//casdoorClient, err := getCasdoorClientFromSite(site)
|
||||
//if err != nil {
|
||||
// responseError(w, "CasWAF error: getCasdoorClientFromSite() error: %s", err.Error())
|
||||
// return
|
||||
//}
|
||||
|
||||
token, tokenError, err := object.GetAuthorizationCodeToken(application, application.ClientSecret, code, "", "")
|
||||
if tokenError != nil {
|
||||
responseError(w, "CasWAF error: casdoorClient.GetOAuthToken() error: %s", tokenError.Error)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
responseError(w, "CasWAF error: casdoorClient.GetOAuthToken() error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
cookie := &http.Cookie{
|
||||
Name: "casdoor_access_token",
|
||||
Value: token.AccessToken,
|
||||
Path: "/",
|
||||
}
|
||||
http.SetCookie(w, cookie)
|
||||
|
||||
originalPath := state
|
||||
http.Redirect(w, r, originalPath, http.StatusFound)
|
||||
}
|
||||
372
service/proxy.go
Normal file
372
service/proxy.go
Normal file
@@ -0,0 +1,372 @@
|
||||
// Copyright 2023 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/rule"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/casvisor/casvisor-go-sdk/casvisorsdk"
|
||||
)
|
||||
|
||||
func forwardHandler(targetUrl string, writer http.ResponseWriter, request *http.Request) {
|
||||
target, err := url.Parse(targetUrl)
|
||||
|
||||
if nil != err {
|
||||
panic(err)
|
||||
return
|
||||
}
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(target)
|
||||
proxy.Director = func(r *http.Request) {
|
||||
r.URL = target
|
||||
|
||||
if clientIP, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
|
||||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" && xff != clientIP {
|
||||
newXff := fmt.Sprintf("%s, %s", xff, clientIP)
|
||||
// r.Header.Set("X-Forwarded-For", newXff)
|
||||
r.Header.Set("X-Real-Ip", newXff)
|
||||
} else {
|
||||
// r.Header.Set("X-Forwarded-For", clientIP)
|
||||
r.Header.Set("X-Real-Ip", clientIP)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
proxy.ModifyResponse = func(resp *http.Response) error {
|
||||
// Add Secure flag to all Set-Cookie headers in HTTPS responses
|
||||
if request.TLS != nil {
|
||||
// Add HSTS header for HTTPS responses if not already set by backend
|
||||
if resp.Header.Get("Strict-Transport-Security") == "" {
|
||||
resp.Header.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
|
||||
}
|
||||
|
||||
cookies := resp.Header["Set-Cookie"]
|
||||
if len(cookies) > 0 {
|
||||
// Clear existing Set-Cookie headers
|
||||
resp.Header.Del("Set-Cookie")
|
||||
// Add them back with Secure flag if not already present
|
||||
for _, cookie := range cookies {
|
||||
// Check if Secure attribute is already present (case-insensitive)
|
||||
cookieLower := strings.ToLower(cookie)
|
||||
hasSecure := strings.Contains(cookieLower, ";secure;") ||
|
||||
strings.Contains(cookieLower, "; secure;") ||
|
||||
strings.HasSuffix(cookieLower, ";secure") ||
|
||||
strings.HasSuffix(cookieLower, "; secure")
|
||||
if !hasSecure {
|
||||
cookie = cookie + "; Secure"
|
||||
}
|
||||
resp.Header.Add("Set-Cookie", cookie)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fix CORS issue: Remove CORS header combinations that allow credential theft from any origin
|
||||
allowOrigin := resp.Header.Get("Access-Control-Allow-Origin")
|
||||
allowCredentials := resp.Header.Get("Access-Control-Allow-Credentials")
|
||||
|
||||
// Remove CORS headers when the combination is present:
|
||||
// 1. Access-Control-Allow-Credentials: true with Access-Control-Allow-Origin: *
|
||||
// This is actually blocked by browsers but we sanitize it anyway
|
||||
// 2. Access-Control-Allow-Credentials: true with any origin
|
||||
// Without a configured allowlist, we cannot safely validate if the origin
|
||||
// is trusted or if it's being reflected from the request, so we remove all
|
||||
// CORS headers for credential-bearing responses to prevent theft
|
||||
if strings.EqualFold(allowCredentials, "true") && allowOrigin != "" {
|
||||
// Remove CORS headers to prevent credential theft
|
||||
resp.Header.Del("Access-Control-Allow-Origin")
|
||||
resp.Header.Del("Access-Control-Allow-Credentials")
|
||||
resp.Header.Del("Access-Control-Allow-Methods")
|
||||
resp.Header.Del("Access-Control-Allow-Headers")
|
||||
resp.Header.Del("Access-Control-Expose-Headers")
|
||||
resp.Header.Del("Access-Control-Max-Age")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
proxy.ServeHTTP(writer, request)
|
||||
}
|
||||
|
||||
func getHostNonWww(host string) string {
|
||||
res := ""
|
||||
tokens := strings.Split(host, ".")
|
||||
if len(tokens) > 2 && tokens[0] == "www" {
|
||||
res = strings.Join(tokens[1:], ".")
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func logRequest(clientIp string, r *http.Request) {
|
||||
if !strings.Contains(r.UserAgent(), "Uptime-Kuma") {
|
||||
fmt.Printf("handleRequest: %s\t%s\t%s\t%s\t%s\t%s\n", clientIp, r.Method, r.Host, r.RequestURI, r.UserAgent(), r.RemoteAddr)
|
||||
record := casvisorsdk.Record{
|
||||
Owner: "admin",
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
Method: r.Method,
|
||||
RequestUri: r.RequestURI,
|
||||
ClientIp: clientIp,
|
||||
}
|
||||
object.AddRecord(&record)
|
||||
}
|
||||
}
|
||||
|
||||
func redirectToHttps(w http.ResponseWriter, r *http.Request) {
|
||||
targetUrl := fmt.Sprintf("https://%s", joinPath(r.Host, r.RequestURI))
|
||||
http.Redirect(w, r, targetUrl, http.StatusMovedPermanently)
|
||||
}
|
||||
|
||||
func redirectToHost(w http.ResponseWriter, r *http.Request, host string) {
|
||||
protocol := "https"
|
||||
if r.TLS == nil {
|
||||
protocol = "http"
|
||||
}
|
||||
|
||||
targetUrl := fmt.Sprintf("%s://%s", protocol, joinPath(host, r.RequestURI))
|
||||
http.Redirect(w, r, targetUrl, http.StatusMovedPermanently)
|
||||
}
|
||||
|
||||
func handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||
clientIp := util.GetClientIp(r)
|
||||
logRequest(clientIp, r)
|
||||
|
||||
site := getSiteByDomainWithWww(r.Host)
|
||||
if site == nil {
|
||||
if isHostIp(r.Host) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasSuffix(r.Host, ".casdoor.com") && r.RequestURI == "/health-ping" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, err := fmt.Fprintf(w, "OK")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
responseError(w, "CasWAF error: site not found for host: %s", r.Host)
|
||||
return
|
||||
}
|
||||
|
||||
hostNonWww := getHostNonWww(r.Host)
|
||||
if hostNonWww != "" {
|
||||
redirectToHost(w, r, hostNonWww)
|
||||
return
|
||||
}
|
||||
|
||||
if site.Domain != r.Host && site.NeedRedirect {
|
||||
redirectToHost(w, r, site.Domain)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(r.RequestURI, "/.well-known/acme-challenge/") {
|
||||
challengeMap := site.GetChallengeMap()
|
||||
for token, keyAuth := range challengeMap {
|
||||
if r.RequestURI == fmt.Sprintf("/.well-known/acme-challenge/%s", token) {
|
||||
responseOk(w, keyAuth)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
responseError(w, fmt.Sprintf("CasWAF error: ACME HTTP-01 challenge failed, requestUri cannot match with challengeMap, requestUri = %s, challengeMap = %v", r.RequestURI, challengeMap))
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(r.RequestURI, "/MP_verify_") {
|
||||
challengeMap := site.GetChallengeMap()
|
||||
for path, value := range challengeMap {
|
||||
if r.RequestURI == fmt.Sprintf("/%s", path) {
|
||||
responseOk(w, value)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if site.SslMode == "HTTPS Only" {
|
||||
// This domain only supports https but receive http request, redirect to https
|
||||
if r.TLS == nil {
|
||||
redirectToHttps(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// oAuth proxy
|
||||
if site.CasdoorApplication != "" {
|
||||
// handle oAuth proxy
|
||||
cookie, err := r.Cookie("casdoor_access_token")
|
||||
if err != nil && err.Error() != "http: named cookie not present" {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
casdoorClient, err := getCasdoorClientFromSite(site)
|
||||
if err != nil {
|
||||
responseError(w, "CasWAF error: getCasdoorClientFromSite() error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if cookie == nil {
|
||||
// not logged in
|
||||
redirectToCasdoor(casdoorClient, w, r)
|
||||
return
|
||||
} else {
|
||||
_, err = casdoorClient.ParseJwtToken(cookie.Value)
|
||||
if err != nil {
|
||||
responseError(w, "CasWAF error: casdoorClient.ParseJwtToken() error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
host := site.GetHost()
|
||||
if host == "" {
|
||||
responseError(w, "CasWAF error: targetUrl should not be empty for host: %s, site = %v", r.Host, site)
|
||||
return
|
||||
}
|
||||
|
||||
if len(site.Rules) == 0 {
|
||||
nextHandle(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := rule.CheckRules(site.Rules, r)
|
||||
if err != nil {
|
||||
responseError(w, "Internal Server Error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
reason := result.Reason
|
||||
if reason != "" && site.DisableVerbose {
|
||||
reason = "the rule has been hit"
|
||||
}
|
||||
|
||||
switch result.Action {
|
||||
case "", "Allow":
|
||||
// Do not write header for Allow action, let the proxy handle it
|
||||
case "Block":
|
||||
w.WriteHeader(result.StatusCode)
|
||||
responseErrorWithoutCode(w, "Blocked by CasWAF: %s", reason)
|
||||
return
|
||||
case "Drop":
|
||||
w.WriteHeader(result.StatusCode)
|
||||
responseErrorWithoutCode(w, "Dropped by CasWAF: %s", reason)
|
||||
return
|
||||
default:
|
||||
responseError(w, "Error in CasWAF: %s", reason)
|
||||
}
|
||||
nextHandle(w, r)
|
||||
}
|
||||
|
||||
func nextHandle(w http.ResponseWriter, r *http.Request) {
|
||||
site := getSiteByDomainWithWww(r.Host)
|
||||
host := site.GetHost()
|
||||
if site.SslMode == "Static Folder" {
|
||||
var path string
|
||||
if r.RequestURI != "/" {
|
||||
path = filepath.Join(host, r.RequestURI)
|
||||
} else {
|
||||
path = filepath.Join(host, "/index.htm")
|
||||
if !util.FileExist(path) {
|
||||
path = filepath.Join(host, "/index.html")
|
||||
if !util.FileExist(path) {
|
||||
path = filepath.Join(host, r.RequestURI)
|
||||
}
|
||||
}
|
||||
}
|
||||
http.ServeFile(w, r, path)
|
||||
} else {
|
||||
targetUrl := joinPath(site.GetHost(), r.RequestURI)
|
||||
forwardHandler(targetUrl, w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func Start() {
|
||||
serverMux := http.NewServeMux()
|
||||
serverMux.HandleFunc("/", handleRequest)
|
||||
serverMux.HandleFunc("/caswaf-handler", handleAuthCallback)
|
||||
|
||||
gatewayHttpPort, err := conf.GetConfigInt64("gatewayHttpPort")
|
||||
if err != nil {
|
||||
gatewayHttpPort = 80
|
||||
}
|
||||
|
||||
gatewayHttpsPort, err := conf.GetConfigInt64("gatewayHttpsPort")
|
||||
if err != nil {
|
||||
gatewayHttpsPort = 443
|
||||
}
|
||||
|
||||
go func() {
|
||||
fmt.Printf("CasWAF gateway running on: http://127.0.0.1:%d\n", gatewayHttpPort)
|
||||
err := http.ListenAndServe(fmt.Sprintf(":%d", gatewayHttpPort), serverMux)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
fmt.Printf("CasWAF gateway running on: https://127.0.0.1:%d\n", gatewayHttpsPort)
|
||||
server := &http.Server{
|
||||
Handler: serverMux,
|
||||
Addr: fmt.Sprintf(":%d", gatewayHttpsPort),
|
||||
TLSConfig: &tls.Config{
|
||||
// Minimum TLS version 1.2, TLS 1.3 is automatically supported
|
||||
MinVersion: tls.VersionTLS12,
|
||||
// Secure cipher suites for TLS 1.2 (excluding 3DES to prevent Sweet32 attack)
|
||||
// TLS 1.3 cipher suites are automatically configured by Go
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
},
|
||||
// Prefer strong elliptic curves
|
||||
CurvePreferences: []tls.CurveID{
|
||||
tls.X25519,
|
||||
tls.CurveP256,
|
||||
tls.CurveP384,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// start https server and set how to get certificate
|
||||
server.TLSConfig.GetCertificate = func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
domain := info.ServerName
|
||||
cert, err := getX509CertByDomain(domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
err := server.ListenAndServeTLS("", "")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
142
service/util.go
Normal file
142
service/util.go
Normal file
@@ -0,0 +1,142 @@
|
||||
// Copyright 2023 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
)
|
||||
|
||||
func joinPath(a string, b string) string {
|
||||
if strings.HasSuffix(a, "/") && strings.HasPrefix(b, "/") {
|
||||
b = b[1:]
|
||||
} else if !strings.HasSuffix(a, "/") && !strings.HasPrefix(b, "/") {
|
||||
b = "/" + b
|
||||
}
|
||||
res := a + b
|
||||
return res
|
||||
}
|
||||
|
||||
func isHostIp(host string) bool {
|
||||
hostWithoutPort := strings.Split(host, ":")[0]
|
||||
ip := net.ParseIP(hostWithoutPort)
|
||||
return ip != nil
|
||||
}
|
||||
|
||||
func responseOk(w http.ResponseWriter, format string, a ...interface{}) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
msg := fmt.Sprintf(format, a...)
|
||||
fmt.Println(msg)
|
||||
_, err := fmt.Fprintf(w, msg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func responseError(w http.ResponseWriter, format string, a ...interface{}) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
msg := fmt.Sprintf(format, a...)
|
||||
fmt.Println(msg)
|
||||
_, err := fmt.Fprintf(w, msg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func responseErrorWithoutCode(w http.ResponseWriter, format string, a ...interface{}) {
|
||||
msg := fmt.Sprintf(format, a...)
|
||||
fmt.Println(msg)
|
||||
_, err := fmt.Fprintf(w, msg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func getDomainWithoutPort(domain string) string {
|
||||
if !strings.Contains(domain, ":") {
|
||||
return domain
|
||||
}
|
||||
|
||||
tokens := strings.SplitN(domain, ":", 2)
|
||||
if len(tokens) > 1 {
|
||||
return tokens[0]
|
||||
}
|
||||
return domain
|
||||
}
|
||||
|
||||
func getSiteByDomainWithWww(domain string) *object.Site {
|
||||
hostNonWww := getHostNonWww(domain)
|
||||
if hostNonWww != "" {
|
||||
domain = hostNonWww
|
||||
}
|
||||
|
||||
domainWithoutPort := getDomainWithoutPort(domain)
|
||||
|
||||
site := object.GetSiteByDomain(domainWithoutPort)
|
||||
return site
|
||||
}
|
||||
|
||||
func getX509CertByDomain(domain string) (*tls.Certificate, error) {
|
||||
cert, err := object.GetCertByDomain(domain)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getX509CertByDomain() error: %v, domain: [%s]", err, domain)
|
||||
}
|
||||
if cert == nil {
|
||||
return nil, fmt.Errorf("getX509CertByDomain() error: cert not found for domain: [%s]", domain)
|
||||
}
|
||||
|
||||
tlsCert, certErr := tls.X509KeyPair([]byte(cert.Certificate), []byte(cert.PrivateKey))
|
||||
|
||||
return &tlsCert, certErr
|
||||
}
|
||||
|
||||
func getCasdoorClientFromSite(site *object.Site) (*casdoorsdk.Client, error) {
|
||||
if site.ApplicationObj == nil {
|
||||
return nil, fmt.Errorf("site.ApplicationObj is empty")
|
||||
}
|
||||
|
||||
casdoorEndpoint := conf.GetConfigString("origin")
|
||||
if casdoorEndpoint == "" {
|
||||
casdoorEndpoint = "http://localhost:8000"
|
||||
}
|
||||
|
||||
clientId := site.ApplicationObj.ClientId
|
||||
clientSecret := site.ApplicationObj.ClientSecret
|
||||
|
||||
certificate := ""
|
||||
if site.ApplicationObj.CertObj != nil {
|
||||
certificate = site.ApplicationObj.CertObj.Certificate
|
||||
}
|
||||
|
||||
res := casdoorsdk.NewClient(casdoorEndpoint, clientId, clientSecret, certificate, site.ApplicationObj.Organization, site.CasdoorApplication)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func getScheme(r *http.Request) string {
|
||||
scheme := r.URL.Scheme
|
||||
if scheme == "" {
|
||||
scheme = "http"
|
||||
}
|
||||
return scheme
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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} />)} />
|
||||
|
||||
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;
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
|
||||
483
web/src/SiteEditPage.js
Normal file
483
web/src/SiteEditPage.js
Normal file
@@ -0,0 +1,483 @@
|
||||
// Copyright 2023 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Card, Col, Input, InputNumber, Row, Select, Switch} from "antd";
|
||||
import {LinkOutlined} from "@ant-design/icons";
|
||||
import * as ProviderBackend from "./backend/ProviderBackend";
|
||||
import * as SiteBackend from "./backend/SiteBackend";
|
||||
import * as CertBackend from "./backend/CertBackend";
|
||||
import * as RuleBackend from "./backend/RuleBackend";
|
||||
import * as ApplicationBackend from "./backend/ApplicationBackend";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
import RuleTable from "./table/RuleTable";
|
||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
class SiteEditPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
owner: props.match.params.organizationName,
|
||||
siteName: props.match.params.siteName,
|
||||
rules: [],
|
||||
providers: [],
|
||||
site: null,
|
||||
certs: null,
|
||||
applications: null,
|
||||
organizations: [],
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getOrganizations();
|
||||
this.getSite();
|
||||
this.getCerts();
|
||||
this.getRules();
|
||||
this.getApplications();
|
||||
this.getAlertProviders();
|
||||
}
|
||||
|
||||
getOrganizations() {
|
||||
if (Setting.isAdminUser(this.props.account)) {
|
||||
OrganizationBackend.getOrganizations("admin")
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
organizations: res.data || [],
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getSite() {
|
||||
SiteBackend.getSite(this.state.owner, this.state.siteName)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
site: res.data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `Failed to get site: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getCerts() {
|
||||
CertBackend.getCerts(this.state.owner)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
certs: res.data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `Failed to get certs: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getRules() {
|
||||
RuleBackend.getRules(this.state.owner)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
rules: res.data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `Failed to get rules: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getApplications(owner) {
|
||||
ApplicationBackend.getApplicationsByOrganization("admin", owner || this.state.owner)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
applications: res.data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `Failed to get applications: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getAlertProviders() {
|
||||
ProviderBackend.getProviders()
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
const data = [];
|
||||
for (let i = 0; i < res.data.length; i++) {
|
||||
const provider = res.data[i];
|
||||
if (provider.category === "SMS" || provider.category === "Email") {
|
||||
data.push(provider.category + "/" + provider.name);
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
providers: data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `Failed to get providers: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
parseSiteField(key, value) {
|
||||
if (["score"].includes(key)) {
|
||||
value = Setting.myParseInt(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
updateSiteField(key, value) {
|
||||
value = this.parseSiteField(key, value);
|
||||
|
||||
const site = this.state.site;
|
||||
site[key] = value;
|
||||
this.setState({
|
||||
site: site,
|
||||
});
|
||||
}
|
||||
|
||||
renderSite() {
|
||||
return (
|
||||
<Card size="small" title={
|
||||
<div>
|
||||
{i18next.t("site:Edit Site")}
|
||||
<Button type="primary" onClick={this.submitSiteEdit.bind(this)}>{i18next.t("general:Save")}</Button>
|
||||
</div>
|
||||
} style={{marginLeft: "5px"}} type="inner">
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} disabled={!Setting.isAdminUser(this.props.account)} value={this.state.site.owner} onChange={(value => {
|
||||
this.updateSiteField("owner", value);
|
||||
this.getApplications(value);
|
||||
})}>
|
||||
{
|
||||
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("general:Name")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.site.name} onChange={e => {
|
||||
this.updateSiteField("name", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("general:Display name")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.site.displayName} onChange={e => {
|
||||
this.updateSiteField("displayName", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("general:Tag")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.site.tag} onChange={e => {
|
||||
this.updateSiteField("tag", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Domain")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.site.domain} onChange={e => {
|
||||
this.updateSiteField("domain", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Other domains")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} mode="tags" style={{width: "100%"}} value={this.state.site.otherDomains} onChange={(value => {this.updateSiteField("otherDomains", value);})}>
|
||||
{
|
||||
this.state.site.otherDomains?.map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Need redirect")}:
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.site.needRedirect} onChange={checked => {
|
||||
this.updateSiteField("needRedirect", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Disable verbose")}:
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.site.disableVerbose} onChange={checked => {
|
||||
this.updateSiteField("disableVerbose", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Rules")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<RuleTable
|
||||
title={"Rules"}
|
||||
account={this.props.account}
|
||||
sources={this.state.rules}
|
||||
rules={this.state.site.rules}
|
||||
onUpdateRules={(value) => this.updateSiteField("rules", value)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col span={2} style={{marginTop: "5px"}}>
|
||||
{i18next.t("site:Enable alert")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Switch checked={this.state.site.enableAlert} onChange={checked => {
|
||||
this.updateSiteField("enableAlert", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
this.state.site.enableAlert ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col span={2} style={{marginTop: "5px"}}>
|
||||
{i18next.t("site:Alert interval")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<InputNumber min={1} value={this.state.site.alertInterval} addonAfter={i18next.t("usage:seconds")} onChange={value => {
|
||||
this.updateSiteField("alertInterval", value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
this.state.site.enableAlert ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col span={2} style={{marginTop: "5px"}}>
|
||||
{i18next.t("site:Alert try times")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<InputNumber min={1} value={this.state.site.alertTryTimes} onChange={value => {
|
||||
this.updateSiteField("alertTryTimes", value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
this.state.site.enableAlert ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Alert providers")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} mode="tags" style={{width: "100%"}} value={this.state.site.alertProviders} onChange={(value => {this.updateSiteField("alertProviders", value);})}>
|
||||
{
|
||||
this.state.providers.map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
) : null
|
||||
}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Challenges")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} mode="tags" style={{width: "100%"}} value={this.state.site.challenges} onChange={(value => {this.updateSiteField("challenges", value);})}>
|
||||
{
|
||||
this.state.site.challenges?.map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Host")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input prefix={<LinkOutlined />} value={this.state.site.host} onChange={e => {
|
||||
this.updateSiteField("host", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Port")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<InputNumber min={0} max={65535} value={this.state.site.port} onChange={value => {
|
||||
this.updateSiteField("port", value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Hosts")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} mode="tags" style={{width: "100%"}} value={this.state.site.hosts} onChange={(value => {this.updateSiteField("hosts", value);})}>
|
||||
{
|
||||
this.state.site.hosts?.map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Public IP")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.site.publicIp} onChange={e => {
|
||||
this.updateSiteField("publicIp", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Mode")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.site.sslMode} onChange={(value => {this.updateSiteField("sslMode", value);})}>
|
||||
{
|
||||
[
|
||||
{id: "HTTP", name: "HTTP"},
|
||||
{id: "HTTPS and HTTP", name: "HTTPS and HTTP"},
|
||||
{id: "HTTPS Only", name: "HTTPS Only"},
|
||||
{id: "Static Folder", name: "Static Folder"},
|
||||
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:SSL cert")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select disabled={true} virtual={false} style={{width: "100%"}} showSearch value={this.state.site.sslCert} onChange={(value => {
|
||||
this.updateSiteField("sslCert", value);
|
||||
})}>
|
||||
{
|
||||
this.state.certs?.map((cert, index) => <Option key={index} value={cert.name}>{cert.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Casdoor app")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} showSearch value={this.state.site.casdoorApplication} onChange={(value => {
|
||||
this.updateSiteField("casdoorApplication", value);
|
||||
})}>
|
||||
{
|
||||
this.state.applications?.map((application, index) => <Option key={index} value={application.name}>{application.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{i18next.t("site:Status")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.site.status} onChange={(value => {this.updateSiteField("status", value);})}>
|
||||
{
|
||||
[
|
||||
{id: "Active", name: "Active"},
|
||||
{id: "Inactive", name: "Inactive"},
|
||||
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
submitSiteEdit() {
|
||||
const site = Setting.deepCopy(this.state.site);
|
||||
SiteBackend.updateSite(this.state.site.owner, this.state.siteName, site)
|
||||
.then((res) => {
|
||||
if (res.status === "error") {
|
||||
Setting.showMessage("error", `Failed to save: ${res.msg}`);
|
||||
this.updateSiteField("name", this.state.siteName);
|
||||
} else {
|
||||
Setting.showMessage("success", "Successfully saved");
|
||||
this.setState({
|
||||
siteName: this.state.site.name,
|
||||
});
|
||||
this.props.history.push(`/sites/${this.state.site.owner}/${this.state.site.name}`);
|
||||
this.getSite();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `failed to save: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Row style={{width: "100%"}}>
|
||||
<Col span={1}>
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
{
|
||||
this.state.site !== null ? this.renderSite() : null
|
||||
}
|
||||
</Col>
|
||||
<Col span={1}>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{margin: 10}}>
|
||||
<Col span={2}>
|
||||
</Col>
|
||||
<Col span={18}>
|
||||
<Button type="primary" size="large" onClick={this.submitSiteEdit.bind(this)}>{i18next.t("general:Save")}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SiteEditPage;
|
||||
459
web/src/SiteListPage.js
Normal file
459
web/src/SiteListPage.js
Normal file
@@ -0,0 +1,459 @@
|
||||
// Copyright 2023 The casbin Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {Button, Popconfirm, Table, Tag, Tooltip} from "antd";
|
||||
import moment from "moment";
|
||||
import * as Setting from "./Setting";
|
||||
import * as SiteBackend from "./backend/SiteBackend";
|
||||
import i18next from "i18next";
|
||||
import BaseListPage from "./BaseListPage";
|
||||
|
||||
class SiteListPage extends BaseListPage {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.setState({
|
||||
pagination: {
|
||||
...this.state.pagination,
|
||||
current: 1,
|
||||
pageSize: 1000,
|
||||
},
|
||||
});
|
||||
this.fetch({pagination: this.state.pagination});
|
||||
}
|
||||
|
||||
newSite() {
|
||||
const randomName = Setting.getRandomName();
|
||||
const owner = Setting.getRequestOrganization(this.props.account);
|
||||
return {
|
||||
owner: owner,
|
||||
name: `site_${randomName}`,
|
||||
createdTime: moment().format(),
|
||||
displayName: `New Site - ${randomName}`,
|
||||
domain: "door.casdoor.com",
|
||||
otherDomains: [],
|
||||
needRedirect: false,
|
||||
disableVerbose: false,
|
||||
rules: [],
|
||||
enableAlert: false,
|
||||
alertInterval: 60,
|
||||
alertTryTimes: 3,
|
||||
alertProviders: [],
|
||||
challenges: [],
|
||||
host: "",
|
||||
port: 8000,
|
||||
hosts: [],
|
||||
sslMode: "HTTPS Only",
|
||||
sslCert: "",
|
||||
publicIp: "8.131.81.162",
|
||||
node: "",
|
||||
isSelf: false,
|
||||
nodes: [],
|
||||
casdoorApplication: "",
|
||||
organizations: [],
|
||||
};
|
||||
}
|
||||
|
||||
addSite() {
|
||||
const newSite = this.newSite();
|
||||
SiteBackend.addSite(newSite)
|
||||
.then((res) => {
|
||||
if (res.status === "error") {
|
||||
Setting.showMessage("error", `Failed to add: ${res.msg}`);
|
||||
} else {
|
||||
Setting.showMessage("success", "Site added successfully");
|
||||
this.setState({
|
||||
data: Setting.prependRow(this.state.data, newSite),
|
||||
});
|
||||
this.fetch();
|
||||
}
|
||||
}
|
||||
)
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `Site failed to add: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
deleteSite(i) {
|
||||
SiteBackend.deleteSite(this.state.data[i])
|
||||
.then((res) => {
|
||||
if (res.status === "error") {
|
||||
Setting.showMessage("error", `Failed to delete: ${res.msg}`);
|
||||
} else {
|
||||
Setting.showMessage("success", "Site deleted successfully");
|
||||
this.fetch({
|
||||
pagination: {
|
||||
...this.state.pagination,
|
||||
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
)
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `Site failed to delete: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
renderTable(data) {
|
||||
// const renderExternalLink = () => {
|
||||
// return (
|
||||
// <svg style={{marginLeft: "5px"}} width="13.5" height="13.5" aria-hidden="true" viewBox="0 0 24 24" className="iconExternalLink_nPIU">
|
||||
// <path fill="currentColor" d="M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"></path>
|
||||
// </svg>
|
||||
// );
|
||||
// };
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("general:Owner"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
width: "90px",
|
||||
sorter: (a, b) => a.owner.localeCompare(b.owner),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Tag"),
|
||||
dataIndex: "tag",
|
||||
key: "tag",
|
||||
width: "140px",
|
||||
sorter: (a, b) => a.tag.localeCompare(b.tag),
|
||||
render: (text, record, index) => {
|
||||
if (text === "") {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Link to={`/nodes/${record.owner}/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: "120px",
|
||||
sorter: (a, b) => a.name.localeCompare(b.name),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/sites/${record.owner}/${record.name}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
// {
|
||||
// title: i18next.t("general:Create time"),
|
||||
// dataIndex: "createdTime",
|
||||
// key: "createdTime",
|
||||
// width: "180px",
|
||||
// sorter: (a, b) => a.createdTime.localeCompare(b.createdTime),
|
||||
// render: (text, record, index) => {
|
||||
// return Setting.getFormattedDate(text);
|
||||
// },
|
||||
// },
|
||||
{
|
||||
title: i18next.t("general:Display name"),
|
||||
dataIndex: "displayName",
|
||||
key: "displayName",
|
||||
// width: "200px",
|
||||
sorter: (a, b) => a.displayName.localeCompare(b.displayName),
|
||||
},
|
||||
{
|
||||
title: i18next.t("site:Domain"),
|
||||
dataIndex: "domain",
|
||||
key: "domain",
|
||||
width: "150px",
|
||||
sorter: (a, b) => a.domain.localeCompare(b.domain),
|
||||
render: (text, record, index) => {
|
||||
if (record.publicIp === "") {
|
||||
return text;
|
||||
}
|
||||
|
||||
return (
|
||||
<a target="_blank" rel="noreferrer" href={`https://${text}`}>
|
||||
{text}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("site:Other domains"),
|
||||
dataIndex: "otherDomains",
|
||||
key: "otherDomains",
|
||||
width: "120px",
|
||||
sorter: (a, b) => a.otherDomains.localeCompare(b.otherDomains),
|
||||
render: (text, record, index) => {
|
||||
return record.otherDomains.map(domain => {
|
||||
return (
|
||||
<a key={domain} target="_blank" rel="noreferrer" href={`https://${domain}`}>
|
||||
<Tag color={record.needRedirect ? "default" : "processing"}>
|
||||
{domain}
|
||||
</Tag>
|
||||
</a>
|
||||
);
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Rules"),
|
||||
dataIndex: "rules",
|
||||
key: "rules",
|
||||
width: "120px",
|
||||
sorter: (a, b) => a.rules.localeCompare(b.rules),
|
||||
render: (text, record, index) => {
|
||||
if (!record.rules) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return record.rules.map(rule => {
|
||||
return (
|
||||
<a key={rule} target="_blank" rel="noreferrer" href={`/rules/${rule}`}>
|
||||
<Tag color={"processing"}>
|
||||
{rule}
|
||||
</Tag>
|
||||
</a>
|
||||
);
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("site:Host"),
|
||||
dataIndex: "host",
|
||||
key: "host",
|
||||
width: "80px",
|
||||
sorter: (a, b) => a.host.localeCompare(b.host),
|
||||
render: (text, record, index) => {
|
||||
let host = record.port;
|
||||
if (record.host !== "") {
|
||||
host = `${record.host}:${record.port}`;
|
||||
}
|
||||
|
||||
if (record.status === "Active") {
|
||||
return host;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tag color={"warning"}>
|
||||
{host}
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("site:Hosts"),
|
||||
dataIndex: "hosts",
|
||||
key: "hosts",
|
||||
width: "200px",
|
||||
sorter: (a, b) => a.hosts.length - b.hosts.length,
|
||||
render: (hosts) => {
|
||||
if (!Array.isArray(hosts)) {
|
||||
return null;
|
||||
}
|
||||
return hosts.map((host, index) => (
|
||||
<Tag color="blue" key={index}>
|
||||
{host}
|
||||
</Tag>
|
||||
));
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("site:Nodes"),
|
||||
dataIndex: "nodes",
|
||||
key: "nodes",
|
||||
width: "180px",
|
||||
sorter: (a, b) => a.nodes.length - b.nodes.length,
|
||||
render: (text, record, index) => {
|
||||
return record.nodes.map(node => {
|
||||
const versionInfo = Setting.getVersionInfo(node.version, record.name);
|
||||
let color = node.message === "" ? "processing" : "error";
|
||||
if (color === "processing" && node.provider !== "") {
|
||||
if (node.version === "") {
|
||||
color = "warning";
|
||||
} else if (node.provider !== "") {
|
||||
color = "success";
|
||||
}
|
||||
}
|
||||
|
||||
const getTag = () => {
|
||||
if (versionInfo === null) {
|
||||
return (
|
||||
<Tag key={node.name} color={color}>
|
||||
{node.name}
|
||||
</Tag>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<a key={node.name} target="_blank" rel="noreferrer" href={versionInfo.link}>
|
||||
<Tag color={color}>
|
||||
{`${node.name} (${versionInfo.text})`}
|
||||
</Tag>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (node.message === "") {
|
||||
return getTag();
|
||||
} else {
|
||||
return (
|
||||
<Tooltip key={node.name} title={node.message}>
|
||||
{getTag()}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
// {
|
||||
// title: i18next.t("site:Public IP"),
|
||||
// dataIndex: "publicIp",
|
||||
// key: "publicIp",
|
||||
// width: "120px",
|
||||
// sorter: (a, b) => a.publicIp.localeCompare(b.publicIp),
|
||||
// },
|
||||
// {
|
||||
// title: i18next.t("site:Node"),
|
||||
// dataIndex: "node",
|
||||
// key: "node",
|
||||
// width: "180px",
|
||||
// sorter: (a, b) => a.node.localeCompare(b.node),
|
||||
// render: (text, record, index) => {
|
||||
// return (
|
||||
// <div>
|
||||
// {text}
|
||||
// {
|
||||
// !record.isSelf ? null : (
|
||||
// <Tag style={{marginLeft: "10px"}} icon={<CheckCircleOutlined />} color="success">
|
||||
// {i18next.t("general:Self")}
|
||||
// </Tag>
|
||||
// )
|
||||
// }
|
||||
// </div>
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// title: i18next.t("site:Mode"),
|
||||
// dataIndex: "sslMode",
|
||||
// key: "sslMode",
|
||||
// width: "100px",
|
||||
// sorter: (a, b) => a.sslMode.localeCompare(b.sslMode),
|
||||
// },
|
||||
{
|
||||
title: i18next.t("site:SSL cert"),
|
||||
dataIndex: "sslCert",
|
||||
key: "sslCert",
|
||||
width: "130px",
|
||||
sorter: (a, b) => a.sslCert.localeCompare(b.sslCert),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/certs/admin/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
// {
|
||||
// title: i18next.t("site:Casdoor app"),
|
||||
// dataIndex: "casdoorApplication",
|
||||
// key: "casdoorApplication",
|
||||
// width: "140px",
|
||||
// sorter: (a, b) => a.casdoorApplication.localeCompare(b.casdoorApplication),
|
||||
// render: (text, record, index) => {
|
||||
// if (text === "") {
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// return (
|
||||
// <a target="_blank" rel="noreferrer" href={Setting.getMyProfileUrl(this.state.account).replace("/account", `/applications/${this.props.account.owner}/${text}`)}>
|
||||
// {text}
|
||||
// {renderExternalLink()}
|
||||
// </a>
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
dataIndex: "action",
|
||||
key: "action",
|
||||
width: "180px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/sites/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<Popconfirm
|
||||
title={`Sure to delete site: ${record.name} ?`}
|
||||
onConfirm={() => this.deleteSite(index)}
|
||||
okText="OK"
|
||||
cancelText="Cancel"
|
||||
>
|
||||
<Button style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table columns={columns} dataSource={data} rowKey="name" size="middle" bordered pagination={this.state.pagination}
|
||||
title={() => (
|
||||
<div>
|
||||
{i18next.t("general:Sites")}
|
||||
<Button type="primary" size="small" onClick={this.addSite.bind(this)}>{i18next.t("general:Add")}</Button>
|
||||
</div>
|
||||
)}
|
||||
loading={data === null}
|
||||
onChange={this.handleTableChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
fetch = (params = {}) => {
|
||||
const field = params.searchedColumn, value = params.searchText;
|
||||
const sortField = params.sortField, sortOrder = params.sortOrder;
|
||||
if (!params.pagination) {
|
||||
params.pagination = {current: 1, pageSize: 10};
|
||||
}
|
||||
this.setState({loading: true});
|
||||
// SiteBackend.getSites(this.props.account.name, params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
|
||||
SiteBackend.getSites(this.props.account.owner, "", "", field, value, sortField, sortOrder)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
data: res.data,
|
||||
pagination: {
|
||||
...params.pagination,
|
||||
total: res.data2,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `Failed to get sites: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default SiteListPage;
|
||||
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",
|
||||
|
||||
114
web/src/table/IpRateRuleTable.js
Normal file
114
web/src/table/IpRateRuleTable.js
Normal file
@@ -0,0 +1,114 @@
|
||||
// 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, Col, Input, InputNumber, Row, Table} from "antd";
|
||||
import i18next from "i18next";
|
||||
|
||||
class IpRateRuleTable extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
defaultRules: [
|
||||
{
|
||||
name: "Default IP Rate",
|
||||
operator: "100",
|
||||
value: "6000",
|
||||
},
|
||||
],
|
||||
};
|
||||
if (this.props.table.length === 0) {
|
||||
this.restore();
|
||||
}
|
||||
}
|
||||
|
||||
updateTable(table) {
|
||||
this.props.onUpdateTable(table);
|
||||
}
|
||||
|
||||
updateField(table, index, key, value) {
|
||||
table[index][key] = String(value);
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
restore() {
|
||||
this.updateTable(this.state.defaultRules);
|
||||
}
|
||||
|
||||
renderTable(table) {
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("general:Name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: "20%",
|
||||
render: (text, record, index) => (
|
||||
<Input value={record.name} onChange={e => {
|
||||
this.updateField(table, index, "name", e.target.value);
|
||||
}} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18next.t("rule:Rate"),
|
||||
dataIndex: "operator",
|
||||
key: "operator",
|
||||
width: "40%",
|
||||
render: (text, record, index) => (
|
||||
<InputNumber style={{"width": "100%"}} value={Number(record.operator)} addonAfter="requests / ip / s" onChange={e => {
|
||||
this.updateField(table, index, "operator", e);
|
||||
}} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18next.t("rule:Block Duration"),
|
||||
dataIndex: "value",
|
||||
key: "value",
|
||||
width: "100%",
|
||||
render: (text, record, index) => (
|
||||
<InputNumber style={{"width": "100%"}} value={Number(record.value)} addonAfter={i18next.t("usage:seconds")} onChange={e => {
|
||||
this.updateField(table, index, "value", e);
|
||||
}} />
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
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.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 IpRateRuleTable;
|
||||
196
web/src/table/IpRuleTable.js
Normal file
196
web/src/table/IpRuleTable.js
Normal file
@@ -0,0 +1,196 @@
|
||||
// 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, Input, Row, Select, Table, Tooltip} from "antd";
|
||||
import * as Setting from "../Setting";
|
||||
import i18next from "i18next";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
class IpRuleTable extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
options: [],
|
||||
defaultRules: [
|
||||
{
|
||||
name: "loopback",
|
||||
operator: "is in",
|
||||
value: "127.0.0.1",
|
||||
},
|
||||
{
|
||||
name: "lan cidr",
|
||||
operator: "is in",
|
||||
value: "10.0.0.0/8,192.168.0.0/16",
|
||||
},
|
||||
],
|
||||
};
|
||||
if (this.props.table.length === 0) {
|
||||
this.restore();
|
||||
}
|
||||
for (let i = 0; i < this.props.table.length; i++) {
|
||||
const values = this.props.table[i].value.split(",");
|
||||
const options = [];
|
||||
for (let j = 0; j < values.length; j++) {
|
||||
options[j] = {value: values[j], label: values[j]};
|
||||
}
|
||||
this.state.options.push(options);
|
||||
}
|
||||
}
|
||||
|
||||
updateTable(table) {
|
||||
this.props.onUpdateTable(table);
|
||||
}
|
||||
|
||||
updateField(table, index, key, value) {
|
||||
if (key === "value") {
|
||||
let v = "";
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
v += value[i].trim() + ",";
|
||||
}
|
||||
table[index][key] = v.slice(0, -1);
|
||||
} else {
|
||||
table[index][key] = value;
|
||||
}
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
addRow(table) {
|
||||
const row = {name: `New IP Rule - ${table.length}`, operator: "is in", value: "127.0.0.1"};
|
||||
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);
|
||||
Setting.swapRow(this.state.options, i - 1, i);
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
downRow(table, i) {
|
||||
table = Setting.swapRow(table, i, i + 1);
|
||||
Setting.swapRow(this.state.options, i, i + 1);
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
restore() {
|
||||
this.updateTable(this.state.defaultRules);
|
||||
}
|
||||
|
||||
renderTable(table) {
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("general:Name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: "180px",
|
||||
render: (text, record, index) => (
|
||||
<Input value={text} onChange={e => {
|
||||
this.updateField(table, index, "name", e.target.value);
|
||||
}} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18next.t("rule:Operator"),
|
||||
dataIndex: "operator",
|
||||
key: "operator",
|
||||
width: "180px",
|
||||
render: (text, record, index) => (
|
||||
<Select value={text} virtual={false} style={{width: "100%"}} onChange={value => {
|
||||
this.updateField(table, index, "operator", value);
|
||||
}}>
|
||||
{
|
||||
[
|
||||
{value: "is in", text: i18next.t("rule:is in")},
|
||||
{value: "is not in", text: i18next.t("rule:is not in")},
|
||||
].map((item, index) => <Option key={index} value={item.value}>{item.text}</Option>)
|
||||
}
|
||||
</Select>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18next.t("rule:IP List"),
|
||||
dataIndex: "value",
|
||||
key: "value",
|
||||
render: (text, record, index) => (
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{width: "100%"}}
|
||||
placeholder="Input IP Addresses"
|
||||
value={record.value ? record.value.split(",") : []}
|
||||
onChange={value => this.updateField(table, index, "value", value)}
|
||||
options={this.state.options[index]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
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 IpRuleTable;
|
||||
141
web/src/table/RuleTable.js
Normal file
141
web/src/table/RuleTable.js
Normal file
@@ -0,0 +1,141 @@
|
||||
// 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 * as Setting from "../Setting";
|
||||
import i18next from "i18next";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
class RuleTable extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
};
|
||||
if (this.props.rules === null) {
|
||||
// rerender
|
||||
this.props.onUpdateRules([]);
|
||||
}
|
||||
}
|
||||
|
||||
updateTable(table) {
|
||||
const rules = [];
|
||||
for (let i = 0; i < table.length; i++) {
|
||||
rules.push(table[i].owner + "/" + table[i].name);
|
||||
}
|
||||
this.props.onUpdateRules(rules);
|
||||
}
|
||||
|
||||
updateField(table, index, key, value) {
|
||||
table[index][key] = value;
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
addRow(table) {
|
||||
const row = {owner: this.props.account.owner, name: ""};
|
||||
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);
|
||||
}
|
||||
|
||||
renderTable(table) {
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("general:Name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: "180px",
|
||||
render: (text, record, index) => (
|
||||
<Select value={text} virtual={false} style={{width: "100%"}} onChange={value => {
|
||||
this.updateField(table, index, "name", value);
|
||||
}}>
|
||||
{
|
||||
Setting.getDeduplicatedArray(this.props.sources, table, "name").map((record, index) => {
|
||||
return <Option key={record.name} value={record.name}>{record.name}</Option>;
|
||||
})
|
||||
}
|
||||
</Select>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "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>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col span={24}>
|
||||
{
|
||||
this.props.rules === null ? null : this.renderTable(this.props.rules.map((item, index) => {
|
||||
const values = item.split("/");
|
||||
return {owner: values[0], name: values[1]};
|
||||
}))
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default RuleTable;
|
||||
172
web/src/table/UaRuleTable.js
Normal file
172
web/src/table/UaRuleTable.js
Normal file
@@ -0,0 +1,172 @@
|
||||
// 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, Input, Row, Select, Table, Tooltip} from "antd";
|
||||
import * as Setting from "../Setting";
|
||||
import i18next from "i18next";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
class UaRuleTable extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
defaultRules: [
|
||||
{
|
||||
name: "Current User-Agent",
|
||||
operator: "equals",
|
||||
value: window.navigator.userAgent,
|
||||
},
|
||||
],
|
||||
};
|
||||
if (this.props.table.length === 0) {
|
||||
this.restore();
|
||||
}
|
||||
}
|
||||
|
||||
updateTable(table) {
|
||||
this.props.onUpdateTable(table);
|
||||
}
|
||||
|
||||
updateField(table, index, key, value) {
|
||||
table[index][key] = value;
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
addRow(table) {
|
||||
const row = {name: `New UA Rule - ${table.length}`, operator: "equals", 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("general:Name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: "180px",
|
||||
render: (text, record, index) => (
|
||||
<Input value={text} onChange={e => {
|
||||
this.updateField(table, index, "name", e.target.value);
|
||||
}} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18next.t("rule:Operator"),
|
||||
dataIndex: "operator",
|
||||
key: "operator",
|
||||
width: "180px",
|
||||
render: (text, record, index) => (
|
||||
<Select value={text} virtual={false} style={{width: "100%"}} onChange={value => {
|
||||
this.updateField(table, index, "operator", value);
|
||||
}}>
|
||||
{
|
||||
[
|
||||
{value: "equals", text: i18next.t("rule:equals")},
|
||||
{value: "does not equal", text: i18next.t("rule:does not equal")},
|
||||
{value: "contains", text: i18next.t("rule:contains")},
|
||||
{value: "does not contain", text: i18next.t("rule:does not contain")},
|
||||
{value: "match", text: i18next.t("rule:regex match")},
|
||||
].map((item, index) => <Option key={index} value={item.value}>{item.text}</Option>)
|
||||
}
|
||||
</Select>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18next.t("rule:Value"),
|
||||
dataIndex: "value",
|
||||
key: "value",
|
||||
render: (text, record, index) => (
|
||||
<Input value={text} onChange={e => {
|
||||
this.updateField(table, index, "value", e.target.value);
|
||||
}} onBlur={e => {
|
||||
this.updateField(table, index, "value", e.target.value.replace(/\s+/g, " ").trim());
|
||||
}} />
|
||||
),
|
||||
},
|
||||
{
|
||||
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 UaRuleTable;
|
||||
164
web/src/table/WafRuleTable.js
Normal file
164
web/src/table/WafRuleTable.js
Normal file
@@ -0,0 +1,164 @@
|
||||
// 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, Input, Row, Table, Tooltip} from "antd";
|
||||
import * as Setting from "../Setting";
|
||||
import i18next from "i18next";
|
||||
|
||||
class WafRuleTable extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
defaultRules: [
|
||||
{
|
||||
name: "Enable XML request body parser",
|
||||
operator: "match",
|
||||
value: "SecRule REQUEST_HEADERS:Content-Type \"^(?:application(?:/soap\\+|/)|text/)xml\" \"id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML\"",
|
||||
},
|
||||
{
|
||||
name: "Enable JSON request body parser",
|
||||
operator: "match",
|
||||
value: "SecRule REQUEST_HEADERS:Content-Type \"^application/json\" \"id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON\"",
|
||||
},
|
||||
{
|
||||
name: "Verify that we've correctly processed the request body",
|
||||
operator: "match",
|
||||
value: "SecRule &REQUEST_BODY \"@eq 0\" \"id:'200002',phase:2,t:none,deny,status:400,msg:'Failed to parse request body.'\"",
|
||||
},
|
||||
],
|
||||
};
|
||||
if (this.props.table.length === 0) {
|
||||
this.restore();
|
||||
}
|
||||
}
|
||||
|
||||
updateTable(table) {
|
||||
this.props.onUpdateTable(table);
|
||||
}
|
||||
|
||||
updateField(table, index, key, value) {
|
||||
table[index][key] = value;
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
addRow(table) {
|
||||
const row = {name: `New WAF Rule - ${table.length}`, operator: "match", 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("general:Name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: "180px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Input value={text} onChange={e => {
|
||||
this.updateField(table, index, "name", e.target.value);
|
||||
}} />
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("rule:Expression"),
|
||||
dataIndex: "value",
|
||||
key: "value",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Input value={text} onChange={e => {
|
||||
this.updateField(table, index, "value", e.target.value);
|
||||
}} />
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
key: "action",
|
||||
width: "100px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<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 WafRuleTable;
|
||||
Reference in New Issue
Block a user