principal.hpp 3.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. // v2.7.0 — caller identity, resolved PER CALL.
  2. //
  3. // How a principal is decided, in order:
  4. // 1. the request carries a bearer token matching a NAMED key -> that name
  5. // 2. the request carries a token matching a BARE key -> `unnamed`
  6. // 3. the request carries no usable token -> `anonymous`
  7. //
  8. // Case 3 is not an error. The default 127.0.0.1 plaintext listener has no auth
  9. // and `smartbotic-db-cli` plus operator tooling ride on it, so `anonymous` is a
  10. // real, grantable principal: local access is allowed by an explicit policy
  11. // rather than by an implicit hole. See
  12. // docs/superpowers/specs/2026-08-08-rls-cls-design.md.
  13. //
  14. // WHY NOT AuthContext
  15. // -------------------
  16. // The obvious implementation - have BearerAuthProcessor stamp the name into the
  17. // gRPC AuthContext and read it back in the handler - is UNSOUND, and was
  18. // actually shipped and caught by
  19. // tests/load_test/test_policy_enforcement.sh before it could be enabled.
  20. //
  21. // AuthContext properties belong to the CONNECTION, not the call. gRPC pools
  22. // channels by target address, so several clients using DIFFERENT tokens against
  23. // the same address can share one connection and every one of them observes
  24. // whichever principal was stamped first. In the reproducer, three clients with
  25. // three distinct named keys all resolved as `ops`, so every check that should
  26. // have denied passed instead.
  27. //
  28. // Misattributing one caller's identity to another is worse than having no
  29. // identity at all. So identity is resolved from the request's own
  30. // `authorization` metadata on every call, and the auth processor no longer
  31. // consumes that header - handlers need to see it.
  32. #pragma once
  33. #include <string>
  34. #include <string_view>
  35. #include <vector>
  36. namespace grpc {
  37. class ServerContext;
  38. }
  39. namespace smartbotic::database::auth {
  40. // Reserved principal names. Rejected as key names in config, because a key
  41. // called "anonymous" would be indistinguishable from an unauthenticated caller
  42. // in a policy.
  43. inline constexpr const char* kPrincipalAnonymous = "anonymous";
  44. inline constexpr const char* kPrincipalUnnamed = "unnamed";
  45. inline bool isReservedPrincipal(std::string_view name) {
  46. return name == kPrincipalAnonymous || name == kPrincipalUnnamed;
  47. }
  48. // Maps a bearer token to the principal that owns it.
  49. //
  50. // Holds the union of every listener's keys, because one DatabaseGrpcImpl is
  51. // registered on all listeners and a request does not carry which listener it
  52. // arrived on. A token is only accepted at all if some listener's auth processor
  53. // accepted it, so the union cannot widen authentication - it only names what
  54. // was already authenticated.
  55. class PrincipalResolver {
  56. public:
  57. struct NamedKey {
  58. std::string name;
  59. std::string key;
  60. };
  61. PrincipalResolver() = default;
  62. explicit PrincipalResolver(std::vector<NamedKey> keys) : keys_(std::move(keys)) {}
  63. void setKeys(std::vector<NamedKey> keys) { keys_ = std::move(keys); }
  64. [[nodiscard]] bool empty() const noexcept { return keys_.empty(); }
  65. // Never throws, never returns empty: no identity means `anonymous`, which is
  66. // a decision rather than a failure.
  67. [[nodiscard]] std::string resolve(const grpc::ServerContext* context) const;
  68. private:
  69. std::vector<NamedKey> keys_;
  70. };
  71. } // namespace smartbotic::database::auth