ORE Studio 0.0.4
Loading...
Searching...
No Matches
role_handler.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_IAM_MESSAGING_ROLE_HANDLER_HPP
21#define ORES_IAM_MESSAGING_ROLE_HANDLER_HPP
22
23#include <boost/uuid/string_generator.hpp>
24#include <boost/uuid/uuid_io.hpp>
25#include "ores.logging/make_logger.hpp"
26#include "ores.nats/domain/message.hpp"
27#include "ores.nats/service/client.hpp"
28#include "ores.database/domain/context.hpp"
29#include "ores.database/service/tenant_context.hpp"
30#include "ores.database/repository/bitemporal_operations.hpp"
31#include "ores.security/jwt/jwt_authenticator.hpp"
32#include "ores.service/messaging/handler_helpers.hpp"
33#include "ores.service/service/request_context.hpp"
34#include "ores.iam.api/messaging/authorization_protocol.hpp"
35#include "ores.iam.api/domain/permission.hpp"
36#include "ores.iam.core/repository/account_repository.hpp"
37#include "ores.iam.core/repository/tenant_repository.hpp"
38#include "ores.iam.core/service/authorization_service.hpp"
39
40namespace ores::iam::messaging {
41
42namespace {
43
44inline auto& role_handler_lg() {
45 static auto instance = ores::logging::make_logger(
46 "ores.iam.messaging.role_handler");
47 return instance;
48}
49
50} // namespace
51
52using ores::service::messaging::reply;
53using ores::service::messaging::decode;
54using ores::service::messaging::log_handler_entry;
55using namespace ores::logging;
56using ores::service::messaging::error_reply;
57
58class role_handler {
59public:
60 role_handler(ores::nats::service::client& nats,
63 : nats_(nats), ctx_(std::move(ctx)), signer_(std::move(signer)) {}
64
65 void list(ores::nats::message msg) {
66 [[maybe_unused]] const auto correlation_id =
67 log_handler_entry(role_handler_lg(), msg);
68 try {
69 auto ctx_expected = ores::service::service::make_request_context(
70 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
71 if (!ctx_expected) {
72 error_reply(nats_, msg, ctx_expected.error());
73 return;
74 }
75 const auto& ctx = *ctx_expected;
76 service::authorization_service svc(ctx);
77 auto roles = svc.list_roles();
78 BOOST_LOG_SEV(role_handler_lg(), debug)
79 << "Completed " << msg.subject;
80 reply(nats_, msg,
81 list_roles_response{.roles = std::move(roles)});
82 } catch (const std::exception& e) {
83 BOOST_LOG_SEV(role_handler_lg(), error)
84 << msg.subject << " failed: " << e.what();
85 reply(nats_, msg, list_roles_response{});
86 }
87 }
88
89 void assign(ores::nats::message msg) {
90 [[maybe_unused]] const auto correlation_id =
91 log_handler_entry(role_handler_lg(), msg);
92 auto req = decode<assign_role_request>(msg);
93 if (!req) {
94 BOOST_LOG_SEV(role_handler_lg(), warn)
95 << "Failed to decode: " << msg.subject;
96 return;
97 }
98 try {
99 auto ctx_expected = ores::service::service::make_request_context(
100 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
101 if (!ctx_expected) {
102 error_reply(nats_, msg, ctx_expected.error());
103 return;
104 }
105 const auto& ctx = *ctx_expected;
106 service::authorization_service svc(ctx);
107 boost::uuids::string_generator sg;
108 svc.assign_role(sg(req->account_id),
109 sg(req->role_id), ctx.actor());
110 BOOST_LOG_SEV(role_handler_lg(), debug)
111 << "Completed " << msg.subject;
112 reply(nats_, msg,
113 assign_role_response{.success = true});
114 } catch (const std::exception& e) {
115 BOOST_LOG_SEV(role_handler_lg(), error)
116 << msg.subject << " failed: " << e.what();
117 reply(nats_, msg, assign_role_response{
118 .success = false,
119 .error_message = e.what()});
120 }
121 }
122
123 void revoke(ores::nats::message msg) {
124 [[maybe_unused]] const auto correlation_id =
125 log_handler_entry(role_handler_lg(), msg);
126 auto req = decode<revoke_role_request>(msg);
127 if (!req) {
128 BOOST_LOG_SEV(role_handler_lg(), warn)
129 << "Failed to decode: " << msg.subject;
130 return;
131 }
132 try {
133 auto ctx_expected = ores::service::service::make_request_context(
134 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
135 if (!ctx_expected) {
136 error_reply(nats_, msg, ctx_expected.error());
137 return;
138 }
139 const auto& ctx = *ctx_expected;
140 boost::uuids::string_generator sg;
141 const auto caller_id = sg(ctx.actor());
142 service::authorization_service svc(ctx);
143 if (!svc.has_permission(caller_id,
144 domain::permissions::roles_revoke)) {
145 BOOST_LOG_SEV(role_handler_lg(), warn)
146 << msg.subject
147 << " denied: caller lacks iam::roles:revoke permission";
148 reply(nats_, msg, revoke_role_response{
149 .success = false,
150 .error_message =
151 "Permission denied: iam::roles:revoke required"});
152 return;
153 }
154 svc.revoke_role(sg(req->account_id), sg(req->role_id));
155 BOOST_LOG_SEV(role_handler_lg(), debug)
156 << "Completed " << msg.subject;
157 reply(nats_, msg,
158 revoke_role_response{.success = true});
159 } catch (const std::exception& e) {
160 BOOST_LOG_SEV(role_handler_lg(), error)
161 << msg.subject << " failed: " << e.what();
162 reply(nats_, msg, revoke_role_response{
163 .success = false,
164 .error_message = e.what()});
165 }
166 }
167
168 void by_account(ores::nats::message msg) {
169 [[maybe_unused]] const auto correlation_id =
170 log_handler_entry(role_handler_lg(), msg);
171 auto req = decode<get_account_roles_request>(msg);
172 if (!req) {
173 BOOST_LOG_SEV(role_handler_lg(), warn)
174 << "Failed to decode: " << msg.subject;
175 return;
176 }
177 try {
178 auto ctx_expected = ores::service::service::make_request_context(
179 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
180 if (!ctx_expected) {
181 error_reply(nats_, msg, ctx_expected.error());
182 return;
183 }
184 const auto& ctx = *ctx_expected;
185 service::authorization_service svc(ctx);
186 boost::uuids::string_generator sg;
187 auto roles = svc.get_account_roles(
188 sg(req->account_id));
189 BOOST_LOG_SEV(role_handler_lg(), debug)
190 << "Completed " << msg.subject;
191 reply(nats_, msg,
192 get_account_roles_response{
193 .roles = std::move(roles)});
194 } catch (const std::exception& e) {
195 BOOST_LOG_SEV(role_handler_lg(), error)
196 << msg.subject << " failed: " << e.what();
197 reply(nats_, msg, get_account_roles_response{});
198 }
199 }
200
201 void assign_by_name(ores::nats::message msg) {
202 [[maybe_unused]] const auto correlation_id =
203 log_handler_entry(role_handler_lg(), msg);
204 auto req = decode<assign_role_by_name_request>(msg);
205 if (!req) {
206 BOOST_LOG_SEV(role_handler_lg(), warn)
207 << "Failed to decode: " << msg.subject;
208 return;
209 }
210 try {
211 auto ctx_expected = ores::service::service::make_request_context(
212 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
213 if (!ctx_expected) {
214 error_reply(nats_, msg, ctx_expected.error());
215 return;
216 }
217 const auto& ctx = *ctx_expected;
218
219 // Parse principal: username@hostname
220 const auto at_pos = req->principal.rfind('@');
221 if (at_pos == std::string::npos) {
222 reply(nats_, msg, assign_role_by_name_response{
223 .success = false,
224 .error_message =
225 "Principal must be in username@hostname format"});
226 return;
227 }
228 const auto username = req->principal.substr(0, at_pos);
229 const auto hostname = req->principal.substr(at_pos + 1);
230
231 // Resolve tenant by hostname
232 repository::tenant_repository tenant_repo(ctx_);
233 auto tenants = tenant_repo.read_latest_by_hostname(hostname);
234 if (tenants.empty()) {
235 reply(nats_, msg, assign_role_by_name_response{
236 .success = false,
237 .error_message =
238 "Tenant not found for hostname: " + hostname});
239 return;
240 }
241
243 auto tenant_ctx = tenant_context::with_tenant(
244 ctx_, boost::uuids::to_string(tenants.front().id));
245
246 // Look up account by username in the target tenant
247 repository::account_repository acct_repo(tenant_ctx);
248 auto accounts = acct_repo.read_latest_by_username(username);
249 if (accounts.empty()) {
250 reply(nats_, msg, assign_role_by_name_response{
251 .success = false,
252 .error_message = "Account not found: " + username});
253 return;
254 }
255
256 // Resolve role by name
257 service::authorization_service auth_svc(tenant_ctx);
258 auto role = auth_svc.find_role_by_name(req->role_name);
259 if (!role) {
260 reply(nats_, msg, assign_role_by_name_response{
261 .success = false,
262 .error_message = "Role not found: " + req->role_name});
263 return;
264 }
265
266 auth_svc.assign_role(accounts.front().id, role->id, ctx.actor());
267 BOOST_LOG_SEV(role_handler_lg(), debug)
268 << "Completed " << msg.subject;
269 reply(nats_, msg, assign_role_by_name_response{.success = true});
270 } catch (const std::exception& e) {
271 BOOST_LOG_SEV(role_handler_lg(), error)
272 << msg.subject << " failed: " << e.what();
273 reply(nats_, msg, assign_role_by_name_response{
274 .success = false,
275 .error_message = e.what()});
276 }
277 }
278
279 void revoke_by_name(ores::nats::message msg) {
280 [[maybe_unused]] const auto correlation_id =
281 log_handler_entry(role_handler_lg(), msg);
282 auto req = decode<revoke_role_by_name_request>(msg);
283 if (!req) {
284 BOOST_LOG_SEV(role_handler_lg(), warn)
285 << "Failed to decode: " << msg.subject;
286 return;
287 }
288 try {
289 // Authenticate and check roles:revoke permission
290 auto ctx_expected = ores::service::service::make_request_context(
291 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
292 if (!ctx_expected) {
293 error_reply(nats_, msg, ctx_expected.error());
294 return;
295 }
296 const auto& ctx = *ctx_expected;
297 boost::uuids::string_generator sg;
298 const auto caller_id = sg(ctx.actor());
299 service::authorization_service caller_svc(ctx);
300 if (!caller_svc.has_permission(caller_id,
301 domain::permissions::roles_revoke)) {
302 BOOST_LOG_SEV(role_handler_lg(), warn)
303 << msg.subject
304 << " denied: caller lacks iam::roles:revoke permission";
305 reply(nats_, msg, revoke_role_by_name_response{
306 .success = false,
307 .error_message =
308 "Permission denied: iam::roles:revoke required"});
309 return;
310 }
311
312 // Parse principal: username@hostname
313 const auto at_pos = req->principal.rfind('@');
314 if (at_pos == std::string::npos) {
315 reply(nats_, msg, revoke_role_by_name_response{
316 .success = false,
317 .error_message =
318 "Principal must be in username@hostname format"});
319 return;
320 }
321 const auto username = req->principal.substr(0, at_pos);
322 const auto hostname = req->principal.substr(at_pos + 1);
323
324 // Resolve tenant by hostname
325 repository::tenant_repository tenant_repo(ctx_);
326 auto tenants = tenant_repo.read_latest_by_hostname(hostname);
327 if (tenants.empty()) {
328 reply(nats_, msg, revoke_role_by_name_response{
329 .success = false,
330 .error_message =
331 "Tenant not found for hostname: " + hostname});
332 return;
333 }
334
336 auto tenant_ctx = tenant_context::with_tenant(
337 ctx_, boost::uuids::to_string(tenants.front().id));
338
339 // Look up account by username in the target tenant
340 repository::account_repository acct_repo(tenant_ctx);
341 auto accounts = acct_repo.read_latest_by_username(username);
342 if (accounts.empty()) {
343 reply(nats_, msg, revoke_role_by_name_response{
344 .success = false,
345 .error_message = "Account not found: " + username});
346 return;
347 }
348
349 // Resolve role by name
350 service::authorization_service auth_svc(tenant_ctx);
351 auto role = auth_svc.find_role_by_name(req->role_name);
352 if (!role) {
353 reply(nats_, msg, revoke_role_by_name_response{
354 .success = false,
355 .error_message = "Role not found: " + req->role_name});
356 return;
357 }
358
359 auth_svc.revoke_role(accounts.front().id, role->id);
360 BOOST_LOG_SEV(role_handler_lg(), debug)
361 << "Completed " << msg.subject;
362 reply(nats_, msg, revoke_role_by_name_response{.success = true});
363 } catch (const std::exception& e) {
364 BOOST_LOG_SEV(role_handler_lg(), error)
365 << msg.subject << " failed: " << e.what();
366 reply(nats_, msg, revoke_role_by_name_response{
367 .success = false,
368 .error_message = e.what()});
369 }
370 }
371
372 void suggest_commands(ores::nats::message msg) {
373 [[maybe_unused]] const auto correlation_id =
374 log_handler_entry(role_handler_lg(), msg);
375 auto req = decode<suggest_role_commands_request>(msg);
376 if (!req) {
377 BOOST_LOG_SEV(role_handler_lg(), warn)
378 << "Failed to decode: " << msg.subject;
379 return;
380 }
381 try {
382 using ores::database::repository::execute_parameterized_string_query;
383 std::vector<std::string> results;
384 if (!req->tenant_id.empty()) {
385 results = execute_parameterized_string_query(ctx_,
386 "SELECT command FROM "
387 "ores_iam_generate_role_commands_fn($1, NULL, $2::uuid)",
388 {req->username, req->tenant_id},
389 role_handler_lg(), "Suggest role commands by tenant_id");
390 } else if (!req->hostname.empty()) {
391 results = execute_parameterized_string_query(ctx_,
392 "SELECT command FROM "
393 "ores_iam_generate_role_commands_fn($1, $2)",
394 {req->username, req->hostname},
395 role_handler_lg(), "Suggest role commands by hostname");
396 } else {
397 reply(nats_, msg, suggest_role_commands_response{});
398 return;
399 }
400 BOOST_LOG_SEV(role_handler_lg(), debug)
401 << "Completed " << msg.subject;
402 reply(nats_, msg, suggest_role_commands_response{
403 .commands = std::move(results)});
404 } catch (const std::exception& e) {
405 BOOST_LOG_SEV(role_handler_lg(), error)
406 << msg.subject << " failed: " << e.what();
407 reply(nats_, msg, suggest_role_commands_response{});
408 }
409 }
410
411private:
415};
416
417} // namespace ores::iam::messaging
418#endif
STL namespace.
Implements logging infrastructure for ORE Studio.
Definition boost_severity.hpp:28
Context for the operations on a postgres database.
Definition context.hpp:47
Manages tenant context for multi-tenant database operations.
Definition tenant_context.hpp:37
A received NATS message.
Definition message.hpp:40
std::string subject
The subject the message was published to.
Definition message.hpp:44
NATS client: connection, pub/sub, request/reply, and JetStream.
Definition client.hpp:73
JWT authentication primitive.
Definition jwt_authenticator.hpp:45