AE
Ash Elixirโ€ข2y ago
barnabasj

We created a small extensions for rbac policies

GitHub
GitHub - traveltechdeluxe/ash-rbac
Contribute to traveltechdeluxe/ash-rbac development by creating an account on GitHub.
104 Replies
natterstefan
natterstefanโ€ข2y ago
Awesome! ๐Ÿš€
Blibs
Blibsโ€ข2y ago
Does this generate, under the hood, a policies code block, meaning it still uses the policies mechanism from Ash? Will it always check for the roles field in a resource for its role or is there any way to configure that (for example, for my users, I normally have a roles and organization_roles fields that mean different things).
barnabasj
barnabasjOPโ€ข2y ago
It is just generating normal policy and field_policy blocks. I was thinking of making the role getter function configurable, but as of now it expects an actor with a roles attribute We also combine this with normal policy blocks to add filter policies on resources that need them, as the extension is not handling that case at the moment. Tests are the best docs atm As relationships work a bit differently, e.g. not supported by field_policies. But you can specify that a user can only use an action if it is accessed from a relationship
Blibs
Blibsโ€ข2y ago
Super cool man, this seems to have the potential to make my projects policies way simpler, amazing work ๐Ÿ‘
barnabasj
barnabasjOPโ€ข2y ago
Thanks, field policies really brought everything together.
Blibs
Blibsโ€ข2y ago
Would you be interested in a PR adding support for custom checkers? I was able to change the code to allow it like this:
rbac do
role :admin do
fields [:*]
actions [:create, :read]
end

role :user, checker: HasRole2 do
fields [:id, :child, :children, :number]
actions [:read]
end
end
rbac do
role :admin do
fields [:*]
actions [:create, :read]
end

role :user, checker: HasRole2 do
fields [:id, :child, :children, :number]
actions [:read]
end
end
I can't figure out how to make the bypass part take 2 arguments instead of one to allow something like this:
bypass :admin, checker: HasRole2
bypass :admin, checker: HasRole2
barnabasj
barnabasjOPโ€ข2y ago
Sure, if you open the PR we can check what is needed for the bypass and stuff also not sure if changing the whole module is the right approach, as the semantics of the check shouldn't change. Would it be enough for you use case if you were able to pass a function that takes the actor and returns the roles?
Blibs
Blibsโ€ข2y ago
Sure, I thought about passing a module just to try to make less verbose, but I guess something like this:
role :user, field: &get_role_field/1 do
role :user, field: &get_role_field/1 do
Is fine too. What would be a good key name for it? Or should it not even be a keyword and just the function directly like this:
role :user, &get_role_field/1 do
role :user, &get_role_field/1 do
barnabasj
barnabasjOPโ€ข2y ago
could be just a function if it is an argument i think. Could you pleaseopen an issue on the repo. I think that is the better place for this discussion we probably also need a way to configure this globaly if the actor looks different in general, having to specify it on every resource would become tedious very quickly
Blibs
Blibsโ€ข2y ago
So, another unrelated question about the lib (not sure if I should ask that here or in the repo since that is not a PR). Does AshRbac works alongside a normal policies block? For example, let's say I have this policies:
policies do
policy action(:read) do
forbid_if actor_attribute_equals(:confirmed?, false)
authorize_if always()
end

policy action(:read) do
authorize_if expr(:user in ^actor(:roles))
end
end
policies do
policy action(:read) do
forbid_if actor_attribute_equals(:confirmed?, false)
authorize_if always()
end

policy action(:read) do
authorize_if expr(:user in ^actor(:roles))
end
end
I was under the impression that this would be equivalent to this:
rbac do
role :user do
actions [:read]
end
end

policies do
policy action(:read) do
forbid_if actor_attribute_equals(:confirmed?, false)
authorize_if always()
end
end
rbac do
role :user do
actions [:read]
end
end

policies do
policy action(:read) do
forbid_if actor_attribute_equals(:confirmed?, false)
authorize_if always()
end
end
But from what I can tell from my testings the rbac rule is simply ignored. Is that not supported right now?
barnabasj
barnabasjOPโ€ข2y ago
As of now it is creating policies like this
# this

rbac do
role :user do
actions [:read]
end
end

# ends up being this

policies do
policy action(:read), has_role(:user) do
authorize_if always()
end
end
# this

rbac do
role :user do
actions [:read]
end
end

# ends up being this

policies do
policy action(:read), has_role(:user) do
authorize_if always()
end
end
As of now I'm not completely sure what the best way is to make this composable. I think allowing custom conditions shouldn't be to hard and might solve this problem.
rbac do
role :user do
actions [:read]
end
end

policies do
policy action(:read) do
forbid_if actor_attribute_equals(:confirmed?, false)
authorize_if always()
end
end
rbac do
role :user do
actions [:read]
end
end

policies do
policy action(:read) do
forbid_if actor_attribute_equals(:confirmed?, false)
authorize_if always()
end
end
In your example your read check would forbid it if the user was not confirmed but otherwise would authrize it. the block created from rbac would not lead to a definitive answer, therefore the authorization from your policy would allow the query to go through The default for the rbac extension is to deny acccess and explicilty set conditions where an action is allowed. so to make it play well together you need to either add policies that deny certain stuff or use the same condition as the rbac role creates.
Blibs
Blibsโ€ข2y ago
By custom conditions you mean add the conditions directly to rbac code block? Would that mean that we would be able to add custom conditions inside the role or actions blocks? I guess that would alleviate the problem, but seems like something that would create a lot of code duplication if I have to set the same rules for all roles (like my confirmed example since that applies to all roles). @Zach Daniel I'm not familiar with the policies code, but is there any technical issue that makes it hard to be composable (meaning that I can split my policies into multiple policies code blocks inside a resource)?
barnabasj
barnabasjOPโ€ข2y ago
you can already add conditions to the actions like this:
rbac do
role :user do
actions [
{:read, accessing_from(OtherResource, :relationship)}
]
end
end
rbac do
role :user do
actions [
{:read, accessing_from(OtherResource, :relationship)}
]
end
end
It would be possible to also allow a third element in the tuple with custom checks. e.g.
rbac do
role :user do
actions [
{:read, accessing_from(OtherResource, :relationship), [forbid_if: [actor_attribute_equals(:confirmed?, false)]}
]
end
end
rbac do
role :user do
actions [
{:read, accessing_from(OtherResource, :relationship), [forbid_if: [actor_attribute_equals(:confirmed?, false)]}
]
end
end
which would result in
policies do
policy has_role(:user), action(:read), accesing_from(OtherResource, :relationship) do
forbid_if actor_attribute_equals(:confirmed?, false)
authorize_if always()
end
end
policies do
policy has_role(:user), action(:read), accesing_from(OtherResource, :relationship) do
forbid_if actor_attribute_equals(:confirmed?, false)
authorize_if always()
end
end
Maybe composable was not the right word. Policies follow certain rules, if you follow them you can put your policies into different blocks. If I'm not mistaken: all policy block with a matching conditions are executed. a block executes all checks until an answer is found (:authorized | :forbidden) or all checks are done (:unknown) afterwards the result of policy blocks is checked if any returned :forbidden the request is stopped if all are :unknown the request is also stopped if one of them :authorized and the rest is :unknown the request is processed the extension relies on the default being forbidden and explicitly authorizes if a condition matches. if you add another policies like your read policy, that policy essentially bypasses the extensions policies because their policy conditions are not matching. Now that I think about, you could possible already pass your actor_attribute_equals(:confirmed?, false} as condition.
rbac do
roles :user do
actions [
{:read, actor_attribute_equals(:confirmed?, :true)}
]
end
end
rbac do
roles :user do
actions [
{:read, actor_attribute_equals(:confirmed?, :true)}
]
end
end
That way the :user would only be allowed to execute the read actor is confirmed
policies do
policy has_role(:user), action(:read), actor_attribute_equals(:confirmed?, :true) do
authorize_if always()
end
end
policies do
policy has_role(:user), action(:read), actor_attribute_equals(:confirmed?, :true) do
authorize_if always()
end
end
I have to ask chatgpt to summarize this thread and create docs from it afterwards ๐Ÿคฃ In this case I think I probably only have to allow an array of conditions. I think this is more in line with allow list philosophy. That way you can define explicit conditions for when a user should be allowed to do something.
ZachDaniel
ZachDanielโ€ข2y ago
Policies compose just fine, but itโ€™s important to know the main things: policies apply in order (important to account for bypasses), and all policies that apply to a request must pass. (Except for bypasses) So the essentially question is where does rbac put the policies it adds to your resource. As long as it puts them at the end, then there should be no issue ๐Ÿ™‚ And all dsl entities from fragments also get put at the end, so if you have a fragment for policies you should put rbac and your policies in the same fragment.
barnabasj
barnabasjOPโ€ข2y ago
It prepends bypasses and appends policies. But mixing allow-list policies together with deny-list style policies leads to unwanted results
ZachDaniel
ZachDanielโ€ข2y ago
Hmโ€ฆit shouldnโ€™t Every policy has to pass always no matter what So why does that lead to unwanted results?
barnabasj
barnabasjOPโ€ข2y ago
Ok, in the example above the policy with the forbid_if also has an authorize_if always in there. That one overrides all the other allow-list style policies. But it's only the authorize if in there that is the problem
ZachDaniel
ZachDanielโ€ข2y ago
that should not override any other policies no policy will ever override another policy the only thing capable of doing that is a bypass higher up in the policy list
barnabasj
barnabasjOPโ€ข2y ago
policies do
policy action(:read) do
forbid_if some_check
authorize_if always
end

policy has_role(:user), action(:read) do
authorize_if always()
end
end
policies do
policy action(:read) do
forbid_if some_check
authorize_if always
end

policy has_role(:user), action(:read) do
authorize_if always()
end
end
in this example the second one is never looked at if the user has not the role :user and the first policy doesn't care about the role and only forbids in case of some_check therefore the request is authorized no matter the role of the user
ZachDaniel
ZachDanielโ€ข2y ago
if that's actually happening then it is a massive bug ๐Ÿ™‚ All policies that apply have to result in an authorized status
barnabasj
barnabasjOPโ€ข2y ago
no the logic is sound the second policy just isn't applied because of the condition
ZachDaniel
ZachDanielโ€ข2y ago
you said "therefore the request is authorized no matter the role of the user" that is not what those policies say
barnabasj
barnabasjOPโ€ข2y ago
yes because the first policy returns :authorized and the second one does not apply
ZachDaniel
ZachDanielโ€ข2y ago
if has_role(:user) is true, the second policy must pass oh well if its just authorize_if always() then yes, you're right sorry a policy authorize_if always() has no way of failing so is effectively a noop if it applies or not The reason its probably working when you only use the rbac extension is because we require that at least one policy applies
barnabasj
barnabasjOPโ€ข2y ago
if no policy applies it is denied right
ZachDaniel
ZachDanielโ€ข2y ago
Gotcha, so I think what your rbac extension needs to do is
policy action(:read) do
authorize_if has_role(:user)
end
policy action(:read) do
authorize_if has_role(:user)
end
barnabasj
barnabasjOPโ€ข2y ago
I can not recall at the moment what it was, but I did this at first and had similar problems too.
ZachDaniel
ZachDanielโ€ข2y ago
gotcha. Well in the short term the library should probably express that it can't actually be mixed w/ your own policies on any action w/ rbac applied currently
barnabasj
barnabasjOPโ€ข2y ago
you can, but can only add other allow-list style policies but yeah, that should be in the docs
ZachDaniel
ZachDanielโ€ข2y ago
it doesn't matter what kind of policies they are if any other policy applies, then they've gotten past the "at least one policy must apply" rule a policy that contains only authorize_if always() can never forbid a request So it actually only works if you have no other policies that apply
barnabasj
barnabasjOPโ€ข2y ago
yes, but if your policy is explicitly allowing something than that is what you want and it is ok if it applies
ZachDaniel
ZachDanielโ€ข2y ago
sorry, but I don't think so. Let me show you what I mean
policy action(:create) do
authorize_if actor_attribute_equals(:can_create, true)
forbid_if always()
end

rbac_policy_about_create
policy action(:create) do
authorize_if actor_attribute_equals(:can_create, true)
forbid_if always()
end

rbac_policy_about_create
if that first policy applies, it will override the rbac policy which means you can really easily accidentally poke holes in your authorization
barnabasj
barnabasjOPโ€ข2y ago
ok, i got what you mean. What i was thinking about was something with a condition more specific than the one from the extension
ZachDaniel
ZachDanielโ€ข2y ago
Yeah, if you are like narrowing it down then it works, but that's only kind of by chance
policy has_role(:foo) do
authorize_if actor_attribute_equals(:foo_is_active, true)
forbid_if always()
end
policy has_role(:foo) do
authorize_if actor_attribute_equals(:foo_is_active, true)
forbid_if always()
end
i.e if you only want users w/ a given role to be able to work against foo_is_active, you're actually making any rbac policies about that role ignored based on the rbac stuff, I think what you actually want is for the role to be the condition, and the allowed actions to be the checks i.e
policy has_role(:foo) do
authorize_if action(:read)
end
policy has_role(:foo) do
authorize_if action(:read)
end
barnabasj
barnabasjOPโ€ข2y ago
how would I add an filter to this?
ZachDaniel
ZachDanielโ€ข2y ago
what kind of filter?
barnabasj
barnabasjOPโ€ข2y ago
expr(id == ^actor(:id)) for example
ZachDaniel
ZachDanielโ€ข2y ago
is that something the rbac does?
barnabasj
barnabasjOPโ€ข2y ago
not at the moment I just think filters were where I had my first problem and i started moving it over to conditions
ZachDaniel
ZachDanielโ€ข2y ago
if you do
policy has_role(:foo) do
authorize_if action(:read)
end

policy [has_role(:foo), action(:read)] do
authorize_if expr(id == ^actor)
end
policy has_role(:foo) do
authorize_if action(:read)
end

policy [has_role(:foo), action(:read)] do
authorize_if expr(id == ^actor)
end
Those two policies would result in :foo role being able to call the read action, and automatically applying the expr(id == ^actor) filter. note: If building them in a transformer you may need to reference the built in Expr check module
barnabasj
barnabasjOPโ€ข2y ago
but if you leave of the has_role(:foo) from the second policy you would ave the same problem as before, right?
ZachDaniel
ZachDanielโ€ข2y ago
๐Ÿค” sorry, what exactly should the behavior be? should all users have the filter applied? or only has_role(:foo) users?
barnabasj
barnabasjOPโ€ข2y ago
only has_role this also leads to narrowing
ZachDaniel
ZachDanielโ€ข2y ago
I don't see how The first policy says "any user w/ has_role(:foo) can only call the read action"
barnabasj
barnabasjOPโ€ข2y ago
policy has_role(:foo) do
authorize_if action(:read)
end

# narrower condition
policy [has_role(:foo), action(:read)] do
authorize_if expr(id == ^actor)
end
policy has_role(:foo) do
authorize_if action(:read)
end

# narrower condition
policy [has_role(:foo), action(:read)] do
authorize_if expr(id == ^actor)
end
ZachDaniel
ZachDanielโ€ข2y ago
The second policy says "any user w/ `has_role(:foo), calling the read action, can only read themselves"
barnabasj
barnabasjOPโ€ข2y ago
I'm just saying that there is no way around narrowing the condition
ZachDaniel
ZachDanielโ€ข2y ago
Well, that's just because its the example you gave are you talking about the rbac extension adding this filter? or you as a user of the rbac extension adding the filter?
barnabasj
barnabasjOPโ€ข2y ago
if the rbac extensio would do this
policy has_role(:foo) do
authorize_if action(:read)
end
policy has_role(:foo) do
authorize_if action(:read)
end
you as a user would still need to narrow the condition to add the filter
ZachDaniel
ZachDanielโ€ข2y ago
Not necessarily if you as a user just did this:
policy action(:read) do
authorize_if expr(id == ^actor(:id))
end
policy action(:read) do
authorize_if expr(id == ^actor(:id))
end
then both policies would apply
barnabasj
barnabasjOPโ€ข2y ago
only if the user has_role(:foo) right?
ZachDaniel
ZachDanielโ€ข2y ago
Nope well the first one applies if the user has_role(:foo), yes the second one applies if action(:read) oh, lol I see what you're saying
barnabasj
barnabasjOPโ€ข2y ago
the circle closes xD
ZachDaniel
ZachDanielโ€ข2y ago
you're right it has to be
policy action(:read) do
authorize_if has_role(:foo)
end
policy action(:read) do
authorize_if has_role(:foo)
end
So what you'd need to do to do this right is group all roles for each action
barnabasj
barnabasjOPโ€ข2y ago
but the filter would still authorize? or is filter == :unkown
ZachDaniel
ZachDanielโ€ข2y ago
the filter authorizes, but applies the filter
barnabasj
barnabasjOPโ€ข2y ago
yes but it would apply if no role matches also
ZachDaniel
ZachDanielโ€ข2y ago
So what you'd do is, for each action, list all roles that can do it
barnabasj
barnabasjOPโ€ข2y ago
and add forbid_if always at the end
ZachDaniel
ZachDanielโ€ข2y ago
policy action(:read) do
authorize_if has_role(:foo)
authorize_if has_role(:bar)
end
policy action(:read) do
authorize_if has_role(:foo)
authorize_if has_role(:bar)
end
you don't technically need forbid_if always() at the end its implied
barnabasj
barnabasjOPโ€ข2y ago
but the filter would still authorize if none of the roles match
ZachDaniel
ZachDanielโ€ข2y ago
Nah, this time for real lol
# applied by rbac extension
policy action(:read) do
authorize_if has_role(:foo)
authorize_if has_role(:bar)
end

# added by user
policy action(:read) do
authorize_if expr(id == ^actor(:id))
end
# applied by rbac extension
policy action(:read) do
authorize_if has_role(:foo)
authorize_if has_role(:bar)
end

# added by user
policy action(:read) do
authorize_if expr(id == ^actor(:id))
end
if action(:read) then both apply this time so if you don't have an allowed role, the policy is forbidden and the filter will be applied
barnabasj
barnabasjOPโ€ข2y ago
yes, but if the actor acutally has_role(:baz)
ZachDaniel
ZachDanielโ€ข2y ago
then its forbidden
barnabasj
barnabasjOPโ€ข2y ago
why?
ZachDaniel
ZachDanielโ€ข2y ago
because the first policy applies (because action(:read)) and you don't have one of those roles
barnabasj
barnabasjOPโ€ข2y ago
ok, now I think i understand where my problem in understanding was. I thought if there was no matching check it would result in :unknown and the other policies would either :authorize or :forbid and if all policies returned :unkown it would default to :forbidden
ZachDaniel
ZachDanielโ€ข2y ago
nope, every policy that applies must always result in :authorized ๐Ÿ‘
barnabasj
barnabasjOPโ€ข2y ago
Thanks for clearing things up for me. gotta fix this tomorrow
Myrmyr
Myrmyrโ€ข2y ago
Did you do any performance tests? I've started using this and sadly found out that somehow it made app really unresponsive and pinned my CPU to 100% and while in normal conditions LiveView took max of 1 second to send a response, now it took 35 seconds. I've started :observer to check what's happening and this is what I've seen(screenshot attached) I've added
rbac do
role "public" do
fields([:*])
actions([:read])
end
end
rbac do
role "public" do
fields([:*])
actions([:read])
end
end
to like 6 resources, which are in a relationship. Sadly I don't have time to debug or test it further but will try if I will have some free-ish time.
No description
ZachDaniel
ZachDanielโ€ข2y ago
๐Ÿค” Something seems strange there well, not "seems", something is very strange there. It probably isn't @barnabasj's fault, but something in core doing something strange. Can you show me the result of Ash.Policy.Info.policies(Resource) of the resource w/ those roles? @Myrmyr
barnabasj
barnabasjOPโ€ข2y ago
I also felt, that since I started using field policies my tests were executing slower. But I haven't investigated yet.
ZachDaniel
ZachDanielโ€ข2y ago
Interesting It might actually be the way they are modeled with conditions, now that I think about it but with a single one that shouldn't be enough to cause problems...
barnabasj
barnabasjOPโ€ข2y ago
I just published 0.2.0 with the refactor
ZachDaniel
ZachDanielโ€ข2y ago
oh, nice ๐Ÿ™‚ Maybe you can try that out @Myrmyr
barnabasj
barnabasjOPโ€ข2y ago
there are only conditions in certain cases. But I didn't feel like it was faster afterwards. only if you use the condition for a relationship for example
ZachDaniel
ZachDanielโ€ข2y ago
okay, so probably won't help then. How strange I mean, I don't see what would cause the issue from a policies perspective, since its basically just generating the policies you would have hand written before
barnabasj
barnabasjOPโ€ข2y ago
have you benchmarked field_policies in general?
ZachDaniel
ZachDanielโ€ข2y ago
um...no ๐Ÿ™‚ So maybe its actually just field policies
Myrmyr
Myrmyrโ€ข2y ago
FYI I'm working on providing Ash.Policy.Info.policies but have to bleep some details so might take a while
ZachDaniel
ZachDanielโ€ข2y ago
I'd bet it is here is a way to test this out @barnabasj can you make it so that [:*] doesn't add any field policies? or what does it do for :* currently?
Myrmyr
Myrmyrโ€ข2y ago
I feel like this extension adding a lot of field policies, probably one for every field, is triggering this behavior
barnabasj
barnabasjOPโ€ข2y ago
what i"m doing is going over every field and generate one policy each, I think I saw that you were doing that for :* somewhere anyway.. roles with the :* field are added to every policiy as authorize_if check.
ZachDaniel
ZachDanielโ€ข2y ago
Well, what we do for :* is generate one field policy for all of those fields since you can do field_policy [:list, :of, :fields] even still, the idea that it could take 35 seconds is pretty wild
barnabasj
barnabasjOPโ€ข2y ago
ok, let me try and combine those then.
ZachDaniel
ZachDanielโ€ข2y ago
but it would be good to confirm in some way that the cause is field policies, and then I can benchmark them in core and figure out whats going on
Myrmyr
Myrmyrโ€ข2y ago
Pastebin
[ %Ash.Policy.Policy{ condition: [{Checks.IsSiteAdminActor, [ac...
Pastebin.com is the number one paste tool since 2002. Pastebin is a website where you can store text online for a set period of time.
Pastebin
[ %Ash.Policy.FieldPolicy{ fields: [:organization_id], condi...
Pastebin.com is the number one paste tool since 2002. Pastebin is a website where you can store text online for a set period of time.
ZachDaniel
ZachDanielโ€ข2y ago
you can also have multiple field policies for a given field, and those can have a condition. So what I might suggest is doing this:
for {role, fields} <- roles do
# you'd have to use transforms for this of course
field_policy fields, [has_role(role)] do
authorize_if always()
end
end
for {role, fields} <- roles do
# you'd have to use transforms for this of course
field_policy fields, [has_role(role)] do
authorize_if always()
end
end
barnabasj
barnabasjOPโ€ข2y ago
12 field policies if I counted correctly
ZachDaniel
ZachDanielโ€ข2y ago
no, wait
for {role, fields} <- roles do
# you'd have to use transforms for this of course
field_policy fields do
authorize_if has_role(role)
end
end
for {role, fields} <- roles do
# you'd have to use transforms for this of course
field_policy fields do
authorize_if has_role(role)
end
end
Myrmyr
Myrmyrโ€ข2y ago
35 seconds happens only if I use this extension in like 7 resources that are in a relationship. If I only add this to the one I've printed policies for above it takes a bit longer(like 2-3 seconds).
ZachDaniel
ZachDanielโ€ข2y ago
ah, okay. Even 2-3 seconds is pretty wildly long but that makes sense
barnabasj
barnabasjOPโ€ข2y ago
I'll try and combine the field policies into less policies and see if that helps
ZachDaniel
ZachDanielโ€ข2y ago
both of my examples above were wrong ๐Ÿ˜†
barnabasj
barnabasjOPโ€ข2y ago
in what way? I'm currenly looking at my role check and see if it can be done in a more performant way. Right now the check is probably not the fastest and because it is executed a lot it might also be the problem. I mean I didn't see numbers as high as yours but the implementation is definetively more on the it's nice to read, then on the performance side of things
Operating System: Linux
CPU Information: AMD Ryzen 9 5950X 16-Core Processor
Number of Available Cores: 32
Available memory: 62.70 GB
Elixir 1.14.3
Erlang 25.2.1

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 5 s
memory time: 0 ns
reduction time: 0 ns
parallel: 1
inputs: none specified
Estimated total run time: 28 s

Benchmarking role_check_atom ...
Benchmarking role_check_string ...
Benchmarking roles_check_atom ...
Benchmarking roles_check_string ...

Name ips average deviation median 99th %
roles_check_atom 2.80 M 356.52 ns ยฑ6899.27% 300 ns 610 ns
role_check_string 2.70 M 369.74 ns ยฑ9181.27% 310 ns 610 ns
role_check_atom 2.70 M 370.80 ns ยฑ8415.90% 310 ns 630 ns
roles_check_string 2.58 M 388.06 ns ยฑ7806.80% 330 ns 640 ns

Comparison:
roles_check_atom 2.80 M
role_check_string 2.70 M - 1.04x slower +13.22 ns
role_check_atom 2.70 M - 1.04x slower +14.27 ns
roles_check_string 2.58 M - 1.09x slower +31.54 ns
Operating System: Linux
CPU Information: AMD Ryzen 9 5950X 16-Core Processor
Number of Available Cores: 32
Available memory: 62.70 GB
Elixir 1.14.3
Erlang 25.2.1

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 5 s
memory time: 0 ns
reduction time: 0 ns
parallel: 1
inputs: none specified
Estimated total run time: 28 s

Benchmarking role_check_atom ...
Benchmarking role_check_string ...
Benchmarking roles_check_atom ...
Benchmarking roles_check_string ...

Name ips average deviation median 99th %
roles_check_atom 2.80 M 356.52 ns ยฑ6899.27% 300 ns 610 ns
role_check_string 2.70 M 369.74 ns ยฑ9181.27% 310 ns 610 ns
role_check_atom 2.70 M 370.80 ns ยฑ8415.90% 310 ns 630 ns
roles_check_string 2.58 M 388.06 ns ยฑ7806.80% 330 ns 640 ns

Comparison:
roles_check_atom 2.80 M
role_check_string 2.70 M - 1.04x slower +13.22 ns
role_check_atom 2.70 M - 1.04x slower +14.27 ns
roles_check_string 2.58 M - 1.09x slower +31.54 ns
without changes:
alias AshRbacTest.{Api, ChildResource, RootResource, SharedResource}


root_resource = Api.create!(Ash.Changeset.for_create(RootResource, :create))
shared_resource = Api.create!(Ash.Changeset.for_create(SharedResource, :create))

child_resource =
Api.create!(Ash.Changeset.for_create(ChildResource, :create, %{root_id: root_resource.id}))

Benchee.run(%{
"read" => fn ->
RootResource
|> Ash.Query.select([:id])
|> Ash.Query.load([:child, :number, :children])
|> Api.read(actor: %{roles: [:user]})
end
})
alias AshRbacTest.{Api, ChildResource, RootResource, SharedResource}


root_resource = Api.create!(Ash.Changeset.for_create(RootResource, :create))
shared_resource = Api.create!(Ash.Changeset.for_create(SharedResource, :create))

child_resource =
Api.create!(Ash.Changeset.for_create(ChildResource, :create, %{root_id: root_resource.id}))

Benchee.run(%{
"read" => fn ->
RootResource
|> Ash.Query.select([:id])
|> Ash.Query.load([:child, :number, :children])
|> Api.read(actor: %{roles: [:user]})
end
})
this is the result:
Operating System: Linux
CPU Information: AMD Ryzen 9 5950X 16-Core Processor
Number of Available Cores: 32
Available memory: 62.70 GB
Elixir 1.14.3
Erlang 25.2.1

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 5 s
memory time: 0 ns
reduction time: 0 ns
parallel: 1
inputs: none specified
Estimated total run time: 7 s

Benchmarking read ...

Name ips average deviation median 99th %
read 210.20 4.76 ms ยฑ6.22% 4.65 ms 5.63 ms
Operating System: Linux
CPU Information: AMD Ryzen 9 5950X 16-Core Processor
Number of Available Cores: 32
Available memory: 62.70 GB
Elixir 1.14.3
Erlang 25.2.1

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 5 s
memory time: 0 ns
reduction time: 0 ns
parallel: 1
inputs: none specified
Estimated total run time: 7 s

Benchmarking read ...

Name ips average deviation median 99th %
read 210.20 4.76 ms ยฑ6.22% 4.65 ms 5.63 ms
Myrmyr
Myrmyrโ€ข2y ago
If I find a bit of time, but sadly that probably will take a while, I will try to recreate this issue in a separate app.
barnabasj
barnabasjOPโ€ข2y ago
Thanks, I'll try and optimize as best I can in the meantime.
ZachDaniel
ZachDanielโ€ข2y ago
interesting So your tests show that its quite fast We may need to wait for more instances of this to show up, or a usable reproduction in that case ๐Ÿ˜ฆ
barnabasj
barnabasjOPโ€ข2y ago
I created a pr that should result in a lot less policies, maybe you can try it out and it helps already https://github.com/traveltechdeluxe/ash-rbac/pull/11
Blibs
Blibsโ€ข2y ago
It took me some time, but I finally was able to reserve some time to finish my PR for adding support for other roles fields. there is the PR @barnabasj https://github.com/traveltechdeluxe/ash-rbac/pull/15 The only thing that I believe is missing there is adding support for bypass.
barnabasj
barnabasjOPโ€ข2y ago
I just released a new version with your changes
Blibs
Blibsโ€ข2y ago
Hey @barnabasj I noticed that when I use ash_rbac, id fields from relationships return ForbiddenField. For example, if resource A has a relationship with resource B, b_id attribute will be returned with ForbiddenField even if I use fields [:*]. Removing the rbac rules and using this one instead works fine:
field_policies do
field_policy :* do
authorize_if always()
end
end
field_policies do
field_policy :* do
authorize_if always()
end
end
Is this a bug that should be reported or an I just doing something wrong? Looking at the policy generated by rbac, seems like it filters all the :*_id attributes when generating the policy where field_policy :* doesn't
barnabasj
barnabasjOPโ€ข2y ago
Sounds like a bug to me, I'll take a look today I publish a new version that I think should fix it, if not I have to dig a little deeper.
Blibs
Blibsโ€ข2y ago
Yep, that fixed it, thanks a lot!

Did you find this page helpful?