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

178 lines
6.8 KiB
C++

#ifndef OATPP_AUTHKIT_DB_USER_PERMISSION_DB_HPP
#define OATPP_AUTHKIT_DB_USER_PERMISSION_DB_HPP
// DbClient + declarative schema for user_property_permissions and
// user_group_permissions (authkit#14 PRs 2 & 3).
//
// Cross-table effective-permission queries that join consumer-side
// tables (e.g. fewo's property_set_members) stay in the consumer — only
// the standalone DbClient queries that operate on these two tables move
// here.
#include "oatpp-authkit/dto/UserPermissionDto.hpp"
#include "oatpp-authkit/repo/SchemaContract.hpp"
#include "oatpp-sqlite/orm.hpp"
#include OATPP_CODEGEN_BEGIN(DbClient)
namespace oatpp_authkit::db {
/**
* @brief DbClient for user_property_permissions and user_group_permissions.
*/
class UserPermissionDb : public oatpp::orm::DbClient {
public:
UserPermissionDb(const std::shared_ptr<oatpp::orm::Executor>& executor)
: oatpp::orm::DbClient(executor) {}
// ---- user_property_permissions ----
QUERY(getAllPropertyPermissions,
"SELECT * FROM user_property_permissions "
"WHERE valid_from <= datetime('now') AND valid_until > datetime('now');")
QUERY(getAllPropertyPermissionsRaw,
"SELECT * FROM user_property_permissions;")
QUERY(getPropertyPermissionsForUser,
"SELECT * FROM user_property_permissions "
"WHERE user_id = :userId "
" AND valid_from <= datetime('now') AND valid_until > datetime('now');",
PARAM(oatpp::String, userId))
QUERY(getPropertyPermissionByEntityId,
"SELECT * FROM user_property_permissions "
"WHERE entity_id = :entityId "
" AND valid_from <= datetime('now') AND valid_until > datetime('now');",
PARAM(oatpp::String, entityId))
QUERY(upsertPropertyPermissionById,
"INSERT INTO user_property_permissions "
" (id, entity_id, user_id, property_id, permission, valid_from, valid_until) "
"VALUES "
" (:p.id, :p.entityId, :p.userId, :p.propertyId, :p.permission, "
" :p.validFrom, :p.validUntil) "
"ON CONFLICT(id) DO UPDATE SET "
" entity_id = excluded.entity_id, "
" user_id = excluded.user_id, "
" property_id = excluded.property_id, "
" permission = excluded.permission, "
" valid_from = excluded.valid_from, "
" valid_until = excluded.valid_until;",
PARAM(oatpp::Object<dto::UserPropertyPermissionDto>, p))
QUERY(softDeletePropertyPermission,
"UPDATE user_property_permissions SET valid_until = datetime('now') "
"WHERE entity_id = :entityId AND valid_until > datetime('now');",
PARAM(oatpp::String, entityId))
// ---- user_group_permissions ----
QUERY(getAllGroupPermissions,
"SELECT * FROM user_group_permissions "
"WHERE valid_from <= datetime('now') AND valid_until > datetime('now');")
QUERY(getAllGroupPermissionsRaw,
"SELECT * FROM user_group_permissions;")
QUERY(getGroupPermissionsForUser,
"SELECT * FROM user_group_permissions "
"WHERE user_id = :userId "
" AND valid_from <= datetime('now') AND valid_until > datetime('now');",
PARAM(oatpp::String, userId))
QUERY(getGroupPermissionByEntityId,
"SELECT * FROM user_group_permissions "
"WHERE entity_id = :entityId "
" AND valid_from <= datetime('now') AND valid_until > datetime('now');",
PARAM(oatpp::String, entityId))
QUERY(upsertGroupPermissionById,
"INSERT INTO user_group_permissions "
" (id, entity_id, user_id, set_id, permission, valid_from, valid_until) "
"VALUES "
" (:p.id, :p.entityId, :p.userId, :p.setId, :p.permission, "
" :p.validFrom, :p.validUntil) "
"ON CONFLICT(id) DO UPDATE SET "
" entity_id = excluded.entity_id, "
" user_id = excluded.user_id, "
" set_id = excluded.set_id, "
" permission = excluded.permission, "
" valid_from = excluded.valid_from, "
" valid_until = excluded.valid_until;",
PARAM(oatpp::Object<dto::UserGroupPermissionDto>, p))
QUERY(softDeleteGroupPermission,
"UPDATE user_group_permissions SET valid_until = datetime('now') "
"WHERE entity_id = :entityId AND valid_until > datetime('now');",
PARAM(oatpp::String, entityId))
};
/**
* @brief Declarative schema for `user_property_permissions`.
*
* Composes with `TemporalRepository<UserPropertyPermissionDto>` to produce
* the full table including the temporal `valid_until` + composite UNIQUE
* index. The natural-key UNIQUE `(user_id, property_id, valid_until)` is
* carried as an explicit index here so duplicate live grants for the
* same (user, property) pair are prevented at the DB level.
*/
struct UserPropertyPermissionSchema {
inline static constexpr repo::ColumnSpec kColumns[] = {
{"id", "TEXT PRIMARY KEY"},
{"entity_id", "TEXT NOT NULL"},
{"user_id", "TEXT NOT NULL"},
{"property_id", "TEXT NOT NULL"},
{"permission", "TEXT NOT NULL DEFAULT 'readonly'"},
// valid_from / valid_until come from TemporalRepository.
};
inline static constexpr repo::IndexSpec kIndexes[] = {
{"ix_{table}_entity_id", false, "(entity_id)"},
{"ix_{table}_user_id", false, "(user_id)"},
{"ux_{table}_user_property_until", true,
"(user_id, property_id, valid_until)"},
};
inline static constexpr repo::DecoratorSchema kSchema = {
"UserPropertyPermissionSchema",
kColumns, sizeof(kColumns)/sizeof(kColumns[0]),
kIndexes, sizeof(kIndexes)/sizeof(kIndexes[0]),
nullptr, 0,
};
};
/**
* @brief Declarative schema for `user_group_permissions`.
*
* Mirrors `UserPropertyPermissionSchema` with `set_id` instead of
* `property_id`. The natural-key UNIQUE prevents duplicate live grants
* for the same (user, set) pair.
*/
struct UserGroupPermissionSchema {
inline static constexpr repo::ColumnSpec kColumns[] = {
{"id", "TEXT PRIMARY KEY"},
{"entity_id", "TEXT NOT NULL"},
{"user_id", "TEXT NOT NULL"},
{"set_id", "TEXT NOT NULL"},
{"permission", "TEXT NOT NULL DEFAULT 'readonly'"},
};
inline static constexpr repo::IndexSpec kIndexes[] = {
{"ix_{table}_entity_id", false, "(entity_id)"},
{"ix_{table}_user_id", false, "(user_id)"},
{"ux_{table}_user_set_until", true, "(user_id, set_id, valid_until)"},
};
inline static constexpr repo::DecoratorSchema kSchema = {
"UserGroupPermissionSchema",
kColumns, sizeof(kColumns)/sizeof(kColumns[0]),
kIndexes, sizeof(kIndexes)/sizeof(kIndexes[0]),
nullptr, 0,
};
};
} // namespace oatpp_authkit::db
#include OATPP_CODEGEN_END(DbClient)
#endif