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_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
|
||||||
|
|
|
||||||
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'
|
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}`
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
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