fix: correct user self management on metadata and delete (#10666)

# Which Problems Are Solved

This PR fixes the self-management of users for metadata and own removal
and improves the corresponding permission checks.
While looking into the problems, I also noticed that there's a bug in
the metadata mapping when using `api.metadata.push` in actions v1 and
that re-adding a previously existing key after its removal was not
possible.

# How the Problems Are Solved

- Added a parameter `allowSelfManagement` to checkPermissionOnUser to
not require a permission if a user is changing its own data.
- Updated use of `NewPermissionCheckUserWrite` including prevention of
self-management for metadata.
- Pass permission check to the command side (for metadata functions) to
allow it implicitly for login v1 and actions v1.
- Use of json.Marshal for the metadata mapping (as with
`AppendMetadata`)
- Check the metadata state when comparing the value.

# Additional Changes

- added a variadic `roles` parameter to the `CreateOrgMembership`
integration test helper function to allow defining specific roles.

# Additional Context

- noted internally while testing v4.1.x
- requires backport to v4.x
- closes https://github.com/zitadel/zitadel/issues/10470
- relates to https://github.com/zitadel/zitadel/pull/10426

(cherry picked from commit 5329d50509)
This commit is contained in:
Livio Spring
2025-09-16 14:26:21 +02:00
parent 389f908041
commit fa83c39510
31 changed files with 695 additions and 208 deletions

View File

@@ -147,6 +147,7 @@ func TestCommands_CheckPermissionUserWrite(t *testing.T) {
type args struct {
ctx context.Context
resourceOwner, aggregateID string
allowSelfManagement bool
}
type want struct {
err func(error) bool
@@ -163,8 +164,29 @@ func TestCommands_CheckPermissionUserWrite(t *testing.T) {
ctx: authz.SetCtxData(context.Background(), authz.CtxData{
UserID: "aggregateID",
}),
resourceOwner: "resourceOwner",
aggregateID: "aggregateID",
resourceOwner: "resourceOwner",
aggregateID: "aggregateID",
allowSelfManagement: true,
},
},
{
name: "self, no selfManagementAllowed, permission check",
fields: fields{
domainPermissionCheck: mockDomainPermissionCheck(
authz.SetCtxData(context.Background(), authz.CtxData{
UserID: "aggregateID",
}),
"user.write",
"resourceOwner",
"aggregateID"),
},
args: args{
ctx: authz.SetCtxData(context.Background(), authz.CtxData{
UserID: "aggregateID",
}),
resourceOwner: "resourceOwner",
aggregateID: "aggregateID",
allowSelfManagement: false,
},
},
{
@@ -194,7 +216,7 @@ func TestCommands_CheckPermissionUserWrite(t *testing.T) {
if tt.fields.domainPermissionCheck != nil {
c.checkPermission = tt.fields.domainPermissionCheck(t)
}
err := c.NewPermissionCheckUserWrite(tt.args.ctx)(tt.args.resourceOwner, tt.args.aggregateID)
err := c.NewPermissionCheckUserWrite(tt.args.ctx, tt.args.allowSelfManagement)(tt.args.resourceOwner, tt.args.aggregateID)
if tt.want.err != nil {
assert.True(t, tt.want.err(err))
}
@@ -223,7 +245,40 @@ func TestCommands_CheckPermissionUserDelete(t *testing.T) {
want want
}{
{
name: "self, no permission check",
name: "self permission allowed, permission check",
fields: fields{
domainPermissionCheck: mockDomainPermissionCheck(
userCtx,
"user.delete",
"resourceOwner",
"aggregateID"),
},
args: args{
ctx: userCtx,
resourceOwner: "resourceOwner",
aggregateID: "aggregateID",
},
},
{
name: "self user.delete not allowed, user.self.delete permission check",
fields: fields{
domainPermissionCheck: mockDomainPermissionChecks(
expectedCheck{
userCtx,
"user.delete",
"resourceOwner",
"aggregateID",
zerrors.ThrowPermissionDenied(nil, "id", "permission denied"),
},
expectedCheck{
userCtx,
"user.self.delete",
"resourceOwner",
"aggregateID",
nil,
},
),
},
args: args{
ctx: userCtx,
resourceOwner: "resourceOwner",
@@ -276,3 +331,41 @@ func mockDomainPermissionCheck(expectCtx context.Context, expectPermission, expe
}
}
}
type expectedCheck struct {
ctx context.Context
permission string
resourceOwner string
resourceID string
err error
}
func mockDomainPermissionChecks(checks ...expectedCheck) func(t *testing.T) domain.PermissionCheck {
var i int
return func(t *testing.T) domain.PermissionCheck {
t.Cleanup(func() {
t.Helper()
if i != len(checks) {
t.Logf("not all expected checks were called, expected: %d, got: %d", len(checks), i)
for ; i < len(checks); i++ {
t.Logf("missing call: %+v", checks[i])
}
t.Fail()
}
})
return func(ctx context.Context, permission, orgID, resourceID string) (err error) {
if i >= len(checks) {
assert.Fail(t, "no more checks expected")
return nil
}
expect := checks[i]
assert.Equal(t, expect.ctx, ctx)
assert.Equal(t, expect.permission, permission)
assert.Equal(t, expect.resourceOwner, orgID)
assert.Equal(t, expect.resourceID, resourceID)
i++
return expect.err
}
}
}