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

command()

@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()]}

password_hash()

@type password_hash() :: String.t()

permissions()

@type permissions() :: [atom()]

principal()

@type principal() :: %{
  username: username(),
  permissions: permissions(),
  role: role() | nil
}

reply()

@type reply() ::
  :ok
  | {:error, :user_exists | :user_not_found | :invalid_role | :unknown_command}
  | {:ok, %{imported: non_neg_integer(), skipped: non_neg_integer()}}

role()

@type role() :: Malachi.Auth.ConsoleRole.t()

role_entry()

@type role_entry() :: {username(), password_hash(), permissions(), role() | nil}

t()

@type t() :: %Malachi.Auth.UserRegistry{users: %{required(username()) => user()}}

user()

@type user() :: %{
  :hash => password_hash(),
  :permissions => permissions(),
  optional(:role) => role() | nil,
  created_at: integer(),
  updated_at: integer()
}

username()

@type username() :: String.t()

Functions

apply(state, arg2, now)

@spec apply(t(), command(), integer()) :: {t(), reply()}

Applies a command at time now (the ra leader's system_time). Returns {new_state, reply}. Deterministic given now.

command_versions()

Every command shape, mapped to the machine version that introduced it (see Malachi.Cluster.MachineVersion).

export_users(user_registry)

@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.

get_principal(user_registry, username)

@spec get_principal(t(), username()) :: {:ok, principal()} | {:error, :user_not_found}

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).

get_user(user_registry, username)

@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}.

list_users(user_registry)

@spec list_users(t()) :: [principal()]

Every user as %{username, permissions, role} (no hashes).

new()

@spec new() :: t()

An empty registry.