basic sessions (#337)

This commit is contained in:
Jim authored and GitHub committed 2020-09-16 12:02:02 -04:00
1 parent 0a44ed3edd
commit 611288bdc7
35 files changed
+5934 -111

No files matched your search

+439 -42
View File
@@ -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;
+134 -42
View File
@@ -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;
+19
View File
@@ -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 {
+18
View File
@@ -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")
+4 -23
View File
@@ -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)
}
+179
View File
@@ -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 = &timestamp.Timestamp{
Timestamp: &timestamppb.Timestamp{
Seconds: c.CreateTime.Timestamp.Seconds,
Nanos: c.CreateTime.Timestamp.Nanos,
},
}
}
if c.UpdateTime != nil {
clone.UpdateTime = &timestamp.Timestamp{
Timestamp: &timestamppb.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)
}
}
+142
View File
@@ -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 = &timestamp.Timestamp{
Timestamp: &timestamppb.Timestamp{
Seconds: s.PreviousEndTime.Timestamp.Seconds,
Nanos: s.PreviousEndTime.Timestamp.Nanos,
},
}
}
if s.StartTime != nil {
clone.StartTime = &timestamp.Timestamp{
Timestamp: &timestamppb.Timestamp{
Seconds: s.StartTime.Timestamp.Seconds,
Nanos: s.StartTime.Timestamp.Nanos,
},
}
}
if s.EndTime != nil {
clone.EndTime = &timestamp.Timestamp{
Timestamp: &timestamppb.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
}
+212
View File
@@ -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())
})
}
}
+255
View File
@@ -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())
})
}
}
+53
View File
@@ -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
}
+33
View File
@@ -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+"_"))
})
}
+334
View File
@@ -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: &timestamppb.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: &timestamppb.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: &timestamppb.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: &timestamppb.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)
})
}
}
+75
View File
@@ -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
}
}
+73
View File
@@ -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(&timestamp.Timestamp{Timestamp: now}))
testOpts := getDefaultOptions()
testOpts.withExpirationTime = &timestamp.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)
})
}
+20
View File
@@ -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;
`
)
+66
View File
@@ -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...)
}
+297
View File
@@ -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)
})
}
}
+441
View File
@@ -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
}
+856
View File
@@ -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)
})
}
}
+74
View File
@@ -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)
})
}
}
+329
View File
@@ -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 = &timestamp.Timestamp{
Timestamp: &timestamppb.Timestamp{
Seconds: s.ExpirationTime.Timestamp.Seconds,
Nanos: s.ExpirationTime.Timestamp.Nanos,
},
}
}
if s.CreateTime != nil {
clone.CreateTime = &timestamp.Timestamp{
Timestamp: &timestamppb.Timestamp{
Seconds: s.CreateTime.Timestamp.Seconds,
Nanos: s.CreateTime.Timestamp.Nanos,
},
}
}
if s.UpdateTime != nil {
clone.UpdateTime = &timestamp.Timestamp{
Timestamp: &timestamppb.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
}
+268
View File
@@ -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 := &timestamp.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())
})
}
}
+145
View File
@@ -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 = &timestamp.Timestamp{
Timestamp: &timestamppb.Timestamp{
Seconds: s.PreviousEndTime.Timestamp.Seconds,
Nanos: s.PreviousEndTime.Timestamp.Nanos,
},
}
}
if s.StartTime != nil {
clone.StartTime = &timestamp.Timestamp{
Timestamp: &timestamppb.Timestamp{
Seconds: s.StartTime.Timestamp.Seconds,
Nanos: s.StartTime.Timestamp.Nanos,
},
}
}
if s.EndTime != nil {
clone.EndTime = &timestamp.Timestamp{
Timestamp: &timestamppb.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
}
+209
View File
@@ -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())
})
}
}
+42
View File
@@ -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)
}
}
+151
View File
@@ -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 := &timestamp.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)
}
+88
View File
@@ -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