diff --git a/src/server.zig b/src/server.zig index 80ff72c..aa2c5d2 100644 --- a/src/server.zig +++ b/src/server.zig @@ -5645,3 +5645,161 @@ test "auth_token is rejected as unauthorized when sessions are not enabled" { try server.start(); defer server.stop(); + const client_mod = @import("client.zig"); + var client = try client_mod.Client.connect(allocator, server.boundAddress()); + defer client.close(); + try std.testing.expectError(client_mod.ClientError.ServerUnauthorized, client.authenticateWithToken("whatever")); +} + +test "an unknown or garbage session token is rejected" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const path = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(path); + + var db = try testDb(allocator, path); + defer db.close(); + const store = authmod.Store.init(&db); + try store.createUser(allocator, "alice", "hunter2", .readwrite, 16); + + var sessions = sessionmod.SessionStore.init(allocator); + defer sessions.deinit(); + + const address = try std.net.Address.parseIp("127.0.0.1", 0); + var server = try Server.init(allocator, &db, address); + server.enableAuth(store); + server.enableSessions(&sessions, 60 * std.time.ns_per_s); + try server.start(); + defer server.stop(); + + const client_mod = @import("client.zig"); + var client = try client_mod.Client.connect(allocator, server.boundAddress()); + defer client.close(); + try std.testing.expectError(client_mod.ClientError.ServerUnauthorized, client.authenticateWithToken("not-a-real-token")); +} + +test "an expired session forces re-authentication even on the same open connection" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const path = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(path); + + var db = try testDb(allocator, path); + defer db.close(); + const store = authmod.Store.init(&db); + try store.createUser(allocator, "alice", "hunter2", .readwrite, 16); + + var sessions = sessionmod.SessionStore.init(allocator); + defer sessions.deinit(); + + const address = try std.net.Address.parseIp("127.0.0.1", 0); + var server = try Server.init(allocator, &db, address); + server.enableAuth(store); + server.enableSessions(&sessions, 500 * std.time.ns_per_ms); + try server.start(); + defer server.stop(); + + const client_mod = @import("client.zig"); + var client = try client_mod.Client.connect(allocator, server.boundAddress()); + defer client.close(); + + try client.authenticate("alice", "hunter2"); + try client.set("k", "v"); + + std.time.sleep(700 * std.time.ns_per_ms); + try std.testing.expectError(client_mod.ClientError.ServerUnauthorized, client.get(allocator, "k")); + try std.testing.expectEqual(@as(u64, 1), server.sessionExpiredTotal.get()); + + // The very same connection re-authenticates and keeps working - the + // socket itself was never the thing that expired, only the session. + try client.authenticate("alice", "hunter2"); + const got = try client.get(allocator, "k"); + defer if (got) |g| allocator.free(g); + try std.testing.expectEqualStrings("v", got.?); +} + +test "each connection authenticates independently under auth enforcement" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const path = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(path); + + var db = try testDb(allocator, path); + defer db.close(); + const store = authmod.Store.init(&db); + try store.createUser(allocator, "alice", "hunter2", .readwrite, 16); + + const address = try std.net.Address.parseIp("127.0.0.1", 0); + var server = try Server.init(allocator, &db, address); + server.enableAuth(store); + try server.start(); + defer server.stop(); + + const client_mod = @import("client.zig"); + var clientA = try client_mod.Client.connect(allocator, server.boundAddress()); + defer clientA.close(); + var clientB = try client_mod.Client.connect(allocator, server.boundAddress()); + defer clientB.close(); + + try clientA.authenticate("alice", "hunter2"); + try clientA.set("shared", "1"); + + // clientB never authenticated - a separate socket does not inherit + // clientA's role even though it hits the exact same server/store. + try std.testing.expectError(client_mod.ClientError.ServerUnauthorized, clientB.get(allocator, "shared")); +} + +test "server without auth enabled allows user_admin to create and list users" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const path = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(path); + + var db = try testDb(allocator, path); + defer db.close(); + + const address = try std.net.Address.parseIp("127.0.0.1", 0); + var server = try Server.init(allocator, &db, address); + try server.start(); + defer server.stop(); + + const client_mod = @import("client.zig"); + var client = try client_mod.Client.connect(allocator, server.boundAddress()); + defer client.close(); + + try client.adminCreateUser("alice", "hunter2", .readwrite, 16); + const users = try client.adminListUsers(allocator); + defer authmod.freeUserList(allocator, users); + try std.testing.expectEqual(@as(usize, 1), users.len); + try std.testing.expectEqualStrings("alice", users[0].username); + try std.testing.expectEqual(authmod.Role.readwrite, users[0].role); + + // The account just created over the wire really works for real auth. + var second = try client_mod.Client.connect(allocator, server.boundAddress()); + defer second.close(); + try second.authenticate("alice", "hunter2"); +} + +test "user_admin requires admin role once auth is enabled" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const path = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(path); + + var db = try testDb(allocator, path); + defer db.close(); + const store = authmod.Store.init(&db); + try store.createUser(allocator, "root", "toor", .admin, 16); + try store.createUser(allocator, "viewer", "pass", .readonly, 16); + + const address = try std.net.Address.parseIp("127.0.0.1", 0); + var server = try Server.init(allocator, &db, address); + server.enableAuth(store); + try server.start(); + defer server.stop(); +