Noob pointers for resource authorization

I’m trying to learn Elixir/Phoenix and I’m struggling to understand how to create a resource that would belong to a user and should only be accessible to them. For example, in a task management app, users could create tasks, each task would belong to a user and no user should ever have access to tasks from other users.

I’m going through Phoenix’s HexDocs a second time but I still don’t feel confident moving forward after running the various code generators.

I’m wondering if it’d be easy for someone to walk me through the parts of the code that I should edit to add such a Task schema, after running phx.new and phx.gen.auth. Or maybe point me to a good resource on this topic that I might have missed so far. Thanks!

Hey @raulrpearson at a high level, you would add some sort of user_id column to your "tasks" table, and your schema would say have belongs_to(:user, YourApp.Whatever.User). Then you’d use something like phx.gen.auth to have users sign in. Now, if you have a path like /tasks the controller or live view that you are using will know who the current user is, and you can go query from the database only those tasks that have a user_id column equal to your current user.

If that’s too high level and working with ecto associations isn’t comfortable for you I’d strongly suggest checking out one of the Phoenix books that sort of walks you through using Phoenix in a more step by step way.

3 Likes

I recently learned of Oso - Authorization Academy, which is a great introduction to authorization from a practical yet framework agnostic view.

7 Likes

That looks like a really great resource! I might adapt some of that terminology/framing to the documentation for Janus (an authorization library I’ve released). (I should perhaps add “shameless plug” at this point :laughing:)

3 Likes

I have not used Janus yet but after an initial search and doc-reading, Janus and Let Me are the two I’m going to evaluate when I get to authorization (which should be soon as I’m building up a loooot of domain functions I’m going to have to add a user arg to). I do very much appreciate how Janus is tied in with Ecto as I’m a fan of pushing things to the db and the API is quite nice. I remember when you announced it and I’m a little disappointed it hasn’t gotten more traction!

@raulrpearson what authorization strategies/libs are you used to using? Do you generally generally authorize only at the controller level or the model (domain) level? Or are you brand spanking new to this in general? Like even outside of Phoenix?

2 Likes

I always misread questions, so sorry if this isn’t what you’re after :slight_smile:

When you run phx.gen.auth it allows you to create users. Each user has an ID that gets automatically generated.

When you create your tasks schema, you need to add whatever your field or fields are that the user will be submitting, but also need to create a reference to the user id

defmodule MYAPP.Repo.Migrations.CreatePosts do
  use Ecto.Migration

  def change do
    create table(:tasks) do
      add :YOUR FIELD
      add :user_id, references(:users, on_delete: :delete_all)

      timestamps()
    end

    create index(:tasks, [:user_id])
  end
end

In the file containing the schema “tasks” you need to add “belongs_to :user” and replace the automatica :user field it will create.

alias MYAPP.Tasks.Task

  schema "tasks" do
    field :YOUR FIELD, :string
    belongs_to :user, User

    timestamps()
  end

   @doc false
   def changeset(post, attrs) do
    post
    |> cast(attrs, [:YOUR FIELD, :user_id])
    |> validate_required([])
  end

In Accounts.User that is created by gen.auth you need to do similar but instead of belongs_to you have to add has_many in the same fashion

alias MYAPP.Tasks.Task

schema "users" do
*irrelevant code*
timestamps()
  end

has_many :posts, Post

I’m going to assume you have a form already, are trying to figure out how to link the logged in user submitting the form and it being linked to them

If you have a logged in user, odds are you have @current_user in your socket already
After doing all of the above you can simply add the this to your “save_task” function within the form_component and it will store the logged in users ID value to the “tasks” schema’s user_id field.
If you add this before the “create_task” function it will take the task_params which contain your form data already, and update them to as a “user_id” value which will be your current users ID.

task_params = task_params
      |> Map.put("user_id", socket.assigns.current_user.id)

In order for users to only see thier own tasks, you can modify the default list_tasks function to filter using the user_id field of the tasks table we created earlier.

For example

def list_tasks(current_user) do   
    Repo.all(
      from t in Task,
      where: t.user_id == ^current_user.id
    )   
  end

When you use list_tasks in an apply_action, you just need to use list_tasks(socket.assigns.current_user)

The list_tasks function will now check the Tasks database for all user_id values that match the current_user ID value and only return values that specific to the current user.

Edit: I forgot to add that belongs_to and has_many are not the only things you can use, but sound relevant to your case. There are other options like has_one (user only has a single task) and many_to_many (if multiple of tasks are owned by multiple users for example)

1 Like

Thank you all for taking some time to reply :pray:. I’ll need some time to digest and apply your suggestions but I think I now have a better idea of how to start implementing this.

None, really. I have experience mostly in the frontend (React) and usually rely on third-party APIs for any backend features I need. I’m trying to learn Elixir/Phoenix with the hope of becoming a more well-rounded fullstack dev and being able to build more of my own apps.