deployd/server/admin.c
Arija A. 8fbb8e8c18
Implement command client
Signed-off-by: Arija A. <ari@ari.lt>
2025-07-24 19:48:39 +03:00

457 lines
15 KiB
C

#include "include/conf.h"
#include "include/cmd.h"
#include "include/auth.h"
#include "include/admin.h"
#include "include/blake2s.h"
#include <time.h>
#include <ctype.h>
#include <stdio.h>
#include <string.h>
#include <sys/random.h>
typedef void (*dp_AdminCommandFunction)(int argc,
char *argv[],
sqlite3 *database);
typedef struct {
const char *name;
const char *help;
dp_AdminCommandFunction func;
} dp_AdminCommand;
static void dp_admin_cmd_help(int argc, char *argv[], sqlite3 *database);
static void dp_admin_cmd_newsecret(int argc, char *argv[], sqlite3 *database);
static void dp_admin_cmd_exit(int argc, char *argv[], sqlite3 *database);
static void dp_admin_cmd_listsecrets(int argc, char *argv[], sqlite3 *database);
static void
dp_admin_cmd_revokesecret(int argc, char *argv[], sqlite3 *database);
static void
dp_admin_cmd_updatesecret(int argc, char *argv[], sqlite3 *database);
/* clang-format off */
static dp_AdminCommand dp_admin_commands[] = {
{"help", "Show this help message", dp_admin_cmd_help},
{"newsecret", "newsecret <domain> \"<description>\" [expires]. Create a new secret for the domain", dp_admin_cmd_newsecret},
{"listsecrets", "List all secrets", dp_admin_cmd_listsecrets},
{"revokesecret", "revokesecret <id|domain|secret>. Remove secret by id, domain, or exact secret", dp_admin_cmd_revokesecret},
{"updatesecret", "updatesecret <domain> <domain|expires?description> <value>. Update description or expires", dp_admin_cmd_updatesecret},
{"exit", "Exit the REPL", dp_admin_cmd_exit},
{NULL, NULL, NULL},
};
/* clang-format on */
static bool dp_admin_repl_running = true;
static void dp_admin_cmd_help(int argc, char *argv[], sqlite3 *database) {
(void)argc;
(void)argv;
(void)database;
puts("Available commands:");
for (dp_AdminCommand *cmd = dp_admin_commands; cmd->name; ++cmd) {
printf(" %s - %s\n", cmd->name, cmd->help);
}
}
static void dp_admin_cmd_newsecret(int argc, char *argv[], sqlite3 *database) {
if (argc < 3 || argc > 4) {
printf("Usage: newsecret <domain> \"<description>\" [expires]\n");
printf(" expires format: 0 or '1h', '1d', '1m', '1y' (h=hour, "
"d=day, m=month, y=year)\n");
return;
}
const char *domain = argv[1];
const char *description = argv[2];
const char *expires_str = NULL;
if (argc == 4) {
expires_str = argv[3];
}
uint8_t secret[DP_AUTH_SECRET_SIZE] = {0};
uint8_t key[DP_AUTH_KEY_SIZE] = {0};
uint64_t timestamp = 0;
if (!dp_auth_gen_secret(secret, key, &timestamp)) {
(void)fputs("Failed to generate a new key\n", stderr);
return;
}
uint8_t salt[DP_BLAKE2S_SALT_SIZE] = {0};
uint8_t key_hash[DP_BLAKE2S_OUTPUT_SIZE_MAX] = {0};
if (!dp_BLAKE2s_hash_password_gen(key, DP_AUTH_KEY_SIZE, key_hash, salt)) {
(void)fputs("Failed to hash the key\n", stderr);
return;
}
uint64_t expires = 0;
if (expires_str && expires_str[0] != '\0') {
const uint64_t duration_seconds =
dp_parse_duration_to_seconds(expires_str);
if (duration_seconds == 0 && strcmp(expires_str, "0") != 0) {
printf("Warning: Invalid expires format '%s', setting to never "
"expire (0).\n",
expires_str);
expires = 0;
} else {
expires =
(duration_seconds == 0) ? 0 : timestamp + duration_seconds;
}
}
sqlite3_stmt *stmt = NULL;
const char *sql =
"INSERT INTO secrets(domain, secret, key_hash, key_salt, "
"timestamp, expires, description) VALUES (?, ?, ?, ?, ?, ?, ?)";
if (sqlite3_prepare_v2(database, sql, -1, &stmt, NULL) != SQLITE_OK) {
(void)fprintf(stderr, "Failed to prepare statement: %s\n",
sqlite3_errmsg(database));
return;
}
sqlite3_bind_text(stmt, 1, domain, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, (const char *)secret, DP_AUTH_SECRET_SIZE,
SQLITE_TRANSIENT);
sqlite3_bind_blob(stmt, 3, key_hash, DP_BLAKE2S_OUTPUT_SIZE_MAX,
SQLITE_TRANSIENT);
sqlite3_bind_blob(stmt, 4, salt, DP_BLAKE2S_SALT_SIZE, SQLITE_TRANSIENT);
sqlite3_bind_int64(stmt, 5, (sqlite3_int64)timestamp);
sqlite3_bind_int64(stmt, 6, (sqlite3_int64)expires);
sqlite3_bind_text(stmt, 7, description, -1, SQLITE_TRANSIENT);
if (sqlite3_step(stmt) != SQLITE_DONE) {
(void)fprintf(stderr, "Failed to execute statement: %s\n",
sqlite3_errmsg(database));
sqlite3_finalize(stmt);
return;
}
const sqlite3_int64 last_id = sqlite3_last_insert_rowid(database);
sqlite3_finalize(stmt);
puts("New secret generated and inserted:");
printf("id: %lld\n", last_id);
printf("domain: %s\n", domain);
printf("secret: %.*s\n", DP_AUTH_SECRET_SIZE, secret);
printf("key: %.*s\n", DP_AUTH_KEY_SIZE, key);
printf("timestamp: %lu\n", timestamp);
printf("*expires: %lu\n", expires);
printf("*description: %s\n", description);
puts("(fields starting with `*` are NOT directly used for authentication, "
"hence should be ignored)");
}
static void dp_admin_cmd_exit(int argc, char *argv[], sqlite3 *database) {
(void)argc;
(void)argv;
(void)database;
dp_admin_repl_running = false;
}
static void
dp_admin_cmd_listsecrets(int argc, char *argv[], sqlite3 *database) {
(void)argc;
(void)argv;
const char *sql = "SELECT id, domain, secret, description, timestamp, "
"expires FROM secrets";
sqlite3_stmt *stmt = NULL;
if (sqlite3_prepare_v2(database, sql, -1, &stmt, NULL) != SQLITE_OK) {
(void)fprintf(stderr, "Failed to prepare statement: %s\n",
sqlite3_errmsg(database));
return;
}
/* clang-format off */
puts("ID | Domain | Secret | Description | Timestamp | Expires ");
puts("--------------------------------------------------------------------------------------------------------------------------------------");
/* clang-format on */
while (sqlite3_step(stmt) == SQLITE_ROW) {
sqlite3_int64 id = sqlite3_column_int64(stmt, 0);
const uint8_t *domain = sqlite3_column_text(stmt, 1);
const uint8_t *secret = sqlite3_column_text(stmt, 2);
const uint8_t *description = sqlite3_column_text(stmt, 3);
sqlite3_int64 timestamp = sqlite3_column_int64(stmt, 4);
sqlite3_int64 expires = sqlite3_column_int64(stmt, 5);
printf("%-3lld | %-22s | %-24s | %-30s | %-20lld | ", id,
domain ? (const char *)domain : "",
secret ? (const char *)secret : "",
description ? (const char *)description : "", timestamp);
if (expires == 0) {
printf("never\n");
} else {
printf("%-20lld\n", expires);
}
}
sqlite3_finalize(stmt);
}
static void
dp_admin_cmd_revokesecret(int argc, char *argv[], sqlite3 *database) {
if (argc != 2) {
printf("Usage: revokesecret <id|domain|secret>\n");
return;
}
const char *arg = argv[1];
sqlite3_stmt *stmt = NULL;
const char *sql = NULL;
int ret = 0;
int changes = 0;
/* 1. Try by id (integer) */
if (dp_isnumber(arg)) {
sql = "DELETE FROM secrets WHERE id = ?";
if (sqlite3_prepare_v2(database, sql, -1, &stmt, NULL) != SQLITE_OK) {
(void)fprintf(stderr, "Failed to prepare statement: %s\n",
sqlite3_errmsg(database));
return;
}
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)dp_str2u64(arg));
ret = sqlite3_step(stmt);
if (ret != SQLITE_DONE) {
(void)fprintf(stderr, "Failed to execute delete by id: %s\n",
sqlite3_errmsg(database));
sqlite3_finalize(stmt);
return;
}
changes = sqlite3_changes(database);
sqlite3_finalize(stmt);
if (changes > 0) {
printf("Deleted %d secret(s) by id %s\n", changes, arg);
return;
}
}
/* 2. Try by domain */
sql = "DELETE FROM secrets WHERE domain = ?";
if (sqlite3_prepare_v2(database, sql, -1, &stmt, NULL) != SQLITE_OK) {
(void)fprintf(stderr, "Failed to prepare statement: %s\n",
sqlite3_errmsg(database));
return;
}
sqlite3_bind_text(stmt, 1, arg, -1, SQLITE_TRANSIENT);
ret = sqlite3_step(stmt);
if (ret != SQLITE_DONE) {
(void)fprintf(stderr, "Failed to execute delete by domain: %s\n",
sqlite3_errmsg(database));
sqlite3_finalize(stmt);
return;
}
changes = sqlite3_changes(database);
sqlite3_finalize(stmt);
if (changes > 0) {
printf("Deleted %d secret(s) by domain '%s'\n", changes, arg);
return;
}
/* 3. Try by secret */
sql = "DELETE FROM secrets WHERE secret = ?";
if (sqlite3_prepare_v2(database, sql, -1, &stmt, NULL) != SQLITE_OK) {
(void)fprintf(stderr, "Failed to prepare statement: %s\n",
sqlite3_errmsg(database));
return;
}
sqlite3_bind_text(stmt, 1, arg, -1, SQLITE_TRANSIENT);
ret = sqlite3_step(stmt);
if (ret != SQLITE_DONE) {
(void)fprintf(stderr, "Failed to execute delete by secret: %s\n",
sqlite3_errmsg(database));
sqlite3_finalize(stmt);
return;
}
changes = sqlite3_changes(database);
sqlite3_finalize(stmt);
if (changes > 0) {
printf("Deleted %d secret(s) by secret '%s'\n", changes, arg);
return;
}
printf("No secret found matching '%s'\n", arg);
}
static void
dp_admin_cmd_updatesecret(int argc, char *argv[], sqlite3 *database) {
if (argc != 4) {
printf("Usage: updatesecret <domain> <domain|expires|description> "
"<value>\n");
printf("Fields supported: domain, expires, description\n");
return;
}
const char *domain = argv[1];
const char *field = argv[2];
const char *value = argv[3];
if (strcmp(field, "domain") == 0) {
const char *sql = "UPDATE secrets SET domain = ? WHERE domain = ?";
sqlite3_stmt *stmt = NULL;
if (sqlite3_prepare_v2(database, sql, -1, &stmt, NULL) != SQLITE_OK) {
(void)fprintf(stderr, "Failed to prepare statement: %s\n",
sqlite3_errmsg(database));
return;
}
sqlite3_bind_text(stmt, 1, value, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, domain, -1, SQLITE_TRANSIENT);
if (sqlite3_step(stmt) != SQLITE_DONE) {
(void)fprintf(stderr, "Failed to update domain: %s\n",
sqlite3_errmsg(database));
sqlite3_finalize(stmt);
return;
}
int changed = sqlite3_changes(database);
sqlite3_finalize(stmt);
if (changed == 0) {
printf("No secret found with domain '%s'.\n", domain);
} else {
printf("Updated domain from '%s' to '%s'.\n", domain, value);
}
return;
}
if (strcmp(field, "expires") == 0) {
uint64_t expires = dp_parse_duration_to_seconds(value);
if (expires == 0 && strcmp(value, "0") != 0) {
printf("Warning: invalid expires format '%s', setting to never "
"expire (0)\n",
value);
expires = 0;
} else if (expires != 0) {
bool numeric = dp_isnumber(value);
if (numeric) {
expires = (uint64_t)strtoull(value, NULL, 10);
} else {
/* If original value was like "1h", "1d", add current time: */
expires = (uint64_t)time(NULL) + expires;
}
}
const char *sql = "UPDATE secrets SET expires = ? WHERE domain = ?";
sqlite3_stmt *stmt = NULL;
if (sqlite3_prepare_v2(database, sql, -1, &stmt, NULL) != SQLITE_OK) {
(void)fprintf(stderr, "Failed to prepare statement: %s\n",
sqlite3_errmsg(database));
return;
}
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)expires);
sqlite3_bind_text(stmt, 2, domain, -1, SQLITE_TRANSIENT);
if (sqlite3_step(stmt) != SQLITE_DONE) {
(void)fprintf(stderr, "Failed to update expires: %s\n",
sqlite3_errmsg(database));
sqlite3_finalize(stmt);
return;
}
int changed = sqlite3_changes(database);
sqlite3_finalize(stmt);
if (changed == 0) {
printf("No secret found with domain '%s'.\n", domain);
} else {
printf("Updated expires for domain '%s'.\n", domain);
}
return;
}
if (strcmp(field, "description") == 0) {
const char *sql = "UPDATE secrets SET description = ? WHERE domain = ?";
sqlite3_stmt *stmt = NULL;
if (sqlite3_prepare_v2(database, sql, -1, &stmt, NULL) != SQLITE_OK) {
(void)fprintf(stderr, "Failed to prepare statement: %s\n",
sqlite3_errmsg(database));
return;
}
sqlite3_bind_text(stmt, 1, value, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, domain, -1, SQLITE_TRANSIENT);
if (sqlite3_step(stmt) != SQLITE_DONE) {
(void)fprintf(stderr, "Failed to update description: %s\n",
sqlite3_errmsg(database));
sqlite3_finalize(stmt);
return;
}
int changed = sqlite3_changes(database);
sqlite3_finalize(stmt);
if (changed == 0) {
printf("No secret found with domain '%s'.\n", domain);
} else {
printf("Updated description for domain '%s'.\n", domain);
}
return;
}
printf("Invalid field '%s'. Valid fields: domain, expires, description\n",
field);
}
bool dp_admin_repl(sqlite3 *database) {
char line[1024] = {0};
puts("Admin REPL started. Type 'help' for commands.");
while (dp_admin_repl_running) {
size_t len = dp_read_line("\nadmin> ", line, sizeof(line));
if (len == 0) {
(void)putchar('\n');
break;
}
if (len == 1) {
continue;
}
char *argv[DP_ADMIN_MAX_ARGS] = {0};
const int argc = dp_parse_line(line, argv, DP_ADMIN_MAX_ARGS);
if (argc == 0) {
continue;
}
dp_AdminCommand *cmd = NULL;
for (dp_AdminCommand *cmd_p = dp_admin_commands; cmd_p->name; ++cmd_p) {
if (strcmp(cmd_p->name, argv[0]) == 0) {
cmd = cmd_p;
break;
}
}
if (!cmd) {
printf("Unknown command: %s\n", argv[0]);
puts("Type 'help' for a list of commands.");
continue;
}
cmd->func(argc, argv, database);
}
return true;
}