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>
118 lines
4.2 KiB
C++
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
|