snapshot of https://github.com/casbin/casbin.git (master, 2026-09-15; blobs > 131072B omitted)
This commit is contained in:
285
model/constraint.go
Normal file
285
model/constraint.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user