ORE Studio 0.0.4
Loading...
Searching...
No Matches
bootstrap_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_BOOTSTRAP_HANDLER_HPP
21#define ORES_IAM_MESSAGING_BOOTSTRAP_HANDLER_HPP
22
23#include <memory>
24#include <stdexcept>
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.security/jwt/jwt_authenticator.hpp"
30#include "ores.service/messaging/handler_helpers.hpp"
31#include "ores.iam.api/messaging/bootstrap_protocol.hpp"
32#include "ores.iam.api/domain/account_party.hpp"
33#include "ores.iam.core/service/account_service.hpp"
34#include "ores.iam.core/service/account_party_service.hpp"
35#include "ores.iam.core/service/authorization_service.hpp"
36#include "ores.iam.core/service/bootstrap_mode_service.hpp"
37#include "ores.database/repository/bitemporal_operations.hpp"
38#include "ores.database/service/tenant_context.hpp"
39#include "ores.security/crypto/password_hasher.hpp"
40#include "ores.refdata.api/domain/party.hpp"
41#include "ores.refdata.core/repository/party_repository.hpp"
42#include "ores.utility/uuid/tenant_id.hpp"
43#include <boost/uuid/uuid_io.hpp>
44
45namespace ores::iam::messaging {
46
47namespace {
48
49inline auto& bootstrap_handler_lg() {
50 static auto instance = ores::logging::make_logger(
51 "ores.iam.messaging.bootstrap_handler");
52 return instance;
53}
54
55} // namespace
56
57using ores::service::messaging::reply;
58using ores::service::messaging::decode;
59using ores::service::messaging::log_handler_entry;
60using namespace ores::logging;
61
62class bootstrap_handler {
63public:
64 bootstrap_handler(ores::nats::service::client& nats,
67 : nats_(nats), ctx_(std::move(ctx)), signer_(std::move(signer)) {}
68
69 void status(ores::nats::message msg) {
70 [[maybe_unused]] const auto correlation_id =
71 log_handler_entry(bootstrap_handler_lg(), msg);
72 try {
73 auto auth_svc =
74 std::make_shared<service::authorization_service>(ctx_);
75 service::bootstrap_mode_service bms(
76 ctx_, ctx_.tenant_id().to_string(), auth_svc);
77 BOOST_LOG_SEV(bootstrap_handler_lg(), debug)
78 << "Completed " << msg.subject;
79 reply(nats_, msg, bootstrap_status_response{
80 .is_in_bootstrap_mode = bms.is_in_bootstrap_mode()});
81 } catch (const std::exception& e) {
82 BOOST_LOG_SEV(bootstrap_handler_lg(), error)
83 << msg.subject << " failed: " << e.what();
84 reply(nats_, msg, bootstrap_status_response{
85 .is_in_bootstrap_mode = false,
86 .message = e.what()});
87 }
88 }
89
90 void create_admin(ores::nats::message msg) {
91 [[maybe_unused]] const auto correlation_id =
92 log_handler_entry(bootstrap_handler_lg(), msg);
93 auto req = decode<create_initial_admin_request>(msg);
94 if (!req) {
95 BOOST_LOG_SEV(bootstrap_handler_lg(), warn)
96 << "Failed to decode: " << msg.subject;
97 return;
98 }
99
100 // Guard: reject if system is not in bootstrap mode.
101 {
102 auto auth_svc =
103 std::make_shared<service::authorization_service>(ctx_);
104 service::bootstrap_mode_service bms(
105 ctx_, ctx_.tenant_id().to_string(), auth_svc);
106 if (!bms.is_in_bootstrap_mode()) {
107 BOOST_LOG_SEV(bootstrap_handler_lg(), warn)
108 << "Rejected " << msg.subject
109 << ": system is not in bootstrap mode";
110 reply(nats_, msg, create_initial_admin_response{
111 .success = false,
112 .error_message = "System is not in bootstrap mode"});
113 return;
114 }
115 }
116
117 try {
118 using ores::database::repository::execute_parameterized_string_query;
119
120 // Hash the password in C++ — pgcrypto is not required in the DB.
121 const auto password_hash =
123
124 // The stored procedure creates the account, assigns SuperAdmin,
125 // associates with the system party, and exits bootstrap mode.
126 const auto results = execute_parameterized_string_query(ctx_,
127 "SELECT ores_iam_create_initial_admin_fn($1, $2, $3, $4)::text",
128 {req->principal, req->email, password_hash, req->principal},
129 bootstrap_handler_lg(), "Creating initial admin");
130
131 if (results.empty()) {
132 reply(nats_, msg, create_initial_admin_response{
133 .success = false,
134 .error_message = "Procedure returned no account_id"});
135 return;
136 }
137
138 const auto& account_id_str = results[0];
139 BOOST_LOG_SEV(bootstrap_handler_lg(), debug)
140 << "Completed " << msg.subject;
141 reply(nats_, msg, create_initial_admin_response{
142 .success = true,
143 .account_id = account_id_str,
144 .tenant_id = ctx_.tenant_id().to_string()});
145 } catch (const std::exception& e) {
146 BOOST_LOG_SEV(bootstrap_handler_lg(), error)
147 << msg.subject << " failed: " << e.what();
148 reply(nats_, msg, create_initial_admin_response{
149 .success = false, .error_message = e.what()});
150 }
151 }
152
153 void provision_tenant(ores::nats::message msg) {
154 [[maybe_unused]] const auto correlation_id =
155 log_handler_entry(bootstrap_handler_lg(), msg);
156 auto req = decode<provision_tenant_request>(msg);
157 if (!req) {
158 BOOST_LOG_SEV(bootstrap_handler_lg(), warn)
159 << "Failed to decode: " << msg.subject;
160 return;
161 }
162 try {
164 using ores::database::repository::execute_parameterized_string_query;
165
166 // Call the SQL provisioner in system tenant context. The procedure
167 // creates the tenant, copies all system-tenant reference data (roles,
168 // permissions, lookup tables), seeds the WRLD business centre, creates
169 // the system party, and sets the bootstrap mode flag.
170 auto sys_ctx = tenant_context::with_system_tenant(ctx_);
171 // The actor for the provisioning operation is the IAM service
172 // account, not the new tenant admin (req->principal). The admin
173 // username is not yet a known account at this point and would
174 // fail the modified_by validation in the SQL trigger.
175 // Use ctx_.service_account() so the name is always env-scoped and
176 // matches what iam_service_accounts_populate.sql registered.
177 const auto results = execute_parameterized_string_query(sys_ctx,
178 "SELECT ores_iam_provision_tenant_fn($1, $2, $3, $4, $5, $6)::text",
179 {req->type, req->code, req->name, req->hostname,
180 req->description, ctx_.service_account()},
181 bootstrap_handler_lg(), "Provisioning tenant");
182
183 if (results.empty()) {
184 reply(nats_, msg, provision_tenant_response{
185 .success = false,
186 .error_message = "Provisioner returned no tenant_id"});
187 return;
188 }
189
190 const auto& tenant_id_str = results[0];
191 BOOST_LOG_SEV(bootstrap_handler_lg(), info)
192 << "Provisioned tenant: " << req->code
193 << " (id: " << tenant_id_str << ")";
194
195 // Create the admin account in the new tenant's context.
196 auto tenant_ctx = tenant_context::with_tenant(ctx_, tenant_id_str);
197 service::account_service svc(tenant_ctx);
198 auto acct = svc.create_account(
199 req->principal, req->email, req->password,
200 ctx_.service_account());
201
202 // Associate the admin account with the system party.
203 refdata::repository::party_repository party_repo(tenant_ctx);
204 auto system_parties = party_repo.read_system_party(tenant_id_str);
205 if (!system_parties.empty()) {
206 const auto& sys_party = system_parties.front();
207 domain::account_party ap;
208 ap.account_id = acct.id;
209 ap.party_id = sys_party.id;
210 ap.tenant_id = tenant_id_str;
211 ap.modified_by = req->principal;
212 ap.performed_by = req->principal;
213 ap.change_reason_code = "system.initial_load";
214 ap.change_commentary =
215 "Provision tenant: associate admin with system party";
216 service::account_party_service ap_svc(tenant_ctx);
217 ap_svc.save_account_party(ap);
218 BOOST_LOG_SEV(bootstrap_handler_lg(), info)
219 << "Associated " << req->principal
220 << " with system party "
221 << boost::uuids::to_string(sys_party.id);
222 } else {
223 BOOST_LOG_SEV(bootstrap_handler_lg(), warn)
224 << "No system party found for tenant " << tenant_id_str
225 << "; account has no party context";
226 }
227
228 BOOST_LOG_SEV(bootstrap_handler_lg(), debug)
229 << "Completed " << msg.subject;
230 reply(nats_, msg, provision_tenant_response{
231 .success = true,
232 .account_id = boost::uuids::to_string(acct.id),
233 .tenant_id = tenant_id_str});
234 } catch (const std::exception& e) {
235 BOOST_LOG_SEV(bootstrap_handler_lg(), error)
236 << msg.subject << " failed: " << e.what();
237 reply(nats_, msg, provision_tenant_response{
238 .success = false, .error_message = e.what()});
239 }
240 }
241
242private:
246};
247
248} // namespace ores::iam::messaging
249#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
Reads and writes parties to data storage.
Definition party_repository.hpp:37
static std::string hash(const std::string &password)
Creates a password hash from the given password.
Definition password_hasher.cpp:50
JWT authentication primitive.
Definition jwt_authenticator.hpp:45