| // 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 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 model["c"] == nil || len(model["c"]) == 0 { |
| return nil // No constraints to validate |
| } |
| |
| // Check if RBAC is enabled |
| if model["g"] == nil || 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 |
| } |