Repository: casbin/casbin

Issue:
[feature] add "constraint_definition" section in Casbin model
See:

```
[constraint_definition]
c = sod("finance_requester", "finance_approver")
c2 = sodMax(["payroll_view", "payroll_edit", "payroll_approve"], 1)
c3 = roleMax("superadmin", 2)
c4 = rolePre("db_admin", "security_trained")
```

In `model.conf`, we add a `[constraint_definition]` section to declare RBAC policy invariants that must be satisfied before any `Enforce()` evaluation. For example: `c = sod("finance_requester", "finance_approver")` expresses a static Separation-of-Duties constraint, meaning no subject may be assigned both roles at the same time; `c2 = sodMax(["payroll_view", "payroll_edit", "payroll_approve"], 1)` limits each subject to at most one role from the given role set; `c3 = roleMax("superadmin", 2)` enforces a role cardinality rule that the `superadmin` role can be assigned to no more than two subjects in total; and `c4 = rolePre("db_admin", "security_trained")` defines a prerequisite-role requirement such that any subject assigned `db_admin` must also be assigned `security_trained`. If any constraint is violated, the policy state is considered invalid and enforcement should fail fast (e.g., return a constraint-violation error) rather than merely denying access.

the constraints should be checked when policy is updated. The constraint parsing should report error when constraint is meaningless, like parsing error, or RBAC is not enabled, which causes constraints to unable to work

Hints from issue comments:

Solution patch:
diff --git a/errors/constraint_errors.go b/errors/constraint_errors.go
new file mode 100644
index 0000000..e2a0b87
--- /dev/null
+++ b/errors/constraint_errors.go
@@ -0,0 +1,46 @@
+// 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 errors
+
+import (
+	"errors"
+	"fmt"
+)
+
+// Global errors for constraints defined here.
+var (
+	ErrConstraintViolation         = errors.New("constraint violation")
+	ErrConstraintParsingError      = errors.New("constraint parsing error")
+	ErrConstraintRequiresRBAC      = errors.New("constraints require RBAC to be enabled (role_definition section must exist)")
+	ErrInvalidConstraintDefinition = errors.New("invalid constraint definition")
+)
+
+// ConstraintViolationError represents a specific constraint violation.
+type ConstraintViolationError struct {
+	ConstraintName string
+	Message        string
+}
+
+func (e *ConstraintViolationError) Error() string {
+	return fmt.Sprintf("constraint violation [%s]: %s", e.ConstraintName, e.Message)
+}
+
+// NewConstraintViolationError creates a new constraint violation error.
+func NewConstraintViolationError(constraintName, message string) error {
+	return &ConstraintViolationError{
+		ConstraintName: constraintName,
+		Message:        message,
+	}
+}
diff --git a/examples/rbac_with_constraints_model.conf b/examples/rbac_with_constraints_model.conf
new file mode 100644
index 0000000..2cf26ef
--- /dev/null
+++ b/examples/rbac_with_constraints_model.conf
@@ -0,0 +1,20 @@
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act
+
+[role_definition]
+g = _, _
+
+[constraint_definition]
+c = sod("finance_requester", "finance_approver")
+c2 = sodMax(["payroll_view", "payroll_edit", "payroll_approve"], 1)
+c3 = roleMax("superadmin", 2)
+c4 = rolePre("db_admin", "security_trained")
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
diff --git a/internal_api.go b/internal_api.go
index 1a573e0..32a1db0 100644
--- a/internal_api.go
+++ b/internal_api.go
@@ -34,6 +34,12 @@ func (e *Enforcer) shouldNotify() bool {
 	return e.watcher != nil && e.autoNotifyWatcher
 }
 
+// validateConstraintsForGroupingPolicy validates constraints for grouping policy changes.
+// It returns an error if constraint validation fails.
+func (e *Enforcer) validateConstraintsForGroupingPolicy() error {
+	return e.model.ValidateConstraints()
+}
+
 // addPolicy adds a rule to the current policy.
 func (e *Enforcer) addPolicyWithoutNotify(sec string, ptype string, rule []string) (bool, error) {
 	if e.dispatcher != nil && e.autoNotifyDispatcher {
@@ -63,6 +69,11 @@ func (e *Enforcer) addPolicyWithoutNotify(sec string, ptype string, rule []strin
 		if err != nil {
 			return true, err
 		}
+
+		// Validate constraints after adding grouping policy
+		if err := e.validateConstraintsForGroupingPolicy(); err != nil {
+			return false, err
+		}
 	}
 
 	return true, nil
@@ -106,6 +117,11 @@ func (e *Enforcer) addPoliciesWithoutNotify(sec string, ptype string, rules [][]
 		if err != nil {
 			return true, err
 		}
+
+		// Validate constraints after adding grouping policies
+		if err := e.validateConstraintsForGroupingPolicy(); err != nil {
+			return false, err
+		}
 	}
 
 	return true, nil
@@ -135,6 +151,11 @@ func (e *Enforcer) removePolicyWithoutNotify(sec string, ptype string, rule []st
 		if err != nil {
 			return ruleRemoved, err
 		}
+
+		// Validate constraints after removing grouping policy
+		if err := e.validateConstraintsForGroupingPolicy(); err != nil {
+			return false, err
+		}
 	}
 
 	return ruleRemoved, nil
@@ -166,6 +187,11 @@ func (e *Enforcer) updatePolicyWithoutNotify(sec string, ptype string, oldRule [
 		if err != nil {
 			return ruleUpdated, err
 		}
+
+		// Validate constraints after updating grouping policy
+		if err := e.validateConstraintsForGroupingPolicy(); err != nil {
+			return false, err
+		}
 	}
 
 	return ruleUpdated, nil
@@ -202,6 +228,11 @@ func (e *Enforcer) updatePoliciesWithoutNotify(sec string, ptype string, oldRule
 		if err != nil {
 			return ruleUpdated, err
 		}
+
+		// Validate constraints after updating grouping policies
+		if err := e.validateConstraintsForGroupingPolicy(); err != nil {
+			return false, err
+		}
 	}
 
 	return ruleUpdated, nil
@@ -235,6 +266,11 @@ func (e *Enforcer) removePoliciesWithoutNotify(sec string, ptype string, rules [
 		if err != nil {
 			return rulesRemoved, err
 		}
+
+		// Validate constraints after removing grouping policies
+		if err := e.validateConstraintsForGroupingPolicy(); err != nil {
+			return false, err
+		}
 	}
 	return rulesRemoved, nil
 }
@@ -267,6 +303,11 @@ func (e *Enforcer) removeFilteredPolicyWithoutNotify(sec string, ptype string, f
 		if err != nil {
 			return ruleRemoved, err
 		}
+
+		// Validate constraints after removing filtered grouping policies
+		if err := e.validateConstraintsForGroupingPolicy(); err != nil {
+			return false, err
+		}
 	}
 
 	return ruleRemoved, nil
@@ -322,6 +363,11 @@ func (e *Enforcer) updateFilteredPoliciesWithoutNotify(sec string, ptype string,
 		if err != nil {
 			return oldRules, err
 		}
+
+		// Validate constraints after updating filtered grouping policies
+		if err := e.validateConstraintsForGroupingPolicy(); err != nil {
+			return oldRules, err
+		}
 	}
 
 	return oldRules, nil
diff --git a/model/constraint.go b/model/constraint.go
new file mode 100644
index 0000000..b960c1d
--- /dev/null
+++ b/model/constraint.go
@@ -0,0 +1,282 @@
+// 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
+}
diff --git a/model/model.go b/model/model.go
index 3f76010..29474dc 100644
--- a/model/model.go
+++ b/model/model.go
@@ -44,6 +44,7 @@ var sectionNameMap = map[string]string{
 	"g": "role_definition",
 	"e": "policy_effect",
 	"m": "matchers",
+	"c": "constraint_definition",
 }
 
 // Minimal required sections for a model to be valid.
@@ -196,6 +197,11 @@ func (model Model) LoadModelFromText(text string) error {
 	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)
@@ -209,6 +215,12 @@ func (model Model) loadModelFromConfig(cfg config.ConfigInterface) error {
 	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
 }
 
@@ -400,6 +412,12 @@ func (model Model) ToText() string {
 			s.WriteString(fmt.Sprintf("%s = %s\n", ptype, model["g"][ptype].Value))
 		}
 	}
+	if _, ok := model["c"]; ok {
+		s.WriteString("[constraint_definition]\n")
+		for ptype := range model["c"] {
+			s.WriteString(fmt.Sprintf("%s = %s\n", ptype, model["c"][ptype].Value))
+		}
+	}
 	s.WriteString("[policy_effect]\n")
 	writeString("e")
 	s.WriteString("[matchers]\n")



Test patch:
diff --git a/constraint_test.go b/constraint_test.go
new file mode 100644
index 0000000..4061c68
--- /dev/null
+++ b/constraint_test.go
@@ -0,0 +1,333 @@
+// 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 casbin
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/casbin/casbin/v3/errors"
+	"github.com/casbin/casbin/v3/model"
+)
+
+func TestConstraintSOD(t *testing.T) {
+	modelText := `
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act
+
+[role_definition]
+g = _, _
+
+[constraint_definition]
+c = sod("role1", "role2")
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
+`
+
+	m, err := model.NewModelFromString(modelText)
+	if err != nil {
+		t.Fatalf("Failed to create model: %v", err)
+	}
+
+	e, err := NewEnforcer(m)
+	if err != nil {
+		t.Fatalf("Failed to create enforcer: %v", err)
+	}
+
+	// Add a user to role1 should succeed
+	_, err = e.AddRoleForUser("alice", "role1")
+	if err != nil {
+		t.Fatalf("Failed to add role1 to alice: %v", err)
+	}
+
+	// Add a different user to role2 should succeed
+	_, err = e.AddRoleForUser("bob", "role2")
+	if err != nil {
+		t.Fatalf("Failed to add role2 to bob: %v", err)
+	}
+
+	// Try to add role2 to alice should fail (SOD violation)
+	_, err = e.AddRoleForUser("alice", "role2")
+	if err == nil {
+		t.Fatal("Expected constraint violation error, got nil")
+	}
+	if !strings.Contains(err.Error(), "constraint violation") {
+		t.Fatalf("Expected constraint violation error, got: %v", err)
+	}
+}
+
+func TestConstraintSODMax(t *testing.T) {
+	modelText := `
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act
+
+[role_definition]
+g = _, _
+
+[constraint_definition]
+c = sodMax(["role1", "role2", "role3"], 1)
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
+`
+
+	m, err := model.NewModelFromString(modelText)
+	if err != nil {
+		t.Fatalf("Failed to create model: %v", err)
+	}
+
+	e, err := NewEnforcer(m)
+	if err != nil {
+		t.Fatalf("Failed to create enforcer: %v", err)
+	}
+
+	// Add user to one role should succeed
+	_, err = e.AddRoleForUser("alice", "role1")
+	if err != nil {
+		t.Fatalf("Failed to add role1 to alice: %v", err)
+	}
+
+	// Try to add user to another role from the set should fail
+	_, err = e.AddRoleForUser("alice", "role2")
+	if err == nil {
+		t.Fatal("Expected constraint violation error, got nil")
+	}
+	if !strings.Contains(err.Error(), "constraint violation") {
+		t.Fatalf("Expected constraint violation error, got: %v", err)
+	}
+}
+
+func TestConstraintRoleMax(t *testing.T) {
+	modelText := `
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act
+
+[role_definition]
+g = _, _
+
+[constraint_definition]
+c = roleMax("admin", 2)
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
+`
+
+	m, err := model.NewModelFromString(modelText)
+	if err != nil {
+		t.Fatalf("Failed to create model: %v", err)
+	}
+
+	e, err := NewEnforcer(m)
+	if err != nil {
+		t.Fatalf("Failed to create enforcer: %v", err)
+	}
+
+	// Add first user to admin role should succeed
+	_, err = e.AddRoleForUser("alice", "admin")
+	if err != nil {
+		t.Fatalf("Failed to add admin to alice: %v", err)
+	}
+
+	// Add second user to admin role should succeed
+	_, err = e.AddRoleForUser("bob", "admin")
+	if err != nil {
+		t.Fatalf("Failed to add admin to bob: %v", err)
+	}
+
+	// Try to add third user to admin role should fail (exceeds max)
+	_, err = e.AddRoleForUser("charlie", "admin")
+	if err == nil {
+		t.Fatal("Expected constraint violation error, got nil")
+	}
+	if !strings.Contains(err.Error(), "constraint violation") {
+		t.Fatalf("Expected constraint violation error, got: %v", err)
+	}
+}
+
+func TestConstraintRolePre(t *testing.T) {
+	modelText := `
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act
+
+[role_definition]
+g = _, _
+
+[constraint_definition]
+c = rolePre("db_admin", "security_trained")
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
+`
+
+	m, err := model.NewModelFromString(modelText)
+	if err != nil {
+		t.Fatalf("Failed to create model: %v", err)
+	}
+
+	e, err := NewEnforcer(m)
+	if err != nil {
+		t.Fatalf("Failed to create enforcer: %v", err)
+	}
+
+	// Try to add db_admin without prerequisite should fail
+	_, err = e.AddRoleForUser("alice", "db_admin")
+	if err == nil {
+		t.Fatal("Expected constraint violation error, got nil")
+	}
+	if !strings.Contains(err.Error(), "constraint violation") {
+		t.Fatalf("Expected constraint violation error, got: %v", err)
+	}
+
+	// Add prerequisite role first
+	_, err = e.AddRoleForUser("alice", "security_trained")
+	if err != nil {
+		t.Fatalf("Failed to add security_trained to alice: %v", err)
+	}
+
+	// Now adding db_admin should succeed
+	_, err = e.AddRoleForUser("alice", "db_admin")
+	if err != nil {
+		t.Fatalf("Failed to add db_admin to alice: %v", err)
+	}
+}
+
+func TestConstraintWithoutRBAC(t *testing.T) {
+	modelText := `
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act
+
+[constraint_definition]
+c = sod("role1", "role2")
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
+`
+
+	_, err := model.NewModelFromString(modelText)
+	if err == nil {
+		t.Fatal("Expected error for constraints without RBAC, got nil")
+	}
+	if err != errors.ErrConstraintRequiresRBAC {
+		t.Fatalf("Expected ErrConstraintRequiresRBAC, got: %v", err)
+	}
+}
+
+func TestConstraintParsingError(t *testing.T) {
+	modelText := `
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act
+
+[role_definition]
+g = _, _
+
+[constraint_definition]
+c = invalidFunction("role1", "role2")
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
+`
+
+	_, err := model.NewModelFromString(modelText)
+	if err == nil {
+		t.Fatal("Expected parsing error for invalid constraint, got nil")
+	}
+	if !strings.Contains(err.Error(), "constraint parsing error") {
+		t.Fatalf("Expected constraint parsing error, got: %v", err)
+	}
+}
+
+func TestConstraintRollback(t *testing.T) {
+	modelText := `
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act
+
+[role_definition]
+g = _, _
+
+[constraint_definition]
+c = sod("role1", "role2")
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
+`
+
+	m, err := model.NewModelFromString(modelText)
+	if err != nil {
+		t.Fatalf("Failed to create model: %v", err)
+	}
+
+	e, err := NewEnforcer(m)
+	if err != nil {
+		t.Fatalf("Failed to create enforcer: %v", err)
+	}
+
+	// Add alice to role1
+	_, err = e.AddRoleForUser("alice", "role1")
+	if err != nil {
+		t.Fatalf("Failed to add role1 to alice: %v", err)
+	}
+
+	// Try to add alice to role2 (should fail with constraint violation)
+	_, err = e.AddRoleForUser("alice", "role2")
+	if err == nil {
+		t.Fatal("Expected constraint violation error, got nil")
+	}
+	if !strings.Contains(err.Error(), "constraint violation") {
+		t.Fatalf("Expected constraint violation error, got: %v", err)
+	}
+}