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;
59
60class bootstrap_handler {
61public:
62 bootstrap_handler(ores::nats::service::client& nats,
65 : nats_(nats), ctx_(std::move(ctx)), signer_(std::move(signer)) {}
66
67 void status(ores::nats::message msg) {
68 using namespace ores::logging;
69 BOOST_LOG_SEV(bootstrap_handler_lg(), debug)
70 << "Handling " << msg.subject;
71 try {
72 auto auth_svc =
73 std::make_shared<service::authorization_service>(ctx_);
74 service::bootstrap_mode_service bms(
75 ctx_, ctx_.tenant_id().to_string(), auth_svc);
76 BOOST_LOG_SEV(bootstrap_handler_lg(), debug)
77 << "Completed " << msg.subject;
78 reply(nats_, msg, bootstrap_status_response{
79 .is_in_bootstrap_mode = bms.is_in_bootstrap_mode()});
80 } catch (const std::exception& e) {
81 BOOST_LOG_SEV(bootstrap_handler_lg(), error)
82 << msg.subject << " failed: " << e.what();
83 reply(nats_, msg, bootstrap_status_response{
84 .is_in_bootstrap_mode = false,
85 .message = e.what()});
86 }
87 }
88
89 void create_admin(ores::nats::message msg) {
90 using namespace ores::logging;
91 BOOST_LOG_SEV(bootstrap_handler_lg(), debug)
92 << "Handling " << msg.subject;
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 using namespace ores::logging;
155 BOOST_LOG_SEV(bootstrap_handler_lg(), debug)
156 << "Handling " << msg.subject;
157 auto req = decode<provision_tenant_request>(msg);
158 if (!req) {
159 BOOST_LOG_SEV(bootstrap_handler_lg(), warn)
160 << "Failed to decode: " << msg.subject;
161 return;
162 }
163 try {
165 using ores::database::repository::execute_parameterized_string_query;
166
167 // Call the SQL provisioner in system tenant context. The procedure
168 // creates the tenant, copies all system-tenant reference data (roles,
169 // permissions, lookup tables), seeds the WRLD business centre, creates
170 // the system party, and sets the bootstrap mode flag.
171 auto sys_ctx = tenant_context::with_system_tenant(ctx_);
172 // The actor for the provisioning operation is the IAM service
173 // account, not the new tenant admin (req->principal). The admin
174 // username is not yet a known account at this point and would
175 // fail the modified_by validation in the SQL trigger.
176 // Use ctx_.service_account() so the name is always env-scoped and
177 // matches what iam_service_accounts_populate.sql registered.
178 const auto results = execute_parameterized_string_query(sys_ctx,
179 "SELECT ores_iam_provision_tenant_fn($1, $2, $3, $4, $5, $6)::text",
180 {req->type, req->code, req->name, req->hostname,
181 req->description, ctx_.service_account()},
182 bootstrap_handler_lg(), "Provisioning tenant");
183
184 if (results.empty()) {
185 reply(nats_, msg, provision_tenant_response{
186 .success = false,
187 .error_message = "Provisioner returned no tenant_id"});
188 return;
189 }
190
191 const auto& tenant_id_str = results[0];
192 BOOST_LOG_SEV(bootstrap_handler_lg(), info)
193 << "Provisioned tenant: " << req->code
194 << " (id: " << tenant_id_str << ")";
195
196 // Create the admin account in the new tenant's context.
197 auto tenant_ctx = tenant_context::with_tenant(ctx_, tenant_id_str);
198 service::account_service svc(tenant_ctx);
199 auto acct = svc.create_account(
200 req->principal, req->email, req->password,
201 ctx_.service_account());
202
203 // Associate the admin account with the system party.
204 refdata::repository::party_repository party_repo(tenant_ctx);
205 auto system_parties = party_repo.read_system_party(tenant_id_str);
206 if (!system_parties.empty()) {
207 const auto& sys_party = system_parties.front();
208 domain::account_party ap;
209 ap.account_id = acct.id;
210 ap.party_id = sys_party.id;
211 ap.tenant_id = tenant_id_str;
212 ap.modified_by = req->principal;
213 ap.performed_by = req->principal;
214 ap.change_reason_code = "system.initial_load";
215 ap.change_commentary =
216 "Provision tenant: associate admin with system party";
217 service::account_party_service ap_svc(tenant_ctx);
218 ap_svc.save_account_party(ap);
219 BOOST_LOG_SEV(bootstrap_handler_lg(), info)
220 << "Associated " << req->principal
221 << " with system party "
222 << boost::uuids::to_string(sys_party.id);
223 } else {
224 BOOST_LOG_SEV(bootstrap_handler_lg(), warn)
225 << "No system party found for tenant " << tenant_id_str
226 << "; account has no party context";
227 }
228
229 BOOST_LOG_SEV(bootstrap_handler_lg(), debug)
230 << "Completed " << msg.subject;
231 reply(nats_, msg, provision_tenant_response{
232 .success = true,
233 .account_id = boost::uuids::to_string(acct.id),
234 .tenant_id = tenant_id_str});
235 } catch (const std::exception& e) {
236 BOOST_LOG_SEV(bootstrap_handler_lg(), error)
237 << msg.subject << " failed: " << e.what();
238 reply(nats_, msg, provision_tenant_response{
239 .success = false, .error_message = e.what()});
240 }
241 }
242
243private:
247};
248
249} // namespace ores::iam::messaging
250#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