// Code generated by "make migrations"; DO NOT EDIT. package migrations var postgresMigrations = map[string]*fakeFile{ "migrations": { name: "migrations", }, "migrations/01_domain_types.down.sql": { name: "01_domain_types.down.sql", bytes: []byte(` begin; drop domain wt_timestamp; drop domain wt_public_id; drop domain wt_private_id; drop domain wt_scope_id; drop domain wt_user_id; drop domain wt_version; drop function default_create_time; drop function update_time_column; drop function update_version_column; drop function immutable_columns; commit; `), }, "migrations/01_domain_types.up.sql": { name: "01_domain_types.up.sql", bytes: []byte(` begin; create domain wt_public_id as text check( length(trim(value)) > 10 ); comment on domain wt_public_id is 'Random ID generated with github.com/hashicorp/vault/sdk/helper/base62'; create domain wt_private_id as text not null check( length(trim(value)) > 10 ); comment on domain wt_private_id is 'Random ID generated with github.com/hashicorp/vault/sdk/helper/base62'; create domain wt_scope_id as text check( length(trim(value)) > 10 or value = 'global' ); comment on domain wt_scope_id is '"global" or random ID generated with github.com/hashicorp/vault/sdk/helper/base62'; create domain wt_user_id as text not null check( length(trim(value)) > 10 or value = 'u_anon' or value = 'u_auth' or value = 'u_recovery' ); comment on domain wt_scope_id is '"u_anon", "u_auth", or random ID generated with github.com/hashicorp/vault/sdk/helper/base62'; create domain wt_role_id as text not null check( length(trim(value)) > 10 or value = 'r_default' ); comment on domain wt_scope_id is '"r_default", or random ID generated with github.com/hashicorp/vault/sdk/helper/base62'; create domain wt_timestamp as timestamp with time zone default current_timestamp; comment on domain wt_timestamp is 'Standard timestamp for all create_time and update_time columns'; create or replace function update_time_column() returns trigger as $$ begin if row(new.*) is distinct from row(old.*) then new.update_time = now(); return new; else return old; end if; end; $$ language plpgsql; comment on function update_time_column() is 'function used in before update triggers to properly set update_time columns'; create or replace function default_create_time() returns trigger as $$ begin if new.create_time is distinct from now() then raise warning 'create_time cannot be set to %', new.create_time; new.create_time = now(); end if; return new; end; $$ language plpgsql; comment on function default_create_time() is 'function used in before insert triggers to set create_time column to now'; create domain wt_version as bigint default 1 not null check( value > 0 ); comment on domain wt_version is 'standard column for row version'; -- update_version_column() will increment the version column whenever row data -- is updated and should only be used in an update after trigger. This function -- will overwrite any explicit updates to the version column. The function -- accepts an optional parameter of 'private_id' for the tables primary key. create or replace function update_version_column() returns trigger as $$ begin if pg_trigger_depth() = 1 then if row(new.*) is distinct from row(old.*) then if tg_nargs = 0 then execute format('update %I set version = $1 where public_id = $2', tg_relid::regclass) using old.version+1, new.public_id; new.version = old.version + 1; return new; end if; if tg_argv[0] = 'private_id' then execute format('update %I set version = $1 where private_id = $2', tg_relid::regclass) using old.version+1, new.private_id; new.version = old.version + 1; return new; end if; end if; end if; return new; end; $$ language plpgsql; comment on function update_version_column() is 'function used in after update triggers to properly set version columns'; -- immutable_columns() will make the column names immutable which are passed as -- parameters when the trigger is created. It raises error code 23601 which is a -- class 23 integrity constraint violation: immutable column create or replace function immutable_columns() returns trigger as $$ declare col_name text; new_value text; old_value text; begin foreach col_name in array tg_argv loop execute format('SELECT $1.%I', col_name) into new_value using new; execute format('SELECT $1.%I', col_name) into old_value using old; if new_value is distinct from old_value then raise exception 'immutable column: %.%', tg_table_name, col_name using errcode = '23601', schema = tg_table_schema, table = tg_table_name, column = col_name; end if; end loop; return new; end; $$ language plpgsql; comment on function immutable_columns() is 'function used in before update triggers to make columns immutable'; commit; `), }, "migrations/02_oplog.down.sql": { name: "02_oplog.down.sql", bytes: []byte(` begin; drop table oplog_metadata cascade; drop table oplog_ticket cascade; drop table oplog_entry cascade; commit; `), }, "migrations/02_oplog.up.sql": { name: "02_oplog.up.sql", bytes: []byte(` begin; -- TODO (jimlambrt 7/2020) remove update_time create table if not exists oplog_entry ( id bigint generated always as identity primary key, create_time wt_timestamp, update_time wt_timestamp, version text not null, aggregate_name text not null, "data" bytea not null ); create trigger update_time_column before update on oplog_entry for each row execute procedure update_time_column(); create trigger default_create_time_column before insert on oplog_entry for each row execute procedure default_create_time(); -- oplog_entry is immutable. create trigger immutable_columns before update on oplog_entry for each row execute procedure immutable_columns('id','update_time','create_time','version','aggregate_name', 'data'); create table if not exists oplog_ticket ( id bigint generated always as identity primary key, create_time wt_timestamp, update_time wt_timestamp, "name" text not null unique, "version" bigint not null ); create trigger update_time_column before update on oplog_ticket for each row execute procedure update_time_column(); create trigger default_create_time_column before insert on oplog_ticket for each row execute procedure default_create_time(); -- oplog_ticket: only allow updates to: version and update_time create trigger immutable_columns before update on oplog_ticket for each row execute procedure immutable_columns('id','create_time','name'); -- TODO (jimlambrt 7/2020) remove update_time create table if not exists oplog_metadata ( id bigint generated always as identity primary key, create_time wt_timestamp, update_time wt_timestamp, entry_id bigint not null references oplog_entry(id) on delete cascade on update cascade, "key" text not null, value text null ); create trigger update_time_column before update on oplog_metadata for each row execute procedure update_time_column(); create trigger default_create_time_column before insert on oplog_metadata for each row execute procedure default_create_time(); -- oplog_metadata is immutable create trigger immutable_columns before update on oplog_metadata for each row execute procedure immutable_columns('id','create_time','update_time','entry_id','key','value'); create index if not exists idx_oplog_metatadata_key on oplog_metadata(key); create index if not exists idx_oplog_metatadata_value on oplog_metadata(value); insert into oplog_ticket (name, version) values ('auth_token', 1), ('default', 1), ('iam_scope', 1), ('iam_user', 1), ('iam_group', 1), ('iam_group_member', 1), ('iam_role', 1), ('iam_role_grant', 1), ('iam_group_role', 1), ('iam_user_role', 1), ('db_test_user', 1), ('db_test_car', 1), ('db_test_rental', 1), ('db_test_scooter', 1), ('auth_account', 1), ('iam_principal_role', 1); commit; `), }, "migrations/03_db.down.sql": { name: "03_db.down.sql", bytes: []byte(` begin; drop table db_test_rental cascade; drop table db_test_car cascade; drop table db_test_user cascade; drop table db_test_scooter cascade; commit; `), }, "migrations/03_db.up.sql": { name: "03_db.up.sql", bytes: []byte(` begin; -- create test tables used in the unit tests for the internal/db package -- these tables (db_test_user, db_test_car, db_test_rental, db_test_scooter) are -- not part of the boundary domain model... they are simply used for testing -- the internal/db package create table if not exists db_test_user ( id bigint generated always as identity primary key, create_time wt_timestamp, update_time wt_timestamp, public_id text not null unique, name text unique, phone_number text, email text, version wt_version ); create trigger update_time_column before update on db_test_user for each row execute procedure update_time_column(); -- define the immutable fields for db_test_user create trigger immutable_columns before update on db_test_user for each row execute procedure immutable_columns('create_time'); create trigger default_create_time_column before insert on db_test_user for each row execute procedure default_create_time(); create trigger update_version_column after update on db_test_user for each row execute procedure update_version_column(); create table if not exists db_test_car ( id bigint generated always as identity primary key, create_time wt_timestamp, update_time wt_timestamp, public_id text not null unique, name text unique, model text, mpg smallint ); create trigger update_time_column before update on db_test_car for each row execute procedure update_time_column(); -- define the immutable fields for db_test_car create trigger immutable_columns before update on db_test_car for each row execute procedure immutable_columns('create_time'); create trigger default_create_time_column before insert on db_test_car for each row execute procedure default_create_time(); create table if not exists db_test_rental ( id bigint generated always as identity primary key, create_time wt_timestamp, update_time wt_timestamp, public_id text not null unique, name text unique, user_id bigint not null references db_test_user(id), car_id bigint not null references db_test_car(id) ); create trigger update_time_column before update on db_test_rental for each row execute procedure update_time_column(); -- define the immutable fields for db_test_rental create trigger immutable_columns before update on db_test_rental for each row execute procedure immutable_columns('create_time'); create trigger default_create_time_column before insert on db_test_rental for each row execute procedure default_create_time(); create table if not exists db_test_scooter ( id bigint generated always as identity primary key, create_time wt_timestamp, update_time wt_timestamp, private_id text not null unique, name text unique, model text, mpg smallint ); create trigger update_time_column before update on db_test_scooter for each row execute procedure update_time_column(); -- define the immutable fields for db_test_scooter create trigger immutable_columns before update on db_test_scooter for each row execute procedure immutable_columns('create_time'); create trigger default_create_time_column before insert on db_test_scooter for each row execute procedure default_create_time(); commit; `), }, "migrations/06_iam.down.sql": { name: "06_iam.down.sql", bytes: []byte(` BEGIN; drop table iam_group cascade; drop table iam_user cascade; drop table iam_scope_project cascade; drop table iam_scope_org cascade; drop table iam_scope_global cascade; drop table iam_scope cascade; drop table iam_scope_type_enm cascade; drop table iam_role cascade; drop view iam_principal_role cascade; drop table iam_group_role cascade; drop table iam_user_role cascade; drop table iam_group_member_user cascade; drop view iam_group_member cascade; drop table iam_role_grant cascade; drop function iam_sub_names cascade; drop function iam_immutable_scope_type_func cascade; drop function iam_sub_scopes_func cascade; drop function iam_immutable_role_principal cascade; drop function iam_user_role_scope_check cascade; drop function iam_group_role_scope_check cascade; drop function iam_group_member_scope_check cascade; drop function iam_immutable_group_member cascade; drop function get_scoped_member_id cascade; drop function grant_scope_id_valid cascade; drop function disallow_global_scope_deletion cascade; drop function user_scope_id_valid cascade; drop function iam_immutable_role_grant cascade; drop function disallow_iam_predefined_user_deletion cascade; drop function recovery_user_not_allowed cascade; COMMIT; `), }, "migrations/06_iam.up.sql": { name: "06_iam.up.sql", bytes: []byte(` begin; create table iam_scope_type_enm ( string text not null primary key check(string in ('unknown', 'global', 'org', 'project')) ); insert into iam_scope_type_enm (string) values ('unknown'), ('global'), ('org'), ('project'); -- define the immutable fields of iam_scope_type_enm create trigger immutable_columns before update on iam_scope_type_enm for each row execute procedure immutable_columns('string'); create table iam_scope ( public_id wt_scope_id primary key, create_time wt_timestamp, update_time wt_timestamp, name text, type text not null references iam_scope_type_enm(string) check( ( type = 'global' and parent_id is null ) or ( type = 'org' and parent_id = 'global' ) or ( type = 'project' and parent_id is not null and parent_id != 'global' ) ), description text, parent_id text references iam_scope(public_id) on delete cascade on update cascade, -- version allows optimistic locking of the role when modifying the role -- itself and when modifying dependent items like principal roles. version wt_version ); create table iam_scope_global ( scope_id wt_scope_id primary key references iam_scope(public_id) on delete cascade on update cascade check( scope_id = 'global' ), name text unique ); create table iam_scope_org ( scope_id wt_scope_id primary key references iam_scope(public_id) on delete cascade on update cascade, parent_id wt_scope_id not null references iam_scope_global(scope_id) on delete cascade on update cascade, name text, unique (parent_id, name) ); create table iam_scope_project ( scope_id wt_scope_id not null references iam_scope(public_id) on delete cascade on update cascade, parent_id wt_public_id not null references iam_scope_org(scope_id) on delete cascade on update cascade, name text, unique(parent_id, name), primary key(scope_id, parent_id) ); create or replace function iam_sub_scopes_func() returns trigger as $$ declare parent_type int; begin if new.type = 'global' then insert into iam_scope_global (scope_id, name) values (new.public_id, new.name); return new; end if; if new.type = 'org' then insert into iam_scope_org (scope_id, parent_id, name) values (new.public_id, new.parent_id, new.name); return new; end if; if new.type = 'project' then insert into iam_scope_project (scope_id, parent_id, name) values (new.public_id, new.parent_id, new.name); return new; end if; raise exception 'unknown scope type'; end; $$ language plpgsql; create trigger iam_scope_insert after insert on iam_scope for each row execute procedure iam_sub_scopes_func(); create or replace function disallow_global_scope_deletion() returns trigger as $$ begin if old.type = 'global' then raise exception 'deletion of global scope not allowed'; end if; return old; end; $$ language plpgsql; create trigger iam_scope_disallow_global_deletion before delete on iam_scope for each row execute procedure disallow_global_scope_deletion(); create trigger update_time_column before update on iam_scope for each row execute procedure update_time_column(); create trigger default_create_time_column before insert on iam_scope for each row execute procedure default_create_time(); create trigger update_version_column after update on iam_scope for each row execute procedure update_version_column(); -- define the immutable fields for iam_scope create trigger immutable_columns before update on iam_scope for each row execute procedure immutable_columns('public_id', 'create_time', 'type', 'parent_id'); -- define the immutable fields of iam_scope_global create trigger immutable_columns before update on iam_scope_global for each row execute procedure immutable_columns('scope_id'); -- define the immutable fields of iam_scope_org create trigger immutable_columns before update on iam_scope_org for each row execute procedure immutable_columns('scope_id'); -- define the immutable fields of iam_scope_project create trigger immutable_columns before update on iam_scope_project for each row execute procedure immutable_columns('scope_id'); -- iam_sub_names will allow us to enforce the different name constraints for -- orgs and projects via a before update trigger on the iam_scope -- table. create or replace function iam_sub_names() returns trigger as $$ begin if new.name != old.name then if new.type = 'global' then update iam_scope_global set name = new.name where scope_id = old.public_id; return new; end if; if new.type = 'org' then update iam_scope_org set name = new.name where scope_id = old.public_id; return new; end if; if new.type = 'project' then update iam_scope_project set name = new.name where scope_id = old.public_id; return new; end if; raise exception 'unknown scope type'; end if; return new; end; $$ language plpgsql; create trigger iam_sub_names before update on iam_scope for each row execute procedure iam_sub_names(); insert into iam_scope (public_id, name, type, description) values ('global', 'global', 'global', 'Global Scope'); create table iam_user ( public_id wt_user_id primary key, create_time wt_timestamp, update_time wt_timestamp, name text, description text, scope_id wt_scope_id not null references iam_scope(public_id) on delete cascade on update cascade, unique(name, scope_id), version wt_version, -- The order of columns is important for performance. See: -- https://dba.stackexchange.com/questions/58970/enforcing-constraints-two-tables-away/58972#58972 -- https://dba.stackexchange.com/questions/27481/is-a-composite-index-also-good-for-queries-on-the-first-field unique(scope_id, public_id) ); create or replace function user_scope_id_valid() returns trigger as $$ begin perform from iam_scope where public_id = new.scope_id and type in ('global', 'org'); if not found then raise exception 'invalid scope type for user creation'; end if; return new; end; $$ language plpgsql; create or replace function grant_scope_id_valid() returns trigger as $$ declare parent_scope_id text; declare role_scope_type text; begin -- There is a not-null constraint so ensure that if the value passed in is -- empty we simply set to the scope ID if new.grant_scope_id = '' or new.grant_scope_id is null then new.grant_scope_id = new.scope_id; end if; -- If the scopes match, it's allowed if new.grant_scope_id = new.scope_id then return new; end if; -- Fetch the type of scope select isc.type from iam_scope isc where isc.public_id = new.scope_id into role_scope_type; -- Always allowed if role_scope_type = 'global' then return new; end if; -- Never allowed; the case where it's set to the same scope ID as the project -- itself is covered above if role_scope_type = 'project' then raise exception 'invalid to set grant_scope_id to non-same scope_id when role scope type is project'; end if; if role_scope_type = 'org' then -- Look up the parent scope ID for the scope ID given select isc.parent_id from iam_scope isc where isc.public_id = new.grant_scope_id into parent_scope_id; -- Allow iff the grant scope ID's parent matches the role's scope ID; that -- is, match if the role belongs to a direct child scope of this -- org if parent_scope_id = new.scope_id then return new; end if; raise exception 'grant_scope_id is not a child project of the role scope'; end if; raise exception 'unknown scope type'; end; $$ language plpgsql; create or replace function disallow_iam_predefined_user_deletion() returns trigger as $$ begin if old.public_id = 'u_anon' then raise exception 'deletion of anonymous user not allowed'; end if; if old.public_id = 'u_auth' then raise exception 'deletion of authenticated user not allowed'; end if; if old.public_id = 'u_recovery' then raise exception 'deletion of recovery user not allowed'; end if; return old; end; $$ language plpgsql; create trigger update_version_column after update on iam_user for each row execute procedure update_version_column(); create trigger ensure_user_scope_id_valid before insert or update on iam_user for each row execute procedure user_scope_id_valid(); create trigger update_time_column before update on iam_user for each row execute procedure update_time_column(); create trigger default_create_time_column before insert on iam_user for each row execute procedure default_create_time(); create trigger iam_user_disallow_predefined_user_deletion before delete on iam_user for each row execute procedure disallow_iam_predefined_user_deletion(); -- TODO: Do we want to disallow changing the name or description? insert into iam_user (public_id, name, description, scope_id) values ('u_anon', 'anonymous', 'The anonymous user matches any request, whether authenticated or not', 'global'); insert into iam_user (public_id, name, description, scope_id) values ('u_auth', 'authenticated', 'The authenticated user matches any user that has a valid token', 'global'); insert into iam_user (public_id, name, description, scope_id) values ('u_recovery', 'recovery', 'The recovery user is used for any request that was performed with the recovery KMS workflow', 'global'); -- define the immutable fields for iam_user create trigger immutable_columns before update on iam_user for each row execute procedure immutable_columns('public_id', 'create_time', 'scope_id'); create table iam_role ( public_id wt_role_id primary key, create_time wt_timestamp, update_time wt_timestamp, name text, description text, scope_id wt_scope_id not null references iam_scope(public_id) on delete cascade on update cascade, grant_scope_id wt_scope_id not null references iam_scope(public_id) on delete cascade on update cascade, unique(name, scope_id), version wt_version, -- add unique index so a composite fk can be declared. unique(scope_id, public_id) ); -- Grants are immutable, which is enforced via the trigger below create table iam_role_grant ( create_time wt_timestamp, role_id wt_role_id -- pk references iam_role(public_id) on delete cascade on update cascade, canonical_grant text -- pk check( length(trim(canonical_grant)) > 0 ), raw_grant text not null check( length(trim(raw_grant)) > 0 ), primary key(role_id, canonical_grant) ); -- iam_immutable_role_grant() ensures that grants assigned to roles are immutable. create or replace function iam_immutable_role_grant() returns trigger as $$ begin raise exception 'role grants are immutable'; end; $$ language plpgsql; create trigger immutable_role_grant before update on iam_role_grant for each row execute procedure iam_immutable_role_grant(); create trigger default_create_time_column before insert on iam_role_grant for each row execute procedure default_create_time(); create trigger update_version_column after update on iam_role for each row execute procedure update_version_column(); create trigger update_time_column before update on iam_role for each row execute procedure update_time_column(); create trigger default_create_time_column before insert on iam_role for each row execute procedure default_create_time(); create trigger ensure_grant_scope_id_valid before insert or update on iam_role for each row execute procedure grant_scope_id_valid(); -- define the immutable fields for iam_role (started trigger name with "a_" so -- it will run first) create trigger a_immutable_columns before update on iam_role for each row execute procedure immutable_columns('public_id', 'create_time', 'scope_id'); create or replace function recovery_user_not_allowed() returns trigger as $$ declare new_value text; begin execute format('SELECT $1.%I', tg_argv[0]) into new_value using new; if new_value = 'u_recovery' then raise exception '"u_recovery" not allowed here"'; end if; return new; end; $$ language plpgsql; create table iam_group ( public_id wt_public_id primary key, create_time wt_timestamp, update_time wt_timestamp, name text, description text, scope_id wt_scope_id not null references iam_scope(public_id) on delete cascade on update cascade, unique(name, scope_id), -- version allows optimistic locking of the group when modifying the group -- itself and when modifying dependent items like group members. version wt_version, -- add unique index so a composite fk can be declared. unique(scope_id, public_id) ); create trigger update_version_column after update on iam_group for each row execute procedure update_version_column(); create trigger update_time_column before update on iam_group for each row execute procedure update_time_column(); create trigger default_create_time_column before insert on iam_group for each row execute procedure default_create_time(); -- define the immutable fields for iam_group create trigger immutable_columns before update on iam_group for each row execute procedure immutable_columns('public_id', 'create_time', 'scope_id'); -- iam_user_role contains roles that have been assigned to users. Users can be -- from any scope. The rows in this table must be immutable after insert, which -- will be ensured with a before update trigger using -- iam_immutable_role_principal(). create table iam_user_role ( create_time wt_timestamp, role_id wt_role_id references iam_role(public_id) on delete cascade on update cascade, principal_id wt_user_id references iam_user(public_id) on delete cascade on update cascade, primary key (role_id, principal_id) ); -- iam_group_role contains roles that have been assigned to groups. -- Groups can be from any scope. The rows in this table must be immutable after -- insert, which will be ensured with a before update trigger using -- iam_immutable_role_principal(). create table iam_group_role ( create_time wt_timestamp, role_id wt_role_id references iam_role(public_id) on delete cascade on update cascade, principal_id wt_public_id references iam_group(public_id) on delete cascade on update cascade, primary key (role_id, principal_id) ); -- get_scoped_principal_id is used by the iam_principle_role view as a convient -- way to create : to reference principals from -- other scopes than the role's scope. create or replace function get_scoped_principal_id(role_scope text, principal_scope text, principal_id text) returns text as $$ begin if role_scope = principal_scope then return principal_id; end if; return principal_scope || ':' || principal_id; end; $$ language plpgsql; insert into iam_role (public_id, name, description, scope_id) values('r_default', 'default', 'Default role created on first instantiation of Boundary. It is meant to provide enough permissions for users to successfully authenticate via various client types.', 'global'); insert into iam_role_grant (role_id, canonical_grant, raw_grant) values ('r_default', 'type=scope;actions=list', 'type=scope;actions=list'), ('r_default', 'type=auth-method;actions=authenticate,list', 'type=auth-method;actions=authenticate'); insert into iam_user_role (role_id, principal_id) values ('r_default', 'u_anon'), ('r_default', 'u_auth'); -- iam_principle_role provides a consolidated view all principal roles assigned -- (user and group roles). create view iam_principal_role as select ur.create_time, ur.principal_id, ur.role_id, u.scope_id as principal_scope_id, r.scope_id as role_scope_id, get_scoped_principal_id(r.scope_id, u.scope_id, ur.principal_id) as scoped_principal_id, 'user' as type from iam_user_role ur, iam_role r, iam_user u where ur.role_id = r.public_id and u.public_id = ur.principal_id union select gr.create_time, gr.principal_id, gr.role_id, g.scope_id as principal_scope_id, r.scope_id as role_scope_id, get_scoped_principal_id(r.scope_id, g.scope_id, gr.principal_id) as scoped_principal_id, 'group' as type from iam_group_role gr, iam_role r, iam_group g where gr.role_id = r.public_id and g.public_id = gr.principal_id; -- iam_immutable_role_principal() ensures that roles assigned to principals are immutable. create or replace function iam_immutable_role_principal() returns trigger as $$ begin raise exception 'roles are immutable'; end; $$ language plpgsql; create trigger immutable_role_principal before update on iam_user_role for each row execute procedure iam_immutable_role_principal(); create trigger recovery_user_not_allowed_user_role before insert on iam_user_role for each row execute procedure recovery_user_not_allowed('principal_id'); create trigger default_create_time_column before insert on iam_user_role for each row execute procedure default_create_time(); create trigger immutable_role_principal before update on iam_group_role for each row execute procedure iam_immutable_role_principal(); create trigger default_create_time_column before insert on iam_group_role for each row execute procedure default_create_time(); -- iam_group_member_user is an association table that represents groups with -- associated users. create table iam_group_member_user ( create_time wt_timestamp, group_id wt_public_id references iam_group(public_id) on delete cascade on update cascade, member_id wt_user_id references iam_user(public_id) on delete cascade on update cascade, primary key (group_id, member_id) ); -- iam_immutable_group_member() ensures that group members are immutable. create or replace function iam_immutable_group_member() returns trigger as $$ begin raise exception 'group members are immutable'; end; $$ language plpgsql; create trigger default_create_time_column before insert on iam_group_member_user for each row execute procedure default_create_time(); create trigger iam_immutable_group_member before update on iam_group_member_user for each row execute procedure iam_immutable_group_member(); create trigger recovery_user_not_allowed_group_member before insert on iam_group_member_user for each row execute procedure recovery_user_not_allowed('member_id'); -- get_scoped_principal_id is used by the iam_group_member view as a convient -- way to create : to reference members from -- other scopes than the group's scope. create or replace function get_scoped_member_id(group_scope text, member_scope text, member_id text) returns text as $$ begin if group_scope = member_scope then return member_id; end if; return member_scope || ':' || member_id; end; $$ language plpgsql; -- iam_group_member provides a consolidated view of group members. create view iam_group_member as select gm.create_time, gm.group_id, gm.member_id, u.scope_id as member_scope_id, g.scope_id as group_scope_id, get_scoped_member_id(g.scope_id, u.scope_id, gm.member_id) as scoped_member_id, 'user' as type from iam_group_member_user gm, iam_user u, iam_group g where gm.member_id = u.public_id and gm.group_id = g.public_id; commit; `), }, "migrations/07_auth.down.sql": { name: "07_auth.down.sql", bytes: []byte(` begin; drop function insert_auth_account_subtype; drop function insert_auth_method_subtype; drop table auth_account cascade; drop table auth_method cascade; commit; `), }, "migrations/07_auth.up.sql": { name: "07_auth.up.sql", bytes: []byte(` begin; /* ┌────────────────┐ ┌────────────────┐ │ iam_scope │ │ auth_method │ ├────────────────┤ ├────────────────┤ │ public_id (pk) │ ╱│ public_id (pk) │ │ │┼┼───────────○─│ scope_id (fk) │ │ │ ╲│ │ └────────────────┘ └────────────────┘ ┼ ┼ ┼ ┼ │ │ │ │ ▲fk1 │ │ ○ ○ ╱│╲ ╱│╲ ┌────────────────┐ ┌──────────────────────────┐ │ iam_user │ │ auth_account │ ├────────────────┤ ├──────────────────────────┤ │ public_id (pk) │ │ public_id (pk) │ │ scope_id (fk) │ ◀fk2 │ scope_id (fk1,fk2) │ │ │┼○──────○┼│ auth_method_id (fk1) │ │ │ │ iam_user_id (fk2) │ └────────────────┘ └──────────────────────────┘ An iam_scope can have 0 to many iam_users. An iam_scope can have 0 to many auth_methods. An iam_user belongs to 1 iam_scope. An auth_method belongs to 1 iam_scope. An iam_user can have 0 or 1 auth_account. An auth_account belongs to 0 or 1 iam_user. An auth_method can have 0 to many auth_accounts. An auth_account belongs to 1 auth_account. An auth_account can only be associated with an iam_user in the same scope of the auth_account's auth_method. Including scope_id in fk1 and fk2 ensures this restriction is not violated. Design influenced by: https://dba.stackexchange.com/questions/58970/enforcing-constraints-two-tables-away/58972#58972 */ -- base table for auth methods create table auth_method ( public_id wt_public_id primary key, scope_id wt_scope_id not null references iam_scope(public_id) on delete cascade on update cascade, -- The order of columns is important for performance. See: -- https://dba.stackexchange.com/questions/58970/enforcing-constraints-two-tables-away/58972#58972 -- https://dba.stackexchange.com/questions/27481/is-a-composite-index-also-good-for-queries-on-the-first-field unique(scope_id, public_id) ); -- base table for auth accounts create table auth_account ( public_id wt_public_id primary key, auth_method_id wt_public_id not null, scope_id wt_scope_id not null, iam_user_id wt_public_id, -- including scope_id in fk1 and fk2 ensures the scope_id of the owning -- auth_method and the scope_id of the owning iam_user are the same foreign key (scope_id, auth_method_id) -- fk1 references auth_method (scope_id, public_id) on delete cascade on update cascade, foreign key (scope_id, iam_user_id) -- fk2 references iam_user (scope_id, public_id) on delete set null on update cascade, unique(scope_id, auth_method_id, public_id) ); create or replace function insert_auth_method_subtype() returns trigger as $$ begin insert into auth_method (public_id, scope_id) values (new.public_id, new.scope_id); return new; end; $$ language plpgsql; create or replace function insert_auth_account_subtype() returns trigger as $$ begin select auth_method.scope_id into new.scope_id from auth_method where auth_method.public_id = new.auth_method_id; insert into auth_account (public_id, auth_method_id, scope_id) values (new.public_id, new.auth_method_id, new.scope_id); return new; end; $$ language plpgsql; commit; `), }, "migrations/08_servers.down.sql": { name: "08_servers.down.sql", bytes: []byte(` begin; drop table server; commit; `), }, "migrations/08_servers.up.sql": { name: "08_servers.up.sql", bytes: []byte(` begin; -- For now at least the IDs will be the same as the name, because this allows us -- to not have to persist some generated ID to worker and controller nodes. -- Eventually we may want them to diverge, so we have both here for now. create table server ( private_id text, type text, name text not null unique check(length(trim(name)) > 0), description text, address text, create_time wt_timestamp, update_time wt_timestamp, primary key (private_id, type) ); create trigger immutable_columns before update on server for each row execute procedure immutable_columns('create_time'); create trigger default_create_time_column before insert on server for each row execute procedure default_create_time(); create table recovery_nonces ( nonce text primary key, create_time wt_timestamp ); create trigger default_create_time_column before insert on recovery_nonces for each row execute procedure default_create_time(); create trigger immutable_columns before update on recovery_nonces for each row execute procedure immutable_columns('nonce', 'create_time'); commit; `), }, "migrations/11_auth_token.down.sql": { name: "11_auth_token.down.sql", bytes: []byte(` begin; drop view auth_token_account cascade; drop table auth_token cascade; drop function update_last_access_time cascade; drop function immutable_auth_token_columns cascade; drop function expire_time_not_older_than_token cascade; commit; `), }, "migrations/11_auth_token.up.sql": { name: "11_auth_token.up.sql", bytes: []byte(` begin; -- an auth token belongs to 1 and only 1 auth account -- an auth account can have 0 to many auth tokens create table auth_token ( public_id wt_public_id primary key, token bytea not null unique, -- TODO: Make key_id a foreign key once we have DEKs key_id text not null check(length(trim(key_id)) > 0), auth_account_id wt_public_id not null references auth_account(public_id) on delete cascade on update cascade, create_time wt_timestamp, update_time wt_timestamp, -- This column is not updated every time this auth token is accessed. -- It is updated after X minutes from the last time it was updated on -- a per row basis. approximate_last_access_time wt_timestamp check( approximate_last_access_time <= expiration_time ), expiration_time wt_timestamp check( create_time <= expiration_time ) ); create view auth_token_account as select at.public_id, at.token, at.auth_account_id, at.create_time, at.update_time, at.approximate_last_access_time, at.expiration_time, aa.scope_id, aa.iam_user_id, aa.auth_method_id from auth_token as at inner join auth_account as aa on at.auth_account_id = aa.public_id; create or replace function update_last_access_time() returns trigger as $$ begin if new.approximate_last_access_time is distinct from old.approximate_last_access_time then new.approximate_last_access_time = now(); end if; return new; end; $$ language plpgsql; comment on function update_last_access_time() is 'function used in before update triggers to properly set last_access_time columns'; create or replace function immutable_auth_token_columns() returns trigger as $$ begin if new.auth_account_id is distinct from old.auth_account_id then raise exception 'auth_account_id is read-only'; end if; if new.token is distinct from old.token then raise exception 'token is read-only'; end if; return new; end; $$ language plpgsql; comment on function immutable_auth_token_columns() is 'function used in before update triggers to make specific columns immutable'; -- This allows the expiration to be calculated on the server side and still hold the constraint that -- the expiration time cant be before the creation time of the auth token. create or replace function expire_time_not_older_than_token() returns trigger as $$ begin if new.expiration_time < new.create_time then new.expiration_time = new.create_time; end if; return new; end; $$ language plpgsql; comment on function expire_time_not_older_than_token() is 'function used in before insert triggers to ensure expiration time is not older than create time'; create trigger default_create_time_column before insert on auth_token for each row execute procedure default_create_time(); create trigger expire_time_not_older_than_token before insert on auth_token for each row execute procedure expire_time_not_older_than_token(); create trigger update_time_column before update on auth_token for each row execute procedure update_time_column(); create trigger update_last_access_time before update on auth_token for each row execute procedure update_last_access_time(); create trigger immutable_auth_token_columns before update on auth_token for each row execute procedure immutable_auth_token_columns(); create trigger immutable_columns before update on auth_token for each row execute procedure immutable_columns('public_id', 'auth_account_id', 'create_time'); commit; `), }, "migrations/12_auth_password.down.sql": { name: "12_auth_password.down.sql", bytes: []byte(` begin; drop table auth_password_credential; drop table auth_password_conf cascade; drop table if exists auth_password_account; drop table if exists auth_password_method; drop function insert_auth_password_credential_subtype; drop function insert_auth_password_conf_subtype; commit; `), }, "migrations/12_auth_password.up.sql": { name: "12_auth_password.up.sql", bytes: []byte(` begin; /* ┌────────────────┐ ┌──────────────────────┐ ┌────────────────────────────┐ │ auth_method │ │ auth_password_method │ │ auth_password_conf │ ├────────────────┤ ├──────────────────────┤ ├────────────────────────────┤ │ public_id (pk) │ │ public_id (pk,fk) │ ╱│ private_id (pk,fk) │ │ scope_id (fk) │┼┼─────────────○┼│ scope_id (fk) │┼┼─────────○─│ password_method_id (fk) │ │ │ │ ... │ ╲│ │ └────────────────┘ └──────────────────────┘ └────────────────────────────┘ ┼ ┼ ┼ ┼ ┼ ┼ │ │ │ │ ▲fk1 │ ▲fk1 │ ▲fk1 │ │ │ ○ ○ ○ ╱│╲ ╱│╲ ╱│╲ ┌──────────────────────────┐ ┌──────────────────────────┐ ┌───────────────────────────────┐ │ auth_account │ │ auth_password_account │ │ auth_password_credential │ ├──────────────────────────┤ ├──────────────────────────┤ ├───────────────────────────────┤ │ public_id (pk) │ │ public_id (pk,fk2) │ │ private_id (pk) │ │ scope_id (fk1,fk2) │ ◀fk2 │ scope_id (fk1,fk2) │ ◀fk2 │ password_method_id (fk1,fk2) │ │ auth_method_id (fk1) │┼┼──────○┼│ auth_method_id (fk1,fk2) │┼┼──────○┼│ password_conf_id (fk1) │ │ iam_user_id (fk2) │ │ ... │ │ password_account_id (fk2) │ └──────────────────────────┘ └──────────────────────────┘ └───────────────────────────────┘ An auth_method is a base type. An auth_password_method is an auth_method subtype. For every row in auth_password_method there is one row in auth_method with the same public_id and scope_id. Similarly, an auth_account is a base type. An auth_password_account is an auth_account subtype. For every row in auth_password_account there is one row in auth_account with the same public_id, scope_id, and auth_method_id. Both auth_password_conf and auth_password_credential are base types. Each password key derivation function will require a auth_password_conf and auth_password_credential table. An auth_method can have 0 or 1 auth_password_method. An auth_account can have 0 or 1 auth_password_account. An auth_password_method belongs to 1 auth_method. An auth_password_method can have 0 to many auth_password_accounts. An auth_password_method can have 0 to many auth_password_confs. An auth_password_account belongs to 1 auth_account. An auth_password_account belongs to 1 auth_password_method. An auth_password_account can have 0 or 1 auth_password_credential. An auth_password_conf belongs to 1 auth_password_method. An auth_password_conf can have 0 to many auth_password_credentials. An auth_password_credential belongs to 1 auth_password_account. An auth_password_credential belongs to 1 auth_password_conf. */ create table auth_password_method ( public_id wt_public_id primary key, scope_id wt_scope_id not null, password_conf_id wt_private_id, -- FK to auth_password_conf added below name text, description text, create_time wt_timestamp, update_time wt_timestamp, min_login_name_length int not null default 3, min_password_length int not null default 8, version wt_version, foreign key (scope_id, public_id) references auth_method (scope_id, public_id) on delete cascade on update cascade, unique(scope_id, name), unique(scope_id, public_id) ); create trigger update_version_column after update on auth_password_method for each row execute procedure update_version_column(); create trigger insert_auth_method_subtype before insert on auth_password_method for each row execute procedure insert_auth_method_subtype(); create table auth_password_account ( public_id wt_public_id primary key, auth_method_id wt_public_id not null, -- NOTE(mgaffney): The scope_id type is not wt_scope_id because the domain -- check is executed before the insert trigger which retrieves the scope_id -- causing an insert to fail. scope_id text not null, name text, description text, create_time wt_timestamp, update_time wt_timestamp, login_name text not null check( lower(trim(login_name)) = login_name and length(login_name) > 0 ), version wt_version, foreign key (scope_id, auth_method_id) references auth_password_method (scope_id, public_id) on delete cascade on update cascade, foreign key (scope_id, auth_method_id, public_id) references auth_account (scope_id, auth_method_id, public_id) on delete cascade on update cascade, unique(auth_method_id, name), unique(auth_method_id, login_name), unique(auth_method_id, public_id) ); create trigger update_version_column after update on auth_password_account for each row execute procedure update_version_column(); create trigger insert_auth_account_subtype before insert on auth_password_account for each row execute procedure insert_auth_account_subtype(); create table auth_password_conf ( private_id wt_private_id primary key, password_method_id wt_public_id not null references auth_password_method (public_id) on delete cascade on update cascade deferrable initially deferred, unique(password_method_id, private_id) ); alter table auth_password_method add constraint current_conf_fkey foreign key (public_id, password_conf_id) references auth_password_conf (password_method_id, private_id) on delete cascade on update cascade deferrable initially deferred; -- insert_auth_password_conf_subtype() is a trigger function for subtypes of -- auth_password_conf create or replace function insert_auth_password_conf_subtype() returns trigger as $$ begin insert into auth_password_conf (private_id, password_method_id) values (new.private_id, new.password_method_id); return new; end; $$ language plpgsql; create table auth_password_credential ( private_id wt_private_id primary key, password_account_id wt_public_id not null unique, password_conf_id wt_private_id, password_method_id wt_public_id not null, foreign key (password_method_id, password_conf_id) references auth_password_conf (password_method_id, private_id) on delete cascade on update cascade, foreign key (password_method_id, password_account_id) references auth_password_account (auth_method_id, public_id) on delete cascade on update cascade, unique(password_method_id, password_conf_id, password_account_id) ); -- insert_auth_password_credential_subtype() is a trigger function for -- subtypes of auth_password_credential create or replace function insert_auth_password_credential_subtype() returns trigger as $$ begin select auth_password_account.auth_method_id into new.password_method_id from auth_password_account where auth_password_account.public_id = new.password_account_id; insert into auth_password_credential (private_id, password_account_id, password_conf_id, password_method_id) values (new.private_id, new.password_account_id, new.password_conf_id, new.password_method_id); return new; end; $$ language plpgsql; -- update_auth_password_credential_subtype() is an after update trigger -- function for subtypes of auth_password_credential create or replace function update_auth_password_credential_subtype() returns trigger as $$ begin /* The configuration id of a credential is updated when a credential is rehashed during authentication. */ if new.password_conf_id is distinct from old.password_conf_id then update auth_password_credential set password_conf_id = new.password_conf_id where private_id = new.private_id; end if; return null; -- result is ignored since this is an after trigger end; $$ language plpgsql; -- delete_auth_password_credential_subtype() is an after delete trigger -- function for subtypes of auth_password_credential create or replace function delete_auth_password_credential_subtype() returns trigger as $$ begin delete from auth_password_credential where private_id = old.private_id; return null; -- result is ignored since this is an after trigger end; $$ language plpgsql; -- -- triggers for time columns -- create trigger update_time_column before update on auth_password_method for each row execute procedure update_time_column(); create trigger immutable_columns before update on auth_password_method for each row execute procedure immutable_columns('create_time'); create trigger default_create_time_column before insert on auth_password_method for each row execute procedure default_create_time(); create trigger update_time_column before update on auth_password_account for each row execute procedure update_time_column(); create trigger immutable_columns before update on auth_password_account for each row execute procedure immutable_columns('create_time'); create trigger default_create_time_column before insert on auth_password_account for each row execute procedure default_create_time(); -- The tickets for oplog are the subtypes not the base types because no updates -- are done to any values in the base types. insert into oplog_ticket (name, version) values ('auth_password_method', 1), ('auth_password_account', 1), ('auth_password_credential', 1); commit; `), }, "migrations/13_auth_password_argon.down.sql": { name: "13_auth_password_argon.down.sql", bytes: []byte(` begin; drop table auth_password_argon2_cred; drop table auth_password_argon2_conf; commit; `), }, "migrations/13_auth_password_argon.up.sql": { name: "13_auth_password_argon.up.sql", bytes: []byte(` begin; create table auth_password_argon2_conf ( private_id wt_private_id primary key references auth_password_conf (private_id) on delete cascade on update cascade, password_method_id wt_public_id not null, create_time wt_timestamp, iterations int not null default 3 check(iterations > 0), memory int not null default 65536 check(memory > 0), threads int not null default 1 check(threads > 0), -- salt_length unit is bytes salt_length int not null default 32 -- minimum of 16 bytes (128 bits) check(salt_length >= 16), -- key_length unit is bytes key_length int not null default 32 -- minimum of 16 bytes (128 bits) check(key_length >= 16), unique(password_method_id, iterations, memory, threads, salt_length, key_length), unique (password_method_id, private_id), foreign key (password_method_id, private_id) references auth_password_conf (password_method_id, private_id) on delete cascade on update cascade deferrable initially deferred ); create or replace function read_only_auth_password_argon2_conf() returns trigger as $$ begin raise exception 'auth_password_argon2_conf is read-only'; end; $$ language plpgsql; create trigger read_only_auth_password_argon2_conf before update on auth_password_argon2_conf for each row execute procedure read_only_auth_password_argon2_conf(); create trigger insert_auth_password_conf_subtype before insert on auth_password_argon2_conf for each row execute procedure insert_auth_password_conf_subtype(); create table auth_password_argon2_cred ( private_id wt_private_id primary key references auth_password_credential (private_id) on delete cascade on update cascade, password_account_id wt_public_id not null, password_conf_id wt_private_id, -- NOTE(mgaffney): The password_method_id type is not wt_public_id because -- the domain check is executed before the insert trigger which retrieves -- the password_method_id causing an insert to fail. password_method_id text not null, create_time wt_timestamp, update_time wt_timestamp, salt bytea not null -- cannot be changed unless derived_key is changed too check(length(salt) > 0), derived_key bytea not null check(length(derived_key) > 0), -- TODO: Make key_id a foreign key once we have DEKs key_id text not null check(length(trim(key_id)) > 0), foreign key (password_method_id, password_conf_id) references auth_password_argon2_conf (password_method_id, private_id) on delete cascade on update cascade, foreign key (password_method_id, password_conf_id, password_account_id) references auth_password_credential (password_method_id, password_conf_id, password_account_id) on delete cascade on update cascade deferrable initially deferred ); create trigger insert_auth_password_credential_subtype before insert on auth_password_argon2_cred for each row execute procedure insert_auth_password_credential_subtype(); create trigger update_auth_password_credential_subtype after update on auth_password_argon2_cred for each row execute procedure update_auth_password_credential_subtype(); create trigger delete_auth_password_credential_subtype after delete on auth_password_argon2_cred for each row execute procedure delete_auth_password_credential_subtype(); -- -- triggers for time columns -- create trigger immutable_columns before update on auth_password_argon2_conf for each row execute procedure immutable_columns('create_time'); create trigger default_create_time_column before insert on auth_password_argon2_conf for each row execute procedure default_create_time(); create trigger update_time_column before update on auth_password_argon2_cred for each row execute procedure update_time_column(); create trigger immutable_columns before update on auth_password_argon2_cred for each row execute procedure immutable_columns('create_time'); create trigger default_create_time_column before insert on auth_password_argon2_cred for each row execute procedure default_create_time(); -- The tickets for oplog are the subtypes not the base types because no updates -- are done to any values in the base types. insert into oplog_ticket (name, version) values ('auth_password_argon2_conf', 1), ('auth_password_argon2_cred', 1); commit; `), }, "migrations/14_auth_password_views.down.sql": { name: "14_auth_password_views.down.sql", bytes: []byte(` begin; drop view auth_password_current_conf; drop view auth_password_conf_union; commit; `), }, "migrations/14_auth_password_views.up.sql": { name: "14_auth_password_views.up.sql", bytes: []byte(` begin; -- auth_password_conf_union is a union of the configuration settings -- of all supported key derivation functions. -- It will be updated as new key derivation functions are supported. create or replace view auth_password_conf_union as -- Do not change the order of the columns when adding new configurations. -- Union with new tables appending new columns as needed. select c.password_method_id, c.private_id as password_conf_id, c.private_id, 'argon2' as conf_type, c.iterations, c.memory, c.threads, c.salt_length, c.key_length from auth_password_argon2_conf c; -- auth_password_current_conf provides a view of the current password -- configuration for each password auth method. -- The view will be updated as new key derivation functions are supported -- but the query to create the view should not need to be updated. create or replace view auth_password_current_conf as -- Rerun this query whenever auth_password_conf_union is updated. select pm.min_login_name_length, pm.min_password_length, c.* from auth_password_method pm inner join auth_password_conf_union c on pm.password_conf_id = c.password_conf_id; commit; `), }, "migrations/20_host.down.sql": { name: "20_host.down.sql", bytes: []byte(` begin; drop table host_set; drop table host; drop table host_catalog; drop function insert_host_set_subtype; drop function insert_host_subtype; drop function insert_host_catalog_subtype; delete from oplog_ticket where name in ( 'host_catalog', 'host', 'host_set' ); commit; `), }, "migrations/20_host.up.sql": { name: "20_host.up.sql", bytes: []byte(` begin; /* ┌─────────────────┐ │ host │ ├─────────────────┤ │ public_id (pk) │ │ catalog_id (fk) │ │ │ └─────────────────┘ ╲│╱ ○ │ ┼ ┼ ┌─────────────────┐ ┌─────────────────┐ │ iam_scope │ │ host_catalog │ ├─────────────────┤ ├─────────────────┤ │ public_id (pk) │ ╱│ public_id (pk) │ │ │┼┼──────○─│ scope_id (fk) │ │ │ ╲│ │ └─────────────────┘ └─────────────────┘ ┼ ┼ │ ○ ╱│╲ ┌─────────────────┐ │ host_set │ ├─────────────────┤ │ public_id (pk) │ │ catalog_id (fk) │ │ │ └─────────────────┘ */ -- host_catalog create table host_catalog ( public_id wt_public_id primary key, scope_id wt_scope_id not null references iam_scope (public_id) on delete cascade on update cascade, -- The order of columns is important for performance. See: -- https://dba.stackexchange.com/questions/58970/enforcing-constraints-two-tables-away/58972#58972 -- https://dba.stackexchange.com/questions/27481/is-a-composite-index-also-good-for-queries-on-the-first-field unique(scope_id, public_id) ); create trigger immutable_columns before update on host_catalog for each row execute procedure immutable_columns('public_id', 'scope_id'); -- insert_host_catalog_subtype() is a before insert trigger -- function for subtypes of host_catalog create or replace function insert_host_catalog_subtype() returns trigger as $$ begin insert into host_catalog (public_id, scope_id) values (new.public_id, new.scope_id); return new; end; $$ language plpgsql; -- delete_host_catalog_subtype() is an after delete trigger -- function for subtypes of host_catalog create or replace function delete_host_catalog_subtype() returns trigger as $$ begin delete from host_catalog where public_id = old.public_id; return null; -- result is ignored since this is an after trigger end; $$ language plpgsql; -- host create table host ( public_id wt_public_id primary key, catalog_id wt_public_id not null references host_catalog (public_id) on delete cascade on update cascade, unique(catalog_id, public_id) ); create trigger immutable_columns before update on host for each row execute procedure immutable_columns('public_id', 'catalog_id'); -- insert_host_subtype() is a before insert trigger -- function for subtypes of host create or replace function insert_host_subtype() returns trigger as $$ begin insert into host (public_id, catalog_id) values (new.public_id, new.catalog_id); return new; end; $$ language plpgsql; -- delete_host_subtype() is an after delete trigger -- function for subtypes of host create or replace function delete_host_subtype() returns trigger as $$ begin delete from host where public_id = old.public_id; return null; -- result is ignored since this is an after trigger end; $$ language plpgsql; -- host_set create table host_set ( public_id wt_public_id primary key, catalog_id wt_public_id not null references host_catalog (public_id) on delete cascade on update cascade, unique(catalog_id, public_id) ); create trigger immutable_columns before update on host_set for each row execute procedure immutable_columns('public_id', 'catalog_id'); -- insert_host_set_subtype() is a before insert trigger -- function for subtypes of host_set create or replace function insert_host_set_subtype() returns trigger as $$ begin insert into host_set (public_id, catalog_id) values (new.public_id, new.catalog_id); return new; end; $$ language plpgsql; -- delete_host_set_subtype() is an after delete trigger -- function for subtypes of host_set create or replace function delete_host_set_subtype() returns trigger as $$ begin delete from host_set where public_id = old.public_id; return null; -- result is ignored since this is an after trigger end; $$ language plpgsql; insert into oplog_ticket (name, version) values ('host_catalog', 1), ('host', 1), ('host_set', 1); commit; `), }, "migrations/22_static_host.down.sql": { name: "22_static_host.down.sql", bytes: []byte(` begin; drop table static_host_set_member cascade; drop table static_host_set cascade; drop table static_host cascade; drop table static_host_catalog cascade; delete from oplog_ticket where name in ( 'static_host_catalog', 'static_host', 'static_host_set', 'static_host_set_member' ); commit; `), }, "migrations/22_static_host.up.sql": { name: "22_static_host.up.sql", bytes: []byte(` begin; /* ┌─────────────────┐ ┌─────────────────────┐ │ host │ │ static_host │ ├─────────────────┤ ├─────────────────────┤ │ public_id (pk) │ │ public_id (pk) │ │ catalog_id (fk) │┼┼──────○┼│ catalog_id (fk) │┼┼─────────────────────┐ │ │ │ address │ ◀fk1 │ └─────────────────┘ └─────────────────────┘ │ ╲│╱ ╲│╱ │ ○ ○ │ │ │ │ ┼ ┼ ○ ┼ ┼ ╱│╲ ┌─────────────────┐ ┌─────────────────────┐ ┌────────────────────────┐ │ host_catalog │ │ static_host_catalog │ │ static_host_set_member │ ├─────────────────┤ ├─────────────────────┤ ├────────────────────────┤ │ public_id (pk) │ │ public_id (pk) │ │ host_id (pk,fk1) │ │ scope_id (fk) │┼┼──────○┼│ scope_id (fk) │ │ set_id (pk,fk2) │ │ │ │ │ │ catalog_id (fk1,fk2) │ └─────────────────┘ └─────────────────────┘ └────────────────────────┘ ┼ ┼ ╲│╱ ┼ ┼ ○ │ │ │ ○ ○ │ ╱│╲ ╱│╲ │ ┌─────────────────┐ ┌─────────────────────┐ │ │ host_set │ │ static_host_set │ │ ├─────────────────┤ ├─────────────────────┤ │ │ public_id (pk) │ │ public_id (pk) │ ◀fk2 │ │ catalog_id (fk) │┼┼──────○┼│ catalog_id (fk) │┼┼─────────────────────┘ │ │ │ │ └─────────────────┘ └─────────────────────┘ */ create table static_host_catalog ( public_id wt_public_id primary key, scope_id wt_scope_id not null references iam_scope (public_id) on delete cascade on update cascade, name text, description text, create_time wt_timestamp, update_time wt_timestamp, version wt_version, foreign key (scope_id, public_id) references host_catalog (scope_id, public_id) on delete cascade on update cascade, unique(scope_id, name) ); create trigger update_version_column after update on static_host_catalog for each row execute procedure update_version_column(); create trigger update_time_column before update on static_host_catalog for each row execute procedure update_time_column(); create trigger default_create_time_column before insert on static_host_catalog for each row execute procedure default_create_time(); create trigger immutable_columns before update on static_host_catalog for each row execute procedure immutable_columns('public_id', 'scope_id','create_time'); create trigger insert_host_catalog_subtype before insert on static_host_catalog for each row execute procedure insert_host_catalog_subtype(); create trigger delete_host_catalog_subtype after delete on static_host_catalog for each row execute procedure delete_host_catalog_subtype(); create table static_host ( public_id wt_public_id primary key, catalog_id wt_public_id not null references static_host_catalog (public_id) on delete cascade on update cascade, name text, description text, address text not null check( length(trim(address)) > 2 and length(trim(address)) < 256 ), create_time wt_timestamp, update_time wt_timestamp, version wt_version, unique(catalog_id, name), foreign key (catalog_id, public_id) references host (catalog_id, public_id) on delete cascade on update cascade, -- The order of columns is important for performance. See: -- https://dba.stackexchange.com/questions/58970/enforcing-constraints-two-tables-away/58972#58972 -- https://dba.stackexchange.com/questions/27481/is-a-composite-index-also-good-for-queries-on-the-first-field unique(catalog_id, public_id) ); create trigger update_version_column after update on static_host for each row execute procedure update_version_column(); create trigger update_time_column before update on static_host for each row execute procedure update_time_column(); create trigger default_create_time_column before insert on static_host for each row execute procedure default_create_time(); create trigger immutable_columns before update on static_host for each row execute procedure immutable_columns('public_id', 'catalog_id','create_time'); create trigger insert_host_subtype before insert on static_host for each row execute procedure insert_host_subtype(); create trigger delete_host_subtype after delete on static_host for each row execute procedure delete_host_subtype(); create table static_host_set ( public_id wt_public_id primary key, catalog_id wt_public_id not null references static_host_catalog (public_id) on delete cascade on update cascade, name text, description text, create_time wt_timestamp, update_time wt_timestamp, version wt_version, unique(catalog_id, name), foreign key (catalog_id, public_id) references host_set (catalog_id, public_id) on delete cascade on update cascade, unique(catalog_id, public_id) ); create trigger update_version_column after update on static_host_set for each row execute procedure update_version_column(); create trigger update_time_column before update on static_host_set for each row execute procedure update_time_column(); create trigger default_create_time_column before insert on static_host_set for each row execute procedure default_create_time(); create trigger immutable_columns before update on static_host_set for each row execute procedure immutable_columns('public_id', 'catalog_id','create_time'); create trigger insert_host_set_subtype before insert on static_host_set for each row execute procedure insert_host_set_subtype(); create trigger delete_host_set_subtype after delete on static_host_set for each row execute procedure delete_host_set_subtype(); create table static_host_set_member ( host_id wt_public_id not null, set_id wt_public_id not null, catalog_id wt_public_id not null, primary key(host_id, set_id), foreign key (catalog_id, host_id) -- fk1 references static_host (catalog_id, public_id) on delete cascade on update cascade, foreign key (catalog_id, set_id) -- fk2 references static_host_set (catalog_id, public_id) on delete cascade on update cascade ); create trigger immutable_columns before update on static_host_set_member for each row execute procedure immutable_columns('host_id', 'set_id', 'catalog_id'); create or replace function insert_static_host_set_member() returns trigger as $$ begin select static_host_set.catalog_id into new.catalog_id from static_host_set where static_host_set.public_id = new.set_id; return new; end; $$ language plpgsql; create trigger insert_static_host_set_member before insert on static_host_set_member for each row execute procedure insert_static_host_set_member(); insert into oplog_ticket (name, version) values ('static_host_catalog', 1), ('static_host', 1), ('static_host_set', 1), ('static_host_set_member', 1); commit; `), }, "migrations/30_keys.down.sql": { name: "30_keys.down.sql", bytes: []byte(` begin; drop function kms_version_column cascade; commit; `), }, "migrations/30_keys.up.sql": { name: "30_keys.up.sql", bytes: []byte(` begin; -- kms_version_column() will increment the version column whenever row data -- is inserted and should only be used in an before insert trigger. This -- function will overwrite any explicit values to the version column. create or replace function kms_version_column() returns trigger as $$ declare _key_id text; _max bigint; begin execute format('SELECT $1.%I', tg_argv[0]) into _key_id using new; execute format('select max(version) + 1 from %I where %I = $1', tg_relid::regclass, tg_argv[0]) using _key_id into _max; if _max is null then _max = 1; end if; new.version = _max; return new; end; $$ language plpgsql; comment on function kms_version_column() is 'function used in before insert triggers to properly set version columns for kms_* tables with a version column'; commit; `), }, "migrations/31_keys.down.sql": { name: "31_keys.down.sql", bytes: []byte(` begin; drop table kms_root_key cascade; drop table kms_root_key_version cascade; drop table kms_database_key cascade; drop table kms_database_key_version cascade; drop table kms_oplog_key cascade; drop table kms_oplog_key_version cascade; drop table kms_session_key cascade; drop table kms_session_key_version cascade; commit; `), }, "migrations/31_keys.up.sql": { name: "31_keys.up.sql", bytes: []byte(` begin; /* ┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ ├────────────────────────────────────────────────────────────────────────────────────────────┐ ○ ├────────────────────────────────────────────────────────────────┐ ○ ┼ ├────────────────────────────────────┐ ○ ┼ ┌────────────────────────┐ │ ○ ┼ ┌────────────────────────┐ │ kms_token_key │ ┼ ┼ ┌────────────────────────┐ │ kms_session_key │ ├────────────────────────┤ ┌────────────────────────┐ ┌────────────────────────┐ │ kms_oplog_key │ ├────────────────────────┤ │private_id │ │ kms_root_key │ │ kms_database_key │ ├────────────────────────┤ │private_id │ │root_key_id │ ├────────────────────────┤ ├────────────────────────┤ │private_id │ │root_key_id │ │ │ │private_id │ │private_id │ │root_key_id │ │ │ │ │ │scope_id │ │root_key_id │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └────────────────────────┘ └────────────────────────┘ └────────────────────────┘ └────────────────────────┘ └────────────────────────┘ ┼ ┼ ┼ ┼ ┼ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ┼ ┼ ┼ ┼ │ ╱│╲ ╱│╲ ╱│╲ ╱│╲ ┼ ┌────────────────────────┐ ┌────────────────────────┐ ┌────────────────────────┐ ┌────────────────────────┐ ╱│╲ │ kms_root_key_version │ │kms_database_key_version│ │ kms_oplog_key_version │ │kms_session_key_version │ ┌────────────────────────┐ ├────────────────────────┤ ├────────────────────────┤ ├────────────────────────┤ ├────────────────────────┤ │ kms_token_key_version │ │private_id │ │private_id │ │private_id │ │private_id │ ├────────────────────────┤ │root_key_id │ │database_key_id │ │oplog_key_id │ │session_key_id │ │private_id │ │key │ │root_key_id │ │root_key_id │ │root_key_id │ │token_key_id │ │version │ │key │ │key │ │key │ │root_key_id │ │ │ │version │ │version │ │version │ │key │ └────────────────────────┘ └────────────────────────┘ │ │ │ │ │version │ ┼ ┼ └────────────────────────┘ │ │ │ │ │ ○ ┼ └────────────────────────┘ │ │ ├────────────────────────────────────┘ ○ ┼ └────────────────────────┘ ├────────────────────────────────────────────────────────────────┘ ○ ┼ ├────────────────────────────────────────────────────────────────────────────────────────────┘ ○ └────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ create table kms_root_key ( private_id wt_private_id primary key, scope_id wt_scope_id not null unique -- there can only be one root key for a scope. references iam_scope(public_id) on delete cascade on update cascade, create_time wt_timestamp ); -- define the immutable fields for kms_root_key (all of them) create trigger immutable_columns before update on kms_root_key for each row execute procedure immutable_columns('private_id', 'scope_id', 'create_time'); create trigger default_create_time_column before insert on kms_root_key for each row execute procedure default_create_time(); create table kms_root_key_version ( private_id wt_private_id primary key, root_key_id wt_private_id not null references kms_root_key(private_id) on delete cascade on update cascade, version wt_version, key bytea not null, create_time wt_timestamp, unique(root_key_id, version) ); -- define the immutable fields for kms_root_key_version (all of them) create trigger immutable_columns before update on kms_root_key_version for each row execute procedure immutable_columns('private_id', 'root_key_id', 'version', 'key', 'create_time'); create trigger default_create_time_column before insert on kms_root_key_version for each row execute procedure default_create_time(); create trigger kms_version_column before insert on kms_root_key_version for each row execute procedure kms_version_column('root_key_id'); create table kms_database_key ( private_id wt_private_id primary key, root_key_id wt_private_id not null unique -- there can be only one database dek per root key references kms_root_key(private_id) on delete cascade on update cascade, create_time wt_timestamp ); -- define the immutable fields for kms_database_key (all of them) create trigger immutable_columns before update on kms_database_key for each row execute procedure immutable_columns('private_id', 'root_key_id', 'create_time'); create trigger default_create_time_column before insert on kms_database_key for each row execute procedure default_create_time(); create table kms_database_key_version ( private_id wt_private_id primary key, database_key_id wt_private_id not null references kms_database_key(private_id) on delete cascade on update cascade, root_key_version_id wt_private_id not null references kms_root_key_version(private_id) on delete cascade on update cascade, version wt_version, key bytea not null, create_time wt_timestamp, unique(database_key_id, version) ); -- define the immutable fields for kms_database_key_version (all of them) create trigger immutable_columns before update on kms_database_key_version for each row execute procedure immutable_columns('private_id', 'database_key_id', 'root_key_version_id', 'version', 'key', 'create_time'); create trigger default_create_time_column before insert on kms_database_key_version for each row execute procedure default_create_time(); create trigger kms_version_column before insert on kms_database_key_version for each row execute procedure kms_version_column('database_key_id'); create table kms_oplog_key ( private_id wt_private_id primary key, root_key_id wt_private_id not null unique -- there can be only one oplog dek per root key references kms_root_key(private_id) on delete cascade on update cascade, create_time wt_timestamp ); -- define the immutable fields for kms_oplog_key (all of them) create trigger immutable_columns before update on kms_oplog_key for each row execute procedure immutable_columns('private_id', 'root_key_id', 'create_time'); create trigger default_create_time_column before insert on kms_oplog_key for each row execute procedure default_create_time(); create table kms_oplog_key_version ( private_id wt_private_id primary key, oplog_key_id wt_private_id not null references kms_oplog_key(private_id) on delete cascade on update cascade, root_key_version_id wt_private_id not null references kms_root_key_version(private_id) on delete cascade on update cascade, version wt_version, key bytea not null, create_time wt_timestamp, unique(oplog_key_id, version) ); -- define the immutable fields for kms_oplog_key_version (all of them) create trigger immutable_columns before update on kms_oplog_key_version for each row execute procedure immutable_columns('private_id', 'oplog_key_id', 'root_key_version_id', 'version', 'key', 'create_time'); create trigger default_create_time_column before insert on kms_oplog_key_version for each row execute procedure default_create_time(); create trigger kms_version_column before insert on kms_oplog_key_version for each row execute procedure kms_version_column('oplog_key_id'); create table kms_session_key ( private_id wt_private_id primary key, root_key_id wt_private_id not null unique -- there can be only one session dek per root key references kms_root_key(private_id) on delete cascade on update cascade, create_time wt_timestamp ); -- define the immutable fields for kms_oplog_key (all of them) create trigger immutable_columns before update on kms_session_key for each row execute procedure immutable_columns('private_id', 'root_key_id', 'create_time'); create trigger default_create_time_column before insert on kms_session_key for each row execute procedure default_create_time(); create table kms_session_key_version ( private_id wt_private_id primary key, session_key_id wt_private_id not null references kms_session_key(private_id) on delete cascade on update cascade, root_key_version_id wt_private_id not null references kms_root_key_version(private_id) on delete cascade on update cascade, version wt_version, key bytea not null, create_time wt_timestamp, unique(session_key_id, version) ); -- define the immutable fields for kms_session_key_version (all of them) create trigger immutable_columns before update on kms_session_key_version for each row execute procedure immutable_columns('private_id', 'session_key_id', 'root_key_version_id', 'version', 'key', 'create_time'); create trigger default_create_time_column before insert on kms_session_key_version for each row execute procedure default_create_time(); create trigger kms_version_column before insert on kms_session_key_version for each row execute procedure kms_version_column('session_key_id'); create table kms_token_key ( private_id wt_private_id primary key, root_key_id wt_private_id not null unique -- there can be only one token dek per root key references kms_root_key(private_id) on delete cascade on update cascade, create_time wt_timestamp ); -- define the immutable fields for kms_token_key (all of them) create trigger immutable_columns before update on kms_token_key for each row execute procedure immutable_columns('private_id', 'root_key_id', 'create_time'); create trigger default_create_time_column before insert on kms_token_key for each row execute procedure default_create_time(); create table kms_token_key_version ( private_id wt_private_id primary key, token_key_id wt_private_id not null references kms_token_key(private_id) on delete cascade on update cascade, root_key_version_id wt_private_id not null references kms_root_key_version(private_id) on delete cascade on update cascade, version wt_version, key bytea not null, create_time wt_timestamp, unique(token_key_id, version) ); -- define the immutable fields for kms_token_key_version (all of them) create trigger immutable_columns before update on kms_token_key_version for each row execute procedure immutable_columns('private_id', 'token_key_id', 'root_key_version_id', 'version', 'key', 'create_time'); create trigger default_create_time_column before insert on kms_token_key_version for each row execute procedure default_create_time(); create trigger kms_version_column before insert on kms_token_key_version for each row execute procedure kms_version_column('token_key_id'); insert into oplog_ticket (name, version) values ('kms_root_key', 1), ('kms_root_key_version', 1); commit; `), }, "migrations/40_targets.down.sql": { name: "40_targets.down.sql", bytes: []byte(` begin; drop function insert_target_subtype; drop function delete_target_subtype; drop function target_scope_valid; drop function target_host_set_scope_valid commit; `), }, "migrations/40_targets.up.sql": { name: "40_targets.up.sql", bytes: []byte(` begin; -- insert_target_subtype() is a before insert trigger -- function for subtypes of target create or replace function insert_target_subtype() returns trigger as $$ begin insert into target (public_id, scope_id) values (new.public_id, new.scope_id); return new; end; $$ language plpgsql; -- delete_target_subtype() is an after delete trigger -- function for subtypes of host create or replace function delete_target_subtype() returns trigger as $$ begin delete from target where public_id = old.public_id; return null; -- result is ignored since this is an after trigger end; $$ language plpgsql; -- target_scope_valid() is a before insert trigger function for target create or replace function target_scope_valid() returns trigger as $$ declare scope_type text; begin -- Fetch the type of scope select isc.type from iam_scope isc where isc.public_id = new.scope_id into scope_type; if scope_type = 'project' then return new; end if; raise exception 'invalid target scope type % (must be project)', scope_type; end; $$ language plpgsql; -- target_host_set_scope_valid() is a before insert trigger function for target_host_set create or replace function target_host_set_scope_valid() returns trigger as $$ begin perform from host_catalog hc, host_set hs, target t, iam_scope s where hc.public_id = hs.catalog_id and hc.scope_id = t.scope_id and t.public_id = new.target_id; if not found then raise exception 'target scope and host set scope are not equal'; end if; return new; end; $$ language plpgsql; commit; `), }, "migrations/41_targets.down.sql": { name: "41_targets.down.sql", bytes: []byte(` begin; drop table target cascade; drop table target_host_set cascade; drop table target_tcp; drop view target_all_subtypes; drop view target_host_set_catalog; delete from oplog_ticket where name in ( 'target_tcp' ); commit; `), }, "migrations/41_targets.up.sql": { name: "41_targets.up.sql", bytes: []byte(` /* ┌─────────────────┐ ┌─────────────────┐ │ target_tcp │ │ target │ ├─────────────────┤ ├─────────────────┤ │public_id │ │public_id │┼─────────────○┼│scope_id │ │scope_id │ │default_port │ │ │ │name (not null) │ └─────────────────┘ │description │ ┼ └─────────────────┘ │ ○ ╱│╲ ┌─────────────────┐ │ target_host_set │ ├─────────────────┤ │target_id │ │host_set_id │ │ │ └─────────────────┘ ╲│╱ ○ │ │ ┼ ┌─────────────────┐ │ host_set │ ├─────────────────┤ │public_id │ │catalog_id │ │ │ └─────────────────┘ */ begin; create table target ( public_id wt_public_id primary key, scope_id wt_scope_id not null references iam_scope(public_id) on delete cascade on update cascade, create_time wt_timestamp ); create trigger immutable_columns before update on target for each row execute procedure immutable_columns('public_id', 'scope_id', 'create_time'); create trigger default_create_time_column before insert on target for each row execute procedure default_create_time(); create trigger target_scope_valid before insert on target for each row execute procedure target_scope_valid(); create table target_host_set( target_id wt_public_id references target(public_id) on delete cascade on update cascade, host_set_id wt_public_id references host_set(public_id) on delete cascade on update cascade, primary key(target_id, host_set_id), create_time wt_timestamp ); create trigger immutable_columns before update on target_host_set for each row execute procedure immutable_columns('target_id', 'host_set_id', 'create_time'); create trigger target_host_set_scope_valid before insert on target_host_set for each row execute procedure target_host_set_scope_valid(); create table target_tcp ( public_id wt_public_id primary key references target(public_id) on delete cascade on update cascade, scope_id wt_scope_id not null references iam_scope(public_id) on delete cascade on update cascade, name text not null, -- name is not optional for a target subtype description text, default_port int, -- default_port can be null create_time wt_timestamp, update_time wt_timestamp, version wt_version, unique(scope_id, name) -- name must be unique within a scope ); create trigger insert_target_subtype before insert on target_tcp for each row execute procedure insert_target_subtype(); create trigger delete_target_subtype after delete on target_tcp for each row execute procedure delete_target_subtype(); -- define the immutable fields for target create trigger immutable_columns before update on target_tcp for each row execute procedure immutable_columns('public_id', 'scope_id', 'create_time'); create trigger update_version_column after update on target_tcp for each row execute procedure update_version_column(); create trigger update_time_column before update on target_tcp for each row execute procedure update_time_column(); create trigger default_create_time_column before insert on target_tcp for each row execute procedure default_create_time(); create trigger target_scope_valid before insert on target_tcp for each row execute procedure target_scope_valid(); -- target_all_subtypes is a union of all target subtypes create view target_all_subtypes as select public_id, scope_id, name, description, default_port, version, create_time, update_time, 'tcp' as type from target_tcp; create view target_set as select hs.public_id, hs.catalog_id, ths.target_id from target_host_set ths, host_set hs where hs.public_id = ths.host_set_id; insert into oplog_ticket (name, version) values ('target_tcp', 1); commit; `), }, "migrations/50_session.down.sql": { name: "50_session.down.sql", bytes: []byte(` begin; drop table session_state; drop table session_state_enm; drop table session; drop table session_termination_reason_enm; drop function insert_session_state; commit; `), }, "migrations/50_session.up.sql": { name: "50_session.up.sql", bytes: []byte(` begin; /* ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ iam_scope_org │ │ iam_user │ │ auth_token │ ├─────────────────┤ ├─────────────────┤ ├─────────────────┤ │ public_id (pk) │ │ public_id (pk) │ │ public_id (pk) │ │ │ │ │ │ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ ▲fk7 ┼ ▲fk1 ┼ ▲fk6 ┼ ┼ ┼ ┼ ├─────────────────────────────────┴─────────────────────┘ │ ○ ╱│╲ ┌──────────────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ session │╲ fk3▶ │ server │ │ target │ ├──────────────────────────┤─○──────○┼├─────────────────┤ ├─────────────────┤ │ public_id (pk) │╱ │ private_id (pk) │ │ public_id (pk) │ │ user_id (fk1) │ │ type (pk) │ │ │ │ host_id (fk2) │ └─────────────────┘ └─────────────────┘ │ server_id (fk3) │ ▲fk4 ┼ │ server_type (fk3) │╲ ┼ │ target_id (fk4) │─○─────────────────┬─────────────────────┤ │ set_id (fk5) │╱ ┼ ┼ │ auth_token_id (fk6) │ ▼fk5 ┼ ▼fk2 ┼ │ scope_id (fk7) │ ┌─────────────────┐ ┌─────────────────┐ │ termination_reason (fk8) │ │ host_set │ │ host │ └──────────────────────────┘ ├─────────────────┤ ├─────────────────┤ ▲fk1 ┼ ╲│╱ │ public_id (pk) │ │ public_id (pk) │ ┼ ○ │ │ │ │ │ │ └─────────────────┘ └─────────────────┘ │ │ └─┐ │ │ │ ┌───────────────────────────────┐ │ │ │session_termination_reason_enm │ │ │ fk8▶ ├───────────────────────────────┤ ┼ └──────────○┼│ name │ ╱│╲ └───────────────────────────────┘ ┌──────────────────────────────────────────┐ │ session_state │ ├──────────────────────────────────────────┤┼○┐ │ session_id (pk,fk1,fk2,unq1,unq2) │ │◀fk2 │ state (fk3) │ │ │ previous_end_time (fk2,unq1) │┼○┘ │ start_time (pk) │ │ end_time (unq2) │ └──────────────────────────────────────────┘ ╲│╱ ○ │ │ ┼ ▼fk3 ┼ ┌───────────────────────────────┐ │ session_state_enm │ ├───────────────────────────────┤ │ name │ └───────────────────────────────┘ */ create table session_termination_reason_enm ( name text primary key check ( name in ( 'unknown', 'timed out', 'closed by end-user', 'terminated', 'network error', 'system error' ) ) ); insert into session_termination_reason_enm (name) values ('unknown'), ('timed out'), ('closed by end-user'), ('terminated'), ('network error'), ('system error'); create table session ( public_id wt_public_id primary key, -- the user of the session user_id text -- fk1 -- not using the wt_user_id domain type because it is marked 'not null' references iam_user (public_id) on delete set null on update cascade, -- the host the user is connected to via this session host_id wt_public_id -- fk2 references host (public_id) on delete set null on update cascade, -- the worker proxying the connection between the user and the host server_id text, -- fk3 server_type text,-- fk3 foreign key (server_id, server_type) references server (private_id, type) on delete set null on update cascade, -- the target the host was chosen from and the user was authorized to -- connect to target_id wt_public_id -- fk4 references target (public_id) on delete set null on update cascade, -- the host set the host was chosen from and the user was authorized to -- connect to via the target set_id wt_public_id -- fk5 references host_set (public_id) on delete set null on update cascade, -- the auth token of the user when this session was created auth_token_id wt_public_id -- fk6 references auth_token (public_id) on delete set null on update cascade, -- the organization which owns this session scope_id wt_scope_id -- fk7 references iam_scope_org (scope_id) on delete set null on update cascade, -- the reason this session ended (null until terminated) termination_reason text -- fk8 references session_termination_reason_enm (name) on delete restrict on update cascade, -- the network address of the host the worker proxied a connection to for -- the user address text not null check( length(trim(address)) > 7 and length(trim(address)) < 256 ), -- the network port at the address of the host the worker proxied a -- connection to for the user port integer not null check( port > 0 and port <= 65535 ), -- the total number of bytes received by the worker from the user and sent -- to the host for this session bytes_up bigint -- can be null check ( bytes_up is null or bytes_up >= 0 ), -- the total number of bytes received by the worker from the host and sent -- to the user for this session bytes_down bigint -- can be null check ( bytes_down is null or bytes_down >= 0 ) ); create table session_state_enm ( name text primary key check ( name in ('pending', 'connected', 'canceling', 'closed') ) ); insert into session_state_enm (name) values ('pending'), ('connected'), ('canceling'), ('closed'); /* ┌────────────────┐ start │ │ . │ Canceling │ (●) ┌────▶│ │─────┐ ' │ │ │ │ │ │ └────────────────┘ │ │ │ │ ▼ │ ▼ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │ │ │ │ │ │ │ Pending │ │ Connected │ │ Closed │ │ │──────────▶│ │──────────▶│ │ │ │ │ │ │ │ └────────────────┘ └────────────────┘ └────────────────┘ │ │ │ │ │ │ │ │ └──────────────────────▶ ◉ End ◀────────────────────────┘ */ -- Design influenced by: -- Joe Celko's SQL for Smarties: Advanced SQL Programming, 5th edition -- Chapter 12, p270 create table session_state ( session_id wt_public_id not null -- fk1, fk2 references session (public_id) on delete cascade on update cascade, state text not null -- fk3 references session_state_enm(name) on delete restrict on update cascade, previous_end_time timestamp with time zone, -- fk2 -- null means first state start_time timestamp with time zone default current_timestamp not null, constraint previous_end_time_and_start_time_in_sequence check (previous_end_time <= start_time), end_time timestamp with time zone, -- null means unfinished current state constraint start_and_end_times_in_sequence check (start_time <= end_time), constraint end_times_in_sequence check (previous_end_time <> end_time), primary key (session_id, start_time), unique (session_id, previous_end_time), -- null means first state unique (session_id, end_time), -- one null current state foreign key (session_id, previous_end_time) -- self-reference references session_state (session_id, end_time) ); create or replace function insert_session_state() returns trigger as $$ begin update session_state set end_time = now() where session_id = new.session_id and end_time is null; if not found then new.previous_end_time = null; new.start_time = now(); new.end_time = null; return new; end if; new.previous_end_time = now(); new.start_time = now(); new.end_time = null; return new; end; $$ language plpgsql; create trigger insert_session_state before insert on session_state for each row execute procedure insert_session_state(); commit; `), }, }