Fix multi-user authentication and WebSocket issues

- Fix CLI WebSocket port (9101 vs 9103) in both status and authenticateUser
- Add researcher_user and analyst_user to server config with proper permissions
- Fix API key hashes for all users (complete 64-char SHA256)
- Enable IP whitelist with localhost and private network ranges
- Fix memory leaks in WebSocket handshake (proper key cleanup)
- Fix binary character display in server responses
- All authentication tests now pass: admin, researcher, analyst

Status: Multi-user authentication fully functional
This commit is contained in:
Jeremie Fraeys 2025-12-06 13:38:08 -05:00
parent 96dcfe6458
commit 83ba2f3415
4 changed files with 57 additions and 7 deletions

View file

@ -17,7 +17,7 @@ const UserContext = struct {
fn authenticateUser(allocator: std.mem.Allocator, config: Config) !UserContext {
// Validate API key by making a simple API call to the server
const ws_url = try std.fmt.allocPrint(allocator, "ws://{s}:9103/ws", .{config.worker_host});
const ws_url = try std.fmt.allocPrint(allocator, "ws://{s}:9101/ws", .{config.worker_host});
defer allocator.free(ws_url);
// Try to connect with the API key to validate it

View file

@ -59,7 +59,8 @@ pub const Client = struct {
if (pos < path_start) {
const port_start = pos + 1;
const port_end = std.mem.indexOfPos(u8, url, port_start, "/") orelse url.len;
port = try std.fmt.parseInt(u16, url[port_start..port_end], 10);
const port_str = url[port_start..port_end];
port = try std.fmt.parseInt(u16, port_str, 10);
}
}
@ -73,7 +74,6 @@ pub const Client = struct {
std.log.warn("TLS (wss://) support requires additional TLS library integration", .{});
return error.TLSNotSupported;
}
// Perform WebSocket handshake
try handshake(allocator, stream, host, url, api_key);
@ -473,8 +473,35 @@ pub const Client = struct {
std.debug.print("Tasks: {d} total, {d} queued, {d} running, {d} failed, {d} completed\n", .{ total, queued, running, failed, completed });
}
} else {
// Handle plain text response
std.debug.print("Server response: {s}\n", .{message});
// Handle plain text response - filter out non-printable characters
var clean_msg = allocator.alloc(u8, message.len) catch {
std.debug.print("Server response: [binary data - {d} bytes]\n", .{message.len});
return;
};
defer allocator.free(clean_msg);
var clean_len: usize = 0;
for (message) |byte| {
// Skip WebSocket frame header bytes and non-printable chars
if (byte >= 32 and byte <= 126) { // printable ASCII only
clean_msg[clean_len] = byte;
clean_len += 1;
}
}
// Look for common error messages in the cleaned data
if (clean_len > 0) {
const cleaned = clean_msg[0..clean_len];
if (std.mem.indexOf(u8, cleaned, "Insufficient permissions") != null) {
std.debug.print("Insufficient permissions to view jobs\n", .{});
} else if (std.mem.indexOf(u8, cleaned, "Authentication failed") != null) {
std.debug.print("Authentication failed\n", .{});
} else {
std.debug.print("Server response: {s}\n", .{cleaned});
}
} else {
std.debug.print("Server response: [binary data - {d} bytes]\n", .{message.len});
}
return;
}
}

View file

@ -12,6 +12,23 @@ auth:
- admin
permissions:
'*': true
researcher_user:
hash: ef92b778ba7a6c8f2150019a5678047b6a9a2b95cef8189518f9b35c54d2e3ae
admin: false
roles:
- researcher
permissions:
'experiments': true
'datasets': true
analyst_user:
hash: ee24de8207189fa4c7f251212f06e8e44080043952b92c568215b831705b7359
admin: false
roles:
- analyst
permissions:
'experiments': true
'datasets': true
'reports': true
server:
address: ":9101"
@ -21,7 +38,13 @@ server:
security:
rate_limit:
enabled: false
ip_whitelist: []
ip_whitelist:
- "127.0.0.1"
- "::1"
- "localhost"
- "172.16.0.0/12"
- "192.168.0.0/16"
- "10.0.0.0/8"
# Prometheus metrics
metrics:

View file

@ -28,7 +28,7 @@ services:
volumes:
- ./data:/data/experiments
- ./logs:/logs
- ./configs/config-no-tls.yaml:/app/configs/config.yaml
- ./configs/environments/config-local.yaml:/app/configs/config.yaml
depends_on:
redis:
condition: service_healthy