fix: migrate external id of federated users (#6312)

* feat: migrate external id

* implement tests and some renaming

* fix projection

* cleanup

* i18n

* fix event type

* handle migration for new services as well

* typo
This commit is contained in:
Livio Spring
2023-08-04 11:35:36 +02:00
committed by GitHub
parent d33a4fbb2f
commit 45262e6829
28 changed files with 611 additions and 9 deletions

View File

@@ -348,6 +348,95 @@ func TestCommands_AuthURLFromProvider(t *testing.T) {
authURL: "auth?client_id=clientID&prompt=select_account&redirect_uri=url&response_type=code&state=state",
},
},
{
"migrated and push",
fields{
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusherWithInstanceID(
"instance",
instance.NewOIDCIDPAddedEvent(context.Background(), &instance.NewAggregate("instance").Aggregate,
"idp",
"name",
"issuer",
"clientID",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("clientSecret"),
},
[]string{"openid", "profile", "User.Read"},
false,
rep_idp.Options{},
)),
eventFromEventPusherWithInstanceID(
"instance",
instance.NewOIDCIDPMigratedAzureADEvent(context.Background(), &instance.NewAggregate("instance").Aggregate,
"idp",
"name",
"clientID",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("clientSecret"),
},
[]string{"openid", "profile", "User.Read"},
"tenant",
true,
rep_idp.Options{},
)),
),
expectFilter(
eventFromEventPusherWithInstanceID(
"instance",
instance.NewOIDCIDPAddedEvent(context.Background(), &instance.NewAggregate("instance").Aggregate,
"idp",
"name",
"issuer",
"clientID",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("clientSecret"),
},
[]string{"openid", "profile", "User.Read"},
false,
rep_idp.Options{},
)),
eventFromEventPusherWithInstanceID(
"instance",
instance.NewOIDCIDPMigratedAzureADEvent(context.Background(), &instance.NewAggregate("instance").Aggregate,
"idp",
"name",
"clientID",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("clientSecret"),
},
[]string{"openid", "profile", "User.Read"},
"tenant",
true,
rep_idp.Options{},
)),
),
),
},
args{
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
idpID: "idp",
state: "state",
callbackURL: "url",
},
res{
authURL: "https://login.microsoftonline.com/tenant/oauth2/v2.0/authorize?client_id=clientID&prompt=select_account&redirect_uri=url&response_type=code&scope=openid+profile+User.Read&state=state",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@@ -1653,6 +1653,14 @@ func (wm *IDPTypeWriteModel) Reduce() error {
wm.reduceAdded(e.ID, domain.IDPTypeLDAP, e.Aggregate())
case *org.LDAPIDPAddedEvent:
wm.reduceAdded(e.ID, domain.IDPTypeLDAP, e.Aggregate())
case *instance.OIDCIDPMigratedAzureADEvent:
wm.reduceChanged(e.ID, domain.IDPTypeAzureAD)
case *org.OIDCIDPMigratedAzureADEvent:
wm.reduceChanged(e.ID, domain.IDPTypeAzureAD)
case *instance.OIDCIDPMigratedGoogleEvent:
wm.reduceChanged(e.ID, domain.IDPTypeGoogle)
case *org.OIDCIDPMigratedGoogleEvent:
wm.reduceChanged(e.ID, domain.IDPTypeGoogle)
case *instance.IDPRemovedEvent:
wm.reduceRemoved(e.ID)
case *org.IDPRemovedEvent:
@@ -1688,6 +1696,13 @@ func (wm *IDPTypeWriteModel) reduceAdded(id string, t domain.IDPType, agg events
wm.InstanceID = agg.InstanceID
}
func (wm *IDPTypeWriteModel) reduceChanged(id string, t domain.IDPType) {
if wm.ID != id {
return
}
wm.Type = t
}
func (wm *IDPTypeWriteModel) reduceRemoved(id string) {
if wm.ID != id {
return
@@ -1713,6 +1728,8 @@ func (wm *IDPTypeWriteModel) Query() *eventstore.SearchQueryBuilder {
instance.GitLabSelfHostedIDPAddedEventType,
instance.GoogleIDPAddedEventType,
instance.LDAPIDPAddedEventType,
instance.OIDCIDPMigratedAzureADEventType,
instance.OIDCIDPMigratedGoogleEventType,
instance.IDPRemovedEventType,
).
EventData(map[string]interface{}{"id": wm.ID}).
@@ -1729,6 +1746,8 @@ func (wm *IDPTypeWriteModel) Query() *eventstore.SearchQueryBuilder {
org.GitLabSelfHostedIDPAddedEventType,
org.GoogleIDPAddedEventType,
org.LDAPIDPAddedEventType,
org.OIDCIDPMigratedAzureADEventType,
org.OIDCIDPMigratedGoogleEventType,
org.IDPRemovedEventType,
).
EventData(map[string]interface{}{"id": wm.ID}).

View File

@@ -139,6 +139,24 @@ func (c *Commands) UserIDPLoginChecked(ctx context.Context, orgID, userID string
return err
}
func (c *Commands) MigrateUserIDP(ctx context.Context, userID, orgID, idpConfigID, previousID, newID string) (err error) {
if userID == "" {
return caos_errs.ThrowInvalidArgument(nil, "COMMAND-Sn3l1", "Errors.IDMissing")
}
writeModel, err := c.userIDPLinkWriteModelByID(ctx, userID, idpConfigID, previousID, orgID)
if err != nil {
return err
}
if writeModel.State != domain.UserIDPLinkStateActive {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-KJH2o", "Errors.User.ExternalIDP.NotFound")
}
userAgg := UserAggregateFromWriteModel(&writeModel.WriteModel)
_, err = c.eventstore.Push(ctx, user.NewUserIDPExternalIDMigratedEvent(ctx, userAgg, idpConfigID, previousID, newID))
return err
}
func (c *Commands) userIDPLinkWriteModelByID(ctx context.Context, userID, idpConfigID, externalUserID, resourceOwner string) (writeModel *UserIDPLinkWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()

View File

@@ -35,6 +35,11 @@ func (wm *UserIDPLinkWriteModel) AppendEvents(events ...eventstore.Event) {
continue
}
wm.WriteModel.AppendEvents(e)
case *user.UserIDPExternalIDMigratedEvent:
if e.IDPConfigID != wm.IDPConfigID || e.PreviousID != wm.ExternalUserID {
continue
}
wm.WriteModel.AppendEvents(e)
case *user.UserIDPLinkRemovedEvent:
if e.IDPConfigID != wm.IDPConfigID || e.ExternalUserID != wm.ExternalUserID {
continue
@@ -59,6 +64,8 @@ func (wm *UserIDPLinkWriteModel) Reduce() error {
wm.DisplayName = e.DisplayName
wm.ExternalUserID = e.ExternalUserID
wm.State = domain.UserIDPLinkStateActive
case *user.UserIDPExternalIDMigratedEvent:
wm.ExternalUserID = e.NewID
case *user.UserIDPLinkRemovedEvent:
wm.State = domain.UserIDPLinkStateRemoved
case *user.UserIDPLinkCascadeRemovedEvent:
@@ -77,6 +84,7 @@ func (wm *UserIDPLinkWriteModel) Query() *eventstore.SearchQueryBuilder {
AggregateTypes(user.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(user.UserIDPLinkAddedType,
user.UserIDPExternalIDMigratedType,
user.UserIDPLinkRemovedType,
user.UserIDPLinkCascadeRemovedType,
user.UserRemovedType).

View File

@@ -669,3 +669,119 @@ func TestCommandSide_ExternalLoginCheck(t *testing.T) {
})
}
}
func TestCommandSide_MigrateUserIDP(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
userID string
orgID string
idpConfigID string
previousID string
newID string
}
type res struct {
err error
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "userid missing, invalid argument error",
fields: fields{
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
userID: "",
orgID: "org1",
idpConfigID: "idpConfig1",
previousID: "previousID",
newID: "newID",
},
res: res{
err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-Sn3l1", "Errors.IDMissing"),
},
},
{
name: "idp link not active, precondition failed error",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewUserIDPLinkAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"idpConfig1",
"displayName",
"externalUserID",
),
),
),
),
},
args: args{
ctx: context.Background(),
userID: "user1",
orgID: "org1",
idpConfigID: "idpConfig1",
previousID: "previousID",
newID: "newID",
},
res: res{
err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-KJH2o", "Errors.User.ExternalIDP.NotFound"),
},
},
{
name: "external login check, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewUserIDPLinkAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"idpConfig1",
"displayName",
"previousID",
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
user.NewUserIDPExternalIDMigratedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"idpConfig1",
"previousID",
"newID",
),
),
},
),
),
},
args: args{
ctx: context.Background(),
userID: "user1",
orgID: "org1",
idpConfigID: "idpConfig1",
previousID: "previousID",
newID: "newID",
},
res: res{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore(t),
}
err := r.MigrateUserIDP(tt.args.ctx, tt.args.userID, tt.args.orgID, tt.args.idpConfigID, tt.args.previousID, tt.args.newID)
assert.ErrorIs(t, err, tt.res.err)
})
}
}