casbin / casbin-rs

An authorization library that supports access control models like ACL, RBAC, ABAC in Rust.
https://casbin.org
Apache License 2.0
845 stars 71 forks source link

Implement ABAC model #78

Closed xcaptain closed 4 years ago

xcaptain commented 4 years ago

Current we are matching directly over object, means subject can act on object, with ABAC we can do matching like: subject can act on object is this object has specific attributes.

ABAC doc

This problem may be a little hard because we are using Vec<&str> as the parameter for enforce, which requires every element in the vector has the same type that is &str. I have 2 ideas about this issue.

  1. Change from Vec<&str> to tuple, so we don't need to worry about the type.
  2. Still passing obj as a string, parsing this string into a struct in rhai then do the matching.
GopherJ commented 4 years ago

@xcaptain Could you give us an example of usage? The first and the second. Also are you interested in implementation of this?

xcaptain commented 4 years ago

@GopherJ A simple example, Trump can access this box of masks only if it has a label says "for USA".

or

Trump can access this box of masks only if it has a label says "for USA" and it has been paid. :smile:

We can simply say

RBAC: a subject can act on an object if this subject has some attributes(role) ABAC: a subject can act on an object if this object has some attributes

I would like to try this issue but I still didn't find any good ideas to solve it.

GopherJ commented 4 years ago

Thanks I mean some example code. I have no idea on how does it should look like.

Tuple is not a good solution.

xcaptain commented 4 years ago

Have no clue yet, need to investigate more.

GopherJ commented 4 years ago

hello @xcaptain could you help on finding a solution for this? yesterday I spent some time on it. I think the main problem is how to register all the types to rhai::Engine.

I'll take care of #92 #94 and if I have time #79, it'll be good if we can solve this so that we have a good feature for 1.0.0

schungx commented 4 years ago

I have some ideas for this: https://github.com/jonathandturner/rhai/issues/131

GopherJ commented 4 years ago

Thanks @schungx I have read your ideas and it seems good. I'll give a try recently.

GopherJ commented 4 years ago

@xcaptain Maybe you would like to have a look at @schungx 's idea

GopherJ commented 4 years ago

I think in the first argument of enforce, we should accept also serde_json::Value, so that we convert it to object-map in rhai:

let a = #{              // object map literal with 3 properties
    a: 1,
    bar: "hello",
    "baz!$@": 123.456,  // like JS, you can use any string as property names...
    "": false,          // even the empty string!

    a: 42               // <- syntax error: duplicated property name
};
schungx commented 4 years ago

In PR# https://github.com/jonathandturner/rhai/pull/128 I've added a nice helper method called parse_json which takes a JSON string and parses it into an object map that you can pass onwards to Rhai.

GopherJ commented 4 years ago

@schungx we need to bring serde_json::Value into rhai::Scope, I think the follow is enough:

use rhai::{Engine, EvalAltResult, Scope};
use serde_json::json;

#[cfg(not(feature = "no_object"))]
fn main() -> Result<(), EvalAltResult> {
    let mut engine = Engine::new();
    let mut scope = Scope::new();

    let my_json: String = serde_json::to_string(&json!({
        "age": 19
    }))
    .unwrap();
    // Todo: we should check if it's object
    engine.eval_with_scope::<()>(&mut scope, &format!("let my_json = #{}", my_json))?;
    let result = engine.eval_with_scope::<bool>(&mut scope, "my_json.age > 18")?;

    println!("result: {}", result);

    Ok(())
}

is there a better way to bring an user-defined serde_json::Value into rhai::Scope ?

schungx commented 4 years ago

Concat the JSON into a script is the easiest way, but the problem being you have to create a different script text during each call. I'd suggest making it constant:

let script = format!("const my_json = #{}; my_json.age > 18", my_json);
let result = engine.eval::<bool>(&script)?;

If you want to keep the object in Scope, you have to create a Map (basically a HashMap<String, Dynamic>) for it. This way you don't have to create a new script during each call, and you can reuse the Map object for other calls. The helper method I just put in the PR will be easiest:

let mut scope = Scope::new();
let map = engine.parse_json(&my_json, false);    // true if you need null's
scope.push_constant("my_json", map);

let result = engine.eval_with_scope::<bool>(&mut scope, "my_json.age > 18")?;

If you want to use the existing crate, you can simulate parse_json by:

my_json.insert('#', 0);
let map = eval_expression::<Map>(&my_json)?;
xcaptain commented 4 years ago

@GopherJ I don't think it's possible to add serde_json into rhai, the parse_json method is enough.

@schungx Thank you for the awesome work, I will test your pull request now.

GopherJ commented 4 years ago

@xcaptain we can bring serde_json::Value into rhai, I prefer it because I don't want users to write json manually.

Or maybe rhai can add the support:

allow any data which has implemented serde::Serialize to be added into scope

GopherJ commented 4 years ago

@schungx Is it possible to add a feature in rhai which is called: serde?

And in this feature, it allows users to bring Serializable value into scope. Sure we should also consider rhai::Array not only object map.

Because now it seems we need to serialize the given data (I don't want users to write json manually), and then convert it to rhai json by adding # prefix, and then we need to call parse_json for having a map and push it to Scope.

GopherJ commented 4 years ago

@xcaptain on our side I think we should define a new trait ToRhai, we let enforce function accept ToRhai.

Then we do the implementation for &'static str or String so that users can pass &'static str or String, and we also implement ToRhai for all types which have implemented serde::ser::Serialize.

fn enforce<T: ToRhai>(rvals: &[T])
trait ToRhai {
  fn to_rhai(self) -> String;
}
impl ToRhai for String
impl<T> ToRhai for T where T: Serialize

maybe we'll get conflicts because String has implemented also Serialize but I want that they can have different results. Because for normal string we shouldn't add # prefix, but for other serializable types we should add # prefix

#![feature(specialization)]

will help

GopherJ commented 4 years ago

I tried the following code but it seems doesn't compile:

#![feature(specialization)]
use serde::Serialize;

trait ToRhai {
    fn to_rhai(&self) -> String;
}

impl ToRhai for String {
    fn to_rhai(&self) -> String {
        self.to_owned()
    }
}

impl<T> ToRhai for T
where
    T: serde::ser::Serialize,
{
    default fn to_rhai(&self) -> String {
        format!("#{}", serde_json::to_string(self).unwrap())
    }
}

#[derive(Serialize)]
struct MyJson {
    age: usize,
}

fn enforce<S: ToRhai>(rvals: &[S]) {
    let rvals: Vec<String> = rvals.iter().map(|x| x.to_rhai()).collect();
    println!("{:?}", rvals);
}

fn main() {
    let j = MyJson { age: 18 };

    enforce(&["alice".to_owned(), j, "read".to_owned()]);
}
xcaptain commented 4 years ago

@schungx I tried parse_json and it worked well :smile: .

https://github.com/xcaptain/casbin-rs-1/commit/77c41d1b3b6452268577e81d39d7f74d8c608ea3

@GopherJ You idea looks well, I will give it a try.

GopherJ commented 4 years ago

it works after switching to dynamic dispatcher:

#![feature(specialization)]
use serde::Serialize;

trait ToRhai {
    fn to_rhai(&self) -> String;
}

impl ToRhai for String {
    fn to_rhai(&self) -> String {
        self.to_owned()
    }
}

impl<T> ToRhai for T
where
    T: serde::ser::Serialize,
{
    default fn to_rhai(&self) -> String {
        format!("#{}", serde_json::to_string(self).unwrap())
    }
}

#[derive(Serialize)]
struct MyJson {
    age: usize,
}

fn enforce(rvals: &[Box<dyn ToRhai>]) {
    let rvals: Vec<String> = rvals.iter().map(|x| x.to_rhai()).collect();
    println!("{:?}", rvals);
}

fn main() {
    let j = MyJson { age: 18 };

    enforce(&[
        Box::new("alice".to_owned()),
        Box::new(j),
        Box::new("read".to_owned()),
    ]);
}
schungx commented 4 years ago

@schungx I tried parse_json and it worked well.

If you're not going to keep the object around (possibly to evaluate many expressions with that single object), it might be faster simply to splice the JSON into the beginning of the expression text using the const my_json = #... template. This way, you simply copy and merge the text once, but you avoid the copying of the object, plus all the values inside (which may be strings, etc.).

schungx commented 4 years ago

@GopherJ my suggestion: keep enforce taking strings. It seems to be a very heavy price to pay to add an additional level of indirection, plus all the boxing and allocations, just to support objects. Especially when you're gonna transparently convert them into JSON text strings anyway!

Therefore, it would be much better to have enforce take a slice of an enum, which contains either a wrapped &str or a wrapped Box<dyn Serialize> - then you only pay the price of indirection if your users use objects.

Finally, I'm quite sure it is OK if enforce takes only strings, and your user needs to manually serialize to JSON (perhaps with some form of marker to indicate that it is JSON instead of simple text). Then you don't have to depend on serde_json at all. It is easy for whoever wants to use objects to serialize to JSON.

GopherJ commented 4 years ago

@schungx Yes we decided to go this way to support just Strings, otherwise we need to bring many overhead like nightly, serde, Box....

GopherJ commented 4 years ago

closed as #102