L-1 RequireRole: guard std::stoi on the bundle id — a non-numeric/out-of-range
value now yields a clean 401 instead of an uncaught exception → 500.
AuthPrincipal::id documented as numeric-only (carry UUIDs in username).
L-2 SmtpTransport: require TLS (CURLUSESSL_ALL) for non-loopback relays so a
stripped STARTTLS can't downgrade credentials/body to cleartext; localhost
relay stays opportunistic.
L-3 AuditLog: escapeJson now escapes all control chars (RFC 8259) so a newline
in a field can't forge/corrupt the audit JSON; SKIP_FIELDS gains credential
names (password/passwordHash/tlsCertDn/apiKey/token/secret) so secrets never
land in changed_fields.
L-4 ws/Hub: consume the thread_local auth handoff once, up front, and clear it
unconditionally — a stale value can't attach to a later connection on a
reused worker thread.
L-5 TemporalRepository: default id generator draws 128 bits from the platform
CSPRNG (std::random_device) per call instead of a once-seeded mt19937_64,
so entity_ids aren't predictable from observed output.
L-6 AuthInterceptor: expired-session sweep is now a lock-free atomic timer and
exception-isolated; documented that resolveBySessionHash() must enforce
expiry at query time (the sweep is GC only).
L-7 new util/ConstantTime.hpp (constantTimeEquals) + TokenHasher doc requiring a
>=256-bit cryptographic hash.
L-8 IQueryable: likeEscape + Field::likeContains/likePrefix emit
`LIKE ? ESCAPE '\'` with %/_/\ escaped for untrusted terms; documented the
compile-time identifier-source invariant.
Tests: new test_constant_time; likeEscape/likeContains/likePrefix cases added to
test_queryable. All 20 ctest targets pass. README + header docs updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
M-1 TokenExtract: exact-name cookie parse (new pure cookieValue helper) —
a substring find("session=") could be shadowed by a sibling xsession=,
defeating __Host-/__Secure- prefix guarantees.
M-2 AuthInterceptor: gate setup-mode pseudo-admin on a loopback bind and log
the grant; document that IAuthBackend::hasActiveUsers() must fail closed.
M-3 ws/Hub: empty propertyIds now means NO access for non-admins (was "all") —
a non-admin whose scope set failed to populate no longer gets every
property's notifications. Admins still get all via role.
M-4 new util/OriginCheck.hpp (originHostname/sameOrigin/originAllowed) +
Hub doc: WSController must validate Origin at the handshake (CSWSH).
M-6 RedactedFieldRepository: ctor throws on an unknown redaction field name
(a typo would silently redact nothing, leaving credentials in history).
M-7 RateLimiter: ctor validates capacity (finite >=1) / refillRate (finite >0),
throws std::invalid_argument — zero/negative/NaN silently disabled it.
M-8 TokenExtract: document that clientIpTrusted's "unknown"/"invalid" sentinels
collapse to one shared rate-limit bucket off-proxy.
M-9 new util/SessionCookie.hpp: safe-by-default Set-Cookie builder
(HttpOnly+Secure+SameSite=Strict+Path=/), rejects control chars / ';'.
M-10 AuthInterceptor: Origin/Referer-vs-Host check on session mutations
(defence in depth atop X-Requested-With); cert path documented as
non-browser / not CSRF-gated.
M-11 AuthInterceptor: optional injected RateLimiter throttles invalid-token
attempts per client IP → 429.
M-12 AuthInterceptor: sanitize request method/path (strip control chars, cap
length) before logging — closes log-line forging (CWE-117).
(M-5 — temporal non-atomic save — was already resolved by the H-4 fix.)
Tests: new test_token_extract / test_rate_limiter / test_origin_check /
test_session_cookie; extended test_redacted_field_repository. All 19 ctest
targets pass. README + header docs updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- H-1 cert-DN spoofing: IRuntimeConfig::certAuthTrusted() now defaults to
false (fail-closed). X-SSL-Client-DN is an ordinary request header; a
loopback bind does not prove it came from a TLS-terminating proxy.
Consumers must opt in explicitly behind a header-stripping proxy.
- H-3 scope reparenting: ScopeGuardRepository::save() now also checks the
EXISTING row's scope (via a new required entity-id accessor), so an actor
can't claim an out-of-scope row by relabelling it in the request body.
- H-2 IQueryable bypass: add ScopeGuardQueryable<T> — filters query()
results through the same predicate so the queryable surface can't escape
the scope guard.
- H-4 TemporalRepository TOCTOU: serialise the read-modify-write with a
per-instance mutex (no more duplicate-live / lost-update under concurrent
same-entity saves) and add an optional TxRunner so the close-then-insert
pair can commit/rollback atomically.
- H-5 SMTP header injection: reject CR/LF/NUL in `to`/`fromAddress` before
building the envelope and From:/To: header lines.
Tests: expand test_repository_decorators (reparenting + queryable filtering),
add curl-guarded test_smtp_transport (base64 vectors + CRLF guard). All 15
ctest targets pass. README updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a decorator that sits below TemporalRepository and redacts
configured fields whenever it sees a save with valid_until != SENTINEL
(i.e., a historical row being closed by the temporal close-then-update
flow). The live row keeps its values intact.
Per Option B from the issue thread: by default the user-repo factory
redacts both passwordHash and tlsCertDn. Empty redaction list passes
everything through unchanged, so non-user temporal stacks compose the
decorator without surprise behaviour.
Files:
- repo/RedactedFieldRepository.hpp — new decorator. Schema contribution
is empty (purely a save-time transform). Field-name matching uses
oatpp's reflective property dispatcher and matches against the C++
identifier name (first DTO_FIELD argument).
- repo/ConcreteUserRepository.hpp — makeUserRepository now wraps the
concrete repo in RedactedFieldRepository<UserDto>{"passwordHash",
"tlsCertDn"} before passing to TemporalRepository. Optional second
argument lets consumers override the redaction list.
- test/test_redacted_field_repository.cpp — five tests cover live-row
pass-through, historical-row redaction (both fields), partial
redaction list, empty list, and null-valid_until treated as live.
- README.md — adds RedactedFieldRepository to the header inventory.
14 of 14 tests pass. Bumped 0.12.0 → 0.13.0.
Closes#15
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
D-replace per #14: rip out PREREQ + RESHAPE_STEPS + applyDecoratorMigrations
and replace with declarative DecoratorSchema (entity columns + indexes +
sidecar tables). SchemaBuilder<Decorators...>::create composes the stack
into a single CREATE TABLE per entity table; SchemaContract::verify
introspects-and-asserts at runtime so code can never run against an
under-migrated DB. Atlas (atlasgo.io) becomes the authority for schema
evolution between deploys — decorator code never runs ALTER at runtime.
- TemporalRepository contributes valid_from/valid_until + UNIQUE composite index
- AuditLogRepository contributes the audit_log sidecar table
- ScopeGuardRepository declares empty contributions for clean stacking
- 8 new tests in test_schema_contract.cpp covering compose / dedup / verify
- README updated; bumped 0.8.0 → 0.9.0
fewo-webapp does not yet call applyDecoratorMigrations, so this is a
clean cut — no consumer-side breakage. PRs 1-4 (role_templates,
user_property_permissions, user_group_permissions, users) follow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The decorator's save() flow now preserves the live row's id PK across
updates and captures each prior version as a fresh row with a new id.
This unblocks fewo-webapp#459: the consumer's composite-FK schema needs
stable child references to the live row (UNIQUE(entity_id, valid_until)
with ON UPDATE CASCADE on every child FK), which the previous
close-then-insert flow couldn't provide.
New flow on update (when a live row exists for entity_id):
1. Clone the live row in memory (cloneDto via oatpp reflection),
assign a fresh id and set valid_until=now, save → INSERT historical.
2. Set the new dto's id=live.id (preserve PK), valid_from=now,
valid_until=SENTINEL, save → inner UPDATEs the live row in place by
PK.
Inner adapter contract changes from "upsert keyed by (entity_id,
valid_from)" to "upsert keyed by id (per-row PK)". TemporalFieldTraits
gains an id() accessor; OATPP_AUTHKIT_REGISTER_TEMPORAL grows from 4 to
5 args (Dto + IdMember + EntityIdMember + FromMember + UntilMember).
Tests: test_repository_decorators asserts livePk stability across saves
and fresh historicalPk per version; remaining decorator tests updated to
the 5-arg macro form. README's TemporalRepository.hpp row rewritten to
describe the new write semantics.
Bumped CMake version 0.7.0 → 0.8.0 (semantic break — save() no longer
reallocates the live PK; consumers depending on the old contract need
audit).
Closes#13
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each decorator now bundles its schema prereqs alongside its code via
DecoratorPrereq (additive CREATE-IF-NOT-EXISTS) and ReshapeStep
(non-idempotent reshape gated on a detectSql probe).
applyDecoratorMigrations<Decorators...>(table, probe, exec, recorder)
walks the listed decorators at startup, runs every PREREQ, runs every
reshape step whose probe returns false. Database-agnostic — consumer
wires probe/exec to their DbClient. SCHEMA_MIGRATIONS_TABLE_SQL is
provided for observability; the detect-probe is the source of truth.
TemporalRepository ships add_valid_from / add_valid_until /
drop_unique_entity_id / composite_unique (UNIQUE(entity_id, valid_until)
so close-then-insert can run in a deferred-FK transaction).
AuditLogRepository ships the audit_log CREATE TABLE.
ScopeGuardRepository ships nothing — exposes empty PREREQ + zero-length
RESHAPE_STEPS so it can be listed in applyDecoratorMigrations alongside
the schema-touching decorators without SFINAE.
Closes#12
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace hard-coded dto->entity_id/valid_from/valid_until accesses in
TemporalRepository with trait calls (F::entityId/validFrom/validUntil).
DTOs register canonical→actual member name mapping via
OATPP_AUTHKIT_REGISTER_TEMPORAL. Forgetting to register is a hard
compile error. ITemporalEntity marker is gone; the trait specialisation
carries the contract. Bumps version 0.4.0 → 0.5.0.
New test verifies the full save/close/history/softDelete flow against a
DTO whose columns are id/effective_from/effective_until rather than the
canonical names — exercises the renaming the trait enables.
Closes#10
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Header-only foundation for the structural refactor that moves fewo-webapp
from per-entity *Db clients to a shared Repository<TDto> abstraction. This
ships interfaces only — no concrete implementations, no callers updated.
Decisions baked in (all settled in the issue body):
- Mixed entity_id allocation: caller may supply, otherwise the concrete
repo generates a UUID inside save().
- UnitOfWork / cross-repo transactions: explicitly out of scope.
- Repository<T> is a virtual-method interface, not a C++20 concept.
- History queries live on a separate IHistoryRepository<T> so non-temporal
repos don't have to implement a stub.
Decorators (TemporalRepository<T>, ScopeGuardRepository<T>) follow in #8;
the optional IQueryable<T> capability for typed filtering follows in #9.
The fewo-webapp Person pilot (uwe.admin/fewo-webapp#457) and the wider
26-entity rollout (uwe.admin/fewo-webapp#458) build on this.
Closes#7
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AuthInterceptor previously returned application/json for every rejection,
which is wrong for browser navigation: the user followed a /set-password
link and saw a raw {"status":"Unauthorized"} blob.
Add wantsJson() negotiation (path /api/* OR X-Requested-With OR Accept
prefers application/json over text/html) and an IAuthPolicy hook
unauthenticatedRedirect(path) that lets consumers bounce browser
navigations to a landing/login page. JSON callers (fetch/axios) still
get JSON 401/403. Default policy returns nullopt → minimal HTML error
page, never raw JSON to a browser.
Same hook covers both 401 and 403 (decision Option A on the issue) so
consumers wire one redirect target for both unauth and forbidden cases.
Bootstrap a minimal test harness (decision Option T2): CMake option
OATPP_AUTHKIT_BUILD_TESTS gates enable_testing() + a tests subdir.
Adds test_negotiation covering wantsJson + urlEncode. No third-party
test framework — assertions use <cassert> + a tiny REQUIRE macro so the
suite stays dependency-free for future tests.
Closes#2
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>