What is the best way to set an authorization for the primary `:read` action when creating a linked Resource?

Hello!!

I have a UserEmotion Resource (it’s a join between User resource and Emotion resource).

In my User resource, I allow only users with role == :admin to access the :read action:

policy action(:read) do
      authorize_if actor_attribute_equals(:role, :admin)
    end

The UserEmotion exposes a create function named add_user_emotion which is quite basic:

    create :add_user_emotion do
      description "Adds a new entry to user_emotions."

      argument :user, :uuid do
        allow_nil? false
      end

      argument :emotion, :uuid do
        allow_nil? false
      end

      change manage_relationship(:user, type: :append)
      change manage_relationship(:emotion, type: :append)
    end

I want this function to be available for all Users, including those who are not :admin (maybe I’ll populate the :user myself from the actor to ensure a User only adds Emotions for itself).

To fix the permission issue, I could:

  • create a :list function in User resource, and I expose this function instead of :read in the API, so I put restricting policies on :list and I open all permissions on :read → it doesn’t feel the right way to me, since there’s a breach if I later expose the :read function by mistake
    OR
  • create an intermediary function before the :add_user_emotion that will set an actor like actor: %{internal: true}, and then authorize this internal new actor in the policy like so:
policy action(:read) do
      authorize_if actor_attribute_equals(:role, :admin)
      authorize_if actor_attribute_equals(:internal, :true) // NEW ACTOR
    end

None of my solutions seems good to me. Do you have any advice on this case? I guess it’s a common challenge, but I haven’t found a solution while checking at the docs (but I found many responses to other questions I had :slightly_smiling_face: ).

P.S. I’ve also tried the accessing_from builtin policy, but It doesn’t work since my UserEmotion Resource is not yet created but on the path to be created…

    policy action(:read) do
      authorize_if actor_attribute_equals(:role, :admin)
      authorize_if accessing_from(UserEmotion, :emotions)
    end

I found a better way to do what I want: it looks like this

# in UserEmotion Resource

  actions do
    create :add_user_emotion do
      description "Adds a new entry to user_emotions."

      change relate_actor(:user)

      argument :emotion, :uuid do
        allow_nil? false
      end

      change manage_relationship(:emotion, type: :append)

      change load(:emotion)
    end

   […]
  end

This way I don’t need to retrieve the User from a given uuid, and it logically allows only the current actor to manage its own Emotions.

I’m still curious if there is a way to allow the current action (once it’s authorized) to call other internal actions that would not directly be authorized for the initial actor.

Maybe it would be a bad design since it would be a way to retrieve data that is not supposed to be callable by the inital actor?
But so is there a trick to keep a way of retrieving User for instance from their uuid to link them to a Resource under creation? (my first “solution” works, after all… but I’m not sure of it’s legitimacy)

You can pass an authorize?: false option to manage_relationship if you want the parent action’s authorization rules to be the arbiter in this context. That might have helped with your original design.

2 Likes

Oh yeah, that’s great, I’ll keep this one close :slight_smile:
Thanks a lot!

In this case you might also want an authorization rule for users, to let them read their own records. ie. authorize_if expr(id == ^actor(:id))

I think you still need to be able to read the user record, to use relate_actor!

2 Likes

Thanks a lot for the precision @sevenseacat !!

To give you my feedback: the relate_actor works, even if the actor has no rights on the primary User’s :read function.

I guess it’s because the actor is already a User Resource in the context, rather than being only an uuid, and that Ash needs to retrieve the Resource to link tho User Resource to the UserEmotion.

My feeling says use relate_actor and then if you need an admin to set any user you do something like:

create :add_user_emotion_admin do
  argument :emotion, :uuid, allow_nil?: false
  argument :user, :uuid, allow_nil?: false
  change manage_relationship(:emotion, type: :append)
  change manage_relationship(:user, type: :append)
end

policies
  action(:add_user_emotion_admin) do
    authorize_if actor_attribute_equals(:admin, true)
  end
end
1 Like

Gotcha. I just did some experimenting, and it seems it will work on create actions, and atomic update actions (it’ll fetch the attribute from the User resource) - but for non-atomic update actions, it won’t :thinking:

Yeah, seems like a safe and practical way to do what I want :slight_smile:

@sevenseacat Thanks for this new feedback! So it feels safer to add the policy you gave me: authorize_if expr(id == ^actor(:id)) on the User’s :read action.