view auth/auth_store.h @ 271:13d61401c57d

redirect Copilot cache to service state Set XDG_CACHE_HOME from the configured inference state so the systemd service can extract Copilot outside its protected home directory. Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Fri, 07 Aug 2026 13:24:05 -0700
parents 04fee26ecce0
children
line wrap: on
line source

#ifndef ZENBU_AUTH_STORE_H
#define ZENBU_AUTH_STORE_H

#include "dowa/dowa.h"
#include "auth/auth_crypto.h"

#define AUTH_STORE_USERNAME_MIN 3
#define AUTH_STORE_USERNAME_MAX 32

typedef struct Auth_Store Auth_Store;

typedef enum {
  AUTH_STORE_ERROR          = -1,
  AUTH_STORE_OK             = 0,
  AUTH_STORE_NOT_FOUND      = 1,
  AUTH_STORE_CONFLICT       = 2,
  AUTH_STORE_EXPIRED        = 3,
  AUTH_STORE_REVOKED        = 4,
  AUTH_STORE_USER_DISABLED  = 5,
  AUTH_STORE_STALE_PASSWORD = 6,
  AUTH_STORE_LAST_ADMIN     = 7,
  AUTH_STORE_INVALID_ARG    = 8,
} Auth_Store_Result;

typedef enum {
  AUTH_STORE_BOOTSTRAP_CREATED       = 0,
  AUTH_STORE_BOOTSTRAP_ALREADY_PRESENT = 1,
} Auth_Store_Bootstrap_Result;

/*
 * Public user record — password_hash is never included here.
 * All fields are fixed-size; no arena pointer is needed.
 */
typedef struct {
  char    id[37];
  char    username[AUTH_STORE_USERNAME_MAX + 1];
  char    normalized_username[AUTH_STORE_USERNAME_MAX + 1];
  char    role[8];    /* "admin" or "member" */
  char    status[9];  /* "active" or "disabled" */
  boolean must_change_password;
  int64   password_changed_at;
  int64   created_at;
  int64   updated_at;
} Auth_User_Record;

/*
 * Authentication lookup record — includes the encoded password hash.
 * Callers must zero this struct after use; never log the hash field.
 */
typedef struct {
  Auth_User_Record user;
  char             password_hash[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE];
} Auth_User_Auth_Record;

/*
 * Session record — token/CSRF digests are not exposed here; the caller
 * already holds them before calling Create_Session or Find_Session.
 */
typedef struct {
  char  user_id[37];
  int64 created_at;
  int64 last_seen_at;
  int64 idle_expires_at;
  int64 absolute_expires_at;
  int64 password_changed_at_snapshot;
} Auth_Session_Record;

typedef struct {
  char  id[37];
  int64 created_at;
  int64 last_seen_at;
  int64 expires_at;
} Auth_Guest_Identity_Record;

/* --- Store lifecycle --- */

Auth_Store *Auth_Store_Create(const char *database_path);
void        Auth_Store_Destroy(Auth_Store *p_store);

/* --- Username utilities --- */

/*
 * Trim ASCII spaces, lowercase, validate character set and length.
 * Writes the normalized form to 'normalized' (capacity must include NUL).
 * Returns TRUE on success; FALSE if invalid or buffer too small.
 */
boolean Auth_Store_Normalize_Username(
    const char *username,
    char       *normalized,
    size_t      capacity);

/*
 * Validate a pre-normalized username (lowercase, no leading/trailing spaces).
 * Returns TRUE iff length and character set are within policy.
 */
boolean Auth_Store_Validate_Username(const char *normalized_username);

/* --- User management --- */

Auth_Store_Result Auth_Store_Create_User(
    Auth_Store *p_store,
    const char *username,
    const char *encoded_hash,
    const char *role,
    boolean     must_change_password,
    char        output_id[37]);

/* Create a user and its audit record in one transaction. */
Auth_Store_Result Auth_Store_Create_User_Audited(
    Auth_Store *p_store,
    const char *username,
    const char *encoded_hash,
    const char *role,
    boolean     must_change_password,
    const char *actor_user_id,
    char        output_id[37]);

/*
 * Idempotent: creates an admin user only when no admin exists at all.
 * p_bootstrap_result receives CREATED or ALREADY_PRESENT.
 * output_id receives the UUID (new or existing) and may be NULL.
 */
Auth_Store_Result Auth_Store_Bootstrap_Admin(
    Auth_Store               *p_store,
    const char               *username,
    const char               *encoded_hash,
    Auth_Store_Bootstrap_Result *p_bootstrap_result,
    char                      output_id[37]);

/* Includes password_hash for authentication — zero record after use. */
Auth_Store_Result Auth_Store_Find_User_By_Username(
    Auth_Store         *p_store,
    const char         *username,
    Auth_User_Auth_Record *p_record);

Auth_Store_Result Auth_Store_Get_User(
    Auth_Store       *p_store,
    const char       *user_id,
    Auth_User_Record *p_record);

/*
 * List all users into a Dowa arena-backed array.
 * After a successful call, use Dowa_Array_Length(*pp_records) for count.
 */
Auth_Store_Result Auth_Store_List_Users(
    Auth_Store        *p_store,
    Auth_User_Record **pp_records,
    Dowa_Arena        *p_arena);

/*
 * new_status must be "active" or "disabled".
 * Fails with AUTH_STORE_LAST_ADMIN if disabling the last active admin.
 */
Auth_Store_Result Auth_Store_Update_User_Status(
    Auth_Store *p_store,
    const char *user_id,
    const char *new_status,
    const char *actor_user_id);

/* Atomically enable a user and write audit; revoked sessions stay revoked. */
Auth_Store_Result Auth_Store_Enable_User(
    Auth_Store *p_store,
    const char *user_id,
    const char *actor_user_id);

/* Atomically enforce last-admin policy, disable, revoke, and write audit. */
Auth_Store_Result Auth_Store_Disable_User_And_Revoke_Sessions(
    Auth_Store *p_store,
    const char *user_id,
    const char *actor_user_id);

/*
 * new_role must be "admin" or "member".
 * Fails with AUTH_STORE_LAST_ADMIN if demoting the last active admin.
 */
Auth_Store_Result Auth_Store_Update_User_Role(
    Auth_Store *p_store,
    const char *user_id,
    const char *new_role,
    const char *actor_user_id);

/* Atomically enforce last-admin policy, update role, revoke, and audit. */
Auth_Store_Result Auth_Store_Update_Role_And_Revoke_Sessions(
    Auth_Store *p_store,
    const char *user_id,
    const char *new_role,
    const char *actor_user_id);

Auth_Store_Result Auth_Store_Set_Must_Change_Password(
    Auth_Store *p_store,
    const char *user_id,
    boolean     value,
    const char *actor_user_id);

/*
 * Updates the encoded password hash; clears must_change_password.
 * If revoke_other_sessions is TRUE, all sessions except keep_token_digest
 * (which may be NULL) are revoked atomically in the same transaction.
 */
Auth_Store_Result Auth_Store_Update_Password(
    Auth_Store *p_store,
    const char *user_id,
    const char *new_encoded_hash,
    boolean     revoke_other_sessions,
    const char *keep_token_digest);

/* --- Session management --- */

/*
 * Creates a new session.  Fails if the user is disabled.
 * current_unix is the caller-supplied Unix timestamp.
 */
Auth_Store_Result Auth_Store_Create_Session(
    Auth_Store          *p_store,
    const char          *user_id,
    const char          *token_digest,
    const char          *csrf_digest,
    int64                idle_ttl_secs,
    int64                absolute_ttl_secs,
    int64                current_unix,
    Auth_Session_Record *p_record);

/*
 * CAS session create: only creates a session if the user's current
 * password_hash still exactly matches expected_password_hash.
 * Returns AUTH_STORE_STALE_PASSWORD when an administrator changed it.
 * The caller must cleanse expected_password_hash after this call.
 */
Auth_Store_Result Auth_Store_Create_Session_CAS(
    Auth_Store          *p_store,
    const char          *user_id,
    const char          *expected_password_hash,
    const char          *token_digest,
    const char          *csrf_digest,
    int64                idle_ttl_secs,
    int64                absolute_ttl_secs,
    int64                current_unix,
    Auth_Session_Record *p_record);

/*
 * Resolves a session by token_digest.
 * Returns:
 *   OK             — session is valid; p_session and p_user are populated.
 *   NOT_FOUND      — no such session.
 *   REVOKED        — session was explicitly revoked.
 *   EXPIRED        — idle or absolute expiry exceeded.
 *   USER_DISABLED  — session owner has been disabled.
 *   STALE_PASSWORD — password changed after session was created.
 */
Auth_Store_Result Auth_Store_Find_Session(
    Auth_Store          *p_store,
    const char          *token_digest,
    int64                current_unix,
    Auth_Session_Record *p_session,
    Auth_User_Record    *p_user);

/* Extends the idle expiry; does nothing if the session is revoked. */
Auth_Store_Result Auth_Store_Touch_Session(
    Auth_Store *p_store,
    const char *token_digest,
    int64       current_unix,
    int64       idle_ttl_secs);

Auth_Store_Result Auth_Store_Revoke_Session(
    Auth_Store *p_store,
    const char *token_digest);

/*
 * Revokes all sessions for user_id.
 * If except_token_digest is non-NULL, that session is preserved.
 */
Auth_Store_Result Auth_Store_Revoke_All_Sessions(
    Auth_Store *p_store,
    const char *user_id,
    const char *except_token_digest);

/* Atomically revoke sessions and write one audit record. */
Auth_Store_Result Auth_Store_Revoke_All_Sessions_Audited(
    Auth_Store *p_store,
    const char *user_id,
    const char *except_token_digest,
    const char *actor_user_id);

/*
 * Atomically create a new session and revoke the old one in a single
 * transaction.  Use this after a password change to rotate the current
 * session without any window where neither or both sessions are valid.
 *
 * old_token_digest: the existing session to revoke (must not be NULL or "").
 *   If old_token_digest is already revoked the rotation still succeeds and
 *   the new session is created.
 * All other parameters are the same as Auth_Store_Create_Session.
 */
Auth_Store_Result Auth_Store_Rotate_Session(
    Auth_Store          *p_store,
    const char          *user_id,
    const char          *old_token_digest,
    const char          *new_token_digest,
    const char          *new_csrf_digest,
    int64                idle_ttl_secs,
    int64                absolute_ttl_secs,
    int64                current_unix,
    Auth_Session_Record *p_record);

/*
 * Atomically compare-and-swap the password hash, clear forced change,
 * revoke every existing session, and create one replacement session.
 * On AUTH_STORE_STALE_PASSWORD no state is changed. The caller must cleanse
 * both password hashes after this call.
 */
Auth_Store_Result Auth_Store_Self_Change_Password(
    Auth_Store          *p_store,
    const char          *user_id,
    const char          *old_encoded_hash,
    const char          *new_encoded_hash,
    const char          *new_token_digest,
    const char          *new_csrf_digest,
    int64                idle_ttl_secs,
    int64                absolute_ttl_secs,
    int64                current_unix,
    Auth_Session_Record *p_record);

/* --- Guest quota --- */

typedef enum {
  AUTH_STORE_GUEST_QUOTA_OK               =  0,
  AUTH_STORE_GUEST_QUOTA_TURNS_EXHAUSTED  =  1,
  AUTH_STORE_GUEST_QUOTA_TOKENS_EXHAUSTED =  2,
  AUTH_STORE_GUEST_QUOTA_ERROR            = -1,
} Auth_Store_Guest_Quota_Result;

typedef struct {
  int64 turns_used;
  int64 output_tokens_used;
  int64 output_tokens_reserved;
} Auth_Store_Guest_Usage;

/*
 * Read current daily usage for a guest window.
 * window_start: UTC midnight Unix timestamp for the current day.
 * Returns OK with zeroed usage if no row exists yet, or ERROR.
 */
Auth_Store_Result Auth_Store_Guest_Get_Usage(
    Auth_Store             *p_store,
    const char             *guest_id,
    int64                   window_start,
    Auth_Store_Guest_Usage *p_usage);

/*
 * Atomically reserve quota for one inference turn.
 * On success: increments turns_used by 1 and output_tokens_reserved by
 *   max_output_tokens in the usage row for window_start.
 * Invariant after every reservation:
 *   output_tokens_used + output_tokens_reserved <= tokens_limit.
 * Returns TURNS_EXHAUSTED or TOKENS_EXHAUSTED when the limit would be exceeded.
 * reservation_expires: Unix timestamp after which the reservation may be
 *   discarded by cleanup; the turn charge (turns_used) persists regardless.
 */
Auth_Store_Guest_Quota_Result Auth_Store_Guest_Reserve(
    Auth_Store *p_store,
    const char *guest_id,
    const char *request_id,
    int64       window_start,
    int64       max_output_tokens,
    int64       turns_limit,
    int64       tokens_limit,
    int64       reservation_expires);

/*
 * Reconcile a completed turn with the actual provider output token count.
 * actual_output_tokens is capped to the reserved amount (fail closed; if
 * the provider reports more than reserved, the reserved amount is charged).
 * Adds capped tokens to output_tokens_used; subtracts reserved from
 * output_tokens_reserved; deletes the reservation row.
 * Idempotent by request_id: safe to call when already reconciled.
 */
Auth_Store_Result Auth_Store_Guest_Reconcile(
    Auth_Store *p_store,
    const char *request_id,
    int64       actual_output_tokens);

/*
 * Release a reservation on failure or abort.
 * Decrements output_tokens_reserved by the reserved amount; does NOT
 * change turns_used (the turn charge is retained).
 * Deletes the reservation row.
 * Idempotent by request_id.
 */
Auth_Store_Result Auth_Store_Guest_Release(
    Auth_Store *p_store,
    const char *request_id);

/*
 * Clear all outstanding reservations for a guest.
 * Used after a successful guest→user login transfer.
 * Decrements output_tokens_reserved for each affected usage window.
 * Does NOT change turns_used.
 */
Auth_Store_Result Auth_Store_Guest_Clear_Reservations(
    Auth_Store *p_store,
    const char *guest_id);

/*
 * Atomically reap expired reservations: decrement output_tokens_reserved
 * in the matching guest_usage rows and delete expired reservation rows, all
 * in one transaction.  Uses MAX(0,...) to avoid underflow.
 * Idempotent: safe to call repeatedly with the same or decreasing current_unix.
 * Called at store startup and before quota read/reserve operations.
 */
Auth_Store_Result Auth_Store_Guest_Reap_Expired(
    Auth_Store *p_store,
    int64       current_unix);

/* --- Guest identity --- */

/*
 * Insert or refresh a guest identity.
 * ip_binding_digest must be an HMAC digest — never the raw IP address.
 * last_seen_at is updated to now; expires_at is refreshed.
 */
Auth_Store_Result Auth_Store_Upsert_Guest_Identity(
    Auth_Store                *p_store,
    const char                *guest_id,
    const char                *ip_binding_digest,
    int64                      expires_at,
    Auth_Guest_Identity_Record *p_record);

Auth_Store_Result Auth_Store_Find_Guest_Identity(
    Auth_Store                *p_store,
    const char                *guest_id,
    int64                      current_unix,
    Auth_Guest_Identity_Record *p_record);

/* --- Audit log --- */

/*
 * Append one bounded audit entry.  actor/target/detail may be NULL.
 * Never include passwords, hashes, tokens, or session digests.
 * Returns OK or ERROR.
 */
Auth_Store_Result Auth_Store_Insert_Audit_Log(
    Auth_Store *p_store,
    const char *actor_user_id,
    const char *action,
    const char *target_user_id,
    const char *detail);

/* --- Admin operations --- */

/*
 * Atomically: update password hash, set must_change_password=1,
 * revoke all target sessions, write audit row.
 * Use for admin-initiated temporary-password reset only.
 * new_encoded_hash must be a valid encoded zenbu-scrypt hash.
 */
Auth_Store_Result Auth_Store_Admin_Reset_Password(
    Auth_Store *p_store,
    const char *user_id,
    const char *new_encoded_hash,
    const char *actor_user_id);

#endif