فهرست منبع

v2.4 Stage D: API-key auth metadata processor

Per-listener BearerAuthProcessor implementing grpc::AuthMetadataProcessor.
Pulls `authorization: Bearer <key>` from request metadata, constant-
time compares against the listener's configured `auth.keys[]`, returns
UNAUTHENTICATED on miss. gRPC handles the rejection automatically —
no per-handler check.

Attached via SetAuthMetadataProcessor on the ServerCredentials when
the listener has `auth.required=true`. Refuses to start when keys[]
is empty in that case. Logs a WARN when auth.required but tls.enabled=
false (token sent cleartext).

End-to-end auth verification (client sending token, server accepting/
rejecting) needs the Stage E client work. Stage G integration tests
will close the loop.
fszontagh 1 ماه پیش
والد
کامیت
98c213aa69
4فایلهای تغییر یافته به همراه132 افزوده شده و 0 حذف شده
  1. 1 0
      service/CMakeLists.txt
  2. 68 0
      service/src/auth/auth_interceptor.cpp
  3. 40 0
      service/src/auth/auth_interceptor.hpp
  4. 23 0
      service/src/database_service.cpp

+ 1 - 0
service/CMakeLists.txt

@@ -66,6 +66,7 @@ set(DATABASE_SERVICE_SOURCES
     src/storage/migrate_v1_to_v2.cpp
     src/storage/project_store.cpp
     src/tls/cert_generator.cpp
+    src/auth/auth_interceptor.cpp
 )
 
 # Create executable

+ 68 - 0
service/src/auth/auth_interceptor.cpp

@@ -0,0 +1,68 @@
+// v2.4 — per-listener API-key auth processor implementation.
+
+#include "auth/auth_interceptor.hpp"
+
+#include <cstring>
+#include <string_view>
+
+#include <spdlog/spdlog.h>
+
+namespace smartbotic::database::auth {
+
+namespace {
+
+bool constant_time_eq(std::string_view a, std::string_view b) {
+    unsigned int diff = static_cast<unsigned int>(a.size())
+                      ^ static_cast<unsigned int>(b.size());
+    const size_t n = std::min(a.size(), b.size());
+    for (size_t i = 0; i < n; ++i) {
+        diff |= static_cast<unsigned int>(static_cast<unsigned char>(a[i])
+                                       ^ static_cast<unsigned char>(b[i]));
+    }
+    return diff == 0;
+}
+
+bool starts_with_bearer(std::string_view s) {
+    constexpr std::string_view kPrefix = "Bearer ";
+    return s.size() >= kPrefix.size()
+        && std::memcmp(s.data(), kPrefix.data(), kPrefix.size()) == 0;
+}
+
+}  // namespace
+
+grpc::Status BearerAuthProcessor::Process(
+    const InputMetadata& auth_metadata,
+    grpc::AuthContext* /*context*/,
+    OutputMetadata* consumed_auth_metadata,
+    OutputMetadata* /*response_metadata*/) {
+
+    auto it = auth_metadata.find("authorization");
+    if (it == auth_metadata.end()) {
+        spdlog::warn("auth: rejected — no `authorization` metadata");
+        return grpc::Status(grpc::StatusCode::UNAUTHENTICATED,
+                            "missing authorization metadata");
+    }
+    std::string_view value(it->second.data(), it->second.size());
+    if (!starts_with_bearer(value)) {
+        spdlog::warn("auth: rejected — `authorization` not 'Bearer <key>'");
+        return grpc::Status(grpc::StatusCode::UNAUTHENTICATED,
+                            "authorization must be 'Bearer <key>'");
+    }
+    std::string_view tok = value.substr(7);
+
+    for (const auto& k : keys_) {
+        if (constant_time_eq(tok, k)) {
+            // Mark the metadata as consumed so gRPC doesn't surface it
+            // to handler code.
+            consumed_auth_metadata->insert(
+                std::make_pair(std::string("authorization"),
+                                std::string(value)));
+            return grpc::Status::OK;
+        }
+    }
+    spdlog::warn("auth: rejected — token does not match any configured key");
+    return grpc::Status(grpc::StatusCode::UNAUTHENTICATED,
+                        "invalid auth token");
+}
+
+}  // namespace smartbotic::database::auth

+ 40 - 0
service/src/auth/auth_interceptor.hpp

@@ -0,0 +1,40 @@
+// v2.4 — per-listener API-key auth processor.
+//
+// Pulls `authorization: Bearer <key>` from request metadata,
+// constant-time compares against the listener's configured
+// `auth.keys[]`, returns UNAUTHENTICATED on miss. Plumbed into the
+// listener's ServerCredentials via SetAuthMetadataProcessor() so the
+// gRPC runtime handles rejection without each handler having to
+// check. Listeners with auth.required=false don't register a
+// processor at all.
+
+#pragma once
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <grpcpp/grpcpp.h>
+#include <grpcpp/security/auth_metadata_processor.h>
+
+namespace smartbotic::database::auth {
+
+class BearerAuthProcessor : public grpc::AuthMetadataProcessor {
+public:
+    explicit BearerAuthProcessor(std::vector<std::string> keys)
+        : keys_(std::move(keys)) {}
+
+    grpc::Status Process(const InputMetadata& auth_metadata,
+                          grpc::AuthContext* context,
+                          OutputMetadata* consumed_auth_metadata,
+                          OutputMetadata* response_metadata) override;
+
+    // Called from gRPC worker threads only — return true unless you
+    // need the C++ stub thread (almost never).
+    bool IsBlocking() const override { return false; }
+
+private:
+    std::vector<std::string> keys_;
+};
+
+}  // namespace smartbotic::database::auth

+ 23 - 0
service/src/database_service.cpp

@@ -4,6 +4,7 @@
 #include "storage/document_store.hpp"
 #include "storage/document_store_lmdb.hpp"
 #include "storage/dual_write_mirror.hpp"
+#include "auth/auth_interceptor.hpp"
 #include "tls/cert_generator.hpp"
 
 #include <fstream>
@@ -1180,6 +1181,28 @@ void DatabaseService::startGrpcServer() {
         } else {
             creds = grpc::InsecureServerCredentials();
         }
+
+        // v2.4 Stage D — attach the bearer-token auth processor when
+        // the listener requires it. The processor validates the
+        // `authorization: Bearer <key>` metadata against the listener's
+        // configured keys list. gRPC rejects with UNAUTHENTICATED
+        // (no per-handler code needed).
+        if (listener.auth.required) {
+            if (listener.auth.keys.empty()) {
+                throw std::runtime_error(
+                    "Listener " + addr + ": auth.required=true but auth.keys is empty");
+            }
+            if (!listener.tls.enabled) {
+                spdlog::warn(
+                    "Listener {}: auth.required=true but tls.enabled=false — "
+                    "the bearer token is sent in cleartext over the wire. "
+                    "Consider enabling TLS for any non-loopback listener.",
+                    addr);
+            }
+            creds->SetAuthMetadataProcessor(
+                std::make_shared<smartbotic::database::auth::BearerAuthProcessor>(
+                    listener.auth.keys));
+        }
         builder.AddListeningPort(addr, creds);
 
         builder.RegisterService(storageImpl_.get());