Contextual authorization for "collection" operations

So, in my quest to convert an existing system (slowly) over to Ash, I’m trying to figure out how to replicate certain patterns in the system.

For reading a single resource or updating, the Ash flow is pretty obvious with authorization. The resource is in the context and I can do whatever. Awesome.

I’m not seeing a clear example of how to handle “collection” operations.

(And by the way, now that I’m really digging in, it’s all kind of overwhelming! In good and bad ways, lol. It’s much easier to build something to learn it, which is what’s up right now.)

For instance, creating a resource. In my existing system, most of our collection type functions look like this:

defmodule Comments
  def create(context_resource, attrs, actor, opts \\ []) do
    ...
  end

  def query(context_resource, query_params, actor, opts \\ []) do
    ...
  end
end

If I’m going to create a comment, I’m likely going to want to authorize create in relation to the article. “Does this article allow comments? Is the user a real user?” This also goes really well with REST APIs that actually make use of the URL.

POST /articles/:id/comments

“Create a comment where the context is the article.”

My code is going use something in Ecto like put_assoc/3, in many cases, to attach the context to the newly created resource. The changset functions also typically take the same form: create_changeset(context_resource, attrs, user, opts \\ []). I have to imagine, thinking about your own JSON:API support, that there’s definitely a path for this, even where the implementation is different.

Queries work similarly. In the system I’m working on, I rarely deal with a collection of things in context to a user (and when I do, I pass in the user as the context resource and maybe a different one as the actor). It’s usually some children of a thing that the user has access to. So in an API example:

GET /articles/:id/comments

“I want all the comments for a blog.” Where does that play out in actions? (I probably won’t dive into relationships until I get policies working a bit better, so maybe I am ahead of myself.) I want to authorize against the article and then get the appropriate comments.

Consider this example from the docs. In this create, the only context I have is the concept of a beer. What if I wanted to create a beer in context of being in France versus the US in terms of determining the drinking age? What’s the “proper” way to provide that to the action?

I know this is long. I sense a combination of relationships and your own context stuff is how this goes together.

Also, I got hung up in authorization, lol. The answer might be, “Bruh, just go read up in on relationships.” That’s probably not on the menu till Saturday.

So you have two options here.

Use the arguments to look up related info

The first one is to use the input arguments to determine the related thing, and then operate on that information.

For example:

defmodule YourApp.Checks.MatchesDrinkingAge do
  use Ash.Policy.SimpleCheck

  def match?(actor, %{source: %Ash.Changeset{} = changeset}, _opts) do
     country_id = Ash.Changeset.get_argument(changeset, :country_id)
     # lookup the country id, and return `true` or `false` depending
  end
end

Put the action on the thing that controls the rules

This is probably not what you want in this case, but can be useful to simplify

# on Country
create :add_beer do
  # create the beer in a hook
  ...
end

It’s actually a little bit inconvenient to authorize data creation when compared with our other check types, and I would really like to build up some more tools for this kind of thing.
But, although slightly more verbose than we’d prefer, you can ultimately do “anything you want” in simple checks, and those should allow for any authorization logic you need in this case.

Actually, this is fine.

I’m sorta learning that, at least for mutations, the changeset in Ash is “all the things.” It’s all the changes, hooks, and other “stuff.” Context is there. Arguments are there. I haven’t messed with with notifications yet, but… I presume they link in there too. Map.from_struct/1 has been super enlightening.

This is enough to mess with for now. Thanks!

I was thinking about starting a new topic, but this one has a lot of the context.

For creates, I just pass in %{context: %{resource: <probably-a-belongs-to-resource>}}

This works as I wanted. (I’ll have to see precisely how this plays out with APIs, but that’s not today’s problem.)

As per our Discord conversation, I was able to make an excellent filter check that can dip into related access lists on a resource. This works great for reads and updates.

I blindly tossed the check onto a create action though and was greeted with the following error:

     ** (Ash.Error.Forbidden) 
     Bread Crumbs:
       > Exception raised in: MyApp.Resource.create

     Forbidden Error

     * Cannot use a filter to authorize a create.

The actual error message is quite informative and also, duh!

Okay, fine. What I want to do is effectively run the filter query on the related resource. “Do I have management permission to the context resource?” Is there some way to utilize the filter check against the context resource inside the check?

I have a kinda crappy hack around this right now but… I’m trying to find a way to just use the check inside another check or move the expression in the check into some shared space. (It’s hard to understand, at my current state what expr/1 is applied to or how. I haven’t dug into that yet.)

And I guess on that note… the pattern I am looking for here is, “If I have permission to do X on Y resource, I should pass this check.”

I’m not really sure how to go about doing this. I can say this is precisely how my existing system works.

“Oh, you want to delete this comment? Are you the owner (check one), no? Okay, are you someone who can update the article (check two)?”

@zachdaniel I probably should have replied to you instead of the topic. (I felt like this overkill for the Discord channel.)

We don’t currently have a check like that, doing it would likely require some moderately difficult code. You could consolidate the logic reasonably well with custom checks and calculations etc. please open an issue to track the concept of delegating permission checks. We may have one already though, so do a search before hand if possible :slight_smile: