main.cpp 53 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269
  1. #include <smartbotic/database/client.hpp>
  2. #include <nlohmann/json.hpp>
  3. #include "storage/subdb_placement.hpp"
  4. #include <openssl/bio.h>
  5. #include <openssl/bn.h>
  6. #include <openssl/err.h>
  7. #include <openssl/evp.h>
  8. #include <openssl/pem.h>
  9. #include <openssl/rand.h>
  10. #include <openssl/x509.h>
  11. #include <openssl/x509v3.h>
  12. #include <arpa/inet.h>
  13. #include <fcntl.h>
  14. #include <sys/random.h>
  15. #include <sys/stat.h>
  16. #include <unistd.h>
  17. #include <algorithm>
  18. #include <cerrno>
  19. #include <cstdio>
  20. #include <cstring>
  21. #include <fstream>
  22. #include <iostream>
  23. #include <sstream>
  24. #include <string>
  25. #include <vector>
  26. using json = nlohmann::json;
  27. namespace {
  28. struct Args {
  29. std::string address = "localhost:9004";
  30. // v2.11.0 T6b — needed to exercise relation management against a
  31. // non-default project from the CLI. Empty means the client's own
  32. // "default" (unchanged behaviour for every existing command).
  33. std::string project;
  34. std::string command;
  35. std::vector<std::string> params;
  36. };
  37. // ANSI colors
  38. constexpr auto C_RESET = "\033[0m";
  39. constexpr auto C_BOLD = "\033[1m";
  40. constexpr auto C_DIM = "\033[2m";
  41. constexpr auto C_CYAN = "\033[36m";
  42. constexpr auto C_GREEN = "\033[32m";
  43. constexpr auto C_RED = "\033[31m";
  44. constexpr auto C_YELLOW = "\033[33m";
  45. void printJson(const json& j) {
  46. std::cout << j.dump(2) << "\n";
  47. }
  48. void printError(const std::string& msg) {
  49. std::cerr << C_RED << "error: " << C_RESET << msg << "\n";
  50. }
  51. void printUsage() {
  52. std::cout << C_BOLD << "smartbotic-db-cli" << C_RESET << " — admin tool for smartbotic-database\n\n"
  53. << C_BOLD << "Usage:" << C_RESET << "\n"
  54. << " smartbotic-db-cli [--address HOST:PORT] <command> [args...]\n"
  55. << " smartbotic-db-cli [--address HOST:PORT] " << C_DIM << "# interactive mode" << C_RESET << "\n\n"
  56. << C_BOLD << "Commands:" << C_RESET << "\n"
  57. << " " << C_CYAN << "collections" << C_RESET << " List all collections\n"
  58. << " " << C_CYAN << "info" << C_RESET << " <collection> Collection info\n"
  59. << " " << C_CYAN << "find" << C_RESET << " <collection> List documents\n"
  60. << " " << C_CYAN << "get" << C_RESET << " <collection> <id> Get a document\n"
  61. << " " << C_CYAN << "upsert" << C_RESET << " <collection> <id> '<json>' Insert or update\n"
  62. << " " << C_CYAN << "remove" << C_RESET << " <collection> <id> Delete a document\n"
  63. << " " << C_CYAN << "count" << C_RESET << " <collection> Count documents\n"
  64. << " " << C_CYAN << "lock" << C_RESET << " Lock database (read-only)\n"
  65. << " " << C_CYAN << "unlock" << C_RESET << " Unlock database (accept writes)\n"
  66. << " " << C_CYAN << "status" << C_RESET << " Show read-only + recovery status\n"
  67. << C_BOLD << " Access policy (v2.7.0+)" << C_RESET << "\n"
  68. << " " << C_CYAN << "security" << C_RESET << " [project] Show whether a project enforces policy\n"
  69. << " " << C_CYAN << "indexes" << C_RESET << " <collection>"
  70. << " list secondary indexes\n"
  71. << " " << C_CYAN << "index-create" << C_RESET << " <collection> <field>"
  72. << " declare an index and backfill it\n"
  73. << " " << C_CYAN << "index-drop" << C_RESET << " <collection> <field>"
  74. << " remove an index\n"
  75. << " " << C_CYAN << "index-values" << C_RESET << " <coll> <field> [n] [asc|desc]"
  76. << " distinct values + counts\n"
  77. << C_BOLD << " Relations / referential integrity (v2.11.0+)" << C_RESET << "\n"
  78. << " " << C_CYAN << "relations" << C_RESET
  79. << " List declared relations\n"
  80. << " " << C_CYAN << "relation" << C_RESET << " <name>"
  81. << " Show one relation's declaration\n"
  82. << " " << C_CYAN << "relation-create" << C_RESET
  83. << " <name> <child> <child_field> <parent> [on_delete] [validate_on_write]\n"
  84. << " " << C_CYAN << "relation-drop" << C_RESET << " <name>\n"
  85. << " " << C_CYAN << "relation-check" << C_RESET << " <name>"
  86. << " Report dangling references (read-only)\n"
  87. << " " << C_CYAN << "configure-relations" << C_RESET
  88. << " <collection> <on|off> Enable/disable enforcement for a collection\n"
  89. << " " << C_CYAN << "describe-delete" << C_RESET << " <collection> <id>"
  90. << " What would happen if this document were deleted\n"
  91. << " " << C_CYAN << "security-set" << C_RESET << " <project> <on|off> [enforce|audit]\n"
  92. << " " << C_CYAN << "policies" << C_RESET << " [project] List principals with a policy\n"
  93. << " " << C_CYAN << "policy" << C_RESET << " <project> <principal> Show one policy\n"
  94. << " " << C_CYAN << "policy-set" << C_RESET << " <project> <principal> '<json>'\n"
  95. << " " << C_CYAN << "policy-rm" << C_RESET << " <project> <principal>\n"
  96. << " " << C_CYAN << "help" << C_RESET << " Show this help\n\n"
  97. << C_BOLD << "Offline helpers" << C_RESET << " " << C_DIM << "(no server connection required)" << C_RESET << ":\n"
  98. << " " << C_CYAN << "generate-auth-key" << C_RESET << " Emit a base64 32-byte API key\n"
  99. << " " << C_CYAN << "generate-tls-cert" << C_RESET << " --bind <addr> Generate a self-signed TLS cert + key\n"
  100. << " " << C_DIM << "[--out-cert PATH] [--out-key PATH] [--days N]" << C_RESET << "\n"
  101. << " " << C_CYAN << "verify-subdbs" << C_RESET << " --env <path> Report documents filed under the wrong collection\n"
  102. << " " << C_DIM << "[--project NAME] read-only; safe against a running server" << C_RESET << "\n"
  103. << " " << C_CYAN << "reconcile-subdbs" << C_RESET << " --env <path> Repair misfiled documents (dry run by default)\n"
  104. << " " << C_DIM << "[--project NAME] [--apply] STOP THE SERVICE and back up before --apply" << C_RESET << "\n\n"
  105. << C_BOLD << "Options:" << C_RESET << "\n"
  106. << " --address HOST:PORT Database address (default: localhost:9004)\n"
  107. << " --project NAME Operate as this project namespace (default: default)\n";
  108. }
  109. // ---------------------------------------------------------------------------
  110. // v2.4 Stage F: offline helpers (no server connection required)
  111. // ---------------------------------------------------------------------------
  112. // Base64-encode raw bytes. Uses OpenSSL's EVP_EncodeBlock which emits the
  113. // standard base64 alphabet (no newlines) and pads with '='.
  114. std::string base64Encode(const unsigned char* data, size_t len) {
  115. if (len == 0) return {};
  116. // EVP_EncodeBlock writes ((len + 2) / 3) * 4 bytes + a NUL terminator.
  117. const size_t out_len = 4 * ((len + 2) / 3);
  118. std::string out(out_len, '\0');
  119. int written = EVP_EncodeBlock(
  120. reinterpret_cast<unsigned char*>(out.data()),
  121. data, static_cast<int>(len));
  122. if (written < 0) return {};
  123. out.resize(static_cast<size_t>(written));
  124. return out;
  125. }
  126. // Pull 32 cryptographically random bytes from getentropy() (preferred) or
  127. // /dev/urandom (fallback). Returns true on success.
  128. bool fillRandomBytes(unsigned char* buf, size_t len) {
  129. // getentropy() is limited to 256 bytes per call; our use-case is 32.
  130. if (len <= 256) {
  131. if (getentropy(buf, len) == 0) return true;
  132. }
  133. // Fallback: /dev/urandom.
  134. int fd = ::open("/dev/urandom", O_RDONLY | O_CLOEXEC);
  135. if (fd < 0) return false;
  136. size_t got = 0;
  137. while (got < len) {
  138. ssize_t n = ::read(fd, buf + got, len - got);
  139. if (n <= 0) {
  140. if (errno == EINTR) continue;
  141. ::close(fd);
  142. return false;
  143. }
  144. got += static_cast<size_t>(n);
  145. }
  146. ::close(fd);
  147. return true;
  148. }
  149. int cmdGenerateAuthKey() {
  150. unsigned char key[32];
  151. if (!fillRandomBytes(key, sizeof(key))) {
  152. std::fprintf(stderr, "error: failed to obtain entropy for key\n");
  153. return 1;
  154. }
  155. auto encoded = base64Encode(key, sizeof(key));
  156. if (encoded.empty()) {
  157. std::fprintf(stderr, "error: base64 encoding failed\n");
  158. return 1;
  159. }
  160. std::cout << encoded << "\n";
  161. return 0;
  162. }
  163. namespace {
  164. // Print the topmost OpenSSL error to stderr with a prefix.
  165. void printOpenSslError(const char* prefix) {
  166. unsigned long e = ERR_get_error();
  167. char buf[256] = {0};
  168. if (e != 0) {
  169. ERR_error_string_n(e, buf, sizeof(buf));
  170. std::fprintf(stderr, "error: %s: %s\n", prefix, buf);
  171. } else {
  172. std::fprintf(stderr, "error: %s\n", prefix);
  173. }
  174. }
  175. // Returns true if `s` parses as an IPv4 or IPv6 literal.
  176. bool looksLikeIp(const std::string& s) {
  177. unsigned char buf[16];
  178. if (inet_pton(AF_INET, s.c_str(), buf) == 1) return true;
  179. if (inet_pton(AF_INET6, s.c_str(), buf) == 1) return true;
  180. return false;
  181. }
  182. // Generate an RSA private key as an EVP_PKEY using the modern (3.x) API.
  183. EVP_PKEY* generateRsaKey(int bits) {
  184. EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr);
  185. if (!ctx) return nullptr;
  186. EVP_PKEY* pkey = nullptr;
  187. if (EVP_PKEY_keygen_init(ctx) <= 0) {
  188. EVP_PKEY_CTX_free(ctx);
  189. return nullptr;
  190. }
  191. if (EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, bits) <= 0) {
  192. EVP_PKEY_CTX_free(ctx);
  193. return nullptr;
  194. }
  195. if (EVP_PKEY_keygen(ctx, &pkey) <= 0) {
  196. EVP_PKEY_CTX_free(ctx);
  197. return nullptr;
  198. }
  199. EVP_PKEY_CTX_free(ctx);
  200. return pkey;
  201. }
  202. // Write a string to disk with the given file mode. Truncates existing files.
  203. bool writeFileWithMode(const std::string& path, const std::string& contents, mode_t mode) {
  204. int fd = ::open(path.c_str(),
  205. O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC,
  206. mode);
  207. if (fd < 0) {
  208. std::fprintf(stderr, "error: open %s: %s\n", path.c_str(), std::strerror(errno));
  209. return false;
  210. }
  211. // Re-assert mode in case the file pre-existed with a wider mode and
  212. // O_CREAT was a no-op (open(2) only applies the mode on create).
  213. if (fchmod(fd, mode) != 0) {
  214. std::fprintf(stderr, "error: fchmod %s: %s\n", path.c_str(), std::strerror(errno));
  215. ::close(fd);
  216. return false;
  217. }
  218. size_t off = 0;
  219. while (off < contents.size()) {
  220. ssize_t n = ::write(fd, contents.data() + off, contents.size() - off);
  221. if (n <= 0) {
  222. if (errno == EINTR) continue;
  223. std::fprintf(stderr, "error: write %s: %s\n", path.c_str(), std::strerror(errno));
  224. ::close(fd);
  225. return false;
  226. }
  227. off += static_cast<size_t>(n);
  228. }
  229. if (::close(fd) != 0) {
  230. std::fprintf(stderr, "error: close %s: %s\n", path.c_str(), std::strerror(errno));
  231. return false;
  232. }
  233. return true;
  234. }
  235. // Serialize an X509* to a PEM string.
  236. std::string pemEncodeCert(X509* cert) {
  237. BIO* bio = BIO_new(BIO_s_mem());
  238. if (!bio) return {};
  239. if (PEM_write_bio_X509(bio, cert) != 1) {
  240. BIO_free(bio);
  241. return {};
  242. }
  243. BUF_MEM* mem = nullptr;
  244. BIO_get_mem_ptr(bio, &mem);
  245. std::string out(mem->data, mem->length);
  246. BIO_free(bio);
  247. return out;
  248. }
  249. // Serialize an EVP_PKEY* to an unencrypted PEM string (PKCS#8 format via
  250. // PEM_write_bio_PrivateKey).
  251. std::string pemEncodeKey(EVP_PKEY* key) {
  252. BIO* bio = BIO_new(BIO_s_mem());
  253. if (!bio) return {};
  254. if (PEM_write_bio_PrivateKey(bio, key, nullptr, nullptr, 0, nullptr, nullptr) != 1) {
  255. BIO_free(bio);
  256. return {};
  257. }
  258. BUF_MEM* mem = nullptr;
  259. BIO_get_mem_ptr(bio, &mem);
  260. std::string out(mem->data, mem->length);
  261. BIO_free(bio);
  262. return out;
  263. }
  264. } // anonymous namespace
  265. int cmdGenerateTlsCert(const std::vector<std::string>& params) {
  266. std::string bind;
  267. std::string out_cert = "./server.pem";
  268. std::string out_key = "./server.key";
  269. int days = 3650;
  270. for (size_t i = 0; i < params.size(); ++i) {
  271. const auto& p = params[i];
  272. auto need = [&](const char* flag) -> const std::string* {
  273. if (i + 1 >= params.size()) {
  274. std::fprintf(stderr, "error: %s requires a value\n", flag);
  275. return nullptr;
  276. }
  277. return &params[++i];
  278. };
  279. if (p == "--bind") {
  280. auto* v = need("--bind"); if (!v) return 2; bind = *v;
  281. } else if (p == "--out-cert") {
  282. auto* v = need("--out-cert"); if (!v) return 2; out_cert = *v;
  283. } else if (p == "--out-key") {
  284. auto* v = need("--out-key"); if (!v) return 2; out_key = *v;
  285. } else if (p == "--days") {
  286. auto* v = need("--days"); if (!v) return 2;
  287. try { days = std::stoi(*v); }
  288. catch (...) {
  289. std::fprintf(stderr, "error: --days must be an integer\n");
  290. return 2;
  291. }
  292. if (days <= 0) {
  293. std::fprintf(stderr, "error: --days must be > 0\n");
  294. return 2;
  295. }
  296. } else {
  297. std::fprintf(stderr, "error: unknown argument: %s\n", p.c_str());
  298. return 2;
  299. }
  300. }
  301. if (bind.empty()) {
  302. std::fprintf(stderr,
  303. "usage: generate-tls-cert --bind <addr> "
  304. "[--out-cert PATH] [--out-key PATH] [--days N]\n");
  305. return 2;
  306. }
  307. // 1. RSA 4096-bit key.
  308. EVP_PKEY* pkey = generateRsaKey(4096);
  309. if (!pkey) {
  310. printOpenSslError("RSA key generation failed");
  311. return 1;
  312. }
  313. // 2. X.509 certificate.
  314. X509* cert = X509_new();
  315. if (!cert) {
  316. printOpenSslError("X509_new failed");
  317. EVP_PKEY_free(pkey);
  318. return 1;
  319. }
  320. // Version 3 (the integer field encodes v3 as 2).
  321. if (X509_set_version(cert, 2) != 1) {
  322. printOpenSslError("X509_set_version failed");
  323. X509_free(cert); EVP_PKEY_free(pkey);
  324. return 1;
  325. }
  326. // Random 64-bit serial number.
  327. {
  328. unsigned char serial_bytes[8];
  329. if (RAND_bytes(serial_bytes, sizeof(serial_bytes)) != 1) {
  330. printOpenSslError("RAND_bytes for serial failed");
  331. X509_free(cert); EVP_PKEY_free(pkey);
  332. return 1;
  333. }
  334. // Mask the top bit so the BIGNUM is positive.
  335. serial_bytes[0] &= 0x7F;
  336. BIGNUM* bn = BN_bin2bn(serial_bytes, sizeof(serial_bytes), nullptr);
  337. if (!bn) {
  338. printOpenSslError("BN_bin2bn failed");
  339. X509_free(cert); EVP_PKEY_free(pkey);
  340. return 1;
  341. }
  342. ASN1_INTEGER* ai = BN_to_ASN1_INTEGER(bn, nullptr);
  343. BN_free(bn);
  344. if (!ai) {
  345. printOpenSslError("BN_to_ASN1_INTEGER failed");
  346. X509_free(cert); EVP_PKEY_free(pkey);
  347. return 1;
  348. }
  349. if (X509_set_serialNumber(cert, ai) != 1) {
  350. printOpenSslError("X509_set_serialNumber failed");
  351. ASN1_INTEGER_free(ai);
  352. X509_free(cert); EVP_PKEY_free(pkey);
  353. return 1;
  354. }
  355. ASN1_INTEGER_free(ai);
  356. }
  357. // Validity period.
  358. if (!X509_gmtime_adj(X509_get_notBefore(cert), 0)) {
  359. printOpenSslError("X509_gmtime_adj(notBefore) failed");
  360. X509_free(cert); EVP_PKEY_free(pkey);
  361. return 1;
  362. }
  363. long seconds = static_cast<long>(days) * 24L * 60L * 60L;
  364. if (!X509_gmtime_adj(X509_get_notAfter(cert), seconds)) {
  365. printOpenSslError("X509_gmtime_adj(notAfter) failed");
  366. X509_free(cert); EVP_PKEY_free(pkey);
  367. return 1;
  368. }
  369. // Public key.
  370. if (X509_set_pubkey(cert, pkey) != 1) {
  371. printOpenSslError("X509_set_pubkey failed");
  372. X509_free(cert); EVP_PKEY_free(pkey);
  373. return 1;
  374. }
  375. // Subject + issuer name (self-signed, so identical).
  376. X509_NAME* name = X509_get_subject_name(cert);
  377. if (X509_NAME_add_entry_by_txt(
  378. name, "CN", MBSTRING_UTF8,
  379. reinterpret_cast<const unsigned char*>(bind.c_str()),
  380. -1, -1, 0) != 1) {
  381. printOpenSslError("X509_NAME_add_entry_by_txt(CN) failed");
  382. X509_free(cert); EVP_PKEY_free(pkey);
  383. return 1;
  384. }
  385. if (X509_set_issuer_name(cert, name) != 1) {
  386. printOpenSslError("X509_set_issuer_name failed");
  387. X509_free(cert); EVP_PKEY_free(pkey);
  388. return 1;
  389. }
  390. // SubjectAltName.
  391. {
  392. std::string san = "DNS:localhost,IP:127.0.0.1";
  393. if (bind != "localhost" && bind != "127.0.0.1") {
  394. san += ',';
  395. san += looksLikeIp(bind) ? "IP:" : "DNS:";
  396. san += bind;
  397. }
  398. X509_EXTENSION* ext = X509V3_EXT_conf_nid(
  399. nullptr, nullptr, NID_subject_alt_name, san.c_str());
  400. if (!ext) {
  401. printOpenSslError("X509V3_EXT_conf_nid(SAN) failed");
  402. X509_free(cert); EVP_PKEY_free(pkey);
  403. return 1;
  404. }
  405. if (X509_add_ext(cert, ext, -1) != 1) {
  406. printOpenSslError("X509_add_ext(SAN) failed");
  407. X509_EXTENSION_free(ext);
  408. X509_free(cert); EVP_PKEY_free(pkey);
  409. return 1;
  410. }
  411. X509_EXTENSION_free(ext);
  412. }
  413. // basicConstraints CA:FALSE — a server leaf cert, not a CA.
  414. {
  415. X509_EXTENSION* ext = X509V3_EXT_conf_nid(
  416. nullptr, nullptr, NID_basic_constraints, "critical,CA:FALSE");
  417. if (ext) {
  418. X509_add_ext(cert, ext, -1);
  419. X509_EXTENSION_free(ext);
  420. }
  421. }
  422. // Sign with SHA-256.
  423. if (X509_sign(cert, pkey, EVP_sha256()) == 0) {
  424. printOpenSslError("X509_sign failed");
  425. X509_free(cert); EVP_PKEY_free(pkey);
  426. return 1;
  427. }
  428. // Serialize.
  429. std::string cert_pem = pemEncodeCert(cert);
  430. std::string key_pem = pemEncodeKey(pkey);
  431. X509_free(cert);
  432. EVP_PKEY_free(pkey);
  433. if (cert_pem.empty() || key_pem.empty()) {
  434. std::fprintf(stderr, "error: PEM encoding failed\n");
  435. return 1;
  436. }
  437. // Write key first (0600), then cert (0644). If either fails, the other
  438. // may have been written — that's acceptable; the operator will see the
  439. // error and retry.
  440. if (!writeFileWithMode(out_key, key_pem, 0600)) return 1;
  441. if (!writeFileWithMode(out_cert, cert_pem, 0644)) return 1;
  442. std::cout << "Wrote cert: " << out_cert << "\n"
  443. << "Wrote key: " << out_key << "\n";
  444. return 0;
  445. }
  446. bool execCommand(smartbotic::database::Client& client,
  447. const std::string& cmd, const std::vector<std::string>& params) {
  448. try {
  449. if (cmd == "collections") {
  450. auto collections = client.listCollections();
  451. std::cout << C_BOLD << "Collections:" << C_RESET << "\n";
  452. for (const auto& c : collections) {
  453. auto info = client.getCollectionInfo(c);
  454. int64_t count = 0;
  455. if (info) count = info->documentCount;
  456. std::cout << " " << C_CYAN << c << C_RESET
  457. << C_DIM << " (" << count << " docs)" << C_RESET << "\n";
  458. }
  459. return true;
  460. }
  461. if (cmd == "info") {
  462. if (params.empty()) { printError("usage: info <collection>"); return false; }
  463. auto info = client.getCollectionInfo(params[0]);
  464. if (!info) { printError("collection not found: " + params[0]); return false; }
  465. std::cout << C_BOLD << params[0] << C_RESET << ":\n"
  466. << " documents: " << info->documentCount << "\n"
  467. << " size: " << info->sizeBytes << " bytes\n"
  468. << " encrypted: " << (info->encrypted ? "yes" : "no") << "\n"
  469. << " max_versions: " << info->maxVersions << "\n";
  470. if (info->defaultTtlSeconds > 0)
  471. std::cout << " ttl: " << info->defaultTtlSeconds << "s\n";
  472. return true;
  473. }
  474. if (cmd == "find") {
  475. if (params.empty()) { printError("usage: find <collection> [--limit N] [--exists FIELD]"); return false; }
  476. smartbotic::database::Client::QueryOptions opts;
  477. opts.limit = 100;
  478. for (size_t i = 1; i < params.size(); ++i) {
  479. if (params[i] == "--limit" && i + 1 < params.size()) {
  480. opts.limit = static_cast<uint32_t>(std::stoul(params[++i]));
  481. } else if (params[i] == "--exists" && i + 1 < params.size()) {
  482. opts.filters.emplace_back(params[++i], smartbotic::database::Client::FilterOp::EXISTS, true);
  483. }
  484. }
  485. auto docs = client.find(params[0], opts);
  486. std::cout << C_DIM << "(" << docs.size() << " documents)" << C_RESET << "\n";
  487. for (const auto& doc : docs) {
  488. auto id = doc.value("_id", "");
  489. // Print compact summary line
  490. std::cout << C_GREEN << id << C_RESET;
  491. // Show a few key fields
  492. for (const auto& [k, v] : doc.items()) {
  493. if (k == "_id" || k == "_created_at" || k == "_updated_at") continue;
  494. auto val = v.dump();
  495. if (val.size() > 60) val = val.substr(0, 57) + "...";
  496. std::cout << " " << C_DIM << k << "=" << C_RESET << val;
  497. // Limit to 3 fields per line
  498. static int field_count = 0;
  499. if (++field_count >= 3) { field_count = 0; break; }
  500. }
  501. std::cout << "\n";
  502. }
  503. return true;
  504. }
  505. if (cmd == "get") {
  506. if (params.size() < 2) { printError("usage: get <collection> <id>"); return false; }
  507. auto doc = client.get(params[0], params[1]);
  508. if (!doc) { printError("not found: " + params[0] + "/" + params[1]); return false; }
  509. printJson(*doc);
  510. return true;
  511. }
  512. if (cmd == "upsert") {
  513. if (params.size() < 3) { printError("usage: upsert <collection> <id> '<json>'"); return false; }
  514. auto data = json::parse(params[2], nullptr, false);
  515. if (!data.is_object()) { printError("invalid JSON: " + params[2]); return false; }
  516. client.upsert(params[0], data, params[1]);
  517. std::cout << C_GREEN << "ok" << C_RESET << " " << params[0] << "/" << params[1] << "\n";
  518. return true;
  519. }
  520. if (cmd == "remove" || cmd == "delete") {
  521. if (params.size() < 2) { printError("usage: remove <collection> <id>"); return false; }
  522. // v2.11.0 T6b — the return value used to be discarded and "ok
  523. // removed" printed unconditionally. That was merely sloppy
  524. // before referential integrity: now a delete can be REFUSED (a
  525. // relation with on_delete=restrict/no_action still has children
  526. // referencing it), and the server reports that as a real error,
  527. // not deleted=false. Report the actual outcome, and surface the
  528. // server's message on refusal so an operator learns what
  529. // blocked them rather than being told it worked.
  530. std::string err;
  531. bool deleted = client.remove(params[0], params[1], err);
  532. if (!err.empty()) {
  533. printError("remove " + params[0] + "/" + params[1] + ": " + err);
  534. return false;
  535. }
  536. if (!deleted) {
  537. std::cout << C_YELLOW << "not found" << C_RESET << " "
  538. << params[0] << "/" << params[1] << "\n";
  539. return true;
  540. }
  541. std::cout << C_GREEN << "ok" << C_RESET << " removed " << params[0] << "/" << params[1] << "\n";
  542. return true;
  543. }
  544. if (cmd == "count") {
  545. if (params.empty()) { printError("usage: count <collection>"); return false; }
  546. auto info = client.getCollectionInfo(params[0]);
  547. if (!info) { printError("collection not found: " + params[0]); return false; }
  548. std::cout << info->documentCount << "\n";
  549. return true;
  550. }
  551. // ===== v2.9.0 secondary indexes =====
  552. //
  553. // Declaration is explicit because an index costs write throughput and is
  554. // not always a win: on a low-cardinality field the planner will decline to
  555. // use it, since reading the index and then fetching most of the collection
  556. // by id loses to scanning. `indexes` reports distinct values precisely so
  557. // an operator can see that coming.
  558. if (cmd == "indexes") {
  559. if (params.empty()) { printError("usage: indexes <collection>"); return false; }
  560. std::vector<bool> uniqueFlags;
  561. auto list = client.listIndexes(params[0], uniqueFlags);
  562. if (list.empty()) {
  563. std::cout << "no indexes on " << params[0] << "\n";
  564. return true;
  565. }
  566. std::cout << C_BOLD << "field distinct entries unique"
  567. << C_RESET << "\n";
  568. for (size_t idx = 0; idx < list.size(); ++idx) {
  569. const auto& i = list[idx];
  570. const bool unique = idx < uniqueFlags.size() && uniqueFlags[idx];
  571. std::cout << " " << i.field
  572. << std::string(i.field.size() < 29 ? 29 - i.field.size() : 1, ' ')
  573. << i.distinctValues
  574. << std::string(std::to_string(i.distinctValues).size() < 13
  575. ? 13 - std::to_string(i.distinctValues).size()
  576. : 1, ' ')
  577. << i.entries
  578. << std::string(std::to_string(i.entries).size() < 9
  579. ? 9 - std::to_string(i.entries).size()
  580. : 1, ' ')
  581. << (unique ? "yes" : "no") << "\n";
  582. }
  583. return true;
  584. }
  585. if (cmd == "index-values") {
  586. if (params.size() < 2) {
  587. printError("usage: index-values <collection> <field> [limit] [asc|desc]");
  588. return false;
  589. }
  590. const uint32_t limit = params.size() > 2 ? std::stoul(params[2]) : 20;
  591. const bool asc = params.size() > 3 ? (params[3] != "desc") : true;
  592. auto vals = client.indexValues(params[0], params[1], limit, asc);
  593. if (vals.empty()) {
  594. std::cout << "no values (is " << params[1] << " indexed?)\n";
  595. return true;
  596. }
  597. std::cout << C_BOLD << "rows value" << C_RESET << "\n";
  598. for (const auto& v : vals) {
  599. const std::string c = std::to_string(v.count);
  600. std::cout << " " << c
  601. << std::string(c.size() < 9 ? 9 - c.size() : 1, ' ')
  602. << v.value.dump() << "\n";
  603. }
  604. return true;
  605. }
  606. if (cmd == "index-create") {
  607. if (params.size() < 2) {
  608. printError("usage: index-create <collection> <field> [--unique]");
  609. return false;
  610. }
  611. const bool unique = std::find(params.begin(), params.end(), "--unique") != params.end();
  612. if (unique) {
  613. // v2.11.0 T11 — a duplicate-refusal is not a generic failure:
  614. // surface the examples so the operator can go fix the data
  615. // rather than guess what "could not create the index" meant.
  616. auto result = client.createUniqueIndex(params[0], params[1]);
  617. if (!result.success) {
  618. printError(result.error);
  619. if (!result.duplicateExamples.empty()) {
  620. std::cout << " colliding document ids: ";
  621. for (size_t i = 0; i < result.duplicateExamples.size(); ++i) {
  622. if (i) std::cout << ", ";
  623. std::cout << result.duplicateExamples[i];
  624. }
  625. std::cout << "\n";
  626. }
  627. return false;
  628. }
  629. std::cout << "indexed " << result.rowsIndexed << " existing row(s) on "
  630. << params[0] << "#" << params[1] << " (unique)\n";
  631. return true;
  632. }
  633. uint64_t rows = 0;
  634. if (!client.createIndex(params[0], params[1], rows)) {
  635. printError("could not create the index (see the service log)");
  636. return false;
  637. }
  638. std::cout << "indexed " << rows << " existing row(s) on "
  639. << params[0] << "#" << params[1] << "\n";
  640. return true;
  641. }
  642. if (cmd == "index-drop") {
  643. if (params.size() < 2) {
  644. printError("usage: index-drop <collection> <field>");
  645. return false;
  646. }
  647. if (!client.dropIndex(params[0], params[1])) {
  648. printError("could not drop the index (see the service log)");
  649. return false;
  650. }
  651. std::cout << "dropped " << params[0] << "#" << params[1] << "\n";
  652. return true;
  653. }
  654. // ===== v2.11.0 T6b — relations (referential integrity) =====
  655. //
  656. // Declaration is admin-only: `_relations` is a system collection and a
  657. // relation names another collection's schema, which is not ordinary
  658. // per-collection write access. Follows the `indexes` output shape.
  659. if (cmd == "relations") {
  660. auto list = client.listRelations();
  661. if (list.empty()) {
  662. std::cout << "no relations declared\n";
  663. return true;
  664. }
  665. std::cout << C_BOLD << "name child.field -> parent on_delete enforced-at-write"
  666. << C_RESET << "\n";
  667. for (const auto& r : list) {
  668. std::cout << " " << C_CYAN << r.name << C_RESET
  669. << std::string(r.name.size() < 19 ? 19 - r.name.size() : 1, ' ')
  670. << r.child << "." << r.childField << " -> " << r.parent
  671. << " " << r.onDelete
  672. << (r.validateOnWrite ? " (validate_on_write)" : "") << "\n";
  673. }
  674. return true;
  675. }
  676. if (cmd == "relation") {
  677. if (params.empty()) { printError("usage: relation <name>"); return false; }
  678. auto r = client.getRelationInfo(params[0]);
  679. if (!r) { printError("relation not found: " + params[0]); return false; }
  680. std::cout << C_BOLD << r->name << C_RESET << ":\n"
  681. << " child: " << r->child << "\n"
  682. << " child_field: " << r->childField << "\n"
  683. << " parent: " << r->parent << "\n"
  684. << " on_delete: " << r->onDelete << "\n"
  685. << " validate_on_write: " << (r->validateOnWrite ? "yes" : "no") << "\n";
  686. return true;
  687. }
  688. if (cmd == "relation-create") {
  689. if (params.size() < 4) {
  690. printError("usage: relation-create <name> <child> <child_field> <parent> "
  691. "[on_delete] [validate_on_write]\n"
  692. " on_delete: restrict (default) | cascade | set_null | no_action\n"
  693. " note (v2.11.0+): cascade deletes the referencing document "
  694. "(scalar reference) or pulls the id from the array and keeps the "
  695. "document (array reference - cascade and set_null are the same "
  696. "for arrays); set_null nulls the scalar field. no_action permits "
  697. "the delete and leaves the reference dangling.");
  698. return false;
  699. }
  700. const std::string onDelete = params.size() > 4 ? params[4] : "restrict";
  701. const bool validateOnWrite = params.size() > 5
  702. && (params[5] == "true" || params[5] == "1" || params[5] == "yes");
  703. // v2.11.0 T7 - the server backfills the reverse index over rows
  704. // already in the child collection as part of this call, so
  705. // rowsIndexed reports coverage the same way index-create does.
  706. uint64_t rowsIndexed = 0;
  707. if (!client.createRelation(params[0], params[1], params[2], params[3],
  708. onDelete, validateOnWrite, rowsIndexed)) {
  709. printError("could not create the relation (see the service log)");
  710. return false;
  711. }
  712. std::cout << "declared relation " << params[0] << " (" << params[1] << "."
  713. << params[2] << " -> " << params[3] << ", on_delete=" << onDelete << ")\n"
  714. << "indexed " << rowsIndexed << " existing row(s) in " << params[1] << "\n";
  715. return true;
  716. }
  717. if (cmd == "relation-drop") {
  718. if (params.empty()) { printError("usage: relation-drop <name>"); return false; }
  719. if (!client.dropRelation(params[0])) {
  720. printError("could not drop the relation (see the service log)");
  721. return false;
  722. }
  723. std::cout << "dropped relation " << params[0] << "\n";
  724. return true;
  725. }
  726. // v2.11.0 T7 - "does this relation's reverse index actually match
  727. // live data?" Read-only, changes nothing. Walks the whole reverse
  728. // index (cost is proportional to distinct parents referenced, not to
  729. // the child collection's size) so this is a migration/operator tool,
  730. // not something to run in a loop.
  731. if (cmd == "relation-check") {
  732. if (params.empty()) { printError("usage: relation-check <name>"); return false; }
  733. auto result = client.checkRelation(params[0]);
  734. if (!result.success) {
  735. printError("could not check the relation: " + result.error);
  736. return false;
  737. }
  738. if (result.totalDangling == 0) {
  739. std::cout << C_GREEN << "clean" << C_RESET << " - no dangling references for "
  740. << params[0] << "\n";
  741. return true;
  742. }
  743. std::cout << C_RED << result.totalDangling << " dangling reference(s)" << C_RESET
  744. << " for " << params[0] << ":\n";
  745. for (const auto& d : result.dangling) {
  746. std::cout << " parent " << d.parentId << " does not exist, referenced by "
  747. << d.childCount << " child document(s)";
  748. if (!d.sampleChildIds.empty()) {
  749. std::cout << " (e.g. ";
  750. for (size_t i = 0; i < d.sampleChildIds.size(); ++i) {
  751. if (i) std::cout << ", ";
  752. std::cout << d.sampleChildIds[i];
  753. }
  754. std::cout << ")";
  755. }
  756. std::cout << "\n";
  757. }
  758. if (result.dangling.size() < result.totalDangling) {
  759. std::cout << " ... " << (result.totalDangling - result.dangling.size())
  760. << " more not shown\n";
  761. }
  762. return true;
  763. }
  764. // v2.11.0 T8 — the only reachable path to relations_enforced besides
  765. // grpcurl. A partial update: touches only this one knob.
  766. if (cmd == "configure-relations") {
  767. if (params.size() < 2) {
  768. printError("usage: configure-relations <collection> <on|off>");
  769. return false;
  770. }
  771. if (params[1] != "on" && params[1] != "off") {
  772. printError("expected 'on' or 'off', got: " + params[1]);
  773. return false;
  774. }
  775. const bool enforced = params[1] == "on";
  776. if (!client.setRelationsEnforced(params[0], enforced)) {
  777. printError("could not update relations_enforced (see the service log)");
  778. return false;
  779. }
  780. std::cout << C_GREEN << "ok" << C_RESET << " " << params[0]
  781. << " relations_enforced=" << (enforced ? "on" : "off") << "\n";
  782. return true;
  783. }
  784. // v2.11.0 T5 — DescribeDelete: "what would happen if I deleted this?"
  785. // without deleting anything. A per-collection READ, not admin - any
  786. // caller who can read the collection can ask this. Cheap enough to
  787. // run before every delete: counts come from the reverse index.
  788. if (cmd == "describe-delete") {
  789. if (params.size() < 2) {
  790. printError("usage: describe-delete <collection> <id>");
  791. return false;
  792. }
  793. auto d = client.describeDelete(params[0], params[1]);
  794. if (!d.success) {
  795. printError("could not describe the delete: " + d.error);
  796. return false;
  797. }
  798. if (d.impacts.empty()) {
  799. std::cout << "no relations reference " << params[0] << "/" << params[1]
  800. << " - safe to delete\n";
  801. return true;
  802. }
  803. std::cout << (d.wouldBeBlocked
  804. ? (C_RED + std::string("would be BLOCKED") + C_RESET)
  805. : (C_GREEN + std::string("would proceed") + C_RESET))
  806. << " deleting " << params[0] << "/" << params[1] << ":\n";
  807. for (const auto& imp : d.impacts) {
  808. std::cout << " " << (imp.blocks ? C_RED : C_DIM) << "[" << imp.onDelete << "]"
  809. << C_RESET << " " << imp.relation << ": " << imp.childCount
  810. << " child document(s) in " << imp.childCollection
  811. << " via " << imp.childField;
  812. if (!imp.sampleChildIds.empty()) {
  813. std::cout << " (e.g. ";
  814. for (size_t i = 0; i < imp.sampleChildIds.size(); ++i) {
  815. if (i) std::cout << ", ";
  816. std::cout << imp.sampleChildIds[i];
  817. }
  818. std::cout << ")";
  819. }
  820. std::cout << (imp.blocks ? " BLOCKS" : "") << "\n";
  821. }
  822. return true;
  823. }
  824. // ===== v2.7.0 access policy =====
  825. //
  826. // Policy lives in the `_policies` collection and is managed through the
  827. // ordinary document API, which the server intercepts so edits refresh
  828. // its cache and the `__security__` record goes through the lockout
  829. // guards. These commands are ergonomics over that, not a second
  830. // mechanism - which is why there is no policy RPC to keep in step.
  831. if (cmd == "policies") {
  832. const std::string project = params.empty() ? "default" : params[0];
  833. auto rows = client.find("_policies", smartbotic::database::Client::QueryOptions{.limit = 1000});
  834. std::cout << C_BOLD << "policies in project '" << project << "'" << C_RESET << "\n";
  835. size_t shown = 0;
  836. for (const auto& r : rows) {
  837. const std::string id = r.value("_id", "");
  838. if (id.rfind(project + ":", 0) != 0) continue;
  839. const std::string tail = id.substr(project.size() + 1);
  840. if (tail == "__security__") continue;
  841. std::cout << " " << C_CYAN << tail << C_RESET
  842. << (r.value("admin", false) ? " (admin)" : "") << "\n";
  843. ++shown;
  844. }
  845. if (shown == 0) std::cout << C_DIM << " (none)" << C_RESET << "\n";
  846. return true;
  847. }
  848. if (cmd == "policy") {
  849. if (params.size() < 2) {
  850. printError("usage: policy <project> <principal>");
  851. return false;
  852. }
  853. auto doc = client.get("_policies", params[0] + ":" + params[1]);
  854. if (!doc) { printError("no policy for " + params[0] + ":" + params[1]); return false; }
  855. printJson(*doc);
  856. return true;
  857. }
  858. if (cmd == "policy-set") {
  859. if (params.size() < 3) {
  860. printError("usage: policy-set <project> <principal> '<json>'\n"
  861. " e.g. policy-set acme svc "
  862. "'{\"collections\":{\"users\":{\"read\":true,\"mask\":[\"ssn\"]}}}'\n"
  863. " admin: policy-set acme ops '{\"admin\":true}'");
  864. return false;
  865. }
  866. try {
  867. auto body = json::parse(params[2]);
  868. client.upsert("_policies", body, params[0] + ":" + params[1]);
  869. std::cout << C_GREEN << "ok" << C_RESET << " policy set for "
  870. << params[0] << ":" << params[1] << "\n";
  871. return true;
  872. } catch (const std::exception& e) {
  873. printError(e.what());
  874. return false;
  875. }
  876. }
  877. if (cmd == "policy-rm") {
  878. if (params.size() < 2) { printError("usage: policy-rm <project> <principal>"); return false; }
  879. try {
  880. bool ok = client.remove("_policies", params[0] + ":" + params[1]);
  881. std::cout << (ok ? "removed\n" : "not found\n");
  882. return ok;
  883. } catch (const std::exception& e) {
  884. // The server refuses to remove the last admin of a secured
  885. // project - that refusal is the lockout guard, not an error to
  886. // work around.
  887. printError(e.what());
  888. return false;
  889. }
  890. }
  891. if (cmd == "security") {
  892. const std::string project = params.empty() ? "default" : params[0];
  893. auto doc = client.get("_policies", project + ":__security__");
  894. if (!doc) {
  895. std::cout << "project '" << project << "': security "
  896. << C_GREEN << "disabled" << C_RESET
  897. << C_DIM << " (default - all access allowed)" << C_RESET << "\n";
  898. return true;
  899. }
  900. const bool on = doc->value("enabled", false);
  901. const std::string mode = doc->value("mode", "enforce");
  902. std::cout << "project '" << project << "': security "
  903. << (on ? (mode == "audit" ? C_YELLOW : C_RED) : C_GREEN)
  904. << (on ? (mode == "audit" ? "AUDIT" : "ENFORCED") : "disabled")
  905. << C_RESET << "\n";
  906. if (on && mode == "audit") {
  907. std::cout << C_DIM << " audit mode logs what it would deny and "
  908. "allows the request - watch the service log, then "
  909. "switch to enforce." << C_RESET << "\n";
  910. }
  911. return true;
  912. }
  913. if (cmd == "security-set") {
  914. if (params.size() < 2) {
  915. printError("usage: security-set <project> <on|off> [enforce|audit]\n"
  916. " Enabling is REFUSED unless some policy in the project has "
  917. "admin=true.\n"
  918. " Switch a live project on with 'audit' first.");
  919. return false;
  920. }
  921. const bool on = params[1] == "on" || params[1] == "true";
  922. const std::string mode = params.size() > 2 ? params[2] : "enforce";
  923. if (mode != "enforce" && mode != "audit") {
  924. printError("mode must be 'enforce' or 'audit'");
  925. return false;
  926. }
  927. try {
  928. json body;
  929. body["enabled"] = on;
  930. body["mode"] = mode;
  931. client.upsert("_policies", body, params[0] + ":__security__");
  932. std::cout << C_GREEN << "ok" << C_RESET << " project '" << params[0]
  933. << "' security " << (on ? mode : "disabled") << "\n";
  934. return true;
  935. } catch (const std::exception& e) {
  936. printError(e.what());
  937. return false;
  938. }
  939. }
  940. if (cmd == "lock") {
  941. if (client.setReadOnly(true)) {
  942. std::cout << C_GREEN << "ok" << C_RESET << " Database locked (read-only)\n";
  943. return true;
  944. }
  945. printError("failed to lock database");
  946. return false;
  947. }
  948. if (cmd == "unlock") {
  949. if (client.setReadOnly(false)) {
  950. std::cout << C_GREEN << "ok" << C_RESET << " Database unlocked (writes accepted)\n";
  951. return true;
  952. }
  953. printError("failed to unlock database");
  954. return false;
  955. }
  956. if (cmd == "status") {
  957. auto s = client.getReadOnlyStatus();
  958. std::cout << "Read-only: " << (s.readOnly ? (std::string(C_RED) + "YES" + C_RESET) : "no") << "\n";
  959. if (s.readOnly) {
  960. std::cout << "Reason: " << s.reason << "\n";
  961. }
  962. std::cout << "Recovery outcome: " << s.recoveryOutcome << "\n";
  963. if (!s.expectedSnapshot.empty()) {
  964. std::cout << "Expected snapshot: " << s.expectedSnapshot << "\n";
  965. }
  966. if (!s.snapshotUsed.empty()) {
  967. std::cout << "Snapshot used: " << s.snapshotUsed << "\n";
  968. }
  969. if (!s.failureReason.empty()) {
  970. std::cout << "Failure reason: " << s.failureReason << "\n";
  971. }
  972. std::cout << "WAL replayed: " << s.walEntriesReplayed << " entries\n";
  973. std::cout << "Snapshots tried: " << s.snapshotsAttempted << "\n";
  974. return true;
  975. }
  976. if (cmd == "help" || cmd == "?") {
  977. printUsage();
  978. return true;
  979. }
  980. printError("unknown command: " + cmd + " (try 'help')");
  981. return false;
  982. } catch (const std::exception& e) {
  983. printError(e.what());
  984. return false;
  985. }
  986. }
  987. // Tokenize a line, respecting single/double quotes and JSON braces
  988. std::vector<std::string> tokenize(const std::string& line) {
  989. std::vector<std::string> tokens;
  990. std::string current;
  991. int brace_depth = 0;
  992. char in_quote = 0;
  993. for (size_t i = 0; i < line.size(); ++i) {
  994. char c = line[i];
  995. if (in_quote) {
  996. current += c;
  997. if (c == in_quote && (i == 0 || line[i-1] != '\\')) {
  998. in_quote = 0;
  999. // Strip surrounding quotes for simple string tokens
  1000. if (brace_depth == 0 && current.size() >= 2
  1001. && (current.front() == '\'' || current.front() == '"')
  1002. && current.front() == current.back()) {
  1003. current = current.substr(1, current.size() - 2);
  1004. }
  1005. }
  1006. continue;
  1007. }
  1008. if (c == '\'' || c == '"') {
  1009. in_quote = c;
  1010. current += c;
  1011. continue;
  1012. }
  1013. if (c == '{') { brace_depth++; current += c; continue; }
  1014. if (c == '}') {
  1015. brace_depth--;
  1016. current += c;
  1017. if (brace_depth <= 0) {
  1018. brace_depth = 0;
  1019. tokens.push_back(current);
  1020. current.clear();
  1021. }
  1022. continue;
  1023. }
  1024. if (brace_depth > 0) { current += c; continue; }
  1025. if (c == ' ' || c == '\t') {
  1026. if (!current.empty()) {
  1027. tokens.push_back(current);
  1028. current.clear();
  1029. }
  1030. continue;
  1031. }
  1032. current += c;
  1033. }
  1034. if (!current.empty()) tokens.push_back(current);
  1035. return tokens;
  1036. }
  1037. // v2.4.4 — offline sub-db placement audit / repair.
  1038. //
  1039. // `verify-subdbs` is read-only and safe against a running server.
  1040. // `reconcile-subdbs --apply` rewrites document placement and REQUIRES the
  1041. // service to be stopped: it holds an LMDB write txn over the whole env.
  1042. int cmdSubdbs(const std::string& command, const std::vector<std::string>& params) {
  1043. std::string env_path, project;
  1044. bool apply = false;
  1045. bool stamp = false;
  1046. for (size_t i = 0; i < params.size(); ++i) {
  1047. const std::string& p = params[i];
  1048. if (p == "--env" && i + 1 < params.size()) {
  1049. env_path = params[++i];
  1050. } else if (p == "--project" && i + 1 < params.size()) {
  1051. project = params[++i];
  1052. } else if (p == "--apply") {
  1053. apply = true;
  1054. } else if (p == "--stamp-identity") {
  1055. stamp = true;
  1056. } else {
  1057. printError("unknown argument: " + p);
  1058. return 2;
  1059. }
  1060. }
  1061. if (env_path.empty()) {
  1062. printError("--env <path> is required (e.g. "
  1063. "/var/lib/smartbotic-database/projects/default/env)");
  1064. return 2;
  1065. }
  1066. if (command == "verify-subdbs" && apply) {
  1067. printError("--apply is not valid for verify-subdbs; use reconcile-subdbs");
  1068. return 2;
  1069. }
  1070. try {
  1071. auto report = smartbotic::db::storage::audit(env_path, project);
  1072. smartbotic::db::storage::print_audit(report);
  1073. if (command == "verify-subdbs") {
  1074. return report.misplaced.empty() ? 0 : 1;
  1075. }
  1076. if (!apply) {
  1077. std::cout << "\n" << C_YELLOW << "Dry run." << C_RESET
  1078. << " Re-run with --apply to perform the repair.\n"
  1079. << C_DIM
  1080. << "Stop the service first, and take a backup: the repair "
  1081. "rewrites document placement in place.\n"
  1082. << C_RESET;
  1083. return report.misplaced.empty() ? 0 : 1;
  1084. }
  1085. if (stamp) {
  1086. std::cout << "\n" << C_YELLOW << "--stamp-identity:" << C_RESET
  1087. << " writing sentinels. This REQUIRES the server binary to be"
  1088. " v2.4.4 or newer;\n earlier builds do not skip the sentinel"
  1089. " and will fail on scan.\n";
  1090. }
  1091. std::cout << "\nApplying...\n";
  1092. auto res = smartbotic::db::storage::repair(env_path, report, stamp);
  1093. std::cout << C_GREEN << "done." << C_RESET
  1094. << " moved=" << res.moved
  1095. << " quarantined=" << res.quarantined
  1096. << " stamped=" << res.stamped << "\n";
  1097. for (const auto& e : res.errors) {
  1098. std::cerr << C_RED << " warn: " << C_RESET << e << "\n";
  1099. }
  1100. return res.errors.empty() ? 0 : 1;
  1101. } catch (const std::exception& e) {
  1102. printError(e.what());
  1103. return 1;
  1104. }
  1105. }
  1106. } // anonymous namespace
  1107. int main(int argc, char* argv[]) {
  1108. Args args;
  1109. // Parse flags
  1110. std::vector<std::string> positional;
  1111. for (int i = 1; i < argc; ++i) {
  1112. std::string arg = argv[i];
  1113. if (arg == "--address" && i + 1 < argc) {
  1114. args.address = argv[++i];
  1115. } else if (arg == "--project" && i + 1 < argc) {
  1116. args.project = argv[++i];
  1117. } else if (arg == "--help" || arg == "-h") {
  1118. printUsage();
  1119. return 0;
  1120. } else {
  1121. positional.push_back(arg);
  1122. }
  1123. }
  1124. if (!positional.empty()) {
  1125. args.command = positional[0];
  1126. args.params.assign(positional.begin() + 1, positional.end());
  1127. }
  1128. // Offline helpers — these don't need a running server, so dispatch
  1129. // them before opening the gRPC channel.
  1130. if (args.command == "generate-auth-key") {
  1131. return cmdGenerateAuthKey();
  1132. }
  1133. if (args.command == "generate-tls-cert") {
  1134. return cmdGenerateTlsCert(args.params);
  1135. }
  1136. if (args.command == "verify-subdbs" || args.command == "reconcile-subdbs") {
  1137. return cmdSubdbs(args.command, args.params);
  1138. }
  1139. // Connect
  1140. smartbotic::database::Client::Config clientCfg{.address = args.address};
  1141. if (!args.project.empty()) clientCfg.project = args.project;
  1142. smartbotic::database::Client client(clientCfg);
  1143. client.connect();
  1144. // Scriptable mode: single command
  1145. if (!args.command.empty()) {
  1146. return execCommand(client, args.command, args.params) ? 0 : 1;
  1147. }
  1148. // Interactive mode
  1149. std::cout << C_BOLD << "smartbotic-db-cli" << C_RESET
  1150. << " connected to " << C_CYAN << args.address << C_RESET << "\n"
  1151. << C_DIM << "Type 'help' for commands, 'exit' to quit." << C_RESET << "\n";
  1152. std::string line;
  1153. while (true) {
  1154. std::cout << C_YELLOW << "> " << C_RESET;
  1155. if (!std::getline(std::cin, line)) break;
  1156. // Trim
  1157. auto start = line.find_first_not_of(" \t");
  1158. if (start == std::string::npos) continue;
  1159. line = line.substr(start);
  1160. if (line == "exit" || line == "quit" || line == "q") break;
  1161. if (line.empty()) continue;
  1162. auto tokens = tokenize(line);
  1163. if (tokens.empty()) continue;
  1164. auto cmd = tokens[0];
  1165. std::vector<std::string> params(tokens.begin() + 1, tokens.end());
  1166. execCommand(client, cmd, params);
  1167. }
  1168. return 0;
  1169. }