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

Implement ABAC model #78

Closed
xcaptain opened this issue Apr 5, 2020 · 23 comments
Closed

Implement ABAC model #78

xcaptain opened this issue Apr 5, 2020 · 23 comments

Comments

@xcaptain
Copy link
Contributor

xcaptain commented Apr 5, 2020

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.
@xcaptain xcaptain mentioned this issue Apr 5, 2020
24 tasks
@GopherJ
Copy link
Member

GopherJ commented Apr 6, 2020

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

@xcaptain
Copy link
Contributor Author

xcaptain commented Apr 6, 2020

@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. 😄

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
Copy link
Member

GopherJ commented Apr 6, 2020

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

Tuple is not a good solution.

@xcaptain
Copy link
Contributor Author

xcaptain commented Apr 6, 2020

Have no clue yet, need to investigate more.

@GopherJ
Copy link
Member

GopherJ commented Apr 7, 2020

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
Copy link
Contributor

schungx commented Apr 10, 2020

I have some ideas for this: rhaiscript/rhai#131

@GopherJ
Copy link
Member

GopherJ commented Apr 10, 2020

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

@GopherJ
Copy link
Member

GopherJ commented Apr 10, 2020

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

@GopherJ
Copy link
Member

GopherJ commented Apr 10, 2020

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
Copy link
Contributor

schungx commented Apr 10, 2020

In PR# rhaiscript/rhai#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
Copy link
Member

GopherJ commented Apr 10, 2020

@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
Copy link
Contributor

schungx commented Apr 10, 2020

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
Copy link
Contributor Author

@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
Copy link
Member

GopherJ commented Apr 10, 2020

@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
Copy link
Member

GopherJ commented Apr 10, 2020

@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
Copy link
Member

GopherJ commented Apr 10, 2020

@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
Copy link
Member

GopherJ commented Apr 10, 2020

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
Copy link
Contributor Author

xcaptain commented Apr 10, 2020

@schungx I tried parse_json and it worked well 😄 .

xcaptain@77c41d1

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

@GopherJ
Copy link
Member

GopherJ commented Apr 10, 2020

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
Copy link
Contributor

schungx commented Apr 10, 2020

@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
Copy link
Contributor

schungx commented Apr 10, 2020

@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
Copy link
Member

GopherJ commented Apr 10, 2020

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

@GopherJ
Copy link
Member

GopherJ commented Apr 12, 2020

closed as #102

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

No branches or pull requests

3 participants