begin;

-- auth_oidc_method entries are the current oidc auth methods configured for
-- existing scopes. 
create table auth_oidc_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_oidc_method_state_enm_fkey
      references auth_oidc_method_state_enm(name)
      on delete restrict
      on update cascade,
  disable_discovered_config_validation bool not null default false,
  api_url wt_url, -- an address prefix at which the boundary api is reachable.
  issuer wt_url,
  client_id text  -- oidc client identifier issued by the oidc provider.
    constraint client_id_not_empty
    check(length(trim(client_id)) > 0), 
  client_secret bytea, -- encrypted oidc client secret issued by the oidc provider.
  client_secret_hmac text 
    constraint client_secret_hmac_not_empty
    check(length(trim(client_secret_hmac)) > 0),
  key_id wt_private_id not null -- key used to encrypt entries via wrapping wrapper. 
    constraint kms_database_key_version_fkey
      references kms_database_key_version(private_id) 
      on delete restrict
      on update cascade, 
    constraint key_id_not_empty
      check(length(trim(key_id)) > 0),
  max_age int  -- the allowable elapsed time in secs since the last time the user was authenticated. A value -1 basically forces the IdP to re-authenticate the End-User.  Zero is not a valid value. 
    constraint max_age_not_equal_zero
      check(max_age != 0)
    constraint max_age_not_less_then_negative_one
      check(max_age >= -1), 
  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_oidc_method_scope_id_name_uq
    unique(scope_id, name),
  constraint auth_oidc_method_scope_id_public_id_uq
    unique(scope_id, public_id),
  constraint auth_oidc_method_scope_id_issuer_client_id_unique
    unique(scope_id, issuer, client_id) -- a client_id must be unique for a provider within a scope.
);
comment on table auth_oidc_method is
'auth_oidc_method entries are the current oidc auth methods configured for existing scopes.';

-- auth_oidc_signing_alg entries are the signing algorithms allowed for an oidc
-- auth method.  There must be at least one allowed alg for each oidc auth method.
create table auth_oidc_signing_alg (
  create_time wt_timestamp,
  oidc_method_id wt_public_id 
    constraint auth_oidc_method_fkey
    references auth_oidc_method(public_id)
    on delete cascade
    on update cascade,
  signing_alg_name text 
    constraint auth_oidc_signing_alg_enm_fkey
    references auth_oidc_signing_alg_enm(name)
    on delete restrict
    on update cascade,
  primary key(oidc_method_id, signing_alg_name)
);
comment on table auth_oidc_signing_alg is
'auth_oidc_signing_alg entries are the signing algorithms allowed for an oidc auth method. There must be at least one allowed alg for each oidc auth method';

-- auth_oidc_aud_claim entries are the audience claims for a specific oidc auth
-- method.  There can be 0 or more for each parent oidc auth method.  If an auth
-- method has any aud claims, an ID token must contain one of them to be valid. 
create table auth_oidc_aud_claim (
  create_time wt_timestamp,
  oidc_method_id wt_public_id 
    constraint auth_oidc_method_fkey
    references auth_oidc_method(public_id)
    on delete cascade
    on update cascade,
  aud_claim text not null
    constraint aud_claim_must_not_be_empty
    check(length(trim(aud_claim)) > 0) 
    constraint aud_claim_must_be_less_than_1024_chars
      check(length(trim(aud_claim)) < 1024),
  primary key(oidc_method_id, aud_claim)
);
comment on table auth_oidc_aud_claim is
'auth_oidc_aud_claim entries are the audience claims for a specific oidc auth method.  There can be 0 or more for each parent oidc auth method.  If an auth method has any aud claims, an ID token must contain one of them to be valid.';


-- auth_oidc_certificate entries are optional PEM encoded x509 certificates.
-- Each entry is a single certificate.  An oidc 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 oidc provider
-- (instead of the host system's cert chain).
create table auth_oidc_certificate (
  create_time wt_timestamp,
  oidc_method_id wt_public_id 
    constraint auth_oidc_method_fkey
    references auth_oidc_method(public_id)
    on delete cascade
    on update cascade,
  certificate bytea not null,
  primary key(oidc_method_id, certificate)
);
comment on table auth_oidc_certificate is
'auth_oidc_certificate entries are optional PEM encoded x509 certificates. Each entry is a single certificate.  An oidc 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 oidc provider (instead of the host system cert chain)';


-- auth_oidc_account entries are subtypes of auth_account and represent an
-- oidc account.
create table auth_oidc_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,
    issuer wt_url not null, -- case-sensitive URL that maps to an id_token's iss claim,
    subject text not null -- case-sensitive string that maps to an id_token's sub claim
      constraint subject_must_not_be_empty
      check (
        length(trim(subject)) > 0
      )
      constraint subject_must_be_less_than_256_chars
      check(
        length(trim(subject)) <= 255 -- length limit per OIDC spec
      ),
    full_name wt_full_name, -- may be null and maps to an id_token's name claim
    email wt_email, -- may be null and maps to the id_token's email claim
    constraint auth_oidc_method_fkey
      foreign key (scope_id, auth_method_id)
        references auth_oidc_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_oidc_account_auth_method_id_name_uq
      unique(auth_method_id, name),
    -- ###############################################################
    -- any change to this constraints name must be aligned with the 
    -- acctUpsertQuery const in internal/auth/oidc/query.go
    -- ###############################################################
    constraint auth_oidc_account_auth_method_id_issuer_subject_uq
      unique(auth_method_id, issuer, subject), -- subject must be unique for a provider within specific auth method
    constraint auth_oidc_account_auth_method_id_public_id_uq
      unique(auth_method_id, public_id)
);
comment on table auth_oidc_method is
'auth_oidc_account entries are subtypes of auth_account and represent an oidc account.';

-- auth_oidc_method column triggers
create trigger
  insert_auth_method_subtype
before insert on auth_oidc_method
  for each row execute procedure insert_auth_method_subtype();

create trigger
  update_time_column
before
update on auth_oidc_method
  for each row execute procedure update_time_column();

create trigger
  immutable_columns
before
update on auth_oidc_method
  for each row execute procedure immutable_columns('public_id', 'scope_id', 'create_time');

create trigger
  default_create_time_column
before
insert on auth_oidc_method
  for each row execute procedure default_create_time();

create trigger
  update_version_column
after update on auth_oidc_method
  for each row execute procedure update_version_column();

-- active_auth_oidc_method_must_be_complete() defines a function to be used in 
-- a "before update" trigger for auth_oidc_method entries.  Its intent: prevent
-- incomplete oidc methods from transitioning out of the "inactive" state.
create or replace function
  active_auth_oidc_method_must_be_complete()
  returns trigger
as $$
  begin
    -- validate signing alg
    if old.state = 'inactive' and new.state != 'inactive' then
      perform 
      from 
        auth_oidc_method am
       join auth_oidc_signing_alg alg on am.public_id = alg.oidc_method_id
      where
        new.public_id = am.public_id;
      if not found then 
        raise exception 'an incomplete oidc auth method must remain inactive';
      end if;
      -- validate issuer
      case 
        when new.issuer != old.issuer then
          if length(trim(new.issuer)) = 0 then
            raise exception 'empty issuer: an incomplete oidc auth method must remain inactive';
          end if;
        when new.issuer = old.issuer then
          if length(trim(old.issuer)) = 0 then
            raise exception 'empty issuer: an incomplete oidc auth method must remain inactive';
          end if;
        else
      end case;
      -- validate client_id
      case 
        when new.client_id != old.client_id then
          if length(trim(new.client_id)) = 0 then
            raise exception 'empty client_id: an incomplete oidc auth method must remain inactive';
          end if;
        when new.client_id = old.client_id then
          if length(trim(old.client_id)) = 0 then
            raise exception 'empty client_id: an incomplete oidc auth method must remain inactive';
          end if;
        else
      end case;
      -- validate client_secret
      case 
        when new.client_secret != old.client_secret then
          if length(new.client_secret) = 0 then
            raise exception 'empty client_secret: an incomplete oidc auth method must remain inactive';
          end if;
        when new.client_secret = old.client_secret then
          if length(old.client_secret) = 0 then
            raise exception 'empty client_secret: an incomplete oidc auth method must remain inactive';
          end if;
        else
      end case;


    end if;
    return new;
  end;
$$ language plpgsql;
comment on function active_auth_oidc_method_must_be_complete() is
'active_auth_oidc_method_must_be_complete() will raise an error if the oidc auth method is not complete';

create trigger 
  update_active_auth_oidc_method_must_be_complete
before
update on auth_oidc_method
  for each row execute procedure active_auth_oidc_method_must_be_complete();

-- new_auth_oidc_method_must_be_inactive() defines a function to be used in 
-- a "before insert" trigger for auth_oidc_method entries.  Its intent: 
-- only allow "inactive" auth methods to be inserted.  Why? there's no way
-- you can insert an entry that's anything but incomplete, since we have a 
-- chicken/egg problem: you need the auth method id to create the required
-- signing algs value objects.
create or replace function
  new_auth_oidc_method_must_be_inactive()
  returns trigger 
as $$
  begin
    if new.state != 'inactive' then
      raise exception 'an incomplete oidc method must be inactive';
    end if;
  end;
$$ language plpgsql;
comment on function new_auth_oidc_method_must_be_inactive() is
'new_auth_oidc_method_must_be_inactive ensures that new incomplete oidc auth methods must remain inactive';

create trigger 
  new_auth_oidc_method_must_be_inactive
before
insert on auth_oidc_method
  for each row execute procedure active_auth_oidc_method_must_be_complete();

-- auth_oidc_account column triggers
create trigger
  update_time_column
before
update on auth_oidc_account
  for each row execute procedure update_time_column();

create trigger
  immutable_columns
before
update on auth_oidc_account
  for each row execute procedure immutable_columns('public_id', 'auth_method_id', 'scope_id', 'create_time', 'issuer', 'subject');

create trigger
  default_create_time_column
before
insert on auth_oidc_account
  for each row execute procedure default_create_time();

create trigger
  update_version_column
after update on auth_oidc_account
  for each row execute procedure update_version_column();


-- insert_auth_oidc_account_subtype is intended as a before insert
-- trigger on auth_oidc_account. Its purpose is to insert a base
-- auth_account for new oidc 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_oidc_account table.
-- this is all necessary because of we're using predictable public ids
-- for oidc accounts.
create or replace function
  insert_auth_oidc_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_oidc_account_subtype
before insert on auth_oidc_account
  for each row execute procedure insert_auth_oidc_account_subtype();

-- triggers for auth_oidc_method children tables: auth_oidc_aud_claim,
-- auth_oidc_certificate, auth_oidc_signing_alg


-- on_delete_active_auth_oidc_method_must_be_complete() defines a function
-- to be used in an "after delete" trigger for auth_oidc_signing_alg
-- Its intent: prevent deletes that would result in an "active" oidc
-- auth method which is incomplete.
create or replace function
  on_delete_active_auth_oidc_method_must_be_complete()
  returns trigger
as $$
declare am_state text;
declare alg_cnt int;
  begin
    select 
      am.state,
      count(alg.oidc_method_id) as alg_cnt
    from
      auth_oidc_method am
      left outer join auth_oidc_signing_alg alg on am.public_id = alg.oidc_method_id
    where
      new.oidc_method_id = am.public_id
    group by am.public_id
    into am_state, alg_cnt;
    
    if not found then 
      return new; -- auth method was deleted, so we're done
    end if;

    if am_state != inactive then
      case 
        when alg_cnt = 0 then
          raise exception 'delete would have resulted in an incomplete active oidc auth method with no signing algorithms';
      end case;
    end if; 
  
    return new;
  end;
$$ language plpgsql;
comment on function on_delete_active_auth_oidc_method_must_be_complete() is
'on_delete_active_auth_oidc_method_must_be_complete() will raise an error if the oidc auth method is not complete after a delete on algs';

create trigger
  default_create_time_column
before
insert on auth_oidc_aud_claim
  for each row execute procedure default_create_time();

create trigger
  default_create_time_column
before
insert on auth_oidc_certificate
  for each row execute procedure default_create_time();

create trigger
  default_create_time_column
before
insert on auth_oidc_signing_alg
  for each row execute procedure default_create_time();

create trigger 
  on_delete_active_auth_oidc_method_must_be_complete
after
delete on auth_oidc_signing_alg
  for each row execute procedure on_delete_active_auth_oidc_method_must_be_complete();
    
insert into oplog_ticket (name, version)
values
  ('auth_oidc_method', 1), -- auth method is the root aggregate itself and all of its value objects.
  ('auth_oidc_account', 1);

commit;
