`change manage_relationship(...type: :remove)` not working

I have a many_to_many relationship between Organization and SubscriptionPlan. The Organization has the following two actions:
update :subscribe do
argument :plan, :struct, constraints: [instance_of: SubscriptionPlan]
accept []
require_atomic? false
change manage_relationship(:plan, :subscription_plans, type: :append)
end

update :unsubscribe do
argument :plan, :struct, constraints: [instance_of: SubscriptionPlan]
accept []
require_atomic? false
change manage_relationship(:plan, :subscription_plans, type: :remove)
end
update :subscribe do
argument :plan, :struct, constraints: [instance_of: SubscriptionPlan]
accept []
require_atomic? false
change manage_relationship(:plan, :subscription_plans, type: :append)
end

update :unsubscribe do
argument :plan, :struct, constraints: [instance_of: SubscriptionPlan]
accept []
require_atomic? false
change manage_relationship(:plan, :subscription_plans, type: :remove)
end
The :subscribe action seems to work fine. But the :unsubscribe action fails with the following :invalid error:
[%Ash.Error.Changes.InvalidRelationship{relationship: :subscription_plans, message: "changes would create a new related record", splode: Ash.Error, bread_crumbs: ["Error returned from: MyApp.Accounts.Organization.unsubscribe"]
[%Ash.Error.Changes.InvalidRelationship{relationship: :subscription_plans, message: "changes would create a new related record", splode: Ash.Error, bread_crumbs: ["Error returned from: MyApp.Accounts.Organization.unsubscribe"]
I'm a little baffled by this one. Also, if I change the manage_relationship options from type: :remove to the following:
[
on_no_match: :ignore,
on_match: :unrelate,
on_missing: :ignore
]
[
on_no_match: :ignore,
on_match: :unrelate,
on_missing: :ignore
]
(this basically just changes on_no_match from :error to :ignore) ... my test also fails. It seems to remove the subscription from the Organization but not the SubscriptionPlan:
plan = plan |> Ash.load!([:subscribers], actor: user) |> dbg
assert plan.subscribers |> length == 0
plan = plan |> Ash.load!([:subscribers], actor: user) |> dbg
assert plan.subscribers |> length == 0
Not sure what I'm doing wrong here 🤔
Solution:
So the mistake I was making was in my accessing_through policy: ```elixir policy accessing_from(MyApp.Accounts.Organization, :subscribers) do authorize_if always()...
Jump to solution
14 Replies
â¿» eileen
⿻ eileenOP•4mo ago
Could it have to do with the relationship / policies?
###################
# On Organization #
###################
relationships do
many_to_many :subscription_plans, MyApp.Subscriptions.SubscriptionPlan do
through MyApp.Subscriptions.OrgSubscription
source_attribute_on_join_resource :organization_id
destination_attribute_on_join_resource :subcription_plan_id
end
end

policies do
bypass actor_attribute_equals(:role, :super) do
authorize_if always()
end

bypass actor_attribute_equals(:role, :admin) do
authorize_if always()
end

policy action_type([:read, :update, :destroy]) do
authorize_if(relates_to_actor_via(:owner))
end

policy action_type(:create) do
authorize_if always()
end
end

#######################
# on SubscriptionPlan #
#######################

relationships do
many_to_many :subscribers, MyApp.Accounts.Organization do
through MyApp.Subscriptions.OrgSubscription
source_attribute_on_join_resource :subcription_plan_id
destination_attribute_on_join_resource :organization_id
end
end

policies do
bypass actor_attribute_equals(:role, :super) do
authorize_if always()
end

bypass actor_attribute_equals(:role, :admin) do
authorize_if always()
end

policy do
forbid_if always()
end
end
###################
# On Organization #
###################
relationships do
many_to_many :subscription_plans, MyApp.Subscriptions.SubscriptionPlan do
through MyApp.Subscriptions.OrgSubscription
source_attribute_on_join_resource :organization_id
destination_attribute_on_join_resource :subcription_plan_id
end
end

policies do
bypass actor_attribute_equals(:role, :super) do
authorize_if always()
end

bypass actor_attribute_equals(:role, :admin) do
authorize_if always()
end

policy action_type([:read, :update, :destroy]) do
authorize_if(relates_to_actor_via(:owner))
end

policy action_type(:create) do
authorize_if always()
end
end

#######################
# on SubscriptionPlan #
#######################

relationships do
many_to_many :subscribers, MyApp.Accounts.Organization do
through MyApp.Subscriptions.OrgSubscription
source_attribute_on_join_resource :subcription_plan_id
destination_attribute_on_join_resource :organization_id
end
end

policies do
bypass actor_attribute_equals(:role, :super) do
authorize_if always()
end

bypass actor_attribute_equals(:role, :admin) do
authorize_if always()
end

policy do
forbid_if always()
end
end
So I assume it's that last policy do forbid_if always() end that's causing my trouble here. If I set the user's role to :admin or use authorize?: false when I call unsubscribe, the test passes. What I don't understand is how I can set a policy on the SubscriptionPlan that will allow deleting one of its OrgSubscription records Is the following advisable?
update :unsubscribe do
argument :plan, :struct, constraints: [instance_of: SubscriptionPlan]
accept []
require_atomic? false
change manage_relationship(:plan, :subscription_plans, type: :remove, authorize?: false)
end
update :unsubscribe do
argument :plan, :struct, constraints: [instance_of: SubscriptionPlan]
accept []
require_atomic? false
change manage_relationship(:plan, :subscription_plans, type: :remove, authorize?: false)
end
I've added authorize?: false to the change manage_relationship call on the Organization resource. The Organization itself is protected by policies. So does that mean its ok for some of its internal manage_relationship call to use authorize?: false?
â¿» eileen
⿻ eileenOP•4mo ago
Will give it a try 😄 Hmm. So accessing_from gives me the same error I started with:
policy accessing_from(MyApp.Accounts.Organization, :subscribers) do
authorize_if always()
end

# results in error:

[%Ash.Error.Changes.InvalidRelationship{relationship: :subscription_plans, message: "changes would create a new related record", splode: Ash.Error, bread_crumbs: ["Error returned from: MyApp.Accounts.Organization.unsubscribe"]
policy accessing_from(MyApp.Accounts.Organization, :subscribers) do
authorize_if always()
end

# results in error:

[%Ash.Error.Changes.InvalidRelationship{relationship: :subscription_plans, message: "changes would create a new related record", splode: Ash.Error, bread_crumbs: ["Error returned from: MyApp.Accounts.Organization.unsubscribe"]
This is a many_to_many relationship - not sure I mentioned that before
ZachDaniel
ZachDaniel•4mo ago
do you have other policies that apply? All policies that apply have to pass
â¿» eileen
⿻ eileenOP•4mo ago
On the SubscriptionPlan resource, which seems to be the one causing the trouble, these are my policies:
policies do
bypass actor_attribute_equals(:role, :super) do
authorize_if always()
end

bypass actor_attribute_equals(:role, :admin) do
authorize_if always()
end

policy accessing_from(MyApp.Accounts.Organization, :subscribers) do
authorize_if always()
end

policy do
forbid_if always()
end
end
policies do
bypass actor_attribute_equals(:role, :super) do
authorize_if always()
end

bypass actor_attribute_equals(:role, :admin) do
authorize_if always()
end

policy accessing_from(MyApp.Accounts.Organization, :subscribers) do
authorize_if always()
end

policy do
forbid_if always()
end
end
Then on the Organization resource, these are my policies:
policies do
bypass actor_attribute_equals(:role, :super) do
authorize_if always()
end

bypass actor_attribute_equals(:role, :admin) do
authorize_if always()
end

policy action_type([:read, :update, :destroy]) do
authorize_if(relates_to_actor_via(:owner))
end

policy action_type(:create) do
authorize_if always()
end
end
policies do
bypass actor_attribute_equals(:role, :super) do
authorize_if always()
end

bypass actor_attribute_equals(:role, :admin) do
authorize_if always()
end

policy action_type([:read, :update, :destroy]) do
authorize_if(relates_to_actor_via(:owner))
end

policy action_type(:create) do
authorize_if always()
end
end
The intermediate OrgSubscription resource doesn't have any policies
ZachDaniel
ZachDaniel•4mo ago
policy do
forbid_if always()
end
policy do
forbid_if always()
end
that forbids everyone from doing anything always And always applies
â¿» eileen
⿻ eileenOP•4mo ago
Unless bypassed .... got it
ZachDaniel
ZachDaniel•4mo ago
Careful of overusing bypasses
â¿» eileen
⿻ eileenOP•4mo ago
I'm only using them for admin / super users Oddly enough, removing the forbid_if always() policy has now resulted in a Forbidden error on both subscribe and unsubscribe actions I find policies generally to be slippery things Got it!
Solution
â¿» eileen
⿻ eileen•4mo ago
So the mistake I was making was in my accessing_through policy:
policy accessing_from(MyApp.Accounts.Organization, :subscribers) do
authorize_if always()
end
policy accessing_from(MyApp.Accounts.Organization, :subscribers) do
authorize_if always()
end
I thought that second parameter was supposed to refer to the relationship defined on the resource that was defining the policy, but it's actually the relationship defined on the resource being referenced in the policy.
â¿» eileen
⿻ eileenOP•4mo ago
So the corrected version is this:
policy accessing_from(Insi.Accounts.Organization, :subscription_plans) do
authorize_if always()
end
policy accessing_from(Insi.Accounts.Organization, :subscription_plans) do
authorize_if always()
end
Tests green ✅
â¿» eileen
⿻ eileenOP•4mo ago
Thank you!
ZachDaniel
ZachDaniel•4mo ago
🥳 haha I didn't even see that
â¿» eileen
⿻ eileenOP•4mo ago
Easy to miss Also the forbid_if always() was also important

Did you find this page helpful?