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 00000000..93e8a3d6 --- /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 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 00000000..3202e369 --- /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, _ diff --git a/examples/rbac_with_temporal_roles_model.conf b/examples/rbac_with_temporal_roles_model.conf new file mode 100644 index 00000000..86dad188 --- /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 diff --git a/examples/rbac_with_temporal_roles_policy.csv b/examples/rbac_with_temporal_roles_policy.csv new file mode 100644 index 00000000..42c1aaf7 --- /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, _ diff --git a/src/main/java/org/casbin/jcasbin/main/CoreEnforcer.java b/src/main/java/org/casbin/jcasbin/main/CoreEnforcer.java index a1cbdf89..4567e994 100644 --- a/src/main/java/org/casbin/jcasbin/main/CoreEnforcer.java +++ b/src/main/java/org/casbin/jcasbin/main/CoreEnforcer.java @@ -33,14 +33,14 @@ import org.casbin.jcasbin.model.Model; import org.casbin.jcasbin.persist.*; import org.casbin.jcasbin.persist.file_adapter.FileAdapter; -import org.casbin.jcasbin.rbac.DomainManager; -import org.casbin.jcasbin.rbac.RoleManager; +import org.casbin.jcasbin.rbac.*; import org.casbin.jcasbin.util.BuiltInFunctions; import org.casbin.jcasbin.util.EnforceContext; import org.casbin.jcasbin.util.Util; import java.util.*; import java.util.function.BiPredicate; +import java.util.function.Function; /** * CoreEnforcer defines the core functionality of an enforcer. @@ -55,6 +55,7 @@ public class CoreEnforcer { Watcher watcher; Dispatcher dispatcher; Map rmMap; + Map condRmMap; private boolean enabled; boolean autoSave; @@ -67,6 +68,7 @@ public class CoreEnforcer { void initialize() { rmMap = new HashMap<>(); + condRmMap = new HashMap<>(); eft = new DefaultEffector(); watcher = null; @@ -287,6 +289,7 @@ public void loadPolicy() { } if (autoBuildRoleLinks) { buildRoleLinks(); + buildConditionalRoleLinks(); } } @@ -317,6 +320,7 @@ public void loadFilteredPolicy(Object filter) { } if (autoBuildRoleLinks) { buildRoleLinks(); + buildConditionalRoleLinks(); } } @@ -372,8 +376,31 @@ private void initRmMap() { for (String ptype : model.model.get("g").keySet()) { if (rmMap.containsKey(ptype)) { rmMap.get(ptype).clear(); - } else { - addOrUpdateDomainManagerMatching(ptype); + continue; + } + Assertion assertion = model.model.get("g").get(ptype); + int token_length = (assertion.tokens != null ? assertion.tokens.length : 0); + int paramsToken_length = (assertion.paramsTokens != null ? assertion.paramsTokens.length : 0); + if (token_length <= 2 && paramsToken_length == 0) { + assertion.rm = new DomainManager(10); + rmMap.put(ptype, assertion.rm); + } + if (token_length <= 2 && paramsToken_length != 0) { + assertion.condRM =new ConditionalRoleManager(10); + condRmMap.put(ptype, assertion.condRM); + } + if (token_length > 2) { + if (paramsToken_length == 0) { + assertion.rm = new DomainManager(10); + rmMap.put(ptype, assertion.rm); + } else { + assertion.condRM = new ConditionalRoleManager(10); + condRmMap.put(ptype, assertion.condRM); + } + String matchFun = "keyMatch(r_dom, p_dom)"; + if (model.model.get("m").get("m").value.contains(matchFun)) { + addNamedDomainMatchingFunc(ptype, "g", BuiltInFunctions::keyMatch); + } } } } @@ -409,7 +436,9 @@ private void clearRmMap() { } for (String ptype : model.model.get("g").keySet()) { - rmMap.get(ptype).clear(); + if (rmMap.get(ptype) != null) { + rmMap.get(ptype).clear(); + } } } @@ -466,10 +495,21 @@ public void enableAcceptJsonRequest(boolean acceptJsonRequest) { * role inheritance relations. */ public void buildRoleLinks() { - for (RoleManager rm : rmMap.values()) { - rm.clear(); + if (!rmMap.isEmpty()) { + for (RoleManager rm : rmMap.values()) { + rm.clear(); + } + model.buildRoleLinks(rmMap); + } + } + + public void buildConditionalRoleLinks(){ + if (!condRmMap.isEmpty()) { + for (ConditionalRoleManager condRm : condRmMap.values()) { + condRm.clear(); + } + model.buildConditionalRoleLinks(condRmMap); } - model.buildRoleLinks(rmMap); } /** @@ -499,8 +539,17 @@ private EnforceResult enforce(String matcher, Object... rvals) { Assertion ast = entry.getValue(); RoleManager rm = ast.rm; - AviatorFunction aviatorFunction = BuiltInFunctions.GenerateGFunctionClass.generateGFunction(key, rm); - gFunctions.put(key, aviatorFunction); + if (rm != null){ + AviatorFunction aviatorFunction = BuiltInFunctions.GenerateGFunctionClass.generateGFunction(key, rm); + gFunctions.put(key, aviatorFunction); + } + + ConditionalRoleManager condRM = ast.condRM; + if (condRM != null){ + AviatorFunction aviatorFunction = BuiltInFunctions.GenerateConditionalGFunctionClass.generateConditionalGFunction(key, condRM); + gFunctions.put(key, aviatorFunction); + } + } } for (AviatorFunction f : gFunctions.values()) { @@ -736,10 +785,10 @@ public boolean addNamedMatchingFunc(String ptype, String name, BiPredicateroleName, + * when fn returns true, Link is valid, otherwise invalid + */ + public boolean addNamedLinkConditionFunc(String ptype, String user, String role, Function fn){ + if (condRmMap.containsKey(ptype)){ + ConditionalRoleManager condRm = condRmMap.get(ptype); + condRm.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 + */ + public boolean addNamedDomainLinkConditionFunc(String ptype, String user, String role, String domain, Function fn) { + if (condRmMap.containsKey(ptype)){ + ConditionalRoleManager condRm = condRmMap.get(ptype); + condRm.addDomainLinkConditionFunc(user, role, domain, fn); + return true; + } + return false; + } + + /** + * setNamedLinkConditionFuncParams Sets the parameters of the condition function fn for Link userName->roleName + */ + public boolean setNamedLinkConditionFuncParams(String ptype, String user, String role, String... params){ + if (condRmMap.containsKey(ptype)){ + ConditionalRoleManager condRm = condRmMap.get(ptype); + condRm.setLinkConditionFuncParams(user, role, params); + return true; + } + return false; + } + + /** + * setNamedDomainLinkConditionFuncParams Sets the parameters of the condition function fn + * for Link userName->{roleName, domain} + */ + public boolean setNamedDomainLinkConditionFuncParams(String ptype, String user, String role, String domain, String... params){ + if (condRmMap.containsKey(ptype)){ + ConditionalRoleManager condRm = condRmMap.get(ptype); + condRm.setDomainLinkConditionFuncParams(user, role, domain, params); + return true; + } + return false; + } + private void getRTokens(Map parameters, String rType, Object... rvals) { String[] requestTokens = model.model.get("r").get(rType).tokens; if(requestTokens.length != rvals.length) { diff --git a/src/main/java/org/casbin/jcasbin/model/Assertion.java b/src/main/java/org/casbin/jcasbin/model/Assertion.java index a57593f2..15c5243d 100644 --- a/src/main/java/org/casbin/jcasbin/model/Assertion.java +++ b/src/main/java/org/casbin/jcasbin/model/Assertion.java @@ -15,6 +15,7 @@ package org.casbin.jcasbin.model; import org.casbin.jcasbin.log.Logger; +import org.casbin.jcasbin.rbac.ConditionalRoleManager; import org.casbin.jcasbin.rbac.RoleManager; import org.casbin.jcasbin.util.Util; @@ -31,9 +32,11 @@ public class Assertion { public String key; public String value; public String[] tokens; + public String[] paramsTokens; public List> policy; public Map policyIndex; public RoleManager rm; + public ConditionalRoleManager condRM; public int priorityIndex; private Logger logger; @@ -63,7 +66,10 @@ protected void buildRoleLinks(RoleManager rm) { if (rule.size() < count) { throw new IllegalArgumentException("grouping policy elements do not meet role definition"); } - rm.addLink(rule.get(0), rule.get(1), rule.subList(2, rule.size()).toArray(new String[0])); + if (rule.size() > count){ + rule = rule.subList(0, count); + } + this.rm.addLink(rule.get(0), rule.get(1), rule.subList(2, rule.size()).toArray(new String[0])); } Util.logPrint("Role links for: " + key); @@ -101,6 +107,81 @@ public void buildIncrementalRoleLinks(RoleManager rm, Model.PolicyOperations op, } } + public void buildIncrementalConditionalRoleLinks(ConditionalRoleManager condRM, Model.PolicyOperations op, List> rules){ + this.condRM = condRM; + int count = 0; + for (int i = 0; i < value.length(); i++) { + if (value.charAt(i) == '_') { + count++; + } + } + if (count < 2) { + throw new IllegalArgumentException("the number of \"_\" in role definition should be at least 2"); + } + + for (List rule : rules) { + if (count < 2) { + throw new IllegalArgumentException("the number of \"_\" in role definition should be at least 2"); + } + if (rule.size() < count) { + throw new IllegalArgumentException("grouping policy elements do not meet role definition"); + } + if (rule.size() > count) { + rule = rule.subList(0, count); + } + List domainRule = rule.subList(2, tokens.length); + switch (op) { + case POLICY_ADD: + addConditionalRoleLink(rule, domainRule); + break; + case POLICY_REMOVE: + condRM.deleteLink(rule.get(0), rule.get(1), rule.subList(2, rule.size()).toArray(new String[0])); + break; + default: + throw new IllegalArgumentException("invalid operation:" + op.toString()); + } + } + } + + public void buildConditionalRoleLinks(ConditionalRoleManager condRM){ + this.condRM = condRM; + int count = 0; + for (int i = 0; i < value.length(); i++) { + if (value.charAt(i) == '_') { + count++; + } + } + for (List rule : policy) { + if (count < 2) { + throw new IllegalArgumentException("the number of \"_\" in role definition should be at least 2"); + } + if (rule.size() < count) { + throw new IllegalArgumentException("grouping policy elements do not meet role definition"); + } + if (rule.size() > count){ + rule = rule.subList(0, count); + } + List domainRule = rule.subList(2, tokens.length); + addConditionalRoleLink(rule, domainRule); + } + + Util.logPrint("Role links for: " + key); + condRM.printRoles(); + } + + // addConditionalRoleLinks adds Link to rbac.ConditionalRoleManager and sets the parameters for LinkConditionFunc + public void addConditionalRoleLink(List rule, List domainRule){ + int domainRule_num = (domainRule!=null? domainRule.size() : 0); + if (domainRule_num == 0){ + condRM.addLink(rule.get(0), rule.get(1)); + condRM.setLinkConditionFuncParams(rule.get(0), rule.get(1), rule.subList(tokens.length, rule.size()).toArray(new String[0])); + }else { + String domain = domainRule.get(0); + condRM.addLink(rule.get(0), rule.get(1)); + condRM.setDomainLinkConditionFuncParams(rule.get(0), rule.get(1), domain, rule.subList(tokens.length, rule.size()).toArray(new String[0])); + } + } + public void initPriorityIndex() { priorityIndex = -1; } diff --git a/src/main/java/org/casbin/jcasbin/model/Model.java b/src/main/java/org/casbin/jcasbin/model/Model.java index f44349eb..1f7f3a6a 100644 --- a/src/main/java/org/casbin/jcasbin/model/Model.java +++ b/src/main/java/org/casbin/jcasbin/model/Model.java @@ -18,13 +18,9 @@ import org.casbin.jcasbin.log.*; import org.casbin.jcasbin.util.Util; +import java.util.*; +import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; import static org.casbin.jcasbin.util.Util.splitCommaDelimited; @@ -64,6 +60,28 @@ private boolean loadAssertion(Model model, Config cfg, String sec, String key) { return model.addDef(sec, key, value); } + private static final Pattern paramsPattern = Pattern.compile("\\((.*?)\\)"); + + /** + * getParamsToken Get ParamsToken from Assertion.Value + */ + private static List getParamsToken(String value) { + Matcher matcher = paramsPattern.matcher(value); + if (!matcher.find()) { + return null; + } + String paramsString = matcher.group(1); + if (paramsString == null || paramsString.isEmpty()) { + return null; + } + String[] paramsArray = paramsString.split(","); + List paramsList = new ArrayList<>(); + for (String param : paramsArray) { + paramsList.add(param.trim()); + } + return paramsList; + } + /** * addDef adds an assertion to the model. * @@ -91,6 +109,14 @@ public boolean addDef(String sec, String key, String value) { ast.priorityIndex = i; } } + } else if ("g".equals(sec)) { + if (getParamsToken(ast.value) != null){ + ast.paramsTokens = getParamsToken(ast.value).toArray(new String[0]); + } + int paramsTokens_length = ast.paramsTokens==null?0:ast.paramsTokens.length; + String[] tokens_array = value.split(","); + List tokens_list = Arrays.asList(tokens_array).subList(0, tokens_array.length - paramsTokens_length); + ast.tokens = tokens_list.toArray(new String[0]); } else { ast.value = Util.removeComments(Util.escapeAssertion(ast.value)); } diff --git a/src/main/java/org/casbin/jcasbin/model/Policy.java b/src/main/java/org/casbin/jcasbin/model/Policy.java index bcc27741..316831a0 100644 --- a/src/main/java/org/casbin/jcasbin/model/Policy.java +++ b/src/main/java/org/casbin/jcasbin/model/Policy.java @@ -14,6 +14,7 @@ package org.casbin.jcasbin.model; +import org.casbin.jcasbin.rbac.ConditionalRoleManager; import org.casbin.jcasbin.rbac.RoleManager; import org.casbin.jcasbin.util.Util; @@ -39,7 +40,9 @@ public void buildRoleLinks(Map rmMap) { String ptype = entry.getKey(); Assertion ast = entry.getValue(); RoleManager rm = rmMap.get(ptype); - ast.buildRoleLinks(rm); + if (rm != null) { + ast.buildRoleLinks(rm); + } } } } @@ -373,7 +376,7 @@ public List getValuesForFieldInPolicy(String sec, String ptype, int fiel } public void buildIncrementalRoleLinks(Map rmMap, Model.PolicyOperations op, String sec, String ptype, List> rules) { - if ("g".equals(sec)) { + if ("g".equals(sec) && rmMap.containsKey(ptype)) { model.get(sec).get(ptype).buildIncrementalRoleLinks(rmMap.get(ptype), op, rules); } } @@ -386,4 +389,30 @@ public boolean hasPolicies(String sec, String ptype, List> rules) { } return false; } + + /** + * buildIncrementalConditionalRoleLinks provides incremental build the role inheritance relations. + */ + public void buildIncrementalConditionalRoleLinks(Map condRmMap, Model.PolicyOperations op, String sec, String ptype, List> rules){ + if ("g".equals(sec) && condRmMap.containsKey(ptype)) { + model.get(sec).get(ptype).buildIncrementalConditionalRoleLinks(condRmMap.get(ptype), op, rules); + } + } + + /** + * buildConditionalRoleLinks initializes the roles in RBAC. + */ + public void buildConditionalRoleLinks(Map condRmMap){ + printPolicy(); + if (model.containsKey("g")) { + for (Map.Entry entry : model.get("g").entrySet()) { + String ptype = entry.getKey(); + Assertion ast = entry.getValue(); + if (condRmMap.get(ptype) != null){ + ConditionalRoleManager condRm = condRmMap.get(ptype); + ast.buildConditionalRoleLinks(condRm); + } + } + } + } } diff --git a/src/main/java/org/casbin/jcasbin/rbac/ConditionalRoleManager.java b/src/main/java/org/casbin/jcasbin/rbac/ConditionalRoleManager.java new file mode 100644 index 00000000..b5954ed8 --- /dev/null +++ b/src/main/java/org/casbin/jcasbin/rbac/ConditionalRoleManager.java @@ -0,0 +1,199 @@ +// 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 org.casbin.jcasbin.rbac; + +import java.util.*; +import java.util.function.BiPredicate; +import java.util.function.Consumer; +import java.util.function.Function; + +public class ConditionalRoleManager extends DefaultRoleManager{ + public ConditionalRoleManager(int maxHierarchyLevel) { + super(maxHierarchyLevel); + } + + public ConditionalRoleManager(int maxHierarchyLevel, BiPredicate matchingFunc, BiPredicate domainMatchingFunc) { + super(maxHierarchyLevel, matchingFunc, domainMatchingFunc); + } + + public synchronized boolean hasLink(String name1, String name2, String... domains) { + if (name1.equals(name2) || (this.matchingFunc != null && this.matchingFunc.test(name1, name2))) { + return true; + } + + boolean userCreated = !this.allRoles.containsKey(name1); + boolean roleCreated = !this.allRoles.containsKey(name2); + Role user = getRole(name1); + Role role = getRole(name2); + + Map roles = new HashMap<>(); + roles.put(user.getName(), user); + + try { + return hasLinkHelper(role.getName(), roles, this.maxHierarchyLevel, domains); + } finally { + if (userCreated) { + removeRole(user.getName()); + } + if (roleCreated) { + removeRole(role.getName()); + } + } + } + + public boolean hasLinkHelper(String targetName, Map roles, int level, String... domains) { + if (level < 0 || roles.isEmpty()) { + return false; + } + Map nextRoles = new HashMap<>(); + for (Role role : roles.values()) { + if (targetName.equals(role.getName()) || (matchingFunc != null && match(role.getName(), targetName))) { + return true; + } + role.rangeRoles(new Consumer() { + @Override + public void accept(Role nextRole) { + getNextRoles(role, nextRole, domains, nextRoles); + } + }); + } + return hasLinkHelper(targetName, nextRoles, level - 1, domains); + } + + public boolean getNextRoles(Role currentRole, Role nextRole, String[] domains, Map nextRoles) { + boolean passLinkConditionFunc = true; + Exception err = null; + + // If LinkConditionFunc exists, it needs to pass the verification to get nextRole + if (domains.length == 0) { + Function linkConditionFunc = getLinkConditionFunc(currentRole.getName(), nextRole.getName()); + if (linkConditionFunc != null) { + List params = getLinkConditionFuncParams(currentRole.getName(), nextRole.getName(), domains); + try { + passLinkConditionFunc = linkConditionFunc.apply(params.toArray(new String[0])); + } catch (Exception e) { + err = e; + } + } + } else { + Function linkConditionFunc = getDomainLinkConditionFunc(currentRole.getName(), nextRole.getName(), domains[0]); + if (linkConditionFunc != null) { + List params = getLinkConditionFuncParams(currentRole.getName(), nextRole.getName(), domains); + try { + passLinkConditionFunc = linkConditionFunc.apply(params.toArray(new String[0])); + } catch (Exception e) { + err = e; + } + } + } + + if (err != null) { + System.err.println("hasLinkHelper LinkCondition Error"); + err.printStackTrace(); + return false; + } + + if (passLinkConditionFunc) { + nextRoles.put(nextRole.getName(), nextRole); + } + + return true; + } + + /** + * getLinkConditionFunc get LinkConditionFunc based on userName, roleName + */ + public Function getLinkConditionFunc(String userName, String roleName){ + return getDomainLinkConditionFunc(userName, roleName, ""); + } + + /** + * getDomainLinkConditionFunc get LinkConditionFunc based on userName, roleName, domain + */ + public Function getDomainLinkConditionFunc(String userName, String roleName, String domain){ + Role user = getRole(userName); + Role role = getRole(roleName); + + if (user == null) { + return null; + } + if (role == null) { + return null; + } + + return user.getLinkConditionFunc(role, domain); + } + + /** + * getLinkConditionFuncParams gets parameters of LinkConditionFunc based on userName, roleName, domain + */ + public List getLinkConditionFuncParams(String userName, String roleName, String[] domain){ + boolean userCreated = !this.allRoles.containsKey(userName); + boolean roleCreated = !this.allRoles.containsKey(roleName); + Role user = getRole(userName); + Role role = getRole(roleName); + + if (userCreated) + removeRole(user.getName()); + if (roleCreated) + removeRole(role.getName()); + + String domainName = ""; + if (domain.length != 0) { + domainName = domain[0]; + } + + String[] params = user.getLinkConditionFuncParams(role, domainName); + if (params != null){ + return Arrays.asList(params); + } else { + return null; + } + } + + /** + * addLinkConditionFunc is based on userName, roleName, add LinkConditionFunc + */ + public void addLinkConditionFunc(String userName, String roleName, Function fn){ + addDomainLinkConditionFunc(userName, roleName, "", fn); + } + + /** + * addDomainLinkConditionFunc is based on userName, roleName, domain, add LinkConditionFunc + */ + public void addDomainLinkConditionFunc(String userName, String roleName, String domain, Function fn){ + Role user = getRole(userName); + Role role = getRole(roleName); + + user.addLinkConditionFunc(role, domain, fn); + } + + /** + * SetLinkConditionFuncParams sets parameters of LinkConditionFunc based on userName, roleName, domain + */ + public void setLinkConditionFuncParams(String userName, String roleName, String... params) { + setDomainLinkConditionFuncParams(userName, roleName, "", params); + } + + /** + * SetDomainLinkConditionFuncParams sets parameters of LinkConditionFunc based on userName, roleName, domain + */ + public void setDomainLinkConditionFuncParams(String userName, String roleName, String domain, String... params) { + Role user = getRole(userName); + Role role = getRole(roleName); + + user.setLinkConditionFuncParams(role, domain, params); + } +} diff --git a/src/main/java/org/casbin/jcasbin/rbac/DefaultRoleManager.java b/src/main/java/org/casbin/jcasbin/rbac/DefaultRoleManager.java index 0589db90..38262053 100644 --- a/src/main/java/org/casbin/jcasbin/rbac/DefaultRoleManager.java +++ b/src/main/java/org/casbin/jcasbin/rbac/DefaultRoleManager.java @@ -23,9 +23,9 @@ public class DefaultRoleManager implements RoleManager { private static final String DEFAULT_DOMAIN = "casbin::default"; Map allRoles; - private final int maxHierarchyLevel; + final int maxHierarchyLevel; - private BiPredicate matchingFunc; + BiPredicate matchingFunc; private SyncedLRUCache matchingFuncCache; /** @@ -87,7 +87,7 @@ private void rebuild() { }); } - private boolean match(String str, String pattern) { + boolean match(String str, String pattern) { String cacheKey = String.join("$$", str, pattern); Boolean matched = this.matchingFuncCache.get(cacheKey); if (matched == null) { @@ -101,7 +101,7 @@ private boolean match(String str, String pattern) { return matched; } - private Role getRole(String name) { + Role getRole(String name) { Role role = this.allRoles.get(name); if (role == null) { role = new Role(name); @@ -125,7 +125,7 @@ private Role getRole(String name) { return role; } - private void removeRole(String name) { + void removeRole(String name) { final Role role = this.allRoles.get(name); if (role != null) { this.allRoles.remove(name); diff --git a/src/main/java/org/casbin/jcasbin/rbac/LinkConditionFuncKey.java b/src/main/java/org/casbin/jcasbin/rbac/LinkConditionFuncKey.java new file mode 100644 index 00000000..1e02d896 --- /dev/null +++ b/src/main/java/org/casbin/jcasbin/rbac/LinkConditionFuncKey.java @@ -0,0 +1,57 @@ +// 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 org.casbin.jcasbin.rbac; + +import java.util.Objects; + +public class LinkConditionFuncKey { + private String roleName; + private String domainName; + + public LinkConditionFuncKey(String roleName, String domainName) { + this.roleName = roleName; + this.domainName = domainName; + } + + public String getRoleName() { + return roleName; + } + + public void setRoleName(String roleName) { + this.roleName = roleName; + } + + public String getDomainName() { + return domainName; + } + + public void setDomainName(String domainName) { + this.domainName = domainName; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + LinkConditionFuncKey that = (LinkConditionFuncKey) o; + return Objects.equals(roleName, that.roleName) && + Objects.equals(domainName, that.domainName); + } + + @Override + public int hashCode() { + return Objects.hash(roleName, domainName); + } +} diff --git a/src/main/java/org/casbin/jcasbin/rbac/Role.java b/src/main/java/org/casbin/jcasbin/rbac/Role.java index de8e6440..cfff67a2 100644 --- a/src/main/java/org/casbin/jcasbin/rbac/Role.java +++ b/src/main/java/org/casbin/jcasbin/rbac/Role.java @@ -15,6 +15,8 @@ package org.casbin.jcasbin.rbac; import java.util.*; +import java.util.function.Consumer; +import java.util.function.Function; /** * Role represents the data structure for a role in RBAC. @@ -25,6 +27,8 @@ class Role { private final Map users; private final Map matched; private final Map matchedBy; + private final Map> linkConditionFuncMap; + private final Map linkConditionFuncParamsMap; protected Role(String name) { this.name = name; @@ -32,6 +36,8 @@ protected Role(String name) { this.users = new HashMap<>(); this.matched = new HashMap<>(); this.matchedBy = new HashMap<>(); + this.linkConditionFuncMap = new HashMap<>(); + this.linkConditionFuncParamsMap = new HashMap<>(); } String getName() { @@ -76,6 +82,25 @@ void removeMatches() { } } + public void rangeRoles(Consumer fn) { + roles.forEach((key, value) -> { + Role role = (Role) value; + fn.accept(role); + role.matched.forEach((matchedKey, matchedValue) -> { + Role matchedRole = (Role) matchedValue; + fn.accept(matchedRole); + }); + }); + + matchedBy.forEach((key, value) -> { + Role role = (Role) value; + role.roles.forEach((roleKey, roleValue) -> { + Role subRole = (Role) roleValue; + fn.accept(subRole); + }); + }); + } + @Override public String toString() { List roles = getRoles(); @@ -128,4 +153,24 @@ Map getAllUsers() { this.matchedBy.values().forEach(role -> allUsers.putAll(role.users)); return allUsers; } + + void addLinkConditionFunc(Role role, String domain, Function fn){ + linkConditionFuncMap.put(new LinkConditionFuncKey(role.name, domain), fn); + } + + Function getLinkConditionFunc(Role role, String domain){ + Function function = linkConditionFuncMap.get(new LinkConditionFuncKey(role.name, domain)); + if (function == null) { + return null; + } + return linkConditionFuncMap.get(new LinkConditionFuncKey(role.name, domain)); + } + + void setLinkConditionFuncParams(Role role, String domain, String... params){ + linkConditionFuncParamsMap.put(new LinkConditionFuncKey(role.name, domain), params); + } + + String[] getLinkConditionFuncParams(Role role, String domain){ + return linkConditionFuncParamsMap.get(new LinkConditionFuncKey(role.name, domain)); + } } diff --git a/src/main/java/org/casbin/jcasbin/util/BuiltInFunctions.java b/src/main/java/org/casbin/jcasbin/util/BuiltInFunctions.java index 72b87c69..9dbf2471 100644 --- a/src/main/java/org/casbin/jcasbin/util/BuiltInFunctions.java +++ b/src/main/java/org/casbin/jcasbin/util/BuiltInFunctions.java @@ -22,8 +22,11 @@ import inet.ipaddr.AddressStringException; import inet.ipaddr.IPAddress; import inet.ipaddr.IPAddressString; +import org.casbin.jcasbin.rbac.ConditionalRoleManager; import org.casbin.jcasbin.rbac.RoleManager; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; @@ -35,6 +38,16 @@ public class BuiltInFunctions { private static final Pattern KEY_MATCH2_PATTERN = Pattern.compile(":[^/]+"); private static final Pattern KEY_MATCH3_PATTERN = Pattern.compile("\\{[^/]+\\}"); + /** + * validate the variadic string parameter size + */ + public static void validateVariadicStringArgs(int expectedLen, String... args) throws IllegalArgumentException { + int length = args!=null?args.length:0; + if (length != expectedLen) { + throw new IllegalArgumentException(String.format("Expected %d arguments, but got %d", expectedLen, args.length)); + } + } + /** * keyMatch determines whether key1 matches the pattern of key2 (similar to RESTful path), key2 * can contain a *. @@ -397,6 +410,66 @@ public String getName() { } } + public static class GenerateConditionalGFunctionClass { + // key:name such as g,g2 value:user-role mapping + private static Map> memorizedMap = new ConcurrentHashMap<>(); + + /** + * GenerateConditionalGFunction is the factory method of the g(_, _[, _]) function with conditions. + * + * @param name the name of the g(_, _) function, can be "g", "g2", .. + * @param condRm the conditional role manager used by the function. + * @return the function. + */ + public static AviatorFunction generateConditionalGFunction(String name, ConditionalRoleManager condRm) { + memorizedMap.put(name, new ConcurrentHashMap<>()); + + return new AbstractVariadicFunction() { + @Override + public AviatorObject variadicCall(Map env, AviatorObject... args) { + Map memorized = memorizedMap.get(name); + int len = args.length; + if (len < 2) { + return AviatorBoolean.valueOf(false); + } + String name1 = FunctionUtils.getStringValue(args[0], env); + String name2 = FunctionUtils.getStringValue(args[1], env); + + String key = ""; + for (AviatorObject arg : args) { + String name = FunctionUtils.getStringValue(arg, env); + key += ";" + name; + } + + AviatorBoolean value = memorized.get(key); + if (value != null) { + return value; + } + + boolean hasLink; + if (condRm == null) { + hasLink = name1.equals(name2); + } else if (len == 2) { + hasLink = condRm.hasLink(name1, name2); + } else if (len == 3) { + String domain = FunctionUtils.getStringValue(args[2], env); + hasLink = condRm.hasLink(name1, name2, domain); + } else { + hasLink = false; + } + value = AviatorBoolean.valueOf(hasLink); + memorized.put(key, value); + return value; + } + + @Override + public String getName() { + return name; + } + }; + } + } + /** * eval calculates the stringified boolean expression and return its result. * @@ -419,4 +492,56 @@ public static boolean eval(String eval, Map env, AviatorEvaluato } return res; } + + // builtin LinkConditionFunc + + /** + * timeMatchFunc is the wrapper for TimeMatch. + */ + public static boolean timeMatchFunc(String... args) { + try { + validateVariadicStringArgs(2, args); + return timeMatch(args[0], args[1]); + } catch (IllegalArgumentException e) { + System.err.println("TimeMatch: " + e.getMessage()); + return false; + } + } + + /** + * TimeMatch determines whether the current time is between startTime and endTime. + * You can use "_" to indicate that the parameter is ignored + */ + public static boolean timeMatch(String startTime, String endTime) { + LocalDateTime now = LocalDateTime.now(); + + if (!startTime.equals("_")) { + LocalDateTime start; + // special process for "0000" year,LocalDateTime range is 1-999999999 + if (startTime.startsWith("0000")){ + start = LocalDateTime.MIN; + }else { + start = LocalDateTime.parse(startTime, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + } + if (!now.isAfter(start)) { + return false; + } + } + + if (!endTime.equals("_")) { + + LocalDateTime end; + // special process for "0000" year,LocalDateTime range is 1-999999999 + if (endTime.startsWith("0000")){ + end = LocalDateTime.MIN; + }else { + end = LocalDateTime.parse(endTime, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + } + if (!now.isBefore(end)) { + return false; + } + } + + return true; + } } diff --git a/src/test/java/org/casbin/jcasbin/main/ModelUnitTest.java b/src/test/java/org/casbin/jcasbin/main/ModelUnitTest.java index 9ccc50f6..a3ae26d6 100644 --- a/src/test/java/org/casbin/jcasbin/main/ModelUnitTest.java +++ b/src/test/java/org/casbin/jcasbin/main/ModelUnitTest.java @@ -881,4 +881,81 @@ public void testRbacWithResourceRolesAndDomain() { testDomainEnforce(e, "bob", "domain2", "data2", "read", true); testDomainEnforce(e, "bob", "domain2", "data2", "write", true); } + + @Test + public void testTemporalRolesModel(){ + Enforcer e = new Enforcer("examples/rbac_with_temporal_roles_model.conf", "examples/rbac_with_temporal_roles_policy.csv"); + + e.addNamedLinkConditionFunc("g", "alice", "data2_admin", BuiltInFunctions::timeMatchFunc); + e.addNamedLinkConditionFunc("g", "alice", "data3_admin", BuiltInFunctions::timeMatchFunc); + e.addNamedLinkConditionFunc("g", "alice", "data4_admin", BuiltInFunctions::timeMatchFunc); + e.addNamedLinkConditionFunc("g", "alice", "data5_admin", BuiltInFunctions::timeMatchFunc); + e.addNamedLinkConditionFunc("g", "alice", "data6_admin", BuiltInFunctions::timeMatchFunc); + e.addNamedLinkConditionFunc("g", "alice", "data7_admin", BuiltInFunctions::timeMatchFunc); + e.addNamedLinkConditionFunc("g", "alice", "data8_admin", BuiltInFunctions::timeMatchFunc); + + testEnforce(e, "alice", "data1", "read", true); + testEnforce(e, "alice", "data1", "write", true); + testEnforce(e, "alice", "data2", "read", false); + testEnforce(e, "alice", "data2", "write", false); + testEnforce(e, "alice", "data3", "read", true); + testEnforce(e, "alice", "data3", "write", true); + testEnforce(e, "alice", "data4", "read", true); + testEnforce(e, "alice", "data4", "write", true); + testEnforce(e, "alice", "data5", "read", true); + testEnforce(e, "alice", "data5", "write", true); + testEnforce(e, "alice", "data6", "read", false); + testEnforce(e, "alice", "data6", "write", false); + testEnforce(e, "alice", "data7", "read", true); + testEnforce(e, "alice", "data7", "write", true); + testEnforce(e, "alice", "data8", "read", false); + testEnforce(e, "alice", "data8", "write", false); + } + + @Test + public void testTemporalRolesModelWithDomain(){ + Enforcer e = new Enforcer("examples/rbac_with_domain_temporal_roles_model.conf", "examples/rbac_with_domain_temporal_roles_policy.csv"); + + e.addNamedDomainLinkConditionFunc("g", "alice", "data2_admin", "domain2", BuiltInFunctions::timeMatchFunc); + e.addNamedDomainLinkConditionFunc("g", "alice", "data3_admin", "domain3", BuiltInFunctions::timeMatchFunc); + e.addNamedDomainLinkConditionFunc("g", "alice", "data4_admin", "domain4", BuiltInFunctions::timeMatchFunc); + e.addNamedDomainLinkConditionFunc("g", "alice", "data5_admin", "domain5", BuiltInFunctions::timeMatchFunc); + e.addNamedDomainLinkConditionFunc("g", "alice", "data6_admin", "domain6", BuiltInFunctions::timeMatchFunc); + e.addNamedDomainLinkConditionFunc("g", "alice", "data7_admin", "domain7", BuiltInFunctions::timeMatchFunc); + e.addNamedDomainLinkConditionFunc("g", "alice", "data8_admin", "domain8", BuiltInFunctions::timeMatchFunc); + + testDomainEnforce(e, "alice", "domain1", "data1", "read", true); + testDomainEnforce(e, "alice", "domain1", "data1", "write", true); + testDomainEnforce(e, "alice", "domain2", "data2", "read", false); + testDomainEnforce(e, "alice", "domain2", "data2", "write", false); + testDomainEnforce(e, "alice", "domain3", "data3", "read", true); + testDomainEnforce(e, "alice", "domain3", "data3", "write", true); + testDomainEnforce(e, "alice", "domain4", "data4", "read", true); + testDomainEnforce(e, "alice", "domain4", "data4", "write", true); + testDomainEnforce(e, "alice", "domain5", "data5", "read", true); + testDomainEnforce(e, "alice", "domain5", "data5", "write", true); + testDomainEnforce(e, "alice", "domain6", "data6", "read", false); + testDomainEnforce(e, "alice", "domain6", "data6", "write", false); + testDomainEnforce(e, "alice", "domain7", "data7", "read", true); + testDomainEnforce(e, "alice", "domain7", "data7", "write", true); + testDomainEnforce(e, "alice", "domain8", "data8", "read", false); + testDomainEnforce(e, "alice", "domain8", "data8", "write", false); + + testDomainEnforce(e, "alice", "domain_not_exist", "data1", "read", false); + testDomainEnforce(e, "alice", "domain_not_exist", "data1", "write", false); + testDomainEnforce(e, "alice", "domain_not_exist", "data2", "read", false); + testDomainEnforce(e, "alice", "domain_not_exist", "data2", "write", false); + testDomainEnforce(e, "alice", "domain_not_exist", "data3", "read", false); + testDomainEnforce(e, "alice", "domain_not_exist", "data3", "write", false); + testDomainEnforce(e, "alice", "domain_not_exist", "data4", "read", false); + testDomainEnforce(e, "alice", "domain_not_exist", "data4", "write", false); + testDomainEnforce(e, "alice", "domain_not_exist", "data5", "read", false); + testDomainEnforce(e, "alice", "domain_not_exist", "data5", "write", false); + testDomainEnforce(e, "alice", "domain_not_exist", "data6", "read", false); + testDomainEnforce(e, "alice", "domain_not_exist", "data6", "write", false); + testDomainEnforce(e, "alice", "domain_not_exist", "data7", "read", false); + testDomainEnforce(e, "alice", "domain_not_exist", "data7", "write", false); + testDomainEnforce(e, "alice", "domain_not_exist", "data8", "read", false); + testDomainEnforce(e, "alice", "domain_not_exist", "data8", "write", false); + } }