ORE Studio 0.0.4
Loading...
Searching...
No Matches
handler_helpers.hpp
1/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
2 *
3 * Copyright (C) 2026 Marco Craveiro <marco.craveiro@gmail.com>
4 *
5 * This program is free software; you can redistribute it and/or modify it under
6 * the terms of the GNU General Public License as published by the Free Software
7 * Foundation; either version 3 of the License, or (at your option) any later
8 * version.
9 *
10 * This program is distributed in the hope that it will be useful, but WITHOUT
11 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
13 * details.
14 *
15 * You should have received a copy of the GNU General Public License along with
16 * this program; if not, write to the Free Software Foundation, Inc., 51
17 * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 *
19 */
20#ifndef ORES_SERVICE_MESSAGING_HANDLER_HELPERS_HPP
21#define ORES_SERVICE_MESSAGING_HANDLER_HELPERS_HPP
22
23#include <optional>
24#include <span>
25#include <string_view>
26#include <boost/uuid/uuid.hpp>
27#include <rfl/json.hpp>
28#include "ores.nats/domain/message.hpp"
29#include "ores.nats/service/client.hpp"
30#include "ores.database/domain/context.hpp"
31#include "ores.utility/rfl/reflectors.hpp"
32#include "ores.service/error_code.hpp"
33
34namespace ores::service::messaging {
35
39namespace change_reasons {
40 inline constexpr std::string_view new_record = "system.new_record";
41 inline constexpr std::string_view update = "system.update";
42} // namespace change_reasons
43
65template<typename T>
66void stamp(T& obj, const ores::database::context& ctx,
67 std::string_view change_reason = change_reasons::new_record) {
68 // tenant_id is a security boundary — always derived from the validated JWT,
69 // never from client-supplied data.
70 if constexpr (requires { obj.tenant_id; }) {
71 if constexpr (std::is_assignable_v<decltype(obj.tenant_id)&, std::string>)
72 obj.tenant_id = ctx.tenant_id().to_string();
73 else if constexpr (std::is_assignable_v<decltype(obj.tenant_id)&,
74 boost::uuids::uuid>)
75 obj.tenant_id = ctx.tenant_id().to_uuid();
76 else
77 obj.tenant_id = ctx.tenant_id();
78 }
79 const auto& actor = ctx.actor();
80 const auto& svc = ctx.service_account();
81 if constexpr (requires { obj.modified_by; }) {
82 if (!actor.empty())
83 obj.modified_by = actor;
84 else if (!svc.empty())
85 obj.modified_by = svc;
86 }
87 if (!svc.empty()) {
88 if constexpr (requires { obj.performed_by; })
89 obj.performed_by = svc;
90 }
91 if constexpr (requires { obj.change_reason_code; }) {
92 if (obj.change_reason_code.empty())
93 obj.change_reason_code = std::string(change_reason);
94 }
95}
96
97// Serialise resp to JSON and publish to the message's reply subject.
98// No-op if the message has no reply subject.
99template<typename Resp>
100void reply(ores::nats::service::client& nats,
101 const ores::nats::message& msg,
102 const Resp& resp) {
103 if (msg.reply_subject.empty()) return;
104 const auto json = rfl::json::write(resp);
105 const auto* p = reinterpret_cast<const std::byte*>(json.data());
106 nats.publish(msg.reply_subject, std::span<const std::byte>(p, json.size()));
107}
108
128inline bool has_permission(const ores::database::context& ctx,
129 std::string_view required_permission) {
130 const auto& perms = ctx.roles();
131 // Empty list: token predates RBAC — allow to preserve backward compat.
132 if (perms.empty()) return true;
133 // Exact match or component-level wildcard
134 for (const auto& p : perms) {
135 if (p == "*" || p == required_permission) return true;
136 // Component-level wildcard: "iam::*" satisfies "iam::accounts:create"
137 if (p.size() >= 2 && p.ends_with("::*")) {
138 const auto prefix = std::string_view(p).substr(0, p.size() - 1);
139 if (required_permission.starts_with(prefix)) return true;
140 }
141 }
142 return false;
143}
144
145// Publish an error reply with an X-Error header.
146// The reply subject is used if present; no-op otherwise.
147inline void error_reply(ores::nats::service::client& nats,
148 const ores::nats::message& msg,
150 if (msg.reply_subject.empty()) return;
151 std::string_view error_str;
152 switch (code) {
153 case ores::service::error_code::token_expired: error_str = "token_expired"; break;
154 case ores::service::error_code::forbidden: error_str = "forbidden"; break;
155 default: error_str = "unauthorized"; break;
156 }
157 nats.publish(msg.reply_subject, std::span<const std::byte>{},
158 {{"X-Error", std::string(error_str)}});
159}
160
161// Deserialise the message payload from JSON into Req.
162// Returns nullopt on parse failure.
163template<typename Req>
164std::optional<Req> decode(const ores::nats::message& msg) {
165 const std::string_view sv(
166 reinterpret_cast<const char*>(msg.data.data()), msg.data.size());
167 auto r = rfl::json::read<Req>(sv);
168 if (!r) return std::nullopt;
169 return *r;
170}
171
172} // namespace ores::service::messaging
173
174#endif
error_code
Error codes returned by service-layer request helpers.
Definition error_code.hpp:28
@ forbidden
The caller is authenticated but lacks the required permission.
@ token_expired
The JWT token has expired.
Context for the operations on a postgres database.
Definition context.hpp:47
const std::string & service_account() const
Gets the service account for this context.
Definition context.hpp:127
const std::vector< std::string > & roles() const
Gets the permission codes carried in this context.
Definition context.hpp:142
const std::string & actor() const
Gets the current actor (end-user) for this context.
Definition context.hpp:118
const utility::uuid::tenant_id & tenant_id() const
Gets the tenant ID for this context.
Definition context.hpp:94
A received NATS message.
Definition message.hpp:40
std::string reply_subject
The reply-to subject, empty for one-way publishes.
Definition message.hpp:51
std::vector< std::byte > data
The message payload bytes.
Definition message.hpp:58
NATS client: connection, pub/sub, request/reply, and JetStream.
Definition client.hpp:73
void publish(std::string_view subject, std::span< const std::byte > data, std::unordered_map< std::string, std::string > headers={})
Publish a message to a subject.
Definition client.cpp:298
const boost::uuids::uuid & to_uuid() const noexcept
Returns the underlying boost UUID.
Definition tenant_id.cpp:77
std::string to_string() const
Converts the tenant_id to its string representation.
Definition tenant_id.cpp:81