basic sessions (#337)
This commit is contained in:
35 files changed
+5934
-111
No files matched your search
@@ -598,6 +598,7 @@ create table iam_scope_project (
|
||||
parent_id wt_public_id not null references iam_scope_org(scope_id) on delete cascade on update cascade,
|
||||
name text,
|
||||
unique(parent_id, name),
|
||||
unique(scope_id),
|
||||
primary key(scope_id, parent_id)
|
||||
);
|
||||
|
||||
@@ -3242,6 +3243,17 @@ begin;
|
||||
drop table session;
|
||||
drop table session_termination_reason_enm;
|
||||
drop function insert_session_state;
|
||||
drop function insert_new_session_state;
|
||||
drop function insert_session;
|
||||
drop function update_session_state_on_termination_reason;
|
||||
drop function insert_session_state;
|
||||
|
||||
|
||||
delete
|
||||
from oplog_ticket
|
||||
where name in (
|
||||
'session'
|
||||
);
|
||||
|
||||
commit;
|
||||
|
||||
@@ -3275,7 +3287,7 @@ begin;
|
||||
│ server_id (fk3) │ ▲fk4 ┼
|
||||
│ server_type (fk3) │╲ ┼
|
||||
│ target_id (fk4) │─○─────────────────┬─────────────────────┤
|
||||
│ set_id (fk5) │╱ ┼ ┼
|
||||
│ host_set_id (fk5) │╱ ┼ ┼
|
||||
│ auth_token_id (fk6) │ ▼fk5 ┼ ▼fk2 ┼
|
||||
│ scope_id (fk7) │ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ termination_reason (fk8) │ │ host_set │ │ host │
|
||||
@@ -3364,7 +3376,7 @@ begin;
|
||||
on update cascade,
|
||||
-- the host set the host was chosen from and the user was authorized to
|
||||
-- connect to via the target
|
||||
set_id wt_public_id -- fk5
|
||||
host_set_id wt_public_id -- fk5
|
||||
references host_set (public_id)
|
||||
on delete set null
|
||||
on update cascade,
|
||||
@@ -3373,68 +3385,153 @@ begin;
|
||||
references auth_token (public_id)
|
||||
on delete set null
|
||||
on update cascade,
|
||||
-- the organization which owns this session
|
||||
-- the project which owns this session
|
||||
scope_id wt_scope_id -- fk7
|
||||
references iam_scope_org (scope_id)
|
||||
references iam_scope_project (scope_id)
|
||||
on delete set null
|
||||
on update cascade,
|
||||
-- Certificate to use when connecting (or if using custom certs, to
|
||||
-- serve as the "login"). Raw DER bytes.
|
||||
certificate bytea not null,
|
||||
-- after this time the connection will be expired, e.g. forcefully terminated
|
||||
expiration_time wt_timestamp, -- maybe null
|
||||
-- trust of first use token
|
||||
tofu_token bytea, -- will be null when session is first created
|
||||
-- the reason this session ended (null until terminated)
|
||||
-- TODO: Make key_id a foreign key once we have DEKs
|
||||
key_id text, -- will be null on insert
|
||||
-- references kms_database_key_version(private_id)
|
||||
-- on delete restrict
|
||||
-- on update cascade,
|
||||
termination_reason text -- fk8
|
||||
references session_termination_reason_enm (name)
|
||||
on delete restrict
|
||||
on update cascade,
|
||||
-- the network address of the host the worker proxied a connection to for
|
||||
-- the user
|
||||
address text
|
||||
not null
|
||||
check(
|
||||
length(trim(address)) > 7
|
||||
and
|
||||
length(trim(address)) < 256
|
||||
),
|
||||
-- the network port at the address of the host the worker proxied a
|
||||
-- connection to for the user
|
||||
port integer
|
||||
not null
|
||||
check(
|
||||
port > 0
|
||||
and
|
||||
port <= 65535
|
||||
),
|
||||
-- the total number of bytes received by the worker from the user and sent
|
||||
-- to the host for this session
|
||||
bytes_up bigint -- can be null
|
||||
check (
|
||||
bytes_up is null
|
||||
or
|
||||
bytes_up >= 0
|
||||
),
|
||||
-- the total number of bytes received by the worker from the host and sent
|
||||
-- to the user for this session
|
||||
bytes_down bigint -- can be null
|
||||
check (
|
||||
bytes_down is null
|
||||
or
|
||||
bytes_down >= 0
|
||||
)
|
||||
version wt_version,
|
||||
create_time wt_timestamp,
|
||||
update_time wt_timestamp
|
||||
);
|
||||
|
||||
create trigger
|
||||
immutable_columns
|
||||
before
|
||||
update on session
|
||||
for each row execute procedure immutable_columns('public_id', 'certificate', 'expiration_time', 'create_time');
|
||||
|
||||
create trigger
|
||||
update_version_column
|
||||
after update on session
|
||||
for each row execute procedure update_version_column();
|
||||
|
||||
create trigger
|
||||
update_time_column
|
||||
before update on session
|
||||
for each row execute procedure update_time_column();
|
||||
|
||||
create trigger
|
||||
default_create_time_column
|
||||
before
|
||||
insert on session
|
||||
for each row execute procedure default_create_time();
|
||||
|
||||
create or replace function
|
||||
insert_session()
|
||||
returns trigger
|
||||
as $$
|
||||
begin
|
||||
case
|
||||
when new.user_id is null then
|
||||
raise exception 'user_id is null';
|
||||
when new.host_id is null then
|
||||
raise exception 'host_id is null';
|
||||
when new.target_id is null then
|
||||
raise exception 'target_id is null';
|
||||
when new.host_set_id is null then
|
||||
raise exception 'host_set_id is null';
|
||||
when new.auth_token_id is null then
|
||||
raise exception 'auth_token_id is null';
|
||||
when new.scope_id is null then
|
||||
raise exception 'scope_id is null';
|
||||
else
|
||||
end case;
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
|
||||
create trigger
|
||||
insert_session
|
||||
before insert on session
|
||||
for each row execute procedure insert_session();
|
||||
|
||||
create or replace function
|
||||
insert_new_session_state()
|
||||
returns trigger
|
||||
as $$
|
||||
begin
|
||||
insert into session_state (session_id, state)
|
||||
values
|
||||
(new.public_id, 'pending');
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
|
||||
create trigger
|
||||
insert_new_session_state
|
||||
after insert on session
|
||||
for each row execute procedure insert_new_session_state();
|
||||
|
||||
-- update_connection_state_on_closed_reason() is used in an update insert trigger on the
|
||||
-- session_connection table. it will valiadate that all the session's
|
||||
-- connections are closed, and then insert a state of "closed" in
|
||||
-- session_connection_state for the closed session connection.
|
||||
create or replace function
|
||||
update_session_state_on_termination_reason()
|
||||
returns trigger
|
||||
as $$
|
||||
begin
|
||||
if new.termination_reason is not null then
|
||||
perform from
|
||||
session_connection sc,
|
||||
session_connection_state scs
|
||||
where
|
||||
sc.public_id = scs.connection_id and
|
||||
scs.state != 'closed' and
|
||||
sc.session_id = new.public_id;
|
||||
if found then
|
||||
raise 'session %s has existing open connections', new.public_id;
|
||||
end if;
|
||||
insert into session_state (session_id, state)
|
||||
values
|
||||
(new.public_id, 'terminated');
|
||||
end if;
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
|
||||
|
||||
create trigger
|
||||
update_session_state_on_termination_reason
|
||||
after update of termination_reason on session
|
||||
for each row execute procedure update_session_state_on_termination_reason();
|
||||
|
||||
|
||||
create table session_state_enm (
|
||||
name text primary key
|
||||
check (
|
||||
name in ('pending', 'connected', 'canceling', 'closed')
|
||||
name in ('pending', 'active', 'canceling', 'terminated')
|
||||
)
|
||||
);
|
||||
|
||||
insert into session_state_enm (name)
|
||||
values
|
||||
('pending'),
|
||||
('connected'),
|
||||
('active'),
|
||||
('canceling'),
|
||||
('closed');
|
||||
('terminated');
|
||||
|
||||
/*
|
||||
|
||||
|
||||
┌────────────────┐
|
||||
start │ │
|
||||
. │ Canceling │
|
||||
@@ -3445,7 +3542,7 @@ begin;
|
||||
▼ │ ▼
|
||||
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
|
||||
│ │ │ │ │ │
|
||||
│ Pending │ │ Connected │ │ Closed │
|
||||
│ Pending │ │ Active │ │ Terminated │
|
||||
│ │──────────▶│ │──────────▶│ │
|
||||
│ │ │ │ │ │
|
||||
└────────────────┘ └────────────────┘ └────────────────┘
|
||||
@@ -3486,6 +3583,12 @@ begin;
|
||||
);
|
||||
|
||||
|
||||
create trigger
|
||||
immutable_columns
|
||||
before
|
||||
update on session_state
|
||||
for each row execute procedure immutable_columns('session_id', 'start_time', 'previous_end_time');
|
||||
|
||||
create or replace function
|
||||
insert_session_state()
|
||||
returns trigger
|
||||
@@ -3516,6 +3619,300 @@ begin;
|
||||
create trigger insert_session_state before insert on session_state
|
||||
for each row execute procedure insert_session_state();
|
||||
|
||||
|
||||
commit;
|
||||
|
||||
`),
|
||||
},
|
||||
"migrations/51_connection.down.sql": {
|
||||
name: "51_connection.down.sql",
|
||||
bytes: []byte(`
|
||||
begin;
|
||||
|
||||
drop table session_connection_state;
|
||||
drop table session_connection_state_enm;
|
||||
drop table session_connection;
|
||||
drop table session_connection_closed_reason_enm;
|
||||
drop function insert_session_connection_state;
|
||||
drop function insert_new_connection_state;
|
||||
drop function update_connection_state_on_closed_reason;
|
||||
commit;
|
||||
|
||||
`),
|
||||
},
|
||||
"migrations/51_connection.up.sql": {
|
||||
name: "51_connection.up.sql",
|
||||
bytes: []byte(`
|
||||
begin;
|
||||
|
||||
/*
|
||||
|
||||
┌────────────────┐
|
||||
│ session │
|
||||
├────────────────┤
|
||||
│ public_id (pk) │
|
||||
│ │
|
||||
│ │
|
||||
└────────────────┘
|
||||
▲fk1 ┼
|
||||
┼
|
||||
│
|
||||
│
|
||||
┼
|
||||
╱│╲ ┌──────────────────────────────────────┐
|
||||
┌───────────────────────┐ │ session_connection_closed_reason_enm │
|
||||
│ session_connection │╲ fk2▶ ├──────────────────────────────────────┤
|
||||
├───────────────────────┤─○───○┼│ name │
|
||||
│ public_id (pk) │╱ └──────────────────────────────────────┘
|
||||
│ session_id (fk1) │
|
||||
│ closed_reason (fk2) │
|
||||
└───────────────────────┘
|
||||
▲fk1 ┼
|
||||
┼
|
||||
│
|
||||
│
|
||||
┼
|
||||
╱│╲
|
||||
┌──────────────────────────────────────────┐
|
||||
│ session_connection_state │
|
||||
├──────────────────────────────────────────┤┼○┐
|
||||
│ connection_id (pk,fk1,fk2,unq1,unq2) │ │◀fk2
|
||||
│ state (fk3) │ │
|
||||
│ previous_end_time (fk2,unq1) │┼○┘
|
||||
│ start_time (pk) │
|
||||
│ end_time (unq2) │
|
||||
└──────────────────────────────────────────┘
|
||||
╲│╱
|
||||
○
|
||||
│
|
||||
│
|
||||
┼
|
||||
▼fk3 ┼
|
||||
┌───────────────────────────────┐
|
||||
│ session_connection_state_enm │
|
||||
├───────────────────────────────┤
|
||||
│ name │
|
||||
└───────────────────────────────┘
|
||||
|
||||
*/
|
||||
|
||||
create table session_connection_closed_reason_enm (
|
||||
name text primary key
|
||||
check (
|
||||
name in (
|
||||
'unknown',
|
||||
'timed out',
|
||||
'closed by end-user',
|
||||
'cancelled',
|
||||
'network error',
|
||||
'system error'
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
insert into session_connection_closed_reason_enm (name)
|
||||
values
|
||||
('unknown'),
|
||||
('timed out'),
|
||||
('closed by end-user'),
|
||||
('cancelled'),
|
||||
('network error'),
|
||||
('system error');
|
||||
|
||||
-- A session connection is one connection proxied by a worker from a client to
|
||||
-- a backend for a session. The client initiates the connection to the worker
|
||||
-- and the worker initiates the connection to the backend.
|
||||
-- A session can have zero or more session connections.
|
||||
create table session_connection (
|
||||
public_id wt_public_id primary key,
|
||||
session_id wt_public_id not null
|
||||
references session (public_id)
|
||||
on delete cascade
|
||||
on update cascade,
|
||||
-- the client_tcp_address is the network address of the client which initiated
|
||||
-- the connection to a worker
|
||||
client_tcp_address inet not null,
|
||||
-- the client_tcp_port is the network port at the address of the client the
|
||||
-- worker proxied a connection for the user
|
||||
client_tcp_port integer not null
|
||||
check(
|
||||
client_tcp_port > 0
|
||||
and
|
||||
client_tcp_port <= 65535
|
||||
),
|
||||
-- the backend_tcp_address is the network address of the backend which the
|
||||
-- worker initiated the connection to, for the user
|
||||
backend_tcp_address inet not null,
|
||||
-- the backend_tcp_port is the network port at the address of the backend the
|
||||
-- worker proxied a connection to, for the user
|
||||
backend_tcp_port integer not null
|
||||
check(
|
||||
backend_tcp_port > 0
|
||||
and
|
||||
backend_tcp_port <= 65535
|
||||
),
|
||||
-- the total number of bytes received by the worker from the client and sent
|
||||
-- to the backend for this connection
|
||||
bytes_up bigint -- can be null
|
||||
check (
|
||||
bytes_up is null
|
||||
or
|
||||
bytes_up >= 0
|
||||
),
|
||||
-- the total number of bytes received by the worker from the backend and sent
|
||||
-- to the client for this connection
|
||||
bytes_down bigint -- can be null
|
||||
check (
|
||||
bytes_down is null
|
||||
or
|
||||
bytes_down >= 0
|
||||
),
|
||||
closed_reason text
|
||||
references session_connection_closed_reason_enm (name)
|
||||
on delete restrict
|
||||
on update cascade,
|
||||
version wt_version,
|
||||
create_time wt_timestamp,
|
||||
update_time wt_timestamp
|
||||
);
|
||||
|
||||
create trigger
|
||||
immutable_columns
|
||||
before
|
||||
update on session_connection
|
||||
for each row execute procedure immutable_columns('public_id', 'session_id', 'client_tcp_address', 'client_tcp_port', 'backend_tcp_address', 'backend_tcp_port', 'create_time');
|
||||
|
||||
create trigger
|
||||
update_version_column
|
||||
after update on session_connection
|
||||
for each row execute procedure update_version_column();
|
||||
|
||||
create trigger
|
||||
update_time_column
|
||||
before update on session_connection
|
||||
for each row execute procedure update_time_column();
|
||||
|
||||
create trigger
|
||||
default_create_time_column
|
||||
before
|
||||
insert on session_connection
|
||||
for each row execute procedure default_create_time();
|
||||
|
||||
-- insert_new_connection_state() is used in an after insert trigger on the
|
||||
-- session_connection table. it will insert a state of "connected" in
|
||||
-- session_connection_state for the new session connection.
|
||||
create or replace function
|
||||
insert_new_connection_state()
|
||||
returns trigger
|
||||
as $$
|
||||
begin
|
||||
insert into session_connection_state (connection_id, state)
|
||||
values
|
||||
(new.public_id, 'connected');
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
|
||||
create trigger
|
||||
insert_new_connection_state
|
||||
after insert on session_connection
|
||||
for each row execute procedure insert_new_connection_state();
|
||||
|
||||
-- update_connection_state_on_closed_reason() is used in an update insert trigger on the
|
||||
-- session_connection table. it will insert a state of "closed" in
|
||||
-- session_connection_state for the closed session connection.
|
||||
create or replace function
|
||||
update_connection_state_on_closed_reason()
|
||||
returns trigger
|
||||
as $$
|
||||
begin
|
||||
if new.closed_reason is not null then
|
||||
insert into session_connection_state (connection_id, state)
|
||||
values
|
||||
(new.public_id, 'closed');
|
||||
end if;
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
|
||||
create trigger
|
||||
update_connection_state_on_closed_reason
|
||||
after update of closed_reason on session_connection
|
||||
for each row execute procedure update_connection_state_on_closed_reason();
|
||||
|
||||
create table session_connection_state_enm (
|
||||
name text primary key
|
||||
check (
|
||||
name in ('connected', 'closed')
|
||||
)
|
||||
);
|
||||
|
||||
insert into session_connection_state_enm (name)
|
||||
values
|
||||
('connected'),
|
||||
('closed');
|
||||
|
||||
|
||||
create table session_connection_state (
|
||||
connection_id wt_public_id not null
|
||||
references session_connection (public_id)
|
||||
on delete cascade
|
||||
on update cascade,
|
||||
state text not null
|
||||
references session_connection_state_enm(name)
|
||||
on delete restrict
|
||||
on update cascade,
|
||||
previous_end_time timestamp with time zone, -- fk2 -- null means first state
|
||||
start_time timestamp with time zone default current_timestamp not null,
|
||||
constraint previous_end_time_and_start_time_in_sequence
|
||||
check (previous_end_time <= start_time),
|
||||
end_time timestamp with time zone, -- null means unfinished current state
|
||||
constraint start_and_end_times_in_sequence
|
||||
check (start_time <= end_time),
|
||||
constraint end_times_in_sequence
|
||||
check (previous_end_time <> end_time),
|
||||
primary key (connection_id, start_time),
|
||||
unique (connection_id, previous_end_time), -- null means first state
|
||||
unique (connection_id, end_time), -- one null current state
|
||||
foreign key (connection_id, previous_end_time) -- self-reference
|
||||
references session_connection_state (connection_id, end_time)
|
||||
);
|
||||
|
||||
create trigger
|
||||
immutable_columns
|
||||
before
|
||||
update on session_connection_state
|
||||
for each row execute procedure immutable_columns('connection_id', 'state', 'start_time');
|
||||
|
||||
create or replace function
|
||||
insert_session_connection_state()
|
||||
returns trigger
|
||||
as $$
|
||||
begin
|
||||
|
||||
update session_connection_state
|
||||
set end_time = now()
|
||||
where connection_id = new.connection_id
|
||||
and end_time is null;
|
||||
|
||||
if not found then
|
||||
new.previous_end_time = null;
|
||||
new.start_time = now();
|
||||
new.end_time = null;
|
||||
return new;
|
||||
end if;
|
||||
|
||||
new.previous_end_time = now();
|
||||
new.start_time = now();
|
||||
new.end_time = null;
|
||||
return new;
|
||||
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
|
||||
create trigger insert_session_connection_state before insert on session_connection_state
|
||||
for each row execute procedure insert_session_connection_state();
|
||||
|
||||
commit;
|
||||
|
||||
`),
|
||||
|
||||
@@ -80,6 +80,7 @@ create table iam_scope_project (
|
||||
parent_id wt_public_id not null references iam_scope_org(scope_id) on delete cascade on update cascade,
|
||||
name text,
|
||||
unique(parent_id, name),
|
||||
unique(scope_id),
|
||||
primary key(scope_id, parent_id)
|
||||
);
|
||||
|
||||
|
||||
@@ -5,5 +5,16 @@ begin;
|
||||
drop table session;
|
||||
drop table session_termination_reason_enm;
|
||||
drop function insert_session_state;
|
||||
drop function insert_new_session_state;
|
||||
drop function insert_session;
|
||||
drop function update_session_state_on_termination_reason;
|
||||
drop function insert_session_state;
|
||||
|
||||
|
||||
delete
|
||||
from oplog_ticket
|
||||
where name in (
|
||||
'session'
|
||||
);
|
||||
|
||||
commit;
|
||||
@@ -23,7 +23,7 @@ begin;
|
||||
│ server_id (fk3) │ ▲fk4 ┼
|
||||
│ server_type (fk3) │╲ ┼
|
||||
│ target_id (fk4) │─○─────────────────┬─────────────────────┤
|
||||
│ set_id (fk5) │╱ ┼ ┼
|
||||
│ host_set_id (fk5) │╱ ┼ ┼
|
||||
│ auth_token_id (fk6) │ ▼fk5 ┼ ▼fk2 ┼
|
||||
│ scope_id (fk7) │ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ termination_reason (fk8) │ │ host_set │ │ host │
|
||||
@@ -112,7 +112,7 @@ begin;
|
||||
on update cascade,
|
||||
-- the host set the host was chosen from and the user was authorized to
|
||||
-- connect to via the target
|
||||
set_id wt_public_id -- fk5
|
||||
host_set_id wt_public_id -- fk5
|
||||
references host_set (public_id)
|
||||
on delete set null
|
||||
on update cascade,
|
||||
@@ -121,68 +121,153 @@ begin;
|
||||
references auth_token (public_id)
|
||||
on delete set null
|
||||
on update cascade,
|
||||
-- the organization which owns this session
|
||||
-- the project which owns this session
|
||||
scope_id wt_scope_id -- fk7
|
||||
references iam_scope_org (scope_id)
|
||||
references iam_scope_project (scope_id)
|
||||
on delete set null
|
||||
on update cascade,
|
||||
-- Certificate to use when connecting (or if using custom certs, to
|
||||
-- serve as the "login"). Raw DER bytes.
|
||||
certificate bytea not null,
|
||||
-- after this time the connection will be expired, e.g. forcefully terminated
|
||||
expiration_time wt_timestamp, -- maybe null
|
||||
-- trust of first use token
|
||||
tofu_token bytea, -- will be null when session is first created
|
||||
-- the reason this session ended (null until terminated)
|
||||
-- TODO: Make key_id a foreign key once we have DEKs
|
||||
key_id text, -- will be null on insert
|
||||
-- references kms_database_key_version(private_id)
|
||||
-- on delete restrict
|
||||
-- on update cascade,
|
||||
termination_reason text -- fk8
|
||||
references session_termination_reason_enm (name)
|
||||
on delete restrict
|
||||
on update cascade,
|
||||
-- the network address of the host the worker proxied a connection to for
|
||||
-- the user
|
||||
address text
|
||||
not null
|
||||
check(
|
||||
length(trim(address)) > 7
|
||||
and
|
||||
length(trim(address)) < 256
|
||||
),
|
||||
-- the network port at the address of the host the worker proxied a
|
||||
-- connection to for the user
|
||||
port integer
|
||||
not null
|
||||
check(
|
||||
port > 0
|
||||
and
|
||||
port <= 65535
|
||||
),
|
||||
-- the total number of bytes received by the worker from the user and sent
|
||||
-- to the host for this session
|
||||
bytes_up bigint -- can be null
|
||||
check (
|
||||
bytes_up is null
|
||||
or
|
||||
bytes_up >= 0
|
||||
),
|
||||
-- the total number of bytes received by the worker from the host and sent
|
||||
-- to the user for this session
|
||||
bytes_down bigint -- can be null
|
||||
check (
|
||||
bytes_down is null
|
||||
or
|
||||
bytes_down >= 0
|
||||
)
|
||||
version wt_version,
|
||||
create_time wt_timestamp,
|
||||
update_time wt_timestamp
|
||||
);
|
||||
|
||||
create trigger
|
||||
immutable_columns
|
||||
before
|
||||
update on session
|
||||
for each row execute procedure immutable_columns('public_id', 'certificate', 'expiration_time', 'create_time');
|
||||
|
||||
create trigger
|
||||
update_version_column
|
||||
after update on session
|
||||
for each row execute procedure update_version_column();
|
||||
|
||||
create trigger
|
||||
update_time_column
|
||||
before update on session
|
||||
for each row execute procedure update_time_column();
|
||||
|
||||
create trigger
|
||||
default_create_time_column
|
||||
before
|
||||
insert on session
|
||||
for each row execute procedure default_create_time();
|
||||
|
||||
create or replace function
|
||||
insert_session()
|
||||
returns trigger
|
||||
as $$
|
||||
begin
|
||||
case
|
||||
when new.user_id is null then
|
||||
raise exception 'user_id is null';
|
||||
when new.host_id is null then
|
||||
raise exception 'host_id is null';
|
||||
when new.target_id is null then
|
||||
raise exception 'target_id is null';
|
||||
when new.host_set_id is null then
|
||||
raise exception 'host_set_id is null';
|
||||
when new.auth_token_id is null then
|
||||
raise exception 'auth_token_id is null';
|
||||
when new.scope_id is null then
|
||||
raise exception 'scope_id is null';
|
||||
else
|
||||
end case;
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
|
||||
create trigger
|
||||
insert_session
|
||||
before insert on session
|
||||
for each row execute procedure insert_session();
|
||||
|
||||
create or replace function
|
||||
insert_new_session_state()
|
||||
returns trigger
|
||||
as $$
|
||||
begin
|
||||
insert into session_state (session_id, state)
|
||||
values
|
||||
(new.public_id, 'pending');
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
|
||||
create trigger
|
||||
insert_new_session_state
|
||||
after insert on session
|
||||
for each row execute procedure insert_new_session_state();
|
||||
|
||||
-- update_connection_state_on_closed_reason() is used in an update insert trigger on the
|
||||
-- session_connection table. it will valiadate that all the session's
|
||||
-- connections are closed, and then insert a state of "closed" in
|
||||
-- session_connection_state for the closed session connection.
|
||||
create or replace function
|
||||
update_session_state_on_termination_reason()
|
||||
returns trigger
|
||||
as $$
|
||||
begin
|
||||
if new.termination_reason is not null then
|
||||
perform from
|
||||
session_connection sc,
|
||||
session_connection_state scs
|
||||
where
|
||||
sc.public_id = scs.connection_id and
|
||||
scs.state != 'closed' and
|
||||
sc.session_id = new.public_id;
|
||||
if found then
|
||||
raise 'session %s has existing open connections', new.public_id;
|
||||
end if;
|
||||
insert into session_state (session_id, state)
|
||||
values
|
||||
(new.public_id, 'terminated');
|
||||
end if;
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
|
||||
|
||||
create trigger
|
||||
update_session_state_on_termination_reason
|
||||
after update of termination_reason on session
|
||||
for each row execute procedure update_session_state_on_termination_reason();
|
||||
|
||||
|
||||
create table session_state_enm (
|
||||
name text primary key
|
||||
check (
|
||||
name in ('pending', 'connected', 'canceling', 'closed')
|
||||
name in ('pending', 'active', 'canceling', 'terminated')
|
||||
)
|
||||
);
|
||||
|
||||
insert into session_state_enm (name)
|
||||
values
|
||||
('pending'),
|
||||
('connected'),
|
||||
('active'),
|
||||
('canceling'),
|
||||
('closed');
|
||||
('terminated');
|
||||
|
||||
/*
|
||||
|
||||
|
||||
┌────────────────┐
|
||||
start │ │
|
||||
. │ Canceling │
|
||||
@@ -193,7 +278,7 @@ begin;
|
||||
▼ │ ▼
|
||||
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
|
||||
│ │ │ │ │ │
|
||||
│ Pending │ │ Connected │ │ Closed │
|
||||
│ Pending │ │ Active │ │ Terminated │
|
||||
│ │──────────▶│ │──────────▶│ │
|
||||
│ │ │ │ │ │
|
||||
└────────────────┘ └────────────────┘ └────────────────┘
|
||||
@@ -234,6 +319,12 @@ begin;
|
||||
);
|
||||
|
||||
|
||||
create trigger
|
||||
immutable_columns
|
||||
before
|
||||
update on session_state
|
||||
for each row execute procedure immutable_columns('session_id', 'start_time', 'previous_end_time');
|
||||
|
||||
create or replace function
|
||||
insert_session_state()
|
||||
returns trigger
|
||||
@@ -264,4 +355,5 @@ begin;
|
||||
create trigger insert_session_state before insert on session_state
|
||||
for each row execute procedure insert_session_state();
|
||||
|
||||
|
||||
commit;
|
||||
@@ -0,0 +1,10 @@
|
||||
begin;
|
||||
|
||||
drop table session_connection_state;
|
||||
drop table session_connection_state_enm;
|
||||
drop table session_connection;
|
||||
drop table session_connection_closed_reason_enm;
|
||||
drop function insert_session_connection_state;
|
||||
drop function insert_new_connection_state;
|
||||
drop function update_connection_state_on_closed_reason;
|
||||
commit;
|
||||
@@ -0,0 +1,271 @@
|
||||
begin;
|
||||
|
||||
/*
|
||||
|
||||
┌────────────────┐
|
||||
│ session │
|
||||
├────────────────┤
|
||||
│ public_id (pk) │
|
||||
│ │
|
||||
│ │
|
||||
└────────────────┘
|
||||
▲fk1 ┼
|
||||
┼
|
||||
│
|
||||
│
|
||||
┼
|
||||
╱│╲ ┌──────────────────────────────────────┐
|
||||
┌───────────────────────┐ │ session_connection_closed_reason_enm │
|
||||
│ session_connection │╲ fk2▶ ├──────────────────────────────────────┤
|
||||
├───────────────────────┤─○───○┼│ name │
|
||||
│ public_id (pk) │╱ └──────────────────────────────────────┘
|
||||
│ session_id (fk1) │
|
||||
│ closed_reason (fk2) │
|
||||
└───────────────────────┘
|
||||
▲fk1 ┼
|
||||
┼
|
||||
│
|
||||
│
|
||||
┼
|
||||
╱│╲
|
||||
┌──────────────────────────────────────────┐
|
||||
│ session_connection_state │
|
||||
├──────────────────────────────────────────┤┼○┐
|
||||
│ connection_id (pk,fk1,fk2,unq1,unq2) │ │◀fk2
|
||||
│ state (fk3) │ │
|
||||
│ previous_end_time (fk2,unq1) │┼○┘
|
||||
│ start_time (pk) │
|
||||
│ end_time (unq2) │
|
||||
└──────────────────────────────────────────┘
|
||||
╲│╱
|
||||
○
|
||||
│
|
||||
│
|
||||
┼
|
||||
▼fk3 ┼
|
||||
┌───────────────────────────────┐
|
||||
│ session_connection_state_enm │
|
||||
├───────────────────────────────┤
|
||||
│ name │
|
||||
└───────────────────────────────┘
|
||||
|
||||
*/
|
||||
|
||||
create table session_connection_closed_reason_enm (
|
||||
name text primary key
|
||||
check (
|
||||
name in (
|
||||
'unknown',
|
||||
'timed out',
|
||||
'closed by end-user',
|
||||
'cancelled',
|
||||
'network error',
|
||||
'system error'
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
insert into session_connection_closed_reason_enm (name)
|
||||
values
|
||||
('unknown'),
|
||||
('timed out'),
|
||||
('closed by end-user'),
|
||||
('cancelled'),
|
||||
('network error'),
|
||||
('system error');
|
||||
|
||||
-- A session connection is one connection proxied by a worker from a client to
|
||||
-- a backend for a session. The client initiates the connection to the worker
|
||||
-- and the worker initiates the connection to the backend.
|
||||
-- A session can have zero or more session connections.
|
||||
create table session_connection (
|
||||
public_id wt_public_id primary key,
|
||||
session_id wt_public_id not null
|
||||
references session (public_id)
|
||||
on delete cascade
|
||||
on update cascade,
|
||||
-- the client_tcp_address is the network address of the client which initiated
|
||||
-- the connection to a worker
|
||||
client_tcp_address inet not null,
|
||||
-- the client_tcp_port is the network port at the address of the client the
|
||||
-- worker proxied a connection for the user
|
||||
client_tcp_port integer not null
|
||||
check(
|
||||
client_tcp_port > 0
|
||||
and
|
||||
client_tcp_port <= 65535
|
||||
),
|
||||
-- the backend_tcp_address is the network address of the backend which the
|
||||
-- worker initiated the connection to, for the user
|
||||
backend_tcp_address inet not null,
|
||||
-- the backend_tcp_port is the network port at the address of the backend the
|
||||
-- worker proxied a connection to, for the user
|
||||
backend_tcp_port integer not null
|
||||
check(
|
||||
backend_tcp_port > 0
|
||||
and
|
||||
backend_tcp_port <= 65535
|
||||
),
|
||||
-- the total number of bytes received by the worker from the client and sent
|
||||
-- to the backend for this connection
|
||||
bytes_up bigint -- can be null
|
||||
check (
|
||||
bytes_up is null
|
||||
or
|
||||
bytes_up >= 0
|
||||
),
|
||||
-- the total number of bytes received by the worker from the backend and sent
|
||||
-- to the client for this connection
|
||||
bytes_down bigint -- can be null
|
||||
check (
|
||||
bytes_down is null
|
||||
or
|
||||
bytes_down >= 0
|
||||
),
|
||||
closed_reason text
|
||||
references session_connection_closed_reason_enm (name)
|
||||
on delete restrict
|
||||
on update cascade,
|
||||
version wt_version,
|
||||
create_time wt_timestamp,
|
||||
update_time wt_timestamp
|
||||
);
|
||||
|
||||
create trigger
|
||||
immutable_columns
|
||||
before
|
||||
update on session_connection
|
||||
for each row execute procedure immutable_columns('public_id', 'session_id', 'client_tcp_address', 'client_tcp_port', 'backend_tcp_address', 'backend_tcp_port', 'create_time');
|
||||
|
||||
create trigger
|
||||
update_version_column
|
||||
after update on session_connection
|
||||
for each row execute procedure update_version_column();
|
||||
|
||||
create trigger
|
||||
update_time_column
|
||||
before update on session_connection
|
||||
for each row execute procedure update_time_column();
|
||||
|
||||
create trigger
|
||||
default_create_time_column
|
||||
before
|
||||
insert on session_connection
|
||||
for each row execute procedure default_create_time();
|
||||
|
||||
-- insert_new_connection_state() is used in an after insert trigger on the
|
||||
-- session_connection table. it will insert a state of "connected" in
|
||||
-- session_connection_state for the new session connection.
|
||||
create or replace function
|
||||
insert_new_connection_state()
|
||||
returns trigger
|
||||
as $$
|
||||
begin
|
||||
insert into session_connection_state (connection_id, state)
|
||||
values
|
||||
(new.public_id, 'connected');
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
|
||||
create trigger
|
||||
insert_new_connection_state
|
||||
after insert on session_connection
|
||||
for each row execute procedure insert_new_connection_state();
|
||||
|
||||
-- update_connection_state_on_closed_reason() is used in an update insert trigger on the
|
||||
-- session_connection table. it will insert a state of "closed" in
|
||||
-- session_connection_state for the closed session connection.
|
||||
create or replace function
|
||||
update_connection_state_on_closed_reason()
|
||||
returns trigger
|
||||
as $$
|
||||
begin
|
||||
if new.closed_reason is not null then
|
||||
insert into session_connection_state (connection_id, state)
|
||||
values
|
||||
(new.public_id, 'closed');
|
||||
end if;
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
|
||||
create trigger
|
||||
update_connection_state_on_closed_reason
|
||||
after update of closed_reason on session_connection
|
||||
for each row execute procedure update_connection_state_on_closed_reason();
|
||||
|
||||
create table session_connection_state_enm (
|
||||
name text primary key
|
||||
check (
|
||||
name in ('connected', 'closed')
|
||||
)
|
||||
);
|
||||
|
||||
insert into session_connection_state_enm (name)
|
||||
values
|
||||
('connected'),
|
||||
('closed');
|
||||
|
||||
|
||||
create table session_connection_state (
|
||||
connection_id wt_public_id not null
|
||||
references session_connection (public_id)
|
||||
on delete cascade
|
||||
on update cascade,
|
||||
state text not null
|
||||
references session_connection_state_enm(name)
|
||||
on delete restrict
|
||||
on update cascade,
|
||||
previous_end_time timestamp with time zone, -- fk2 -- null means first state
|
||||
start_time timestamp with time zone default current_timestamp not null,
|
||||
constraint previous_end_time_and_start_time_in_sequence
|
||||
check (previous_end_time <= start_time),
|
||||
end_time timestamp with time zone, -- null means unfinished current state
|
||||
constraint start_and_end_times_in_sequence
|
||||
check (start_time <= end_time),
|
||||
constraint end_times_in_sequence
|
||||
check (previous_end_time <> end_time),
|
||||
primary key (connection_id, start_time),
|
||||
unique (connection_id, previous_end_time), -- null means first state
|
||||
unique (connection_id, end_time), -- one null current state
|
||||
foreign key (connection_id, previous_end_time) -- self-reference
|
||||
references session_connection_state (connection_id, end_time)
|
||||
);
|
||||
|
||||
create trigger
|
||||
immutable_columns
|
||||
before
|
||||
update on session_connection_state
|
||||
for each row execute procedure immutable_columns('connection_id', 'state', 'start_time');
|
||||
|
||||
create or replace function
|
||||
insert_session_connection_state()
|
||||
returns trigger
|
||||
as $$
|
||||
begin
|
||||
|
||||
update session_connection_state
|
||||
set end_time = now()
|
||||
where connection_id = new.connection_id
|
||||
and end_time is null;
|
||||
|
||||
if not found then
|
||||
new.previous_end_time = null;
|
||||
new.start_time = now();
|
||||
new.end_time = null;
|
||||
return new;
|
||||
end if;
|
||||
|
||||
new.previous_end_time = now();
|
||||
new.start_time = now();
|
||||
new.end_time = null;
|
||||
return new;
|
||||
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
|
||||
create trigger insert_session_connection_state before insert on session_connection_state
|
||||
for each row execute procedure insert_session_connection_state();
|
||||
|
||||
commit;
|
||||
@@ -103,6 +103,11 @@ type Writer interface {
|
||||
// DB returns the sql.DB
|
||||
DB() (*sql.DB, error)
|
||||
|
||||
// Exec will execute the sql with the values as parameters. The int returned
|
||||
// is the number of rows affected by the sql. No options are currently
|
||||
// supported.
|
||||
Exec(sql string, values []interface{}, opt ...Option) (int, error)
|
||||
|
||||
// GetTicket returns an oplog ticket for the aggregate root of "i" which can
|
||||
// be used to WriteOplogEntryWith for that aggregate root.
|
||||
GetTicket(i interface{}) (*store.Ticket, error)
|
||||
@@ -187,6 +192,20 @@ func (rw *Db) DB() (*sql.DB, error) {
|
||||
return rw.underlying.DB(), nil
|
||||
}
|
||||
|
||||
// Exec will execute the sql with the values as parameters. The int returned
|
||||
// is the number of rows affected by the sql. No options are currently
|
||||
// supported.
|
||||
func (rw *Db) Exec(sql string, values []interface{}, opt ...Option) (int, error) {
|
||||
if sql == "" {
|
||||
return NoRowsAffected, fmt.Errorf("missing sql: %w", ErrInvalidParameter)
|
||||
}
|
||||
gormDb := rw.underlying.Exec(sql, values...)
|
||||
if gormDb.Error != nil {
|
||||
return NoRowsAffected, fmt.Errorf("exec: failed: %w", gormDb.Error)
|
||||
}
|
||||
return int(gormDb.RowsAffected), nil
|
||||
}
|
||||
|
||||
// Scan rows will scan the rows into the interface
|
||||
func (rw *Db) ScanRows(rows *sql.Rows, result interface{}) error {
|
||||
if rw.underlying == nil {
|
||||
|
||||
@@ -1091,6 +1091,24 @@ func TestDb_DB(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestDb_Exec(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("update", func(t *testing.T) {
|
||||
db, _ := TestSetup(t, "postgres")
|
||||
require := require.New(t)
|
||||
w := Db{underlying: db}
|
||||
id := testId(t)
|
||||
user, err := db_test.NewTestUser()
|
||||
require.NoError(err)
|
||||
user.Name = "foo-" + id
|
||||
err = w.Create(context.Background(), user)
|
||||
require.NoError(err)
|
||||
require.NotEmpty(user.Id)
|
||||
rowsAffected, err := w.Exec("update db_test_user set name = ? where public_id = ?", []interface{}{"updated-" + id, user.PublicId})
|
||||
require.NoError(err)
|
||||
require.Equal(1, rowsAffected)
|
||||
})
|
||||
}
|
||||
func TestDb_DoTx(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, _ := TestSetup(t, "postgres")
|
||||
|
||||
@@ -2,13 +2,9 @@ package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
mathrand "math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -28,7 +24,7 @@ import (
|
||||
"github.com/hashicorp/boundary/internal/servers/controller/handlers/authmethods"
|
||||
"github.com/hashicorp/boundary/internal/servers/controller/handlers/host_sets"
|
||||
"github.com/hashicorp/boundary/internal/servers/controller/handlers/targets"
|
||||
"github.com/hashicorp/boundary/internal/sessions"
|
||||
"github.com/hashicorp/boundary/internal/session"
|
||||
"github.com/hashicorp/boundary/internal/types/scope"
|
||||
"github.com/hashicorp/boundary/sdk/strutil"
|
||||
"github.com/hashicorp/shared-secure-libs/configutil"
|
||||
@@ -369,24 +365,9 @@ func jobTestingHandler(c *Controller) http.Handler {
|
||||
errorResp(err)
|
||||
return
|
||||
}
|
||||
jobId = "s_" + jobId
|
||||
pubKey, privKey, err := sessions.DeriveED25519Key(wrapper, "u_1234567890", jobId)
|
||||
|
||||
template := &x509.Certificate{
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{
|
||||
x509.ExtKeyUsageServerAuth,
|
||||
x509.ExtKeyUsageClientAuth,
|
||||
},
|
||||
DNSNames: []string{jobId},
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement | x509.KeyUsageCertSign,
|
||||
SerialNumber: big.NewInt(mathrand.Int63()),
|
||||
NotBefore: time.Now().Add(-1 * time.Minute),
|
||||
NotAfter: time.Now().Add(5 * time.Minute),
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
}
|
||||
|
||||
certBytes, err := x509.CreateCertificate(rand.Reader, template, template, pubKey, privKey)
|
||||
// TODO (jimlambrt 8/2020): this is quite correct. We need to create a
|
||||
// new session here (in the session repo) which would have a cert.
|
||||
privKey, certBytes, err := session.TestCert(wrapper, "u_1234567890", jobId)
|
||||
if err != nil {
|
||||
errorResp(err)
|
||||
return
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/hashicorp/boundary/internal/servers/controller/common"
|
||||
"github.com/hashicorp/boundary/internal/sessions"
|
||||
"github.com/hashicorp/boundary/internal/session"
|
||||
"github.com/hashicorp/boundary/internal/types/resource"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"google.golang.org/grpc/codes"
|
||||
@@ -89,7 +89,7 @@ func (ws *workerServiceServer) GetSession(ctx context.Context, req *pbs.GetSessi
|
||||
|
||||
// Derive the private key, which should match. Deriving on both ends allows
|
||||
// us to not store it in the DB.
|
||||
_, privKey, err := sessions.DeriveED25519Key(wrapper, sessionInfo.GetUserId(), req.GetId())
|
||||
_, privKey, err := session.DeriveED25519Key(wrapper, sessionInfo.GetUserId(), req.GetId())
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "Error deriving session key: %v", err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/db/timestamp"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultConnectionTableName = "session_connection"
|
||||
)
|
||||
|
||||
// Connection contains information about session's connection to a target
|
||||
type Connection struct {
|
||||
// PublicId is used to access the connection via an API
|
||||
PublicId string `json:"public_id,omitempty" gorm:"primary_key"`
|
||||
// SessionId of the connection
|
||||
SessionId string `json:"session_id,omitempty" gorm:"default:null"`
|
||||
// ClientTcpAddress of the connection
|
||||
ClientTcpAddress string `json:"client_tcp_address,omitempty" gorm:"default:null"`
|
||||
// ClientTcpPort of the connection
|
||||
ClientTcpPort uint32 `json:"client_tcp_port,omitempty" gorm:"default:null"`
|
||||
// BackendTcpAddress of the connection
|
||||
BackendTcpAddress string `json:"backend_tcp_address,omitempty" gorm:"default:null"`
|
||||
// BackendTcpPort of the connection
|
||||
BackendTcpPort uint32 `json:"backend_tcp_port,omitempty" gorm:"default:null"`
|
||||
// BytesUp of the connection
|
||||
BytesUp uint64 `json:"bytes_up,omitempty" gorm:"default:null"`
|
||||
// BytesDown of the connection
|
||||
BytesDown uint64 `json:"bytes_down,omitempty" gorm:"default:null"`
|
||||
// ClosedReason of the conneciont
|
||||
ClosedReason string `json:"closed_reason,omitempty" gorm:"default:null"`
|
||||
// CreateTime from the RDBMS
|
||||
CreateTime *timestamp.Timestamp `json:"create_time,omitempty" gorm:"default:current_timestamp"`
|
||||
// UpdateTime from the RDBMS
|
||||
UpdateTime *timestamp.Timestamp `json:"update_time,omitempty" gorm:"default:current_timestamp"`
|
||||
// Version of the connection
|
||||
Version uint32 `json:"version,omitempty" gorm:"default:null"`
|
||||
|
||||
tableName string `gorm:"-"`
|
||||
}
|
||||
|
||||
func (c *Connection) GetPublicId() string {
|
||||
return c.PublicId
|
||||
}
|
||||
|
||||
var _ Cloneable = (*Connection)(nil)
|
||||
var _ db.VetForWriter = (*Connection)(nil)
|
||||
|
||||
// New creates a new in memory session. No options
|
||||
// are currently supported.
|
||||
func NewConnection(sessionID, clientTcpAddress string, clientTcpPort uint32, backendTcpAddr string, backendTcpPort uint32, opt ...Option) (*Connection, error) {
|
||||
c := Connection{
|
||||
SessionId: sessionID,
|
||||
ClientTcpAddress: clientTcpAddress,
|
||||
ClientTcpPort: clientTcpPort,
|
||||
BackendTcpAddress: backendTcpAddr,
|
||||
BackendTcpPort: backendTcpPort,
|
||||
}
|
||||
if err := c.validateNewConnection("new connection:"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
// AllocConnection will allocate a Session
|
||||
func AllocConnection() Connection {
|
||||
return Connection{}
|
||||
}
|
||||
|
||||
// Clone creates a clone of the Session
|
||||
func (c *Connection) Clone() interface{} {
|
||||
clone := &Connection{
|
||||
PublicId: c.PublicId,
|
||||
SessionId: c.SessionId,
|
||||
ClientTcpAddress: c.ClientTcpAddress,
|
||||
ClientTcpPort: c.ClientTcpPort,
|
||||
BackendTcpAddress: c.BackendTcpAddress,
|
||||
BackendTcpPort: c.BackendTcpPort,
|
||||
BytesUp: c.BytesUp,
|
||||
BytesDown: c.BytesDown,
|
||||
ClosedReason: c.ClosedReason,
|
||||
Version: c.Version,
|
||||
}
|
||||
if c.CreateTime != nil {
|
||||
clone.CreateTime = ×tamp.Timestamp{
|
||||
Timestamp: ×tamppb.Timestamp{
|
||||
Seconds: c.CreateTime.Timestamp.Seconds,
|
||||
Nanos: c.CreateTime.Timestamp.Nanos,
|
||||
},
|
||||
}
|
||||
}
|
||||
if c.UpdateTime != nil {
|
||||
clone.UpdateTime = ×tamp.Timestamp{
|
||||
Timestamp: ×tamppb.Timestamp{
|
||||
Seconds: c.UpdateTime.Timestamp.Seconds,
|
||||
Nanos: c.UpdateTime.Timestamp.Nanos,
|
||||
},
|
||||
}
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
// VetForWrite implements db.VetForWrite() interface and validates the connection
|
||||
// before it's written.
|
||||
func (c *Connection) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error {
|
||||
opts := db.GetOpts(opt...)
|
||||
if c.PublicId == "" {
|
||||
return fmt.Errorf("connection vet for write: missing public id: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
switch opType {
|
||||
case db.CreateOp:
|
||||
if err := c.validateNewConnection("connection vet for write:"); err != nil {
|
||||
return err
|
||||
}
|
||||
case db.UpdateOp:
|
||||
switch {
|
||||
case contains(opts.WithFieldMaskPaths, "PublicId"):
|
||||
return fmt.Errorf("connection vet for write: public id is immutable: %w", db.ErrInvalidParameter)
|
||||
case contains(opts.WithFieldMaskPaths, "SessionId"):
|
||||
return fmt.Errorf("connection vet for write: session id is immutable: %w", db.ErrInvalidParameter)
|
||||
case contains(opts.WithFieldMaskPaths, "ClientTcpAddress"):
|
||||
return fmt.Errorf("connection vet for write: client address is immutable: %w", db.ErrInvalidParameter)
|
||||
case contains(opts.WithFieldMaskPaths, "ClientTcpPort"):
|
||||
return fmt.Errorf("connection vet for write: client port is immutable: %w", db.ErrInvalidParameter)
|
||||
case contains(opts.WithFieldMaskPaths, "BackendTcpAddress"):
|
||||
return fmt.Errorf("connection vet for write: backend address is immutable: %w", db.ErrInvalidParameter)
|
||||
case contains(opts.WithFieldMaskPaths, "BackendTcpPort"):
|
||||
return fmt.Errorf("connection vet for write: backend port is immutable: %w", db.ErrInvalidParameter)
|
||||
case contains(opts.WithFieldMaskPaths, "CreateTime"):
|
||||
return fmt.Errorf("connection vet for write: create time is immutable: %w", db.ErrInvalidParameter)
|
||||
case contains(opts.WithFieldMaskPaths, "UpdateTime"):
|
||||
return fmt.Errorf("connection vet for write: update time is immutable: %w", db.ErrInvalidParameter)
|
||||
case contains(opts.WithFieldMaskPaths, "ClosedReason"):
|
||||
if _, err := convertToClosedReason(c.ClosedReason); err != nil {
|
||||
return fmt.Errorf("connection vet for write: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TableName returns the tablename to override the default gorm table name
|
||||
func (c *Connection) TableName() string {
|
||||
if c.tableName != "" {
|
||||
return c.tableName
|
||||
}
|
||||
return defaultConnectionTableName
|
||||
}
|
||||
|
||||
// SetTableName sets the tablename and satisfies the ReplayableMessage
|
||||
// interface. If the caller attempts to set the name to "" the name will be
|
||||
// reset to the default name.
|
||||
func (c *Connection) SetTableName(n string) {
|
||||
c.tableName = n
|
||||
}
|
||||
|
||||
// validateNewConnection checks everything but the connection's PublicId
|
||||
func (c *Connection) validateNewConnection(errorPrefix string) error {
|
||||
if c.SessionId == "" {
|
||||
return fmt.Errorf("%s missing session id: %w", errorPrefix, db.ErrInvalidParameter)
|
||||
}
|
||||
if c.ClientTcpAddress == "" {
|
||||
return fmt.Errorf("%s missing client address: %w", errorPrefix, db.ErrInvalidParameter)
|
||||
}
|
||||
if c.ClientTcpPort == 0 {
|
||||
return fmt.Errorf("%s missing client port: %w", errorPrefix, db.ErrInvalidParameter)
|
||||
}
|
||||
if c.BackendTcpAddress == "" {
|
||||
return fmt.Errorf("%s missing backend address: %w", errorPrefix, db.ErrInvalidParameter)
|
||||
}
|
||||
if c.BackendTcpPort == 0 {
|
||||
return fmt.Errorf("%s missing backend port: %w", errorPrefix, db.ErrInvalidParameter)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
)
|
||||
|
||||
// ClosedReason of the connection
|
||||
type ClosedReason string
|
||||
|
||||
const (
|
||||
UnknownReason ClosedReason = "unknown"
|
||||
ConnectionTimedOut ClosedReason = "timed out"
|
||||
ConnectionClosedByUser ClosedReason = "closed by end-user"
|
||||
ConnectionCancelled ClosedReason = "cancelled"
|
||||
ConnectionNetworkError ClosedReason = "network error"
|
||||
ConnectionSystemError ClosedReason = "system error"
|
||||
)
|
||||
|
||||
// String representation of the termination reason
|
||||
func (r ClosedReason) String() string {
|
||||
return string(r)
|
||||
}
|
||||
func convertToClosedReason(s string) (ClosedReason, error) {
|
||||
switch s {
|
||||
case UnknownReason.String():
|
||||
return UnknownReason, nil
|
||||
case ConnectionTimedOut.String():
|
||||
return ConnectionTimedOut, nil
|
||||
case ConnectionClosedByUser.String():
|
||||
return ConnectionClosedByUser, nil
|
||||
case ConnectionCancelled.String():
|
||||
return ConnectionCancelled, nil
|
||||
case ConnectionNetworkError.String():
|
||||
return ConnectionNetworkError, nil
|
||||
case ConnectionSystemError.String():
|
||||
return ConnectionSystemError, nil
|
||||
default:
|
||||
return "", fmt.Errorf("closed reason: %s is not a valid reason: %w", s, db.ErrInvalidParameter)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/db/timestamp"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultConnectionStateTableName = "session_connection_state"
|
||||
)
|
||||
|
||||
// ConnectionStatus of the connection's state
|
||||
type ConnectionStatus string
|
||||
|
||||
const (
|
||||
StatusConnected ConnectionStatus = "connected"
|
||||
StatusClosed ConnectionStatus = "closed"
|
||||
)
|
||||
|
||||
// String representation of the state's status
|
||||
func (s ConnectionStatus) String() string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
// ConnectionState of the state of the connection
|
||||
type ConnectionState struct {
|
||||
// ConnectionId is used to access the state via an API
|
||||
ConnectionId string `json:"public_id,omitempty" gorm:"primary_key"`
|
||||
// status of the connection
|
||||
Status string `protobuf:"bytes,20,opt,name=status,proto3" json:"status,omitempty" gorm:"column:state"`
|
||||
// PreviousEndTime from the RDBMS
|
||||
PreviousEndTime *timestamp.Timestamp `json:"previous_end_time,omitempty" gorm:"default:current_timestamp"`
|
||||
// StartTime from the RDBMS
|
||||
StartTime *timestamp.Timestamp `json:"start_time,omitempty" gorm:"default:current_timestamp;primary_key"`
|
||||
// EndTime from the RDBMS
|
||||
EndTime *timestamp.Timestamp `json:"end_time,omitempty" gorm:"default:current_timestamp"`
|
||||
|
||||
tableName string `gorm:"-"`
|
||||
}
|
||||
|
||||
var _ Cloneable = (*ConnectionState)(nil)
|
||||
var _ db.VetForWriter = (*ConnectionState)(nil)
|
||||
|
||||
// NewConnectionState creates a new in memory connection state. No options
|
||||
// are currently supported.
|
||||
func NewConnectionState(connectionId string, state ConnectionStatus, opt ...Option) (*ConnectionState, error) {
|
||||
s := ConnectionState{
|
||||
ConnectionId: connectionId,
|
||||
Status: state.String(),
|
||||
}
|
||||
if err := s.validate("new connection state:"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
// allocConnectionState will allocate a connection State
|
||||
func allocConnectionState() ConnectionState {
|
||||
return ConnectionState{}
|
||||
}
|
||||
|
||||
// Clone creates a clone of the State
|
||||
func (s *ConnectionState) Clone() interface{} {
|
||||
clone := &ConnectionState{
|
||||
ConnectionId: s.ConnectionId,
|
||||
Status: s.Status,
|
||||
}
|
||||
if s.PreviousEndTime != nil {
|
||||
clone.PreviousEndTime = ×tamp.Timestamp{
|
||||
Timestamp: ×tamppb.Timestamp{
|
||||
Seconds: s.PreviousEndTime.Timestamp.Seconds,
|
||||
Nanos: s.PreviousEndTime.Timestamp.Nanos,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if s.StartTime != nil {
|
||||
clone.StartTime = ×tamp.Timestamp{
|
||||
Timestamp: ×tamppb.Timestamp{
|
||||
Seconds: s.StartTime.Timestamp.Seconds,
|
||||
Nanos: s.StartTime.Timestamp.Nanos,
|
||||
},
|
||||
}
|
||||
}
|
||||
if s.EndTime != nil {
|
||||
clone.EndTime = ×tamp.Timestamp{
|
||||
Timestamp: ×tamppb.Timestamp{
|
||||
Seconds: s.EndTime.Timestamp.Seconds,
|
||||
Nanos: s.EndTime.Timestamp.Nanos,
|
||||
},
|
||||
}
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
// VetForWrite implements db.VetForWrite() interface and validates the state
|
||||
// before it's written.
|
||||
func (s *ConnectionState) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error {
|
||||
if err := s.validate("connection state vet for write:"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TableName returns the tablename to override the default gorm table name
|
||||
func (s *ConnectionState) TableName() string {
|
||||
if s.tableName != "" {
|
||||
return s.tableName
|
||||
}
|
||||
return defaultConnectionStateTableName
|
||||
}
|
||||
|
||||
// SetTableName sets the tablename and satisfies the ReplayableMessage
|
||||
// interface. If the caller attempts to set the name to "" the name will be
|
||||
// reset to the default name.
|
||||
func (s *ConnectionState) SetTableName(n string) {
|
||||
s.tableName = n
|
||||
}
|
||||
|
||||
// validate checks the session state
|
||||
func (s *ConnectionState) validate(errorPrefix string) error {
|
||||
if s.Status == "" {
|
||||
return fmt.Errorf("%s missing status: %w", errorPrefix, db.ErrInvalidParameter)
|
||||
}
|
||||
if s.ConnectionId == "" {
|
||||
return fmt.Errorf("%s missing connection id: %w", errorPrefix, db.ErrInvalidParameter)
|
||||
}
|
||||
if s.StartTime != nil {
|
||||
return fmt.Errorf("%s start time is not settable: %w", errorPrefix, db.ErrInvalidParameter)
|
||||
}
|
||||
if s.EndTime != nil {
|
||||
return fmt.Errorf("%s end time is not settable: %w", errorPrefix, db.ErrInvalidParameter)
|
||||
}
|
||||
if s.PreviousEndTime != nil {
|
||||
return fmt.Errorf("%s previous end time is not settable: %w", errorPrefix, db.ErrInvalidParameter)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/iam"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConnectionState_Create(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
wrapper := db.TestWrapper(t)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
session := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
connection := TestConnection(t, conn, session.PublicId, "127.0.0.1", 443, "127.0.0.1", 4443)
|
||||
|
||||
type args struct {
|
||||
connectionId string
|
||||
status ConnectionStatus
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *ConnectionState
|
||||
wantErr bool
|
||||
wantIsErr error
|
||||
create bool
|
||||
wantCreateErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
args: args{
|
||||
connectionId: connection.PublicId,
|
||||
status: StatusClosed,
|
||||
},
|
||||
want: &ConnectionState{
|
||||
ConnectionId: connection.PublicId,
|
||||
Status: StatusClosed.String(),
|
||||
},
|
||||
create: true,
|
||||
},
|
||||
{
|
||||
name: "empty-connectionId",
|
||||
args: args{
|
||||
status: StatusClosed,
|
||||
},
|
||||
wantErr: true,
|
||||
wantIsErr: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "empty-status",
|
||||
args: args{
|
||||
connectionId: connection.PublicId,
|
||||
},
|
||||
wantErr: true,
|
||||
wantIsErr: db.ErrInvalidParameter,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := NewConnectionState(tt.args.connectionId, tt.args.status)
|
||||
if tt.wantErr {
|
||||
require.Error(err)
|
||||
assert.True(errors.Is(err, tt.wantIsErr))
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tt.want, got)
|
||||
if tt.create {
|
||||
err = db.New(conn).Create(context.Background(), got)
|
||||
if tt.wantCreateErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
} else {
|
||||
assert.NoError(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectionState_Delete(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
s := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
c := TestConnection(t, conn, s.PublicId, "127.0.0.1", 22, "127.0.0.1", 2222)
|
||||
c2 := TestConnection(t, conn, s.PublicId, "127.0.0.1", 22, "127.0.0.1", 2222)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
state *ConnectionState
|
||||
deleteConnectionStateId string
|
||||
wantRowsDeleted int
|
||||
wantErr bool
|
||||
wantErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
state: TestConnectionState(t, conn, c.PublicId, StatusClosed),
|
||||
wantErr: false,
|
||||
wantRowsDeleted: 1,
|
||||
},
|
||||
{
|
||||
name: "bad-id",
|
||||
state: TestConnectionState(t, conn, c2.PublicId, StatusClosed),
|
||||
deleteConnectionStateId: func() string {
|
||||
id, err := db.NewPublicId(ConnectionStatePrefix)
|
||||
require.NoError(t, err)
|
||||
return id
|
||||
}(),
|
||||
wantErr: false,
|
||||
wantRowsDeleted: 0,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
var initialState ConnectionState
|
||||
err := rw.LookupWhere(context.Background(), &initialState, "connection_id = ? and state = ?", tt.state.ConnectionId, tt.state.Status)
|
||||
require.NoError(err)
|
||||
|
||||
deleteState := allocConnectionState()
|
||||
if tt.deleteConnectionStateId != "" {
|
||||
deleteState.ConnectionId = tt.deleteConnectionStateId
|
||||
} else {
|
||||
deleteState.ConnectionId = tt.state.ConnectionId
|
||||
}
|
||||
deleteState.StartTime = initialState.StartTime
|
||||
deletedRows, err := rw.Delete(context.Background(), &deleteState)
|
||||
if tt.wantErr {
|
||||
require.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
if tt.wantRowsDeleted == 0 {
|
||||
assert.Equal(tt.wantRowsDeleted, deletedRows)
|
||||
return
|
||||
}
|
||||
assert.Equal(tt.wantRowsDeleted, deletedRows)
|
||||
foundState := allocConnectionState()
|
||||
err = rw.LookupWhere(context.Background(), &foundState, "connection_id = ? and start_time = ?", tt.state.ConnectionId, initialState.StartTime)
|
||||
require.Error(err)
|
||||
assert.True(errors.Is(db.ErrRecordNotFound, err))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectionState_Clone(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
wrapper := db.TestWrapper(t)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
s := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
c := TestConnection(t, conn, s.PublicId, "127.0.0.1", 22, "127.0.0.1", 2222)
|
||||
state := TestConnectionState(t, conn, c.PublicId, StatusConnected)
|
||||
cp := state.Clone()
|
||||
assert.Equal(cp.(*ConnectionState), state)
|
||||
})
|
||||
t.Run("not-equal", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
s := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
c := TestConnection(t, conn, s.PublicId, "127.0.0.1", 22, "127.0.0.1", 2222)
|
||||
state := TestConnectionState(t, conn, c.PublicId, StatusConnected)
|
||||
state2 := TestConnectionState(t, conn, c.PublicId, StatusConnected)
|
||||
|
||||
cp := state.Clone()
|
||||
assert.NotEqual(cp.(*ConnectionState), state2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestConnectionState_SetTableName(t *testing.T) {
|
||||
t.Parallel()
|
||||
defaultTableName := defaultConnectionStateTableName
|
||||
tests := []struct {
|
||||
name string
|
||||
setNameTo string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "new-name",
|
||||
setNameTo: "new-name",
|
||||
want: "new-name",
|
||||
},
|
||||
{
|
||||
name: "reset to default",
|
||||
setNameTo: "",
|
||||
want: defaultTableName,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
def := allocConnectionState()
|
||||
require.Equal(defaultTableName, def.TableName())
|
||||
s := allocConnectionState()
|
||||
s.SetTableName(tt.setNameTo)
|
||||
assert.Equal(tt.want, s.TableName())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/iam"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConnection_Create(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
wrapper := db.TestWrapper(t)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
s := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
|
||||
type args struct {
|
||||
sessionId string
|
||||
clientTcpAddress string
|
||||
clientTcpPort uint32
|
||||
backendTcpAddress string
|
||||
backendTcpPort uint32
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *Connection
|
||||
wantErr bool
|
||||
wantIsErr error
|
||||
create bool
|
||||
wantCreateErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
args: args{
|
||||
sessionId: s.PublicId,
|
||||
clientTcpAddress: "127.0.0.1",
|
||||
clientTcpPort: 22,
|
||||
backendTcpAddress: "127.0.0.1",
|
||||
backendTcpPort: 2222,
|
||||
},
|
||||
want: &Connection{
|
||||
SessionId: s.PublicId,
|
||||
ClientTcpAddress: "127.0.0.1",
|
||||
ClientTcpPort: 22,
|
||||
BackendTcpAddress: "127.0.0.1",
|
||||
BackendTcpPort: 2222,
|
||||
},
|
||||
create: true,
|
||||
},
|
||||
{
|
||||
name: "empty-session-id",
|
||||
args: args{
|
||||
clientTcpAddress: "127.0.0.1",
|
||||
clientTcpPort: 22,
|
||||
backendTcpAddress: "127.0.0.1",
|
||||
backendTcpPort: 2222,
|
||||
},
|
||||
wantErr: true,
|
||||
wantIsErr: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "empty-client-address",
|
||||
args: args{
|
||||
sessionId: s.PublicId,
|
||||
clientTcpPort: 22,
|
||||
backendTcpAddress: "127.0.0.1",
|
||||
backendTcpPort: 2222,
|
||||
},
|
||||
wantErr: true,
|
||||
wantIsErr: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "empty-client-port",
|
||||
args: args{
|
||||
sessionId: s.PublicId,
|
||||
clientTcpAddress: "localhost",
|
||||
backendTcpAddress: "127.0.0.1",
|
||||
backendTcpPort: 2222,
|
||||
},
|
||||
wantErr: true,
|
||||
wantIsErr: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "empty-backend-address",
|
||||
args: args{
|
||||
sessionId: s.PublicId,
|
||||
clientTcpAddress: "localhost",
|
||||
clientTcpPort: 22,
|
||||
backendTcpPort: 2222,
|
||||
},
|
||||
wantErr: true,
|
||||
wantIsErr: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "empty-backend-port",
|
||||
args: args{
|
||||
sessionId: s.PublicId,
|
||||
clientTcpAddress: "localhost",
|
||||
clientTcpPort: 22,
|
||||
backendTcpAddress: "127.0.0.1",
|
||||
},
|
||||
wantErr: true,
|
||||
wantIsErr: db.ErrInvalidParameter,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := NewConnection(
|
||||
tt.args.sessionId,
|
||||
tt.args.clientTcpAddress,
|
||||
tt.args.clientTcpPort,
|
||||
tt.args.backendTcpAddress,
|
||||
tt.args.backendTcpPort,
|
||||
)
|
||||
if tt.wantErr {
|
||||
require.Error(err)
|
||||
assert.True(errors.Is(err, tt.wantIsErr))
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tt.want, got)
|
||||
if tt.create {
|
||||
id, err := db.NewPublicId(ConnectionPrefix)
|
||||
require.NoError(err)
|
||||
got.PublicId = id
|
||||
err = db.New(conn).Create(context.Background(), got)
|
||||
if tt.wantCreateErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
} else {
|
||||
assert.NoError(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnection_Delete(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
s := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
connection *Connection
|
||||
wantRowsDeleted int
|
||||
wantErr bool
|
||||
wantErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
connection: TestConnection(t, conn, s.PublicId, "127.0.0.1", 22, "127.0.0.1", 2222),
|
||||
wantErr: false,
|
||||
wantRowsDeleted: 1,
|
||||
},
|
||||
{
|
||||
name: "bad-id",
|
||||
connection: func() *Connection {
|
||||
c := AllocConnection()
|
||||
id, err := db.NewPublicId(ConnectionPrefix)
|
||||
require.NoError(t, err)
|
||||
c.PublicId = id
|
||||
return &c
|
||||
}(),
|
||||
wantErr: false,
|
||||
wantRowsDeleted: 0,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
deleteConnection := AllocConnection()
|
||||
deleteConnection.PublicId = tt.connection.PublicId
|
||||
deletedRows, err := rw.Delete(context.Background(), &deleteConnection)
|
||||
if tt.wantErr {
|
||||
require.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
if tt.wantRowsDeleted == 0 {
|
||||
assert.Equal(tt.wantRowsDeleted, deletedRows)
|
||||
return
|
||||
}
|
||||
assert.Equal(tt.wantRowsDeleted, deletedRows)
|
||||
foundConnection := AllocConnection()
|
||||
foundConnection.PublicId = tt.connection.PublicId
|
||||
err = rw.LookupById(context.Background(), &foundConnection)
|
||||
require.Error(err)
|
||||
assert.True(errors.Is(db.ErrRecordNotFound, err))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnection_Clone(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
wrapper := db.TestWrapper(t)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
s := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
c := TestConnection(t, conn, s.PublicId, "127.0.0.1", 22, "127.0.0.1", 2222)
|
||||
cp := c.Clone()
|
||||
assert.Equal(cp.(*Connection), c)
|
||||
})
|
||||
t.Run("not-equal", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
s := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
c := TestConnection(t, conn, s.PublicId, "127.0.0.1", 22, "127.0.0.1", 2222)
|
||||
c2 := TestConnection(t, conn, s.PublicId, "127.0.0.1", 80, "127.0.0.1", 8080)
|
||||
|
||||
cp := c.Clone()
|
||||
assert.NotEqual(cp.(*Connection), c2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestConnection_SetTableName(t *testing.T) {
|
||||
t.Parallel()
|
||||
defaultTableName := defaultConnectionTableName
|
||||
tests := []struct {
|
||||
name string
|
||||
setNameTo string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "new-name",
|
||||
setNameTo: "new-name",
|
||||
want: "new-name",
|
||||
},
|
||||
{
|
||||
name: "reset to default",
|
||||
setNameTo: "",
|
||||
want: defaultTableName,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
def := AllocConnection()
|
||||
require.Equal(defaultTableName, def.TableName())
|
||||
c := AllocConnection()
|
||||
c.SetTableName(tt.setNameTo)
|
||||
assert.Equal(tt.want, c.TableName())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
)
|
||||
|
||||
const (
|
||||
// SessionPrefix for session PK ids
|
||||
SessionPrefix = "s"
|
||||
|
||||
// StatePrefix for state PK ids
|
||||
StatePrefix = "ss"
|
||||
|
||||
// ConnectionPrefix for connection PK ids
|
||||
ConnectionPrefix = "sc"
|
||||
|
||||
// ConnectionStatePrefix for connection state PK ids
|
||||
ConnectionStatePrefix = "scs"
|
||||
)
|
||||
|
||||
func newId() (string, error) {
|
||||
id, err := db.NewPublicId(SessionPrefix)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("new session id: %w", err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func newStateId() (string, error) {
|
||||
id, err := db.NewPublicId(StatePrefix)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("new session state id: %w", err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func newConnectionId() (string, error) {
|
||||
id, err := db.NewPublicId(ConnectionPrefix)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("new session connection id: %w", err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func newConnectionStateId() (string, error) {
|
||||
id, err := db.NewPublicId(ConnectionStatePrefix)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("new session connection state id: %w", err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_Ids(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("s", func(t *testing.T) {
|
||||
id, err := newId()
|
||||
require.NoError(t, err)
|
||||
assert.True(t, strings.HasPrefix(id, SessionPrefix+"_"))
|
||||
})
|
||||
t.Run("ss", func(t *testing.T) {
|
||||
id, err := newStateId()
|
||||
require.NoError(t, err)
|
||||
assert.True(t, strings.HasPrefix(id, StatePrefix+"_"))
|
||||
})
|
||||
t.Run("sc", func(t *testing.T) {
|
||||
id, err := newConnectionId()
|
||||
require.NoError(t, err)
|
||||
assert.True(t, strings.HasPrefix(id, ConnectionPrefix+"_"))
|
||||
})
|
||||
t.Run("scs", func(t *testing.T) {
|
||||
id, err := newConnectionStateId()
|
||||
require.NoError(t, err)
|
||||
assert.True(t, strings.HasPrefix(id, ConnectionStatePrefix+"_"))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/db/timestamp"
|
||||
"github.com/hashicorp/boundary/internal/iam"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
func TestSession_ImmutableFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
wrapper := db.TestWrapper(t)
|
||||
rw := db.New(conn)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
|
||||
ts := timestamp.Timestamp{Timestamp: ×tamppb.Timestamp{Seconds: 0, Nanos: 0}}
|
||||
|
||||
_, _ = iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
|
||||
new := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
|
||||
var tests = []struct {
|
||||
name string
|
||||
update *Session
|
||||
fieldMask []string
|
||||
}{
|
||||
{
|
||||
name: "public_id",
|
||||
update: func() *Session {
|
||||
s := new.Clone().(*Session)
|
||||
s.PublicId = "o_thisIsNotAValidId"
|
||||
return s
|
||||
}(),
|
||||
fieldMask: []string{"PublicId"},
|
||||
},
|
||||
{
|
||||
name: "certificate",
|
||||
update: func() *Session {
|
||||
s := new.Clone().(*Session)
|
||||
s.Certificate = []byte("fake cert for test")
|
||||
return s
|
||||
}(),
|
||||
fieldMask: []string{"Certificate"},
|
||||
},
|
||||
{
|
||||
name: "expiration time",
|
||||
update: func() *Session {
|
||||
s := new.Clone().(*Session)
|
||||
s.ExpirationTime = &ts
|
||||
return s
|
||||
}(),
|
||||
fieldMask: []string{"ExpirationTime"},
|
||||
},
|
||||
{
|
||||
name: "create time",
|
||||
update: func() *Session {
|
||||
s := new.Clone().(*Session)
|
||||
s.CreateTime = &ts
|
||||
return s
|
||||
}(),
|
||||
fieldMask: []string{"CreateTime"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
orig := new.Clone()
|
||||
err := rw.LookupById(context.Background(), orig)
|
||||
require.NoError(err)
|
||||
|
||||
rowsUpdated, err := rw.Update(context.Background(), tt.update, tt.fieldMask, nil, db.WithSkipVetForWrite(true))
|
||||
require.Error(err)
|
||||
assert.Equal(0, rowsUpdated)
|
||||
|
||||
after := new.Clone()
|
||||
err = rw.LookupById(context.Background(), after)
|
||||
require.NoError(err)
|
||||
|
||||
assert.Equal(orig.(*Session), after)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestState_ImmutableFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
|
||||
ts := timestamp.Timestamp{Timestamp: ×tamppb.Timestamp{Seconds: 0, Nanos: 0}}
|
||||
|
||||
_, _ = iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
|
||||
session := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
state := TestState(t, conn, session.PublicId, StatusPending)
|
||||
|
||||
var new State
|
||||
err := rw.LookupWhere(context.Background(), &new, "session_id = ? and state = ?", state.SessionId, state.Status)
|
||||
require.NoError(t, err)
|
||||
|
||||
var tests = []struct {
|
||||
name string
|
||||
update *State
|
||||
fieldMask []string
|
||||
}{
|
||||
{
|
||||
name: "session_id",
|
||||
update: func() *State {
|
||||
s := new.Clone().(*State)
|
||||
s.SessionId = "s_thisIsNotAValidId"
|
||||
return s
|
||||
}(),
|
||||
fieldMask: []string{"PublicId"},
|
||||
},
|
||||
{
|
||||
name: "start time",
|
||||
update: func() *State {
|
||||
s := new.Clone().(*State)
|
||||
s.StartTime = &ts
|
||||
return s
|
||||
}(),
|
||||
fieldMask: []string{"StartTime"},
|
||||
},
|
||||
{
|
||||
name: "previous_end_time",
|
||||
update: func() *State {
|
||||
s := new.Clone().(*State)
|
||||
s.PreviousEndTime = &ts
|
||||
return s
|
||||
}(),
|
||||
fieldMask: []string{"PreviousEndTime"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
orig := new.Clone()
|
||||
err := rw.LookupWhere(context.Background(), orig, "session_id = ? and start_time = ?", new.SessionId, new.StartTime)
|
||||
require.NoError(err)
|
||||
|
||||
rowsUpdated, err := rw.Update(context.Background(), tt.update, tt.fieldMask, nil, db.WithSkipVetForWrite(true))
|
||||
require.Error(err)
|
||||
assert.Equal(0, rowsUpdated)
|
||||
|
||||
after := new.Clone()
|
||||
err = rw.LookupWhere(context.Background(), after, "session_id = ? and start_time = ?", new.SessionId, new.StartTime)
|
||||
require.NoError(err)
|
||||
assert.Equal(orig.(*State), after)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnection_ImmutableFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
wrapper := db.TestWrapper(t)
|
||||
rw := db.New(conn)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
|
||||
ts := timestamp.Timestamp{Timestamp: ×tamppb.Timestamp{Seconds: 0, Nanos: 0}}
|
||||
|
||||
_, _ = iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
|
||||
session := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
new := TestConnection(t, conn, session.PublicId, "127.0.0.1", 22, "127.0.0.1", 2222)
|
||||
|
||||
var tests = []struct {
|
||||
name string
|
||||
update *Connection
|
||||
fieldMask []string
|
||||
}{
|
||||
{
|
||||
name: "public_id",
|
||||
update: func() *Connection {
|
||||
c := new.Clone().(*Connection)
|
||||
c.PublicId = "sc_thisIsNotAValidId"
|
||||
return c
|
||||
}(),
|
||||
fieldMask: []string{"PublicId"},
|
||||
},
|
||||
{
|
||||
name: "session_id",
|
||||
update: func() *Connection {
|
||||
c := new.Clone().(*Connection)
|
||||
c.SessionId = "s_thisIsNotAValidId"
|
||||
return c
|
||||
}(),
|
||||
fieldMask: []string{"SessionId"},
|
||||
},
|
||||
{
|
||||
name: "client_tcp_address",
|
||||
update: func() *Connection {
|
||||
c := new.Clone().(*Connection)
|
||||
c.ClientTcpAddress = "0.0.0.0"
|
||||
return c
|
||||
}(),
|
||||
fieldMask: []string{"ClientTcpAddress"},
|
||||
},
|
||||
{
|
||||
name: "client_tcp_port",
|
||||
update: func() *Connection {
|
||||
c := new.Clone().(*Connection)
|
||||
c.ClientTcpPort = 443
|
||||
return c
|
||||
}(),
|
||||
fieldMask: []string{"ClientTcpPort"},
|
||||
},
|
||||
{
|
||||
name: "backend_tcp_address",
|
||||
update: func() *Connection {
|
||||
c := new.Clone().(*Connection)
|
||||
c.BackendTcpAddress = "0.0.0.0"
|
||||
return c
|
||||
}(),
|
||||
fieldMask: []string{"BackendTcpAddress"},
|
||||
},
|
||||
{
|
||||
name: "backend_tcp_port",
|
||||
update: func() *Connection {
|
||||
c := new.Clone().(*Connection)
|
||||
c.BackendTcpPort = 443
|
||||
return c
|
||||
}(),
|
||||
fieldMask: []string{"BackendTcpPort"},
|
||||
},
|
||||
{
|
||||
name: "create time",
|
||||
update: func() *Connection {
|
||||
s := new.Clone().(*Connection)
|
||||
s.CreateTime = &ts
|
||||
return s
|
||||
}(),
|
||||
fieldMask: []string{"CreateTime"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
orig := new.Clone()
|
||||
err := rw.LookupById(context.Background(), orig)
|
||||
require.NoError(err)
|
||||
|
||||
rowsUpdated, err := rw.Update(context.Background(), tt.update, tt.fieldMask, nil, db.WithSkipVetForWrite(true))
|
||||
require.Error(err)
|
||||
assert.Equal(0, rowsUpdated)
|
||||
|
||||
after := new.Clone()
|
||||
err = rw.LookupById(context.Background(), after)
|
||||
require.NoError(err)
|
||||
|
||||
assert.Equal(orig.(*Connection), after)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectionState_ImmutableFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
|
||||
ts := timestamp.Timestamp{Timestamp: ×tamppb.Timestamp{Seconds: 0, Nanos: 0}}
|
||||
|
||||
_, _ = iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
|
||||
session := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
connection := TestConnection(t, conn, session.PublicId, "127.0.0.1", 22, "127.0.0.1", 2222)
|
||||
state := TestConnectionState(t, conn, connection.PublicId, StatusClosed)
|
||||
|
||||
var new ConnectionState
|
||||
err := rw.LookupWhere(context.Background(), &new, "connection_id = ? and state = ?", state.ConnectionId, state.Status)
|
||||
require.NoError(t, err)
|
||||
|
||||
var tests = []struct {
|
||||
name string
|
||||
update *ConnectionState
|
||||
fieldMask []string
|
||||
}{
|
||||
{
|
||||
name: "session_id",
|
||||
update: func() *ConnectionState {
|
||||
s := new.Clone().(*ConnectionState)
|
||||
s.ConnectionId = "sc_thisIsNotAValidId"
|
||||
return s
|
||||
}(),
|
||||
fieldMask: []string{"PublicId"},
|
||||
},
|
||||
{
|
||||
name: "start time",
|
||||
update: func() *ConnectionState {
|
||||
s := new.Clone().(*ConnectionState)
|
||||
s.StartTime = &ts
|
||||
return s
|
||||
}(),
|
||||
fieldMask: []string{"StartTime"},
|
||||
},
|
||||
{
|
||||
name: "previous_end_time",
|
||||
update: func() *ConnectionState {
|
||||
s := new.Clone().(*ConnectionState)
|
||||
s.PreviousEndTime = &ts
|
||||
return s
|
||||
}(),
|
||||
fieldMask: []string{"PreviousEndTime"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
orig := new.Clone()
|
||||
err := rw.LookupWhere(context.Background(), orig, "connection_id = ? and start_time = ?", new.ConnectionId, new.StartTime)
|
||||
require.NoError(err)
|
||||
|
||||
rowsUpdated, err := rw.Update(context.Background(), tt.update, tt.fieldMask, nil, db.WithSkipVetForWrite(true))
|
||||
require.Error(err)
|
||||
assert.Equal(0, rowsUpdated)
|
||||
|
||||
after := new.Clone()
|
||||
err = rw.LookupWhere(context.Background(), after, "connection_id = ? and start_time = ?", new.ConnectionId, new.StartTime)
|
||||
require.NoError(err)
|
||||
assert.Equal(orig.(*ConnectionState), after)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/boundary/internal/db/timestamp"
|
||||
)
|
||||
|
||||
// getOpts - iterate the inbound Options and return a struct
|
||||
func getOpts(opt ...Option) options {
|
||||
opts := getDefaultOptions()
|
||||
for _, o := range opt {
|
||||
o(&opts)
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
// Option - how Options are passed as arguments
|
||||
type Option func(*options)
|
||||
|
||||
// options = how options are represented
|
||||
type options struct {
|
||||
withLimit int
|
||||
withOrder string
|
||||
withScopeId string
|
||||
withUserId string
|
||||
withExpirationTime *timestamp.Timestamp
|
||||
withTestTofu []byte
|
||||
}
|
||||
|
||||
func getDefaultOptions() options {
|
||||
return options{}
|
||||
}
|
||||
|
||||
// WithLimit provides an option to provide a limit. Intentionally allowing
|
||||
// negative integers. If WithLimit < 0, then unlimited results are returned. If
|
||||
// WithLimit == 0, then default limits are used for results.
|
||||
func WithLimit(limit int) Option {
|
||||
return func(o *options) {
|
||||
o.withLimit = limit
|
||||
}
|
||||
}
|
||||
|
||||
// WithOrder allows specifying an order for returned values
|
||||
func WithOrder(order string) Option {
|
||||
return func(o *options) {
|
||||
o.withOrder = order
|
||||
}
|
||||
}
|
||||
|
||||
// WithScopeId allows specifying a scope ID criteria for the function.
|
||||
func WithScopeId(scopeId string) Option {
|
||||
return func(o *options) {
|
||||
o.withScopeId = scopeId
|
||||
}
|
||||
}
|
||||
|
||||
// WithUserId allows specifying a user ID criteria for the function.
|
||||
func WithUserId(userId string) Option {
|
||||
return func(o *options) {
|
||||
o.withUserId = userId
|
||||
}
|
||||
}
|
||||
|
||||
// WithExpirationTime allows specifying an expiration time for the session
|
||||
func WithExpirationTime(exp *timestamp.Timestamp) Option {
|
||||
return func(o *options) {
|
||||
o.withExpirationTime = exp
|
||||
}
|
||||
}
|
||||
|
||||
// WithTestTofu allows specifying a test tofu for a test session
|
||||
func WithTestTofu(tofu []byte) Option {
|
||||
return func(o *options) {
|
||||
o.withTestTofu = tofu
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
"github.com/hashicorp/boundary/internal/db/timestamp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Test_GetOpts provides unit tests for GetOpts and all the options
|
||||
func Test_GetOpts(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("WithLimit", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// test default of 0
|
||||
opts := getOpts()
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withLimit = 0
|
||||
assert.Equal(opts, testOpts)
|
||||
|
||||
opts = getOpts(WithLimit(-1))
|
||||
testOpts = getDefaultOptions()
|
||||
testOpts.withLimit = -1
|
||||
assert.Equal(opts, testOpts)
|
||||
|
||||
opts = getOpts(WithLimit(1))
|
||||
testOpts = getDefaultOptions()
|
||||
testOpts.withLimit = 1
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithScopeId", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts := getOpts(WithScopeId("o_1234"))
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withScopeId = "o_1234"
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithOrder", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts := getOpts(WithOrder("create_time asc"))
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withOrder = "create_time asc"
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithUserId", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts := getOpts(WithUserId("u_1234"))
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withUserId = "u_1234"
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithExpirationTime", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
now, err := ptypes.TimestampProto(time.Now())
|
||||
require.NoError(err)
|
||||
opts := getOpts(WithExpirationTime(×tamp.Timestamp{Timestamp: now}))
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withExpirationTime = ×tamp.Timestamp{Timestamp: now}
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithTestTofu", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
tofu := TestTofu(t)
|
||||
opts := getOpts(WithTestTofu(tofu))
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withTestTofu = make([]byte, len(tofu))
|
||||
copy(testOpts.withTestTofu, tofu)
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package session
|
||||
|
||||
const (
|
||||
activateStateCte = `
|
||||
insert into session_state
|
||||
with not_active as (
|
||||
select session_id, 'active' as state
|
||||
from
|
||||
session s,
|
||||
session_state ss
|
||||
where
|
||||
s.public_id = ss.session_id and
|
||||
ss.state = 'pending' and
|
||||
ss.session_id = $1 and
|
||||
s.version = $2 and
|
||||
s.public_id not in(select session_id from session_state where session_id = $1 and state = 'active')
|
||||
)
|
||||
select * from not_active;
|
||||
`
|
||||
)
|
||||
@@ -0,0 +1,66 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
)
|
||||
|
||||
// Clonable provides a cloning interface
|
||||
type Cloneable interface {
|
||||
Clone() interface{}
|
||||
}
|
||||
|
||||
// Repository is the session database repository
|
||||
type Repository struct {
|
||||
reader db.Reader
|
||||
writer db.Writer
|
||||
kms *kms.Kms
|
||||
|
||||
// defaultLimit provides a default for limiting the number of results returned from the repo
|
||||
defaultLimit int
|
||||
}
|
||||
|
||||
// NewRepository creates a new session Repository. Supports the options: WithLimit
|
||||
// which sets a default limit on results returned by repo operations.
|
||||
func NewRepository(r db.Reader, w db.Writer, kms *kms.Kms, opt ...Option) (*Repository, error) {
|
||||
if r == nil {
|
||||
return nil, errors.New("error creating db repository with nil reader")
|
||||
}
|
||||
if w == nil {
|
||||
return nil, errors.New("error creating db repository with nil writer")
|
||||
}
|
||||
if kms == nil {
|
||||
return nil, errors.New("error creating db repository with nil kms")
|
||||
}
|
||||
opts := getOpts(opt...)
|
||||
if opts.withLimit == 0 {
|
||||
// zero signals the boundary defaults should be used.
|
||||
opts.withLimit = db.DefaultLimit
|
||||
}
|
||||
return &Repository{
|
||||
reader: r,
|
||||
writer: w,
|
||||
kms: kms,
|
||||
defaultLimit: opts.withLimit,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// list will return a listing of resources and honor the WithLimit option or the
|
||||
// repo defaultLimit. Supports WithOrder option.
|
||||
func (r *Repository) list(ctx context.Context, resources interface{}, where string, args []interface{}, opt ...Option) error {
|
||||
opts := getOpts(opt...)
|
||||
limit := r.defaultLimit
|
||||
var dbOpts []db.Option
|
||||
if opts.withLimit != 0 {
|
||||
// non-zero signals an override of the default limit for the repo.
|
||||
limit = opts.withLimit
|
||||
}
|
||||
dbOpts = append(dbOpts, db.WithLimit(limit))
|
||||
if opts.withOrder != "" {
|
||||
dbOpts = append(dbOpts, db.WithOrder(opts.withOrder))
|
||||
}
|
||||
return r.reader.SearchWhere(ctx, resources, where, args, dbOpts...)
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
dbcommon "github.com/hashicorp/boundary/internal/db/common"
|
||||
)
|
||||
|
||||
// CreateConnection inserts into the repository and returns the new Connection with
|
||||
// its State of "Connected". The following fields must be empty when creating a
|
||||
// session: PublicId, BytesUp, BytesDown, ClosedReason, Version, CreateTime,
|
||||
// UpdateTime. No options are currently supported.
|
||||
func (r *Repository) CreateConnection(ctx context.Context, newConnection *Connection, opt ...Option) (*Connection, *ConnectionState, error) {
|
||||
if newConnection == nil {
|
||||
return nil, nil, fmt.Errorf("create connection: missing connection: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if newConnection.PublicId != "" {
|
||||
return nil, nil, fmt.Errorf("create connection: public id is not empty: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if newConnection.BytesUp != 0 {
|
||||
return nil, nil, fmt.Errorf("create connection: bytes down is not empty: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if newConnection.BytesDown != 0 {
|
||||
return nil, nil, fmt.Errorf("create connection: bytes up is not empty: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if newConnection.ClosedReason != "" {
|
||||
return nil, nil, fmt.Errorf("create connection: closed reason is not empty: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if newConnection.Version != 0 {
|
||||
return nil, nil, fmt.Errorf("create connection: version is not empty: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if newConnection.CreateTime != nil {
|
||||
return nil, nil, fmt.Errorf("create connection: create time is not empty: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if newConnection.UpdateTime != nil {
|
||||
return nil, nil, fmt.Errorf("create connection: update time is not empty: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
id, err := newConnectionId()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("create connection: %w", err)
|
||||
}
|
||||
newConnection.PublicId = id
|
||||
|
||||
var returnedConnection *Connection
|
||||
var returnedState *ConnectionState
|
||||
_, err = r.writer.DoTx(
|
||||
ctx,
|
||||
db.StdRetryCnt,
|
||||
db.ExpBackoff{},
|
||||
func(read db.Reader, w db.Writer) error {
|
||||
returnedConnection = newConnection.Clone().(*Connection)
|
||||
if err = w.Create(ctx, returnedConnection); err != nil {
|
||||
return err
|
||||
}
|
||||
var foundStates []*ConnectionState
|
||||
// trigger will create new "Connected" state
|
||||
if foundStates, err = fetchConnectionStates(ctx, read, returnedConnection.PublicId); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(foundStates) != 1 {
|
||||
return fmt.Errorf("%d states found for new connection %s", len(foundStates), returnedConnection.PublicId)
|
||||
}
|
||||
returnedState = foundStates[0]
|
||||
if returnedState.Status != StatusConnected.String() {
|
||||
return fmt.Errorf("new connection %s state is not valid: %s", returnedConnection.PublicId, returnedState.Status)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("create connection: %w", err)
|
||||
}
|
||||
return returnedConnection, returnedState, err
|
||||
}
|
||||
|
||||
// LookupConnection will look up a connection in the repository and return the connection
|
||||
// with its states. If the connection is not found, it will return nil, nil, nil.
|
||||
// No options are currently supported.
|
||||
func (r *Repository) LookupConnection(ctx context.Context, connectionId string, opt ...Option) (*Connection, []*ConnectionState, error) {
|
||||
if connectionId == "" {
|
||||
return nil, nil, fmt.Errorf("lookup connection: missing connectionId id: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
connection := AllocConnection()
|
||||
connection.PublicId = connectionId
|
||||
var states []*ConnectionState
|
||||
_, err := r.writer.DoTx(
|
||||
ctx,
|
||||
db.StdRetryCnt,
|
||||
db.ExpBackoff{},
|
||||
func(read db.Reader, w db.Writer) error {
|
||||
if err := read.LookupById(ctx, &connection); err != nil {
|
||||
return fmt.Errorf("lookup connection: failed %w for %s", err, connectionId)
|
||||
}
|
||||
var err error
|
||||
if states, err = fetchConnectionStates(ctx, read, connectionId, db.WithOrder("start_time desc")); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrRecordNotFound) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
return nil, nil, fmt.Errorf("lookup connection: %w", err)
|
||||
}
|
||||
return &connection, states, nil
|
||||
}
|
||||
|
||||
// ListConnections will sessions. Supports the WithLimit and WithOrder options.
|
||||
func (r *Repository) ListConnections(ctx context.Context, sessionId string, opt ...Option) ([]*Connection, error) {
|
||||
var connections []*Connection
|
||||
err := r.list(ctx, &connections, "session_id = ?", []interface{}{sessionId}, opt...) // pass options, so WithLimit and WithOrder are supported
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list connections: %w", err)
|
||||
}
|
||||
return connections, nil
|
||||
}
|
||||
|
||||
// DeleteConnection will delete a connection from the repository.
|
||||
func (r *Repository) DeleteConnection(ctx context.Context, publicId string, opt ...Option) (int, error) {
|
||||
if publicId == "" {
|
||||
return db.NoRowsAffected, fmt.Errorf("delete connection: missing public id %w", db.ErrInvalidParameter)
|
||||
}
|
||||
connection := AllocConnection()
|
||||
connection.PublicId = publicId
|
||||
if err := r.reader.LookupByPublicId(ctx, &connection); err != nil {
|
||||
return db.NoRowsAffected, fmt.Errorf("delete connection: failed %w for %s", err, publicId)
|
||||
}
|
||||
|
||||
var rowsDeleted int
|
||||
_, err := r.writer.DoTx(
|
||||
ctx,
|
||||
db.StdRetryCnt,
|
||||
db.ExpBackoff{},
|
||||
func(_ db.Reader, w db.Writer) error {
|
||||
deleteConnection := connection.Clone()
|
||||
var err error
|
||||
rowsDeleted, err = w.Delete(
|
||||
ctx,
|
||||
deleteConnection,
|
||||
)
|
||||
if err == nil && rowsDeleted > 1 {
|
||||
// return err, which will result in a rollback of the delete
|
||||
return errors.New("error more than 1 connection would have been deleted")
|
||||
}
|
||||
return err
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return db.NoRowsAffected, fmt.Errorf("delete connection: failed %w for %s", err, publicId)
|
||||
}
|
||||
return rowsDeleted, nil
|
||||
}
|
||||
|
||||
// UpdateConnection updates the repository entry for the connection, using the
|
||||
// fieldMaskPaths. Only BytesUp, BytesDown, and ClosedReason are mutable and
|
||||
// will be set to NULL if set to a zero value and included in the fieldMaskPaths.
|
||||
func (r *Repository) UpdateConnection(ctx context.Context, connection *Connection, version uint32, fieldMaskPaths []string, opt ...Option) (*Connection, []*ConnectionState, int, error) {
|
||||
if connection == nil {
|
||||
return nil, nil, db.NoRowsAffected, fmt.Errorf("update connection: missing connection %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if connection.PublicId == "" {
|
||||
return nil, nil, db.NoRowsAffected, fmt.Errorf("update connection: missing connection public id %w", db.ErrInvalidParameter)
|
||||
}
|
||||
for _, f := range fieldMaskPaths {
|
||||
switch {
|
||||
case strings.EqualFold("BytesUp", f):
|
||||
case strings.EqualFold("BytesDown", f):
|
||||
case strings.EqualFold("ClosedReason", f):
|
||||
default:
|
||||
return nil, nil, db.NoRowsAffected, fmt.Errorf("update connection: field: %s: %w", f, db.ErrInvalidFieldMask)
|
||||
}
|
||||
}
|
||||
var dbMask, nullFields []string
|
||||
dbMask, nullFields = dbcommon.BuildUpdatePaths(
|
||||
map[string]interface{}{
|
||||
"BytesUp": connection.BytesUp,
|
||||
"BytesDown": connection.BytesDown,
|
||||
"ClosedReason": connection.ClosedReason,
|
||||
},
|
||||
fieldMaskPaths,
|
||||
)
|
||||
if len(dbMask) == 0 && len(nullFields) == 0 {
|
||||
return nil, nil, db.NoRowsAffected, fmt.Errorf("update connection: %w", db.ErrEmptyFieldMask)
|
||||
}
|
||||
|
||||
var c *Connection
|
||||
var states []*ConnectionState
|
||||
var rowsUpdated int
|
||||
_, err := r.writer.DoTx(
|
||||
ctx,
|
||||
db.StdRetryCnt,
|
||||
db.ExpBackoff{},
|
||||
func(reader db.Reader, w db.Writer) error {
|
||||
var err error
|
||||
c = connection.Clone().(*Connection)
|
||||
rowsUpdated, err = w.Update(
|
||||
ctx,
|
||||
c,
|
||||
dbMask,
|
||||
nullFields,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err == nil && rowsUpdated > 1 {
|
||||
// return err, which will result in a rollback of the update
|
||||
return errors.New("error more than 1 connection would have been updated ")
|
||||
}
|
||||
states, err = fetchConnectionStates(ctx, reader, c.PublicId, db.WithOrder("start_time desc"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, db.NoRowsAffected, fmt.Errorf("update connection: %w for %s", err, connection.PublicId)
|
||||
}
|
||||
return c, states, rowsUpdated, err
|
||||
}
|
||||
|
||||
// UpdateConnectionState will update the connection's state using the connection id and its
|
||||
// version. No options are currently supported.
|
||||
func (r *Repository) UpdateConnectionState(ctx context.Context, connectionId string, connectionVersion uint32, s ConnectionStatus, opt ...Option) (*Connection, []*ConnectionState, error) {
|
||||
if connectionId == "" {
|
||||
return nil, nil, fmt.Errorf("update connection state: missing session id %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if connectionVersion == 0 {
|
||||
return nil, nil, fmt.Errorf("update connection state: version cannot be zero: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if s == "" {
|
||||
return nil, nil, fmt.Errorf("update connection state: missing connection status: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
newState, err := NewConnectionState(connectionId, s)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("update connection state: %w", err)
|
||||
}
|
||||
sessionConnection, _, err := r.LookupConnection(ctx, connectionId)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("update connection state: %w", err)
|
||||
}
|
||||
if sessionConnection == nil {
|
||||
return nil, nil, fmt.Errorf("update connection state: unable to look up connection for %s: %w", connectionId, err)
|
||||
}
|
||||
|
||||
updatedConnection := AllocConnection()
|
||||
var returnedStates []*ConnectionState
|
||||
_, err = r.writer.DoTx(
|
||||
ctx,
|
||||
db.StdRetryCnt,
|
||||
db.ExpBackoff{},
|
||||
func(reader db.Reader, w db.Writer) error {
|
||||
// We need to update the connection version as that's the aggregate
|
||||
updatedConnection.PublicId = connectionId
|
||||
updatedConnection.Version = uint32(connectionVersion) + 1
|
||||
rowsUpdated, err := w.Update(ctx, &updatedConnection, []string{"Version"}, nil, db.WithVersion(&connectionVersion))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update connection version: %w", err)
|
||||
}
|
||||
if rowsUpdated != 1 {
|
||||
return fmt.Errorf("updated connection and %d rows updated", rowsUpdated)
|
||||
}
|
||||
if err := w.Create(ctx, newState); err != nil {
|
||||
return fmt.Errorf("unable to add new state: %w", err)
|
||||
}
|
||||
|
||||
returnedStates, err = fetchConnectionStates(ctx, reader, connectionId, db.WithOrder("start_time desc"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("update connection state: error creating new state: %w", err)
|
||||
}
|
||||
return &updatedConnection, returnedStates, nil
|
||||
}
|
||||
|
||||
func fetchConnectionStates(ctx context.Context, r db.Reader, connectionId string, opt ...db.Option) ([]*ConnectionState, error) {
|
||||
var states []*ConnectionState
|
||||
if err := r.SearchWhere(ctx, &states, "connection_id = ?", []interface{}{connectionId}, opt...); err != nil {
|
||||
return nil, fmt.Errorf("fetch connection states: %w", err)
|
||||
}
|
||||
if len(states) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return states, nil
|
||||
}
|
||||
@@ -0,0 +1,639 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
dbassert "github.com/hashicorp/boundary/internal/db/assert"
|
||||
"github.com/hashicorp/boundary/internal/iam"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/hashicorp/boundary/internal/oplog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRepository_ListConnection(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
const testLimit = 10
|
||||
wrapper := db.TestWrapper(t)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
rw := db.New(conn)
|
||||
kms := kms.TestKms(t, conn, wrapper)
|
||||
repo, err := NewRepository(rw, rw, kms, WithLimit(testLimit))
|
||||
require.NoError(t, err)
|
||||
session := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
|
||||
type args struct {
|
||||
searchForSessionId string
|
||||
opt []Option
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
createCnt int
|
||||
args args
|
||||
wantCnt int
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "no-limit",
|
||||
createCnt: repo.defaultLimit + 1,
|
||||
args: args{
|
||||
searchForSessionId: session.PublicId,
|
||||
opt: []Option{WithLimit(-1)},
|
||||
},
|
||||
wantCnt: repo.defaultLimit + 1,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "default-limit",
|
||||
createCnt: repo.defaultLimit + 1,
|
||||
args: args{
|
||||
searchForSessionId: session.PublicId,
|
||||
},
|
||||
wantCnt: repo.defaultLimit,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "custom-limit",
|
||||
createCnt: repo.defaultLimit + 1,
|
||||
args: args{
|
||||
searchForSessionId: session.PublicId,
|
||||
opt: []Option{WithLimit(3)},
|
||||
},
|
||||
wantCnt: 3,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "bad-session-id",
|
||||
createCnt: repo.defaultLimit + 1,
|
||||
args: args{
|
||||
searchForSessionId: "s_thisIsNotValid",
|
||||
},
|
||||
wantCnt: 0,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
require.NoError(conn.Where("1=1").Delete(AllocConnection()).Error)
|
||||
testConnections := []*Connection{}
|
||||
for i := 0; i < tt.createCnt; i++ {
|
||||
c := TestConnection(t, conn,
|
||||
session.PublicId,
|
||||
"127.0.0.1",
|
||||
22,
|
||||
"127.0.0.1",
|
||||
2222,
|
||||
)
|
||||
testConnections = append(testConnections, c)
|
||||
}
|
||||
assert.Equal(tt.createCnt, len(testConnections))
|
||||
got, err := repo.ListConnections(context.Background(), tt.args.searchForSessionId, tt.args.opt...)
|
||||
if tt.wantErr {
|
||||
require.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tt.wantCnt, len(got))
|
||||
})
|
||||
}
|
||||
t.Run("withOrder", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
require.NoError(conn.Where("1=1").Delete(AllocConnection()).Error)
|
||||
wantCnt := 5
|
||||
for i := 0; i < wantCnt; i++ {
|
||||
_ = TestConnection(t, conn,
|
||||
session.PublicId,
|
||||
"127.0.0.1",
|
||||
22,
|
||||
"127.0.0.1",
|
||||
2222,
|
||||
)
|
||||
}
|
||||
got, err := repo.ListConnections(context.Background(), session.PublicId, WithOrder("create_time asc"))
|
||||
require.NoError(err)
|
||||
assert.Equal(wantCnt, len(got))
|
||||
|
||||
for i := 0; i < len(got)-1; i++ {
|
||||
first, err := ptypes.Timestamp(got[i].CreateTime.Timestamp)
|
||||
require.NoError(err)
|
||||
second, err := ptypes.Timestamp(got[i+1].CreateTime.Timestamp)
|
||||
require.NoError(err)
|
||||
assert.True(first.Before(second))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_CreateConnection(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
kms := kms.TestKms(t, conn, wrapper)
|
||||
repo, err := NewRepository(rw, rw, kms)
|
||||
require.NoError(t, err)
|
||||
session := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
|
||||
type args struct {
|
||||
connection *Connection
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
wantIsError error
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
args: args{
|
||||
connection: func() *Connection {
|
||||
c, err := NewConnection(
|
||||
session.PublicId,
|
||||
"127.0.0.1",
|
||||
22,
|
||||
"127.0.0.1",
|
||||
2222,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
return c
|
||||
}(),
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty-session-id",
|
||||
args: args{
|
||||
connection: &Connection{
|
||||
ClientTcpAddress: "127.0.0.1",
|
||||
ClientTcpPort: 22,
|
||||
BackendTcpAddress: "127.0.0.1",
|
||||
BackendTcpPort: 2222,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
wantIsError: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "empty-client-address",
|
||||
args: args{
|
||||
connection: &Connection{
|
||||
SessionId: session.PublicId,
|
||||
ClientTcpPort: 22,
|
||||
BackendTcpAddress: "127.0.0.1",
|
||||
BackendTcpPort: 2222,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
wantIsError: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "empty-client-port",
|
||||
args: args{
|
||||
connection: &Connection{
|
||||
SessionId: session.PublicId,
|
||||
ClientTcpAddress: "127.0.0.1",
|
||||
BackendTcpAddress: "127.0.0.1",
|
||||
BackendTcpPort: 2222,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
wantIsError: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "empty-backend-address",
|
||||
args: args{
|
||||
connection: &Connection{
|
||||
SessionId: session.PublicId,
|
||||
ClientTcpAddress: "127.0.0.1",
|
||||
ClientTcpPort: 22,
|
||||
BackendTcpPort: 2222,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
wantIsError: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "empty-backend-port",
|
||||
args: args{
|
||||
connection: &Connection{
|
||||
SessionId: session.PublicId,
|
||||
ClientTcpAddress: "127.0.0.1",
|
||||
ClientTcpPort: 22,
|
||||
BackendTcpAddress: "127.0.0.1",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
wantIsError: db.ErrInvalidParameter,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
connection, st, err := repo.CreateConnection(context.Background(), tt.args.connection)
|
||||
if tt.wantErr {
|
||||
assert.Error(err)
|
||||
assert.Nil(connection)
|
||||
assert.Nil(st)
|
||||
if tt.wantIsError != nil {
|
||||
assert.True(errors.Is(err, tt.wantIsError))
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.NotNil(connection.CreateTime)
|
||||
assert.NotNil(st.StartTime)
|
||||
assert.Equal(st.Status, StatusConnected.String())
|
||||
found, foundStates, err := repo.LookupConnection(context.Background(), connection.PublicId)
|
||||
assert.NoError(err)
|
||||
assert.Equal(found, connection)
|
||||
|
||||
err = db.TestVerifyOplog(t, rw, connection.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second))
|
||||
assert.Error(err)
|
||||
|
||||
require.Equal(1, len(foundStates))
|
||||
assert.Equal(foundStates[0].Status, StatusConnected.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepository_UpdateConnectionState(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
kms := kms.TestKms(t, conn, wrapper)
|
||||
repo, err := NewRepository(rw, rw, kms)
|
||||
require.NoError(t, err)
|
||||
session := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
connection *Connection
|
||||
newStatus ConnectionStatus
|
||||
overrideConnectionId *string
|
||||
overrideConnectionVersion *uint32
|
||||
wantStateCnt int
|
||||
wantErr bool
|
||||
wantIsError error
|
||||
}{
|
||||
{
|
||||
name: "closed",
|
||||
connection: TestConnection(t, conn, session.PublicId, "0.0.0.0", 22, "0.0.0.0", 2222),
|
||||
newStatus: StatusClosed,
|
||||
wantStateCnt: 2,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "bad-version",
|
||||
connection: TestConnection(t, conn, session.PublicId, "0.0.0.0", 22, "0.0.0.0", 2222),
|
||||
newStatus: StatusClosed,
|
||||
overrideConnectionVersion: func() *uint32 {
|
||||
v := uint32(22)
|
||||
return &v
|
||||
}(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty-version",
|
||||
connection: TestConnection(t, conn, session.PublicId, "0.0.0.0", 22, "0.0.0.0", 2222),
|
||||
newStatus: StatusClosed,
|
||||
overrideConnectionVersion: func() *uint32 {
|
||||
v := uint32(0)
|
||||
return &v
|
||||
}(),
|
||||
wantErr: true,
|
||||
wantIsError: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "bad-connectionId",
|
||||
connection: TestConnection(t, conn, session.PublicId, "0.0.0.0", 22, "0.0.0.0", 2222),
|
||||
newStatus: StatusClosed,
|
||||
overrideConnectionId: func() *string {
|
||||
s := "sc_thisIsNotValid"
|
||||
return &s
|
||||
}(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty-connectionId",
|
||||
connection: TestConnection(t, conn, session.PublicId, "0.0.0.0", 22, "0.0.0.0", 2222),
|
||||
newStatus: StatusClosed,
|
||||
overrideConnectionId: func() *string {
|
||||
s := ""
|
||||
return &s
|
||||
}(),
|
||||
wantErr: true,
|
||||
wantIsError: db.ErrInvalidParameter,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
var id string
|
||||
var version uint32
|
||||
switch {
|
||||
case tt.overrideConnectionId != nil:
|
||||
id = *tt.overrideConnectionId
|
||||
default:
|
||||
id = tt.connection.PublicId
|
||||
}
|
||||
switch {
|
||||
case tt.overrideConnectionVersion != nil:
|
||||
version = *tt.overrideConnectionVersion
|
||||
default:
|
||||
version = tt.connection.Version
|
||||
}
|
||||
|
||||
s, ss, err := repo.UpdateConnectionState(context.Background(), id, version, tt.newStatus)
|
||||
if tt.wantErr {
|
||||
require.Error(err)
|
||||
if tt.wantIsError != nil {
|
||||
assert.Truef(errors.Is(err, tt.wantIsError), "unexpected error %s", err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
require.NotNil(s)
|
||||
require.NotNil(ss)
|
||||
assert.Equal(tt.wantStateCnt, len(ss))
|
||||
assert.Equal(tt.newStatus.String(), ss[0].Status)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepository_UpdateConnection(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
kms := kms.TestKms(t, conn, wrapper)
|
||||
repo, err := NewRepository(rw, rw, kms)
|
||||
require.NoError(t, err)
|
||||
session := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
|
||||
type args struct {
|
||||
closedReason ClosedReason
|
||||
bytesUp uint64
|
||||
bytesDown uint64
|
||||
fieldMaskPaths []string
|
||||
opt []Option
|
||||
publicId *string // not updateable - db.ErrInvalidFieldMask
|
||||
sessionId string // not updateable - db.ErrInvalidFieldMask
|
||||
clientTcpAddress string // not updateable - db.ErrInvalidFieldMask
|
||||
clientTcpPort uint32 // not updateable - db.ErrInvalidFieldMask
|
||||
backendTcpAddress string // not updateable - db.ErrInvalidFieldMask
|
||||
backendTcpPort uint32 // not updateable - db.ErrInvalidFieldMask
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantRowsUpdate int
|
||||
wantErr bool
|
||||
wantIsError error
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
args: args{
|
||||
closedReason: ConnectionClosedByUser,
|
||||
bytesUp: uint64(111),
|
||||
bytesDown: uint64(1),
|
||||
fieldMaskPaths: []string{"ClosedReason", "BytesUp", "BytesDown"},
|
||||
},
|
||||
wantErr: false,
|
||||
wantRowsUpdate: 1,
|
||||
},
|
||||
{
|
||||
name: "publicId",
|
||||
args: args{
|
||||
publicId: func() *string {
|
||||
id, err := newConnectionId()
|
||||
require.NoError(t, err)
|
||||
return &id
|
||||
}(),
|
||||
fieldMaskPaths: []string{"PublicId"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantRowsUpdate: 0,
|
||||
wantIsError: db.ErrInvalidFieldMask,
|
||||
},
|
||||
{
|
||||
name: "sessionId",
|
||||
args: args{
|
||||
sessionId: func() string {
|
||||
id, err := newId()
|
||||
require.NoError(t, err)
|
||||
return id
|
||||
}(),
|
||||
fieldMaskPaths: []string{"SessionId"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantRowsUpdate: 0,
|
||||
wantIsError: db.ErrInvalidFieldMask,
|
||||
},
|
||||
{
|
||||
name: "clientTcpAddress",
|
||||
args: args{
|
||||
clientTcpAddress: "127.0.0.1",
|
||||
fieldMaskPaths: []string{"ClientTcpAddress"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantRowsUpdate: 0,
|
||||
wantIsError: db.ErrInvalidFieldMask,
|
||||
},
|
||||
{
|
||||
name: "clientTcpPort",
|
||||
args: args{
|
||||
clientTcpPort: 443,
|
||||
fieldMaskPaths: []string{"ClientTcpPort"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantRowsUpdate: 0,
|
||||
wantIsError: db.ErrInvalidFieldMask,
|
||||
},
|
||||
{
|
||||
name: "backendTcpAddress",
|
||||
args: args{
|
||||
backendTcpAddress: "127.0.0.1",
|
||||
fieldMaskPaths: []string{"BackendTcpAddress"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantRowsUpdate: 0,
|
||||
wantIsError: db.ErrInvalidFieldMask,
|
||||
},
|
||||
{
|
||||
name: "backendTcpPort",
|
||||
args: args{
|
||||
backendTcpPort: 4443,
|
||||
fieldMaskPaths: []string{"BackendTcpPort"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantRowsUpdate: 0,
|
||||
wantIsError: db.ErrInvalidFieldMask,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
c := TestConnection(t, conn, session.PublicId, "0.0.0.0", 22, "127.0.0.1", 2222)
|
||||
|
||||
updateConnection := AllocConnection()
|
||||
updateConnection.PublicId = c.PublicId
|
||||
if tt.args.publicId != nil {
|
||||
updateConnection.PublicId = *tt.args.publicId
|
||||
}
|
||||
updateConnection.BytesUp = tt.args.bytesUp
|
||||
updateConnection.BytesDown = tt.args.bytesDown
|
||||
updateConnection.ClosedReason = tt.args.closedReason.String()
|
||||
updateConnection.Version = c.Version
|
||||
afterUpdate, afterUpdateState, updatedRows, err := repo.UpdateConnection(context.Background(), &updateConnection, updateConnection.Version, tt.args.fieldMaskPaths, tt.args.opt...)
|
||||
|
||||
if tt.wantErr {
|
||||
require.Error(err)
|
||||
if tt.wantIsError != nil {
|
||||
assert.Truef(errors.Is(err, tt.wantIsError), "unexpected error: %s", err.Error())
|
||||
}
|
||||
assert.Nil(afterUpdate)
|
||||
assert.Nil(afterUpdateState)
|
||||
assert.Equal(0, updatedRows)
|
||||
err = db.TestVerifyOplog(t, rw, c.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))
|
||||
assert.Error(err)
|
||||
assert.True(errors.Is(db.ErrRecordNotFound, err))
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tt.wantRowsUpdate, updatedRows)
|
||||
require.NotNil(afterUpdate)
|
||||
require.NotNil(afterUpdateState)
|
||||
switch tt.name {
|
||||
case "valid-no-op":
|
||||
assert.Equal(c.UpdateTime, afterUpdate.UpdateTime)
|
||||
default:
|
||||
assert.NotEqual(c.UpdateTime, afterUpdate.UpdateTime)
|
||||
}
|
||||
found, foundStates, err := repo.LookupConnection(context.Background(), c.PublicId)
|
||||
require.NoError(err)
|
||||
assert.Equal(afterUpdate, found)
|
||||
dbassrt := dbassert.New(t, rw)
|
||||
if tt.args.bytesUp == 0 {
|
||||
dbassrt.IsNull(found, "BytesUp")
|
||||
}
|
||||
if tt.args.bytesDown == 0 {
|
||||
dbassrt.IsNull(found, "BytesDown")
|
||||
}
|
||||
if tt.args.closedReason == "" {
|
||||
dbassrt.IsNull(found, "ClosedReason")
|
||||
}
|
||||
assert.Equal(tt.args.closedReason.String(), found.ClosedReason)
|
||||
assert.Equal(tt.args.bytesUp, found.BytesUp)
|
||||
assert.Equal(tt.args.bytesDown, found.BytesDown)
|
||||
|
||||
err = db.TestVerifyOplog(t, rw, c.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))
|
||||
assert.Error(err)
|
||||
|
||||
switch {
|
||||
case tt.args.closedReason != "":
|
||||
require.Equal(2, len(foundStates))
|
||||
// sorted by StartTime desc
|
||||
assert.Equal(StatusClosed.String(), foundStates[0].Status)
|
||||
assert.Equal(StatusConnected.String(), foundStates[1].Status)
|
||||
default:
|
||||
require.Equal(1, len(foundStates))
|
||||
assert.Equal(StatusConnected.String(), foundStates[0].Status)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRepository_DeleteConnection(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
kms := kms.TestKms(t, conn, wrapper)
|
||||
repo, err := NewRepository(rw, rw, kms)
|
||||
require.NoError(t, err)
|
||||
session := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
|
||||
type args struct {
|
||||
connection *Connection
|
||||
opt []Option
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantRowsDeleted int
|
||||
wantErr bool
|
||||
wantErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
args: args{
|
||||
connection: TestConnection(t, conn, session.PublicId, "127.0.0.1", 22, "127.0.0.1", 2222),
|
||||
},
|
||||
wantRowsDeleted: 1,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "no-public-id",
|
||||
args: args{
|
||||
connection: func() *Connection {
|
||||
c := AllocConnection()
|
||||
return &c
|
||||
}(),
|
||||
},
|
||||
wantRowsDeleted: 0,
|
||||
wantErr: true,
|
||||
wantErrMsg: "delete connection: missing public id invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "not-found",
|
||||
args: args{
|
||||
connection: func() *Connection {
|
||||
c := AllocConnection()
|
||||
id, err := newConnectionId()
|
||||
require.NoError(t, err)
|
||||
c.PublicId = id
|
||||
return &c
|
||||
}(),
|
||||
},
|
||||
wantRowsDeleted: 0,
|
||||
wantErr: true,
|
||||
wantErrMsg: "delete connection: failed record not found for ",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
deletedRows, err := repo.DeleteConnection(context.Background(), tt.args.connection.PublicId, tt.args.opt...)
|
||||
if tt.wantErr {
|
||||
assert.Error(err)
|
||||
assert.Equal(0, deletedRows)
|
||||
assert.Contains(err.Error(), tt.wantErrMsg)
|
||||
err = db.TestVerifyOplog(t, rw, tt.args.connection.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second))
|
||||
assert.Error(err)
|
||||
assert.True(errors.Is(db.ErrRecordNotFound, err))
|
||||
return
|
||||
}
|
||||
assert.NoError(err)
|
||||
assert.Equal(tt.wantRowsDeleted, deletedRows)
|
||||
found, _, err := repo.LookupConnection(context.Background(), tt.args.connection.PublicId)
|
||||
assert.NoError(err)
|
||||
assert.Nil(found)
|
||||
|
||||
err = db.TestVerifyOplog(t, rw, tt.args.connection.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second))
|
||||
assert.Error(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,441 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
dbcommon "github.com/hashicorp/boundary/internal/db/common"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
wrapping "github.com/hashicorp/go-kms-wrapping"
|
||||
"github.com/hashicorp/vault/sdk/helper/strutil"
|
||||
)
|
||||
|
||||
// CreateSession inserts into the repository and returns the new Session with
|
||||
// its State of "Pending". The following fields must be empty when creating a
|
||||
// session: ServerId, ServerType, and PublicId. No options are
|
||||
// currently supported.
|
||||
func (r *Repository) CreateSession(ctx context.Context, sessionWrapper wrapping.Wrapper, newSession *Session, opt ...Option) (*Session, *State, ed25519.PrivateKey, error) {
|
||||
if newSession == nil {
|
||||
return nil, nil, nil, fmt.Errorf("create session: missing session: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if newSession.PublicId != "" {
|
||||
return nil, nil, nil, fmt.Errorf("create session: public id is not empty: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if len(newSession.Certificate) != 0 {
|
||||
return nil, nil, nil, fmt.Errorf("create session: certificate is not empty: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if newSession.TargetId == "" {
|
||||
return nil, nil, nil, fmt.Errorf("create session: target id is empty: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if newSession.HostId == "" {
|
||||
return nil, nil, nil, fmt.Errorf("create session: user id is empty: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if newSession.UserId == "" {
|
||||
return nil, nil, nil, fmt.Errorf("create session: user id is empty: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if newSession.HostSetId == "" {
|
||||
return nil, nil, nil, fmt.Errorf("create session: host set id is empty: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if newSession.AuthTokenId == "" {
|
||||
return nil, nil, nil, fmt.Errorf("create session: auth token id is empty: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if newSession.ScopeId == "" {
|
||||
return nil, nil, nil, fmt.Errorf("create session: scope id is empty: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if newSession.ServerId != "" {
|
||||
return nil, nil, nil, fmt.Errorf("create session: server id must empty: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if newSession.ServerType != "" {
|
||||
return nil, nil, nil, fmt.Errorf("create session: server type must empty: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if newSession.CtTofuToken != nil {
|
||||
return nil, nil, nil, fmt.Errorf("create session: ct must be empty: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if newSession.TofuToken != nil {
|
||||
return nil, nil, nil, fmt.Errorf("create session: tofu token must be empty: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
id, err := newId()
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("create session: %w", err)
|
||||
}
|
||||
|
||||
privKey, certBytes, err := newCert(sessionWrapper, newSession.UserId, id)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("create session: %w", err)
|
||||
}
|
||||
newSession.Certificate = certBytes
|
||||
newSession.PublicId = id
|
||||
|
||||
var returnedSession *Session
|
||||
var returnedState *State
|
||||
_, err = r.writer.DoTx(
|
||||
ctx,
|
||||
db.StdRetryCnt,
|
||||
db.ExpBackoff{},
|
||||
func(read db.Reader, w db.Writer) error {
|
||||
returnedSession = newSession.Clone().(*Session)
|
||||
if err = w.Create(ctx, returnedSession); err != nil {
|
||||
return err
|
||||
}
|
||||
var foundStates []*State
|
||||
// trigger will create new "Pending" state
|
||||
if foundStates, err = fetchStates(ctx, read, returnedSession.PublicId); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(foundStates) != 1 {
|
||||
return fmt.Errorf("%d states found for new session %s", len(foundStates), returnedSession.PublicId)
|
||||
}
|
||||
returnedState = foundStates[0]
|
||||
if returnedState.Status != StatusPending.String() {
|
||||
return fmt.Errorf("new session %s state is not valid: %s", returnedSession.PublicId, returnedState.Status)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("create session: %w", err)
|
||||
}
|
||||
return returnedSession, returnedState, privKey, err
|
||||
}
|
||||
|
||||
// LookupSession will look up a session in the repository and return the session
|
||||
// with its states. Returned States are ordered by start time descending. If the
|
||||
// session is not found, it will return nil, nil, nil. No options are currently
|
||||
// supported.
|
||||
func (r *Repository) LookupSession(ctx context.Context, sessionId string, opt ...Option) (*Session, []*State, error) {
|
||||
if sessionId == "" {
|
||||
return nil, nil, fmt.Errorf("lookup session: missing sessionId id: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
session := AllocSession()
|
||||
session.PublicId = sessionId
|
||||
var states []*State
|
||||
_, err := r.writer.DoTx(
|
||||
ctx,
|
||||
db.StdRetryCnt,
|
||||
db.ExpBackoff{},
|
||||
func(read db.Reader, w db.Writer) error {
|
||||
if err := read.LookupById(ctx, &session); err != nil {
|
||||
return fmt.Errorf("lookup session: failed %w for %s", err, sessionId)
|
||||
}
|
||||
var err error
|
||||
if states, err = fetchStates(ctx, read, sessionId, db.WithOrder("start_time desc")); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrRecordNotFound) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
return nil, nil, fmt.Errorf("lookup session: %w", err)
|
||||
}
|
||||
if len(session.CtTofuToken) > 0 {
|
||||
databaseWrapper, err := r.kms.GetWrapper(ctx, session.ScopeId, kms.KeyPurposeDatabase, kms.WithKeyId(session.KeyId))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("lookup session: unable to get database wrapper: %w", err)
|
||||
}
|
||||
if err := session.decrypt(ctx, databaseWrapper); err != nil {
|
||||
return nil, nil, fmt.Errorf("lookup session: cannot decrypt session value: %w", err)
|
||||
}
|
||||
} else {
|
||||
session.CtTofuToken = nil
|
||||
}
|
||||
return &session, states, nil
|
||||
}
|
||||
|
||||
// ListSessions will sessions. Supports the WithLimit, WithScopeId and WithOrder options.
|
||||
func (r *Repository) ListSessions(ctx context.Context, opt ...Option) ([]*Session, error) {
|
||||
opts := getOpts(opt...)
|
||||
var where []string
|
||||
var args []interface{}
|
||||
switch {
|
||||
case opts.withScopeId != "":
|
||||
where, args = append(where, "scope_id = ?"), append(args, opts.withScopeId)
|
||||
case opts.withUserId != "":
|
||||
where, args = append(where, "user_id = ?"), append(args, opts.withUserId)
|
||||
}
|
||||
|
||||
var sessions []*Session
|
||||
err := r.list(ctx, &sessions, strings.Join(where, " and"), args, opt...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list sessions: %w", err)
|
||||
}
|
||||
for _, s := range sessions {
|
||||
s.CtTofuToken = nil
|
||||
s.TofuToken = nil
|
||||
s.KeyId = ""
|
||||
}
|
||||
return sessions, nil
|
||||
}
|
||||
|
||||
// DeleteSession will delete a session from the repository.
|
||||
func (r *Repository) DeleteSession(ctx context.Context, publicId string, opt ...Option) (int, error) {
|
||||
if publicId == "" {
|
||||
return db.NoRowsAffected, fmt.Errorf("delete session: missing public id %w", db.ErrInvalidParameter)
|
||||
}
|
||||
session := AllocSession()
|
||||
session.PublicId = publicId
|
||||
if err := r.reader.LookupByPublicId(ctx, &session); err != nil {
|
||||
return db.NoRowsAffected, fmt.Errorf("delete session: failed %w for %s", err, publicId)
|
||||
}
|
||||
|
||||
var rowsDeleted int
|
||||
_, err := r.writer.DoTx(
|
||||
ctx,
|
||||
db.StdRetryCnt,
|
||||
db.ExpBackoff{},
|
||||
func(_ db.Reader, w db.Writer) error {
|
||||
deleteSession := session.Clone()
|
||||
var err error
|
||||
rowsDeleted, err = w.Delete(
|
||||
ctx,
|
||||
deleteSession,
|
||||
)
|
||||
if err == nil && rowsDeleted > 1 {
|
||||
// return err, which will result in a rollback of the delete
|
||||
return errors.New("error more than 1 session would have been deleted")
|
||||
}
|
||||
return err
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return db.NoRowsAffected, fmt.Errorf("delete session: failed %w for %s", err, publicId)
|
||||
}
|
||||
return rowsDeleted, nil
|
||||
}
|
||||
|
||||
// UpdateSession updates the repository entry for the session, using the
|
||||
// fieldMaskPaths. Only TerminationReason, ServerId and ServerType a muttable
|
||||
// and will be set to NULL if set to a zero value and included in the
|
||||
// fieldMaskPaths. Returned States are ordered by start time descending.
|
||||
func (r *Repository) UpdateSession(ctx context.Context, session *Session, version uint32, fieldMaskPaths []string, opt ...Option) (*Session, []*State, int, error) {
|
||||
if session == nil {
|
||||
return nil, nil, db.NoRowsAffected, fmt.Errorf("update session: missing session %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if session.PublicId == "" {
|
||||
return nil, nil, db.NoRowsAffected, fmt.Errorf("update session: missing session public id %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if session.CtTofuToken != nil {
|
||||
return nil, nil, db.NoRowsAffected, fmt.Errorf("update session: ct must be empty: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
translatedFieldMasks := make([]string, 0, len(fieldMaskPaths))
|
||||
for _, f := range fieldMaskPaths {
|
||||
switch {
|
||||
case strings.EqualFold("TerminationReason", f):
|
||||
translatedFieldMasks = append(translatedFieldMasks, f)
|
||||
case strings.EqualFold("ServerId", f):
|
||||
translatedFieldMasks = append(translatedFieldMasks, f)
|
||||
case strings.EqualFold("ServerType", f):
|
||||
translatedFieldMasks = append(translatedFieldMasks, f)
|
||||
case strings.EqualFold("TofuToken", f):
|
||||
translatedFieldMasks = append(translatedFieldMasks, "CtTofuToken")
|
||||
default:
|
||||
return nil, nil, db.NoRowsAffected, fmt.Errorf("update session: field: %s: %w", f, db.ErrInvalidFieldMask)
|
||||
}
|
||||
}
|
||||
|
||||
updateSession := session.Clone().(*Session)
|
||||
if strutil.StrListContains(translatedFieldMasks, "CtTofuToken") && len(updateSession.TofuToken) != 0 {
|
||||
if err := r.reader.LookupById(ctx, updateSession); err != nil {
|
||||
return nil, nil, db.NoRowsAffected, fmt.Errorf("update session: %w for %s", err, updateSession.PublicId)
|
||||
}
|
||||
databaseWrapper, err := r.kms.GetWrapper(ctx, updateSession.ScopeId, kms.KeyPurposeDatabase)
|
||||
if err != nil {
|
||||
return nil, nil, db.NoRowsAffected, fmt.Errorf("update session: unable to get database wrapper: %w", err)
|
||||
}
|
||||
if err := updateSession.encrypt(ctx, databaseWrapper); err != nil {
|
||||
return nil, nil, db.NoRowsAffected, fmt.Errorf("create session: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var dbMask, nullFields []string
|
||||
dbMask, nullFields = dbcommon.BuildUpdatePaths(
|
||||
map[string]interface{}{
|
||||
"TerminationReason": updateSession.TerminationReason,
|
||||
"ServerId": updateSession.ServerId,
|
||||
"ServerType": updateSession.ServerType,
|
||||
"CtTofuToken": updateSession.CtTofuToken,
|
||||
},
|
||||
translatedFieldMasks,
|
||||
)
|
||||
if len(dbMask) == 0 && len(nullFields) == 0 {
|
||||
return nil, nil, db.NoRowsAffected, fmt.Errorf("update session: %w", db.ErrEmptyFieldMask)
|
||||
}
|
||||
|
||||
var s *Session
|
||||
var states []*State
|
||||
var rowsUpdated int
|
||||
_, err := r.writer.DoTx(
|
||||
ctx,
|
||||
db.StdRetryCnt,
|
||||
db.ExpBackoff{},
|
||||
func(reader db.Reader, w db.Writer) error {
|
||||
var err error
|
||||
s = updateSession.Clone().(*Session)
|
||||
|
||||
rowsUpdated, err = w.Update(
|
||||
ctx,
|
||||
s,
|
||||
dbMask,
|
||||
nullFields,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err == nil && rowsUpdated > 1 {
|
||||
// return err, which will result in a rollback of the update
|
||||
return errors.New("error more than 1 session would have been updated ")
|
||||
}
|
||||
states, err = fetchStates(ctx, reader, s.PublicId, db.WithOrder("start_time desc"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, db.NoRowsAffected, fmt.Errorf("update session: %w for %s", err, session.PublicId)
|
||||
}
|
||||
if len(s.CtTofuToken) == 0 {
|
||||
s.CtTofuToken = nil
|
||||
}
|
||||
return s, states, rowsUpdated, err
|
||||
}
|
||||
|
||||
// ActivateSession will activate the session and is called by a worker after
|
||||
// authenticating the session. States are ordered by start time descending.
|
||||
func (r *Repository) ActivateSession(ctx context.Context, sessionId string, sessionVersion uint32, tofuToken []byte) (*Session, []*State, error) {
|
||||
if sessionId == "" {
|
||||
return nil, nil, fmt.Errorf("activate session state: missing session id %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if sessionVersion == 0 {
|
||||
return nil, nil, fmt.Errorf("activate session state: version cannot be zero: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
updatedSession := AllocSession()
|
||||
updatedSession.PublicId = sessionId
|
||||
var returnedStates []*State
|
||||
_, err := r.writer.DoTx(
|
||||
ctx,
|
||||
db.StdRetryCnt,
|
||||
db.ExpBackoff{},
|
||||
func(reader db.Reader, w db.Writer) error {
|
||||
rowsAffected, err := w.Exec(activateStateCte, []interface{}{sessionId, sessionVersion})
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to activate session %s: %w", sessionId, err)
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
return fmt.Errorf("unable to activate session %s", sessionId)
|
||||
}
|
||||
foundSession := AllocSession()
|
||||
foundSession.PublicId = sessionId
|
||||
if err := r.reader.LookupById(ctx, &foundSession); err != nil {
|
||||
return fmt.Errorf("lookup session: failed %w for %s", err, sessionId)
|
||||
}
|
||||
databaseWrapper, err := r.kms.GetWrapper(ctx, foundSession.ScopeId, kms.KeyPurposeDatabase)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get database wrapper: %w", err)
|
||||
}
|
||||
|
||||
updatedSession.TofuToken = tofuToken
|
||||
if err := updatedSession.encrypt(ctx, databaseWrapper); err != nil {
|
||||
return err
|
||||
}
|
||||
rowsUpdated, err := w.Update(ctx, &updatedSession, []string{"CtTofuToken"}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err == nil && rowsUpdated > 1 {
|
||||
// return err, which will result in a rollback of the update
|
||||
return errors.New("error more than 1 session would have been updated ")
|
||||
}
|
||||
|
||||
returnedStates, err = fetchStates(ctx, reader, sessionId, db.WithOrder("start_time desc"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("activate session: %w", err)
|
||||
}
|
||||
return &updatedSession, returnedStates, nil
|
||||
}
|
||||
|
||||
// UpdateState will update the session's state using the session id and its
|
||||
// version. States are ordered by start time descending. No options are
|
||||
// currently supported.
|
||||
func (r *Repository) UpdateState(ctx context.Context, sessionId string, sessionVersion uint32, s Status, opt ...Option) (*Session, []*State, error) {
|
||||
if sessionId == "" {
|
||||
return nil, nil, fmt.Errorf("update session state: missing session id %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if sessionVersion == 0 {
|
||||
return nil, nil, fmt.Errorf("update session state: version cannot be zero: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if s == "" {
|
||||
return nil, nil, fmt.Errorf("update session state: missing session status: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if s == StatusActive {
|
||||
return nil, nil, fmt.Errorf("update session: you must call ActivateSession to update a session's state to active: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
newState, err := NewState(sessionId, s)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("update session state: %w", err)
|
||||
}
|
||||
|
||||
updatedSession := AllocSession()
|
||||
var returnedStates []*State
|
||||
_, err = r.writer.DoTx(
|
||||
ctx,
|
||||
db.StdRetryCnt,
|
||||
db.ExpBackoff{},
|
||||
func(reader db.Reader, w db.Writer) error {
|
||||
// We need to update the session version as that's the aggregate
|
||||
updatedSession.PublicId = sessionId
|
||||
updatedSession.Version = uint32(sessionVersion) + 1
|
||||
rowsUpdated, err := w.Update(ctx, &updatedSession, []string{"Version"}, nil, db.WithVersion(&sessionVersion))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update session version: %w", err)
|
||||
}
|
||||
if rowsUpdated != 1 {
|
||||
return fmt.Errorf("updated session and %d rows updated", rowsUpdated)
|
||||
}
|
||||
if err := w.Create(ctx, newState); err != nil {
|
||||
return fmt.Errorf("unable to add new state: %w", err)
|
||||
}
|
||||
|
||||
returnedStates, err = fetchStates(ctx, reader, sessionId, db.WithOrder("start_time desc"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("update session state: error creating new state: %w", err)
|
||||
}
|
||||
if len(updatedSession.CtTofuToken) == 0 {
|
||||
updatedSession.CtTofuToken = nil
|
||||
}
|
||||
return &updatedSession, returnedStates, nil
|
||||
}
|
||||
|
||||
func fetchStates(ctx context.Context, r db.Reader, sessionId string, opt ...db.Option) ([]*State, error) {
|
||||
var states []*State
|
||||
if err := r.SearchWhere(ctx, &states, "session_id = ?", []interface{}{sessionId}, opt...); err != nil {
|
||||
return nil, fmt.Errorf("fetch session states: %w", err)
|
||||
}
|
||||
if len(states) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return states, nil
|
||||
}
|
||||
@@ -0,0 +1,856 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
"github.com/hashicorp/boundary/internal/auth/password"
|
||||
"github.com/hashicorp/boundary/internal/authtoken"
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
dbassert "github.com/hashicorp/boundary/internal/db/assert"
|
||||
"github.com/hashicorp/boundary/internal/host/static"
|
||||
"github.com/hashicorp/boundary/internal/iam"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/hashicorp/boundary/internal/oplog"
|
||||
"github.com/hashicorp/boundary/internal/servers"
|
||||
"github.com/hashicorp/boundary/internal/target"
|
||||
"github.com/hashicorp/go-uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRepository_ListSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
const testLimit = 10
|
||||
wrapper := db.TestWrapper(t)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
rw := db.New(conn)
|
||||
kms := kms.TestKms(t, conn, wrapper)
|
||||
repo, err := NewRepository(rw, rw, kms, WithLimit(testLimit))
|
||||
require.NoError(t, err)
|
||||
composedOf := TestSessionParams(t, conn, wrapper, iamRepo)
|
||||
|
||||
type args struct {
|
||||
opt []Option
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
createCnt int
|
||||
args args
|
||||
wantCnt int
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "no-limit",
|
||||
createCnt: repo.defaultLimit + 1,
|
||||
args: args{
|
||||
opt: []Option{WithLimit(-1)},
|
||||
},
|
||||
wantCnt: repo.defaultLimit + 1,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "default-limit",
|
||||
createCnt: repo.defaultLimit + 1,
|
||||
args: args{},
|
||||
wantCnt: repo.defaultLimit,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "custom-limit",
|
||||
createCnt: repo.defaultLimit + 1,
|
||||
args: args{
|
||||
opt: []Option{WithLimit(3)},
|
||||
},
|
||||
wantCnt: 3,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "withScopeId",
|
||||
createCnt: repo.defaultLimit + 1,
|
||||
args: args{
|
||||
opt: []Option{WithScopeId(composedOf.ScopeId)},
|
||||
},
|
||||
wantCnt: repo.defaultLimit,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "bad-withScopeId",
|
||||
createCnt: repo.defaultLimit + 1,
|
||||
args: args{
|
||||
opt: []Option{WithScopeId("o_thisIsNotValid")},
|
||||
},
|
||||
wantCnt: 0,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
require.NoError(conn.Where("1=1").Delete(AllocSession()).Error)
|
||||
testSessions := []*Session{}
|
||||
for i := 0; i < tt.createCnt; i++ {
|
||||
s := TestSession(t, conn, wrapper, composedOf)
|
||||
testSessions = append(testSessions, s)
|
||||
}
|
||||
assert.Equal(tt.createCnt, len(testSessions))
|
||||
got, err := repo.ListSessions(context.Background(), tt.args.opt...)
|
||||
if tt.wantErr {
|
||||
require.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tt.wantCnt, len(got))
|
||||
})
|
||||
}
|
||||
t.Run("withOrder", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
require.NoError(conn.Where("1=1").Delete(AllocSession()).Error)
|
||||
wantCnt := 5
|
||||
for i := 0; i < wantCnt; i++ {
|
||||
_ = TestSession(t, conn, wrapper, composedOf)
|
||||
}
|
||||
got, err := repo.ListSessions(context.Background(), WithOrder("create_time asc"))
|
||||
require.NoError(err)
|
||||
assert.Equal(wantCnt, len(got))
|
||||
|
||||
for i := 0; i < len(got)-1; i++ {
|
||||
first, err := ptypes.Timestamp(got[i].CreateTime.Timestamp)
|
||||
require.NoError(err)
|
||||
second, err := ptypes.Timestamp(got[i+1].CreateTime.Timestamp)
|
||||
require.NoError(err)
|
||||
assert.True(first.Before(second))
|
||||
}
|
||||
})
|
||||
t.Run("withUserId", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
require.NoError(conn.Where("1=1").Delete(AllocSession()).Error)
|
||||
wantCnt := 5
|
||||
for i := 0; i < wantCnt; i++ {
|
||||
_ = TestSession(t, conn, wrapper, composedOf)
|
||||
}
|
||||
s := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
got, err := repo.ListSessions(context.Background(), WithUserId(s.UserId))
|
||||
require.NoError(err)
|
||||
assert.Equal(1, len(got))
|
||||
assert.Equal(got[0].UserId, s.UserId)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_CreateSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
kms := kms.TestKms(t, conn, wrapper)
|
||||
repo, err := NewRepository(rw, rw, kms)
|
||||
require.NoError(t, err)
|
||||
|
||||
type args struct {
|
||||
composedOf ComposedOf
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
wantIsError error
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
args: args{
|
||||
composedOf: TestSessionParams(t, conn, wrapper, iamRepo),
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty-userId",
|
||||
args: args{
|
||||
composedOf: func() ComposedOf {
|
||||
c := TestSessionParams(t, conn, wrapper, iamRepo)
|
||||
c.UserId = ""
|
||||
return c
|
||||
}(),
|
||||
},
|
||||
wantErr: true,
|
||||
wantIsError: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "empty-hostId",
|
||||
args: args{
|
||||
composedOf: func() ComposedOf {
|
||||
c := TestSessionParams(t, conn, wrapper, iamRepo)
|
||||
c.HostId = ""
|
||||
return c
|
||||
}(),
|
||||
},
|
||||
wantErr: true,
|
||||
wantIsError: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "empty-targetId",
|
||||
args: args{
|
||||
composedOf: func() ComposedOf {
|
||||
c := TestSessionParams(t, conn, wrapper, iamRepo)
|
||||
c.TargetId = ""
|
||||
return c
|
||||
}(),
|
||||
},
|
||||
wantErr: true,
|
||||
wantIsError: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "empty-hostSetId",
|
||||
args: args{
|
||||
composedOf: func() ComposedOf {
|
||||
c := TestSessionParams(t, conn, wrapper, iamRepo)
|
||||
c.HostSetId = ""
|
||||
return c
|
||||
}(),
|
||||
},
|
||||
wantErr: true,
|
||||
wantIsError: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "empty-authTokenId",
|
||||
args: args{
|
||||
composedOf: func() ComposedOf {
|
||||
c := TestSessionParams(t, conn, wrapper, iamRepo)
|
||||
c.AuthTokenId = ""
|
||||
return c
|
||||
}(),
|
||||
},
|
||||
wantErr: true,
|
||||
wantIsError: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "empty-scopeId",
|
||||
args: args{
|
||||
composedOf: func() ComposedOf {
|
||||
c := TestSessionParams(t, conn, wrapper, iamRepo)
|
||||
c.ScopeId = ""
|
||||
return c
|
||||
}(),
|
||||
},
|
||||
wantErr: true,
|
||||
wantIsError: db.ErrInvalidParameter,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
s := &Session{
|
||||
UserId: tt.args.composedOf.UserId,
|
||||
HostId: tt.args.composedOf.HostId,
|
||||
TargetId: tt.args.composedOf.TargetId,
|
||||
HostSetId: tt.args.composedOf.HostSetId,
|
||||
AuthTokenId: tt.args.composedOf.AuthTokenId,
|
||||
ScopeId: tt.args.composedOf.ScopeId,
|
||||
}
|
||||
ses, st, privKey, err := repo.CreateSession(context.Background(), wrapper, s)
|
||||
if tt.wantErr {
|
||||
assert.Error(err)
|
||||
assert.Nil(ses)
|
||||
assert.Nil(st)
|
||||
if tt.wantIsError != nil {
|
||||
assert.True(errors.Is(err, tt.wantIsError))
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.NotNil(ses)
|
||||
assert.NotNil(privKey)
|
||||
assert.NotNil(st)
|
||||
assert.NotNil(ses.CreateTime)
|
||||
assert.NotNil(st.StartTime)
|
||||
assert.Equal(st.Status, StatusPending.String())
|
||||
foundSession, foundStates, err := repo.LookupSession(context.Background(), ses.PublicId)
|
||||
assert.NoError(err)
|
||||
assert.Equal(foundSession, ses)
|
||||
|
||||
err = db.TestVerifyOplog(t, rw, ses.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second))
|
||||
assert.Error(err)
|
||||
|
||||
require.Equal(1, len(foundStates))
|
||||
assert.Equal(foundStates[0].Status, StatusPending.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepository_UpdateState(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
kms := kms.TestKms(t, conn, wrapper)
|
||||
repo, err := NewRepository(rw, rw, kms)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
session *Session
|
||||
newStatus Status
|
||||
overrideSessionId *string
|
||||
overrideSessionVersion *uint32
|
||||
wantStateCnt int
|
||||
wantErr bool
|
||||
wantIsError error
|
||||
}{
|
||||
{
|
||||
name: "cancelling",
|
||||
session: TestDefaultSession(t, conn, wrapper, iamRepo),
|
||||
newStatus: StatusCanceling,
|
||||
wantStateCnt: 2,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "closed",
|
||||
session: func() *Session {
|
||||
s := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
_ = TestState(t, conn, s.PublicId, StatusActive)
|
||||
return s
|
||||
}(),
|
||||
newStatus: StatusTerminated,
|
||||
wantStateCnt: 3,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "bad-version",
|
||||
session: TestDefaultSession(t, conn, wrapper, iamRepo),
|
||||
newStatus: StatusCanceling,
|
||||
overrideSessionVersion: func() *uint32 {
|
||||
v := uint32(22)
|
||||
return &v
|
||||
}(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty-version",
|
||||
session: TestDefaultSession(t, conn, wrapper, iamRepo),
|
||||
newStatus: StatusCanceling,
|
||||
overrideSessionVersion: func() *uint32 {
|
||||
v := uint32(0)
|
||||
return &v
|
||||
}(),
|
||||
wantErr: true,
|
||||
wantIsError: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "bad-sessionId",
|
||||
session: TestDefaultSession(t, conn, wrapper, iamRepo),
|
||||
newStatus: StatusCanceling,
|
||||
overrideSessionId: func() *string {
|
||||
s := "s_thisIsNotValid"
|
||||
return &s
|
||||
}(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty-session",
|
||||
session: TestDefaultSession(t, conn, wrapper, iamRepo),
|
||||
newStatus: StatusCanceling,
|
||||
overrideSessionId: func() *string {
|
||||
s := ""
|
||||
return &s
|
||||
}(),
|
||||
wantErr: true,
|
||||
wantIsError: db.ErrInvalidParameter,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
var id string
|
||||
var version uint32
|
||||
switch {
|
||||
case tt.overrideSessionId != nil:
|
||||
id = *tt.overrideSessionId
|
||||
default:
|
||||
id = tt.session.PublicId
|
||||
}
|
||||
switch {
|
||||
case tt.overrideSessionVersion != nil:
|
||||
version = *tt.overrideSessionVersion
|
||||
default:
|
||||
version = tt.session.Version
|
||||
}
|
||||
|
||||
s, ss, err := repo.UpdateState(context.Background(), id, version, tt.newStatus)
|
||||
if tt.wantErr {
|
||||
require.Error(err)
|
||||
if tt.wantIsError != nil {
|
||||
assert.Truef(errors.Is(err, tt.wantIsError), "unexpected error %s", err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
require.NotNil(s)
|
||||
require.NotNil(ss)
|
||||
assert.Equal(tt.wantStateCnt, len(ss))
|
||||
assert.Equal(tt.newStatus.String(), ss[0].Status)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepository_ActivateState(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
kms := kms.TestKms(t, conn, wrapper)
|
||||
repo, err := NewRepository(rw, rw, kms)
|
||||
require.NoError(t, err)
|
||||
|
||||
tofu := TestTofu(t)
|
||||
tests := []struct {
|
||||
name string
|
||||
session *Session
|
||||
overrideSessionId *string
|
||||
overrideSessionVersion *uint32
|
||||
wantErr bool
|
||||
wantIsError error
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
session: TestDefaultSession(t, conn, wrapper, iamRepo),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "already-active",
|
||||
session: func() *Session {
|
||||
s := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
activeSession, _, err := repo.ActivateSession(context.Background(), s.PublicId, s.Version, tofu)
|
||||
require.NoError(t, err)
|
||||
return activeSession
|
||||
}(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "bad-session-id",
|
||||
session: TestDefaultSession(t, conn, wrapper, iamRepo),
|
||||
overrideSessionId: func() *string {
|
||||
id, err := newId()
|
||||
require.NoError(t, err)
|
||||
return &id
|
||||
}(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "bad-session-version",
|
||||
session: TestDefaultSession(t, conn, wrapper, iamRepo),
|
||||
overrideSessionVersion: func() *uint32 {
|
||||
v := uint32(100)
|
||||
return &v
|
||||
}(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty-session-id",
|
||||
session: TestDefaultSession(t, conn, wrapper, iamRepo),
|
||||
overrideSessionId: func() *string {
|
||||
id := ""
|
||||
return &id
|
||||
}(),
|
||||
wantErr: true,
|
||||
wantIsError: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "empty-session-version",
|
||||
session: TestDefaultSession(t, conn, wrapper, iamRepo),
|
||||
overrideSessionVersion: func() *uint32 {
|
||||
v := uint32(0)
|
||||
return &v
|
||||
}(),
|
||||
wantErr: true,
|
||||
wantIsError: db.ErrInvalidParameter,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
var id string
|
||||
var version uint32
|
||||
switch {
|
||||
case tt.overrideSessionId != nil:
|
||||
id = *tt.overrideSessionId
|
||||
default:
|
||||
id = tt.session.PublicId
|
||||
}
|
||||
switch {
|
||||
case tt.overrideSessionVersion != nil:
|
||||
version = *tt.overrideSessionVersion
|
||||
default:
|
||||
version = tt.session.Version
|
||||
}
|
||||
s, ss, err := repo.ActivateSession(context.Background(), id, version, tofu)
|
||||
if tt.wantErr {
|
||||
require.Error(err)
|
||||
if tt.wantIsError != nil {
|
||||
assert.Truef(errors.Is(err, tt.wantIsError), "unexpected error %s", err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
require.NotNil(s)
|
||||
require.NotNil(ss)
|
||||
assert.Equal(tofu, s.TofuToken)
|
||||
assert.Equal(2, len(ss))
|
||||
assert.Equal(StatusActive.String(), ss[0].Status)
|
||||
})
|
||||
t.Run("already active", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
session := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
s, ss, err := repo.ActivateSession(context.Background(), session.PublicId, 1, tofu)
|
||||
require.NoError(err)
|
||||
require.NotNil(s)
|
||||
require.NotNil(ss)
|
||||
assert.Equal(2, len(ss))
|
||||
assert.Equal(StatusActive.String(), ss[0].Status)
|
||||
|
||||
_, _, err = repo.ActivateSession(context.Background(), session.PublicId, 1, tofu)
|
||||
require.Error(err)
|
||||
|
||||
_, _, err = repo.ActivateSession(context.Background(), session.PublicId, 2, tofu)
|
||||
require.Error(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestRepository_UpdateSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
kms := kms.TestKms(t, conn, wrapper)
|
||||
repo, err := NewRepository(rw, rw, kms)
|
||||
require.NoError(t, err)
|
||||
serversRepo, err := servers.NewRepository(rw, rw, kms)
|
||||
require.NoError(t, err)
|
||||
|
||||
newServerFunc := func() string {
|
||||
id, err := uuid.GenerateUUID()
|
||||
require.NoError(t, err)
|
||||
worker := &servers.Server{
|
||||
Name: "test-session-worker-" + id,
|
||||
Type: servers.ServerTypeWorker.String(),
|
||||
Description: "Test Session Worker",
|
||||
Address: "127.0.0.1",
|
||||
}
|
||||
_, _, err = serversRepo.UpsertServer(context.Background(), worker)
|
||||
require.NoError(t, err)
|
||||
return worker.Name
|
||||
}
|
||||
|
||||
type args struct {
|
||||
terminationReason TerminationReason
|
||||
serverId string
|
||||
serverType string
|
||||
tofu []byte
|
||||
fieldMaskPaths []string
|
||||
opt []Option
|
||||
publicId *string // not updateable - db.ErrInvalidFieldMask
|
||||
userId string // not updateable - db.ErrInvalidFieldMask
|
||||
hostId string // not updateable - db.ErrInvalidFieldMask
|
||||
targetId string // not updateable - db.ErrInvalidFieldMask
|
||||
hostSetId string // not updateable - db.ErrInvalidFieldMask
|
||||
authTokenId string // not updateable - db.ErrInvalidFieldMask
|
||||
scopeId string // not updateable - db.ErrInvalidFieldMask
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantRowsUpdate int
|
||||
wantErr bool
|
||||
wantIsError error
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
args: args{
|
||||
terminationReason: Terminated,
|
||||
serverId: newServerFunc(),
|
||||
serverType: servers.ServerTypeWorker.String(),
|
||||
tofu: TestTofu(t),
|
||||
fieldMaskPaths: []string{"TerminationReason", "ServerId", "ServerType", "TofuToken"},
|
||||
},
|
||||
wantErr: false,
|
||||
wantRowsUpdate: 1,
|
||||
},
|
||||
{
|
||||
name: "publicId",
|
||||
args: args{
|
||||
publicId: func() *string {
|
||||
id, err := newId()
|
||||
require.NoError(t, err)
|
||||
return &id
|
||||
}(),
|
||||
fieldMaskPaths: []string{"PublicId"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantRowsUpdate: 0,
|
||||
wantIsError: db.ErrInvalidFieldMask,
|
||||
},
|
||||
{
|
||||
name: "userId",
|
||||
args: args{
|
||||
userId: func() string {
|
||||
org, _ := iam.TestScopes(t, iamRepo)
|
||||
u := iam.TestUser(t, iamRepo, org.PublicId)
|
||||
return u.PublicId
|
||||
}(),
|
||||
fieldMaskPaths: []string{"UserId"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantRowsUpdate: 0,
|
||||
wantIsError: db.ErrInvalidFieldMask,
|
||||
},
|
||||
{
|
||||
name: "hostId",
|
||||
args: args{
|
||||
hostId: func() string {
|
||||
_, proj := iam.TestScopes(t, iamRepo)
|
||||
cats := static.TestCatalogs(t, conn, proj.PublicId, 1)
|
||||
hosts := static.TestHosts(t, conn, cats[0].PublicId, 1)
|
||||
return hosts[0].PublicId
|
||||
}(),
|
||||
fieldMaskPaths: []string{"HostId"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantRowsUpdate: 0,
|
||||
wantIsError: db.ErrInvalidFieldMask,
|
||||
},
|
||||
{
|
||||
name: "targetId",
|
||||
args: args{
|
||||
targetId: func() string {
|
||||
_, proj := iam.TestScopes(t, iamRepo)
|
||||
tcpTarget := target.TestTcpTarget(t, conn, proj.PublicId, "test target")
|
||||
return tcpTarget.PublicId
|
||||
}(),
|
||||
fieldMaskPaths: []string{"TargetId"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantRowsUpdate: 0,
|
||||
wantIsError: db.ErrInvalidFieldMask,
|
||||
},
|
||||
{
|
||||
name: "hostSetId",
|
||||
args: args{
|
||||
hostSetId: func() string {
|
||||
_, proj := iam.TestScopes(t, iamRepo)
|
||||
cats := static.TestCatalogs(t, conn, proj.PublicId, 1)
|
||||
sets := static.TestSets(t, conn, cats[0].PublicId, 1)
|
||||
return sets[0].PublicId
|
||||
}(),
|
||||
fieldMaskPaths: []string{"HostSetId"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantRowsUpdate: 0,
|
||||
wantIsError: db.ErrInvalidFieldMask,
|
||||
},
|
||||
{
|
||||
name: "AuthTokenId",
|
||||
args: args{
|
||||
authTokenId: func() string {
|
||||
ctx := context.Background()
|
||||
org, _ := iam.TestScopes(t, iamRepo)
|
||||
authMethod := password.TestAuthMethods(t, conn, org.PublicId, 1)[0]
|
||||
acct := password.TestAccounts(t, conn, authMethod.GetPublicId(), 1)[0]
|
||||
user, err := iamRepo.LookupUserWithLogin(ctx, acct.GetPublicId(), iam.WithAutoVivify(true))
|
||||
require.NoError(t, err)
|
||||
|
||||
authTokenRepo, err := authtoken.NewRepository(rw, rw, kms)
|
||||
require.NoError(t, err)
|
||||
at, err := authTokenRepo.CreateAuthToken(ctx, user, acct.GetPublicId())
|
||||
require.NoError(t, err)
|
||||
return at.PublicId
|
||||
}(),
|
||||
fieldMaskPaths: []string{"AuthTokenId"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantRowsUpdate: 0,
|
||||
wantIsError: db.ErrInvalidFieldMask,
|
||||
},
|
||||
{
|
||||
name: "ScopeId",
|
||||
args: args{
|
||||
scopeId: func() string {
|
||||
_, proj := iam.TestScopes(t, iamRepo)
|
||||
return proj.PublicId
|
||||
}(),
|
||||
fieldMaskPaths: []string{"ScopeId"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantRowsUpdate: 0,
|
||||
wantIsError: db.ErrInvalidFieldMask,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
composedOf := TestSessionParams(t, conn, wrapper, iamRepo)
|
||||
s := TestSession(t, conn, wrapper, composedOf)
|
||||
|
||||
updateSession := AllocSession()
|
||||
updateSession.PublicId = s.PublicId
|
||||
if tt.args.publicId != nil {
|
||||
updateSession.PublicId = *tt.args.publicId
|
||||
}
|
||||
updateSession.ServerId = tt.args.serverId
|
||||
updateSession.ServerType = tt.args.serverType
|
||||
updateSession.TerminationReason = tt.args.terminationReason.String()
|
||||
if tt.args.tofu != nil {
|
||||
updateSession.TofuToken = make([]byte, len(tt.args.tofu))
|
||||
copy(updateSession.TofuToken, tt.args.tofu)
|
||||
}
|
||||
updateSession.Version = s.Version
|
||||
afterUpdateSession, afterUpdateState, updatedRows, err := repo.UpdateSession(context.Background(), &updateSession, updateSession.Version, tt.args.fieldMaskPaths, tt.args.opt...)
|
||||
|
||||
if tt.wantErr {
|
||||
require.Error(err)
|
||||
if tt.wantIsError != nil {
|
||||
assert.Truef(errors.Is(err, tt.wantIsError), "unexpected error: %s", err.Error())
|
||||
}
|
||||
assert.Nil(afterUpdateSession)
|
||||
assert.Nil(afterUpdateState)
|
||||
assert.Equal(0, updatedRows)
|
||||
err = db.TestVerifyOplog(t, rw, s.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))
|
||||
assert.Error(err)
|
||||
assert.True(errors.Is(db.ErrRecordNotFound, err))
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tt.wantRowsUpdate, updatedRows)
|
||||
require.NotNil(afterUpdateSession)
|
||||
require.NotNil(afterUpdateState)
|
||||
switch tt.name {
|
||||
case "valid-no-op":
|
||||
assert.Equal(s.UpdateTime, afterUpdateSession.UpdateTime)
|
||||
default:
|
||||
assert.NotEqual(s.UpdateTime, afterUpdateSession.UpdateTime)
|
||||
}
|
||||
foundSession, foundStates, err := repo.LookupSession(context.Background(), s.PublicId)
|
||||
require.NoError(err)
|
||||
assert.Equal(afterUpdateSession, foundSession)
|
||||
dbassrt := dbassert.New(t, rw)
|
||||
if tt.args.serverId == "" {
|
||||
dbassrt.IsNull(foundSession, "ServerId")
|
||||
}
|
||||
if tt.args.serverType == "" {
|
||||
dbassrt.IsNull(foundSession, "ServerType")
|
||||
}
|
||||
if tt.args.tofu == nil {
|
||||
dbassrt.IsNull(foundSession, "CtTofuToken")
|
||||
dbassrt.IsNull(foundSession, "KeyId")
|
||||
} else {
|
||||
dbassrt.NotNull(foundSession, "CtTofuToken")
|
||||
dbassrt.NotNull(foundSession, "KeyId")
|
||||
}
|
||||
assert.Equal(tt.args.terminationReason.String(), foundSession.TerminationReason)
|
||||
assert.Equal(tt.args.serverId, foundSession.ServerId)
|
||||
assert.Equal(tt.args.serverType, foundSession.ServerType)
|
||||
|
||||
err = db.TestVerifyOplog(t, rw, s.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))
|
||||
assert.Error(err)
|
||||
|
||||
switch {
|
||||
case tt.args.terminationReason != "":
|
||||
require.Equal(2, len(foundStates))
|
||||
assert.Equal(StatusTerminated.String(), foundStates[0].Status)
|
||||
assert.Equal(StatusPending.String(), foundStates[1].Status)
|
||||
default:
|
||||
require.Equal(1, len(foundStates))
|
||||
assert.Equal(StatusPending.String(), foundStates[0].Status)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRepository_DeleteSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
kms := kms.TestKms(t, conn, wrapper)
|
||||
repo, err := NewRepository(rw, rw, kms)
|
||||
require.NoError(t, err)
|
||||
|
||||
type args struct {
|
||||
session *Session
|
||||
opt []Option
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantRowsDeleted int
|
||||
wantErr bool
|
||||
wantErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
args: args{
|
||||
session: TestDefaultSession(t, conn, wrapper, iamRepo),
|
||||
},
|
||||
wantRowsDeleted: 1,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "no-public-id",
|
||||
args: args{
|
||||
session: func() *Session {
|
||||
s := AllocSession()
|
||||
return &s
|
||||
}(),
|
||||
},
|
||||
wantRowsDeleted: 0,
|
||||
wantErr: true,
|
||||
wantErrMsg: "delete session: missing public id invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "not-found",
|
||||
args: args{
|
||||
session: func() *Session {
|
||||
s := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
id, err := newId()
|
||||
require.NoError(t, err)
|
||||
s.PublicId = id
|
||||
return s
|
||||
}(),
|
||||
},
|
||||
wantRowsDeleted: 0,
|
||||
wantErr: true,
|
||||
wantErrMsg: "delete session: failed record not found for ",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
deletedRows, err := repo.DeleteSession(context.Background(), tt.args.session.PublicId, tt.args.opt...)
|
||||
if tt.wantErr {
|
||||
assert.Error(err)
|
||||
assert.Equal(0, deletedRows)
|
||||
assert.Contains(err.Error(), tt.wantErrMsg)
|
||||
err = db.TestVerifyOplog(t, rw, tt.args.session.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second))
|
||||
assert.Error(err)
|
||||
assert.True(errors.Is(db.ErrRecordNotFound, err))
|
||||
return
|
||||
}
|
||||
assert.NoError(err)
|
||||
assert.Equal(tt.wantRowsDeleted, deletedRows)
|
||||
foundSession, _, err := repo.LookupSession(context.Background(), tt.args.session.PublicId)
|
||||
assert.NoError(err)
|
||||
assert.Nil(foundSession)
|
||||
|
||||
err = db.TestVerifyOplog(t, rw, tt.args.session.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second))
|
||||
assert.Error(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewRepository(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
type args struct {
|
||||
r db.Reader
|
||||
w db.Writer
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *kms.Repository
|
||||
wantErr bool
|
||||
wantErrString string
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
args: args{
|
||||
r: rw,
|
||||
w: rw,
|
||||
},
|
||||
want: func() *kms.Repository {
|
||||
ret, err := kms.NewRepository(rw, rw)
|
||||
require.NoError(t, err)
|
||||
return ret
|
||||
}(),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "nil-writer",
|
||||
args: args{
|
||||
r: rw,
|
||||
w: nil,
|
||||
},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
wantErrString: "error creating db repository with nil writer",
|
||||
},
|
||||
{
|
||||
name: "nil-reader",
|
||||
args: args{
|
||||
r: nil,
|
||||
w: rw,
|
||||
},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
wantErrString: "error creating db repository with nil reader",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := kms.NewRepository(tt.args.r, tt.args.w)
|
||||
if tt.wantErr {
|
||||
require.Error(err)
|
||||
assert.Equal(err.Error(), tt.wantErrString)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"math/big"
|
||||
mathrand "math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/db/timestamp"
|
||||
wrapping "github.com/hashicorp/go-kms-wrapping"
|
||||
"github.com/hashicorp/go-kms-wrapping/structwrapping"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultSessionTableName = "session"
|
||||
)
|
||||
|
||||
// ComposedOf defines the boundary data that is referenced to compose a session.
|
||||
type ComposedOf struct {
|
||||
// UserId of the session
|
||||
UserId string
|
||||
// HostId of the session
|
||||
HostId string
|
||||
// TargetId of the session
|
||||
TargetId string
|
||||
// HostSetId of the session
|
||||
HostSetId string
|
||||
// AuthTokenId of the session
|
||||
AuthTokenId string
|
||||
// ScopeId of the session
|
||||
ScopeId string
|
||||
}
|
||||
|
||||
// Session contains information about a user's session with a target
|
||||
type Session struct {
|
||||
// PublicId is used to access the session via an API
|
||||
PublicId string `json:"public_id,omitempty" gorm:"primary_key"`
|
||||
// UserId for the session
|
||||
UserId string `json:"user_id,omitempty" gorm:"default:null"`
|
||||
// HostId of the session
|
||||
HostId string `json:"host_id,omitempty" gorm:"default:null"`
|
||||
// ServerId that proxied the session
|
||||
ServerId string `json:"server_id,omitempty" gorm:"default:null"`
|
||||
// ServerType that proxied the session
|
||||
ServerType string `json:"server_type,omitempty" gorm:"default:null"`
|
||||
// TargetId for the session
|
||||
TargetId string `json:"target_id,omitempty" gorm:"default:null"`
|
||||
// HostSetId for the session
|
||||
HostSetId string `json:"host_set_id,omitempty" gorm:"default:null"`
|
||||
// AuthTokenId for the session
|
||||
AuthTokenId string `json:"auth_token_id,omitempty" gorm:"default:null"`
|
||||
// ScopeId for the session
|
||||
ScopeId string `json:"scope_id,omitempty" gorm:"default:null"`
|
||||
// Certificate to use when connecting (or if using custom certs, to
|
||||
// serve as the "login"). Raw DER bytes. Private key is not, and should not be
|
||||
// stored in the database.
|
||||
Certificate []byte `json:"certificate,omitempty" gorm:"default:null"`
|
||||
// ExpirationTime - after this time the connection will be expired, e.g. forcefully terminated
|
||||
ExpirationTime *timestamp.Timestamp `json:"expiration_time,omitempty" gorm:"default:null"`
|
||||
// CtTofuToken is the ciphertext Tofutoken value stored in the database
|
||||
CtTofuToken []byte `json:"ct_tofu_token,omitempty" gorm:"column:tofu_token;default:null" wrapping:"ct,tofu_token"`
|
||||
// TofuToken - plain text of the "trust on first use" token for session
|
||||
TofuToken []byte `json:"tofu_token,omitempty" gorm:"-" wrapping:"pt,tofu_token"`
|
||||
// termination_reason for the session
|
||||
TerminationReason string `json:"termination_reason,omitempty" gorm:"default:null"`
|
||||
// CreateTime from the RDBMS
|
||||
CreateTime *timestamp.Timestamp `json:"create_time,omitempty" gorm:"default:current_timestamp"`
|
||||
// UpdateTime from the RDBMS
|
||||
UpdateTime *timestamp.Timestamp `json:"update_time,omitempty" gorm:"default:current_timestamp"`
|
||||
// Version for the session
|
||||
Version uint32 `json:"version,omitempty" gorm:"default:null"`
|
||||
|
||||
// key_id is the key ID that was used for the encryption operation. It can be
|
||||
// used to identify a specific version of the key needed to decrypt the value,
|
||||
// which is useful for caching purposes.
|
||||
// @inject_tag: `gorm:"not_null"`
|
||||
KeyId string `json:"key_id,omitempty" gorm:"not_null"`
|
||||
|
||||
tableName string `gorm:"-"`
|
||||
}
|
||||
|
||||
func (s *Session) GetPublicId() string {
|
||||
return s.PublicId
|
||||
}
|
||||
|
||||
var _ Cloneable = (*Session)(nil)
|
||||
var _ db.VetForWriter = (*Session)(nil)
|
||||
|
||||
// New creates a new in memory session. WithExpirationTime option is support to
|
||||
// set the session's expiration time.
|
||||
func New(c ComposedOf, opt ...Option) (*Session, error) {
|
||||
opts := getOpts(opt...)
|
||||
|
||||
s := Session{
|
||||
UserId: c.UserId,
|
||||
HostId: c.HostId,
|
||||
TargetId: c.TargetId,
|
||||
HostSetId: c.HostSetId,
|
||||
AuthTokenId: c.AuthTokenId,
|
||||
ScopeId: c.ScopeId,
|
||||
ExpirationTime: opts.withExpirationTime,
|
||||
}
|
||||
if err := s.validateNewSession("new session:"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
// AllocSession will allocate a Session
|
||||
func AllocSession() Session {
|
||||
return Session{}
|
||||
}
|
||||
|
||||
// Clone creates a clone of the Session
|
||||
func (s *Session) Clone() interface{} {
|
||||
clone := &Session{
|
||||
PublicId: s.PublicId,
|
||||
UserId: s.UserId,
|
||||
HostId: s.HostId,
|
||||
ServerId: s.ServerId,
|
||||
ServerType: s.ServerType,
|
||||
TargetId: s.TargetId,
|
||||
HostSetId: s.HostSetId,
|
||||
AuthTokenId: s.AuthTokenId,
|
||||
ScopeId: s.ScopeId,
|
||||
TerminationReason: s.TerminationReason,
|
||||
Version: s.Version,
|
||||
}
|
||||
if s.TofuToken != nil {
|
||||
clone.TofuToken = make([]byte, len(s.TofuToken))
|
||||
copy(clone.TofuToken, s.TofuToken)
|
||||
}
|
||||
if s.CtTofuToken != nil {
|
||||
clone.CtTofuToken = make([]byte, len(s.CtTofuToken))
|
||||
copy(clone.CtTofuToken, s.CtTofuToken)
|
||||
}
|
||||
if s.Certificate != nil {
|
||||
clone.Certificate = make([]byte, len(s.Certificate))
|
||||
copy(clone.Certificate, s.Certificate)
|
||||
}
|
||||
if s.ExpirationTime != nil {
|
||||
clone.ExpirationTime = ×tamp.Timestamp{
|
||||
Timestamp: ×tamppb.Timestamp{
|
||||
Seconds: s.ExpirationTime.Timestamp.Seconds,
|
||||
Nanos: s.ExpirationTime.Timestamp.Nanos,
|
||||
},
|
||||
}
|
||||
}
|
||||
if s.CreateTime != nil {
|
||||
clone.CreateTime = ×tamp.Timestamp{
|
||||
Timestamp: ×tamppb.Timestamp{
|
||||
Seconds: s.CreateTime.Timestamp.Seconds,
|
||||
Nanos: s.CreateTime.Timestamp.Nanos,
|
||||
},
|
||||
}
|
||||
}
|
||||
if s.UpdateTime != nil {
|
||||
clone.UpdateTime = ×tamp.Timestamp{
|
||||
Timestamp: ×tamppb.Timestamp{
|
||||
Seconds: s.UpdateTime.Timestamp.Seconds,
|
||||
Nanos: s.UpdateTime.Timestamp.Nanos,
|
||||
},
|
||||
}
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
// VetForWrite implements db.VetForWrite() interface and validates the session
|
||||
// before it's written.
|
||||
func (s *Session) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error {
|
||||
opts := db.GetOpts(opt...)
|
||||
if s.PublicId == "" {
|
||||
return fmt.Errorf("session vet for write: missing public id: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
switch opType {
|
||||
case db.CreateOp:
|
||||
if err := s.validateNewSession("session vet for write:"); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(s.Certificate) == 0 {
|
||||
return fmt.Errorf("session vet for write: certificate is missing: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
case db.UpdateOp:
|
||||
switch {
|
||||
case contains(opts.WithFieldMaskPaths, "PublicId"):
|
||||
return fmt.Errorf("session vet for write: public id is immutable: %w", db.ErrInvalidParameter)
|
||||
case contains(opts.WithFieldMaskPaths, "UserId"):
|
||||
return fmt.Errorf("session vet for write: user id is immutable: %w", db.ErrInvalidParameter)
|
||||
case contains(opts.WithFieldMaskPaths, "HostId"):
|
||||
return fmt.Errorf("session vet for write: host id is immutable: %w", db.ErrInvalidParameter)
|
||||
case contains(opts.WithFieldMaskPaths, "TargetId"):
|
||||
return fmt.Errorf("session vet for write: target id is immutable: %w", db.ErrInvalidParameter)
|
||||
case contains(opts.WithFieldMaskPaths, "HostSetId"):
|
||||
return fmt.Errorf("session vet for write: host set id is immutable: %w", db.ErrInvalidParameter)
|
||||
case contains(opts.WithFieldMaskPaths, "AuthTokenId"):
|
||||
return fmt.Errorf("session vet for write: auth token id is immutable: %w", db.ErrInvalidParameter)
|
||||
case contains(opts.WithFieldMaskPaths, "Certificate"):
|
||||
return fmt.Errorf("session vet for write: certificate is immutable: %w", db.ErrInvalidParameter)
|
||||
case contains(opts.WithFieldMaskPaths, "CreateTime"):
|
||||
return fmt.Errorf("session vet for write: create time is immutable: %w", db.ErrInvalidParameter)
|
||||
case contains(opts.WithFieldMaskPaths, "UpdateTime"):
|
||||
return fmt.Errorf("session vet for write: update time is immutable: %w", db.ErrInvalidParameter)
|
||||
case contains(opts.WithFieldMaskPaths, "TerminationReason"):
|
||||
if _, err := convertToReason(s.TerminationReason); err != nil {
|
||||
return fmt.Errorf("session vet for write: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TableName returns the tablename to override the default gorm table name
|
||||
func (s *Session) TableName() string {
|
||||
if s.tableName != "" {
|
||||
return s.tableName
|
||||
}
|
||||
return defaultSessionTableName
|
||||
}
|
||||
|
||||
// SetTableName sets the tablename and satisfies the ReplayableMessage
|
||||
// interface. If the caller attempts to set the name to "" the name will be
|
||||
// reset to the default name.
|
||||
func (s *Session) SetTableName(n string) {
|
||||
s.tableName = n
|
||||
}
|
||||
|
||||
// validateNewSession checks everything but the session's PublicId
|
||||
func (s *Session) validateNewSession(errorPrefix string) error {
|
||||
if s.UserId == "" {
|
||||
return fmt.Errorf("%s missing user id: %w", errorPrefix, db.ErrInvalidParameter)
|
||||
}
|
||||
if s.HostId == "" {
|
||||
return fmt.Errorf("%s missing host id: %w", errorPrefix, db.ErrInvalidParameter)
|
||||
}
|
||||
if s.TargetId == "" {
|
||||
return fmt.Errorf("%s missing target id: %w", errorPrefix, db.ErrInvalidParameter)
|
||||
}
|
||||
if s.HostSetId == "" {
|
||||
return fmt.Errorf("%s missing host set id: %w", errorPrefix, db.ErrInvalidParameter)
|
||||
}
|
||||
if s.AuthTokenId == "" {
|
||||
return fmt.Errorf("%s missing auth token id: %w", errorPrefix, db.ErrInvalidParameter)
|
||||
}
|
||||
if s.ScopeId == "" {
|
||||
return fmt.Errorf("%s missing scope id: %w", errorPrefix, db.ErrInvalidParameter)
|
||||
}
|
||||
if s.TerminationReason != "" {
|
||||
return fmt.Errorf("%s termination reason must be empty: %w", errorPrefix, db.ErrInvalidParameter)
|
||||
}
|
||||
if s.ServerId != "" {
|
||||
return fmt.Errorf("%s server id must be empty: %w", errorPrefix, db.ErrInvalidParameter)
|
||||
}
|
||||
if s.ServerType != "" {
|
||||
return fmt.Errorf("%s server type must be empty: %w", errorPrefix, db.ErrInvalidParameter)
|
||||
}
|
||||
if s.TofuToken != nil {
|
||||
return fmt.Errorf("%s tofu token must be empty: %w", errorPrefix, db.ErrInvalidParameter)
|
||||
}
|
||||
if s.CtTofuToken != nil {
|
||||
return fmt.Errorf("%s ct must be empty: %w", errorPrefix, db.ErrInvalidParameter)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func contains(ss []string, t string) bool {
|
||||
for _, s := range ss {
|
||||
if strings.EqualFold(s, t) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func newCert(wrapper wrapping.Wrapper, userId, jobId string) (ed25519.PrivateKey, []byte, error) {
|
||||
if wrapper == nil {
|
||||
return nil, nil, fmt.Errorf("new session cert: missing wrapper: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if userId == "" {
|
||||
return nil, nil, fmt.Errorf("new session cert: missing user id: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if jobId == "" {
|
||||
return nil, nil, fmt.Errorf("new session cert: missing job id: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
pubKey, privKey, err := DeriveED25519Key(wrapper, userId, jobId)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("new session cert: ")
|
||||
}
|
||||
template := &x509.Certificate{
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{
|
||||
x509.ExtKeyUsageServerAuth,
|
||||
x509.ExtKeyUsageClientAuth,
|
||||
},
|
||||
DNSNames: []string{jobId},
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement | x509.KeyUsageCertSign,
|
||||
SerialNumber: big.NewInt(mathrand.Int63()),
|
||||
NotBefore: time.Now().Add(-1 * time.Minute),
|
||||
NotAfter: time.Now().Add(5 * time.Minute),
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
}
|
||||
|
||||
certBytes, err := x509.CreateCertificate(rand.Reader, template, template, pubKey, privKey)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("new session cert: %w", err)
|
||||
}
|
||||
return privKey, certBytes, nil
|
||||
}
|
||||
|
||||
func (s *Session) encrypt(ctx context.Context, cipher wrapping.Wrapper) error {
|
||||
if err := structwrapping.WrapStruct(ctx, cipher, s, nil); err != nil {
|
||||
return fmt.Errorf("error encrypting session: %w", err)
|
||||
}
|
||||
s.KeyId = cipher.KeyID()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Session) decrypt(ctx context.Context, cipher wrapping.Wrapper) error {
|
||||
if err := structwrapping.UnwrapStruct(ctx, cipher, s, nil); err != nil {
|
||||
return fmt.Errorf("error decrypting session: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/db/timestamp"
|
||||
"github.com/hashicorp/boundary/internal/iam"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSession_Create(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
wrapper := db.TestWrapper(t)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
|
||||
composedOf := TestSessionParams(t, conn, wrapper, iamRepo)
|
||||
future, err := ptypes.TimestampProto(time.Now().Add(time.Hour))
|
||||
require.NoError(t, err)
|
||||
exp := ×tamp.Timestamp{Timestamp: future}
|
||||
|
||||
type args struct {
|
||||
composedOf ComposedOf
|
||||
opt []Option
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *Session
|
||||
wantErr bool
|
||||
wantIsErr error
|
||||
create bool
|
||||
wantCreateErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
args: args{
|
||||
composedOf: composedOf,
|
||||
opt: []Option{WithExpirationTime(exp)},
|
||||
},
|
||||
want: &Session{
|
||||
UserId: composedOf.UserId,
|
||||
HostId: composedOf.HostId,
|
||||
TargetId: composedOf.TargetId,
|
||||
HostSetId: composedOf.HostSetId,
|
||||
AuthTokenId: composedOf.AuthTokenId,
|
||||
ScopeId: composedOf.ScopeId,
|
||||
},
|
||||
create: true,
|
||||
},
|
||||
{
|
||||
name: "empty-userId",
|
||||
args: args{
|
||||
composedOf: func() ComposedOf {
|
||||
c := composedOf
|
||||
c.UserId = ""
|
||||
return c
|
||||
}(),
|
||||
},
|
||||
wantErr: true,
|
||||
wantIsErr: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "empty-hostId",
|
||||
args: args{
|
||||
composedOf: func() ComposedOf {
|
||||
c := composedOf
|
||||
c.HostId = ""
|
||||
return c
|
||||
}(),
|
||||
},
|
||||
wantErr: true,
|
||||
wantIsErr: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "empty-targetId",
|
||||
args: args{
|
||||
composedOf: func() ComposedOf {
|
||||
c := composedOf
|
||||
c.TargetId = ""
|
||||
return c
|
||||
}(),
|
||||
},
|
||||
wantErr: true,
|
||||
wantIsErr: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "empty-hostSetId",
|
||||
args: args{
|
||||
composedOf: func() ComposedOf {
|
||||
c := composedOf
|
||||
c.HostSetId = ""
|
||||
return c
|
||||
}(),
|
||||
},
|
||||
wantErr: true,
|
||||
wantIsErr: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "empty-authTokenId",
|
||||
args: args{
|
||||
composedOf: func() ComposedOf {
|
||||
c := composedOf
|
||||
c.AuthTokenId = ""
|
||||
return c
|
||||
}(),
|
||||
},
|
||||
wantErr: true,
|
||||
wantIsErr: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "empty-scopeId",
|
||||
args: args{
|
||||
composedOf: func() ComposedOf {
|
||||
c := composedOf
|
||||
c.ScopeId = ""
|
||||
return c
|
||||
}(),
|
||||
},
|
||||
wantErr: true,
|
||||
wantIsErr: db.ErrInvalidParameter,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := New(tt.args.composedOf)
|
||||
if tt.wantErr {
|
||||
require.Error(err)
|
||||
assert.True(errors.Is(err, tt.wantIsErr))
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tt.want, got)
|
||||
if tt.create {
|
||||
id, err := db.NewPublicId(SessionPrefix)
|
||||
require.NoError(err)
|
||||
got.PublicId = id
|
||||
_, certBytes, err := newCert(wrapper, got.UserId, id)
|
||||
require.NoError(err)
|
||||
got.Certificate = certBytes
|
||||
err = db.New(conn).Create(context.Background(), got)
|
||||
if tt.wantCreateErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
} else {
|
||||
assert.NoError(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSession_Delete(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
session *Session
|
||||
wantRowsDeleted int
|
||||
wantErr bool
|
||||
wantErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
session: TestDefaultSession(t, conn, wrapper, iamRepo),
|
||||
wantErr: false,
|
||||
wantRowsDeleted: 1,
|
||||
},
|
||||
{
|
||||
name: "bad-id",
|
||||
session: func() *Session {
|
||||
s := AllocSession()
|
||||
id, err := db.NewPublicId(SessionPrefix)
|
||||
require.NoError(t, err)
|
||||
s.PublicId = id
|
||||
return &s
|
||||
}(),
|
||||
wantErr: false,
|
||||
wantRowsDeleted: 0,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
deleteSession := AllocSession()
|
||||
deleteSession.PublicId = tt.session.PublicId
|
||||
deletedRows, err := rw.Delete(context.Background(), &deleteSession)
|
||||
if tt.wantErr {
|
||||
require.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
if tt.wantRowsDeleted == 0 {
|
||||
assert.Equal(tt.wantRowsDeleted, deletedRows)
|
||||
return
|
||||
}
|
||||
assert.Equal(tt.wantRowsDeleted, deletedRows)
|
||||
foundSession := AllocSession()
|
||||
foundSession.PublicId = tt.session.PublicId
|
||||
err = rw.LookupById(context.Background(), &foundSession)
|
||||
require.Error(err)
|
||||
assert.True(errors.Is(db.ErrRecordNotFound, err))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSession_Clone(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
wrapper := db.TestWrapper(t)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
s := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
cp := s.Clone()
|
||||
assert.Equal(cp.(*Session), s)
|
||||
})
|
||||
t.Run("not-equal", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
s := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
s2 := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
|
||||
cp := s.Clone()
|
||||
assert.NotEqual(cp.(*Session), s2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSession_SetTableName(t *testing.T) {
|
||||
t.Parallel()
|
||||
defaultTableName := defaultSessionTableName
|
||||
tests := []struct {
|
||||
name string
|
||||
setNameTo string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "new-name",
|
||||
setNameTo: "new-name",
|
||||
want: "new-name",
|
||||
},
|
||||
{
|
||||
name: "reset to default",
|
||||
setNameTo: "",
|
||||
want: defaultTableName,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
def := AllocSession()
|
||||
require.Equal(defaultTableName, def.TableName())
|
||||
s := AllocSession()
|
||||
s.SetTableName(tt.setNameTo)
|
||||
assert.Equal(tt.want, s.TableName())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/db/timestamp"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultStateTableName = "session_state"
|
||||
)
|
||||
|
||||
// Status of the session's state
|
||||
type Status string
|
||||
|
||||
const (
|
||||
StatusPending Status = "pending"
|
||||
StatusActive Status = "active"
|
||||
StatusCanceling Status = "canceling"
|
||||
StatusTerminated Status = "terminated"
|
||||
)
|
||||
|
||||
// String representation of the state's status
|
||||
func (s Status) String() string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
// State of the session
|
||||
type State struct {
|
||||
// SessionId references the session public id
|
||||
SessionId string `json:"session_id,omitempty" gorm:"primary_key"`
|
||||
// status of the session
|
||||
Status string `json:"status,omitempty" gorm:"column:state"`
|
||||
// PreviousEndTime from the RDBMS
|
||||
PreviousEndTime *timestamp.Timestamp `json:"previous_end_time,omitempty" gorm:"default:current_timestamp"`
|
||||
// StartTime from the RDBMS
|
||||
StartTime *timestamp.Timestamp `json:"start_time,omitempty" gorm:"default:current_timestamp;primary_key"`
|
||||
// EndTime from the RDBMS
|
||||
EndTime *timestamp.Timestamp `json:"end_time,omitempty" gorm:"default:current_timestamp"`
|
||||
|
||||
tableName string `gorm:"-"`
|
||||
}
|
||||
|
||||
var _ Cloneable = (*State)(nil)
|
||||
var _ db.VetForWriter = (*State)(nil)
|
||||
|
||||
// NewState creates a new in memory session state. No options
|
||||
// are currently supported.
|
||||
func NewState(session_id string, state Status, opt ...Option) (*State, error) {
|
||||
s := State{
|
||||
SessionId: session_id,
|
||||
Status: state.String(),
|
||||
}
|
||||
|
||||
if err := s.validate("new session state:"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
// allocState will allocate a State
|
||||
func allocState() State {
|
||||
return State{}
|
||||
}
|
||||
|
||||
// Clone creates a clone of the State
|
||||
func (s *State) Clone() interface{} {
|
||||
clone := &State{
|
||||
SessionId: s.SessionId,
|
||||
Status: s.Status,
|
||||
}
|
||||
if s.PreviousEndTime != nil {
|
||||
clone.PreviousEndTime = ×tamp.Timestamp{
|
||||
Timestamp: ×tamppb.Timestamp{
|
||||
Seconds: s.PreviousEndTime.Timestamp.Seconds,
|
||||
Nanos: s.PreviousEndTime.Timestamp.Nanos,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if s.StartTime != nil {
|
||||
clone.StartTime = ×tamp.Timestamp{
|
||||
Timestamp: ×tamppb.Timestamp{
|
||||
Seconds: s.StartTime.Timestamp.Seconds,
|
||||
Nanos: s.StartTime.Timestamp.Nanos,
|
||||
},
|
||||
}
|
||||
}
|
||||
if s.EndTime != nil {
|
||||
clone.EndTime = ×tamp.Timestamp{
|
||||
Timestamp: ×tamppb.Timestamp{
|
||||
Seconds: s.EndTime.Timestamp.Seconds,
|
||||
Nanos: s.EndTime.Timestamp.Nanos,
|
||||
},
|
||||
}
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
// VetForWrite implements db.VetForWrite() interface and validates the state
|
||||
// before it's written.
|
||||
func (s *State) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error {
|
||||
if err := s.validate("session state vet for write:"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TableName returns the tablename to override the default gorm table name
|
||||
func (s *State) TableName() string {
|
||||
if s.tableName != "" {
|
||||
return s.tableName
|
||||
}
|
||||
return defaultStateTableName
|
||||
}
|
||||
|
||||
// SetTableName sets the tablename and satisfies the ReplayableMessage
|
||||
// interface. If the caller attempts to set the name to "" the name will be
|
||||
// reset to the default name.
|
||||
func (s *State) SetTableName(n string) {
|
||||
s.tableName = n
|
||||
}
|
||||
|
||||
// validate checks the session state
|
||||
func (s *State) validate(errorPrefix string) error {
|
||||
if s.Status == "" {
|
||||
return fmt.Errorf("%s missing status: %w", errorPrefix, db.ErrInvalidParameter)
|
||||
}
|
||||
if s.SessionId == "" {
|
||||
return fmt.Errorf("%s missing session id: %w", errorPrefix, db.ErrInvalidParameter)
|
||||
}
|
||||
if s.StartTime != nil {
|
||||
return fmt.Errorf("%s start time is not settable: %w", errorPrefix, db.ErrInvalidParameter)
|
||||
}
|
||||
if s.EndTime != nil {
|
||||
return fmt.Errorf("%s end time is not settable: %w", errorPrefix, db.ErrInvalidParameter)
|
||||
}
|
||||
if s.PreviousEndTime != nil {
|
||||
return fmt.Errorf("%s previous end time is not settable: %w", errorPrefix, db.ErrInvalidParameter)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/iam"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestState_Create(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
wrapper := db.TestWrapper(t)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
session := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
|
||||
type args struct {
|
||||
sessionId string
|
||||
status Status
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *State
|
||||
wantErr bool
|
||||
wantIsErr error
|
||||
create bool
|
||||
wantCreateErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
args: args{
|
||||
sessionId: session.PublicId,
|
||||
status: StatusPending,
|
||||
},
|
||||
want: &State{
|
||||
SessionId: session.PublicId,
|
||||
Status: StatusPending.String(),
|
||||
},
|
||||
create: true,
|
||||
},
|
||||
{
|
||||
name: "empty-sessionId",
|
||||
args: args{
|
||||
status: StatusPending,
|
||||
},
|
||||
wantErr: true,
|
||||
wantIsErr: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "empty-status",
|
||||
args: args{
|
||||
sessionId: session.PublicId,
|
||||
},
|
||||
wantErr: true,
|
||||
wantIsErr: db.ErrInvalidParameter,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := NewState(tt.args.sessionId, tt.args.status)
|
||||
if tt.wantErr {
|
||||
require.Error(err)
|
||||
assert.True(errors.Is(err, tt.wantIsErr))
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tt.want, got)
|
||||
if tt.create {
|
||||
err = db.New(conn).Create(context.Background(), got)
|
||||
if tt.wantCreateErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
} else {
|
||||
assert.NoError(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestState_Delete(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
session := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
session2 := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
state *State
|
||||
deleteStateId string
|
||||
wantRowsDeleted int
|
||||
wantErr bool
|
||||
wantErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
state: TestState(t, conn, session.PublicId, StatusTerminated),
|
||||
wantErr: false,
|
||||
wantRowsDeleted: 1,
|
||||
},
|
||||
{
|
||||
name: "bad-id",
|
||||
state: TestState(t, conn, session2.PublicId, StatusTerminated),
|
||||
deleteStateId: func() string {
|
||||
id, err := db.NewPublicId(StatePrefix)
|
||||
require.NoError(t, err)
|
||||
return id
|
||||
}(),
|
||||
wantErr: false,
|
||||
wantRowsDeleted: 0,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
var initialState State
|
||||
err := rw.LookupWhere(context.Background(), &initialState, "session_id = ? and state = ?", tt.state.SessionId, tt.state.Status)
|
||||
require.NoError(err)
|
||||
|
||||
deleteState := allocState()
|
||||
if tt.deleteStateId != "" {
|
||||
deleteState.SessionId = tt.deleteStateId
|
||||
} else {
|
||||
deleteState.SessionId = tt.state.SessionId
|
||||
}
|
||||
deleteState.StartTime = initialState.StartTime
|
||||
deletedRows, err := rw.Delete(context.Background(), &deleteState)
|
||||
if tt.wantErr {
|
||||
require.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
if tt.wantRowsDeleted == 0 {
|
||||
assert.Equal(tt.wantRowsDeleted, deletedRows)
|
||||
return
|
||||
}
|
||||
assert.Equal(tt.wantRowsDeleted, deletedRows)
|
||||
foundState := allocState()
|
||||
err = rw.LookupWhere(context.Background(), &foundState, "session_id = ? and start_time = ?", tt.state.SessionId, initialState.StartTime)
|
||||
require.Error(err)
|
||||
assert.True(errors.Is(db.ErrRecordNotFound, err))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestState_Clone(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
wrapper := db.TestWrapper(t)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
s := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
state := TestState(t, conn, s.PublicId, StatusPending)
|
||||
cp := state.Clone()
|
||||
assert.Equal(cp.(*State), state)
|
||||
})
|
||||
t.Run("not-equal", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
s := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
s2 := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
state := TestState(t, conn, s.PublicId, StatusPending)
|
||||
state2 := TestState(t, conn, s2.PublicId, StatusPending)
|
||||
|
||||
cp := state.Clone()
|
||||
assert.NotEqual(cp.(*State), state2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestState_SetTableName(t *testing.T) {
|
||||
t.Parallel()
|
||||
defaultTableName := defaultStateTableName
|
||||
tests := []struct {
|
||||
name string
|
||||
setNameTo string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "new-name",
|
||||
setNameTo: "new-name",
|
||||
want: "new-name",
|
||||
},
|
||||
{
|
||||
name: "reset to default",
|
||||
setNameTo: "",
|
||||
want: defaultTableName,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
def := allocState()
|
||||
require.Equal(defaultTableName, def.TableName())
|
||||
s := allocState()
|
||||
s.SetTableName(tt.setNameTo)
|
||||
assert.Equal(tt.want, s.TableName())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
)
|
||||
|
||||
// TerminationReason of the session
|
||||
type TerminationReason string
|
||||
|
||||
const (
|
||||
UnknownTermination TerminationReason = "unknown"
|
||||
TimedOut TerminationReason = "timed out"
|
||||
ClosedByUser TerminationReason = "closed by end-user"
|
||||
Terminated TerminationReason = "terminated"
|
||||
NetworkError TerminationReason = "network error"
|
||||
SystemError TerminationReason = "system error"
|
||||
)
|
||||
|
||||
// String representation of the termination reason
|
||||
func (r TerminationReason) String() string {
|
||||
return string(r)
|
||||
}
|
||||
func convertToReason(s string) (TerminationReason, error) {
|
||||
switch s {
|
||||
case UnknownTermination.String():
|
||||
return UnknownTermination, nil
|
||||
case TimedOut.String():
|
||||
return TimedOut, nil
|
||||
case ClosedByUser.String():
|
||||
return ClosedByUser, nil
|
||||
case Terminated.String():
|
||||
return Terminated, nil
|
||||
case NetworkError.String():
|
||||
return NetworkError, nil
|
||||
case SystemError.String():
|
||||
return SystemError, nil
|
||||
default:
|
||||
return "", fmt.Errorf("termination reason: %s is not a valid reason: %w", s, db.ErrInvalidParameter)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
"github.com/hashicorp/boundary/internal/auth/password"
|
||||
"github.com/hashicorp/boundary/internal/authtoken"
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/db/timestamp"
|
||||
"github.com/hashicorp/boundary/internal/host/static"
|
||||
"github.com/hashicorp/boundary/internal/iam"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/hashicorp/boundary/internal/target"
|
||||
wrapping "github.com/hashicorp/go-kms-wrapping"
|
||||
"github.com/hashicorp/vault/sdk/helper/base62"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestConnection creates a test connection for the sessionId in the repository.
|
||||
func TestConnection(t *testing.T, conn *gorm.DB, sessionId, clientTcpAddr string, clientTcpPort uint32, backendTcpAddr string, backendTcpPort uint32) *Connection {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
rw := db.New(conn)
|
||||
c, err := NewConnection(sessionId, clientTcpAddr, clientTcpPort, backendTcpAddr, backendTcpPort)
|
||||
require.NoError(err)
|
||||
id, err := newConnectionId()
|
||||
require.NoError(err)
|
||||
c.PublicId = id
|
||||
err = rw.Create(context.Background(), c)
|
||||
require.NoError(err)
|
||||
return c
|
||||
}
|
||||
|
||||
// TestConnectionState creates a test connection state for the connectionId in the repository.
|
||||
func TestConnectionState(t *testing.T, conn *gorm.DB, connectionId string, state ConnectionStatus) *ConnectionState {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
rw := db.New(conn)
|
||||
s, err := NewConnectionState(connectionId, state)
|
||||
require.NoError(err)
|
||||
err = rw.Create(context.Background(), s)
|
||||
require.NoError(err)
|
||||
return s
|
||||
}
|
||||
|
||||
// TestState creates a test state for the sessionId in the repository.
|
||||
func TestState(t *testing.T, conn *gorm.DB, sessionId string, state Status) *State {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
rw := db.New(conn)
|
||||
s, err := NewState(sessionId, state)
|
||||
require.NoError(err)
|
||||
err = rw.Create(context.Background(), s)
|
||||
require.NoError(err)
|
||||
return s
|
||||
}
|
||||
|
||||
// TestSession creates a test session composed of c in the repository.
|
||||
func TestSession(t *testing.T, conn *gorm.DB, wrapper wrapping.Wrapper, c ComposedOf, opt ...Option) *Session {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
rw := db.New(conn)
|
||||
s, err := New(c, opt...)
|
||||
require.NoError(err)
|
||||
id, err := newId()
|
||||
require.NoError(err)
|
||||
s.PublicId = id
|
||||
_, certBytes, err := newCert(wrapper, c.UserId, id)
|
||||
require.NoError(err)
|
||||
s.Certificate = certBytes
|
||||
err = rw.Create(context.Background(), s)
|
||||
require.NoError(err)
|
||||
return s
|
||||
}
|
||||
|
||||
// TestDefaultSession creates a test session in the repository using defaults.
|
||||
func TestDefaultSession(t *testing.T, conn *gorm.DB, wrapper wrapping.Wrapper, iamRepo *iam.Repository, opt ...Option) *Session {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
composedOf := TestSessionParams(t, conn, wrapper, iamRepo)
|
||||
future, err := ptypes.TimestampProto(time.Now().Add(time.Hour))
|
||||
require.NoError(err)
|
||||
exp := ×tamp.Timestamp{Timestamp: future}
|
||||
return TestSession(t, conn, wrapper, composedOf, WithExpirationTime(exp))
|
||||
}
|
||||
|
||||
// TestSessionParams returns an initialized ComposedOf which can be used to
|
||||
// create a session in the repository.
|
||||
func TestSessionParams(t *testing.T, conn *gorm.DB, wrapper wrapping.Wrapper, iamRepo *iam.Repository) ComposedOf {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
require := require.New(t)
|
||||
rw := db.New(conn)
|
||||
org, proj := iam.TestScopes(t, iamRepo)
|
||||
|
||||
cats := static.TestCatalogs(t, conn, proj.PublicId, 1)
|
||||
|
||||
hosts := static.TestHosts(t, conn, cats[0].PublicId, 1)
|
||||
|
||||
sets := static.TestSets(t, conn, cats[0].PublicId, 1)
|
||||
_ = static.TestSetMembers(t, conn, sets[0].PublicId, hosts)
|
||||
|
||||
tcpTarget := target.TestTcpTarget(t, conn, proj.PublicId, "test target")
|
||||
|
||||
kms := kms.TestKms(t, conn, wrapper)
|
||||
targetRepo, err := target.NewRepository(rw, rw, kms)
|
||||
require.NoError(err)
|
||||
_, _, err = targetRepo.AddTargetHostSets(ctx, tcpTarget.GetPublicId(), tcpTarget.GetVersion(), []string{sets[0].PublicId})
|
||||
require.NoError(err)
|
||||
|
||||
authMethod := password.TestAuthMethods(t, conn, org.PublicId, 1)[0]
|
||||
acct := password.TestAccounts(t, conn, authMethod.GetPublicId(), 1)[0]
|
||||
user, err := iamRepo.LookupUserWithLogin(ctx, acct.GetPublicId(), iam.WithAutoVivify(true))
|
||||
require.NoError(err)
|
||||
|
||||
authTokenRepo, err := authtoken.NewRepository(rw, rw, kms)
|
||||
require.NoError(err)
|
||||
at, err := authTokenRepo.CreateAuthToken(ctx, user, acct.GetPublicId())
|
||||
require.NoError(err)
|
||||
|
||||
return ComposedOf{
|
||||
UserId: user.PublicId,
|
||||
HostId: hosts[0].PublicId,
|
||||
TargetId: tcpTarget.PublicId,
|
||||
HostSetId: sets[0].PublicId,
|
||||
AuthTokenId: at.PublicId,
|
||||
ScopeId: tcpTarget.ScopeId,
|
||||
}
|
||||
}
|
||||
|
||||
// TestTofu will create a test "trust on first use" token
|
||||
func TestTofu(t *testing.T) []byte {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
tofu, err := base62.Random(20)
|
||||
require.NoError(err)
|
||||
return []byte(tofu)
|
||||
}
|
||||
|
||||
// TestCert is a temporary test func that intentionally doesn't take testing.T
|
||||
// as a parameter. It's currently used in controller.jobTestingHandler() and
|
||||
// should be deprecated once that function is refactored to use sessions properly.
|
||||
func TestCert(wrapper wrapping.Wrapper, userId, jobId string) (ed25519.PrivateKey, []byte, error) {
|
||||
return newCert(wrapper, userId, jobId)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/iam"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_TestSession(t *testing.T) {
|
||||
t.Helper()
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
wrapper := db.TestWrapper(t)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
s := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
require.NotNil(s)
|
||||
assert.NotEmpty(s.PublicId)
|
||||
}
|
||||
|
||||
func Test_TestState(t *testing.T) {
|
||||
t.Helper()
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
wrapper := db.TestWrapper(t)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
s := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
require.NotNil(s)
|
||||
assert.NotEmpty(s.PublicId)
|
||||
|
||||
state := TestState(t, conn, s.PublicId, StatusPending)
|
||||
require.NotNil(state)
|
||||
}
|
||||
|
||||
func Test_TestConnection(t *testing.T) {
|
||||
t.Helper()
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
wrapper := db.TestWrapper(t)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
s := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
require.NotNil(s)
|
||||
assert.NotEmpty(s.PublicId)
|
||||
|
||||
c := TestConnection(t, conn, s.PublicId, "127.0.0.1", 6500, "127.0.0.1", 22)
|
||||
require.NotNil(c)
|
||||
}
|
||||
|
||||
func Test_TestConnectionState(t *testing.T) {
|
||||
t.Helper()
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
wrapper := db.TestWrapper(t)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
s := TestDefaultSession(t, conn, wrapper, iamRepo)
|
||||
require.NotNil(s)
|
||||
assert.NotEmpty(s.PublicId)
|
||||
|
||||
c := TestConnection(t, conn, s.PublicId, "0.0.0.0", 22, "0.0.0.0", 2222)
|
||||
require.NotNil(c)
|
||||
assert.NotEmpty(c.PublicId)
|
||||
|
||||
cs := TestConnectionState(t, conn, c.PublicId, StatusClosed)
|
||||
require.NotNil(cs)
|
||||
|
||||
rw := db.New(conn)
|
||||
var initialState ConnectionState
|
||||
err := rw.LookupWhere(context.Background(), &initialState, "connection_id = ? and state = ?", cs.ConnectionId, cs.Status)
|
||||
require.NoError(err)
|
||||
assert.NotEmpty(initialState.StartTime)
|
||||
}
|
||||
|
||||
func Test_TestCert(t *testing.T) {
|
||||
t.Helper()
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
wrapper := db.TestWrapper(t)
|
||||
userId, err := db.NewPublicId(iam.UserPrefix)
|
||||
require.NoError(err)
|
||||
sessionId, err := newId()
|
||||
require.NoError(err)
|
||||
key, cert, err := TestCert(wrapper, userId, sessionId)
|
||||
require.NoError(err)
|
||||
assert.NotNil(key)
|
||||
assert.NotNil(cert)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package sessions
|
||||
package session
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
// DeriveED25519Key generates a key based on the scope's sessions DEK, the
|
||||
// DeriveED25519Key generates a key based on the scope's session DEK, the
|
||||
// requesting user, and the generated job ID.
|
||||
func DeriveED25519Key(wrapper wrapping.Wrapper, userId, jobId string) (ed25519.PublicKey, ed25519.PrivateKey, error) {
|
||||
var aeadWrapper *aead.Wrapper
|
||||
Reference in new issue
Block a user