From 99d0d2f68238218d8cf4ceca046577724a6ffd0a Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Thu, 16 Sep 2021 08:53:11 -0400 Subject: [PATCH] wh: Add wh_credential_dimension and bridge tables This adds new dimension table to record credential information. Since a session can have multiple credentials, the wh_session_accumulating_fact is is linked to the credential_dimensions via bridge tables, wh_credential_group and wh_credential_group_membership. When a session is inserted, the credental group and credential dimensions will be upserted. If the group changed in any way, a new group is inserted along with all of the corresponding credentials. --- .../16/01_wh_credential_dimension.up.sql | 129 +++++ .../16/02_wh_credential_dimension.up.sql | 100 ++++ .../16/03_wh_credential_dimension.up.sql | 170 ++++++ .../16/04_wh_credential_dimension.up.sql | 54 ++ .../16/05_wh_credential_dimension.up.sql | 75 +++ .../16/warehouse_credential_dim_test.go | 189 +++++++ internal/db/schema/postgres_migration.gen.go | 528 +++++++++++++++++- .../db/sqltest/initdb.d/01_colors_persona.sql | 20 +- .../sqltest/initdb.d/03_widgets_persona.sql | 26 + .../wh/credential_dimension/dimension.sql | 53 ++ .../session_multiple_sessions.sql | 108 ++++ .../credential_dimension/session_update.sql | 33 ++ .../three_credentials_one_change.sql | 47 ++ .../wh/credential_dimension_views/source.sql | 51 ++ .../wh/credential_dimension_views/target.sql | 44 ++ .../wh/session_credential_dynamic/insert.sql | 27 + .../insert_multiple.sql | 29 + internal/session/query.go | 36 ++ internal/session/repository_session.go | 18 +- 19 files changed, 1731 insertions(+), 6 deletions(-) create mode 100644 internal/db/schema/migrations/postgres/16/01_wh_credential_dimension.up.sql create mode 100644 internal/db/schema/migrations/postgres/16/02_wh_credential_dimension.up.sql create mode 100644 internal/db/schema/migrations/postgres/16/03_wh_credential_dimension.up.sql create mode 100644 internal/db/schema/migrations/postgres/16/04_wh_credential_dimension.up.sql create mode 100644 internal/db/schema/migrations/postgres/16/05_wh_credential_dimension.up.sql create mode 100644 internal/db/schema/migrations/postgres/16/warehouse_credential_dim_test.go create mode 100644 internal/db/sqltest/tests/wh/credential_dimension/dimension.sql create mode 100644 internal/db/sqltest/tests/wh/credential_dimension/session_multiple_sessions.sql create mode 100644 internal/db/sqltest/tests/wh/credential_dimension/session_update.sql create mode 100644 internal/db/sqltest/tests/wh/credential_dimension/three_credentials_one_change.sql create mode 100644 internal/db/sqltest/tests/wh/credential_dimension_views/source.sql create mode 100644 internal/db/sqltest/tests/wh/credential_dimension_views/target.sql create mode 100644 internal/db/sqltest/tests/wh/session_credential_dynamic/insert.sql create mode 100644 internal/db/sqltest/tests/wh/session_credential_dynamic/insert_multiple.sql diff --git a/internal/db/schema/migrations/postgres/16/01_wh_credential_dimension.up.sql b/internal/db/schema/migrations/postgres/16/01_wh_credential_dimension.up.sql new file mode 100644 index 0000000000..8d8420cbb2 --- /dev/null +++ b/internal/db/schema/migrations/postgres/16/01_wh_credential_dimension.up.sql @@ -0,0 +1,129 @@ +begin; + -- replaces check from internal/db/schema/migrations/postgres/0/60_wh_domain_types.up.sql + alter domain wh_public_id drop constraint wh_public_id_check; + alter domain wh_public_id add constraint wh_public_id_check + check( + value = 'None' + or + value = 'Unknown' + or + length(trim(value)) > 10 + ); + + create table wh_credential_dimension ( + -- random id generated using encode(digest(gen_random_bytes(16), 'sha256'), 'base64') + -- this is done to prevent conflicts with rows in other clusters + -- which enables warehouse data from multiple clusters to be loaded into a + -- single database instance + key wh_dim_key primary key default wh_dim_key(), + + credential_purpose wh_dim_text, + credential_library_id wh_public_id not null, + credential_library_type wh_dim_text, + credential_library_name wh_dim_text, + credential_library_description wh_dim_text, + credential_library_vault_path wh_dim_text, + credential_library_vault_http_method wh_dim_text, + credential_library_vault_http_request_body wh_dim_text, + + credential_store_id wh_public_id not null, + credential_store_type wh_dim_text, + credential_store_name wh_dim_text, + credential_store_description wh_dim_text, + credential_store_vault_namespace wh_dim_text, + credential_store_vault_address wh_dim_text, + + target_id wh_public_id not null, + target_type wh_dim_text, + target_name wh_dim_text, + target_description wh_dim_text, + target_default_port_number integer not null, + target_session_max_seconds integer not null, + target_session_connection_limit integer not null, + + project_id wt_scope_id not null, + project_name wh_dim_text, + project_description wh_dim_text, + + organization_id wt_scope_id not null, + organization_name wh_dim_text, + organization_description wh_dim_text, + + current_row_indicator wh_dim_text, + row_effective_time wh_timestamp, + row_expiration_time wh_timestamp + ); + + -- https://www.postgresql.org/docs/current/indexes-partial.html + create unique index wh_credential_dim_current_constraint + on wh_credential_dimension (credential_library_id, credential_store_id, target_id, credential_purpose) + where current_row_indicator = 'Current'; + + -- One part of a bridge table to associated the set of wh_credential_dimension with a fact table. + -- The other part of the bridge is wh_credential_group_membership. + create table wh_credential_group ( + -- random id generated using encode(digest(gen_random_bytes(16), 'sha256'), 'base64') + -- this is done to prevent conflicts with rows in other clusters + -- which enables warehouse data from multiple clusters to be loaded into a + -- single database instance + key wh_dim_key primary key default wh_dim_key() + ); + + -- The second part of the bridge table. The other part is wh_credential_group. + create table wh_credential_group_membership ( + credential_group_key wh_dim_key not null + references wh_credential_group (key) + on delete restrict + on update cascade, + credential_key wh_dim_key not null + references wh_credential_dimension (key) + on delete restrict + on update cascade + ); + + -- Add "no credentials" and "Unknown" group an dimension. + -- When a session has no credentials "no credentials" is used as the "None" value. + -- "Unknown" is used for existing data prior to the credential_dimension existing. + insert into wh_credential_group + (key) + values + ('no credentials'), + ('Unknown'); + insert into wh_credential_dimension ( + key, + credential_purpose, + credential_library_id, credential_library_type, credential_library_name, credential_library_description, credential_library_vault_path, credential_library_vault_http_method, credential_library_vault_http_request_body, + credential_store_id, credential_store_type, credential_store_name, credential_store_description, credential_store_vault_namespace, credential_store_vault_address, + target_id, target_type, target_name, target_description, target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id, project_name, project_description, + organization_id, organization_name, organization_description, + current_row_indicator, row_effective_time, row_expiration_time + ) + values + ( + 'no credential', + 'None', + 'None', 'None', 'None', 'None', 'None', 'None', 'None', + 'None', 'None', 'None', 'None', 'None', 'None', + 'None', 'None', 'None', 'None', -1, -1, -1, + '00000000000', 'None', 'None', + '00000000000', 'None', 'None', + 'Current', now(), 'infinity'::timestamptz + ), + ( + 'Unknown', + 'Unknown', + 'Unknown', 'Unknown', 'Unknown', 'Unknown', 'Unknown', 'Unknown', 'Unknown', + 'Unknown', 'Unknown', 'Unknown', 'Unknown', 'Unknown', 'Unknown', + 'Unknown', 'Unknown', 'Unknown', 'Unknown', -1, -1, -1, + '00000000000', 'Unknown', 'Unknown', + '00000000000', 'Unknown', 'Unknown', + 'Current', now(), 'infinity'::timestamptz + ); + insert into wh_credential_group_membership + (credential_group_key, credential_key) + values + ('no credentials', 'no credential'), + ('Unknown', 'Unknown'); + +commit; diff --git a/internal/db/schema/migrations/postgres/16/02_wh_credential_dimension.up.sql b/internal/db/schema/migrations/postgres/16/02_wh_credential_dimension.up.sql new file mode 100644 index 0000000000..0d2838fcc7 --- /dev/null +++ b/internal/db/schema/migrations/postgres/16/02_wh_credential_dimension.up.sql @@ -0,0 +1,100 @@ +begin; + -- The whx_credential_dimension_source and whx_credential_dimension_target views are used + -- by an insert trigger to determine if the current row for the dimension has + -- changed and a new one needs to be inserted. The first column in the target view + -- must be the current warehouse id and all remaining columns must match the columns + -- in the source view. + + -- The whx_credential_dimension_source view shows the current values in the + -- operational tables of the credential dimension. + create view whx_credential_dimension_source as + select -- id is the first column in the target view + s.public_id as session_id, + coalesce(scd.credential_purpose, 'None') as credential_purpose, + cl.public_id as credential_library_id, + case + when vcl is null then 'None' + else 'vault credential library' + end as credential_library_type, + coalesce(vcl.name, 'None') as credential_library_name, + coalesce(vcl.description, 'None') as credential_library_description, + coalesce(vcl.vault_path, 'None') as credential_library_vault_path, + coalesce(vcl.http_method, 'None') as credential_library_vault_http_method, + coalesce(vcl.http_request_body, 'None') as credential_library_vault_http_request_body, + cs.public_id as credential_store_id, + case + when vcs is null then 'None' + else 'vault credential store' + end as credential_store_type, + coalesce(vcs.name, 'None') as credential_store_name, + coalesce(vcs.description, 'None') as credential_store_description, + coalesce(vcs.namespace, 'None') as credential_store_vault_namespace, + coalesce(vcs.vault_address, 'None') as credential_store_vault_address, + t.public_id as target_id, + 'tcp target' as target_type, + coalesce(tt.name, 'None') as target_name, + coalesce(tt.description, 'None') as target_description, + coalesce(tt.default_port, 0) as target_default_port_number, + tt.session_max_seconds as target_session_max_seconds, + tt.session_connection_limit as target_session_connection_limit, + p.public_id as project_id, + coalesce(p.name, 'None') as project_name, + coalesce(p.description, 'None') as project_description, + o.public_id as organization_id, + coalesce(o.name, 'None') as organization_name, + coalesce(o.description, 'None') as organization_description + from session_credential_dynamic as scd, + session as s, + credential_library as cl, + credential_store as cs, + credential_vault_library as vcl, + credential_vault_store as vcs, + target as t, + target_tcp as tt, + iam_scope as p, + iam_scope as o + where scd.library_id = cl.public_id + and cl.store_id = cs.public_id + and vcl.public_id = cl.public_id + and vcs.public_id = cs.public_id + and s.public_id = scd.session_id + and s.target_id = t.public_id + and t.public_id = tt.public_id + and p.public_id = t.scope_id + and p.type = 'project' + and o.public_id = p.parent_id + and o.type = 'org'; + + create view whx_credential_dimension_target as + select key, + credential_purpose, + credential_library_id, + credential_library_type, + credential_library_name, + credential_library_description, + credential_library_vault_path, + credential_library_vault_http_method, + credential_library_vault_http_request_body, + credential_store_id, + credential_store_type, + credential_store_name, + credential_store_description, + credential_store_vault_namespace, + credential_store_vault_address, + target_id, + target_type, + target_name, + target_description, + target_default_port_number, + target_session_max_seconds, + target_session_connection_limit, + project_id, + project_name, + project_description, + organization_id, + organization_name, + organization_description + from wh_credential_dimension + where current_row_indicator = 'Current' + ; +commit; diff --git a/internal/db/schema/migrations/postgres/16/03_wh_credential_dimension.up.sql b/internal/db/schema/migrations/postgres/16/03_wh_credential_dimension.up.sql new file mode 100644 index 0000000000..8f3d126734 --- /dev/null +++ b/internal/db/schema/migrations/postgres/16/03_wh_credential_dimension.up.sql @@ -0,0 +1,170 @@ +begin; + -- wh_upsert_credential_dimension compares the current vaules in the wh_credential_dimension + -- with the current values in the operational tables for the given parameters. IF the values + -- between operational tables and the wh_credential_dimension differ, a new row is inserted in + -- the wh_credential_dimension to match the current values in the operational tables. + create function wh_upsert_credential_dimension(p_session_id wt_public_id, p_library_id wt_public_id, p_credential_purpose wh_dim_text) + returns wh_dim_key + as $$ + declare + src whx_credential_dimension_target%rowtype; + target whx_credential_dimension_target%rowtype; + new_row wh_credential_dimension%rowtype; + t_id wt_public_id; + begin + select s.target_id into strict t_id + from session as s + where s.public_id = p_session_id; + + select * into target + from whx_credential_dimension_target as t + where t.credential_library_id = p_library_id + and t.target_id = t_id + and t.credential_purpose = p_credential_purpose; + + select + target.key, t.credential_purpose, + t.credential_library_id, t.credential_library_type, t.credential_library_name, t.credential_library_description, t.credential_library_vault_path, t.credential_library_vault_http_method, t.credential_library_vault_http_request_body, + t.credential_store_id, t.credential_store_type, t.credential_store_name, t.credential_store_description, t.credential_store_vault_namespace, t.credential_store_vault_address, + t.target_id, t.target_type, t.target_name, t.target_description, t.target_default_port_number, t.target_session_max_seconds, t.target_session_connection_limit, + t.project_id, t.project_name, t.project_description, + t.organization_id, t.organization_name, t.organization_description + into src + from whx_credential_dimension_source as t + where t.credential_library_id = p_library_id + and t.session_id = p_session_id + and t.target_id = t_id + and t.credential_purpose = p_credential_purpose; + + if src is distinct from target then + update wh_credential_dimension + set current_row_indicator = 'Expired', + row_expiration_time = current_timestamp + where credential_library_id = p_library_id + and target_id = t_id + and credential_purpose = p_credential_purpose + and current_row_indicator = 'Current'; + + insert into wh_credential_dimension ( + credential_purpose, + credential_library_id, credential_library_type, credential_library_name, credential_library_description, credential_library_vault_path, credential_library_vault_http_method, credential_library_vault_http_request_body, + credential_store_id, credential_store_type, credential_store_name, credential_store_description, credential_store_vault_namespace, credential_store_vault_address, + target_id, target_type, target_name, target_description, target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id, project_name, project_description, + organization_id, organization_name, organization_description, + current_row_indicator, row_effective_time, row_expiration_time + ) + select credential_purpose, + credential_library_id, credential_library_type, credential_library_name, credential_library_description, credential_library_vault_path, credential_library_vault_http_method, credential_library_vault_http_request_body, + credential_store_id, credential_store_type, credential_store_name, credential_store_description, credential_store_vault_namespace, credential_store_vault_address, + target_id, target_type, target_name, target_description, target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id, project_name, project_description, + organization_id, organization_name, organization_description, + 'Current', current_timestamp, 'infinity'::timestamptz + from whx_credential_dimension_source + where credential_library_id = p_library_id + and session_id = p_session_id + and target_id = t_id + and credential_purpose = p_credential_purpose + returning * into new_row; + + return new_row.key; + end if; + + return target.key; + end + $$ language plpgsql; + + -- Run wh_upsert_credential_dimension for session_credential_dynamic row that is inserted. + create function wh_insert_session_credential_dynamic() + returns trigger + as $$ + begin + perform wh_upsert_credential_dimension(new.session_id, new.library_id, new.credential_purpose); + return null; + end; + $$ language plpgsql; + create trigger wh_insert_session_credential_dynamic + after insert on session_credential_dynamic + for each row + execute function wh_insert_session_credential_dynamic(); + + -- wh_upsert_credentail_group determines if a new wh_credential_group needs to be + -- created due to changes to the coresponding wh_credential_dimensions. It then + -- updates the wh_session_accumulating_fact to associate it with the correct wh_credential_group. + create function wh_upsert_credentail_group() + returns trigger + as $$ + declare + cg_key wh_dim_key; + t_id wt_public_id; + s_id wt_public_id; + c_key wh_dim_key; + begin + select distinct scd.session_id into strict s_id + from new_table as scd; + + select distinct s.target_id into strict t_id + from new_table as scd + left join session as s on s.public_id = scd.session_id; + + -- based on query written by Michele Gaffney + with + credential_list (key) as ( + select key + from wh_credential_dimension + where target_id = t_id + and credential_library_id in (select credential_library_id from new_table) + ) + select distinct credential_group_key into cg_key + from wh_credential_group_membership a + where a.credential_key in (select key from credential_list) + and (select count(key) from credential_list) = + ( + select count(b.credential_key) + from wh_credential_group_membership b + where a.credential_key = b.credential_key + and b.credential_key in (select key from credential_list) + ) + and not exists + ( + select 1 + from wh_credential_group_membership b + where a.credential_key = b.credential_key + and b.credential_key not in (select key from credential_list) + ) + ; + if cg_key is null then + insert into wh_credential_group default values returning key into cg_key; + for c_key in + select key + from wh_credential_dimension + where target_id = t_id + and credential_library_id in (select credential_library_id from new_table) + loop + insert into wh_credential_group_membership + (credential_group_key, credential_key) + values + (cg_key, c_key); + end loop; + end if; + + update wh_session_connection_accumulating_fact + set credential_group_key = cg_key + where session_id = s_id; + + return null; + end; + $$ language plpgsql; + + -- Run wh_upsert_credentail_group on statement. This assumes that all relevant + -- session_credential_dynamic rows are inserted as a single statement and that + -- the wh_insert_session_credential_dynamic trigger ran for each row and updated + -- the wh_credential_dimensions. Then this statement trigger can run to update the + -- bridge tables and wh_session_accumulating_fact. + create trigger wh_insert_stmt_session_credential_dynamic + after insert on session_credential_dynamic + referencing new table as new_table + for each statement + execute function wh_upsert_credentail_group(); +commit; diff --git a/internal/db/schema/migrations/postgres/16/04_wh_credential_dimension.up.sql b/internal/db/schema/migrations/postgres/16/04_wh_credential_dimension.up.sql new file mode 100644 index 0000000000..bb4aecbfef --- /dev/null +++ b/internal/db/schema/migrations/postgres/16/04_wh_credential_dimension.up.sql @@ -0,0 +1,54 @@ +begin; + alter table wh_session_accumulating_fact + add column credential_group_key wh_dim_key not null + default 'Unknown' + references wh_credential_group (key) + on delete restrict + on update cascade; + alter table wh_session_accumulating_fact + alter column credential_group_key drop default; + + -- replaces function from 15/01_wh_rename_key_columns.up.sql + drop trigger wh_insert_session on session; + drop function wh_insert_session; + create function wh_insert_session() + returns trigger + as $$ + declare + new_row wh_session_accumulating_fact%rowtype; + begin + with + pending_timestamp (date_dim_key, time_dim_key, ts) as ( + select wh_date_key(start_time), wh_time_key(start_time), start_time + from session_state + where session_id = new.public_id + and state = 'pending' + ) + insert into wh_session_accumulating_fact ( + session_id, + auth_token_id, + host_key, + user_key, + credential_group_key, + session_pending_date_key, + session_pending_time_key, + session_pending_time + ) + select new.public_id, + new.auth_token_id, + wh_upsert_host(new.host_id, new.host_set_id, new.target_id), + wh_upsert_user(new.user_id, new.auth_token_id), + 'no credentials', -- will be updated by wh_upsert_credentail_group + pending_timestamp.date_dim_key, + pending_timestamp.time_dim_key, + pending_timestamp.ts + from pending_timestamp + returning * into strict new_row; + return null; + end; + $$ language plpgsql; + create trigger wh_insert_session + after insert on session + for each row + execute function wh_insert_session(); +commit; diff --git a/internal/db/schema/migrations/postgres/16/05_wh_credential_dimension.up.sql b/internal/db/schema/migrations/postgres/16/05_wh_credential_dimension.up.sql new file mode 100644 index 0000000000..4627b57a0f --- /dev/null +++ b/internal/db/schema/migrations/postgres/16/05_wh_credential_dimension.up.sql @@ -0,0 +1,75 @@ +begin; + alter table wh_session_connection_accumulating_fact + add column credential_group_key wh_dim_key not null + default 'Unknown' + references wh_credential_group (key) + on delete restrict + on update cascade; + alter table wh_session_connection_accumulating_fact + alter column credential_group_key drop default; + + -- replaces function from 15/01_wh_rename_key_columns.up.sql + drop trigger wh_insert_session_connection on session_connection; + drop function wh_insert_session_connection; + create function wh_insert_session_connection() + returns trigger + as $$ + declare + new_row wh_session_connection_accumulating_fact%rowtype; + begin + with + authorized_timestamp (date_dim_key, time_dim_key, ts) as ( + select wh_date_key(start_time), wh_time_key(start_time), start_time + from session_connection_state + where connection_id = new.public_id + and state = 'authorized' + ), + session_dimension (host_dim_key, user_dim_key, credential_group_dim_key) as ( + select host_key, user_key, credential_group_key + from wh_session_accumulating_fact + where session_id = new.session_id + ) + insert into wh_session_connection_accumulating_fact ( + connection_id, + session_id, + host_key, + user_key, + credential_group_key, + connection_authorized_date_key, + connection_authorized_time_key, + connection_authorized_time, + client_tcp_address, + client_tcp_port_number, + endpoint_tcp_address, + endpoint_tcp_port_number, + bytes_up, + bytes_down + ) + select new.public_id, + new.session_id, + session_dimension.host_dim_key, + session_dimension.user_dim_key, + session_dimension.credential_group_dim_key, + authorized_timestamp.date_dim_key, + authorized_timestamp.time_dim_key, + authorized_timestamp.ts, + new.client_tcp_address, + new.client_tcp_port, + new.endpoint_tcp_address, + new.endpoint_tcp_port, + new.bytes_up, + new.bytes_down + from authorized_timestamp, + session_dimension + returning * into strict new_row; + perform wh_rollup_connections(new.session_id); + return null; + end; + $$ language plpgsql; + + create trigger wh_insert_session_connection + after insert on session_connection + for each row + execute function wh_insert_session_connection(); +commit; + diff --git a/internal/db/schema/migrations/postgres/16/warehouse_credential_dim_test.go b/internal/db/schema/migrations/postgres/16/warehouse_credential_dim_test.go new file mode 100644 index 0000000000..ef20f7fadc --- /dev/null +++ b/internal/db/schema/migrations/postgres/16/warehouse_credential_dim_test.go @@ -0,0 +1,189 @@ +package migration + +import ( + "context" + "database/sql" + "testing" + + "github.com/hashicorp/boundary/internal/auth/oidc" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/credential" + "github.com/hashicorp/boundary/internal/credential/vault" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/db/schema" + "github.com/hashicorp/boundary/internal/host/static" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/session" + "github.com/hashicorp/boundary/internal/target" + "github.com/hashicorp/boundary/testing/dbtest" + wrapping "github.com/hashicorp/go-kms-wrapping" + "github.com/jinzhu/gorm" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMigrations_CredentialDimension(t *testing.T) { + const ( + priorMigration = 15002 + currentMigration = 16005 + ) + + t.Parallel() + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + dialect := dbtest.Postgres + + c, u, _, err := dbtest.StartUsingTemplate(dialect, dbtest.WithTemplate(dbtest.Template1)) + require.NoError(err) + t.Cleanup(func() { + require.NoError(c()) + }) + d, err := sql.Open(dialect, u) + require.NoError(err) + + // migration to the prior migration (before the one we want to test) + oState := schema.TestCloneMigrationStates(t) + nState := schema.TestCreatePartialMigrationState(oState["postgres"], priorMigration) + oState["postgres"] = nState + + m, err := schema.NewManager(ctx, dialect, d, schema.WithMigrationStates(oState)) + require.NoError(err) + + assert.NoError(m.RollForward(ctx)) + state, err := m.CurrentState(ctx) + require.NoError(err) + assert.Equal(priorMigration, state.DatabaseSchemaVersion) + assert.False(state.Dirty) + + // okay, now we can seed the database with test data + conn, err := gorm.Open(dialect, u) + require.NoError(err) + + rw := db.New(conn) + wrapper := db.TestWrapper(t) + + org, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + require.NotNil(prj) + assert.NotEmpty(prj.GetPublicId()) + + hc := static.TestCatalogs(t, conn, prj.GetPublicId(), 1)[0] + hs := static.TestSets(t, conn, hc.GetPublicId(), 1)[0] + h := static.TestHosts(t, conn, hc.GetPublicId(), 1)[0] + static.TestSetMembers(t, conn, hs.GetPublicId(), []*static.Host{h}) + + tar := target.TestTcpTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) + var sessions []*session.Session + + kmsCache := kms.TestKms(t, conn, wrapper) + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.GetPublicId(), kms.KeyPurposeDatabase) + require.NoError(err) + + { + at := authtoken.TestAuthToken(t, conn, kmsCache, org.GetPublicId()) + uId := at.GetIamUserId() + + sess := session.TestSession(t, conn, wrapper, session.ComposedOf{ + UserId: uId, + HostId: h.GetPublicId(), + TargetId: tar.GetPublicId(), + HostSetId: hs.GetPublicId(), + AuthTokenId: at.GetPublicId(), + ScopeId: prj.GetPublicId(), + Endpoint: "tcp://127.0.0.1:22", + }) + sessions = append(sessions, sess) + } + + { + at := testOidcAuthToken(t, conn, kmsCache, databaseWrapper, org.GetPublicId()) + uId := at.GetIamUserId() + creds := testSessionCredentialParams(t, conn, kmsCache, wrapper, tar) + + sess := session.TestSession(t, conn, wrapper, session.ComposedOf{ + UserId: uId, + HostId: h.GetPublicId(), + TargetId: tar.GetPublicId(), + HostSetId: hs.GetPublicId(), + AuthTokenId: at.GetPublicId(), + ScopeId: prj.GetPublicId(), + Endpoint: "tcp://127.0.0.1:22", + DynamicCredentials: creds, + }) + sessions = append(sessions, sess) + } + + sessionRepo, err := session.NewRepository(rw, rw, kmsCache) + require.NoError(err) + + count, err := sessionRepo.TerminateCompletedSessions(ctx) + assert.NoError(err) + assert.Zero(count) + + for _, sess := range sessions { + // call TerminateSession + _, err = sessionRepo.TerminateSession(ctx, sess.GetPublicId(), 1, session.ClosedByUser) + assert.NoError(err) + } + + // now we're ready for the migration we want to test. + oState = schema.TestCloneMigrationStates(t) + nState = schema.TestCreatePartialMigrationState(oState["postgres"], currentMigration) + oState["postgres"] = nState + + m, err = schema.NewManager(ctx, dialect, d, schema.WithMigrationStates(oState)) + require.NoError(err) + + assert.NoError(m.RollForward(ctx)) + state, err = m.CurrentState(ctx) + require.NoError(err) + assert.Equal(currentMigration, state.DatabaseSchemaVersion) + assert.False(state.Dirty) +} + +func testOidcAuthToken(t *testing.T, conn *gorm.DB, kms *kms.Kms, wrapper wrapping.Wrapper, scopeId string) *authtoken.AuthToken { + t.Helper() + + authMethod := oidc.TestAuthMethod( + t, conn, wrapper, scopeId, oidc.ActivePrivateState, + "alice-rp", "fido", + oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.alice.com")[0]), + oidc.WithSigningAlgs(oidc.RS256), + oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + acct := oidc.TestAccount(t, conn, authMethod, "test-subject") + + ctx := context.Background() + rw := db.New(conn) + iamRepo, err := iam.NewRepository(rw, rw, kms) + require.NoError(t, err) + + u := iam.TestUser(t, iamRepo, scopeId, iam.WithAccountIds(acct.PublicId)) + + repo, err := authtoken.NewRepository(rw, rw, kms) + require.NoError(t, err) + + at, err := repo.CreateAuthToken(ctx, u, acct.GetPublicId()) + require.NoError(t, err) + return at +} + +func testSessionCredentialParams(t *testing.T, conn *gorm.DB, kms *kms.Kms, wrapper wrapping.Wrapper, tar *target.TcpTarget) []*session.DynamicCredential { + t.Helper() + rw := db.New(conn) + + ctx := context.Background() + stores := vault.TestCredentialStores(t, conn, wrapper, tar.ScopeId, 1) + libs := vault.TestCredentialLibraries(t, conn, wrapper, stores[0].GetPublicId(), 2) + + targetRepo, err := target.NewRepository(rw, rw, kms) + require.NoError(t, err) + _, _, _, err = targetRepo.AddTargetCredentialSources(ctx, tar.GetPublicId(), tar.GetVersion(), []string{libs[0].PublicId, libs[1].PublicId}) + require.NoError(t, err) + creds := []*session.DynamicCredential{ + session.NewDynamicCredential(libs[0].GetPublicId(), credential.ApplicationPurpose), + session.NewDynamicCredential(libs[0].GetPublicId(), credential.IngressPurpose), + session.NewDynamicCredential(libs[1].GetPublicId(), credential.EgressPurpose), + } + return creds +} diff --git a/internal/db/schema/postgres_migration.gen.go b/internal/db/schema/postgres_migration.gen.go index 852676e638..7ced41ff4b 100644 --- a/internal/db/schema/postgres_migration.gen.go +++ b/internal/db/schema/postgres_migration.gen.go @@ -4,7 +4,7 @@ package schema func init() { migrationStates["postgres"] = migrationState{ - binarySchemaVersion: 15002, + binarySchemaVersion: 16005, upMigrations: map[int][]byte{ 1: []byte(` create domain wt_public_id as text @@ -6938,6 +6938,532 @@ alter table wh_host_dimension end; $$ language plpgsql; +`), + 16001: []byte(` +-- replaces check from internal/db/schema/migrations/postgres/0/60_wh_domain_types.up.sql + alter domain wh_public_id drop constraint wh_public_id_check; + alter domain wh_public_id add constraint wh_public_id_check + check( + value = 'None' + or + value = 'Unknown' + or + length(trim(value)) > 10 + ); + + create table wh_credential_dimension ( + -- random id generated using encode(digest(gen_random_bytes(16), 'sha256'), 'base64') + -- this is done to prevent conflicts with rows in other clusters + -- which enables warehouse data from multiple clusters to be loaded into a + -- single database instance + key wh_dim_key primary key default wh_dim_key(), + + credential_purpose wh_dim_text, + credential_library_id wh_public_id not null, + credential_library_type wh_dim_text, + credential_library_name wh_dim_text, + credential_library_description wh_dim_text, + credential_library_vault_path wh_dim_text, + credential_library_vault_http_method wh_dim_text, + credential_library_vault_http_request_body wh_dim_text, + + credential_store_id wh_public_id not null, + credential_store_type wh_dim_text, + credential_store_name wh_dim_text, + credential_store_description wh_dim_text, + credential_store_vault_namespace wh_dim_text, + credential_store_vault_address wh_dim_text, + + target_id wh_public_id not null, + target_type wh_dim_text, + target_name wh_dim_text, + target_description wh_dim_text, + target_default_port_number integer not null, + target_session_max_seconds integer not null, + target_session_connection_limit integer not null, + + project_id wt_scope_id not null, + project_name wh_dim_text, + project_description wh_dim_text, + + organization_id wt_scope_id not null, + organization_name wh_dim_text, + organization_description wh_dim_text, + + current_row_indicator wh_dim_text, + row_effective_time wh_timestamp, + row_expiration_time wh_timestamp + ); + + -- https://www.postgresql.org/docs/current/indexes-partial.html + create unique index wh_credential_dim_current_constraint + on wh_credential_dimension (credential_library_id, credential_store_id, target_id, credential_purpose) + where current_row_indicator = 'Current'; + + -- One part of a bridge table to associated the set of wh_credential_dimension with a fact table. + -- The other part of the bridge is wh_credential_group_membership. + create table wh_credential_group ( + -- random id generated using encode(digest(gen_random_bytes(16), 'sha256'), 'base64') + -- this is done to prevent conflicts with rows in other clusters + -- which enables warehouse data from multiple clusters to be loaded into a + -- single database instance + key wh_dim_key primary key default wh_dim_key() + ); + + -- The second part of the bridge table. The other part is wh_credential_group. + create table wh_credential_group_membership ( + credential_group_key wh_dim_key not null + references wh_credential_group (key) + on delete restrict + on update cascade, + credential_key wh_dim_key not null + references wh_credential_dimension (key) + on delete restrict + on update cascade + ); + + -- Add "no credentials" and "Unknown" group an dimension. + -- When a session has no credentials "no credentials" is used as the "None" value. + -- "Unknown" is used for existing data prior to the credential_dimension existing. + insert into wh_credential_group + (key) + values + ('no credentials'), + ('Unknown'); + insert into wh_credential_dimension ( + key, + credential_purpose, + credential_library_id, credential_library_type, credential_library_name, credential_library_description, credential_library_vault_path, credential_library_vault_http_method, credential_library_vault_http_request_body, + credential_store_id, credential_store_type, credential_store_name, credential_store_description, credential_store_vault_namespace, credential_store_vault_address, + target_id, target_type, target_name, target_description, target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id, project_name, project_description, + organization_id, organization_name, organization_description, + current_row_indicator, row_effective_time, row_expiration_time + ) + values + ( + 'no credential', + 'None', + 'None', 'None', 'None', 'None', 'None', 'None', 'None', + 'None', 'None', 'None', 'None', 'None', 'None', + 'None', 'None', 'None', 'None', -1, -1, -1, + '00000000000', 'None', 'None', + '00000000000', 'None', 'None', + 'Current', now(), 'infinity'::timestamptz + ), + ( + 'Unknown', + 'Unknown', + 'Unknown', 'Unknown', 'Unknown', 'Unknown', 'Unknown', 'Unknown', 'Unknown', + 'Unknown', 'Unknown', 'Unknown', 'Unknown', 'Unknown', 'Unknown', + 'Unknown', 'Unknown', 'Unknown', 'Unknown', -1, -1, -1, + '00000000000', 'Unknown', 'Unknown', + '00000000000', 'Unknown', 'Unknown', + 'Current', now(), 'infinity'::timestamptz + ); + insert into wh_credential_group_membership + (credential_group_key, credential_key) + values + ('no credentials', 'no credential'), + ('Unknown', 'Unknown'); +`), + 16002: []byte(` +-- The whx_credential_dimension_source and whx_credential_dimension_target views are used + -- by an insert trigger to determine if the current row for the dimension has + -- changed and a new one needs to be inserted. The first column in the target view + -- must be the current warehouse id and all remaining columns must match the columns + -- in the source view. + + -- The whx_credential_dimension_source view shows the current values in the + -- operational tables of the credential dimension. + create view whx_credential_dimension_source as + select -- id is the first column in the target view + s.public_id as session_id, + coalesce(scd.credential_purpose, 'None') as credential_purpose, + cl.public_id as credential_library_id, + case + when vcl is null then 'None' + else 'vault credential library' + end as credential_library_type, + coalesce(vcl.name, 'None') as credential_library_name, + coalesce(vcl.description, 'None') as credential_library_description, + coalesce(vcl.vault_path, 'None') as credential_library_vault_path, + coalesce(vcl.http_method, 'None') as credential_library_vault_http_method, + coalesce(vcl.http_request_body, 'None') as credential_library_vault_http_request_body, + cs.public_id as credential_store_id, + case + when vcs is null then 'None' + else 'vault credential store' + end as credential_store_type, + coalesce(vcs.name, 'None') as credential_store_name, + coalesce(vcs.description, 'None') as credential_store_description, + coalesce(vcs.namespace, 'None') as credential_store_vault_namespace, + coalesce(vcs.vault_address, 'None') as credential_store_vault_address, + t.public_id as target_id, + 'tcp target' as target_type, + coalesce(tt.name, 'None') as target_name, + coalesce(tt.description, 'None') as target_description, + coalesce(tt.default_port, 0) as target_default_port_number, + tt.session_max_seconds as target_session_max_seconds, + tt.session_connection_limit as target_session_connection_limit, + p.public_id as project_id, + coalesce(p.name, 'None') as project_name, + coalesce(p.description, 'None') as project_description, + o.public_id as organization_id, + coalesce(o.name, 'None') as organization_name, + coalesce(o.description, 'None') as organization_description + from session_credential_dynamic as scd, + session as s, + credential_library as cl, + credential_store as cs, + credential_vault_library as vcl, + credential_vault_store as vcs, + target as t, + target_tcp as tt, + iam_scope as p, + iam_scope as o + where scd.library_id = cl.public_id + and cl.store_id = cs.public_id + and vcl.public_id = cl.public_id + and vcs.public_id = cs.public_id + and s.public_id = scd.session_id + and s.target_id = t.public_id + and t.public_id = tt.public_id + and p.public_id = t.scope_id + and p.type = 'project' + and o.public_id = p.parent_id + and o.type = 'org'; + + create view whx_credential_dimension_target as + select key, + credential_purpose, + credential_library_id, + credential_library_type, + credential_library_name, + credential_library_description, + credential_library_vault_path, + credential_library_vault_http_method, + credential_library_vault_http_request_body, + credential_store_id, + credential_store_type, + credential_store_name, + credential_store_description, + credential_store_vault_namespace, + credential_store_vault_address, + target_id, + target_type, + target_name, + target_description, + target_default_port_number, + target_session_max_seconds, + target_session_connection_limit, + project_id, + project_name, + project_description, + organization_id, + organization_name, + organization_description + from wh_credential_dimension + where current_row_indicator = 'Current' + ; +`), + 16003: []byte(` +-- wh_upsert_credential_dimension compares the current vaules in the wh_credential_dimension + -- with the current values in the operational tables for the given parameters. IF the values + -- between operational tables and the wh_credential_dimension differ, a new row is inserted in + -- the wh_credential_dimension to match the current values in the operational tables. + create function wh_upsert_credential_dimension(p_session_id wt_public_id, p_library_id wt_public_id, p_credential_purpose wh_dim_text) + returns wh_dim_key + as $$ + declare + src whx_credential_dimension_target%rowtype; + target whx_credential_dimension_target%rowtype; + new_row wh_credential_dimension%rowtype; + t_id wt_public_id; + begin + select s.target_id into strict t_id + from session as s + where s.public_id = p_session_id; + + select * into target + from whx_credential_dimension_target as t + where t.credential_library_id = p_library_id + and t.target_id = t_id + and t.credential_purpose = p_credential_purpose; + + select + target.key, t.credential_purpose, + t.credential_library_id, t.credential_library_type, t.credential_library_name, t.credential_library_description, t.credential_library_vault_path, t.credential_library_vault_http_method, t.credential_library_vault_http_request_body, + t.credential_store_id, t.credential_store_type, t.credential_store_name, t.credential_store_description, t.credential_store_vault_namespace, t.credential_store_vault_address, + t.target_id, t.target_type, t.target_name, t.target_description, t.target_default_port_number, t.target_session_max_seconds, t.target_session_connection_limit, + t.project_id, t.project_name, t.project_description, + t.organization_id, t.organization_name, t.organization_description + into src + from whx_credential_dimension_source as t + where t.credential_library_id = p_library_id + and t.session_id = p_session_id + and t.target_id = t_id + and t.credential_purpose = p_credential_purpose; + + if src is distinct from target then + update wh_credential_dimension + set current_row_indicator = 'Expired', + row_expiration_time = current_timestamp + where credential_library_id = p_library_id + and target_id = t_id + and credential_purpose = p_credential_purpose + and current_row_indicator = 'Current'; + + insert into wh_credential_dimension ( + credential_purpose, + credential_library_id, credential_library_type, credential_library_name, credential_library_description, credential_library_vault_path, credential_library_vault_http_method, credential_library_vault_http_request_body, + credential_store_id, credential_store_type, credential_store_name, credential_store_description, credential_store_vault_namespace, credential_store_vault_address, + target_id, target_type, target_name, target_description, target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id, project_name, project_description, + organization_id, organization_name, organization_description, + current_row_indicator, row_effective_time, row_expiration_time + ) + select credential_purpose, + credential_library_id, credential_library_type, credential_library_name, credential_library_description, credential_library_vault_path, credential_library_vault_http_method, credential_library_vault_http_request_body, + credential_store_id, credential_store_type, credential_store_name, credential_store_description, credential_store_vault_namespace, credential_store_vault_address, + target_id, target_type, target_name, target_description, target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id, project_name, project_description, + organization_id, organization_name, organization_description, + 'Current', current_timestamp, 'infinity'::timestamptz + from whx_credential_dimension_source + where credential_library_id = p_library_id + and session_id = p_session_id + and target_id = t_id + and credential_purpose = p_credential_purpose + returning * into new_row; + + return new_row.key; + end if; + + return target.key; + end + $$ language plpgsql; + + -- Run wh_upsert_credential_dimension for session_credential_dynamic row that is inserted. + create function wh_insert_session_credential_dynamic() + returns trigger + as $$ + begin + perform wh_upsert_credential_dimension(new.session_id, new.library_id, new.credential_purpose); + return null; + end; + $$ language plpgsql; + create trigger wh_insert_session_credential_dynamic + after insert on session_credential_dynamic + for each row + execute function wh_insert_session_credential_dynamic(); + + -- wh_upsert_credentail_group determines if a new wh_credential_group needs to be + -- created due to changes to the coresponding wh_credential_dimensions. It then + -- updates the wh_session_accumulating_fact to associate it with the correct wh_credential_group. + create function wh_upsert_credentail_group() + returns trigger + as $$ + declare + cg_key wh_dim_key; + t_id wt_public_id; + s_id wt_public_id; + c_key wh_dim_key; + begin + select distinct scd.session_id into strict s_id + from new_table as scd; + + select distinct s.target_id into strict t_id + from new_table as scd + left join session as s on s.public_id = scd.session_id; + + -- based on query written by Michele Gaffney + with + credential_list (key) as ( + select key + from wh_credential_dimension + where target_id = t_id + and credential_library_id in (select credential_library_id from new_table) + ) + select distinct credential_group_key into cg_key + from wh_credential_group_membership a + where a.credential_key in (select key from credential_list) + and (select count(key) from credential_list) = + ( + select count(b.credential_key) + from wh_credential_group_membership b + where a.credential_key = b.credential_key + and b.credential_key in (select key from credential_list) + ) + and not exists + ( + select 1 + from wh_credential_group_membership b + where a.credential_key = b.credential_key + and b.credential_key not in (select key from credential_list) + ) + ; + if cg_key is null then + insert into wh_credential_group default values returning key into cg_key; + for c_key in + select key + from wh_credential_dimension + where target_id = t_id + and credential_library_id in (select credential_library_id from new_table) + loop + insert into wh_credential_group_membership + (credential_group_key, credential_key) + values + (cg_key, c_key); + end loop; + end if; + + update wh_session_connection_accumulating_fact + set credential_group_key = cg_key + where session_id = s_id; + + return null; + end; + $$ language plpgsql; + + -- Run wh_upsert_credentail_group on statement. This assumes that all relevant + -- session_credential_dynamic rows are inserted as a single statement and that + -- the wh_insert_session_credential_dynamic trigger ran for each row and updated + -- the wh_credential_dimensions. Then this statement trigger can run to update the + -- bridge tables and wh_session_accumulating_fact. + create trigger wh_insert_stmt_session_credential_dynamic + after insert on session_credential_dynamic + referencing new table as new_table + for each statement + execute function wh_upsert_credentail_group(); +`), + 16004: []byte(` +alter table wh_session_accumulating_fact + add column credential_group_key wh_dim_key not null + default 'Unknown' + references wh_credential_group (key) + on delete restrict + on update cascade; + alter table wh_session_accumulating_fact + alter column credential_group_key drop default; + + -- replaces function from 15/01_wh_rename_key_columns.up.sql + drop trigger wh_insert_session on session; + drop function wh_insert_session; + create function wh_insert_session() + returns trigger + as $$ + declare + new_row wh_session_accumulating_fact%rowtype; + begin + with + pending_timestamp (date_dim_key, time_dim_key, ts) as ( + select wh_date_key(start_time), wh_time_key(start_time), start_time + from session_state + where session_id = new.public_id + and state = 'pending' + ) + insert into wh_session_accumulating_fact ( + session_id, + auth_token_id, + host_key, + user_key, + credential_group_key, + session_pending_date_key, + session_pending_time_key, + session_pending_time + ) + select new.public_id, + new.auth_token_id, + wh_upsert_host(new.host_id, new.host_set_id, new.target_id), + wh_upsert_user(new.user_id, new.auth_token_id), + 'no credentials', -- will be updated by wh_upsert_credentail_group + pending_timestamp.date_dim_key, + pending_timestamp.time_dim_key, + pending_timestamp.ts + from pending_timestamp + returning * into strict new_row; + return null; + end; + $$ language plpgsql; + create trigger wh_insert_session + after insert on session + for each row + execute function wh_insert_session(); +`), + 16005: []byte(` +alter table wh_session_connection_accumulating_fact + add column credential_group_key wh_dim_key not null + default 'Unknown' + references wh_credential_group (key) + on delete restrict + on update cascade; + alter table wh_session_connection_accumulating_fact + alter column credential_group_key drop default; + + -- replaces function from 15/01_wh_rename_key_columns.up.sql + drop trigger wh_insert_session_connection on session_connection; + drop function wh_insert_session_connection; + create function wh_insert_session_connection() + returns trigger + as $$ + declare + new_row wh_session_connection_accumulating_fact%rowtype; + begin + with + authorized_timestamp (date_dim_key, time_dim_key, ts) as ( + select wh_date_key(start_time), wh_time_key(start_time), start_time + from session_connection_state + where connection_id = new.public_id + and state = 'authorized' + ), + session_dimension (host_dim_key, user_dim_key, credential_group_dim_key) as ( + select host_key, user_key, credential_group_key + from wh_session_accumulating_fact + where session_id = new.session_id + ) + insert into wh_session_connection_accumulating_fact ( + connection_id, + session_id, + host_key, + user_key, + credential_group_key, + connection_authorized_date_key, + connection_authorized_time_key, + connection_authorized_time, + client_tcp_address, + client_tcp_port_number, + endpoint_tcp_address, + endpoint_tcp_port_number, + bytes_up, + bytes_down + ) + select new.public_id, + new.session_id, + session_dimension.host_dim_key, + session_dimension.user_dim_key, + session_dimension.credential_group_dim_key, + authorized_timestamp.date_dim_key, + authorized_timestamp.time_dim_key, + authorized_timestamp.ts, + new.client_tcp_address, + new.client_tcp_port, + new.endpoint_tcp_address, + new.endpoint_tcp_port, + new.bytes_up, + new.bytes_down + from authorized_timestamp, + session_dimension + returning * into strict new_row; + perform wh_rollup_connections(new.session_id); + return null; + end; + $$ language plpgsql; + + create trigger wh_insert_session_connection + after insert on session_connection + for each row + execute function wh_insert_session_connection(); `), 2001: []byte(` -- log_migration entries represent logs generated during migrations diff --git a/internal/db/sqltest/initdb.d/01_colors_persona.sql b/internal/db/sqltest/initdb.d/01_colors_persona.sql index 54c4121f63..edad940ce3 100644 --- a/internal/db/sqltest/initdb.d/01_colors_persona.sql +++ b/internal/db/sqltest/initdb.d/01_colors_persona.sql @@ -175,10 +175,26 @@ begin; ('t_________cr', 's___1cr-sths'), ('t_________cr', 's___2cr-sths'); + + insert into credential_vault_store + (scope_id, public_id, name, description, vault_address, namespace) + values + ('p____bcolors', 'vs_______cvs', 'color vault store', 'None', 'https://vault.color', 'blue'); + + insert into credential_vault_library + (store_id, public_id, name, description, vault_path, http_method) + values + ('vs_______cvs', 'vl______cvl', 'color vault library', 'None', '/secrets', 'GET'); + + insert into target_credential_library + (target_id, credential_library_id, credential_purpose) + values + ('t_________cb', 'vl______cvl', 'application'); + insert into session - ( scope_id , target_id , host_set_id , host_id , user_id , auth_token_id , certificate , endpoint , public_id) + ( scope_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) values - ('p____bcolors' , 't_________cb' , 's___1cb-sths' , 'h_____cb__01' , 'u______clare' , 'tok____clare' , 'abc'::bytea , 'ep1' , 's1_____clare'); + ('p____bcolors', 't_________cb', 's___1cb-sths', 'h_____cb__01', 'u______clare', 'tok____clare', 'abc'::bytea, 'ep1', 's1_____clare'); insert into session_connection (session_id, public_id) diff --git a/internal/db/sqltest/initdb.d/03_widgets_persona.sql b/internal/db/sqltest/initdb.d/03_widgets_persona.sql index 9630ee94e4..7ddf2ee738 100644 --- a/internal/db/sqltest/initdb.d/03_widgets_persona.sql +++ b/internal/db/sqltest/initdb.d/03_widgets_persona.sql @@ -261,5 +261,31 @@ begin; end; $$ language plpgsql; + + create function _wtt_load_widgets_credentials() + returns void + as $$ + begin + insert into credential_vault_store + (scope_id, public_id, name, description, vault_address, namespace) + values + ('p____bwidget', 'vs_______wvs', 'widget vault store', 'None', 'https://vault.widget', 'default'); + + insert into credential_vault_library + (store_id, public_id, name, description, vault_path, http_method) + values + ('vs_______wvs', 'vl______wvl1', 'widget vault library', 'None', '/secrets', 'GET'), + ('vs_______wvs', 'vl______wvl2', 'widget vault ssh', 'None', '/secrets/ssh/admin', 'GET'), + ('vs_______wvs', 'vl______wvl3', 'widget vault kv', 'None', '/secrets/kv', 'GET'); + + insert into target_credential_library + (target_id, credential_library_id, credential_purpose) + values + ('t_________wb', 'vl______wvl1', 'application'), + ('t_________wb', 'vl______wvl2', 'application'), + ('t_________wb', 'vl______wvl3', 'application'), + ('t_________wb', 'vl______wvl3', 'egress'); + end; + $$ language plpgsql; commit; diff --git a/internal/db/sqltest/tests/wh/credential_dimension/dimension.sql b/internal/db/sqltest/tests/wh/credential_dimension/dimension.sql new file mode 100644 index 0000000000..d0d9f244de --- /dev/null +++ b/internal/db/sqltest/tests/wh/credential_dimension/dimension.sql @@ -0,0 +1,53 @@ +begin; + select plan(2); + + insert into wh_credential_dimension ( + credential_purpose, + credential_library_id, credential_library_type, credential_library_name, credential_library_description, credential_library_vault_path, credential_library_vault_http_method, credential_library_vault_http_request_body, + credential_store_id, credential_store_type, credential_store_name, credential_store_description, credential_store_vault_namespace, credential_store_vault_address, + target_id, target_type, target_name, target_description, target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id, project_name, project_description, + organization_id, organization_name, organization_description, + current_row_indicator, row_effective_time, row_expiration_time + ) values ( + 'application', + 'vl______wvl1', 'vault credential library', 'gidget vault library', 'None', '/secrets', 'GET', '\x4e6f6e65', + 'vs_______wvs', 'vault credential store', 'widget vault store', 'None', 'default', 'https://vault.widget', + 't_________wb', 'tcp target', 'Big Widget Target', 'None', 0, 28800, 1, + 'p____bwidget', 'Big Widget Factory', 'None', + 'o_____widget', 'Widget Inc', 'None', + 'Current', current_timestamp, 'infinity'::timestamptz + ); + + update wh_credential_dimension + set current_row_indicator = 'Expired', + row_expiration_time = current_timestamp + where credential_library_id = 'vl______wvl1' + and credential_store_id = 'vs_______wvs' + and target_id = 't_________wb' + and credential_purpose = 'application' + and current_row_indicator = 'Current'; + + insert into wh_credential_dimension ( + credential_purpose, + credential_library_id, credential_library_type, credential_library_name, credential_library_description, credential_library_vault_path, credential_library_vault_http_method, credential_library_vault_http_request_body, + credential_store_id, credential_store_type, credential_store_name, credential_store_description, credential_store_vault_namespace, credential_store_vault_address, + target_id, target_type, target_name, target_description, target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id, project_name, project_description, + organization_id, organization_name, organization_description, + current_row_indicator, row_effective_time, row_expiration_time + ) values ( + 'application', + 'vl______wvl1', 'vault credential library', 'gidget vault library', 'None', '/secrets', 'GET', '\x4e6f6e65', + 'vs_______wvs', 'vault credential store', 'widget vault store', 'None', 'default', 'https://vault.widget', + 't_________wb', 'tcp target', 'Big Widget Target', 'None', 0, 28800, 1, + 'p____bwidget', 'Big Widget Factory', 'None', + 'o_____widget', 'Widget Inc', 'None', + 'Current', current_timestamp, 'infinity'::timestamptz + ); + + select is(count(*), 2::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + select is(count(*), 1::bigint) from wh_credential_dimension where organization_id = 'o_____widget' and current_row_indicator = 'Current'; + + select * from finish(); +rollback; diff --git a/internal/db/sqltest/tests/wh/credential_dimension/session_multiple_sessions.sql b/internal/db/sqltest/tests/wh/credential_dimension/session_multiple_sessions.sql new file mode 100644 index 0000000000..076652e684 --- /dev/null +++ b/internal/db/sqltest/tests/wh/credential_dimension/session_multiple_sessions.sql @@ -0,0 +1,108 @@ +-- session_multiple_sessions tests the wh_credential_dimension when +-- multiple sessions are created using. +begin; + select plan(13); + + select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets', 'credentials'); + + -- ensure no existing dimensions + select is(count(*), 0::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + + -- insert first session, should result in a new credentials dimension + insert into session + ( scope_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) + values + ('p____bwidget', 't_________wb', 's___1wb-sths', 'h_____wb__01', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's1____walter'); + insert into session_credential_dynamic + ( session_id, library_id, credential_id, credential_purpose) + values + ('s1____walter', 'vl______wvl1', null, 'application'); + select is(count(*), 1::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + + -- another session with: + -- * same user + -- * same auth + -- * same host + -- should not result in a new credential dimension + insert into session + ( scope_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) + values + ('p____bwidget', 't_________wb', 's___1wb-sths', 'h_____wb__01', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's2____walter'); + insert into session_credential_dynamic + ( session_id, library_id, credential_id, credential_purpose) + values + ('s2____walter', 'vl______wvl1', null, 'application'); + select is(count(*), 1::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + + -- change the crediential for the target + update credential_vault_library set vault_path = '/secrets/tcp/admin' where public_id = 'vl______wvl1'; + + -- start another session, should result in a new credential dimension + insert into session + ( scope_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) + values + ('p____bwidget', 't_________wb', 's___1wb-sths', 'h_____wb__01', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's3____walter'); + insert into session_credential_dynamic + ( session_id, library_id, credential_id, credential_purpose) + values + ('s3____walter', 'vl______wvl1', null, 'application'); + select is(count(*), 2::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + + -- start another session, should result in a one new credential dimensions + insert into session + ( scope_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) + values + ('p____bwidget', 't_________wb', 's___1wb-sths', 'h_____wb__01', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's4____walter'); + insert into session_credential_dynamic + ( session_id, library_id, credential_id, credential_purpose) + values + ('s4____walter', 'vl______wvl1', null, 'application'), + ('s4____walter', 'vl______wvl2', null, 'application'); + select is(count(*), 3::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + + -- change the crediential again for the target + update credential_vault_library set vault_path = '/secrets/tcp/user' where vault_path = '/secrets/tcp/admin'; + + -- start another session, should result in a one new credential dimensions since one changed + insert into session + ( scope_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) + values + ('p____bwidget', 't_________wb', 's___1wb-sths', 'h_____wb__01', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's5____walter'); + insert into session_credential_dynamic + ( session_id, library_id, credential_id, credential_purpose) + values + ('s5____walter', 'vl______wvl1', null, 'application'), + ('s5____walter', 'vl______wvl2', null, 'application'); + select is(count(*), 4::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + select is(count(*), 2::bigint) from wh_credential_dimension where organization_id = 'o_____widget' and current_row_indicator = 'Current'; + + -- remove all credentials from the target + -- then test creating a session + delete from credential_vault_library; + insert into session + ( scope_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) + values + ('p____bwidget', 't_________wb', 's___1wb-sths', 'h_____wb__01', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's6____walter'); + select is(count(*), 4::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + select is(credential_group_key, 'no credentials') from wh_session_accumulating_fact where session_id = 's6____walter'; + insert into session_connection + (session_id, public_id) + values + ('s6____walter', 'sc6____walter'); + select is(credential_group_key, 'no credentials') from wh_session_connection_accumulating_fact where session_id = 's6____walter'; + + -- insert into a session for a target that never had any credentials associated with it. + insert into session + ( scope_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) + values + ('p____bwidget', 't_________ws', 's___1ws-sths', 'h_____ws__01', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's7____walter'); + select is(count(*), 4::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + select is(credential_group_key, 'no credentials') from wh_session_accumulating_fact where session_id = 's7____walter'; + insert into session_connection + (session_id, public_id) + values + ('s7____walter', 'sc7____walter'); + select is(credential_group_key, 'no credentials') from wh_session_connection_accumulating_fact where session_id = 's7____walter'; + + select * from finish(); +rollback; diff --git a/internal/db/sqltest/tests/wh/credential_dimension/session_update.sql b/internal/db/sqltest/tests/wh/credential_dimension/session_update.sql new file mode 100644 index 0000000000..452c0c3d23 --- /dev/null +++ b/internal/db/sqltest/tests/wh/credential_dimension/session_update.sql @@ -0,0 +1,33 @@ +-- session_update tests the wh_credential_dimension when +-- a session is inserted and then updated. +begin; + select plan(3); + + select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets', 'credentials'); + + -- ensure no existing dimensions + select is(count(*), 0::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + + -- insert first session, should result in a new user dimension + insert into session + ( scope_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) + values + ('p____bwidget', 't_________wb', 's___1wb-sths', 'h_____wb__01', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's1____walter'); + insert into session_credential_dynamic + ( session_id, library_id, credential_id, credential_purpose) + values + ('s1____walter', 'vl______wvl1', null, 'application'); + + select is(count(*), 1::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + + -- update session, should not impact wh_credential_dimension + update session set + version = 2 + where + public_id = 's1____walter'; + + select is(count(*), 1::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + + select * from finish(); +rollback; + diff --git a/internal/db/sqltest/tests/wh/credential_dimension/three_credentials_one_change.sql b/internal/db/sqltest/tests/wh/credential_dimension/three_credentials_one_change.sql new file mode 100644 index 0000000000..f541335e29 --- /dev/null +++ b/internal/db/sqltest/tests/wh/credential_dimension/three_credentials_one_change.sql @@ -0,0 +1,47 @@ +-- three_credentials_one_change tests that: +-- when a session with three credentials is created +-- three wh_credential_dimensions are created +-- then when one of the credential libraries is updated +-- and a new session is created +-- only one of the wh_credential_dimensions is updated +begin; + select plan(4); + + select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets', 'credentials'); + + -- ensure no existing dimensions + select is(count(*), 0::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + + -- insert session and session_credential_dynamic, should result in a three new credential dimensions + insert into session + ( scope_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) + values + ('p____bwidget', 't_________wb', 's___1wb-sths', 'h_____wb__01', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's1____walter'); + insert into session_credential_dynamic + ( session_id, library_id, credential_id, credential_purpose) + values + ('s1____walter', 'vl______wvl1', null, 'application'), + ('s1____walter', 'vl______wvl2', null, 'application'), + ('s1____walter', 'vl______wvl3', null, 'application'); + + select is(count(*), 3::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + + update credential_vault_library set vault_path = '/secrets/tcp/user' where public_id = 'vl______wvl2'; + + insert into session + ( scope_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) + values + ('p____bwidget', 't_________wb', 's___1wb-sths', 'h_____wb__01', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's2____walter'); + insert into session_credential_dynamic + ( session_id, library_id, credential_id, credential_purpose) + values + ('s2____walter', 'vl______wvl1', null, 'application'), + ('s2____walter', 'vl______wvl2', null, 'application'), + ('s2____walter', 'vl______wvl3', null, 'application'); + + select is(count(*), 4::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + select is(count(*), 3::bigint) from wh_credential_dimension where organization_id = 'o_____widget' and current_row_indicator = 'Current'; + + select * from finish(); +rollback; + diff --git a/internal/db/sqltest/tests/wh/credential_dimension_views/source.sql b/internal/db/sqltest/tests/wh/credential_dimension_views/source.sql new file mode 100644 index 0000000000..1e263aa96e --- /dev/null +++ b/internal/db/sqltest/tests/wh/credential_dimension_views/source.sql @@ -0,0 +1,51 @@ +-- source tests the whx_credential_dimension_source view. +begin; + select plan(1); + + select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets', 'credentials'); + insert into session + ( scope_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) + values + ('p____bwidget', 't_________wb', 's___1wb-sths', 'h_____wb__01', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's1____walter'); + insert into session_credential_dynamic + ( session_id, library_id, credential_id, credential_purpose) + values + ('s1____walter', 'vl______wvl1', null, 'application'); + + select is(s.*, row( + 's1____walter', + 'application', -- credential_purpose, + 'vl______wvl1', -- credential_library_id, + 'vault credential library', -- credential_library_type, + 'widget vault library', -- credential_library_name, + 'None', -- credential_library_description, + '/secrets', -- credential_library_vault_path, + 'GET', -- credential_library_vault_http_method, + 'None', -- credential_library_vault_http_request_body, + + 'vs_______wvs', -- credential_store_id, + 'vault credential store', -- credential_store_type, + 'widget vault store', -- credential_store_name, + 'None', -- credential_store_description, + 'default', -- credential_store_vault_namespace, + 'https://vault.widget', -- credential_store_vault_address, + + 't_________wb', -- target_id, + 'tcp target', -- target_type, + 'Big Widget Target', -- target_name, + 'None', -- target_description, + 0, -- target_default_port_number, + 28800, -- target_session_max_seconds, + 1, -- target_session_connection_limit, + + 'p____bwidget', -- project_id, + 'Big Widget Factory', -- project_name, + 'None', -- project_description, + 'o_____widget', -- organization_id, + 'Widget Inc', -- organization_name, + 'None' -- organization_description + )::whx_credential_dimension_source) + from whx_credential_dimension_source as s + where s.target_id = 't_________wb'; + +rollback; diff --git a/internal/db/sqltest/tests/wh/credential_dimension_views/target.sql b/internal/db/sqltest/tests/wh/credential_dimension_views/target.sql new file mode 100644 index 0000000000..831c665c66 --- /dev/null +++ b/internal/db/sqltest/tests/wh/credential_dimension_views/target.sql @@ -0,0 +1,44 @@ +-- target tests teh whx_credential_dimension_target view. +begin; + select plan(2); + + select is_empty($$select * from whx_credential_dimension_target where target_id = 't_________wb'$$); + + insert into wh_credential_dimension + ( + key, + credential_purpose, + credential_library_id, credential_library_type, credential_library_name, credential_library_description, credential_library_vault_path, credential_library_vault_http_method, credential_library_vault_http_request_body, + credential_store_id, credential_store_type, credential_store_name, credential_store_description, credential_store_vault_namespace, credential_store_vault_address, + target_id, target_type, target_name, target_description, target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id, project_name, project_description, + organization_id, organization_name, organization_description, + current_row_indicator, row_effective_time, row_expiration_time + ) + values + ( + 'wcd________1', + 'application', + 'vl_______wvl', 'vault credential library', 'widget vault library', 'None', '/secrets', 'GET', 'None', + 'vs_______wvs', 'vault credential store', 'widget vault store', 'None', 'blue', 'https://vault.widget', + 't_________wb', 'tcp target', 'Big Widget Target', 'None', 0, 28800, 1, + 'p____bwidget', 'Big Widget Factory', 'None', + 'o_____widget', 'Widget Inc', 'None', + 'Current', '2021-07-21T12:01'::timestamptz, 'infinity'::timestamptz + ); + + select is(t.*, row( + 'wcd________1', + 'application', + 'vl_______wvl', 'vault credential library', 'widget vault library', 'None', '/secrets', 'GET', 'None', + 'vs_______wvs', 'vault credential store', 'widget vault store', 'None', 'blue', 'https://vault.widget', + 't_________wb', 'tcp target', 'Big Widget Target', 'None', 0, 28800, 1, + 'p____bwidget', 'Big Widget Factory', 'None', + 'o_____widget', 'Widget Inc', 'None' + )::whx_credential_dimension_target) + from whx_credential_dimension_target as t + where t.target_id = 't_________wb'; + + select * from finish(); +rollback; + diff --git a/internal/db/sqltest/tests/wh/session_credential_dynamic/insert.sql b/internal/db/sqltest/tests/wh/session_credential_dynamic/insert.sql new file mode 100644 index 0000000000..b499affc41 --- /dev/null +++ b/internal/db/sqltest/tests/wh/session_credential_dynamic/insert.sql @@ -0,0 +1,27 @@ +--- insert tests a simple insert of session_credential_dynamic +begin; + select plan(6); + + select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets', 'credentials'); + -- ensure no existing dimensions or bridge table rows + select is(count(*), 0::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + select is(count(*), 0::bigint) from wh_credential_group_membership where credential_group_key != 'no credentials' and credential_group_key != 'Unknown'; + select is(count(*), 0::bigint) from wh_credential_group where key != 'no credentials' and key != 'Unknown'; + + --- insert single credential + insert into session + ( scope_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) + values + ('p____bwidget', 't_________wb', 's___1wb-sths', 'h_____wb__01', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's1____walter'); + + insert into session_credential_dynamic + ( session_id, library_id, credential_id, credential_purpose) + values + ('s1____walter', 'vl______wvl1', null, 'application'); + + select is(count(*), 1::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + select is(count(*), 1::bigint) from wh_credential_group_membership where credential_group_key != 'no credentials' and credential_group_key != 'Unknown'; + select is(count(*), 1::bigint) from wh_credential_group where key != 'no credentials' and key != 'Unknown'; + + select * from finish(); +rollback; diff --git a/internal/db/sqltest/tests/wh/session_credential_dynamic/insert_multiple.sql b/internal/db/sqltest/tests/wh/session_credential_dynamic/insert_multiple.sql new file mode 100644 index 0000000000..aaa5c269a7 --- /dev/null +++ b/internal/db/sqltest/tests/wh/session_credential_dynamic/insert_multiple.sql @@ -0,0 +1,29 @@ +--- insert_multiple tests inserting multiple session_credential_dynamic as a single statement +begin; + select plan(6); + + select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets', 'credentials'); + -- ensure no existing dimensions + select is(count(*), 0::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + select is(count(*), 0::bigint) from wh_credential_group_membership where credential_group_key != 'no credentials' and credential_group_key != 'Unknown'; + select is(count(*), 0::bigint) from wh_credential_group where key != 'no credentials' and key != 'Unknown'; + + --- multiple single credentials + insert into session + ( scope_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) + values + ('p____bwidget', 't_________wb', 's___1wb-sths', 'h_____wb__01', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's1____walter'); + insert into session_credential_dynamic + ( session_id, library_id, credential_id, credential_purpose) + values + ('s1____walter', 'vl______wvl1', null, 'application'), + ('s1____walter', 'vl______wvl2', null, 'application'), + ('s1____walter', 'vl______wvl3', null, 'application'), + ('s1____walter', 'vl______wvl3', null, 'egress'); + + select is(count(*), 4::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + select is(count(*), 4::bigint) from wh_credential_group_membership where credential_group_key != 'no credentials' and credential_group_key != 'Unknown'; + select is(count(*), 1::bigint) from wh_credential_group where key != 'no credentials' and key != 'Unknown'; + + select * from finish(); +rollback; diff --git a/internal/session/query.go b/internal/session/query.go index 466a444aff..476bd0e2d1 100644 --- a/internal/session/query.go +++ b/internal/session/query.go @@ -1,5 +1,10 @@ package session +import ( + "fmt" + "strings" +) + const ( activateStateCte = ` insert into session_state @@ -346,3 +351,34 @@ with %s ` ) + +const ( + sessionCredentialDynamicBatchInsertBase = ` +insert into session_credential_dynamic + ( session_id, library_id, credential_purpose ) +values +` + sessionCredentialDynamicBatchInsertValue = ` + (?, ?, ?)` + + sessionCredentialDynamicBatchInsertReturning = ` + returning session_id, library_id, credential_id, credential_purpose +` +) + +func batchInsertsessionCredentialDynamic(creds []*DynamicCredential) (string, []interface{}, error) { + if len(creds) <= 0 { + return "", nil, fmt.Errorf("empty slice of DynamicCredential, cannot build query") + } + batchInsertParams := make([]string, 0, len(creds)) + batchInsertArgs := make([]interface{}, 0, len(creds)*3) + + for _, cred := range creds { + batchInsertParams = append(batchInsertParams, sessionCredentialDynamicBatchInsertValue) + batchInsertArgs = append(batchInsertArgs, []interface{}{cred.SessionId, cred.LibraryId, cred.CredentialPurpose}...) + } + + q := sessionCredentialDynamicBatchInsertBase + strings.Join(batchInsertParams, ",") + sessionCredentialDynamicBatchInsertReturning + + return q, batchInsertArgs, nil +} diff --git a/internal/session/repository_session.go b/internal/session/repository_session.go index 2c5ec397be..7eaf0da806 100644 --- a/internal/session/repository_session.go +++ b/internal/session/repository_session.go @@ -89,14 +89,26 @@ func (r *Repository) CreateSession(ctx context.Context, sessionWrapper wrapping. if err = w.Create(ctx, returnedSession); err != nil { return errors.Wrap(ctx, err, op) } + for _, cred := range newSession.DynamicCredentials { cred.SessionId = newSession.PublicId - returnedCred := cred.clone() - if err = w.Create(ctx, returnedCred); err != nil { + } + + // TODO: after upgrading to gorm v2 this batch insert can be replaced, since gorm v2 supports batch inserts + q, batchInsertArgs, err := batchInsertsessionCredentialDynamic(newSession.DynamicCredentials) + if err == nil { + rows, err := w.Query(ctx, q, batchInsertArgs) + if err != nil { return errors.Wrap(ctx, err, op) } - returnedSession.DynamicCredentials = append(returnedSession.DynamicCredentials, returnedCred) + defer rows.Close() + for rows.Next() { + var returnedCred DynamicCredential + w.ScanRows(rows, &returnedCred) + returnedSession.DynamicCredentials = append(returnedSession.DynamicCredentials, &returnedCred) + } } + var foundStates []*State // trigger will create new "Pending" state if foundStates, err = fetchStates(ctx, read, returnedSession.PublicId); err != nil {