Sfoglia il codice sorgente

feat(auth): session store mapping cookie->key + bearer/cookie parsing

Fszontagh 2 mesi fa
parent
commit
d894059530
5 ha cambiato i file con 137 aggiunte e 0 eliminazioni
  1. 1 0
      src/CMakeLists.txt
  2. 57 0
      src/auth.cpp
  3. 47 0
      src/auth.hpp
  4. 4 0
      tests/CMakeLists.txt
  5. 28 0
      tests/test_auth.cpp

+ 1 - 0
src/CMakeLists.txt

@@ -10,6 +10,7 @@ add_library(vectorapi_core STATIC
     settings_store.cpp
     collection_registry.cpp
     embeddings.cpp
+    auth.cpp
 )
 target_include_directories(vectorapi_core PUBLIC
     ${CMAKE_CURRENT_SOURCE_DIR}

+ 57 - 0
src/auth.cpp

@@ -0,0 +1,57 @@
+#include "auth.hpp"
+#include <openssl/rand.h>
+#include <array>
+#include <stdexcept>
+
+namespace svapi {
+
+std::optional<std::string> bearerToken(const std::string& h) {
+    constexpr const char* P = "Bearer "; constexpr size_t L = 7;
+    if (h.size() <= L || h.compare(0, L, P) != 0) return std::nullopt;
+    return h.substr(L);
+}
+
+std::optional<std::string> cookieValue(const std::string& header, const std::string& name) {
+    size_t pos = 0;
+    while (pos < header.size()) {
+        size_t semi = header.find(';', pos);
+        std::string pair = header.substr(pos, semi == std::string::npos ? std::string::npos : semi - pos);
+        size_t s = pair.find_first_not_of(" \t"); if (s != std::string::npos) pair = pair.substr(s);
+        size_t eq = pair.find('=');
+        if (eq != std::string::npos && pair.substr(0, eq) == name) return pair.substr(eq + 1);
+        if (semi == std::string::npos) break;
+        pos = semi + 1;
+    }
+    return std::nullopt;
+}
+
+std::string randomToken() {
+    std::array<unsigned char, 16> buf{};
+    if (RAND_bytes(buf.data(), (int)buf.size()) != 1) throw std::runtime_error("RAND_bytes failed");
+    static const char* hex = "0123456789abcdef"; std::string o; o.reserve(32);
+    for (unsigned char b : buf) { o.push_back(hex[b >> 4]); o.push_back(hex[b & 0xF]); }
+    return o;
+}
+
+SessionStore::SessionStore(std::chrono::minutes ttl) : ttl_(ttl) {}
+
+std::string SessionStore::create(const std::string& apiKeyValue, std::chrono::system_clock::time_point now) {
+    std::string tok = randomToken();
+    std::lock_guard<std::mutex> lk(m_);
+    sessions_[tok] = Entry{apiKeyValue, now + ttl_};
+    return tok;
+}
+
+std::optional<std::string> SessionStore::keyFor(const std::string& token, std::chrono::system_clock::time_point now) {
+    std::lock_guard<std::mutex> lk(m_);
+    auto it = sessions_.find(token);
+    if (it == sessions_.end()) return std::nullopt;
+    if (now >= it->second.expiry) { sessions_.erase(it); return std::nullopt; }
+    return it->second.key;
+}
+
+void SessionStore::remove(const std::string& token) { std::lock_guard<std::mutex> lk(m_); sessions_.erase(token); }
+
+void SessionStore::setTtl(std::chrono::minutes ttl) { std::lock_guard<std::mutex> lk(m_); ttl_ = ttl; }
+
+} // namespace svapi

+ 47 - 0
src/auth.hpp

@@ -0,0 +1,47 @@
+#pragma once
+#include <chrono>
+#include <mutex>
+#include <optional>
+#include <string>
+#include <unordered_map>
+
+namespace svapi {
+
+/// Extract the token from an Authorization: Bearer <token> header.
+std::optional<std::string> bearerToken(const std::string& authHeader);
+
+/// Extract the value of a named cookie from a Cookie: header string.
+std::optional<std::string> cookieValue(const std::string& cookieHeader, const std::string& name);
+
+/// Generate a 32-character hex token (16 random bytes via OpenSSL RAND_bytes).
+std::string randomToken();
+
+/// In-memory session store: token → (apiKeyValue, expiry).
+class SessionStore {
+public:
+    explicit SessionStore(std::chrono::minutes ttl);
+
+    /// Create a new session for the given API key value, returns the token.
+    std::string create(const std::string& apiKeyValue, std::chrono::system_clock::time_point now);
+
+    /// Return the API key value for the token, or nullopt if missing/expired.
+    std::optional<std::string> keyFor(const std::string& token, std::chrono::system_clock::time_point now);
+
+    /// Remove/invalidate a session token.
+    void remove(const std::string& token);
+
+    /// Update the TTL for future sessions.
+    void setTtl(std::chrono::minutes ttl);
+
+private:
+    struct Entry {
+        std::string key;
+        std::chrono::system_clock::time_point expiry;
+    };
+
+    std::unordered_map<std::string, Entry> sessions_;
+    std::chrono::minutes ttl_;
+    std::mutex m_;
+};
+
+} // namespace svapi

+ 4 - 0
tests/CMakeLists.txt

@@ -36,3 +36,7 @@ gtest_discover_tests(test_registry)
 add_executable(test_embeddings test_embeddings.cpp)
 target_link_libraries(test_embeddings PRIVATE vectorapi_core GTest::gtest_main)
 gtest_discover_tests(test_embeddings)
+
+add_executable(test_auth test_auth.cpp)
+target_link_libraries(test_auth PRIVATE vectorapi_core GTest::gtest_main)
+gtest_discover_tests(test_auth)

+ 28 - 0
tests/test_auth.cpp

@@ -0,0 +1,28 @@
+#include <gtest/gtest.h>
+#include "auth.hpp"
+using namespace svapi;
+using clk = std::chrono::system_clock;
+
+TEST(Auth, ParsesBearer) {
+    EXPECT_EQ(bearerToken("Bearer abc").value(), "abc");
+    EXPECT_FALSE(bearerToken("Basic x").has_value());
+}
+TEST(Auth, ParsesCookie) {
+    EXPECT_EQ(cookieValue("a=1; svapi_session=tok; b=2", "svapi_session").value(), "tok");
+    EXPECT_FALSE(cookieValue("a=1", "svapi_session").has_value());
+}
+TEST(Auth, SessionMapsToKey) {
+    SessionStore s(std::chrono::minutes(10));
+    auto now = clk::now();
+    std::string tok = s.create("APIKEY-XYZ", now);
+    ASSERT_TRUE(s.keyFor(tok, now).has_value());
+    EXPECT_EQ(s.keyFor(tok, now).value(), "APIKEY-XYZ");
+    EXPECT_FALSE(s.keyFor("bad", now).has_value());
+    EXPECT_FALSE(s.keyFor(tok, now + std::chrono::minutes(11)).has_value());  // expired
+}
+TEST(Auth, RemoveInvalidates) {
+    SessionStore s(std::chrono::minutes(10));
+    auto now = clk::now(); std::string tok = s.create("K", now);
+    s.remove(tok);
+    EXPECT_FALSE(s.keyFor(tok, now).has_value());
+}