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:

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
email Email
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
email Email 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
Email email Email 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.

Emacs 29.3 (Org mode 9.6.15)