diff --git a/controllers/rule.go b/controllers/rule.go index 85c288f..6eda28d 100644 --- a/controllers/rule.go +++ b/controllers/rule.go @@ -158,6 +158,8 @@ func checkExpressions(expressions []*object.Expression, ruleType string) error { return checkIpRule(values) case "IP Rate Limiting": return checkIpRateRule(expressions) + case "Compound": + return checkCompoundRules(values) } return nil } @@ -200,3 +202,11 @@ func checkIpRateRule(expressions []*object.Expression) error { } return nil } + +func checkCompoundRules(rules []string) error { + _, err := object.GetRulesByRuleIds(rules) + if err != nil { + return err + } + return nil +} diff --git a/object/rule_cache.go b/object/rule_cache.go index a02eebd..0f40ffa 100644 --- a/object/rule_cache.go +++ b/object/rule_cache.go @@ -15,6 +15,8 @@ package object import ( + "fmt" + "github.com/casbin/caswaf/util" ) @@ -42,12 +44,14 @@ func refreshRuleMap() error { return nil } -func GetRulesByRuleIds(ids []string) []*Rule { +func GetRulesByRuleIds(ids []string) ([]*Rule, error) { var res []*Rule for _, id := range ids { - if rule, ok := ruleMap[id]; ok { - res = append(res, rule) + rule, ok := ruleMap[id] + if !ok { + return nil, fmt.Errorf("rule: %s not found", id) } + res = append(res, rule) } - return res + return res, nil } diff --git a/rule/rule.go b/rule/rule.go index a4a9e02..3934e9a 100644 --- a/rule/rule.go +++ b/rule/rule.go @@ -26,7 +26,10 @@ type Rule interface { } func CheckRules(ruleIds []string, r *http.Request) (string, string, error) { - rules := object.GetRulesByRuleIds(ruleIds) + rules, err := object.GetRulesByRuleIds(ruleIds) + if err != nil { + return "", "", err + } for i, rule := range rules { var ruleObj Rule switch rule.Type { @@ -40,6 +43,8 @@ func CheckRules(ruleIds []string, r *http.Request) (string, string, error) { ruleObj = &IpRateRule{ ruleName: rule.GetId(), } + case "Compound": + ruleObj = &CompoundRule{} default: return "", "", fmt.Errorf("unknown rule type: %s for rule: %s", rule.Type, rule.GetId()) } diff --git a/rule/rule_compound.go b/rule/rule_compound.go new file mode 100644 index 0000000..a6f51c2 --- /dev/null +++ b/rule/rule_compound.go @@ -0,0 +1,57 @@ +// 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/casbin/caswaf/object" + "github.com/casbin/caswaf/util" +) + +type CompoundRule struct{} + +func (r *CompoundRule) checkRule(expressions []*object.Expression, req *http.Request) (bool, string, string, error) { + operators := util.NewStack() + res := true + for _, expression := range expressions { + isHit := true + action, _, err := CheckRules([]string{expression.Value}, req) + if err != nil { + return false, "", "", err + } + if action == "" { + isHit = false + } + switch expression.Operator { + case "and", "begin": + res = res && isHit + case "or": + operators.Push(res) + res = isHit + default: + return false, "", "", 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() + } + } + } + return res, "", "", nil +} diff --git a/util/stacks.go b/util/stacks.go new file mode 100644 index 0000000..ae96110 --- /dev/null +++ b/util/stacks.go @@ -0,0 +1,45 @@ +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{} +} diff --git a/web/src/RuleEditPage.js b/web/src/RuleEditPage.js index 8fca542..ef06d90 100644 --- a/web/src/RuleEditPage.js +++ b/web/src/RuleEditPage.js @@ -21,6 +21,7 @@ import WafRuleTable from "./components/WafRuleTable"; import IpRuleTable from "./components/IpRuleTable"; import UaRuleTable from "./components/UaRuleTable"; import IpRateRuleTable from "./components/IpRateRuleTable"; +import CompoundRule from "./components/CompoundRule"; const {Option} = Select; @@ -97,7 +98,7 @@ class RuleEditPage extends React.Component { {value: "IP", text: "IP"}, {value: "User-Agent", text: "User-Agent"}, {value: "IP Rate Limiting", text: i18next.t("rule:IP Rate Limiting")}, - // {value: "complex", text: "Complex"}, + {value: "Compound", text: i18next.t("rule:Compound")}, ].map((item, index) => ) } @@ -152,6 +153,16 @@ class RuleEditPage extends React.Component { /> ) : null } + { + this.state.rule.type === "Compound" ? ( + {this.updateRuleField("expressions", value);}} /> + ) : null + } { diff --git a/web/src/Setting.js b/web/src/Setting.js index 38267ff..807abfe 100644 --- a/web/src/Setting.js +++ b/web/src/Setting.js @@ -303,3 +303,7 @@ export function getDeduplicatedArray(sourceTable, filterTable, key) { const res = sourceTable.filter(item => !filterTable.some(arrayItem => arrayItem[key] === item[key])); return res; } + +export function getItemId(item) { + return item.owner + "/" + item.name; +} diff --git a/web/src/components/CompoundRule.js b/web/src/components/CompoundRule.js new file mode 100644 index 0000000..74f5208 --- /dev/null +++ b/web/src/components/CompoundRule.js @@ -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 ( + + ); + }, + }, + { + title: i18next.t("rule:Rule"), + dataIndex: "value", + key: "value", + render: (text, record, index) => ( + + ), + }, + { + title: i18next.t("general:Action"), + key: "action", + width: "100px", + render: (text, record, index) => ( +
+ +
+ ), + }, + ]; + return ( + ( +
+ {this.props.title}     + + +
+ )} + /> + ); + } + + render() { + return ( +
+ +
+ { + this.renderTable(this.props.table) + } + + + + ); + } +} + +export default CompoundRule;