main.cpp 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021
  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 <cerrno>
  18. #include <cstdio>
  19. #include <cstring>
  20. #include <fstream>
  21. #include <iostream>
  22. #include <sstream>
  23. #include <string>
  24. #include <vector>
  25. using json = nlohmann::json;
  26. namespace {
  27. struct Args {
  28. std::string address = "localhost:9004";
  29. std::string command;
  30. std::vector<std::string> params;
  31. };
  32. // ANSI colors
  33. constexpr auto C_RESET = "\033[0m";
  34. constexpr auto C_BOLD = "\033[1m";
  35. constexpr auto C_DIM = "\033[2m";
  36. constexpr auto C_CYAN = "\033[36m";
  37. constexpr auto C_GREEN = "\033[32m";
  38. constexpr auto C_RED = "\033[31m";
  39. constexpr auto C_YELLOW = "\033[33m";
  40. void printJson(const json& j) {
  41. std::cout << j.dump(2) << "\n";
  42. }
  43. void printError(const std::string& msg) {
  44. std::cerr << C_RED << "error: " << C_RESET << msg << "\n";
  45. }
  46. void printUsage() {
  47. std::cout << C_BOLD << "smartbotic-db-cli" << C_RESET << " — admin tool for smartbotic-database\n\n"
  48. << C_BOLD << "Usage:" << C_RESET << "\n"
  49. << " smartbotic-db-cli [--address HOST:PORT] <command> [args...]\n"
  50. << " smartbotic-db-cli [--address HOST:PORT] " << C_DIM << "# interactive mode" << C_RESET << "\n\n"
  51. << C_BOLD << "Commands:" << C_RESET << "\n"
  52. << " " << C_CYAN << "collections" << C_RESET << " List all collections\n"
  53. << " " << C_CYAN << "info" << C_RESET << " <collection> Collection info\n"
  54. << " " << C_CYAN << "find" << C_RESET << " <collection> List documents\n"
  55. << " " << C_CYAN << "get" << C_RESET << " <collection> <id> Get a document\n"
  56. << " " << C_CYAN << "upsert" << C_RESET << " <collection> <id> '<json>' Insert or update\n"
  57. << " " << C_CYAN << "remove" << C_RESET << " <collection> <id> Delete a document\n"
  58. << " " << C_CYAN << "count" << C_RESET << " <collection> Count documents\n"
  59. << " " << C_CYAN << "lock" << C_RESET << " Lock database (read-only)\n"
  60. << " " << C_CYAN << "unlock" << C_RESET << " Unlock database (accept writes)\n"
  61. << " " << C_CYAN << "status" << C_RESET << " Show read-only + recovery status\n"
  62. << C_BOLD << " Access policy (v2.7.0+)" << C_RESET << "\n"
  63. << " " << C_CYAN << "security" << C_RESET << " [project] Show whether a project enforces policy\n"
  64. << " " << C_CYAN << "indexes" << C_RESET << " <collection>"
  65. << " list secondary indexes\n"
  66. << " " << C_CYAN << "index-create" << C_RESET << " <collection> <field>"
  67. << " declare an index and backfill it\n"
  68. << " " << C_CYAN << "index-drop" << C_RESET << " <collection> <field>"
  69. << " remove an index\n"
  70. << " " << C_CYAN << "index-values" << C_RESET << " <coll> <field> [n] [asc|desc]"
  71. << " distinct values + counts\n"
  72. << " " << C_CYAN << "security-set" << C_RESET << " <project> <on|off> [enforce|audit]\n"
  73. << " " << C_CYAN << "policies" << C_RESET << " [project] List principals with a policy\n"
  74. << " " << C_CYAN << "policy" << C_RESET << " <project> <principal> Show one policy\n"
  75. << " " << C_CYAN << "policy-set" << C_RESET << " <project> <principal> '<json>'\n"
  76. << " " << C_CYAN << "policy-rm" << C_RESET << " <project> <principal>\n"
  77. << " " << C_CYAN << "help" << C_RESET << " Show this help\n\n"
  78. << C_BOLD << "Offline helpers" << C_RESET << " " << C_DIM << "(no server connection required)" << C_RESET << ":\n"
  79. << " " << C_CYAN << "generate-auth-key" << C_RESET << " Emit a base64 32-byte API key\n"
  80. << " " << C_CYAN << "generate-tls-cert" << C_RESET << " --bind <addr> Generate a self-signed TLS cert + key\n"
  81. << " " << C_DIM << "[--out-cert PATH] [--out-key PATH] [--days N]" << C_RESET << "\n"
  82. << " " << C_CYAN << "verify-subdbs" << C_RESET << " --env <path> Report documents filed under the wrong collection\n"
  83. << " " << C_DIM << "[--project NAME] read-only; safe against a running server" << C_RESET << "\n"
  84. << " " << C_CYAN << "reconcile-subdbs" << C_RESET << " --env <path> Repair misfiled documents (dry run by default)\n"
  85. << " " << C_DIM << "[--project NAME] [--apply] STOP THE SERVICE and back up before --apply" << C_RESET << "\n\n"
  86. << C_BOLD << "Options:" << C_RESET << "\n"
  87. << " --address HOST:PORT Database address (default: localhost:9004)\n";
  88. }
  89. // ---------------------------------------------------------------------------
  90. // v2.4 Stage F: offline helpers (no server connection required)
  91. // ---------------------------------------------------------------------------
  92. // Base64-encode raw bytes. Uses OpenSSL's EVP_EncodeBlock which emits the
  93. // standard base64 alphabet (no newlines) and pads with '='.
  94. std::string base64Encode(const unsigned char* data, size_t len) {
  95. if (len == 0) return {};
  96. // EVP_EncodeBlock writes ((len + 2) / 3) * 4 bytes + a NUL terminator.
  97. const size_t out_len = 4 * ((len + 2) / 3);
  98. std::string out(out_len, '\0');
  99. int written = EVP_EncodeBlock(
  100. reinterpret_cast<unsigned char*>(out.data()),
  101. data, static_cast<int>(len));
  102. if (written < 0) return {};
  103. out.resize(static_cast<size_t>(written));
  104. return out;
  105. }
  106. // Pull 32 cryptographically random bytes from getentropy() (preferred) or
  107. // /dev/urandom (fallback). Returns true on success.
  108. bool fillRandomBytes(unsigned char* buf, size_t len) {
  109. // getentropy() is limited to 256 bytes per call; our use-case is 32.
  110. if (len <= 256) {
  111. if (getentropy(buf, len) == 0) return true;
  112. }
  113. // Fallback: /dev/urandom.
  114. int fd = ::open("/dev/urandom", O_RDONLY | O_CLOEXEC);
  115. if (fd < 0) return false;
  116. size_t got = 0;
  117. while (got < len) {
  118. ssize_t n = ::read(fd, buf + got, len - got);
  119. if (n <= 0) {
  120. if (errno == EINTR) continue;
  121. ::close(fd);
  122. return false;
  123. }
  124. got += static_cast<size_t>(n);
  125. }
  126. ::close(fd);
  127. return true;
  128. }
  129. int cmdGenerateAuthKey() {
  130. unsigned char key[32];
  131. if (!fillRandomBytes(key, sizeof(key))) {
  132. std::fprintf(stderr, "error: failed to obtain entropy for key\n");
  133. return 1;
  134. }
  135. auto encoded = base64Encode(key, sizeof(key));
  136. if (encoded.empty()) {
  137. std::fprintf(stderr, "error: base64 encoding failed\n");
  138. return 1;
  139. }
  140. std::cout << encoded << "\n";
  141. return 0;
  142. }
  143. namespace {
  144. // Print the topmost OpenSSL error to stderr with a prefix.
  145. void printOpenSslError(const char* prefix) {
  146. unsigned long e = ERR_get_error();
  147. char buf[256] = {0};
  148. if (e != 0) {
  149. ERR_error_string_n(e, buf, sizeof(buf));
  150. std::fprintf(stderr, "error: %s: %s\n", prefix, buf);
  151. } else {
  152. std::fprintf(stderr, "error: %s\n", prefix);
  153. }
  154. }
  155. // Returns true if `s` parses as an IPv4 or IPv6 literal.
  156. bool looksLikeIp(const std::string& s) {
  157. unsigned char buf[16];
  158. if (inet_pton(AF_INET, s.c_str(), buf) == 1) return true;
  159. if (inet_pton(AF_INET6, s.c_str(), buf) == 1) return true;
  160. return false;
  161. }
  162. // Generate an RSA private key as an EVP_PKEY using the modern (3.x) API.
  163. EVP_PKEY* generateRsaKey(int bits) {
  164. EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr);
  165. if (!ctx) return nullptr;
  166. EVP_PKEY* pkey = nullptr;
  167. if (EVP_PKEY_keygen_init(ctx) <= 0) {
  168. EVP_PKEY_CTX_free(ctx);
  169. return nullptr;
  170. }
  171. if (EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, bits) <= 0) {
  172. EVP_PKEY_CTX_free(ctx);
  173. return nullptr;
  174. }
  175. if (EVP_PKEY_keygen(ctx, &pkey) <= 0) {
  176. EVP_PKEY_CTX_free(ctx);
  177. return nullptr;
  178. }
  179. EVP_PKEY_CTX_free(ctx);
  180. return pkey;
  181. }
  182. // Write a string to disk with the given file mode. Truncates existing files.
  183. bool writeFileWithMode(const std::string& path, const std::string& contents, mode_t mode) {
  184. int fd = ::open(path.c_str(),
  185. O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC,
  186. mode);
  187. if (fd < 0) {
  188. std::fprintf(stderr, "error: open %s: %s\n", path.c_str(), std::strerror(errno));
  189. return false;
  190. }
  191. // Re-assert mode in case the file pre-existed with a wider mode and
  192. // O_CREAT was a no-op (open(2) only applies the mode on create).
  193. if (fchmod(fd, mode) != 0) {
  194. std::fprintf(stderr, "error: fchmod %s: %s\n", path.c_str(), std::strerror(errno));
  195. ::close(fd);
  196. return false;
  197. }
  198. size_t off = 0;
  199. while (off < contents.size()) {
  200. ssize_t n = ::write(fd, contents.data() + off, contents.size() - off);
  201. if (n <= 0) {
  202. if (errno == EINTR) continue;
  203. std::fprintf(stderr, "error: write %s: %s\n", path.c_str(), std::strerror(errno));
  204. ::close(fd);
  205. return false;
  206. }
  207. off += static_cast<size_t>(n);
  208. }
  209. if (::close(fd) != 0) {
  210. std::fprintf(stderr, "error: close %s: %s\n", path.c_str(), std::strerror(errno));
  211. return false;
  212. }
  213. return true;
  214. }
  215. // Serialize an X509* to a PEM string.
  216. std::string pemEncodeCert(X509* cert) {
  217. BIO* bio = BIO_new(BIO_s_mem());
  218. if (!bio) return {};
  219. if (PEM_write_bio_X509(bio, cert) != 1) {
  220. BIO_free(bio);
  221. return {};
  222. }
  223. BUF_MEM* mem = nullptr;
  224. BIO_get_mem_ptr(bio, &mem);
  225. std::string out(mem->data, mem->length);
  226. BIO_free(bio);
  227. return out;
  228. }
  229. // Serialize an EVP_PKEY* to an unencrypted PEM string (PKCS#8 format via
  230. // PEM_write_bio_PrivateKey).
  231. std::string pemEncodeKey(EVP_PKEY* key) {
  232. BIO* bio = BIO_new(BIO_s_mem());
  233. if (!bio) return {};
  234. if (PEM_write_bio_PrivateKey(bio, key, nullptr, nullptr, 0, nullptr, nullptr) != 1) {
  235. BIO_free(bio);
  236. return {};
  237. }
  238. BUF_MEM* mem = nullptr;
  239. BIO_get_mem_ptr(bio, &mem);
  240. std::string out(mem->data, mem->length);
  241. BIO_free(bio);
  242. return out;
  243. }
  244. } // anonymous namespace
  245. int cmdGenerateTlsCert(const std::vector<std::string>& params) {
  246. std::string bind;
  247. std::string out_cert = "./server.pem";
  248. std::string out_key = "./server.key";
  249. int days = 3650;
  250. for (size_t i = 0; i < params.size(); ++i) {
  251. const auto& p = params[i];
  252. auto need = [&](const char* flag) -> const std::string* {
  253. if (i + 1 >= params.size()) {
  254. std::fprintf(stderr, "error: %s requires a value\n", flag);
  255. return nullptr;
  256. }
  257. return &params[++i];
  258. };
  259. if (p == "--bind") {
  260. auto* v = need("--bind"); if (!v) return 2; bind = *v;
  261. } else if (p == "--out-cert") {
  262. auto* v = need("--out-cert"); if (!v) return 2; out_cert = *v;
  263. } else if (p == "--out-key") {
  264. auto* v = need("--out-key"); if (!v) return 2; out_key = *v;
  265. } else if (p == "--days") {
  266. auto* v = need("--days"); if (!v) return 2;
  267. try { days = std::stoi(*v); }
  268. catch (...) {
  269. std::fprintf(stderr, "error: --days must be an integer\n");
  270. return 2;
  271. }
  272. if (days <= 0) {
  273. std::fprintf(stderr, "error: --days must be > 0\n");
  274. return 2;
  275. }
  276. } else {
  277. std::fprintf(stderr, "error: unknown argument: %s\n", p.c_str());
  278. return 2;
  279. }
  280. }
  281. if (bind.empty()) {
  282. std::fprintf(stderr,
  283. "usage: generate-tls-cert --bind <addr> "
  284. "[--out-cert PATH] [--out-key PATH] [--days N]\n");
  285. return 2;
  286. }
  287. // 1. RSA 4096-bit key.
  288. EVP_PKEY* pkey = generateRsaKey(4096);
  289. if (!pkey) {
  290. printOpenSslError("RSA key generation failed");
  291. return 1;
  292. }
  293. // 2. X.509 certificate.
  294. X509* cert = X509_new();
  295. if (!cert) {
  296. printOpenSslError("X509_new failed");
  297. EVP_PKEY_free(pkey);
  298. return 1;
  299. }
  300. // Version 3 (the integer field encodes v3 as 2).
  301. if (X509_set_version(cert, 2) != 1) {
  302. printOpenSslError("X509_set_version failed");
  303. X509_free(cert); EVP_PKEY_free(pkey);
  304. return 1;
  305. }
  306. // Random 64-bit serial number.
  307. {
  308. unsigned char serial_bytes[8];
  309. if (RAND_bytes(serial_bytes, sizeof(serial_bytes)) != 1) {
  310. printOpenSslError("RAND_bytes for serial failed");
  311. X509_free(cert); EVP_PKEY_free(pkey);
  312. return 1;
  313. }
  314. // Mask the top bit so the BIGNUM is positive.
  315. serial_bytes[0] &= 0x7F;
  316. BIGNUM* bn = BN_bin2bn(serial_bytes, sizeof(serial_bytes), nullptr);
  317. if (!bn) {
  318. printOpenSslError("BN_bin2bn failed");
  319. X509_free(cert); EVP_PKEY_free(pkey);
  320. return 1;
  321. }
  322. ASN1_INTEGER* ai = BN_to_ASN1_INTEGER(bn, nullptr);
  323. BN_free(bn);
  324. if (!ai) {
  325. printOpenSslError("BN_to_ASN1_INTEGER failed");
  326. X509_free(cert); EVP_PKEY_free(pkey);
  327. return 1;
  328. }
  329. if (X509_set_serialNumber(cert, ai) != 1) {
  330. printOpenSslError("X509_set_serialNumber failed");
  331. ASN1_INTEGER_free(ai);
  332. X509_free(cert); EVP_PKEY_free(pkey);
  333. return 1;
  334. }
  335. ASN1_INTEGER_free(ai);
  336. }
  337. // Validity period.
  338. if (!X509_gmtime_adj(X509_get_notBefore(cert), 0)) {
  339. printOpenSslError("X509_gmtime_adj(notBefore) failed");
  340. X509_free(cert); EVP_PKEY_free(pkey);
  341. return 1;
  342. }
  343. long seconds = static_cast<long>(days) * 24L * 60L * 60L;
  344. if (!X509_gmtime_adj(X509_get_notAfter(cert), seconds)) {
  345. printOpenSslError("X509_gmtime_adj(notAfter) failed");
  346. X509_free(cert); EVP_PKEY_free(pkey);
  347. return 1;
  348. }
  349. // Public key.
  350. if (X509_set_pubkey(cert, pkey) != 1) {
  351. printOpenSslError("X509_set_pubkey failed");
  352. X509_free(cert); EVP_PKEY_free(pkey);
  353. return 1;
  354. }
  355. // Subject + issuer name (self-signed, so identical).
  356. X509_NAME* name = X509_get_subject_name(cert);
  357. if (X509_NAME_add_entry_by_txt(
  358. name, "CN", MBSTRING_UTF8,
  359. reinterpret_cast<const unsigned char*>(bind.c_str()),
  360. -1, -1, 0) != 1) {
  361. printOpenSslError("X509_NAME_add_entry_by_txt(CN) failed");
  362. X509_free(cert); EVP_PKEY_free(pkey);
  363. return 1;
  364. }
  365. if (X509_set_issuer_name(cert, name) != 1) {
  366. printOpenSslError("X509_set_issuer_name failed");
  367. X509_free(cert); EVP_PKEY_free(pkey);
  368. return 1;
  369. }
  370. // SubjectAltName.
  371. {
  372. std::string san = "DNS:localhost,IP:127.0.0.1";
  373. if (bind != "localhost" && bind != "127.0.0.1") {
  374. san += ',';
  375. san += looksLikeIp(bind) ? "IP:" : "DNS:";
  376. san += bind;
  377. }
  378. X509_EXTENSION* ext = X509V3_EXT_conf_nid(
  379. nullptr, nullptr, NID_subject_alt_name, san.c_str());
  380. if (!ext) {
  381. printOpenSslError("X509V3_EXT_conf_nid(SAN) failed");
  382. X509_free(cert); EVP_PKEY_free(pkey);
  383. return 1;
  384. }
  385. if (X509_add_ext(cert, ext, -1) != 1) {
  386. printOpenSslError("X509_add_ext(SAN) failed");
  387. X509_EXTENSION_free(ext);
  388. X509_free(cert); EVP_PKEY_free(pkey);
  389. return 1;
  390. }
  391. X509_EXTENSION_free(ext);
  392. }
  393. // basicConstraints CA:FALSE — a server leaf cert, not a CA.
  394. {
  395. X509_EXTENSION* ext = X509V3_EXT_conf_nid(
  396. nullptr, nullptr, NID_basic_constraints, "critical,CA:FALSE");
  397. if (ext) {
  398. X509_add_ext(cert, ext, -1);
  399. X509_EXTENSION_free(ext);
  400. }
  401. }
  402. // Sign with SHA-256.
  403. if (X509_sign(cert, pkey, EVP_sha256()) == 0) {
  404. printOpenSslError("X509_sign failed");
  405. X509_free(cert); EVP_PKEY_free(pkey);
  406. return 1;
  407. }
  408. // Serialize.
  409. std::string cert_pem = pemEncodeCert(cert);
  410. std::string key_pem = pemEncodeKey(pkey);
  411. X509_free(cert);
  412. EVP_PKEY_free(pkey);
  413. if (cert_pem.empty() || key_pem.empty()) {
  414. std::fprintf(stderr, "error: PEM encoding failed\n");
  415. return 1;
  416. }
  417. // Write key first (0600), then cert (0644). If either fails, the other
  418. // may have been written — that's acceptable; the operator will see the
  419. // error and retry.
  420. if (!writeFileWithMode(out_key, key_pem, 0600)) return 1;
  421. if (!writeFileWithMode(out_cert, cert_pem, 0644)) return 1;
  422. std::cout << "Wrote cert: " << out_cert << "\n"
  423. << "Wrote key: " << out_key << "\n";
  424. return 0;
  425. }
  426. bool execCommand(smartbotic::database::Client& client,
  427. const std::string& cmd, const std::vector<std::string>& params) {
  428. try {
  429. if (cmd == "collections") {
  430. auto collections = client.listCollections();
  431. std::cout << C_BOLD << "Collections:" << C_RESET << "\n";
  432. for (const auto& c : collections) {
  433. auto info = client.getCollectionInfo(c);
  434. int64_t count = 0;
  435. if (info) count = info->documentCount;
  436. std::cout << " " << C_CYAN << c << C_RESET
  437. << C_DIM << " (" << count << " docs)" << C_RESET << "\n";
  438. }
  439. return true;
  440. }
  441. if (cmd == "info") {
  442. if (params.empty()) { printError("usage: info <collection>"); return false; }
  443. auto info = client.getCollectionInfo(params[0]);
  444. if (!info) { printError("collection not found: " + params[0]); return false; }
  445. std::cout << C_BOLD << params[0] << C_RESET << ":\n"
  446. << " documents: " << info->documentCount << "\n"
  447. << " size: " << info->sizeBytes << " bytes\n"
  448. << " encrypted: " << (info->encrypted ? "yes" : "no") << "\n"
  449. << " max_versions: " << info->maxVersions << "\n";
  450. if (info->defaultTtlSeconds > 0)
  451. std::cout << " ttl: " << info->defaultTtlSeconds << "s\n";
  452. return true;
  453. }
  454. if (cmd == "find") {
  455. if (params.empty()) { printError("usage: find <collection> [--limit N] [--exists FIELD]"); return false; }
  456. smartbotic::database::Client::QueryOptions opts;
  457. opts.limit = 100;
  458. for (size_t i = 1; i < params.size(); ++i) {
  459. if (params[i] == "--limit" && i + 1 < params.size()) {
  460. opts.limit = static_cast<uint32_t>(std::stoul(params[++i]));
  461. } else if (params[i] == "--exists" && i + 1 < params.size()) {
  462. opts.filters.emplace_back(params[++i], smartbotic::database::Client::FilterOp::EXISTS, true);
  463. }
  464. }
  465. auto docs = client.find(params[0], opts);
  466. std::cout << C_DIM << "(" << docs.size() << " documents)" << C_RESET << "\n";
  467. for (const auto& doc : docs) {
  468. auto id = doc.value("_id", "");
  469. // Print compact summary line
  470. std::cout << C_GREEN << id << C_RESET;
  471. // Show a few key fields
  472. for (const auto& [k, v] : doc.items()) {
  473. if (k == "_id" || k == "_created_at" || k == "_updated_at") continue;
  474. auto val = v.dump();
  475. if (val.size() > 60) val = val.substr(0, 57) + "...";
  476. std::cout << " " << C_DIM << k << "=" << C_RESET << val;
  477. // Limit to 3 fields per line
  478. static int field_count = 0;
  479. if (++field_count >= 3) { field_count = 0; break; }
  480. }
  481. std::cout << "\n";
  482. }
  483. return true;
  484. }
  485. if (cmd == "get") {
  486. if (params.size() < 2) { printError("usage: get <collection> <id>"); return false; }
  487. auto doc = client.get(params[0], params[1]);
  488. if (!doc) { printError("not found: " + params[0] + "/" + params[1]); return false; }
  489. printJson(*doc);
  490. return true;
  491. }
  492. if (cmd == "upsert") {
  493. if (params.size() < 3) { printError("usage: upsert <collection> <id> '<json>'"); return false; }
  494. auto data = json::parse(params[2], nullptr, false);
  495. if (!data.is_object()) { printError("invalid JSON: " + params[2]); return false; }
  496. client.upsert(params[0], data, params[1]);
  497. std::cout << C_GREEN << "ok" << C_RESET << " " << params[0] << "/" << params[1] << "\n";
  498. return true;
  499. }
  500. if (cmd == "remove" || cmd == "delete") {
  501. if (params.size() < 2) { printError("usage: remove <collection> <id>"); return false; }
  502. client.remove(params[0], params[1]);
  503. std::cout << C_GREEN << "ok" << C_RESET << " removed " << params[0] << "/" << params[1] << "\n";
  504. return true;
  505. }
  506. if (cmd == "count") {
  507. if (params.empty()) { printError("usage: count <collection>"); return false; }
  508. auto info = client.getCollectionInfo(params[0]);
  509. if (!info) { printError("collection not found: " + params[0]); return false; }
  510. std::cout << info->documentCount << "\n";
  511. return true;
  512. }
  513. // ===== v2.9.0 secondary indexes =====
  514. //
  515. // Declaration is explicit because an index costs write throughput and is
  516. // not always a win: on a low-cardinality field the planner will decline to
  517. // use it, since reading the index and then fetching most of the collection
  518. // by id loses to scanning. `indexes` reports distinct values precisely so
  519. // an operator can see that coming.
  520. if (cmd == "indexes") {
  521. if (params.empty()) { printError("usage: indexes <collection>"); return false; }
  522. auto list = client.listIndexes(params[0]);
  523. if (list.empty()) {
  524. std::cout << "no indexes on " << params[0] << "\n";
  525. return true;
  526. }
  527. std::cout << C_BOLD << "field distinct entries"
  528. << C_RESET << "\n";
  529. for (const auto& i : list) {
  530. std::cout << " " << i.field
  531. << std::string(i.field.size() < 29 ? 29 - i.field.size() : 1, ' ')
  532. << i.distinctValues
  533. << std::string(std::to_string(i.distinctValues).size() < 13
  534. ? 13 - std::to_string(i.distinctValues).size()
  535. : 1, ' ')
  536. << i.entries << "\n";
  537. }
  538. return true;
  539. }
  540. if (cmd == "index-values") {
  541. if (params.size() < 2) {
  542. printError("usage: index-values <collection> <field> [limit] [asc|desc]");
  543. return false;
  544. }
  545. const uint32_t limit = params.size() > 2 ? std::stoul(params[2]) : 20;
  546. const bool asc = params.size() > 3 ? (params[3] != "desc") : true;
  547. auto vals = client.indexValues(params[0], params[1], limit, asc);
  548. if (vals.empty()) {
  549. std::cout << "no values (is " << params[1] << " indexed?)\n";
  550. return true;
  551. }
  552. std::cout << C_BOLD << "rows value" << C_RESET << "\n";
  553. for (const auto& v : vals) {
  554. const std::string c = std::to_string(v.count);
  555. std::cout << " " << c
  556. << std::string(c.size() < 9 ? 9 - c.size() : 1, ' ')
  557. << v.value.dump() << "\n";
  558. }
  559. return true;
  560. }
  561. if (cmd == "index-create") {
  562. if (params.size() < 2) {
  563. printError("usage: index-create <collection> <field>");
  564. return false;
  565. }
  566. uint64_t rows = 0;
  567. if (!client.createIndex(params[0], params[1], rows)) {
  568. printError("could not create the index (see the service log)");
  569. return false;
  570. }
  571. std::cout << "indexed " << rows << " existing row(s) on "
  572. << params[0] << "#" << params[1] << "\n";
  573. return true;
  574. }
  575. if (cmd == "index-drop") {
  576. if (params.size() < 2) {
  577. printError("usage: index-drop <collection> <field>");
  578. return false;
  579. }
  580. if (!client.dropIndex(params[0], params[1])) {
  581. printError("could not drop the index (see the service log)");
  582. return false;
  583. }
  584. std::cout << "dropped " << params[0] << "#" << params[1] << "\n";
  585. return true;
  586. }
  587. // ===== v2.7.0 access policy =====
  588. //
  589. // Policy lives in the `_policies` collection and is managed through the
  590. // ordinary document API, which the server intercepts so edits refresh
  591. // its cache and the `__security__` record goes through the lockout
  592. // guards. These commands are ergonomics over that, not a second
  593. // mechanism - which is why there is no policy RPC to keep in step.
  594. if (cmd == "policies") {
  595. const std::string project = params.empty() ? "default" : params[0];
  596. auto rows = client.find("_policies", smartbotic::database::Client::QueryOptions{.limit = 1000});
  597. std::cout << C_BOLD << "policies in project '" << project << "'" << C_RESET << "\n";
  598. size_t shown = 0;
  599. for (const auto& r : rows) {
  600. const std::string id = r.value("_id", "");
  601. if (id.rfind(project + ":", 0) != 0) continue;
  602. const std::string tail = id.substr(project.size() + 1);
  603. if (tail == "__security__") continue;
  604. std::cout << " " << C_CYAN << tail << C_RESET
  605. << (r.value("admin", false) ? " (admin)" : "") << "\n";
  606. ++shown;
  607. }
  608. if (shown == 0) std::cout << C_DIM << " (none)" << C_RESET << "\n";
  609. return true;
  610. }
  611. if (cmd == "policy") {
  612. if (params.size() < 2) {
  613. printError("usage: policy <project> <principal>");
  614. return false;
  615. }
  616. auto doc = client.get("_policies", params[0] + ":" + params[1]);
  617. if (!doc) { printError("no policy for " + params[0] + ":" + params[1]); return false; }
  618. printJson(*doc);
  619. return true;
  620. }
  621. if (cmd == "policy-set") {
  622. if (params.size() < 3) {
  623. printError("usage: policy-set <project> <principal> '<json>'\n"
  624. " e.g. policy-set acme svc "
  625. "'{\"collections\":{\"users\":{\"read\":true,\"mask\":[\"ssn\"]}}}'\n"
  626. " admin: policy-set acme ops '{\"admin\":true}'");
  627. return false;
  628. }
  629. try {
  630. auto body = json::parse(params[2]);
  631. client.upsert("_policies", body, params[0] + ":" + params[1]);
  632. std::cout << C_GREEN << "ok" << C_RESET << " policy set for "
  633. << params[0] << ":" << params[1] << "\n";
  634. return true;
  635. } catch (const std::exception& e) {
  636. printError(e.what());
  637. return false;
  638. }
  639. }
  640. if (cmd == "policy-rm") {
  641. if (params.size() < 2) { printError("usage: policy-rm <project> <principal>"); return false; }
  642. try {
  643. bool ok = client.remove("_policies", params[0] + ":" + params[1]);
  644. std::cout << (ok ? "removed\n" : "not found\n");
  645. return ok;
  646. } catch (const std::exception& e) {
  647. // The server refuses to remove the last admin of a secured
  648. // project - that refusal is the lockout guard, not an error to
  649. // work around.
  650. printError(e.what());
  651. return false;
  652. }
  653. }
  654. if (cmd == "security") {
  655. const std::string project = params.empty() ? "default" : params[0];
  656. auto doc = client.get("_policies", project + ":__security__");
  657. if (!doc) {
  658. std::cout << "project '" << project << "': security "
  659. << C_GREEN << "disabled" << C_RESET
  660. << C_DIM << " (default - all access allowed)" << C_RESET << "\n";
  661. return true;
  662. }
  663. const bool on = doc->value("enabled", false);
  664. const std::string mode = doc->value("mode", "enforce");
  665. std::cout << "project '" << project << "': security "
  666. << (on ? (mode == "audit" ? C_YELLOW : C_RED) : C_GREEN)
  667. << (on ? (mode == "audit" ? "AUDIT" : "ENFORCED") : "disabled")
  668. << C_RESET << "\n";
  669. if (on && mode == "audit") {
  670. std::cout << C_DIM << " audit mode logs what it would deny and "
  671. "allows the request - watch the service log, then "
  672. "switch to enforce." << C_RESET << "\n";
  673. }
  674. return true;
  675. }
  676. if (cmd == "security-set") {
  677. if (params.size() < 2) {
  678. printError("usage: security-set <project> <on|off> [enforce|audit]\n"
  679. " Enabling is REFUSED unless some policy in the project has "
  680. "admin=true.\n"
  681. " Switch a live project on with 'audit' first.");
  682. return false;
  683. }
  684. const bool on = params[1] == "on" || params[1] == "true";
  685. const std::string mode = params.size() > 2 ? params[2] : "enforce";
  686. if (mode != "enforce" && mode != "audit") {
  687. printError("mode must be 'enforce' or 'audit'");
  688. return false;
  689. }
  690. try {
  691. json body;
  692. body["enabled"] = on;
  693. body["mode"] = mode;
  694. client.upsert("_policies", body, params[0] + ":__security__");
  695. std::cout << C_GREEN << "ok" << C_RESET << " project '" << params[0]
  696. << "' security " << (on ? mode : "disabled") << "\n";
  697. return true;
  698. } catch (const std::exception& e) {
  699. printError(e.what());
  700. return false;
  701. }
  702. }
  703. if (cmd == "lock") {
  704. if (client.setReadOnly(true)) {
  705. std::cout << C_GREEN << "ok" << C_RESET << " Database locked (read-only)\n";
  706. return true;
  707. }
  708. printError("failed to lock database");
  709. return false;
  710. }
  711. if (cmd == "unlock") {
  712. if (client.setReadOnly(false)) {
  713. std::cout << C_GREEN << "ok" << C_RESET << " Database unlocked (writes accepted)\n";
  714. return true;
  715. }
  716. printError("failed to unlock database");
  717. return false;
  718. }
  719. if (cmd == "status") {
  720. auto s = client.getReadOnlyStatus();
  721. std::cout << "Read-only: " << (s.readOnly ? (std::string(C_RED) + "YES" + C_RESET) : "no") << "\n";
  722. if (s.readOnly) {
  723. std::cout << "Reason: " << s.reason << "\n";
  724. }
  725. std::cout << "Recovery outcome: " << s.recoveryOutcome << "\n";
  726. if (!s.expectedSnapshot.empty()) {
  727. std::cout << "Expected snapshot: " << s.expectedSnapshot << "\n";
  728. }
  729. if (!s.snapshotUsed.empty()) {
  730. std::cout << "Snapshot used: " << s.snapshotUsed << "\n";
  731. }
  732. if (!s.failureReason.empty()) {
  733. std::cout << "Failure reason: " << s.failureReason << "\n";
  734. }
  735. std::cout << "WAL replayed: " << s.walEntriesReplayed << " entries\n";
  736. std::cout << "Snapshots tried: " << s.snapshotsAttempted << "\n";
  737. return true;
  738. }
  739. if (cmd == "help" || cmd == "?") {
  740. printUsage();
  741. return true;
  742. }
  743. printError("unknown command: " + cmd + " (try 'help')");
  744. return false;
  745. } catch (const std::exception& e) {
  746. printError(e.what());
  747. return false;
  748. }
  749. }
  750. // Tokenize a line, respecting single/double quotes and JSON braces
  751. std::vector<std::string> tokenize(const std::string& line) {
  752. std::vector<std::string> tokens;
  753. std::string current;
  754. int brace_depth = 0;
  755. char in_quote = 0;
  756. for (size_t i = 0; i < line.size(); ++i) {
  757. char c = line[i];
  758. if (in_quote) {
  759. current += c;
  760. if (c == in_quote && (i == 0 || line[i-1] != '\\')) {
  761. in_quote = 0;
  762. // Strip surrounding quotes for simple string tokens
  763. if (brace_depth == 0 && current.size() >= 2
  764. && (current.front() == '\'' || current.front() == '"')
  765. && current.front() == current.back()) {
  766. current = current.substr(1, current.size() - 2);
  767. }
  768. }
  769. continue;
  770. }
  771. if (c == '\'' || c == '"') {
  772. in_quote = c;
  773. current += c;
  774. continue;
  775. }
  776. if (c == '{') { brace_depth++; current += c; continue; }
  777. if (c == '}') {
  778. brace_depth--;
  779. current += c;
  780. if (brace_depth <= 0) {
  781. brace_depth = 0;
  782. tokens.push_back(current);
  783. current.clear();
  784. }
  785. continue;
  786. }
  787. if (brace_depth > 0) { current += c; continue; }
  788. if (c == ' ' || c == '\t') {
  789. if (!current.empty()) {
  790. tokens.push_back(current);
  791. current.clear();
  792. }
  793. continue;
  794. }
  795. current += c;
  796. }
  797. if (!current.empty()) tokens.push_back(current);
  798. return tokens;
  799. }
  800. // v2.4.4 — offline sub-db placement audit / repair.
  801. //
  802. // `verify-subdbs` is read-only and safe against a running server.
  803. // `reconcile-subdbs --apply` rewrites document placement and REQUIRES the
  804. // service to be stopped: it holds an LMDB write txn over the whole env.
  805. int cmdSubdbs(const std::string& command, const std::vector<std::string>& params) {
  806. std::string env_path, project;
  807. bool apply = false;
  808. bool stamp = false;
  809. for (size_t i = 0; i < params.size(); ++i) {
  810. const std::string& p = params[i];
  811. if (p == "--env" && i + 1 < params.size()) {
  812. env_path = params[++i];
  813. } else if (p == "--project" && i + 1 < params.size()) {
  814. project = params[++i];
  815. } else if (p == "--apply") {
  816. apply = true;
  817. } else if (p == "--stamp-identity") {
  818. stamp = true;
  819. } else {
  820. printError("unknown argument: " + p);
  821. return 2;
  822. }
  823. }
  824. if (env_path.empty()) {
  825. printError("--env <path> is required (e.g. "
  826. "/var/lib/smartbotic-database/projects/default/env)");
  827. return 2;
  828. }
  829. if (command == "verify-subdbs" && apply) {
  830. printError("--apply is not valid for verify-subdbs; use reconcile-subdbs");
  831. return 2;
  832. }
  833. try {
  834. auto report = smartbotic::db::storage::audit(env_path, project);
  835. smartbotic::db::storage::print_audit(report);
  836. if (command == "verify-subdbs") {
  837. return report.misplaced.empty() ? 0 : 1;
  838. }
  839. if (!apply) {
  840. std::cout << "\n" << C_YELLOW << "Dry run." << C_RESET
  841. << " Re-run with --apply to perform the repair.\n"
  842. << C_DIM
  843. << "Stop the service first, and take a backup: the repair "
  844. "rewrites document placement in place.\n"
  845. << C_RESET;
  846. return report.misplaced.empty() ? 0 : 1;
  847. }
  848. if (stamp) {
  849. std::cout << "\n" << C_YELLOW << "--stamp-identity:" << C_RESET
  850. << " writing sentinels. This REQUIRES the server binary to be"
  851. " v2.4.4 or newer;\n earlier builds do not skip the sentinel"
  852. " and will fail on scan.\n";
  853. }
  854. std::cout << "\nApplying...\n";
  855. auto res = smartbotic::db::storage::repair(env_path, report, stamp);
  856. std::cout << C_GREEN << "done." << C_RESET
  857. << " moved=" << res.moved
  858. << " quarantined=" << res.quarantined
  859. << " stamped=" << res.stamped << "\n";
  860. for (const auto& e : res.errors) {
  861. std::cerr << C_RED << " warn: " << C_RESET << e << "\n";
  862. }
  863. return res.errors.empty() ? 0 : 1;
  864. } catch (const std::exception& e) {
  865. printError(e.what());
  866. return 1;
  867. }
  868. }
  869. } // anonymous namespace
  870. int main(int argc, char* argv[]) {
  871. Args args;
  872. // Parse flags
  873. std::vector<std::string> positional;
  874. for (int i = 1; i < argc; ++i) {
  875. std::string arg = argv[i];
  876. if (arg == "--address" && i + 1 < argc) {
  877. args.address = argv[++i];
  878. } else if (arg == "--help" || arg == "-h") {
  879. printUsage();
  880. return 0;
  881. } else {
  882. positional.push_back(arg);
  883. }
  884. }
  885. if (!positional.empty()) {
  886. args.command = positional[0];
  887. args.params.assign(positional.begin() + 1, positional.end());
  888. }
  889. // Offline helpers — these don't need a running server, so dispatch
  890. // them before opening the gRPC channel.
  891. if (args.command == "generate-auth-key") {
  892. return cmdGenerateAuthKey();
  893. }
  894. if (args.command == "generate-tls-cert") {
  895. return cmdGenerateTlsCert(args.params);
  896. }
  897. if (args.command == "verify-subdbs" || args.command == "reconcile-subdbs") {
  898. return cmdSubdbs(args.command, args.params);
  899. }
  900. // Connect
  901. smartbotic::database::Client client({.address = args.address});
  902. client.connect();
  903. // Scriptable mode: single command
  904. if (!args.command.empty()) {
  905. return execCommand(client, args.command, args.params) ? 0 : 1;
  906. }
  907. // Interactive mode
  908. std::cout << C_BOLD << "smartbotic-db-cli" << C_RESET
  909. << " connected to " << C_CYAN << args.address << C_RESET << "\n"
  910. << C_DIM << "Type 'help' for commands, 'exit' to quit." << C_RESET << "\n";
  911. std::string line;
  912. while (true) {
  913. std::cout << C_YELLOW << "> " << C_RESET;
  914. if (!std::getline(std::cin, line)) break;
  915. // Trim
  916. auto start = line.find_first_not_of(" \t");
  917. if (start == std::string::npos) continue;
  918. line = line.substr(start);
  919. if (line == "exit" || line == "quit" || line == "q") break;
  920. if (line.empty()) continue;
  921. auto tokens = tokenize(line);
  922. if (tokens.empty()) continue;
  923. auto cmd = tokens[0];
  924. std::vector<std::string> params(tokens.begin() + 1, tokens.end());
  925. execCommand(client, cmd, params);
  926. }
  927. return 0;
  928. }