main.cpp 38 KB

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