feat: set metadata at the user level in CreateUser request (#11719)

# Which Problems Are Solved

Currently, metadata can be set/updated only for human users, but it
should be available also for service accounts (machine users).

# How the Problems Are Solved

This is achieved by:
- adding a metadata field at the root level in `CreateUserRequest`,
which makes it available for both `human` and `machine` user types
- returning an error when both the root level and human-level `metadata`
fields are set in the `CreateUserRequest`
- setting `human` and `machine` metadata from the root `metadata` field
in the server layer
- pushing `user.metadata.set` event during machine user creation in the
command layer
- adding integration tests

# Additional Changes
N/A

# Additional Context
- Related to https://github.com/zitadel/zitadel/issues/11369

---------

Co-authored-by: Vitor Bari Buccianti <[email protected]>
Co-authored-by: Silvan <[email protected]>
This commit is contained in:
Gayathri Vijayan
2026-03-03 08:34:43 +00:00
committed by GitHub
co-authored by Vitor Bari Buccianti Silvan
parent 92a628d892
commit 4aacfa842f
29 changed files with 440 additions and 144 deletions
+18 -7
View File
@@ -15,14 +15,14 @@ import (
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) createUserTypeHuman(ctx context.Context, humanPb *user.CreateUserRequest_Human, orgId string, userName, userId *string) (*connect.Response[user.CreateUserResponse], error) {
metadataEntries := make([]*user.SetMetadataEntry, len(humanPb.Metadata))
for i, metadataEntry := range humanPb.Metadata {
metadataEntries[i] = &user.SetMetadataEntry{
Key: metadataEntry.GetKey(),
Value: metadataEntry.GetValue(),
}
func (s *Server) createUserTypeHuman(ctx context.Context, humanPb *user.CreateUserRequest_Human, orgId string, userName, userId *string, metadata []*user.Metadata) (*connect.Response[user.CreateUserResponse], error) {
if len(metadata) > 0 && len(humanPb.Metadata) > 0 { //nolint:staticcheck
return nil, zerrors.ThrowInvalidArgument(nil, "USERv2-j0z7uy", "Errors.User.Metadata.Conflicting")
}
if len(metadata) == 0 && len(humanPb.Metadata) > 0 { //nolint:staticcheck
metadata = humanPb.Metadata //nolint:staticcheck
}
metadataEntries := setMetadataEntries(metadata)
addHumanPb := &user.AddHumanUserRequest{
Username: userName,
UserId: userId,
@@ -69,6 +69,17 @@ func (s *Server) createUserTypeHuman(ctx context.Context, humanPb *user.CreateUs
}), nil
}
func setMetadataEntries(metadata []*user.Metadata) []*user.SetMetadataEntry {
metadataEntries := make([]*user.SetMetadataEntry, len(metadata))
for i, metadataEntry := range metadata {
metadataEntries[i] = &user.SetMetadataEntry{
Key: metadataEntry.GetKey(),
Value: metadataEntry.GetValue(),
}
}
return metadataEntries
}
func (s *Server) updateUserTypeHuman(ctx context.Context, humanPb *user.UpdateUserRequest_Human, userId string, userName *string) (*connect.Response[user.UpdateUserResponse], error) {
cmd, err := updateHumanUserToCommand(userId, userName, humanPb)
if err != nil {
@@ -3111,7 +3111,7 @@ func TestServer_CreateUser(t *testing.T) {
},
},
{
name: "with metadata",
name: "error - metadata on both root and human",
testCase: func(runId string) testCase {
username := fmt.Sprintf("donald.duck+%s", runId)
email := username + "@example.com"
@@ -3135,16 +3135,16 @@ func TestServer_CreateUser(t *testing.T) {
},
Metadata: []*user.Metadata{
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))},
{Key: "key2", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value2")))},
{Key: "key3", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value3")))},
},
},
},
Metadata: []*user.Metadata{
{Key: "key2", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value2")))},
{Key: "key3", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value3")))},
},
},
},
want: &user.CreateUserResponse{
Id: "is generated",
},
wantErr: true,
}
},
},
@@ -3514,154 +3514,271 @@ func TestServer_CreateUser_And_Compare(t *testing.T) {
type testCase struct {
name string
args args
assert func(t *testing.T, createResponse *user.CreateUserResponse, getResponse *user.GetUserByIDResponse)
assert func(
t *testing.T,
createResponse *user.CreateUserResponse,
getResponse *user.GetUserByIDResponse,
getMetadataResponse *user.ListUserMetadataResponse,
)
}
tests := []struct {
name string
testCase func(runId string) testCase
}{{
name: "human given username",
testCase: func(runId string) testCase {
username := fmt.Sprintf("donald.duck+%s", runId)
email := username + "@example.com"
return testCase{
args: args{
ctx: OrgCTX,
req: &user.CreateUserRequest{
OrganizationId: Instance.DefaultOrg.Id,
Username: &username,
UserType: &user.CreateUserRequest_Human_{
Human: &user.CreateUserRequest_Human{
Profile: &user.SetHumanProfile{
GivenName: "Donald",
FamilyName: "Duck",
},
Email: &user.SetHumanEmail{
Email: email,
}{
{
name: "human given username",
testCase: func(runId string) testCase {
username := fmt.Sprintf("donald.duck+%s", runId)
email := username + "@example.com"
return testCase{
args: args{
ctx: OrgCTX,
req: &user.CreateUserRequest{
OrganizationId: Instance.DefaultOrg.Id,
Username: &username,
UserType: &user.CreateUserRequest_Human_{
Human: &user.CreateUserRequest_Human{
Profile: &user.SetHumanProfile{
GivenName: "Donald",
FamilyName: "Duck",
},
Email: &user.SetHumanEmail{
Email: email,
},
},
},
},
},
},
assert: func(t *testing.T, _ *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) {
assert.Equal(t, username, getResponse.GetUser().GetUsername())
},
}
assert: func(t *testing.T, _ *user.CreateUserResponse, getResponse *user.GetUserByIDResponse, _ *user.ListUserMetadataResponse) {
assert.Equal(t, username, getResponse.GetUser().GetUsername())
},
}
},
},
}, {
name: "human username default to email",
testCase: func(runId string) testCase {
username := fmt.Sprintf("donald.duck+%s", runId)
email := username + "@example.com"
return testCase{
args: args{
ctx: OrgCTX,
req: &user.CreateUserRequest{
OrganizationId: Instance.DefaultOrg.Id,
UserType: &user.CreateUserRequest_Human_{
Human: &user.CreateUserRequest_Human{
Profile: &user.SetHumanProfile{
GivenName: "Donald",
FamilyName: "Duck",
},
Email: &user.SetHumanEmail{
Email: email,
{
name: "human with embedded metadata",
testCase: func(runId string) testCase {
username := fmt.Sprintf("donald.duck+%s", runId)
email := username + "@example.com"
return testCase{
args: args{
ctx: OrgCTX,
req: &user.CreateUserRequest{
OrganizationId: Instance.DefaultOrg.Id,
Username: &username,
UserType: &user.CreateUserRequest_Human_{
Human: &user.CreateUserRequest_Human{
Profile: &user.SetHumanProfile{
GivenName: "Donald",
FamilyName: "Duck",
},
Email: &user.SetHumanEmail{
Email: email,
},
Metadata: []*user.Metadata{
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))},
{Key: "key2", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value2")))},
},
},
},
},
},
},
assert: func(t *testing.T, _ *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) {
assert.Equal(t, email, getResponse.GetUser().GetUsername())
},
}
assert: func(t *testing.T, _ *user.CreateUserResponse, getResponse *user.GetUserByIDResponse, getMetadataResponse *user.ListUserMetadataResponse) {
assert.Equal(t, username, getResponse.GetUser().GetUsername())
metadataByKey := getMetadataMap(getMetadataResponse)
assert.Len(t, getMetadataResponse.GetMetadata(), 2)
assert.Equal(t, []byte(base64.StdEncoding.EncodeToString([]byte("value1"))), metadataByKey["key1"])
assert.Equal(t, []byte(base64.StdEncoding.EncodeToString([]byte("value2"))), metadataByKey["key2"])
},
}
},
},
}, {
name: "service accountname given",
testCase: func(runId string) testCase {
username := fmt.Sprintf("donald.duck+%s", runId)
return testCase{
args: args{
ctx: OrgCTX,
req: &user.CreateUserRequest{
OrganizationId: Instance.DefaultOrg.Id,
Username: &username,
UserType: &user.CreateUserRequest_Machine_{
Machine: &user.CreateUserRequest_Machine{
Name: "donald",
{
name: "human with metadata",
testCase: func(runId string) testCase {
username := fmt.Sprintf("donald.duck+%s", runId)
email := username + "@example.com"
return testCase{
args: args{
ctx: OrgCTX,
req: &user.CreateUserRequest{
OrganizationId: Instance.DefaultOrg.Id,
Username: &username,
UserType: &user.CreateUserRequest_Human_{
Human: &user.CreateUserRequest_Human{
Profile: &user.SetHumanProfile{
GivenName: "Donald",
FamilyName: "Duck",
},
Email: &user.SetHumanEmail{
Email: email,
},
},
},
Metadata: []*user.Metadata{
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))},
},
},
},
},
assert: func(t *testing.T, _ *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) {
assert.Equal(t, username, getResponse.GetUser().GetUsername())
},
}
assert: func(t *testing.T, _ *user.CreateUserResponse, getResponse *user.GetUserByIDResponse, getMetadataResponse *user.ListUserMetadataResponse) {
assert.Equal(t, username, getResponse.GetUser().GetUsername())
metadataByKey := getMetadataMap(getMetadataResponse)
assert.Len(t, getMetadataResponse.GetMetadata(), 1)
assert.Equal(t, []byte(base64.StdEncoding.EncodeToString([]byte("value1"))), metadataByKey["key1"])
},
}
},
},
}, {
name: "service accountname default to generated id",
testCase: func(runId string) testCase {
return testCase{
args: args{
ctx: OrgCTX,
req: &user.CreateUserRequest{
OrganizationId: Instance.DefaultOrg.Id,
UserType: &user.CreateUserRequest_Machine_{
Machine: &user.CreateUserRequest_Machine{
Name: "donald",
{
name: "human username default to email",
testCase: func(runId string) testCase {
username := fmt.Sprintf("donald.duck+%s", runId)
email := username + "@example.com"
return testCase{
args: args{
ctx: OrgCTX,
req: &user.CreateUserRequest{
OrganizationId: Instance.DefaultOrg.Id,
UserType: &user.CreateUserRequest_Human_{
Human: &user.CreateUserRequest_Human{
Profile: &user.SetHumanProfile{
GivenName: "Donald",
FamilyName: "Duck",
},
Email: &user.SetHumanEmail{
Email: email,
},
},
},
},
},
},
assert: func(t *testing.T, createResponse *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) {
assert.Equal(t, createResponse.GetId(), getResponse.GetUser().GetUsername())
},
}
assert: func(t *testing.T, _ *user.CreateUserResponse, getResponse *user.GetUserByIDResponse, _ *user.ListUserMetadataResponse) {
assert.Equal(t, email, getResponse.GetUser().GetUsername())
},
}
},
},
}, {
name: "service accountname default to given id",
testCase: func(runId string) testCase {
return testCase{
args: args{
ctx: OrgCTX,
req: &user.CreateUserRequest{
OrganizationId: Instance.DefaultOrg.Id,
UserId: &runId,
UserType: &user.CreateUserRequest_Machine_{
Machine: &user.CreateUserRequest_Machine{
Name: "donald",
{
name: "service accountname given",
testCase: func(runId string) testCase {
username := fmt.Sprintf("donald.duck+%s", runId)
return testCase{
args: args{
ctx: OrgCTX,
req: &user.CreateUserRequest{
OrganizationId: Instance.DefaultOrg.Id,
Username: &username,
UserType: &user.CreateUserRequest_Machine_{
Machine: &user.CreateUserRequest_Machine{
Name: "donald",
},
},
},
},
},
assert: func(t *testing.T, createResponse *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) {
assert.Equal(t, runId, getResponse.GetUser().GetUsername())
},
}
assert: func(t *testing.T, _ *user.CreateUserResponse, getResponse *user.GetUserByIDResponse, _ *user.ListUserMetadataResponse) {
assert.Equal(t, username, getResponse.GetUser().GetUsername())
},
}
},
},
}, {
name: "machine access token type jwt",
testCase: func(runId string) testCase {
return testCase{
args: args{
ctx: OrgCTX,
req: &user.CreateUserRequest{
OrganizationId: Instance.DefaultOrg.Id,
UserId: &runId,
UserType: &user.CreateUserRequest_Machine_{
Machine: &user.CreateUserRequest_Machine{
Name: "donald",
AccessTokenType: user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT,
{
name: "service account with metadata",
testCase: func(runId string) testCase {
username := fmt.Sprintf("donald.duck+%s", runId)
return testCase{
args: args{
ctx: OrgCTX,
req: &user.CreateUserRequest{
OrganizationId: Instance.DefaultOrg.Id,
Username: &username,
UserType: &user.CreateUserRequest_Machine_{
Machine: &user.CreateUserRequest_Machine{
Name: "donald",
},
},
Metadata: []*user.Metadata{
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))},
{Key: "key2", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value2")))},
},
},
},
},
assert: func(t *testing.T, createResponse *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) {
assert.Equal(t, user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT, getResponse.GetUser().GetMachine().GetAccessTokenType())
},
}
assert: func(t *testing.T, _ *user.CreateUserResponse, getResponse *user.GetUserByIDResponse, getMetadataResponse *user.ListUserMetadataResponse) {
assert.Equal(t, username, getResponse.GetUser().GetUsername())
metadataByKey := getMetadataMap(getMetadataResponse)
assert.Len(t, getMetadataResponse.GetMetadata(), 2)
assert.Equal(t, []byte(base64.StdEncoding.EncodeToString([]byte("value1"))), metadataByKey["key1"])
assert.Equal(t, []byte(base64.StdEncoding.EncodeToString([]byte("value2"))), metadataByKey["key2"])
},
}
},
},
}}
{
name: "service accountname default to generated id",
testCase: func(runId string) testCase {
return testCase{
args: args{
ctx: OrgCTX,
req: &user.CreateUserRequest{
OrganizationId: Instance.DefaultOrg.Id,
UserType: &user.CreateUserRequest_Machine_{
Machine: &user.CreateUserRequest_Machine{
Name: "donald",
},
},
},
},
assert: func(t *testing.T, createResponse *user.CreateUserResponse, getResponse *user.GetUserByIDResponse, _ *user.ListUserMetadataResponse) {
assert.Equal(t, createResponse.GetId(), getResponse.GetUser().GetUsername())
},
}
},
},
{
name: "service accountname default to given id",
testCase: func(runId string) testCase {
return testCase{
args: args{
ctx: OrgCTX,
req: &user.CreateUserRequest{
OrganizationId: Instance.DefaultOrg.Id,
UserId: &runId,
UserType: &user.CreateUserRequest_Machine_{
Machine: &user.CreateUserRequest_Machine{
Name: "donald",
},
},
},
},
assert: func(t *testing.T, createResponse *user.CreateUserResponse, getResponse *user.GetUserByIDResponse, _ *user.ListUserMetadataResponse) {
assert.Equal(t, runId, getResponse.GetUser().GetUsername())
},
}
},
},
{
name: "machine access token type jwt",
testCase: func(runId string) testCase {
return testCase{
args: args{
ctx: OrgCTX,
req: &user.CreateUserRequest{
OrganizationId: Instance.DefaultOrg.Id,
UserId: &runId,
UserType: &user.CreateUserRequest_Machine_{
Machine: &user.CreateUserRequest_Machine{
Name: "donald",
AccessTokenType: user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT,
},
},
},
},
assert: func(t *testing.T, createResponse *user.CreateUserResponse, getResponse *user.GetUserByIDResponse, _ *user.ListUserMetadataResponse) {
assert.Equal(t, user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT, getResponse.GetUser().GetMachine().GetAccessTokenType())
},
}
},
},
}
for i, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
now := time.Now()
@@ -3674,11 +3791,23 @@ func TestServer_CreateUser_And_Compare(t *testing.T) {
UserId: createResponse.GetId(),
})
require.NoError(t, err)
test.assert(t, createResponse, getResponse)
gotMetadataResponse, err := Client.ListUserMetadata(test.args.ctx, &user.ListUserMetadataRequest{
UserId: createResponse.GetId(),
})
require.NoError(t, err)
test.assert(t, createResponse, getResponse, gotMetadataResponse)
})
}
}
func getMetadataMap(getMetadataResponse *user.ListUserMetadataResponse) map[string][]byte {
metadataByKey := make(map[string][]byte, len(getMetadataResponse.GetMetadata()))
for _, md := range getMetadataResponse.GetMetadata() {
metadataByKey[md.Key] = md.Value
}
return metadataByKey
}
func TestServer_CreateUser_Permission(t *testing.T) {
newOrgOwnerEmail := integration.Email()
newOrg := Instance.CreateOrganization(IamCTX, integration.OrganizationName(), newOrgOwnerEmail)
+9 -1
View File
@@ -12,7 +12,14 @@ import (
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) createUserTypeMachine(ctx context.Context, machinePb *user.CreateUserRequest_Machine, orgId, userName, userId string) (*connect.Response[user.CreateUserResponse], error) {
func (s *Server) createUserTypeMachine(ctx context.Context, machinePb *user.CreateUserRequest_Machine, orgId, userName, userId string, reqMetadata []*user.Metadata) (*connect.Response[user.CreateUserResponse], error) {
metadata := make([]*command.AddMetadataEntry, len(reqMetadata))
for i, metadataEntry := range reqMetadata {
metadata[i] = &command.AddMetadataEntry{
Key: metadataEntry.GetKey(),
Value: metadataEntry.GetValue(),
}
}
cmd := &command.Machine{
Username: userName,
Name: machinePb.Name,
@@ -22,6 +29,7 @@ func (s *Server) createUserTypeMachine(ctx context.Context, machinePb *user.Crea
ResourceOwner: orgId,
AggregateID: userId,
},
Metadata: metadata,
}
details, err := s.command.AddMachine(
ctx,
+2 -2
View File
@@ -414,9 +414,9 @@ func (s *Server) HumanMFAInitSkipped(ctx context.Context, req *connect.Request[u
func (s *Server) CreateUser(ctx context.Context, req *connect.Request[user.CreateUserRequest]) (*connect.Response[user.CreateUserResponse], error) {
switch userType := req.Msg.GetUserType().(type) {
case *user.CreateUserRequest_Human_:
return s.createUserTypeHuman(ctx, userType.Human, req.Msg.GetOrganizationId(), req.Msg.Username, req.Msg.UserId)
return s.createUserTypeHuman(ctx, userType.Human, req.Msg.GetOrganizationId(), req.Msg.Username, req.Msg.UserId, req.Msg.GetMetadata())
case *user.CreateUserRequest_Machine_:
return s.createUserTypeMachine(ctx, userType.Machine, req.Msg.GetOrganizationId(), req.Msg.GetUsername(), req.Msg.GetUserId())
return s.createUserTypeMachine(ctx, userType.Machine, req.Msg.GetOrganizationId(), req.Msg.GetUsername(), req.Msg.GetUserId(), req.Msg.GetMetadata())
default:
return nil, zerrors.ThrowInternal(nil, "", "user type is not implemented")
}