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::error_reply;
55
56class role_handler {
57public:
58 role_handler(ores::nats::service::client& nats,
61 : nats_(nats), ctx_(std::move(ctx)), signer_(std::move(signer)) {}
62
63 void list(ores::nats::message msg) {
64 using namespace ores::logging;
65 BOOST_LOG_SEV(role_handler_lg(), debug)
66 << "Handling " << msg.subject;
67 try {
68 auto ctx_expected = ores::service::service::make_request_context(
69 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
70 if (!ctx_expected) {
71 error_reply(nats_, msg, ctx_expected.error());
72 return;
73 }
74 const auto& ctx = *ctx_expected;
75 service::authorization_service svc(ctx);
76 auto roles = svc.list_roles();
77 BOOST_LOG_SEV(role_handler_lg(), debug)
78 << "Completed " << msg.subject;
79 reply(nats_, msg,
80 list_roles_response{.roles = std::move(roles)});
81 } catch (const std::exception& e) {
82 BOOST_LOG_SEV(role_handler_lg(), error)
83 << msg.subject << " failed: " << e.what();
84 reply(nats_, msg, list_roles_response{});
85 }
86 }
87
88 void assign(ores::nats::message msg) {
89 using namespace ores::logging;
90 BOOST_LOG_SEV(role_handler_lg(), debug)
91 << "Handling " << msg.subject;
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 using namespace ores::logging;
125 BOOST_LOG_SEV(role_handler_lg(), debug)
126 << "Handling " << msg.subject;
127 auto req = decode<revoke_role_request>(msg);
128 if (!req) {
129 BOOST_LOG_SEV(role_handler_lg(), warn)
130 << "Failed to decode: " << msg.subject;
131 return;
132 }
133 try {
134 auto ctx_expected = ores::service::service::make_request_context(
135 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
136 if (!ctx_expected) {
137 error_reply(nats_, msg, ctx_expected.error());
138 return;
139 }
140 const auto& ctx = *ctx_expected;
141 boost::uuids::string_generator sg;
142 const auto caller_id = sg(ctx.actor());
143 service::authorization_service svc(ctx);
144 if (!svc.has_permission(caller_id,
145 domain::permissions::roles_revoke)) {
146 BOOST_LOG_SEV(role_handler_lg(), warn)
147 << msg.subject
148 << " denied: caller lacks iam::roles:revoke permission";
149 reply(nats_, msg, revoke_role_response{
150 .success = false,
151 .error_message =
152 "Permission denied: iam::roles:revoke required"});
153 return;
154 }
155 svc.revoke_role(sg(req->account_id), sg(req->role_id));
156 BOOST_LOG_SEV(role_handler_lg(), debug)
157 << "Completed " << msg.subject;
158 reply(nats_, msg,
159 revoke_role_response{.success = true});
160 } catch (const std::exception& e) {
161 BOOST_LOG_SEV(role_handler_lg(), error)
162 << msg.subject << " failed: " << e.what();
163 reply(nats_, msg, revoke_role_response{
164 .success = false,
165 .error_message = e.what()});
166 }
167 }
168
169 void by_account(ores::nats::message msg) {
170 using namespace ores::logging;
171 BOOST_LOG_SEV(role_handler_lg(), debug)
172 << "Handling " << msg.subject;
173 auto req = decode<get_account_roles_request>(msg);
174 if (!req) {
175 BOOST_LOG_SEV(role_handler_lg(), warn)
176 << "Failed to decode: " << msg.subject;
177 return;
178 }
179 try {
180 auto ctx_expected = ores::service::service::make_request_context(
181 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
182 if (!ctx_expected) {
183 error_reply(nats_, msg, ctx_expected.error());
184 return;
185 }
186 const auto& ctx = *ctx_expected;
187 service::authorization_service svc(ctx);
188 boost::uuids::string_generator sg;
189 auto roles = svc.get_account_roles(
190 sg(req->account_id));
191 BOOST_LOG_SEV(role_handler_lg(), debug)
192 << "Completed " << msg.subject;
193 reply(nats_, msg,
194 get_account_roles_response{
195 .roles = std::move(roles)});
196 } catch (const std::exception& e) {
197 BOOST_LOG_SEV(role_handler_lg(), error)
198 << msg.subject << " failed: " << e.what();
199 reply(nats_, msg, get_account_roles_response{});
200 }
201 }
202
203 void assign_by_name(ores::nats::message msg) {
204 using namespace ores::logging;
205 BOOST_LOG_SEV(role_handler_lg(), debug)
206 << "Handling " << msg.subject;
207 auto req = decode<assign_role_by_name_request>(msg);
208 if (!req) {
209 BOOST_LOG_SEV(role_handler_lg(), warn)
210 << "Failed to decode: " << msg.subject;
211 return;
212 }
213 try {
214 auto ctx_expected = ores::service::service::make_request_context(
215 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
216 if (!ctx_expected) {
217 error_reply(nats_, msg, ctx_expected.error());
218 return;
219 }
220 const auto& ctx = *ctx_expected;
221
222 // Parse principal: username@hostname
223 const auto at_pos = req->principal.rfind('@');
224 if (at_pos == std::string::npos) {
225 reply(nats_, msg, assign_role_by_name_response{
226 .success = false,
227 .error_message =
228 "Principal must be in username@hostname format"});
229 return;
230 }
231 const auto username = req->principal.substr(0, at_pos);
232 const auto hostname = req->principal.substr(at_pos + 1);
233
234 // Resolve tenant by hostname
235 repository::tenant_repository tenant_repo(ctx_);
236 auto tenants = tenant_repo.read_latest_by_hostname(hostname);
237 if (tenants.empty()) {
238 reply(nats_, msg, assign_role_by_name_response{
239 .success = false,
240 .error_message =
241 "Tenant not found for hostname: " + hostname});
242 return;
243 }
244
246 auto tenant_ctx = tenant_context::with_tenant(
247 ctx_, boost::uuids::to_string(tenants.front().id));
248
249 // Look up account by username in the target tenant
250 repository::account_repository acct_repo(tenant_ctx);
251 auto accounts = acct_repo.read_latest_by_username(username);
252 if (accounts.empty()) {
253 reply(nats_, msg, assign_role_by_name_response{
254 .success = false,
255 .error_message = "Account not found: " + username});
256 return;
257 }
258
259 // Resolve role by name
260 service::authorization_service auth_svc(tenant_ctx);
261 auto role = auth_svc.find_role_by_name(req->role_name);
262 if (!role) {
263 reply(nats_, msg, assign_role_by_name_response{
264 .success = false,
265 .error_message = "Role not found: " + req->role_name});
266 return;
267 }
268
269 auth_svc.assign_role(accounts.front().id, role->id, ctx.actor());
270 BOOST_LOG_SEV(role_handler_lg(), debug)
271 << "Completed " << msg.subject;
272 reply(nats_, msg, assign_role_by_name_response{.success = true});
273 } catch (const std::exception& e) {
274 BOOST_LOG_SEV(role_handler_lg(), error)
275 << msg.subject << " failed: " << e.what();
276 reply(nats_, msg, assign_role_by_name_response{
277 .success = false,
278 .error_message = e.what()});
279 }
280 }
281
282 void revoke_by_name(ores::nats::message msg) {
283 using namespace ores::logging;
284 BOOST_LOG_SEV(role_handler_lg(), debug)
285 << "Handling " << msg.subject;
286 auto req = decode<revoke_role_by_name_request>(msg);
287 if (!req) {
288 BOOST_LOG_SEV(role_handler_lg(), warn)
289 << "Failed to decode: " << msg.subject;
290 return;
291 }
292 try {
293 // Authenticate and check roles:revoke permission
294 auto ctx_expected = ores::service::service::make_request_context(
295 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
296 if (!ctx_expected) {
297 error_reply(nats_, msg, ctx_expected.error());
298 return;
299 }
300 const auto& ctx = *ctx_expected;
301 boost::uuids::string_generator sg;
302 const auto caller_id = sg(ctx.actor());
303 service::authorization_service caller_svc(ctx);
304 if (!caller_svc.has_permission(caller_id,
305 domain::permissions::roles_revoke)) {
306 BOOST_LOG_SEV(role_handler_lg(), warn)
307 << msg.subject
308 << " denied: caller lacks iam::roles:revoke permission";
309 reply(nats_, msg, revoke_role_by_name_response{
310 .success = false,
311 .error_message =
312 "Permission denied: iam::roles:revoke required"});
313 return;
314 }
315
316 // Parse principal: username@hostname
317 const auto at_pos = req->principal.rfind('@');
318 if (at_pos == std::string::npos) {
319 reply(nats_, msg, revoke_role_by_name_response{
320 .success = false,
321 .error_message =
322 "Principal must be in username@hostname format"});
323 return;
324 }
325 const auto username = req->principal.substr(0, at_pos);
326 const auto hostname = req->principal.substr(at_pos + 1);
327
328 // Resolve tenant by hostname
329 repository::tenant_repository tenant_repo(ctx_);
330 auto tenants = tenant_repo.read_latest_by_hostname(hostname);
331 if (tenants.empty()) {
332 reply(nats_, msg, revoke_role_by_name_response{
333 .success = false,
334 .error_message =
335 "Tenant not found for hostname: " + hostname});
336 return;
337 }
338
340 auto tenant_ctx = tenant_context::with_tenant(
341 ctx_, boost::uuids::to_string(tenants.front().id));
342
343 // Look up account by username in the target tenant
344 repository::account_repository acct_repo(tenant_ctx);
345 auto accounts = acct_repo.read_latest_by_username(username);
346 if (accounts.empty()) {
347 reply(nats_, msg, revoke_role_by_name_response{
348 .success = false,
349 .error_message = "Account not found: " + username});
350 return;
351 }
352
353 // Resolve role by name
354 service::authorization_service auth_svc(tenant_ctx);
355 auto role = auth_svc.find_role_by_name(req->role_name);
356 if (!role) {
357 reply(nats_, msg, revoke_role_by_name_response{
358 .success = false,
359 .error_message = "Role not found: " + req->role_name});
360 return;
361 }
362
363 auth_svc.revoke_role(accounts.front().id, role->id);
364 BOOST_LOG_SEV(role_handler_lg(), debug)
365 << "Completed " << msg.subject;
366 reply(nats_, msg, revoke_role_by_name_response{.success = true});
367 } catch (const std::exception& e) {
368 BOOST_LOG_SEV(role_handler_lg(), error)
369 << msg.subject << " failed: " << e.what();
370 reply(nats_, msg, revoke_role_by_name_response{
371 .success = false,
372 .error_message = e.what()});
373 }
374 }
375
376 void suggest_commands(ores::nats::message msg) {
377 using namespace ores::logging;
378 BOOST_LOG_SEV(role_handler_lg(), debug)
379 << "Handling " << msg.subject;
380 auto req = decode<suggest_role_commands_request>(msg);
381 if (!req) {
382 BOOST_LOG_SEV(role_handler_lg(), warn)
383 << "Failed to decode: " << msg.subject;
384 return;
385 }
386 try {
387 using ores::database::repository::execute_parameterized_string_query;
388 std::vector<std::string> results;
389 if (!req->tenant_id.empty()) {
390 results = execute_parameterized_string_query(ctx_,
391 "SELECT command FROM "
392 "ores_iam_generate_role_commands_fn($1, NULL, $2::uuid)",
393 {req->username, req->tenant_id},
394 role_handler_lg(), "Suggest role commands by tenant_id");
395 } else if (!req->hostname.empty()) {
396 results = execute_parameterized_string_query(ctx_,
397 "SELECT command FROM "
398 "ores_iam_generate_role_commands_fn($1, $2)",
399 {req->username, req->hostname},
400 role_handler_lg(), "Suggest role commands by hostname");
401 } else {
402 reply(nats_, msg, suggest_role_commands_response{});
403 return;
404 }
405 BOOST_LOG_SEV(role_handler_lg(), debug)
406 << "Completed " << msg.subject;
407 reply(nats_, msg, suggest_role_commands_response{
408 .commands = std::move(results)});
409 } catch (const std::exception& e) {
410 BOOST_LOG_SEV(role_handler_lg(), error)
411 << msg.subject << " failed: " << e.what();
412 reply(nats_, msg, suggest_role_commands_response{});
413 }
414 }
415
416private:
420};
421
422} // namespace ores::iam::messaging
423#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