miguelcoba

miguelcoba

Hi there, I would like to know your thoughts about how to use Oban workers in a Phoenix app with Scopes.

When using scopes the context functions receive as first parameter the scope, f.e.

# lib/my_app/blog.ex
def list_posts(%Scope{} = scope) do
  Repo.all(from post in Post, where: post.user_id == ^scope.user.id)
end

If I needed to use the list_posts inside a worker, I’ll need somehow to build a scope in order to use those scoped functions:

defmodule MyApp.MyWorker do
  use Oban.Worker

  @impl Oban.Worker
  def perform(%Oban.Job{args: %{"id" => id}}) do
    scope = # ? how to get the scope at the time the worker runs

    list = Blog.list_posts(scope)
    # Do something with list
    :ok
  end
end

Option 1.

Pass the scope (or enought data to rebuild the scope) as params to `Worker.new()`

%{user_id: scope.user.id, other: "data"}
|> MyApp.Worker.new()
|> Oban.insert()

and in the worker use user_id to build a `scope` and continue from that.

Option 2.

Add additional heads in the Context that don’t require scope and are only used by trusted callers (in this case Oban)

# lib/my_app/blog.ex
# additional function head that does not use scope
def list_posts(user_id) do
  Repo.all(from post in Post, where: post.user_id == ^user_id)
end

defmodule MyApp.MyWorker do
  use Oban.Worker

  @impl Oban.Worker
  def perform(%Oban.Job{args: %{"id" => user_id}}) do
    # use the non-scoped functions
    list = Blog.list_posts(user_id)
    # Do something with list
    :ok
  end
end

None of the options is optimal, but from those two, the option 2 is the worst as it totally breaks the idea of scopes in the context modules by allowing anyone to bypass the scoped functions and use the non-scoped ones.

So I’m leaning on passing enough info to the worker params and use that to build a scope inside it and then use it to do whatever thing the worker needs to do using the scoped context functions.

Is there another way?

Do you have suggestions/ideas/experience implementing something like this?

Thanks in advance,

Miguel Cobá

Showing Posts 1 to 10

pjode

pjode

I think it depends on the type of worker. If the job being run is a deferred action a user is making that should be run in the scope of a user, putting the data necessary to reconstruct a user scope in the job is the right choice IMO. If the job is something that executes in a more privileged way, I actually define a new scope for that. For example, I have a scope defined for ingest and use that in function heads instead of a user scope. So you could imagine something like

defmodule MyApp.Accounts do
  def create_user(%Accounts.Scope{admin: true} = scope, attrs) do
    # ...
  end

  def create_user(%Ingest.Scope{} = scope, attrs) do
    # ...
  end
end

and again you’d store what you need in the job to construct such a scope. This way functions still expect some form of scope regardless.

slouchpie

slouchpie

Maybe put the user_id as an entry in the job’s args and then use the Scope.for_user function (or whatever equivalent you have for this)?

tfwright

tfwright

the option 2 is the worst as it totally breaks the idea of scopes in the context modules by allowing anyone to bypass the scoped functions and use the non-scoped ones.

I’m not sure why you think this. Scopes are usually applied to user input at the edges (e.g. to request params) so unless you are allowing users to directly pass args into Oban jobs most likely those params should have already been scoped and so can be considered safe.

Also, it seems that Phoenix or Oban don’t really matter here. This question kind of reminds me of ones you would occasionally see on here a few years back about “how to use contexts in phoenix.” Contexts only “exist” in Phoenix as a pattern in the generators/ecosystem. They don’t have an API you need to (or can) use to accomplish x, y or z in any particular way. They are just modules.

Similarly, “phoenix scopes” are just structs that phoenix will generate for you according to a conventional naming scheme. They exist as a concept in generators, and the docs, it seems mostly to promote good programming practices. As the docs say:

Scopes play an important role in security. OWASP lists “Broken access control” as a top-10 security risk.

rhcarvalho

rhcarvalho

One counter argument to this is if user permissions change between scheduling the job and running the job, one probably wants to check authorization when the job starts (or the user could have been deleted, banned, etc).

Similar to how handle_event and others should check before performing an action, as the system may have changed since the mount.

Authorization rules generally happen on mount (for instance, is the user allowed to see this page?) and also on handle_event (is the user allowed to delete this item?).

tfwright

tfwright

My point was that this is all just business logic, so yeah. There is no “idea of scopes” that can be violated in this way. Usually validating at the edges is fine but depending on the specific requirements of the app/business it might not be. Even if you need to care about permission changes (it might even be the case roles/access are immutable!), it can also go the other way, that you need to respect what they were at the time of the request, or it might be necessary to raise a more specific error, etc etc. Checking on mount in a LV may very well be acceptable or exactly what’s required. I don’t think there is any general rule for any of this, and so there is no utility that Phoenix can/does provide that you can use to ensure you are using scopes “the correct way,” it’s just a nudge to start with a basic pattern in place.

miguelcoba

miguelcoba OP

yeah, most of the times is something simple like that. Imagine an email that is send as a side effect of some UI action that I don’t want to do it in the same request as the UI, but still it needs to access the user info.

I guess that’s a valid option even if the explode of scope < - > rebuild of scope hits me like an extreme middle step.

Thanks for your perspective.

miguelcoba

miguelcoba OP

yes, that’s exactly what I’m currently doing, but it looked weird to me that I needed to extract that into params, then on the other side, rebuild a scope just in order to be able to use the scoped functions.

As it looks, it seems that’s unavoidable and the way to go.

Thanks, James!

miguelcoba

miguelcoba OP

Interesting. I was trying to follow the docs and use the scope to harden/validate access to data. I didn’t even consider to break that “contract” as it seemed to me like diverging of the “good practices”. Maybe I should relax my view and view unscoped functions that are called from places where validation has already happened and it is safe to do so, is ok.

Yeah, maybe I’m reading too much and assigning too much weight to them and that introduces unflexibility

I’ll take a good thinking on this.

Thank you!

miguelcoba

miguelcoba OP

Interesting. So far I have not paid attention to that. In the oban side I just executed the action enqueued. I assumed if the job was scheduled is because on the scheduling function, validation was already performed and I didn’t want to repeat that. Maybe I should reconsider that.

Thanks, this is helpful!

miguelcoba

miguelcoba OP

I like that. Definitely changes the way we approach programming with scopes. I’ll keep it in mind.

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New
ryanwinchester
apply_graft/2 doesn’t rewrite an add_many sub-workflow’s deps on an add step. Grafted jobs cancel with “upstream job was deleted” Version...
New

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
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; 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

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews