diff options
| author | Anders Betts <anders.betts@gmail.com> | 2026-09-17 21:48:00 +0200 |
|---|---|---|
| committer | Anders Betts <anders.betts@gmail.com> | 2026-09-17 21:48:00 +0200 |
| commit | 071b7e9b01e41d4048b1bbb3d9caaac4c8a526de (patch) | |
| tree | b0af0a71e0d9fff2ca166d6ca69167ff32cc2ccb | |
| parent | a145684f0a556820a183db45d27e9b2dc715f056 (diff) | |
| download | bokf-0.1.3.tar.gz bokf-0.1.3.zip | |
Add token login to bokfctl and enforce admin scope for tokensv0.1.3
- bokfctl: --token / BOKFD_TOKEN via client_token_login
- tokens must carry the admin scope for admin commands (was bypassed)
- docs: token-based snapshot for restic backups
| -rw-r--r-- | clients/bokfctl.c | 23 | ||||
| -rw-r--r-- | clients/client.c | 49 | ||||
| -rw-r--r-- | clients/client.h | 4 | ||||
| -rw-r--r-- | docs/DEPLOY.md | 32 | ||||
| -rw-r--r-- | docs/PROTOCOL.md | 4 | ||||
| -rw-r--r-- | src/protocol.c | 3 | ||||
| -rw-r--r-- | tests/test_core.c | 30 |
7 files changed, 129 insertions, 16 deletions
diff --git a/clients/bokfctl.c b/clients/bokfctl.c index 1f1372c..b5ecf47 100644 --- a/clients/bokfctl.c +++ b/clients/bokfctl.c @@ -17,6 +17,7 @@ static void usage(void) " (env BOKFD_SOCKET, default /run/bokfd/bokfd.sock)\n" " --user NAME login user (env BOKFD_USER)\n" " --password PW login password (env BOKFD_PASSWORD)\n" + " --token TOKEN API token instead of user/password (env BOKFD_TOKEN)\n" " --org ID active org for this request\n" " --version\n" "\n" @@ -39,6 +40,7 @@ int main(int argc, char **argv) target = "/run/bokfd/bokfd.sock"; const char *user = getenv("BOKFD_USER"); const char *password = getenv("BOKFD_PASSWORD"); + const char *token = getenv("BOKFD_TOKEN"); int64_t org = 0; const char *cmd = NULL; const char *args_json = NULL; @@ -70,6 +72,14 @@ int main(int argc, char **argv) return 2; } password = val; + } else if (!strncmp(a, "--token", 7) && + (a[7] == '\0' || a[7] == '=')) { + val = a[7] == '=' ? a + 8 : (i + 1 < argc ? argv[++i] : NULL); + if (!val) { + fprintf(stderr, "bokfctl: --token requires a value\n"); + return 2; + } + token = val; } else if (!strncmp(a, "--org", 5) && (a[5] == '\0' || a[5] == '=')) { val = a[5] == '=' ? a + 6 : (i + 1 < argc ? argv[++i] : NULL); @@ -111,14 +121,19 @@ int main(int argc, char **argv) char *session = NULL; if (!is_local_cmd(cmd)) { - if (!user || !password) { + char *lerr = NULL; + int login_rc; + if (token && *token) { + login_rc = client_token_login(&conn, token, &session, &lerr); + } else if (user && password) { + login_rc = client_login(&conn, user, password, &session, &lerr); + } else { fprintf(stderr, - "bokfctl: set BOKFD_USER and BOKFD_PASSWORD (or --user/--password) to log in\n"); + "bokfctl: set BOKFD_TOKEN or BOKFD_USER/BOKFD_PASSWORD (or --token/--user/--password) to log in\n"); client_close(&conn); return 2; } - char *lerr = NULL; - if (client_login(&conn, user, password, &session, &lerr) != 0) { + if (login_rc != 0) { fprintf(stderr, "%s\n", lerr ? lerr : "login failed"); int rc = lerr && lerr[0] == '{' ? 1 : 2; free(lerr); diff --git a/clients/client.c b/clients/client.c index 8fb604b..fd119f1 100644 --- a/clients/client.c +++ b/clients/client.c @@ -361,18 +361,12 @@ char *client_rpc(struct client_conn *c, const char *cmd, const char *session, return client_read_line(c); } -int client_login(struct client_conn *c, const char *user, const char *password, - char **session_out, char **err_out) +static int session_open(struct client_conn *c, const char *args, + char **session_out, char **err_out) { *session_out = NULL; *err_out = NULL; - char *args = client_make_login_args(user, password); - if (!args) { - *err_out = xstrdup("could not build login request"); - return -1; - } char *resp = client_rpc(c, "session.open", NULL, 0, args); - free(args); if (!resp) { *err_out = xstrdup(client_last_error()); return -1; @@ -396,6 +390,45 @@ int client_login(struct client_conn *c, const char *user, const char *password, return 0; } +int client_login(struct client_conn *c, const char *user, const char *password, + char **session_out, char **err_out) +{ + char *args = client_make_login_args(user, password); + if (!args) { + *session_out = NULL; + *err_out = xstrdup("could not build login request"); + return -1; + } + int rc = session_open(c, args, session_out, err_out); + free(args); + return rc; +} + +int client_token_login(struct client_conn *c, const char *token, + char **session_out, char **err_out) +{ + yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); + if (!d) { + *session_out = NULL; + *err_out = xstrdup("out of memory"); + return -1; + } + yyjson_mut_val *o = yyjson_mut_obj(d); + yyjson_mut_doc_set_root(d, o); + yyjson_mut_obj_add_strcpy(d, o, "method", "token"); + yyjson_mut_obj_add_strcpy(d, o, "token", token); + char *args = yyjson_mut_write(d, 0, NULL); + yyjson_mut_doc_free(d); + if (!args) { + *session_out = NULL; + *err_out = xstrdup("could not build token request"); + return -1; + } + int rc = session_open(c, args, session_out, err_out); + free(args); + return rc; +} + int client_ok(const char *response) { if (!response) diff --git a/clients/client.h b/clients/client.h index 9372e72..40a8669 100644 --- a/clients/client.h +++ b/clients/client.h @@ -36,6 +36,10 @@ char *client_rpc(struct client_conn *c, const char *cmd, const char *session, int client_login(struct client_conn *c, const char *user, const char *password, char **session_out, char **err_out); +/* Token login (same contract as client_login). */ +int client_token_login(struct client_conn *c, const char *token, + char **session_out, char **err_out); + /* Convenience: true when the response line has "ok":true. */ int client_ok(const char *response); /* Extract result.session into buf; returns 0 on success. */ diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 8d36453..eb3ec1c 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -170,12 +170,42 @@ Develop against this org; agents get their own API token ## Backup and restore +`backup.snapshot` makes a consistent copy of the live WAL database with +`VACUUM INTO`; the result lands in `var/db/backup/` as +`bokfd-<timestamp>.db` (the response carries its SHA-256). Interactive: + ```sh docker compose exec -e BOKFD_PASSWORD='<pw>' bokfd \ bokfctl --user admin backup.snapshot -ls var/db/backup # <db>-<timestamp>.db + .sha256 +ls var/db/backup +``` + +For unattended backups (restic, cron) use an admin-scoped token instead of +the password, created once: + +```sh +docker compose exec -e BOKFD_PASSWORD='<pw>' bokfd \ + bokfctl --user admin token.create '{"label":"backup","scopes":["admin"]}' +# store the token (shown once) in a root-only file, e.g. /etc/bokf/bokfd-backup.env: +# BOKFD_TOKEN=bokf_... ``` +Then, before every restic run: take a snapshot through the daemon and let +restic read only the snapshots — never the live database: + +```sh +. /etc/bokf/bokfd-backup.env +docker compose --project-directory /mnt/data/bokf exec -T -e BOKFD_TOKEN \ + bokfd bokfctl backup.snapshot >/dev/null +find /mnt/data/bokf/var/db/backup -name 'bokfd-*.db' -mtime +14 -delete +restic backup --exclude='/mnt/data/bokf/var/db/bokfd.db*' \ + --exclude='/mnt/data/bokf/var/run' /mnt/data/bokf +``` + +The WAL and shared-memory files (`bokfd.db-wal`, `bokfd.db-shm`) must be +excluded together with the main file; an inconsistent copy of a live SQLite +database is worse than no backup. + Restore: ```sh diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md index e9c4331..25c9250 100644 --- a/docs/PROTOCOL.md +++ b/docs/PROTOCOL.md @@ -95,7 +95,9 @@ and returns an opaque, high-entropy session id: - A token is bound to one user and one org, has scopes (`read`, `write`, `admin`) and is an independent audit actor (label shown in history). - Tokens are the intended mechanism for agents and for accountant/viewer - access. They can be revoked immediately (`token.revoke`). + access. They can be revoked immediately (`token.revoke`). Scopes are + enforced for every command, including admin commands: `backup.snapshot` + and `user.*` need a token with the `admin` scope. ### 4.3 Roles and permissions diff --git a/src/protocol.c b/src/protocol.c index 7c00d6b..c528759 100644 --- a/src/protocol.c +++ b/src/protocol.c @@ -168,8 +168,7 @@ char *protocol_handle_line(sqlite3 *db, const char *line, size_t len) add_error(rdoc, resp, "FORBIDDEN", "owner role required", NULL); goto done; } - if (cmd->perm != PERM_ADMIN && scope && - !scope_has(r.sess->scopes, scope)) { + if (scope && !scope_has(r.sess->scopes, scope)) { add_error(rdoc, resp, "FORBIDDEN", "token scope does not allow this command", NULL); goto done; diff --git a/tests/test_core.c b/tests/test_core.c index b2c828d..5960665 100644 --- a/tests/test_core.c +++ b/tests/test_core.c @@ -540,6 +540,12 @@ int main(void) CHECK(db_exec(g_db, "DELETE FROM audit_log", &err) != 0); free(err); + d = call(reqf("{\"v\":1,\"id\":\"27\",\"cmd\":\"backup.snapshot\"," + "\"session\":\"%s\"}", + ro_session)); + CHECK_STR(d, "error.code", "FORBIDDEN"); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"28\",\"cmd\":\"backup.snapshot\"," "\"session\":\"%s\"}", g_session)); @@ -548,6 +554,30 @@ int main(void) CHECK(jstr(d, "result.sha256") && strlen(jstr(d, "result.sha256")) == 64); yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"28b\",\"cmd\":\"token.create\"," + "\"session\":\"%s\",\"org\":%d,\"args\":" + "{\"label\":\"backup\",\"scopes\":[\"admin\"]}}", + g_session, (int)org_id)); + CHECK_OK(d); + char backup_token[128] = ""; + snprintf(backup_token, sizeof backup_token, "%s", jstr(d, "result.token")); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"28c\",\"cmd\":\"session.open\",\"args\":" + "{\"method\":\"token\",\"token\":\"%s\"}}", + backup_token)); + CHECK_OK(d); + char backup_session[128] = ""; + snprintf(backup_session, sizeof backup_session, "%s", + jstr(d, "result.session")); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"28d\",\"cmd\":\"backup.snapshot\"," + "\"session\":\"%s\",\"args\":{\"dest\":\"%s/snap-token.db\"}}", + backup_session, tmpdir)); + CHECK_OK(d); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"29\",\"cmd\":\"describe\"," "\"session\":\"%s\"}", g_session)); |
