Malachi.Auth.UserStore (malachi v0.22.7)

View Source

The cluster's user store: a thin facade over the ra-replicated user cluster (Malachi.Auth.UserServer). Replaces the old node-local Mnesia store: users now replicate across the cluster, so a user created on one node exists on every node.

Stateless: the cluster name is fixed and the local server id is {name, node()}. Malachi.Application starts the ra cluster and a reconciler at boot; this module just routes CRUD to it: writes go through the log (consensus); reads come from the local replica (:ra.local_query, fast and eventually consistent). The public API (insert_user/set_role/delete_user/update_password/get_user/ get_principal/list_users/export_users/import_users) keeps Malachi.Auth and its callers agnostic to the backend.

The console role arrived at machine version 5 (Malachi.Auth.UserRegistry). A write that carries no role uses the older command shape, so creating or importing a user without a role keeps working on a cluster still mid upgrade; only a write that carries a role can meet the version refusal.

Summary

Functions

The user cluster's ra cluster name.

Deletes a user (idempotent). Returns :ok.

Exports all users (no hashes) as JSON-serializable maps. Returns {:ok, list}.

Reads a user's permissions and console role (no hash) from the local replica as %{username, permissions, role}, or {:error, :user_not_found}.

Reads a user as {username, hash, permissions} from the local replica, or {:error, :user_not_found}.

The registry command an import of users (the maps import_users/1 takes) is sent as, and how many entries were skipped before it for an unknown role. An entry with an unknown role is dropped here, before the command is chosen, so it cannot push an import that is otherwise free of roles onto the version 5 command, which a cluster still below 5 refuses whole. With no role left, the command is the version 0 {:import_users, entries}.

Imports users from a list of maps (each :username, :password_hash, :permissions and an optional :role; string keys also accepted, so an export_users/0 round trips). Existing and malformed entries, including an unknown role, are skipped. When no entry carries a role the older command shape is used, so an import without roles works on a cluster still below version 5. Returns {:ok, %{imported: n, skipped: n}}, a version refusal, or {:error, reason}.

Inserts a new user, holding console role when it is not nil. Returns :ok, {:error, :user_exists}, {:error, :invalid_role}, a version refusal (see Malachi.Cluster.MachineVersion.refusal?/1) for a role on a cluster below version 5, or {:error, reason} when the store is unreachable.

Lists all users (no hashes) as %{username, permissions, role} maps.

Sets a user's console role, or removes it with nil. Returns :ok, {:error, :user_not_found}, {:error, :invalid_role}, a version refusal, or {:error, reason} when the store is unreachable.

Updates a user's password hash. Returns :ok or {:error, :user_not_found}.

Functions

cluster_name()

@spec cluster_name() :: atom()

The user cluster's ra cluster name.

delete_user(username)

@spec delete_user(String.t()) :: :ok | {:error, atom()}

Deletes a user (idempotent). Returns :ok.

export_users()

@spec export_users() :: {:ok, [map()]} | {:error, atom()}

Exports all users (no hashes) as JSON-serializable maps. Returns {:ok, list}.

get_principal(username)

@spec get_principal(String.t()) :: {:ok, map()} | {:error, term()}

Reads a user's permissions and console role (no hash) from the local replica as %{username, permissions, role}, or {:error, :user_not_found}.

get_user(username)

@spec get_user(String.t()) ::
  {:ok, {String.t(), String.t(), [atom()]}} | {:error, atom()}

Reads a user as {username, hash, permissions} from the local replica, or {:error, :user_not_found}.

import_command(users)

@spec import_command([map()]) ::
  {{:import_users, [tuple()]} | {:import_users_with_roles, [tuple()]},
   non_neg_integer()}

The registry command an import of users (the maps import_users/1 takes) is sent as, and how many entries were skipped before it for an unknown role. An entry with an unknown role is dropped here, before the command is chosen, so it cannot push an import that is otherwise free of roles onto the version 5 command, which a cluster still below 5 refuses whole. With no role left, the command is the version 0 {:import_users, entries}.

import_users(users)

@spec import_users([map()]) :: {:ok, map()} | {:error, term()}

Imports users from a list of maps (each :username, :password_hash, :permissions and an optional :role; string keys also accepted, so an export_users/0 round trips). Existing and malformed entries, including an unknown role, are skipped. When no entry carries a role the older command shape is used, so an import without roles works on a cluster still below version 5. Returns {:ok, %{imported: n, skipped: n}}, a version refusal, or {:error, reason}.

insert_user(username, password_hash, permissions, role \\ nil)

@spec insert_user(
  String.t(),
  String.t(),
  [atom()],
  Malachi.Auth.ConsoleRole.t() | nil
) ::
  :ok | {:error, term()}

Inserts a new user, holding console role when it is not nil. Returns :ok, {:error, :user_exists}, {:error, :invalid_role}, a version refusal (see Malachi.Cluster.MachineVersion.refusal?/1) for a role on a cluster below version 5, or {:error, reason} when the store is unreachable.

list_users()

@spec list_users() :: [map()]

Lists all users (no hashes) as %{username, permissions, role} maps.

set_role(username, role)

@spec set_role(String.t(), Malachi.Auth.ConsoleRole.t() | nil) ::
  :ok | {:error, term()}

Sets a user's console role, or removes it with nil. Returns :ok, {:error, :user_not_found}, {:error, :invalid_role}, a version refusal, or {:error, reason} when the store is unreachable.

update_password(username, new_password_hash)

@spec update_password(String.t(), String.t()) :: :ok | {:error, atom()}

Updates a user's password hash. Returns :ok or {:error, :user_not_found}.