443 lines
11 KiB
Go
443 lines
11 KiB
Go
// 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
|
|
}
|