// 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 }