| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283 |
- // 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 <string>
- #include <string_view>
- #include <vector>
- 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<NamedKey> keys) : keys_(std::move(keys)) {}
- void setKeys(std::vector<NamedKey> 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<NamedKey> keys_;
- };
- } // namespace smartbotic::database::auth
|