fix: honour before_action?: true on validations of event-tracked actions - #97
Merged
Torkan merged 1 commit intoSep 12, 2026
Conversation
`WrapActions` rewrites every action-level `%Ash.Resource.Validation{}` into a
`%Ash.Resource.Change{}` wrapping `ReplayValidationWrapper`, which calls
`validate/3` inline from its `change/3`. `%Ash.Resource.Change{}` has no
`before_action?` field, and `Ash.Changeset.validate/5` — the only code that
honours the flag — never sees a validation, so `before_action?: true` was
silently dropped and the validation kept running at `for_action` time.
Two things break as a result: anything the validation needs from an earlier
`before_action` hook is still unset, so it silently validates against the wrong
data; and the validation runs before authorization, so a read done with
`authorize?: false` answers for any actor who can merely attempt the action, and
a `Forbidden` becomes an `Invalid`. Whether the flag works at all depends on
whether the action is in the resource's `only_actions`/`ignore_actions`, which
makes it easy to miss.
The wrapper already receives the whole validation struct in `opts[:validation]`,
so it can register a `before_action` hook when the struct asks for one.
`only_when_valid?` is re-checked inside the hook, mirroring
`Ash.Changeset.validate/5`; relying on the generated change's field alone would
evaluate it before the hook runs.
Replay keeps running the validation inline. A hook registered there by a
validation outside `allowed_change_modules` is discarded by the existing
hook-restoring branch, which would make the validation silently never run.
Tested with two identical create actions on `Accounts.Org` — one tracked, one in
`ignore_actions` — that both delay a validation reading an attribute a
`before_action` hook sets. Before this change the tracked one failed and the
ignored one passed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
On a resource with the
AshEvents.Eventsextension,validate SomeValidation, before_action?: trueinside a tracked action is silently ignored — the validation still runs atfor_actiontime.WrapActionsrewrites every action-level%Ash.Resource.Validation{}into a%Ash.Resource.Change{}wrappingReplayValidationWrapper:https://github.com/ash-project/ash_events/blob/main/lib/events/transformers/wrap_actions.ex#L116-L135
It carries
on,only_when_valid?,description,whereandmessageacross, but%Ash.Resource.Change{}has nobefore_action?field, so the flag cannot survive the rewrite. The wrapper then callsvalidate/3inline from itschange/3.Ash.Changeset.validate/5is the only code that honours the flag, and it only ever sees a change — so the flag is dropped without a warning.before_action?currently appears nowhere in the codebase.Why it matters
Two things break:
before_actionhook is still unset, so it silently validates against the wrong data. We hit this on a resource where aPrepare*change derivescampaign_idin a hook: the validation readniland resolved a global default limit instead of the record's, letting bad input through to fail later in a background job.before_action?: trueis the documented way to run a validation after authorization. A validation that reads data withauthorize?: falseand then runs before authorization answers for any actor who can merely attempt the action, and turns aForbiddeninto anInvalid.Because only tracked actions are rewritten, whether the flag works depends on whether the action is in the resource's
only_actions/ignore_actions— which makes it very easy to miss. Two otherwise identical actions behave differently based only on event tracking.Fix
The wrapper already receives the whole
%Ash.Resource.Validation{}inopts[:validation], so it can register abefore_actionhook when the struct asks for one.only_when_valid?is re-checked inside the hook, mirroringAsh.Changeset.validate/5. Relying on the generated change's field alone would evaluate it before the hook runs.allowed_change_modulesis discarded by the existing hook-restoring branch, which would make the validation silently never run — so replay semantics are unchanged.Tests
Two identical create actions on
Accounts.Org— one tracked, one inignore_actions— both delay a validation that reads an attribute abefore_actionhook sets. They must behave identically.Before this change the tracked action failed and the ignored one passed; after, both pass.
mix testis otherwise unchanged: the one pre-existing failure (replay events on event log missing clear function throws RuntimeError) fails onmaintoo, unrelated to this change.Notes / out of scope
whereis still evaluated when the change runs rather than inside the hook, because the transformer puts it on the generated%Ash.Resource.Change{}.Ash.Changesetevaluates a validation'swhereinside the hook. Left alone to keep this diff to the ordering bug.always_atomic?: falseon the generated change, dropping the validation's own value. Also left alone.