romele / git-trace public
62 commits · 1 branch · 1 tag · 1 contributor (top: Romele) · first commit 3d ago · last push 11m ago
src/auth.c
#include "auth.h" #include "crypto.h" #include "repo.h" #include "util.h" #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> static int run(sqlite3 *db, const char *sql, const char *a, const char *b, int c, const char *d, int e) { sqlite3_stmt *st; if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1; if (a) sqlite3_bind_text(st, 1, a, -1, SQLITE_TRANSIENT); if (b) sqlite3_bind_text(st, 2, b, -1, SQLITE_TRANSIENT); if (c >= 0) sqlite3_bind_int(st, 3, c); if (d) sqlite3_bind_text(st, 4, d, -1, SQLITE_TRANSIENT); if (e >= 0) sqlite3_bind_int(st, 5, e); int rc = sqlite3_step(st); sqlite3_finalize(st); return (rc == SQLITE_DONE) ? 0 : -1; } int auth_user_add(sqlite3 *db, const char *name, const char *pass, const char *email, int is_admin) { char hash[160]; if (pw_hash(pass, hash, sizeof hash) != 0) return -1; sqlite3_stmt *st; if (sqlite3_prepare_v2(db, "INSERT INTO users(name,email,password_hash,is_admin)" " VALUES(?,?,?,?)", -1, &st, NULL) != SQLITE_OK) return -1; sqlite3_bind_text(st, 1, name, -1, SQLITE_TRANSIENT); if (email && *email) sqlite3_bind_text(st, 2, email, -1, SQLITE_TRANSIENT); else sqlite3_bind_null(st, 2); sqlite3_bind_text(st, 3, hash, -1, SQLITE_TRANSIENT); sqlite3_bind_int(st, 4, is_admin); int rc = sqlite3_step(st); sqlite3_finalize(st); memset(hash, 0, sizeof hash); return rc == SQLITE_DONE ? 0 : -1; } int auth_user_passwd(sqlite3 *db, const char *name, const char *pass) { char hash[160]; if (pw_hash(pass, hash, sizeof hash) != 0) return -1; return run(db, "UPDATE users SET password_hash=? WHERE name=?", hash, name, -1, NULL, -1); } /* diagnostic for ops/migration checks: hash scheme name or NULL */ const char *auth_pw_scheme(sqlite3 *db, const char *name) { static char sch[16]; sqlite3_stmt *st; if (sqlite3_prepare_v2(db, "SELECT password_hash FROM users WHERE name=?", -1, &st, NULL) != SQLITE_OK) return NULL; sqlite3_bind_text(st, 1, name, -1, SQLITE_TRANSIENT); const char *rc = NULL; if (sqlite3_step(st) == SQLITE_ROW) { const char *h = (const char *)sqlite3_column_text(st, 0); if (h) { snprintf(sch, sizeof sch, "%s", pw_scheme(h)); rc = sch; } } sqlite3_finalize(st); return rc; } /* ---------------- billing tiers (manual entitlement) ---------------- * The forge stores only the entitlement (tier + expiry); payment * itself is an operator workflow outside the system. Lazy expiry: * an expired tier reads as 'free' and is written back on first use. */ int tier_rank(const char *t) { if (!t) return 0; if (!strcmp(t, "pro")) return 1; if (!strcmp(t, "self")) return 2; if (!strcmp(t, "enterprise")) return 3; return 0; /* free / unknown */ } int auth_tier_ok(const char *t) { return !strcmp(t, "free") || tier_rank(t) > 0; } const char *auth_user_tier(sqlite3 *db, int uid, long *exp_out) { static char t[16] = "free"; long exp = 0; strcpy(t, "free"); sqlite3_stmt *st; if (sqlite3_prepare_v2(db, "SELECT tier,tier_expires_at FROM users WHERE id=?", -1, &st, NULL) == SQLITE_OK) { sqlite3_bind_int(st, 1, uid); if (sqlite3_step(st) == SQLITE_ROW) { const char *s = (const char *)sqlite3_column_text(st, 0); if (sqlite3_column_type(st, 1) != SQLITE_NULL) exp = sqlite3_column_int64(st, 1); if (s && auth_tier_ok(s)) snprintf(t, sizeof t, "%s", s); } sqlite3_finalize(st); } if (exp && exp < time(NULL)) { /* lazily sweep the expired entitlement */ snprintf(t, sizeof t, "free"); exp = 0; sqlite3_prepare_v2(db, "UPDATE users SET tier='free', tier_expires_at=NULL" " WHERE id=?", -1, &st, NULL); sqlite3_bind_int(st, 1, uid); sqlite3_step(st); sqlite3_finalize(st); } if (exp_out) *exp_out = exp; return t; } int auth_tier_set(sqlite3 *db, const char *name, const char *tier, long expires) { if (!auth_tier_ok(tier)) return -1; sqlite3_stmt *st; if (sqlite3_prepare_v2(db, "UPDATE users SET tier=?, tier_expires_at=? WHERE name=?", -1, &st, NULL) != SQLITE_OK) return -1; sqlite3_bind_text(st, 1, tier, -1, SQLITE_TRANSIENT); if (expires > 0) sqlite3_bind_int64(st, 2, expires); else sqlite3_bind_null(st, 2); sqlite3_bind_text(st, 3, name, -1, SQLITE_TRANSIENT); int rc = sqlite3_step(st); int n = sqlite3_changes(db); sqlite3_finalize(st); return (rc == SQLITE_DONE && n == 1) ? 0 : -1; } int auth_is_admin(sqlite3 *db, int uid) { int a = 0; sqlite3_stmt *st; if (sqlite3_prepare_v2(db, "SELECT is_admin FROM users WHERE id=?", -1, &st, NULL) == SQLITE_OK) { sqlite3_bind_int(st, 1, uid); if (sqlite3_step(st) == SQLITE_ROW) a = sqlite3_column_int(st, 0); sqlite3_finalize(st); } return a; } int auth_user_id(sqlite3 *db, const char *name) { sqlite3_stmt *st; if (sqlite3_prepare_v2(db, "SELECT id FROM users WHERE name=?", -1, &st, NULL) != SQLITE_OK) return -1; sqlite3_bind_text(st, 1, name, -1, SQLITE_TRANSIENT); int uid = (sqlite3_step(st) == SQLITE_ROW) ? sqlite3_column_int(st, 0) : -1; sqlite3_finalize(st); return uid; } static int check_password(sqlite3 *db, const char *name, const char *secret) { sqlite3_stmt *st; if (sqlite3_prepare_v2(db, "SELECT id,password_hash FROM users WHERE name=?", -1, &st, NULL) != SQLITE_OK) return -1; sqlite3_bind_text(st, 1, name, -1, SQLITE_TRANSIENT); int uid = -1, upgrade = 0; if (sqlite3_step(st) == SQLITE_ROW) { const char *h = (const char *)sqlite3_column_text(st, 1); if (h && pw_verify(secret, h)) { uid = sqlite3_column_int(st, 0); upgrade = pw_needs_rehash(h); } } sqlite3_finalize(st); /* transparent KDF upgrade: a legacy pbkdf2 row that just verified * is re-stored under the current scheme — no forced resets */ if (uid >= 0 && upgrade) { char nh[160]; if (pw_hash(secret, nh, sizeof nh) == 0) { sqlite3_prepare_v2(db, "UPDATE users SET password_hash=? WHERE id=?", -1, &st, NULL); sqlite3_bind_text(st, 1, nh, -1, SQLITE_TRANSIENT); sqlite3_bind_int(st, 2, uid); sqlite3_step(st); sqlite3_finalize(st); } gt_bzero(nh, sizeof nh); } return uid; } static int check_token(sqlite3 *db, const char *name, const char *secret) { uint8_t dig[32]; sha256(secret, strlen(secret), dig); char hex[65]; to_hex(dig, 32, hex); sqlite3_stmt *st; if (sqlite3_prepare_v2(db, "SELECT u.id FROM auth_tokens t JOIN users u ON u.id=t.user_id" " WHERE u.name=? AND t.token_hash=?" " AND (t.expires_at IS NULL OR t.expires_at > strftime('%s','now'))", -1, &st, NULL) != SQLITE_OK) return -1; sqlite3_bind_text(st, 1, name, -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 2, hex, -1, SQLITE_TRANSIENT); int uid = (sqlite3_step(st) == SQLITE_ROW) ? sqlite3_column_int(st, 0) : -1; sqlite3_finalize(st); return uid; } int auth_token(sqlite3 *db, const char *name, const char *secret) { return check_token(db, name, secret); } int auth_token_lookup(sqlite3 *db, const char *secret) { uint8_t dig[32]; sha256(secret, strlen(secret), dig); char hex[65]; to_hex(dig, 32, hex); sqlite3_stmt *st; if (sqlite3_prepare_v2(db, "SELECT user_id FROM auth_tokens WHERE token_hash=?" " AND (expires_at IS NULL OR expires_at > strftime('%s','now'))", -1, &st, NULL) != SQLITE_OK) return -1; sqlite3_bind_text(st, 1, hex, -1, SQLITE_TRANSIENT); int uid = (sqlite3_step(st) == SQLITE_ROW) ? sqlite3_column_int(st, 0) : -1; sqlite3_finalize(st); return uid; } int auth_email_uid(sqlite3 *db, const char *email) { sqlite3_stmt *st; if (sqlite3_prepare_v2(db, "SELECT id FROM users WHERE email=? COLLATE NOCASE", -1, &st, NULL) != SQLITE_OK) return -1; sqlite3_bind_text(st, 1, email, -1, SQLITE_TRANSIENT); int uid = (sqlite3_step(st) == SQLITE_ROW) ? sqlite3_column_int(st, 0) : -1; sqlite3_finalize(st); return uid; } int auth_check_secret(sqlite3 *db, const char *name, const char *secret) { int uid = check_password(db, name, secret); if (uid < 0) uid = check_token(db, name, secret); return uid; } int auth_mktoken(sqlite3 *db, const char *name, const char *label, char *token_out, size_t n) { int uid = auth_user_id(db, name); if (uid < 0) return -1; char tok[65]; rand_hex(tok, 32); uint8_t dig[32]; sha256(tok, strlen(tok), dig); char hex[65]; to_hex(dig, 32, hex); sqlite3_stmt *st; if (sqlite3_prepare_v2(db, "INSERT INTO auth_tokens(user_id,name,token_hash) VALUES(?,?,?)", -1, &st, NULL) != SQLITE_OK) return -1; sqlite3_bind_int(st, 1, uid); sqlite3_bind_text(st, 2, label, -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 3, hex, -1, SQLITE_TRANSIENT); int rc = sqlite3_step(st); sqlite3_finalize(st); if (rc != SQLITE_DONE) return -1; snprintf(token_out, n, "%s", tok); return 0; } /* ---------------- HTTP-level ---------------- */ int auth_basic(sqlite3 *db, struct request *r) { char *hv = req_header_dup(r, "authorization"); if (!hv || !starts_with(hv, "Basic ")) { free(hv); return 0; } char dec[256]; long dl = b64_decode(hv + 6, strlen(hv + 6), dec, sizeof dec - 1); free(hv); if (dl <= 0) return 0; dec[dl] = '\0'; char *colon = strchr(dec, ':'); if (!colon) return 0; *colon = '\0'; if (!name_ok(dec)) { memset(dec, 0, sizeof dec); return 0; } char uname[64]; size_t ul = strlen(dec); /* name_ok bounds it to 63 */ memcpy(uname, dec, ul + 1); int uid = auth_check_secret(db, dec, colon + 1); memset(dec, 0, sizeof dec); if (uid > 0) { r->user_id = uid; snprintf(r->user_name, sizeof r->user_name, "%s", uname); } return uid; } static const char *cookie_get(struct request *r, const char *key, char *out, size_t n) { char *hv = req_header_dup(r, "cookie"); if (!hv) return NULL; size_t kl = strlen(key); char *p = hv; while (*p) { while (*p == ' ' || *p == ';') p++; char *e = strchr(p, ';'); size_t pl = e ? (size_t)(e - p) : strlen(p); const char *eq = memchr(p, '=', pl); if (eq && (size_t)(eq - p) == kl && strncmp(p, key, kl) == 0) { size_t vl = pl - (size_t)(eq - p) - 1; if (vl >= n) vl = n - 1; memcpy(out, eq + 1, vl); out[vl] = '\0'; free(hv); return out; } p = e ? e + 1 : p + pl; } free(hv); return NULL; } int session_create(sqlite3 *db, int uid, char token_out[65]) { rand_hex(token_out, 32); sqlite3_stmt *st; if (sqlite3_prepare_v2(db, "INSERT INTO sessions(token,user_id,expires_at)" " VALUES(?,?,strftime('%s','now')+2592000)", -1, &st, NULL) != SQLITE_OK) return -1; sqlite3_bind_text(st, 1, token_out, -1, SQLITE_TRANSIENT); sqlite3_bind_int(st, 2, uid); int rc = sqlite3_step(st); sqlite3_finalize(st); return rc == SQLITE_DONE ? 0 : -1; } void session_delete(sqlite3 *db, const char *token) { run(db, "DELETE FROM sessions WHERE token=?", token, NULL, -1, NULL, -1); } void auth_identify(sqlite3 *db, struct request *r) { r->user_id = 0; r->user_name[0] = '\0'; r->session_tok[0] = '\0'; char tok[80]; if (!cookie_get(r, "gt_session", tok, sizeof tok)) return; if (strlen(tok) != 64) return; snprintf(r->session_tok, sizeof r->session_tok, "%s", tok); sqlite3_stmt *st; if (sqlite3_prepare_v2(db, "SELECT u.id,u.name FROM sessions s JOIN users u ON u.id=s.user_id" " WHERE s.token=? AND s.expires_at > strftime('%s','now')", -1, &st, NULL) != SQLITE_OK) return; sqlite3_bind_text(st, 1, tok, -1, SQLITE_TRANSIENT); if (sqlite3_step(st) == SQLITE_ROW) { r->user_id = sqlite3_column_int(st, 0); snprintf(r->user_name, sizeof r->user_name, "%s", (const char *)sqlite3_column_text(st, 1)); } sqlite3_finalize(st); } /* ---------------- ACL ---------------- */ /* * repo_perm resolves the effective permission of uid over owner/name. * Rules: owner -> 'a'; explicit repo_acl row -> its role; public -> 'r' * for anyone (including anonymous uid 0); otherwise 'n'. * A repo absent from the DB is treated as public/read-only: filesystem * presence is the source of truth for existence, the DB for policy. */ char repo_perm(sqlite3 *db, const char *owner, const char *name, int uid) { sqlite3_stmt *st; int rc = sqlite3_prepare_v2(db, "SELECT r.id, r.owner_id, r.is_public FROM repos r" " LEFT JOIN users u ON u.id = r.owner_id" " WHERE r.name=? AND (u.name=? OR (r.owner_id IS NULL AND ?='_'))", -1, &st, NULL); if (rc != SQLITE_OK) return 'n'; /* fail closed: a DB error must not open private repos to readers */ sqlite3_bind_text(st, 1, name, -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 2, owner, -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 3, owner, -1, SQLITE_TRANSIENT); if (sqlite3_step(st) != SQLITE_ROW) { sqlite3_finalize(st); return 'r'; /* unregistered repo: public read */ } int repo_id = sqlite3_column_int(st, 0); int owner_id = sqlite3_column_type(st, 1) == SQLITE_NULL ? 0 : sqlite3_column_int(st, 1); int is_public = sqlite3_column_int(st, 2); sqlite3_finalize(st); if (uid > 0 && uid == owner_id) return 'a'; /* ownerless (_/) repos belong to the platform: site admins manage * them. Deliberately NOT extended to user-owned repos — a global * admin must not gain read access to other users' private code. */ if (owner_id == 0 && uid > 0 && auth_is_admin(db, uid)) return 'a'; int best = is_public ? 1 : 0; /* rank: 'r' or 'n' */ if (uid > 0) { if (sqlite3_prepare_v2(db, "SELECT role FROM repo_acl WHERE repo_id=? AND user_id=?", -1, &st, NULL) == SQLITE_OK) { sqlite3_bind_int(st, 1, repo_id); sqlite3_bind_int(st, 2, uid); if (sqlite3_step(st) == SQLITE_ROW) { const char *role = (const char *)sqlite3_column_text(st, 0); int rr = role ? perm_rank(role[0]) : 0; if (rr > best) best = rr; } sqlite3_finalize(st); } } return perm_char(best); } int repo_register(sqlite3 *db, int owner_uid, const char *name, int is_public, const char *desc) { sqlite3_stmt *st; if (sqlite3_prepare_v2(db, "INSERT INTO repos(owner_id,name,description,is_public)" " VALUES(?,?,?,?)" " ON CONFLICT(owner_id,name) DO UPDATE SET" " description=excluded.description," " is_public=excluded.is_public", -1, &st, NULL) != SQLITE_OK) return -1; if (owner_uid > 0) sqlite3_bind_int(st, 1, owner_uid); else sqlite3_bind_null(st, 1); sqlite3_bind_text(st, 2, name, -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 3, desc ? desc : "", -1, SQLITE_TRANSIENT); sqlite3_bind_int(st, 4, is_public); int rc = sqlite3_step(st); sqlite3_finalize(st); return rc == SQLITE_DONE ? 0 : -1; } /* ---------------- SSH keys ---------------- */ /* * OpenSSH fingerprint: base64(SHA256(wire_key_blob)) without padding. * The wire blob is the second field of a public key line. */ static int ssh_fingerprint(const char *blob64, char *out, size_t outlen) { uint8_t blob[1024]; long n = b64_decode(blob64, strlen(blob64), blob, sizeof blob); if (n <= 0) return -1; uint8_t dig[32]; sha256(blob, (size_t)n, dig); char b[64]; b64_encode(dig, 32, b, sizeof b, 1); return snprintf(out, outlen, "SHA256:%s", b) < (int)outlen ? 0 : -1; } int sshkey_add(sqlite3 *db, const char *user, const char *pubkey_line, const char *label) { int uid = auth_user_id(db, user); if (uid < 0) return -1; /* line: "<type> <blob> [comment]" */ const char *p = pubkey_line; while (*p == ' ' || *p == '\t') p++; const char *sp = strchr(p, ' '); if (!sp) return -1; const char *blob = sp + 1; while (*blob == ' ') blob++; const char *be = blob; while (*be && *be != ' ' && *be != '\t' && *be != '\r' && *be != '\n') be++; char blobbuf[1200]; size_t bl = (size_t)(be - blob); if (bl == 0 || bl >= sizeof blobbuf) return -1; memcpy(blobbuf, blob, bl); blobbuf[bl] = '\0'; char fp[80]; if (ssh_fingerprint(blobbuf, fp, sizeof fp) != 0) return -1; sqlite3_stmt *st; if (sqlite3_prepare_v2(db, "INSERT INTO ssh_keys(user_id,name,pubkey,fingerprint)" " VALUES(?,?,?,?)", -1, &st, NULL) != SQLITE_OK) return -1; sqlite3_bind_int(st, 1, uid); sqlite3_bind_text(st, 2, label ? label : "key", -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 3, pubkey_line, -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 4, fp, -1, SQLITE_TRANSIENT); int rc = sqlite3_step(st); long id = sqlite3_last_insert_rowid(db); sqlite3_finalize(st); return rc == SQLITE_DONE ? (int)id : -1; } int sshkey_rm(sqlite3 *db, long keyid) { sqlite3_stmt *st; if (sqlite3_prepare_v2(db, "DELETE FROM ssh_keys WHERE id=?", -1, &st, NULL) != SQLITE_OK) return -1; sqlite3_bind_int64(st, 1, keyid); int rc = sqlite3_step(st); int n = sqlite3_changes(db); sqlite3_finalize(st); return (rc == SQLITE_DONE && n > 0) ? 0 : -1; } /* web self-service variant: a user may only remove their own key */ int sshkey_rm_own(sqlite3 *db, int uid, long keyid) { sqlite3_stmt *st; if (sqlite3_prepare_v2(db, "DELETE FROM ssh_keys WHERE id=? AND user_id=?", -1, &st, NULL) != SQLITE_OK) return -1; sqlite3_bind_int64(st, 1, keyid); sqlite3_bind_int(st, 2, uid); int rc = sqlite3_step(st); int n = sqlite3_changes(db); sqlite3_finalize(st); return (rc == SQLITE_DONE && n > 0) ? 0 : -1; } /* revoke only if the token belongs to uid */ int auth_token_rm(sqlite3 *db, int uid, long tokid) { sqlite3_stmt *st; if (sqlite3_prepare_v2(db, "DELETE FROM auth_tokens WHERE id=? AND user_id=?", -1, &st, NULL) != SQLITE_OK) return -1; sqlite3_bind_int64(st, 1, tokid); sqlite3_bind_int(st, 2, uid); int rc = sqlite3_step(st); int n = sqlite3_changes(db); sqlite3_finalize(st); return (rc == SQLITE_DONE && n > 0) ? 0 : -1; } int auth_set_email(sqlite3 *db, int uid, const char *email) { sqlite3_stmt *st; if (sqlite3_prepare_v2(db, "UPDATE users SET email=? WHERE id=?", -1, &st, NULL) != SQLITE_OK) return -1; if (email && *email) sqlite3_bind_text(st, 1, email, -1, SQLITE_TRANSIENT); else sqlite3_bind_null(st, 1); sqlite3_bind_int(st, 2, uid); int rc = sqlite3_step(st); sqlite3_finalize(st); return rc == SQLITE_DONE ? 0 : -1; } int sshkey_uid(sqlite3 *db, long keyid, char *uname, size_t unlen) { sqlite3_stmt *st; if (sqlite3_prepare_v2(db, "SELECT u.id,u.name FROM ssh_keys k JOIN users u" " ON u.id=k.user_id WHERE k.id=?", -1, &st, NULL) != SQLITE_OK) return -1; sqlite3_bind_int64(st, 1, keyid); int uid = -1; if (sqlite3_step(st) == SQLITE_ROW) { uid = sqlite3_column_int(st, 0); snprintf(uname, unlen, "%s", (const char *)sqlite3_column_text(st, 1)); } sqlite3_finalize(st); return uid; } /* ---------------- forms ---------------- */ int form_get(const char *body, size_t blen, const char *key, char *out, size_t outlen) { size_t kl = strlen(key); const char *p = body; const char *end = body + blen; while (p < end) { const char *amp = memchr(p, '&', (size_t)(end - p)); const char *pe = amp ? amp : end; const char *eq = memchr(p, '=', (size_t)(pe - p)); if (eq && (size_t)(eq - p) == kl && strncmp(p, key, kl) == 0) { char *d = url_decode(eq + 1, (size_t)(pe - eq - 1)); int rc = snprintf(out, outlen, "%s", d); free(d); return (rc < 0 || (size_t)rc >= outlen) ? -1 : 0; } p = amp ? amp + 1 : end; } return -1; }