Add per-request space selection via X-Amduat-Space

This commit is contained in:
Carl Niklas Rydberg 2026-01-24 10:16:22 +01:00
parent 5ecb28c84c
commit db3c4b4c93
9 changed files with 218 additions and 6 deletions

View file

@ -84,6 +84,23 @@ target_link_libraries(amduatd_test_store_backend
add_test(NAME amduatd_store_backend COMMAND amduatd_test_store_backend) add_test(NAME amduatd_store_backend COMMAND amduatd_test_store_backend)
add_executable(amduatd_test_space_resolve
tests/test_amduatd_space_resolve.c
src/amduatd_space.c
)
target_include_directories(amduatd_test_space_resolve
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/amduat/include
)
target_link_libraries(amduatd_test_space_resolve
PRIVATE amduat_asl_pointer_fs
)
add_test(NAME amduatd_space_resolve COMMAND amduatd_test_space_resolve)
add_executable(amduatd_test_derivation_index add_executable(amduatd_test_derivation_index
tests/test_amduatd_derivation_index.c tests/test_amduatd_derivation_index.c
src/amduatd_derivation_index.c src/amduatd_derivation_index.c

View file

@ -166,6 +166,24 @@ Artifact info (length + type tag):
curl --unix-socket amduatd.sock 'http://localhost/v1/artifacts/<ref>?format=info' curl --unix-socket amduatd.sock 'http://localhost/v1/artifacts/<ref>?format=info'
``` ```
## Space selection
Requests can select a space via the `X-Amduat-Space` header:
```sh
curl --unix-socket amduatd.sock http://localhost/v1/concepts \
-H 'X-Amduat-Space: demo'
```
Precedence rules:
- `X-Amduat-Space` header (if present)
- daemon `--space` default (if configured)
- unscoped names (no space)
When capability tokens are used, the requested space must match the token's
space (or the token must be unscoped), otherwise the request is rejected.
## Current endpoints ## Current endpoints
- `GET /v1/meta``{store_id, encoding_profile_id, hash_id, api_contract_ref}` - `GET /v1/meta``{store_id, encoding_profile_id, hash_id, api_contract_ref}`

View file

@ -3881,6 +3881,10 @@ static bool amduatd_handle_conn(int fd,
bool has_actor = false; bool has_actor = false;
bool has_uid = false; bool has_uid = false;
uid_t uid = 0; uid_t uid = 0;
amduatd_space_t req_space;
const amduatd_space_t *effective_space = NULL;
amduatd_cfg_t req_cfg;
const amduatd_cfg_t *effective_cfg = dcfg;
if (!amduatd_http_parse_request(fd, &req)) { if (!amduatd_http_parse_request(fd, &req)) {
return false; return false;
@ -3898,6 +3902,30 @@ static bool amduatd_handle_conn(int fd,
amduatd_path_without_query(req.path, no_query, sizeof(no_query)); amduatd_path_without_query(req.path, no_query, sizeof(no_query));
{
const char *space_header = NULL;
amduatd_space_resolve_status_t space_st;
if (req.x_space[0] != '\0') {
space_header = req.x_space;
}
space_st = amduatd_space_resolve_effective(&dcfg->space,
space_header,
&req_space,
&effective_space);
if (space_st != AMDUATD_SPACE_RESOLVE_OK || effective_space == NULL) {
ok = amduatd_send_json_error(fd, 400, "Bad Request",
"invalid X-Amduat-Space");
goto conn_cleanup;
}
if (effective_space != &dcfg->space) {
req_cfg = *dcfg;
req_cfg.space = req_space;
effective_cfg = &req_cfg;
}
req.effective_space = effective_space;
}
if (!(strcmp(req.method, "GET") == 0 && if (!(strcmp(req.method, "GET") == 0 &&
strcmp(no_query, "/v1/cap/resolve") == 0) && strcmp(no_query, "/v1/cap/resolve") == 0) &&
!amduatd_actor_allowed(allowlist, has_uid, uid)) { !amduatd_actor_allowed(allowlist, has_uid, uid)) {
@ -3916,7 +3944,7 @@ static bool amduatd_handle_conn(int fd,
ui_ctx.ui_ref = ui_ref; ui_ctx.ui_ref = ui_ref;
ui_ctx.store_cfg = cfg; ui_ctx.store_cfg = cfg;
ui_ctx.concepts = concepts; ui_ctx.concepts = concepts;
ui_ctx.daemon_cfg = dcfg; ui_ctx.daemon_cfg = effective_cfg;
ui_ctx.root_path = root_path; ui_ctx.root_path = root_path;
ui_ctx.caps = caps; ui_ctx.caps = caps;
ui_resp.fd = fd; ui_resp.fd = fd;
@ -3965,7 +3993,7 @@ static bool amduatd_handle_conn(int fd,
caps_ctx.ui_ref = ui_ref; caps_ctx.ui_ref = ui_ref;
caps_ctx.store_cfg = cfg; caps_ctx.store_cfg = cfg;
caps_ctx.concepts = concepts; caps_ctx.concepts = concepts;
caps_ctx.daemon_cfg = dcfg; caps_ctx.daemon_cfg = effective_cfg;
caps_ctx.root_path = root_path; caps_ctx.root_path = root_path;
caps_ctx.caps = caps; caps_ctx.caps = caps;
caps_resp.fd = fd; caps_resp.fd = fd;
@ -3976,7 +4004,7 @@ static bool amduatd_handle_conn(int fd,
goto conn_cleanup; goto conn_cleanup;
} }
if (strcmp(req.method, "POST") == 0 && strcmp(no_query, "/v1/pel/run") == 0) { if (strcmp(req.method, "POST") == 0 && strcmp(no_query, "/v1/pel/run") == 0) {
ok = amduatd_handle_post_pel_run(fd, store, cfg, concepts, dcfg, ok = amduatd_handle_post_pel_run(fd, store, cfg, concepts, effective_cfg,
root_path, &req); root_path, &req);
goto conn_cleanup; goto conn_cleanup;
} }
@ -3987,7 +4015,8 @@ static bool amduatd_handle_conn(int fd,
} }
if (strcmp(req.method, "POST") == 0 && if (strcmp(req.method, "POST") == 0 &&
strcmp(no_query, "/v1/context_frames") == 0) { strcmp(no_query, "/v1/context_frames") == 0) {
ok = amduatd_handle_post_context_frames(fd, store, cfg, concepts, dcfg, ok = amduatd_handle_post_context_frames(fd, store, cfg, concepts,
effective_cfg,
&req); &req);
goto conn_cleanup; goto conn_cleanup;
} }
@ -3999,7 +4028,7 @@ static bool amduatd_handle_conn(int fd,
concepts_ctx.ui_ref = ui_ref; concepts_ctx.ui_ref = ui_ref;
concepts_ctx.store_cfg = cfg; concepts_ctx.store_cfg = cfg;
concepts_ctx.concepts = concepts; concepts_ctx.concepts = concepts;
concepts_ctx.daemon_cfg = dcfg; concepts_ctx.daemon_cfg = effective_cfg;
concepts_ctx.root_path = root_path; concepts_ctx.root_path = root_path;
concepts_ctx.caps = caps; concepts_ctx.caps = caps;
concepts_resp.fd = fd; concepts_resp.fd = fd;
@ -4025,7 +4054,7 @@ static bool amduatd_handle_conn(int fd,
caps_ctx.ui_ref = ui_ref; caps_ctx.ui_ref = ui_ref;
caps_ctx.store_cfg = cfg; caps_ctx.store_cfg = cfg;
caps_ctx.concepts = concepts; caps_ctx.concepts = concepts;
caps_ctx.daemon_cfg = dcfg; caps_ctx.daemon_cfg = effective_cfg;
caps_ctx.root_path = root_path; caps_ctx.root_path = root_path;
caps_ctx.caps = caps; caps_ctx.caps = caps;
caps_resp.fd = fd; caps_resp.fd = fd;

View file

@ -1618,6 +1618,10 @@ static bool amduatd_handle_get_cap_resolve(
token_hash); token_hash);
free(token_bytes); free(token_bytes);
amduatd_cap_token_free(&token); amduatd_cap_token_free(&token);
if (strcmp(reason, "wrong-space") == 0) {
return amduatd_send_json_error(fd, 403, "Forbidden",
"space not permitted by capability");
}
return amduatd_send_json_error(fd, 403, "Forbidden", return amduatd_send_json_error(fd, 403, "Forbidden",
"invalid capability"); "invalid capability");
} }

View file

@ -178,6 +178,7 @@ static bool amduatd_read_line(int fd, char *buf, size_t cap, size_t *out_len) {
static void amduatd_http_req_init(amduatd_http_req_t *req) { static void amduatd_http_req_init(amduatd_http_req_t *req) {
memset(req, 0, sizeof(*req)); memset(req, 0, sizeof(*req));
req->effective_space = NULL;
} }
bool amduatd_http_parse_request(int fd, amduatd_http_req_t *out_req) { bool amduatd_http_parse_request(int fd, amduatd_http_req_t *out_req) {
@ -254,6 +255,12 @@ bool amduatd_http_parse_request(int fd, amduatd_http_req_t *out_req) {
v++; v++;
} }
strncpy(out_req->x_capability, v, sizeof(out_req->x_capability) - 1); strncpy(out_req->x_capability, v, sizeof(out_req->x_capability) - 1);
} else if (strncasecmp(line, "X-Amduat-Space:", 15) == 0) {
const char *v = line + 15;
while (*v == ' ' || *v == '\t') {
v++;
}
strncpy(out_req->x_space, v, sizeof(out_req->x_space) - 1);
} }
} }

View file

@ -2,6 +2,7 @@
#define AMDUATD_HTTP_H #define AMDUATD_HTTP_H
#include "amduat/asl/core.h" #include "amduat/asl/core.h"
#include "amduatd_space.h"
#include <stdbool.h> #include <stdbool.h>
#include <stddef.h> #include <stddef.h>
@ -19,11 +20,13 @@ typedef struct {
char accept[128]; char accept[128];
char x_type_tag[64]; char x_type_tag[64];
char x_capability[2048]; char x_capability[2048];
char x_space[AMDUAT_ASL_POINTER_NAME_MAX + 1u];
size_t content_length; size_t content_length;
bool has_actor; bool has_actor;
amduat_octets_t actor; amduat_octets_t actor;
bool has_uid; bool has_uid;
uid_t uid; uid_t uid;
const amduatd_space_t *effective_space;
} amduatd_http_req_t; } amduatd_http_req_t;
typedef struct { typedef struct {

View file

@ -272,3 +272,43 @@ bool amduatd_space_edges_index_head_name(const amduatd_space_t *sp,
bool amduatd_space_should_migrate_unscoped_edges(const amduatd_space_t *sp) { bool amduatd_space_should_migrate_unscoped_edges(const amduatd_space_t *sp) {
return sp != NULL && sp->enabled && sp->migrate_unscoped_edges; return sp != NULL && sp->enabled && sp->migrate_unscoped_edges;
} }
amduatd_space_resolve_status_t amduatd_space_resolve_effective(
const amduatd_space_t *default_space,
const char *request_space_id,
amduatd_space_t *out_space,
const amduatd_space_t **out_effective) {
bool migrate = false;
if (out_space != NULL) {
memset(out_space, 0, sizeof(*out_space));
}
if (out_effective != NULL) {
*out_effective = NULL;
}
if (default_space != NULL) {
migrate = default_space->migrate_unscoped_edges;
}
if (request_space_id != NULL) {
if (request_space_id[0] == '\0') {
return AMDUATD_SPACE_RESOLVE_ERR_INVALID;
}
if (out_space == NULL) {
return AMDUATD_SPACE_RESOLVE_ERR_INVALID;
}
if (!amduatd_space_init(out_space, request_space_id, migrate)) {
return AMDUATD_SPACE_RESOLVE_ERR_INVALID;
}
if (out_effective != NULL) {
*out_effective = out_space;
}
return AMDUATD_SPACE_RESOLVE_OK;
}
if (out_effective != NULL) {
*out_effective = default_space;
}
return AMDUATD_SPACE_RESOLVE_OK;
}

View file

@ -48,6 +48,17 @@ bool amduatd_space_edges_index_head_name(const amduatd_space_t *sp,
bool amduatd_space_should_migrate_unscoped_edges(const amduatd_space_t *sp); bool amduatd_space_should_migrate_unscoped_edges(const amduatd_space_t *sp);
typedef enum {
AMDUATD_SPACE_RESOLVE_OK = 0,
AMDUATD_SPACE_RESOLVE_ERR_INVALID = 1
} amduatd_space_resolve_status_t;
amduatd_space_resolve_status_t amduatd_space_resolve_effective(
const amduatd_space_t *default_space,
const char *request_space_id,
amduatd_space_t *out_space,
const amduatd_space_t **out_effective);
#ifdef __cplusplus #ifdef __cplusplus
} /* extern "C" */ } /* extern "C" */
#endif #endif

View file

@ -0,0 +1,83 @@
#include "amduatd_space.h"
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
static int failures = 0;
static void expect(bool cond, const char *msg) {
if (!cond) {
fprintf(stderr, "FAIL: %s\n", msg);
failures++;
}
}
static void expect_space_id(const amduatd_space_t *sp,
const char *expected,
const char *msg) {
if (expected == NULL) {
expect(sp == NULL || sp->space_id.data == NULL, msg);
return;
}
expect(sp != NULL && sp->space_id.data != NULL &&
strcmp((const char *)sp->space_id.data, expected) == 0,
msg);
}
int main(void) {
amduatd_space_t default_space;
amduatd_space_t resolved_space;
const amduatd_space_t *effective = NULL;
amduatd_space_resolve_status_t st;
if (!amduatd_space_init(&default_space, "alpha", false)) {
fprintf(stderr, "FAIL: default space init\n");
return 1;
}
st = amduatd_space_resolve_effective(&default_space,
NULL,
&resolved_space,
&effective);
expect(st == AMDUATD_SPACE_RESOLVE_OK, "resolve default without header");
expect(effective == &default_space, "uses default space");
expect(effective != NULL && effective->enabled, "default enabled");
expect_space_id(effective, "alpha", "default id");
st = amduatd_space_resolve_effective(&default_space,
"beta",
&resolved_space,
&effective);
expect(st == AMDUATD_SPACE_RESOLVE_OK, "resolve header override");
expect(effective == &resolved_space, "header overrides default");
expect(effective != NULL && effective->enabled, "header enabled");
expect_space_id(effective, "beta", "header id");
st = amduatd_space_resolve_effective(&default_space,
"",
&resolved_space,
&effective);
expect(st == AMDUATD_SPACE_RESOLVE_ERR_INVALID, "reject empty header");
st = amduatd_space_resolve_effective(&default_space,
"bad/space",
&resolved_space,
&effective);
expect(st == AMDUATD_SPACE_RESOLVE_ERR_INVALID, "reject invalid header");
if (!amduatd_space_init(&default_space, NULL, false)) {
fprintf(stderr, "FAIL: unscoped default init\n");
return 1;
}
st = amduatd_space_resolve_effective(&default_space,
NULL,
&resolved_space,
&effective);
expect(st == AMDUATD_SPACE_RESOLVE_OK, "resolve unscoped default");
expect(effective == &default_space, "unscoped uses default");
expect(effective != NULL && !effective->enabled, "unscoped disabled");
expect_space_id(effective, NULL, "unscoped id");
return failures == 0 ? 0 : 1;
}