Mercurial
comparison auth/auth_store.c @ 264:04fee26ecce0
add authenticated JRPG conversation platform
Add reusable auth/session storage, owned conversation recovery, guest quotas, admin workflows, URL-routed conversation UI, mobile frame support, and parallel browser acceptance.
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Fri, 07 Aug 2026 07:34:12 -0700 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| 263:ee04e4e69fed | 264:04fee26ecce0 |
|---|---|
| 1 #include "auth/auth_store.h" | |
| 2 | |
| 3 #include "deita/deita.h" | |
| 4 | |
| 5 #include <openssl/crypto.h> | |
| 6 | |
| 7 #include <fcntl.h> | |
| 8 #include <limits.h> | |
| 9 #include <pthread.h> | |
| 10 #include <stdio.h> | |
| 11 #include <stdlib.h> | |
| 12 #include <string.h> | |
| 13 #include <time.h> | |
| 14 #include <unistd.h> | |
| 15 | |
| 16 struct Auth_Store { | |
| 17 Deita_Connection *p_connection; | |
| 18 pthread_mutex_t mutex; | |
| 19 }; | |
| 20 | |
| 21 /* ------------------------------------------------------------------ */ | |
| 22 /* Migration SQL */ | |
| 23 /* ------------------------------------------------------------------ */ | |
| 24 | |
| 25 static const char *k_migration_v1 = | |
| 26 "CREATE TABLE IF NOT EXISTS users (" | |
| 27 " id TEXT PRIMARY KEY," | |
| 28 " username TEXT NOT NULL," | |
| 29 " normalized_username TEXT NOT NULL UNIQUE," | |
| 30 " password_hash TEXT NOT NULL," | |
| 31 " role TEXT NOT NULL CHECK(role IN ('admin','member'))," | |
| 32 " status TEXT NOT NULL DEFAULT 'active'" | |
| 33 " CHECK(status IN ('active','disabled'))," | |
| 34 " must_change_password INTEGER NOT NULL DEFAULT 0," | |
| 35 " password_changed_at INTEGER NOT NULL DEFAULT 0," | |
| 36 " created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))," | |
| 37 " updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))" | |
| 38 ");" | |
| 39 "CREATE TABLE IF NOT EXISTS auth_sessions (" | |
| 40 " token_digest TEXT PRIMARY KEY," | |
| 41 " csrf_digest TEXT NOT NULL," | |
| 42 " user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE," | |
| 43 " created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))," | |
| 44 " last_seen_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))," | |
| 45 " idle_expires_at INTEGER NOT NULL," | |
| 46 " absolute_expires_at INTEGER NOT NULL," | |
| 47 " password_changed_at_snapshot INTEGER NOT NULL DEFAULT 0," | |
| 48 " revoked_at INTEGER" | |
| 49 ");" | |
| 50 "CREATE TABLE IF NOT EXISTS guest_identities (" | |
| 51 " id TEXT PRIMARY KEY," | |
| 52 " ip_binding_digest TEXT NOT NULL," | |
| 53 " created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))," | |
| 54 " last_seen_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))," | |
| 55 " expires_at INTEGER NOT NULL" | |
| 56 ");" | |
| 57 "CREATE TABLE IF NOT EXISTS guest_usage (" | |
| 58 " guest_id TEXT NOT NULL" | |
| 59 " REFERENCES guest_identities(id) ON DELETE CASCADE," | |
| 60 " window_start INTEGER NOT NULL," | |
| 61 " count INTEGER NOT NULL DEFAULT 0," | |
| 62 " PRIMARY KEY (guest_id, window_start)" | |
| 63 ");" | |
| 64 "CREATE TABLE IF NOT EXISTS guest_usage_reservations (" | |
| 65 " id INTEGER PRIMARY KEY AUTOINCREMENT," | |
| 66 " guest_id TEXT NOT NULL" | |
| 67 " REFERENCES guest_identities(id) ON DELETE CASCADE," | |
| 68 " reserved_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))," | |
| 69 " expires_at INTEGER NOT NULL" | |
| 70 ");" | |
| 71 "CREATE TABLE IF NOT EXISTS admin_audit_log (" | |
| 72 " id INTEGER PRIMARY KEY AUTOINCREMENT," | |
| 73 " actor_user_id TEXT," | |
| 74 " action TEXT NOT NULL," | |
| 75 " target_user_id TEXT," | |
| 76 " detail TEXT," | |
| 77 " created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))" | |
| 78 ");" | |
| 79 "CREATE INDEX IF NOT EXISTS idx_users_normalized" | |
| 80 " ON users(normalized_username);" | |
| 81 "CREATE INDEX IF NOT EXISTS idx_sessions_user" | |
| 82 " ON auth_sessions(user_id);" | |
| 83 "CREATE INDEX IF NOT EXISTS idx_sessions_expiry" | |
| 84 " ON auth_sessions(absolute_expires_at) WHERE revoked_at IS NULL;" | |
| 85 "CREATE INDEX IF NOT EXISTS idx_guest_expiry" | |
| 86 " ON guest_identities(expires_at);" | |
| 87 "CREATE INDEX IF NOT EXISTS idx_audit_created" | |
| 88 " ON admin_audit_log(created_at DESC);"; | |
| 89 | |
| 90 /* ------------------------------------------------------------------ */ | |
| 91 /* Internal helpers */ | |
| 92 /* ------------------------------------------------------------------ */ | |
| 93 | |
| 94 static void auth__copy_text_fixed(char *dest, size_t size, const char *text) | |
| 95 { | |
| 96 const char *src = text ? text : ""; | |
| 97 size_t n = strlen(src); | |
| 98 if (n >= size) | |
| 99 n = size - 1; | |
| 100 memcpy(dest, src, n); | |
| 101 dest[n] = '\0'; | |
| 102 } | |
| 103 | |
| 104 static boolean auth__generate_uuid(char output[37]) | |
| 105 { | |
| 106 uint8 bytes[16]; | |
| 107 int fd = open("/dev/urandom", O_RDONLY); | |
| 108 size_t offset = 0; | |
| 109 ssize_t amount; | |
| 110 | |
| 111 if (fd < 0) | |
| 112 return FALSE; | |
| 113 while (offset < sizeof(bytes)) | |
| 114 { | |
| 115 amount = read(fd, bytes + offset, sizeof(bytes) - offset); | |
| 116 if (amount <= 0) | |
| 117 { | |
| 118 close(fd); | |
| 119 return FALSE; | |
| 120 } | |
| 121 offset += (size_t)amount; | |
| 122 } | |
| 123 close(fd); | |
| 124 | |
| 125 bytes[6] = (uint8)((bytes[6] & 0x0f) | 0x40); | |
| 126 bytes[8] = (uint8)((bytes[8] & 0x3f) | 0x80); | |
| 127 snprintf( | |
| 128 output, 37, | |
| 129 "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", | |
| 130 bytes[0], bytes[1], bytes[2], bytes[3], | |
| 131 bytes[4], bytes[5], bytes[6], bytes[7], | |
| 132 bytes[8], bytes[9], bytes[10], bytes[11], | |
| 133 bytes[12], bytes[13], bytes[14], bytes[15]); | |
| 134 return TRUE; | |
| 135 } | |
| 136 | |
| 137 static Auth_Store_Result auth__rollback(Auth_Store *p_store, | |
| 138 Auth_Store_Result result) | |
| 139 { | |
| 140 Deita_Query_Execute_Update(p_store->p_connection, "ROLLBACK"); | |
| 141 return result; | |
| 142 } | |
| 143 | |
| 144 /* Forward declaration — implementation is in the guest quota section. */ | |
| 145 static void auth__i64_str(char *buf, size_t size, int64 value); | |
| 146 | |
| 147 static boolean auth__insert_audit_log_locked( | |
| 148 Auth_Store *p_store, | |
| 149 const char *actor_user_id, | |
| 150 const char *action, | |
| 151 const char *target_user_id, | |
| 152 const char *detail) | |
| 153 { | |
| 154 /* actor_user_id and detail may be NULL — represented as empty string */ | |
| 155 const char *actor = actor_user_id ? actor_user_id : ""; | |
| 156 const char *tgt = target_user_id ? target_user_id : ""; | |
| 157 const char *det = detail ? detail : ""; | |
| 158 const char *params[] = {actor, action, tgt, det}; | |
| 159 if (Deita_Query_Execute_Update_Prepared( | |
| 160 p_store->p_connection, | |
| 161 "INSERT INTO admin_audit_log" | |
| 162 " (actor_user_id, action, target_user_id, detail)" | |
| 163 " VALUES (?, ?, ?, ?)", | |
| 164 4, params) < 0) | |
| 165 return FALSE; | |
| 166 /* Trim to the most recent 10 000 entries. */ | |
| 167 Deita_Query_Execute_Update( | |
| 168 p_store->p_connection, | |
| 169 "DELETE FROM admin_audit_log" | |
| 170 " WHERE id <= (SELECT MAX(id) FROM admin_audit_log) - 10000"); | |
| 171 return TRUE; | |
| 172 } | |
| 173 | |
| 174 static boolean auth__digest_is_valid(const char *digest) | |
| 175 { | |
| 176 if (!digest) | |
| 177 return FALSE; | |
| 178 for (size_t i = 0; i < AUTH_CRYPTO_TOKEN_DIGEST_SIZE - 1; i++) | |
| 179 { | |
| 180 uint8 c = (uint8)digest[i]; | |
| 181 if (c == '\0' || | |
| 182 !((c >= '0' && c <= '9') || | |
| 183 (c >= 'a' && c <= 'f') || | |
| 184 (c >= 'A' && c <= 'F'))) | |
| 185 return FALSE; | |
| 186 } | |
| 187 return digest[AUTH_CRYPTO_TOKEN_DIGEST_SIZE - 1] == '\0'; | |
| 188 } | |
| 189 | |
| 190 static boolean auth__session_arguments_are_valid( | |
| 191 const char *token_digest, | |
| 192 const char *csrf_digest, | |
| 193 int64 idle_ttl_secs, | |
| 194 int64 absolute_ttl_secs, | |
| 195 int64 current_unix) | |
| 196 { | |
| 197 if (!auth__digest_is_valid(token_digest) || | |
| 198 !auth__digest_is_valid(csrf_digest) || | |
| 199 current_unix < 0 || idle_ttl_secs <= 0 || absolute_ttl_secs <= 0 || | |
| 200 idle_ttl_secs > absolute_ttl_secs) | |
| 201 return FALSE; | |
| 202 if (current_unix > (int64)LLONG_MAX - idle_ttl_secs || | |
| 203 current_unix > (int64)LLONG_MAX - absolute_ttl_secs) | |
| 204 return FALSE; | |
| 205 return TRUE; | |
| 206 } | |
| 207 | |
| 208 static void auth__fill_session_record( | |
| 209 Auth_Session_Record *p_record, | |
| 210 const char *user_id, | |
| 211 int64 idle_ttl_secs, | |
| 212 int64 absolute_ttl_secs, | |
| 213 int64 current_unix, | |
| 214 int64 password_changed_at) | |
| 215 { | |
| 216 memset(p_record, 0, sizeof(*p_record)); | |
| 217 auth__copy_text_fixed(p_record->user_id, sizeof(p_record->user_id), user_id); | |
| 218 p_record->created_at = current_unix; | |
| 219 p_record->last_seen_at = current_unix; | |
| 220 p_record->idle_expires_at = current_unix + idle_ttl_secs; | |
| 221 p_record->absolute_expires_at = current_unix + absolute_ttl_secs; | |
| 222 p_record->password_changed_at_snapshot = password_changed_at; | |
| 223 } | |
| 224 | |
| 225 static Auth_Store_Result auth__insert_session_locked( | |
| 226 Auth_Store *p_store, | |
| 227 const char *user_id, | |
| 228 const char *token_digest, | |
| 229 const char *csrf_digest, | |
| 230 int64 idle_ttl_secs, | |
| 231 int64 absolute_ttl_secs, | |
| 232 int64 current_unix, | |
| 233 int64 password_changed_at) | |
| 234 { | |
| 235 char created_str[32], idle_exp_str[32], abs_exp_str[32], snap_str[32]; | |
| 236 snprintf(created_str, sizeof(created_str), "%lld", (long long)current_unix); | |
| 237 snprintf(idle_exp_str, sizeof(idle_exp_str), "%lld", | |
| 238 (long long)(current_unix + idle_ttl_secs)); | |
| 239 snprintf(abs_exp_str, sizeof(abs_exp_str), "%lld", | |
| 240 (long long)(current_unix + absolute_ttl_secs)); | |
| 241 snprintf(snap_str, sizeof(snap_str), "%lld", | |
| 242 (long long)password_changed_at); | |
| 243 | |
| 244 const char *params[] = { | |
| 245 token_digest, csrf_digest, user_id, | |
| 246 created_str, created_str, idle_exp_str, abs_exp_str, snap_str | |
| 247 }; | |
| 248 int32 result = Deita_Query_Execute_Update_Prepared( | |
| 249 p_store->p_connection, | |
| 250 "INSERT INTO auth_sessions" | |
| 251 " (token_digest, csrf_digest, user_id," | |
| 252 " created_at, last_seen_at, idle_expires_at," | |
| 253 " absolute_expires_at, password_changed_at_snapshot)" | |
| 254 " VALUES (?, ?, ?, ?, ?, ?, ?, ?)", | |
| 255 8, params); | |
| 256 return result < 0 ? AUTH_STORE_CONFLICT : AUTH_STORE_OK; | |
| 257 } | |
| 258 | |
| 259 /* ------------------------------------------------------------------ */ | |
| 260 /* Migration system */ | |
| 261 /* ------------------------------------------------------------------ */ | |
| 262 | |
| 263 static boolean auth__apply_migration(Auth_Store *p_store, | |
| 264 int32 version, | |
| 265 const char *sql) | |
| 266 { | |
| 267 char version_str[32]; | |
| 268 snprintf(version_str, sizeof(version_str), "%d", (int)version); | |
| 269 | |
| 270 if (Deita_Query_Execute_Update( | |
| 271 p_store->p_connection, "BEGIN EXCLUSIVE") < 0) | |
| 272 return FALSE; | |
| 273 | |
| 274 /* Check if already applied. */ | |
| 275 Dowa_Arena *p_arena = Dowa_Arena_Create(1024); | |
| 276 if (!p_arena) | |
| 277 { | |
| 278 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 279 return FALSE; | |
| 280 } | |
| 281 const char *check_params[] = {version_str}; | |
| 282 Deita_Result_Set *p_result = Deita_Query_Execute_Prepared( | |
| 283 p_store->p_connection, | |
| 284 "SELECT version FROM auth_schema_migrations WHERE version = ?", | |
| 285 1, check_params, p_arena); | |
| 286 boolean already = p_result && Deita_Result_Set_Next(p_result); | |
| 287 if (p_result) | |
| 288 Deita_Result_Set_Free(p_result); | |
| 289 Dowa_Arena_Free(p_arena); | |
| 290 | |
| 291 if (already) | |
| 292 { | |
| 293 if (Deita_Query_Execute_Update(p_store->p_connection, "COMMIT") < 0) | |
| 294 { | |
| 295 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 296 return FALSE; | |
| 297 } | |
| 298 return TRUE; | |
| 299 } | |
| 300 | |
| 301 /* Apply the migration. */ | |
| 302 if (Deita_Query_Execute_Update(p_store->p_connection, sql) < 0) | |
| 303 { | |
| 304 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 305 return FALSE; | |
| 306 } | |
| 307 | |
| 308 const char *ins_params[] = {version_str}; | |
| 309 if (Deita_Query_Execute_Update_Prepared( | |
| 310 p_store->p_connection, | |
| 311 "INSERT INTO auth_schema_migrations (version) VALUES (?)", | |
| 312 1, ins_params) < 0) | |
| 313 { | |
| 314 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 315 return FALSE; | |
| 316 } | |
| 317 | |
| 318 if (Deita_Query_Execute_Update(p_store->p_connection, "COMMIT") < 0) | |
| 319 { | |
| 320 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 321 return FALSE; | |
| 322 } | |
| 323 return TRUE; | |
| 324 } | |
| 325 | |
| 326 /* | |
| 327 * Migration v2: extend guest quota tables. | |
| 328 * - Backfills turns_used from the legacy count column before any new schema | |
| 329 * is applied, so existing usage is preserved (req 1). | |
| 330 * - Adds turns_used, output_tokens_used, output_tokens_reserved to guest_usage. | |
| 331 * - Replaces guest_usage_reservations with a richer schema keyed by request_id. | |
| 332 * The old reservations table is ephemeral (in-flight requests only), so | |
| 333 * dropping and recreating it is safe; legacy reservations had no token-amount | |
| 334 * column so output_tokens_reserved stays 0 after the replace. | |
| 335 */ | |
| 336 static const char *k_migration_v2 = | |
| 337 "ALTER TABLE guest_usage" | |
| 338 " ADD COLUMN turns_used INTEGER NOT NULL DEFAULT 0;" | |
| 339 "ALTER TABLE guest_usage" | |
| 340 " ADD COLUMN output_tokens_used INTEGER NOT NULL DEFAULT 0;" | |
| 341 "ALTER TABLE guest_usage" | |
| 342 " ADD COLUMN output_tokens_reserved INTEGER NOT NULL DEFAULT 0;" | |
| 343 /* Backfill turns_used from the legacy request-count column. */ | |
| 344 "UPDATE guest_usage SET turns_used = count WHERE count > 0;" | |
| 345 "DROP TABLE IF EXISTS guest_usage_reservations;" | |
| 346 "CREATE TABLE IF NOT EXISTS guest_usage_reservations (" | |
| 347 " request_id TEXT PRIMARY KEY," | |
| 348 " guest_id TEXT NOT NULL" | |
| 349 " REFERENCES guest_identities(id) ON DELETE CASCADE," | |
| 350 " window_start INTEGER NOT NULL," | |
| 351 " output_tokens_reserved INTEGER NOT NULL DEFAULT 0," | |
| 352 " reserved_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))," | |
| 353 " expires_at INTEGER NOT NULL" | |
| 354 ");" | |
| 355 "CREATE INDEX IF NOT EXISTS idx_reservations_guest" | |
| 356 " ON guest_usage_reservations(guest_id);" | |
| 357 "CREATE INDEX IF NOT EXISTS idx_reservations_expiry" | |
| 358 " ON guest_usage_reservations(expires_at);" | |
| 359 "CREATE INDEX IF NOT EXISTS idx_guest_usage_guest" | |
| 360 " ON guest_usage(guest_id);"; | |
| 361 | |
| 362 static boolean auth__run_migrations(Auth_Store *p_store) | |
| 363 { | |
| 364 if (Deita_Query_Execute_Update( | |
| 365 p_store->p_connection, | |
| 366 "CREATE TABLE IF NOT EXISTS auth_schema_migrations (" | |
| 367 " version INTEGER PRIMARY KEY," | |
| 368 " applied_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))" | |
| 369 ")") < 0) | |
| 370 return FALSE; | |
| 371 | |
| 372 if (!auth__apply_migration(p_store, 1, k_migration_v1)) | |
| 373 return FALSE; | |
| 374 return auth__apply_migration(p_store, 2, k_migration_v2); | |
| 375 } | |
| 376 | |
| 377 /* ------------------------------------------------------------------ */ | |
| 378 /* Store lifecycle */ | |
| 379 /* ------------------------------------------------------------------ */ | |
| 380 | |
| 381 /* | |
| 382 * Reap expired reservations with the mutex already held. | |
| 383 * Run atomically in a nested SAVEPOINT to avoid interfering with any outer | |
| 384 * transaction (callers sometimes hold BEGIN IMMEDIATE). | |
| 385 */ | |
| 386 static void auth__reap_expired_reservations_locked( | |
| 387 Auth_Store *p_store, | |
| 388 int64 current_unix) | |
| 389 { | |
| 390 char now_str[32]; | |
| 391 auth__i64_str(now_str, sizeof(now_str), current_unix); | |
| 392 | |
| 393 /* Use a savepoint so we can run inside or outside a transaction. */ | |
| 394 if (Deita_Query_Execute_Update( | |
| 395 p_store->p_connection, | |
| 396 "SAVEPOINT reap_expired") < 0) | |
| 397 return; | |
| 398 | |
| 399 const char *upd_params[] = {now_str, now_str}; | |
| 400 if (Deita_Query_Execute_Update_Prepared( | |
| 401 p_store->p_connection, | |
| 402 "UPDATE guest_usage" | |
| 403 " SET output_tokens_reserved = MAX(0, output_tokens_reserved - (" | |
| 404 " SELECT COALESCE(SUM(r.output_tokens_reserved), 0)" | |
| 405 " FROM guest_usage_reservations r" | |
| 406 " WHERE r.guest_id = guest_usage.guest_id" | |
| 407 " AND r.window_start = guest_usage.window_start" | |
| 408 " AND r.expires_at < ?" | |
| 409 " ))" | |
| 410 " WHERE output_tokens_reserved > 0" | |
| 411 " AND EXISTS (" | |
| 412 " SELECT 1 FROM guest_usage_reservations r2" | |
| 413 " WHERE r2.guest_id = guest_usage.guest_id" | |
| 414 " AND r2.expires_at < ?)", | |
| 415 2, upd_params) < 0) | |
| 416 { | |
| 417 Deita_Query_Execute_Update(p_store->p_connection, | |
| 418 "ROLLBACK TO reap_expired"); | |
| 419 Deita_Query_Execute_Update(p_store->p_connection, "RELEASE reap_expired"); | |
| 420 return; | |
| 421 } | |
| 422 | |
| 423 const char *del_params[] = {now_str}; | |
| 424 if (Deita_Query_Execute_Update_Prepared( | |
| 425 p_store->p_connection, | |
| 426 "DELETE FROM guest_usage_reservations WHERE expires_at < ?", | |
| 427 1, del_params) < 0) | |
| 428 { | |
| 429 Deita_Query_Execute_Update(p_store->p_connection, | |
| 430 "ROLLBACK TO reap_expired"); | |
| 431 Deita_Query_Execute_Update(p_store->p_connection, "RELEASE reap_expired"); | |
| 432 return; | |
| 433 } | |
| 434 | |
| 435 Deita_Query_Execute_Update(p_store->p_connection, "RELEASE reap_expired"); | |
| 436 } | |
| 437 | |
| 438 Auth_Store *Auth_Store_Create(const char *database_path) | |
| 439 { | |
| 440 if (!database_path) | |
| 441 return NULL; | |
| 442 | |
| 443 Auth_Store *p_store = calloc(1, sizeof(*p_store)); | |
| 444 if (!p_store) | |
| 445 return NULL; | |
| 446 | |
| 447 p_store->p_connection = Deita_Connection_Create( | |
| 448 DEITA_DATABASE_TYPE_SQLITE3, database_path); | |
| 449 if (!p_store->p_connection || | |
| 450 !Deita_Connection_Is_Open(p_store->p_connection)) | |
| 451 { | |
| 452 if (p_store->p_connection) | |
| 453 Deita_Connection_Close(p_store->p_connection); | |
| 454 free(p_store); | |
| 455 return NULL; | |
| 456 } | |
| 457 | |
| 458 if (pthread_mutex_init(&p_store->mutex, NULL) != 0) | |
| 459 { | |
| 460 Deita_Connection_Close(p_store->p_connection); | |
| 461 free(p_store); | |
| 462 return NULL; | |
| 463 } | |
| 464 | |
| 465 /* Connection-level settings — must be re-applied on every open. */ | |
| 466 if (Deita_Query_Execute_Update(p_store->p_connection, | |
| 467 "PRAGMA foreign_keys = ON;" | |
| 468 "PRAGMA journal_mode = WAL;") < 0) | |
| 469 { | |
| 470 Auth_Store_Destroy(p_store); | |
| 471 return NULL; | |
| 472 } | |
| 473 | |
| 474 if (!auth__run_migrations(p_store)) | |
| 475 { | |
| 476 Auth_Store_Destroy(p_store); | |
| 477 return NULL; | |
| 478 } | |
| 479 | |
| 480 /* Reap any expired reservations left over from a previous run. */ | |
| 481 pthread_mutex_lock(&p_store->mutex); | |
| 482 auth__reap_expired_reservations_locked(p_store, (int64)time(NULL)); | |
| 483 pthread_mutex_unlock(&p_store->mutex); | |
| 484 | |
| 485 return p_store; | |
| 486 } | |
| 487 | |
| 488 void Auth_Store_Destroy(Auth_Store *p_store) | |
| 489 { | |
| 490 if (!p_store) | |
| 491 return; | |
| 492 if (p_store->p_connection) | |
| 493 Deita_Connection_Close(p_store->p_connection); | |
| 494 pthread_mutex_destroy(&p_store->mutex); | |
| 495 free(p_store); | |
| 496 } | |
| 497 | |
| 498 /* ------------------------------------------------------------------ */ | |
| 499 /* Username utilities */ | |
| 500 /* ------------------------------------------------------------------ */ | |
| 501 | |
| 502 boolean Auth_Store_Normalize_Username( | |
| 503 const char *username, | |
| 504 char *normalized, | |
| 505 size_t capacity) | |
| 506 { | |
| 507 if (!username || !normalized || capacity == 0) | |
| 508 return FALSE; | |
| 509 | |
| 510 size_t len = strlen(username); | |
| 511 size_t start = 0; | |
| 512 size_t end = len; | |
| 513 | |
| 514 while (start < len && username[start] == ' ') | |
| 515 start++; | |
| 516 while (end > start && username[end - 1] == ' ') | |
| 517 end--; | |
| 518 | |
| 519 size_t norm_len = end - start; | |
| 520 if (norm_len < AUTH_STORE_USERNAME_MIN || | |
| 521 norm_len > AUTH_STORE_USERNAME_MAX) | |
| 522 return FALSE; | |
| 523 if (capacity <= norm_len) | |
| 524 return FALSE; | |
| 525 | |
| 526 for (size_t i = 0; i < norm_len; i++) | |
| 527 { | |
| 528 uint8 c = (uint8)username[start + i]; | |
| 529 if (c >= 'A' && c <= 'Z') | |
| 530 c = (uint8)(c - 'A' + 'a'); | |
| 531 else if ((c >= 'a' && c <= 'z') || | |
| 532 (c >= '0' && c <= '9') || | |
| 533 c == '_' || c == '-' || c == '.') | |
| 534 ; /* valid as-is */ | |
| 535 else | |
| 536 return FALSE; | |
| 537 normalized[i] = (char)c; | |
| 538 } | |
| 539 normalized[norm_len] = '\0'; | |
| 540 return TRUE; | |
| 541 } | |
| 542 | |
| 543 boolean Auth_Store_Validate_Username(const char *normalized_username) | |
| 544 { | |
| 545 if (!normalized_username) | |
| 546 return FALSE; | |
| 547 size_t i = 0; | |
| 548 while (normalized_username[i] != '\0') | |
| 549 { | |
| 550 if (i >= AUTH_STORE_USERNAME_MAX) | |
| 551 return FALSE; | |
| 552 uint8 c = (uint8)normalized_username[i]; | |
| 553 if (!((c >= 'a' && c <= 'z') || | |
| 554 (c >= '0' && c <= '9') || | |
| 555 c == '_' || c == '-' || c == '.')) | |
| 556 return FALSE; | |
| 557 i++; | |
| 558 } | |
| 559 return i >= AUTH_STORE_USERNAME_MIN; | |
| 560 } | |
| 561 | |
| 562 /* ------------------------------------------------------------------ */ | |
| 563 /* Internal: populate Auth_User_Record from an open result set */ | |
| 564 /* ------------------------------------------------------------------ */ | |
| 565 | |
| 566 /* | |
| 567 * Columns expected (0-based): | |
| 568 * 0 id, 1 username, 2 normalized_username, 3 role, 4 status, | |
| 569 * 5 must_change_password, 6 password_changed_at, 7 created_at, 8 updated_at | |
| 570 */ | |
| 571 static void auth__read_user_record(Auth_User_Record *r, | |
| 572 Deita_Result_Set *p) | |
| 573 { | |
| 574 auth__copy_text_fixed(r->id, sizeof(r->id), | |
| 575 Deita_Result_Set_Get_Text(p, 0)); | |
| 576 auth__copy_text_fixed(r->username, sizeof(r->username), | |
| 577 Deita_Result_Set_Get_Text(p, 1)); | |
| 578 auth__copy_text_fixed(r->normalized_username,sizeof(r->normalized_username), | |
| 579 Deita_Result_Set_Get_Text(p, 2)); | |
| 580 auth__copy_text_fixed(r->role, sizeof(r->role), | |
| 581 Deita_Result_Set_Get_Text(p, 3)); | |
| 582 auth__copy_text_fixed(r->status, sizeof(r->status), | |
| 583 Deita_Result_Set_Get_Text(p, 4)); | |
| 584 r->must_change_password = Deita_Result_Set_Get_Integer(p, 5) ? TRUE : FALSE; | |
| 585 r->password_changed_at = Deita_Result_Set_Get_Integer(p, 6); | |
| 586 r->created_at = Deita_Result_Set_Get_Integer(p, 7); | |
| 587 r->updated_at = Deita_Result_Set_Get_Integer(p, 8); | |
| 588 } | |
| 589 | |
| 590 /* ------------------------------------------------------------------ */ | |
| 591 /* User management */ | |
| 592 /* ------------------------------------------------------------------ */ | |
| 593 | |
| 594 Auth_Store_Result Auth_Store_Create_User( | |
| 595 Auth_Store *p_store, | |
| 596 const char *username, | |
| 597 const char *encoded_hash, | |
| 598 const char *role, | |
| 599 boolean must_change_password, | |
| 600 char output_id[37]) | |
| 601 { | |
| 602 if (!p_store || !username || !encoded_hash || !role || !output_id) | |
| 603 return AUTH_STORE_INVALID_ARG; | |
| 604 if (encoded_hash[0] == '\0') | |
| 605 return AUTH_STORE_INVALID_ARG; | |
| 606 if (strcmp(role, "admin") != 0 && strcmp(role, "member") != 0) | |
| 607 return AUTH_STORE_INVALID_ARG; | |
| 608 | |
| 609 char normalized[AUTH_STORE_USERNAME_MAX + 1]; | |
| 610 if (!Auth_Store_Normalize_Username( | |
| 611 username, normalized, sizeof(normalized))) | |
| 612 return AUTH_STORE_INVALID_ARG; | |
| 613 | |
| 614 if (!auth__generate_uuid(output_id)) | |
| 615 return AUTH_STORE_ERROR; | |
| 616 | |
| 617 const char *mcp_str = must_change_password ? "1" : "0"; | |
| 618 const char *params[] = { | |
| 619 output_id, username, normalized, encoded_hash, role, mcp_str | |
| 620 }; | |
| 621 | |
| 622 pthread_mutex_lock(&p_store->mutex); | |
| 623 int32 result = Deita_Query_Execute_Update_Prepared( | |
| 624 p_store->p_connection, | |
| 625 "INSERT INTO users" | |
| 626 " (id, username, normalized_username, password_hash, role," | |
| 627 " must_change_password)" | |
| 628 " VALUES (?, ?, ?, ?, ?, ?)", | |
| 629 6, params); | |
| 630 pthread_mutex_unlock(&p_store->mutex); | |
| 631 | |
| 632 if (result < 0) | |
| 633 return AUTH_STORE_CONFLICT; /* most likely UNIQUE constraint on norm_name */ | |
| 634 return AUTH_STORE_OK; | |
| 635 } | |
| 636 | |
| 637 Auth_Store_Result Auth_Store_Create_User_Audited( | |
| 638 Auth_Store *p_store, | |
| 639 const char *username, | |
| 640 const char *encoded_hash, | |
| 641 const char *role, | |
| 642 boolean must_change_password, | |
| 643 const char *actor_user_id, | |
| 644 char output_id[37]) | |
| 645 { | |
| 646 if (!p_store || !username || !encoded_hash || !role || !output_id) | |
| 647 return AUTH_STORE_INVALID_ARG; | |
| 648 if (encoded_hash[0] == '\0') | |
| 649 return AUTH_STORE_INVALID_ARG; | |
| 650 if (strcmp(role, "admin") != 0 && strcmp(role, "member") != 0) | |
| 651 return AUTH_STORE_INVALID_ARG; | |
| 652 | |
| 653 char normalized[AUTH_STORE_USERNAME_MAX + 1]; | |
| 654 if (!Auth_Store_Normalize_Username( | |
| 655 username, normalized, sizeof(normalized))) | |
| 656 return AUTH_STORE_INVALID_ARG; | |
| 657 if (!auth__generate_uuid(output_id)) | |
| 658 return AUTH_STORE_ERROR; | |
| 659 | |
| 660 const char *mcp_str = must_change_password ? "1" : "0"; | |
| 661 const char *params[] = { | |
| 662 output_id, username, normalized, encoded_hash, role, mcp_str | |
| 663 }; | |
| 664 | |
| 665 pthread_mutex_lock(&p_store->mutex); | |
| 666 if (Deita_Query_Execute_Update( | |
| 667 p_store->p_connection, "BEGIN IMMEDIATE") < 0) | |
| 668 { | |
| 669 pthread_mutex_unlock(&p_store->mutex); | |
| 670 return AUTH_STORE_ERROR; | |
| 671 } | |
| 672 | |
| 673 int32 result = Deita_Query_Execute_Update_Prepared( | |
| 674 p_store->p_connection, | |
| 675 "INSERT INTO users" | |
| 676 " (id, username, normalized_username, password_hash, role," | |
| 677 " must_change_password)" | |
| 678 " VALUES (?, ?, ?, ?, ?, ?)", | |
| 679 6, params); | |
| 680 if (result < 0) | |
| 681 { | |
| 682 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 683 pthread_mutex_unlock(&p_store->mutex); | |
| 684 return AUTH_STORE_CONFLICT; | |
| 685 } | |
| 686 | |
| 687 if (!auth__insert_audit_log_locked( | |
| 688 p_store, actor_user_id, "admin_user_created", output_id, role)) | |
| 689 { | |
| 690 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 691 pthread_mutex_unlock(&p_store->mutex); | |
| 692 return AUTH_STORE_ERROR; | |
| 693 } | |
| 694 | |
| 695 if (Deita_Query_Execute_Update(p_store->p_connection, "COMMIT") < 0) | |
| 696 { | |
| 697 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 698 pthread_mutex_unlock(&p_store->mutex); | |
| 699 return AUTH_STORE_ERROR; | |
| 700 } | |
| 701 | |
| 702 pthread_mutex_unlock(&p_store->mutex); | |
| 703 return AUTH_STORE_OK; | |
| 704 } | |
| 705 | |
| 706 Auth_Store_Result Auth_Store_Bootstrap_Admin( | |
| 707 Auth_Store *p_store, | |
| 708 const char *username, | |
| 709 const char *encoded_hash, | |
| 710 Auth_Store_Bootstrap_Result *p_bootstrap_result, | |
| 711 char output_id[37]) | |
| 712 { | |
| 713 if (!p_store || !username || !encoded_hash || !p_bootstrap_result) | |
| 714 return AUTH_STORE_INVALID_ARG; | |
| 715 if (encoded_hash[0] == '\0') | |
| 716 return AUTH_STORE_INVALID_ARG; | |
| 717 | |
| 718 char normalized[AUTH_STORE_USERNAME_MAX + 1]; | |
| 719 if (!Auth_Store_Normalize_Username( | |
| 720 username, normalized, sizeof(normalized))) | |
| 721 return AUTH_STORE_INVALID_ARG; | |
| 722 | |
| 723 char id_buf[37]; | |
| 724 if (!auth__generate_uuid(id_buf)) | |
| 725 return AUTH_STORE_ERROR; | |
| 726 | |
| 727 pthread_mutex_lock(&p_store->mutex); | |
| 728 | |
| 729 if (Deita_Query_Execute_Update( | |
| 730 p_store->p_connection, "BEGIN IMMEDIATE") < 0) | |
| 731 { | |
| 732 pthread_mutex_unlock(&p_store->mutex); | |
| 733 return AUTH_STORE_ERROR; | |
| 734 } | |
| 735 | |
| 736 /* Check for any existing admin (active or disabled). */ | |
| 737 Dowa_Arena *p_arena = Dowa_Arena_Create(2048); | |
| 738 if (!p_arena) | |
| 739 { | |
| 740 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 741 pthread_mutex_unlock(&p_store->mutex); | |
| 742 return AUTH_STORE_ERROR; | |
| 743 } | |
| 744 | |
| 745 Deita_Result_Set *p_result = Deita_Query_Execute( | |
| 746 p_store->p_connection, | |
| 747 "SELECT id FROM users WHERE role = 'admin' LIMIT 1", | |
| 748 p_arena); | |
| 749 boolean admin_exists = p_result && Deita_Result_Set_Next(p_result); | |
| 750 char existing_id[37] = {0}; | |
| 751 if (admin_exists && p_result) | |
| 752 auth__copy_text_fixed(existing_id, sizeof(existing_id), | |
| 753 Deita_Result_Set_Get_Text(p_result, 0)); | |
| 754 if (p_result) | |
| 755 Deita_Result_Set_Free(p_result); | |
| 756 Dowa_Arena_Free(p_arena); | |
| 757 | |
| 758 if (admin_exists) | |
| 759 { | |
| 760 if (Deita_Query_Execute_Update(p_store->p_connection, "COMMIT") < 0) | |
| 761 { | |
| 762 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 763 pthread_mutex_unlock(&p_store->mutex); | |
| 764 return AUTH_STORE_ERROR; | |
| 765 } | |
| 766 pthread_mutex_unlock(&p_store->mutex); | |
| 767 *p_bootstrap_result = AUTH_STORE_BOOTSTRAP_ALREADY_PRESENT; | |
| 768 if (output_id) | |
| 769 auth__copy_text_fixed(output_id, 37, existing_id); | |
| 770 return AUTH_STORE_OK; | |
| 771 } | |
| 772 | |
| 773 const char *params[] = { | |
| 774 id_buf, username, normalized, encoded_hash, "admin" | |
| 775 }; | |
| 776 int32 ins = Deita_Query_Execute_Update_Prepared( | |
| 777 p_store->p_connection, | |
| 778 "INSERT INTO users" | |
| 779 " (id, username, normalized_username, password_hash, role)" | |
| 780 " VALUES (?, ?, ?, ?, ?)", | |
| 781 5, params); | |
| 782 if (ins < 0) | |
| 783 { | |
| 784 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 785 pthread_mutex_unlock(&p_store->mutex); | |
| 786 return AUTH_STORE_ERROR; | |
| 787 } | |
| 788 | |
| 789 if (!auth__insert_audit_log_locked( | |
| 790 p_store, NULL, "bootstrap_admin_created", id_buf, NULL)) | |
| 791 { | |
| 792 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 793 pthread_mutex_unlock(&p_store->mutex); | |
| 794 return AUTH_STORE_ERROR; | |
| 795 } | |
| 796 | |
| 797 if (Deita_Query_Execute_Update(p_store->p_connection, "COMMIT") < 0) | |
| 798 { | |
| 799 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 800 pthread_mutex_unlock(&p_store->mutex); | |
| 801 return AUTH_STORE_ERROR; | |
| 802 } | |
| 803 | |
| 804 pthread_mutex_unlock(&p_store->mutex); | |
| 805 *p_bootstrap_result = AUTH_STORE_BOOTSTRAP_CREATED; | |
| 806 if (output_id) | |
| 807 auth__copy_text_fixed(output_id, 37, id_buf); | |
| 808 return AUTH_STORE_OK; | |
| 809 } | |
| 810 | |
| 811 Auth_Store_Result Auth_Store_Find_User_By_Username( | |
| 812 Auth_Store *p_store, | |
| 813 const char *username, | |
| 814 Auth_User_Auth_Record *p_record) | |
| 815 { | |
| 816 if (!p_store || !username || !p_record) | |
| 817 return AUTH_STORE_INVALID_ARG; | |
| 818 | |
| 819 char normalized[AUTH_STORE_USERNAME_MAX + 1]; | |
| 820 if (!Auth_Store_Normalize_Username( | |
| 821 username, normalized, sizeof(normalized))) | |
| 822 return AUTH_STORE_NOT_FOUND; | |
| 823 | |
| 824 memset(p_record, 0, sizeof(*p_record)); | |
| 825 | |
| 826 Dowa_Arena *p_arena = Dowa_Arena_Create(2048); | |
| 827 if (!p_arena) | |
| 828 return AUTH_STORE_ERROR; | |
| 829 | |
| 830 const char *params[] = {normalized}; | |
| 831 pthread_mutex_lock(&p_store->mutex); | |
| 832 Deita_Result_Set *p_result = Deita_Query_Execute_Prepared( | |
| 833 p_store->p_connection, | |
| 834 "SELECT id, username, normalized_username, role, status," | |
| 835 " must_change_password, password_changed_at," | |
| 836 " created_at, updated_at, password_hash" | |
| 837 " FROM users WHERE normalized_username = ?", | |
| 838 1, params, p_arena); | |
| 839 if (!p_result || !Deita_Result_Set_Next(p_result)) | |
| 840 { | |
| 841 if (p_result) | |
| 842 Deita_Result_Set_Free(p_result); | |
| 843 pthread_mutex_unlock(&p_store->mutex); | |
| 844 Dowa_Arena_Free(p_arena); | |
| 845 return AUTH_STORE_NOT_FOUND; | |
| 846 } | |
| 847 auth__read_user_record(&p_record->user, p_result); | |
| 848 auth__copy_text_fixed(p_record->password_hash, | |
| 849 sizeof(p_record->password_hash), | |
| 850 Deita_Result_Set_Get_Text(p_result, 9)); | |
| 851 Deita_Result_Set_Free(p_result); | |
| 852 pthread_mutex_unlock(&p_store->mutex); | |
| 853 Dowa_Arena_Free(p_arena); | |
| 854 return AUTH_STORE_OK; | |
| 855 } | |
| 856 | |
| 857 Auth_Store_Result Auth_Store_Get_User( | |
| 858 Auth_Store *p_store, | |
| 859 const char *user_id, | |
| 860 Auth_User_Record *p_record) | |
| 861 { | |
| 862 if (!p_store || !user_id || !p_record) | |
| 863 return AUTH_STORE_INVALID_ARG; | |
| 864 | |
| 865 memset(p_record, 0, sizeof(*p_record)); | |
| 866 | |
| 867 Dowa_Arena *p_arena = Dowa_Arena_Create(2048); | |
| 868 if (!p_arena) | |
| 869 return AUTH_STORE_ERROR; | |
| 870 | |
| 871 const char *params[] = {user_id}; | |
| 872 pthread_mutex_lock(&p_store->mutex); | |
| 873 Deita_Result_Set *p_result = Deita_Query_Execute_Prepared( | |
| 874 p_store->p_connection, | |
| 875 "SELECT id, username, normalized_username, role, status," | |
| 876 " must_change_password, password_changed_at," | |
| 877 " created_at, updated_at" | |
| 878 " FROM users WHERE id = ?", | |
| 879 1, params, p_arena); | |
| 880 if (!p_result || !Deita_Result_Set_Next(p_result)) | |
| 881 { | |
| 882 if (p_result) | |
| 883 Deita_Result_Set_Free(p_result); | |
| 884 pthread_mutex_unlock(&p_store->mutex); | |
| 885 Dowa_Arena_Free(p_arena); | |
| 886 return AUTH_STORE_NOT_FOUND; | |
| 887 } | |
| 888 auth__read_user_record(p_record, p_result); | |
| 889 Deita_Result_Set_Free(p_result); | |
| 890 pthread_mutex_unlock(&p_store->mutex); | |
| 891 Dowa_Arena_Free(p_arena); | |
| 892 return AUTH_STORE_OK; | |
| 893 } | |
| 894 | |
| 895 Auth_Store_Result Auth_Store_List_Users( | |
| 896 Auth_Store *p_store, | |
| 897 Auth_User_Record **pp_records, | |
| 898 Dowa_Arena *p_arena) | |
| 899 { | |
| 900 if (!p_store || !pp_records || !p_arena) | |
| 901 return AUTH_STORE_INVALID_ARG; | |
| 902 | |
| 903 *pp_records = NULL; | |
| 904 | |
| 905 Dowa_Arena *p_local = Dowa_Arena_Create(2048); | |
| 906 if (!p_local) | |
| 907 return AUTH_STORE_ERROR; | |
| 908 | |
| 909 pthread_mutex_lock(&p_store->mutex); | |
| 910 Deita_Result_Set *p_result = Deita_Query_Execute( | |
| 911 p_store->p_connection, | |
| 912 "SELECT id, username, normalized_username, role, status," | |
| 913 " must_change_password, password_changed_at," | |
| 914 " created_at, updated_at" | |
| 915 " FROM users ORDER BY created_at", | |
| 916 p_local); | |
| 917 if (!p_result) | |
| 918 { | |
| 919 pthread_mutex_unlock(&p_store->mutex); | |
| 920 Dowa_Arena_Free(p_local); | |
| 921 return AUTH_STORE_ERROR; | |
| 922 } | |
| 923 | |
| 924 Auth_User_Record *records = NULL; | |
| 925 while (Deita_Result_Set_Next(p_result)) | |
| 926 { | |
| 927 Auth_User_Record record; | |
| 928 memset(&record, 0, sizeof(record)); | |
| 929 auth__read_user_record(&record, p_result); | |
| 930 Dowa_Array_Push_Arena(records, record, p_arena); | |
| 931 } | |
| 932 boolean err = Deita_Result_Set_Has_Error(p_result); | |
| 933 Deita_Result_Set_Free(p_result); | |
| 934 pthread_mutex_unlock(&p_store->mutex); | |
| 935 Dowa_Arena_Free(p_local); | |
| 936 | |
| 937 if (err) | |
| 938 return AUTH_STORE_ERROR; | |
| 939 | |
| 940 *pp_records = records; | |
| 941 return AUTH_STORE_OK; | |
| 942 } | |
| 943 | |
| 944 static Auth_Store_Result auth__update_user_status( | |
| 945 Auth_Store *p_store, | |
| 946 const char *user_id, | |
| 947 const char *new_status, | |
| 948 const char *actor_user_id, | |
| 949 boolean revoke_sessions) | |
| 950 { | |
| 951 if (!p_store || !user_id || !new_status) | |
| 952 return AUTH_STORE_INVALID_ARG; | |
| 953 if (strcmp(new_status, "active") != 0 && | |
| 954 strcmp(new_status, "disabled") != 0) | |
| 955 return AUTH_STORE_INVALID_ARG; | |
| 956 | |
| 957 pthread_mutex_lock(&p_store->mutex); | |
| 958 | |
| 959 if (Deita_Query_Execute_Update( | |
| 960 p_store->p_connection, "BEGIN IMMEDIATE") < 0) | |
| 961 { | |
| 962 pthread_mutex_unlock(&p_store->mutex); | |
| 963 return AUTH_STORE_ERROR; | |
| 964 } | |
| 965 | |
| 966 /* Fetch current role and status. */ | |
| 967 Dowa_Arena *p_arena = Dowa_Arena_Create(2048); | |
| 968 if (!p_arena) | |
| 969 { | |
| 970 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 971 pthread_mutex_unlock(&p_store->mutex); | |
| 972 return AUTH_STORE_ERROR; | |
| 973 } | |
| 974 const char *sel_params[] = {user_id}; | |
| 975 Deita_Result_Set *p_result = Deita_Query_Execute_Prepared( | |
| 976 p_store->p_connection, | |
| 977 "SELECT role, status FROM users WHERE id = ?", | |
| 978 1, sel_params, p_arena); | |
| 979 if (!p_result || !Deita_Result_Set_Next(p_result)) | |
| 980 { | |
| 981 if (p_result) | |
| 982 Deita_Result_Set_Free(p_result); | |
| 983 Dowa_Arena_Free(p_arena); | |
| 984 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 985 pthread_mutex_unlock(&p_store->mutex); | |
| 986 return AUTH_STORE_NOT_FOUND; | |
| 987 } | |
| 988 char cur_role[8], cur_status[9]; | |
| 989 auth__copy_text_fixed(cur_role, sizeof(cur_role), | |
| 990 Deita_Result_Set_Get_Text(p_result, 0)); | |
| 991 auth__copy_text_fixed(cur_status, sizeof(cur_status), | |
| 992 Deita_Result_Set_Get_Text(p_result, 1)); | |
| 993 Deita_Result_Set_Free(p_result); | |
| 994 Dowa_Arena_Free(p_arena); | |
| 995 | |
| 996 /* Last-admin protection: prevent disabling the final active admin. */ | |
| 997 if (strcmp(new_status, "disabled") == 0 && | |
| 998 strcmp(cur_role, "admin") == 0 && | |
| 999 strcmp(cur_status, "active") == 0) | |
| 1000 { | |
| 1001 Dowa_Arena *p_count_arena = Dowa_Arena_Create(1024); | |
| 1002 if (!p_count_arena) | |
| 1003 { | |
| 1004 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1005 pthread_mutex_unlock(&p_store->mutex); | |
| 1006 return AUTH_STORE_ERROR; | |
| 1007 } | |
| 1008 p_result = Deita_Query_Execute( | |
| 1009 p_store->p_connection, | |
| 1010 "SELECT COUNT(*) FROM users WHERE role = 'admin' AND status = 'active'", | |
| 1011 p_count_arena); | |
| 1012 int64 count = 0; | |
| 1013 if (p_result && Deita_Result_Set_Next(p_result)) | |
| 1014 count = Deita_Result_Set_Get_Integer(p_result, 0); | |
| 1015 if (p_result) | |
| 1016 Deita_Result_Set_Free(p_result); | |
| 1017 Dowa_Arena_Free(p_count_arena); | |
| 1018 | |
| 1019 if (count <= 1) | |
| 1020 { | |
| 1021 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1022 pthread_mutex_unlock(&p_store->mutex); | |
| 1023 return AUTH_STORE_LAST_ADMIN; | |
| 1024 } | |
| 1025 } | |
| 1026 | |
| 1027 const char *upd_params[] = {new_status, user_id}; | |
| 1028 int32 upd = Deita_Query_Execute_Update_Prepared( | |
| 1029 p_store->p_connection, | |
| 1030 "UPDATE users SET status = ?, updated_at = strftime('%s','now')" | |
| 1031 " WHERE id = ?", | |
| 1032 2, upd_params); | |
| 1033 if (upd <= 0) | |
| 1034 { | |
| 1035 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1036 pthread_mutex_unlock(&p_store->mutex); | |
| 1037 return upd < 0 ? AUTH_STORE_ERROR : AUTH_STORE_NOT_FOUND; | |
| 1038 } | |
| 1039 | |
| 1040 if (revoke_sessions) | |
| 1041 { | |
| 1042 const char *rev_params[] = {user_id}; | |
| 1043 if (Deita_Query_Execute_Update_Prepared( | |
| 1044 p_store->p_connection, | |
| 1045 "UPDATE auth_sessions SET revoked_at = strftime('%s','now')" | |
| 1046 " WHERE user_id = ? AND revoked_at IS NULL", | |
| 1047 1, rev_params) < 0) | |
| 1048 { | |
| 1049 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1050 pthread_mutex_unlock(&p_store->mutex); | |
| 1051 return AUTH_STORE_ERROR; | |
| 1052 } | |
| 1053 } | |
| 1054 | |
| 1055 char detail[64]; | |
| 1056 snprintf(detail, sizeof(detail), "status->%s", new_status); | |
| 1057 if (!auth__insert_audit_log_locked( | |
| 1058 p_store, actor_user_id, "user_status_updated", user_id, detail)) | |
| 1059 { | |
| 1060 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1061 pthread_mutex_unlock(&p_store->mutex); | |
| 1062 return AUTH_STORE_ERROR; | |
| 1063 } | |
| 1064 | |
| 1065 if (Deita_Query_Execute_Update(p_store->p_connection, "COMMIT") < 0) | |
| 1066 { | |
| 1067 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1068 pthread_mutex_unlock(&p_store->mutex); | |
| 1069 return AUTH_STORE_ERROR; | |
| 1070 } | |
| 1071 | |
| 1072 pthread_mutex_unlock(&p_store->mutex); | |
| 1073 return AUTH_STORE_OK; | |
| 1074 } | |
| 1075 | |
| 1076 Auth_Store_Result Auth_Store_Update_User_Status( | |
| 1077 Auth_Store *p_store, | |
| 1078 const char *user_id, | |
| 1079 const char *new_status, | |
| 1080 const char *actor_user_id) | |
| 1081 { | |
| 1082 return auth__update_user_status( | |
| 1083 p_store, user_id, new_status, actor_user_id, FALSE); | |
| 1084 } | |
| 1085 | |
| 1086 Auth_Store_Result Auth_Store_Enable_User( | |
| 1087 Auth_Store *p_store, | |
| 1088 const char *user_id, | |
| 1089 const char *actor_user_id) | |
| 1090 { | |
| 1091 return auth__update_user_status( | |
| 1092 p_store, user_id, "active", actor_user_id, FALSE); | |
| 1093 } | |
| 1094 | |
| 1095 Auth_Store_Result Auth_Store_Disable_User_And_Revoke_Sessions( | |
| 1096 Auth_Store *p_store, | |
| 1097 const char *user_id, | |
| 1098 const char *actor_user_id) | |
| 1099 { | |
| 1100 return auth__update_user_status( | |
| 1101 p_store, user_id, "disabled", actor_user_id, TRUE); | |
| 1102 } | |
| 1103 | |
| 1104 static Auth_Store_Result auth__update_user_role( | |
| 1105 Auth_Store *p_store, | |
| 1106 const char *user_id, | |
| 1107 const char *new_role, | |
| 1108 const char *actor_user_id, | |
| 1109 boolean revoke_sessions) | |
| 1110 { | |
| 1111 if (!p_store || !user_id || !new_role) | |
| 1112 return AUTH_STORE_INVALID_ARG; | |
| 1113 if (strcmp(new_role, "admin") != 0 && strcmp(new_role, "member") != 0) | |
| 1114 return AUTH_STORE_INVALID_ARG; | |
| 1115 | |
| 1116 pthread_mutex_lock(&p_store->mutex); | |
| 1117 | |
| 1118 if (Deita_Query_Execute_Update( | |
| 1119 p_store->p_connection, "BEGIN IMMEDIATE") < 0) | |
| 1120 { | |
| 1121 pthread_mutex_unlock(&p_store->mutex); | |
| 1122 return AUTH_STORE_ERROR; | |
| 1123 } | |
| 1124 | |
| 1125 Dowa_Arena *p_arena = Dowa_Arena_Create(2048); | |
| 1126 if (!p_arena) | |
| 1127 { | |
| 1128 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1129 pthread_mutex_unlock(&p_store->mutex); | |
| 1130 return AUTH_STORE_ERROR; | |
| 1131 } | |
| 1132 const char *sel_params[] = {user_id}; | |
| 1133 Deita_Result_Set *p_result = Deita_Query_Execute_Prepared( | |
| 1134 p_store->p_connection, | |
| 1135 "SELECT role, status FROM users WHERE id = ?", | |
| 1136 1, sel_params, p_arena); | |
| 1137 if (!p_result || !Deita_Result_Set_Next(p_result)) | |
| 1138 { | |
| 1139 if (p_result) | |
| 1140 Deita_Result_Set_Free(p_result); | |
| 1141 Dowa_Arena_Free(p_arena); | |
| 1142 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1143 pthread_mutex_unlock(&p_store->mutex); | |
| 1144 return AUTH_STORE_NOT_FOUND; | |
| 1145 } | |
| 1146 char cur_role[8], cur_status[9]; | |
| 1147 auth__copy_text_fixed(cur_role, sizeof(cur_role), | |
| 1148 Deita_Result_Set_Get_Text(p_result, 0)); | |
| 1149 auth__copy_text_fixed(cur_status, sizeof(cur_status), | |
| 1150 Deita_Result_Set_Get_Text(p_result, 1)); | |
| 1151 Deita_Result_Set_Free(p_result); | |
| 1152 Dowa_Arena_Free(p_arena); | |
| 1153 | |
| 1154 /* Last-admin protection: prevent demoting the final active admin. */ | |
| 1155 if (strcmp(new_role, "member") == 0 && | |
| 1156 strcmp(cur_role, "admin") == 0 && | |
| 1157 strcmp(cur_status, "active") == 0) | |
| 1158 { | |
| 1159 Dowa_Arena *p_count_arena = Dowa_Arena_Create(1024); | |
| 1160 if (!p_count_arena) | |
| 1161 { | |
| 1162 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1163 pthread_mutex_unlock(&p_store->mutex); | |
| 1164 return AUTH_STORE_ERROR; | |
| 1165 } | |
| 1166 p_result = Deita_Query_Execute( | |
| 1167 p_store->p_connection, | |
| 1168 "SELECT COUNT(*) FROM users WHERE role = 'admin' AND status = 'active'", | |
| 1169 p_count_arena); | |
| 1170 int64 count = 0; | |
| 1171 if (p_result && Deita_Result_Set_Next(p_result)) | |
| 1172 count = Deita_Result_Set_Get_Integer(p_result, 0); | |
| 1173 if (p_result) | |
| 1174 Deita_Result_Set_Free(p_result); | |
| 1175 Dowa_Arena_Free(p_count_arena); | |
| 1176 | |
| 1177 if (count <= 1) | |
| 1178 { | |
| 1179 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1180 pthread_mutex_unlock(&p_store->mutex); | |
| 1181 return AUTH_STORE_LAST_ADMIN; | |
| 1182 } | |
| 1183 } | |
| 1184 | |
| 1185 const char *upd_params[] = {new_role, user_id}; | |
| 1186 int32 upd = Deita_Query_Execute_Update_Prepared( | |
| 1187 p_store->p_connection, | |
| 1188 "UPDATE users SET role = ?, updated_at = strftime('%s','now')" | |
| 1189 " WHERE id = ?", | |
| 1190 2, upd_params); | |
| 1191 if (upd <= 0) | |
| 1192 { | |
| 1193 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1194 pthread_mutex_unlock(&p_store->mutex); | |
| 1195 return upd < 0 ? AUTH_STORE_ERROR : AUTH_STORE_NOT_FOUND; | |
| 1196 } | |
| 1197 | |
| 1198 if (revoke_sessions) | |
| 1199 { | |
| 1200 const char *rev_params[] = {user_id}; | |
| 1201 if (Deita_Query_Execute_Update_Prepared( | |
| 1202 p_store->p_connection, | |
| 1203 "UPDATE auth_sessions SET revoked_at = strftime('%s','now')" | |
| 1204 " WHERE user_id = ? AND revoked_at IS NULL", | |
| 1205 1, rev_params) < 0) | |
| 1206 { | |
| 1207 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1208 pthread_mutex_unlock(&p_store->mutex); | |
| 1209 return AUTH_STORE_ERROR; | |
| 1210 } | |
| 1211 } | |
| 1212 | |
| 1213 char detail[64]; | |
| 1214 snprintf(detail, sizeof(detail), "role->%s", new_role); | |
| 1215 if (!auth__insert_audit_log_locked( | |
| 1216 p_store, actor_user_id, "user_role_updated", user_id, detail)) | |
| 1217 { | |
| 1218 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1219 pthread_mutex_unlock(&p_store->mutex); | |
| 1220 return AUTH_STORE_ERROR; | |
| 1221 } | |
| 1222 | |
| 1223 if (Deita_Query_Execute_Update(p_store->p_connection, "COMMIT") < 0) | |
| 1224 { | |
| 1225 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1226 pthread_mutex_unlock(&p_store->mutex); | |
| 1227 return AUTH_STORE_ERROR; | |
| 1228 } | |
| 1229 | |
| 1230 pthread_mutex_unlock(&p_store->mutex); | |
| 1231 return AUTH_STORE_OK; | |
| 1232 } | |
| 1233 | |
| 1234 Auth_Store_Result Auth_Store_Update_User_Role( | |
| 1235 Auth_Store *p_store, | |
| 1236 const char *user_id, | |
| 1237 const char *new_role, | |
| 1238 const char *actor_user_id) | |
| 1239 { | |
| 1240 return auth__update_user_role( | |
| 1241 p_store, user_id, new_role, actor_user_id, FALSE); | |
| 1242 } | |
| 1243 | |
| 1244 Auth_Store_Result Auth_Store_Update_Role_And_Revoke_Sessions( | |
| 1245 Auth_Store *p_store, | |
| 1246 const char *user_id, | |
| 1247 const char *new_role, | |
| 1248 const char *actor_user_id) | |
| 1249 { | |
| 1250 return auth__update_user_role( | |
| 1251 p_store, user_id, new_role, actor_user_id, TRUE); | |
| 1252 } | |
| 1253 | |
| 1254 Auth_Store_Result Auth_Store_Set_Must_Change_Password( | |
| 1255 Auth_Store *p_store, | |
| 1256 const char *user_id, | |
| 1257 boolean value, | |
| 1258 const char *actor_user_id) | |
| 1259 { | |
| 1260 if (!p_store || !user_id) | |
| 1261 return AUTH_STORE_INVALID_ARG; | |
| 1262 | |
| 1263 const char *val_str = value ? "1" : "0"; | |
| 1264 const char *params[] = {val_str, user_id}; | |
| 1265 | |
| 1266 pthread_mutex_lock(&p_store->mutex); | |
| 1267 | |
| 1268 if (Deita_Query_Execute_Update( | |
| 1269 p_store->p_connection, "BEGIN IMMEDIATE") < 0) | |
| 1270 { | |
| 1271 pthread_mutex_unlock(&p_store->mutex); | |
| 1272 return AUTH_STORE_ERROR; | |
| 1273 } | |
| 1274 | |
| 1275 int32 upd = Deita_Query_Execute_Update_Prepared( | |
| 1276 p_store->p_connection, | |
| 1277 "UPDATE users" | |
| 1278 " SET must_change_password = ?, updated_at = strftime('%s','now')" | |
| 1279 " WHERE id = ?", | |
| 1280 2, params); | |
| 1281 if (upd <= 0) | |
| 1282 { | |
| 1283 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1284 pthread_mutex_unlock(&p_store->mutex); | |
| 1285 return upd < 0 ? AUTH_STORE_ERROR : AUTH_STORE_NOT_FOUND; | |
| 1286 } | |
| 1287 | |
| 1288 if (!auth__insert_audit_log_locked( | |
| 1289 p_store, actor_user_id, | |
| 1290 value ? "must_change_password_set" : "must_change_password_cleared", | |
| 1291 user_id, NULL)) | |
| 1292 { | |
| 1293 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1294 pthread_mutex_unlock(&p_store->mutex); | |
| 1295 return AUTH_STORE_ERROR; | |
| 1296 } | |
| 1297 | |
| 1298 if (Deita_Query_Execute_Update(p_store->p_connection, "COMMIT") < 0) | |
| 1299 { | |
| 1300 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1301 pthread_mutex_unlock(&p_store->mutex); | |
| 1302 return AUTH_STORE_ERROR; | |
| 1303 } | |
| 1304 | |
| 1305 pthread_mutex_unlock(&p_store->mutex); | |
| 1306 return AUTH_STORE_OK; | |
| 1307 } | |
| 1308 | |
| 1309 Auth_Store_Result Auth_Store_Update_Password( | |
| 1310 Auth_Store *p_store, | |
| 1311 const char *user_id, | |
| 1312 const char *new_encoded_hash, | |
| 1313 boolean revoke_other_sessions, | |
| 1314 const char *keep_token_digest) | |
| 1315 { | |
| 1316 if (!p_store || !user_id || !new_encoded_hash) | |
| 1317 return AUTH_STORE_INVALID_ARG; | |
| 1318 if (new_encoded_hash[0] == '\0') | |
| 1319 return AUTH_STORE_INVALID_ARG; | |
| 1320 | |
| 1321 pthread_mutex_lock(&p_store->mutex); | |
| 1322 | |
| 1323 if (Deita_Query_Execute_Update( | |
| 1324 p_store->p_connection, "BEGIN IMMEDIATE") < 0) | |
| 1325 { | |
| 1326 pthread_mutex_unlock(&p_store->mutex); | |
| 1327 return AUTH_STORE_ERROR; | |
| 1328 } | |
| 1329 | |
| 1330 const char *upd_params[] = {new_encoded_hash, user_id}; | |
| 1331 int32 upd = Deita_Query_Execute_Update_Prepared( | |
| 1332 p_store->p_connection, | |
| 1333 "UPDATE users" | |
| 1334 " SET password_hash = ?," | |
| 1335 " password_changed_at = strftime('%s','now')," | |
| 1336 " must_change_password = 0," | |
| 1337 " updated_at = strftime('%s','now')" | |
| 1338 " WHERE id = ?", | |
| 1339 2, upd_params); | |
| 1340 if (upd <= 0) | |
| 1341 { | |
| 1342 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1343 pthread_mutex_unlock(&p_store->mutex); | |
| 1344 return upd < 0 ? AUTH_STORE_ERROR : AUTH_STORE_NOT_FOUND; | |
| 1345 } | |
| 1346 | |
| 1347 if (revoke_other_sessions) | |
| 1348 { | |
| 1349 int32 rev; | |
| 1350 if (keep_token_digest && keep_token_digest[0] != '\0') | |
| 1351 { | |
| 1352 const char *rev_params[] = {user_id, keep_token_digest}; | |
| 1353 rev = Deita_Query_Execute_Update_Prepared( | |
| 1354 p_store->p_connection, | |
| 1355 "UPDATE auth_sessions" | |
| 1356 " SET revoked_at = strftime('%s','now')" | |
| 1357 " WHERE user_id = ? AND token_digest != ? AND revoked_at IS NULL", | |
| 1358 2, rev_params); | |
| 1359 } | |
| 1360 else | |
| 1361 { | |
| 1362 const char *rev_params[] = {user_id}; | |
| 1363 rev = Deita_Query_Execute_Update_Prepared( | |
| 1364 p_store->p_connection, | |
| 1365 "UPDATE auth_sessions" | |
| 1366 " SET revoked_at = strftime('%s','now')" | |
| 1367 " WHERE user_id = ? AND revoked_at IS NULL", | |
| 1368 1, rev_params); | |
| 1369 } | |
| 1370 if (rev < 0) | |
| 1371 { | |
| 1372 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1373 pthread_mutex_unlock(&p_store->mutex); | |
| 1374 return AUTH_STORE_ERROR; | |
| 1375 } | |
| 1376 } | |
| 1377 | |
| 1378 if (Deita_Query_Execute_Update(p_store->p_connection, "COMMIT") < 0) | |
| 1379 { | |
| 1380 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1381 pthread_mutex_unlock(&p_store->mutex); | |
| 1382 return AUTH_STORE_ERROR; | |
| 1383 } | |
| 1384 | |
| 1385 pthread_mutex_unlock(&p_store->mutex); | |
| 1386 return AUTH_STORE_OK; | |
| 1387 } | |
| 1388 | |
| 1389 /* ------------------------------------------------------------------ */ | |
| 1390 /* Session management */ | |
| 1391 /* ------------------------------------------------------------------ */ | |
| 1392 | |
| 1393 Auth_Store_Result Auth_Store_Create_Session( | |
| 1394 Auth_Store *p_store, | |
| 1395 const char *user_id, | |
| 1396 const char *token_digest, | |
| 1397 const char *csrf_digest, | |
| 1398 int64 idle_ttl_secs, | |
| 1399 int64 absolute_ttl_secs, | |
| 1400 int64 current_unix, | |
| 1401 Auth_Session_Record *p_record) | |
| 1402 { | |
| 1403 if (!p_store || !user_id || !token_digest || !csrf_digest || !p_record) | |
| 1404 return AUTH_STORE_INVALID_ARG; | |
| 1405 if (!auth__session_arguments_are_valid( | |
| 1406 token_digest, csrf_digest, idle_ttl_secs, | |
| 1407 absolute_ttl_secs, current_unix)) | |
| 1408 return AUTH_STORE_INVALID_ARG; | |
| 1409 | |
| 1410 pthread_mutex_lock(&p_store->mutex); | |
| 1411 | |
| 1412 if (Deita_Query_Execute_Update( | |
| 1413 p_store->p_connection, "BEGIN IMMEDIATE") < 0) | |
| 1414 { | |
| 1415 pthread_mutex_unlock(&p_store->mutex); | |
| 1416 return AUTH_STORE_ERROR; | |
| 1417 } | |
| 1418 | |
| 1419 /* Verify user is active and read password_changed_at snapshot. */ | |
| 1420 Dowa_Arena *p_arena = Dowa_Arena_Create(2048); | |
| 1421 if (!p_arena) | |
| 1422 { | |
| 1423 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1424 pthread_mutex_unlock(&p_store->mutex); | |
| 1425 return AUTH_STORE_ERROR; | |
| 1426 } | |
| 1427 const char *sel_params[] = {user_id}; | |
| 1428 Deita_Result_Set *p_result = Deita_Query_Execute_Prepared( | |
| 1429 p_store->p_connection, | |
| 1430 "SELECT status, password_changed_at FROM users WHERE id = ?", | |
| 1431 1, sel_params, p_arena); | |
| 1432 if (!p_result || !Deita_Result_Set_Next(p_result)) | |
| 1433 { | |
| 1434 if (p_result) | |
| 1435 Deita_Result_Set_Free(p_result); | |
| 1436 Dowa_Arena_Free(p_arena); | |
| 1437 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1438 pthread_mutex_unlock(&p_store->mutex); | |
| 1439 return AUTH_STORE_NOT_FOUND; | |
| 1440 } | |
| 1441 char cur_status[9]; | |
| 1442 int64 pca; | |
| 1443 auth__copy_text_fixed(cur_status, sizeof(cur_status), | |
| 1444 Deita_Result_Set_Get_Text(p_result, 0)); | |
| 1445 pca = Deita_Result_Set_Get_Integer(p_result, 1); | |
| 1446 Deita_Result_Set_Free(p_result); | |
| 1447 Dowa_Arena_Free(p_arena); | |
| 1448 | |
| 1449 if (strcmp(cur_status, "disabled") == 0) | |
| 1450 { | |
| 1451 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1452 pthread_mutex_unlock(&p_store->mutex); | |
| 1453 return AUTH_STORE_USER_DISABLED; | |
| 1454 } | |
| 1455 | |
| 1456 Auth_Store_Result insert_result = auth__insert_session_locked( | |
| 1457 p_store, user_id, token_digest, csrf_digest, | |
| 1458 idle_ttl_secs, absolute_ttl_secs, current_unix, pca); | |
| 1459 if (insert_result != AUTH_STORE_OK) | |
| 1460 { | |
| 1461 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1462 pthread_mutex_unlock(&p_store->mutex); | |
| 1463 return insert_result; | |
| 1464 } | |
| 1465 | |
| 1466 if (Deita_Query_Execute_Update(p_store->p_connection, "COMMIT") < 0) | |
| 1467 { | |
| 1468 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1469 pthread_mutex_unlock(&p_store->mutex); | |
| 1470 return AUTH_STORE_ERROR; | |
| 1471 } | |
| 1472 | |
| 1473 pthread_mutex_unlock(&p_store->mutex); | |
| 1474 | |
| 1475 auth__fill_session_record( | |
| 1476 p_record, user_id, idle_ttl_secs, absolute_ttl_secs, current_unix, pca); | |
| 1477 return AUTH_STORE_OK; | |
| 1478 } | |
| 1479 | |
| 1480 Auth_Store_Result Auth_Store_Create_Session_CAS( | |
| 1481 Auth_Store *p_store, | |
| 1482 const char *user_id, | |
| 1483 const char *expected_password_hash, | |
| 1484 const char *token_digest, | |
| 1485 const char *csrf_digest, | |
| 1486 int64 idle_ttl_secs, | |
| 1487 int64 absolute_ttl_secs, | |
| 1488 int64 current_unix, | |
| 1489 Auth_Session_Record *p_record) | |
| 1490 { | |
| 1491 if (!p_store || !user_id || !expected_password_hash || | |
| 1492 expected_password_hash[0] == '\0' || !p_record) | |
| 1493 return AUTH_STORE_INVALID_ARG; | |
| 1494 if (!auth__session_arguments_are_valid( | |
| 1495 token_digest, csrf_digest, idle_ttl_secs, | |
| 1496 absolute_ttl_secs, current_unix)) | |
| 1497 return AUTH_STORE_INVALID_ARG; | |
| 1498 | |
| 1499 char current_hash[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE] = {0}; | |
| 1500 char current_status[9] = {0}; | |
| 1501 int64 password_changed_at = 0; | |
| 1502 Auth_Store_Result result = AUTH_STORE_ERROR; | |
| 1503 | |
| 1504 pthread_mutex_lock(&p_store->mutex); | |
| 1505 if (Deita_Query_Execute_Update( | |
| 1506 p_store->p_connection, "BEGIN IMMEDIATE") < 0) | |
| 1507 { | |
| 1508 pthread_mutex_unlock(&p_store->mutex); | |
| 1509 OPENSSL_cleanse(current_hash, sizeof(current_hash)); | |
| 1510 return AUTH_STORE_ERROR; | |
| 1511 } | |
| 1512 | |
| 1513 Dowa_Arena *p_arena = Dowa_Arena_Create(2048); | |
| 1514 if (!p_arena) | |
| 1515 { | |
| 1516 result = auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1517 goto create_session_cas_done; | |
| 1518 } | |
| 1519 | |
| 1520 const char *select_params[] = {user_id}; | |
| 1521 Deita_Result_Set *p_result = Deita_Query_Execute_Prepared( | |
| 1522 p_store->p_connection, | |
| 1523 "SELECT status, password_changed_at, password_hash" | |
| 1524 " FROM users WHERE id = ?", | |
| 1525 1, select_params, p_arena); | |
| 1526 if (!p_result) | |
| 1527 { | |
| 1528 Dowa_Arena_Free(p_arena); | |
| 1529 result = auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1530 goto create_session_cas_done; | |
| 1531 } | |
| 1532 if (!Deita_Result_Set_Next(p_result)) | |
| 1533 { | |
| 1534 boolean query_error = Deita_Result_Set_Has_Error(p_result); | |
| 1535 Deita_Result_Set_Free(p_result); | |
| 1536 Dowa_Arena_Free(p_arena); | |
| 1537 result = auth__rollback( | |
| 1538 p_store, query_error ? AUTH_STORE_ERROR : AUTH_STORE_NOT_FOUND); | |
| 1539 goto create_session_cas_done; | |
| 1540 } | |
| 1541 | |
| 1542 auth__copy_text_fixed( | |
| 1543 current_status, sizeof(current_status), | |
| 1544 Deita_Result_Set_Get_Text(p_result, 0)); | |
| 1545 password_changed_at = Deita_Result_Set_Get_Integer(p_result, 1); | |
| 1546 auth__copy_text_fixed( | |
| 1547 current_hash, sizeof(current_hash), | |
| 1548 Deita_Result_Set_Get_Text(p_result, 2)); | |
| 1549 Deita_Result_Set_Free(p_result); | |
| 1550 Dowa_Arena_Free(p_arena); | |
| 1551 | |
| 1552 if (strcmp(current_status, "disabled") == 0) | |
| 1553 { | |
| 1554 result = auth__rollback(p_store, AUTH_STORE_USER_DISABLED); | |
| 1555 goto create_session_cas_done; | |
| 1556 } | |
| 1557 if (strcmp(current_hash, expected_password_hash) != 0) | |
| 1558 { | |
| 1559 result = auth__rollback(p_store, AUTH_STORE_STALE_PASSWORD); | |
| 1560 goto create_session_cas_done; | |
| 1561 } | |
| 1562 | |
| 1563 result = auth__insert_session_locked( | |
| 1564 p_store, user_id, token_digest, csrf_digest, | |
| 1565 idle_ttl_secs, absolute_ttl_secs, current_unix, password_changed_at); | |
| 1566 if (result != AUTH_STORE_OK) | |
| 1567 { | |
| 1568 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1569 goto create_session_cas_done; | |
| 1570 } | |
| 1571 | |
| 1572 if (Deita_Query_Execute_Update(p_store->p_connection, "COMMIT") < 0) | |
| 1573 { | |
| 1574 result = auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1575 goto create_session_cas_done; | |
| 1576 } | |
| 1577 | |
| 1578 auth__fill_session_record( | |
| 1579 p_record, user_id, idle_ttl_secs, absolute_ttl_secs, | |
| 1580 current_unix, password_changed_at); | |
| 1581 result = AUTH_STORE_OK; | |
| 1582 | |
| 1583 create_session_cas_done: | |
| 1584 OPENSSL_cleanse(current_hash, sizeof(current_hash)); | |
| 1585 pthread_mutex_unlock(&p_store->mutex); | |
| 1586 return result; | |
| 1587 } | |
| 1588 | |
| 1589 Auth_Store_Result Auth_Store_Find_Session( | |
| 1590 Auth_Store *p_store, | |
| 1591 const char *token_digest, | |
| 1592 int64 current_unix, | |
| 1593 Auth_Session_Record *p_session, | |
| 1594 Auth_User_Record *p_user) | |
| 1595 { | |
| 1596 if (!p_store || !token_digest || !p_session || !p_user) | |
| 1597 return AUTH_STORE_INVALID_ARG; | |
| 1598 | |
| 1599 memset(p_session, 0, sizeof(*p_session)); | |
| 1600 memset(p_user, 0, sizeof(*p_user)); | |
| 1601 | |
| 1602 Dowa_Arena *p_arena = Dowa_Arena_Create(4096); | |
| 1603 if (!p_arena) | |
| 1604 return AUTH_STORE_ERROR; | |
| 1605 | |
| 1606 const char *params[] = {token_digest}; | |
| 1607 pthread_mutex_lock(&p_store->mutex); | |
| 1608 Deita_Result_Set *p_result = Deita_Query_Execute_Prepared( | |
| 1609 p_store->p_connection, | |
| 1610 "SELECT" | |
| 1611 " s.user_id, s.created_at, s.last_seen_at," | |
| 1612 " s.idle_expires_at, s.absolute_expires_at," | |
| 1613 " s.password_changed_at_snapshot, s.revoked_at," | |
| 1614 " u.id, u.username, u.normalized_username, u.role, u.status," | |
| 1615 " u.must_change_password, u.password_changed_at," | |
| 1616 " u.created_at, u.updated_at" | |
| 1617 " FROM auth_sessions s" | |
| 1618 " JOIN users u ON s.user_id = u.id" | |
| 1619 " WHERE s.token_digest = ?", | |
| 1620 1, params, p_arena); | |
| 1621 | |
| 1622 if (!p_result || !Deita_Result_Set_Next(p_result)) | |
| 1623 { | |
| 1624 if (p_result) | |
| 1625 Deita_Result_Set_Free(p_result); | |
| 1626 pthread_mutex_unlock(&p_store->mutex); | |
| 1627 Dowa_Arena_Free(p_arena); | |
| 1628 return AUTH_STORE_NOT_FOUND; | |
| 1629 } | |
| 1630 | |
| 1631 /* Read session fields. */ | |
| 1632 auth__copy_text_fixed(p_session->user_id, sizeof(p_session->user_id), | |
| 1633 Deita_Result_Set_Get_Text(p_result, 0)); | |
| 1634 p_session->created_at = Deita_Result_Set_Get_Integer(p_result, 1); | |
| 1635 p_session->last_seen_at = Deita_Result_Set_Get_Integer(p_result, 2); | |
| 1636 p_session->idle_expires_at = Deita_Result_Set_Get_Integer(p_result, 3); | |
| 1637 p_session->absolute_expires_at = Deita_Result_Set_Get_Integer(p_result, 4); | |
| 1638 p_session->password_changed_at_snapshot = | |
| 1639 Deita_Result_Set_Get_Integer(p_result, 5); | |
| 1640 boolean is_revoked = | |
| 1641 (Deita_Result_Set_Get_Column_Type(p_result, 6) != DEITA_COLUMN_TYPE_NULL); | |
| 1642 | |
| 1643 /* Read user fields (columns 7-15). */ | |
| 1644 /* Reuse auth__read_user_record after shifting: pass a pointer with offset */ | |
| 1645 Auth_User_Record tmp_user; | |
| 1646 memset(&tmp_user, 0, sizeof(tmp_user)); | |
| 1647 auth__copy_text_fixed(tmp_user.id, sizeof(tmp_user.id), | |
| 1648 Deita_Result_Set_Get_Text(p_result, 7)); | |
| 1649 auth__copy_text_fixed(tmp_user.username, sizeof(tmp_user.username), | |
| 1650 Deita_Result_Set_Get_Text(p_result, 8)); | |
| 1651 auth__copy_text_fixed(tmp_user.normalized_username, | |
| 1652 sizeof(tmp_user.normalized_username), | |
| 1653 Deita_Result_Set_Get_Text(p_result, 9)); | |
| 1654 auth__copy_text_fixed(tmp_user.role, sizeof(tmp_user.role), | |
| 1655 Deita_Result_Set_Get_Text(p_result, 10)); | |
| 1656 auth__copy_text_fixed(tmp_user.status, sizeof(tmp_user.status), | |
| 1657 Deita_Result_Set_Get_Text(p_result, 11)); | |
| 1658 tmp_user.must_change_password = Deita_Result_Set_Get_Integer(p_result, 12) ? | |
| 1659 TRUE : FALSE; | |
| 1660 tmp_user.password_changed_at = Deita_Result_Set_Get_Integer(p_result, 13); | |
| 1661 tmp_user.created_at = Deita_Result_Set_Get_Integer(p_result, 14); | |
| 1662 tmp_user.updated_at = Deita_Result_Set_Get_Integer(p_result, 15); | |
| 1663 | |
| 1664 Deita_Result_Set_Free(p_result); | |
| 1665 pthread_mutex_unlock(&p_store->mutex); | |
| 1666 Dowa_Arena_Free(p_arena); | |
| 1667 | |
| 1668 /* Apply validity checks in order of specificity. */ | |
| 1669 if (is_revoked) | |
| 1670 return AUTH_STORE_REVOKED; | |
| 1671 | |
| 1672 if (current_unix >= p_session->idle_expires_at || | |
| 1673 current_unix >= p_session->absolute_expires_at) | |
| 1674 return AUTH_STORE_EXPIRED; | |
| 1675 | |
| 1676 if (strcmp(tmp_user.status, "disabled") == 0) | |
| 1677 return AUTH_STORE_USER_DISABLED; | |
| 1678 | |
| 1679 if (tmp_user.password_changed_at != p_session->password_changed_at_snapshot) | |
| 1680 return AUTH_STORE_STALE_PASSWORD; | |
| 1681 | |
| 1682 *p_user = tmp_user; | |
| 1683 return AUTH_STORE_OK; | |
| 1684 } | |
| 1685 | |
| 1686 Auth_Store_Result Auth_Store_Touch_Session( | |
| 1687 Auth_Store *p_store, | |
| 1688 const char *token_digest, | |
| 1689 int64 current_unix, | |
| 1690 int64 idle_ttl_secs) | |
| 1691 { | |
| 1692 if (!p_store || !token_digest) | |
| 1693 return AUTH_STORE_INVALID_ARG; | |
| 1694 | |
| 1695 char last_seen_str[32], idle_exp_str[32]; | |
| 1696 snprintf(last_seen_str, sizeof(last_seen_str), "%lld", (long long)current_unix); | |
| 1697 snprintf(idle_exp_str, sizeof(idle_exp_str), "%lld", | |
| 1698 (long long)(current_unix + idle_ttl_secs)); | |
| 1699 | |
| 1700 const char *params[] = {last_seen_str, idle_exp_str, token_digest}; | |
| 1701 | |
| 1702 pthread_mutex_lock(&p_store->mutex); | |
| 1703 int32 result = Deita_Query_Execute_Update_Prepared( | |
| 1704 p_store->p_connection, | |
| 1705 "UPDATE auth_sessions" | |
| 1706 " SET last_seen_at = ?, idle_expires_at = ?" | |
| 1707 " WHERE token_digest = ? AND revoked_at IS NULL", | |
| 1708 3, params); | |
| 1709 pthread_mutex_unlock(&p_store->mutex); | |
| 1710 | |
| 1711 if (result < 0) | |
| 1712 return AUTH_STORE_ERROR; | |
| 1713 return result == 0 ? AUTH_STORE_NOT_FOUND : AUTH_STORE_OK; | |
| 1714 } | |
| 1715 | |
| 1716 Auth_Store_Result Auth_Store_Revoke_Session( | |
| 1717 Auth_Store *p_store, | |
| 1718 const char *token_digest) | |
| 1719 { | |
| 1720 if (!p_store || !token_digest) | |
| 1721 return AUTH_STORE_INVALID_ARG; | |
| 1722 | |
| 1723 const char *params[] = {token_digest}; | |
| 1724 | |
| 1725 pthread_mutex_lock(&p_store->mutex); | |
| 1726 int32 result = Deita_Query_Execute_Update_Prepared( | |
| 1727 p_store->p_connection, | |
| 1728 "UPDATE auth_sessions SET revoked_at = strftime('%s','now')" | |
| 1729 " WHERE token_digest = ?", | |
| 1730 1, params); | |
| 1731 pthread_mutex_unlock(&p_store->mutex); | |
| 1732 | |
| 1733 if (result < 0) | |
| 1734 return AUTH_STORE_ERROR; | |
| 1735 return result == 0 ? AUTH_STORE_NOT_FOUND : AUTH_STORE_OK; | |
| 1736 } | |
| 1737 | |
| 1738 Auth_Store_Result Auth_Store_Revoke_All_Sessions( | |
| 1739 Auth_Store *p_store, | |
| 1740 const char *user_id, | |
| 1741 const char *except_token_digest) | |
| 1742 { | |
| 1743 if (!p_store || !user_id) | |
| 1744 return AUTH_STORE_INVALID_ARG; | |
| 1745 | |
| 1746 pthread_mutex_lock(&p_store->mutex); | |
| 1747 int32 result; | |
| 1748 if (except_token_digest && except_token_digest[0] != '\0') | |
| 1749 { | |
| 1750 const char *params[] = {user_id, except_token_digest}; | |
| 1751 result = Deita_Query_Execute_Update_Prepared( | |
| 1752 p_store->p_connection, | |
| 1753 "UPDATE auth_sessions SET revoked_at = strftime('%s','now')" | |
| 1754 " WHERE user_id = ? AND token_digest != ? AND revoked_at IS NULL", | |
| 1755 2, params); | |
| 1756 } | |
| 1757 else | |
| 1758 { | |
| 1759 const char *params[] = {user_id}; | |
| 1760 result = Deita_Query_Execute_Update_Prepared( | |
| 1761 p_store->p_connection, | |
| 1762 "UPDATE auth_sessions SET revoked_at = strftime('%s','now')" | |
| 1763 " WHERE user_id = ? AND revoked_at IS NULL", | |
| 1764 1, params); | |
| 1765 } | |
| 1766 pthread_mutex_unlock(&p_store->mutex); | |
| 1767 | |
| 1768 return result < 0 ? AUTH_STORE_ERROR : AUTH_STORE_OK; | |
| 1769 } | |
| 1770 | |
| 1771 Auth_Store_Result Auth_Store_Revoke_All_Sessions_Audited( | |
| 1772 Auth_Store *p_store, | |
| 1773 const char *user_id, | |
| 1774 const char *except_token_digest, | |
| 1775 const char *actor_user_id) | |
| 1776 { | |
| 1777 if (!p_store || !user_id) | |
| 1778 return AUTH_STORE_INVALID_ARG; | |
| 1779 if (except_token_digest && except_token_digest[0] != '\0' && | |
| 1780 !auth__digest_is_valid(except_token_digest)) | |
| 1781 return AUTH_STORE_INVALID_ARG; | |
| 1782 | |
| 1783 pthread_mutex_lock(&p_store->mutex); | |
| 1784 if (Deita_Query_Execute_Update( | |
| 1785 p_store->p_connection, "BEGIN IMMEDIATE") < 0) | |
| 1786 { | |
| 1787 pthread_mutex_unlock(&p_store->mutex); | |
| 1788 return AUTH_STORE_ERROR; | |
| 1789 } | |
| 1790 | |
| 1791 Dowa_Arena *p_arena = Dowa_Arena_Create(1024); | |
| 1792 if (!p_arena) | |
| 1793 { | |
| 1794 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1795 pthread_mutex_unlock(&p_store->mutex); | |
| 1796 return AUTH_STORE_ERROR; | |
| 1797 } | |
| 1798 | |
| 1799 const char *find_params[] = {user_id}; | |
| 1800 Deita_Result_Set *p_result = Deita_Query_Execute_Prepared( | |
| 1801 p_store->p_connection, | |
| 1802 "SELECT 1 FROM users WHERE id = ?", | |
| 1803 1, find_params, p_arena); | |
| 1804 if (!p_result) | |
| 1805 { | |
| 1806 Dowa_Arena_Free(p_arena); | |
| 1807 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1808 pthread_mutex_unlock(&p_store->mutex); | |
| 1809 return AUTH_STORE_ERROR; | |
| 1810 } | |
| 1811 if (!Deita_Result_Set_Next(p_result)) | |
| 1812 { | |
| 1813 boolean query_error = Deita_Result_Set_Has_Error(p_result); | |
| 1814 Deita_Result_Set_Free(p_result); | |
| 1815 Dowa_Arena_Free(p_arena); | |
| 1816 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1817 pthread_mutex_unlock(&p_store->mutex); | |
| 1818 return query_error ? AUTH_STORE_ERROR : AUTH_STORE_NOT_FOUND; | |
| 1819 } | |
| 1820 Deita_Result_Set_Free(p_result); | |
| 1821 Dowa_Arena_Free(p_arena); | |
| 1822 | |
| 1823 int32 revoke_result; | |
| 1824 if (except_token_digest && except_token_digest[0] != '\0') | |
| 1825 { | |
| 1826 const char *params[] = {user_id, except_token_digest}; | |
| 1827 revoke_result = Deita_Query_Execute_Update_Prepared( | |
| 1828 p_store->p_connection, | |
| 1829 "UPDATE auth_sessions SET revoked_at = strftime('%s','now')" | |
| 1830 " WHERE user_id = ? AND token_digest != ? AND revoked_at IS NULL", | |
| 1831 2, params); | |
| 1832 } | |
| 1833 else | |
| 1834 { | |
| 1835 const char *params[] = {user_id}; | |
| 1836 revoke_result = Deita_Query_Execute_Update_Prepared( | |
| 1837 p_store->p_connection, | |
| 1838 "UPDATE auth_sessions SET revoked_at = strftime('%s','now')" | |
| 1839 " WHERE user_id = ? AND revoked_at IS NULL", | |
| 1840 1, params); | |
| 1841 } | |
| 1842 if (revoke_result < 0) | |
| 1843 { | |
| 1844 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1845 pthread_mutex_unlock(&p_store->mutex); | |
| 1846 return AUTH_STORE_ERROR; | |
| 1847 } | |
| 1848 | |
| 1849 if (!auth__insert_audit_log_locked( | |
| 1850 p_store, actor_user_id, "admin_sessions_revoked", user_id, NULL)) | |
| 1851 { | |
| 1852 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1853 pthread_mutex_unlock(&p_store->mutex); | |
| 1854 return AUTH_STORE_ERROR; | |
| 1855 } | |
| 1856 | |
| 1857 if (Deita_Query_Execute_Update(p_store->p_connection, "COMMIT") < 0) | |
| 1858 { | |
| 1859 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1860 pthread_mutex_unlock(&p_store->mutex); | |
| 1861 return AUTH_STORE_ERROR; | |
| 1862 } | |
| 1863 | |
| 1864 pthread_mutex_unlock(&p_store->mutex); | |
| 1865 return AUTH_STORE_OK; | |
| 1866 } | |
| 1867 | |
| 1868 Auth_Store_Result Auth_Store_Rotate_Session( | |
| 1869 Auth_Store *p_store, | |
| 1870 const char *user_id, | |
| 1871 const char *old_token_digest, | |
| 1872 const char *new_token_digest, | |
| 1873 const char *new_csrf_digest, | |
| 1874 int64 idle_ttl_secs, | |
| 1875 int64 absolute_ttl_secs, | |
| 1876 int64 current_unix, | |
| 1877 Auth_Session_Record *p_record) | |
| 1878 { | |
| 1879 if (!p_store || !user_id || !old_token_digest || !new_token_digest || | |
| 1880 !new_csrf_digest || !p_record || !auth__digest_is_valid(old_token_digest)) | |
| 1881 return AUTH_STORE_INVALID_ARG; | |
| 1882 if (!auth__session_arguments_are_valid( | |
| 1883 new_token_digest, new_csrf_digest, idle_ttl_secs, | |
| 1884 absolute_ttl_secs, current_unix)) | |
| 1885 return AUTH_STORE_INVALID_ARG; | |
| 1886 | |
| 1887 pthread_mutex_lock(&p_store->mutex); | |
| 1888 | |
| 1889 if (Deita_Query_Execute_Update( | |
| 1890 p_store->p_connection, "BEGIN IMMEDIATE") < 0) | |
| 1891 { | |
| 1892 pthread_mutex_unlock(&p_store->mutex); | |
| 1893 return AUTH_STORE_ERROR; | |
| 1894 } | |
| 1895 | |
| 1896 /* Verify user is active and read password_changed_at snapshot. */ | |
| 1897 Dowa_Arena *p_arena = Dowa_Arena_Create(2048); | |
| 1898 if (!p_arena) | |
| 1899 { | |
| 1900 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1901 pthread_mutex_unlock(&p_store->mutex); | |
| 1902 return AUTH_STORE_ERROR; | |
| 1903 } | |
| 1904 const char *sel_params[] = {user_id}; | |
| 1905 Deita_Result_Set *p_result = Deita_Query_Execute_Prepared( | |
| 1906 p_store->p_connection, | |
| 1907 "SELECT status, password_changed_at FROM users WHERE id = ?", | |
| 1908 1, sel_params, p_arena); | |
| 1909 if (!p_result || !Deita_Result_Set_Next(p_result)) | |
| 1910 { | |
| 1911 if (p_result) | |
| 1912 Deita_Result_Set_Free(p_result); | |
| 1913 Dowa_Arena_Free(p_arena); | |
| 1914 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1915 pthread_mutex_unlock(&p_store->mutex); | |
| 1916 return AUTH_STORE_NOT_FOUND; | |
| 1917 } | |
| 1918 char cur_status[9]; | |
| 1919 int64 pca; | |
| 1920 auth__copy_text_fixed(cur_status, sizeof(cur_status), | |
| 1921 Deita_Result_Set_Get_Text(p_result, 0)); | |
| 1922 pca = Deita_Result_Set_Get_Integer(p_result, 1); | |
| 1923 Deita_Result_Set_Free(p_result); | |
| 1924 Dowa_Arena_Free(p_arena); | |
| 1925 | |
| 1926 if (strcmp(cur_status, "disabled") == 0) | |
| 1927 { | |
| 1928 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1929 pthread_mutex_unlock(&p_store->mutex); | |
| 1930 return AUTH_STORE_USER_DISABLED; | |
| 1931 } | |
| 1932 | |
| 1933 /* Insert new session. */ | |
| 1934 Auth_Store_Result insert_result = auth__insert_session_locked( | |
| 1935 p_store, user_id, new_token_digest, new_csrf_digest, | |
| 1936 idle_ttl_secs, absolute_ttl_secs, current_unix, pca); | |
| 1937 if (insert_result != AUTH_STORE_OK) | |
| 1938 { | |
| 1939 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1940 pthread_mutex_unlock(&p_store->mutex); | |
| 1941 return insert_result; | |
| 1942 } | |
| 1943 | |
| 1944 /* Revoke old session (idempotent: ignore if already revoked). */ | |
| 1945 const char *rev_params[] = {old_token_digest}; | |
| 1946 int32 revoke_result = Deita_Query_Execute_Update_Prepared( | |
| 1947 p_store->p_connection, | |
| 1948 "UPDATE auth_sessions SET revoked_at = strftime('%s','now')" | |
| 1949 " WHERE token_digest = ? AND revoked_at IS NULL", | |
| 1950 1, rev_params); | |
| 1951 if (revoke_result < 0) | |
| 1952 { | |
| 1953 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1954 pthread_mutex_unlock(&p_store->mutex); | |
| 1955 return AUTH_STORE_ERROR; | |
| 1956 } | |
| 1957 | |
| 1958 if (Deita_Query_Execute_Update(p_store->p_connection, "COMMIT") < 0) | |
| 1959 { | |
| 1960 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 1961 pthread_mutex_unlock(&p_store->mutex); | |
| 1962 return AUTH_STORE_ERROR; | |
| 1963 } | |
| 1964 | |
| 1965 pthread_mutex_unlock(&p_store->mutex); | |
| 1966 | |
| 1967 auth__fill_session_record( | |
| 1968 p_record, user_id, idle_ttl_secs, absolute_ttl_secs, current_unix, pca); | |
| 1969 return AUTH_STORE_OK; | |
| 1970 } | |
| 1971 | |
| 1972 Auth_Store_Result Auth_Store_Self_Change_Password( | |
| 1973 Auth_Store *p_store, | |
| 1974 const char *user_id, | |
| 1975 const char *old_encoded_hash, | |
| 1976 const char *new_encoded_hash, | |
| 1977 const char *new_token_digest, | |
| 1978 const char *new_csrf_digest, | |
| 1979 int64 idle_ttl_secs, | |
| 1980 int64 absolute_ttl_secs, | |
| 1981 int64 current_unix, | |
| 1982 Auth_Session_Record *p_record) | |
| 1983 { | |
| 1984 if (!p_store || !user_id || !old_encoded_hash || !new_encoded_hash || | |
| 1985 old_encoded_hash[0] == '\0' || new_encoded_hash[0] == '\0' || !p_record) | |
| 1986 return AUTH_STORE_INVALID_ARG; | |
| 1987 if (!auth__session_arguments_are_valid( | |
| 1988 new_token_digest, new_csrf_digest, idle_ttl_secs, | |
| 1989 absolute_ttl_secs, current_unix)) | |
| 1990 return AUTH_STORE_INVALID_ARG; | |
| 1991 | |
| 1992 char current_hash[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE] = {0}; | |
| 1993 char reread_hash[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE] = {0}; | |
| 1994 char status[9] = {0}; | |
| 1995 char timestamp[32]; | |
| 1996 snprintf(timestamp, sizeof(timestamp), "%lld", (long long)current_unix); | |
| 1997 Auth_Store_Result result = AUTH_STORE_ERROR; | |
| 1998 | |
| 1999 pthread_mutex_lock(&p_store->mutex); | |
| 2000 if (Deita_Query_Execute_Update( | |
| 2001 p_store->p_connection, "BEGIN IMMEDIATE") < 0) | |
| 2002 { | |
| 2003 pthread_mutex_unlock(&p_store->mutex); | |
| 2004 OPENSSL_cleanse(current_hash, sizeof(current_hash)); | |
| 2005 OPENSSL_cleanse(reread_hash, sizeof(reread_hash)); | |
| 2006 return AUTH_STORE_ERROR; | |
| 2007 } | |
| 2008 | |
| 2009 Dowa_Arena *p_arena = Dowa_Arena_Create(2048); | |
| 2010 if (!p_arena) | |
| 2011 { | |
| 2012 result = auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2013 goto self_change_done; | |
| 2014 } | |
| 2015 | |
| 2016 const char *select_params[] = {user_id}; | |
| 2017 Deita_Result_Set *p_result = Deita_Query_Execute_Prepared( | |
| 2018 p_store->p_connection, | |
| 2019 "SELECT status, password_hash FROM users WHERE id = ?", | |
| 2020 1, select_params, p_arena); | |
| 2021 if (!p_result) | |
| 2022 { | |
| 2023 Dowa_Arena_Free(p_arena); | |
| 2024 result = auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2025 goto self_change_done; | |
| 2026 } | |
| 2027 if (!Deita_Result_Set_Next(p_result)) | |
| 2028 { | |
| 2029 boolean query_error = Deita_Result_Set_Has_Error(p_result); | |
| 2030 Deita_Result_Set_Free(p_result); | |
| 2031 Dowa_Arena_Free(p_arena); | |
| 2032 result = auth__rollback( | |
| 2033 p_store, query_error ? AUTH_STORE_ERROR : AUTH_STORE_NOT_FOUND); | |
| 2034 goto self_change_done; | |
| 2035 } | |
| 2036 auth__copy_text_fixed( | |
| 2037 status, sizeof(status), Deita_Result_Set_Get_Text(p_result, 0)); | |
| 2038 auth__copy_text_fixed( | |
| 2039 current_hash, sizeof(current_hash), | |
| 2040 Deita_Result_Set_Get_Text(p_result, 1)); | |
| 2041 Deita_Result_Set_Free(p_result); | |
| 2042 Dowa_Arena_Free(p_arena); | |
| 2043 | |
| 2044 if (strcmp(status, "disabled") == 0) | |
| 2045 { | |
| 2046 result = auth__rollback(p_store, AUTH_STORE_USER_DISABLED); | |
| 2047 goto self_change_done; | |
| 2048 } | |
| 2049 if (strcmp(current_hash, old_encoded_hash) != 0) | |
| 2050 { | |
| 2051 result = auth__rollback(p_store, AUTH_STORE_STALE_PASSWORD); | |
| 2052 goto self_change_done; | |
| 2053 } | |
| 2054 | |
| 2055 const char *update_params[] = { | |
| 2056 new_encoded_hash, timestamp, timestamp, user_id, old_encoded_hash | |
| 2057 }; | |
| 2058 int32 update_result = Deita_Query_Execute_Update_Prepared( | |
| 2059 p_store->p_connection, | |
| 2060 "UPDATE users" | |
| 2061 " SET password_hash = ?, password_changed_at = ?," | |
| 2062 " must_change_password = 0, updated_at = ?" | |
| 2063 " WHERE id = ? AND password_hash = ?", | |
| 2064 5, update_params); | |
| 2065 if (update_result < 0) | |
| 2066 { | |
| 2067 result = auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2068 goto self_change_done; | |
| 2069 } | |
| 2070 if (update_result == 0) | |
| 2071 { | |
| 2072 result = auth__rollback(p_store, AUTH_STORE_STALE_PASSWORD); | |
| 2073 goto self_change_done; | |
| 2074 } | |
| 2075 | |
| 2076 p_arena = Dowa_Arena_Create(2048); | |
| 2077 if (!p_arena) | |
| 2078 { | |
| 2079 result = auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2080 goto self_change_done; | |
| 2081 } | |
| 2082 p_result = Deita_Query_Execute_Prepared( | |
| 2083 p_store->p_connection, | |
| 2084 "SELECT status, password_changed_at, password_hash" | |
| 2085 " FROM users WHERE id = ?", | |
| 2086 1, select_params, p_arena); | |
| 2087 if (!p_result) | |
| 2088 { | |
| 2089 Dowa_Arena_Free(p_arena); | |
| 2090 result = auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2091 goto self_change_done; | |
| 2092 } | |
| 2093 if (!Deita_Result_Set_Next(p_result)) | |
| 2094 { | |
| 2095 Deita_Result_Set_Free(p_result); | |
| 2096 Dowa_Arena_Free(p_arena); | |
| 2097 result = auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2098 goto self_change_done; | |
| 2099 } | |
| 2100 auth__copy_text_fixed( | |
| 2101 status, sizeof(status), Deita_Result_Set_Get_Text(p_result, 0)); | |
| 2102 int64 password_changed_at = Deita_Result_Set_Get_Integer(p_result, 1); | |
| 2103 auth__copy_text_fixed( | |
| 2104 reread_hash, sizeof(reread_hash), | |
| 2105 Deita_Result_Set_Get_Text(p_result, 2)); | |
| 2106 Deita_Result_Set_Free(p_result); | |
| 2107 Dowa_Arena_Free(p_arena); | |
| 2108 if (strcmp(status, "active") != 0 || | |
| 2109 password_changed_at != current_unix || | |
| 2110 strcmp(reread_hash, new_encoded_hash) != 0) | |
| 2111 { | |
| 2112 result = auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2113 goto self_change_done; | |
| 2114 } | |
| 2115 | |
| 2116 const char *revoke_params[] = {timestamp, user_id}; | |
| 2117 if (Deita_Query_Execute_Update_Prepared( | |
| 2118 p_store->p_connection, | |
| 2119 "UPDATE auth_sessions SET revoked_at = ?" | |
| 2120 " WHERE user_id = ? AND revoked_at IS NULL", | |
| 2121 2, revoke_params) < 0) | |
| 2122 { | |
| 2123 result = auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2124 goto self_change_done; | |
| 2125 } | |
| 2126 | |
| 2127 result = auth__insert_session_locked( | |
| 2128 p_store, user_id, new_token_digest, new_csrf_digest, | |
| 2129 idle_ttl_secs, absolute_ttl_secs, current_unix, password_changed_at); | |
| 2130 if (result != AUTH_STORE_OK) | |
| 2131 { | |
| 2132 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2133 goto self_change_done; | |
| 2134 } | |
| 2135 | |
| 2136 if (Deita_Query_Execute_Update(p_store->p_connection, "COMMIT") < 0) | |
| 2137 { | |
| 2138 result = auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2139 goto self_change_done; | |
| 2140 } | |
| 2141 | |
| 2142 auth__fill_session_record( | |
| 2143 p_record, user_id, idle_ttl_secs, absolute_ttl_secs, | |
| 2144 current_unix, password_changed_at); | |
| 2145 result = AUTH_STORE_OK; | |
| 2146 | |
| 2147 self_change_done: | |
| 2148 OPENSSL_cleanse(current_hash, sizeof(current_hash)); | |
| 2149 OPENSSL_cleanse(reread_hash, sizeof(reread_hash)); | |
| 2150 pthread_mutex_unlock(&p_store->mutex); | |
| 2151 return result; | |
| 2152 } | |
| 2153 | |
| 2154 /* ------------------------------------------------------------------ */ | |
| 2155 /* Guest identity */ | |
| 2156 /* ------------------------------------------------------------------ */ | |
| 2157 | |
| 2158 Auth_Store_Result Auth_Store_Upsert_Guest_Identity( | |
| 2159 Auth_Store *p_store, | |
| 2160 const char *guest_id, | |
| 2161 const char *ip_binding_digest, | |
| 2162 int64 expires_at, | |
| 2163 Auth_Guest_Identity_Record *p_record) | |
| 2164 { | |
| 2165 if (!p_store || !guest_id || !ip_binding_digest || !p_record) | |
| 2166 return AUTH_STORE_INVALID_ARG; | |
| 2167 | |
| 2168 char exp_str[32]; | |
| 2169 snprintf(exp_str, sizeof(exp_str), "%lld", (long long)expires_at); | |
| 2170 | |
| 2171 pthread_mutex_lock(&p_store->mutex); | |
| 2172 | |
| 2173 if (Deita_Query_Execute_Update( | |
| 2174 p_store->p_connection, "BEGIN IMMEDIATE") < 0) | |
| 2175 { | |
| 2176 pthread_mutex_unlock(&p_store->mutex); | |
| 2177 return AUTH_STORE_ERROR; | |
| 2178 } | |
| 2179 | |
| 2180 /* INSERT OR IGNORE so usage data is not cascade-deleted on upsert. */ | |
| 2181 const char *ins_params[] = {guest_id, ip_binding_digest, exp_str}; | |
| 2182 Deita_Query_Execute_Update_Prepared( | |
| 2183 p_store->p_connection, | |
| 2184 "INSERT OR IGNORE INTO guest_identities (id, ip_binding_digest, expires_at)" | |
| 2185 " VALUES (?, ?, ?)", | |
| 2186 3, ins_params); | |
| 2187 | |
| 2188 /* Always refresh last_seen_at and expires_at. */ | |
| 2189 const char *upd_params[] = {exp_str, guest_id}; | |
| 2190 if (Deita_Query_Execute_Update_Prepared( | |
| 2191 p_store->p_connection, | |
| 2192 "UPDATE guest_identities" | |
| 2193 " SET last_seen_at = strftime('%s','now'), expires_at = ?" | |
| 2194 " WHERE id = ?", | |
| 2195 2, upd_params) < 0) | |
| 2196 { | |
| 2197 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2198 pthread_mutex_unlock(&p_store->mutex); | |
| 2199 return AUTH_STORE_ERROR; | |
| 2200 } | |
| 2201 | |
| 2202 /* Read back the current row. */ | |
| 2203 Dowa_Arena *p_arena = Dowa_Arena_Create(2048); | |
| 2204 if (!p_arena) | |
| 2205 { | |
| 2206 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2207 pthread_mutex_unlock(&p_store->mutex); | |
| 2208 return AUTH_STORE_ERROR; | |
| 2209 } | |
| 2210 const char *sel_params[] = {guest_id}; | |
| 2211 Deita_Result_Set *p_result = Deita_Query_Execute_Prepared( | |
| 2212 p_store->p_connection, | |
| 2213 "SELECT id, created_at, last_seen_at, expires_at" | |
| 2214 " FROM guest_identities WHERE id = ?", | |
| 2215 1, sel_params, p_arena); | |
| 2216 if (!p_result || !Deita_Result_Set_Next(p_result)) | |
| 2217 { | |
| 2218 if (p_result) | |
| 2219 Deita_Result_Set_Free(p_result); | |
| 2220 Dowa_Arena_Free(p_arena); | |
| 2221 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2222 pthread_mutex_unlock(&p_store->mutex); | |
| 2223 return AUTH_STORE_ERROR; | |
| 2224 } | |
| 2225 auth__copy_text_fixed(p_record->id, sizeof(p_record->id), | |
| 2226 Deita_Result_Set_Get_Text(p_result, 0)); | |
| 2227 p_record->created_at = Deita_Result_Set_Get_Integer(p_result, 1); | |
| 2228 p_record->last_seen_at = Deita_Result_Set_Get_Integer(p_result, 2); | |
| 2229 p_record->expires_at = Deita_Result_Set_Get_Integer(p_result, 3); | |
| 2230 Deita_Result_Set_Free(p_result); | |
| 2231 Dowa_Arena_Free(p_arena); | |
| 2232 | |
| 2233 if (Deita_Query_Execute_Update(p_store->p_connection, "COMMIT") < 0) | |
| 2234 { | |
| 2235 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2236 pthread_mutex_unlock(&p_store->mutex); | |
| 2237 return AUTH_STORE_ERROR; | |
| 2238 } | |
| 2239 | |
| 2240 pthread_mutex_unlock(&p_store->mutex); | |
| 2241 return AUTH_STORE_OK; | |
| 2242 } | |
| 2243 | |
| 2244 Auth_Store_Result Auth_Store_Find_Guest_Identity( | |
| 2245 Auth_Store *p_store, | |
| 2246 const char *guest_id, | |
| 2247 int64 current_unix, | |
| 2248 Auth_Guest_Identity_Record *p_record) | |
| 2249 { | |
| 2250 if (!p_store || !guest_id || !p_record) | |
| 2251 return AUTH_STORE_INVALID_ARG; | |
| 2252 | |
| 2253 memset(p_record, 0, sizeof(*p_record)); | |
| 2254 | |
| 2255 Dowa_Arena *p_arena = Dowa_Arena_Create(2048); | |
| 2256 if (!p_arena) | |
| 2257 return AUTH_STORE_ERROR; | |
| 2258 | |
| 2259 const char *params[] = {guest_id}; | |
| 2260 pthread_mutex_lock(&p_store->mutex); | |
| 2261 Deita_Result_Set *p_result = Deita_Query_Execute_Prepared( | |
| 2262 p_store->p_connection, | |
| 2263 "SELECT id, created_at, last_seen_at, expires_at" | |
| 2264 " FROM guest_identities WHERE id = ?", | |
| 2265 1, params, p_arena); | |
| 2266 if (!p_result || !Deita_Result_Set_Next(p_result)) | |
| 2267 { | |
| 2268 if (p_result) | |
| 2269 Deita_Result_Set_Free(p_result); | |
| 2270 pthread_mutex_unlock(&p_store->mutex); | |
| 2271 Dowa_Arena_Free(p_arena); | |
| 2272 return AUTH_STORE_NOT_FOUND; | |
| 2273 } | |
| 2274 auth__copy_text_fixed(p_record->id, sizeof(p_record->id), | |
| 2275 Deita_Result_Set_Get_Text(p_result, 0)); | |
| 2276 p_record->created_at = Deita_Result_Set_Get_Integer(p_result, 1); | |
| 2277 p_record->last_seen_at = Deita_Result_Set_Get_Integer(p_result, 2); | |
| 2278 p_record->expires_at = Deita_Result_Set_Get_Integer(p_result, 3); | |
| 2279 Deita_Result_Set_Free(p_result); | |
| 2280 pthread_mutex_unlock(&p_store->mutex); | |
| 2281 Dowa_Arena_Free(p_arena); | |
| 2282 | |
| 2283 if (current_unix >= p_record->expires_at) | |
| 2284 return AUTH_STORE_EXPIRED; | |
| 2285 | |
| 2286 return AUTH_STORE_OK; | |
| 2287 } | |
| 2288 | |
| 2289 /* ------------------------------------------------------------------ */ | |
| 2290 /* Guest quota */ | |
| 2291 /* ------------------------------------------------------------------ */ | |
| 2292 | |
| 2293 static void auth__i64_str(char *buf, size_t size, int64 value) | |
| 2294 { | |
| 2295 snprintf(buf, size, "%lld", (long long)value); | |
| 2296 } | |
| 2297 | |
| 2298 Auth_Store_Result Auth_Store_Guest_Get_Usage( | |
| 2299 Auth_Store *p_store, | |
| 2300 const char *guest_id, | |
| 2301 int64 window_start, | |
| 2302 Auth_Store_Guest_Usage *p_usage) | |
| 2303 { | |
| 2304 if (!p_store || !guest_id || !p_usage) | |
| 2305 return AUTH_STORE_INVALID_ARG; | |
| 2306 | |
| 2307 memset(p_usage, 0, sizeof(*p_usage)); | |
| 2308 | |
| 2309 char ws_str[32]; | |
| 2310 auth__i64_str(ws_str, sizeof(ws_str), window_start); | |
| 2311 | |
| 2312 Dowa_Arena *p_arena = Dowa_Arena_Create(2048); | |
| 2313 if (!p_arena) | |
| 2314 return AUTH_STORE_ERROR; | |
| 2315 | |
| 2316 const char *params[] = {guest_id, ws_str}; | |
| 2317 pthread_mutex_lock(&p_store->mutex); | |
| 2318 auth__reap_expired_reservations_locked(p_store, (int64)time(NULL)); | |
| 2319 Deita_Result_Set *p_result = Deita_Query_Execute_Prepared( | |
| 2320 p_store->p_connection, | |
| 2321 "SELECT turns_used, output_tokens_used, output_tokens_reserved" | |
| 2322 " FROM guest_usage WHERE guest_id = ? AND window_start = ?", | |
| 2323 2, params, p_arena); | |
| 2324 if (p_result && Deita_Result_Set_Next(p_result)) | |
| 2325 { | |
| 2326 p_usage->turns_used = Deita_Result_Set_Get_Integer(p_result, 0); | |
| 2327 p_usage->output_tokens_used = Deita_Result_Set_Get_Integer(p_result, 1); | |
| 2328 p_usage->output_tokens_reserved = Deita_Result_Set_Get_Integer(p_result, 2); | |
| 2329 } | |
| 2330 if (p_result) | |
| 2331 Deita_Result_Set_Free(p_result); | |
| 2332 pthread_mutex_unlock(&p_store->mutex); | |
| 2333 Dowa_Arena_Free(p_arena); | |
| 2334 return AUTH_STORE_OK; | |
| 2335 } | |
| 2336 | |
| 2337 Auth_Store_Guest_Quota_Result Auth_Store_Guest_Reserve( | |
| 2338 Auth_Store *p_store, | |
| 2339 const char *guest_id, | |
| 2340 const char *request_id, | |
| 2341 int64 window_start, | |
| 2342 int64 max_output_tokens, | |
| 2343 int64 turns_limit, | |
| 2344 int64 tokens_limit, | |
| 2345 int64 reservation_expires) | |
| 2346 { | |
| 2347 if (!p_store || !guest_id || !request_id || | |
| 2348 max_output_tokens <= 0 || turns_limit <= 0 || tokens_limit <= 0) | |
| 2349 return AUTH_STORE_GUEST_QUOTA_ERROR; | |
| 2350 | |
| 2351 char ws_str[32], exp_str[32], tok_str[32]; | |
| 2352 auth__i64_str(ws_str, sizeof(ws_str), window_start); | |
| 2353 auth__i64_str(exp_str, sizeof(exp_str), reservation_expires); | |
| 2354 auth__i64_str(tok_str, sizeof(tok_str), max_output_tokens); | |
| 2355 | |
| 2356 Dowa_Arena *p_arena = Dowa_Arena_Create(4096); | |
| 2357 if (!p_arena) | |
| 2358 return AUTH_STORE_GUEST_QUOTA_ERROR; | |
| 2359 | |
| 2360 pthread_mutex_lock(&p_store->mutex); | |
| 2361 | |
| 2362 if (Deita_Query_Execute_Update(p_store->p_connection, "BEGIN IMMEDIATE") < 0) | |
| 2363 { | |
| 2364 pthread_mutex_unlock(&p_store->mutex); | |
| 2365 Dowa_Arena_Free(p_arena); | |
| 2366 return AUTH_STORE_GUEST_QUOTA_ERROR; | |
| 2367 } | |
| 2368 auth__reap_expired_reservations_locked(p_store, (int64)time(NULL)); | |
| 2369 | |
| 2370 /* Ensure usage row exists for this window. */ | |
| 2371 const char *ins_params[] = {guest_id, ws_str}; | |
| 2372 if (Deita_Query_Execute_Update_Prepared( | |
| 2373 p_store->p_connection, | |
| 2374 "INSERT OR IGNORE INTO guest_usage" | |
| 2375 " (guest_id, window_start, count)" | |
| 2376 " VALUES (?, ?, 0)", | |
| 2377 2, ins_params) < 0) | |
| 2378 { | |
| 2379 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2380 pthread_mutex_unlock(&p_store->mutex); | |
| 2381 Dowa_Arena_Free(p_arena); | |
| 2382 return AUTH_STORE_GUEST_QUOTA_ERROR; | |
| 2383 } | |
| 2384 | |
| 2385 /* Read current usage. */ | |
| 2386 const char *sel_params[] = {guest_id, ws_str}; | |
| 2387 Deita_Result_Set *p_result = Deita_Query_Execute_Prepared( | |
| 2388 p_store->p_connection, | |
| 2389 "SELECT turns_used, output_tokens_used, output_tokens_reserved" | |
| 2390 " FROM guest_usage WHERE guest_id = ? AND window_start = ?", | |
| 2391 2, sel_params, p_arena); | |
| 2392 if (!p_result || !Deita_Result_Set_Next(p_result)) | |
| 2393 { | |
| 2394 if (p_result) Deita_Result_Set_Free(p_result); | |
| 2395 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2396 pthread_mutex_unlock(&p_store->mutex); | |
| 2397 Dowa_Arena_Free(p_arena); | |
| 2398 return AUTH_STORE_GUEST_QUOTA_ERROR; | |
| 2399 } | |
| 2400 int64 turns_used = Deita_Result_Set_Get_Integer(p_result, 0); | |
| 2401 int64 tokens_used = Deita_Result_Set_Get_Integer(p_result, 1); | |
| 2402 int64 tokens_resvd = Deita_Result_Set_Get_Integer(p_result, 2); | |
| 2403 Deita_Result_Set_Free(p_result); | |
| 2404 | |
| 2405 /* Check turn limit. */ | |
| 2406 if (turns_used >= turns_limit) | |
| 2407 { | |
| 2408 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2409 pthread_mutex_unlock(&p_store->mutex); | |
| 2410 Dowa_Arena_Free(p_arena); | |
| 2411 return AUTH_STORE_GUEST_QUOTA_TURNS_EXHAUSTED; | |
| 2412 } | |
| 2413 | |
| 2414 /* Check token limit: used + reserved + new_reservation <= limit. */ | |
| 2415 if (tokens_used + tokens_resvd + max_output_tokens > tokens_limit) | |
| 2416 { | |
| 2417 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2418 pthread_mutex_unlock(&p_store->mutex); | |
| 2419 Dowa_Arena_Free(p_arena); | |
| 2420 return AUTH_STORE_GUEST_QUOTA_TOKENS_EXHAUSTED; | |
| 2421 } | |
| 2422 | |
| 2423 /* Update usage: charge turn, add token reservation. */ | |
| 2424 const char *upd_params[] = {tok_str, guest_id, ws_str}; | |
| 2425 if (Deita_Query_Execute_Update_Prepared( | |
| 2426 p_store->p_connection, | |
| 2427 "UPDATE guest_usage" | |
| 2428 " SET turns_used = turns_used + 1," | |
| 2429 " output_tokens_reserved = output_tokens_reserved + ?," | |
| 2430 " count = count + 1" | |
| 2431 " WHERE guest_id = ? AND window_start = ?", | |
| 2432 3, upd_params) <= 0) | |
| 2433 { | |
| 2434 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2435 pthread_mutex_unlock(&p_store->mutex); | |
| 2436 Dowa_Arena_Free(p_arena); | |
| 2437 return AUTH_STORE_GUEST_QUOTA_ERROR; | |
| 2438 } | |
| 2439 | |
| 2440 /* Insert reservation row. */ | |
| 2441 const char *res_params[] = {request_id, guest_id, ws_str, tok_str, exp_str}; | |
| 2442 if (Deita_Query_Execute_Update_Prepared( | |
| 2443 p_store->p_connection, | |
| 2444 "INSERT INTO guest_usage_reservations" | |
| 2445 " (request_id, guest_id, window_start, output_tokens_reserved, expires_at)" | |
| 2446 " VALUES (?, ?, ?, ?, ?)", | |
| 2447 5, res_params) < 0) | |
| 2448 { | |
| 2449 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2450 pthread_mutex_unlock(&p_store->mutex); | |
| 2451 Dowa_Arena_Free(p_arena); | |
| 2452 return AUTH_STORE_GUEST_QUOTA_ERROR; | |
| 2453 } | |
| 2454 | |
| 2455 if (Deita_Query_Execute_Update(p_store->p_connection, "COMMIT") < 0) | |
| 2456 { | |
| 2457 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2458 pthread_mutex_unlock(&p_store->mutex); | |
| 2459 Dowa_Arena_Free(p_arena); | |
| 2460 return AUTH_STORE_GUEST_QUOTA_ERROR; | |
| 2461 } | |
| 2462 | |
| 2463 pthread_mutex_unlock(&p_store->mutex); | |
| 2464 Dowa_Arena_Free(p_arena); | |
| 2465 return AUTH_STORE_GUEST_QUOTA_OK; | |
| 2466 } | |
| 2467 | |
| 2468 Auth_Store_Result Auth_Store_Guest_Reconcile( | |
| 2469 Auth_Store *p_store, | |
| 2470 const char *request_id, | |
| 2471 int64 actual_output_tokens) | |
| 2472 { | |
| 2473 if (!p_store || !request_id) | |
| 2474 return AUTH_STORE_INVALID_ARG; | |
| 2475 if (actual_output_tokens < 0) | |
| 2476 actual_output_tokens = 0; | |
| 2477 | |
| 2478 Dowa_Arena *p_arena = Dowa_Arena_Create(4096); | |
| 2479 if (!p_arena) | |
| 2480 return AUTH_STORE_ERROR; | |
| 2481 | |
| 2482 pthread_mutex_lock(&p_store->mutex); | |
| 2483 | |
| 2484 if (Deita_Query_Execute_Update(p_store->p_connection, "BEGIN IMMEDIATE") < 0) | |
| 2485 { | |
| 2486 pthread_mutex_unlock(&p_store->mutex); | |
| 2487 Dowa_Arena_Free(p_arena); | |
| 2488 return AUTH_STORE_ERROR; | |
| 2489 } | |
| 2490 | |
| 2491 /* Fetch reservation. */ | |
| 2492 const char *sel_params[] = {request_id}; | |
| 2493 Deita_Result_Set *p_result = Deita_Query_Execute_Prepared( | |
| 2494 p_store->p_connection, | |
| 2495 "SELECT guest_id, window_start, output_tokens_reserved" | |
| 2496 " FROM guest_usage_reservations WHERE request_id = ?", | |
| 2497 1, sel_params, p_arena); | |
| 2498 if (!p_result || !Deita_Result_Set_Next(p_result)) | |
| 2499 { | |
| 2500 /* Idempotent: reservation already gone. */ | |
| 2501 if (p_result) Deita_Result_Set_Free(p_result); | |
| 2502 Deita_Query_Execute_Update(p_store->p_connection, "COMMIT"); | |
| 2503 pthread_mutex_unlock(&p_store->mutex); | |
| 2504 Dowa_Arena_Free(p_arena); | |
| 2505 return AUTH_STORE_OK; | |
| 2506 } | |
| 2507 | |
| 2508 char guest_id[37]; | |
| 2509 auth__copy_text_fixed(guest_id, sizeof(guest_id), | |
| 2510 Deita_Result_Set_Get_Text(p_result, 0)); | |
| 2511 int64 window_start = Deita_Result_Set_Get_Integer(p_result, 1); | |
| 2512 int64 tokens_reserved = Deita_Result_Set_Get_Integer(p_result, 2); | |
| 2513 Deita_Result_Set_Free(p_result); | |
| 2514 | |
| 2515 /* Charge provider-reported usage even when it exceeds the reservation. */ | |
| 2516 int64 to_charge = actual_output_tokens; | |
| 2517 | |
| 2518 char ws_str[32], charge_str[32], res_str[32]; | |
| 2519 auth__i64_str(ws_str, sizeof(ws_str), window_start); | |
| 2520 auth__i64_str(charge_str, sizeof(charge_str), to_charge); | |
| 2521 auth__i64_str(res_str, sizeof(res_str), tokens_reserved); | |
| 2522 | |
| 2523 /* Update usage: add actual tokens, subtract reservation. */ | |
| 2524 const char *upd_params[] = {charge_str, res_str, guest_id, ws_str}; | |
| 2525 if (Deita_Query_Execute_Update_Prepared( | |
| 2526 p_store->p_connection, | |
| 2527 "UPDATE guest_usage" | |
| 2528 " SET output_tokens_used = output_tokens_used + ?," | |
| 2529 " output_tokens_reserved = MAX(0, output_tokens_reserved - ?)" | |
| 2530 " WHERE guest_id = ? AND window_start = ?", | |
| 2531 4, upd_params) < 0) | |
| 2532 { | |
| 2533 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2534 pthread_mutex_unlock(&p_store->mutex); | |
| 2535 Dowa_Arena_Free(p_arena); | |
| 2536 return AUTH_STORE_ERROR; | |
| 2537 } | |
| 2538 | |
| 2539 /* Delete reservation. */ | |
| 2540 const char *del_params[] = {request_id}; | |
| 2541 if (Deita_Query_Execute_Update_Prepared( | |
| 2542 p_store->p_connection, | |
| 2543 "DELETE FROM guest_usage_reservations WHERE request_id = ?", | |
| 2544 1, del_params) < 0) | |
| 2545 { | |
| 2546 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2547 pthread_mutex_unlock(&p_store->mutex); | |
| 2548 Dowa_Arena_Free(p_arena); | |
| 2549 return AUTH_STORE_ERROR; | |
| 2550 } | |
| 2551 | |
| 2552 if (Deita_Query_Execute_Update(p_store->p_connection, "COMMIT") < 0) | |
| 2553 { | |
| 2554 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2555 pthread_mutex_unlock(&p_store->mutex); | |
| 2556 Dowa_Arena_Free(p_arena); | |
| 2557 return AUTH_STORE_ERROR; | |
| 2558 } | |
| 2559 | |
| 2560 pthread_mutex_unlock(&p_store->mutex); | |
| 2561 Dowa_Arena_Free(p_arena); | |
| 2562 return AUTH_STORE_OK; | |
| 2563 } | |
| 2564 | |
| 2565 Auth_Store_Result Auth_Store_Guest_Release( | |
| 2566 Auth_Store *p_store, | |
| 2567 const char *request_id) | |
| 2568 { | |
| 2569 if (!p_store || !request_id) | |
| 2570 return AUTH_STORE_INVALID_ARG; | |
| 2571 | |
| 2572 Dowa_Arena *p_arena = Dowa_Arena_Create(4096); | |
| 2573 if (!p_arena) | |
| 2574 return AUTH_STORE_ERROR; | |
| 2575 | |
| 2576 pthread_mutex_lock(&p_store->mutex); | |
| 2577 | |
| 2578 if (Deita_Query_Execute_Update(p_store->p_connection, "BEGIN IMMEDIATE") < 0) | |
| 2579 { | |
| 2580 pthread_mutex_unlock(&p_store->mutex); | |
| 2581 Dowa_Arena_Free(p_arena); | |
| 2582 return AUTH_STORE_ERROR; | |
| 2583 } | |
| 2584 | |
| 2585 /* Fetch reservation. */ | |
| 2586 const char *sel_params[] = {request_id}; | |
| 2587 Deita_Result_Set *p_result = Deita_Query_Execute_Prepared( | |
| 2588 p_store->p_connection, | |
| 2589 "SELECT guest_id, window_start, output_tokens_reserved" | |
| 2590 " FROM guest_usage_reservations WHERE request_id = ?", | |
| 2591 1, sel_params, p_arena); | |
| 2592 if (!p_result || !Deita_Result_Set_Next(p_result)) | |
| 2593 { | |
| 2594 /* Idempotent: reservation already gone. */ | |
| 2595 if (p_result) Deita_Result_Set_Free(p_result); | |
| 2596 Deita_Query_Execute_Update(p_store->p_connection, "COMMIT"); | |
| 2597 pthread_mutex_unlock(&p_store->mutex); | |
| 2598 Dowa_Arena_Free(p_arena); | |
| 2599 return AUTH_STORE_OK; | |
| 2600 } | |
| 2601 | |
| 2602 char guest_id[37]; | |
| 2603 auth__copy_text_fixed(guest_id, sizeof(guest_id), | |
| 2604 Deita_Result_Set_Get_Text(p_result, 0)); | |
| 2605 int64 window_start = Deita_Result_Set_Get_Integer(p_result, 1); | |
| 2606 int64 tokens_reserved = Deita_Result_Set_Get_Integer(p_result, 2); | |
| 2607 Deita_Result_Set_Free(p_result); | |
| 2608 | |
| 2609 char ws_str[32], res_str[32]; | |
| 2610 auth__i64_str(ws_str, sizeof(ws_str), window_start); | |
| 2611 auth__i64_str(res_str, sizeof(res_str), tokens_reserved); | |
| 2612 | |
| 2613 /* Release token reservation; turns_used is unchanged (turn already charged). */ | |
| 2614 const char *upd_params[] = {res_str, guest_id, ws_str}; | |
| 2615 if (Deita_Query_Execute_Update_Prepared( | |
| 2616 p_store->p_connection, | |
| 2617 "UPDATE guest_usage" | |
| 2618 " SET output_tokens_reserved = MAX(0, output_tokens_reserved - ?)" | |
| 2619 " WHERE guest_id = ? AND window_start = ?", | |
| 2620 3, upd_params) < 0) | |
| 2621 { | |
| 2622 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2623 pthread_mutex_unlock(&p_store->mutex); | |
| 2624 Dowa_Arena_Free(p_arena); | |
| 2625 return AUTH_STORE_ERROR; | |
| 2626 } | |
| 2627 | |
| 2628 /* Delete reservation. */ | |
| 2629 const char *del_params[] = {request_id}; | |
| 2630 if (Deita_Query_Execute_Update_Prepared( | |
| 2631 p_store->p_connection, | |
| 2632 "DELETE FROM guest_usage_reservations WHERE request_id = ?", | |
| 2633 1, del_params) < 0) | |
| 2634 { | |
| 2635 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2636 pthread_mutex_unlock(&p_store->mutex); | |
| 2637 Dowa_Arena_Free(p_arena); | |
| 2638 return AUTH_STORE_ERROR; | |
| 2639 } | |
| 2640 | |
| 2641 if (Deita_Query_Execute_Update(p_store->p_connection, "COMMIT") < 0) | |
| 2642 { | |
| 2643 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2644 pthread_mutex_unlock(&p_store->mutex); | |
| 2645 Dowa_Arena_Free(p_arena); | |
| 2646 return AUTH_STORE_ERROR; | |
| 2647 } | |
| 2648 | |
| 2649 pthread_mutex_unlock(&p_store->mutex); | |
| 2650 Dowa_Arena_Free(p_arena); | |
| 2651 return AUTH_STORE_OK; | |
| 2652 } | |
| 2653 | |
| 2654 Auth_Store_Result Auth_Store_Guest_Clear_Reservations( | |
| 2655 Auth_Store *p_store, | |
| 2656 const char *guest_id) | |
| 2657 { | |
| 2658 if (!p_store || !guest_id) | |
| 2659 return AUTH_STORE_INVALID_ARG; | |
| 2660 | |
| 2661 pthread_mutex_lock(&p_store->mutex); | |
| 2662 | |
| 2663 if (Deita_Query_Execute_Update(p_store->p_connection, "BEGIN IMMEDIATE") < 0) | |
| 2664 { | |
| 2665 pthread_mutex_unlock(&p_store->mutex); | |
| 2666 return AUTH_STORE_ERROR; | |
| 2667 } | |
| 2668 | |
| 2669 /* | |
| 2670 * Subtract each window's reserved tokens from the usage row. | |
| 2671 * The correlated subquery aggregates reservations per window. | |
| 2672 */ | |
| 2673 const char *upd_params[] = {guest_id, guest_id}; | |
| 2674 if (Deita_Query_Execute_Update_Prepared( | |
| 2675 p_store->p_connection, | |
| 2676 "UPDATE guest_usage" | |
| 2677 " SET output_tokens_reserved = MAX(0, output_tokens_reserved - (" | |
| 2678 " SELECT COALESCE(SUM(r.output_tokens_reserved), 0)" | |
| 2679 " FROM guest_usage_reservations r" | |
| 2680 " WHERE r.guest_id = ? AND r.window_start = guest_usage.window_start" | |
| 2681 " ))" | |
| 2682 " WHERE guest_id = ?", | |
| 2683 2, upd_params) < 0) | |
| 2684 { | |
| 2685 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2686 pthread_mutex_unlock(&p_store->mutex); | |
| 2687 return AUTH_STORE_ERROR; | |
| 2688 } | |
| 2689 | |
| 2690 /* Delete all reservations for this guest. */ | |
| 2691 const char *del_params[] = {guest_id}; | |
| 2692 if (Deita_Query_Execute_Update_Prepared( | |
| 2693 p_store->p_connection, | |
| 2694 "DELETE FROM guest_usage_reservations WHERE guest_id = ?", | |
| 2695 1, del_params) < 0) | |
| 2696 { | |
| 2697 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2698 pthread_mutex_unlock(&p_store->mutex); | |
| 2699 return AUTH_STORE_ERROR; | |
| 2700 } | |
| 2701 | |
| 2702 if (Deita_Query_Execute_Update(p_store->p_connection, "COMMIT") < 0) | |
| 2703 { | |
| 2704 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2705 pthread_mutex_unlock(&p_store->mutex); | |
| 2706 return AUTH_STORE_ERROR; | |
| 2707 } | |
| 2708 | |
| 2709 pthread_mutex_unlock(&p_store->mutex); | |
| 2710 return AUTH_STORE_OK; | |
| 2711 } | |
| 2712 | |
| 2713 Auth_Store_Result Auth_Store_Guest_Reap_Expired( | |
| 2714 Auth_Store *p_store, | |
| 2715 int64 current_unix) | |
| 2716 { | |
| 2717 if (!p_store) | |
| 2718 return AUTH_STORE_INVALID_ARG; | |
| 2719 pthread_mutex_lock(&p_store->mutex); | |
| 2720 auth__reap_expired_reservations_locked(p_store, current_unix); | |
| 2721 pthread_mutex_unlock(&p_store->mutex); | |
| 2722 return AUTH_STORE_OK; | |
| 2723 } | |
| 2724 | |
| 2725 /* ------------------------------------------------------------------ */ | |
| 2726 /* Audit log (public) */ | |
| 2727 /* ------------------------------------------------------------------ */ | |
| 2728 | |
| 2729 Auth_Store_Result Auth_Store_Insert_Audit_Log( | |
| 2730 Auth_Store *p_store, | |
| 2731 const char *actor_user_id, | |
| 2732 const char *action, | |
| 2733 const char *target_user_id, | |
| 2734 const char *detail) | |
| 2735 { | |
| 2736 if (!p_store || !action || action[0] == '\0') | |
| 2737 return AUTH_STORE_INVALID_ARG; | |
| 2738 | |
| 2739 pthread_mutex_lock(&p_store->mutex); | |
| 2740 boolean inserted = auth__insert_audit_log_locked( | |
| 2741 p_store, actor_user_id, action, target_user_id, detail); | |
| 2742 pthread_mutex_unlock(&p_store->mutex); | |
| 2743 return inserted ? AUTH_STORE_OK : AUTH_STORE_ERROR; | |
| 2744 } | |
| 2745 | |
| 2746 /* ------------------------------------------------------------------ */ | |
| 2747 /* Admin password reset */ | |
| 2748 /* ------------------------------------------------------------------ */ | |
| 2749 | |
| 2750 Auth_Store_Result Auth_Store_Admin_Reset_Password( | |
| 2751 Auth_Store *p_store, | |
| 2752 const char *user_id, | |
| 2753 const char *new_encoded_hash, | |
| 2754 const char *actor_user_id) | |
| 2755 { | |
| 2756 if (!p_store || !user_id || !new_encoded_hash) | |
| 2757 return AUTH_STORE_INVALID_ARG; | |
| 2758 if (new_encoded_hash[0] == '\0') | |
| 2759 return AUTH_STORE_INVALID_ARG; | |
| 2760 | |
| 2761 pthread_mutex_lock(&p_store->mutex); | |
| 2762 | |
| 2763 if (Deita_Query_Execute_Update( | |
| 2764 p_store->p_connection, "BEGIN IMMEDIATE") < 0) | |
| 2765 { | |
| 2766 pthread_mutex_unlock(&p_store->mutex); | |
| 2767 return AUTH_STORE_ERROR; | |
| 2768 } | |
| 2769 | |
| 2770 const char *upd_params[] = {new_encoded_hash, user_id}; | |
| 2771 int32 upd = Deita_Query_Execute_Update_Prepared( | |
| 2772 p_store->p_connection, | |
| 2773 "UPDATE users" | |
| 2774 " SET password_hash = ?," | |
| 2775 " password_changed_at = strftime('%s','now')," | |
| 2776 " must_change_password = 1," | |
| 2777 " updated_at = strftime('%s','now')" | |
| 2778 " WHERE id = ?", | |
| 2779 2, upd_params); | |
| 2780 if (upd <= 0) | |
| 2781 { | |
| 2782 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2783 pthread_mutex_unlock(&p_store->mutex); | |
| 2784 return upd < 0 ? AUTH_STORE_ERROR : AUTH_STORE_NOT_FOUND; | |
| 2785 } | |
| 2786 | |
| 2787 const char *rev_params[] = {user_id}; | |
| 2788 int32 rev = Deita_Query_Execute_Update_Prepared( | |
| 2789 p_store->p_connection, | |
| 2790 "UPDATE auth_sessions" | |
| 2791 " SET revoked_at = strftime('%s','now')" | |
| 2792 " WHERE user_id = ? AND revoked_at IS NULL", | |
| 2793 1, rev_params); | |
| 2794 if (rev < 0) | |
| 2795 { | |
| 2796 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2797 pthread_mutex_unlock(&p_store->mutex); | |
| 2798 return AUTH_STORE_ERROR; | |
| 2799 } | |
| 2800 | |
| 2801 if (!auth__insert_audit_log_locked( | |
| 2802 p_store, actor_user_id, "admin_temp_password_reset", user_id, NULL)) | |
| 2803 { | |
| 2804 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2805 pthread_mutex_unlock(&p_store->mutex); | |
| 2806 return AUTH_STORE_ERROR; | |
| 2807 } | |
| 2808 | |
| 2809 if (Deita_Query_Execute_Update(p_store->p_connection, "COMMIT") < 0) | |
| 2810 { | |
| 2811 auth__rollback(p_store, AUTH_STORE_ERROR); | |
| 2812 pthread_mutex_unlock(&p_store->mutex); | |
| 2813 return AUTH_STORE_ERROR; | |
| 2814 } | |
| 2815 | |
| 2816 pthread_mutex_unlock(&p_store->mutex); | |
| 2817 return AUTH_STORE_OK; | |
| 2818 } |