snapshot of https://github.com/casbin/casbin.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
Build / test (1.21) (push) Successful in 2m21s
Build / benchmark (1.21) (push) Failing after 2m28s
golangci-lint / golangci (push) Successful in 19m40s

This commit is contained in:
upstream-mirror
2026-09-15 21:46:19 +05:00
commit d8971e16fb
234 changed files with 28137 additions and 0 deletions

202
model/assertion.go Normal file
View File

@@ -0,0 +1,202 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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 model
import (
"errors"
"strings"
"sync"
"github.com/casbin/casbin/v3/rbac"
)
// Assertion represents an expression in a section of the model.
// For example: r = sub, obj, act.
type Assertion struct {
Key string
Value string
Tokens []string
ParamsTokens []string
Policy [][]string
PolicyMap map[string]int
RM rbac.RoleManager
CondRM rbac.ConditionalRoleManager
FieldIndexMap map[string]int
FieldIndexMutex sync.RWMutex
}
func (ast *Assertion) buildIncrementalRoleLinks(rm rbac.RoleManager, op PolicyOp, rules [][]string) error {
ast.RM = rm
count := strings.Count(ast.Value, "_")
if count < 2 {
return errors.New("the number of \"_\" in role definition should be at least 2")
}
for _, rule := range rules {
if len(rule) < count {
return errors.New("grouping policy elements do not meet role definition")
}
if len(rule) > count {
rule = rule[:count]
}
switch op {
case PolicyAdd:
err := rm.AddLink(rule[0], rule[1], rule[2:]...)
if err != nil {
return err
}
case PolicyRemove:
err := rm.DeleteLink(rule[0], rule[1], rule[2:]...)
if err != nil {
return err
}
}
}
return nil
}
func (ast *Assertion) buildRoleLinks(rm rbac.RoleManager) error {
ast.RM = rm
count := strings.Count(ast.Value, "_")
if count < 2 {
return errors.New("the number of \"_\" in role definition should be at least 2")
}
for _, rule := range ast.Policy {
if len(rule) < count {
return errors.New("grouping policy elements do not meet role definition")
}
if len(rule) > count {
rule = rule[:count]
}
err := ast.RM.AddLink(rule[0], rule[1], rule[2:]...)
if err != nil {
return err
}
}
return nil
}
func (ast *Assertion) buildIncrementalConditionalRoleLinks(condRM rbac.ConditionalRoleManager, op PolicyOp, rules [][]string) error {
ast.CondRM = condRM
count := strings.Count(ast.Value, "_")
if count < 2 {
return errors.New("the number of \"_\" in role definition should be at least 2")
}
for _, rule := range rules {
if len(rule) < count {
return errors.New("grouping policy elements do not meet role definition")
}
if len(rule) > count {
rule = rule[:count]
}
var err error
domainRule := rule[2:len(ast.Tokens)]
switch op {
case PolicyAdd:
err = ast.addConditionalRoleLink(rule, domainRule)
case PolicyRemove:
err = ast.CondRM.DeleteLink(rule[0], rule[1], rule[2:]...)
}
if err != nil {
return err
}
}
return nil
}
func (ast *Assertion) buildConditionalRoleLinks(condRM rbac.ConditionalRoleManager) error {
ast.CondRM = condRM
count := strings.Count(ast.Value, "_")
if count < 2 {
return errors.New("the number of \"_\" in role definition should be at least 2")
}
for _, rule := range ast.Policy {
if len(rule) < count {
return errors.New("grouping policy elements do not meet role definition")
}
if len(rule) > count {
rule = rule[:count]
}
domainRule := rule[2:len(ast.Tokens)]
err := ast.addConditionalRoleLink(rule, domainRule)
if err != nil {
return err
}
}
return nil
}
// addConditionalRoleLink adds Link to rbac.ConditionalRoleManager and sets the parameters for LinkConditionFunc.
func (ast *Assertion) addConditionalRoleLink(rule []string, domainRule []string) error {
var err error
if len(domainRule) == 0 {
err = ast.CondRM.AddLink(rule[0], rule[1])
if err == nil {
ast.CondRM.SetLinkConditionFuncParams(rule[0], rule[1], rule[len(ast.Tokens):]...)
}
} else {
domain := domainRule[0]
err = ast.CondRM.AddLink(rule[0], rule[1], domain)
if err == nil {
ast.CondRM.SetDomainLinkConditionFuncParams(rule[0], rule[1], domain, rule[len(ast.Tokens):]...)
}
}
return err
}
func (ast *Assertion) copy() *Assertion {
tokens := append([]string(nil), ast.Tokens...)
policy := make([][]string, len(ast.Policy))
for i, p := range ast.Policy {
policy[i] = append(policy[i], p...)
}
policyMap := make(map[string]int)
for k, v := range ast.PolicyMap {
policyMap[k] = v
}
ast.FieldIndexMutex.RLock()
fieldIndexMap := make(map[string]int)
for k, v := range ast.FieldIndexMap {
fieldIndexMap[k] = v
}
ast.FieldIndexMutex.RUnlock()
newAst := &Assertion{
Key: ast.Key,
Value: ast.Value,
PolicyMap: policyMap,
Tokens: tokens,
Policy: policy,
FieldIndexMap: fieldIndexMap,
ParamsTokens: append([]string(nil), ast.ParamsTokens...),
RM: ast.RM,
CondRM: ast.CondRM,
}
return newAst
}

285
model/constraint.go Normal file
View File

@@ -0,0 +1,285 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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 model
import (
"fmt"
"regexp"
"strconv"
"strings"
"github.com/casbin/casbin/v3/errors"
)
// ConstraintType represents the type of constraint.
type ConstraintType int
const (
ConstraintTypeSOD ConstraintType = iota
ConstraintTypeSODMax
ConstraintTypeRoleMax
ConstraintTypeRolePre
)
// Constraint represents a policy constraint.
type Constraint struct {
Key string
Type ConstraintType
Roles []string
Role string
MaxCount int
PreReqRole string
}
var (
// Regex patterns for parsing constraints (compiled once at package initialization).
sodPattern = regexp.MustCompile(`^sod\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)$`)
sodMaxPattern = regexp.MustCompile(`^sodMax\s*\(\s*\[([^\]]+)\]\s*,\s*(\d+)\s*\)$`)
roleMaxPattern = regexp.MustCompile(`^roleMax\s*\(\s*"([^"]+)"\s*,\s*(\d+)\s*\)$`)
rolePrePattern = regexp.MustCompile(`^rolePre\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)$`)
)
// parseRolesArray parses a comma-separated string of quoted role names.
func parseRolesArray(rolesStr string) ([]string, error) {
var roles []string
for _, role := range strings.Split(rolesStr, ",") {
role = strings.TrimSpace(role)
role = strings.Trim(role, `"`)
if role != "" {
roles = append(roles, role)
}
}
if len(roles) == 0 {
return nil, fmt.Errorf("no roles found in role array")
}
return roles, nil
}
// parseConstraint parses a constraint definition string.
func parseConstraint(key, value string) (*Constraint, error) {
value = strings.TrimSpace(value)
// Try to match sod pattern
if matches := sodPattern.FindStringSubmatch(value); matches != nil {
return &Constraint{
Key: key,
Type: ConstraintTypeSOD,
Roles: []string{matches[1], matches[2]},
}, nil
}
// Try to match sodMax pattern
if matches := sodMaxPattern.FindStringSubmatch(value); matches != nil {
maxCount, err := strconv.Atoi(matches[2])
if err != nil {
return nil, fmt.Errorf("invalid max count in sodMax: %w", err)
}
roles, err := parseRolesArray(matches[1])
if err != nil {
return nil, fmt.Errorf("sodMax: %w", err)
}
return &Constraint{
Key: key,
Type: ConstraintTypeSODMax,
Roles: roles,
MaxCount: maxCount,
}, nil
}
// Try to match roleMax pattern
if matches := roleMaxPattern.FindStringSubmatch(value); matches != nil {
maxCount, err := strconv.Atoi(matches[2])
if err != nil {
return nil, fmt.Errorf("invalid max count in roleMax: %w", err)
}
return &Constraint{
Key: key,
Type: ConstraintTypeRoleMax,
Role: matches[1],
MaxCount: maxCount,
}, nil
}
// Try to match rolePre pattern
if matches := rolePrePattern.FindStringSubmatch(value); matches != nil {
return &Constraint{
Key: key,
Type: ConstraintTypeRolePre,
Role: matches[1],
PreReqRole: matches[2],
}, nil
}
return nil, fmt.Errorf("unrecognized constraint format: %s", value)
}
// ValidateConstraints validates all constraints against the current policy.
func (model Model) ValidateConstraints() error {
// Check if constraints exist
if len(model["c"]) == 0 {
return nil // No constraints to validate
}
// Check if RBAC is enabled
if len(model["g"]) == 0 {
return errors.ErrConstraintRequiresRBAC
}
// Get grouping policy
gAssertion := model["g"]["g"]
if gAssertion == nil {
return errors.ErrConstraintRequiresRBAC
}
// Validate each constraint
for _, assertion := range model["c"] {
constraint, err := parseConstraint(assertion.Key, assertion.Value)
if err != nil {
return fmt.Errorf("%w: %s", errors.ErrConstraintParsingError, err.Error())
}
if err := model.validateConstraint(constraint, gAssertion.Policy); err != nil {
return err
}
}
return nil
}
// validateConstraint validates a single constraint against the policy.
func (model Model) validateConstraint(constraint *Constraint, groupingPolicy [][]string) error {
switch constraint.Type {
case ConstraintTypeSOD:
return model.validateSOD(constraint, groupingPolicy)
case ConstraintTypeSODMax:
return model.validateSODMax(constraint, groupingPolicy)
case ConstraintTypeRoleMax:
return model.validateRoleMax(constraint, groupingPolicy)
case ConstraintTypeRolePre:
return model.validateRolePre(constraint, groupingPolicy)
default:
return fmt.Errorf("unknown constraint type")
}
}
// buildUserRoleMap builds a map of users to their assigned roles from grouping policy.
func buildUserRoleMap(groupingPolicy [][]string) map[string]map[string]bool {
userRoles := make(map[string]map[string]bool)
for _, rule := range groupingPolicy {
if len(rule) < 2 {
continue
}
user := rule[0]
role := rule[1]
if userRoles[user] == nil {
userRoles[user] = make(map[string]bool)
}
userRoles[user][role] = true
}
return userRoles
}
// validateSOD validates a Separation of Duties constraint.
func (model Model) validateSOD(constraint *Constraint, groupingPolicy [][]string) error {
if len(constraint.Roles) != 2 {
return errors.NewConstraintViolationError(constraint.Key, "sod requires exactly 2 roles")
}
role1, role2 := constraint.Roles[0], constraint.Roles[1]
userRoles := buildUserRoleMap(groupingPolicy)
// Check if any user has both roles
for user, roles := range userRoles {
if roles[role1] && roles[role2] {
return errors.NewConstraintViolationError(constraint.Key,
fmt.Sprintf("user '%s' cannot have both roles '%s' and '%s'", user, role1, role2))
}
}
return nil
}
// validateSODMax validates a maximum role count constraint for a role set.
func (model Model) validateSODMax(constraint *Constraint, groupingPolicy [][]string) error {
userRoles := buildUserRoleMap(groupingPolicy)
// Check if any user has more than maxCount roles from the role set
for user, roles := range userRoles {
count := 0
for _, role := range constraint.Roles {
if roles[role] {
count++
}
}
if count > constraint.MaxCount {
return errors.NewConstraintViolationError(constraint.Key,
fmt.Sprintf("user '%s' has %d roles from %v, exceeds maximum of %d",
user, count, constraint.Roles, constraint.MaxCount))
}
}
return nil
}
// validateRoleMax validates a role cardinality constraint.
func (model Model) validateRoleMax(constraint *Constraint, groupingPolicy [][]string) error {
roleCount := 0
// Count how many users have this role
for _, rule := range groupingPolicy {
if len(rule) < 2 {
continue
}
role := rule[1]
if role == constraint.Role {
roleCount++
}
}
if roleCount > constraint.MaxCount {
return errors.NewConstraintViolationError(constraint.Key,
fmt.Sprintf("role '%s' assigned to %d users, exceeds maximum of %d",
constraint.Role, roleCount, constraint.MaxCount))
}
return nil
}
// validateRolePre validates a prerequisite role constraint.
func (model Model) validateRolePre(constraint *Constraint, groupingPolicy [][]string) error {
userRoles := buildUserRoleMap(groupingPolicy)
// Check if any user has the main role without the prerequisite role
for user, roles := range userRoles {
if roles[constraint.Role] && !roles[constraint.PreReqRole] {
return errors.NewConstraintViolationError(constraint.Key,
fmt.Sprintf("user '%s' has role '%s' but lacks prerequisite role '%s'",
user, constraint.Role, constraint.PreReqRole))
}
}
return nil
}

69
model/function.go Normal file
View File

@@ -0,0 +1,69 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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 model
import (
"sync"
"github.com/casbin/casbin/v3/util"
"github.com/casbin/govaluate"
)
// FunctionMap represents the collection of Function.
type FunctionMap struct {
fns *sync.Map
}
// [string]govaluate.ExpressionFunction
// AddFunction adds an expression function.
func (fm *FunctionMap) AddFunction(name string, function govaluate.ExpressionFunction) {
fm.fns.LoadOrStore(name, function)
}
// LoadFunctionMap loads an initial function map.
func LoadFunctionMap() FunctionMap {
fm := &FunctionMap{}
fm.fns = &sync.Map{}
fm.AddFunction("keyMatch", util.KeyMatchFunc)
fm.AddFunction("keyGet", util.KeyGetFunc)
fm.AddFunction("keyMatch2", util.KeyMatch2Func)
fm.AddFunction("keyGet2", util.KeyGet2Func)
fm.AddFunction("keyMatch3", util.KeyMatch3Func)
fm.AddFunction("keyGet3", util.KeyGet3Func)
fm.AddFunction("keyMatch4", util.KeyMatch4Func)
fm.AddFunction("keyMatch5", util.KeyMatch5Func)
fm.AddFunction("regexMatch", util.RegexMatchFunc)
fm.AddFunction("ipMatch", util.IPMatchFunc)
fm.AddFunction("globMatch", util.GlobMatchFunc)
return *fm
}
// GetFunctions return a map with all the functions.
func (fm *FunctionMap) GetFunctions() map[string]govaluate.ExpressionFunction {
ret := make(map[string]govaluate.ExpressionFunction)
fm.fns.Range(func(k interface{}, v interface{}) bool {
ret[k.(string)] = v.(govaluate.ExpressionFunction)
return true
})
return ret
}

442
model/model.go Normal file
View File

@@ -0,0 +1,442 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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 model
import (
"container/list"
"errors"
"fmt"
"regexp"
"sort"
"strconv"
"strings"
"github.com/casbin/casbin/v3/config"
"github.com/casbin/casbin/v3/constant"
"github.com/casbin/casbin/v3/util"
)
// Model represents the whole access control model.
type Model map[string]AssertionMap
// AssertionMap is the collection of assertions, can be "r", "p", "g", "e", "m".
type AssertionMap map[string]*Assertion
const (
defaultDomain string = ""
defaultSeparator string = "::"
)
var sectionNameMap = map[string]string{
"r": "request_definition",
"p": "policy_definition",
"g": "role_definition",
"e": "policy_effect",
"m": "matchers",
"c": "constraint_definition",
}
// Minimal required sections for a model to be valid.
var requiredSections = []string{"r", "p", "e", "m"}
func loadAssertion(model Model, cfg config.ConfigInterface, sec string, key string) bool {
value := cfg.String(sectionNameMap[sec] + "::" + key)
return model.AddDef(sec, key, value)
}
var paramsRegex = regexp.MustCompile(`\((.*?)\)`)
// getParamsToken Get ParamsToken from Assertion.Value.
func getParamsToken(value string) []string {
paramsString := paramsRegex.FindString(value)
if paramsString == "" {
return nil
}
paramsString = strings.TrimSuffix(strings.TrimPrefix(paramsString, "("), ")")
return strings.Split(paramsString, ",")
}
// AddDef adds an assertion to the model.
func (model Model) AddDef(sec string, key string, value string) bool {
if value == "" {
return false
}
ast := Assertion{}
ast.Key = key
ast.Value = value
ast.PolicyMap = make(map[string]int)
ast.FieldIndexMap = make(map[string]int)
switch sec {
case "r", "p":
ast.Tokens = strings.Split(ast.Value, ",")
for i := range ast.Tokens {
ast.Tokens[i] = key + "_" + strings.TrimSpace(ast.Tokens[i])
}
case "g":
ast.ParamsTokens = getParamsToken(ast.Value)
ast.Tokens = strings.Split(ast.Value, ",")
ast.Tokens = ast.Tokens[:len(ast.Tokens)-len(ast.ParamsTokens)]
default:
ast.Value = util.RemoveComments(util.EscapeAssertion(ast.Value))
}
if sec == "m" {
// Escape backslashes in string literals to match CSV parsing behavior
ast.Value = util.EscapeStringLiterals(ast.Value)
if strings.Contains(ast.Value, "in") {
ast.Value = strings.ReplaceAll(strings.ReplaceAll(ast.Value, "[", "("), "]", ")")
}
}
_, ok := model[sec]
if !ok {
model[sec] = make(AssertionMap)
}
model[sec][key] = &ast
return true
}
func getKeySuffix(i int) string {
if i == 1 {
return ""
}
return strconv.Itoa(i)
}
func loadSection(model Model, cfg config.ConfigInterface, sec string) {
i := 1
for {
if !loadAssertion(model, cfg, sec, sec+getKeySuffix(i)) {
break
} else {
i++
}
}
}
// NewModel creates an empty model.
func NewModel() Model {
m := make(Model)
return m
}
// NewModelFromFile creates a model from a .CONF file.
func NewModelFromFile(path string) (Model, error) {
m := NewModel()
err := m.LoadModel(path)
if err != nil {
return nil, err
}
return m, nil
}
// NewModelFromString creates a model from a string which contains model text.
func NewModelFromString(text string) (Model, error) {
m := NewModel()
err := m.LoadModelFromText(text)
if err != nil {
return nil, err
}
return m, nil
}
// LoadModel loads the model from model CONF file.
func (model Model) LoadModel(path string) error {
cfg, err := config.NewConfig(path)
if err != nil {
return err
}
return model.loadModelFromConfig(cfg)
}
// LoadModelFromText loads the model from the text.
func (model Model) LoadModelFromText(text string) error {
cfg, err := config.NewConfigFromText(text)
if err != nil {
return err
}
return model.loadModelFromConfig(cfg)
}
// loadModelFromConfig loads the model from a config interface.
// It loads all sections defined in sectionNameMap and validates that required sections are present.
// If constraint_definition section exists, it validates all constraints against the current policy.
// Note: Constraint validation is performed during model loading, which may affect loading performance
// and can cause model loading to fail if constraints are violated or invalid.
func (model Model) loadModelFromConfig(cfg config.ConfigInterface) error {
for s := range sectionNameMap {
loadSection(model, cfg, s)
}
ms := make([]string, 0)
for _, rs := range requiredSections {
if !model.hasSection(rs) {
ms = append(ms, sectionNameMap[rs])
}
}
if len(ms) > 0 {
return fmt.Errorf("missing required sections: %s", strings.Join(ms, ","))
}
// Validate constraints after model is loaded
if err := model.ValidateConstraints(); err != nil {
return err
}
return nil
}
func (model Model) hasSection(sec string) bool {
section := model[sec]
return section != nil
}
func (model Model) GetAssertion(sec string, ptype string) (*Assertion, error) {
if model[sec] == nil {
return nil, fmt.Errorf("missing required section %s", sec)
}
if model[sec][ptype] == nil {
return nil, fmt.Errorf("missing required definition %s in section %s", ptype, sec)
}
return model[sec][ptype], nil
}
// PrintModel prints the model to the log.
func (model Model) PrintModel() {
// Logger has been removed - this is now a no-op
}
func (model Model) SortPoliciesBySubjectHierarchy() error {
if model["e"]["e"].Value != constant.SubjectPriorityEffect {
return nil
}
g, err := model.GetAssertion("g", "g")
if err != nil {
return err
}
subIndex := 0
for ptype, assertion := range model["p"] {
domainIndex, err := model.GetFieldIndex(ptype, constant.DomainIndex)
if err != nil {
domainIndex = -1
}
policies := assertion.Policy
subjectHierarchyMap, err := getSubjectHierarchyMap(g.Policy)
if err != nil {
return err
}
sort.SliceStable(policies, func(i, j int) bool {
domain1, domain2 := defaultDomain, defaultDomain
if domainIndex != -1 {
domain1 = policies[i][domainIndex]
domain2 = policies[j][domainIndex]
}
name1, name2 := getNameWithDomain(domain1, policies[i][subIndex]), getNameWithDomain(domain2, policies[j][subIndex])
p1 := subjectHierarchyMap[name1]
p2 := subjectHierarchyMap[name2]
return p1 > p2
})
for i, policy := range assertion.Policy {
assertion.PolicyMap[strings.Join(policy, ",")] = i
}
}
return nil
}
func getSubjectHierarchyMap(policies [][]string) (map[string]int, error) {
subjectHierarchyMap := make(map[string]int)
// Tree structure of role
policyMap := make(map[string][]string)
for _, policy := range policies {
if len(policy) < 2 {
return nil, errors.New("policy g expect 2 more params")
}
domain := defaultDomain
if len(policy) != 2 {
domain = policy[2]
}
child := getNameWithDomain(domain, policy[0])
parent := getNameWithDomain(domain, policy[1])
policyMap[parent] = append(policyMap[parent], child)
if _, ok := subjectHierarchyMap[child]; !ok {
subjectHierarchyMap[child] = 0
}
if _, ok := subjectHierarchyMap[parent]; !ok {
subjectHierarchyMap[parent] = 0
}
subjectHierarchyMap[child] = 1
}
// Use queues for levelOrder
queue := list.New()
for k, v := range subjectHierarchyMap {
root := k
if v != 0 {
continue
}
lv := 0
queue.PushBack(root)
for queue.Len() != 0 {
sz := queue.Len()
for i := 0; i < sz; i++ {
node := queue.Front()
queue.Remove(node)
nodeValue := node.Value.(string)
subjectHierarchyMap[nodeValue] = lv
if _, ok := policyMap[nodeValue]; ok {
for _, child := range policyMap[nodeValue] {
queue.PushBack(child)
}
}
}
lv++
}
}
return subjectHierarchyMap, nil
}
func getNameWithDomain(domain string, name string) string {
return domain + defaultSeparator + name
}
func (model Model) SortPoliciesByPriority() error {
for ptype, assertion := range model["p"] {
priorityIndex, err := model.GetFieldIndex(ptype, constant.PriorityIndex)
if err != nil {
continue
}
policies := assertion.Policy
sort.SliceStable(policies, func(i, j int) bool {
p1, err := strconv.Atoi(policies[i][priorityIndex])
if err != nil {
return true
}
p2, err := strconv.Atoi(policies[j][priorityIndex])
if err != nil {
return true
}
return p1 < p2
})
for i, policy := range assertion.Policy {
assertion.PolicyMap[strings.Join(policy, ",")] = i
}
}
return nil
}
var (
pPattern = regexp.MustCompile("^p_")
rPattern = regexp.MustCompile("^r_")
)
func (model Model) ToText() string {
tokenPatterns := make(map[string]string)
for _, ptype := range []string{"r", "p"} {
for _, token := range model[ptype][ptype].Tokens {
tokenPatterns[token] = rPattern.ReplaceAllString(pPattern.ReplaceAllString(token, "p."), "r.")
}
}
if strings.Contains(model["e"]["e"].Value, "p_eft") {
tokenPatterns["p_eft"] = "p.eft"
}
s := strings.Builder{}
writeString := func(sec string) {
for ptype := range model[sec] {
value := model[sec][ptype].Value
for tokenPattern, newToken := range tokenPatterns {
value = strings.ReplaceAll(value, tokenPattern, newToken)
}
fmt.Fprintf(&s, "%s = %s\n", sec, value)
}
}
s.WriteString("[request_definition]\n")
writeString("r")
s.WriteString("[policy_definition]\n")
writeString("p")
if _, ok := model["g"]; ok {
s.WriteString("[role_definition]\n")
for ptype := range model["g"] {
fmt.Fprintf(&s, "%s = %s\n", ptype, model["g"][ptype].Value)
}
}
if _, ok := model["c"]; ok {
s.WriteString("[constraint_definition]\n")
for ptype := range model["c"] {
fmt.Fprintf(&s, "%s = %s\n", ptype, model["c"][ptype].Value)
}
}
s.WriteString("[policy_effect]\n")
writeString("e")
s.WriteString("[matchers]\n")
writeString("m")
return s.String()
}
func (model Model) Copy() Model {
newModel := NewModel()
for sec, m := range model {
newAstMap := make(AssertionMap)
for ptype, ast := range m {
newAstMap[ptype] = ast.copy()
}
newModel[sec] = newAstMap
}
return newModel
}
func (model Model) GetFieldIndex(ptype string, field string) (int, error) {
assertion := model["p"][ptype]
assertion.FieldIndexMutex.RLock()
if index, ok := assertion.FieldIndexMap[field]; ok {
assertion.FieldIndexMutex.RUnlock()
return index, nil
}
assertion.FieldIndexMutex.RUnlock()
pattern := fmt.Sprintf("%s_"+field, ptype)
index := -1
for i, token := range assertion.Tokens {
if token == pattern {
index = i
break
}
}
if index == -1 {
return index, fmt.Errorf(field + " index is not set, please use enforcer.SetFieldIndex() to set index")
}
assertion.FieldIndexMutex.Lock()
assertion.FieldIndexMap[field] = index
assertion.FieldIndexMutex.Unlock()
return index, nil
}

164
model/model_test.go Normal file
View File

@@ -0,0 +1,164 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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 model
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/casbin/casbin/v3/config"
"github.com/casbin/casbin/v3/constant"
)
var (
basicExample = filepath.Join("..", "examples", "basic_model.conf")
basicConfig = &MockConfig{
data: map[string]string{
"request_definition::r": "sub, obj, act",
"policy_definition::p": "sub, obj, act",
"policy_effect::e": "some(where (p.eft == allow))",
"matchers::m": "r.sub == p.sub && r.obj == p.obj && r.act == p.act",
},
}
)
type MockConfig struct {
data map[string]string
config.ConfigInterface
}
func (mc *MockConfig) String(key string) string {
return mc.data[key]
}
func TestNewModel(t *testing.T) {
m := NewModel()
if m == nil {
t.Error("new model should not be nil")
}
}
func TestNewModelFromFile(t *testing.T) {
m, err := NewModelFromFile(basicExample)
if err != nil {
t.Errorf("model failed to load from file: %s", err)
}
if m == nil {
t.Error("model should not be nil")
}
}
func TestNewModelFromString(t *testing.T) {
modelBytes, _ := os.ReadFile(basicExample)
modelString := string(modelBytes)
m, err := NewModelFromString(modelString)
if err != nil {
t.Errorf("model failed to load from string: %s", err)
}
if m == nil {
t.Error("model should not be nil")
}
}
func TestLoadModelFromConfig(t *testing.T) {
m := NewModel()
err := m.loadModelFromConfig(basicConfig)
if err != nil {
t.Error("basic config should not return an error")
}
m = NewModel()
err = m.loadModelFromConfig(&MockConfig{})
if err == nil {
t.Error("empty config should return error")
} else {
// check for missing sections in message
for _, rs := range requiredSections {
if !strings.Contains(err.Error(), sectionNameMap[rs]) {
t.Errorf("section name: %s should be in message", sectionNameMap[rs])
}
}
}
}
func TestHasSection(t *testing.T) {
m := NewModel()
_ = m.loadModelFromConfig(basicConfig)
for _, sec := range requiredSections {
if !m.hasSection(sec) {
t.Errorf("%s section was expected in model", sec)
}
}
m = NewModel()
_ = m.loadModelFromConfig(&MockConfig{})
for _, sec := range requiredSections {
if m.hasSection(sec) {
t.Errorf("%s section was not expected in model", sec)
}
}
}
func TestModel_AddDef(t *testing.T) {
m := NewModel()
s := "r"
v := "sub, obj, act"
ok := m.AddDef(s, s, v)
if !ok {
t.Errorf("non empty assertion should be added")
}
ok = m.AddDef(s, s, "")
if ok {
t.Errorf("empty assertion value should not be added")
}
}
func TestModelToTest(t *testing.T) {
testModelToText(t, "r.sub == p.sub && r.obj == p.obj && r_func(r.act, p.act) && testr_func(r.act, p.act)", "r_sub == p_sub && r_obj == p_obj && r_func(r_act, p_act) && testr_func(r_act, p_act)")
testModelToText(t, "r.sub == p.sub && r.obj == p.obj && p_func(r.act, p.act) && testp_func(r.act, p.act)", "r_sub == p_sub && r_obj == p_obj && p_func(r_act, p_act) && testp_func(r_act, p_act)")
}
func testModelToText(t *testing.T, mData, mExpected string) {
m := NewModel()
data := map[string]string{
"r": "sub, obj, act",
"p": "sub, obj, act",
"e": "some(where (p.eft == allow))",
"m": mData,
}
expected := map[string]string{
"r": "sub, obj, act",
"p": "sub, obj, act",
"e": constant.AllowOverrideEffect,
"m": mExpected,
}
addData := func(ptype string) {
m.AddDef(ptype, ptype, data[ptype])
}
for ptype := range data {
addData(ptype)
}
newM := NewModel()
print(m.ToText())
_ = newM.LoadModelFromText(m.ToText())
for ptype := range data {
if newM[ptype][ptype].Value != expected[ptype] {
t.Errorf("\"%s\" assertion value changed, current value: %s, it should be: %s", ptype, newM[ptype][ptype].Value, expected[ptype])
}
}
}

482
model/policy.go Normal file
View File

@@ -0,0 +1,482 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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 model
import (
"fmt"
"strconv"
"strings"
"github.com/casbin/casbin/v3/constant"
"github.com/casbin/casbin/v3/rbac"
"github.com/casbin/casbin/v3/util"
)
type (
PolicyOp int
)
const (
PolicyAdd PolicyOp = iota
PolicyRemove
)
const DefaultSep = ","
// BuildIncrementalRoleLinks provides incremental build the role inheritance relations.
func (model Model) BuildIncrementalRoleLinks(rmMap map[string]rbac.RoleManager, op PolicyOp, sec string, ptype string, rules [][]string) error {
if sec == "g" && rmMap[ptype] != nil {
_, err := model.GetAssertion(sec, ptype)
if err != nil {
return err
}
return model[sec][ptype].buildIncrementalRoleLinks(rmMap[ptype], op, rules)
}
return nil
}
// BuildRoleLinks initializes the roles in RBAC.
func (model Model) BuildRoleLinks(rmMap map[string]rbac.RoleManager) error {
model.PrintPolicy()
for ptype, ast := range model["g"] {
if rm := rmMap[ptype]; rm != nil {
err := ast.buildRoleLinks(rm)
if err != nil {
return err
}
}
}
return nil
}
// BuildIncrementalConditionalRoleLinks provides incremental build the role inheritance relations.
func (model Model) BuildIncrementalConditionalRoleLinks(condRmMap map[string]rbac.ConditionalRoleManager, op PolicyOp, sec string, ptype string, rules [][]string) error {
if sec == "g" && condRmMap[ptype] != nil {
_, err := model.GetAssertion(sec, ptype)
if err != nil {
return err
}
return model[sec][ptype].buildIncrementalConditionalRoleLinks(condRmMap[ptype], op, rules)
}
return nil
}
// BuildConditionalRoleLinks initializes the roles in RBAC.
func (model Model) BuildConditionalRoleLinks(condRmMap map[string]rbac.ConditionalRoleManager) error {
model.PrintPolicy()
for ptype, ast := range model["g"] {
if condRm := condRmMap[ptype]; condRm != nil {
err := ast.buildConditionalRoleLinks(condRm)
if err != nil {
return err
}
}
}
return nil
}
// PrintPolicy prints the policy to log.
func (model Model) PrintPolicy() {
// Logger has been removed - this is now a no-op
}
// ClearPolicy clears all current policy.
func (model Model) ClearPolicy() {
for _, ast := range model["p"] {
ast.Policy = nil
ast.PolicyMap = map[string]int{}
}
for _, ast := range model["g"] {
ast.Policy = nil
ast.PolicyMap = map[string]int{}
}
}
// GetPolicy gets all rules in a policy.
func (model Model) GetPolicy(sec string, ptype string) ([][]string, error) {
_, err := model.GetAssertion(sec, ptype)
if err != nil {
return nil, err
}
return model[sec][ptype].Policy, nil
}
// GetFilteredPolicy gets rules based on field filters from a policy.
func (model Model) GetFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) ([][]string, error) {
_, err := model.GetAssertion(sec, ptype)
if err != nil {
return nil, err
}
res := [][]string{}
for _, rule := range model[sec][ptype].Policy {
matched := true
for i, fieldValue := range fieldValues {
if fieldValue != "" && rule[fieldIndex+i] != fieldValue {
matched = false
break
}
}
if matched {
res = append(res, rule)
}
}
return res, nil
}
// HasPolicyEx determines whether a model has the specified policy rule with error.
func (model Model) HasPolicyEx(sec string, ptype string, rule []string) (bool, error) {
assertion, err := model.GetAssertion(sec, ptype)
if err != nil {
return false, err
}
switch sec {
case "p":
if len(rule) != len(assertion.Tokens) {
return false, fmt.Errorf(
"invalid policy rule size: expected %d, got %d, rule: %v",
len(model["p"][ptype].Tokens),
len(rule),
rule)
}
case "g":
if len(rule) < len(assertion.Tokens) {
return false, fmt.Errorf(
"invalid policy rule size: expected %d, got %d, rule: %v",
len(model["g"][ptype].Tokens),
len(rule),
rule)
}
}
return model.HasPolicy(sec, ptype, rule)
}
// HasPolicy determines whether a model has the specified policy rule.
func (model Model) HasPolicy(sec string, ptype string, rule []string) (bool, error) {
_, err := model.GetAssertion(sec, ptype)
if err != nil {
return false, err
}
_, ok := model[sec][ptype].PolicyMap[strings.Join(rule, DefaultSep)]
return ok, nil
}
// HasPolicies determines whether a model has any of the specified policies. If one is found we return true.
func (model Model) HasPolicies(sec string, ptype string, rules [][]string) (bool, error) {
for i := 0; i < len(rules); i++ {
ok, err := model.HasPolicy(sec, ptype, rules[i])
if err != nil {
return false, err
}
if ok {
return true, nil
}
}
return false, nil
}
// AddPolicy adds a policy rule to the model.
func (model Model) AddPolicy(sec string, ptype string, rule []string) error {
assertion, err := model.GetAssertion(sec, ptype)
if err != nil {
return err
}
assertion.Policy = append(assertion.Policy, rule)
assertion.PolicyMap[strings.Join(rule, DefaultSep)] = len(model[sec][ptype].Policy) - 1
hasPriority := false
if _, ok := assertion.FieldIndexMap[constant.PriorityIndex]; ok {
hasPriority = true
}
if sec == "p" && hasPriority {
if idxInsert, err := strconv.Atoi(rule[assertion.FieldIndexMap[constant.PriorityIndex]]); err == nil {
i := len(assertion.Policy) - 1
for ; i > 0; i-- {
idx, err := strconv.Atoi(assertion.Policy[i-1][assertion.FieldIndexMap[constant.PriorityIndex]])
if err != nil || idx <= idxInsert {
break
}
assertion.Policy[i] = assertion.Policy[i-1]
assertion.PolicyMap[strings.Join(assertion.Policy[i-1], DefaultSep)]++
}
assertion.Policy[i] = rule
assertion.PolicyMap[strings.Join(rule, DefaultSep)] = i
}
}
return nil
}
// AddPolicies adds policy rules to the model.
func (model Model) AddPolicies(sec string, ptype string, rules [][]string) error {
_, err := model.AddPoliciesWithAffected(sec, ptype, rules)
return err
}
// AddPoliciesWithAffected adds policy rules to the model, and returns affected rules.
func (model Model) AddPoliciesWithAffected(sec string, ptype string, rules [][]string) ([][]string, error) {
_, err := model.GetAssertion(sec, ptype)
if err != nil {
return nil, err
}
var affected [][]string
for _, rule := range rules {
hashKey := strings.Join(rule, DefaultSep)
_, ok := model[sec][ptype].PolicyMap[hashKey]
if ok {
continue
}
affected = append(affected, rule)
err = model.AddPolicy(sec, ptype, rule)
if err != nil {
return affected, err
}
}
return affected, err
}
// RemovePolicy removes a policy rule from the model.
func (model Model) RemovePolicy(sec string, ptype string, rule []string) (bool, error) {
ast, err := model.GetAssertion(sec, ptype)
if err != nil {
return false, err
}
key := strings.Join(rule, DefaultSep)
index, ok := ast.PolicyMap[key]
if !ok {
return false, nil
}
// The order of Policy is significant: it carries the priority semantics of
// the "priority(p_eft)" effector, both for explicit priority models and for
// implicit ones where the priority is the policy order itself. So the rule
// has to be shifted out instead of being swapped with the last one.
ast.Policy = append(ast.Policy[:index], ast.Policy[index+1:]...)
delete(ast.PolicyMap, key)
for i := index; i < len(ast.Policy); i++ {
ast.PolicyMap[strings.Join(ast.Policy[i], DefaultSep)] = i
}
return true, nil
}
// UpdatePolicy updates a policy rule from the model.
func (model Model) UpdatePolicy(sec string, ptype string, oldRule []string, newRule []string) (bool, error) {
_, err := model.GetAssertion(sec, ptype)
if err != nil {
return false, err
}
oldPolicy := strings.Join(oldRule, DefaultSep)
index, ok := model[sec][ptype].PolicyMap[oldPolicy]
if !ok {
return false, nil
}
model[sec][ptype].Policy[index] = newRule
delete(model[sec][ptype].PolicyMap, oldPolicy)
model[sec][ptype].PolicyMap[strings.Join(newRule, DefaultSep)] = index
return true, nil
}
// UpdatePolicies updates a policy rule from the model.
func (model Model) UpdatePolicies(sec string, ptype string, oldRules, newRules [][]string) (bool, error) {
_, err := model.GetAssertion(sec, ptype)
if err != nil {
return false, err
}
rollbackFlag := false
// index -> []{oldIndex, newIndex}
modifiedRuleIndex := make(map[int][]int)
// rollback
defer func() {
if rollbackFlag {
for index, oldNewIndex := range modifiedRuleIndex {
model[sec][ptype].Policy[index] = oldRules[oldNewIndex[0]]
oldPolicy := strings.Join(oldRules[oldNewIndex[0]], DefaultSep)
newPolicy := strings.Join(newRules[oldNewIndex[1]], DefaultSep)
delete(model[sec][ptype].PolicyMap, newPolicy)
model[sec][ptype].PolicyMap[oldPolicy] = index
}
}
}()
newIndex := 0
for oldIndex, oldRule := range oldRules {
oldPolicy := strings.Join(oldRule, DefaultSep)
index, ok := model[sec][ptype].PolicyMap[oldPolicy]
if !ok {
rollbackFlag = true
return false, nil
}
model[sec][ptype].Policy[index] = newRules[newIndex]
delete(model[sec][ptype].PolicyMap, oldPolicy)
model[sec][ptype].PolicyMap[strings.Join(newRules[newIndex], DefaultSep)] = index
modifiedRuleIndex[index] = []int{oldIndex, newIndex}
newIndex++
}
return true, nil
}
// RemovePolicies removes policy rules from the model.
func (model Model) RemovePolicies(sec string, ptype string, rules [][]string) (bool, error) {
affected, err := model.RemovePoliciesWithAffected(sec, ptype, rules)
return len(affected) != 0, err
}
// RemovePoliciesWithAffected removes policy rules from the model, and returns affected rules.
func (model Model) RemovePoliciesWithAffected(sec string, ptype string, rules [][]string) ([][]string, error) {
assertion, err := model.GetAssertion(sec, ptype)
if err != nil {
return nil, err
}
var affected [][]string
removeSet := make(map[string]struct{}, len(rules))
for _, rule := range rules {
key := strings.Join(rule, DefaultSep)
if _, ok := assertion.PolicyMap[key]; !ok {
continue
}
if _, exists := removeSet[key]; exists {
continue
}
affected = append(affected, rule)
removeSet[key] = struct{}{}
}
if len(removeSet) == 0 {
return affected, nil
}
compactPolicy := assertion.Policy[:0]
compactPolicyMap := make(map[string]int, len(assertion.Policy)-len(removeSet))
for _, policyRule := range assertion.Policy {
key := strings.Join(policyRule, DefaultSep)
if _, ok := removeSet[key]; ok {
continue
}
compactPolicyMap[key] = len(compactPolicy)
compactPolicy = append(compactPolicy, policyRule)
}
assertion.Policy = compactPolicy
assertion.PolicyMap = compactPolicyMap
return affected, nil
}
// RemoveFilteredPolicy removes policy rules based on field filters from the model.
func (model Model) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) (bool, [][]string, error) {
_, err := model.GetAssertion(sec, ptype)
if err != nil {
return false, nil, err
}
var tmp [][]string
var effects [][]string
res := false
model[sec][ptype].PolicyMap = map[string]int{}
for _, rule := range model[sec][ptype].Policy {
matched := true
for i, fieldValue := range fieldValues {
if fieldValue != "" && rule[fieldIndex+i] != fieldValue {
matched = false
break
}
}
if matched {
effects = append(effects, rule)
} else {
tmp = append(tmp, rule)
model[sec][ptype].PolicyMap[strings.Join(rule, DefaultSep)] = len(tmp) - 1
}
}
if len(tmp) != len(model[sec][ptype].Policy) {
model[sec][ptype].Policy = tmp
res = true
}
return res, effects, nil
}
// GetValuesForFieldInPolicy gets all values for a field for all rules in a policy, duplicated values are removed.
func (model Model) GetValuesForFieldInPolicy(sec string, ptype string, fieldIndex int) ([]string, error) {
values := []string{}
_, err := model.GetAssertion(sec, ptype)
if err != nil {
return nil, err
}
for _, rule := range model[sec][ptype].Policy {
values = append(values, rule[fieldIndex])
}
util.ArrayRemoveDuplicates(&values)
return values, nil
}
// GetValuesForFieldInPolicyAllTypes gets all values for a field for all rules in a policy of all ptypes, duplicated values are removed.
func (model Model) GetValuesForFieldInPolicyAllTypes(sec string, fieldIndex int) ([]string, error) {
values := []string{}
for ptype := range model[sec] {
v, err := model.GetValuesForFieldInPolicy(sec, ptype, fieldIndex)
if err != nil {
return nil, err
}
values = append(values, v...)
}
util.ArrayRemoveDuplicates(&values)
return values, nil
}
// GetValuesForFieldInPolicyAllTypesByName gets all values for a field for all rules in a policy of all ptypes, duplicated values are removed.
func (model Model) GetValuesForFieldInPolicyAllTypesByName(sec string, field string) ([]string, error) {
values := []string{}
for ptype := range model[sec] {
// GetFieldIndex will return (-1, err) if field is not found, ignore it
index, err := model.GetFieldIndex(ptype, field)
if err != nil {
continue
}
v, err := model.GetValuesForFieldInPolicy(sec, ptype, index)
if err != nil {
return nil, err
}
values = append(values, v...)
}
util.ArrayRemoveDuplicates(&values)
return values, nil
}