AE
Ash Elixirβ€’3y ago
Myrmyr

Embed `has_one` relationship into resource

Let's say I have a resource A. It has relationship:
relationships do
has_many :a_version, Ash.Api.AVersion do
sort created_at: :desc
end
end
relationships do
has_many :a_version, Ash.Api.AVersion do
sort created_at: :desc
end
end
which holds some fields that should be versioned if any changes happen, it has at least one :a_version Let's say the AVersion has the a :price attribute. We can define the manual fake "has_one" relationship, by making a query that takes the latest version of AVersion. Now, here's question. Is there any way to embed the fields from the latest AVersion from the fake "has_one" manual relationship into A so that they can be: - Accessed at top-level like A.price - Loaded automatically as if they were attributes of the A itself?
20 Replies
ZachDaniel
ZachDanielβ€’3y ago
There are ways, yes. The basic building block there is a first aggregate.
first :foo, :a_version, :foo
first :bar, :a_version, :bar
first :foo, :a_version, :foo
first :bar, :a_version, :bar
that would provide the foo and bar fields on the top level resource And then to load them automatically all the time:
preparations do
prepare build(load: [:foo, :bar])
end
preparations do
prepare build(load: [:foo, :bar])
end
Although I'd suggest finding a way to only load them when necessary πŸ™‚
Myrmyr
MyrmyrOPβ€’3y ago
Woah, had no idea this existed, kinda feel bad that it's the second Topic in the docs and I hadn't looked at it. That's awesome, thanks!
Although I'd suggest finding a way to only load them when necessary πŸ™‚
The catch is that, there are fields that are used everywhere where the resource is loaded. So there will be no scenario like that probably
ZachDaniel
ZachDanielβ€’3y ago
πŸ‘
Myrmyr
MyrmyrOPβ€’3y ago
Bringing this back from the dead solved because the aggragations caused the :update action to fail authentication. For some reason the :read action that is performed after :update fails with Ash.Error.Forbidden on the IsAllowedTenant check. The :update without aggregates was working fine, also doing the :read action alone is also working. A resource:
@embed_from_version [
...fields_to_be_aggegated_from_A_version
]

actions do
read :read do
primary? true

prepare MyApp.Api.Preparations.SetTenantFromActor
prepare build(load: @embed_from_version)
end

update :update do
primary? true
...arguments

# This takes organization id from actor and sets
# its as tenant
change MyApp.Api.Changes.SetTenantFromActor

# Manual action that handles versioning
manual MyApp.Api.Actions.UpdateA
end
end

aggregates do
for field <- @embed_from_version do
first field, :a_versions, field do
sort created_at: :desc
end
end
end

multitenancy do
strategy :attribute
attribute :organization_id
end

policies do
policy action(:read) do
forbid_unless MyApp.Api.Checks.IsAllowedTenant
authorize_if MyApp.Api.Checks.IsValidActor
end

policy action(:update) do
forbid_unless MyApp.Api.Checks.IsAllowedTenant
authorize_if MyApp.Api.Checks.IsValidActor
end
end
@embed_from_version [
...fields_to_be_aggegated_from_A_version
]

actions do
read :read do
primary? true

prepare MyApp.Api.Preparations.SetTenantFromActor
prepare build(load: @embed_from_version)
end

update :update do
primary? true
...arguments

# This takes organization id from actor and sets
# its as tenant
change MyApp.Api.Changes.SetTenantFromActor

# Manual action that handles versioning
manual MyApp.Api.Actions.UpdateA
end
end

aggregates do
for field <- @embed_from_version do
first field, :a_versions, field do
sort created_at: :desc
end
end
end

multitenancy do
strategy :attribute
attribute :organization_id
end

policies do
policy action(:read) do
forbid_unless MyApp.Api.Checks.IsAllowedTenant
authorize_if MyApp.Api.Checks.IsValidActor
end

policy action(:update) do
forbid_unless MyApp.Api.Checks.IsAllowedTenant
authorize_if MyApp.Api.Checks.IsValidActor
end
end
The A_version resource:
actions do
defaults [:create, :read, :update]
end

policies do
policy action(:create) do
authorize_if MyApp.Api.Checks.IsValidActor
end

policy action(:read) do
authorize_if MyApp.Api.Checks.IsValidActor
end

policy action(:update) do
authorize_if MyApp.Api.Checks.IsValidActor
end
end
actions do
defaults [:create, :read, :update]
end

policies do
policy action(:create) do
authorize_if MyApp.Api.Checks.IsValidActor
end

policy action(:read) do
authorize_if MyApp.Api.Checks.IsValidActor
end

policy action(:update) do
authorize_if MyApp.Api.Checks.IsValidActor
end
end
1/2 IsAllowedTenant check:
defmodule MyApp.Api.Checks.IsAllowedTenant do
@moduledoc false
use Ash.Policy.SimpleCheck

@impl true
def describe(_), do: "actor and tenant combination is not forbidden"

@impl true
def match?(
%CustomActor{organization_id: organization_id},
%{changeset: %Ash.Changeset{tenant: organization_id}},
_options
) do
true
end

def match?(
%CustomActor{organization_id: organization_id},
%{query: %Ash.Query{tenant: organization_id}},
_options
) do
true
end

def match?(_actor, _authorizer, _options) do
false
end
end
defmodule MyApp.Api.Checks.IsAllowedTenant do
@moduledoc false
use Ash.Policy.SimpleCheck

@impl true
def describe(_), do: "actor and tenant combination is not forbidden"

@impl true
def match?(
%CustomActor{organization_id: organization_id},
%{changeset: %Ash.Changeset{tenant: organization_id}},
_options
) do
true
end

def match?(
%CustomActor{organization_id: organization_id},
%{query: %Ash.Query{tenant: organization_id}},
_options
) do
true
end

def match?(_actor, _authorizer, _options) do
false
end
end
From the logs I've gathered the IsAllowedTenant check is getting a valid actor, but the query itself doesn't have any information about tenant, so it make sense why it fails. BUT, the :read query usually gets it's tenant information from the SetTenantFromActor prepare. And there's a problem, because when I've logged what SetTenantFromActor is getting with this "read after update" action, it turns out it does not get an actor at all. The context in the prepare function is %{actor: nil, authorize?: nil, tracer: nil}
ZachDaniel
ZachDanielβ€’3y ago
Interesting. So the read after updating is likely just reading to load the aggregate We authorize access to aggregates as well I’ll have to look into that code in a few hours when I’m back at my computer
Myrmyr
MyrmyrOPβ€’3y ago
Yup, the query that IsAllowedTenant is getting is:
#Ash.Query<
resource: MyApp.Api.A,
aggregates: <here_are_all_the_aggregates>
>,
#Ash.Query<
resource: MyApp.Api.A,
aggregates: <here_are_all_the_aggregates>
>,
and there's no info about tenant whatsoever. I will try to debug it in the meantime, but so far I've just got lost in the codebase πŸ˜†
ZachDaniel
ZachDanielβ€’3y ago
Oh, interesting actually: It’s the parent query that is failing. That is probably a bug. We shouldn’t be reauthorizing the parent query again to load data.
Myrmyr
MyrmyrOPβ€’3y ago
Also the log from the verbose?: true option of the :update
13:58:45.187 [debug] 1: Engine Starting - prepare MyApp.Api.A.update, commit MyApp.Api.A.update

13:58:45.187 [debug] prepare MyApp.Api.A.update: strict checking

13:58:45.187 [debug] prepare MyApp.Api.A.update: resolving changeset

13:58:45.190 [debug] prepare MyApp.Api.A.update: successfully resolved changeset

13:58:45.190 [debug] prepare MyApp.Api.A.update: strict checking for Ash.Policy.Authorizer

13:58:45.207 [info] Potential Scenarios
action == :create => false
action == :destroy => false
action == :read => false
action == :update => true
actor is <custom_actor> => true
actor and tenant combination is not forbidden => true

13:58:45.207 [info] Real Scenarios
action == :create => false
action == :destroy => false
action == :read => false
action == :update => true
actor is <custom_actor> => true
actor and tenant combination is not forbidden => true

13:58:45.207 [debug] prepare MyApp.Api.A.update: strict check succeeded for Ash.Policy.Authorizer

13:58:45.207 [debug] prepare MyApp.Api.A.update: Strict check complete

13:58:45.207 [debug] commit MyApp.Api.A.update: Skipping strict check due to authorize?: false

13:58:45.207 [debug] prepare MyApp.Api.A.update: resolving data

13:58:45.207 [debug] prepare MyApp.Api.A.update: successfully resolved data

13:58:45.207 [debug] prepare MyApp.Api.A.update: data fetched: []

13:58:45.207 [debug] commit MyApp.Api.A.update: changeset waiting on dependencies: [[:data, :changeset]]

13:58:45.207 [debug] prepare MyApp.Api.A.update: Attempting to provide :changeset for [:commit]

13:58:45.207 [debug] prepare MyApp.Api.A.update: storing dependency on changeset from [:commit]

13:58:45.207 [debug] prepare MyApp.Api.A.update: Field changeset, was resolved and provided

13:58:45.207 [debug] commit MyApp.Api.A.update: Receiving field changeset from [:data]

13:58:45.207 [debug] prepare MyApp.Api.A.update: Check complete
13:58:45.187 [debug] 1: Engine Starting - prepare MyApp.Api.A.update, commit MyApp.Api.A.update

13:58:45.187 [debug] prepare MyApp.Api.A.update: strict checking

13:58:45.187 [debug] prepare MyApp.Api.A.update: resolving changeset

13:58:45.190 [debug] prepare MyApp.Api.A.update: successfully resolved changeset

13:58:45.190 [debug] prepare MyApp.Api.A.update: strict checking for Ash.Policy.Authorizer

13:58:45.207 [info] Potential Scenarios
action == :create => false
action == :destroy => false
action == :read => false
action == :update => true
actor is <custom_actor> => true
actor and tenant combination is not forbidden => true

13:58:45.207 [info] Real Scenarios
action == :create => false
action == :destroy => false
action == :read => false
action == :update => true
actor is <custom_actor> => true
actor and tenant combination is not forbidden => true

13:58:45.207 [debug] prepare MyApp.Api.A.update: strict check succeeded for Ash.Policy.Authorizer

13:58:45.207 [debug] prepare MyApp.Api.A.update: Strict check complete

13:58:45.207 [debug] commit MyApp.Api.A.update: Skipping strict check due to authorize?: false

13:58:45.207 [debug] prepare MyApp.Api.A.update: resolving data

13:58:45.207 [debug] prepare MyApp.Api.A.update: successfully resolved data

13:58:45.207 [debug] prepare MyApp.Api.A.update: data fetched: []

13:58:45.207 [debug] commit MyApp.Api.A.update: changeset waiting on dependencies: [[:data, :changeset]]

13:58:45.207 [debug] prepare MyApp.Api.A.update: Attempting to provide :changeset for [:commit]

13:58:45.207 [debug] prepare MyApp.Api.A.update: storing dependency on changeset from [:commit]

13:58:45.207 [debug] prepare MyApp.Api.A.update: Field changeset, was resolved and provided

13:58:45.207 [debug] commit MyApp.Api.A.update: Receiving field changeset from [:data]

13:58:45.207 [debug] prepare MyApp.Api.A.update: Check complete
1/2
13:58:45.207 [debug] commit MyApp.Api.A.update: resolving changeset

13:58:45.207 [debug] commit MyApp.Api.A.update: successfully resolved changeset

13:58:45.207 [debug] commit MyApp.Api.A.update: resolving data

<Here is the logs from failing `IsAllowedTenant` action and database query to the `A_version`>

13:58:45.278 [debug] commit MyApp.Api.A.update: error fetching data: <Really long error esentially telling that Tenant is invalid>

13:58:45.278 [debug] 1: Engine Complete
13:58:45.207 [debug] commit MyApp.Api.A.update: resolving changeset

13:58:45.207 [debug] commit MyApp.Api.A.update: successfully resolved changeset

13:58:45.207 [debug] commit MyApp.Api.A.update: resolving data

<Here is the logs from failing `IsAllowedTenant` action and database query to the `A_version`>

13:58:45.278 [debug] commit MyApp.Api.A.update: error fetching data: <Really long error esentially telling that Tenant is invalid>

13:58:45.278 [debug] 1: Engine Complete
ZachDaniel
ZachDanielβ€’3y ago
Can I see your IsAllowedTenant check?
ZachDaniel
ZachDanielβ€’3y ago
Oh, yeah sorry πŸ™‚ Okay, back at my computer First thing that we should confirm: if you remove the aggregates, it just all works?
Myrmyr
MyrmyrOPβ€’3y ago
Yup
ZachDaniel
ZachDanielβ€’3y ago
Okay, interesting I should be able to reproduce this πŸ™‚
Myrmyr
MyrmyrOPβ€’3y ago
What I'm currently checking is that in the Ash.Actions.Load:445 in the load_request there are opts which contain an actor, they are passed to the data option, but the actor is not explicitly set in the Request.new
ZachDaniel
ZachDanielβ€’3y ago
take a look at lib/ash/actions/read.ex line 346 can you IO.inspect the value we're setting for that :authorize?? nvm I found the problem πŸ™‚ So this is us authorizing the actual aggregate. The reason you aren't seeing it in the verbose logs is because in update when we're loading we aren't passing through the verbose flag The problematic line is lib/ash/query/aggregate.ex line 393 The base query we're creating for authorizing the aggregate does not contain the actor/tenant (this was a recent-ish change, sorry about that) will push a fix up shortly
Myrmyr
MyrmyrOPβ€’3y ago
Shoot! I was looking at this function like an hour ago, so close xD Well, it didn't break anything that existed in our system, we are just introducing it πŸ˜„ Thanks! ❀️
ZachDaniel
ZachDanielβ€’3y ago
Okay, took longer than I thought, but also ended up adding some much needed options to aggregates. Mind giving it a try now @Myrmyr ?
Myrmyr
MyrmyrOPβ€’3y ago
Sadly I've just finished work and left computer in the office, I can test it tomorrow but ideally on Monday :/
ZachDaniel
ZachDanielβ€’3y ago
No worries πŸ™‚
Myrmyr
MyrmyrOPβ€’3y ago
It's working πŸŽ‰ Thanks a lot!

Did you find this page helpful?