mirror of https://github.com/hashicorp/boundary
feat(oidc): OIDC Prompt (#4053)
Boundary OIDC method does not currently support passing in prompts during authentication. This change adds the capability of passing OIDC prompts. Prompts are optional OIDC parameters that determine the behaviour of the authentication server: https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest ## Changes - New `auth_oidc_prompt` table which contains all the prompts for OIDC auth method - New `auth_oidc_prompt_enm` table which contains possible enum values for a prompt - Currently supported: - `none`: The Authorization Server MUST NOT display any authentication or consent user interface pages - `login`: The Authorization Server SHOULD prompt the End-User for reauthentication - `consent`: The Authorization Server SHOULD prompt the End-User for consent before returning information to the Client - `select_account`: The Authorization Server SHOULD prompt the End-User to select a user account - `oidc_auth_method_with_value_obj` view has been updated to return `prompt` value - Add `prompt` option for OIDC auth method CLI create and update - Pass configured prompt during OIDC authentication - Add `prompt` API validation for create and updatepull/4055/head
parent
b6df5693b5
commit
8b8d2822df
@ -0,0 +1,117 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/auth/oidc/store"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/cap/oidc"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// Prompt represents OIDC authentication prompt
|
||||
type PromptParam string
|
||||
|
||||
const (
|
||||
// Prompt values defined by OpenID specs.
|
||||
// See: https://openid.net/specs/openid-connect-basic-1_0.html#RequestParameters
|
||||
None PromptParam = "none"
|
||||
Login PromptParam = "login"
|
||||
Consent PromptParam = "consent"
|
||||
SelectAccount PromptParam = "select_account"
|
||||
)
|
||||
|
||||
var supportedPrompts = map[PromptParam]bool{
|
||||
None: true,
|
||||
Login: true,
|
||||
Consent: true,
|
||||
SelectAccount: true,
|
||||
}
|
||||
|
||||
// SupportedPrompt returns true if the provided prompt is supported
|
||||
// by boundary.
|
||||
func SupportedPrompt(p PromptParam) bool {
|
||||
return supportedPrompts[p]
|
||||
}
|
||||
|
||||
// defaultPromptTableName defines the default table name for a Prompt
|
||||
const defaultPromptTableName = "auth_oidc_prompt"
|
||||
|
||||
// Prompt defines an prompt supported by an OIDC auth method.
|
||||
// It is assigned to an OIDC AuthMethod and updates/deletes to that AuthMethod
|
||||
// are cascaded to its Prompts. Prompts are value objects of an AuthMethod,
|
||||
// therefore there's no need for oplog metadata, since only the AuthMethod will have
|
||||
// metadata because it's the root aggregate.
|
||||
type Prompt struct {
|
||||
*store.Prompt
|
||||
tableName string
|
||||
}
|
||||
|
||||
// NewPrompt creates a new in memory prompt assigned to an OIDC
|
||||
// AuthMethod. It supports no options.
|
||||
func NewPrompt(ctx context.Context, authMethodId string, p PromptParam) (*Prompt, error) {
|
||||
const op = "oidc.NewPrompt"
|
||||
prompt := &Prompt{
|
||||
Prompt: &store.Prompt{
|
||||
OidcMethodId: authMethodId,
|
||||
PromptParam: string(p),
|
||||
},
|
||||
}
|
||||
if err := prompt.validate(ctx, op); err != nil {
|
||||
return nil, err // intentionally not wrapped
|
||||
}
|
||||
return prompt, nil
|
||||
}
|
||||
|
||||
// validate the Prompt. On success, it will return nil.
|
||||
func (s *Prompt) validate(ctx context.Context, caller errors.Op) error {
|
||||
if s.OidcMethodId == "" {
|
||||
return errors.New(ctx, errors.InvalidParameter, caller, "missing oidc auth method id")
|
||||
}
|
||||
if _, ok := supportedPrompts[PromptParam(s.PromptParam)]; !ok {
|
||||
return errors.New(ctx, errors.InvalidParameter, caller, fmt.Sprintf("unsupported prompt: %s", s.Prompt))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertToOIDCPrompts(ctx context.Context, p []string) []oidc.Prompt {
|
||||
prompts := make([]oidc.Prompt, 0, len(p))
|
||||
for _, a := range p {
|
||||
prompt := oidc.Prompt(a)
|
||||
prompts = append(prompts, prompt)
|
||||
}
|
||||
|
||||
return prompts
|
||||
}
|
||||
|
||||
// AllocPrompt makes an empty one in memory
|
||||
func AllocPrompt() Prompt {
|
||||
return Prompt{
|
||||
Prompt: &store.Prompt{},
|
||||
}
|
||||
}
|
||||
|
||||
// Clone a Prompt
|
||||
func (s *Prompt) Clone() *Prompt {
|
||||
cp := proto.Clone(s.Prompt)
|
||||
return &Prompt{
|
||||
Prompt: cp.(*store.Prompt),
|
||||
}
|
||||
}
|
||||
|
||||
// TableName returns the table name.
|
||||
func (s *Prompt) TableName() string {
|
||||
if s.tableName != "" {
|
||||
return s.tableName
|
||||
}
|
||||
return defaultPromptTableName
|
||||
}
|
||||
|
||||
// SetTableName sets the table name.
|
||||
func (s *Prompt) SetTableName(n string) {
|
||||
s.tableName = n
|
||||
}
|
||||
@ -0,0 +1,330 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/iam"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestPrompts_Create(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.TODO()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
wrapper := db.TestWrapper(t)
|
||||
kmsCache := kms.TestKms(t, conn, wrapper)
|
||||
org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
|
||||
rw := db.New(conn)
|
||||
|
||||
databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase)
|
||||
require.NoError(t, err)
|
||||
|
||||
testAuthMethod := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name",
|
||||
WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0]))
|
||||
|
||||
type args struct {
|
||||
authMethodId string
|
||||
prompt PromptParam
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *Prompt
|
||||
wantErr bool
|
||||
wantIsErr errors.Code
|
||||
create bool
|
||||
wantCreateErr bool
|
||||
wantCreateIsErr errors.Code
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
args: args{
|
||||
authMethodId: testAuthMethod.PublicId,
|
||||
prompt: SelectAccount,
|
||||
},
|
||||
create: true,
|
||||
want: func() *Prompt {
|
||||
want := AllocPrompt()
|
||||
want.OidcMethodId = testAuthMethod.PublicId
|
||||
want.PromptParam = string(SelectAccount)
|
||||
return &want
|
||||
}(),
|
||||
},
|
||||
{
|
||||
name: "dup", // must follow "valid" test. Prompt must be be unique for an OidcMethodId
|
||||
args: args{
|
||||
authMethodId: testAuthMethod.PublicId,
|
||||
prompt: SelectAccount,
|
||||
},
|
||||
create: true,
|
||||
want: func() *Prompt {
|
||||
want := AllocPrompt()
|
||||
want.OidcMethodId = testAuthMethod.PublicId
|
||||
want.PromptParam = string(SelectAccount)
|
||||
return &want
|
||||
}(),
|
||||
wantCreateErr: true,
|
||||
wantCreateIsErr: errors.NotUnique,
|
||||
},
|
||||
{
|
||||
name: "empty-auth-method",
|
||||
args: args{
|
||||
authMethodId: "",
|
||||
prompt: Consent,
|
||||
},
|
||||
wantErr: true,
|
||||
wantIsErr: errors.InvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "empty-prompt",
|
||||
args: args{
|
||||
authMethodId: testAuthMethod.PublicId,
|
||||
prompt: "",
|
||||
},
|
||||
wantErr: true,
|
||||
wantIsErr: errors.InvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "supported-prompt",
|
||||
args: args{
|
||||
authMethodId: testAuthMethod.PublicId,
|
||||
prompt: PromptParam("EVE256"), // The unsupported evesdropper 256 curve
|
||||
},
|
||||
wantErr: true,
|
||||
wantIsErr: errors.InvalidParameter,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := NewPrompt(ctx, tt.args.authMethodId, tt.args.prompt)
|
||||
if tt.wantErr {
|
||||
require.Error(err)
|
||||
assert.True(errors.Match(errors.T(tt.wantIsErr), err))
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tt.want, got)
|
||||
if tt.create {
|
||||
ctx := context.Background()
|
||||
err = rw.Create(ctx, got)
|
||||
if tt.wantCreateErr {
|
||||
assert.Error(err)
|
||||
assert.True(errors.Match(errors.T(tt.wantCreateIsErr), err))
|
||||
return
|
||||
} else {
|
||||
assert.NoError(err)
|
||||
}
|
||||
found := AllocPrompt()
|
||||
require.NoError(rw.LookupWhere(ctx, &found, "oidc_method_id = ? and prompt = ?", []any{tt.args.authMethodId, string(tt.args.prompt)}))
|
||||
assert.Equal(got, &found)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrompt_Delete(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.TODO()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
wrapper := db.TestWrapper(t)
|
||||
kmsCache := kms.TestKms(t, conn, wrapper)
|
||||
org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
|
||||
rw := db.New(conn)
|
||||
|
||||
databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase)
|
||||
require.NoError(t, err)
|
||||
|
||||
testAuthMethod := TestAuthMethod(
|
||||
t,
|
||||
conn,
|
||||
databaseWrapper,
|
||||
org.PublicId,
|
||||
InactiveState,
|
||||
"alice_rp",
|
||||
"my-dogs-name",
|
||||
WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]),
|
||||
WithApiUrl(TestConvertToUrls(t, "https://api.com")[0]),
|
||||
WithPrompts(Consent))
|
||||
|
||||
testResource := func(authMethodId string, prompt PromptParam) *Prompt {
|
||||
c, err := NewPrompt(ctx, authMethodId, prompt)
|
||||
require.NoError(t, err)
|
||||
return c
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
Prompt *Prompt
|
||||
wantRowsDeleted int
|
||||
overrides func(*Prompt)
|
||||
wantErr bool
|
||||
wantErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
Prompt: testResource(testAuthMethod.PublicId, SelectAccount),
|
||||
wantErr: false,
|
||||
wantRowsDeleted: 1,
|
||||
},
|
||||
{
|
||||
name: "bad-OidcMethodId",
|
||||
Prompt: testResource(testAuthMethod.PublicId, Login),
|
||||
overrides: func(c *Prompt) { c.OidcMethodId = "bad-id" },
|
||||
wantErr: false,
|
||||
wantRowsDeleted: 0,
|
||||
},
|
||||
{
|
||||
name: "bad-prompt",
|
||||
Prompt: testResource(testAuthMethod.PublicId, None),
|
||||
overrides: func(c *Prompt) { c.PromptParam = "bad-prompt" },
|
||||
wantErr: false,
|
||||
wantRowsDeleted: 0,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
ctx := context.Background()
|
||||
cp := tt.Prompt.Clone()
|
||||
require.NoError(rw.Create(ctx, &cp))
|
||||
|
||||
if tt.overrides != nil {
|
||||
tt.overrides(cp)
|
||||
}
|
||||
deletedRows, err := rw.Delete(ctx, &cp)
|
||||
if tt.wantErr {
|
||||
require.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
if tt.wantRowsDeleted == 0 {
|
||||
assert.Equal(tt.wantRowsDeleted, deletedRows)
|
||||
return
|
||||
}
|
||||
assert.Equal(tt.wantRowsDeleted, deletedRows)
|
||||
found := AllocPrompt()
|
||||
err = rw.LookupWhere(ctx, &found, "oidc_method_id = ? and prompt = ?", []any{tt.Prompt.OidcMethodId, tt.Prompt.String()})
|
||||
assert.Truef(errors.IsNotFoundError(err), "unexpected error: %s", err.Error())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrompt_Clone(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.TODO()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
wrapper := db.TestWrapper(t)
|
||||
kmsCache := kms.TestKms(t, conn, wrapper)
|
||||
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
|
||||
databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase)
|
||||
require.NoError(err)
|
||||
m := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name",
|
||||
WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0]))
|
||||
orig, err := NewPrompt(ctx, m.PublicId, Consent)
|
||||
require.NoError(err)
|
||||
cp := orig.Clone()
|
||||
assert.True(proto.Equal(cp.Prompt, orig.Prompt))
|
||||
})
|
||||
t.Run("not-equal", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
|
||||
databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase)
|
||||
require.NoError(err)
|
||||
m := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name",
|
||||
WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0]))
|
||||
orig, err := NewPrompt(ctx, m.PublicId, Consent)
|
||||
require.NoError(err)
|
||||
orig2, err := NewPrompt(ctx, m.PublicId, SelectAccount)
|
||||
require.NoError(err)
|
||||
|
||||
cp := orig.Clone()
|
||||
assert.True(!proto.Equal(cp.Prompt, orig2.Prompt))
|
||||
})
|
||||
}
|
||||
|
||||
func TestPrompt_SetTableName(t *testing.T) {
|
||||
t.Parallel()
|
||||
defaultTableName := defaultPromptTableName
|
||||
tests := []struct {
|
||||
name string
|
||||
setNameTo string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "new-name",
|
||||
setNameTo: "new-name",
|
||||
want: "new-name",
|
||||
},
|
||||
{
|
||||
name: "reset to default",
|
||||
setNameTo: "",
|
||||
want: defaultTableName,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
def := AllocPrompt()
|
||||
require.Equal(defaultTableName, def.TableName())
|
||||
m := AllocPrompt()
|
||||
m.SetTableName(tt.setNameTo)
|
||||
assert.Equal(tt.want, m.TableName())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrompt_SupportedPrompt(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
prompt PromptParam
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "none-prompt",
|
||||
prompt: None,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "login-prompt",
|
||||
prompt: Login,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "consent-prompt",
|
||||
prompt: Consent,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "select-account-prompt",
|
||||
prompt: SelectAccount,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "invalid-prompt",
|
||||
prompt: "invalid",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
got := SupportedPrompt(tt.prompt)
|
||||
assert.Equal(tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,112 @@
|
||||
-- Copyright (c) HashiCorp, Inc.
|
||||
-- SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
begin;
|
||||
|
||||
-- auth_oidc_prompt_enm entries define the supported oidc auth method prompts.
|
||||
create table auth_oidc_prompt_enm (
|
||||
name text primary key
|
||||
constraint only_predefined_auth_oidc_prompts_allowed
|
||||
check (
|
||||
name in (
|
||||
'none',
|
||||
'login',
|
||||
'consent',
|
||||
'select_account'
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
-- define the immutable fields for auth_oidc_prompt_enm (all of them)
|
||||
create trigger immutable_columns before update on auth_oidc_prompt_enm
|
||||
for each row execute procedure immutable_columns('name');
|
||||
|
||||
insert into auth_oidc_prompt_enm (name) values
|
||||
('none'),
|
||||
('login'),
|
||||
('consent'),
|
||||
('select_account');
|
||||
|
||||
-- auth_oidc_prompt entries are the prompts allowed for an oidc auth method.
|
||||
create table auth_oidc_prompt (
|
||||
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,
|
||||
prompt text
|
||||
constraint auth_oidc_prompt_enm_fkey
|
||||
references auth_oidc_prompt_enm(name)
|
||||
on delete restrict
|
||||
on update cascade,
|
||||
primary key(oidc_method_id, prompt)
|
||||
);
|
||||
comment on table auth_oidc_prompt is
|
||||
'auth_oidc_prompt entries are the prompts allowed for an oidc auth method.';
|
||||
|
||||
create trigger immutable_columns before update on auth_oidc_prompt
|
||||
for each row execute procedure immutable_columns('create_time', 'oidc_method_id', 'prompt');
|
||||
|
||||
create trigger
|
||||
default_create_time_column
|
||||
before
|
||||
insert on auth_oidc_prompt
|
||||
for each row execute procedure default_create_time();
|
||||
|
||||
-- we will drop the oidc_auth_method_with_value_obj view, so we can recreate it
|
||||
-- and add the oidc prompts to the returned set.
|
||||
drop view oidc_auth_method_with_value_obj;
|
||||
|
||||
-- oidc_auth_method_with_value_obj is useful for reading an oidc auth method
|
||||
-- with its associated value objects (algs, auds, certs, claims scopes,
|
||||
-- account claim maps and prompts) as columns with | delimited values. The
|
||||
-- use of the postgres string_agg(...) to aggregate the 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 oidc auth methods fairly
|
||||
-- straightforward to implement for the oidc repo. The view also includes an
|
||||
-- is_primary_auth_method bool
|
||||
create view oidc_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.api_url,
|
||||
am.disable_discovered_config_validation,
|
||||
am.issuer,
|
||||
am.client_id,
|
||||
am.client_secret,
|
||||
am.client_secret_hmac,
|
||||
am.key_id,
|
||||
am.max_age,
|
||||
-- the string_agg(..) column will be null if there are no associated value objects
|
||||
string_agg(distinct alg.signing_alg_name, '|') as algs,
|
||||
string_agg(distinct aud.aud_claim, '|') as auds,
|
||||
string_agg(distinct cert.certificate, '|') as certs,
|
||||
string_agg(distinct cs.scope, '|') as claims_scopes,
|
||||
string_agg(distinct p.prompt, '|') as prompts,
|
||||
string_agg(distinct concat_ws('=', acm.from_claim, acm.to_claim), '|') as account_claim_maps
|
||||
from
|
||||
auth_oidc_method am
|
||||
left outer join iam_scope s on am.public_id = s.primary_auth_method_id
|
||||
left outer join auth_oidc_signing_alg alg on am.public_id = alg.oidc_method_id
|
||||
left outer join auth_oidc_aud_claim aud on am.public_id = aud.oidc_method_id
|
||||
left outer join auth_oidc_certificate cert on am.public_id = cert.oidc_method_id
|
||||
left outer join auth_oidc_scope cs on am.public_id = cs.oidc_method_id
|
||||
left outer join auth_oidc_account_claim_map acm on am.public_id = acm.oidc_method_id
|
||||
left outer join auth_oidc_prompt p on am.public_id = p.oidc_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 oidc_auth_method_with_value_obj is
|
||||
'oidc auth method with its associated value objects (algs, auds, certs, scopes, prompts) as columns with | delimited values';
|
||||
|
||||
commit;
|
||||
@ -0,0 +1,63 @@
|
||||
-- Copyright (c) HashiCorp, Inc.
|
||||
-- SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
begin;
|
||||
select plan(5);
|
||||
select wtt_load('widgets', 'iam', 'kms', 'auth');
|
||||
|
||||
-- Try to insert invalid auth_oidc_prompt value to test constraint
|
||||
prepare invalid_auth_oidc_prompt_value as
|
||||
insert into auth_oidc_prompt
|
||||
(oidc_method_id, prompt)
|
||||
values
|
||||
('aom___widget', 'invalid');
|
||||
|
||||
select throws_ok(
|
||||
'invalid_auth_oidc_prompt_value',
|
||||
'23503',
|
||||
'insert or update on table "auth_oidc_prompt" violates foreign key constraint "auth_oidc_prompt_enm_fkey"',
|
||||
'inserting a row with invalid auth_oidc_prompt value'
|
||||
);
|
||||
|
||||
-- Insert valid valid_auth_oidc_prompt_value value to test constraint with a valid value
|
||||
prepare valid_auth_oidc_prompt_value as
|
||||
insert into auth_oidc_prompt
|
||||
(oidc_method_id, prompt)
|
||||
values
|
||||
('aom___widget', 'select_account');
|
||||
select lives_ok('valid_auth_oidc_prompt_value');
|
||||
|
||||
-- Update immutable prompt field
|
||||
prepare update_auth_oidc_prompt_value as
|
||||
update auth_oidc_prompt
|
||||
set prompt = 'consent'
|
||||
where oidc_method_id = 'aom___widget';
|
||||
|
||||
select throws_ok(
|
||||
'update_auth_oidc_prompt_value',
|
||||
'23601',
|
||||
'immutable column: auth_oidc_prompt.prompt',
|
||||
'updating an immutable auth_oidc_prompt column'
|
||||
);
|
||||
|
||||
-- validate oidc_auth_method_with_value_obj view
|
||||
select has_view('oidc_auth_method_with_value_obj', 'view for reading an oidc auth method with its associated value objects does not exist');
|
||||
|
||||
insert into auth_oidc_prompt
|
||||
(oidc_method_id, prompt)
|
||||
values
|
||||
('aom___widget', 'consent');
|
||||
|
||||
prepare select_oidc_auth_method_aggregate as
|
||||
select public_id::text, prompts::text
|
||||
from oidc_auth_method_with_value_obj
|
||||
where public_id = 'aom___widget';
|
||||
|
||||
select results_eq(
|
||||
'select_oidc_auth_method_aggregate',
|
||||
$$VALUES
|
||||
('aom___widget', 'consent|select_account')$$
|
||||
);
|
||||
|
||||
select * from finish();
|
||||
rollback;
|
||||
Loading…
Reference in new issue