20#ifndef ORES_IAM_MESSAGING_ACCOUNT_HANDLER_HPP
21#define ORES_IAM_MESSAGING_ACCOUNT_HANDLER_HPP
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/service/request_context.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"
50namespace ores::iam::messaging {
54inline auto& account_handler_lg() {
55 static auto instance = ores::logging::make_logger(
56 "ores.iam.messaging.account_handler");
61 auto it = msg.
headers.find(
"Authorization");
64 const auto& val = it->second;
65 constexpr std::string_view prefix =
"Bearer ";
66 if (!val.starts_with(prefix))
68 return val.substr(prefix.size());
71inline std::vector<boost::uuids::uuid> acct_compute_visible_party_ids(
73 const boost::uuids::uuid& party_id) {
75 refdata::repository::party_repository repo(ctx);
76 auto ids = repo.read_descendants(party_id);
80 }
catch (
const std::exception& e) {
82 BOOST_LOG_SEV(account_handler_lg(), warn)
83 <<
"Failed to compute visible party IDs: " << e.what();
88inline std::optional<refdata::domain::party> acct_lookup_party(
90 const boost::uuids::uuid& party_id) {
93 auto parties = repo.read_latest(party_id);
95 return parties.front();
96 }
catch (
const std::exception& e) {
98 BOOST_LOG_SEV(account_handler_lg(), warn)
99 <<
"Failed to look up party: " << e.what();
104inline std::string acct_lookup_tenant_name(
106 const boost::uuids::uuid& tenant_id) {
108 repository::tenant_repository repo(ctx);
109 auto tenants = repo.read_latest(tenant_id);
110 if (!tenants.empty())
111 return tenants.front().name;
112 }
catch (
const std::exception& e) {
114 BOOST_LOG_SEV(account_handler_lg(), warn)
115 <<
"Failed to look up tenant name: " << e.what();
122using ores::service::messaging::reply;
123using ores::service::messaging::decode;
124using ores::service::messaging::error_reply;
125using ores::service::messaging::has_permission;
127class account_handler {
132 : nats_(nats), ctx_(
std::move(ctx)), signer_(
std::move(signer)) {
133 reload_token_settings();
136 void reload_token_settings() {
141 }
catch (
const std::exception& e) {
143 BOOST_LOG_SEV(account_handler_lg(), warn)
144 <<
"Failed to load token settings, using defaults: " << e.what();
150 BOOST_LOG_SEV(account_handler_lg(), debug)
153 auto ctx_expected = ores::service::service::make_request_context(
154 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
156 error_reply(nats_, msg, ctx_expected.error());
159 const auto& ctx = *ctx_expected;
160 service::account_service svc(ctx);
161 auto accounts = svc.list_accounts();
162 get_accounts_response resp;
163 resp.total_available_count =
164 static_cast<int>(accounts.size());
165 resp.accounts = std::move(accounts);
166 BOOST_LOG_SEV(account_handler_lg(), debug)
167 <<
"Completed " << msg.
subject;
168 reply(nats_, msg, resp);
169 }
catch (
const std::exception& e) {
170 BOOST_LOG_SEV(account_handler_lg(), error)
171 << msg.
subject <<
" failed: " << e.what();
172 reply(nats_, msg, get_accounts_response{});
178 BOOST_LOG_SEV(account_handler_lg(), debug)
180 auto req = decode<save_account_request>(msg);
182 BOOST_LOG_SEV(account_handler_lg(), warn)
183 <<
"Failed to decode: " << msg.
subject;
187 auto base_ctx_expected = ores::service::service::make_request_context(
188 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
189 if (!base_ctx_expected) {
190 error_reply(nats_, msg, base_ctx_expected.error());
193 const auto& base_ctx = *base_ctx_expected;
194 if (!has_permission(base_ctx,
"iam::accounts:create")) {
202 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 const auto hostname = req->principal.substr(at_pos + 1);
208 repository::tenant_repository tenant_repo(ctx_);
209 auto tenants = tenant_repo.read_latest_by_hostname(hostname);
210 if (!tenants.empty()) {
212 op_ctx = tenant_context::with_tenant(
213 ctx_, boost::uuids::to_string(tenants.front().id));
215 throw std::runtime_error(
216 "Tenant not found for hostname: " + hostname +
217 ". Cannot create account in an unknown tenant.");
221 service::account_service acct_svc(op_ctx);
223 std::make_shared<service::authorization_service>(op_ctx);
224 service::account_setup_service setup_svc(acct_svc, auth_svc);
225 auto acct = setup_svc.create_account(
226 username, req->email, req->password, base_ctx.actor());
227 BOOST_LOG_SEV(account_handler_lg(), debug)
228 <<
"Completed " << msg.
subject;
229 reply(nats_, msg, save_account_response{
231 .account_id = boost::uuids::to_string(acct.id)});
232 }
catch (
const std::exception& e) {
233 BOOST_LOG_SEV(account_handler_lg(), error)
234 << msg.
subject <<
" failed: " << e.what();
235 reply(nats_, msg, save_account_response{
236 .success =
false, .message = e.what()});
242 BOOST_LOG_SEV(account_handler_lg(), debug)
244 auto req = decode<delete_account_request>(msg);
246 BOOST_LOG_SEV(account_handler_lg(), warn)
247 <<
"Failed to decode: " << msg.
subject;
251 auto ctx_expected = ores::service::service::make_request_context(
252 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
254 error_reply(nats_, msg, ctx_expected.error());
257 const auto& ctx = *ctx_expected;
258 if (!has_permission(ctx,
"iam::accounts:delete")) {
262 service::account_service svc(ctx);
263 boost::uuids::string_generator sg;
264 svc.delete_account(sg(req->account_id));
265 BOOST_LOG_SEV(account_handler_lg(), debug)
266 <<
"Completed " << msg.
subject;
268 delete_account_response{.success =
true});
269 }
catch (
const std::exception& e) {
270 BOOST_LOG_SEV(account_handler_lg(), error)
271 << msg.
subject <<
" failed: " << e.what();
272 reply(nats_, msg, delete_account_response{
273 .success =
false, .message = e.what()});
279 BOOST_LOG_SEV(account_handler_lg(), debug)
281 auto req = decode<lock_account_request>(msg);
283 BOOST_LOG_SEV(account_handler_lg(), warn)
284 <<
"Failed to decode: " << msg.
subject;
287 lock_account_response resp;
288 auto ctx_expected = ores::service::service::make_request_context(
289 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
291 error_reply(nats_, msg, ctx_expected.error());
294 const auto& ctx = *ctx_expected;
295 if (!has_permission(ctx,
"iam::accounts:lock")) {
299 service::account_service svc(ctx);
300 boost::uuids::string_generator sg;
301 for (
const auto&
id : req->account_ids) {
303 svc.lock_account(sg(
id));
304 resp.results.push_back({.success =
true});
305 }
catch (
const std::exception& e) {
306 BOOST_LOG_SEV(account_handler_lg(), error)
307 << msg.
subject <<
" failed: " << e.what();
308 resp.results.push_back({
309 .success =
false, .message = e.what()});
312 BOOST_LOG_SEV(account_handler_lg(), debug)
313 <<
"Completed " << msg.
subject;
314 reply(nats_, msg, resp);
319 BOOST_LOG_SEV(account_handler_lg(), debug)
321 auto req = decode<unlock_account_request>(msg);
323 BOOST_LOG_SEV(account_handler_lg(), warn)
324 <<
"Failed to decode: " << msg.
subject;
327 unlock_account_response resp;
328 auto ctx_expected = ores::service::service::make_request_context(
329 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
331 error_reply(nats_, msg, ctx_expected.error());
334 const auto& ctx = *ctx_expected;
335 if (!has_permission(ctx,
"iam::accounts:unlock")) {
339 service::account_service svc(ctx);
340 boost::uuids::string_generator sg;
341 for (
const auto&
id : req->account_ids) {
343 svc.unlock_account(sg(
id));
344 resp.results.push_back({.success =
true});
345 }
catch (
const std::exception& e) {
346 BOOST_LOG_SEV(account_handler_lg(), error)
347 << msg.
subject <<
" failed: " << e.what();
348 resp.results.push_back({
349 .success =
false, .message = e.what()});
352 BOOST_LOG_SEV(account_handler_lg(), debug)
353 <<
"Completed " << msg.
subject;
354 reply(nats_, msg, resp);
359 BOOST_LOG_SEV(account_handler_lg(), debug)
362 auto ctx_expected = ores::service::service::make_request_context(
363 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
365 error_reply(nats_, msg, ctx_expected.error());
368 const auto& ctx = *ctx_expected;
369 service::account_service svc(ctx);
370 auto infos = svc.list_login_info();
371 BOOST_LOG_SEV(account_handler_lg(), debug)
372 <<
"Completed " << msg.
subject;
374 list_login_info_response{
375 .login_infos = std::move(infos)});
376 }
catch (
const std::exception& e) {
377 BOOST_LOG_SEV(account_handler_lg(), error)
378 << msg.
subject <<
" failed: " << e.what();
379 reply(nats_, msg, list_login_info_response{});
385 BOOST_LOG_SEV(account_handler_lg(), debug)
387 auto req = decode<reset_password_request>(msg);
389 BOOST_LOG_SEV(account_handler_lg(), warn)
390 <<
"Failed to decode: " << msg.
subject;
393 reset_password_response resp;
394 auto ctx_expected = ores::service::service::make_request_context(
395 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
397 error_reply(nats_, msg, ctx_expected.error());
400 const auto& ctx = *ctx_expected;
401 if (!has_permission(ctx,
"iam::accounts:reset_password")) {
405 service::account_service svc(ctx);
406 boost::uuids::string_generator sg;
407 for (
const auto& id_str : req->account_ids) {
409 auto account_id = sg(id_str);
410 auto err = svc.change_password(
411 account_id, req->new_password);
413 resp.results.push_back({.success =
true});
415 resp.results.push_back({
416 .success =
false, .message = err});
418 }
catch (
const std::exception& e) {
419 BOOST_LOG_SEV(account_handler_lg(), error)
420 << msg.
subject <<
" failed: " << e.what();
421 resp.results.push_back({
422 .success =
false, .message = e.what()});
426 BOOST_LOG_SEV(account_handler_lg(), debug)
427 <<
"Completed " << msg.
subject;
428 reply(nats_, msg, resp);
433 BOOST_LOG_SEV(account_handler_lg(), debug)
435 auto req = decode<change_password_request>(msg);
437 BOOST_LOG_SEV(account_handler_lg(), warn)
438 <<
"Failed to decode: " << msg.
subject;
442 auto token = acct_extract_bearer_token(msg);
444 reply(nats_, msg, change_password_response{
446 .message =
"Missing authorization token"});
449 auto claims_result = signer_.validate(token);
450 if (!claims_result) {
451 reply(nats_, msg, change_password_response{
453 .message =
"Invalid or expired token"});
456 boost::uuids::string_generator sg;
457 auto account_id = sg(claims_result->subject);
458 auto ctx_expected = ores::service::service::make_request_context(
459 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
461 error_reply(nats_, msg, ctx_expected.error());
464 const auto& ctx = *ctx_expected;
465 service::account_service svc(ctx);
466 auto err = svc.change_password(account_id,
469 BOOST_LOG_SEV(account_handler_lg(), debug)
470 <<
"Completed " << msg.
subject;
471 reply(nats_, msg, change_password_response{
474 reply(nats_, msg, change_password_response{
475 .success =
false, .message = err});
477 }
catch (
const std::exception& e) {
478 BOOST_LOG_SEV(account_handler_lg(), error)
479 << msg.
subject <<
" failed: " << e.what();
480 reply(nats_, msg, change_password_response{
481 .success =
false, .message = e.what()});
487 BOOST_LOG_SEV(account_handler_lg(), debug)
489 auto req = decode<update_account_request>(msg);
491 BOOST_LOG_SEV(account_handler_lg(), warn)
492 <<
"Failed to decode: " << msg.
subject;
496 auto ctx_expected = ores::service::service::make_request_context(
497 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
499 error_reply(nats_, msg, ctx_expected.error());
502 const auto& ctx = *ctx_expected;
503 if (!has_permission(ctx,
"iam::accounts:update")) {
507 service::account_service svc(ctx);
508 boost::uuids::string_generator sg;
509 svc.update_account(sg(req->account_id), req->email,
510 ctx.
actor(), req->change_reason_code,
511 req->change_commentary);
512 BOOST_LOG_SEV(account_handler_lg(), debug)
513 <<
"Completed " << msg.
subject;
515 update_account_response{.success =
true});
516 }
catch (
const std::exception& e) {
517 BOOST_LOG_SEV(account_handler_lg(), error)
518 << msg.
subject <<
" failed: " << e.what();
519 reply(nats_, msg, update_account_response{
520 .success =
false, .message = e.what()});
526 BOOST_LOG_SEV(account_handler_lg(), debug)
528 auto req = decode<update_my_email_request>(msg);
530 BOOST_LOG_SEV(account_handler_lg(), warn)
531 <<
"Failed to decode: " << msg.
subject;
535 auto token = acct_extract_bearer_token(msg);
537 reply(nats_, msg, update_my_email_response{
539 .message =
"Missing authorization token"});
542 auto claims_result = signer_.validate(token);
543 if (!claims_result) {
544 reply(nats_, msg, update_my_email_response{
546 .message =
"Invalid or expired token"});
549 boost::uuids::string_generator sg;
550 auto account_id = sg(claims_result->subject);
551 auto ctx_expected = ores::service::service::make_request_context(
552 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
554 error_reply(nats_, msg, ctx_expected.error());
557 const auto& ctx = *ctx_expected;
558 service::account_service svc(ctx);
559 auto err = svc.update_my_email(account_id, req->email);
561 BOOST_LOG_SEV(account_handler_lg(), debug)
562 <<
"Completed " << msg.
subject;
563 reply(nats_, msg, update_my_email_response{
566 reply(nats_, msg, update_my_email_response{
567 .success =
false, .message = err});
569 }
catch (
const std::exception& e) {
570 BOOST_LOG_SEV(account_handler_lg(), error)
571 << msg.
subject <<
" failed: " << e.what();
572 reply(nats_, msg, update_my_email_response{
573 .success =
false, .message = e.what()});
579 BOOST_LOG_SEV(account_handler_lg(), debug)
581 auto req = decode<select_party_request>(msg);
583 BOOST_LOG_SEV(account_handler_lg(), warn)
584 <<
"Failed to decode: " << msg.
subject;
588 auto token = acct_extract_bearer_token(msg);
590 reply(nats_, msg, select_party_response{
592 .message =
"Missing authorization token"});
596 auto claims_result = signer_.validate(token);
597 if (!claims_result ||
598 claims_result->audience !=
"select_party_only") {
599 reply(nats_, msg, select_party_response{
601 .message =
"Invalid or expired token"});
605 boost::uuids::string_generator sg;
606 auto account_id = sg(claims_result->subject);
608 auto ctx_expected = ores::service::service::make_request_context(
609 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
611 error_reply(nats_, msg, ctx_expected.error());
614 const auto& ctx = *ctx_expected;
615 boost::uuids::uuid requested_party_id =
617 repository::account_party_repository ap_repo(ctx);
619 ap_repo.read_latest_by_account(account_id);
621 bool is_member =
false;
622 for (
const auto& ap : parties) {
623 if (ap.party_id == requested_party_id) {
630 reply(nats_, msg, select_party_response{
633 "User is not a member of requested party"});
637 auto tenant_id_str = claims_result->tenant_id.value_or(
638 ctx_.tenant_id().to_string());
639 auto visible = acct_compute_visible_party_ids(
640 ctx, requested_party_id);
643 new_claims.
subject = claims_result->subject;
644 new_claims.
issued_at = std::chrono::system_clock::now();
646 new_claims.
issued_at + std::chrono::seconds(
647 token_settings_.access_lifetime_s);
648 new_claims.
username = claims_result->username;
649 new_claims.
email = claims_result->email;
652 boost::uuids::to_string(requested_party_id);
655 new_claims.
session_id = claims_result->session_id;
657 for (
const auto& vid : visible)
658 new_claims.visible_party_ids.push_back(
659 boost::uuids::to_string(vid));
662 signer_.create_token(new_claims).value_or(
"");
667 auto tid = sg(tenant_id_str);
668 t_name = acct_lookup_tenant_name(ctx, tid);
669 }
catch (
const std::exception& e) {
670 BOOST_LOG_SEV(account_handler_lg(), warn)
671 <<
"Failed to look up tenant name during "
672 "party selection: " << e.what();
674 if (
const auto p = acct_lookup_party(ctx, requested_party_id))
675 p_name = p->full_name;
677 BOOST_LOG_SEV(account_handler_lg(), debug)
678 <<
"Completed " << msg.
subject;
679 reply(nats_, msg, select_party_response{
681 .message =
"Party selected",
683 .username = claims_result->username.value_or(
""),
684 .tenant_name = t_name,
687 }
catch (
const std::exception& e) {
688 BOOST_LOG_SEV(account_handler_lg(), error)
689 << msg.
subject <<
" failed: " << e.what();
690 reply(nats_, msg, select_party_response{
691 .success =
false, .message = e.what()});
697 BOOST_LOG_SEV(account_handler_lg(), debug)
699 auto req = decode<get_account_history_request>(msg);
701 BOOST_LOG_SEV(account_handler_lg(), warn)
702 <<
"Failed to decode: " << msg.
subject;
706 auto ctx_expected = ores::service::service::make_request_context(
707 ctx_, msg, std::optional<ores::security::jwt::jwt_authenticator>{signer_});
709 error_reply(nats_, msg, ctx_expected.error());
712 const auto& ctx = *ctx_expected;
713 service::account_service svc(ctx);
714 auto accounts = svc.get_account_history(req->username);
715 account_version_history avh;
716 int vnum =
static_cast<int>(accounts.size());
717 for (
const auto& a : accounts) {
723 avh.versions.push_back(std::move(av));
725 BOOST_LOG_SEV(account_handler_lg(), debug)
726 <<
"Completed " << msg.
subject;
727 reply(nats_, msg, get_account_history_response{
729 .history = std::move(avh)});
730 }
catch (
const std::exception& e) {
731 BOOST_LOG_SEV(account_handler_lg(), error)
732 << msg.
subject <<
" failed: " << e.what();
733 reply(nats_, msg, get_account_history_response{
734 .success =
false, .message = e.what()});
742 domain::token_settings token_settings_;
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:118
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
Service for managing typed system settings.
Definition system_settings_service.hpp:41