Malachi. Auth. UserRegistry
(malachi v0.22.7)
View Source
The pure state of the cluster's user registry: the credentials and permissions of every principal
(admins, producers, consumers, service accounts). It is the deterministic core replicated by
Malachi.Auth.UserMachine over a dedicated ra cluster (exactly as Malachi.Cluster.Lease sits behind
LeaseMachine), so every node reaches the same user set from the same command log, replacing the old
node-local Mnesia store, which never replicated.
Users are global, small, rarely-written metadata: a single replicated Raft group is the right home
(as Kafka KRaft / Redpanda keep credentials + ACLs in one controller quorum), while the data plane scales
by sharding vnodes. Auth reads come from the local ra replica (fast, local); writes go through the log.
Each user may also hold a console role (:viewer, :editor or :admin, see
Malachi.Console.Access), orthogonal to its wire permissions. The role arrived at machine version 5 as
three new command shapes (set_role, a five element put_user, import_users_with_roles); the older
shapes stay, so a log written before version 5 replays unchanged. A record written before then has no
:role key at all, and every read treats that as no role.
apply/3 takes the current time from the caller: UserMachine passes the ra leader's system_time:
and never reads a clock itself: reading a wall clock inside apply would be non-deterministic and break
Raft. created_at/updated_at are therefore the leader's stamp, replicated in the log.
Summary
Functions
Applies a command at time now (the ra leader's system_time). Returns {new_state, reply}.
Deterministic given now.
Every command shape, mapped to the machine version that introduced it (see Malachi.Cluster.MachineVersion).
Every user as a JSON-serializable map (no hashes): %{username, permissions (as strings), role (a string or nil), created_at, updated_at}: the export shape consumed by Malachi.Auth.UserStore.import_users/1.
The user's permissions and console role, without the hash, or {:error, :user_not_found}. The read the
console access rules make on every HTTP request (Malachi.Console.Access).
The user record as {username, hash, permissions}, or {:error, :user_not_found}.
Every user as %{username, permissions, role} (no hashes).
An empty registry.
Types
@type command() :: {:put_user, username(), password_hash(), permissions()} | {:delete_user, username()} | {:update_password, username(), password_hash()} | {:import_users, [{username(), password_hash(), permissions()}]} | {:put_user, username(), password_hash(), permissions(), role() | nil} | {:set_role, username(), role() | nil} | {:import_users_with_roles, [role_entry()]}
@type password_hash() :: String.t()
@type permissions() :: [atom()]
@type principal() :: %{ username: username(), permissions: permissions(), role: role() | nil }
@type reply() :: :ok | {:error, :user_exists | :user_not_found | :invalid_role | :unknown_command} | {:ok, %{imported: non_neg_integer(), skipped: non_neg_integer()}}
@type role() :: Malachi.Auth.ConsoleRole.t()
@type role_entry() :: {username(), password_hash(), permissions(), role() | nil}
@type user() :: %{ :hash => password_hash(), :permissions => permissions(), optional(:role) => role() | nil, created_at: integer(), updated_at: integer() }
@type username() :: String.t()
Functions
Applies a command at time now (the ra leader's system_time). Returns {new_state, reply}.
Deterministic given now.
Every command shape, mapped to the machine version that introduced it (see Malachi.Cluster.MachineVersion).
@spec export_users(t()) :: [ %{ username: username(), permissions: [String.t()], role: String.t() | nil, created_at: integer(), updated_at: integer() } ]
Every user as a JSON-serializable map (no hashes): %{username, permissions (as strings), role (a string or nil), created_at, updated_at}: the export shape consumed by Malachi.Auth.UserStore.import_users/1.
The user's permissions and console role, without the hash, or {:error, :user_not_found}. The read the
console access rules make on every HTTP request (Malachi.Console.Access).
@spec get_user(t(), username()) :: {:ok, {username(), password_hash(), permissions()}} | {:error, :user_not_found}
The user record as {username, hash, permissions}, or {:error, :user_not_found}.
Every user as %{username, permissions, role} (no hashes).
@spec new() :: t()
An empty registry.