diff options
| author | Anders Betts <anders.betts@gmail.com> | 2026-09-17 19:55:36 +0200 |
|---|---|---|
| committer | Anders Betts <anders.betts@gmail.com> | 2026-09-17 19:55:36 +0200 |
| commit | 380195f7cd5e57acf2c1cf2bc41069e6b0b979ed (patch) | |
| tree | 32a88fb22a7fbe8f1fd5c105156d1f928c93950d /src/protocol.c | |
| download | bokf-380195f7cd5e57acf2c1cf2bc41069e6b0b979ed.tar.gz bokf-380195f7cd5e57acf2c1cf2bc41069e6b0b979ed.zip | |
Initial commit: daemon, clients, docs, Docker deploy pipelinev0.1.0
Diffstat (limited to 'src/protocol.c')
| -rw-r--r-- | src/protocol.c | 204 |
1 files changed, 204 insertions, 0 deletions
diff --git a/src/protocol.c b/src/protocol.c new file mode 100644 index 0000000..7c00d6b --- /dev/null +++ b/src/protocol.c @@ -0,0 +1,204 @@ +#include "protocol.h" + +#include <stdlib.h> +#include <string.h> + +#include "commands.h" +#include "db.h" +#include "log.h" +#include "util.h" +#include "version.h" + +static void add_error(yyjson_mut_doc *doc, yyjson_mut_val *resp, + const char *code, const char *msg, + yyjson_mut_val *details) +{ + yyjson_mut_obj_add_bool(doc, resp, "ok", false); + yyjson_mut_val *e = yyjson_mut_obj(doc); + yyjson_mut_obj_add_strcpy(doc, e, "code", code); + yyjson_mut_obj_add_strcpy(doc, e, "message", msg); + if (details) + yyjson_mut_obj_add_val(doc, e, "details", details); + yyjson_mut_obj_add_val(doc, resp, "error", e); +} + +static int scope_has(const char *list, const char *scope) +{ + if (!list || !scope) + return 0; + size_t n = strlen(scope); + const char *p = list; + while (*p) { + const char *comma = strchr(p, ','); + size_t len = comma ? (size_t)(comma - p) : strlen(p); + if (len == n && strncmp(p, scope, n) == 0) + return 1; + if (!comma) + break; + p = comma + 1; + } + return 0; +} + +static int role_rank(const char *role) +{ + if (!role) + return 0; + if (strcmp(role, "viewer") == 0) + return 1; + if (strcmp(role, "bookkeeper") == 0) + return 2; + if (strcmp(role, "owner") == 0) + return 3; + return 0; +} + +char *protocol_handle_line(sqlite3 *db, const char *line, size_t len) +{ + yyjson_doc *doc = yyjson_read(line, len, 0); + yyjson_mut_doc *rdoc = yyjson_mut_doc_new(NULL); + yyjson_mut_val *resp = yyjson_mut_obj(rdoc); + yyjson_mut_doc_set_root(rdoc, resp); + + const char *id = ""; + struct req r; + memset(&r, 0, sizeof r); + char *role = NULL; + + if (!doc || !yyjson_is_obj(yyjson_doc_get_root(doc))) { + yyjson_mut_obj_add_strcpy(rdoc, resp, "id", id); + add_error(rdoc, resp, "PARSE_ERROR", "request must be a JSON object", + NULL); + goto done; + } + + yyjson_val *root = yyjson_doc_get_root(doc); + yyjson_val *idv = yyjson_obj_get(root, "id"); + if (idv && yyjson_is_str(idv)) + id = yyjson_get_str(idv); + yyjson_mut_obj_add_strcpy(rdoc, resp, "id", id); + + yyjson_val *vv = yyjson_obj_get(root, "v"); + int v = vv && yyjson_is_int(vv) ? yyjson_get_int(vv) : -1; + if (v != BOKF_PROTOCOL_VERSION) { + add_error(rdoc, resp, "UNSUPPORTED_VERSION", + "unsupported protocol version; this server speaks v1", NULL); + goto done; + } + + yyjson_val *cmdv = yyjson_obj_get(root, "cmd"); + const char *cmdname = + cmdv && yyjson_is_str(cmdv) ? yyjson_get_str(cmdv) : NULL; + if (!cmdname) { + add_error(rdoc, resp, "INVALID_ARGS", "cmd is required", NULL); + goto done; + } + const struct command *cmd = command_find(cmdname); + if (!cmd) { + add_error(rdoc, resp, "UNKNOWN_COMMAND", "unknown command", NULL); + goto done; + } + + r.db = db; + r.rdoc = rdoc; + r.id = id; + r.cmd = cmdname; + r.args = yyjson_obj_get(root, "args"); + yyjson_val *drv = yyjson_obj_get(root, "dry_run"); + r.dry_run = drv && yyjson_is_bool(drv) && yyjson_get_bool(drv); + + if (cmd->perm != PERM_PUBLIC) { + yyjson_val *sv = yyjson_obj_get(root, "session"); + const char *sid = sv && yyjson_is_str(sv) ? yyjson_get_str(sv) : NULL; + r.sess = sessions_get(sid); + if (!r.sess) { + add_error(rdoc, resp, sid ? "SESSION_EXPIRED" : "AUTH_REQUIRED", + sid ? "session is invalid or expired" + : "session is required", + NULL); + goto done; + } + r.is_admin = r.sess->is_admin; + + int64_t org = r.sess->active_org; + yyjson_val *ov = yyjson_obj_get(root, "org"); + if (ov && yyjson_is_int(ov)) + org = yyjson_get_int(ov); + + if (cmd->need_org) { + if (r.sess->bound_org) { + if (org != r.sess->bound_org) { + add_error(rdoc, resp, "ORG_FORBIDDEN", + "token is bound to another org", NULL); + goto done; + } + org = r.sess->bound_org; + } + if (!org) { + add_error(rdoc, resp, "ORG_REQUIRED", + "no active org; pass org or use session.use_org", + NULL); + goto done; + } + role = db_membership_role(db, org, r.sess->user_id); + if (!role) { + add_error(rdoc, resp, "ORG_FORBIDDEN", + "not a member of this org", NULL); + goto done; + } + r.org_id = org; + r.has_org = 1; + r.role = role; + } else { + r.org_id = org; + } + + int rank = role_rank(r.role); + const char *scope = perm_scope(cmd->perm); + if (cmd->perm == PERM_ADMIN && !r.is_admin) { + add_error(rdoc, resp, "FORBIDDEN", "system admin required", NULL); + goto done; + } + if (cmd->perm == PERM_WRITE && rank < 2) { + add_error(rdoc, resp, "FORBIDDEN", "bookkeeper role required", + NULL); + goto done; + } + if (cmd->perm == PERM_OWNER && rank < 3) { + add_error(rdoc, resp, "FORBIDDEN", "owner role required", NULL); + goto done; + } + if (cmd->perm != PERM_ADMIN && scope && + !scope_has(r.sess->scopes, scope)) { + add_error(rdoc, resp, "FORBIDDEN", + "token scope does not allow this command", NULL); + goto done; + } + } + + { + yyjson_mut_val *result = cmd->fn(&r); + if (!result) { + add_error(rdoc, resp, r.err_code ? r.err_code : "INTERNAL", + r.err_msg[0] ? r.err_msg : "internal error", + r.err_details); + } else { + yyjson_mut_obj_add_bool(rdoc, resp, "ok", true); + yyjson_mut_obj_add_val(rdoc, resp, "result", result); + } + } + +done: + free(role); + if (doc) + yyjson_doc_free(doc); + yyjson_write_err werr; + werr.code = 0; + werr.msg = NULL; + char *out = yyjson_mut_write_opts(rdoc, 0, NULL, NULL, &werr); + if (!out) + log_error("response write failed: code=%d %s", (int)werr.code, + werr.msg ? werr.msg : ""); + yyjson_mut_doc_free(rdoc); + return out ? out : xstrdup("{\"id\":\"\",\"ok\":false}"); +} |
