diff options
Diffstat (limited to 'src/cmd_settings.c')
| -rw-r--r-- | src/cmd_settings.c | 234 |
1 files changed, 234 insertions, 0 deletions
diff --git a/src/cmd_settings.c b/src/cmd_settings.c new file mode 100644 index 0000000..8ae60ed --- /dev/null +++ b/src/cmd_settings.c @@ -0,0 +1,234 @@ +#include "commands.h" +#include "cmd_util.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "audit.h" +#include "config.h" +#include "db.h" +#include "secret.h" +#include "util.h" + +/* ------------------------------------------------------------------ */ +/* settings */ +/* ------------------------------------------------------------------ */ + +static yyjson_mut_val *h_settings_get(struct req *r) +{ + yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); + int have_default = 0, have_bank = 0, have_receivable = 0, have_revenue = 0; + int have_password = 0, have_security = 0; + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2( + r->db, "SELECT key,value FROM settings WHERE org_id=?1", -1, &st, + NULL) == SQLITE_OK) { + sqlite3_bind_int64(st, 1, r->org_id); + while (sqlite3_step(st) == SQLITE_ROW) { + const char *k = (const char *)sqlite3_column_text(st, 0); + const char *v = (const char *)sqlite3_column_text(st, 1); + if (k) { + if (secret_key_is_secret(k)) { + have_password = 1; + continue; + } + yyjson_mut_obj_add(o, yyjson_mut_strcpy(r->rdoc, k), + yyjson_mut_strcpy(r->rdoc, v ? v : "")); + if (strcmp(k, "default_series") == 0) + have_default = 1; + if (strcmp(k, "bank_account") == 0) + have_bank = 1; + if (strcmp(k, "invoice_receivable_account") == 0) + have_receivable = 1; + if (strcmp(k, "invoice_revenue_account") == 0) + have_revenue = 1; + if (strcmp(k, "smtp_security") == 0) + have_security = 1; + } + } + sqlite3_finalize(st); + } + if (!have_default) + yyjson_mut_obj_add_strcpy(r->rdoc, o, "default_series", "A"); + if (!have_bank) + yyjson_mut_obj_add_strcpy(r->rdoc, o, "bank_account", "1930"); + if (!have_receivable) + yyjson_mut_obj_add_strcpy(r->rdoc, o, "invoice_receivable_account", + "1510"); + if (!have_revenue) + yyjson_mut_obj_add_strcpy(r->rdoc, o, "invoice_revenue_account", + "3001"); + if (!have_security) + yyjson_mut_obj_add_strcpy(r->rdoc, o, "smtp_security", "starttls"); + yyjson_mut_obj_add_bool(r->rdoc, o, "smtp_password_set", have_password); + return o; +} + +static yyjson_mut_val *settings_result(struct req *r, const char *key, + const char *value, int dry) +{ + yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "key", key); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "value", value); + if (dry) + yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true); + return o; +} + +static char *settings_secret_audit_json(const char *key) +{ + yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL); + yyjson_mut_val *o = yyjson_mut_obj(doc); + yyjson_mut_doc_set_root(doc, o); + yyjson_mut_obj_add_strcpy(doc, o, "key", key); + yyjson_mut_obj_add_strcpy(doc, o, "value", "[redacted]"); + char *json = yyjson_mut_write(doc, 0, NULL); + yyjson_mut_doc_free(doc); + return json ? json : xstrdup("{}"); +} + +static yyjson_mut_val *h_settings_set(struct req *r) +{ + const char *key = arg_str(r->args, "key"); + const char *value = arg_str(r->args, "value"); + if (!key || !value) + return fail(r, "INVALID_ARGS", "key and value are required"); + if (secret_key_is_secret(key)) { + if (!secret_available()) + return fail(r, "INTERNAL", + "BOKFD_SECRET_KEY is missing or invalid"); + char *stored = NULL; + if (*value && secret_encrypt(value, &stored) != 0) + return fail(r, "INTERNAL", + "BOKFD_SECRET_KEY is missing or invalid"); + if (r->dry_run) { + free(stored); + return settings_result(r, key, "[redacted]", 1); + } + sqlite3_stmt *st = NULL; + const char *sql = *value + ? "INSERT INTO settings(org_id,key,value)" + " VALUES(?1,?2,?3)" + " ON CONFLICT(org_id,key) DO UPDATE SET" + " value=excluded.value" + : "DELETE FROM settings WHERE org_id=?1" + " AND key=?2"; + if (sqlite3_prepare_v2(r->db, sql, -1, &st, NULL) != SQLITE_OK) { + free(stored); + return fail(r, "INTERNAL", "database error"); + } + sqlite3_bind_int64(st, 1, r->org_id); + sqlite3_bind_text(st, 2, key, -1, SQLITE_TRANSIENT); + if (*value) + sqlite3_bind_text(st, 3, stored, -1, SQLITE_TRANSIENT); + int rc = sqlite3_step(st); + sqlite3_finalize(st); + free(stored); + if (rc != SQLITE_DONE) + return fail(r, "INTERNAL", sqlite3_errmsg(r->db)); + char *reqjson = settings_secret_audit_json(key); + audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id, + "settings.set", reqjson, "OK", NULL); + free(reqjson); + return settings_result(r, key, "[redacted]", 0); + } + size_t maxlen; + int digits_only = 0, bankgiro = 0, port = 0, security = 0; + if (strcmp(key, "default_series") == 0) + maxlen = 8; + else if (strcmp(key, "attachment_dir") == 0 || + strcmp(key, "smtp_host") == 0 || + strcmp(key, "smtp_user") == 0) + maxlen = 255; + else if (strcmp(key, "smtp_from") == 0 || + strcmp(key, "smtp_reply_to") == 0) + maxlen = 254; + else if (strcmp(key, "smtp_port") == 0) { + maxlen = 5; + port = 1; + } else if (strcmp(key, "smtp_security") == 0) { + maxlen = 8; + security = 1; + } else if (strcmp(key, "bank_account") == 0 || + strcmp(key, "invoice_receivable_account") == 0 || + strcmp(key, "invoice_revenue_account") == 0) { + maxlen = 10; + digits_only = 1; + } else if (strcmp(key, "invoice_bankgiro") == 0) { + maxlen = 16; + bankgiro = 1; + } else if (strcmp(key, "invoice_our_ref") == 0) { + maxlen = 64; + } else + return fail(r, "UNSUPPORTED", "unknown setting"); + size_t len = strlen(value); + if (len == 0 || len > maxlen) + return failf(r, "INVALID_ARGS", "%s must be 1-%zu characters", key, + maxlen); + for (const char *p = value; *p; p++) { + if ((digits_only || port) && (*p < '0' || *p > '9')) + return failf(r, "INVALID_ARGS", "%s must be digits only", key); + if (bankgiro && !((*p >= '0' && *p <= '9') || *p == '-')) + return failf(r, "INVALID_ARGS", + "%s must be digits and hyphens", key); + if ((unsigned char)*p < 32) + return failf(r, "INVALID_ARGS", + "%s must not contain control characters", key); + } + if (port) { + long v = strtol(value, NULL, 10); + if (v < 1 || v > 65535) + return failf(r, "INVALID_ARGS", "%s must be 1-65535", key); + } + if (security && strcmp(value, "starttls") != 0 && + strcmp(value, "tls") != 0 && strcmp(value, "plain") != 0) + return failf(r, "INVALID_ARGS", "%s must be starttls, tls or plain", + key); + if (r->dry_run) + return settings_result(r, key, value, 1); + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2( + r->db, + "INSERT INTO settings(org_id,key,value) VALUES(?1,?2,?3)" + " ON CONFLICT(org_id,key) DO UPDATE SET value=excluded.value", + -1, &st, NULL) != SQLITE_OK) + return fail(r, "INTERNAL", "database error"); + sqlite3_bind_int64(st, 1, r->org_id); + sqlite3_bind_text(st, 2, key, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(st, 3, value, -1, SQLITE_TRANSIENT); + int rc = sqlite3_step(st); + sqlite3_finalize(st); + if (rc != SQLITE_DONE) + return fail(r, "INTERNAL", sqlite3_errmsg(r->db)); + char *reqjson = audit_args_json(r->args); + audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id, + "settings.set", reqjson, "OK", NULL); + free(reqjson); + yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "key", key); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "value", value); + return o; +} + + +static const struct cmd_arg args_settings_set[] = { + { "key", ARG_STR, 1, NULL, NULL, + "default_series, attachment_dir, bank_account," + " invoice_receivable_account, invoice_revenue_account, smtp_host," + " smtp_port, smtp_user, smtp_from, smtp_reply_to, smtp_security or" + " smtp_password" }, + { "value", ARG_STR, 0, NULL, NULL, + "Setting value; an empty value clears smtp_password" }, +}; + +const struct command g_cmd_settings[] = { + { "settings.get", "Read org settings", PERM_READ, 1, 0, 0, + h_settings_get, NULL, 0 }, + { "settings.set", "Change an org setting", PERM_WRITE, 1, 1, 1, + h_settings_set, CMD_ARGS(args_settings_set) }, +}; + +const struct cmd_table g_cmd_table_settings = { + g_cmd_settings, sizeof g_cmd_settings / sizeof g_cmd_settings[0] +}; |
