oatpp-authkit/include/oatpp-authkit/repo/ScopeGuardRepository.hpp
Uwe Schuster 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

118 lines
4.2 KiB
C++

#ifndef OATPP_AUTHKIT_REPO_SCOPE_GUARD_REPOSITORY_HPP
#define OATPP_AUTHKIT_REPO_SCOPE_GUARD_REPOSITORY_HPP
#include "oatpp-authkit/repo/Repository.hpp"
#include "oatpp-authkit/repo/ActorContext.hpp"
#include "oatpp-authkit/repo/SchemaContract.hpp"
#include "oatpp/core/Types.hpp"
#include <functional>
#include <memory>
#include <stdexcept>
#include <utility>
namespace oatpp_authkit::repo {
/**
* @brief Thrown when the scope guard predicate denies an operation.
*
* Catchers (typically the controller layer) translate this into the
* appropriate HTTP error — 403 Forbidden in fewo-webapp's case. The
* decorator stays library-portable by throwing a plain exception rather
* than coupling to oatpp's `OatppException` hierarchy.
*/
class ScopeDeniedException : public std::runtime_error {
public:
using std::runtime_error::runtime_error;
};
/**
* @brief Decorator that gates every repository operation on a predicate.
*
* Generic — knows nothing about "property" / "tenant" / any consumer-
* specific scope concept. The predicate decides; this class just calls it.
*
* @section semantics Per-method behaviour
*
* - `findByEntityId(id)`: load from inner; if non-null and predicate
* denies, throw `ScopeDeniedException`. (Information-leak vs. clean
* error tradeoff: throwing is the safer default — callers that want to
* silently 404 instead can catch and translate.)
* - `list()`: load from inner; filter out rows the predicate denies.
* - `save(dto)`: predicate evaluated on the incoming dto; deny ⇒ throw.
* - `softDelete(id)`: load from inner; if denied, throw; otherwise delegate.
*
* The actor is provided via a constructor-injected accessor so a single
* `ScopeGuardRepository` instance can serve many requests with different
* actors (typically the accessor reads from the per-request authenticated
* principal — fewo-webapp's `AuthInterceptor` populates one).
*/
template <class TDto>
class ScopeGuardRepository : public Repository<TDto> {
public:
using Predicate = std::function<bool(const ActorContext&, const oatpp::Object<TDto>&)>;
using ActorAccess = std::function<ActorContext()>;
/// Declarative schema contribution (authkit#14, D-replace).
/// ScopeGuard touches no schema — empty contributions exposed so it
/// composes cleanly into `SchemaBuilder<…>` parameter packs.
inline static constexpr DecoratorSchema kSchema = {
"ScopeGuardRepository",
nullptr, 0,
nullptr, 0,
nullptr, 0,
};
ScopeGuardRepository(std::shared_ptr<Repository<TDto>> inner,
Predicate isAllowed,
ActorAccess currentActor)
: m_inner(std::move(inner))
, m_isAllowed(std::move(isAllowed))
, m_currentActor(std::move(currentActor))
{}
oatpp::Object<TDto> findByEntityId(const oatpp::String& entityId) override {
auto row = m_inner->findByEntityId(entityId);
if (!row) return row;
if (!m_isAllowed(m_currentActor(), row)) {
throw ScopeDeniedException("scope guard denied findByEntityId");
}
return row;
}
oatpp::Vector<oatpp::Object<TDto>> list() override {
auto inAll = m_inner->list();
auto out = oatpp::Vector<oatpp::Object<TDto>>::createShared();
const ActorContext actor = m_currentActor();
for (auto& row : *inAll) {
if (m_isAllowed(actor, row)) out->push_back(row);
}
return out;
}
void save(const oatpp::Object<TDto>& dto) override {
if (!m_isAllowed(m_currentActor(), dto)) {
throw ScopeDeniedException("scope guard denied save");
}
m_inner->save(dto);
}
void softDelete(const oatpp::String& entityId) override {
auto row = m_inner->findByEntityId(entityId);
if (!row) return; // Nothing to delete; matches Repository<T>::softDelete being a no-op for unknown ids.
if (!m_isAllowed(m_currentActor(), row)) {
throw ScopeDeniedException("scope guard denied softDelete");
}
m_inner->softDelete(entityId);
}
private:
std::shared_ptr<Repository<TDto>> m_inner;
Predicate m_isAllowed;
ActorAccess m_currentActor;
};
} // namespace oatpp_authkit::repo
#endif