Malachi. Auth. LockoutServer
(malachi v0.22.7)
View Source
A thin named facade over Malachi.Cluster.RaCluster for the cluster's account-lockout store (Malachi.Auth.LockoutMachine): start
the dedicated Raft cluster, submit lockout commands through the log, and read the replicated lockout state.
Mirrors Malachi.Auth.UserServer; ra must already be running. This module owns only the lockout cluster,
not ra's lifecycle.
Writes (record_failed_attempt/record_successful_auth/unlock_user/unlock_key/cleanup) go
through the log: replicated by consensus, so brute-force protection is cluster-wide (attempts spread
across nodes still count against one limit) and survives a restart. record_failed_attempt returns the
machine reply %{count, locked} so the caller can drive metrics/logging/audit for a newly applied lockout.
Reads (locked?/failed_attempts/list_locked) use :ra.local_query against the local replica:
fast (no consensus round-trip) and adequate for the auth hot path, which checks the lock once per attempt.
Expiry is evaluated against a caller-supplied now (the local node's clock), seconds of clock skew are
irrelevant to minutes-long lockouts, and reads are eventually consistent (a just-written lockout propagates
within replication lag), which is acceptable for brute-force defense.
Summary
Functions
Compacts expired lockouts and attempts older than attempt_ttl_ms. Machine reply is :ok.
Stops and deletes the lockout store's Raft cluster (removing its on-disk state).
The number of failed attempts recorded for key.
Every account currently locked at now, as info maps.
Whether key is locked at now (ms): :not_locked or {:locked, time_remaining_ms}.
Ensures this node participates in the lockout cluster (self-join), so a staggered boot converges to a
fully-replicated lockout store. Idempotent and best-effort. Mirrors UserServer.reconcile/2.
Records a failed authentication attempt for key ({username, ip}), applying config
(%{max_attempts, base_duration_ms, progressive}). Machine reply is %{count, locked} where locked is
nil or %{duration_ms, locked_until} when this attempt (re)applied a lockout.
Clears failed attempts and any lockout for key after a successful login. Machine reply is :ok.
Starts the lockout store's Raft cluster named cluster_name across nodes (default the local node) and
returns a server_id addressing a real member (the local node when it is one, else the first).
Unlocks a single {username, ip} key (admin action). Machine reply is :ok.
Unlocks every IP for username (admin action). Machine reply is {:ok, cleared_count}.
Types
@type cluster_name() :: atom()
@type server_id() :: {cluster_name(), node()}
Functions
@spec cleanup(server_id(), non_neg_integer()) :: {:ok, :ok} | {:error, term()}
Compacts expired lockouts and attempts older than attempt_ttl_ms. Machine reply is :ok.
@spec delete(cluster_name()) :: :ok
Stops and deletes the lockout store's Raft cluster (removing its on-disk state).
@spec failed_attempts(server_id(), Malachi.Auth.LockoutRegistry.key()) :: {:ok, non_neg_integer()} | {:error, term()}
The number of failed attempts recorded for key.
Every account currently locked at now, as info maps.
@spec locked?(server_id(), Malachi.Auth.LockoutRegistry.key(), integer()) :: {:ok, :not_locked | {:locked, non_neg_integer()}} | {:error, term()}
Whether key is locked at now (ms): :not_locked or {:locked, time_remaining_ms}.
@spec reconcile(cluster_name(), [node()]) :: :ok
Ensures this node participates in the lockout cluster (self-join), so a staggered boot converges to a
fully-replicated lockout store. Idempotent and best-effort. Mirrors UserServer.reconcile/2.
@spec record_failed_attempt( server_id(), Malachi.Auth.LockoutRegistry.key(), Malachi.Auth.LockoutRegistry.config() ) :: {:ok, map()} | {:error, term()}
Records a failed authentication attempt for key ({username, ip}), applying config
(%{max_attempts, base_duration_ms, progressive}). Machine reply is %{count, locked} where locked is
nil or %{duration_ms, locked_until} when this attempt (re)applied a lockout.
@spec record_successful_auth(server_id(), Malachi.Auth.LockoutRegistry.key()) :: {:ok, :ok} | {:error, term()}
Clears failed attempts and any lockout for key after a successful login. Machine reply is :ok.
@spec start(cluster_name(), [node()]) :: {:ok, server_id()} | {:error, term()}
Starts the lockout store's Raft cluster named cluster_name across nodes (default the local node) and
returns a server_id addressing a real member (the local node when it is one, else the first).
@spec unlock_key(server_id(), Malachi.Auth.LockoutRegistry.key()) :: {:ok, :ok} | {:error, term()}
Unlocks a single {username, ip} key (admin action). Machine reply is :ok.
@spec unlock_user(server_id(), String.t()) :: {:ok, {:ok, non_neg_integer()}} | {:error, term()}
Unlocks every IP for username (admin action). Machine reply is {:ok, cleared_count}.