Skip to content

Commit 1ec78af

Browse files
justrachclaude
andauthored
feat: add --auth-key CLI flag and wire auth into HTTP server (#48)
The AuthStore existed but was never integrated into the server or exposed via CLI. This meant auth was effectively dead code. Changes: - Add --auth-key <key> CLI flag that registers an admin API key - Wire AuthStore into Database struct so it's accessible from server - HTTP server now enforces auth on all data endpoints when enabled - /health and /metrics remain public (no key needed) - All /db/*, /collections, /branch/*, /cdc/* require X-Api-Key - Add 401 Unauthorized HTTP status to error handler - Unknown CLI flags now error immediately instead of being silently ignored No auth configured → open access (dev mode), same as before. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent befe436 commit 1ec78af

3 files changed

Lines changed: 26 additions & 0 deletions

File tree

src/collection.zig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1471,6 +1471,7 @@ pub const Database = struct {
14711471
data_dir_len: usize,
14721472
alloc: std.mem.Allocator,
14731473
mu: std.Thread.RwLock,
1474+
auth: @import("auth.zig").AuthStore,
14741475

14751476
pub const TenantQuota = struct {
14761477
max_collections: u32 = std.math.maxInt(u32),
@@ -1503,6 +1504,7 @@ pub const Database = struct {
15031504
try db.cdc.start();
15041505
db.alloc = alloc;
15051506
db.mu = .{};
1507+
db.auth = .{};
15061508

15071509
const n = @min(resolved_data_dir.len, 255);
15081510
@memcpy(db.data_dir_buf[0..n], resolved_data_dir[0..n]);

src/main.zig

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub fn main() !void {
1818
var use_wire: bool = true; // wire protocol by default
1919
var use_http: bool = false;
2020
var unix_path: ?[]const u8 = null;
21+
var auth_key: ?[]const u8 = null;
2122

2223
// Replication flags
2324
var repl_enabled: bool = false;
@@ -44,6 +45,9 @@ pub fn main() !void {
4445
} else if (std.mem.eql(u8, args[i], "--unix") and i + 1 < args.len) {
4546
i += 1;
4647
unix_path = args[i];
48+
} else if (std.mem.eql(u8, args[i], "--auth-key") and i + 1 < args.len) {
49+
i += 1;
50+
auth_key = args[i];
4751
} else if (std.mem.eql(u8, args[i], "--replicate")) {
4852
repl_enabled = true;
4953
} else if (std.mem.eql(u8, args[i], "--node-id") and i + 1 < args.len) {
@@ -67,6 +71,7 @@ pub fn main() !void {
6771
\\ --http HTTP REST API
6872
\\ --both run wire + HTTP (wire on port, HTTP on port+1)
6973
\\ --unix <path> also listen on a Unix domain socket
74+
\\ --auth-key <key> require this API key for all requests
7075
\\
7176
\\Replication (Calvin deterministic):
7277
\\ --replicate enable Calvin replication
@@ -87,6 +92,9 @@ pub fn main() !void {
8792
\\
8893
, .{});
8994
return;
95+
} else {
96+
std.log.err("unknown flag: {s}", .{args[i]});
97+
return error.InvalidArgument;
9098
}
9199
}
92100

@@ -101,6 +109,12 @@ pub fn main() !void {
101109
const db = try collection.Database.open(alloc, data_dir);
102110
defer db.close();
103111

112+
// ── configure auth ────────────────────────────────────────────────────
113+
if (auth_key) |key| {
114+
_ = db.auth.addKey(key, "cli", .admin);
115+
std.log.info("Auth enabled (--auth-key)", .{});
116+
}
117+
104118
// ── replication setup ─────────────────────────────────────────────────
105119
if (repl_enabled) {
106120
std.log.info("Calvin replication: node={d} leader={} repl_port={d}", .{

src/server.zig

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
/// GET /context/:col smart context discovery (q, limit query params)
1313
const std = @import("std");
1414
const activity = @import("activity.zig");
15+
const auth = @import("auth.zig");
1516
const collection = @import("collection.zig");
1617
const Database = collection.Database;
1718

@@ -233,6 +234,14 @@ fn dispatch(srv: *Server, raw: []const u8, alloc: std.mem.Allocator) usize {
233234
return ok(getBodyBuf()[0..fbs.pos]);
234235
}
235236

237+
// ── Auth gate — public endpoints above, protected endpoints below ────
238+
if (srv.db.auth.isEnabled()) {
239+
const api_key = auth.AuthStore.extractHttpKey(raw) orelse
240+
return err(401, "unauthorized — missing X-Api-Key header");
241+
if (srv.db.auth.verify(api_key) == null)
242+
return err(401, "unauthorized — invalid API key");
243+
}
244+
236245
if (std.mem.eql(u8, path, "/billing") and std.mem.eql(u8, method, "GET"))
237246
return handleBillingLog(srv);
238247

@@ -760,6 +769,7 @@ fn err(code: u16, msg: []const u8) usize {
760769
const body = std.fmt.bufPrint(&scratch, "{{\"error\":\"{s}\"}}", .{msg}) catch msg;
761770
const status = switch (code) {
762771
400 => "Bad Request",
772+
401 => "Unauthorized",
763773
429 => "Too Many Requests",
764774
404 => "Not Found",
765775
else => "Internal Server Error",

0 commit comments

Comments
 (0)