zachallaun

zachallaun

Hi friends,

I wanted an authorization utility for an existing project I’m working on where I have a fair amount of permissions logic duplicated between functions that check whether a person can do something to a given resource and query utilities that filter a query based on what a person is allowed to do.

After spending a bit of time looking at existing solutions, I decided to spend a couple days putting something together myself.

Janus is the result.

The library is extremely alpha and not yet released on hex. It’s basically at the “works enough to test in my own project” stage, but I’d like to hear from folks on the approach I’ve taken and whether it’s worth pursuing further. Basically: I (think I) like it and will use it myself, but I’m not sure whether it makes sense to invest in bits that may make it more useful for others.

If you have a spare 5-10 minutes, please consider reading through the example in the README. I’d love to hear any thoughts or feedback. Thanks!

Showing Posts 1 to 10

csadewa

csadewa

I just looked at Janus, and i love the ease and flexibility for defining rule for security!

I have a bit of concern though, how about the performance of Janus? I look at code for a bit and it seems to depend on DB (via ecto query), so i have concern on latency and throughput of it.

zachallaun

zachallaun OP

Thanks for taking a look!

As for performance: calls to allows? and forbids? do not hit the database, but operate only on the in-memory resource. filter creates an Ecto query, the performance of which will depend on the indexes, etc. that you have set up for your database — this is not really any different than if you had written the query yourself.

I’ve not benchmarked anything, but I’m fairly certain that performance will be acceptable for the vast majority of use-cases.

al2o3cr

al2o3cr

Some quick reactions:

  • what about preloads? filter? could help, but it’s going to take a lot more code than “normal” preloads

  • what about conditions that involve other records? For instance, “admin can only edit a post if it doesn’t have any replies”

zachallaun

zachallaun OP

Thanks for the questions.

Can you give a quick example of what you mean by this vs. “normal” preloads?

filter does no preloading, it just filters the query using joins. I don’t think it makes sense to couple preloading to authorization, since the concerns are separate — you may need more/less/different associations preloaded than are needed for auth.

allows? also does no preloading for now and just raises if a required association isn’t loaded. This is not optimal though, as your code checking permissions with allows? shouldn’t need to know what needs to be preloaded. It might make sense to accept a repo as an option, so you could do something like

MyPolicy.allows?(someone, :to_do_thing_to, resource, preload_with: Repo)

If you’re checking auth in a loop this could lead to N+1 queries, but in that case you should likely be using filter to filter the set of resources once.

Good question and something I’ve thought about a bit. Two possibilities:

  • Add an escape hatch to allow, forbid, etc. that lets you pass two functions, one that will run for authorizing a single resource and one that will filter a query. This way you can get as complex as needed. This would be very easy to add support for.
policy
|> allow(:edit, Post, allow_func: &allow_edit_post?/3, filter_func: &filter_edit_post/3)
  • Override allows? and filter in your policy module — this is possible now but requires some boilerplate.
def allows?(admin, :edit, %Post{}) do
  #…
end
 
def allows?(actor, action, resource), do: super(actor, action, resource)

def filter(Post, :edit, admin) do
  #…
end

def filter(schema, action, actor), do: super(schema, action, actor)

I think I prefer the first option.

al2o3cr

al2o3cr

Sticking to the models in the README for concreteness, imagine you wanted to load:

  • the first 20 posts that a user can see
  • all the comments on those that the user can see
  • all the authors on those comments that the user can see

A no-auth approach (the “normal” preloads case) would be straightforward:

from(p in Post, limit: 20)
|> Repo.all()
|> Repo.preload(comments: :author)
zachallaun

zachallaun OP

Got it.

This kind of API should be fairly straightforward to implement, and I can certainly see the utility:

Post
|> Policy.filter(:read, user, preload: [comments: :author]
|> limit(20)
|> Repo.all()

This would basically be sugar for:

posts_query = Policy.filter(Post, :read, user)
comments_query = Policy.filter(Comment, :read, user)
authors_query = Policy.filter(User, :read, user)

from(p in posts_query,
  limit: 20,
  preload: [comments: {^comments_query, author: ^authors_query}])
|> Repo.all()
zachallaun

zachallaun OP

This proved a little trickier, but the result is actually pretty dang cool. I’m not quite done implementing this yet but I’m confident it can be done.

To take your example a little further: Let’s say you were implementing user search. In the results, you wanted to display the user’s last thread, the first post in that thread, and user’s latest post in any thread.

Normal users can’t see banned users or archived threads or posts, but moderators can see both.

Here’s how it might look like using Janus:

latest_thread = Thread |> order_by(desc: :inserted_at) |> limit(1)
first_post = Post |> where(index: 0) |> limit(1)
latest_post = Post |> order_by(desc: :inserted_at) |> limit(1)

search_params
|> Accounts.search()
|> Policy.filter(:read, current_user,
  preload_filtered: [
    threads: {latest_thread, posts: first_post},
    posts: latest_post
  ]
)
|> Repo.all()

The result of this would be the users that the current user can read, preloaded with the latest thread created by that user (and that thread’s first post) that the current user can read, and the latest post by that user that the current user can read. So a normal user would see non-banned users and non-archived threads/posts, while a moderator may see banned users and archived threads/posts mixed into the results. This all done with a single query and includes users that may have no threads or posts.

Man, Ecto is really cool.

PJUllrich

PJUllrich

Author of Building Table Views with Phoenix LiveView

Loving this :flexed_biceps: It’s late here, so I’ll get back to this tomorrow, but have you also looked into implementing query restrictions using “Tenancy” instead of implementing another DDL for Ecto? Personally, I’d want the impact of my RAP as minimal as possible on the “normal” syntax like Ecto’s query language. Also, it should be easily replaceable/removable. That’s why I love our RAP at Remote. You can remove it by removing the plug and that’s it! As a dev, I wasn’t even aware that we had a RAP for a whole year :grinning_face_with_smiling_eyes:

See here for example: Multi tenancy with foreign keys — Ecto v3.14.0

zachallaun

zachallaun OP

Thanks for the suggestion! I’m using this exact approach in one of my own applications (based on the same guide that you linked). I see tenancy as somewhat separate/tangential to authorization, however – in my eyes, tenancy is about creating a hard separation between entities that should never interact, whereas authorization is about giving permission to actors within an entity to operate on resources also within that entity.

There’s certainly a chance I’ve misunderstood you, however, so I’m definitely eager for you to expand on this idea when you are able!

I think that we’re in total agreement here. The DDL used by Janus (in :where clauses, for instance) is meant to be essentially a simplified subset of Ecto’s keyword syntax. It’s simplified so that it can be used to both authorize a single resource and build up an Ecto query. I toyed with the idea of somehow using Ecto.Query.API, but I think that adds too much complexity for too little benefit.

To my eyes, the impact of Janus on the normal way of querying Ecto is extremely small. From outside of your policy module, you don’t do anything differently while building up your query except for appending a call that composes with your query to filter to authorized resources.

Here’s a sketch of what a context function might look like that loads authorized resources:

@doc """
Load the resources!

## Options

  * `:limit` - limit number of results (default 20)
  * `:authorize` - `{action, actor}` tuple to filter query to only those authorized
"""
def load_the_resources(opts \\ []) do
  query =
    MyResource
    |> order_by(desc: :inserted_at)
    |> whatever_other_filter()
    |> limit(Keyword.get(opts, :limit, 20))

  query =
    case opts[:authorize] do
      {action, actor} -> Policy.authorize(query, action, actor)
      nil -> query
    end

  Repo.all(query)
end

I think this approach is similarly replaceable/removable. Since you “own” your policy module and no code outside of that module should reference Janus, the impact of removing/replacing is limited. I’m trying to keep the API very small (currently authorize, any_authorized? and authorized [which I think I’m going to rename to filter_authorized or similar]).

Excited to hear more of your thoughts tomorrow!

zachallaun

zachallaun OP

@PJUllrich Just read part 2 and updated my gist with notes on how I might implement that using Janus!

I see now what you mean by using the “tenancy” approach! I think it translates nicely in Janus using filter_authorized and a process-dictionary-cached policy:

@operation_actions %{
  all: "read",
  stream: "read",
  update_all: "update",
  delete_all: "delete",
  insert_all: "create"
}

@impl true
def prepare_query(op, query, opts) do
  cond do
    opts[:schema_migration] || opts[:policy] == :ignore ->
      {query, opts}

    policy = opts[:policy] ->
      {RAP.Policy.filter_authorized(query, @operation_actions[op], policy), opts}

    true ->
      raise "expected policy to be set"
  end
end

Where Next? Top

Trending in RFCs Top

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
wintermeyer
There are three potential reasons for members of this forum to have a look at https://vutuv.de You are tired or annoyed of LinkedIn. Yo...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews