Not sure how to apply a policy on an attribute of a resource. Keep getting an error.

I have a policy for a resource called user_tokens which is just a way to test a user sign up. User enters phone number and user_tokens will save and send an otp to the mobile number. To avoid abuse, there is a status column which when "locked" should stop sending the SMS. I tried the things as the error suggested but could not overcome the problem yet. Also searched the prior posts but yet to find an answer. Very new to Ash, but hopefully I am not bugging and taking up much of your time in the process of getting past my learning curve. Apologies if I am.
policy action_type([:create]) do
forbid_if expr(status == :locked)
authorize_if always()
end
policy action_type([:create]) do
forbid_if expr(status == :locked)
authorize_if always()
end
iex(1)> Ash.Changeset.for_create(Arrive.Account.UserToken, :send_otp, %{sent_to: "1112223333"}) |> Ash.create!()
** (Ash.Error.Forbidden)
Bread Crumbs:
> Exception raised in: Arrive.Account.UserToken.send_otp

Forbidden Error

* Cannot use a filter to authorize a create.

Filter: not status == :locked

If you are using Ash.Policy.Authorizer:

Many expressions, like those that reference relationships, require using custom checks when used with create actions.

Expressions that only reference the actor or context, for example `expr(^actor(:is_admin) == true)` will work
because those are evaluated without needing to reference data.

For create actions, there is no data yet. In the future we may support referencing simple attributes and those
references will be referring to the values of the data about to be created, but at this time we do not.

Given a policy like:

policy expr(special == true) do
authorize_if expr(allows_special == true)
end

You would rewrite it to not include create actions like so:

policy [expr(special == true), action_type([:read, :update, :destroy])] do
authorize_if expr(allows_special == true)
end
iex(1)> Ash.Changeset.for_create(Arrive.Account.UserToken, :send_otp, %{sent_to: "1112223333"}) |> Ash.create!()
** (Ash.Error.Forbidden)
Bread Crumbs:
> Exception raised in: Arrive.Account.UserToken.send_otp

Forbidden Error

* Cannot use a filter to authorize a create.

Filter: not status == :locked

If you are using Ash.Policy.Authorizer:

Many expressions, like those that reference relationships, require using custom checks when used with create actions.

Expressions that only reference the actor or context, for example `expr(^actor(:is_admin) == true)` will work
because those are evaluated without needing to reference data.

For create actions, there is no data yet. In the future we may support referencing simple attributes and those
references will be referring to the values of the data about to be created, but at this time we do not.

Given a policy like:

policy expr(special == true) do
authorize_if expr(allows_special == true)
end

You would rewrite it to not include create actions like so:

policy [expr(special == true), action_type([:read, :update, :destroy])] do
authorize_if expr(allows_special == true)
end
21 Replies
ZachDaniel
ZachDaniel2mo ago
Use the changing check for creates changing(status: [to: :locked]) something like this
thinklt
thinkltOP2mo ago
The error message says it further down which I could not post due to size limitation of the post, but it did say this and I did try this. Will do so again so I get the error I got with that.
policy [changing_attributes(special: [to: true]), action_type(:create)] do
authorize_if changing_attributes(special: [to: true])
end
policy [changing_attributes(special: [to: true]), action_type(:create)] do
authorize_if changing_attributes(special: [to: true])
end
With this, now it says there is no actor as the error message. But I don't have any actor yet in the system.
policy [changing_attributes(status: [to: :locked]), action_type(:create)] do
forbid_if changing_attributes(status: [to: :locked])
authorize_if always()
end
policy [changing_attributes(status: [to: :locked]), action_type(:create)] do
forbid_if changing_attributes(status: [to: :locked])
authorize_if always()
end
iex(3)> Ash.Changeset.for_create(Arrive.Account.UserToken, :send_otp, %{sent_to: "1112223333"}) |> Ash.create!()
** (Ash.Error.Forbidden)
Bread Crumbs:
> Error returned from: Arrive.Account.UserToken.send_otp

Forbidden Error

* forbidden:

Arrive.Account.UserToken.send_otp


No policy conditions applied to this request.
For safety, at least one policy must apply to all requests.

unknown actor
iex(3)> Ash.Changeset.for_create(Arrive.Account.UserToken, :send_otp, %{sent_to: "1112223333"}) |> Ash.create!()
** (Ash.Error.Forbidden)
Bread Crumbs:
> Error returned from: Arrive.Account.UserToken.send_otp

Forbidden Error

* forbidden:

Arrive.Account.UserToken.send_otp


No policy conditions applied to this request.
For safety, at least one policy must apply to all requests.

unknown actor
ZachDaniel
ZachDaniel2mo ago
At lease one policy has to apply. You don't want the changing_attributes check to also be in the condition
thinklt
thinkltOP2mo ago
Thank you. Inserted 1 record now with this policy.
policy action_type(:create) do
forbid_if changing_attributes(status: [to: :locked])
authorize_if always()
end
policy action_type(:create) do
forbid_if changing_attributes(status: [to: :locked])
authorize_if always()
end
Doing the same action again should do an upsert based as defined in the action. But I am unable to persist an increment of the number of times the SMS was sent.
actions do
create :send_otp do
accept [:sent_to]
change set_attribute(:context, :sms)
change set_attribute(:num_attempts, 0)
change set_attribute(:status, :active)
change set_attribute(:sent_at, &DateTime.utc_now/0)
change set_attribute(:token, &__MODULE__.generate_token/0)
change &increment_sent_count/2
change &lock_on_abuse/2
upsert? true
upsert_identity :sent_to
upsert_fields [:num_sent, :sent_at, :token, :status]
end
end

def increment_sent_count(changeset, _) do
sent_count = (get_data(changeset, :num_sent) || 0) + 1
change_attribute(changeset, :num_sent, (get_data(changeset, :num_sent) || 0) + 1)
end
actions do
create :send_otp do
accept [:sent_to]
change set_attribute(:context, :sms)
change set_attribute(:num_attempts, 0)
change set_attribute(:status, :active)
change set_attribute(:sent_at, &DateTime.utc_now/0)
change set_attribute(:token, &__MODULE__.generate_token/0)
change &increment_sent_count/2
change &lock_on_abuse/2
upsert? true
upsert_identity :sent_to
upsert_fields [:num_sent, :sent_at, :token, :status]
end
end

def increment_sent_count(changeset, _) do
sent_count = (get_data(changeset, :num_sent) || 0) + 1
change_attribute(changeset, :num_sent, (get_data(changeset, :num_sent) || 0) + 1)
end
The sent_count is always shows 1 upon inspection. Inspecting changeset also doesn't show the count incremented. It did not insert another record in the db, so it must be doing the upsert. But Changeset inspection shows action is still :create Sorry, if these questions seem too basic. This is my very first attempt at trying to learn Ash
ZachDaniel
ZachDaniel2mo ago
You'll need to use atomic_update because on an upsert operation you don't know what is in the database change atomic_update(:num_sent, expr((^atomic_ref(:num_sent) || 0) + 1)) something like that
thinklt
thinkltOP2mo ago
I was looking at it but atomic sounded more like to manage concurrency. Would an upsert always have the action in the changeset as :create? How do we find things like atomic_ref? Couldn’t locate it anywhere in the docs. Is it by looking at code only I can know those or did I miss it anywhere in the docs? Tried that. A different error. Seems like it won't pick up the required attribute with the "change atomic_update"
** (Ash.Error.Invalid)
Bread Crumbs:
> Error returned from: Arrive.Account.UserToken.send_otp

Invalid Error

* attribute num_sent is required
** (Ash.Error.Invalid)
Bread Crumbs:
> Error returned from: Arrive.Account.UserToken.send_otp

Invalid Error

* attribute num_sent is required
ZachDaniel
ZachDaniel2mo ago
Ah, yeah so you need both The first one should be 1 and all subsequent should be incremented
create :send_otp do
accept [:sent_to]
change set_attribute(:context, :sms)
change set_attribute(:num_attempts, 1)
change set_attribute(:status, :active)
change set_attribute(:sent_at, &DateTime.utc_now/0)
change set_attribute(:token, &__MODULE__.generate_token/0)
change &increment_sent_count/2
change &lock_on_abuse/2
change atomic_update(:num_attempts, expr(^atomic_ref(:num_attempts) + 1))
upsert? true
upsert_identity :sent_to
upsert_fields [:num_sent, :sent_at, :token, :status]
end
create :send_otp do
accept [:sent_to]
change set_attribute(:context, :sms)
change set_attribute(:num_attempts, 1)
change set_attribute(:status, :active)
change set_attribute(:sent_at, &DateTime.utc_now/0)
change set_attribute(:token, &__MODULE__.generate_token/0)
change &increment_sent_count/2
change &lock_on_abuse/2
change atomic_update(:num_attempts, expr(^atomic_ref(:num_attempts) + 1))
upsert? true
upsert_identity :sent_to
upsert_fields [:num_sent, :sent_at, :token, :status]
end
thinklt
thinkltOP2mo ago
It isn't incrementing the count even with that. num_sent is always 1. There is no select statement in the iex output, so I presume there is no way to know what the count is ahead of time to increment it right?
ZachDaniel
ZachDaniel2mo ago
🤔 it should only increment on upsert. oh remove num_sent from upsert_fields
thinklt
thinkltOP2mo ago
A bit confused because we do want that field to be updated on update right? Anway, still stuck on 1 which was when it was originally inserted. Not updating after that
upsert_identity :sent_to
upsert_fields [:sent_at, :token, :status]
upsert_identity :sent_to
upsert_fields [:sent_at, :token, :status]
ZachDaniel
ZachDaniel2mo ago
🤔 I could swear that I've done exaclty this before. The num_attempts in upsert_fields would set it from the input attribute name by leaving it out it should still use the atomic update from num_attempts what does the actual upsert statement look like? Does it not have the incrementing? Are you sure that you are updating the record etc?
thinklt
thinkltOP2mo ago
Yeah. I see these inserts on the console. Same thing every time I run it
iex(53)> Ash.Changeset.for_create(Arrive.Account.UserToken, :send_otp, %{sent_to: "1112223333"}) |> Ash.create!()
[debug] QUERY OK source="user_tokens" db=1.5ms queue=0.7ms idle=1964.6ms
INSERT INTO "user_tokens" AS u0 ("id","status","context","token","sent_at","inserted_at","updated_at","sent_to","num_attempts","num_sent") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT ("sent_to") DO UPDATE SET "num_attempts" = ($11::bigint + $12::bigint)::bigint, "sent_at" = EXCLUDED."sent_at", "token" = EXCLUDED."token", "status" = EXCLUDED."status" RETURNING "updated_at","inserted_at","sent_at","num_sent","status","token","sent_to","num_attempts","context","id" ["1acdeee5-7ba8-4c94-bfb5-424e1cb6b8cf", "active", "sms", "97219", ~U[2025-08-20 16:10:16.438543Z], ~U[2025-08-20 16:10:16.438972Z], ~U[2025-08-20 16:10:16.438972Z], "1112223333", 1, 1, 1, 1]
iex(53)> Ash.Changeset.for_create(Arrive.Account.UserToken, :send_otp, %{sent_to: "1112223333"}) |> Ash.create!()
[debug] QUERY OK source="user_tokens" db=1.5ms queue=0.7ms idle=1964.6ms
INSERT INTO "user_tokens" AS u0 ("id","status","context","token","sent_at","inserted_at","updated_at","sent_to","num_attempts","num_sent") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT ("sent_to") DO UPDATE SET "num_attempts" = ($11::bigint + $12::bigint)::bigint, "sent_at" = EXCLUDED."sent_at", "token" = EXCLUDED."token", "status" = EXCLUDED."status" RETURNING "updated_at","inserted_at","sent_at","num_sent","status","token","sent_to","num_attempts","context","id" ["1acdeee5-7ba8-4c94-bfb5-424e1cb6b8cf", "active", "sms", "97219", ~U[2025-08-20 16:10:16.438543Z], ~U[2025-08-20 16:10:16.438972Z], ~U[2025-08-20 16:10:16.438972Z], "1112223333", 1, 1, 1, 1]
ZachDaniel
ZachDaniel2mo ago
DO UPDATE SET "num_attempts" = ($11::bigint + $12::bigint)::bigint, that looks like its upserting to me? oh its always 1 😆
thinklt
thinkltOP2mo ago
num_sent is the one always 1
ZachDaniel
ZachDaniel2mo ago
Oh
create :send_otp do
accept [:sent_to]
change set_attribute(:context, :sms)
change set_attribute(:num_attempts, 1)
change set_attribute(:status, :active)
change set_attribute(:sent_at, &DateTime.utc_now/0)
change set_attribute(:token, &__MODULE__.generate_token/0)
change &increment_sent_count/2
change &lock_on_abuse/2
change atomic_update(:num_sent, expr(^atomic_ref(:num_sent) + 1))
upsert? true
upsert_identity :sent_to
upsert_fields [:sent_at, :token, :status]
end
create :send_otp do
accept [:sent_to]
change set_attribute(:context, :sms)
change set_attribute(:num_attempts, 1)
change set_attribute(:status, :active)
change set_attribute(:sent_at, &DateTime.utc_now/0)
change set_attribute(:token, &__MODULE__.generate_token/0)
change &increment_sent_count/2
change &lock_on_abuse/2
change atomic_update(:num_sent, expr(^atomic_ref(:num_sent) + 1))
upsert? true
upsert_identity :sent_to
upsert_fields [:sent_at, :token, :status]
end
I just gave you the wrong example I thought it was num_attempts
thinklt
thinkltOP2mo ago
Interestingly, it did bump it up to 2 now but running again wouldn't change it. Probably it picked up during macro expansion and wouldn't change it again?
iex(63)> Ash.Changeset.for_create(Arrive.Account.UserToken, :send_otp, %{sent_to: "1112223333"}) |> Ash.create!()
[debug] QUERY OK source="user_tokens" db=1.0ms idle=1507.0ms
INSERT INTO "user_tokens" AS u0 ("id","status","context","token","sent_at","inserted_at","updated_at","sent_to","num_attempts","num_sent") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT ("sent_to") DO UPDATE SET "num_sent" = ($11::bigint + $12::bigint)::bigint, "sent_at" = EXCLUDED."sent_at", "token" = EXCLUDED."token", "status" = EXCLUDED."status" RETURNING "updated_at","inserted_at","sent_at","num_sent","status","token","sent_to","num_attempts","context","id" ["5850dd2c-9d77-4fb9-90bb-a4a8b57324ed", "active", "sms", "67038", ~U[2025-08-20 20:33:51.824407Z], ~U[2025-08-20 20:33:51.826163Z], ~U[2025-08-20 20:33:51.826163Z], "1112223333", 1, 1, 1, 1]

%Arrive.Account.UserToken{
id: "287b21b1-4c0c-4868-8180-58fa0c8a1aca",
context: "sms",
num_attempts: 2,
sent_to: "1112223333",
token: "77328",
status: "active",
num_sent: 2,
sent_at: ~U[2025-08-20 20:34:13.173682Z],
inserted_at: ~U[2025-08-20 10:55:13.108864Z],
updated_at: ~U[2025-08-20 10:55:13.108864Z],
__meta__: #Ecto.Schema.Metadata<:loaded, "user_tokens">
}
iex(63)> Ash.Changeset.for_create(Arrive.Account.UserToken, :send_otp, %{sent_to: "1112223333"}) |> Ash.create!()
[debug] QUERY OK source="user_tokens" db=1.0ms idle=1507.0ms
INSERT INTO "user_tokens" AS u0 ("id","status","context","token","sent_at","inserted_at","updated_at","sent_to","num_attempts","num_sent") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT ("sent_to") DO UPDATE SET "num_sent" = ($11::bigint + $12::bigint)::bigint, "sent_at" = EXCLUDED."sent_at", "token" = EXCLUDED."token", "status" = EXCLUDED."status" RETURNING "updated_at","inserted_at","sent_at","num_sent","status","token","sent_to","num_attempts","context","id" ["5850dd2c-9d77-4fb9-90bb-a4a8b57324ed", "active", "sms", "67038", ~U[2025-08-20 20:33:51.824407Z], ~U[2025-08-20 20:33:51.826163Z], ~U[2025-08-20 20:33:51.826163Z], "1112223333", 1, 1, 1, 1]

%Arrive.Account.UserToken{
id: "287b21b1-4c0c-4868-8180-58fa0c8a1aca",
context: "sms",
num_attempts: 2,
sent_to: "1112223333",
token: "77328",
status: "active",
num_sent: 2,
sent_at: ~U[2025-08-20 20:34:13.173682Z],
inserted_at: ~U[2025-08-20 10:55:13.108864Z],
updated_at: ~U[2025-08-20 10:55:13.108864Z],
__meta__: #Ecto.Schema.Metadata<:loaded, "user_tokens">
}
ZachDaniel
ZachDaniel2mo ago
Put the atomic update before the set_attribute for num attempts
thinklt
thinkltOP2mo ago
Wow! That did it finally. Now I have to understand what is going on Why atomic_update here? What is atomic_ref used for and when should we just it versus using the attribute directly? Why did the order of the set_attributes matter?
ZachDaniel
ZachDaniel2mo ago
There should be some information on atomics in the update action guide Maybe not perfect but it will be a start and maybe we can use this to enhance that guide 🙂
thinklt
thinkltOP2mo ago
I went through that but I couldn't quite grok the reason why one should be used over the other I understood the concurrency part and atomic in the function name is aptly named so, but not sure why that would apply in this case
ZachDaniel
ZachDaniel2mo ago
For create actions atomic updates only apply in an upsert case and are used when conflicts exist

Did you find this page helpful?