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 a nullable timestamp with time zone: a session that has not ended has no closing instant at all, so the column holds NULL and the domain and protocol types hold std::optional<time_point>. There is no empty-string spelling and no other sentinel for "still open"; absence is the representation, and the wire carries null. client_ip is text, not inet, so the richer reading of an IP address belongs to the consumer that needs it.

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.ops.get_active_sessions and iam.v1.ops.get_session_samples – so the two coexist rather than compete, and no handler is suppressed here.

1. Flags

The row names the account a session belongs to, where it came from and how much it moved, so the generated reads are not open to every signed-in caller: the generated iam.v1.sessions.list, .get and .get_many handlers require iam::sessions:read before they serve anything, as every generated read requires its resource's read code.

:schema: public :product: ores :component: iam :profile: readonly-timeseries

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. A list may be ordered by it, so an account's sessions read newest first.

3.3. account_id

Foreign key referencing the associated account. A list may be filtered by it, so an account's page reads that account's sessions and no other.

3.4. end_time

Timestamp when the session ended (logout or disconnect). NULL while the session is still open, because an open session has no closing instant.

std::chrono::system_clock::now()

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 is null
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 <chrono>
#include <boost/asio/ip/address.hpp>
#include <boost/uuid/uuid.hpp>
#include <cstdint>
#include <optional>
#include <string>

5.4. Entity includes

#include <cstdint>
#include <string>

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.hpp>
#include <boost/uuid/uuid_io.hpp>
#include <chrono>
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 = query_builder::read<std::vector<session_entity>> |
                       query_builder::where("id"_c == session_id_str) | query_builder::limit(1);

    const auto sessions = query_builder::execute_read_query<session_entity, domain::session>(
        ctx,
        query,
        [](const auto& entities) { return session_mapper::map(entities); },
        lg(),
        "Reading session");
    if (sessions.empty())
        return std::nullopt;
    return sessions.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) {
    query_builder::execute_parameterized_multi_column_query(
        ctx,
        std::string("update ") + session_entity::tablename +
            " set bytes_sent = $1::bigint, bytes_received = $2::bigint"
            " where id = $3::uuid and start_time = $4::timestamptz",
        {std::to_string(bytes_sent),
         std::to_string(bytes_received),
         boost::lexical_cast<std::string>(session_id),
         ores::platform::time::datetime::to_db_string(start_time)},
        lg(),
        "Updating session bytes");
}

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) {
    query_builder::execute_parameterized_multi_column_query(
        ctx,
        std::string("update ") + session_entity::tablename +
            " set end_time = $1::timestamptz, bytes_sent = $2::bigint,"
            " bytes_received = $3::bigint where id = $4::uuid and start_time = $5::timestamptz",
        {ores::platform::time::datetime::to_db_string(end_time),
         std::to_string(bytes_sent),
         std::to_string(bytes_received),
         boost::lexical_cast<std::string>(session_id),
         ores::platform::time::datetime::to_db_string(start_time)},
        lg(),
        "Ending session");
}

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);
    auto query = query_builder::read<std::vector<session_entity>> |
                 query_builder::where("account_id"_c == account_id_str) |
                 query_builder::order_by("start_time"_c.desc()) |
                 query_builder::offset(offset_count);
    if (limit_count > 0)
        query = std::move(query) | query_builder::limit(limit_count);

    return query_builder::execute_read_query<session_entity, domain::session>(
        ctx,
        query,
        [](const auto& entities) { return session_mapper::map(entities); },
        lg(),
        "Reading sessions by account");
}

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 auto query =
        query_builder::read<std::vector<session_entity>> |
        query_builder::where("account_id"_c == account_id_str && query_builder::is_null("end_time")) |
        query_builder::order_by("start_time"_c.desc());

    return query_builder::execute_read_query<session_entity, domain::session>(
        ctx,
        query,
        [](const auto& entities) { return session_mapper::map(entities); },
        lg(),
        "Reading active sessions by account");
}

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);
    const auto query = query_builder::read<std::vector<session_entity>>;
    return query_builder::execute_count_query(
        ctx, query, "account_id"_c == account_id_str, lg(), "Counting sessions by account");
}

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

    return query_builder::execute_read_query<session_entity, domain::session>(
        ctx,
        query,
        [](const auto& entities) { return session_mapper::map(entities); },
        lg(),
        "Reading all active sessions");
}

6. See also

  • ores.iam — component group overview.

Emacs 29.3 (Org mode 9.6.15)