summaryrefslogtreecommitdiff
path: root/src/protocol.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/protocol.c')
-rw-r--r--src/protocol.c204
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}");
+}