Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scaling ABAC Rules #354

Closed
harsimranb opened this issue Jan 19, 2020 · 28 comments · Fixed by #427
Closed

Scaling ABAC Rules #354

harsimranb opened this issue Jan 19, 2020 · 28 comments · Fixed by #427
Assignees

Comments

@harsimranb
Copy link

harsimranb commented Jan 19, 2020

I am looking to support ABAC for a CMS-type system, where there could be thousands to potentially even more ABAC rules. Writing one long matcher isn't feasible in this case, nor is having multiple enforcers. Is there any other workaround this?

An ABAC policy for us is something like If user age is between 24 and 64, then they can "view" some "resource". Any thoughts?

Also, if this isn't supported, yet, what's the roadmap for something like this?

@hsluoyz
Copy link
Member

hsluoyz commented Feb 7, 2020

I think you want to write ABAC expressions in policy rules instead of matcher. But a blocking issue is that Golang is a static language. How to load dynamic rules into static logic?

@hsluoyz hsluoyz self-assigned this Feb 7, 2020
@jruizaranguren
Copy link
Contributor

Just my two cents here: It would be great if it was possible to use any custom function (a predicate) instead of current "effect" function, the same way we can extend the matcher with custom functions.

Then we could use arbitrary expressions on the result of the match, and casbin would be much more general. I don't know how difficult is to provide pluggable/extensible functions in casbin/go. But in python, for instance, this should not be very difficult. Just make the enforcer to accept a function (matched_rules -> boolean) that, if defined, would be used instead of the built-in expressions such as (some(where (p.eft == allow)).

This could also have the benefit that we could have an Enforcer that returns other things than a boolean.

@divy9881
Copy link
Member

In cases as posted, where ABAC rules get more complex, clingy and so as to say infeasible to add entire boolean expressions to the matchers, we must use custom function registering member function which is already implemented in the enforcer AddFunction(). As it is flexible enough to take any type and any number of arguments, you just have to plugin the wrapper MatchFunc() and the actual Match().

     Hence, it will save you from a lot of hassle incurred during writing complex ABAC matchers by writing procedures or methods which can be modularized enough to be used in different enforcers using somewhat same ABAC rules.

This can be definitely a solution to this problem, but this is open for discuss1.ion

@hsluoyz hsluoyz pinned this issue Mar 23, 2020
@hsluoyz
Copy link
Member

hsluoyz commented Mar 23, 2020

We should have a way to write ABAC policy like If user age is between 24 and 64, then they can "view" some "resource" into rules instead of matcher. Can anyone work on this?

@dovics
Copy link
Member

dovics commented Mar 23, 2020

Do you mean we need to define policy like this p = sub, obj, act, match

p,  user, /book/1, read,  sub.age > 24 && sub.age < 64 

We just need to generate a matcher for each policy. I am doing this now, but it will take some time.

@jruizaranguren
Copy link
Contributor

That approach can work for some scenarios, but I doubt it is general enough. Wouldn't be easier to just allow to inject any function that takes the matched rules and return the effect?

@dovics
Copy link
Member

dovics commented Mar 23, 2020

Yes, In my way, we can also write function like this

p,  user, /book/1, read,  ageMatch(sub.age)

addMatch() can be freely defined, Just need to satisfy the govaluate.ExpressionFunction type

@divy9881
Copy link
Member

I have been working on this from last 5 days,
I would suggest a solution which is a lot more general and goes with the reusability principle.

Let's take the original example :
If user age is between 24 and 64, then they can "view" some "resource"

func AgeMatch(r interface{}) bool {
    return (r.sub.age >= 14 && r.sub.age <= 64)
}
 
func AgeMatchFunc(args... interface{}) (interface{}, error) {
    return (bool)(AgeMatch(args[0])), nil
}
 
e.AddFunction("age_matcher", AgeMatchFunc)

Now, the CONF file looks like this,

[request_definition]
r = sub, obj, act
 
[policy_definition]
p = sub, obj, act
 
[policy_effect]
e = some(where (p.eft == allow))
 
[matchers]
m = age_matcher(r)

@divy9881
Copy link
Member

A lot more complexity would be scaled in this way, a lot more functions can be employed in this way to carry the complexity of ABAC rules with it in this way.

@divy9881
Copy link
Member

If we add the ABAC rules in the policy file, it will bloat the entire file with it.

@divy9881
Copy link
Member

What do you think, @jruizaranguren and @hsluoyz?

@dovics
Copy link
Member

dovics commented Mar 24, 2020

Hi ~ @divypatel9881
I think there is something wrong with your solution, Please take a look at this

there could be thousands to potentially even more ABAC rules.

So it is difficult to generalize with a single matcher.

In addition, Your one matcher works on all resources, and the focus is on some resources

@divy9881
Copy link
Member

Well, @dovics it was just an example, you can concatenate more restrictions like this,

 func Age1Match(r interface{}) bool {
    return (r.sub.age >= 14 && r.sub.age <= 64 && r.obj==resource && r.act==view)
}

 func Age2Match(r interface{}) bool {
    return (r.sub.age >= 65 && r.obj==resource && r.act==view)
}

 func Age3Match(r interface{}) bool {
    return (r.sub.age <= 14 && r.obj==resource && r.act==view)
}
 
func AgeMatchFunc(args... interface{}) (interface{}, error) {
    return (bool)(Age1Match(args[0])) || (bool)(Age2Match(args[0])) || (bool)(Age3Match(args[0])), nil
}
 
e.AddFunction("age_matcher", AgeMatchFunc)

So it is difficult to generalize with a single matcher.
That's the case of just 3 ABAC rules, many more can be added.

In addition, Your one matcher works on all resources, and the focus is on some resources
Please refer to the above code.

I thought it was understood, but I hope, I have made more clearer for you by this.

@dovics
Copy link
Member

dovics commented Mar 24, 2020

Thank you for your explanation, you are correct, this does solve the problem,

But these functions need to be defined by the user. Don't you think that the user needs to do too much work?

p,  user, resource1, read,  sub.age >= 24 && sub.age =< 64 
p,  user, resource2, read,  sub.age < 24
p,  user, resource3, read,  sub.age > 64

You don't think this solution is simpler

I can't find the difference with single matcher

matcher = (r.sub.age >= 14 && r.sub.age <= 64 && r.obj == "resource1" && r.act== p.act )  /
         || (r.sub.age >= 65 && r.obj == ”resource2“ && r.act == p.act)  /
         || (r.sub.age <= 14 && r.obj == “resource3” && r.act == p.act)

Maybe your solution has more features, can you show me?

@divy9881
Copy link
Member

Yeah, why not, for example, you can,
It's up to the user how he should enclose the complexity, as another solution can be

 func Age1Match(r interface{}, l int, h int, resource string, act view) bool {
    return (r.sub.age >= l && r.sub.age <= h && r.obj==resource && r.act==act)
}
 
func AgeMatchFunc(args... interface{}) (interface{}, error) {
    return (bool)(AgeMatch(args[0], args[1], args[2], args[3], args[4])) || (bool)(AgeMatch(args[0], args[5], args[6], args[7], args[8])) , nil
}
 
e.AddFunction("age_matcher", AgeMatchFunc)

CONF file :

[request_definition]
r = sub, obj, act
 
[policy_definition]
p = sub, obj, act
 
[policy_effect]
e = some(where (p.eft == allow))
 
[matchers]
m = age_matcher(r, 0, 13, "resource1", "view", 14, 64, "resource2" , "write")

It's literally up to user how he wants to make the ABAC policies, it gives a huge range of flexibility.

@divy9881
Copy link
Member

You can certainly reduce the code in the functions, by using control structures like for loops.

@jruizaranguren
Copy link
Contributor

@divypatel9881, your approach is very interesting, but I see two problems:

  1. logic encoded in data. Policies and rules. This change current casbin approach to encode logic only in the policy model. I prefer to have policies and roles only storing data, which allows to implement more efficient storage and retrieval of policies. And have proper functions in code, which can be extended as desired.
  2. if we satisfy the nice layour of casbin model, it can not solve many ABAC problems.
    • matcher: rules -> rules.
    • effect: rules -> bool

Thus, any function that we use within matcher, can only serve to filter policy rules. But we may build arbitrary predicate models over the rules that depend on context (ABAC). We may check cardinality, use implication, or even machine learning models.

This could be more easily solved, simply by allowing the injection of custom effect functions:

[request_definition]
r = sub, obj, act
 
[policy_definition]
p = sub, obj, act
 
[policy_effect]
e = custom_effect
 
[matchers]
m = any casbin matcher, using custom functions if required, as they are defined today 

e.add_effect(custom_effect)

And we can define any logic in a Turing complete language at our disposal. This is a great balance between what casbin offers you without customization, and what can you achieve when needed.

@divy9881
Copy link
Member

Oh, then how do you suggest to encode data in policy model? Do you have some ideas regarding this?

@divy9881
Copy link
Member

@jruizaranguren can you elaborate on your solution?

@GopherJ
Copy link
Member

GopherJ commented Apr 17, 2020

@jruizaranguren I don't think custom effect is good and will help solving the problems in this issue, but I do agree many people want :

p,  user, /book/1, read,  sub.age > 24 && sub.age < 64 

including me...To implement this we need something similar like a json rule engine https://www.npmjs.com/package/json-rules-engine

and the users will be able to give:

r.sub

{
    name: "alice",
    age: 17,
    gender: "female"
}

then our json rule parsed from policy

[
   {
      "field": "age",
      "operator": "greater",
      "value": 18
   },
   {
      "field": "gender",
      "operator": "equal",
      "value": "male"
   }
]

I would really to have this in casbin and I'll try to implement in casbin-rs.

@GopherJ
Copy link
Member

GopherJ commented Apr 17, 2020

@jruizaranguren Your senario isn't that easy, because it requires keeping many things in memory. Let's say a subset of policies which are related to request. However to achieve this we need to keep a subset of policies in memory to be returned.

That's not good... If you want only EffetKind::Allow maybe it's ok but in some senario I can see huge amount of policies should be keeped in memory => so we are keeping a huge subset policies in memory, to me it's not good idea

@jruizaranguren
Copy link
Contributor

@GopherJ Managing or not policies in memory is a matter of plumbing and specific scenarios and infrastructure. In the cases I see, with up to only thousands of policies, I find that having all of them in memory is the best approach and the most efficient.

Returnin tens or hundreds of policies that will feed the effect function is not a big deal. In any case, I can't think a scenario where your matcher returns large subset of policies.

@GopherJ
Copy link
Member

GopherJ commented Apr 18, 2020

@jruizaranguren Ok after some thoughts I think it's ok.

I've also made some new tests to try to solve this issue in casbin-rs: casbin/casbin-rs#121

Now in model we can define some thing like this:

[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = Any(_) && r.obj == p.obj && r.act == p.act 
# Any(_) is the place holder which matches Any(....) in policy
# inside `Any()` we can put abac rules and use `r.sub, r.obj, r.act, &&, ||` etc

and in policy we can write like this:

p, Any(r.sub.age > 18), /data1, read

This policy means that we don't care who is the person sending request, but his/her age should be greater than 18, if yes then he/she will be able to read /data1

I made it works in casbin-rs, not sure if we should do like this but I think it may helps solving some problems.

@GopherJ
Copy link
Member

GopherJ commented Apr 18, 2020

Will If(r.sub.age > 18) be better than Any(r.sub.age > 18)?

@GopherJ
Copy link
Member

GopherJ commented Apr 20, 2020

Can anyone sync casbin/casbin-rs#121 to casbin golang?

@hsluoyz
Copy link
Member

hsluoyz commented May 20, 2020

@jlecount
Copy link

I just found this page and wanted to provide a link update for anybody who stumbles upon this in the future. The correct link mentioned above is now https://casbin.org/docs/abac#scaling-the-model-for-complex-and-large-number-of-abac-rules

@hsluoyz
Copy link
Member

hsluoyz commented Feb 22, 2023

@jlecount link updated, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants