Master and tenant users
v0.0.1
Gervaz assumes that two kinds of people use an admin: a master, who works across every tenant, and a tenant user, who works inside one site or organization. One DSL declaration serves both. This page explains how Gervaz tells them apart, and which options change with the role: the Ash action that runs, the relationships that load, what a person sees, and which live updates they receive.
What it is and why
A platform that hosts many sites has operators who see every site and site staff who see their own. The two groups use the same screens but not the same data, and often not the same Ash actions. A master reads through an action that bypasses multitenancy, and a tenant user reads through one that is scoped by the tenant. Gervaz lets one resource declare both, so you do not write two admin surfaces.
How Gervaz tells them apart
Each component computes state.master_user? when it starts. The built-in access module treats a user whose site_id is nil as a master (MishkaGervaz.Helpers.master_user?/1). The user map must have the site_id key, because a map without the key is not a master. Gervaz reads the tenant of a user from the same site_id key.
The master_check option of a domain or a resource is stored in the config, but the built-in access modules do not call it. To use another rule, write an access module that overrides master_user?/1 and name it in the DSL. The table and the form each have one:
defmodule MyApp.Table.Access do
use MishkaGervaz.Table.Web.State.Access
def master_user?(%{role: :admin}), do: true
def master_user?(user), do: super(user)
end
mishka_gervaz do
table do
state do
access MyApp.Table.Access
end
end
endFor a form, use MishkaGervaz.Form.Web.State.Access in the same way.
What changes with the role
The Ash action
Every action option takes an atom, which serves both roles, or a tuple {master_action, tenant_action}. Gervaz picks the element with state.master_user?. If your application has no master and tenant split, use plain atoms. The Mishka Blog domain names both:
actions do
read {:master_read, :read}
get {:master_get, :read}
destroy {:master_destroy, :destroy}
endThe preloads
A preload has three tiers: always, master and tenant. They let the two roles expose different relationships under one alias. The tags table loads master_language_variants for a master and language_variants for a tenant user, and a column reads language_variants in both cases:
preload do
always [:site, :version_count, :post_count]
tenant [:language_variants]
master master_language_variants: :language_variants
endGervaz keeps an alias map per role, and MishkaGervaz.Resource.Info.Table.preload_aliases/2 returns it.
What a person can see
restricted true means master only. It is an option of filters, filter groups, row and bulk actions, form fields, groups and submit buttons, headers, footers, notices and the archive toggle. The tags table shows its site filter and its site field to masters only, and shows the field only when a tag is created:
filter :site_id, :relation do
mode :search_multi
display_field :name
search_field :name
restricted true
end
field :site_id, :relation do
restricted true
required true
show_on :create
mode :search
endA whole form mode can be master only with access :create, restricted: true in the form's source, and source restricted true closes the entire form to everyone but masters.
The live updates
A table subscribes to "<prefix>:created", "<prefix>:updated" and "<prefix>:destroyed". A user whose site_id is set also subscribes to "<prefix>:<site_id>:created", :updated and :destroyed. The subscriptions follow the site_id of the user and not state.master_user?. The realtime block's visible function runs only for users who are not masters, and a master always passes. It replaces the default check, which compares the tenant attribute of the record with the user's, so it must keep other tenants out itself.
realtime do
prefix "tag"
visible fn record, user ->
is_nil(user.site_id) or record.site_id == user.site_id
end
endThe code you write
Functions in the DSL read state.master_user? and state.current_user to branch. This option list of the tags form reads the site that a master picked in the form, and the site of a tenant user:
load fn query, state ->
site_id =
if state.master_user? do
Map.get(state.field_values, :site_id)
else
Map.get(state.current_user, :site_id)
end
Ash.Query.filter_input(query, %{site_id: site_id})
end