PhoenixKit.Users.ActiveRole (phoenix_kit v2.23.0)

Copy Markdown View Source

The role a session is currently acting as.

By default a user's access is the union of every role they hold. With the role switcher on (role_switcher_enabled), a user holding two or more switchable roles acts as exactly one of them at a time, and their scope — roles and permissions alike — is narrowed to that role plus every always-on role they hold. An Admin who switches to "Seller" has no admin access at all until they switch back.

Switchable and always-on roles

  • Owner and Admin are always switchable — otherwise narrowing could never take admin access away.
  • User is always on: it applies whichever role is active.
  • Custom roles are switchable unless listed in role_switcher_always_on_roles (comma-separated role uuids).

Where the active role lives

On the session token (phoenix_kit_users_tokens.active_role_uuid, V190), so it is per browser session: Admin on the laptop, Seller on the phone, a role of its own for an impersonation, a fresh start for a second multi-session account. NULL means the default role — the first switchable role the user holds in role order (phoenix_kit_user_roles. position: Owner, Admin, then the operator's order) — and nothing is written until the user switches.

It reaches PhoenixKit.Users.Auth.Scope.for_user/1 through the user's virtual field active_role_uuid, which only PhoenixKit.Users.Auth.UserToken.verify_session_token_query/1 fills: every web path starts from a session token, so every scope build narrows without knowing about tokens. A user loaded any other way (by uuid, in a background job, in an admin list) has nil there and acts as their default role — the same rule as a session that never switched, and never wider than a session could be. Never copy the value anywhere else (a session key, an assign): the scope is rebuilt from the token in plugs, every LiveView mount and the role-change refresh.

The stored value is never trusted on its own. resolve/3 only ever returns a role the user holds and that is switchable right now, so the narrowed scope can never exceed the user's real grants. Nothing here writes on read.

The functions taking a config/0 are pure, so the rules are unit-testable without a database.

Summary

Functions

The switcher configuration, read from settings.

The names of the roles in effect for user: the active role plus the always-on roles while narrowed, every held role otherwise.

The roles in effect while acting as active: the active role plus every always-on role held. With no active role, every held role.

Where the switcher is shown: :menu (the account dropdown, default) or :header (a header control from sm up, the account dropdown below it).

Parses the role_switcher_always_on_roles setting: comma-separated role uuids, returned without duplicates. Anything that is not a string is [].

Parses role_switcher_location. Anything unrecognised is :menu.

The role a session acts as, given the roles the user holds (in role order) and the uuid stored on the session. nil means no narrowing: the feature is off, or the user holds fewer than two switchable roles.

The role in effect for each session in sessions, for the sessions lists.

The role uuid the current session stored, carried on the user loaded from that session's token, or nil (no session, or the session never switched).

Makes role_uuid the role the session identified by the raw session token acts as. user is that session's user, as loaded from the token.

Whether role is a mode the user can act as, rather than always on.

The held roles that are switchable, in the order given.

Types

config()

@type config() :: %{enabled?: boolean(), always_on: [String.t()]}

role()

@type role() :: %{uuid: String.t(), name: String.t()}

Functions

config()

@spec config() :: config()

The switcher configuration, read from settings.

Only the enabled flag is read while the feature is off.

effective_role_names(user)

@spec effective_role_names(PhoenixKit.Users.Auth.User.t()) :: [String.t()]

The names of the roles in effect for user: the active role plus the always-on roles while narrowed, every held role otherwise.

The same roles Scope.for_user/1 puts in cached_roles, without loading permissions — for callers that only need names (account labels, the impersonation authority). Like for_user/1, honours the session role only when user was loaded from its session token.

effective_roles(held, arg2, config)

@spec effective_roles([role()], role() | nil, config()) :: [role()]

The roles in effect while acting as active: the active role plus every always-on role held. With no active role, every held role.

location()

@spec location() :: :menu | :header

Where the switcher is shown: :menu (the account dropdown, default) or :header (a header control from sm up, the account dropdown below it).

parse_always_on(value)

@spec parse_always_on(term()) :: [String.t()]

Parses the role_switcher_always_on_roles setting: comma-separated role uuids, returned without duplicates. Anything that is not a string is [].

parse_location(arg1)

@spec parse_location(term()) :: :menu | :header

Parses role_switcher_location. Anything unrecognised is :menu.

resolve(held, stored_uuid, config)

@spec resolve([role()], String.t() | nil, config()) :: role() | nil

The role a session acts as, given the roles the user holds (in role order) and the uuid stored on the session. nil means no narrowing: the feature is off, or the user holds fewer than two switchable roles.

A stored uuid naming a role that is not held, or not switchable, is ignored and the default applies: the first switchable role in role order. So is nil — a session that never switched.

session_role_names(sessions)

@spec session_role_names([map()]) :: %{optional(String.t()) => String.t() | nil}

The role in effect for each session in sessions, for the sessions lists.

Takes maps with :token_uuid, :user_uuid and :active_role_uuid (what PhoenixKit.Users.Sessions selects) and returns %{token_uuid => name} with nil where the session is not narrowed — one roles query for the whole page, then the pure rules per row.

session_role_uuid(user)

@spec session_role_uuid(PhoenixKit.Users.Auth.User.t()) :: String.t() | nil

The role uuid the current session stored, carried on the user loaded from that session's token, or nil (no session, or the session never switched).

switch(user, token, role_uuid)

@spec switch(PhoenixKit.Users.Auth.User.t(), binary(), String.t()) ::
  {:ok, role()} | {:error, :disabled | :not_switchable | :not_found}

Makes role_uuid the role the session identified by the raw session token acts as. user is that session's user, as loaded from the token.

Refused unless the switcher is on and the role is one of the user's switchable roles — and there are at least two of those, or there is nothing to switch between. Switching to the role already in effect writes nothing.

On a change the choice is stored on the session token, logged as session.role_switched, and broadcast through ScopeNotifier, so every open LiveView of this user rebuilds its scope — each from its own token, so only this session actually changes — and leaves pages the new role cannot reach. Other sessions of the same user, and the user's own sessions while someone impersonates them, are untouched.

switchable?(map, config)

@spec switchable?(role(), config()) :: boolean()

Whether role is a mode the user can act as, rather than always on.

switchable_roles(held, config)

@spec switchable_roles([role()], config()) :: [role()]

The held roles that are switchable, in the order given.