Files
threshold/internal/db/migrations/postgres/06_iam.up.sql
T

489 lines
13 KiB
PL/PgSQL

begin;
create table iam_scope_type_enm (
string text not null primary key check(string in ('unknown', 'global', 'organization', 'project'))
);
insert into iam_scope_type_enm (string)
values
('unknown'),
('global'),
('organization'),
('project');
create or replace function
iam_immutable_scope_id_func()
returns trigger
as $$
begin
if new.scope_id is distinct from old.scope_id then
raise exception 'scope_id cannot be set to %', new.scope_id;
end if;
return new;
end;
$$ language plpgsql;
comment on function
iam_immutable_scope_id_func()
is
'function used in before update triggers to make scope_id column is immutable';
create table iam_scope (
public_id wt_scope_id primary key,
create_time wt_timestamp,
update_time wt_timestamp,
name text,
type text not null references iam_scope_type_enm(string) check(
(
type = 'global'
and parent_id is null
)
or (
type = 'organization'
and parent_id = 'global'
)
or (
type = 'project'
and parent_id is not null
and parent_id != 'global'
)
),
description text,
parent_id text references iam_scope(public_id) on delete cascade on update cascade
);
create table iam_scope_global (
scope_id wt_scope_id primary key
references iam_scope(public_id)
on delete cascade
on update cascade
check(
scope_id = 'global'
),
name text unique
);
create table iam_scope_organization (
scope_id wt_scope_id primary key
references iam_scope(public_id)
on delete cascade
on update cascade,
parent_id wt_scope_id not null
references iam_scope_global(scope_id)
on delete cascade
on update cascade,
name text,
unique (parent_id, name)
);
create table iam_scope_project (
scope_id wt_scope_id not null references iam_scope(public_id) on delete cascade on update cascade,
parent_id wt_public_id not null references iam_scope_organization(scope_id) on delete cascade on update cascade,
name text,
unique(parent_id, name),
primary key(scope_id, parent_id)
);
create or replace function
iam_sub_scopes_func()
returns trigger
as $$
declare parent_type int;
begin
if new.type = 'global' then
insert into iam_scope_global (scope_id, name)
values
(new.public_id, new.name);
return new;
end if;
if new.type = 'organization' then
insert into iam_scope_organization (scope_id, parent_id, name)
values
(new.public_id, new.parent_id, new.name);
return new;
end if;
if new.type = 'project' then
insert into iam_scope_project (scope_id, parent_id, name)
values
(new.public_id, new.parent_id, new.name);
return new;
end if;
raise exception 'unknown scope type';
end;
$$ language plpgsql;
create trigger
iam_scope_insert
after
insert on iam_scope
for each row execute procedure iam_sub_scopes_func();
create or replace function
disallow_global_scope_deletion()
returns trigger
as $$
begin
if old.type = 'global' then
raise exception 'deletion of global scope not allowed';
end if;
return old;
end;
$$ language plpgsql;
create trigger
iam_scope_disallow_global_deletion
before
delete on iam_scope
for each row execute procedure disallow_global_scope_deletion();
create or replace function
iam_immutable_scope_type_func()
returns trigger
as $$
declare parent_type int;
begin
if new.type != old.type then
raise exception 'scope type cannot be updated';
end if;
return new;
end;
$$ language plpgsql;
create trigger
iam_scope_update
before
update on iam_scope
for each row execute procedure iam_immutable_scope_type_func();
create trigger
update_time_column
before update on iam_scope
for each row execute procedure update_time_column();
create trigger
immutable_create_time
before
update on iam_scope
for each row execute procedure immutable_create_time_func();
create trigger
default_create_time_column
before
insert on iam_scope
for each row execute procedure default_create_time();
-- iam_sub_names will allow us to enforce the different name constraints for
-- organizations and projects via a before update trigger on the iam_scope
-- table.
create or replace function
iam_sub_names()
returns trigger
as $$
begin
if new.name != old.name then
if new.type = 'global' then
update iam_scope_global set name = new.name where scope_id = old.public_id;
return new;
end if;
if new.type = 'organization' then
update iam_scope_organization set name = new.name where scope_id = old.public_id;
return new;
end if;
if new.type = 'project' then
update iam_scope_project set name = new.name where scope_id = old.public_id;
return new;
end if;
raise exception 'unknown scope type';
end if;
return new;
end;
$$ language plpgsql;
create trigger
iam_sub_names
before
update on iam_scope
for each row execute procedure iam_sub_names();
insert into iam_scope (public_id, name, type, description)
values ('global', 'global', 'global', 'Global Scope');
create table iam_user (
public_id wt_public_id not null primary key,
create_time wt_timestamp,
update_time wt_timestamp,
name text,
description text,
scope_id wt_scope_id not null references iam_scope(public_id) on delete cascade on update cascade,
unique(name, scope_id),
disabled boolean not null default false,
-- The order of columns is important for performance. See:
-- https://dba.stackexchange.com/questions/58970/enforcing-constraints-two-tables-away/58972#58972
-- https://dba.stackexchange.com/questions/27481/is-a-composite-index-also-good-for-queries-on-the-first-field
unique(scope_id, public_id)
);
create or replace function
user_scope_id_valid()
returns trigger
as $$
begin
perform from iam_scope where public_id = new.scope_id and type in ('global', 'organization');
if not found then
raise exception 'invalid scope type for user creation';
end if;
return new;
end;
$$ language plpgsql;
create trigger
ensure_user_scope_id_valid
before
insert or update on iam_user
for each row execute procedure user_scope_id_valid();
create trigger
update_time_column
before update on iam_user
for each row execute procedure update_time_column();
create trigger
immutable_create_time
before
update on iam_user
for each row execute procedure immutable_create_time_func();
create trigger
default_create_time_column
before
insert on iam_user
for each row execute procedure default_create_time();
create trigger immutable_scope_id_user
before
update on iam_user
for each row execute procedure iam_immutable_scope_id_func();
create table iam_role (
public_id wt_public_id not null primary key,
create_time wt_timestamp,
update_time wt_timestamp,
name text,
description text,
scope_id wt_scope_id not null references iam_scope(public_id) on delete cascade on update cascade,
unique(name, scope_id),
disabled boolean not null default false,
-- version allows optimistic locking of the role when modifying the role
-- itself and when modifying dependent items like principal roles.
-- TODO (jlambert 6/2020) add before update trigger to automatically
-- increment the version when needed. This trigger can be addded when PR
-- #126 is merged and update_version_column() is available.
version bigint not null default 1,
-- add unique index so a composite fk can be declared.
unique(scope_id, public_id)
);
create trigger immutable_scope_id
before
update on iam_role
for each row execute procedure iam_immutable_scope_id_func();
create trigger
update_version_column
after update on iam_role
for each row execute procedure update_version_column();
create trigger
update_time_column
before update on iam_role
for each row execute procedure update_time_column();
create trigger
immutable_create_time
before
update on iam_role
for each row execute procedure immutable_create_time_func();
create trigger
default_create_time_column
before
insert on iam_role
for each row execute procedure default_create_time();
create table iam_group (
public_id wt_public_id not null primary key,
create_time wt_timestamp,
update_time wt_timestamp,
name text,
description text,
scope_id wt_scope_id not null references iam_scope(public_id) on delete cascade on update cascade,
unique(name, scope_id),
disabled boolean not null default false,
-- add unique index so a composite fk can be declared.
unique(scope_id, public_id)
);
create trigger
update_time_column
before update on iam_group
for each row execute procedure update_time_column();
create trigger
immutable_create_time
before
update on iam_group
for each row execute procedure immutable_create_time_func();
create trigger
default_create_time_column
before
insert on iam_group
for each row execute procedure default_create_time();
create trigger immutable_scope_id_group
before
update on iam_group
for each row execute procedure iam_immutable_scope_id_func();
-- iam_user_role contains roles that have been assigned to users. Users can only
-- be assigned roles which are within its organization, or the role is within a project within its
-- organization. There's no way to declare this constraint, so it will be
-- maintained with a before insert trigger using iam_user_role_scope_check().
-- The rows in this table must be immutable after insert, which will be ensured
-- with a before update trigger using iam_immutable_role().
create table iam_user_role (
create_time wt_timestamp,
scope_id wt_scope_id not null,
role_id wt_public_id not null,
principal_id wt_public_id not null references iam_user(public_id) on delete cascade on update cascade,
primary key (role_id, principal_id),
foreign key (scope_id, role_id)
references iam_role(scope_id, public_id)
on delete cascade
on update cascade
);
-- iam_group_role contains roles that have been assigned to groups. Groups can
-- only be assigned roles which are within its scope (organization or project)
-- and that integrity can be maintained with a foreign key. The rows in this
-- table must be immutable after insert, which will be ensured with a before
-- update trigger using iam_immutable_role().
create table iam_group_role (
create_time wt_timestamp,
scope_id wt_scope_id not null,
role_id wt_public_id not null,
principal_id wt_public_id not null,
primary key (role_id, principal_id),
foreign key (scope_id, role_id)
references iam_role(scope_id, public_id)
on delete cascade
on update cascade,
foreign key (scope_id, principal_id)
references iam_group(scope_id, public_id)
on delete cascade
on update cascade
);
-- iam_principle_role provides a consolidated view all principal roles assigned
-- (user and group roles).
create view iam_principal_role as
select
-- intentionally using * to specify the view which requires that the concrete role assignment tables match
*, 'user' as type
from iam_user_role
union
select
-- intentionally using * to specify the view which requires that the concrete role assignment tables match
*, 'group' as type
from iam_group_role;
-- iam_user_role_scope_check() ensures that the user is only assigned roles
-- which are within its organization, or the role is within a project within its
-- organization.
create or replace function
iam_user_role_scope_check()
returns trigger
as $$
declare cnt int;
begin
select count(*) into cnt
from iam_user
where
public_id = new.principal_id and
scope_id in(
-- check to see if they have the same org scope
select s.public_id
from iam_scope s, iam_role r
where s.public_id = r.scope_id and r.public_id = new.role_id and r.scope_id = new.scope_id
union
-- check to see if the role has a parent that's the same org
select s.parent_id as public_id
from iam_scope s, iam_role r
where s.public_id = r.scope_id and r.public_id = new.role_id and r.scope_id = new.scope_id
);
if cnt = 0 then
raise exception 'user and role do not belong to the same organization';
end if;
return new;
end;
$$ language plpgsql;
-- iam_immutable_role() ensures that roles assigned to principals are immutable.
create or replace function
iam_immutable_role()
returns trigger
as $$
begin
if row(new.*) is distinct from row(old.*) then
raise exception 'roles are immutable';
end if;
return new;
end;
$$ language plpgsql;
create trigger iam_user_role_scope_check
before
insert on iam_user_role
for each row execute procedure iam_user_role_scope_check();
create trigger immutable_role
before
update on iam_user_role
for each row execute procedure iam_immutable_role();
create trigger
immutable_create_time
before
update on iam_user_role
for each row execute procedure immutable_create_time_func();
create trigger
default_create_time_column
before
insert on iam_user_role
for each row execute procedure default_create_time();
create trigger immutable_role
before
update on iam_group_role
for each row execute procedure iam_immutable_role();
create trigger
immutable_create_time
before
update on iam_group_role
for each row execute procedure immutable_create_time_func();
create trigger
default_create_time_column
before
insert on iam_group_role
for each row execute procedure default_create_time();
commit;