MishkaCmsCore.Accounts.AuthGate (Mishka CMS Core v0.0.1)

Copy Markdown View Source

The ways into an account a site's admin — or a master, for any site or every site — can close: :sign_in, :register, :reset (forgot password), :password (signing in or registering with a password) and :magic_link (sign-in links). Set on the settings page's Security tab, the last two as switches in its "Sign-in methods" table; see definitions/0.

Who still gets in

A master (site_id nil) always. Anyone else passes a closed scope only by its own "still allowed" lists: an address on auth.allowed_emails, or a permission on auth.allowed_permissions (* by default) that the account holds. The installation's switch closes every site, with the installation's lists; a site's switch closes that site, with the site's lists. Both are asked, so either can refuse.

Where it is asked

At the account actions, so the dashboard, the sign-in pages and the API all hear the same answer, refused with MishkaCmsCore.Accounts.AuthGate.Closed:

  • registering and completing a reset — policies on MishkaCmsCore.Accounts.User before the AshAuthentication bypass (MishkaCmsCore.Accounts.Checks.AuthGateOpen), whose error_message is refused/3, the way AshAuthentication's own guide asks for policies on its actions;
  • signing in by password, sign-in link or sign-in token — after the password or the token is checked (prepare/3), so only someone who knew it learns that sign-in is closed, and they read that rather than "Incorrect email or password"; the token AshAuthentication had just made for them is revoked;
  • asking for a reset or a sign-in link — never refused, the answer is the same for every address, but the email goes only to an account the gate lets through.

Who is already signed in

Closing sign-in leaves them signed in, unless the same scope also turns on auth.sign_in.sign_out: then whoever the gate would not let sign in now is signed out everywhere (signs_out?/2, sign_out/2) at their next request — a page (MishkaCmsCore.Runtime.Plugs.SiteLookupPlug.confirm_tenant/2), an API call or a GraphQL socket (MishkaCmsCore.Runtime.Plugs.ApiTenantLookupPlug.confirm_bearer_tenant/2) — and a page already open reloads on its next navigation or click, or as soon as the switch is saved (MishkaCmsCore.Runtime.SiteAvailability).

The site being offline

Another question, asked on its own (MishkaCmsCore.Runtime.SiteAvailability): a visitor gets in only when neither refuses. Taking a site offline signs nobody out.

Plugins

"auth:gate:check" hears every decision before it is final: %{action:, site_id:, user:, email:, allowed?:}, and the allowed? it hands back is the answer — a captcha or an IP list can close a way the settings left open, or open one they closed. While its plugins cannot run, its cannot_run: answer closes every way in: what they would close cannot be told. "auth:gate:refused" hears each refusal (return: false), and "auth:gate:signed_out" each account sign_out/2 signs out, with %{site_id:, user_id:, email:} (return: false).

Summary

Types

Values about to be saved, by scope (nil for Global) and key, read before the saved ones.

Functions

The ways in a gate can close.

Whether who — %{user: account} once the account is known, %{email: address} before — may take action on site_id (nil for a master's sign-in), with pending read over the saved settings.

:ok, or the refusal for the first of actions the gate keeps who out of — "auth:gate:refused" told about it.

Whether action is closed on site_id at all — by the installation or the site — whoever asks. What a page says before anyone is known.

The Security tab: what can close, and who still gets in.

What to tell whoever was refused, in their language.

Whether saving pending would shut user out of site_id when they are not shut out now: sign them out, or leave them unable to sign in again. The settings page refuses such a save, so an admin cannot close the way in behind themselves; what someone else's switch already does to them is not theirs to undo, and is not asked.

The read preparation that refuses a sign-in through a closed gate, once the account is found and its password or token checked: prepare &AuthGate.prepare(&1, &2, [:sign_in]).

The refusal anywhere inside reason, or nil: an error or error class, a form's raw_errors, or the JSON:API errors document written for one — the shape of MishkaCmsCore.Accounts.AuthRateLimit.limited/1.

A gate policy's error_message: the refusal for action, told to "auth:gate:refused" first.

Signs user out on every device with AshAuthentication's log_out_everywhere, which revokes every token it holds for them, and cuts their open pages.

Whether user, signed in on site_id, is to be signed out: a scope that closed sign-in there also signs out who is in, and the gate would not let user sign in now. Read from the settings cache and the permissions user carries, so a cached viewer (MishkaCmsCore.Runtime.States.ViewerState) is asked as an account is. pending is read over the saved settings.

Who an action's changeset is for: the address being registered, or the account being reset.

Types

action()

@type action() :: :sign_in | :register | :reset | :password | :magic_link

pending()

@type pending() :: %{optional({String.t() | nil, String.t()}) => term()}

Values about to be saved, by scope (nil for Global) and key, read before the saved ones.

who()

@type who() :: %{optional(:user) => map() | nil, optional(:email) => String.t() | nil}

Functions

actions()

@spec actions() :: [action()]

The ways in a gate can close.

allowed?(action, site_id, who, pending \\ %{})

@spec allowed?(action(), String.t() | nil, who(), pending()) :: boolean()

Whether who — %{user: account} once the account is known, %{email: address} before — may take action on site_id (nil for a master's sign-in), with pending read over the saved settings.

check(actions, site_id, who)

@spec check([action()], String.t() | nil, who()) ::
  :ok | {:error, MishkaCmsCore.Accounts.AuthGate.Closed.t()}

:ok, or the refusal for the first of actions the gate keeps who out of — "auth:gate:refused" told about it.

closed?(action, site_id)

@spec closed?(action(), String.t() | nil) :: boolean()

Whether action is closed on site_id at all — by the installation or the site — whoever asks. What a page says before anyone is known.

definitions()

@spec definitions() :: [MishkaCmsCore.Settings.definition()]

The Security tab: what can close, and who still gets in.

describe(atom)

@spec describe(action()) :: %{title: String.t(), text: String.t()}

What to tell whoever was refused, in their language.

locks_out?(site_id, user, pending)

@spec locks_out?(String.t() | nil, map(), pending()) :: boolean()

Whether saving pending would shut user out of site_id when they are not shut out now: sign them out, or leave them unable to sign in again. The settings page refuses such a save, so an admin cannot close the way in behind themselves; what someone else's switch already does to them is not theirs to undo, and is not asked.

prepare(query, context, actions)

@spec prepare(Ash.Query.t(), map(), [action()]) :: Ash.Query.t()

The read preparation that refuses a sign-in through a closed gate, once the account is found and its password or token checked: prepare &AuthGate.prepare(&1, &2, [:sign_in]).

refusal(errors)

@spec refusal(term()) :: %{action: action()} | nil

The refusal anywhere inside reason, or nil: an error or error class, a form's raw_errors, or the JSON:API errors document written for one — the shape of MishkaCmsCore.Accounts.AuthRateLimit.limited/1.

refused(subject, context, action)

A gate policy's error_message: the refusal for action, told to "auth:gate:refused" first.

error_message &MishkaCmsCore.Accounts.AuthGate.refused(&1, &2, :register)

sign_out(user, site_id)

@spec sign_out(Ash.Resource.Record.t(), String.t() | nil) :: :ok

Signs user out on every device with AshAuthentication's log_out_everywhere, which revokes every token it holds for them, and cuts their open pages.

signs_out?(site_id, user, pending \\ %{})

@spec signs_out?(String.t() | nil, map() | nil, pending()) :: boolean()

Whether user, signed in on site_id, is to be signed out: a scope that closed sign-in there also signs out who is in, and the gate would not let user sign in now. Read from the settings cache and the permissions user carries, so a cached viewer (MishkaCmsCore.Runtime.States.ViewerState) is asked as an account is. pending is read over the saved settings.

who(changeset)

@spec who(term()) :: who()

Who an action's changeset is for: the address being registered, or the account being reset.