ORE Studio 0.0.4
Loading...
Searching...
No Matches
account_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_ACCOUNT_HANDLER_HPP
21#define ORES_IAM_MESSAGING_ACCOUNT_HANDLER_HPP
22
23#include <chrono>
24#include <stdexcept>
25#include <boost/uuid/string_generator.hpp>
26#include <boost/uuid/uuid_io.hpp>
27#include "ores.logging/make_logger.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.security/jwt/jwt_authenticator.hpp"
32#include "ores.security/jwt/jwt_claims.hpp"
33#include "ores.service/messaging/handler_helpers.hpp"
34#include "ores.service/messaging/workflow_helpers.hpp"
35#include "ores.iam.api/messaging/account_protocol.hpp"
36#include "ores.iam.api/messaging/account_history_protocol.hpp"
37#include "ores.iam.api/messaging/login_protocol.hpp"
38#include "ores.iam.api/domain/account_version.hpp"
39#include "ores.iam.api/domain/session.hpp"
40#include "ores.iam.core/repository/account_party_repository.hpp"
41#include "ores.iam.core/repository/tenant_repository.hpp"
42#include "ores.refdata.core/repository/party_repository.hpp"
43#include "ores.database/service/tenant_context.hpp"
44#include "ores.iam.core/service/account_service.hpp"
45#include "ores.iam.core/service/authorization_service.hpp"
46#include "ores.iam.core/service/account_setup_service.hpp"
47#include "ores.variability.core/service/system_settings_service.hpp"
48#include "ores.iam.core/domain/token_settings.hpp"
49#include "ores.service/service/request_context.hpp"
50#include "ores.utility/uuid/tenant_id.hpp"
51
52namespace ores::iam::messaging {
53
54namespace {
55
56inline auto& account_handler_lg() {
57 static auto instance = ores::logging::make_logger(
58 "ores.iam.messaging.account_handler");
59 return instance;
60}
61
62inline std::string acct_extract_bearer_token(const ores::nats::message& msg) {
63 auto it = msg.headers.find(std::string(ores::nats::headers::authorization));
64 if (it == msg.headers.end())
65 return {};
66 const auto& val = it->second;
67 if (!val.starts_with(ores::nats::headers::bearer_prefix))
68 return {};
69 return val.substr(ores::nats::headers::bearer_prefix.size());
70}
71
72inline std::vector<boost::uuids::uuid> acct_compute_visible_party_ids(
73 const ores::database::context& ctx,
74 const boost::uuids::uuid& party_id) {
75 try {
76 refdata::repository::party_repository repo(ctx);
77 auto ids = repo.read_descendants(party_id);
78 if (ids.empty())
79 return {party_id};
80 return ids;
81 } catch (const std::exception& e) {
82 using namespace ores::logging;
83 BOOST_LOG_SEV(account_handler_lg(), warn)
84 << "Failed to compute visible party IDs: " << e.what();
85 return {party_id};
86 }
87}
88
89inline std::optional<refdata::domain::party> acct_lookup_party(
90 const ores::database::context& ctx,
91 const boost::uuids::uuid& party_id) {
92 try {
94 auto parties = repo.read_latest(party_id);
95 if (!parties.empty())
96 return parties.front();
97 } catch (const std::exception& e) {
98 using namespace ores::logging;
99 BOOST_LOG_SEV(account_handler_lg(), warn)
100 << "Failed to look up party: " << e.what();
101 }
102 return std::nullopt;
103}
104
105inline std::string acct_lookup_tenant_name(
106 const ores::database::context& ctx,
107 const boost::uuids::uuid& tenant_id) {
108 try {
109 repository::tenant_repository repo(ctx);
110 auto tenants = repo.read_latest(tenant_id);
111 if (!tenants.empty())
112 return tenants.front().name;
113 } catch (const std::exception& e) {
114 using namespace ores::logging;
115 BOOST_LOG_SEV(account_handler_lg(), warn)
116 << "Failed to look up tenant name: " << e.what();
117 }
118 return {};
119}
120
121} // namespace
122
123using ores::service::messaging::reply;
124using ores::service::messaging::decode;
125using ores::service::messaging::error_reply;
126using ores::service::messaging::has_permission;
127using ores::service::messaging::log_handler_entry;
128using namespace ores::logging;
129
130class account_handler {
131public:
132 account_handler(ores::nats::service::client& nats,
135 : nats_(nats), ctx_(std::move(ctx)), signer_(std::move(signer)) {
136 reload_token_settings();
137 }
138
139 void reload_token_settings() {
140 try {
142 svc.refresh();
143 token_settings_ = domain::token_settings::load(svc);
144 } catch (const std::exception& e) {
145 using namespace ores::logging;
146 BOOST_LOG_SEV(account_handler_lg(), warn)
147 << "Failed to load token settings, using defaults: " << e.what();
148 }
149 }
150
151 void list(ores::nats::message msg) {
152 [[maybe_unused]] const auto correlation_id =
153 log_handler_entry(account_handler_lg(), msg);
154 try {
155 auto ctx_expected = ores::service::service::make_request_context(
156 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
157 if (!ctx_expected) {
158 error_reply(nats_, msg, ctx_expected.error());
159 return;
160 }
161 const auto& ctx = *ctx_expected;
162 service::account_service svc(ctx);
163 auto accounts = svc.list_accounts();
164 get_accounts_response resp;
165 resp.total_available_count =
166 static_cast<int>(accounts.size());
167 resp.accounts = std::move(accounts);
168 BOOST_LOG_SEV(account_handler_lg(), debug)
169 << "Completed " << msg.subject;
170 reply(nats_, msg, resp);
171 } catch (const std::exception& e) {
172 BOOST_LOG_SEV(account_handler_lg(), error)
173 << msg.subject << " failed: " << e.what();
174 reply(nats_, msg, get_accounts_response{});
175 }
176 }
177
178 void save(ores::nats::message msg) {
179 using ores::service::messaging::is_workflow_command;
180 using ores::service::messaging::extract_workflow_header;
181 using ores::service::messaging::publish_step_completion;
182 using ores::service::messaging::workflow_step_id_header;
183 using ores::service::messaging::workflow_instance_id_header;
184
185 // Workflow step command: bypass JWT auth; use X-Tenant-Id header.
186 if (is_workflow_command(msg)) {
187 using ores::service::messaging::workflow_tenant_id_header;
188 const auto step_id = extract_workflow_header(msg, workflow_step_id_header);
189 const auto inst_id = extract_workflow_header(msg, workflow_instance_id_header);
190 const auto tenant_id = extract_workflow_header(msg, workflow_tenant_id_header);
191
192 auto req = decode<save_account_request>(msg);
193 if (!req) {
194 publish_step_completion(nats_, step_id, inst_id, false, "",
195 "Failed to decode save_account_request");
196 return;
197 }
198 try {
200 auto wf_ctx = tenant_context::with_tenant(ctx_, tenant_id);
201
202 // Extract username from principal (strip @hostname suffix).
203 std::string username = req->principal;
204 const auto at_pos = req->principal.rfind('@');
205 if (at_pos != std::string::npos)
206 username = req->principal.substr(0, at_pos);
207
208 service::account_service acct_svc(wf_ctx);
209 auto auth_svc =
210 std::make_shared<service::authorization_service>(wf_ctx);
211 service::account_setup_service setup_svc(acct_svc, auth_svc);
212 auto acct = setup_svc.create_account(
213 username, req->email, req->password,
214 ctx_.service_account());
215
216 BOOST_LOG_SEV(account_handler_lg(), debug)
217 << "Workflow step completed: " << msg.subject;
218 const auto resp = save_account_response{
219 .success = true,
220 .account_id = boost::uuids::to_string(acct.id)};
221 publish_step_completion(nats_, step_id, inst_id, true,
222 rfl::json::write(resp), "");
223 } catch (const std::exception& e) {
224 BOOST_LOG_SEV(account_handler_lg(), error)
225 << "Workflow step failed: " << msg.subject
226 << " — " << e.what();
227 publish_step_completion(nats_, step_id, inst_id, false, "", e.what());
228 }
229 return;
230 }
231
232 [[maybe_unused]] const auto correlation_id =
233 log_handler_entry(account_handler_lg(), msg);
234 auto req = decode<save_account_request>(msg);
235 if (!req) {
236 BOOST_LOG_SEV(account_handler_lg(), warn)
237 << "Failed to decode: " << msg.subject;
238 return;
239 }
240 try {
241 auto base_ctx_expected = ores::service::service::make_request_context(
242 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
243 if (!base_ctx_expected) {
244 error_reply(nats_, msg, base_ctx_expected.error());
245 return;
246 }
247 const auto& base_ctx = *base_ctx_expected;
248 if (!has_permission(base_ctx, "iam::accounts:create")) {
249 error_reply(nats_, msg, ores::service::error_code::forbidden);
250 return;
251 }
252
253 // Parse principal: if username@hostname, route to that tenant's
254 // context so accounts can be created in any tenant by a system
255 // admin whose JWT is in the system tenant.
256 std::string username = req->principal;
257 ores::database::context op_ctx = base_ctx;
258 const auto at_pos = req->principal.rfind('@');
259 if (at_pos != std::string::npos) {
260 username = req->principal.substr(0, at_pos);
261 const auto hostname = req->principal.substr(at_pos + 1);
262 repository::tenant_repository tenant_repo(ctx_);
263 auto tenants = tenant_repo.read_latest_by_hostname(hostname);
264 if (!tenants.empty()) {
266 op_ctx = tenant_context::with_tenant(
267 ctx_, boost::uuids::to_string(tenants.front().id));
268 } else {
269 throw std::runtime_error(
270 "Tenant not found for hostname: " + hostname +
271 ". Cannot create account in an unknown tenant.");
272 }
273 }
274
275 service::account_service acct_svc(op_ctx);
276 auto auth_svc =
277 std::make_shared<service::authorization_service>(op_ctx);
278 service::account_setup_service setup_svc(acct_svc, auth_svc);
279 auto acct = setup_svc.create_account(
280 username, req->email, req->password, base_ctx.actor());
281 BOOST_LOG_SEV(account_handler_lg(), debug)
282 << "Completed " << msg.subject;
283 reply(nats_, msg, save_account_response{
284 .success = true,
285 .account_id = boost::uuids::to_string(acct.id)});
286 } catch (const std::exception& e) {
287 BOOST_LOG_SEV(account_handler_lg(), error)
288 << msg.subject << " failed: " << e.what();
289 reply(nats_, msg, save_account_response{
290 .success = false, .message = e.what()});
291 }
292 }
293
294 void remove(ores::nats::message msg) {
295 [[maybe_unused]] const auto correlation_id =
296 log_handler_entry(account_handler_lg(), msg);
297 auto req = decode<delete_account_request>(msg);
298 if (!req) {
299 BOOST_LOG_SEV(account_handler_lg(), warn)
300 << "Failed to decode: " << msg.subject;
301 return;
302 }
303 try {
304 auto ctx_expected = ores::service::service::make_request_context(
305 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
306 if (!ctx_expected) {
307 error_reply(nats_, msg, ctx_expected.error());
308 return;
309 }
310 const auto& ctx = *ctx_expected;
311 if (!has_permission(ctx, "iam::accounts:delete")) {
312 error_reply(nats_, msg, ores::service::error_code::forbidden);
313 return;
314 }
315 service::account_service svc(ctx);
316 boost::uuids::string_generator sg;
317 svc.delete_account(sg(req->account_id));
318 BOOST_LOG_SEV(account_handler_lg(), debug)
319 << "Completed " << msg.subject;
320 reply(nats_, msg,
321 delete_account_response{.success = true});
322 } catch (const std::exception& e) {
323 BOOST_LOG_SEV(account_handler_lg(), error)
324 << msg.subject << " failed: " << e.what();
325 reply(nats_, msg, delete_account_response{
326 .success = false, .message = e.what()});
327 }
328 }
329
330 void lock(ores::nats::message msg) {
331 [[maybe_unused]] const auto correlation_id =
332 log_handler_entry(account_handler_lg(), msg);
333 auto req = decode<lock_account_request>(msg);
334 if (!req) {
335 BOOST_LOG_SEV(account_handler_lg(), warn)
336 << "Failed to decode: " << msg.subject;
337 return;
338 }
339 lock_account_response resp;
340 auto ctx_expected = ores::service::service::make_request_context(
341 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
342 if (!ctx_expected) {
343 error_reply(nats_, msg, ctx_expected.error());
344 return;
345 }
346 const auto& ctx = *ctx_expected;
347 if (!has_permission(ctx, "iam::accounts:lock")) {
348 error_reply(nats_, msg, ores::service::error_code::forbidden);
349 return;
350 }
351 service::account_service svc(ctx);
352 boost::uuids::string_generator sg;
353 for (const auto& id : req->account_ids) {
354 try {
355 svc.lock_account(sg(id));
356 resp.results.push_back({.success = true});
357 } catch (const std::exception& e) {
358 BOOST_LOG_SEV(account_handler_lg(), error)
359 << msg.subject << " failed: " << e.what();
360 resp.results.push_back({
361 .success = false, .message = e.what()});
362 }
363 }
364 BOOST_LOG_SEV(account_handler_lg(), debug)
365 << "Completed " << msg.subject;
366 reply(nats_, msg, resp);
367 }
368
369 void unlock(ores::nats::message msg) {
370 [[maybe_unused]] const auto correlation_id =
371 log_handler_entry(account_handler_lg(), msg);
372 auto req = decode<unlock_account_request>(msg);
373 if (!req) {
374 BOOST_LOG_SEV(account_handler_lg(), warn)
375 << "Failed to decode: " << msg.subject;
376 return;
377 }
378 unlock_account_response resp;
379 auto ctx_expected = ores::service::service::make_request_context(
380 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
381 if (!ctx_expected) {
382 error_reply(nats_, msg, ctx_expected.error());
383 return;
384 }
385 const auto& ctx = *ctx_expected;
386 if (!has_permission(ctx, "iam::accounts:unlock")) {
387 error_reply(nats_, msg, ores::service::error_code::forbidden);
388 return;
389 }
390 service::account_service svc(ctx);
391 boost::uuids::string_generator sg;
392 for (const auto& id : req->account_ids) {
393 try {
394 svc.unlock_account(sg(id));
395 resp.results.push_back({.success = true});
396 } catch (const std::exception& e) {
397 BOOST_LOG_SEV(account_handler_lg(), error)
398 << msg.subject << " failed: " << e.what();
399 resp.results.push_back({
400 .success = false, .message = e.what()});
401 }
402 }
403 BOOST_LOG_SEV(account_handler_lg(), debug)
404 << "Completed " << msg.subject;
405 reply(nats_, msg, resp);
406 }
407
408 void login_info(ores::nats::message msg) {
409 [[maybe_unused]] const auto correlation_id =
410 log_handler_entry(account_handler_lg(), msg);
411 try {
412 auto ctx_expected = ores::service::service::make_request_context(
413 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
414 if (!ctx_expected) {
415 error_reply(nats_, msg, ctx_expected.error());
416 return;
417 }
418 const auto& ctx = *ctx_expected;
419 service::account_service svc(ctx);
420 auto infos = svc.list_login_info();
421 BOOST_LOG_SEV(account_handler_lg(), debug)
422 << "Completed " << msg.subject;
423 reply(nats_, msg,
424 list_login_info_response{
425 .login_infos = std::move(infos)});
426 } catch (const std::exception& e) {
427 BOOST_LOG_SEV(account_handler_lg(), error)
428 << msg.subject << " failed: " << e.what();
429 reply(nats_, msg, list_login_info_response{});
430 }
431 }
432
433 void reset_password(ores::nats::message msg) {
434 [[maybe_unused]] const auto correlation_id =
435 log_handler_entry(account_handler_lg(), msg);
436 auto req = decode<reset_password_request>(msg);
437 if (!req) {
438 BOOST_LOG_SEV(account_handler_lg(), warn)
439 << "Failed to decode: " << msg.subject;
440 return;
441 }
442 reset_password_response resp;
443 auto ctx_expected = ores::service::service::make_request_context(
444 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
445 if (!ctx_expected) {
446 error_reply(nats_, msg, ctx_expected.error());
447 return;
448 }
449 const auto& ctx = *ctx_expected;
450 if (!has_permission(ctx, "iam::accounts:reset_password")) {
451 error_reply(nats_, msg, ores::service::error_code::forbidden);
452 return;
453 }
454 service::account_service svc(ctx);
455 boost::uuids::string_generator sg;
456 for (const auto& id_str : req->account_ids) {
457 try {
458 auto account_id = sg(id_str);
459 auto err = svc.change_password(
460 account_id, req->new_password);
461 if (err.empty()) {
462 resp.results.push_back({.success = true});
463 } else {
464 resp.results.push_back({
465 .success = false, .message = err});
466 }
467 } catch (const std::exception& e) {
468 BOOST_LOG_SEV(account_handler_lg(), error)
469 << msg.subject << " failed: " << e.what();
470 resp.results.push_back({
471 .success = false, .message = e.what()});
472 }
473 }
474 resp.success = true;
475 BOOST_LOG_SEV(account_handler_lg(), debug)
476 << "Completed " << msg.subject;
477 reply(nats_, msg, resp);
478 }
479
480 void change_password(ores::nats::message msg) {
481 [[maybe_unused]] const auto correlation_id =
482 log_handler_entry(account_handler_lg(), msg);
483 auto req = decode<change_password_request>(msg);
484 if (!req) {
485 BOOST_LOG_SEV(account_handler_lg(), warn)
486 << "Failed to decode: " << msg.subject;
487 return;
488 }
489 try {
490 auto token = acct_extract_bearer_token(msg);
491 if (token.empty()) {
492 reply(nats_, msg, change_password_response{
493 .success = false,
494 .message = "Missing authorization token"});
495 return;
496 }
497 auto claims_result = signer_.validate(token);
498 if (!claims_result) {
499 reply(nats_, msg, change_password_response{
500 .success = false,
501 .message = "Invalid or expired token"});
502 return;
503 }
504 boost::uuids::string_generator sg;
505 auto account_id = sg(claims_result->subject);
506 auto ctx_expected = ores::service::service::make_request_context(
507 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
508 if (!ctx_expected) {
509 error_reply(nats_, msg, ctx_expected.error());
510 return;
511 }
512 const auto& ctx = *ctx_expected;
513 service::account_service svc(ctx);
514 auto err = svc.change_password(account_id,
515 req->new_password);
516 if (err.empty()) {
517 BOOST_LOG_SEV(account_handler_lg(), debug)
518 << "Completed " << msg.subject;
519 reply(nats_, msg, change_password_response{
520 .success = true});
521 } else {
522 reply(nats_, msg, change_password_response{
523 .success = false, .message = err});
524 }
525 } catch (const std::exception& e) {
526 BOOST_LOG_SEV(account_handler_lg(), error)
527 << msg.subject << " failed: " << e.what();
528 reply(nats_, msg, change_password_response{
529 .success = false, .message = e.what()});
530 }
531 }
532
533 void update(ores::nats::message msg) {
534 [[maybe_unused]] const auto correlation_id =
535 log_handler_entry(account_handler_lg(), msg);
536 auto req = decode<update_account_request>(msg);
537 if (!req) {
538 BOOST_LOG_SEV(account_handler_lg(), warn)
539 << "Failed to decode: " << msg.subject;
540 return;
541 }
542 try {
543 auto ctx_expected = ores::service::service::make_request_context(
544 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
545 if (!ctx_expected) {
546 error_reply(nats_, msg, ctx_expected.error());
547 return;
548 }
549 const auto& ctx = *ctx_expected;
550 if (!has_permission(ctx, "iam::accounts:update")) {
551 error_reply(nats_, msg, ores::service::error_code::forbidden);
552 return;
553 }
554 service::account_service svc(ctx);
555 boost::uuids::string_generator sg;
556 svc.update_account(sg(req->account_id), req->email,
557 ctx.actor(), req->change_reason_code,
558 req->change_commentary);
559 BOOST_LOG_SEV(account_handler_lg(), debug)
560 << "Completed " << msg.subject;
561 reply(nats_, msg,
562 update_account_response{.success = true});
563 } catch (const std::exception& e) {
564 BOOST_LOG_SEV(account_handler_lg(), error)
565 << msg.subject << " failed: " << e.what();
566 reply(nats_, msg, update_account_response{
567 .success = false, .message = e.what()});
568 }
569 }
570
571 void update_email(ores::nats::message msg) {
572 [[maybe_unused]] const auto correlation_id =
573 log_handler_entry(account_handler_lg(), msg);
574 auto req = decode<update_my_email_request>(msg);
575 if (!req) {
576 BOOST_LOG_SEV(account_handler_lg(), warn)
577 << "Failed to decode: " << msg.subject;
578 return;
579 }
580 try {
581 auto token = acct_extract_bearer_token(msg);
582 if (token.empty()) {
583 reply(nats_, msg, update_my_email_response{
584 .success = false,
585 .message = "Missing authorization token"});
586 return;
587 }
588 auto claims_result = signer_.validate(token);
589 if (!claims_result) {
590 reply(nats_, msg, update_my_email_response{
591 .success = false,
592 .message = "Invalid or expired token"});
593 return;
594 }
595 boost::uuids::string_generator sg;
596 auto account_id = sg(claims_result->subject);
597 auto ctx_expected = ores::service::service::make_request_context(
598 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
599 if (!ctx_expected) {
600 error_reply(nats_, msg, ctx_expected.error());
601 return;
602 }
603 const auto& ctx = *ctx_expected;
604 service::account_service svc(ctx);
605 auto err = svc.update_my_email(account_id, req->email);
606 if (err.empty()) {
607 BOOST_LOG_SEV(account_handler_lg(), debug)
608 << "Completed " << msg.subject;
609 reply(nats_, msg, update_my_email_response{
610 .success = true});
611 } else {
612 reply(nats_, msg, update_my_email_response{
613 .success = false, .message = err});
614 }
615 } catch (const std::exception& e) {
616 BOOST_LOG_SEV(account_handler_lg(), error)
617 << msg.subject << " failed: " << e.what();
618 reply(nats_, msg, update_my_email_response{
619 .success = false, .message = e.what()});
620 }
621 }
622
623 void select_party(ores::nats::message msg) {
624 [[maybe_unused]] const auto correlation_id =
625 log_handler_entry(account_handler_lg(), msg);
626 auto req = decode<select_party_request>(msg);
627 if (!req) {
628 BOOST_LOG_SEV(account_handler_lg(), warn)
629 << "Failed to decode: " << msg.subject;
630 return;
631 }
632 try {
633 auto token = acct_extract_bearer_token(msg);
634 if (token.empty()) {
635 reply(nats_, msg, select_party_response{
636 .success = false,
637 .message = "Missing authorization token"});
638 return;
639 }
640
641 auto claims_result = signer_.validate(token);
642 if (!claims_result) {
643 BOOST_LOG_SEV(account_handler_lg(), warn)
644 << "select_party: JWT validation failed";
645 reply(nats_, msg, select_party_response{
646 .success = false,
647 .message = "Invalid or expired token"});
648 return;
649 }
650 if (claims_result->audience != "select_party_only") {
651 BOOST_LOG_SEV(account_handler_lg(), warn)
652 << "select_party: unexpected token audience: "
653 << claims_result->audience;
654 reply(nats_, msg, select_party_response{
655 .success = false,
656 .message = "Invalid or expired token"});
657 return;
658 }
659
660 boost::uuids::string_generator sg;
661 auto account_id = sg(claims_result->subject);
662 BOOST_LOG_SEV(account_handler_lg(), debug)
663 << "select_party: account=" << claims_result->subject
664 << " requested_party=" << req->party_id;
665
666 // Build the DB context directly from the already-validated claims.
667 // Do NOT call make_request_context here — it re-validates the JWT
668 // using the standard audience ("authenticated"), which would reject
669 // the "select_party_only" token.
670 const auto tenant_id_str = claims_result->tenant_id.value_or("");
671 if (tenant_id_str.empty()) {
672 BOOST_LOG_SEV(account_handler_lg(), warn)
673 << "select_party: token has no tenant_id";
674 reply(nats_, msg, select_party_response{
675 .success = false,
676 .message = "Invalid token: missing tenant"});
677 return;
678 }
679 auto tid_result =
681 if (!tid_result) {
682 BOOST_LOG_SEV(account_handler_lg(), warn)
683 << "select_party: invalid tenant_id in token: "
684 << tenant_id_str;
685 reply(nats_, msg, select_party_response{
686 .success = false,
687 .message = "Invalid token: malformed tenant"});
688 return;
689 }
690 const auto ctx = ctx_.with_tenant(
691 *tid_result, claims_result->username.value_or(""));
692
693 boost::uuids::uuid requested_party_id;
694 try {
695 requested_party_id = sg(req->party_id);
696 } catch (const std::exception&) {
697 BOOST_LOG_SEV(account_handler_lg(), warn)
698 << "select_party: invalid party_id: " << req->party_id;
699 reply(nats_, msg, select_party_response{
700 .success = false,
701 .message = "Invalid party_id format"});
702 return;
703 }
704 repository::account_party_repository ap_repo(ctx);
705 auto parties = ap_repo.read_latest_by_account(account_id);
706
707 BOOST_LOG_SEV(account_handler_lg(), debug)
708 << "select_party: account has " << parties.size()
709 << " party membership(s)";
710
711 bool is_member = false;
712 for (const auto& ap : parties) {
713 if (ap.party_id == requested_party_id) {
714 is_member = true;
715 break;
716 }
717 }
718
719 if (!is_member) {
720 BOOST_LOG_SEV(account_handler_lg(), warn)
721 << "select_party: party " << req->party_id
722 << " not in account's party list (account has "
723 << parties.size() << " parties)";
724 reply(nats_, msg, select_party_response{
725 .success = false,
726 .message = "User is not a member of requested party"});
727 return;
728 }
729
730 auto visible = acct_compute_visible_party_ids(
731 ctx, requested_party_id);
732
733 security::jwt::jwt_claims new_claims;
734 new_claims.subject = claims_result->subject;
735 new_claims.issued_at = std::chrono::system_clock::now();
736 new_claims.expires_at =
737 new_claims.issued_at + std::chrono::seconds(
738 token_settings_.access_lifetime_s);
739 new_claims.username = claims_result->username;
740 new_claims.email = claims_result->email;
741 new_claims.tenant_id = tenant_id_str;
742 new_claims.party_id =
743 boost::uuids::to_string(requested_party_id);
744 // Carry the session identifiers forward so logout can end
745 // the session record created at login.
746 new_claims.session_id = claims_result->session_id;
747 new_claims.session_start_time = claims_result->session_start_time;
748 for (const auto& vid : visible)
749 new_claims.visible_party_ids.push_back(
750 boost::uuids::to_string(vid));
751
752 auto new_token =
753 signer_.create_token(new_claims).value_or("");
754
755 std::string t_name;
756 std::string p_name;
757 try {
758 auto tid = sg(tenant_id_str);
759 t_name = acct_lookup_tenant_name(ctx, tid);
760 } catch (const std::exception& e) {
761 BOOST_LOG_SEV(account_handler_lg(), warn)
762 << "Failed to look up tenant name during "
763 "party selection: " << e.what();
764 }
765 bool party_setup_required = false;
766 if (const auto p = acct_lookup_party(ctx, requested_party_id)) {
767 p_name = p->full_name;
768 party_setup_required = p->status == "Inactive";
769 }
770
771 BOOST_LOG_SEV(account_handler_lg(), debug)
772 << "Completed " << msg.subject;
773 reply(nats_, msg, select_party_response{
774 .success = true,
775 .message = "Party selected",
776 .token = new_token,
777 .username = claims_result->username.value_or(""),
778 .tenant_name = t_name,
779 .party_name = p_name,
780 .party_setup_required = party_setup_required
781 });
782 } catch (const std::exception& e) {
783 BOOST_LOG_SEV(account_handler_lg(), error)
784 << msg.subject << " failed: " << e.what();
785 reply(nats_, msg, select_party_response{
786 .success = false, .message = e.what()});
787 }
788 }
789
790 void history(ores::nats::message msg) {
791 [[maybe_unused]] const auto correlation_id =
792 log_handler_entry(account_handler_lg(), msg);
793 auto req = decode<get_account_history_request>(msg);
794 if (!req) {
795 BOOST_LOG_SEV(account_handler_lg(), warn)
796 << "Failed to decode: " << msg.subject;
797 return;
798 }
799 try {
800 auto ctx_expected = ores::service::service::make_request_context(
801 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
802 if (!ctx_expected) {
803 error_reply(nats_, msg, ctx_expected.error());
804 return;
805 }
806 const auto& ctx = *ctx_expected;
807 service::account_service svc(ctx);
808 auto accounts = svc.get_account_history(req->username);
809 account_version_history avh;
810 int vnum = static_cast<int>(accounts.size());
811 for (const auto& a : accounts) {
813 av.data = a;
814 av.version_number = vnum--;
815 av.modified_by = a.modified_by;
816 av.recorded_at = a.recorded_at;
817 avh.versions.push_back(std::move(av));
818 }
819 BOOST_LOG_SEV(account_handler_lg(), debug)
820 << "Completed " << msg.subject;
821 reply(nats_, msg, get_account_history_response{
822 .success = true,
823 .history = std::move(avh)});
824 } catch (const std::exception& e) {
825 BOOST_LOG_SEV(account_handler_lg(), error)
826 << msg.subject << " failed: " << e.what();
827 reply(nats_, msg, get_account_history_response{
828 .success = false, .message = e.what()});
829 }
830 }
831
832private:
836 domain::token_settings token_settings_;
837};
838
839} // namespace ores::iam::messaging
840#endif
STL namespace.
Implements logging infrastructure for ORE Studio.
Definition boost_severity.hpp:28
@ forbidden
The caller is authenticated but lacks the required permission.
Context for the operations on a postgres database.
Definition context.hpp:47
const std::string & actor() const
Gets the current actor (end-user) for this context.
Definition context.hpp:119
context with_tenant(utility::uuid::tenant_id tenant_id, std::string actor) const
Creates a new context with a different tenant ID (no party).
Definition context.hpp:162
Manages tenant context for multi-tenant database operations.
Definition tenant_context.hpp:37
Represents a specific version of an account with metadata.
Definition account_version.hpp:32
std::string modified_by
Username of the person who recorded this version in the system.
Definition account_version.hpp:46
int version_number
Version number (1-based, higher is newer).
Definition account_version.hpp:41
std::chrono::system_clock::time_point recorded_at
Timestamp when this version was recorded in the system.
Definition account_version.hpp:51
account data
The account data at this version.
Definition account_version.hpp:36
static token_settings load(variability::service::system_settings_service &svc)
Loads token settings from the system settings service.
Definition token_settings.cpp:24
A received NATS message.
Definition message.hpp:40
std::string subject
The subject the message was published to.
Definition message.hpp:44
std::unordered_map< std::string, std::string > headers
NATS message headers (NATS 2.2+).
Definition message.hpp:66
NATS client: connection, pub/sub, request/reply, and JetStream.
Definition client.hpp:73
Reads and writes parties to data storage.
Definition party_repository.hpp:37
JWT authentication primitive.
Definition jwt_authenticator.hpp:45
Represents the claims extracted from a JWT token.
Definition jwt_claims.hpp:33
std::string subject
Subject claim - typically the account ID.
Definition jwt_claims.hpp:37
std::optional< std::string > session_id
Optional session ID for tracking sessions.
Definition jwt_claims.hpp:81
std::chrono::system_clock::time_point issued_at
Time when the token was issued.
Definition jwt_claims.hpp:57
std::optional< std::string > tenant_id
Optional tenant ID (UUID string).
Definition jwt_claims.hpp:97
std::optional< std::string > party_id
Optional party ID (UUID string, nil UUID if no party selected).
Definition jwt_claims.hpp:104
std::optional< std::chrono::system_clock::time_point > session_start_time
Optional session start time for efficient database updates.
Definition jwt_claims.hpp:90
std::optional< std::string > username
Optional username claim.
Definition jwt_claims.hpp:67
std::chrono::system_clock::time_point expires_at
Time when the token expires.
Definition jwt_claims.hpp:52
std::optional< std::string > email
Optional email claim.
Definition jwt_claims.hpp:72
static std::expected< tenant_id, std::string > from_string(std::string_view str)
Creates a tenant_id from a string representation.
Definition tenant_id.cpp:57
Service for managing typed system settings.
Definition system_settings_service.hpp:41