Malachi.Auth (malachi v0.22.7)

View Source

Simple authentication system for Malachi. Manages users with username/password credentials.

Summary

Functions

Adds a new user with wire permissions (:admin, :produce, :consume) and an optional console role (Malachi.Auth.ConsoleRole). Returns :ok, {:error, :user_exists}, {:error, :invalid_role}, a version refusal (Malachi.Cluster.MachineVersion.refusal?/1) when a role is given on a cluster still below machine version 5, or {:error, :persist_failed}.

Authenticates a user with username and password (legacy compatibility). Uses a dummy IP address. For production use, prefer authenticate/3 with actual client IP. Returns {:ok, session_token} or {:error, reason}

Authenticates a user with username and password. Returns {:ok, session_token} or {:error, reason}

Returns a specification to start this module under a supervisor.

Generates a random password for username (an admin) and seeds it, but only when generation is enabled (:generate_admin config, set when no admin password is configured) and no such user exists yet. The password is logged once; the replicated store dedups, so on a multi-node boot exactly one node's seed succeeds and announces its password (the others get :user_exists and discard theirs). A no-op when generation is disabled or the admin already exists. Called at boot after seed_default_users/0.

Grants username a per-topic ACL: operation (:produce/:consume) on pattern (an exact topic, or a *-suffixed prefix like "orders.*"). Returns :ok, or {:error, :invalid_acl} for a bad operation/pattern.

Whether the subject has permission (or is :admin). Accepts either a username, looked up in the user table, where an unknown user has no permissions, or a permission list directly.

Lists username's ACL grants as %{operation, resource} maps (resource rendered as its pattern string).

Lists all users (without passwords).

Invalidates a session token (logout).

Parses an ACL operation string into :produce/:consume, or :error. Mapping explicitly (rather than String.to_atom/1) keeps an untrusted client from exhausting the atom table. Shared by the ACL management surfaces (wire ops, dashboard).

Parses a list of permission strings into the allowed permission atoms, or :error if any is unknown (or the input is not a list). The allowed permissions are :admin, :produce, :consume. Mapping explicitly (rather than String.to_atom/1) keeps an untrusted client from exhausting the atom table.

Removes a user.

Revokes a per-topic ACL grant (idempotent). Returns :ok or {:error, reason}.

Whether validate_token/3 would accept token from this client, answered without any of its effects (no audit event, no hijack warning, no expired session deleted, no activity recorded). A caller uses it to decide what a validation will cost before paying for it.

Sets username's console role, or removes it with nil, on behalf of actor (who the audit event names, :user_role_changed, whatever the outcome). Takes effect on the user's next HTTP request on each node once that node's replica has applied it, since the console access rules read the role from the local replica on each request rather than from the session (Malachi.Console.Access); a node cut off from the leader keeps the old role until it rejoins. Returns :ok, {:error, :user_not_found}, {:error, :invalid_role}, a version refusal on a cluster below machine version 5, or {:error, :persist_failed}.

Starts the auth server, which owns the in-memory user and session ETS tables.

Validates a session token without IP binding (legacy compatibility).

Validates a session token with IP binding. Returns {:ok, %{username: username, permissions: permissions}} or {:error, reason}

Verifies a username/password against the user store without creating a session: the session-less core of authentication, used by Malachi.Auth.PasswordProvider. Returns {:ok, permissions} or {:error, :invalid_password | :user_not_found}.

Functions

add_user(username, password, permissions \\ [:produce, :consume], role \\ nil)

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

Adds a new user with wire permissions (:admin, :produce, :consume) and an optional console role (Malachi.Auth.ConsoleRole). Returns :ok, {:error, :user_exists}, {:error, :invalid_role}, a version refusal (Malachi.Cluster.MachineVersion.refusal?/1) when a role is given on a cluster still below machine version 5, or {:error, :persist_failed}.

authenticate(username, password)

Authenticates a user with username and password (legacy compatibility). Uses a dummy IP address. For production use, prefer authenticate/3 with actual client IP. Returns {:ok, session_token} or {:error, reason}

authenticate(username, password, client_ip, user_agent \\ "")

Authenticates a user with username and password. Returns {:ok, session_token} or {:error, reason}

Parameters

  • username - Username to authenticate
  • password - Password to verify
  • client_ip - Client IP address (tuple) for session binding and audit logging

change_password(username, new_password)

Changes user password.

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

generate_admin_if_absent(username \\ "admin")

@spec generate_admin_if_absent(String.t()) :: :ok

Generates a random password for username (an admin) and seeds it, but only when generation is enabled (:generate_admin config, set when no admin password is configured) and no such user exists yet. The password is logged once; the replicated store dedups, so on a multi-node boot exactly one node's seed succeeds and announces its password (the others get :user_exists and discard theirs). A no-op when generation is disabled or the admin already exists. Called at boot after seed_default_users/0.

grant_acl(username, operation, pattern)

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

Grants username a per-topic ACL: operation (:produce/:consume) on pattern (an exact topic, or a *-suffixed prefix like "orders.*"). Returns :ok, or {:error, :invalid_acl} for a bad operation/pattern.

has_permission?(username, permission)

Whether the subject has permission (or is :admin). Accepts either a username, looked up in the user table, where an unknown user has no permissions, or a permission list directly.

list_acls(username)

@spec list_acls(String.t()) :: [%{operation: atom(), resource: String.t()}]

Lists username's ACL grants as %{operation, resource} maps (resource rendered as its pattern string).

list_users()

Lists all users (without passwords).

logout(token)

Invalidates a session token (logout).

parse_acl_operation(arg1)

@spec parse_acl_operation(String.t()) :: {:ok, :produce | :consume} | :error

Parses an ACL operation string into :produce/:consume, or :error. Mapping explicitly (rather than String.to_atom/1) keeps an untrusted client from exhausting the atom table. Shared by the ACL management surfaces (wire ops, dashboard).

parse_permissions(strings)

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

Parses a list of permission strings into the allowed permission atoms, or :error if any is unknown (or the input is not a list). The allowed permissions are :admin, :produce, :consume. Mapping explicitly (rather than String.to_atom/1) keeps an untrusted client from exhausting the atom table.

remove_user(username)

Removes a user.

revoke_acl(username, operation, pattern)

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

Revokes a per-topic ACL grant (idempotent). Returns :ok or {:error, reason}.

session_valid?(token, client_ip, user_agent)

@spec session_valid?(String.t(), term(), String.t()) :: boolean()

Whether validate_token/3 would accept token from this client, answered without any of its effects (no audit event, no hijack warning, no expired session deleted, no activity recorded). A caller uses it to decide what a validation will cost before paying for it.

set_role(username, role, actor)

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

Sets username's console role, or removes it with nil, on behalf of actor (who the audit event names, :user_role_changed, whatever the outcome). Takes effect on the user's next HTTP request on each node once that node's replica has applied it, since the console access rules read the role from the local replica on each request rather than from the session (Malachi.Console.Access); a node cut off from the leader keeps the old role until it rejoins. Returns :ok, {:error, :user_not_found}, {:error, :invalid_role}, a version refusal on a cluster below machine version 5, or {:error, :persist_failed}.

start_link(_)

Starts the auth server, which owns the in-memory user and session ETS tables.

validate_token(token)

Validates a session token without IP binding (legacy compatibility).

validate_token(token, client_ip, user_agent \\ "")

Validates a session token with IP binding. Returns {:ok, %{username: username, permissions: permissions}} or {:error, reason}

Parameters

  • token - Session token to validate
  • client_ip - Current client IP address for binding verification

verify_credentials(username, password)

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

Verifies a username/password against the user store without creating a session: the session-less core of authentication, used by Malachi.Auth.PasswordProvider. Returns {:ok, permissions} or {:error, :invalid_password | :user_not_found}.

Runs a dummy hash (Argon2.no_user_verify/0) for an unknown user so the response time does not reveal whether the username exists (timing-attack mitigation). The caller mints the session and maps both error reasons to a single client-facing :invalid_credentials.