fix(projection): remove users with factors (#9877)

# Which Problems Are Solved

When users are removed, their auth factors stay in the projection. This
data inconsistency is visible if a removed user is recreated with the
same ID. In such a case, the login UI and the query API methods show the
removed users auth methods. This is unexpected behavior.

The old users auth methods are not usable to log in and they are not
found by the command side. This is expected behavior.

# How the Problems Are Solved

The auth factors projection reduces the user removed event by deleting
all factors.

# Additional Context

- Reported by support request
- requires backport to 2.x and 3.x

(cherry picked from commit d79d5e7b96)
This commit is contained in:
Elio Bischof
2025-05-12 12:05:12 +02:00
committed by Livio Spring
parent d4498ad136
commit 8aeb4705df
2 changed files with 47 additions and 0 deletions

View File

@@ -125,6 +125,10 @@ func (p *userAuthMethodProjection) Reducers() []handler.AggregateReducer {
Event: user.HumanOTPEmailRemovedType, Event: user.HumanOTPEmailRemovedType,
Reduce: p.reduceRemoveAuthMethod, Reduce: p.reduceRemoveAuthMethod,
}, },
{
Event: user.UserRemovedType,
Reduce: p.reduceUserRemoved,
},
}, },
}, },
{ {
@@ -311,3 +315,18 @@ func (p *userAuthMethodProjection) reduceOwnerRemoved(event eventstore.Event) (*
}, },
), nil ), nil
} }
func (p *userAuthMethodProjection) reduceUserRemoved(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*user.UserRemovedEvent)
if !ok {
return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-FwDZ8", "reduce.wrong.event.type %s", user.UserRemovedType)
}
return handler.NewDeleteStatement(
e,
[]handler.Condition{
handler.NewCond(UserAuthMethodInstanceIDCol, e.Aggregate().InstanceID),
handler.NewCond(UserAuthMethodResourceOwnerCol, e.Aggregate().ResourceOwner),
handler.NewCond(UserAuthMethodUserIDCol, e.Aggregate().ID),
},
), nil
}

View File

@@ -528,6 +528,34 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) {
}, },
}, },
}, },
{
name: "reduceUserRemoved",
reduce: (&userAuthMethodProjection{}).reduceUserRemoved,
args: args{
event: getEvent(
testEvent(
user.UserRemovedType,
user.AggregateType,
nil,
), user.UserRemovedEventMapper),
},
want: wantReduce{
aggregateType: eventstore.AggregateType("user"),
sequence: 15,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.user_auth_methods5 WHERE (instance_id = $1) AND (resource_owner = $2) AND (user_id = $3)",
expectedArgs: []interface{}{
"instance-id",
"ro-id",
"agg-id",
},
},
},
},
},
},
{ {
name: "org reduceOwnerRemoved", name: "org reduceOwnerRemoved",
reduce: (&userAuthMethodProjection{}).reduceOwnerRemoved, reduce: (&userAuthMethodProjection{}).reduceOwnerRemoved,