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
endWhat 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
endNote 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
endA 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
| Name | Type | Default | Docs |
|---|---|---|---|
otp_app | atom | The OTP application whose config holds the OpenFeed credentials, read as Application.get_env(otp_app, config_key). Required unless config_provider is set. | |
config_key | atom | :openfeed | The key under otp_app's config holding the OpenFeed settings. |
config_provider | atom | {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_store | atom | {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. | |
scopes | list(any) | [:banking, :energy] | Scopes to request at authorization. Accepts OpenFeed.Scopes shorthands or literal scope strings. |
grant_management? | boolean | false | Adds 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_accept | list(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. |