diff --git a/internal/db/domains_test.go b/internal/db/domains_test.go index 22a9c007f8..46265fb4a9 100644 --- a/internal/db/domains_test.go +++ b/internal/db/domains_test.go @@ -669,3 +669,234 @@ set id = null; assert.Equal(orig.CreateTime, found.CreateTime) }) } + +func TestDomain_wt_url(t *testing.T) { + const ( + createTable = ` +create table if not exists test_table_wt_url ( + id bigint generated always as identity primary key, + url wt_url +); +` + insert = ` +insert into test_table_wt_url(url) +values ($1) +returning id; +` + ) + + conn, _ := TestSetup(t, "postgres") + db := conn.DB() + + if _, err := db.Exec(createTable); err != nil { + t.Fatalf("query: \n%s\n error: %s", createTable, err) + } + + failTests := []struct { + value string + exception string + }{ + {"http://", ""}, + {"htt://", "wt_url_invalid_protocol"}, + {"ht", "wt_url_invalid_protocol"}, + {"http://" + strings.Repeat("a", 4001), "wt_url_too_long"}, + } + for _, tt := range failTests { + t.Run(tt.value, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + t.Logf("insert value: %q", tt.value) + _, err := db.Query(insert, tt.value) + if tt.exception != "" { + require.Error(err) + assert.Containsf(err.Error(), tt.exception, "missing %s from err: %s", tt.exception, err.Error()) + return + } + require.NoError(err) + }) + } +} + +func TestDomain_wt_email(t *testing.T) { + const ( + createTable = ` +create table if not exists test_table_wt_email ( + id bigint generated always as identity primary key, + email wt_email +); +` + insert = ` +insert into test_table_wt_email(email) +values ($1) +returning id; +` + ) + + conn, _ := TestSetup(t, "postgres") + db := conn.DB() + + if _, err := db.Exec(createTable); err != nil { + t.Fatalf("query: \n%s\n error: %s", createTable, err) + } + + failTests := []struct { + value string + exception string + }{ + {" ", "wt_email_too_short"}, + {"1" + strings.Repeat("1", 320), "wt_email_too_long"}, + {"alice@bob.com", ""}, + } + for _, tt := range failTests { + t.Run(tt.value, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + t.Logf("insert value: %q", tt.value) + _, err := db.Query(insert, tt.value) + if tt.exception != "" { + require.Error(err) + assert.Containsf(err.Error(), tt.exception, "missing %s from err: %s", tt.exception, err.Error()) + return + } + require.NoError(err) + }) + } +} + +func TestDomain_wt_full_name(t *testing.T) { + const ( + createTable = ` +create table if not exists test_table_wt_full_name ( + id bigint generated always as identity primary key, + full_name wt_full_name +); +` + insert = ` +insert into test_table_wt_full_name(full_name) +values ($1) +returning id; +` + ) + + conn, _ := TestSetup(t, "postgres") + db := conn.DB() + + if _, err := db.Exec(createTable); err != nil { + t.Fatalf("query: \n%s\n error: %s", createTable, err) + } + + failTests := []struct { + value string + exception string + }{ + {" ", "wt_full_name_too_short"}, + {"1" + strings.Repeat("1", 512), "wt_full_name_too_long"}, + {"alice and bob's dinner", ""}, + } + for _, tt := range failTests { + t.Run(tt.value, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + t.Logf("insert value: %q", tt.value) + _, err := db.Query(insert, tt.value) + if tt.exception != "" { + require.Error(err) + assert.Containsf(err.Error(), tt.exception, "missing %s from err: %s", tt.exception, err.Error()) + return + } + require.NoError(err) + }) + } +} + +func TestDomain_wt_name(t *testing.T) { + const ( + createTable = ` +create table if not exists test_table_wt_name ( + id bigint generated always as identity primary key, + name wt_name +); +` + insert = ` +insert into test_table_wt_name(name) +values ($1) +returning id; +` + ) + + conn, _ := TestSetup(t, "postgres") + db := conn.DB() + + if _, err := db.Exec(createTable); err != nil { + t.Fatalf("query: \n%s\n error: %s", createTable, err) + } + + failTests := []struct { + value string + exception string + }{ + {" ", "wt_name_too_short"}, + {"1" + strings.Repeat("1", 512), "wt_name_too_long"}, + {"alice and bob's dinner", ""}, + } + for _, tt := range failTests { + t.Run(tt.value, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + t.Logf("insert value: %q", tt.value) + _, err := db.Query(insert, tt.value) + if tt.exception != "" { + require.Error(err) + assert.Containsf(err.Error(), tt.exception, "missing %s from err: %s", tt.exception, err.Error()) + return + } + require.NoError(err) + }) + } +} + +func TestDomain_wt_description(t *testing.T) { + const ( + createTable = ` +create table if not exists test_table_wt_description ( + id bigint generated always as identity primary key, + description wt_description +); +` + insert = ` +insert into test_table_wt_description(description) +values ($1) +returning id; +` + ) + + conn, _ := TestSetup(t, "postgres") + db := conn.DB() + + if _, err := db.Exec(createTable); err != nil { + t.Fatalf("query: \n%s\n error: %s", createTable, err) + } + + failTests := []struct { + value string + exception string + }{ + {" ", "wt_description_too_short"}, + {"1" + strings.Repeat("1", 1024), "wt_description_too_long"}, + {"alice and bob's dinner has delicious pancakes", ""}, + } + for _, tt := range failTests { + t.Run(tt.value, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + t.Logf("insert value: %q", tt.value) + _, err := db.Query(insert, tt.value) + if tt.exception != "" { + require.Error(err) + assert.Containsf(err.Error(), tt.exception, "missing %s from err: %s", tt.exception, err.Error()) + return + } + require.NoError(err) + }) + } +} diff --git a/internal/db/schema/migrations/postgres/dev/80_domains.down.sql b/internal/db/schema/migrations/postgres/dev/80_domains.down.sql new file mode 100644 index 0000000000..019b6b36bb --- /dev/null +++ b/internal/db/schema/migrations/postgres/dev/80_domains.down.sql @@ -0,0 +1,4 @@ +begin; + + +commit; \ No newline at end of file diff --git a/internal/db/schema/migrations/postgres/dev/80_domains.up.sql b/internal/db/schema/migrations/postgres/dev/80_domains.up.sql new file mode 100644 index 0000000000..1901389e5a --- /dev/null +++ b/internal/db/schema/migrations/postgres/dev/80_domains.up.sql @@ -0,0 +1,58 @@ +begin; + +-- wt_email defines a type for email which must be less than 320 chars and only +-- contain lower case values. The type is defined to allow nulls and not be +-- unique, which can be overriden as needed when used in tables. +create domain wt_email as text + constraint wt_email_too_short + check (length(trim(value)) > 0) + constraint wt_email_too_long + check (length(trim(value)) < 320); +comment on domain wt_email is +'standard column for email addresses'; + +-- wt_full_name defines a type for a person's full name which must be less than +-- 512 chars. The type is defined to allow nulls and not be unique, which can +-- be overriden as needed when used in tables. +create domain wt_full_name text + constraint wt_full_name_too_short + check (length(trim(value)) > 0) + constraint wt_full_name_too_long + check(length(trim(value)) <= 512); -- gotta pick some upper limit. +comment on domain wt_full_name is +'standard column for the full name of a person'; + +-- wt_url defines a type for URLs which must be longer that 3 chars and +-- less than 4k chars. It's defined to allow nulls, which can be overriden as +-- needed when used in tables. +create domain wt_url as text + constraint wt_url_too_short + check (length(trim(value)) > 3) + constraint wt_url_too_long + check (length(trim(value)) < 4000) + constraint wt_url_invalid_protocol + check (value ~ 'https?:\/\/*'); +comment on domain wt_url is +'standard column for URLs'; + +-- wt_name defines a type for resource names that must be less than 128 chars. +-- It's defined to allow nulls. +create domain wt_name as text + constraint wt_name_too_short + check (length(trim(value)) > 0) + constraint wt_name_too_long + check (length(trim(value)) < 128); +comment on domain wt_name is +'standard column for resource names'; + +-- wt_description defines a type for resource descriptionss that must be less +-- than 1024 chars. It's defined to allow nulls. +create domain wt_description as text + constraint wt_description_too_short + check (length(trim(value)) > 0) + constraint wt_description_too_long + check (length(trim(value)) < 1024); +comment on domain wt_description is +'standard column for resource descriptions'; + +commit; diff --git a/internal/db/schema/migrations/postgres/dev/82_kms.up.sql b/internal/db/schema/migrations/postgres/dev/82_kms.up.sql index 3935fb5392..bcdbbc9d07 100644 --- a/internal/db/schema/migrations/postgres/dev/82_kms.up.sql +++ b/internal/db/schema/migrations/postgres/dev/82_kms.up.sql @@ -62,4 +62,4 @@ create trigger before insert on kms_oidc_key_version for each row execute procedure kms_version_column('oidc_key_id'); -commit; \ No newline at end of file +commit; diff --git a/internal/db/schema/migrations/postgres/dev/83_oidc_enm.down.sql b/internal/db/schema/migrations/postgres/dev/83_oidc_enm.down.sql new file mode 100644 index 0000000000..c3a3255651 --- /dev/null +++ b/internal/db/schema/migrations/postgres/dev/83_oidc_enm.down.sql @@ -0,0 +1,3 @@ +begin; + +commit; \ No newline at end of file diff --git a/internal/db/schema/migrations/postgres/dev/83_oidc_enm.up.sql b/internal/db/schema/migrations/postgres/dev/83_oidc_enm.up.sql new file mode 100644 index 0000000000..d33072d86e --- /dev/null +++ b/internal/db/schema/migrations/postgres/dev/83_oidc_enm.up.sql @@ -0,0 +1,71 @@ +begin; + +-- auth_oidc_method_state_enum entries define the possible oidc auth method +-- states. +create table auth_oidc_method_state_enm ( + name text primary key + constraint only_predefined_oidc_method_states_allowed + check ( + name in ('inactive', 'active-private', 'active-public', 'stopping') + ) +); + +-- populate the values of auth_oidc_method_state_enm +insert into auth_oidc_method_state_enm(name) + values + ('inactive'), + ('active-private'), + ('active-public'), + ('stopping'); + + -- define the immutable fields for auth_oidc_method_state_enm (all of them) +create trigger + immutable_columns +before +update on auth_oidc_method_state_enm + for each row execute procedure immutable_columns('name'); + + +-- auth_oidc_signing_alg entries define the supported oidc auth method +-- signing algorithms. +create table auth_oidc_signing_alg_enm ( + name text primary key + constraint only_predefined_auth_oidc_signing_algs_allowed + check ( + name in ( + 'RS256', + 'RS384', + 'RS512', + 'ES256', + 'ES384', + 'ES512', + 'PS256', + 'PS384', + 'PS512', + 'EdDSA') + ) +); + +-- populate the values of auth_oidc_signing_alg +insert into auth_oidc_signing_alg_enm (name) + values + ('RS256'), + ('RS384'), + ('RS512'), + ('ES256'), + ('ES384'), + ('ES512'), + ('PS256'), + ('PS384'), + ('PS512'), + ('EdDSA') + ; + + -- define the immutable fields for auth_oidc_signing_alg (all of them) +create trigger + immutable_columns +before +update on auth_oidc_signing_alg_enm + for each row execute procedure immutable_columns('name'); + +commit; diff --git a/internal/db/schema/migrations/postgres/dev/84_oidc.down.sql b/internal/db/schema/migrations/postgres/dev/84_oidc.down.sql new file mode 100644 index 0000000000..e3090523af --- /dev/null +++ b/internal/db/schema/migrations/postgres/dev/84_oidc.down.sql @@ -0,0 +1,3 @@ +begin; + +commit; diff --git a/internal/db/schema/migrations/postgres/dev/84_oidc.up.sql b/internal/db/schema/migrations/postgres/dev/84_oidc.up.sql new file mode 100644 index 0000000000..2d7bacb86b --- /dev/null +++ b/internal/db/schema/migrations/postgres/dev/84_oidc.up.sql @@ -0,0 +1,193 @@ +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 + references auth_oidc_method_state_enm(name) + on delete restrict + on update cascade, + discovery_url wt_url not null, -- oidc discovery URL without any .well-known component + client_id text not null -- oidc client identifier issued by the oidc provider. + constraint client_id_not_empty + check(length(trim(client_id)) > 0), + client_secret bytea not null, -- encrypted oidc client secret issued by the oidc provider. + key_id wt_private_id not null -- key used to encrypt entries via wrapping wrapper. + references kms_oidc_key_version(private_id) + on delete restrict + on update cascade, + max_age int not null -- the allowable elapsed time in secs since the last time the user was authenticated. zero is allowed and should force the user to be re-authenticated. + constraint max_age_equal_or_greater_than_zero + check(max_age >= 0), + foreign key (scope_id, public_id) + references auth_method (scope_id, public_id) + on delete cascade + on update cascade, + unique(scope_id, name), + unique(scope_id, public_id), + unique(scope_id, discovery_url, client_id) -- a client_id must be unique for a provider within a scope. +); + +-- 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 ( + oidc_method_id wt_public_id + references auth_oidc_method(public_id) + on delete cascade + on update cascade, + signing_alg_name text + references auth_oidc_signing_alg_enm(name) + on delete restrict + on update cascade, + primary key(oidc_method_id, signing_alg_name) +); + +-- auth_oidc_callback_url entries are the callback URLs allowed for a specific +-- oidc auth method. There must be at least one callback url for each oidc auth +-- method. +create table auth_oidc_callback_url ( + oidc_method_id wt_public_id + references auth_oidc_method(public_id) + on delete cascade + on update cascade, + callback_url wt_url not null +); + +-- 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 ( + oidc_method_id wt_public_id + 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) +); + +-- 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 ( + oidc_method_id wt_public_id + references auth_oidc_method(public_id) + on delete cascade + on update cascade, + certificate bytea not null, + primary key(oidc_method_id, certificate) +); + + +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_id wt_url not null, -- case-sensitive URL that maps to an id_token's iss claim + subject_id text not null -- case-senstive string that maps to an id_token's sub claim + constraint subject_id_must_not_be_empty + check ( + length(trim(subject_id)) > 0 + ) + constraint subject_id_must_be_less_than_256_chars + check( + length(trim(subject_id)) <= 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 + foreign key (scope_id, auth_method_id) + references auth_oidc_method (scope_id, public_id) + on delete cascade + on update cascade, + 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, + unique(auth_method_id, name), + unique(auth_method_id, issuer_id, subject_id), -- subject must be unique for a provider within specific auth method + unique(auth_method_id, public_id) +); + +-- 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(); + +-- 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_id', 'subject_id'); + +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(); + +create trigger + insert_auth_account_subtype +before insert on auth_oidc_account + for each row execute procedure insert_auth_account_subtype(); + +commit; diff --git a/internal/db/schema/migrations/postgres/dev/85_authtoken.down.sql b/internal/db/schema/migrations/postgres/dev/85_authtoken.down.sql new file mode 100644 index 0000000000..e3090523af --- /dev/null +++ b/internal/db/schema/migrations/postgres/dev/85_authtoken.down.sql @@ -0,0 +1,3 @@ +begin; + +commit; diff --git a/internal/db/schema/migrations/postgres/dev/85_authtoken.up.sql b/internal/db/schema/migrations/postgres/dev/85_authtoken.up.sql new file mode 100644 index 0000000000..edde6a6f93 --- /dev/null +++ b/internal/db/schema/migrations/postgres/dev/85_authtoken.up.sql @@ -0,0 +1,31 @@ +begin; + +-- auth_token_status_enm entries define the possible auth token +-- states. +create table auth_token_status_enm ( + name text primary key + constraint only_predefined_auth_token_states_allowed + check ( + name in ('auth token pending','token issued', 'authentication failed', 'system error') + ) +); + +-- populate the values of auth_token_status_enm +insert into auth_token_status_enm(name) + values + ('auth token pending'), + ('token issued'), + ('authentication failed'), + ('system error'); + + +-- add the state column with a default to the auth_token table. +alter table auth_token +add column status text +not null +default 'token issued' -- safest default +references auth_token_status_enm(name) + on update cascade + on delete restrict; + +commit; \ No newline at end of file diff --git a/internal/db/schema/migrations/postgres/dev/86_iam.down.sql b/internal/db/schema/migrations/postgres/dev/86_iam.down.sql new file mode 100644 index 0000000000..c3a3255651 --- /dev/null +++ b/internal/db/schema/migrations/postgres/dev/86_iam.down.sql @@ -0,0 +1,3 @@ +begin; + +commit; \ No newline at end of file diff --git a/internal/db/schema/migrations/postgres/dev/86_iam.up.sql b/internal/db/schema/migrations/postgres/dev/86_iam.up.sql new file mode 100644 index 0000000000..a63fcc6f84 --- /dev/null +++ b/internal/db/schema/migrations/postgres/dev/86_iam.up.sql @@ -0,0 +1,66 @@ +begin; + +-- add the account_info_auth_method_id which determines which auth_method is +-- designated as for "account info" in the user's scope. +alter table iam_scope +add column account_info_auth_method_id wt_public_id -- allowed to be null and is mutable of course. +references auth_method(public_id) + on update cascade + on delete set null; + +-- establish a compond fk, but there's no cascading of deletes or updates, since +-- we only want to cascade changes to the account_info_auth_method_id portion of +-- the compond fk and that is handled in a separate fk declaration. +alter table iam_scope +add foreign key (public_id, account_info_auth_method_id) references auth_method(scope_id, public_id); + +-- iam_user_acct_info provides account info for users by determining which +-- auth_method is designated as for "account info" in the user's scope via the +-- scope's account_info_auth_method_id. Every sub-type of auth_account must be +-- added to this view's union. +create view iam_acct_info as +select + aa.iam_user_id, + oa.subject_id as login_name, + oa.full_name as full_name, + oa.email as email +from + iam_scope s, + auth_account aa, + auth_oidc_account oa +where + aa.public_id = oa.public_id and + aa.auth_method_id = s.account_info_auth_method_id +union +select + aa.iam_user_id, + pa.login_name, + '' as full_name, + '' as email +from + iam_scope s, + auth_account aa, + auth_password_account pa +where + aa.public_id = pa.public_id and + aa.auth_method_id = s.account_info_auth_method_id; + +-- iam_user_acct_info provides a simple way to retrieve entries that include +-- both the iam_user fields with an outer join to the user's account info. +create view iam_user_acct_info as +select + u.public_id, + u.scope_id, + u.name, + u.description, + u.create_time, + u.update_time, + u.version, + i.login_name, + i.full_name, + i.email +from + iam_user u +left outer join iam_acct_info i on u.public_id = i.iam_user_id; + +commit; \ No newline at end of file diff --git a/internal/db/schema/postgres_migration.gen.go b/internal/db/schema/postgres_migration.gen.go index fac16214ee..2b84eb79ed 100644 --- a/internal/db/schema/postgres_migration.gen.go +++ b/internal/db/schema/postgres_migration.gen.go @@ -5,7 +5,7 @@ package schema func init() { migrationStates["postgres"] = migrationState{ devMigration: true, - binarySchemaVersion: 1082, + binarySchemaVersion: 1086, upMigrations: map[int][]byte{ 1: []byte(` begin; @@ -4989,6 +4989,67 @@ create table server_tag ( ); commit; +`), + 1080: []byte(` +begin; + +-- wt_email defines a type for email which must be less than 320 chars and only +-- contain lower case values. The type is defined to allow nulls and not be +-- unique, which can be overriden as needed when used in tables. +create domain wt_email as text + constraint wt_email_too_short + check (length(trim(value)) > 0) + constraint wt_email_too_long + check (length(trim(value)) < 320); +comment on domain wt_email is +'standard column for email addresses'; + +-- wt_full_name defines a type for a person's full name which must be less than +-- 512 chars. The type is defined to allow nulls and not be unique, which can +-- be overriden as needed when used in tables. +create domain wt_full_name text + constraint wt_full_name_too_short + check (length(trim(value)) > 0) + constraint wt_full_name_too_long + check(length(trim(value)) <= 512); -- gotta pick some upper limit. +comment on domain wt_full_name is +'standard column for the full name of a person'; + +-- wt_url defines a type for URLs which must be longer that 3 chars and +-- less than 4k chars. It's defined to allow nulls, which can be overriden as +-- needed when used in tables. +create domain wt_url as text + constraint wt_url_too_short + check (length(trim(value)) > 3) + constraint wt_url_too_long + check (length(trim(value)) < 4000) + constraint wt_url_invalid_protocol + check (value ~ 'https?:\/\/*'); +comment on domain wt_url is +'standard column for URLs'; + +-- wt_name defines a type for resource names that must be less than 128 chars. +-- It's defined to allow nulls. +create domain wt_name as text + constraint wt_name_too_short + check (length(trim(value)) > 0) + constraint wt_name_too_long + check (length(trim(value)) < 128); +comment on domain wt_name is +'standard column for resource names'; + +-- wt_description defines a type for resource descriptionss that must be less +-- than 1024 chars. It's defined to allow nulls. +create domain wt_description as text + constraint wt_description_too_short + check (length(trim(value)) > 0) + constraint wt_description_too_long + check (length(trim(value)) < 1024); +comment on domain wt_description is +'standard column for resource descriptions'; + +commit; + `), 1082: []byte(` begin; @@ -5055,6 +5116,378 @@ create trigger before insert on kms_oidc_key_version for each row execute procedure kms_version_column('oidc_key_id'); +commit; + +`), + 1083: []byte(` +begin; + +-- auth_oidc_method_state_enum entries define the possible oidc auth method +-- states. +create table auth_oidc_method_state_enm ( + name text primary key + constraint only_predefined_oidc_method_states_allowed + check ( + name in ('inactive', 'active-private', 'active-public', 'stopping') + ) +); + +-- populate the values of auth_oidc_method_state_enm +insert into auth_oidc_method_state_enm(name) + values + ('inactive'), + ('active-private'), + ('active-public'), + ('stopping'); + + -- define the immutable fields for auth_oidc_method_state_enm (all of them) +create trigger + immutable_columns +before +update on auth_oidc_method_state_enm + for each row execute procedure immutable_columns('name'); + + +-- auth_oidc_signing_alg entries define the supported oidc auth method +-- signing algorithms. +create table auth_oidc_signing_alg_enm ( + name text primary key + constraint only_predefined_auth_oidc_signing_algs_allowed + check ( + name in ( + 'RS256', + 'RS384', + 'RS512', + 'ES256', + 'ES384', + 'ES512', + 'PS256', + 'PS384', + 'PS512', + 'EdDSA') + ) +); + +-- populate the values of auth_oidc_signing_alg +insert into auth_oidc_signing_alg_enm (name) + values + ('RS256'), + ('RS384'), + ('RS512'), + ('ES256'), + ('ES384'), + ('ES512'), + ('PS256'), + ('PS384'), + ('PS512'), + ('EdDSA') + ; + + -- define the immutable fields for auth_oidc_signing_alg (all of them) +create trigger + immutable_columns +before +update on auth_oidc_signing_alg_enm + for each row execute procedure immutable_columns('name'); + +commit; + +`), + 1084: []byte(` +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 + references auth_oidc_method_state_enm(name) + on delete restrict + on update cascade, + discovery_url wt_url not null, -- oidc discovery URL without any .well-known component + client_id text not null -- oidc client identifier issued by the oidc provider. + constraint client_id_not_empty + check(length(trim(client_id)) > 0), + client_secret bytea not null, -- encrypted oidc client secret issued by the oidc provider. + key_id wt_private_id not null -- key used to encrypt entries via wrapping wrapper. + references kms_oidc_key_version(private_id) + on delete restrict + on update cascade, + max_age int not null -- the allowable elapsed time in secs since the last time the user was authenticated. zero is allowed and should force the user to be re-authenticated. + constraint max_age_equal_or_greater_than_zero + check(max_age >= 0), + foreign key (scope_id, public_id) + references auth_method (scope_id, public_id) + on delete cascade + on update cascade, + unique(scope_id, name), + unique(scope_id, public_id), + unique(scope_id, discovery_url, client_id) -- a client_id must be unique for a provider within a scope. +); + +-- 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 ( + oidc_method_id wt_public_id + references auth_oidc_method(public_id) + on delete cascade + on update cascade, + signing_alg_name text + references auth_oidc_signing_alg_enm(name) + on delete restrict + on update cascade, + primary key(oidc_method_id, signing_alg_name) +); + +-- auth_oidc_callback_url entries are the callback URLs allowed for a specific +-- oidc auth method. There must be at least one callback url for each oidc auth +-- method. +create table auth_oidc_callback_url ( + oidc_method_id wt_public_id + references auth_oidc_method(public_id) + on delete cascade + on update cascade, + callback_url wt_url not null +); + +-- 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 ( + oidc_method_id wt_public_id + 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) +); + +-- 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 ( + oidc_method_id wt_public_id + references auth_oidc_method(public_id) + on delete cascade + on update cascade, + certificate bytea not null, + primary key(oidc_method_id, certificate) +); + + +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_id wt_url not null, -- case-sensitive URL that maps to an id_token's iss claim + subject_id text not null -- case-senstive string that maps to an id_token's sub claim + constraint subject_id_must_not_be_empty + check ( + length(trim(subject_id)) > 0 + ) + constraint subject_id_must_be_less_than_256_chars + check( + length(trim(subject_id)) <= 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 + foreign key (scope_id, auth_method_id) + references auth_oidc_method (scope_id, public_id) + on delete cascade + on update cascade, + 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, + unique(auth_method_id, name), + unique(auth_method_id, issuer_id, subject_id), -- subject must be unique for a provider within specific auth method + unique(auth_method_id, public_id) +); + +-- 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(); + +-- 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_id', 'subject_id'); + +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(); + +create trigger + insert_auth_account_subtype +before insert on auth_oidc_account + for each row execute procedure insert_auth_account_subtype(); + +commit; + +`), + 1085: []byte(` +begin; + +-- auth_token_status_enm entries define the possible auth token +-- states. +create table auth_token_status_enm ( + name text primary key + constraint only_predefined_auth_token_states_allowed + check ( + name in ('auth token pending','token issued', 'authentication failed', 'system error') + ) +); + +-- populate the values of auth_token_status_enm +insert into auth_token_status_enm(name) + values + ('auth token pending'), + ('token issued'), + ('authentication failed'), + ('system error'); + + +-- add the state column with a default to the auth_token table. +alter table auth_token +add column status text +not null +default 'token issued' -- safest default +references auth_token_status_enm(name) + on update cascade + on delete restrict; + +commit; +`), + 1086: []byte(` +begin; + +-- add the account_info_auth_method_id which determines which auth_method is +-- designated as for "account info" in the user's scope. +alter table iam_scope +add column account_info_auth_method_id wt_public_id -- allowed to be null and is mutable of course. +references auth_method(public_id) + on update cascade + on delete set null; + +-- establish a compond fk, but there's no cascading of deletes or updates, since +-- we only want to cascade changes to the account_info_auth_method_id portion of +-- the compond fk and that is handled in a separate fk declaration. +alter table iam_scope +add foreign key (public_id, account_info_auth_method_id) references auth_method(scope_id, public_id); + +-- iam_user_acct_info provides account info for users by determining which +-- auth_method is designated as for "account info" in the user's scope via the +-- scope's account_info_auth_method_id. Every sub-type of auth_account must be +-- added to this view's union. +create view iam_acct_info as +select + aa.iam_user_id, + oa.subject_id as login_name, + oa.full_name as full_name, + oa.email as email +from + iam_scope s, + auth_account aa, + auth_oidc_account oa +where + aa.public_id = oa.public_id and + aa.auth_method_id = s.account_info_auth_method_id +union +select + aa.iam_user_id, + pa.login_name, + '' as full_name, + '' as email +from + iam_scope s, + auth_account aa, + auth_password_account pa +where + aa.public_id = pa.public_id and + aa.auth_method_id = s.account_info_auth_method_id; + +-- iam_user_acct_info provides a simple way to retrieve entries that include +-- both the iam_user fields with an outer join to the user's account info. +create view iam_user_acct_info as +select + u.public_id, + u.scope_id, + u.name, + u.description, + u.create_time, + u.update_time, + u.version, + i.login_name, + i.full_name, + i.email +from + iam_user u +left outer join iam_acct_info i on u.public_id = i.iam_user_id; + commit; `), },