Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/collection.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1471,6 +1471,7 @@ pub const Database = struct {
data_dir_len: usize,
alloc: std.mem.Allocator,
mu: std.Thread.RwLock,
auth: @import("auth.zig").AuthStore,

pub const TenantQuota = struct {
max_collections: u32 = std.math.maxInt(u32),
Expand Down Expand Up @@ -1503,6 +1504,7 @@ pub const Database = struct {
try db.cdc.start();
db.alloc = alloc;
db.mu = .{};
db.auth = .{};

const n = @min(resolved_data_dir.len, 255);
@memcpy(db.data_dir_buf[0..n], resolved_data_dir[0..n]);
Expand Down
14 changes: 14 additions & 0 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub fn main() !void {
var use_wire: bool = true; // wire protocol by default
var use_http: bool = false;
var unix_path: ?[]const u8 = null;
var auth_key: ?[]const u8 = null;

// Replication flags
var repl_enabled: bool = false;
Expand All @@ -44,6 +45,9 @@ pub fn main() !void {
} else if (std.mem.eql(u8, args[i], "--unix") and i + 1 < args.len) {
i += 1;
unix_path = args[i];
} else if (std.mem.eql(u8, args[i], "--auth-key") and i + 1 < args.len) {
i += 1;
auth_key = args[i];
} else if (std.mem.eql(u8, args[i], "--replicate")) {
repl_enabled = true;
} else if (std.mem.eql(u8, args[i], "--node-id") and i + 1 < args.len) {
Expand All @@ -67,6 +71,7 @@ pub fn main() !void {
\\ --http HTTP REST API
\\ --both run wire + HTTP (wire on port, HTTP on port+1)
\\ --unix <path> also listen on a Unix domain socket
\\ --auth-key <key> require this API key for all requests
\\
\\Replication (Calvin deterministic):
\\ --replicate enable Calvin replication
Expand All @@ -87,6 +92,9 @@ pub fn main() !void {
\\
, .{});
return;
} else {
std.log.err("unknown flag: {s}", .{args[i]});
return error.InvalidArgument;
}
}

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

// ── configure auth ────────────────────────────────────────────────────
if (auth_key) |key| {
_ = db.auth.addKey(key, "cli", .admin);
std.log.info("Auth enabled (--auth-key)", .{});
Comment on lines +114 to +115
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Enforce --auth-key on wire protocol requests

Adding the CLI key here enables auth state globally, but only the HTTP path checks it; WireServer still executes INSERT/GET/UPDATE/DELETE/SCAN without any auth handshake or key verification. In the default --wire mode (or --both for wire clients), users can still read/write data anonymously even after starting with --auth-key, which violates the flag contract and leaves production deployments unintentionally exposed.

Useful? React with 👍 / 👎.

}

// ── replication setup ─────────────────────────────────────────────────
if (repl_enabled) {
std.log.info("Calvin replication: node={d} leader={} repl_port={d}", .{
Expand Down
10 changes: 10 additions & 0 deletions src/server.zig
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
/// GET /context/:col smart context discovery (q, limit query params)
const std = @import("std");
const activity = @import("activity.zig");
const auth = @import("auth.zig");
const collection = @import("collection.zig");
const Database = collection.Database;

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

// ── Auth gate — public endpoints above, protected endpoints below ────
if (srv.db.auth.isEnabled()) {
const api_key = auth.AuthStore.extractHttpKey(raw) orelse
return err(401, "unauthorized — missing X-Api-Key header");
Comment on lines +239 to +240
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Parse X-Api-Key header case-insensitively

This auth gate depends on extractHttpKey, which matches only the exact literal X-Api-Key: ; HTTP header names are case-insensitive, and many clients/proxies normalize to lowercase (x-api-key). With auth enabled, correctly authenticated requests from those environments will be rejected as missing credentials, causing avoidable 401s for valid traffic.

Useful? React with 👍 / 👎.

if (srv.db.auth.verify(api_key) == null)
return err(401, "unauthorized — invalid API key");
}

if (std.mem.eql(u8, path, "/billing") and std.mem.eql(u8, method, "GET"))
return handleBillingLog(srv);

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