oatpp-authkit/README.md
Uwe Schuster 08cd32446f #8: TemporalRepository<T> + ScopeGuardRepository<T> decorators
Two cross-cutting decorators that wrap any Repository<TDto> from #7.

TemporalRepository<TDto>:
- Requires TDto : ITemporalEntity (compile-time static_assert).
- save() finds the existing live version, closes its valid_until, and
  inserts a new row at valid_until = '9999-12-31T23:59:59Z' sentinel.
- findByEntityId() returns the live row; findByEntityIdAt(id, at) does
  the [valid_from, valid_until) point-in-time read.
- list() returns live rows only; history(id) returns all versions
  ordered by valid_from. Implements IHistoryRepository<TDto>.
- softDelete closes the live row without inserting a new version.
- Clock and id-generator are constructor-injected (defaults: system_clock
  + 32-char hex from mt19937_64) so the unit tests are deterministic.

The decorator's contract on the inner repository: list() must expose all
rows including historical, and save() must be upsert keyed by
(entity_id, valid_from). Documented on the class.

ScopeGuardRepository<TDto>:
- Generic; knows nothing about "property"/"tenant"/etc. Constructor
  takes a std::function<bool(ActorContext, TDto)> predicate plus a
  std::function<ActorContext()> accessor (so a single instance can
  serve many requests with different actors).
- list() filters; findByEntityId/save/softDelete throw
  ScopeDeniedException on deny.

Tests cover the five acceptance criteria from the issue body:
  - Temporal save closes the prior version
  - Live read returns only the row with valid_until = sentinel
  - Point-in-time read returns the version live at that time
  - History returns all versions in order
  - Scope guard short-circuits when the predicate returns false

ctest: 6/6 green (4 prior + repository_interface + repository_decorators).

Closes #8

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 22:51:39 +02:00

86 lines
4.3 KiB
Markdown

# oatpp-authkit
Header-only C++ library distilled from [fewo-webapp](https://git.uwe-schuster.info/uwe.admin/fewo-webapp)'s
hardened auth / security stack. Header-only, oatpp 1.3+, C++17.
## What's in v0.1 (the clean-lift set)
| Header | Purpose |
|--------|---------|
| `interceptor/SecurityHeadersInterceptor.hpp` | CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy. Strict defaults. |
| `interceptor/BodySizeLimitInterceptor.hpp` | Reject request bodies above a configurable limit with 413 before they hit your handlers. |
| `handler/JsonErrorHandler.hpp` | Normalises thrown exceptions into `{status, message}` JSON so controllers never leak raw HTML error pages. |
| `util/RateLimiter.hpp` | In-memory token-bucket keyed on an arbitrary string (typically the client IP from `clientIpTrusted`). |
| `util/TokenExtract.hpp` | `extractToken` (Cookie/Bearer), `isValidIp` (IPv4/IPv6 via `inet_pton`), `clientIpTrusted` (loopback-gated XFF). |
| `startup/RequireEncryptionKey.hpp` | `requireEncryptionKey(envVarName, encryptionEnabled, allowPlaintext)` — refuse startup without a symmetric key unless a dev flag overrides. |
| `repo/Repository.hpp` + `IHistoryRepository.hpp` + `ITemporalEntity.hpp` + `TemporalAt.hpp` + `ActorContext.hpp` | Pure-abstract `Repository<TDto>` interface set distilled from fewo-webapp's per-entity `*Db` clients. Mixed UUID allocation on `save`, separate `IHistoryRepository<T>` for temporal versions, `ActorContext` placeholder for the upcoming scope-guard decorator. |
| `repo/TemporalRepository.hpp` | Decorator that wraps any `Repository<TDto : ITemporalEntity>` and turns it into a temporally-versioned one. `save` closes the prior live version and inserts a new one; `findByEntityIdAt(id, at)` returns the version live at a point in time; implements `IHistoryRepository<T>`. Inner adapter is expected to expose all rows (live + historical) and treat `save` as upsert keyed by `(entity_id, valid_from)`. |
| `repo/ScopeGuardRepository.hpp` | Generic resource-scope decorator. Takes a `bool(ActorContext, TDto)` predicate at construction; gates every method on it. Throws `ScopeDeniedException` on deny (catchers translate to 403). Knows nothing about consumer-specific concepts like "property" or "tenant" — the predicate decides. |
## Consume via CMake
```cmake
# FetchContent (pin to a tag):
include(FetchContent)
FetchContent_Declare(oatpp-authkit
GIT_REPOSITORY https://git.uwe-schuster.info/uwe.admin/oatpp-authkit.git
GIT_TAG v0.1.0)
FetchContent_MakeAvailable(oatpp-authkit)
target_link_libraries(app PRIVATE oatpp::authkit)
```
Or after `cmake --install`:
```cmake
find_package(oatpp-authkit 0.1 REQUIRED)
target_link_libraries(app PRIVATE oatpp::authkit)
```
## Browser-friendly 401/403
By default `AuthInterceptor` returns `application/json` for every rejection,
which is correct for `/api/*` callers but breaks browser navigation: a user
following a stale link or an expired password-reset URL sees a raw
`{"status":"Unauthorized"}` instead of a real page.
Override `IAuthPolicy::unauthenticatedRedirect(path)` to redirect browser
navigations to a login or landing page while keeping JSON responses for
`fetch`/`axios` callers (detected via path prefix `/api/`,
`X-Requested-With: XMLHttpRequest`, or an `Accept` header that prefers
`application/json`):
```cpp
class AppAuthPolicy : public oatpp_authkit::IAuthPolicy {
public:
std::optional<std::string>
unauthenticatedRedirect(const std::string& path) override {
return "/?next=" + oatpp_authkit::AuthInterceptor::urlEncode(path);
}
};
```
Returning `std::nullopt` (the default) preserves the legacy JSON behaviour
for all responses.
## Tests
```bash
cmake -B build -DOATPP_AUTHKIT_BUILD_TESTS=ON
cmake --build build
ctest --test-dir build --output-on-failure
```
Tests are off by default so consumers pulling the library in via
`FetchContent` don't pay the cost.
## Roadmap
- **v0.2** — `AuthInterceptor` + `requireAdmin` ported onto three seams
(`IAuthBackend`, `IAuthPolicy`, `IRuntimeConfig`) so consumers plug in their
own user store, public-path list, and admin role set without forking the
interceptor.
- **Later** — session cookie helpers, API-key rotation, re-encryption migration.
See `docs/security-baseline.md` for language-neutral CSP / rate-limit / body-size
constants that non-C++ consumers can re-implement directly.