Ash resource extension for storing OpenFeed disclosure grants.

Usage

defmodule MyApp.OpenFeed.Grant do
  use Ash.Resource,
    domain: MyApp.OpenFeed,
    data_layer: AshPostgres.DataLayer,
    extensions: [AshOpenFeed.Grant]

  openfeed do
    otp_app :my_app
    key_store AshOpenFeed.KeyStore.Ash
    grant_management? true
  end

  postgres do
    table "openfeed_grants"
    repo MyApp.Repo
  end

  relationships do
    belongs_to :user, MyApp.Accounts.User
  end
end

What gets added

Attributes: grant_id, status, metering_state, revision, access_token, refresh_token, token_type, expires_at, sub, scopes. The token attributes are marked sensitive? so they are redacted from inspect output and error messages.

Actions: read, destroy, upsert_from_tokens, update_tokens, mark_revoked, mark_metering_suspended, mark_metering_active.

An identity unique_grant_id on grant_id.

Everything is added with Ash's add_new_* builders, so anything you define yourself wins. If you need a different shape, declare it and the extension will leave it alone.

Data layers that cannot enforce identities

AshPostgres backs the unique_grant_id identity with a real unique constraint. Ash.DataLayer.Ets and Mnesia cannot, and Ash refuses to compile an identity on those without pre_check_with. Since the identity is injected rather than hand-written, the extension detects this and sets pre_check_with to the resource's domain for you.

The one case it cannot handle is a resource with no domain (domain: nil, for a resource shared across domains) on one of those data layers — there is nothing to pre-check with. Declare the identity yourself and the extension will leave it alone:

identities do
  identity :unique_grant_id, [:grant_id], pre_check_with: MyApp.SomeDomain
end

Note also that a pre-checked identity installs a before_action hook, which makes updates non-atomic. The extension therefore sets require_atomic? false on the update actions only when pre-checking is in play, so AshPostgres users keep atomic updates.

Encrypt your tokens

The token attributes are plaintext columns unless you encrypt them. Because this is your resource, you can:

defmodule MyApp.OpenFeed.Grant do
  use Ash.Resource,
    extensions: [AshOpenFeed.Grant, AshCloak.Resource]

  cloak do
    vault MyApp.Vault
    attributes [:access_token, :refresh_token]
  end
end

A verifier emits a compile-time warning if it cannot see encryption configured on a resource holding real tokens. Set warn_unencrypted?: false in application config to silence it once you have made a deliberate choice.

DSL Documentation

See AshOpenFeed.Grant.openfeed for the full option reference.

openfeed

Configures this resource as the store for OpenFeed disclosure grants.

The extension adds the token attributes, a unique identity on grant_id, and the lifecycle actions. It does not add relationships, policies or encryption — those are yours, which is the point of putting the extension on a resource you own rather than shipping one.

Examples

openfeed do
  otp_app :my_app
  scopes [:banking, :energy]
  grant_management? true
end

Options

NameTypeDefaultDocs
otp_appatomThe OTP application whose config holds the OpenFeed credentials, read as Application.get_env(otp_app, config_key). Required unless config_provider is set.
config_keyatom:openfeedThe key under otp_app's config holding the OpenFeed settings.
config_provideratom | {atom, keyword}A module implementing AshOpenFeed.ConfigProvider, for credentials that cannot come from static config — a registration per tenant, say. The callback receives the grant record, so tenant context is available. Mutually exclusive with otp_app.
key_storeatom | {atom, keyword}Overrides the key store from application config. See OpenFeed.KeyStore. AshOpenFeed.KeyStore.Ash is the only multi-node-safe option that needs no external secret plumbing.
scopeslist(any)[:banking, :energy]Scopes to request at authorization. Accepts OpenFeed.Scopes shorthands or literal scope strings.
grant_management?booleanfalseAdds the openfeed-au:grant:self:query and openfeed-au:grant:self:revoke scopes, so AshOpenFeed.revoke/2 and AshOpenFeed.refresh_status/2 can be used. Revocation is worth having: without it you can only infer that a consumer withdrew consent, from a 403 on the next data call.
extra_acceptlist(atom)[]Attributes of your own that upsert_from_tokens should accept, on top of the token fields. Almost every application needs at least one of these, to attach the grant to a user: openfeed do otp_app :my_app extra_accept [:user_id] end then: AshOpenFeed.complete_authorization(Grant, code, flow, attributes: %{user_id: current_user.id} ) For anything more involved than accepting a few attributes, declare your own upsert_from_tokens action — the extension will not overwrite it.