Commit graph

15 commits

Author SHA1 Message Date
52449e4159 #15: RedactedFieldRepository — null credentials on historical rows
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>
2026-05-06 20:52:02 +02:00
9040a9ec48 #14 PR 4: relocate users with temporal shape (Option B)
Lifts the auth-essential users table from fewo-webapp into oatpp-authkit
in temporal form per Option B from the issue body. The previous shape
(id INTEGER autoinc + is_active flag) is replaced with the entity_id +
valid_from/valid_until triple; soft-delete via valid_until = now()
instead of toggling is_active.

New files (all in oatpp-authkit):
- dto/UserDto.hpp — auth-essential columns only: id, entity_id, username,
  password_hash, role, tls_cert_dn, valid_from, valid_until. Registered
  as temporal so TemporalRepository composes cleanly. Application-
  specific columns (email, profile data) belong on a consumer-side DTO
  + parallel SchemaContract that contributes additional columns to the
  same users table.
- db/UserDb.hpp — DbClient with login-path queries (findLiveByUsername,
  findLiveByTlsCertDn) plus generic CRUD. UserSchema declares the
  schema: TEXT id, entity_id, username, password_hash, role, tls_cert_dn,
  with natural-key UNIQUE on (username, valid_until) so no two live rows
  can share a username while historical rows for the same username are
  allowed.
- repo/ConcreteUserRepository.hpp — Repository<UserDto> adapter +
  makeUserRepository factory wrapping in TemporalRepository.
- test/test_user_schema.cpp — verifies SchemaBuilder<UserSchema,
  TemporalRepository<UserDto>>::create produces the expected 5 DDL
  statements; specifically asserts is_active and created_at are NOT
  present in the temporal shape (Option B replacement).

13 of 13 tests pass. Bumped 0.11.0 → 0.12.0.

Per owner directive on authkit#14: password_hash rides the temporal row.
A separate security follow-up issue tracks the redaction policy for
historical password hashes (likely blank the hash but keep the row so
change-history is auditable).

The migration of an existing non-temporal users table to this shape is
documented in db/UserDb.hpp: Atlas-generated migration handles the
structural conversion + backfill (each existing row becomes its own
entity with entity_id = CAST(id AS TEXT)). Sessions/certificates FKs
that referenced users.id (INTEGER) need rewiring to reference
users.entity_id — that's a consumer-side rewire, separate PR.

Closes #14 — the four migration sub-PRs (PR 1 role_templates, PRs 2+3
permissions, PR 4 users) are now landed; the umbrella issue can close.
Follow-ups (security hash redaction, fewo-webapp consumer migration,
Atlas CI integration) get their own issues.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 12:57:59 +02:00
0bb8bef634 #14 PRs 2 & 3: relocate user_property_permissions + user_group_permissions
Lifts both per-property and per-property-set RBAC tables from fewo-webapp
into oatpp-authkit. Combined into one commit because they share a
DbClient and the cross-table effective-permission resolver — the resolver
itself stays in fewo since it joins property_set_members (a fewo-side
concept).

New files (all in oatpp-authkit):
- dto/UserPermissionDto.hpp — UserPropertyPermissionDto +
  UserGroupPermissionDto, both registered as temporal.
  EffectivePermissionDto stays in fewo (it's the result shape of fewo's
  property_set_members JOIN).
- db/UserPermissionDb.hpp — DbClient with CRUD for both tables. Each
  table also has a *Schema struct exposing kSchema for SchemaBuilder
  composition. Natural-key UNIQUE indexes carried explicitly:
  ux_..._user_property_until, ux_..._user_set_until.
- repo/ConcreteUserPermissionRepository.hpp — two concrete repos +
  makeUserPropertyPermissionRepository / makeUserGroupPermissionRepository
  factories that wrap each in TemporalRepository.
- test/test_user_permission_schema.cpp — verifies both schemas compose
  with TemporalRepository to produce the expected 5 DDL statements each
  (entity table + 3 schema indexes + 1 temporal composite index).

12 of 12 tests pass. Bumped 0.10.0 → 0.11.0.

Per-row natural-key UNIQUE prevents duplicate live grants for the same
(user_id, property_id) or (user_id, set_id) pair while still allowing
historical rows for the same key (their valid_until differs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 12:39:52 +02:00
3ccc25f231 #14 PR 1: relocate role_templates module + Atlas migration docs
Lifts role_templates / role_template_fields / user_role_assignments from
fewo-webapp into oatpp-authkit, exposed via the declarative SchemaContract
introduced in PR 0.

New files (all in oatpp-authkit):
- dto/RoleTemplateDto.hpp — RoleTemplateDto, RoleTemplateFieldDto,
  UserRoleAssignmentDto. UserWithPermissionsDto stays in fewo (fewo-
  specific /api/auth/me response shape).
- db/RoleTemplateDb.hpp — DbClient with all queries (CRUD + cascade
  soft-delete + getEffectiveFieldPermissions). RoleTemplateSchema struct
  declares the three tables' columns/indexes/sidecar tables in the new
  declarative form. TemporalRepository overlays valid_until + the
  composite UNIQUE(entity_id, valid_until) index.
- repo/ConcreteRoleTemplateRepository.hpp — Repository<RoleTemplateDto>
  inner adapter; makeRoleTemplateRepository helper composes the stack.
- docs/MIGRATIONS.md — Atlas workflow for consumers (atlasgo.io as the
  diff-driven migration tool; SchemaBuilder produces desired state, Atlas
  generates versioned SQL, SchemaContract::verify asserts at runtime).
- test/test_role_template_schema.cpp — verifies SchemaBuilder<
  RoleTemplateSchema, TemporalRepository<RoleTemplateDto>> emits the
  expected 5 DDL statements (2 sidecars + entity table + 2 indexes) with
  composite-FK + ON UPDATE CASCADE on both sidecars.

11 of 11 tests pass. RoleTemplateDto is registered as temporal via
OATPP_AUTHKIT_REGISTER_TEMPORAL so TemporalRepository compiles cleanly.

Atlas binary integration in CI is documented but not yet wired — owner
deferred to a follow-up after the first concrete consumer migration. The
shipped role_templates stack itself is fully consumable today; fewo-
webapp's switch from local copies to oatpp-authkit-shipped headers is
the natural next PR.

Bumped 0.9.0 → 0.10.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 12:36:18 +02:00
606db5a109 #14 PR 0: replace imperative migration kit with declarative SchemaContract
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>
2026-05-06 12:14:51 +02:00
b5e1ea1894 #12: per-decorator migration kit (Prereq.hpp)
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>
2026-04-29 21:47:03 +02:00
c6a2dba22b #11: AuditLogRepository<T> + IAuditSink — cross-cutting audit decorator
Closes #11

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 15:54:11 +02:00
1baff07b71 #10: TemporalFieldTraits<T> — decouple decorator from canonical column names
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>
2026-04-29 14:23:40 +02:00
55516d4cf1 #9: Optional IQueryable<T> capability + in-house query AST
Closes #9

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:55:29 +02:00
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
a0c61b3d94 #7: Repository<T> interface set + ITemporalEntity + IHistoryRepository<T>
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>
2026-04-27 22:42:47 +02:00
f43f5f0633 #6: route ad-hoc JSON through ObjectMapper (Option A — DI everywhere, all-in-one)
- New dto/InternalDto.hpp with JsonErrorDto, WsEntityEventDto,
  WsPresenceUpdateDto, WsClientMsgDto.

- JsonErrorHandler: now takes a shared ObjectMapper (DI). Body built
  via writeToString on JsonErrorDto. Closes the audit's concrete bug
  where status.description was embedded raw — a Status with a `"`/`\\`
  in the description previously emitted invalid JSON.

- AuthInterceptor: takes an optional ObjectMapper ctor arg (defaults to
  a fresh mapper). makeForbidden's `msg` is now serialised via
  JsonErrorDto + ObjectMapper, so a `"` in a forbidden-reason no longer
  breaks the response envelope.

- Hub: process-wide sharedMapper() with optional setObjectMapper()
  override. buildPresenceMsg / notifyBooking / notifyPerson all go
  through ObjectMapper-emitted DTOs. User-supplied IDs / property IDs
  / usernames containing `"`/`\\`/control chars are now escaped.

- Listener: jsonStr/jsonInt regex parsers gone. handleMessage parses
  inbound frames via ObjectMapper::readFromString into WsClientMsgDto.
  Malformed JSON / nested objects / escaped quotes — previously silent
  corruption — now produce a clean drop of the frame.

- test/test_json_serialization.cpp: 4 cases pinning the round-trip
  behaviour (special chars in usernames, IDs, status.description, and
  malformed-input rejection).

Bump to 0.4.0 — ctor signatures changed (additive defaults, but the
behaviour of the JSON envelopes is now governed by ObjectMapper).

Closes #6

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:56:05 +02:00
0d2312499e #3: SecurityHeadersInterceptor — strict baseline + CspOverride ctor (Option B)
Aligns the default CSP, X-Frame-Options, HSTS and Permissions-Policy with
docs/security-baseline.md:
  - script-src/style-src drop 'unsafe-inline' and the unpkg.com allowance
  - img-src narrows from 'self' data: https: → 'self' data:
  - connect-src narrows from 'self' wss: ws: → 'self'
  - frame-ancestors flips from 'self' → 'none'
  - X-Frame-Options flips from SAMEORIGIN → DENY
  - HSTS keeps max-age=63072000 but drops includeSubDomains by default
    (apex-clobbering hazard noted in audit #1)
  - Permissions-Policy header added with the baseline sensor allowlist

Adds a CspOverride struct + ctor so consumers that genuinely need a
relaxation (Swagger UI subtree, cross-origin connect, …) can flip
individual directives without forking the interceptor. Empty fields
inherit the strict baseline.

Bumps to 0.3.6 (alongside owner's pending #4 + #5 + #6 work).

Closes #3

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:54:58 +02:00
950012d946 #4: BodySizeLimitInterceptor — fail-closed on missing/malformed Content-Length
Body-bearing methods (POST/PUT/PATCH) now reject:
- missing Content-Length → 411
- malformed Content-Length → 400
- non-identity Transfer-Encoding (chunked, etc.) → 411
- declared length > maxBytes → 413 (unchanged)

GET/HEAD/DELETE/OPTIONS/TRACE pass through unchanged. Consumers needing
the legacy fail-open behaviour pass `requireContentLength = false`.

Bump to 0.3.3 (behaviour tightening — consumers on default ctor see new
411/400 responses on requests that previously sailed through).

Closes #4

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:36:50 +02:00
abf6153439 #2: Browser-friendly 401/403 — content-negotiate JSON vs HTML/redirect
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>
2026-04-25 13:23:08 +02:00