From af8e4d63be4c247b5053f1091862eb63f8a0f33f Mon Sep 17 00:00:00 2001 From: Johan Brandhorst-Satzkorn Date: Fri, 12 May 2023 10:40:44 -0700 Subject: [PATCH] db: add channel summary details --- .../postgres/70/11_session_recording.up.sql | 218 +++++++++++++++++- .../tests/recording/recording_channel_ssh.sql | 54 ++++- ...rding_channel_ssh_validation_functions.sql | 131 +++++++++++ 3 files changed, 387 insertions(+), 16 deletions(-) create mode 100644 internal/db/sqltest/tests/recording/recording_channel_ssh_validation_functions.sql diff --git a/internal/db/schema/migrations/oss/postgres/70/11_session_recording.up.sql b/internal/db/schema/migrations/oss/postgres/70/11_session_recording.up.sql index 819f3f939..d37d74759 100644 --- a/internal/db/schema/migrations/oss/postgres/70/11_session_recording.up.sql +++ b/internal/db/schema/migrations/oss/postgres/70/11_session_recording.up.sql @@ -273,11 +273,7 @@ begin; 'delete_recording_channel_subtype deletes a row from the base table when a row is deleted in a subtype table.'; - -- TODO: We currently only include the channel type here, - -- but it's not enough on its own to determine what mime types - -- are supported by a channel. We'll need extra information in - -- the schema for this, but it's not clear what yet. - -- Channel types reference: https://www.iana.org/assignments/ssh-parameters/ssh-parameters.xml#ssh-parameters-11 + -- Channel types reference: https://www.rfc-editor.org/rfc/rfc4250.html#section-4.9.1 create table recording_channel_ssh_channel_type_enm ( name text primary key constraint only_predefined_channel_types_allowed @@ -285,7 +281,7 @@ begin; ); comment on table recording_channel_ssh_channel_type_enm is 'recording_channel_ssh_channel_type_enm holds valid values for the channel_type of a recording_channel_ssh row. ' - 'Some known channel types are defined in https://www.iana.org/assignments/ssh-parameters/ssh-parameters.xml#ssh-parameters-11'; + 'Some known channel types are defined in https://www.rfc-editor.org/rfc/rfc4250.html#section-4.9.1'; insert into recording_channel_ssh_channel_type_enm (name) values @@ -344,4 +340,214 @@ begin; create trigger delete_recording_channel_subtype after delete on recording_channel_ssh for each row execute procedure delete_recording_channel_subtype(); + create function validate_recording_channel_ssh_insert(new_channel_type text, new_public_id text) returns void + as $$ + begin + -- If the channel type is session, the program table must have exactly 1 entry matching this row + if new_channel_type = 'session' then + if count(*) != 1 from recording_channel_ssh_session_channel where recording_channel_id = new_public_id then + raise exception 'Channel recording of type ''session'' must populate recording_channel_ssh_session_channel'; + end if; + end if; + end; + $$ language plpgsql; + comment on function validate_recording_channel_ssh_insert is + 'function used to ensure each recording_channel_ssh entry with channel_type session has an associated ' + 'entry in recording_channel_ssh_session_channel.'; + + create function insert_recording_channel_ssh() returns trigger + as $$ + begin + perform validate_recording_channel_ssh_insert(new.channel_type, new.public_id); + return null; + end; + $$ language plpgsql; + comment on function insert_recording_channel_ssh() is + 'function used on recording_channel_ssh after insert initially deferred to ensure each ' + 'entry with a channel_type of ''session'' has an associated entry in recording_channel_ssh_session_channel'; + + create constraint trigger insert_recording_channel_ssh + after insert on recording_channel_ssh deferrable initially deferred + for each row execute procedure insert_recording_channel_ssh(); + + + create table recording_channel_ssh_session_channel_program_enm ( + name text primary key + constraint only_predefined_programs_allowed + check(name in ('none', 'shell', 'exec', 'subsystem')) + ); + comment on table recording_channel_ssh_session_channel_program_enm is + 'recording_channel_ssh_session_channel_program_enm holds valid values for the program column in recording_channel_ssh_session_channel'; + + insert into recording_channel_ssh_session_channel_program_enm (name) + values + ('none'), + ('shell'), + ('exec'), + ('subsystem'); + + create trigger immutable_columns before update on recording_channel_ssh_session_channel_program_enm + for each row execute procedure immutable_columns('name'); + + create table recording_channel_ssh_session_channel ( + recording_channel_id wt_public_id primary key + constraint recording_channel_ssh_fkey + references recording_channel_ssh (public_id) + on delete cascade + on update cascade, + program text not null + constraint recording_channel_ssh_session_channel_program_enm_fkey + references recording_channel_ssh_session_channel_program_enm (name) + on delete restrict + on update cascade + ); + + create trigger immutable_columns before update on recording_channel_ssh_session_channel + for each row execute procedure immutable_columns('recording_channel_id', 'program'); + + create function validate_recording_channel_ssh_session_channel_insert(new_recording_channel_id text, new_program text) returns void + as $$ + begin + -- First check that the channel_type is session in the channel recording table + if channel_type != 'session' from recording_channel_ssh where public_id = new_recording_channel_id then + raise exception 'Session channel must have channel_type ''session'''; + end if; + -- If the program is subsystem, the subsystem table must have exactly 1 entry matching this row + if new_program = 'subsystem' then + if count(*) != 1 from recording_channel_ssh_session_channel_program_subsystem where recording_channel_id = new_recording_channel_id then + raise exception 'Session channel with program ''subsystem'' must populate recording_channel_ssh_session_channel_program_subsystem'; + end if; + end if; + -- If the program is exec, the exec table must have exactly 1 entry matching this row + if new_program = 'exec' then + if count(*) != 1 from recording_channel_ssh_session_channel_program_exec where recording_channel_id = new_recording_channel_id then + raise exception 'Session channel with program ''exec'' must populate recording_channel_ssh_session_channel_program_exec'; + end if; + end if; + end; + $$ language plpgsql; + comment on function validate_recording_channel_ssh_session_channel_insert is + 'function used to ensure each recording_channel_ssh_session_channel entry has an associated ' + 'entry in recording_channel_ssh with a channel_type of ''session''.'; + + create function insert_recording_channel_ssh_session_channel() returns trigger + as $$ + begin + perform validate_recording_channel_ssh_session_channel_insert(new.recording_channel_id, new.program); + return null; + end; + $$ language plpgsql; + comment on function insert_recording_channel_ssh_session_channel() is + 'function used on recording_channel_ssh_session_channel after insert initially deferred to ' + 'validate inserts.'; + + create constraint trigger insert_recording_channel_ssh_session_channel + after insert on recording_channel_ssh_session_channel deferrable initially deferred + for each row execute procedure insert_recording_channel_ssh_session_channel(); + + + create table recording_channel_ssh_session_channel_program_subsystem ( + recording_channel_id wt_public_id primary key + constraint recording_channel_ssh_session_channel_fkey + references recording_channel_ssh_session_channel (recording_channel_id) + on delete cascade + on update cascade, + -- https://www.rfc-editor.org/rfc/rfc4250.html#section-4.6.1 defines + -- this as <= 64 bytes. Lets use a larger number just in case. + subsystem_name text not null + constraint subsystem_lte_1024_bytes + check(length(subsystem_name) <= 1024) + ); + + create trigger immutable_columns before update on recording_channel_ssh_session_channel_program_subsystem + for each row execute procedure immutable_columns('recording_channel_id', 'subsystem_name'); + + create function validate_recording_channel_ssh_subsystem_insert(new_recording_channel_id text) returns void + as $$ + begin + if program != 'subsystem' from recording_channel_ssh_session_channel where recording_channel_id = new_recording_channel_id then + raise exception 'Session channel subsystem program must have program ''subsystem'''; + end if; + end; + $$ language plpgsql; + comment on function validate_recording_channel_ssh_subsystem_insert is + 'function used to ensure each recording_channel_ssh_session_channel_program_subsystem entry has an associated ' + 'entry in recording_channel_ssh_session_channel with a program of ''subsystem''.'; + + create function insert_recording_channel_ssh_session_channel_program_subsystem() returns trigger + as $$ + begin + perform validate_recording_channel_ssh_subsystem_insert(new.recording_channel_id); + return null; + end; + $$ language plpgsql; + comment on function insert_recording_channel_ssh_session_channel_program_subsystem() is + 'function used on recording_channel_ssh_session_channel_program_subsystem after insert initially deferred to ensure each ' + 'entry has an associated entry in recording_channel_ssh_session_channel with a program of ''subsystem'''; + + create constraint trigger insert_recording_channel_ssh_session_channel_program_subsystem + after insert on recording_channel_ssh_session_channel_program_subsystem deferrable initially deferred + for each row execute procedure insert_recording_channel_ssh_session_channel_program_subsystem(); + + + create table recording_channel_ssh_session_channel_program_exec_enm ( + name text primary key + constraint only_predefined_execs_allowed + check(name in ('unknown', 'scp', 'rsync')) + ); + comment on table recording_channel_ssh_session_channel_program_exec_enm is + 'recording_channel_ssh_session_channel_program_exec_enm holds valid values for the exec_program column in recording_channel_ssh_session_channel_program_exec'; + + insert into recording_channel_ssh_session_channel_program_exec_enm (name) + values + ('unknown'), + ('scp'), + ('rsync'); + + create trigger immutable_columns before update on recording_channel_ssh_session_channel_program_exec_enm + for each row execute procedure immutable_columns('name'); + + create table recording_channel_ssh_session_channel_program_exec ( + recording_channel_id wt_public_id primary key + constraint recording_channel_ssh_session_channel_fkey + references recording_channel_ssh_session_channel (recording_channel_id) + on delete cascade + on update cascade, + exec_program text not null + constraint recording_channel_ssh_session_channel_program_exec_enm_fkey + references recording_channel_ssh_session_channel_program_exec_enm (name) + on delete restrict + on update cascade + ); + + create trigger immutable_columns before update on recording_channel_ssh_session_channel_program_exec + for each row execute procedure immutable_columns('recording_channel_id'); + + create function validate_recording_channel_ssh_exec_insert(new_recording_channel_id text) returns void + as $$ + begin + if program != 'exec' from recording_channel_ssh_session_channel where recording_channel_id = new_recording_channel_id then + raise exception 'Session channel exec program must have program ''exec'''; + end if; + end; + $$ language plpgsql; + comment on function validate_recording_channel_ssh_exec_insert is + 'function used to ensure each recording_channel_ssh_session_channel_program_exec entry has an associated ' + 'entry in recording_channel_ssh_session_channel with a program of ''exec''.'; + + create function insert_recording_channel_ssh_session_channel_program_exec() returns trigger + as $$ + begin + perform validate_recording_channel_ssh_exec_insert(new.recording_channel_id); + return null; + end; + $$ language plpgsql; + comment on function insert_recording_channel_ssh_session_channel_program_exec() is + 'function used on recording_channel_ssh_session_channel_program_exec after insert initially deferred to ensure each ' + 'entry has an associated entry in recording_channel_ssh_session_channel with a program of ''exec'''; + + create constraint trigger insert_recording_channel_ssh_session_channel_program_exec + after insert on recording_channel_ssh_session_channel_program_exec deferrable initially deferred + for each row execute procedure insert_recording_channel_ssh_session_channel_program_exec(); + commit; diff --git a/internal/db/sqltest/tests/recording/recording_channel_ssh.sql b/internal/db/sqltest/tests/recording/recording_channel_ssh.sql index eff7c0ccb..c2229ebb1 100644 --- a/internal/db/sqltest/tests/recording/recording_channel_ssh.sql +++ b/internal/db/sqltest/tests/recording/recording_channel_ssh.sql @@ -8,9 +8,10 @@ -- end_time_null_or_after_start_time -- bytes_up_null_zero_or_positive -- bytes_down_null_zero_or_positive +-- subsystem_shorter_than_1024_bytes begin; - select plan(9); + select plan(12); select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets', 'sessions'); insert into recording_session @@ -31,7 +32,7 @@ begin; insert into recording_channel_ssh (public_id, recording_connection_id, start_time, end_time, bytes_up, bytes_down, channel_type) values - ('chr_123456789', 'cr_123456789', clock_timestamp()::timestamptz, clock_timestamp()::timestamptz - '1s'::interval, 10, 10, 'session'); + ('chr_123456789', 'cr_123456789', clock_timestamp()::timestamptz, clock_timestamp()::timestamptz - '1s'::interval, 10, 10, 'x11'); select throws_ok('end_time_before_start_time', '23514', null, 'inserting a row with end_time before star_time succeeded'); -- Try to set bytes_up to a negative number @@ -39,7 +40,7 @@ begin; insert into recording_channel_ssh (public_id, recording_connection_id, start_time, end_time, bytes_up, bytes_down, channel_type) values - ('chr_123456789', 'cr_123456789', clock_timestamp()::timestamptz, clock_timestamp()::timestamptz + '1s'::interval, -1, 10, 'session'); + ('chr_123456789', 'cr_123456789', clock_timestamp()::timestamptz, clock_timestamp()::timestamptz + '1s'::interval, -1, 10, 'x11'); select throws_ok('negative_bytes_up', '23514', null, 'inserting a row with a negative bytes_up value succeeded'); -- Try to set bytes_down to a negative number @@ -47,34 +48,67 @@ begin; insert into recording_channel_ssh (public_id, recording_connection_id, start_time, end_time, bytes_up, bytes_down, channel_type) values - ('chr_123456789', 'cr_123456789', clock_timestamp()::timestamptz, clock_timestamp()::timestamptz + '1s'::interval, 10, -1, 'session'); + ('chr_123456789', 'cr_123456789', clock_timestamp()::timestamptz, clock_timestamp()::timestamptz + '1s'::interval, 10, -1, 'x11'); select throws_ok('negative_bytes_down', '23514', null, 'inserting a row with a negative bytes_down value succeeded'); -- Check that there are no rows + select is(count(*), 0::bigint) from recording_channel_ssh; select is(count(*), 0::bigint) from recording_channel; insert into recording_channel_ssh (public_id, recording_connection_id, start_time, end_time, bytes_up, bytes_down, channel_type) values - ('chr_123456789', 'cr_123456789', clock_timestamp()::timestamptz, clock_timestamp()::timestamptz + '1s'::interval, 10, 10, 'session'); + ('chr_123456789', 'cr_123456789', clock_timestamp()::timestamptz, clock_timestamp()::timestamptz + '1s'::interval, 10, 10, 'session'),-- 'none', null, null), + ('chr_234567891', 'cr_123456789', clock_timestamp()::timestamptz, clock_timestamp()::timestamptz + '1s'::interval, 10, 10, 'session'),-- 'shell', null, null), + ('chr_345678912', 'cr_123456789', clock_timestamp()::timestamptz, clock_timestamp()::timestamptz + '1s'::interval, 10, 10, 'session'),-- 'exec', 'scp', null), + ('chr_456789123', 'cr_123456789', clock_timestamp()::timestamptz, clock_timestamp()::timestamptz + '1s'::interval, 10, 10, 'session'),-- 'subsystem', null, 'sftp'), + ('chr_567891234', 'cr_123456789', clock_timestamp()::timestamptz, clock_timestamp()::timestamptz + '1s'::interval, 10, 10, 'x11');--, null, null, null); - -- Check that a row was inserted - select is(count(*), 1::bigint) from recording_channel where public_id = 'chr_123456789' and recording_connection_id = 'cr_123456789'; + insert into recording_channel_ssh_session_channel + (recording_channel_id, program) + values + ('chr_123456789', 'none'), + ('chr_234567891', 'shell'), + ('chr_345678912', 'exec'), + ('chr_456789123', 'subsystem'); + + insert into recording_channel_ssh_session_channel_program_exec + (recording_channel_id, exec_program) + values + ('chr_345678912', 'scp'); + + -- Try to insert a large subsystem row + prepare insert_large_subsystem_name as + insert into recording_channel_ssh_session_channel_program_subsystem + (recording_channel_id, subsystem_name) + values + ('chr_456789123', repeat('sftp', 1000)); + select throws_ok('insert_large_subsystem_name', '23514', null, 'inserting a large subsystem_name succeeded'); + + + insert into recording_channel_ssh_session_channel_program_subsystem + (recording_channel_id, subsystem_name) + values + ('chr_456789123', 'sftp'); + + -- Check that the rows were inserted + select is(count(*), 5::bigint) from recording_channel_ssh where recording_connection_id = 'cr_123456789'; + select is(count(*), 5::bigint) from recording_channel where recording_connection_id = 'cr_123456789'; -- Deleting the session connection should leave the recording in place delete from session_connection where public_id = 'sc_123456789'; -- Row should still be present - select is(count(*), 1::bigint) from recording_channel where public_id = 'chr_123456789'; + select is(count(*), 5::bigint) from recording_channel_ssh where recording_connection_id = 'cr_123456789'; -- Deleting the session should leave the recording in place delete from session where public_id = 's2_____clare'; -- Row should still be present - select is(count(*), 1::bigint) from recording_channel where public_id = 'chr_123456789'; + select is(count(*), 5::bigint) from recording_channel_ssh where recording_connection_id = 'cr_123456789'; -- Deleting the session recording should cascade to the channel recording delete from recording_session where public_id = 'sr_123456789'; -- Row should be deleted - select is(count(*), 0::bigint) from recording_channel where public_id = 'chr_123456789'; + select is(count(*), 0::bigint) from recording_channel_ssh where recording_connection_id = 'cr_123456789'; -- Check that it was also deleted from recording_channel select is(count(*), 0::bigint) from recording_channel; diff --git a/internal/db/sqltest/tests/recording/recording_channel_ssh_validation_functions.sql b/internal/db/sqltest/tests/recording/recording_channel_ssh_validation_functions.sql new file mode 100644 index 000000000..ef8162406 --- /dev/null +++ b/internal/db/sqltest/tests/recording/recording_channel_ssh_validation_functions.sql @@ -0,0 +1,131 @@ +-- Copyright (c) HashiCorp, Inc. +-- SPDX-License-Identifier: MPL-2.0 + +-- recording_channel_ssh_validation_functions tests the following functions: +-- validate_recording_channel_ssh_insert +-- validate_recording_channel_ssh_session_channel_insert +-- validate_recording_channel_ssh_subsystem_insert +-- validate_recording_channel_ssh_exec_insert + +begin; + select plan(14); + select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets', 'sessions'); + + insert into recording_connection + (public_id, session_id, session_connection_id, recording_session_id) + values + ('cr_123456789', 's1_____clare', 'sc1_____clare', 'sr1____clare'); + insert into recording_channel_ssh + (public_id, recording_connection_id, start_time, end_time, bytes_up, bytes_down, channel_type) + values + ('chr_123456789', 'cr_123456789', clock_timestamp()::timestamptz, clock_timestamp()::timestamptz + '1s'::interval, 10, 10, 'session'), + ('chr_234567891', 'cr_123456789', clock_timestamp()::timestamptz, clock_timestamp()::timestamptz + '1s'::interval, 10, 10, 'session'), + ('chr_345678912', 'cr_123456789', clock_timestamp()::timestamptz, clock_timestamp()::timestamptz + '1s'::interval, 10, 10, 'session'), + ('chr_456789123', 'cr_123456789', clock_timestamp()::timestamptz, clock_timestamp()::timestamptz + '1s'::interval, 10, 10, 'session'), + ('chr_567891234', 'cr_123456789', clock_timestamp()::timestamptz, clock_timestamp()::timestamptz + '1s'::interval, 10, 10, 'x11'); + insert into recording_channel_ssh_session_channel + (recording_channel_id, program) + values + ('chr_123456789', 'none'), + ('chr_234567891', 'shell'), + ('chr_345678912', 'exec'), + ('chr_456789123', 'subsystem'); + insert into recording_channel_ssh_session_channel_program_exec + (recording_channel_id, exec_program) + values + ('chr_345678912', 'scp'); + insert into recording_channel_ssh_session_channel_program_subsystem + (recording_channel_id, subsystem_name) + values + ('chr_456789123', 'sftp'); + + --- + -- validate_recording_channel_ssh_insert tests + ---- + -- Validate the valid row with id chr_123456789 + prepare valid_session_channel_passes as + select validate_recording_channel_ssh_insert('session', 'chr_123456789'); + select lives_ok('valid_session_channel_passes'); + + -- Validate the valid non-session row with id chr_567891234 + prepare non_session_channel_passes as + select validate_recording_channel_ssh_insert('x11', 'chr_567891234'); + select lives_ok('non_session_channel_passes'); + + -- Should error when pretending chr_567891234 is a session channel, + -- since there is no corresponding row in recording_channel_ssh_session_channel + prepare invalid_session_channel_fails as + select validate_recording_channel_ssh_insert('session', 'chr_567891234'); + select throws_ok('invalid_session_channel_fails', null, 'Channel recording of type ''session'' must populate recording_channel_ssh_session_channel'); + + --- + -- validate_recording_channel_ssh_session_channel_insert tests + ---- + -- Validate the valid row with id chr_123456789 + prepare valid_session_channel_insert_none_passes as + select validate_recording_channel_ssh_session_channel_insert('chr_123456789', 'none'); + select lives_ok('valid_session_channel_insert_none_passes'); + + -- Validate the valid row with id chr_234567891 + prepare valid_session_channel_insert_shell_passes as + select validate_recording_channel_ssh_session_channel_insert('chr_234567891', 'shell'); + select lives_ok('valid_session_channel_insert_shell_passes'); + + -- Validate the valid row with id chr_345678912 + prepare valid_session_channel_insert_exec_passes as + select validate_recording_channel_ssh_session_channel_insert('chr_345678912', 'exec'); + select lives_ok('valid_session_channel_insert_exec_passes'); + + -- Validate the valid row with id chr_456789123 + prepare valid_session_channel_insert_subsystem_passes as + select validate_recording_channel_ssh_session_channel_insert('chr_456789123', 'subsystem'); + select lives_ok('valid_session_channel_insert_subsystem_passes'); + + -- Should error when pretending chr_123456789 is a subsystem program, + -- since there is no corresponding row in recording_channel_ssh_session_channel_program_subsystem + prepare invalid_session_channel_subsystem_fails as + select validate_recording_channel_ssh_session_channel_insert('chr_123456789', 'subsystem'); + select throws_ok('invalid_session_channel_subsystem_fails', null, 'Session channel with program ''exec'' must populate recording_channel_ssh_session_channel_program_exec'); + + -- Should error when pretending chr_123456789 is an exec program, + -- since there is no corresponding row in recording_channel_ssh_session_channel_program_exec + prepare invalid_session_channel_exec_fails as + select validate_recording_channel_ssh_session_channel_insert('chr_123456789', 'exec'); + select throws_ok('invalid_session_channel_exec_fails', null, 'Session channel with program ''subsystem'' must populate recording_channel_ssh_session_channel_program_subsystem'); + + -- Should error when using the non-session channel chr_567891234, + -- as it doesn't have the channel_type session. + prepare non_session_channel_id_fails as + select validate_recording_channel_ssh_session_channel_insert('chr_567891234', 'none'); + select throws_ok('non_session_channel_id_fails', null, 'Session channel must have channel_type ''session'''); + + --- + -- validate_recording_channel_ssh_subsystem_insert tests + ---- + -- Validate the valid row with id chr_456789123 + prepare valid_session_channel_subsystem_insert_passes as + select validate_recording_channel_ssh_subsystem_insert('chr_456789123'); + select lives_ok('valid_session_channel_subsystem_insert_passes'); + + -- Should error when using the non-subsystem session channel chr_123456789, + -- as it doesn't have the program subsystem. + prepare non_subsystem_session_channel_id_fails as + select validate_recording_channel_ssh_subsystem_insert('chr_123456789'); + select throws_ok('non_subsystem_session_channel_id_fails', null, 'Session channel subsystem program must have program ''subsystem'''); + + --- + -- validate_recording_channel_ssh_exec_insert tests + ---- + -- Validate the valid row with id chr_345678912 + prepare valid_session_channel_exec_insert_passes as + select validate_recording_channel_ssh_exec_insert('chr_345678912'); + select lives_ok('valid_session_channel_exec_insert_passes'); + + -- Should error when using the non-exec session channel chr_123456789, + -- as it doesn't have the program exec. + prepare non_exec_session_channel_id_fails as + select validate_recording_channel_ssh_exec_insert('chr_123456789'); + select throws_ok('non_exec_session_channel_id_fails', null, 'Session channel exec program must have program ''exec'''); + + select * from finish(); +rollback;