ores.iam.session

Table of Contents

A user session: one login or service-account connection, recorded for analytics, session listing and end-time tracking. The table is a TimescaleDB hypertable partitioned by start_time with 7-day chunks (see projects/ores.sql/create/iam/iam_sessions_create.sql).

The table has no valid_from=/=valid_to, no GIST exclusion, no version column and no audit tail, and it keys on (id, start_time) rather than a single surrogate key – the partition column must sit in the primary key of a hypertable. The :current_state: and :hypertable: flags in the * SQL ** Flags drawer select exactly that shape. The compound key is what makes this the first entity in the estate whose key carries a timestamp; the mapper, entity and generator templates gained the timestamp key-column branches they were missing.

end_time is text not null default '', not a nullable timestamp, and client_ip is text, not inet. The model mirrors each column's real type; the domain projection is the column's type too, so a session's end_time is an empty string while the session is active and client_ip is a string. The richer readings (an instant, an IP address) belong to the consumer that needs them.

The hand-written domain struct also carried party_id, visible_party_ids and username. No column backs any of them; they are the denormalised, party-scoped shape of a session, and the rule that an entity describes its table puts them on a message instead. They are declared as message fields on session_view in ores.iam.session_messages.

The entity's canonical CRUD subjects are generated: session_registrar owns list/get/get-many/put/put-many/delete on iam.v1.sessions.*. The operation models declare a different set under the same prefix – iam.v1.sessions.active and iam.v1.sessions.samples – so the two coexist rather than compete, and no handler is suppressed here.

1. Flags

2. Natural keys

3. Columns

3.1. id

Unique identifier for the session.

3.2. start_time

Timestamp when the session started (login time). It is the hypertable's partition column, so it is part of the primary key.

3.3. account_id

Foreign key referencing the associated account.

3.4. end_time

Timestamp when the session ended (logout or disconnect), stored as an ISO 8601 string. Empty while the session is still active.

std::string("")

3.5. client_ip

Client IP address (IPv4 or IPv6).

boost::asio::ip::make_address("192.168.1.100")

3.6. client_identifier

Client identifier string from the handshake, typically the client application name.

std::string(faker::word::noun()) + " Client"

3.7. client_version_major

Client protocol version major number.

1

3.8. client_version_minor

Client protocol version minor number.

3.9. bytes_sent

Total bytes sent to the client during this session.

3.10. bytes_received

Total bytes received from the client during this session.

3.11. country_code

ISO 3166-1 alpha-2 country code from geolocation. Empty if geolocation is unavailable or the IP is private or localhost.

std::string("GB")

3.12. protocol

Protocol used for this session: binary or http.

std::string("binary")

4. SQL

4.1. Flags

4.2. Indexes

name columns unique current_only where_extra
tenant tenant_id, start_time desc false false  
account_id account_id, start_time desc false false  
active account_id false false end_time = ''
country country_code, start_time desc false false country_code != ''
protocol protocol, start_time desc false false  

5. C++

5.1. Flags

5.2. Repository

5.3. Domain includes

#include <boost/asio/ip/address.hpp>
#include <boost/uuid/uuid.hpp>
#include <chrono>
#include <cstdint>
#include <string>

5.4. Entity includes

#include <cstdint>
#include <string>
#include "sqlgen/Timestamp.hpp"
#include "sqlgen/PrimaryKey.hpp"

5.5. Conventions

5.6. Table display

column header
id ID (UUID)
account_id Account
start_time Start
end_time End
client_ip Client IP
client_identifier Client
country_code Country
protocol Protocol
bytes_sent Bytes Sent
bytes_received Bytes Received

5.7. Presentation

The screen's declaration. A session is addressed by its id, which is the session's own identifier: the storage key is composite because the table is a hypertable partitioned by start_time, and a partition column has to be part of a primary key. That composite addresses a row for the store; it is not what an operator means by a session, and it is not what a path segment can carry.

The screen is read-only. A session is opened by signing in and closed by the service that ends it, so there is nothing for a person to write here; what the screen is for is seeing who is connected and from where.

5.7.1. Detail fields

field label widget type is_key is_required placeholder
id ID idEdit line_edit true true Enter the session id
account_id Account accountIdEdit line_edit      
start_time Start startTimeEdit date      
end_time End endTimeEdit date      
client_ip Client IP clientIpEdit line_edit      
client_identifier Client clientIdentifierEdit line_edit      
country_code Country countryCodeEdit line_edit      
protocol Protocol protocolEdit line_edit      

5.7.2. Columns

enum_name field header type width
AccountId account_id Account string 300
StartTime start_time Start timestamp 160
EndTime end_time End timestamp 160
ClientIp client_ip Client IP string 150
CountryCode country_code Country string 90
Protocol protocol Protocol string 110
BytesSent bytes_sent Bytes Sent int 110
BytesReceived bytes_received Bytes Recv. int 110

5.8. Paste blocks

5.8.1. repository_reads

The generated CRUD set reads by compound primary key and orders by it. The consumers on the session surface read by account, by active state, by session id alone (the logout path needs the start_time before it can address the row), and update end_time and the byte counters in place on the hot path. Those reads and the two in-place updates stay declared here, over the generated entity and mapper, rather than forcing the consumers onto full-row upserts.

#include "ores.platform/time/datetime.hpp"
#include <boost/lexical_cast.hpp>
#include <boost/uuid/uuid_io.hpp>
std::optional<domain::session> read(context ctx, const boost::uuids::uuid& session_id);

void update_bytes(context ctx,
                  const boost::uuids::uuid& session_id,
                  const std::chrono::system_clock::time_point& start_time,
                  std::uint64_t bytes_sent,
                  std::uint64_t bytes_received);

void end_session(context ctx,
                 const boost::uuids::uuid& session_id,
                 const std::chrono::system_clock::time_point& start_time,
                 const std::chrono::system_clock::time_point& end_time,
                 std::uint64_t bytes_sent,
                 std::uint64_t bytes_received);

std::vector<domain::session> read_by_account(context ctx,
                                             const boost::uuids::uuid& account_id,
                                             std::uint32_t limit = 0,
                                             std::uint32_t offset = 0);

std::vector<domain::session> read_active_by_account(context ctx,
                                                    const boost::uuids::uuid& account_id);

std::uint32_t count_by_account(context ctx, const boost::uuids::uuid& account_id);

std::vector<domain::session> read_all_active(context ctx);
std::optional<domain::session>
session_repository::read(context ctx, const boost::uuids::uuid& session_id) {
    const auto session_id_str = boost::lexical_cast<std::string>(session_id);
    const auto query = sqlgen::read<std::vector<session_entity>> |
                       where("id"_c == session_id_str) |
                       limit(static_cast<std::size_t>(1));

    const auto r = sqlgen::session(ctx.connection_pool()).and_then(query);
    ensure_success(r, lg());

    if (r->empty()) {
        return std::nullopt;
    }

    return session_mapper::map(r->front());
}

void session_repository::update_bytes(context ctx,
                                      const boost::uuids::uuid& session_id,
                                      const std::chrono::system_clock::time_point& start_time,
                                      std::uint64_t bytes_sent,
                                      std::uint64_t bytes_received) {
    const auto session_id_str = boost::lexical_cast<std::string>(session_id);
    const auto start_time_str = ores::platform::time::datetime::to_db_string(start_time);

    const auto query = sqlgen::update<session_entity>(
                           "bytes_sent"_c.set(static_cast<std::int64_t>(bytes_sent)),
                           "bytes_received"_c.set(static_cast<std::int64_t>(bytes_received))) |
                       where("id"_c == session_id_str && "start_time"_c == start_time_str);

    const auto r = sqlgen::session(ctx.connection_pool())
                       .and_then(begin_transaction)
                       .and_then(query)
                       .and_then(commit);
    ensure_success(r, lg());
}

void session_repository::end_session(context ctx,
                                     const boost::uuids::uuid& session_id,
                                     const std::chrono::system_clock::time_point& start_time,
                                     const std::chrono::system_clock::time_point& end_time,
                                     std::uint64_t bytes_sent,
                                     std::uint64_t bytes_received) {
    const auto session_id_str = boost::lexical_cast<std::string>(session_id);
    const auto start_time_str = ores::platform::time::datetime::to_db_string(start_time);
    const auto end_time_str = ores::platform::time::datetime::to_db_string(end_time);

    const auto query = sqlgen::update<session_entity>(
                           "end_time"_c.set(end_time_str),
                           "bytes_sent"_c.set(static_cast<std::int64_t>(bytes_sent)),
                           "bytes_received"_c.set(static_cast<std::int64_t>(bytes_received))) |
                       where("id"_c == session_id_str && "start_time"_c == start_time_str);

    const auto r = sqlgen::session(ctx.connection_pool())
                       .and_then(begin_transaction)
                       .and_then(query)
                       .and_then(commit);
    ensure_success(r, lg());
}

std::vector<domain::session> session_repository::read_by_account(context ctx,
                                                                 const boost::uuids::uuid& account_id,
                                                                 std::uint32_t limit_count,
                                                                 std::uint32_t offset_count) {
    const auto account_id_str = boost::lexical_cast<std::string>(account_id);

    std::vector<session_entity> entities;
    if (limit_count > 0) {
        const auto query =
            sqlgen::read<std::vector<session_entity>> | where("account_id"_c == account_id_str) |
            order_by("start_time"_c.desc()) | sqlgen::offset(static_cast<std::size_t>(offset_count)) |
            sqlgen::limit(static_cast<std::size_t>(limit_count));
        const auto r = sqlgen::session(ctx.connection_pool()).and_then(query);
        ensure_success(r, lg());
        entities = *r;
    } else {
        const auto query =
            sqlgen::read<std::vector<session_entity>> | where("account_id"_c == account_id_str) |
            order_by("start_time"_c.desc()) | sqlgen::offset(static_cast<std::size_t>(offset_count));
        const auto r = sqlgen::session(ctx.connection_pool()).and_then(query);
        ensure_success(r, lg());
        entities = *r;
    }

    return session_mapper::map(entities);
}

std::vector<domain::session>
session_repository::read_active_by_account(context ctx, const boost::uuids::uuid& account_id) {
    const auto account_id_str = boost::lexical_cast<std::string>(account_id);
    const std::string empty_end_time;
    const auto query = sqlgen::read<std::vector<session_entity>> |
                       where("account_id"_c == account_id_str && "end_time"_c == empty_end_time) |
                       order_by("start_time"_c.desc());

    const auto r = sqlgen::session(ctx.connection_pool()).and_then(query);
    ensure_success(r, lg());

    return session_mapper::map(*r);
}

std::uint32_t session_repository::count_by_account(context ctx,
                                                   const boost::uuids::uuid& account_id) {
    const auto account_id_str = boost::lexical_cast<std::string>(account_id);

    struct count_result {
        long long count;
    };

    const auto query = sqlgen::select_from<session_entity>(sqlgen::count().as<"count">()) |
                       where("account_id"_c == account_id_str) | sqlgen::to<count_result>;

    const auto r = sqlgen::session(ctx.connection_pool()).and_then(query);
    ensure_success(r, lg());

    return static_cast<std::uint32_t>(r->count);
}

std::vector<domain::session> session_repository::read_all_active(context ctx) {
    const std::string empty_end_time;
    const auto query = sqlgen::read<std::vector<session_entity>> |
                       where("end_time"_c == empty_end_time) | order_by("start_time"_c.desc());

    const auto r = sqlgen::session(ctx.connection_pool()).and_then(query);
    ensure_success(r, lg());

    return session_mapper::map(*r);
}

6. See also

  • ores.iam — component group overview.

Emacs 29.3 (Org mode 9.6.15)