From 663484e1fb31c55721c80bdee0c9da88bc277b76 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Wed, 12 Jun 2024 06:49:14 +0200 Subject: [PATCH] fix: consider oidc session events for authN milestones (#8089) # Which Problems Are Solved After migrating the access token events in #7822, milestones based on authentication, resp. theses events would not be reached. # How the Problems Are Solved Additionally use the `oidc_session.Added` event to check for `milestone.AuthenticationSucceededOnInstance` and `milestone.AuthenticationSucceededOnApplication`. # Additional Changes None. # Additional Context - relates to #7822 - noticed internally (cherry picked from commit b6c10c4c83ce05c5de10e9193503327eef3c2a88) --- ...ion_allow_public_org_registrations_test.go | 2 +- ...ions_integration_allowed_languages_test.go | 2 +- .../grpc/system/instance_integration_test.go | 2 +- ...mits_integration_auditlogretention_test.go | 2 +- .../system/limits_integration_block_test.go | 2 +- .../quotas_enabled/quota_integration_test.go | 4 +- internal/integration/client.go | 15 ++- internal/integration/oidc.go | 11 +- .../handlers/quota_notifier_test.go | 4 +- .../telemetry_pusher_integration_test.go | 120 +++++++++++++++--- internal/query/projection/milestones.go | 44 +++++++ internal/query/projection/milestones_test.go | 38 ++++++ 12 files changed, 213 insertions(+), 33 deletions(-) diff --git a/internal/api/grpc/admin/restrictions_integration_allow_public_org_registrations_test.go b/internal/api/grpc/admin/restrictions_integration_allow_public_org_registrations_test.go index 30250e772a..5b29de19bb 100644 --- a/internal/api/grpc/admin/restrictions_integration_allow_public_org_registrations_test.go +++ b/internal/api/grpc/admin/restrictions_integration_allow_public_org_registrations_test.go @@ -22,7 +22,7 @@ import ( func TestServer_Restrictions_DisallowPublicOrgRegistration(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - domain, _, iamOwnerCtx := Tester.UseIsolatedInstance(t, ctx, SystemCTX) + domain, _, _, iamOwnerCtx := Tester.UseIsolatedInstance(t, ctx, SystemCTX) regOrgUrl, err := url.Parse("http://" + domain + ":8080/ui/login/register/org") require.NoError(t, err) // The CSRF cookie must be sent with every request. diff --git a/internal/api/grpc/admin/restrictions_integration_allowed_languages_test.go b/internal/api/grpc/admin/restrictions_integration_allowed_languages_test.go index 277375f525..3e00978676 100644 --- a/internal/api/grpc/admin/restrictions_integration_allowed_languages_test.go +++ b/internal/api/grpc/admin/restrictions_integration_allowed_languages_test.go @@ -33,7 +33,7 @@ func TestServer_Restrictions_AllowedLanguages(t *testing.T) { unsupportedLanguage = language.Afrikaans ) - domain, _, iamOwnerCtx := Tester.UseIsolatedInstance(t, ctx, SystemCTX) + domain, _, _, iamOwnerCtx := Tester.UseIsolatedInstance(t, ctx, SystemCTX) t.Run("assumed defaults are correct", func(tt *testing.T) { tt.Run("languages are not restricted by default", func(ttt *testing.T) { restrictions, err := Tester.Client.Admin.GetRestrictions(iamOwnerCtx, &admin.GetRestrictionsRequest{}) diff --git a/internal/api/grpc/system/instance_integration_test.go b/internal/api/grpc/system/instance_integration_test.go index b4a6cea227..50d1dda50b 100644 --- a/internal/api/grpc/system/instance_integration_test.go +++ b/internal/api/grpc/system/instance_integration_test.go @@ -14,7 +14,7 @@ import ( ) func TestServer_ListInstances(t *testing.T) { - domain, instanceID, _ := Tester.UseIsolatedInstance(t, CTX, SystemCTX) + domain, instanceID, _, _ := Tester.UseIsolatedInstance(t, CTX, SystemCTX) tests := []struct { name string diff --git a/internal/api/grpc/system/limits_integration_auditlogretention_test.go b/internal/api/grpc/system/limits_integration_auditlogretention_test.go index 55ac76e998..b31ee818bf 100644 --- a/internal/api/grpc/system/limits_integration_auditlogretention_test.go +++ b/internal/api/grpc/system/limits_integration_auditlogretention_test.go @@ -21,7 +21,7 @@ import ( ) func TestServer_Limits_AuditLogRetention(t *testing.T) { - _, instanceID, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX) + _, instanceID, _, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX) userID, projectID, appID, projectGrantID := seedObjects(iamOwnerCtx, t) beforeTime := time.Now() farPast := timestamppb.New(beforeTime.Add(-10 * time.Hour).UTC()) diff --git a/internal/api/grpc/system/limits_integration_block_test.go b/internal/api/grpc/system/limits_integration_block_test.go index f007dd46c4..68426dd05e 100644 --- a/internal/api/grpc/system/limits_integration_block_test.go +++ b/internal/api/grpc/system/limits_integration_block_test.go @@ -27,7 +27,7 @@ import ( ) func TestServer_Limits_Block(t *testing.T) { - domain, instanceID, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX) + domain, instanceID, _, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX) tests := []*test{ publicAPIBlockingTest(domain), { diff --git a/internal/api/grpc/system/quotas_enabled/quota_integration_test.go b/internal/api/grpc/system/quotas_enabled/quota_integration_test.go index a6bd30707f..225b4a2daa 100644 --- a/internal/api/grpc/system/quotas_enabled/quota_integration_test.go +++ b/internal/api/grpc/system/quotas_enabled/quota_integration_test.go @@ -66,7 +66,7 @@ func TestServer_QuotaNotification_Limit(t *testing.T) { } func TestServer_QuotaNotification_NoLimit(t *testing.T) { - _, instanceID, IAMOwnerCTX := Tester.UseIsolatedInstance(t, CTX, SystemCTX) + _, instanceID, _, IAMOwnerCTX := Tester.UseIsolatedInstance(t, CTX, SystemCTX) amount := 10 percent := 50 percentAmount := amount * percent / 100 @@ -148,7 +148,7 @@ func awaitNotification(t *testing.T, bodies chan []byte, unit quota.Unit, percen } func TestServer_AddAndRemoveQuota(t *testing.T) { - _, instanceID, _ := Tester.UseIsolatedInstance(t, CTX, SystemCTX) + _, instanceID, _, _ := Tester.UseIsolatedInstance(t, CTX, SystemCTX) got, err := Tester.Client.System.SetQuota(SystemCTX, &system.SetQuotaRequest{ InstanceId: instanceID, diff --git a/internal/integration/client.go b/internal/integration/client.go index 72f50e9988..4b400bfb39 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -76,7 +76,7 @@ func newClient(cc *grpc.ClientConn) Client { } } -func (t *Tester) UseIsolatedInstance(tt *testing.T, iamOwnerCtx, systemCtx context.Context) (primaryDomain, instanceId string, authenticatedIamOwnerCtx context.Context) { +func (t *Tester) UseIsolatedInstance(tt *testing.T, iamOwnerCtx, systemCtx context.Context) (primaryDomain, instanceId, adminID string, authenticatedIamOwnerCtx context.Context) { primaryDomain = RandString(5) + ".integration.localhost" instance, err := t.Client.System.CreateInstance(systemCtx, &system.CreateInstanceRequest{ InstanceName: "testinstance", @@ -89,20 +89,23 @@ func (t *Tester) UseIsolatedInstance(tt *testing.T, iamOwnerCtx, systemCtx conte }, }, }) - if err != nil { - panic(err) - } + require.NoError(tt, err) t.createClientConn(iamOwnerCtx, fmt.Sprintf("%s:%d", primaryDomain, t.Config.Port)) instanceId = instance.GetInstanceId() + owner, err := t.Queries.GetUserByLoginName(authz.WithInstanceID(iamOwnerCtx, instanceId), true, "owner@"+primaryDomain) + require.NoError(tt, err) t.Users.Set(instanceId, IAMOwner, &User{ + User: owner, Token: instance.GetPat(), }) newCtx := t.WithInstanceAuthorization(iamOwnerCtx, IAMOwner, instanceId) + var adminUser *mgmt.ImportHumanUserResponse // the following serves two purposes: // 1. it ensures that the instance is ready to be used // 2. it enables a normal login with the default admin user credentials require.EventuallyWithT(tt, func(collectT *assert.CollectT) { - _, importErr := t.Client.Mgmt.ImportHumanUser(newCtx, &mgmt.ImportHumanUserRequest{ + var importErr error + adminUser, importErr = t.Client.Mgmt.ImportHumanUser(newCtx, &mgmt.ImportHumanUserRequest{ UserName: "zitadel-admin@zitadel.localhost", Email: &mgmt.ImportHumanUserRequest_Email{ Email: "zitadel-admin@zitadel.localhost", @@ -117,7 +120,7 @@ func (t *Tester) UseIsolatedInstance(tt *testing.T, iamOwnerCtx, systemCtx conte }) assert.NoError(collectT, importErr) }, 2*time.Minute, 100*time.Millisecond, "instance not ready") - return primaryDomain, instanceId, newCtx + return primaryDomain, instanceId, adminUser.GetUserId(), newCtx } func (s *Tester) CreateHumanUser(ctx context.Context) *user.AddHumanUserResponse { diff --git a/internal/integration/oidc.go b/internal/integration/oidc.go index 2d7d6a105e..3e90cb6856 100644 --- a/internal/integration/oidc.go +++ b/internal/integration/oidc.go @@ -151,7 +151,10 @@ func (s *Tester) CreateAPIClientBasic(ctx context.Context, projectID string) (*m const CodeVerifier = "codeVerifier" func (s *Tester) CreateOIDCAuthRequest(ctx context.Context, clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) { - provider, err := s.CreateRelyingParty(ctx, clientID, redirectURI, scope...) + return s.CreateOIDCAuthRequestWithDomain(ctx, s.Config.ExternalDomain, clientID, loginClient, redirectURI, scope...) +} +func (s *Tester) CreateOIDCAuthRequestWithDomain(ctx context.Context, domain, clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) { + provider, err := s.CreateRelyingPartyForDomain(ctx, domain, clientID, redirectURI, scope...) if err != nil { return "", err } @@ -212,11 +215,15 @@ func (s *Tester) OIDCIssuer() string { } func (s *Tester) CreateRelyingParty(ctx context.Context, clientID, redirectURI string, scope ...string) (rp.RelyingParty, error) { + return s.CreateRelyingPartyForDomain(ctx, s.Config.ExternalDomain, clientID, redirectURI, scope...) +} + +func (s *Tester) CreateRelyingPartyForDomain(ctx context.Context, domain, clientID, redirectURI string, scope ...string) (rp.RelyingParty, error) { if len(scope) == 0 { scope = []string{oidc.ScopeOpenID} } loginClient := &http.Client{Transport: &loginRoundTripper{http.DefaultTransport}} - return rp.NewRelyingPartyOIDC(ctx, s.OIDCIssuer(), clientID, "", redirectURI, scope, rp.WithHTTPClient(loginClient)) + return rp.NewRelyingPartyOIDC(ctx, http_util.BuildHTTP(domain, s.Config.Port, s.Config.ExternalSecure), clientID, "", redirectURI, scope, rp.WithHTTPClient(loginClient)) } type loginRoundTripper struct { diff --git a/internal/notification/handlers/quota_notifier_test.go b/internal/notification/handlers/quota_notifier_test.go index 72991019da..14de4e369c 100644 --- a/internal/notification/handlers/quota_notifier_test.go +++ b/internal/notification/handlers/quota_notifier_test.go @@ -21,7 +21,7 @@ import ( ) func TestServer_QuotaNotification_Limit(t *testing.T) { - _, instanceID, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX) + _, instanceID, _, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX) amount := 10 percent := 50 percentAmount := amount * percent / 100 @@ -67,7 +67,7 @@ func TestServer_QuotaNotification_Limit(t *testing.T) { } func TestServer_QuotaNotification_NoLimit(t *testing.T) { - _, instanceID, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX) + _, instanceID, _, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX) amount := 10 percent := 50 percentAmount := amount * percent / 100 diff --git a/internal/notification/handlers/telemetry_pusher_integration_test.go b/internal/notification/handlers/telemetry_pusher_integration_test.go index 9520253ade..b84f02fa79 100644 --- a/internal/notification/handlers/telemetry_pusher_integration_test.go +++ b/internal/notification/handlers/telemetry_pusher_integration_test.go @@ -4,38 +4,126 @@ package handlers_test import ( "bytes" + "context" "encoding/json" + "net/url" "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/oidc" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/app" "github.com/zitadel/zitadel/pkg/grpc/management" + "github.com/zitadel/zitadel/pkg/grpc/object" + oidc_v2 "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/project" "github.com/zitadel/zitadel/pkg/grpc/system" ) func TestServer_TelemetryPushMilestones(t *testing.T) { - primaryDomain, instanceID, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX) + primaryDomain, instanceID, adminID, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX) t.Log("testing against instance with primary domain", primaryDomain) awaitMilestone(t, Tester.MilestoneChan, primaryDomain, "InstanceCreated") - project, err := Tester.Client.Mgmt.AddProject(iamOwnerCtx, &management.AddProjectRequest{Name: "integration"}) - if err != nil { - t.Fatal(err) - } + + projectAdded, err := Tester.Client.Mgmt.AddProject(iamOwnerCtx, &management.AddProjectRequest{Name: "integration"}) + require.NoError(t, err) awaitMilestone(t, Tester.MilestoneChan, primaryDomain, "ProjectCreated") - if _, err = Tester.Client.Mgmt.AddOIDCApp(iamOwnerCtx, &management.AddOIDCAppRequest{ - ProjectId: project.GetId(), - Name: "integration", - }); err != nil { - t.Fatal(err) - } + + redirectURI := "http://localhost:8888" + application, err := Tester.Client.Mgmt.AddOIDCApp(iamOwnerCtx, &management.AddOIDCAppRequest{ + ProjectId: projectAdded.GetId(), + Name: "integration", + RedirectUris: []string{redirectURI}, + ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE}, + GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE}, + AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB, + AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, + DevMode: true, + AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT, + }) + require.NoError(t, err) awaitMilestone(t, Tester.MilestoneChan, primaryDomain, "ApplicationCreated") - // TODO: trigger and await milestone AuthenticationSucceededOnInstance - // TODO: trigger and await milestone AuthenticationSucceededOnApplication - if _, err = Tester.Client.System.RemoveInstance(SystemCTX, &system.RemoveInstanceRequest{InstanceId: instanceID}); err != nil { - t.Fatal(err) - } + + // create the session to be used for the authN of the clients + sessionID, sessionToken, _, _ := Tester.CreatePasswordSession(t, iamOwnerCtx, adminID, "Password1!") + + console := consoleOIDCConfig(iamOwnerCtx, t) + loginToClient(iamOwnerCtx, t, primaryDomain, console.GetClientId(), instanceID, console.GetRedirectUris()[0], sessionID, sessionToken) + awaitMilestone(t, Tester.MilestoneChan, primaryDomain, "AuthenticationSucceededOnInstance") + + // make sure the client has been projected + require.EventuallyWithT(t, func(collectT *assert.CollectT) { + _, err := Tester.Client.Mgmt.GetAppByID(iamOwnerCtx, &management.GetAppByIDRequest{ + ProjectId: projectAdded.GetId(), + AppId: application.GetAppId(), + }) + assert.NoError(collectT, err) + }, 1*time.Minute, 100*time.Millisecond, "app not found") + loginToClient(iamOwnerCtx, t, primaryDomain, application.GetClientId(), instanceID, redirectURI, sessionID, sessionToken) + awaitMilestone(t, Tester.MilestoneChan, primaryDomain, "AuthenticationSucceededOnApplication") + + _, err = Tester.Client.System.RemoveInstance(SystemCTX, &system.RemoveInstanceRequest{InstanceId: instanceID}) + require.NoError(t, err) awaitMilestone(t, Tester.MilestoneChan, primaryDomain, "InstanceDeleted") } +func loginToClient(iamOwnerCtx context.Context, t *testing.T, primaryDomain, clientID, instanceID, redirectURI, sessionID, sessionToken string) { + authRequestID, err := Tester.CreateOIDCAuthRequestWithDomain(iamOwnerCtx, primaryDomain, clientID, Tester.Users.Get(instanceID, integration.IAMOwner).ID, redirectURI, "openid") + require.NoError(t, err) + callback, err := Tester.Client.OIDCv2.CreateCallback(iamOwnerCtx, &oidc_v2.CreateCallbackRequest{ + AuthRequestId: authRequestID, + CallbackKind: &oidc_v2.CreateCallbackRequest_Session{Session: &oidc_v2.Session{ + SessionId: sessionID, + SessionToken: sessionToken, + }}, + }) + require.NoError(t, err) + provider, err := Tester.CreateRelyingPartyForDomain(iamOwnerCtx, primaryDomain, clientID, redirectURI) + require.NoError(t, err) + callbackURL, err := url.Parse(callback.GetCallbackUrl()) + require.NoError(t, err) + code := callbackURL.Query().Get("code") + _, err = rp.CodeExchange[*oidc.IDTokenClaims](iamOwnerCtx, code, provider, rp.WithCodeVerifier(integration.CodeVerifier)) + require.NoError(t, err) +} + +func consoleOIDCConfig(iamOwnerCtx context.Context, t *testing.T) *app.OIDCConfig { + projects, err := Tester.Client.Mgmt.ListProjects(iamOwnerCtx, &management.ListProjectsRequest{ + Queries: []*project.ProjectQuery{ + { + Query: &project.ProjectQuery_NameQuery{ + NameQuery: &project.ProjectNameQuery{ + Name: "ZITADEL", + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + }, + }) + require.NoError(t, err) + require.Len(t, projects.GetResult(), 1) + apps, err := Tester.Client.Mgmt.ListApps(iamOwnerCtx, &management.ListAppsRequest{ + ProjectId: projects.GetResult()[0].GetId(), + Queries: []*app.AppQuery{ + { + Query: &app.AppQuery_NameQuery{ + NameQuery: &app.AppNameQuery{ + Name: "Console", + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + }, + }) + require.NoError(t, err) + require.Len(t, apps.GetResult(), 1) + return apps.GetResult()[0].GetOidcConfig() +} + func awaitMilestone(t *testing.T, bodies chan []byte, primaryDomain, expectMilestoneType string) { for { select { diff --git a/internal/query/projection/milestones.go b/internal/query/projection/milestones.go index 0ac5e81842..7c344001b3 100644 --- a/internal/query/projection/milestones.go +++ b/internal/query/projection/milestones.go @@ -10,6 +10,7 @@ import ( "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/repository/milestone" + "github.com/zitadel/zitadel/internal/repository/oidcsession" "github.com/zitadel/zitadel/internal/repository/project" "github.com/zitadel/zitadel/internal/repository/user" ) @@ -104,6 +105,15 @@ func (p *milestoneProjection) Reducers() []handler.AggregateReducer { }, }, }, + { + Aggregate: oidcsession.AggregateType, + EventReducers: []handler.EventReducer{ + { + Event: oidcsession.AddedType, + Reduce: p.reduceOIDCSessionAdded, + }, + }, + }, { Aggregate: milestone.AggregateType, EventReducers: []handler.EventReducer{ @@ -217,6 +227,40 @@ func (p *milestoneProjection) reduceUserTokenAdded(event eventstore.Event) (*han return handler.NewMultiStatement(e, statements...), nil } +func (p *milestoneProjection) reduceOIDCSessionAdded(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*oidcsession.AddedEvent](event) + if err != nil { + return nil, err + } + statements := []func(eventstore.Event) handler.Exec{ + handler.AddUpdateStatement( + []handler.Column{ + handler.NewCol(MilestoneColumnReachedDate, event.CreatedAt()), + }, + []handler.Condition{ + handler.NewCond(MilestoneColumnInstanceID, event.Aggregate().InstanceID), + handler.NewCond(MilestoneColumnType, milestone.AuthenticationSucceededOnInstance), + handler.NewIsNullCond(MilestoneColumnReachedDate), + }, + ), + } + // We ignore authentications without app, for example JWT profile or PAT + if e.ClientID != "" { + statements = append(statements, handler.AddUpdateStatement( + []handler.Column{ + handler.NewCol(MilestoneColumnReachedDate, event.CreatedAt()), + }, + []handler.Condition{ + handler.NewCond(MilestoneColumnInstanceID, event.Aggregate().InstanceID), + handler.NewCond(MilestoneColumnType, milestone.AuthenticationSucceededOnApplication), + handler.Not(handler.NewTextArrayContainsCond(MilestoneColumnIgnoreClientIDs, e.ClientID)), + handler.NewIsNullCond(MilestoneColumnReachedDate), + }, + )) + } + return handler.NewMultiStatement(e, statements...), nil +} + func (p *milestoneProjection) reduceInstanceRemoved(event eventstore.Event) (*handler.Statement, error) { if _, err := assertEvent[*instance.InstanceRemovedEvent](event); err != nil { return nil, err diff --git a/internal/query/projection/milestones_test.go b/internal/query/projection/milestones_test.go index 884c7e27de..f9eb4d40d1 100644 --- a/internal/query/projection/milestones_test.go +++ b/internal/query/projection/milestones_test.go @@ -9,6 +9,7 @@ import ( "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/repository/milestone" + "github.com/zitadel/zitadel/internal/repository/oidcsession" "github.com/zitadel/zitadel/internal/repository/project" "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/zerrors" @@ -294,6 +295,43 @@ func TestMilestonesProjection_reduces(t *testing.T) { }, }, }, + { + name: "reduceOIDCSessionAdded", + args: args{ + event: getEvent(timedTestEvent( + oidcsession.AddedType, + oidcsession.AggregateType, + []byte(`{"clientID": "client-id"}`), + now, + ), eventstore.GenericEventMapper[oidcsession.AddedEvent]), + }, + reduce: (&milestoneProjection{}).reduceOIDCSessionAdded, + want: wantReduce{ + aggregateType: eventstore.AggregateType("oidc_session"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.milestones SET reached_date = $1 WHERE (instance_id = $2) AND (type = $3) AND (reached_date IS NULL)", + expectedArgs: []interface{}{ + now, + "instance-id", + milestone.AuthenticationSucceededOnInstance, + }, + }, + { + expectedStmt: "UPDATE projections.milestones SET reached_date = $1 WHERE (instance_id = $2) AND (type = $3) AND (NOT (ignore_client_ids @> $4)) AND (reached_date IS NULL)", + expectedArgs: []interface{}{ + now, + "instance-id", + milestone.AuthenticationSucceededOnApplication, + database.TextArray[string]{"client-id"}, + }, + }, + }, + }, + }, + }, { name: "reduceInstanceRemoved", args: args{