feat(internal/db): Storage policies implementation (#767)

This commit implements part of the storage policy's database model,
focusing specifically on the operational tables for this feature.
This commit is contained in:
Hugo authored and Louis Ruch committed 2024-01-16 13:20:11 -08:00
1 parent edc616ea9d
commit 978db86186
5 files changed
+465

No files matched your search

@@ -0,0 +1,178 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
create table policy (
public_id wt_public_id primary key,
scope_id wt_scope_id not null
constraint policy_scope_id_fkey
references iam_scope(public_id)
on delete restrict
on update cascade
);
comment on table policy is
'policy contains entries that represent an abstract policy';
create table policy_storage_policy (
public_id wt_public_id primary key,
scope_id wt_scope_id not null
constraint policy_storage_policy_scope_id_fkey
references iam_scope(public_id)
on delete restrict
on update cascade,
retain_for_days integer not null,
retain_for_days_overridable boolean not null default true,
delete_after_days integer not null,
constraint delete_after_days_non_negative
check(delete_after_days >= 0),
constraint delete_after_days_greater_or_equal_than_retain_for_days
check(delete_after_days >= retain_for_days or delete_after_days = 0),
delete_after_days_overridable boolean not null default true,
name wt_name,
constraint policy_storage_policy_scope_id_name_uq
unique(scope_id, name),
description wt_description,
create_time wt_timestamp,
update_time wt_timestamp,
version wt_version
);
comment on table policy_storage_policy is
'policy_storage_policy is a subtype of policy and contains entries that codify '
'a storage policy and how long to keep session recordings';
create trigger default_create_time_column before insert on policy_storage_policy
for each row execute procedure default_create_time();
create trigger update_time_column before update on policy_storage_policy
for each row execute procedure update_time_column();
create trigger update_version_column after update on policy_storage_policy
for each row execute procedure update_version_column();
create trigger immutable_columns before update on policy_storage_policy
for each row execute procedure immutable_columns('public_id', 'scope_id', 'create_time');
create function retain_for_days_and_delete_after_days_not_zero() returns trigger
as $$
begin
if new.retain_for_days = 0 and new.delete_after_days = 0 then
raise exception 'retain_for_days and delete_after_days are both zero';
end if;
return new;
end;
$$ language plpgsql;
comment on function retain_for_days_and_delete_after_days_not_zero is
'retain_for_days_and_delete_after_days_not_zero is a trigger function that '
'ensures that retain_for_days and delete_after_days cannot both be zero';
create trigger retain_for_days_and_delete_after_days_not_zero before insert or update on policy_storage_policy
for each row execute procedure retain_for_days_and_delete_after_days_not_zero();
create function delete_after_days_zero_if_infinite_retain_for_days() returns trigger
as $$
begin
if new.retain_for_days < 0 and new.delete_after_days != 0 then
raise exception 'deletion period set on infinite retention period policy';
end if;
return new;
end;
$$ language plpgsql;
comment on function delete_after_days_zero_if_infinite_retain_for_days is
'delete_after_days_zero_if_infinite_retain_for_days is a trigger function that '
'ensures that if retain_for_days is set to infinity (-1), delete_after_days is '
'zero (working alongside the existing check constraints)';
create trigger delete_after_days_zero_if_infinite_retain_for_days before insert or update on policy_storage_policy
for each row execute procedure delete_after_days_zero_if_infinite_retain_for_days();
create function policy_storage_policy_scope_id_valid() returns trigger
as $$
begin
perform from iam_scope where public_id = new.scope_id and type in ('global', 'org');
if not found then
raise exception 'invalid scope type for storage policy creation';
end if;
return new;
end;
$$ language plpgsql;
comment on function policy_storage_policy_scope_id_valid is
'policy_storage_policy_scope_id_valid is a trigger function that checks that the '
'scope_id being inserted is a global or org level scope.';
create trigger policy_storage_policy_scope_id_valid before insert on policy_storage_policy
for each row execute procedure policy_storage_policy_scope_id_valid();
create function insert_policy_subtype() returns trigger
as $$
begin
insert into policy
(public_id, scope_id)
values
(new.public_id, new.scope_id);
return new;
end;
$$ language plpgsql;
create trigger insert_policy_subtype before insert on policy_storage_policy
for each row execute procedure insert_policy_subtype();
create function delete_policy_subtype() returns trigger
as $$
begin
delete from policy where public_id = old.public_id;
return null;
end;
$$ language plpgsql;
create trigger delete_policy_subtype after delete on policy_storage_policy
for each row execute procedure delete_policy_subtype();
create table scope_policy_storage_policy (
scope_id wt_scope_id primary key
constraint scope_policy_storage_policy_scope_id_fkey
references iam_scope (public_id)
on update cascade
on delete cascade,
storage_policy_id wt_public_id not null
constraint scope_policy_storage_policy_storage_policy_id_fkey
references policy_storage_policy (public_id)
on update cascade
on delete cascade
);
comment on table scope_policy_storage_policy is
'scope_policy_storage_policy is an association table between a scope and a storage '
'policy. The scope can only be global or the same scope present in the storage '
'policy to be associated';
create trigger immutable_columns before update on scope_policy_storage_policy
for each row execute procedure immutable_columns('scope_id');
create function scope_policy_storage_policy_scope_id_valid() returns trigger
as $$
begin
-- An org storage policy can only be applied to that specific org scope,
-- whereas a `global` storage policy can be applied to any scope.
perform from policy_storage_policy
where ( public_id = new.storage_policy_id and scope_id = new.scope_id ) or scope_id = 'global';
if not found then
raise exception 'invalid scope_id for scope_storage_policy association';
end if;
return new;
end;
$$ language plpgsql;
comment on function scope_policy_storage_policy_scope_id_valid is
'scope_policy_storage_policy_scope_id_valid is a trigger function that '
'checks that the scope_id being inserted is either global or the same org as '
'the storage policy we want to associate';
create trigger scope_policy_storage_policy_scope_id_valid before insert on scope_policy_storage_policy
for each row execute procedure scope_policy_storage_policy_scope_id_valid();
insert into oplog_ticket (name, version)
values
('policy', 1),
('policy_storage_policy', 1),
('scope_policy_storage_policy', 1);
commit;
@@ -0,0 +1,34 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
select plan(2);
select wtt_load('widgets', 'iam');
insert into iam_scope
(parent_id, type, public_id, name)
values
('global', 'org', 'o_____widget2', 'Widget 2 Inc');
-- insert storage policy.
insert into policy_storage_policy
(public_id, scope_id, retain_for_days, delete_after_days, name, description)
values
('pst____policy', 'o_____widget', -1, 0, 'Test Storage Policy', 'This is a test storage policy');
-- invalid scope_policy_storage_policy: scope is not global and does not match
-- the storage policy scope.
select throws_ok($$
insert into scope_policy_storage_policy
(scope_id, storage_policy_id)
values
('o_____widget2', 'pst____policy')
$$, 'invalid scope_id for scope_storage_policy association');
select is(count(*), 0::bigint) from scope_policy_storage_policy
where
scope_id = 'o_____widget2' and
storage_policy_id = 'pst____policy';
delete from iam_scope where public_id = 'o_____widget2';
rollback;
@@ -0,0 +1,133 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
select plan(14);
select wtt_load('widgets', 'iam');
-- insert invalid storage policy: delete_after_days is lower than
-- retain_for_days.
select throws_ok($$
insert into policy_storage_policy
(public_id, scope_id, retain_for_days, delete_after_days, name, description)
values
('pst____policy', 'o_____widget', 5, 1, 'Test Storage Policy', 'This is a test storage policy')
$$, 'new row for relation "policy_storage_policy" violates check constraint "delete_after_days_greater_or_equal_than_retain_for_days"');
-- 'policy' abstract table should not have a new association.
select is(count(*), 0::bigint) from policy
where
public_id = 'pst____policy' and
scope_id = 'o_____widget';
-- insert invalid storage policy: use project-level scope.
select throws_ok($$
insert into policy_storage_policy
(public_id, scope_id, retain_for_days, delete_after_days, name, description)
values
('pst____policy', 'p____bwidget', -1, 0, 'Test Storage Policy', 'This is a test storage policy')
$$, 'invalid scope type for storage policy creation');
-- 'policy' abstract table should not have a new association.
select is(count(*), 0::bigint) from policy
where
public_id = 'pst____policy' and
scope_id = 'p____bwidget';
-- insert invalid storage policy: negative delete_after_days.
select throws_ok($$
insert into policy_storage_policy
(public_id, scope_id, retain_for_days, delete_after_days, name, description)
values
('pst____policy', 'o_____widget', -1, -1, 'Test Storage Policy', 'This is a test storage policy')
$$, 'deletion period set on infinite retention period policy');
-- 'policy' abstract table should not have a new association.
select is(count(*), 0::bigint) from policy
where
public_id = 'pst____policy' and
scope_id = 'o_____widget';
-- insert valid storage policy to test using same policy name on a given scope.
insert into policy_storage_policy
(public_id, scope_id, retain_for_days, delete_after_days, name, description)
values
('pst____policy', 'o_____widget', -1, 0, 'Test Storage Policy', 'This is a test storage policy');
-- since we already have a 'Test Storage Policy', this should fail.
select throws_ok($$
insert into policy_storage_policy
(public_id, scope_id, retain_for_days, delete_after_days, name, description)
values
('pst____policy2', 'o_____widget', -1, 0, 'Test Storage Policy', 'This is a test storage policy')
$$, 'duplicate key value violates unique constraint "policy_storage_policy_scope_id_name_uq"');
-- cleanup so that we can run more tests with the same insert query below.
delete from policy_storage_policy where public_id = 'pst____policy';
-- 'policy' abstract table should not have a new association.
select is(count(*), 0::bigint) from policy
where
public_id = 'pst____policy' and
scope_id = 'o_____widget';
-- insert invalid storage policy: retain_for_days and delete_after_days are
-- both zero.
select throws_ok($$
insert into policy_storage_policy
(public_id, scope_id, retain_for_days, delete_after_days, name, description)
values
('pst____policy', 'o_____widget', 0, 0, 'Test Storage Policy', 'This is a test storage policy')
$$, 'retain_for_days and delete_after_days are both zero');
-- insert valid storage policy to test update with both retain_for_days and
-- delete_after_days set to zero.
insert into policy_storage_policy
(public_id, scope_id, retain_for_days, delete_after_days, name, description)
values
('pst____policy', 'o_____widget', -1, 0, 'Test Storage Policy', 'This is a test storage policy');
select throws_ok($$
update policy_storage_policy set (retain_for_days, delete_after_days) = (0, 0)
where public_id = 'pst____policy'
$$, 'retain_for_days and delete_after_days are both zero');
-- cleanup so that we can run more tests with the same insert query below.
delete from policy_storage_policy where public_id = 'pst____policy';
-- 'policy' abstract table should not have a new association.
select is(count(*), 0::bigint) from policy
where
public_id = 'pst____policy' and
scope_id = 'o_____widget';
-- insert invalid storage policy: infinite retention policy (-1) with a
-- deletion policy.
select throws_ok($$
insert into policy_storage_policy
(public_id, scope_id, retain_for_days, delete_after_days, name, description)
values
('pst____policy', 'o_____widget', -1, 5, 'Test Storage Policy', 'This is a test storage policy')
$$, 'deletion period set on infinite retention period policy');
-- 'policy' abstract table should not have a new association.
select is(count(*), 0::bigint) from policy
where
public_id = 'pst____policy' and
scope_id = 'o_____widget';
-- insert valid storage policy to test updating infinite retention policy (-1)
-- with a deletion policy present.
insert into policy_storage_policy
(public_id, scope_id, retain_for_days, delete_after_days, name, description)
values
('pst____policy', 'o_____widget', 5, 5, 'Test Storage Policy', 'This is a test storage policy');
select throws_ok($$
update policy_storage_policy set retain_for_days = -1
where public_id = 'pst____policy'
$$, 'deletion period set on infinite retention period policy');
-- cleanup so that we can run more tests with the same insert query below.
delete from policy_storage_policy where public_id = 'pst____policy';
rollback;
@@ -0,0 +1,54 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
select plan(2);
select wtt_load('widgets', 'iam');
-- insert storage policy.
insert into policy_storage_policy
(public_id, scope_id, retain_for_days, delete_after_days, name, description)
values
('pst____policy', 'o_____widget', -1, 0, 'Test Storage Policy', 'This is a test storage policy');
-- 'policy' abstract table should now have a new association due to insert
-- above.
select is(count(*), 1::bigint) from policy
where
public_id = 'pst____policy' and
scope_id = 'o_____widget';
-- insert into scope_policy_storage_policy association table.
insert into scope_policy_storage_policy
(scope_id, storage_policy_id)
values
('o_____widget', 'pst____policy');
select is(count(*), 1::bigint) from scope_policy_storage_policy
where
scope_id = 'o_____widget' and
storage_policy_id = 'pst____policy';
-- Reset to allow for next test.
delete from scope_policy_storage_policy
where
scope_id = 'o_____widget' and
storage_policy_id = 'pst____policy';
-- A `global` storage policy should be able to be associated with any scope
-- insert storage policy.
insert into policy_storage_policy
(public_id, scope_id, retain_for_days, delete_after_days, name, description)
values
('pst____policy2', 'global', -1, 0, 'Test Storage Policy 2', 'This is a test storage policy');
insert into scope_policy_storage_policy
(scope_id, storage_policy_id)
values
('global', 'pst____policy2');
insert into scope_policy_storage_policy
(scope_id, storage_policy_id)
values
('o_____widget', 'pst____policy2');
rollback;
@@ -0,0 +1,66 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
select plan(5);
select wtt_load('widgets', 'iam');
-- insert storage policy.
insert into policy_storage_policy
(public_id, scope_id, retain_for_days, delete_after_days, name, description)
values
('pst____policy', 'o_____widget', 5, 10, 'Test Storage Policy', 'This is a test storage policy');
-- 'policy' abstract table should now have a new association due to insert
-- above.
select is(count(*), 1::bigint) from policy
where
public_id = 'pst____policy' and
scope_id = 'o_____widget';
-- update a storage policy's retain_for_days
update policy_storage_policy set retain_for_days = 1
where
public_id = 'pst____policy' and
scope_id = 'o_____widget';
select is(retain_for_days, 1::integer) from policy_storage_policy
where
public_id = 'pst____policy' and
scope_id = 'o_____widget';
-- update a storage policy's delete_after_days
update policy_storage_policy set delete_after_days = 20
where
public_id = 'pst____policy' and
scope_id = 'o_____widget';
select is(delete_after_days, 20::integer) from policy_storage_policy
where
public_id = 'pst____policy' and
scope_id = 'o_____widget';
-- delete a storage policy.
delete from policy_storage_policy
where
public_id = 'pst____policy' and
scope_id = 'o_____widget';
-- row should be deleted from 'policy' abstract table.
select is(count(*), 0::bigint) from policy
where
public_id = 'pst____policy' and
scope_id = 'o_____widget';
-- delete_after_days is less than retain_for_days, however it is 0, which is a
-- valid carve-out on delete_after_days_greater_or_equal_than_retain_for_days.
insert into policy_storage_policy
(public_id, scope_id, retain_for_days, delete_after_days, name, description)
values
('pst____policy2', 'o_____widget', 5, 0, 'Test Storage Policy 2', 'This is a test storage policy');
select is(count(*), 1::bigint) from policy
where
public_id = 'pst____policy2' and
scope_id = 'o_____widget';
rollback;