|
@@ -2,10 +2,99 @@
|
|
|
#include <openssl/rand.h>
|
|
#include <openssl/rand.h>
|
|
|
#include <algorithm>
|
|
#include <algorithm>
|
|
|
#include <array>
|
|
#include <array>
|
|
|
|
|
+#include <optional>
|
|
|
#include <stdexcept>
|
|
#include <stdexcept>
|
|
|
|
|
|
|
|
namespace svapi {
|
|
namespace svapi {
|
|
|
|
|
|
|
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
|
|
+// KeyOp helpers
|
|
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+std::optional<KeyOp> keyOpFromString(const std::string& s) {
|
|
|
|
|
+ if (s == "read") return KeyOp::Read;
|
|
|
|
|
+ if (s == "list") return KeyOp::List;
|
|
|
|
|
+ if (s == "search") return KeyOp::Search;
|
|
|
|
|
+ if (s == "insert") return KeyOp::Insert;
|
|
|
|
|
+ if (s == "update") return KeyOp::Update;
|
|
|
|
|
+ if (s == "delete") return KeyOp::Delete;
|
|
|
|
|
+ return std::nullopt;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+std::string keyOpToString(KeyOp op) {
|
|
|
|
|
+ switch (op) {
|
|
|
|
|
+ case KeyOp::Read: return "read";
|
|
|
|
|
+ case KeyOp::List: return "list";
|
|
|
|
|
+ case KeyOp::Search: return "search";
|
|
|
|
|
+ case KeyOp::Insert: return "insert";
|
|
|
|
|
+ case KeyOp::Update: return "update";
|
|
|
|
|
+ case KeyOp::Delete: return "delete";
|
|
|
|
|
+ }
|
|
|
|
|
+ return "";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
|
|
+// KeyScope helpers
|
|
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+static bool collMatches(const std::string& pat, const std::string& coll) {
|
|
|
|
|
+ if (!pat.empty() && pat.back() == '*')
|
|
|
|
|
+ return coll.compare(0, pat.size() - 1, pat, 0, pat.size() - 1) == 0;
|
|
|
|
|
+ return pat == coll;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const KeyScopeRule* KeyScope::findRule(const std::string& collection, KeyOp op) const {
|
|
|
|
|
+ for (const auto& r : rules)
|
|
|
|
|
+ if (collMatches(r.collection, collection) && r.ops.count(op)) return &r;
|
|
|
|
|
+ return nullptr;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+bool KeyScope::originAllowed(const std::string& origin) const {
|
|
|
|
|
+ if (origins.empty()) return true;
|
|
|
|
|
+ for (const auto& o : origins) if (o == origin) return true;
|
|
|
|
|
+ return false;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+bool KeyScope::expired(uint64_t nowEpochSec) const {
|
|
|
|
|
+ return expiresAt != 0 && nowEpochSec > expiresAt;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+KeyScope KeyScope::fromJson(const nlohmann::json& j) {
|
|
|
|
|
+ KeyScope s;
|
|
|
|
|
+ if (j.contains("rules") && j["rules"].is_array()) {
|
|
|
|
|
+ for (const auto& rj : j["rules"]) {
|
|
|
|
|
+ KeyScopeRule r;
|
|
|
|
|
+ r.collection = rj.value("collection", "");
|
|
|
|
|
+ r.requireHumanToken = rj.value("require_human_token", false);
|
|
|
|
|
+ if (rj.contains("ops") && rj["ops"].is_array())
|
|
|
|
|
+ for (const auto& oj : rj["ops"])
|
|
|
|
|
+ if (auto op = keyOpFromString(oj.get<std::string>())) r.ops.insert(*op);
|
|
|
|
|
+ s.rules.push_back(std::move(r));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (j.contains("origins") && j["origins"].is_array())
|
|
|
|
|
+ s.origins = j["origins"].get<std::vector<std::string>>();
|
|
|
|
|
+ s.rateLimitPerMin = j.value("rate_limit_per_min", 0u);
|
|
|
|
|
+ s.expiresAt = j.value("expires_at", 0ull);
|
|
|
|
|
+ return s;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+nlohmann::json KeyScope::toJson() const {
|
|
|
|
|
+ nlohmann::json rules_j = nlohmann::json::array();
|
|
|
|
|
+ for (const auto& r : rules) {
|
|
|
|
|
+ nlohmann::json ops_j = nlohmann::json::array();
|
|
|
|
|
+ for (KeyOp op : r.ops) ops_j.push_back(keyOpToString(op));
|
|
|
|
|
+ rules_j.push_back({{"collection", r.collection}, {"ops", ops_j},
|
|
|
|
|
+ {"require_human_token", r.requireHumanToken}});
|
|
|
|
|
+ }
|
|
|
|
|
+ return {{"rules", rules_j}, {"origins", origins},
|
|
|
|
|
+ {"rate_limit_per_min", rateLimitPerMin}, {"expires_at", expiresAt}};
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
|
|
+// ApiKey
|
|
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
ApiKey ApiKey::fromJson(const nlohmann::json& j) {
|
|
ApiKey ApiKey::fromJson(const nlohmann::json& j) {
|
|
|
ApiKey k;
|
|
ApiKey k;
|
|
|
// "key_id" is the stable non-secret id stored in the document body.
|
|
// "key_id" is the stable non-secret id stored in the document body.
|
|
@@ -17,20 +106,25 @@ ApiKey ApiKey::fromJson(const nlohmann::json& j) {
|
|
|
k.projects = j["projects"].get<std::vector<std::string>>();
|
|
k.projects = j["projects"].get<std::vector<std::string>>();
|
|
|
k.admin = j.value("admin", false);
|
|
k.admin = j.value("admin", false);
|
|
|
k.createdAt = j.value("created_at", 0ull);
|
|
k.createdAt = j.value("created_at", 0ull);
|
|
|
|
|
+ if (j.contains("scope") && j["scope"].is_object()) k.scope = KeyScope::fromJson(j["scope"]);
|
|
|
return k;
|
|
return k;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
nlohmann::json ApiKey::toJson() const {
|
|
nlohmann::json ApiKey::toJson() const {
|
|
|
// "key_id" stores the stable non-secret id in the body.
|
|
// "key_id" stores the stable non-secret id in the body.
|
|
|
// We avoid using "id" as the DB treats that as a reserved/internal field.
|
|
// We avoid using "id" as the DB treats that as a reserved/internal field.
|
|
|
- return {{"key_id", id}, {"key", key}, {"label", label}, {"projects", projects},
|
|
|
|
|
- {"admin", admin}, {"created_at", createdAt}};
|
|
|
|
|
|
|
+ nlohmann::json j = {{"key_id", id}, {"key", key}, {"label", label}, {"projects", projects},
|
|
|
|
|
+ {"admin", admin}, {"created_at", createdAt}};
|
|
|
|
|
+ if (scope) j["scope"] = scope->toJson();
|
|
|
|
|
+ return j;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
nlohmann::json ApiKey::toPublicJson() const {
|
|
nlohmann::json ApiKey::toPublicJson() const {
|
|
|
- return {{"id", id}, {"label", label}, {"projects", projects}, {"admin", admin},
|
|
|
|
|
- {"created_at", createdAt},
|
|
|
|
|
- {"key_prefix", key.substr(0, std::min<size_t>(8, key.size()))}};
|
|
|
|
|
|
|
+ nlohmann::json j = {{"id", id}, {"label", label}, {"projects", projects}, {"admin", admin},
|
|
|
|
|
+ {"created_at", createdAt},
|
|
|
|
|
+ {"key_prefix", key.substr(0, std::min<size_t>(8, key.size()))}};
|
|
|
|
|
+ if (scope) j["scope"] = scope->toJson();
|
|
|
|
|
+ return j;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
bool ApiKey::canAccess(const std::string& project) const {
|
|
bool ApiKey::canAccess(const std::string& project) const {
|