Repository: casbin/casbin
Pull request #1294 (2023-08-12 07:48:20)

feat: support temporal roles model
fix: https://github.com/casbin/casbin/issues/1259

casbin-bot:  @tangyang9464 @JalinWang please review
hsluoyz: @tony-spark plz review

Patch:
diff --git a/enforcer.go b/enforcer.go
index 6b32436..9f9e550 100644
--- a/enforcer.go
+++ b/enforcer.go
@@ -46,6 +46,7 @@ type Enforcer struct {
 	watcher    persist.Watcher
 	dispatcher persist.Dispatcher
 	rmMap      map[string]rbac.RoleManager
+	condRmMap  map[string]rbac.ConditionalRoleManager
 	matcherMap sync.Map
 
 	enabled              bool
@@ -205,6 +206,7 @@ func (e *Enforcer) SetLogger(logger log.Logger) {
 
 func (e *Enforcer) initialize() {
 	e.rmMap = map[string]rbac.RoleManager{}
+	e.condRmMap = map[string]rbac.ConditionalRoleManager{}
 	e.eft = effector.NewDefaultEffector()
 	e.watcher = nil
 	e.matcherMap = sync.Map{}
@@ -340,18 +342,50 @@ func (e *Enforcer) LoadPolicy() error {
 
 	if e.autoBuildRoleLinks {
 		needToRebuild = true
+		if err := e.rebuildRoleLinks(newModel); err != nil {
+			return err
+		}
+
+		if err := e.rebuildConditionalRoleLinks(newModel); err != nil {
+			return err
+		}
+	}
+	e.model = newModel
+	return nil
+}
+
+func (e *Enforcer) rebuildRoleLinks(newModel model.Model) error {
+	if len(e.rmMap) != 0 {
 		for _, rm := range e.rmMap {
 			err := rm.Clear()
 			if err != nil {
 				return err
 			}
 		}
-		err = newModel.BuildRoleLinks(e.rmMap)
+
+		err := newModel.BuildRoleLinks(e.rmMap)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (e *Enforcer) rebuildConditionalRoleLinks(newModel model.Model) error {
+	if len(e.condRmMap) != 0 {
+		for _, crm := range e.condRmMap {
+			err := crm.Clear()
+			if err != nil {
+				return err
+			}
+		}
+
+		err := newModel.BuildConditionalRoleLinks(e.condRmMap)
 		if err != nil {
 			return err
 		}
 	}
-	e.model = newModel
 	return nil
 }
 
@@ -432,11 +466,23 @@ func (e *Enforcer) SavePolicy() error {
 }
 
 func (e *Enforcer) initRmMap() {
-	for ptype := range e.model["g"] {
+	for ptype, assertion := range e.model["g"] {
 		if rm, ok := e.rmMap[ptype]; ok {
 			_ = rm.Clear()
-		} else {
-			e.rmMap[ptype] = defaultrolemanager.NewRoleManager(10)
+			continue
+		}
+		if len(assertion.Tokens) <= 2 && len(assertion.ParamsTokens) == 0 {
+			e.rmMap[ptype] = defaultrolemanager.NewRoleManagerImpl(10)
+		}
+		if len(assertion.Tokens) <= 2 && len(assertion.ParamsTokens) != 0 {
+			e.condRmMap[ptype] = defaultrolemanager.NewConditionalRoleManager(10)
+		}
+		if len(assertion.Tokens) > 2 {
+			if len(assertion.ParamsTokens) == 0 {
+				e.rmMap[ptype] = defaultrolemanager.NewRoleManager(10)
+			} else {
+				e.condRmMap[ptype] = defaultrolemanager.NewConditionalDomainManager(10)
+			}
 			matchFun := "keyMatch(r_dom, p_dom)"
 			if strings.Contains(e.model["m"]["m"].Value, matchFun) {
 				e.AddNamedDomainMatchingFunc(ptype, "g", util.KeyMatch)
@@ -503,6 +549,12 @@ func (e *Enforcer) BuildIncrementalRoleLinks(op model.PolicyOp, ptype string, ru
 	return e.model.BuildIncrementalRoleLinks(e.rmMap, op, "g", ptype, rules)
 }
 
+// BuildIncrementalConditionalRoleLinks provides incremental build the role inheritance relations with conditions.
+func (e *Enforcer) BuildIncrementalConditionalRoleLinks(op model.PolicyOp, ptype string, rules [][]string) error {
+	e.invalidateMatcherMap()
+	return e.model.BuildIncrementalConditionalRoleLinks(e.condRmMap, op, "g", ptype, rules)
+}
+
 // NewEnforceContext Create a default structure based on the suffix
 func NewEnforceContext(suffix string) EnforceContext {
 	return EnforceContext{
@@ -532,8 +584,12 @@ func (e *Enforcer) enforce(matcher string, explains *[]string, rvals ...interfac
 	functions := e.fm.GetFunctions()
 	if _, ok := e.model["g"]; ok {
 		for key, ast := range e.model["g"] {
-			rm := ast.RM
-			functions[key] = util.GenerateGFunction(rm)
+			if len(e.rmMap) != 0 {
+				functions[key] = util.GenerateGFunction(ast.RM)
+			}
+			if len(e.condRmMap) != 0 {
+				functions[key] = util.GenerateConditionalGFunction(ast.CondRM)
+			}
 		}
 	}
 
@@ -843,6 +899,45 @@ func (e *Enforcer) AddNamedDomainMatchingFunc(ptype, name string, fn rbac.Matchi
 	return false
 }
 
+// AddNamedLinkConditionFunc Add condition function fn for Link userName->roleName,
+// when fn returns true, Link is valid, otherwise invalid
+func (e *Enforcer) AddNamedLinkConditionFunc(ptype, user, role string, fn rbac.LinkConditionFunc) bool {
+	if rm, ok := e.condRmMap[ptype]; ok {
+		rm.AddLinkConditionFunc(user, role, fn)
+		return true
+	}
+	return false
+}
+
+// AddNamedDomainLinkConditionFunc Add condition function fn for Link userName-> {roleName, domain},
+// when fn returns true, Link is valid, otherwise invalid
+func (e *Enforcer) AddNamedDomainLinkConditionFunc(ptype, user, role string, domain string, fn rbac.LinkConditionFunc) bool {
+	if rm, ok := e.condRmMap[ptype]; ok {
+		rm.AddDomainLinkConditionFunc(user, role, domain, fn)
+		return true
+	}
+	return false
+}
+
+// SetNamedLinkConditionFuncParams Sets the parameters of the condition function fn for Link userName->roleName
+func (e *Enforcer) SetNamedLinkConditionFuncParams(ptype, user, role string, params ...string) bool {
+	if rm, ok := e.condRmMap[ptype]; ok {
+		rm.SetLinkConditionFuncParams(user, role, params...)
+		return true
+	}
+	return false
+}
+
+// SetNamedDomainLinkConditionFuncParams Sets the parameters of the condition function fn
+// for Link userName->{roleName, domain}
+func (e *Enforcer) SetNamedDomainLinkConditionFuncParams(ptype, user, role, domain string, params ...string) bool {
+	if rm, ok := e.condRmMap[ptype]; ok {
+		rm.SetDomainLinkConditionFuncParams(user, role, domain, params...)
+		return true
+	}
+	return false
+}
+
 // assumes bounds have already been checked
 type enforceParameters struct {
 	rTokens map[string]int
diff --git a/examples/rbac_with_domain_temporal_roles_model.conf b/examples/rbac_with_domain_temporal_roles_model.conf
new file mode 100644
index 0000000..e1ab8a4
--- /dev/null
+++ b/examples/rbac_with_domain_temporal_roles_model.conf
@@ -0,0 +1,14 @@
+[request_definition]
+r = sub, dom, obj, act
+
+[policy_definition]
+p = sub, dom, obj, act
+
+[role_definition]
+g = _, _, _, (_, _)
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act
\ No newline at end of file
diff --git a/examples/rbac_with_domain_temporal_roles_policy.csv b/examples/rbac_with_domain_temporal_roles_policy.csv
new file mode 100644
index 0000000..8234b64
--- /dev/null
+++ b/examples/rbac_with_domain_temporal_roles_policy.csv
@@ -0,0 +1,24 @@
+p, alice, domain1, data1, read
+p, alice, domain1, data1, write
+p, data2_admin, domain2, data2, read
+p, data2_admin, domain2, data2, write
+p, data3_admin, domain3, data3, read
+p, data3_admin, domain3, data3, write
+p, data4_admin, domain4, data4, read
+p, data4_admin, domain4, data4, write
+p, data5_admin, domain5, data5, read
+p, data5_admin, domain5, data5, write
+p, data6_admin, domain6, data6, read
+p, data6_admin, domain6, data6, write
+p, data7_admin, domain7, data7, read
+p, data7_admin, domain7, data7, write
+p, data8_admin, domain8, data8, read
+p, data8_admin, domain8, data8, write
+
+g, alice, data2_admin, domain2, 0000-01-01 00:00:00, 0000-01-02 00:00:00
+g, alice, data3_admin, domain3, 0000-01-01 00:00:00, 9999-12-30 00:00:00
+g, alice, data4_admin, domain4, _, _
+g, alice, data5_admin, domain5, _, 9999-12-30 00:00:00
+g, alice, data6_admin, domain6, _, 0000-01-02 00:00:00
+g, alice, data7_admin, domain7, 0000-01-01 00:00:00, _
+g, alice, data8_admin, domain8, 9999-12-30 00:00:00, _
\ No newline at end of file
diff --git a/examples/rbac_with_temporal_roles_model.conf b/examples/rbac_with_temporal_roles_model.conf
new file mode 100644
index 0000000..feeae16
--- /dev/null
+++ b/examples/rbac_with_temporal_roles_model.conf
@@ -0,0 +1,14 @@
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act
+
+[role_definition]
+g = _, _, (_, _)
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
\ No newline at end of file
diff --git a/examples/rbac_with_temporal_roles_policy.csv b/examples/rbac_with_temporal_roles_policy.csv
new file mode 100644
index 0000000..c8f77e7
--- /dev/null
+++ b/examples/rbac_with_temporal_roles_policy.csv
@@ -0,0 +1,24 @@
+p, alice, data1, read
+p, alice, data1, write
+p, data2_admin, data2, read
+p, data2_admin, data2, write
+p, data3_admin, data3, read
+p, data3_admin, data3, write
+p, data4_admin, data4, read
+p, data4_admin, data4, write
+p, data5_admin, data5, read
+p, data5_admin, data5, write
+p, data6_admin, data6, read
+p, data6_admin, data6, write
+p, data7_admin, data7, read
+p, data7_admin, data7, write
+p, data8_admin, data8, read
+p, data8_admin, data8, write
+
+g, alice, data2_admin, 0000-01-01 00:00:00, 0000-01-02 00:00:00
+g, alice, data3_admin, 0000-01-01 00:00:00, 9999-12-30 00:00:00
+g, alice, data4_admin, _, _
+g, alice, data5_admin, _, 9999-12-30 00:00:00
+g, alice, data6_admin, _, 0000-01-02 00:00:00
+g, alice, data7_admin, 0000-01-01 00:00:00, _
+g, alice, data8_admin, 9999-12-30 00:00:00, _
\ No newline at end of file
diff --git a/internal_api.go b/internal_api.go
index d3d5640..42991b5 100644
--- a/internal_api.go
+++ b/internal_api.go
@@ -91,6 +91,11 @@ func (e *Enforcer) addPoliciesWithoutNotify(sec string, ptype string, rules [][]
 		if err != nil {
 			return true, err
 		}
+
+		err = e.BuildIncrementalConditionalRoleLinks(model.PolicyAdd, ptype, rules)
+		if err != nil {
+			return true, err
+		}
 	}
 
 	return true, nil
diff --git a/log/default_logger.go b/log/default_logger.go
index 1ad3a92..9994f39 100644
--- a/log/default_logger.go
+++ b/log/default_logger.go
@@ -95,3 +95,10 @@ func (l *DefaultLogger) LogRole(roles []string) {
 
 	log.Println("Roles: ", strings.Join(roles, "\n"))
 }
+
+func (l *DefaultLogger) LogError(err error, msg ...string) {
+	if !l.enabled {
+		return
+	}
+	log.Println(msg, err)
+}
diff --git a/log/log_util.go b/log/log_util.go
index 191c817..617c039 100644
--- a/log/log_util.go
+++ b/log/log_util.go
@@ -45,3 +45,8 @@ func LogRole(roles []string) {
 func LogPolicy(policy map[string][][]string) {
 	logger.LogPolicy(policy)
 }
+
+// LogError logs the error information
+func LogError(err error, msg ...string) {
+	logger.LogError(err, msg...)
+}
diff --git a/log/logger.go b/log/logger.go
index c60e07a..8982cae 100644
--- a/log/logger.go
+++ b/log/logger.go
@@ -35,4 +35,7 @@ type Logger interface {
 
 	// LogPolicy log info related to policy.
 	LogPolicy(policy map[string][][]string)
+
+	// LogError log info relate to error
+	LogError(err error, msg ...string)
 }
diff --git a/log/mocks/mock_logger.go b/log/mocks/mock_logger.go
index 4fd438d..d6107a8 100644
--- a/log/mocks/mock_logger.go
+++ b/log/mocks/mock_logger.go
@@ -10,42 +10,42 @@ import (
 	gomock "github.com/golang/mock/gomock"
 )
 
-// MockLogger is a mock of Logger interface
+// MockLogger is a mock of Logger interface.
 type MockLogger struct {
 	ctrl     *gomock.Controller
 	recorder *MockLoggerMockRecorder
 }
 
-// MockLoggerMockRecorder is the mock recorder for MockLogger
+// MockLoggerMockRecorder is the mock recorder for MockLogger.
 type MockLoggerMockRecorder struct {
 	mock *MockLogger
 }
 
-// NewMockLogger creates a new mock instance
+// NewMockLogger creates a new mock instance.
 func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
 	mock := &MockLogger{ctrl: ctrl}
 	mock.recorder = &MockLoggerMockRecorder{mock}
 	return mock
 }
 
-// EXPECT returns an object that allows the caller to indicate expected use
+// EXPECT returns an object that allows the caller to indicate expected use.
 func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
 	return m.recorder
 }
 
-// EnableLog mocks base method
+// EnableLog mocks base method.
 func (m *MockLogger) EnableLog(arg0 bool) {
 	m.ctrl.T.Helper()
 	m.ctrl.Call(m, "EnableLog", arg0)
 }
 
-// EnableLog indicates an expected call of EnableLog
+// EnableLog indicates an expected call of EnableLog.
 func (mr *MockLoggerMockRecorder) EnableLog(arg0 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnableLog", reflect.TypeOf((*MockLogger)(nil).EnableLog), arg0)
 }
 
-// IsEnabled mocks base method
+// IsEnabled mocks base method.
 func (m *MockLogger) IsEnabled() bool {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "IsEnabled")
@@ -53,55 +53,72 @@ func (m *MockLogger) IsEnabled() bool {
 	return ret0
 }
 
-// IsEnabled indicates an expected call of IsEnabled
+// IsEnabled indicates an expected call of IsEnabled.
 func (mr *MockLoggerMockRecorder) IsEnabled() *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsEnabled", reflect.TypeOf((*MockLogger)(nil).IsEnabled))
 }
 
-// LogEnforce mocks base method
+// LogEnforce mocks base method.
 func (m *MockLogger) LogEnforce(arg0 string, arg1 []interface{}, arg2 bool, arg3 [][]string) {
 	m.ctrl.T.Helper()
 	m.ctrl.Call(m, "LogEnforce", arg0, arg1, arg2, arg3)
 }
 
-// LogEnforce indicates an expected call of LogEnforce
+// LogEnforce indicates an expected call of LogEnforce.
 func (mr *MockLoggerMockRecorder) LogEnforce(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogEnforce", reflect.TypeOf((*MockLogger)(nil).LogEnforce), arg0, arg1, arg2, arg3)
 }
 
-// LogModel mocks base method
+// LogError mocks base method.
+func (m *MockLogger) LogError(arg0 error, arg1 ...string) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0}
+	for _, a := range arg1 {
+		varargs = append(varargs, a)
+	}
+	m.ctrl.Call(m, "LogError", varargs...)
+}
+
+// LogError indicates an expected call of LogError.
+func (mr *MockLoggerMockRecorder) LogError(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0}, arg1...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogError", reflect.TypeOf((*MockLogger)(nil).LogError), varargs...)
+}
+
+// LogModel mocks base method.
 func (m *MockLogger) LogModel(arg0 [][]string) {
 	m.ctrl.T.Helper()
 	m.ctrl.Call(m, "LogModel", arg0)
 }
 
-// LogModel indicates an expected call of LogModel
+// LogModel indicates an expected call of LogModel.
 func (mr *MockLoggerMockRecorder) LogModel(arg0 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogModel", reflect.TypeOf((*MockLogger)(nil).LogModel), arg0)
 }
 
-// LogPolicy mocks base method
+// LogPolicy mocks base method.
 func (m *MockLogger) LogPolicy(arg0 map[string][][]string) {
 	m.ctrl.T.Helper()
 	m.ctrl.Call(m, "LogPolicy", arg0)
 }
 
-// LogPolicy indicates an expected call of LogPolicy
+// LogPolicy indicates an expected call of LogPolicy.
 func (mr *MockLoggerMockRecorder) LogPolicy(arg0 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogPolicy", reflect.TypeOf((*MockLogger)(nil).LogPolicy), arg0)
 }
 
-// LogRole mocks base method
+// LogRole mocks base method.
 func (m *MockLogger) LogRole(arg0 []string) {
 	m.ctrl.T.Helper()
 	m.ctrl.Call(m, "LogRole", arg0)
 }
 
-// LogRole indicates an expected call of LogRole
+// LogRole indicates an expected call of LogRole.
 func (mr *MockLoggerMockRecorder) LogRole(arg0 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogRole", reflect.TypeOf((*MockLogger)(nil).LogRole), arg0)
diff --git a/model/assertion.go b/model/assertion.go
index 054dd0f..cbe0edd 100644
--- a/model/assertion.go
+++ b/model/assertion.go
@@ -28,9 +28,11 @@ type Assertion struct {
 	Key           string
 	Value         string
 	Tokens        []string
+	ParamsTokens  []string
 	Policy        [][]string
 	PolicyMap     map[string]int
 	RM            rbac.RoleManager
+	CondRM        rbac.ConditionalRoleManager
 	FieldIndexMap map[string]int
 
 	logger log.Logger
@@ -63,7 +65,6 @@ func (ast *Assertion) buildIncrementalRoleLinks(rm rbac.RoleManager, op PolicyOp
 			}
 		}
 	}
-
 	return nil
 }
 
@@ -89,6 +90,81 @@ func (ast *Assertion) buildRoleLinks(rm rbac.RoleManager) error {
 	return nil
 }
 
+func (ast *Assertion) buildIncrementalConditionalRoleLinks(condRM rbac.ConditionalRoleManager, op PolicyOp, rules [][]string) error {
+	ast.CondRM = condRM
+	count := strings.Count(ast.Value, "_")
+	if count < 2 {
+		return errors.New("the number of \"_\" in role definition should be at least 2")
+	}
+
+	for _, rule := range rules {
+		if len(rule) < count {
+			return errors.New("grouping policy elements do not meet role definition")
+		}
+		if len(rule) > count {
+			rule = rule[:count]
+		}
+
+		var err error
+		domainRule := rule[2:len(ast.Tokens)]
+
+		switch op {
+		case PolicyAdd:
+			err = ast.addConditionalRoleLink(rule, domainRule)
+		case PolicyRemove:
+			err = ast.CondRM.DeleteLink(rule[0], rule[1], rule[2:]...)
+		}
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (ast *Assertion) buildConditionalRoleLinks(condRM rbac.ConditionalRoleManager) error {
+	ast.CondRM = condRM
+	count := strings.Count(ast.Value, "_")
+	if count < 2 {
+		return errors.New("the number of \"_\" in role definition should be at least 2")
+	}
+	for _, rule := range ast.Policy {
+		if len(rule) < count {
+			return errors.New("grouping policy elements do not meet role definition")
+		}
+		if len(rule) > count {
+			rule = rule[:count]
+		}
+
+		domainRule := rule[2:len(ast.Tokens)]
+
+		err := ast.addConditionalRoleLink(rule, domainRule)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// addConditionalRoleLinks adds Link to rbac.ConditionalRoleManager and sets the parameters for LinkConditionFunc
+func (ast *Assertion) addConditionalRoleLink(rule []string, domainRule []string) error {
+	var err error
+	if len(domainRule) == 0 {
+		err = ast.CondRM.AddLink(rule[0], rule[1])
+		if err == nil {
+			ast.CondRM.SetLinkConditionFuncParams(rule[0], rule[1], rule[len(ast.Tokens):]...)
+		}
+	} else {
+		domain := domainRule[0]
+		err = ast.CondRM.AddLink(rule[0], rule[1], domain)
+		if err == nil {
+			ast.CondRM.SetDomainLinkConditionFuncParams(rule[0], rule[1], domain, rule[len(ast.Tokens):]...)
+		}
+	}
+	return err
+}
+
 func (ast *Assertion) setLogger(logger log.Logger) {
 	ast.logger = logger
 }
diff --git a/model/model.go b/model/model.go
index 25ce824..5db4c2c 100644
--- a/model/model.go
+++ b/model/model.go
@@ -54,6 +54,18 @@ func loadAssertion(model Model, cfg config.ConfigInterface, sec string, key stri
 	return model.AddDef(sec, key, value)
 }
 
+var paramsRegex = regexp.MustCompile(`\((.*?)\)`)
+
+// getParamsToken Get ParamsToken from Assertion.Value
+func getParamsToken(value string) []string {
+	paramsString := paramsRegex.FindString(value)
+	if paramsString == "" {
+		return nil
+	}
+	paramsString = strings.TrimSuffix(strings.TrimPrefix(paramsString, "("), ")")
+	return strings.Split(paramsString, ",")
+}
+
 // AddDef adds an assertion to the model.
 func (model Model) AddDef(sec string, key string, value string) bool {
 	if value == "" {
@@ -73,7 +85,9 @@ func (model Model) AddDef(sec string, key string, value string) bool {
 			ast.Tokens[i] = key + "_" + strings.TrimSpace(ast.Tokens[i])
 		}
 	} else if sec == "g" {
+		ast.ParamsTokens = getParamsToken(ast.Value)
 		ast.Tokens = strings.Split(ast.Value, ",")
+		ast.Tokens = ast.Tokens[:len(ast.Tokens)-len(ast.ParamsTokens)]
 	} else {
 		ast.Value = util.RemoveComments(util.EscapeAssertion(ast.Value))
 	}
diff --git a/model/policy.go b/model/policy.go
index d4b4a7a..23b16e9 100644
--- a/model/policy.go
+++ b/model/policy.go
@@ -37,7 +37,7 @@ const DefaultSep = ","
 
 // BuildIncrementalRoleLinks provides incremental build the role inheritance relations.
 func (model Model) BuildIncrementalRoleLinks(rmMap map[string]rbac.RoleManager, op PolicyOp, sec string, ptype string, rules [][]string) error {
-	if sec == "g" {
+	if sec == "g" && rmMap[ptype] != nil {
 		return model[sec][ptype].buildIncrementalRoleLinks(rmMap[ptype], op, rules)
 	}
 	return nil
@@ -57,6 +57,28 @@ func (model Model) BuildRoleLinks(rmMap map[string]rbac.RoleManager) error {
 	return nil
 }
 
+// BuildIncrementalConditionalRoleLinks provides incremental build the role inheritance relations.
+func (model Model) BuildIncrementalConditionalRoleLinks(condRmMap map[string]rbac.ConditionalRoleManager, op PolicyOp, sec string, ptype string, rules [][]string) error {
+	if sec == "g" && condRmMap[ptype] != nil {
+		return model[sec][ptype].buildIncrementalConditionalRoleLinks(condRmMap[ptype], op, rules)
+	}
+	return nil
+}
+
+// BuildConditionalRoleLinks initializes the roles in RBAC.
+func (model Model) BuildConditionalRoleLinks(condRmMap map[string]rbac.ConditionalRoleManager) error {
+	model.PrintPolicy()
+	for ptype, ast := range model["g"] {
+		condRm := condRmMap[ptype]
+		err := ast.buildConditionalRoleLinks(condRm)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
 // PrintPolicy prints the policy to log.
 func (model Model) PrintPolicy() {
 	if !model.GetLogger().IsEnabled() {
diff --git a/rbac/default-role-manager/role_manager.go b/rbac/default-role-manager/role_manager.go
index a0f0104..3b6ad8b 100644
--- a/rbac/default-role-manager/role_manager.go
+++ b/rbac/default-role-manager/role_manager.go
@@ -19,7 +19,6 @@ import (
 	"strings"
 	"sync"
 
-	"github.com/casbin/casbin/v2/errors"
 	"github.com/casbin/casbin/v2/log"
 	"github.com/casbin/casbin/v2/rbac"
 	"github.com/casbin/casbin/v2/util"
@@ -29,11 +28,13 @@ const defaultDomain string = ""
 
 // Role represents the data structure for a role in RBAC.
 type Role struct {
-	name      string
-	roles     *sync.Map
-	users     *sync.Map
-	matched   *sync.Map
-	matchedBy *sync.Map
+	name                       string
+	roles                      *sync.Map
+	users                      *sync.Map
+	matched                    *sync.Map
+	matchedBy                  *sync.Map
+	linkConditionFuncMap       *sync.Map
+	linkConditionFuncParamsMap *sync.Map
 }
 
 func newRole(name string) *Role {
@@ -43,6 +44,8 @@ func newRole(name string) *Role {
 	r.users = &sync.Map{}
 	r.matched = &sync.Map{}
 	r.matchedBy = &sync.Map{}
+	r.linkConditionFuncMap = &sync.Map{}
+	r.linkConditionFuncParamsMap = &sync.Map{}
 	return &r
 }
 
@@ -163,6 +166,35 @@ func (r *Role) getUsers() []string {
 	return names
 }
 
+type linkConditionFuncKey struct {
+	roleName   string
+	domainName string
+}
+
+func (r *Role) addLinkConditionFunc(role *Role, domain string, fn rbac.LinkConditionFunc) {
+	r.linkConditionFuncMap.Store(linkConditionFuncKey{role.name, domain}, fn)
+}
+
+func (r *Role) getLinkConditionFunc(role *Role, domain string) (rbac.LinkConditionFunc, bool) {
+	fn, ok := r.linkConditionFuncMap.Load(linkConditionFuncKey{role.name, domain})
+	if fn == nil {
+		return nil, ok
+	}
+	return fn.(rbac.LinkConditionFunc), ok
+}
+
+func (r *Role) setLinkConditionFuncParams(role *Role, domain string, params ...string) {
+	r.linkConditionFuncParamsMap.Store(linkConditionFuncKey{role.name, domain}, params)
+}
+
+func (r *Role) getLinkConditionFuncParams(role *Role, domain string) ([]string, bool) {
+	params, ok := r.linkConditionFuncParamsMap.Load(linkConditionFuncKey{role.name, domain})
+	if params == nil {
+		return nil, ok
+	}
+	return params.([]string), ok
+}
+
 // RoleManagerImpl provides a default implementation for the RoleManager interface
 type RoleManagerImpl struct {
 	allRoles           *sync.Map
@@ -500,10 +532,8 @@ func (dm *DomainManager) getDomain(domains ...string) (domain string, err error)
 	switch len(domains) {
 	case 0:
 		return defaultDomain, nil
-	case 1:
-		return domains[0], nil
 	default:
-		return "", errors.ErrDomainParameter
+		return domains[0], nil
 	}
 }
 
@@ -691,3 +721,294 @@ func NewRoleManager(maxHierarchyLevel int) *RoleManager {
 	rm.DomainManager = NewDomainManager(maxHierarchyLevel)
 	return rm
 }
+
+type ConditionalRoleManager struct {
+	RoleManagerImpl
+}
+
+func (crm *ConditionalRoleManager) copyFrom(other *ConditionalRoleManager) {
+	other.Range(func(name1, name2 string, domain ...string) bool {
+		_ = crm.AddLink(name1, name2, domain...)
+		return true
+	})
+}
+
+// use this constructor to avoid rebuild of AddMatchingFunc
+func newConditionalRoleManagerWithMatchingFunc(maxHierarchyLevel int, fn rbac.MatchingFunc) *ConditionalRoleManager {
+	rm := NewConditionalRoleManager(maxHierarchyLevel)
+	rm.matchingFunc = fn
+	return rm
+}
+
+// NewConditionalRoleManager is the constructor for creating an instance of the
+// ConditionalRoleManager implementation.
+func NewConditionalRoleManager(maxHierarchyLevel int) *ConditionalRoleManager {
+	rm := ConditionalRoleManager{}
+	_ = rm.Clear() //init allRoles and matchingFuncCache
+	rm.maxHierarchyLevel = maxHierarchyLevel
+	rm.SetLogger(&log.DefaultLogger{})
+	return &rm
+}
+
+// HasLink determines whether role: name1 inherits role: name2.
+func (crm *ConditionalRoleManager) HasLink(name1 string, name2 string, domains ...string) (bool, error) {
+	if name1 == name2 || (crm.matchingFunc != nil && crm.Match(name1, name2)) {
+		return true, nil
+	}
+
+	user, userCreated := crm.getRole(name1)
+	role, roleCreated := crm.getRole(name2)
+
+	if userCreated {
+		defer crm.removeRole(user.name)
+	}
+	if roleCreated {
+		defer crm.removeRole(role.name)
+	}
+
+	return crm.hasLinkHelper(role.name, map[string]*Role{user.name: user}, crm.maxHierarchyLevel, domains...), nil
+}
+
+// hasLinkHelper use the Breadth First Search algorithm to traverse the Role tree
+// Judging whether the user has a role (has link) is to judge whether the role node can be reached from the user node
+func (crm *ConditionalRoleManager) hasLinkHelper(targetName string, roles map[string]*Role, level int, domains ...string) bool {
+	if level < 0 || len(roles) == 0 {
+		return false
+	}
+	nextRoles := map[string]*Role{}
+	for _, role := range roles {
+		if targetName == role.name || (crm.matchingFunc != nil && crm.Match(role.name, targetName)) {
+			return true
+		}
+		role.rangeRoles(func(key, value interface{}) bool {
+			nextRole := value.(*Role)
+			return crm.getNextRoles(role, nextRole, domains, nextRoles)
+		})
+	}
+
+	return crm.hasLinkHelper(targetName, nextRoles, level-1)
+}
+
+func (crm *ConditionalRoleManager) getNextRoles(currentRole, nextRole *Role, domains []string, nextRoles map[string]*Role) bool {
+	passLinkConditionFunc := true
+	var err error
+	// If LinkConditionFunc exists, it needs to pass the verification to get nextRole
+	if len(domains) == 0 {
+		if linkConditionFunc, existLinkCondition := crm.GetLinkConditionFunc(currentRole.name, nextRole.name); existLinkCondition {
+			params, _ := crm.GetLinkConditionFuncParams(currentRole.name, nextRole.name)
+			passLinkConditionFunc, err = linkConditionFunc(params...)
+		}
+	} else {
+		if linkConditionFunc, existLinkCondition := crm.GetDomainLinkConditionFunc(currentRole.name, nextRole.name, domains[0]); existLinkCondition {
+			params, _ := crm.GetLinkConditionFuncParams(currentRole.name, nextRole.name, domains[0])
+			passLinkConditionFunc, err = linkConditionFunc(params...)
+		}
+	}
+
+	if err != nil {
+		crm.logger.LogError(err, "hasLinkHelper LinkCondition Error")
+		return false
+	}
+
+	if passLinkConditionFunc {
+		nextRoles[nextRole.name] = nextRole
+	}
+
+	return true
+}
+
+// GetLinkConditionFunc get LinkConditionFunc based on userName, roleName
+func (crm *ConditionalRoleManager) GetLinkConditionFunc(userName, roleName string) (rbac.LinkConditionFunc, bool) {
+	return crm.GetDomainLinkConditionFunc(userName, roleName, defaultDomain)
+}
+
+// GetDomainLinkConditionFunc get LinkConditionFunc based on userName, roleName, domain
+func (crm *ConditionalRoleManager) GetDomainLinkConditionFunc(userName, roleName, domain string) (rbac.LinkConditionFunc, bool) {
+	user, userCreated := crm.getRole(userName)
+	role, roleCreated := crm.getRole(roleName)
+
+	if userCreated {
+		crm.removeRole(user.name)
+		return nil, false
+	}
+
+	if roleCreated {
+		crm.removeRole(role.name)
+		return nil, false
+	}
+
+	return user.getLinkConditionFunc(role, domain)
+}
+
+// GetLinkConditionFuncParams gets parameters of LinkConditionFunc based on userName, roleName, domain
+func (crm *ConditionalRoleManager) GetLinkConditionFuncParams(userName, roleName string, domain ...string) ([]string, bool) {
+	user, userCreated := crm.getRole(userName)
+	role, roleCreated := crm.getRole(roleName)
+
+	if userCreated {
+		crm.removeRole(user.name)
+		return nil, false
+	}
+
+	if roleCreated {
+		crm.removeRole(role.name)
+		return nil, false
+	}
+
+	domainName := defaultDomain
+	if len(domain) != 0 {
+		domainName = domain[0]
+	}
+
+	if params, ok := user.getLinkConditionFuncParams(role, domainName); ok {
+		return params, true
+	} else {
+		return nil, false
+	}
+}
+
+// AddLinkConditionFunc is based on userName, roleName, add LinkConditionFunc
+func (crm *ConditionalRoleManager) AddLinkConditionFunc(userName, roleName string, fn rbac.LinkConditionFunc) {
+	crm.AddDomainLinkConditionFunc(userName, roleName, defaultDomain, fn)
+}
+
+// AddDomainLinkConditionFunc is based on userName, roleName, domain, add LinkConditionFunc
+func (crm *ConditionalRoleManager) AddDomainLinkConditionFunc(userName, roleName, domain string, fn rbac.LinkConditionFunc) {
+	user, _ := crm.getRole(userName)
+	role, _ := crm.getRole(roleName)
+
+	user.addLinkConditionFunc(role, domain, fn)
+}
+
+// SetLinkConditionFuncParams sets parameters of LinkConditionFunc based on userName, roleName, domain
+func (crm *ConditionalRoleManager) SetLinkConditionFuncParams(userName, roleName string, params ...string) {
+	crm.SetDomainLinkConditionFuncParams(userName, roleName, defaultDomain, params...)
+}
+
+// SetDomainLinkConditionFuncParams sets parameters of LinkConditionFunc based on userName, roleName, domain
+func (crm *ConditionalRoleManager) SetDomainLinkConditionFuncParams(userName, roleName, domain string, params ...string) {
+	user, _ := crm.getRole(userName)
+	role, _ := crm.getRole(roleName)
+
+	user.setLinkConditionFuncParams(role, domain, params...)
+}
+
+type ConditionalDomainManager struct {
+	ConditionalRoleManager
+	DomainManager
+}
+
+// NewConditionalDomainManager is the constructor for creating an instance of the
+// ConditionalDomainManager implementation.
+func NewConditionalDomainManager(maxHierarchyLevel int) *ConditionalDomainManager {
+	rm := ConditionalDomainManager{}
+	_ = rm.Clear() //init allRoles and matchingFuncCache
+	rm.maxHierarchyLevel = maxHierarchyLevel
+	rm.SetLogger(&log.DefaultLogger{})
+	return &rm
+}
+
+func (dm *ConditionalDomainManager) load(name interface{}) (value *ConditionalRoleManager, ok bool) {
+	if r, ok := dm.rmMap.Load(name); ok {
+		return r.(*ConditionalRoleManager), true
+	}
+	return nil, false
+}
+
+// load or create a ConditionalRoleManager instance of domain
+func (cdm *ConditionalDomainManager) getConditionalRoleManager(domain string, store bool) *ConditionalRoleManager {
+	var rm *ConditionalRoleManager
+	var ok bool
+
+	if rm, ok = cdm.load(domain); !ok {
+		rm = newConditionalRoleManagerWithMatchingFunc(cdm.maxHierarchyLevel, cdm.matchingFunc)
+		if store {
+			cdm.rmMap.Store(domain, rm)
+		}
+		if cdm.domainMatchingFunc != nil {
+			cdm.rmMap.Range(func(key, value interface{}) bool {
+				domain2 := key.(string)
+				rm2 := value.(*ConditionalRoleManager)
+				if domain != domain2 && cdm.Match(domain, domain2) {
+					rm.copyFrom(rm2)
+				}
+				return true
+			})
+		}
+	}
+	return rm
+}
+
+// HasLink determines whether role: name1 inherits role: name2.
+func (cdm *ConditionalDomainManager) HasLink(name1 string, name2 string, domains ...string) (bool, error) {
+	domain, err := cdm.getDomain(domains...)
+	if err != nil {
+		return false, err
+	}
+	rm := cdm.getConditionalRoleManager(domain, false)
+	return rm.HasLink(name1, name2, domains...)
+}
+
+// AddLink adds the inheritance link between role: name1 and role: name2.
+// aka role: name1 inherits role: name2.
+func (cdm *ConditionalDomainManager) AddLink(name1 string, name2 string, domains ...string) error {
+	domain, err := cdm.getDomain(domains...)
+	if err != nil {
+		return err
+	}
+	conditionalRoleManager := cdm.getConditionalRoleManager(domain, true) //create role manager if it does not exist
+	_ = conditionalRoleManager.AddLink(name1, name2, domain)
+
+	cdm.rangeAffectedRoleManagers(domain, func(rm *RoleManagerImpl) {
+		_ = rm.AddLink(name1, name2, domain)
+	})
+	return nil
+}
+
+// DeleteLink deletes the inheritance link between role: name1 and role: name2.
+// aka role: name1 does not inherit role: name2 any more.
+func (cdm *ConditionalDomainManager) DeleteLink(name1 string, name2 string, domains ...string) error {
+	domain, err := cdm.getDomain(domains...)
+	if err != nil {
+		return err
+	}
+	conditionalRoleManager := cdm.getConditionalRoleManager(domain, true) //create role manager if it does not exist
+	_ = conditionalRoleManager.DeleteLink(name1, name2, domain)
+
+	cdm.rangeAffectedRoleManagers(domain, func(rm *RoleManagerImpl) {
+		_ = rm.DeleteLink(name1, name2, domain)
+	})
+	return nil
+}
+
+// AddLinkConditionFunc is based on userName, roleName, add LinkConditionFunc
+func (cdm *ConditionalDomainManager) AddLinkConditionFunc(userName, roleName string, fn rbac.LinkConditionFunc) {
+	cdm.rmMap.Range(func(key, value interface{}) bool {
+		value.(*ConditionalRoleManager).AddLinkConditionFunc(userName, roleName, fn)
+		return true
+	})
+}
+
+// AddDomainLinkConditionFunc is based on userName, roleName, domain, add LinkConditionFunc
+func (cdm *ConditionalDomainManager) AddDomainLinkConditionFunc(userName, roleName, domain string, fn rbac.LinkConditionFunc) {
+	cdm.rmMap.Range(func(key, value interface{}) bool {
+		value.(*ConditionalRoleManager).AddDomainLinkConditionFunc(userName, roleName, domain, fn)
+		return true
+	})
+}
+
+// SetLinkConditionFuncParams sets parameters of LinkConditionFunc based on userName, roleName
+func (cdm *ConditionalDomainManager) SetLinkConditionFuncParams(userName, roleName string, params ...string) {
+	cdm.rmMap.Range(func(key, value interface{}) bool {
+		value.(*ConditionalRoleManager).SetLinkConditionFuncParams(userName, roleName, params...)
+		return true
+	})
+}
+
+// SetDomainLinkConditionFuncParams sets parameters of LinkConditionFunc based on userName, roleName, domain
+func (cdm *ConditionalDomainManager) SetDomainLinkConditionFuncParams(userName, roleName, domain string, params ...string) {
+	cdm.rmMap.Range(func(key, value interface{}) bool {
+		value.(*ConditionalRoleManager).SetDomainLinkConditionFuncParams(userName, roleName, domain, params...)
+		return true
+	})
+}
diff --git a/rbac/role_manager.go b/rbac/role_manager.go
index 18b4ca8..331367d 100644
--- a/rbac/role_manager.go
+++ b/rbac/role_manager.go
@@ -22,6 +22,8 @@ import (
 
 type MatchingFunc func(arg1 string, arg2 string) bool
 
+type LinkConditionFunc = func(args ...string) (bool, error)
+
 // RoleManager provides interface to define the operations for managing roles.
 type RoleManager interface {
 	// Clear clears all stored data and resets the role manager to the initial state.
@@ -59,6 +61,23 @@ type RoleManager interface {
 	AddDomainMatchingFunc(name string, fn MatchingFunc)
 }
 
+// ConditionalRoleManager provides interface to define the operations for managing roles.
+// Link with conditions is supported
+type ConditionalRoleManager interface {
+	RoleManager
+	// AddLinkConditionFunc Add condition function fn for Link userName->roleName,
+	// when fn returns true, Link is valid, otherwise invalid
+	AddLinkConditionFunc(userName, roleName string, fn LinkConditionFunc)
+	// SetLinkConditionFuncParams Sets the parameters of the condition function fn for Link userName->roleName
+	SetLinkConditionFuncParams(userName, roleName string, params ...string)
+	// AddDomainLinkConditionFunc Add condition function fn for Link userName-> {roleName, domain},
+	// when fn returns true, Link is valid, otherwise invalid
+	AddDomainLinkConditionFunc(user string, role string, domain string, fn LinkConditionFunc)
+	// SetDomainLinkConditionFuncParams Sets the parameters of the condition function fn
+	// for Link userName->{roleName, domain}
+	SetDomainLinkConditionFuncParams(user string, role string, domain string, params ...string)
+}
+
 // RoleManagerWithContext provides a context-aware interface to define the operations for managing roles.
 // Prefer this over RoleManager interface for context propagation, which is useful for things like handling
 // request timeouts.
diff --git a/util/builtin_operators.go b/util/builtin_operators.go
index 4171380..2206d07 100644
--- a/util/builtin_operators.go
+++ b/util/builtin_operators.go
@@ -22,9 +22,11 @@ import (
 	"regexp"
 	"strings"
 	"sync"
+	"time"
 
-	"github.com/Knetic/govaluate"
 	"github.com/casbin/casbin/v2/rbac"
+
+	"github.com/Knetic/govaluate"
 )
 
 var (
@@ -47,6 +49,14 @@ func validateVariadicArgs(expectedLen int, args ...interface{}) error {
 	return nil
 }
 
+// validate the variadic string parameter size
+func validateVariadicStringArgs(expectedLen int, args ...string) error {
+	if len(args) != expectedLen {
+		return fmt.Errorf("expected %d arguments, but got %d", expectedLen, len(args))
+	}
+	return nil
+}
+
 // KeyMatch determines whether key1 matches the pattern of key2 (similar to RESTful path), key2 can contain a *.
 // For example, "/foo/bar" matches "/foo/*"
 func KeyMatch(key1 string, key2 string) bool {
@@ -273,7 +283,7 @@ func KeyMatch4Func(args ...interface{}) (interface{}, error) {
 }
 
 // KeyMatch5 determines whether key1 matches the pattern of key2 (similar to RESTful path), key2 can contain a *
-// For example, 
+// For example,
 // - "/foo/bar?status=1&type=2" matches "/foo/bar"
 // - "/parent/child1" and "/parent/child1" matches "/parent/*"
 // - "/parent/child1?status=1" matches "/parent/*"
@@ -417,3 +427,56 @@ func GenerateGFunction(rm rbac.RoleManager) govaluate.ExpressionFunction {
 		return v, nil
 	}
 }
+
+// GenerateConditionalGFunction is the factory method of the g(_, _[, _]) function with conditions.
+func GenerateConditionalGFunction(crm rbac.ConditionalRoleManager) govaluate.ExpressionFunction {
+	return func(args ...interface{}) (interface{}, error) {
+		// Like all our other govaluate functions, all args are strings.
+		var hasLink bool
+
+		name1, name2 := args[0].(string), args[1].(string)
+		if crm == nil {
+			hasLink = name1 == name2
+		} else if len(args) == 2 {
+			hasLink, _ = crm.HasLink(name1, name2)
+		} else {
+			domain := args[2].(string)
+			hasLink, _ = crm.HasLink(name1, name2, domain)
+		}
+
+		return hasLink, nil
+	}
+}
+
+// builtin LinkConditionFunc
+
+// TimeMatchFunc is the wrapper for TimeMatch.
+func TimeMatchFunc(args ...string) (bool, error) {
+	if err := validateVariadicStringArgs(2, args...); err != nil {
+		return false, fmt.Errorf("%s: %s", "TimeMatch", err)
+	}
+	return TimeMatch(args[0], args[1])
+}
+
+// TimeMatch determines whether the current time is between startTime and endTime.
+// You can use "_" to indicate that the parameter is ignored
+func TimeMatch(startTime, endTime string) (bool, error) {
+	now := time.Now()
+	if startTime != "_" {
+		if start, err := time.Parse("2006-01-02 15:04:05", startTime); err != nil {
+			return false, err
+		} else if !now.After(start) {
+			return false, nil
+		}
+	}
+
+	if endTime != "_" {
+		if end, err := time.Parse("2006-01-02 15:04:05", endTime); err != nil {
+			return false, err
+		} else if !now.Before(end) {
+			return false, nil
+		}
+	}
+
+	return true, nil
+}


Test patch:
diff --git a/enforcer_test.go b/enforcer_test.go
index 8111552..0e30149 100644
--- a/enforcer_test.go
+++ b/enforcer_test.go
@@ -629,3 +629,107 @@ func TestEvalPriority(t *testing.T) {
 	testEnforce(t, e, "admin", "none", "write", false)
 	testEnforce(t, e, "user", "users", "write", false)
 }
+
+func TestLinkConditionFunc(t *testing.T) {
+	TrueFunc := func(args ...string) (bool, error) {
+		if len(args) != 0 {
+			return args[0] == "_" || args[0] == "true", nil
+		}
+		return false, nil
+	}
+
+	FalseFunc := func(args ...string) (bool, error) {
+		if len(args) != 0 {
+			return args[0] == "_" || args[0] == "false", nil
+		}
+		return false, nil
+	}
+
+	m, _ := model.NewModelFromFile("examples/rbac_with_temporal_roles_model.conf")
+	e, _ := NewEnforcer(m)
+
+	_, _ = e.AddPolicies([][]string{
+		{"alice", "data1", "read"},
+		{"alice", "data1", "write"},
+		{"data2_admin", "data2", "read"},
+		{"data2_admin", "data2", "write"},
+		{"data3_admin", "data3", "read"},
+		{"data3_admin", "data3", "write"},
+		{"data4_admin", "data4", "read"},
+		{"data4_admin", "data4", "write"},
+		{"data5_admin", "data5", "read"},
+		{"data5_admin", "data5", "write"},
+	})
+
+	_, _ = e.AddGroupingPolicies([][]string{
+		{"alice", "data2_admin", "_", "_"},
+		{"alice", "data3_admin", "_", "_"},
+		{"alice", "data4_admin", "_", "_"},
+		{"alice", "data5_admin", "_", "_"},
+	})
+
+	e.AddNamedLinkConditionFunc("g", "alice", "data2_admin", TrueFunc)
+	e.AddNamedLinkConditionFunc("g", "alice", "data3_admin", TrueFunc)
+	e.AddNamedLinkConditionFunc("g", "alice", "data4_admin", FalseFunc)
+	e.AddNamedLinkConditionFunc("g", "alice", "data5_admin", FalseFunc)
+
+	e.SetNamedLinkConditionFuncParams("g", "alice", "data2_admin", "true")
+	e.SetNamedLinkConditionFuncParams("g", "alice", "data3_admin", "not true")
+	e.SetNamedLinkConditionFuncParams("g", "alice", "data4_admin", "false")
+	e.SetNamedLinkConditionFuncParams("g", "alice", "data5_admin", "not false")
+
+	testEnforce(t, e, "alice", "data1", "read", true)
+	testEnforce(t, e, "alice", "data1", "write", true)
+	testEnforce(t, e, "alice", "data2", "read", true)
+	testEnforce(t, e, "alice", "data2", "write", true)
+	testEnforce(t, e, "alice", "data3", "read", false)
+	testEnforce(t, e, "alice", "data3", "write", false)
+	testEnforce(t, e, "alice", "data4", "read", true)
+	testEnforce(t, e, "alice", "data4", "write", true)
+	testEnforce(t, e, "alice", "data5", "read", false)
+	testEnforce(t, e, "alice", "data5", "write", false)
+
+	m, _ = model.NewModelFromFile("examples/rbac_with_domain_temporal_roles_model.conf")
+	e, _ = NewEnforcer(m)
+
+	_, _ = e.AddPolicies([][]string{
+		{"alice", "domain1", "data1", "read"},
+		{"alice", "domain1", "data1", "write"},
+		{"data2_admin", "domain2", "data2", "read"},
+		{"data2_admin", "domain2", "data2", "write"},
+		{"data3_admin", "domain3", "data3", "read"},
+		{"data3_admin", "domain3", "data3", "write"},
+		{"data4_admin", "domain4", "data4", "read"},
+		{"data4_admin", "domain4", "data4", "write"},
+		{"data5_admin", "domain5", "data5", "read"},
+		{"data5_admin", "domain5", "data5", "write"},
+	})
+
+	_, _ = e.AddGroupingPolicies([][]string{
+		{"alice", "data2_admin", "domain2", "_", "_"},
+		{"alice", "data3_admin", "domain3", "_", "_"},
+		{"alice", "data4_admin", "domain4", "_", "_"},
+		{"alice", "data5_admin", "domain5", "_", "_"},
+	})
+
+	e.AddNamedDomainLinkConditionFunc("g", "alice", "data2_admin", "domain2", TrueFunc)
+	e.AddNamedDomainLinkConditionFunc("g", "alice", "data3_admin", "domain3", TrueFunc)
+	e.AddNamedDomainLinkConditionFunc("g", "alice", "data4_admin", "domain4", FalseFunc)
+	e.AddNamedDomainLinkConditionFunc("g", "alice", "data5_admin", "domain5", FalseFunc)
+
+	e.SetNamedDomainLinkConditionFuncParams("g", "alice", "data2_admin", "domain2", "true")
+	e.SetNamedDomainLinkConditionFuncParams("g", "alice", "data3_admin", "domain3", "not true")
+	e.SetNamedDomainLinkConditionFuncParams("g", "alice", "data4_admin", "domain4", "false")
+	e.SetNamedDomainLinkConditionFuncParams("g", "alice", "data5_admin", "domain5", "not false")
+
+	testDomainEnforce(t, e, "alice", "domain1", "data1", "read", true)
+	testDomainEnforce(t, e, "alice", "domain1", "data1", "write", true)
+	testDomainEnforce(t, e, "alice", "domain2", "data2", "read", true)
+	testDomainEnforce(t, e, "alice", "domain2", "data2", "write", true)
+	testDomainEnforce(t, e, "alice", "domain3", "data3", "read", false)
+	testDomainEnforce(t, e, "alice", "domain3", "data3", "write", false)
+	testDomainEnforce(t, e, "alice", "domain4", "data4", "read", true)
+	testDomainEnforce(t, e, "alice", "domain4", "data4", "write", true)
+	testDomainEnforce(t, e, "alice", "domain5", "data5", "read", false)
+	testDomainEnforce(t, e, "alice", "domain5", "data5", "write", false)
+}
diff --git a/managerment_api_b_test.go b/managerment_api_b_test.go
index 6b6b26c..054192e 100644
--- a/managerment_api_b_test.go
+++ b/managerment_api_b_test.go
@@ -4,7 +4,7 @@
 // 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
+//	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,
diff --git a/model_test.go b/model_test.go
index 8ff2903..489d9c0 100644
--- a/model_test.go
+++ b/model_test.go
@@ -26,7 +26,9 @@ import (
 
 func testEnforce(t *testing.T, e *Enforcer, sub interface{}, obj interface{}, act string, res bool) {
 	t.Helper()
-	if myRes, _ := e.Enforce(sub, obj, act); myRes != res {
+	if myRes, err := e.Enforce(sub, obj, act); err != nil {
+		t.Errorf("Enforce Error: %s", err)
+	} else if myRes != res {
 		t.Errorf("%s, %v, %s: %t, supposed to be %t", sub, obj, act, myRes, res)
 	}
 }
@@ -40,7 +42,9 @@ func testEnforceWithoutUsers(t *testing.T, e *Enforcer, obj string, act string,
 
 func testDomainEnforce(t *testing.T, e *Enforcer, sub string, dom string, obj string, act string, res bool) {
 	t.Helper()
-	if myRes, _ := e.Enforce(sub, dom, obj, act); myRes != res {
+	if myRes, err := e.Enforce(sub, dom, obj, act); err != nil {
+		t.Errorf("Enforce Error: %s", err)
+	} else if myRes != res {
 		t.Errorf("%s, %s, %s, %s: %t, supposed to be %t", sub, dom, obj, act, myRes, res)
 	}
 }
@@ -370,6 +374,15 @@ func (rm *testCustomRoleManager) Match(str string, pattern string) bool
 func (rm *testCustomRoleManager) AddMatchingFunc(name string, fn rbac.MatchingFunc)       {}
 func (rm *testCustomRoleManager) AddDomainMatchingFunc(name string, fn rbac.MatchingFunc) {}
 
+func (rm *testCustomRoleManager) AddLinkConditionFunc(userName, roleName string, fn rbac.LinkConditionFunc) {
+}
+func (rm *testCustomRoleManager) SetLinkConditionFuncParams(userName, roleName string, params ...string) {
+}
+func (rm *testCustomRoleManager) AddDomainLinkConditionFunc(user string, role string, domain string, fn rbac.LinkConditionFunc) {
+}
+func (rm *testCustomRoleManager) SetDomainLinkConditionFuncParams(user string, role string, domain string, params ...string) {
+}
+
 func TestRBACModelWithCustomRoleManager(t *testing.T) {
 	e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
 	e.SetRoleManager(NewRoleManager())
@@ -682,3 +695,78 @@ func TestAllMatchModel(t *testing.T) {
 	testDomainEnforce(t, e, "alice", "domain2", "/book/1", "read", false)
 	testDomainEnforce(t, e, "alice", "domain2", "/book/1", "write", true)
 }
+
+func TestTemporalRolesModel(t *testing.T) {
+	e, _ := NewEnforcer("examples/rbac_with_temporal_roles_model.conf", "examples/rbac_with_temporal_roles_policy.csv")
+
+	e.AddNamedLinkConditionFunc("g", "alice", "data2_admin", util.TimeMatchFunc)
+	e.AddNamedLinkConditionFunc("g", "alice", "data3_admin", util.TimeMatchFunc)
+	e.AddNamedLinkConditionFunc("g", "alice", "data4_admin", util.TimeMatchFunc)
+	e.AddNamedLinkConditionFunc("g", "alice", "data5_admin", util.TimeMatchFunc)
+	e.AddNamedLinkConditionFunc("g", "alice", "data6_admin", util.TimeMatchFunc)
+	e.AddNamedLinkConditionFunc("g", "alice", "data7_admin", util.TimeMatchFunc)
+	e.AddNamedLinkConditionFunc("g", "alice", "data8_admin", util.TimeMatchFunc)
+
+	//testEnforce(t, e, "alice", "data1", "read", true)
+	//testEnforce(t, e, "alice", "data1", "write", true)
+	//testEnforce(t, e, "alice", "data2", "read", false)
+	//testEnforce(t, e, "alice", "data2", "write", false)
+	testEnforce(t, e, "alice", "data3", "read", true)
+	testEnforce(t, e, "alice", "data3", "write", true)
+	testEnforce(t, e, "alice", "data4", "read", true)
+	testEnforce(t, e, "alice", "data4", "write", true)
+	testEnforce(t, e, "alice", "data5", "read", true)
+	testEnforce(t, e, "alice", "data5", "write", true)
+	testEnforce(t, e, "alice", "data6", "read", false)
+	testEnforce(t, e, "alice", "data6", "write", false)
+	testEnforce(t, e, "alice", "data7", "read", true)
+	testEnforce(t, e, "alice", "data7", "write", true)
+	testEnforce(t, e, "alice", "data8", "read", false)
+	testEnforce(t, e, "alice", "data8", "write", false)
+}
+
+func TestTemporalRolesModelWithDomain(t *testing.T) {
+	e, _ := NewEnforcer("examples/rbac_with_domain_temporal_roles_model.conf", "examples/rbac_with_domain_temporal_roles_policy.csv")
+
+	e.AddNamedDomainLinkConditionFunc("g", "alice", "data2_admin", "domain2", util.TimeMatchFunc)
+	e.AddNamedDomainLinkConditionFunc("g", "alice", "data3_admin", "domain3", util.TimeMatchFunc)
+	e.AddNamedDomainLinkConditionFunc("g", "alice", "data4_admin", "domain4", util.TimeMatchFunc)
+	e.AddNamedDomainLinkConditionFunc("g", "alice", "data5_admin", "domain5", util.TimeMatchFunc)
+	e.AddNamedDomainLinkConditionFunc("g", "alice", "data6_admin", "domain6", util.TimeMatchFunc)
+	e.AddNamedDomainLinkConditionFunc("g", "alice", "data7_admin", "domain7", util.TimeMatchFunc)
+	e.AddNamedDomainLinkConditionFunc("g", "alice", "data8_admin", "domain8", util.TimeMatchFunc)
+
+	testDomainEnforce(t, e, "alice", "domain1", "data1", "read", true)
+	testDomainEnforce(t, e, "alice", "domain1", "data1", "write", true)
+	testDomainEnforce(t, e, "alice", "domain2", "data2", "read", false)
+	testDomainEnforce(t, e, "alice", "domain2", "data2", "write", false)
+	testDomainEnforce(t, e, "alice", "domain3", "data3", "read", true)
+	testDomainEnforce(t, e, "alice", "domain3", "data3", "write", true)
+	testDomainEnforce(t, e, "alice", "domain4", "data4", "read", true)
+	testDomainEnforce(t, e, "alice", "domain4", "data4", "write", true)
+	testDomainEnforce(t, e, "alice", "domain5", "data5", "read", true)
+	testDomainEnforce(t, e, "alice", "domain5", "data5", "write", true)
+	testDomainEnforce(t, e, "alice", "domain6", "data6", "read", false)
+	testDomainEnforce(t, e, "alice", "domain6", "data6", "write", false)
+	testDomainEnforce(t, e, "alice", "domain7", "data7", "read", true)
+	testDomainEnforce(t, e, "alice", "domain7", "data7", "write", true)
+	testDomainEnforce(t, e, "alice", "domain8", "data8", "read", false)
+	testDomainEnforce(t, e, "alice", "domain8", "data8", "write", false)
+
+	testDomainEnforce(t, e, "alice", "domain_not_exist", "data1", "read", false)
+	testDomainEnforce(t, e, "alice", "domain_not_exist", "data1", "write", false)
+	testDomainEnforce(t, e, "alice", "domain_not_exist", "data2", "read", false)
+	testDomainEnforce(t, e, "alice", "domain_not_exist", "data2", "write", false)
+	testDomainEnforce(t, e, "alice", "domain_not_exist", "data3", "read", false)
+	testDomainEnforce(t, e, "alice", "domain_not_exist", "data3", "write", false)
+	testDomainEnforce(t, e, "alice", "domain_not_exist", "data4", "read", false)
+	testDomainEnforce(t, e, "alice", "domain_not_exist", "data4", "write", false)
+	testDomainEnforce(t, e, "alice", "domain_not_exist", "data5", "read", false)
+	testDomainEnforce(t, e, "alice", "domain_not_exist", "data5", "write", false)
+	testDomainEnforce(t, e, "alice", "domain_not_exist", "data6", "read", false)
+	testDomainEnforce(t, e, "alice", "domain_not_exist", "data6", "write", false)
+	testDomainEnforce(t, e, "alice", "domain_not_exist", "data7", "read", false)
+	testDomainEnforce(t, e, "alice", "domain_not_exist", "data7", "write", false)
+	testDomainEnforce(t, e, "alice", "domain_not_exist", "data8", "read", false)
+	testDomainEnforce(t, e, "alice", "domain_not_exist", "data8", "write", false)
+}
diff --git a/rbac/default-role-manager/role_manager_test.go b/rbac/default-role-manager/role_manager_test.go
index 1f68b17..5af35c6 100644
--- a/rbac/default-role-manager/role_manager_test.go
+++ b/rbac/default-role-manager/role_manager_test.go
@@ -139,7 +139,6 @@ func TestRole(t *testing.T) {
 	testPrintRoles(t, rm, "g2", []string{})
 	testPrintRoles(t, rm, "g3", []string{})
 
-
 	rm = NewRoleManager(3)
 	rm.AddMatchingFunc("keyMatch", util.KeyMatch)
 
diff --git a/util/builtin_operators_test.go b/util/builtin_operators_test.go
index aa5987e..70ea4c7 100644
--- a/util/builtin_operators_test.go
+++ b/util/builtin_operators_test.go
@@ -572,3 +572,26 @@ func TestGlobMatch(t *testing.T) {
 	testGlobMatch(t, "/prefix/subprefix/foobar", "*/foo*", false)
 	testGlobMatch(t, "/prefix/subprefix/foobar", "*/foo/*", false)
 }
+
+func testTimeMatch(t *testing.T, startTime string, endTime string, res bool) {
+	t.Helper()
+	myRes, err := TimeMatch(startTime, endTime)
+	if err != nil {
+		panic(err)
+	}
+	t.Logf("%s < %s: %t", startTime, endTime, myRes)
+
+	if myRes != res {
+		t.Errorf("%s < %s: %t, supposed to be %t", startTime, endTime, !res, res)
+	}
+}
+
+func TestTestMatch(t *testing.T) {
+	testTimeMatch(t, "0000-01-01 00:00:00", "0000-01-02 00:00:00", false)
+	testTimeMatch(t, "0000-01-01 00:00:00", "9999-12-30 00:00:00", true)
+	testTimeMatch(t, "_", "_", true)
+	testTimeMatch(t, "_", "9999-12-30 00:00:00", true)
+	testTimeMatch(t, "_", "0000-01-02 00:00:00", false)
+	testTimeMatch(t, "0000-01-01 00:00:00", "_", true)
+	testTimeMatch(t, "9999-12-30 00:00:00", "_", false)
+}