mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
* fix(security): prevent unauthenticated IDP-linking account takeover Two distinct defects allowed an attacker to bind their own external identity to a victim's account and, because external IDP links are globally unique (first claim wins), hijack the victim's future federated logins. LoginV2: resolveUserIdFromSession returned the session's user id after only a null check, trusting an "identify-only" session (login name submitted, no verified primary factor). An unauthenticated attacker who knew a victim's login name could drive explicit linking against that session. Gate it on isSessionValid so only a fully authenticated session can link. Backend: AddUserIDPLink skipped the user-write permission check when the caller targeted their own account (userID == ctx user). Because the external subject is caller-supplied and unverified, this let any authenticated user self-link an unproven identity. Make the permission check unconditional. The login/federation flow uses a service account (userID != ctx user) and already took the permission branch, so it is unaffected; verified self-service linking goes through the IDP-intent flow. Adds a LoginV2 regression test (identify-only session is rejected) and the first command-level test for AddUserIDPLink (self-link without permission is denied). Co-Authored-By: Claude Opus 4.8 <[email protected]> * test(user_idp_link): address review on AddUserIDPLink hardening - add success regression cases for AddUserIDPLink (self-link with permission, other-user link with permission) so the login-client and managed-admin paths are pinned and the assert.NoError branch is live - add an integration case: UserCTX self-linking on its own account is now denied - document the intentional no-permission-check invariant on BulkAddedUserIDPLinks (verified Login V1 flow only; not API-exposed) Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01RUhwzSqHhycHNAJEfMcrdd * fix(login): authorize IDP linking via enrollment guard, not isSessionValid Replace the isSessionValid gate in resolveUserIdFromSession with getEnrollmentAuthorizationError. isSessionValid requires a verified primary factor, which rejected the legitimate "link an IDP as the first authenticator" onboarding flow (a freshly verified user with no credentials yet). The enrollment guard authorizes when the session has a verified, non-expired primary factor OR the user has zero auth methods and passed the user-verification check (email/invite-code proof bound to the browser fingerprint) — closing the identify-only ATO while preserving first-authenticator linking. Replace the single mocked rejection test with four that run the real guard: identify-only + has credentials -> rejected; identify-only + no credentials + no verification check -> rejected; expired primary factor -> rejected; identify-only + no credentials + verification check -> allowed. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01T24WwsRLbaC7aHxBmyznnd --------- Co-authored-by: Claude Opus 4.8 <[email protected]>
1565 lines
38 KiB
Go
1565 lines
38 KiB
Go
package command
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/muhlemmer/gu"
|
|
"github.com/stretchr/testify/assert"
|
|
"golang.org/x/text/language"
|
|
|
|
"github.com/zitadel/zitadel/internal/api/authz"
|
|
"github.com/zitadel/zitadel/internal/domain"
|
|
"github.com/zitadel/zitadel/internal/eventstore"
|
|
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
|
"github.com/zitadel/zitadel/internal/repository/idp"
|
|
"github.com/zitadel/zitadel/internal/repository/instance"
|
|
"github.com/zitadel/zitadel/internal/repository/org"
|
|
"github.com/zitadel/zitadel/internal/repository/user"
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
|
)
|
|
|
|
func TestCommandSide_BulkAddUserIDPLinks(t *testing.T) {
|
|
type fields struct {
|
|
eventstore func(*testing.T) *eventstore.Eventstore
|
|
}
|
|
type args struct {
|
|
ctx context.Context
|
|
userID string
|
|
resourceOwner string
|
|
links []*domain.UserIDPLink
|
|
}
|
|
type res struct {
|
|
err error
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
res res
|
|
}{
|
|
{
|
|
name: "missing userid, invalid argument error",
|
|
fields: fields{
|
|
eventstore: expectEventstore(),
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
userID: "",
|
|
links: []*domain.UserIDPLink{
|
|
{
|
|
IDPConfigID: "config1",
|
|
ExternalUserID: "externaluser1",
|
|
},
|
|
},
|
|
resourceOwner: "org1",
|
|
},
|
|
res: res{
|
|
err: zerrors.ThrowInvalidArgument(nil, "COMMAND-03j8f", "Errors.IDMissing"),
|
|
},
|
|
},
|
|
{
|
|
name: "no external idps, invalid argument error",
|
|
fields: fields{
|
|
eventstore: expectEventstore(),
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
},
|
|
res: res{
|
|
err: zerrors.ThrowInvalidArgument(nil, "COMMAND-Ek9s", "Errors.User.ExternalIDP.MinimumExternalIDPNeeded"),
|
|
},
|
|
},
|
|
{
|
|
name: "userID doesnt match aggregate id, invalid argument error",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
user.NewHumanAddedEvent(
|
|
context.Background(),
|
|
&user.NewAggregate("user1", "org1").Aggregate,
|
|
"userName",
|
|
"firstName",
|
|
"lastName",
|
|
"nickName",
|
|
"displayName",
|
|
language.German,
|
|
domain.GenderFemale,
|
|
"[email protected]",
|
|
false,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
links: []*domain.UserIDPLink{
|
|
{
|
|
ObjectRoot: models.ObjectRoot{
|
|
AggregateID: "user2",
|
|
},
|
|
IDPConfigID: "config1",
|
|
ExternalUserID: "externaluser1",
|
|
},
|
|
},
|
|
},
|
|
res: res{
|
|
err: zerrors.ThrowInvalidArgument(nil, "COMMAND-33M0g", "Errors.IDMissing"),
|
|
},
|
|
},
|
|
{
|
|
name: "invalid external idp, invalid argument error",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
user.NewHumanAddedEvent(
|
|
context.Background(),
|
|
&user.NewAggregate("user1", "org1").Aggregate,
|
|
"userName",
|
|
"firstName",
|
|
"lastName",
|
|
"nickName",
|
|
"displayName",
|
|
language.German,
|
|
domain.GenderFemale,
|
|
"[email protected]",
|
|
false,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
links: []*domain.UserIDPLink{
|
|
{
|
|
ObjectRoot: models.ObjectRoot{
|
|
AggregateID: "user1",
|
|
},
|
|
IDPConfigID: "",
|
|
ExternalUserID: "externaluser1",
|
|
},
|
|
},
|
|
},
|
|
res: res{
|
|
err: zerrors.ThrowInvalidArgument(nil, "COMMAND-6m9Kd", "Errors.User.ExternalIDP.Invalid"),
|
|
},
|
|
},
|
|
{
|
|
name: "config not existing, precondition error",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
user.NewHumanAddedEvent(
|
|
context.Background(),
|
|
&user.NewAggregate("user1", "org1").Aggregate,
|
|
"userName",
|
|
"firstName",
|
|
"lastName",
|
|
"nickName",
|
|
"displayName",
|
|
language.German,
|
|
domain.GenderFemale,
|
|
"[email protected]",
|
|
false,
|
|
),
|
|
),
|
|
),
|
|
expectFilter(),
|
|
),
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
links: []*domain.UserIDPLink{
|
|
{
|
|
ObjectRoot: models.ObjectRoot{
|
|
AggregateID: "user1",
|
|
},
|
|
IDPConfigID: "config1",
|
|
ExternalUserID: "externaluser1",
|
|
},
|
|
},
|
|
},
|
|
res: res{
|
|
err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-as02jin", "Errors.IDPConfig.NotExisting"),
|
|
},
|
|
},
|
|
{
|
|
name: "no linking not allowed, precondition failed",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
user.NewHumanAddedEvent(
|
|
context.Background(),
|
|
&user.NewAggregate("user1", "org1").Aggregate,
|
|
"userName",
|
|
"firstName",
|
|
"lastName",
|
|
"nickName",
|
|
"displayName",
|
|
language.German,
|
|
domain.GenderFemale,
|
|
"[email protected]",
|
|
false,
|
|
),
|
|
),
|
|
),
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
org.NewIDPConfigAddedEvent(context.Background(),
|
|
&org.NewAggregate("org1").Aggregate,
|
|
"config1",
|
|
"name",
|
|
domain.IDPConfigTypeOIDC,
|
|
domain.IDPConfigStylingTypeUnspecified,
|
|
true,
|
|
),
|
|
),
|
|
eventFromEventPusher(
|
|
org.NewIDPOIDCConfigAddedEvent(context.Background(),
|
|
&org.NewAggregate("org1").Aggregate,
|
|
"clientID",
|
|
"config1",
|
|
"issuer",
|
|
"authEndpoint",
|
|
"tokenEndpoint",
|
|
nil,
|
|
domain.OIDCMappingFieldUnspecified,
|
|
domain.OIDCMappingFieldUnspecified,
|
|
),
|
|
),
|
|
),
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
org.NewIDPConfigAddedEvent(context.Background(),
|
|
&org.NewAggregate("org1").Aggregate,
|
|
"config1",
|
|
"name",
|
|
domain.IDPConfigTypeOIDC,
|
|
domain.IDPConfigStylingTypeUnspecified,
|
|
true,
|
|
),
|
|
),
|
|
eventFromEventPusher(
|
|
org.NewIDPOIDCConfigAddedEvent(context.Background(),
|
|
&org.NewAggregate("org1").Aggregate,
|
|
"clientID",
|
|
"config1",
|
|
"issuer",
|
|
"authEndpoint",
|
|
"tokenEndpoint",
|
|
nil,
|
|
domain.OIDCMappingFieldUnspecified,
|
|
domain.OIDCMappingFieldUnspecified,
|
|
),
|
|
),
|
|
eventFromEventPusher(
|
|
func() eventstore.Command {
|
|
e, _ := org.NewOIDCIDPChangedEvent(context.Background(),
|
|
&org.NewAggregate("org1").Aggregate,
|
|
"config1",
|
|
[]idp.OIDCIDPChanges{
|
|
idp.ChangeOIDCOptions(idp.OptionChanges{IsLinkingAllowed: gu.Ptr(false)}),
|
|
},
|
|
)
|
|
return e
|
|
}(),
|
|
),
|
|
),
|
|
),
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
links: []*domain.UserIDPLink{
|
|
{
|
|
ObjectRoot: models.ObjectRoot{
|
|
AggregateID: "user1",
|
|
},
|
|
IDPConfigID: "config1",
|
|
DisplayName: "name",
|
|
ExternalUserID: "externaluser1",
|
|
},
|
|
},
|
|
},
|
|
res: res{
|
|
err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Sfee2", "Errors.ExternalIDP.LinkingNotAllowed"),
|
|
},
|
|
},
|
|
{
|
|
name: "auto linking not allowed (manual linking allowed), ok",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
user.NewHumanAddedEvent(
|
|
context.Background(),
|
|
&user.NewAggregate("user1", "org1").Aggregate,
|
|
"userName",
|
|
"firstName",
|
|
"lastName",
|
|
"nickName",
|
|
"displayName",
|
|
language.German,
|
|
domain.GenderFemale,
|
|
"[email protected]",
|
|
false,
|
|
),
|
|
),
|
|
),
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
org.NewIDPConfigAddedEvent(context.Background(),
|
|
&org.NewAggregate("org1").Aggregate,
|
|
"config1",
|
|
"name",
|
|
domain.IDPConfigTypeOIDC,
|
|
domain.IDPConfigStylingTypeUnspecified,
|
|
true,
|
|
),
|
|
),
|
|
eventFromEventPusher(
|
|
org.NewIDPOIDCConfigAddedEvent(context.Background(),
|
|
&org.NewAggregate("org1").Aggregate,
|
|
"clientID",
|
|
"config1",
|
|
"issuer",
|
|
"authEndpoint",
|
|
"tokenEndpoint",
|
|
nil,
|
|
domain.OIDCMappingFieldUnspecified,
|
|
domain.OIDCMappingFieldUnspecified,
|
|
),
|
|
),
|
|
),
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
org.NewIDPConfigAddedEvent(context.Background(),
|
|
&org.NewAggregate("org1").Aggregate,
|
|
"config1",
|
|
"name",
|
|
domain.IDPConfigTypeOIDC,
|
|
domain.IDPConfigStylingTypeUnspecified,
|
|
true,
|
|
),
|
|
),
|
|
eventFromEventPusher(
|
|
org.NewIDPOIDCConfigAddedEvent(context.Background(),
|
|
&org.NewAggregate("org1").Aggregate,
|
|
"clientID",
|
|
"config1",
|
|
"issuer",
|
|
"authEndpoint",
|
|
"tokenEndpoint",
|
|
nil,
|
|
domain.OIDCMappingFieldUnspecified,
|
|
domain.OIDCMappingFieldUnspecified,
|
|
),
|
|
),
|
|
eventFromEventPusher(
|
|
func() eventstore.Command {
|
|
e, _ := org.NewOIDCIDPChangedEvent(context.Background(),
|
|
&org.NewAggregate("org1").Aggregate,
|
|
"config1",
|
|
[]idp.OIDCIDPChanges{
|
|
idp.ChangeOIDCOptions(idp.OptionChanges{
|
|
IsLinkingAllowed: gu.Ptr(true),
|
|
AutoLinkingOption: gu.Ptr(domain.AutoLinkingOptionUnspecified),
|
|
}),
|
|
},
|
|
)
|
|
return e
|
|
}(),
|
|
),
|
|
),
|
|
expectPush(
|
|
user.NewUserIDPLinkAddedEvent(context.Background(),
|
|
&user.NewAggregate("user1", "org1").Aggregate,
|
|
"config1",
|
|
"name",
|
|
"externaluser1",
|
|
),
|
|
),
|
|
),
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
links: []*domain.UserIDPLink{
|
|
{
|
|
ObjectRoot: models.ObjectRoot{
|
|
AggregateID: "user1",
|
|
},
|
|
IDPConfigID: "config1",
|
|
DisplayName: "name",
|
|
ExternalUserID: "externaluser1",
|
|
},
|
|
},
|
|
},
|
|
res: res{},
|
|
},
|
|
{
|
|
name: "manual linking not allowed (auto linking allowed), ok",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
user.NewHumanAddedEvent(
|
|
context.Background(),
|
|
&user.NewAggregate("user1", "org1").Aggregate,
|
|
"userName",
|
|
"firstName",
|
|
"lastName",
|
|
"nickName",
|
|
"displayName",
|
|
language.German,
|
|
domain.GenderFemale,
|
|
"[email protected]",
|
|
false,
|
|
),
|
|
),
|
|
),
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
org.NewIDPConfigAddedEvent(context.Background(),
|
|
&org.NewAggregate("org1").Aggregate,
|
|
"config1",
|
|
"name",
|
|
domain.IDPConfigTypeOIDC,
|
|
domain.IDPConfigStylingTypeUnspecified,
|
|
true,
|
|
),
|
|
),
|
|
eventFromEventPusher(
|
|
org.NewIDPOIDCConfigAddedEvent(context.Background(),
|
|
&org.NewAggregate("org1").Aggregate,
|
|
"clientID",
|
|
"config1",
|
|
"issuer",
|
|
"authEndpoint",
|
|
"tokenEndpoint",
|
|
nil,
|
|
domain.OIDCMappingFieldUnspecified,
|
|
domain.OIDCMappingFieldUnspecified,
|
|
),
|
|
),
|
|
),
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
org.NewIDPConfigAddedEvent(context.Background(),
|
|
&org.NewAggregate("org1").Aggregate,
|
|
"config1",
|
|
"name",
|
|
domain.IDPConfigTypeOIDC,
|
|
domain.IDPConfigStylingTypeUnspecified,
|
|
true,
|
|
),
|
|
),
|
|
eventFromEventPusher(
|
|
org.NewIDPOIDCConfigAddedEvent(context.Background(),
|
|
&org.NewAggregate("org1").Aggregate,
|
|
"clientID",
|
|
"config1",
|
|
"issuer",
|
|
"authEndpoint",
|
|
"tokenEndpoint",
|
|
nil,
|
|
domain.OIDCMappingFieldUnspecified,
|
|
domain.OIDCMappingFieldUnspecified,
|
|
),
|
|
),
|
|
eventFromEventPusher(
|
|
func() eventstore.Command {
|
|
e, _ := org.NewOIDCIDPChangedEvent(context.Background(),
|
|
&org.NewAggregate("org1").Aggregate,
|
|
"config1",
|
|
[]idp.OIDCIDPChanges{
|
|
idp.ChangeOIDCOptions(idp.OptionChanges{
|
|
IsLinkingAllowed: gu.Ptr(false),
|
|
AutoLinkingOption: gu.Ptr(domain.AutoLinkingOptionEmail),
|
|
}),
|
|
},
|
|
)
|
|
return e
|
|
}(),
|
|
),
|
|
),
|
|
expectPush(
|
|
user.NewUserIDPLinkAddedEvent(context.Background(),
|
|
&user.NewAggregate("user1", "org1").Aggregate,
|
|
"config1",
|
|
"name",
|
|
"externaluser1",
|
|
),
|
|
),
|
|
),
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
links: []*domain.UserIDPLink{
|
|
{
|
|
ObjectRoot: models.ObjectRoot{
|
|
AggregateID: "user1",
|
|
},
|
|
IDPConfigID: "config1",
|
|
DisplayName: "name",
|
|
ExternalUserID: "externaluser1",
|
|
},
|
|
},
|
|
},
|
|
res: res{},
|
|
},
|
|
{
|
|
name: "add external idp org config, ok",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
user.NewHumanAddedEvent(
|
|
context.Background(),
|
|
&user.NewAggregate("user1", "org1").Aggregate,
|
|
"userName",
|
|
"firstName",
|
|
"lastName",
|
|
"nickName",
|
|
"displayName",
|
|
language.German,
|
|
domain.GenderFemale,
|
|
"[email protected]",
|
|
false,
|
|
),
|
|
),
|
|
),
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
org.NewIDPConfigAddedEvent(context.Background(),
|
|
&org.NewAggregate("org1").Aggregate,
|
|
"config1",
|
|
"name",
|
|
domain.IDPConfigTypeOIDC,
|
|
domain.IDPConfigStylingTypeUnspecified,
|
|
true,
|
|
),
|
|
),
|
|
eventFromEventPusher(
|
|
org.NewIDPOIDCConfigAddedEvent(context.Background(),
|
|
&org.NewAggregate("org1").Aggregate,
|
|
"clientID",
|
|
"config1",
|
|
"issuer",
|
|
"authEndpoint",
|
|
"tokenEndpoint",
|
|
nil,
|
|
domain.OIDCMappingFieldUnspecified,
|
|
domain.OIDCMappingFieldUnspecified,
|
|
),
|
|
),
|
|
),
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
org.NewIDPConfigAddedEvent(context.Background(),
|
|
&org.NewAggregate("org1").Aggregate,
|
|
"config1",
|
|
"name",
|
|
domain.IDPConfigTypeOIDC,
|
|
domain.IDPConfigStylingTypeUnspecified,
|
|
true,
|
|
),
|
|
),
|
|
eventFromEventPusher(
|
|
org.NewIDPOIDCConfigAddedEvent(context.Background(),
|
|
&org.NewAggregate("org1").Aggregate,
|
|
"clientID",
|
|
"config1",
|
|
"issuer",
|
|
"authEndpoint",
|
|
"tokenEndpoint",
|
|
nil,
|
|
domain.OIDCMappingFieldUnspecified,
|
|
domain.OIDCMappingFieldUnspecified,
|
|
),
|
|
),
|
|
),
|
|
expectPush(
|
|
user.NewUserIDPLinkAddedEvent(context.Background(),
|
|
&user.NewAggregate("user1", "org1").Aggregate,
|
|
"config1",
|
|
"name",
|
|
"externaluser1",
|
|
),
|
|
),
|
|
),
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
links: []*domain.UserIDPLink{
|
|
{
|
|
ObjectRoot: models.ObjectRoot{
|
|
AggregateID: "user1",
|
|
},
|
|
IDPConfigID: "config1",
|
|
DisplayName: "name",
|
|
ExternalUserID: "externaluser1",
|
|
},
|
|
},
|
|
},
|
|
res: res{},
|
|
},
|
|
{
|
|
name: "add external idp iam config, ok",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
user.NewHumanAddedEvent(
|
|
context.Background(),
|
|
&user.NewAggregate("user1", "org1").Aggregate,
|
|
"userName",
|
|
"firstName",
|
|
"lastName",
|
|
"nickName",
|
|
"displayName",
|
|
language.German,
|
|
domain.GenderFemale,
|
|
"[email protected]",
|
|
false,
|
|
),
|
|
),
|
|
),
|
|
expectFilter(
|
|
eventFromEventPusherWithInstanceID("instance1",
|
|
instance.NewIDPConfigAddedEvent(context.Background(),
|
|
&instance.NewAggregate("instance1").Aggregate,
|
|
"config1",
|
|
"name",
|
|
domain.IDPConfigTypeOIDC,
|
|
domain.IDPConfigStylingTypeUnspecified,
|
|
true,
|
|
),
|
|
),
|
|
eventFromEventPusherWithInstanceID("instance1",
|
|
instance.NewIDPOIDCConfigAddedEvent(context.Background(),
|
|
&instance.NewAggregate("instance1").Aggregate,
|
|
"clientID",
|
|
"config1",
|
|
"issuer",
|
|
"authEndpoint",
|
|
"tokenEndpoint",
|
|
nil,
|
|
domain.OIDCMappingFieldUnspecified,
|
|
domain.OIDCMappingFieldUnspecified,
|
|
),
|
|
),
|
|
),
|
|
expectFilter(
|
|
eventFromEventPusherWithInstanceID("instance1",
|
|
instance.NewIDPConfigAddedEvent(context.Background(),
|
|
&instance.NewAggregate("instance1").Aggregate,
|
|
"config1",
|
|
"name",
|
|
domain.IDPConfigTypeOIDC,
|
|
domain.IDPConfigStylingTypeUnspecified,
|
|
true,
|
|
),
|
|
),
|
|
eventFromEventPusherWithInstanceID("instance1",
|
|
instance.NewIDPOIDCConfigAddedEvent(context.Background(),
|
|
&instance.NewAggregate("instance1").Aggregate,
|
|
"clientID",
|
|
"config1",
|
|
"issuer",
|
|
"authEndpoint",
|
|
"tokenEndpoint",
|
|
nil,
|
|
domain.OIDCMappingFieldUnspecified,
|
|
domain.OIDCMappingFieldUnspecified,
|
|
),
|
|
),
|
|
),
|
|
expectPush(
|
|
user.NewUserIDPLinkAddedEvent(context.Background(),
|
|
&user.NewAggregate("user1", "org1").Aggregate,
|
|
"config1",
|
|
"name",
|
|
"externaluser1",
|
|
),
|
|
),
|
|
),
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
links: []*domain.UserIDPLink{
|
|
{
|
|
ObjectRoot: models.ObjectRoot{
|
|
AggregateID: "user1",
|
|
},
|
|
IDPConfigID: "config1",
|
|
DisplayName: "name",
|
|
ExternalUserID: "externaluser1",
|
|
},
|
|
},
|
|
},
|
|
res: res{},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
r := &Commands{
|
|
eventstore: tt.fields.eventstore(t),
|
|
}
|
|
err := r.BulkAddedUserIDPLinks(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.links)
|
|
assert.ErrorIs(t, err, tt.res.err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommandSide_AddUserIDPLink(t *testing.T) {
|
|
type fields struct {
|
|
eventstore func(*testing.T) *eventstore.Eventstore
|
|
checkPermission domain.PermissionCheck
|
|
}
|
|
type args struct {
|
|
ctx context.Context
|
|
userID string
|
|
resourceOwner string
|
|
link *AddLink
|
|
}
|
|
type res struct {
|
|
err func(error) bool
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
res res
|
|
}{
|
|
{
|
|
name: "missing userid, invalid argument error",
|
|
fields: fields{
|
|
eventstore: expectEventstore(),
|
|
checkPermission: newMockPermissionCheckAllowed(),
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
userID: "",
|
|
link: &AddLink{
|
|
IDPID: "config1",
|
|
IDPExternalID: "externaluser1",
|
|
},
|
|
},
|
|
res: res{
|
|
err: zerrors.IsErrorInvalidArgument,
|
|
},
|
|
},
|
|
{
|
|
name: "user not existing, precondition failed error",
|
|
fields: fields{
|
|
eventstore: expectEventstore(expectFilter()),
|
|
checkPermission: newMockPermissionCheckAllowed(),
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
link: &AddLink{
|
|
IDPID: "config1",
|
|
IDPExternalID: "externaluser1",
|
|
},
|
|
},
|
|
res: res{
|
|
err: zerrors.IsPreconditionFailed,
|
|
},
|
|
},
|
|
{
|
|
// Security regression (account takeover): binding an external identity to a user
|
|
// must require an explicit user-write permission even when the caller targets
|
|
// their own account (self-link). The previous self-link bypass let any
|
|
// authenticated user claim an unproven external subject on themselves and, because
|
|
// external links are globally unique, hijack a victim's future federated logins.
|
|
name: "self-link without permission, permission denied error",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
user.NewHumanAddedEvent(
|
|
context.Background(),
|
|
&user.NewAggregate("user1", "org1").Aggregate,
|
|
"userName",
|
|
"firstName",
|
|
"lastName",
|
|
"nickName",
|
|
"displayName",
|
|
language.German,
|
|
domain.GenderFemale,
|
|
"[email protected]",
|
|
false,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
checkPermission: newMockPermissionCheckNotAllowed(),
|
|
},
|
|
args: args{
|
|
// ctx acts as the target user itself (self-link).
|
|
ctx: authz.SetCtxData(context.Background(), authz.CtxData{UserID: "user1"}),
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
link: &AddLink{
|
|
IDPID: "config1",
|
|
DisplayName: "name",
|
|
IDPExternalID: "externaluser1",
|
|
},
|
|
},
|
|
res: res{
|
|
err: zerrors.IsPermissionDenied,
|
|
},
|
|
},
|
|
{
|
|
// Self-link is allowed when the caller holds the user-write permission.
|
|
// This pins the login-client flow (service user with user.write links a
|
|
// verified external subject to its own IDP-intent session) so a future
|
|
// refactor cannot silently break it.
|
|
name: "self-link with permission, ok",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
user.NewHumanAddedEvent(
|
|
context.Background(),
|
|
&user.NewAggregate("user1", "org1").Aggregate,
|
|
"userName",
|
|
"firstName",
|
|
"lastName",
|
|
"nickName",
|
|
"displayName",
|
|
language.German,
|
|
domain.GenderFemale,
|
|
"[email protected]",
|
|
false,
|
|
),
|
|
),
|
|
),
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
org.NewIDPConfigAddedEvent(
|
|
context.Background(),
|
|
&org.NewAggregate("org1").Aggregate,
|
|
"config1",
|
|
"name",
|
|
domain.IDPConfigTypeOIDC,
|
|
domain.IDPConfigStylingTypeUnspecified,
|
|
false,
|
|
),
|
|
),
|
|
),
|
|
expectPush(
|
|
user.NewUserIDPLinkAddedEvent(
|
|
authz.SetCtxData(context.Background(), authz.CtxData{UserID: "user1"}),
|
|
&user.NewAggregate("user1", "org1").Aggregate,
|
|
"config1",
|
|
"name",
|
|
"externaluser1",
|
|
),
|
|
),
|
|
),
|
|
checkPermission: newMockPermissionCheckAllowed(),
|
|
},
|
|
args: args{
|
|
// ctx acts as the target user itself (self-link).
|
|
ctx: authz.SetCtxData(context.Background(), authz.CtxData{UserID: "user1"}),
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
link: &AddLink{
|
|
IDPID: "config1",
|
|
DisplayName: "name",
|
|
IDPExternalID: "externaluser1",
|
|
},
|
|
},
|
|
res: res{},
|
|
},
|
|
{
|
|
// Linking another user's account with the user-write permission is the
|
|
// ordinary managed path and must keep working.
|
|
name: "other-user link with permission, ok",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
user.NewHumanAddedEvent(
|
|
context.Background(),
|
|
&user.NewAggregate("user1", "org1").Aggregate,
|
|
"userName",
|
|
"firstName",
|
|
"lastName",
|
|
"nickName",
|
|
"displayName",
|
|
language.German,
|
|
domain.GenderFemale,
|
|
"[email protected]",
|
|
false,
|
|
),
|
|
),
|
|
),
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
org.NewIDPConfigAddedEvent(
|
|
context.Background(),
|
|
&org.NewAggregate("org1").Aggregate,
|
|
"config1",
|
|
"name",
|
|
domain.IDPConfigTypeOIDC,
|
|
domain.IDPConfigStylingTypeUnspecified,
|
|
false,
|
|
),
|
|
),
|
|
),
|
|
expectPush(
|
|
user.NewUserIDPLinkAddedEvent(
|
|
authz.SetCtxData(context.Background(), authz.CtxData{UserID: "admin1"}),
|
|
&user.NewAggregate("user1", "org1").Aggregate,
|
|
"config1",
|
|
"name",
|
|
"externaluser1",
|
|
),
|
|
),
|
|
),
|
|
checkPermission: newMockPermissionCheckAllowed(),
|
|
},
|
|
args: args{
|
|
// ctx is a different user (e.g. an admin) than the link target.
|
|
ctx: authz.SetCtxData(context.Background(), authz.CtxData{UserID: "admin1"}),
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
link: &AddLink{
|
|
IDPID: "config1",
|
|
DisplayName: "name",
|
|
IDPExternalID: "externaluser1",
|
|
},
|
|
},
|
|
res: res{},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
r := &Commands{
|
|
eventstore: tt.fields.eventstore(t),
|
|
checkPermission: tt.fields.checkPermission,
|
|
}
|
|
_, err := r.AddUserIDPLink(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.link)
|
|
if tt.res.err == nil {
|
|
assert.NoError(t, err)
|
|
}
|
|
if tt.res.err != nil && !tt.res.err(err) {
|
|
t.Errorf("got wrong err: %v ", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommandSide_RemoveUserIDPLink(t *testing.T) {
|
|
type fields struct {
|
|
eventstore func(*testing.T) *eventstore.Eventstore
|
|
checkPermission domain.PermissionCheck
|
|
}
|
|
type args struct {
|
|
ctx context.Context
|
|
link *domain.UserIDPLink
|
|
}
|
|
type res struct {
|
|
want *domain.ObjectDetails
|
|
err func(error) bool
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
res res
|
|
}{
|
|
{
|
|
name: "invalid idp, invalid argument error",
|
|
fields: fields{
|
|
eventstore: expectEventstore(),
|
|
checkPermission: newMockPermissionCheckAllowed(),
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
link: &domain.UserIDPLink{
|
|
ObjectRoot: models.ObjectRoot{
|
|
AggregateID: "user1",
|
|
},
|
|
IDPConfigID: "",
|
|
ExternalUserID: "externaluser1",
|
|
},
|
|
},
|
|
res: res{
|
|
err: zerrors.IsErrorInvalidArgument,
|
|
},
|
|
},
|
|
{
|
|
name: "aggregate id missing, invalid argument error",
|
|
fields: fields{
|
|
eventstore: expectEventstore(),
|
|
checkPermission: newMockPermissionCheckAllowed(),
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
link: &domain.UserIDPLink{
|
|
IDPConfigID: "config1",
|
|
ExternalUserID: "externaluser1",
|
|
},
|
|
},
|
|
res: res{
|
|
err: zerrors.IsErrorInvalidArgument,
|
|
},
|
|
},
|
|
{
|
|
name: "user removed, not found error",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
user.NewUserIDPLinkAddedEvent(context.Background(),
|
|
&user.NewAggregate("user1", "org1").Aggregate,
|
|
"config1",
|
|
"name",
|
|
"externaluser1",
|
|
),
|
|
),
|
|
eventFromEventPusher(
|
|
user.NewUserRemovedEvent(context.Background(),
|
|
&user.NewAggregate("user1", "org1").Aggregate,
|
|
"username",
|
|
nil,
|
|
true,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
checkPermission: newMockPermissionCheckAllowed(),
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
link: &domain.UserIDPLink{
|
|
ObjectRoot: models.ObjectRoot{
|
|
AggregateID: "user1",
|
|
},
|
|
IDPConfigID: "config1",
|
|
ExternalUserID: "externaluser1",
|
|
},
|
|
},
|
|
res: res{
|
|
err: zerrors.IsNotFound,
|
|
},
|
|
},
|
|
{
|
|
name: "external idp not existing, not found error",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(),
|
|
),
|
|
checkPermission: newMockPermissionCheckAllowed(),
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
link: &domain.UserIDPLink{
|
|
ObjectRoot: models.ObjectRoot{
|
|
AggregateID: "user1",
|
|
},
|
|
IDPConfigID: "config1",
|
|
ExternalUserID: "externaluser1",
|
|
},
|
|
},
|
|
res: res{
|
|
err: zerrors.IsNotFound,
|
|
},
|
|
},
|
|
{
|
|
name: "remove external idp, permission error",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
user.NewUserIDPLinkAddedEvent(context.Background(),
|
|
&user.NewAggregate("user1", "org1").Aggregate,
|
|
"config1",
|
|
"name",
|
|
"externaluser1",
|
|
),
|
|
),
|
|
),
|
|
),
|
|
checkPermission: newMockPermissionCheckNotAllowed(),
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
link: &domain.UserIDPLink{
|
|
ObjectRoot: models.ObjectRoot{
|
|
AggregateID: "user1",
|
|
},
|
|
IDPConfigID: "config1",
|
|
ExternalUserID: "externaluser1",
|
|
},
|
|
},
|
|
res: res{
|
|
err: zerrors.IsPermissionDenied,
|
|
},
|
|
},
|
|
{
|
|
name: "remove external idp, ok",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
user.NewUserIDPLinkAddedEvent(context.Background(),
|
|
&user.NewAggregate("user1", "org1").Aggregate,
|
|
"config1",
|
|
"name",
|
|
"externaluser1",
|
|
),
|
|
),
|
|
),
|
|
expectPush(
|
|
user.NewUserIDPLinkRemovedEvent(context.Background(),
|
|
&user.NewAggregate("user1", "org1").Aggregate,
|
|
"config1",
|
|
"externaluser1",
|
|
),
|
|
),
|
|
),
|
|
checkPermission: newMockPermissionCheckAllowed(),
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
link: &domain.UserIDPLink{
|
|
ObjectRoot: models.ObjectRoot{
|
|
AggregateID: "user1",
|
|
},
|
|
IDPConfigID: "config1",
|
|
ExternalUserID: "externaluser1",
|
|
},
|
|
},
|
|
res: res{
|
|
want: &domain.ObjectDetails{
|
|
ResourceOwner: "org1",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
r := &Commands{
|
|
eventstore: tt.fields.eventstore(t),
|
|
checkPermission: tt.fields.checkPermission,
|
|
}
|
|
got, err := r.RemoveUserIDPLink(tt.args.ctx, tt.args.link)
|
|
if tt.res.err == nil {
|
|
assert.NoError(t, err)
|
|
}
|
|
if tt.res.err != nil && !tt.res.err(err) {
|
|
t.Errorf("got wrong err: %v ", err)
|
|
}
|
|
if tt.res.err == nil {
|
|
assertObjectDetails(t, tt.res.want, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommandSide_ExternalLoginCheck(t *testing.T) {
|
|
type fields struct {
|
|
eventstore func(*testing.T) *eventstore.Eventstore
|
|
}
|
|
type args struct {
|
|
ctx context.Context
|
|
orgID string
|
|
userID string
|
|
authRequest *domain.AuthRequest
|
|
}
|
|
type res struct {
|
|
err func(error) bool
|
|
}
|
|
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(),
|
|
orgID: "org1",
|
|
userID: "",
|
|
},
|
|
res: res{
|
|
err: zerrors.IsErrorInvalidArgument,
|
|
},
|
|
},
|
|
{
|
|
name: "user removed, not found error",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
user.NewUserIDPLinkAddedEvent(context.Background(),
|
|
&user.NewAggregate("user1", "org1").Aggregate,
|
|
"config1",
|
|
"name",
|
|
"externaluser1",
|
|
),
|
|
),
|
|
eventFromEventPusher(
|
|
user.NewUserRemovedEvent(context.Background(),
|
|
&user.NewAggregate("user1", "org1").Aggregate,
|
|
"username",
|
|
nil,
|
|
true,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
orgID: "org1",
|
|
userID: "user1",
|
|
},
|
|
res: res{
|
|
err: zerrors.IsPreconditionFailed,
|
|
},
|
|
},
|
|
{
|
|
name: "external login check, ok",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
user.NewHumanAddedEvent(context.Background(),
|
|
&user.NewAggregate("user1", "org1").Aggregate,
|
|
"username",
|
|
"firstname",
|
|
"lastname",
|
|
"nickname",
|
|
"displayname",
|
|
language.German,
|
|
domain.GenderUnspecified,
|
|
"[email protected]",
|
|
true,
|
|
),
|
|
),
|
|
),
|
|
expectPush(
|
|
user.NewUserIDPCheckSucceededEvent(context.Background(),
|
|
&user.NewAggregate("user1", "org1").Aggregate,
|
|
&user.AuthRequestInfo{
|
|
ID: "request1",
|
|
UserAgentID: "useragent1",
|
|
SelectedIDPConfigID: "config1",
|
|
},
|
|
),
|
|
),
|
|
),
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
orgID: "org1",
|
|
userID: "user1",
|
|
authRequest: &domain.AuthRequest{
|
|
ID: "request1",
|
|
AgentID: "useragent1",
|
|
SelectedIDPConfigID: "config1",
|
|
},
|
|
},
|
|
res: res{},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
r := &Commands{
|
|
eventstore: tt.fields.eventstore(t),
|
|
}
|
|
err := r.UserIDPLoginChecked(tt.args.ctx, tt.args.orgID, tt.args.userID, tt.args.authRequest)
|
|
if tt.res.err == nil {
|
|
assert.NoError(t, err)
|
|
}
|
|
if tt.res.err != nil && !tt.res.err(err) {
|
|
t.Errorf("got wrong err: %v ", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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: zerrors.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: zerrors.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(
|
|
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)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommandSide_UpdateUserIDPLinkUsername(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
|
|
externalUserID string
|
|
newUsername 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",
|
|
externalUserID: "externalUserID",
|
|
newUsername: "newUsername",
|
|
},
|
|
res: res{
|
|
err: zerrors.ThrowInvalidArgument(nil, "COMMAND-SFegz", "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",
|
|
externalUserID: "otherID",
|
|
newUsername: "newUsername",
|
|
},
|
|
res: res{
|
|
err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-DGhre", "Errors.User.ExternalIDP.NotFound"),
|
|
},
|
|
},
|
|
{
|
|
name: "external username not changed, ok",
|
|
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",
|
|
externalUserID: "externalUserID",
|
|
newUsername: "displayName",
|
|
},
|
|
res: res{},
|
|
},
|
|
{
|
|
name: "external username update, ok",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
user.NewUserIDPLinkAddedEvent(context.Background(),
|
|
&user.NewAggregate("user1", "org1").Aggregate,
|
|
"idpConfig1",
|
|
"displayName",
|
|
"externalUserID",
|
|
),
|
|
),
|
|
),
|
|
expectPush(
|
|
user.NewUserIDPExternalUsernameEvent(context.Background(),
|
|
&user.NewAggregate("user1", "org1").Aggregate,
|
|
"idpConfig1",
|
|
"externalUserID",
|
|
"newUsername",
|
|
),
|
|
),
|
|
),
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
userID: "user1",
|
|
orgID: "org1",
|
|
idpConfigID: "idpConfig1",
|
|
externalUserID: "externalUserID",
|
|
newUsername: "newUsername",
|
|
},
|
|
res: res{},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
r := &Commands{
|
|
eventstore: tt.fields.eventstore(t),
|
|
}
|
|
err := r.UpdateUserIDPLinkUsername(tt.args.ctx, tt.args.userID, tt.args.orgID, tt.args.idpConfigID, tt.args.externalUserID, tt.args.newUsername)
|
|
assert.ErrorIs(t, err, tt.res.err)
|
|
})
|
|
}
|
|
}
|