Add per-request space selection via X-Amduat-Space
This commit is contained in:
parent
5ecb28c84c
commit
db3c4b4c93
|
|
@ -84,6 +84,23 @@ target_link_libraries(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
|
||||
tests/test_amduatd_derivation_index.c
|
||||
src/amduatd_derivation_index.c
|
||||
|
|
|
|||
18
README.md
18
README.md
|
|
@ -166,6 +166,24 @@ Artifact info (length + type tag):
|
|||
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
|
||||
|
||||
- `GET /v1/meta` → `{store_id, encoding_profile_id, hash_id, api_contract_ref}`
|
||||
|
|
|
|||
|
|
@ -3881,6 +3881,10 @@ static bool amduatd_handle_conn(int fd,
|
|||
bool has_actor = false;
|
||||
bool has_uid = false;
|
||||
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)) {
|
||||
return false;
|
||||
|
|
@ -3898,6 +3902,30 @@ static bool amduatd_handle_conn(int fd,
|
|||
|
||||
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 &&
|
||||
strcmp(no_query, "/v1/cap/resolve") == 0) &&
|
||||
!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.store_cfg = cfg;
|
||||
ui_ctx.concepts = concepts;
|
||||
ui_ctx.daemon_cfg = dcfg;
|
||||
ui_ctx.daemon_cfg = effective_cfg;
|
||||
ui_ctx.root_path = root_path;
|
||||
ui_ctx.caps = caps;
|
||||
ui_resp.fd = fd;
|
||||
|
|
@ -3965,7 +3993,7 @@ static bool amduatd_handle_conn(int fd,
|
|||
caps_ctx.ui_ref = ui_ref;
|
||||
caps_ctx.store_cfg = cfg;
|
||||
caps_ctx.concepts = concepts;
|
||||
caps_ctx.daemon_cfg = dcfg;
|
||||
caps_ctx.daemon_cfg = effective_cfg;
|
||||
caps_ctx.root_path = root_path;
|
||||
caps_ctx.caps = caps;
|
||||
caps_resp.fd = fd;
|
||||
|
|
@ -3976,7 +4004,7 @@ static bool amduatd_handle_conn(int fd,
|
|||
goto conn_cleanup;
|
||||
}
|
||||
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);
|
||||
goto conn_cleanup;
|
||||
}
|
||||
|
|
@ -3987,7 +4015,8 @@ static bool amduatd_handle_conn(int fd,
|
|||
}
|
||||
if (strcmp(req.method, "POST") == 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);
|
||||
goto conn_cleanup;
|
||||
}
|
||||
|
|
@ -3999,7 +4028,7 @@ static bool amduatd_handle_conn(int fd,
|
|||
concepts_ctx.ui_ref = ui_ref;
|
||||
concepts_ctx.store_cfg = cfg;
|
||||
concepts_ctx.concepts = concepts;
|
||||
concepts_ctx.daemon_cfg = dcfg;
|
||||
concepts_ctx.daemon_cfg = effective_cfg;
|
||||
concepts_ctx.root_path = root_path;
|
||||
concepts_ctx.caps = caps;
|
||||
concepts_resp.fd = fd;
|
||||
|
|
@ -4025,7 +4054,7 @@ static bool amduatd_handle_conn(int fd,
|
|||
caps_ctx.ui_ref = ui_ref;
|
||||
caps_ctx.store_cfg = cfg;
|
||||
caps_ctx.concepts = concepts;
|
||||
caps_ctx.daemon_cfg = dcfg;
|
||||
caps_ctx.daemon_cfg = effective_cfg;
|
||||
caps_ctx.root_path = root_path;
|
||||
caps_ctx.caps = caps;
|
||||
caps_resp.fd = fd;
|
||||
|
|
|
|||
|
|
@ -1618,6 +1618,10 @@ static bool amduatd_handle_get_cap_resolve(
|
|||
token_hash);
|
||||
free(token_bytes);
|
||||
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",
|
||||
"invalid capability");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
memset(req, 0, sizeof(*req));
|
||||
req->effective_space = NULL;
|
||||
}
|
||||
|
||||
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++;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#define AMDUATD_HTTP_H
|
||||
|
||||
#include "amduat/asl/core.h"
|
||||
#include "amduatd_space.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
|
@ -19,11 +20,13 @@ typedef struct {
|
|||
char accept[128];
|
||||
char x_type_tag[64];
|
||||
char x_capability[2048];
|
||||
char x_space[AMDUAT_ASL_POINTER_NAME_MAX + 1u];
|
||||
size_t content_length;
|
||||
bool has_actor;
|
||||
amduat_octets_t actor;
|
||||
bool has_uid;
|
||||
uid_t uid;
|
||||
const amduatd_space_t *effective_space;
|
||||
} amduatd_http_req_t;
|
||||
|
||||
typedef struct {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
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
|
||||
} /* extern "C" */
|
||||
#endif
|
||||
|
|
|
|||
83
tests/test_amduatd_space_resolve.c
Normal file
83
tests/test_amduatd_space_resolve.c
Normal 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;
|
||||
}
|
||||
Loading…
Reference in a new issue