fix: scim v2 endpoints enforce user resource owner (#9273)

# Which Problems Are Solved
- If a SCIM endpoint is called with an orgID in the URL that is not the
resource owner, no error is returned, and the action is executed.

# How the Problems Are Solved
- The orgID provided in the SCIM URL path must match the resource owner
of the target user. Otherwise, an error will be returned.

# Additional Context

Part of https://github.com/zitadel/zitadel/issues/8140
This commit is contained in:
Lars
2025-01-30 16:43:13 +01:00
committed by GitHub
parent 60cfa6cb76
commit 563f74640e
16 changed files with 153 additions and 78 deletions

View File

@@ -159,12 +159,12 @@ func (c *Commands) userStateWriteModel(ctx context.Context, userID string) (writ
return writeModel, nil
}
func (c *Commands) RemoveUserV2(ctx context.Context, userID string, cascadingUserMemberships []*CascadingMembership, cascadingGrantIDs ...string) (*domain.ObjectDetails, error) {
func (c *Commands) RemoveUserV2(ctx context.Context, userID, resourceOwner string, cascadingUserMemberships []*CascadingMembership, cascadingGrantIDs ...string) (*domain.ObjectDetails, error) {
if userID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-vaipl7s13l", "Errors.User.UserIDMissing")
}
existingUser, err := c.userRemoveWriteModel(ctx, userID)
existingUser, err := c.userRemoveWriteModel(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
@@ -210,11 +210,11 @@ func (c *Commands) RemoveUserV2(ctx context.Context, userID string, cascadingUse
return writeModelToObjectDetails(&existingUser.WriteModel), nil
}
func (c *Commands) userRemoveWriteModel(ctx context.Context, userID string) (writeModel *UserV2WriteModel, err error) {
func (c *Commands) userRemoveWriteModel(ctx context.Context, userID, resourceOwner string) (writeModel *UserV2WriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
writeModel = NewUserRemoveWriteModel(userID, "")
writeModel = NewUserRemoveWriteModel(userID, resourceOwner)
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
if err != nil {
return nil, err

View File

@@ -14,12 +14,13 @@ import (
)
type ChangeHuman struct {
ID string
State *domain.UserState
Username *string
Profile *Profile
Email *Email
Phone *Phone
ID string
ResourceOwner string
State *domain.UserState
Username *string
Profile *Profile
Email *Email
Phone *Phone
Metadata []*domain.Metadata
MetadataKeysToRemove []string
@@ -267,6 +268,7 @@ func (c *Commands) ChangeUserHuman(ctx context.Context, human *ChangeHuman, alg
existingHuman, err := c.UserHumanWriteModel(
ctx,
human.ID,
human.ResourceOwner,
human.Profile != nil,
human.Email != nil,
human.Phone != nil,
@@ -525,11 +527,11 @@ func (c *Commands) userExistsWriteModel(ctx context.Context, userID string) (wri
return writeModel, nil
}
func (c *Commands) UserHumanWriteModel(ctx context.Context, userID string, profileWM, emailWM, phoneWM, passwordWM, avatarWM, idpLinksWM, metadataWM bool) (writeModel *UserV2WriteModel, err error) {
func (c *Commands) UserHumanWriteModel(ctx context.Context, userID, resourceOwner string, profileWM, emailWM, phoneWM, passwordWM, avatarWM, idpLinksWM, metadataWM bool) (writeModel *UserV2WriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
writeModel = NewUserHumanWriteModel(userID, "", profileWM, emailWM, phoneWM, passwordWM, avatarWM, idpLinksWM, metadataWM)
writeModel = NewUserHumanWriteModel(userID, resourceOwner, profileWM, emailWM, phoneWM, passwordWM, avatarWM, idpLinksWM, metadataWM)
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
if err != nil {
return nil, err

View File

@@ -567,7 +567,7 @@ func TestCommandSide_userHumanWriteModel_profile(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore(t),
}
wm, err := r.UserHumanWriteModel(tt.args.ctx, tt.args.userID, true, false, false, false, false, false, false)
wm, err := r.UserHumanWriteModel(tt.args.ctx, tt.args.userID, "", true, false, false, false, false, false, false)
if tt.res.err == nil {
if !assert.NoError(t, err) {
t.FailNow()
@@ -912,7 +912,7 @@ func TestCommandSide_userHumanWriteModel_email(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore(t),
}
wm, err := r.UserHumanWriteModel(tt.args.ctx, tt.args.userID, false, true, false, false, false, false, false)
wm, err := r.UserHumanWriteModel(tt.args.ctx, tt.args.userID, "", false, true, false, false, false, false, false)
if tt.res.err == nil {
if !assert.NoError(t, err) {
t.FailNow()
@@ -1344,7 +1344,7 @@ func TestCommandSide_userHumanWriteModel_phone(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore(t),
}
wm, err := r.UserHumanWriteModel(tt.args.ctx, tt.args.userID, false, false, true, false, false, false, false)
wm, err := r.UserHumanWriteModel(tt.args.ctx, tt.args.userID, "", false, false, true, false, false, false, false)
if tt.res.err == nil {
if !assert.NoError(t, err) {
t.FailNow()
@@ -1605,7 +1605,7 @@ func TestCommandSide_userHumanWriteModel_password(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore(t),
}
wm, err := r.UserHumanWriteModel(tt.args.ctx, tt.args.userID, false, false, false, true, false, false, false)
wm, err := r.UserHumanWriteModel(tt.args.ctx, tt.args.userID, "", false, false, false, true, false, false, false)
if tt.res.err == nil {
if !assert.NoError(t, err) {
t.FailNow()
@@ -2132,7 +2132,7 @@ func TestCommandSide_userHumanWriteModel_avatar(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore(t),
}
wm, err := r.UserHumanWriteModel(tt.args.ctx, tt.args.userID, false, false, false, false, true, false, false)
wm, err := r.UserHumanWriteModel(tt.args.ctx, tt.args.userID, "", false, false, false, false, true, false, false)
if tt.res.err == nil {
if !assert.NoError(t, err) {
t.FailNow()
@@ -2441,7 +2441,7 @@ func TestCommandSide_userHumanWriteModel_idpLinks(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore(t),
}
wm, err := r.userRemoveWriteModel(tt.args.ctx, tt.args.userID)
wm, err := r.userRemoveWriteModel(tt.args.ctx, tt.args.userID, "")
if tt.res.err == nil {
if !assert.NoError(t, err) {
t.FailNow()
@@ -2744,7 +2744,7 @@ func TestCommandSide_userHumanWriteModel_metadata(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore(t),
}
wm, err := r.UserHumanWriteModel(tt.args.ctx, tt.args.userID, false, false, false, false, false, false, true)
wm, err := r.UserHumanWriteModel(tt.args.ctx, tt.args.userID, "", false, false, false, false, false, false, true)
if tt.res.err == nil {
if !assert.NoError(t, err) {
t.FailNow()

View File

@@ -1358,7 +1358,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
eventstore: tt.fields.eventstore(t),
checkPermission: tt.fields.checkPermission,
}
got, err := r.RemoveUserV2(tt.args.ctx, tt.args.userID, tt.args.cascadingMemberships, tt.args.grantIDs...)
got, err := r.RemoveUserV2(tt.args.ctx, tt.args.userID, "", tt.args.cascadingMemberships, tt.args.grantIDs...)
if tt.res.err == nil {
assert.NoError(t, err)
}