-- Copyright (c) HashiCorp, Inc. -- SPDX-License-Identifier: BUSL-1.1 begin; create table auth_ldap_method_state_enm ( name text primary key constraint name_only_predefined_ldap_method_states_allowed check (name in ('inactive', 'active-private', 'active-public')) ); comment on table auth_ldap_method_state_enm is 'auth_ldap_method_state_enm entries enumerate the valid auth ldap method states'; -- populate the values of auth_ldap_method_state_enm insert into auth_ldap_method_state_enm(name) values ('inactive'), ('active-private'), ('active-public'); -- column added to auth_ldap_method in in 76/01_ldap.up.sql create table auth_ldap_method ( public_id wt_public_id primary key, scope_id wt_scope_id not null, name wt_name, description wt_description, create_time wt_timestamp, update_time wt_timestamp, version wt_version, state text not null constraint auth_ldap_method_state_enm_fkey references auth_ldap_method_state_enm(name) on delete restrict on update cascade, start_tls bool not null default false, insecure_tls bool not null default false, discover_dn bool not null default false, anon_group_search bool not null default false, upn_domain text constraint upn_domain_too_short check (length(trim(upn_domain)) > 0) constraint upn_domain_too_long check (length(trim(upn_domain)) < 253), enable_groups bool not null default false, use_token_groups bool not null default false, constraint auth_method_fkey foreign key (scope_id, public_id) references auth_method (scope_id, public_id) on delete cascade on update cascade, constraint auth_ldap_method_scope_id_name_uq unique(scope_id, name), constraint auth_ldap_method_scope_id_public_id_uq unique(scope_id, public_id) ); comment on table auth_ldap_method is 'auth_ldap_method entries are the current ldap auth methods configured for ' 'existing scopes'; -- auth_ldap_method column triggers create trigger insert_auth_method_subtype before insert on auth_ldap_method for each row execute procedure insert_auth_method_subtype(); create trigger delete_auth_method_subtype after delete on auth_ldap_method for each row execute procedure delete_auth_method_subtype(); create trigger update_auth_method_subtype before update on auth_ldap_method for each row execute procedure update_auth_method_subtype(); create trigger update_time_column before update on auth_ldap_method for each row execute procedure update_time_column(); create trigger immutable_columns before update on auth_ldap_method for each row execute procedure immutable_columns('public_id', 'scope_id', 'create_time'); create trigger default_create_time_column before insert on auth_ldap_method for each row execute procedure default_create_time(); create trigger update_version_column after update on auth_ldap_method for each row execute procedure update_version_column(); -- auth_ldap_url entries are LDAP URLs that specify an LDAP servers to connect -- to. Examples: ldap://ldap.myorg.com, ldaps://ldap.myorg.com:636. There must -- be at least one and if there's more than one URL configured for an auth -- method, the directories will be tried in connection_priority order if there -- are errors during the connection process. The URL scheme must be either ldap -- or ldaps. The port is optional.If no port is specified, then a default of 389 -- is used for ldap and a default of 689 is used for ldaps. (see rfc4516 for -- more information about LDAP URLs) -- -- Updates will be implemented as a delete + insert with the auth_ldap_method -- being used as the root aggregate for auth_ldap_url updates. create table auth_ldap_url ( create_time wt_timestamp, ldap_method_id wt_public_id not null constraint auth_ldap_method_fkey references auth_ldap_method(public_id) on delete cascade on update cascade, url text not null constraint url_too_short check (length(trim(url)) > 3) constraint url_too_long check (length(trim(url)) < 4000), constraint url_invalid_protocol check (url ~ 'ldaps?:\/\/*'), connection_priority int not null constraint connection_priority_less_than_one check(connection_priority >= 1), primary key(ldap_method_id, connection_priority) ); comment on table auth_ldap_url is 'auth_ldap_url entries specify a connection URL an LDAP'; create function auth_ldap_url_parent_children() returns trigger as $$ declare n integer; begin if tg_op = 'INSERT' or tg_op = 'UPDATE' then select into n count(*) from auth_ldap_url where ldap_method_id = new.ldap_method_id; if n < 1 then raise exception 'During % of auth_ldap_url: auth_ldap_method id=% must have at least one url, not %',tg_op,new.ldap_method_id,n; end if; end if; if tg_op = 'UPDATE' then select into n count(*) from auth_ldap_url where ldap_method_id = old.ldap_method_id; if n < 1 then raise exception 'During % of %: auth_ldap_method id=% must have at least one url, not %',tg_op,tg_table_name,old.ldap_method_id,n; end if; end if; return null; end; $$ language plpgsql; comment on function auth_ldap_url_parent_children() is 'function used on auth_ldap_url after insert/update initially deferred to ensure each ' 'auth_ldap_method has at least one auth_ldap_url. Unfortunately, it cannot be used on ' 'delete since that would make it impossible to delete an ldap auth method, because you ' 'would not be able to remove all of its urls'; create constraint trigger auth_ldap_url_children_per_parent_tg after insert or update or delete on auth_ldap_url deferrable initially deferred for each row execute procedure auth_ldap_url_parent_children(); create function auth_ldap_method_children() returns trigger as $$ declare n integer; begin if tg_op = 'INSERT' then select into n count(*) from auth_ldap_url where ldap_method_id = new.public_id; if n < 1 then raise exception 'During % of auth_ldap_method public_id=% must have at least one url, not %',tg_op,new.public_id,n; end if; -- No need for an UPDATE or DELETE check, as regular referential integrity constraints -- and the trigger on `child' will do the job. return null; end if; end; $$ language plpgsql; comment on function auth_ldap_method_children() is 'function used on auth_ldap_method after insert initially deferred to ensure each ' 'auth_ldap_method has at least one auth_ldap_url'; create constraint trigger auth_ldap_method_children_tg after insert on auth_ldap_method deferrable initially deferred for each row execute procedure auth_ldap_method_children(); -- auth_ldap_user_entry_search entries specify the required parameters to find a -- user entry before attempting to authenticate the user. -- -- Updates will be implemented as a delete + insert with the auth_ldap_method -- being used as the root aggregate for auth_ldap_user_entry_search updates. create table auth_ldap_user_entry_search ( create_time wt_timestamp, ldap_method_id wt_public_id primary key constraint auth_ldap_method_fkey references auth_ldap_method(public_id) on delete cascade on update cascade, user_dn text constraint user_dn_too_short check (length(trim(user_dn)) > 0) constraint user_dn_too_long check (length(trim(user_dn)) < 1025), user_attr text constraint user_attr_too_short check (length(trim(user_attr)) > 0) constraint user_attr_too_long check (length(trim(user_attr)) < 1025), user_filter text constraint user_filter_too_short check (length(trim(user_filter)) > 0) constraint user_filter_too_long check (length(trim(user_filter)) < 2049), constraint all_fields_are_not_null check ( not(user_dn, user_attr, user_filter) is null ) ); comment on table auth_ldap_user_entry_search is 'auth_ldap_user_entry_search entries specify the required parameters to find ' 'a user entry before attempting to authenticate the user'; -- auth_ldap_group_entry_search entries specify the required parameters to find -- the groups a user is a member of -- -- Updates will be implemented as a delete + insert with the auth_ldap_method -- being used as the root aggregate for auth_ldap_user_entry_search updates. create table auth_ldap_group_entry_search ( create_time wt_timestamp, ldap_method_id wt_public_id primary key constraint auth_ldap_method_fkey references auth_ldap_method(public_id) on delete cascade on update cascade, group_dn text not null -- required constraint group_dn_too_short check (length(trim(group_dn)) > 0) constraint group_dn_too_long check (length(trim(group_dn)) < 1025), group_attr text constraint group_attr_too_short check (length(trim(group_attr)) > 0) constraint group_attr_too_long check (length(trim(group_attr)) < 1025), group_filter text constraint group_filter_too_short check (length(trim(group_filter)) > 0) constraint group_filter_too_long check (length(trim(group_filter)) < 2049) ); comment on table auth_ldap_group_entry_search is 'auth_ldap_group_entry_search entries specify the required parameters to find ' 'the groups a user is a member of'; create function auth_ldap_method_group_search() returns trigger as $$ declare n integer; begin if new.enable_groups = true and new.use_token_groups = false then select into n count(*) from auth_ldap_group_entry_search where ldap_method_id = new.public_id; if n < 1 then raise exception 'During % of auth_ldap_method public_id=% must have a configured group_dn when enable_groups = true and use_token_groups = false',tg_op,new.public_id; end if; end if; return null; end; $$ language plpgsql; comment on function auth_ldap_method_children() is 'function used on auth_ldap_method after insert/update initially deferred to ensure each ' 'groups search is properly configured when enable_groups is true and use_token_groups is false'; create constraint trigger auth_ldap_method_group_search after insert or update on auth_ldap_method deferrable initially deferred for each row execute procedure auth_ldap_method_group_search(); -- auth_ldap_certificate entries are optional PEM encoded x509 certificates. -- Each entry is a single certificate. An ldap auth method may have 0 or more -- of these optional x509s. If an auth method has any cert entries, they are -- used as trust anchors when connecting to the auth method's ldap provider -- (instead of the host system's cert chain). create table auth_ldap_certificate ( create_time wt_timestamp, ldap_method_id wt_public_id not null constraint auth_ldap_method_fkey references auth_ldap_method(public_id) on delete cascade on update cascade, certificate bytea not null constraint certificate_must_not_be_empty check(length(certificate) > 0), primary key(ldap_method_id, certificate) ); comment on table auth_ldap_certificate is 'auth_ldap_certificate entries are optional PEM encoded x509 certificates. ' 'Each entry is a single certificate. An ldap auth method may have 0 or more ' 'of these optional x509s. If an auth method has any cert entries, they are ' 'used as trust anchors when connecting to the auth methods ldap provider ' '(instead of the host system cert chain)'; create table auth_ldap_client_certificate ( create_time wt_timestamp, ldap_method_id wt_public_id primary key constraint auth_ldap_method_fkey references auth_ldap_method (public_id) on delete cascade on update cascade, certificate bytea not null -- PEM encoded certificate constraint certificate_must_not_be_empty check(length(certificate) > 0), certificate_key bytea not null -- encrypted PEM encoded private key for certificate constraint certificate_key_must_not_be_empty check(length(certificate_key) > 0), certificate_key_hmac bytea not null constraint certificate_key_hmac_must_not_be_empty check(length(certificate_key_hmac) > 0), key_id text not null constraint kms_data_key_version_fkey references kms_data_key_version (private_id) on delete restrict on update cascade ); comment on table auth_ldap_client_certificate is 'auth_ldap_client_certificate entries contains a client certificate that a ' 'auth_ldap_method uses for mTLS when connecting to an LDAP server. ' 'An auth_ldap_method can have 0 or 1 client certificates.'; create table auth_ldap_bind_credential ( create_time wt_timestamp, ldap_method_id wt_public_id primary key constraint auth_ldap_method_fkey references auth_ldap_method (public_id) on delete cascade on update cascade, dn text not null constraint dn_too_short check (length(trim(dn)) > 0) constraint dn_too_long check (length(trim(dn)) < 2049), password bytea not null constraint password_not_empty check(length(password) > 0), -- encrypted password0 password_hmac bytea not null constraint password_hmac_not_empty check(length(password_hmac) > 0), key_id text not null constraint kms_data_key_version_fkey references kms_data_key_version (private_id) on delete restrict on update cascade ); comment on table auth_ldap_bind_credential is 'auth_ldap_bind_credential entries allow Boundary to bind (aka authenticate) using ' 'the provided credentials when searching for the user entry used to authenticate.'; -- auth_ldap_account_attribute_map entries are the optional attribute maps from custom -- attributes to the standard attribute of fullname and email. There can be 0 or more -- for each parent ldap auth method. create table auth_ldap_account_attribute_map ( create_time wt_timestamp, ldap_method_id wt_public_id constraint auth_ldap_method_fkey references auth_ldap_method(public_id) on delete cascade on update cascade, from_attribute text not null constraint from_attribute_must_not_be_empty check(length(trim(from_attribute)) > 0) constraint from_attribute_must_be_less_than_1024_chars check(length(trim(from_attribute)) < 1024), to_attribute text not null constraint to_attribute_valid_values check (lower(to_attribute) in ('fullname', 'email')), -- intentionally case-sensitive matching primary key(ldap_method_id, to_attribute) ); comment on table auth_ldap_account_attribute_map is 'auth_ldap_account_attribute_map entries are the optional attribute maps from custom attributes to ' 'the standard attributes of sub, name and email. There can be 0 or more for each parent ldap auth method.'; create trigger default_create_time_column before insert on auth_ldap_account_attribute_map for each row execute procedure default_create_time(); create trigger immutable_columns before update on auth_ldap_account_attribute_map for each row execute procedure immutable_columns('ldap_method_id', 'from_attribute', 'to_attribute', 'create_time'); create table auth_ldap_account ( public_id wt_public_id primary key, auth_method_id wt_public_id not null, -- NOTE(mgaffney): The scope_id type is not wt_scope_id because the domain -- check is executed before the insert trigger which retrieves the scope_id -- causing an insert to fail. scope_id text not null, name wt_name, description wt_description, create_time wt_timestamp, update_time wt_timestamp, version wt_version, login_name text not null constraint login_name_must_be_lowercase check(lower(trim(login_name)) = login_name) constraint login_name_must_not_be_empty check(length(trim(login_name)) > 0), email wt_email, full_name wt_full_name, dn text -- will be null until the first successful authentication constraint dn_must_not_be_empty check(length(trim(dn)) > 0), member_of_groups jsonb -- will be null until the first successful authentication constraint member_of_groups_must_not_be_empty check(length(trim(member_of_groups::text)) > 0), constraint auth_ldap_method_fkey foreign key (scope_id, auth_method_id) references auth_ldap_method (scope_id, public_id) on delete cascade on update cascade, constraint auth_account_fkey foreign key (scope_id, auth_method_id, public_id) references auth_account (scope_id, auth_method_id, public_id) on delete cascade on update cascade, constraint auth_ldap_account_auth_method_id_name_uq unique(auth_method_id, name), constraint auth_ldap_account_auth_method_id_login_name_uq unique(auth_method_id, login_name), constraint auth_ldap_account_auth_method_id_dn_uq unique(auth_method_id, dn), constraint auth_ldap_account_auth_method_id_public_id_uq unique(auth_method_id, public_id) ); comment on table auth_ldap_account is 'auth_ldap_account entries are subtypes of auth_account and represent an ldap account.'; -- insert_auth_ldap_account_subtype is intended as a before insert -- trigger on auth_ldap_account. Its purpose is to insert a base -- auth_account for new ldap accounts. It's a bit different than the -- standard trigger for this, because it will have conflicting PKs -- and we just want to "do nothing" on those conflicts, deferring the -- raising on an error to insert into the auth_ldap_account table. -- this is all necessary because of we're using predictable public ids -- for ldap accounts. create or replace function insert_auth_ldap_account_subtype() returns trigger as $$ begin select auth_method.scope_id into new.scope_id from auth_method where auth_method.public_id = new.auth_method_id; insert into auth_account (public_id, auth_method_id, scope_id) values (new.public_id, new.auth_method_id, new.scope_id) on conflict do nothing; return new; end; $$ language plpgsql; create trigger insert_auth_ldap_account_subtype before insert on auth_ldap_account for each row execute procedure insert_auth_ldap_account_subtype(); create trigger delete_auth_account_subtype after delete on auth_ldap_account for each row execute procedure delete_auth_account_subtype(); create trigger update_time_column before update on auth_ldap_account for each row execute procedure update_time_column(); create trigger immutable_columns before update on auth_ldap_account for each row execute procedure immutable_columns('public_id', 'auth_method_id', 'scope_id', 'create_time', 'login_name'); create trigger default_create_time_column before insert on auth_ldap_account for each row execute procedure default_create_time(); create trigger update_version_column after update on auth_ldap_account for each row execute procedure update_version_column(); insert into oplog_ticket (name, version) values ('auth_ldap_method', 1), -- auth_ldap_method is the root aggregate itself and all of its value objects. ('auth_ldap_account', 1), ('auth_ldap_managed_group', 1); -- updated in 76/01_ldap.up.sql -- ldap_auth_method_with_value_obj is useful for reading an ldap auth method -- with its associated value objects (urls, certs, search config, etc). The use -- of the postgres string_agg(...) to aggregate the url and cert value objects -- into a column works because we are only pulling in one column from the -- associated tables and that value is part of the primary key and unique. This -- view will make things like recursive listing of ldap auth methods fairly -- straightforward to implement for the ldap repo. The view also includes an -- is_primary_auth_method bool create view ldap_auth_method_with_value_obj as select case when s.primary_auth_method_id is not null then true else false end as is_primary_auth_method, am.public_id, am.scope_id, am.name, am.description, am.create_time, am.update_time, am.version, am.state, am.start_tls, am.insecure_tls, am.discover_dn, am.anon_group_search, am.upn_domain, am.enable_groups, am.use_token_groups, -- the string_agg(..) column will be null if there are no associated value objects string_agg(distinct url.url, '|') as urls, string_agg(distinct cert.certificate, '|') as certs, string_agg(distinct concat_ws('=', aam.from_attribute, aam.to_attribute), '|') as account_attribute_map, -- the rest of the fields are zero to one relationships that are stored in -- related tables. Since we're outer joining with these tables, we need to -- either add them to the group by, use an aggregating func, or handle -- multiple rows returning for each auth method. I've chosen to just use -- string_agg(...) string_agg(distinct uc.user_dn, '|') as user_dn, string_agg(distinct uc.user_attr, '|') as user_attr, string_agg(distinct uc.user_filter, '|') as user_filter, string_agg(distinct gc.group_dn, '|') as group_dn, string_agg(distinct gc.group_attr, '|') as group_attr, string_agg(distinct gc.group_filter, '|') as group_filter, string_agg(distinct cc.certificate_key, '|') as client_certificate_key, string_agg(distinct cc.certificate_key_hmac, '|') as client_certificate_key_hmac, string_agg(distinct cc.key_id, '|') as client_certificate_key_id, string_agg(distinct cc.certificate, '|') as client_certificate_cert, string_agg(distinct bc.dn, '|') as bind_dn, string_agg(distinct bc.password, '|') as bind_password, string_agg(distinct bc.password_hmac, '|') as bind_password_hmac, string_agg(distinct bc.key_id, '|') as bind_password_key_id from auth_ldap_method am left outer join iam_scope s on am.public_id = s.primary_auth_method_id left outer join auth_ldap_url url on am.public_id = url.ldap_method_id left outer join auth_ldap_certificate cert on am.public_id = cert.ldap_method_id left outer join auth_ldap_account_attribute_map aam on am.public_id = aam.ldap_method_id left outer join auth_ldap_user_entry_search uc on am.public_id = uc.ldap_method_id left outer join auth_ldap_group_entry_search gc on am.public_id = gc.ldap_method_id left outer join auth_ldap_client_certificate cc on am.public_id = cc.ldap_method_id left outer join auth_ldap_bind_credential bc on am.public_id = bc.ldap_method_id group by am.public_id, is_primary_auth_method; -- there can be only one public_id + is_primary_auth_method, so group by isn't a problem. comment on view ldap_auth_method_with_value_obj is 'ldap auth method with its associated value objects (urls, certs, search config, etc)'; create table auth_ldap_managed_group ( public_id wt_public_id primary key, auth_method_id wt_public_id not null, name wt_name, description wt_description, create_time wt_timestamp, update_time wt_timestamp, version wt_version, group_names jsonb not null constraint group_names_must_not_be_empty check(length(trim(group_names::text)) > 0), constraint auth_ldap_method_fkey foreign key (auth_method_id) -- fk1 references auth_ldap_method (public_id) on delete cascade on update cascade, -- Ensure it relates to an abstract managed group constraint auth_managed_group_fkey foreign key (auth_method_id, public_id) -- fk2 references auth_managed_group (auth_method_id, public_id) on delete cascade on update cascade, constraint auth_ldap_managed_group_auth_method_id_name_uq unique(auth_method_id, name) ); comment on table auth_ldap_managed_group is 'auth_ldap_managed_group entries are subtypes of auth_managed_group and represent an ldap managed group.'; -- Define the immutable fields of auth_ldap_managed_group create trigger immutable_columns before update on auth_ldap_managed_group for each row execute procedure immutable_columns('public_id', 'auth_method_id', 'create_time'); -- Populate create time on insert create trigger default_create_time_column before insert on auth_ldap_managed_group for each row execute procedure default_create_time(); -- Generate update time on update create trigger update_time_column before update on auth_ldap_managed_group for each row execute procedure update_time_column(); -- Update version when something changes create trigger update_version_column after update on auth_ldap_managed_group for each row execute procedure update_version_column(); -- Add into the base table when inserting into the concrete table create trigger insert_managed_group_subtype before insert on auth_ldap_managed_group for each row execute procedure insert_managed_group_subtype(); -- Ensure that deletions in the ldap subtype result in deletions to the base -- table. create trigger delete_managed_group_subtype after delete on auth_ldap_managed_group for each row execute procedure delete_managed_group_subtype(); -- auth_ldap_managed_group_member_account uses CTEs to expand and "normalize" the jsonb column -- containing groups in both the accounts and managed groups, then it joins these -- "normalized" expressions into a join table of mangagd group member account entries create view auth_ldap_managed_group_member_account as with account(id, group_name) as ( select a.public_id, ag.group_name from auth_ldap_account a left join jsonb_array_elements(a.member_of_groups) as ag(group_name) on true ), groups (create_time, id, group_name) as ( select g.create_time, g.public_id, mg.group_name from auth_ldap_managed_group g left join jsonb_array_elements(g.group_names) as mg(group_name) on true ) select distinct groups.create_time, account.id as member_id, groups.id as managed_group_id from account, groups where account.group_name = groups.group_name; comment on view auth_ldap_managed_group_member_account is 'auth_ldap_managed_group_member_account is the join view for ' 'managed ldap groups and accounts'; -- recreate view defined in postgres/9/03_oidc_managed_group_member.up.sql -- so the new view can include both oidc and ldap managed groups drop view auth_managed_group_member_account; -- create view with both oidc and ldap managed groups; we can replace this view -- to union with other subtype tables as needed in the future. create view auth_managed_group_member_account as select oidc.create_time, oidc.managed_group_id, oidc.member_id from auth_oidc_managed_group_member_account oidc union select ldap.create_time, ldap.managed_group_id, ldap.member_id from auth_ldap_managed_group_member_account ldap; comment on view auth_managed_group_member_account is ''; commit;