286 lines
7.9 KiB
Go
286 lines
7.9 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 (
|
|
"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
|
|
}
|