PhoenixKit.Users.Roles (phoenix_kit v3.0.0)

Copy Markdown View Source

API for managing user roles in PhoenixKit.

This module provides functions for assigning, removing, and querying user roles. It works with the role system to provide authorization capabilities.

Role Management

  • Assign and remove roles from users
  • Query users by role
  • Check user permissions
  • Bulk role operations

Who may change roles

Every entry point that gives a role, takes one away or deletes one accepts an :actor (a %User{} or a %Scope{}; omitted or :system = unrestricted, for seeding and system callers). With a real actor the rule is can_manage_role?/2: an Owner may do anything; anyone else only for a role that is neither Owner nor Admin and whose every permission key they hold themselves (a role holding "*" only by a holder of "*"). Nobody but an Owner changes their own roles ({:error, :cannot_modify_own_roles}).

System Roles

PhoenixKit includes three built-in system roles:

  • Owner: System owner with full access (assigned automatically to first user)
  • Admin: Administrator with elevated privileges
  • User: Standard user with basic access (default for new users)

Examples

# Check if user has a role
iex> user_has_role?(user, "Admin")
true

# Get all user roles
iex> get_user_roles(user)
["Admin", "User"]

# Assign a role to user
iex> assign_role(user, "Admin")
{:ok, %RoleAssignment{}}

# Get all users with a specific role
iex> users_with_role("Admin")
[%User{}, %User{}]

Summary

Functions

Assigns roles to existing users who don't have any PhoenixKit roles.

Checks if user can be deactivated safely.

Whether actor may give role to someone, take it away from someone, delete it, or make it the new-user default.

Counts active users with Owner role.

Role counts for the admin list's summary cards: %{total: n, system: n, custom: n}.

Counts users with a specific role.

Creates a new role.

The role a new self-registered account gets: the new_user_default_role setting when it names an existing role other than Owner and Admin, else "User". Owner is reserved for the first user, and Admin would hand every self-registered account the admin panel, so a stored value naming either is never used.

Deletes a role safely.

Safely demotes a user from Admin or Owner role with protection.

Safely assigns Owner role to first user using database transaction.

Gets only custom (non-system) roles.

Gets comprehensive user statistics including activity and confirmation status.

Gets a role by its name.

Gets a single role by its UUID.

get_user_role_records/1 for many users at once: %{user_uuid => [role]}, each list in role order. Users holding no role are absent.

Gets role statistics for dashboard display.

Gets the roles a user holds as %{uuid, name} maps, in role order (position, then name).

Gets all active roles for a user.

Lists all roles.

One page of roles for the admin list, system roles first, then by name.

The names of the roles actor may manage (can_manage_role?/2), in role order. One permissions query for all roles — for a UI that disables the rest.

Moves one role one step :up or :down in role order. A move past either end is a no-op.

Removes a role from a user by deleting the assignment.

Sets the role order (position) to the order of ordered_uuids; any role not listed keeps its relative order after them.

Safely removes role with Owner protection.

Synchronizes user roles with a given list of role names.

Updates a role.

Checks if a user has a specific role.

Checks if a user has an "Admin" role.

Checks if a user has an "Owner" role.

Gets all users who have a specific role.

Functions

assign_role(user, role_name, assigned_by \\ nil, opts \\ [])

Assigns a role to a user.

Security

The Owner role cannot be assigned through this function to maintain system security. Only one Owner is automatically assigned during initialization.

Parameters

  • user: The user to assign the role to
  • role_name: The name of the role to assign
  • assigned_by (optional): The user who is assigning the role
  • opts: :broadcast (default true); :actor — the acting %User{} or %Scope{}, checked against can_manage_role?/2 and the own-roles rule ({:error, :not_permitted} / {:error, :cannot_modify_own_roles}). Omitted = a system caller, unchecked.

Examples

iex> assign_role(user, "Admin")
{:ok, %RoleAssignment{}}

iex> assign_role(user, "Admin", assigned_by_user)
{:ok, %RoleAssignment{}}

iex> assign_role(user, "Owner")
{:error, :owner_role_protected}

iex> assign_role(user, "NonexistentRole")
{:error, :role_not_found}

assign_roles_to_existing_users(opts \\ [])

Assigns roles to existing users who don't have any PhoenixKit roles.

This is useful for migration scenarios where PhoenixKit is installed into an existing application with users.

Parameters

  • opts: Options for role assignment
    • :make_first_owner (default: true) - Make first user without roles an Owner

Returns

  • {:ok, stats} with assignment statistics
  • {:error, reason} on failure

Examples

iex> assign_roles_to_existing_users()
{:ok, %{assigned_owner: 1, assigned_users: 5, total_processed: 6}}

can_deactivate_user?(user)

Checks if user can be deactivated safely.

Prevents deactivation of last active Owner.

Parameters

  • user: User to check for deactivation

Examples

iex> can_deactivate_user?(last_owner)
{:error, :cannot_deactivate_last_owner}

iex> can_deactivate_user?(regular_user)
:ok

can_manage_role?(actor, role_name)

@spec can_manage_role?(
  PhoenixKit.Users.Auth.User.t() | PhoenixKit.Users.Auth.Scope.t() | :system,
  PhoenixKit.Users.Role.t() | String.t()
) :: boolean()

Whether actor may give role to someone, take it away from someone, delete it, or make it the new-user default.

actor is a %User{}, a %Scope{} or :system (always true). An Owner (by their real, held roles) may manage any role. Anyone else may manage a role only when it is neither Owner nor Admin and they hold every permission key the role holds — checked against their effective permissions (the active role, when they act as one), so a role holding "*" needs "*". role is a %Role{} or a role name; an unknown name answers false.

count_active_owners()

Counts active users with Owner role.

Critical security function - ensures we never have zero owners.

Examples

iex> count_active_owners()
1

count_roles()

Role counts for the admin list's summary cards: %{total: n, system: n, custom: n}.

count_users_with_role(role_name)

Counts users with a specific role.

Parameters

  • role_name: The name of the role to count

Examples

iex> count_users_with_role("Admin")
3

create_role(attrs \\ %{})

Creates a new role.

Parameters

  • attrs: Attributes for the new role

Examples

iex> create_role(%{name: "Manager", description: "Department manager"})
{:ok, %Role{}}

iex> create_role(%{name: ""})
{:error, %Ecto.Changeset{}}

default_role_for_new_users()

@spec default_role_for_new_users() :: String.t()

The role a new self-registered account gets: the new_user_default_role setting when it names an existing role other than Owner and Admin, else "User". Owner is reserved for the first user, and Admin would hand every self-registered account the admin panel, so a stored value naming either is never used.

delete_role(role, opts \\ [])

Deletes a role safely.

Prevents deletion of system roles and roles currently assigned to users.

Parameters

  • role: The role to delete
  • opts: :actor — the acting %User{} or %Scope{}. Only an actor who holds every key of the role may delete it (can_manage_role?/2; an Owner always): {:error, :not_permitted} otherwise. Omitted = a system caller.

Examples

iex> delete_role(custom_role)
{:ok, %Role{}}

iex> delete_role(system_role)
{:error, :system_role_protected}

iex> delete_role(role_with_users)
{:error, :role_in_use}

demote_to_user(user, opts \\ [])

Safely demotes a user from Admin or Owner role with protection.

Prevents demotion of last Owner to maintain system security.

Parameters

  • user: The user to demote
  • opts: :actor — as for remove_role/3: only an Owner actor may take Owner or Admin away. Omitted = a system caller, unchecked.

Examples

iex> demote_to_user(admin_user)
{:ok, %RoleAssignment{}}

iex> demote_to_user(last_owner)
{:error, :cannot_demote_last_owner}

ensure_first_user_is_owner(user)

Safely assigns Owner role to first user using database transaction.

Concurrent registrations are serialized by a row lock on the Owner role row, taken only while no active Owner exists yet — see the transaction body.

Parameters

  • user: The user to potentially make Owner

Returns

  • {:ok, :owner} if user became Owner
  • {:ok, :user} if user became regular User
  • {:error, reason} on failure

Examples

iex> ensure_first_user_is_owner(user)
{:ok, :owner}

get_custom_roles()

Gets only custom (non-system) roles.

Examples

iex> get_custom_roles()
[%Role{name: "Manager"}, %Role{name: "Editor"}]

get_extended_stats()

Gets comprehensive user statistics including activity and confirmation status.

Examples

iex> get_extended_stats()
%{
  total_users: 10,
  owner_count: 1,
  admin_count: 2,
  user_count: 7,
  active_users: 8,
  inactive_users: 2,
  confirmed_users: 9,
  pending_users: 1
}

get_role_by_name(name)

Gets a role by its name.

Parameters

  • name: The name of the role

Examples

iex> get_role_by_name("Admin")
%Role{name: "Admin"}

iex> get_role_by_name("NonexistentRole")
nil

get_role_by_uuid(uuid)

Gets a single role by its UUID.

Returns nil if no role is found.

Parameters

  • uuid: The UUID of the role to find

Examples

iex> get_role_by_uuid("01234567-89ab-cdef-0123-456789abcdef")
%Role{uuid: "01234567-89ab-cdef-0123-456789abcdef", name: "Editor"}

iex> get_role_by_uuid("nonexistent-uuid")
nil

get_role_records_for_users(user_uuids)

@spec get_role_records_for_users([String.t()]) :: %{
  optional(String.t()) => [%{uuid: String.t(), name: String.t()}]
}

get_user_role_records/1 for many users at once: %{user_uuid => [role]}, each list in role order. Users holding no role are absent.

get_role_stats()

Gets role statistics for dashboard display.

Examples

iex> get_role_stats()
%{
  total_users: 10,
  owner_count: 1,
  admin_count: 2,
  user_count: 7
}

get_user_role_records(user)

@spec get_user_role_records(PhoenixKit.Users.Auth.User.t()) :: [
  %{uuid: String.t(), name: String.t()}
]

Gets the roles a user holds as %{uuid, name} maps, in role order (position, then name).

The same query as get_user_roles/1, keeping the uuid: the active-role resolution in PhoenixKit.Users.ActiveRole identifies roles by uuid, because role names are editable — and relies on this order: the first switchable role here is a session's default.

get_user_roles(user)

Gets all active roles for a user.

Parameters

  • user: The user to get roles for

Examples

iex> get_user_roles(user)
["Admin", "User"]

iex> get_user_roles(user_with_no_roles)
[]

list_roles()

Lists all roles.

Examples

iex> list_roles()
[%Role{}, %Role{}, %Role{}]

list_roles_paginated(opts \\ [])

One page of roles for the admin list, system roles first, then by name.

Options :page (default 1) and :per_page (default 25). Returns %{roles: [...], total_count: n, total_pages: n, page: n} — page is clamped into [1, total_pages] before the offset is applied, so a link past the end (the last custom role on the last page was just deleted) shows the last page rather than an empty one.

manageable_role_names(actor)

@spec manageable_role_names(
  PhoenixKit.Users.Auth.User.t()
  | PhoenixKit.Users.Auth.Scope.t()
  | :system
) ::
  [String.t()]

The names of the roles actor may manage (can_manage_role?/2), in role order. One permissions query for all roles — for a UI that disables the rest.

move_role(role_uuid, direction)

@spec move_role(String.t(), :up | :down) :: :ok

Moves one role one step :up or :down in role order. A move past either end is a no-op.

promote_to_admin(user, assigned_by \\ nil, opts \\ [])

Promotes a user to admin role.

Parameters

  • user: The user to promote
  • assigned_by (optional): The user who is doing the promotion
  • opts: :actor — as for assign_role/4. Admin is a system role, so only an Owner actor may promote ({:error, :not_permitted} otherwise). Omitted = a system caller (seeding, fixtures), unchecked — no web page calls this.

Examples

iex> promote_to_admin(user)
{:ok, %RoleAssignment{}}

remove_role(user, role_name, opts \\ [])

Removes a role from a user by deleting the assignment.

Parameters

  • user: The user to remove the role from
  • role_name: The name of the role to remove
  • opts: :broadcast (default true); :actor, as for assign_role/4

Examples

iex> remove_role(user, "Admin")
{:ok, %RoleAssignment{}}

iex> remove_role(user, "NonexistentRole")
{:error, :assignment_not_found}

reorder_roles(ordered_uuids)

@spec reorder_roles([String.t()]) :: :ok

Sets the role order (position) to the order of ordered_uuids; any role not listed keeps its relative order after them.

The order decides the default role of a new session and how the role switcher lists roles (PhoenixKit.Users.ActiveRole). Broadcasts :role_updated for the roles whose position changed.

safely_remove_role(user, role_name, opts \\ [])

Safely removes role with Owner protection.

Prevents removal of Owner role if it would leave system without owners.

Parameters

  • user: User to remove role from
  • role_name: Name of role to remove
  • opts: :actor — as for remove_role/3. Omitted = a system caller.

Examples

iex> safely_remove_role(owner_user, "Owner")
{:error, :cannot_remove_last_owner}

iex> safely_remove_role(admin_user, "Admin")
{:ok, %RoleAssignment{}}

sync_user_roles(user, role_names, opts \\ [])

Synchronizes user roles with a given list of role names.

This function ensures the user has exactly the specified roles.

Authorization (:actor option)

This is the authorization boundary for role changes — never trust the caller UI to have gated it. Pass actor: %User{} (the acting admin) and:

  • Changes to the system roles (Owner, Admin) are applied only when the actor is an Owner. A non-Owner's attempt to grant Owner/Admin to another account (privilege escalation) or to strip them (the disabled-checkbox form drops Owner from the submitted set, which would otherwise read as a removal) is DROPPED — that role is left exactly as it was.
  • Any other role is changed only when the actor may manage it (can_manage_role?/2: the actor holds every permission key of the role). A change the actor may not make is DROPPED like a system-role change and its role name is returned in :refused, so the caller can say so.
  • A non-Owner actor may not change their own roles at all: the whole call fails with {:error, :cannot_modify_own_roles} when it would change anything.
  • The last Owner can never be removed, regardless of actor, via the same guard as safely_remove_role/2.

Omitting :actor (or actor: :system) skips the actor gate — for seeding/system callers only — but the last-Owner guard still applies.

Parameters

  • user: The user to synchronize roles for
  • role_names: List of role names the user should have
  • opts: :actor — the acting %User{} or %Scope{} (see above)

Returns {:ok, %{assignments: [...], roles_before: [...], roles_after: [...], refused: [...]}} on success — roles_before/roles_after are the role-name lists captured inside the transaction, so callers can audit the exact delta this transaction applied (which can differ from what was submitted when unauthorized changes are dropped or a concurrent edit intervened).

Examples

iex> sync_user_roles(user, ["Admin", "Manager"], actor: current_user)
{:ok, %{assignments: [%RoleAssignment{}], roles_before: ["Manager"], roles_after: ["Admin", "Manager"]}}

update_role(role, attrs)

Updates a role.

Parameters

  • role: The role to update
  • attrs: Attributes to update

Examples

iex> update_role(role, %{description: "Updated description"})
{:ok, %Role{}}

iex> update_role(system_role, %{name: "NewName"})
{:error, %Ecto.Changeset{}}

user_has_role?(user, role_name)

Checks if a user has a specific role.

Parameters

  • user: The user to check
  • role_name: The name of the role to check for

Examples

iex> user_has_role?(user, "Admin")
true

iex> user_has_role?(user, "Owner")
false

user_has_role_admin?(user)

Checks if a user has an "Admin" role.

Parameters

  • user: The user to check

Examples

iex> user_has_role_admin?(user)
true

user_has_role_owner?(user)

Checks if a user has an "Owner" role.

Parameters

  • user: The user to check

Examples

iex> user_has_role_owner?(user)
true

users_with_role(role_name)

Gets all users who have a specific role.

Parameters

  • role_name: The name of the role to search for

Examples

iex> users_with_role("Admin")
[%User{}, %User{}]

iex> users_with_role("NonexistentRole")
[]