diff --git a/src/relay/src/_index.yaml b/src/relay/src/_index.yaml index 41b9544..67c6f26 100644 --- a/src/relay/src/_index.yaml +++ b/src/relay/src/_index.yaml @@ -191,3 +191,23 @@ entries: - time - security method: run + + # wippy.relay:central_test + - name: central_test + kind: function.lua + meta: + name: Relay Central Identity Test + type: test + suite: relay + comment: Tests user identity and scope propagation from websocket metadata. + group: Relay + tags: + - relay + - identity + - tests + source: file://central_test.lua + imports: + central: wippy.relay:central + test: wippy.test:test + modules: [] + method: run diff --git a/src/relay/src/central.lua b/src/relay/src/central.lua index 22bcc4e..71c3416 100644 --- a/src/relay/src/central.lua +++ b/src/relay/src/central.lua @@ -5,6 +5,7 @@ local consts = require("consts") local plugin_discovery = require("plugin_discovery") local logger = require("logger"):named("relay") +local security_mod = security type UserHubInfo = { hub_pid: string, @@ -39,19 +40,27 @@ local function table_length(t: any): number return count end +local function identity_from_metadata(config: ValidatedConfig, user_id: string, metadata: any): (any?, any?, any?, string?) + local user_metadata = type(metadata.user_metadata) == "table" and metadata.user_metadata or {} + local scope_id = type(metadata.scope_id) == "string" and metadata.scope_id ~= "" and metadata.scope_id or config.user_security_scope + local user_actor = security_mod.new_actor(user_id, user_metadata) + + local user_scope, scope_err = security_mod.named_scope(tostring(scope_id)) + if scope_err then + return nil, nil, nil, "Failed to get user security scope: " .. scope_err + end + + return user_actor, user_scope, user_metadata, nil +end + local function get_or_create_user_hub(state: CentralState, user_id: string, metadata: any): string? local hub = state.user_hubs[user_id] if hub then return hub.hub_pid end - local user_metadata = metadata.user_metadata or {} - local user_actor = security.new_actor(user_id, user_metadata) - - local user_scope, scope_err = security.named_scope(state.config.user_security_scope) - if scope_err then - error("Failed to get user security scope: " .. scope_err) - end + local user_actor, user_scope, user_metadata, identity_err = identity_from_metadata(state.config, user_id, metadata or {}) + if identity_err then error(identity_err) end local hub_pid, err = process.with_context({}) :with_scope(user_scope) @@ -284,4 +293,10 @@ local function run(): any return { status = "shutdown", hubs = state.total_hubs } end -return { run = run } +return { + run = run, + _identity_from_metadata = identity_from_metadata, + _set_security_for_test = function(fake: any) + security_mod = fake or security + end, +} diff --git a/src/relay/src/central_test.lua b/src/relay/src/central_test.lua new file mode 100644 index 0000000..63ca7ff --- /dev/null +++ b/src/relay/src/central_test.lua @@ -0,0 +1,72 @@ +local test = require("test") +local central = require("central") + +local function fake_security(calls: any): any + return { + new_actor = function(user_id: string, meta: any) + calls.actor_id = user_id + calls.actor_meta = meta + return { + id = function() return user_id end, + meta = function() return meta end, + } + end, + named_scope = function(scope_id: string) + calls.scope_id = scope_id + return { id = scope_id }, nil + end, + } +end + +local function config(scope_id: string) + return { + max_connections_per_user = 10, + user_hub_inactivity_timeout = "5m", + user_hub_host = "wippy.terminal:host", + user_security_scope = scope_id, + gc_check_interval = "1m", + heartbeat_interval = "30s", + message_queue_size = 100, + queue_multiplier = 2, + } +end + +local function define_tests() + test.describe("relay central identity", function() + test.it("uses the authenticated scope id when relay metadata carries it", function() + local calls = {} + central._set_security_for_test(fake_security(calls)) + local actor, scope, meta, err = central._identity_from_metadata(config("fallback:scope"), "admin@wippy.local", { + scope_id = "app.security:admin", + user_metadata = { + security_groups = { "app.security:admin", "app.security:user" }, + email = "admin@wippy.local", + }, + }) + + test.expect(err).to_be_nil() + test.expect(actor).not_to_be_nil() + test.expect(scope).not_to_be_nil() + test.expect(actor:id()).to_equal("admin@wippy.local") + test.expect(meta.security_groups[1]).to_equal("app.security:admin") + test.expect(calls.scope_id).to_equal("app.security:admin") + central._set_security_for_test(nil) + end) + + test.it("falls back to the configured user scope for legacy relay metadata", function() + local calls = {} + central._set_security_for_test(fake_security(calls)) + local actor, scope, meta, err = central._identity_from_metadata(config("app.security:user"), "member@wippy.local", {}) + + test.expect(err).to_be_nil() + test.expect(actor).not_to_be_nil() + test.expect(scope).not_to_be_nil() + test.expect(actor:id()).to_equal("member@wippy.local") + test.expect(type(meta)).to_equal("table") + test.expect(calls.scope_id).to_equal("app.security:user") + central._set_security_for_test(nil) + end) + end) +end + +return { run = test.run_cases(define_tests) }