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