ores.refdata.sandbox

Table of Contents

A sandbox is a space where actions have no effect outside it: users import samples, try what-ifs and run reports there without touching official data (see Sandbox).

The anchor is the node of the official portfolio tree the sandbox belongs to. It decides who may open the sandbox and who may see it; it does not place the sandbox in the official tree. The sandbox's own portfolios carry its id, and a portfolio always shares its parent's sandbox, so no official portfolio contains a sandbox portfolio.

Opening a sandbox needs the open_sandbox portfolio right at an official anchor; the check runs again when the owner changes. ores_refdata_account_sees_sandbox_fn answers whether an account may see a sandbox: its owner always may; anyone with read at the anchor may when it is shared; its members may when it is shared with members. A restrictive row-level security policy on portfolios applies it to the session's actor through ores_refdata_actor_sees_sandbox_fn, so a sandbox's portfolios are read and written only by those who may see the sandbox, and a session with no actor sees none of them.

Closing a sandbox does not close its portfolios, as for every soft foreign key in the schema. Handing a sandbox to a new owner needs the right at the anchor on the new owner's side; the new owner's consent is not recorded.

1. Flags

2. Columns

2.1. id

UUID uniquely identifying this sandbox.

Surrogate key for the sandbox record.

2.2. name

The sandbox's name.

Unique within the tenant.

std::string("SB-") + std::string(faker::string::alphanumeric(10))

2.3. purpose

What the sandbox is for.

experiment, sample or import.

std::string("experiment")

2.4. anchor_portfolio_id

The official portfolio node the sandbox belongs to.

Fixed for the sandbox's life.

ctx.generate_uuid()

2.5. owner_account_id

The account answerable for the sandbox.

It must hold open_sandbox at the anchor.

ctx.generate_uuid()

2.6. visibility

Who may see the sandbox.

private to the owner; shared with everyone who may read the anchor; members for the named members.

std::string("private")

2.7. status

Whether the sandbox is in use.

open, or archived and read-only.

std::string("open")

2.8. review_date

When the owner must confirm the sandbox is still needed.

std::chrono::year_month_day{std::chrono::floor<std::chrono::days>(ctx.past_timepoint())}

2.9. description

Optional description of the sandbox.

std::optional<std::string>("Test sandbox")

3. SQL

3.1. Flags

3.2. Checks

expression
"purpose" in ('experiment', 'sample', 'import')
"visibility" in ('private', 'shared', 'members')
"status" in ('open', 'archived')

4. Foreign keys

4.1. anchor_portfolio_id

4.2. owner_account_id

The seed is a service account granted open_sandbox at the seeded anchor: the insert trigger refuses an owner without the right, and a user account would end the test tenant's bootstrap.

#include "ores.iam.api/generators/account_generator.hpp"
#include "ores.iam.core/repository/account_repository.hpp"
#include "ores.refdata.api/generators/portfolio_right_generator.hpp"
#include "ores.refdata.core/repository/portfolio_right_repository.hpp"
{
    auto owner = ores::iam::generators::generate_synthetic_account(ctx);
    owner.account_type = "service";
    owner.change_reason_code = "system.test";
    ores::iam::repository::account_repository owner_repo;
    owner_repo.write(party_ctx, owner);
    v.owner_account_id = owner.id;

    auto right = ores::refdata::generators::generate_synthetic_portfolio_right(ctx);
    right.account_id = owner.id;
    right.portfolio_id = v.anchor_portfolio_id;
    right.right_code = "open_sandbox";
    right.change_reason_code = "system.test";
    ores::refdata::repository::portfolio_right_repository right_repo;
    right_repo.write(party_ctx, right);
}

5. Insert trigger

-- The anchor is a node of the official tree, never a sandbox portfolio.
if exists (
    select 1 from ores_refdata_portfolios_tbl
    where tenant_id = NEW.tenant_id
      and id = NEW.anchor_portfolio_id
      and valid_to = ores_utility_infinity_timestamp_fn()
      and sandbox_id is not null
) then
    raise exception 'Invalid anchor_portfolio_id: %. A sandbox is anchored at an official portfolio.',
        NEW.anchor_portfolio_id
        using errcode = '23514';
end if;

-- Opening a sandbox, or handing it to a new owner, needs the right to
-- open sandboxes at the anchor. A version that keeps the owner, such as
-- archiving, does not ask again.
if not exists (
    select 1 from ores_refdata_sandboxes_tbl
    where tenant_id = NEW.tenant_id
      and id = NEW.id
      and owner_account_id = NEW.owner_account_id
      and valid_to = ores_utility_infinity_timestamp_fn()
) and not ores_refdata_account_holds_portfolio_right_fn(
    NEW.tenant_id, NEW.owner_account_id, NEW.anchor_portfolio_id, 'open_sandbox'
) then
    raise exception 'Account % may not open a sandbox at portfolio %: it holds no open_sandbox right there.',
        NEW.owner_account_id, NEW.anchor_portfolio_id
        using errcode = '42501';
end if;
-- Whether an account may see a sandbox: its owner always may; anyone with
-- read at the anchor may when it is shared; its members may when it is
-- shared with members. Runs with the caller's rights, so row-level security
-- confines it to the session's tenant. PL/pgSQL, so the members table, which
-- is created after this one, is resolved when the function runs.
create or replace function ores_refdata_account_sees_sandbox_fn(
    p_tenant_id uuid,
    p_account_id uuid,
    p_sandbox_id uuid
) returns boolean as $$
declare
    v_sandbox record;
begin
    select owner_account_id, anchor_portfolio_id, visibility into v_sandbox
    from ores_refdata_sandboxes_tbl
    where tenant_id = p_tenant_id
      and id = p_sandbox_id
      and valid_to = ores_utility_infinity_timestamp_fn();
    if not found then
        return false;
    end if;
    if v_sandbox.owner_account_id = p_account_id then
        return true;
    end if;
    if v_sandbox.visibility = 'shared' then
        return ores_refdata_account_holds_portfolio_right_fn(
            p_tenant_id, p_account_id, v_sandbox.anchor_portfolio_id, 'read');
    end if;
    if v_sandbox.visibility = 'members' then
        return exists (
            select 1 from ores_refdata_sandbox_members_tbl
            where tenant_id = p_tenant_id
              and sandbox_id = p_sandbox_id
              and account_id = p_account_id
              and valid_to = ores_utility_infinity_timestamp_fn()
        );
    end if;
    return false;
end;
$$ language plpgsql stable set search_path = public, pg_temp;

-- The account of the user the session acts for, from app.current_actor.
-- Security definer and narrow, for the same reason as the liveness check:
-- it answers one id, for the session's own actor.
create or replace function ores_refdata_actor_account_id_fn(
    p_tenant_id uuid
) returns uuid as $$
declare
    v_id uuid;
begin
    select id into v_id
    from ores_iam_accounts_tbl
    where username = ores_iam_current_actor_fn()
      and tenant_id in (p_tenant_id, ores_utility_system_tenant_id_fn())
      and valid_to = ores_utility_infinity_timestamp_fn()
    order by (tenant_id = p_tenant_id) desc
    limit 1;
    return v_id;
end;
$$ language plpgsql stable security definer set search_path = public, pg_temp;

-- Whether the session's actor may see a sandbox. A session with no actor,
-- such as a system process, sees none. The portfolio policy uses it to keep
-- a sandbox's portfolios out of every read and write by anyone else.
create or replace function ores_refdata_actor_sees_sandbox_fn(
    p_tenant_id uuid,
    p_sandbox_id uuid
) returns boolean as $$
declare
    v_account uuid := ores_refdata_actor_account_id_fn(p_tenant_id);
begin
    return v_account is not null
       and ores_refdata_account_sees_sandbox_fn(p_tenant_id, v_account, p_sandbox_id);
end;
$$ language plpgsql stable set search_path = public, pg_temp;

6. C++

6.1. Flags

6.2. Repository

6.3. Domain includes

#include <optional>
#include <string>
#include <boost/uuid/uuid.hpp>

6.4. Entity includes

#include <optional>
#include <string>

6.5. Conventions

6.6. Table display

column header
name Name
purpose Purpose
visibility Visibility
status Status
modified_by Modified By
version Version

6.7. Presentation

6.7.1. Detail fields

field label widget type is_key is_required placeholder
name Name nameEdit line_edit true true Sandbox name
purpose Purpose purposeEdit line_edit false true experiment, sample or import
visibility Visibility visibilityEdit line_edit false true private, shared or members
status Status statusEdit line_edit false true open or archived
description Description descriptionEdit line_edit false false Enter description

6.7.2. Columns

enum_name field header type width
Name name Name string 160
Purpose purpose Purpose string 100
Visibility visibility Visibility string 100
Status status Status string 80
Version version Version int 80
ModifiedBy modified_by Modified By string 120
RecordedAt recorded_at Recorded At timestamp 150

6.8. Paste blocks

6.8.1. Extra drop statements

drop function if exists ores_refdata_actor_sees_sandbox_fn;
drop function if exists ores_refdata_actor_account_id_fn;
drop function if exists ores_refdata_account_sees_sandbox_fn;

7. See also

Emacs 29.3 (Org mode 9.6.15)