ores.iam.account
Table of Contents
An account that can authenticate against the system: one row per user,
service, algorithm or LLM identity, carrying the name it signs in with,
the email address and the optional profile and reporting links. The table
is bi-temporal and audited (see
projects/ores.sql/create/iam/iam_accounts_create.sql): it carries
version, the four audit columns and the valid_from=/=valid_to pair
with the GIST exclusion, so the model takes the ordinary audited shape
and needs no shape flag.
The secret material lives apart, in ores.iam.account_credential. Held here, a profile write replaced the whole row, and the column the domain type could not carry came back empty. Every column this model declares reaches the domain type, so a whole-row write is lossless, and no write through this entity can touch a credential.
The table is a composite parent: ores_iam_accounts_touch_version_fn
lets a child entity (account contact information, party association)
bump this account's own version when the child is written. The model
declares :generate_touch_function: true, which renders that function
under its existing name rather than leaving it hand-written.
The model describes the table and nothing else. image_id and
reports_to_account_id are nullable UUID soft references. The hand-written
domain struct represented both as a plain boost::uuids::uuid with a nil
sentinel, on the claim that a second
std::optional<boost::uuids::uuid> member corrupts
aggregate serialisation for multi-element vectors. Re-verified under
the generated estate: all three nullable UUIDs are modelled as
std::optional<boost::uuids::uuid>, and the api suite's multi-element
JSON and table tests plus the core repository's five-account round trip
pass, so the workaround is not needed here.
The generated read surface is live, and it does not collide with the
hand-written one. The hand-written
account_operations_protocol.hpp owns the writes,
iam.v1.accounts.{save,delete,update,lock,unlock,change-password,reset-password,select-party,set-default-party,switch-party,update-email,publish-from-dq},
and the generated account_protocol.hpp owns the reads,
iam.v1.accounts.list and iam.v1.accounts.get, with the version reads
alongside them. Both registrars are composed in
ores.iam/core/src/messaging/registrar.cpp. This entity sets
:read_only: true, so the generated half carries no write verb and the split
falls out of the flag rather than out of a suppression.
The row holds the account holder's own name and mail address, so the
generated reads are not open to every signed-in caller: the generated
iam.v1.accounts.list and iam.v1.accounts.get
handlers require iam::accounts:read before they serve anything, as every
generated read requires its resource's read code. See
Authorised reads.
Two behavioural facets are switched off, each with a reason:
- The entity's CRUD handler and sub-registrar, because the hand-written
account_operations_handleralready owns the write verbs. - The generated CRUD service, because the hand-written
account_operations_serviceis the authentication surface (login, lock, unlock, password change and reset, party selection, service-credential check) and the generated service'sget_account_history(id)collides in name and signature with the hand-writtenget_account_history(username)while meaning a different read.
1. Flags
2. Columns
2.1. id
Unique identifier for the account.
2.2. account_type
Account type classification. Determines account capabilities: user
accounts can login with password, while service, algorithm and
llm accounts authenticate via sessions.
std::string("user")
2.3. account_status
Lifecycle status of the account, validated against ores.iam.account_status.
An account is active when it has everything it needs to sign in. It is
pending when an administrator has not finished setting it up: the account
exists, but it holds no party association, so sign-in refuses it with its own
code.
An account that registers into a tenant which nominates no default party is
created pending. An administrator who gives it its first party moves it to
active; that transition belongs to the roster's journey and not to a timer.
std::string("active")
2.4. username
Unique username for login purposes. It is the natural key, so the
generated table adds the partial unique index on (tenant_id, username)
the hand-written table already had.
std::string(faker::internet::username(std::string(faker::person::firstName()), std::string(faker::person::lastName())))
2.5. full_name
The account holder's full (real) name. Not every account represents a person (service, algorithm and llm accounts leave this empty), and this is the only place a human account's real name is recorded.
std::string(faker::person::firstName()) + " " + std::string(faker::person::lastName())
2.6. email
Email address associated with the account. It is unique within a
tenant, so the generated table adds the partial unique index on
(tenant_id, email) the hand-written table already had.
std::string(faker::internet::email(std::string(faker::person::firstName()), std::string(faker::person::lastName())))
2.7. default_party_id
Party to log into automatically when quick-login is enabled. Soft reference to a party owned by ores.refdata; unset means the account must always go through the party picker.
2.8. image_id
Profile picture for this account. Soft reference to an image owned by ores.assets; unset means the account has no profile picture.
2.9. picture_code
The image code this account wants as its profile picture, such as
acme_staff_photo:female_age20_european_02. The reconcile attaches the
image the code names, so the wanted picture is data about the account
rather than something a handler reads out of a staging table. Unset means
the account wants no picture.
2.10. job_title
Job title / functional role of the person holding this account (for example "Head of Desk", "Senior Trader"). Distinct from the RBAC role assignments, which grant coarse permission sets rather than describe what the person actually does.
faker::person::jobTitle()
2.11. reports_to_account_id
The account this person reports to, capturing the functional reporting line. Soft self-reference to another row in this same table; unset means no reporting line is recorded.
2.12. shared
Whether every tenant may read this account. A shared account is owned by the system tenant: the service accounts, which write under their own name into the history of every tenant, and the super administrator. A history names its author for every reader only if the reader can read that author's account.
The flag is the whole rule. The row-level policy admits a system tenant row to another tenant only when this column is true, so a later human account in the system tenant stays hidden until the flag is set. A tenant never writes a shared row, and the seed is the only writer of the flag.
3. Artefact columns
The import carries business_unit_code, role, reports_to_username, photo_key and password_hash, all five written by the Acme populate. business_unit_code is informational only today because ores_iam_accounts_tbl has no business-unit FK, and reports_to_username is resolved to reports_to_account_id at publish. password_hash is empty in the Acme data: the publish still writes the account's credential row, but with no hash, and the set_staff_passwords provisioning step that follows sets the password through the server's own API, so no password material is staged. The service password hash and the TOTP secret are deliberately absent: staging is not where those belong.
3.1. id
3.2. version
3.3. username
3.4. full_name
3.5. email
3.6. password_hash
3.7. account_type
3.8. business_unit_code
3.9. role
3.10. job_title
3.11. reports_to_username
3.12. photo_key
4. SQL
4.1. Flags
5. Foreign keys
5.1. image_id
Profile picture for this account, validated against the images table when set.
5.2. reports_to_account_id
Reporting line, validated against this same table when set.
The line may not close a cycle. The manager existing is the only other check, so without this the store accepts A reports to B while B reports to A, and the hierarchy has no root. The generated trigger walks the manager chain and refuses a write that would reach the row being written.
6. Insert trigger
6.1. Validations
| column | validation_function |
|---|---|
| account_type | ores_iam_validate_account_type_fn |
| account_status | ores_iam_validate_account_status_fn |
7. C++
7.1. Flags
7.2. Repository
The service accounts and the super administrator are shared rows of the system
tenant. :system_tenant_visible: true makes the generated repository read own
tenant OR system tenant rows on every read path. The row-level policy decides
which system tenant rows a reader sees: only those with shared set. Mutations
stay own-tenant scoped.
7.3. Domain includes
#include <boost/uuid/nil_generator.hpp> #include <boost/uuid/uuid.hpp> #include <optional> #include <string>
7.4. Entity includes
#include <string>
7.5. Conventions
7.6. Table display
| column | header |
|---|---|
| id | ID (UUID) |
| username | Username |
| full_name | Full Name |
| job_title | Job Title |
| change_reason_code | Change Reason |
| modified_by | Modified By |
| recorded_at | Recorded At |
| version | Version |
7.7. Presentation
The screen's declaration. An account is addressed by its username, the
natural key, which is what every caller already holds: an operator knows the
name an account signs in with, not its surrogate id. The id stays for
foreign-key stability, which is what it is for, and no caller states it.
username is the key the module's own shell commands already take, so the
declaration states what its callers already assume rather than asking them to
translate.
7.7.1. Detail fields
| field | label | widget | type | is_key | is_required | placeholder |
|---|---|---|---|---|---|---|
| username | Username | usernameEdit | line_edit | true | true | Enter the account name |
| full_name | Full Name | fullNameEdit | line_edit | Enter the full name | ||
| emailEdit | line_edit | Enter the email address | ||||
| job_title | Job Title | jobTitleEdit | line_edit | Enter the job title | ||
| account_type | Type | accountTypeEdit | line_edit | Enter the account type |
7.7.2. Columns
| enum_name | field | header | type | width |
|---|---|---|---|---|
| Username | username | Username | string | 200 |
| FullName | full_name | Full Name | string | 240 |
| string | 260 | |||
| JobTitle | job_title | Job Title | string | 200 |
| AccountType | account_type | Type | string | 140 |
7.8. Paste blocks
7.8.1. read_all
Reads every version of every account. The generated CRUD set reads all versions of one account by primary key, which is what the history path needs; the CLI export and the repository test read the whole table, so that unfiltered read stays declared here. It states the session's tenant, because a tenant also reads the shared rows of the system tenant, and an export of a tenant's accounts is not an export of the platform's.
std::vector<domain::account> read_all(context ctx);
std::vector<domain::account> account_repository::read_all(context ctx) { const auto tid = ctx.tenant_id().to_string(); const auto query = query_builder::read<std::vector<account_entity>> | query_builder::where("tenant_id"_c == tid) | query_builder::order_by("valid_from"_c.desc()); return query_builder::execute_read_query<account_entity, domain::account>( ctx, query, [](const auto& entities) { return account_mapper::map(entities); }, lg(), "Reading all accounts."); }
7.8.2. read_latest_by_username
Reads the latest active account with the given username. The generated CRUD set reads by primary key only, and the login and password-reset paths address the row by username, so the read stays declared here. It names the session's tenant. A tenant reads the shared rows of the system tenant too, and a username that a tenant account shares with a shared account must resolve to the tenant's own, so the sign-in lookup never picks a service or the super admin for a tenant user. The login flow takes its tenant from the host name before it looks the account up.
std::vector<domain::account> read_latest_by_username(context ctx, const std::string& username);
std::vector<domain::account> account_repository::read_latest_by_username(context ctx, const std::string& username) { BOOST_LOG_SEV(lg(), debug) << "Reading latest account by username: " << username; static constexpr auto max = db_timestamp::max(); const auto tid = ctx.tenant_id().to_string(); const auto query = query_builder::read<std::vector<account_entity>> | query_builder::where("tenant_id"_c == tid && "username"_c == username && "valid_to"_c == max) | query_builder::order_by("valid_from"_c.desc()); return query_builder::execute_read_query<account_entity, domain::account>( ctx, query, [](const auto& entities) { return account_mapper::map(entities); }, lg(), "Reading latest account by username"); }
7.8.3. read_latest_by_email
Reads the latest active account with the given email. The password-reset and self-service paths address the row by email, so the read stays declared here rather than forcing those callers onto a full table scan. It names the session's tenant, for the same reason as the username read.
std::vector<domain::account> read_latest_by_email(context ctx, const std::string& email);
std::vector<domain::account> account_repository::read_latest_by_email(context ctx, const std::string& email) { BOOST_LOG_SEV(lg(), debug) << "Reading latest account by email: " << email; static constexpr auto max = db_timestamp::max(); const auto tid = ctx.tenant_id().to_string(); const auto query = query_builder::read<std::vector<account_entity>> | query_builder::where("tenant_id"_c == tid && "email"_c == email && "valid_to"_c == max) | query_builder::order_by("valid_from"_c.desc()); return query_builder::execute_read_query<account_entity, domain::account>( ctx, query, [](const auto& entities) { return account_mapper::map(entities); }, lg(), "Reading latest account by email"); }
8. See also
- ores.iam — component group overview.