Restrict access to attributes and use one action to set a timestamp

I have the following Task resource which is used in a To-Do-List application. I want to make sure that one can not populate the started_at and ended_at fields with :create or :update. I don’t want the user to manipulate those values.

  • How can I do that?
  • How can I add an action :start which populates the current server time to started_at?
defmodule App.Desk.Task do
  use Ash.Resource, data_layer: Ash.DataLayer.Ets

  attributes do
    uuid_primary_key :id
    attribute :name, :string
    attribute :started_at, :utc_datetime
    attribute :ended_at, :utc_datetime

  actions do
    defaults [:create, :read, :update, :destroy]

  code_interface do
    define_for App.Desk
    define :create
    define :read
    define :by_id, get_by: [:id], action: :read
    define :by_name, get_by: [:name], action: :read
    define :update
    define :destroy
1 Like

Restricting Writes

There are many ways you can do this, that you would use in different scenarios.

If there is no circumstance by which an attribute can be set by the external world (i.e its only set by an action’s internals), use writable?: false on the attribute. You can write to it from within an action using Ash.Changeset.force_change_attribute(changeset, :attribute, value).

You can also ensure that those attributes aren’t “accepted” by any action. Do this if the action might be writable by one action, but isn’t writable generally.

actions do
  default_accept [:foo, :bar, :baz]

  update :set_timestamp do
     accept [:timestamp]

If there are specific conditions that would prevent changing an attribute, it can be done with a validation:

# don't allow changing foo and bar together
validate negate(changing(:foo)), where: [changing(:bar)]

You can use policies to restrict specific kinds of users or otherwise base the condition on the actor

policy changing(:foo) do
  authorize_if actor_attribute_equals(:admin, true)
  authorize_if some_condition(..)

Setting an attribute in an action

You can use custom changes that use force_change_attribute.

update :start do
  change fn changeset, _ -> 
    Ash.Changeset.force_change_attribute(changeset, :started_at, DateTime.utc_now())

Or you can use the built in that does the same thing

update :start do
  # notice we pass a zero arg function here. This is evaluated at compile time
  # so if I did `DateTime.utc_now()` it would always be a single datetime (the time that we compiled)
  change set_attribute(:started_at, &DateTime.utc_now/0)

Really need to go back through these answers and put them in the docs :laughing: