// v2.7.0 — caller identity, resolved PER CALL. // // How a principal is decided, in order: // 1. the request carries a bearer token matching a NAMED key -> that name // 2. the request carries a token matching a BARE key -> `unnamed` // 3. the request carries no usable token -> `anonymous` // // Case 3 is not an error. The default 127.0.0.1 plaintext listener has no auth // and `smartbotic-db-cli` plus operator tooling ride on it, so `anonymous` is a // real, grantable principal: local access is allowed by an explicit policy // rather than by an implicit hole. See // docs/superpowers/specs/2026-08-08-rls-cls-design.md. // // WHY NOT AuthContext // ------------------- // The obvious implementation - have BearerAuthProcessor stamp the name into the // gRPC AuthContext and read it back in the handler - is UNSOUND, and was // actually shipped and caught by // tests/load_test/test_policy_enforcement.sh before it could be enabled. // // AuthContext properties belong to the CONNECTION, not the call. gRPC pools // channels by target address, so several clients using DIFFERENT tokens against // the same address can share one connection and every one of them observes // whichever principal was stamped first. In the reproducer, three clients with // three distinct named keys all resolved as `ops`, so every check that should // have denied passed instead. // // Misattributing one caller's identity to another is worse than having no // identity at all. So identity is resolved from the request's own // `authorization` metadata on every call, and the auth processor no longer // consumes that header - handlers need to see it. #pragma once #include #include #include namespace grpc { class ServerContext; } namespace smartbotic::database::auth { // Reserved principal names. Rejected as key names in config, because a key // called "anonymous" would be indistinguishable from an unauthenticated caller // in a policy. inline constexpr const char* kPrincipalAnonymous = "anonymous"; inline constexpr const char* kPrincipalUnnamed = "unnamed"; inline bool isReservedPrincipal(std::string_view name) { return name == kPrincipalAnonymous || name == kPrincipalUnnamed; } // Maps a bearer token to the principal that owns it. // // Holds the union of every listener's keys, because one DatabaseGrpcImpl is // registered on all listeners and a request does not carry which listener it // arrived on. A token is only accepted at all if some listener's auth processor // accepted it, so the union cannot widen authentication - it only names what // was already authenticated. class PrincipalResolver { public: struct NamedKey { std::string name; std::string key; }; PrincipalResolver() = default; explicit PrincipalResolver(std::vector keys) : keys_(std::move(keys)) {} void setKeys(std::vector keys) { keys_ = std::move(keys); } [[nodiscard]] bool empty() const noexcept { return keys_.empty(); } // Never throws, never returns empty: no identity means `anonymous`, which is // a decision rather than a failure. [[nodiscard]] std::string resolve(const grpc::ServerContext* context) const; private: std::vector keys_; }; } // namespace smartbotic::database::auth