#include "protocol.h" #include #include #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}"); }