diff --git a/docs/docs/apis/proto/management.md b/docs/docs/apis/proto/management.md
index 43eebe443..77b950965 100644
--- a/docs/docs/apis/proto/management.md
+++ b/docs/docs/apis/proto/management.md
@@ -5408,6 +5408,7 @@ This is an empty response
| password_change_required | bool | - | |
| request_passwordless_registration | bool | - | |
| otp_code | string | - | |
+| idps | repeated ImportHumanUserRequest.IDP | - | |
@@ -5436,6 +5437,19 @@ This is an empty response
+### ImportHumanUserRequest.IDP
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| config_id | string | internal id of the IDP in ZITADEL | string.min_len: 1
string.max_len: 200
|
+| external_user_id | string | id of the user on the IDP | string.min_len: 1
string.max_len: 200
|
+| display_name | string | (display) name of the user on the IDP | string.max_len: 200
|
+
+
+
+
### ImportHumanUserRequest.Phone
diff --git a/internal/api/grpc/admin/import.go b/internal/api/grpc/admin/import.go
index b56d80128..b3f0ec449 100644
--- a/internal/api/grpc/admin/import.go
+++ b/internal/api/grpc/admin/import.go
@@ -535,9 +535,9 @@ func (s *Server) importData(ctx context.Context, orgs []*admin_pb.DataOrg) (*adm
if org.HumanUsers != nil {
for _, user := range org.GetHumanUsers() {
logging.Debugf("import user: %s", user.GetUserId())
- human, passwordless := management.ImportHumanUserRequestToDomain(user.User)
+ human, passwordless, links := management.ImportHumanUserRequestToDomain(user.User)
human.AggregateID = user.UserId
- _, _, err := s.command.ImportHuman(ctx, org.GetOrgId(), human, passwordless, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode)
+ _, _, err := s.command.ImportHuman(ctx, org.GetOrgId(), human, passwordless, links, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode)
if err != nil {
errors = append(errors, &admin_pb.ImportDataError{Type: "human_user", Id: user.GetUserId(), Message: err.Error()})
if isCtxTimeout(ctx) {
diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go
index 2adb399b0..191bc82bc 100644
--- a/internal/api/grpc/management/user.go
+++ b/internal/api/grpc/management/user.go
@@ -230,7 +230,7 @@ func AddHumanUserRequestToAddHuman(req *mgmt_pb.AddHumanUserRequest) *command.Ad
}
func (s *Server) ImportHumanUser(ctx context.Context, req *mgmt_pb.ImportHumanUserRequest) (*mgmt_pb.ImportHumanUserResponse, error) {
- human, passwordless := ImportHumanUserRequestToDomain(req)
+ human, passwordless, links := ImportHumanUserRequestToDomain(req)
initCodeGenerator, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeInitCode, s.userCodeAlg)
if err != nil {
return nil, err
@@ -247,7 +247,7 @@ func (s *Server) ImportHumanUser(ctx context.Context, req *mgmt_pb.ImportHumanUs
if err != nil {
return nil, err
}
- addedHuman, code, err := s.command.ImportHuman(ctx, authz.GetCtxData(ctx).OrgID, human, passwordless, initCodeGenerator, phoneCodeGenerator, emailCodeGenerator, passwordlessInitCode)
+ addedHuman, code, err := s.command.ImportHuman(ctx, authz.GetCtxData(ctx).OrgID, human, passwordless, links, initCodeGenerator, phoneCodeGenerator, emailCodeGenerator, passwordlessInitCode)
if err != nil {
return nil, err
}
diff --git a/internal/api/grpc/management/user_converter.go b/internal/api/grpc/management/user_converter.go
index 638316d6b..33d85bba9 100644
--- a/internal/api/grpc/management/user_converter.go
+++ b/internal/api/grpc/management/user_converter.go
@@ -120,7 +120,7 @@ func AddHumanUserRequestToDomain(req *mgmt_pb.AddHumanUserRequest) *domain.Human
return h
}
-func ImportHumanUserRequestToDomain(req *mgmt_pb.ImportHumanUserRequest) (human *domain.Human, passwordless bool) {
+func ImportHumanUserRequestToDomain(req *mgmt_pb.ImportHumanUserRequest) (human *domain.Human, passwordless bool, links []*domain.UserIDPLink) {
human = &domain.Human{
Username: req.UserName,
}
@@ -153,8 +153,16 @@ func ImportHumanUserRequestToDomain(req *mgmt_pb.ImportHumanUserRequest) (human
if req.HashedPassword != nil && req.HashedPassword.Value != "" && req.HashedPassword.Algorithm != "" {
human.HashedPassword = domain.NewHashedPassword(req.HashedPassword.Value, req.HashedPassword.Algorithm)
}
+ links = make([]*domain.UserIDPLink, len(req.Idps))
+ for i, idp := range req.Idps {
+ links[i] = &domain.UserIDPLink{
+ IDPConfigID: idp.ConfigId,
+ ExternalUserID: idp.ExternalUserId,
+ DisplayName: idp.DisplayName,
+ }
+ }
- return human, req.RequestPasswordlessRegistration
+ return human, req.RequestPasswordlessRegistration, links
}
func AddMachineUserRequestToCommand(req *mgmt_pb.AddMachineUserRequest, resourceowner string) *command.Machine {
diff --git a/internal/command/user_human.go b/internal/command/user_human.go
index 4003a7cf9..0eeece11c 100644
--- a/internal/command/user_human.go
+++ b/internal/command/user_human.go
@@ -285,7 +285,7 @@ func (h *AddHuman) shouldAddInitCode() bool {
h.Password == ""
}
-func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator crypto.Generator) (_ *domain.Human, passwordlessCode *domain.PasswordlessInitCode, err error) {
+func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, links []*domain.UserIDPLink, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator crypto.Generator) (_ *domain.Human, passwordlessCode *domain.PasswordlessInitCode, err error) {
if orgID == "" {
return nil, nil, errors.ThrowInvalidArgument(nil, "COMMAND-5N8fs", "Errors.ResourceOwnerMissing")
}
@@ -309,7 +309,7 @@ func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain.
}
}
- events, addedHuman, addedCode, code, err := c.importHuman(ctx, orgID, human, passwordless, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator)
+ events, addedHuman, addedCode, code, err := c.importHuman(ctx, orgID, human, passwordless, links, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator)
if err != nil {
return nil, nil, err
}
@@ -396,11 +396,11 @@ func (c *Commands) addHuman(ctx context.Context, orgID string, human *domain.Hum
return c.createHuman(ctx, orgID, human, nil, false, false, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator)
}
-func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator crypto.Generator) (events []eventstore.Command, humanWriteModel *HumanWriteModel, passwordlessCodeWriteModel *HumanPasswordlessInitCodeWriteModel, code string, err error) {
+func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, links []*domain.UserIDPLink, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator crypto.Generator) (events []eventstore.Command, humanWriteModel *HumanWriteModel, passwordlessCodeWriteModel *HumanPasswordlessInitCodeWriteModel, code string, err error) {
if orgID == "" || !human.IsValid() {
return nil, nil, nil, "", errors.ThrowInvalidArgument(nil, "COMMAND-00p2b", "Errors.User.Invalid")
}
- events, humanWriteModel, err = c.createHuman(ctx, orgID, human, nil, false, passwordless, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator)
+ events, humanWriteModel, err = c.createHuman(ctx, orgID, human, links, false, passwordless, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator)
if err != nil {
return nil, nil, nil, "", err
}
@@ -428,10 +428,14 @@ func (c *Commands) registerHuman(ctx context.Context, orgID string, human *domai
if human.Password != nil && human.Password.SecretString != "" {
human.Password.ChangeRequired = false
}
- return c.createHuman(ctx, orgID, human, link, true, false, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator)
+ var links []*domain.UserIDPLink
+ if link != nil {
+ links = append(links, link)
+ }
+ return c.createHuman(ctx, orgID, human, links, true, false, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator)
}
-func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.Human, link *domain.UserIDPLink, selfregister, passwordless bool, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator crypto.Generator) (events []eventstore.Command, addedHuman *HumanWriteModel, err error) {
+func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.Human, links []*domain.UserIDPLink, selfregister, passwordless bool, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator crypto.Generator) (events []eventstore.Command, addedHuman *HumanWriteModel, err error) {
if err := human.CheckDomainPolicy(domainPolicy); err != nil {
return nil, nil, err
}
@@ -476,7 +480,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.
events = append(events, createAddHumanEvent(ctx, userAgg, human, domainPolicy.UserLoginMustBeDomain))
}
- if link != nil {
+ for _, link := range links {
event, err := c.addUserIDPLink(ctx, userAgg, link)
if err != nil {
return nil, nil, err
@@ -484,7 +488,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.
events = append(events, event)
}
- if human.IsInitialState(passwordless, link != nil) {
+ if human.IsInitialState(passwordless, len(links) > 0) {
initCode, err := domain.NewInitUserCode(initCodeGenerator)
if err != nil {
return nil, nil, err
diff --git a/internal/command/user_human_test.go b/internal/command/user_human_test.go
index 9d0e7110d..2dc0d66a4 100644
--- a/internal/command/user_human_test.go
+++ b/internal/command/user_human_test.go
@@ -703,6 +703,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
orgID string
human *domain.Human
passwordless bool
+ links []*domain.UserIDPLink
secretGenerator crypto.Generator
passwordlessInitCode crypto.Generator
}
@@ -1452,6 +1453,135 @@ func TestCommandSide_ImportHuman(t *testing.T) {
},
},
},
+ {
+ name: "add human (with idp), ok",
+ fields: fields{
+ eventstore: eventstoreExpect(
+ t,
+ expectFilter(
+ eventFromEventPusher(
+ org.NewDomainPolicyAddedEvent(context.Background(),
+ &user.NewAggregate("user1", "org1").Aggregate,
+ true,
+ true,
+ true,
+ ),
+ ),
+ ),
+ expectFilter(
+ eventFromEventPusher(
+ org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
+ &user.NewAggregate("user1", "org1").Aggregate,
+ 1,
+ false,
+ false,
+ false,
+ false,
+ ),
+ ),
+ ),
+ expectFilter(
+ eventFromEventPusher(
+ org.NewIDPConfigAddedEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ "idpID",
+ "name",
+ domain.IDPConfigTypeOIDC,
+ domain.IDPConfigStylingTypeUnspecified,
+ false,
+ ),
+ ),
+ eventFromEventPusher(
+ org.NewIDPOIDCConfigAddedEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ "clientID",
+ "idpID",
+ "issuer",
+ "authEndpoint",
+ "tokenEndpoint",
+ nil,
+ domain.OIDCMappingFieldUnspecified,
+ domain.OIDCMappingFieldUnspecified,
+ ),
+ ),
+ eventFromEventPusher(
+ org.NewIdentityProviderAddedEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ "idpID",
+ domain.IdentityProviderTypeOrg,
+ ),
+ ),
+ ),
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusher(
+ newAddHumanEvent("", false, ""),
+ ),
+ eventFromEventPusher(
+ user.NewUserIDPLinkAddedEvent(context.Background(),
+ &user.NewAggregate("user1", "org1").Aggregate,
+ "idpID",
+ "name",
+ "externalID",
+ ),
+ ),
+ eventFromEventPusher(
+ user.NewHumanEmailVerifiedEvent(context.Background(),
+ &user.NewAggregate("user1", "org1").Aggregate),
+ ),
+ },
+ uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
+ uniqueConstraintsFromEventConstraint(user.NewAddUserIDPLinkUniqueConstraint("idpID", "externalID")),
+ ),
+ ),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
+ userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
+ },
+ args: args{
+ ctx: context.Background(),
+ orgID: "org1",
+ human: &domain.Human{
+ Username: "username",
+ Profile: &domain.Profile{
+ FirstName: "firstname",
+ LastName: "lastname",
+ PreferredLanguage: language.English,
+ },
+ Email: &domain.Email{
+ EmailAddress: "email@test.ch",
+ IsEmailVerified: true,
+ },
+ },
+ links: []*domain.UserIDPLink{
+ {
+ IDPConfigID: "idpID",
+ ExternalUserID: "externalID",
+ DisplayName: "name",
+ },
+ },
+ secretGenerator: GetMockSecretGenerator(t),
+ },
+ res: res{
+ wantHuman: &domain.Human{
+ ObjectRoot: models.ObjectRoot{
+ AggregateID: "user1",
+ ResourceOwner: "org1",
+ },
+ Username: "username",
+ Profile: &domain.Profile{
+ FirstName: "firstname",
+ LastName: "lastname",
+ DisplayName: "firstname lastname",
+ PreferredLanguage: language.English,
+ },
+ Email: &domain.Email{
+ EmailAddress: "email@test.ch",
+ IsEmailVerified: true,
+ },
+ State: domain.UserStateActive,
+ },
+ },
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -1460,7 +1590,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
idGenerator: tt.fields.idGenerator,
userPasswordAlg: tt.fields.userPasswordAlg,
}
- gotHuman, gotCode, err := r.ImportHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.passwordless, tt.args.secretGenerator, tt.args.secretGenerator, tt.args.secretGenerator, tt.args.secretGenerator)
+ gotHuman, gotCode, err := r.ImportHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.passwordless, tt.args.links, tt.args.secretGenerator, tt.args.secretGenerator, tt.args.secretGenerator, tt.args.secretGenerator)
if tt.res.err == nil {
assert.NoError(t, err)
}
@@ -2479,6 +2609,158 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
},
},
},
+ {
+ name: "add with idp link, email verified, ok",
+ fields: fields{
+ eventstore: eventstoreExpect(
+ t,
+ expectFilter(
+ eventFromEventPusher(
+ org.NewDomainPolicyAddedEvent(context.Background(),
+ &user.NewAggregate("org1", "org1").Aggregate,
+ true,
+ true,
+ true,
+ ),
+ ),
+ ),
+ expectFilter(
+ eventFromEventPusher(
+ org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ 1,
+ false,
+ false,
+ false,
+ false,
+ ),
+ ),
+ ),
+ expectFilter(
+ eventFromEventPusher(
+ org.NewLoginPolicyAddedEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ false,
+ true,
+ true,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ domain.PasswordlessTypeNotAllowed,
+ "",
+ time.Hour*1,
+ time.Hour*2,
+ time.Hour*3,
+ time.Hour*4,
+ time.Hour*5,
+ ),
+ ),
+ ),
+ expectFilter(
+ eventFromEventPusher(
+ org.NewIDPConfigAddedEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ "idpID",
+ "name",
+ domain.IDPConfigTypeOIDC,
+ domain.IDPConfigStylingTypeUnspecified,
+ false,
+ ),
+ ),
+ eventFromEventPusher(
+ org.NewIDPOIDCConfigAddedEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ "clientID",
+ "idpID",
+ "issuer",
+ "authEndpoint",
+ "tokenEndpoint",
+ nil,
+ domain.OIDCMappingFieldUnspecified,
+ domain.OIDCMappingFieldUnspecified,
+ ),
+ ),
+ eventFromEventPusher(
+ org.NewIdentityProviderAddedEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ "idpID",
+ domain.IdentityProviderTypeOrg,
+ ),
+ ),
+ ),
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusher(
+ newRegisterHumanEvent("username", "password", false, ""),
+ ),
+ eventFromEventPusher(
+ user.NewUserIDPLinkAddedEvent(context.Background(),
+ &user.NewAggregate("user1", "org1").Aggregate,
+ "idpID",
+ "displayName",
+ "externalID",
+ ),
+ ),
+ eventFromEventPusher(
+ user.NewHumanEmailVerifiedEvent(context.Background(),
+ &user.NewAggregate("user1", "org1").Aggregate),
+ ),
+ },
+ uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
+ uniqueConstraintsFromEventConstraint(user.NewAddUserIDPLinkUniqueConstraint("idpID", "externalID")),
+ ),
+ ),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
+ userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
+ },
+ args: args{
+ ctx: context.Background(),
+ orgID: "org1",
+ human: &domain.Human{
+ Username: "username",
+ Password: &domain.Password{
+ SecretString: "password",
+ },
+ Profile: &domain.Profile{
+ FirstName: "firstname",
+ LastName: "lastname",
+ },
+ Email: &domain.Email{
+ EmailAddress: "email@test.ch",
+ IsEmailVerified: true,
+ },
+ },
+ link: &domain.UserIDPLink{
+ IDPConfigID: "idpID",
+ ExternalUserID: "externalID",
+ DisplayName: "displayName",
+ },
+ secretGenerator: GetMockSecretGenerator(t),
+ },
+ res: res{
+ want: &domain.Human{
+ ObjectRoot: models.ObjectRoot{
+ AggregateID: "user1",
+ ResourceOwner: "org1",
+ },
+ Username: "username",
+ Profile: &domain.Profile{
+ FirstName: "firstname",
+ LastName: "lastname",
+ DisplayName: "firstname lastname",
+ PreferredLanguage: language.Und,
+ },
+ Email: &domain.Email{
+ EmailAddress: "email@test.ch",
+ IsEmailVerified: true,
+ },
+ State: domain.UserStateActive,
+ },
+ },
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto
index ce0f28f7f..e74863a89 100644
--- a/proto/zitadel/management.proto
+++ b/proto/zitadel/management.proto
@@ -3128,6 +3128,14 @@ message ImportHumanUserRequest {
string value = 1;
string algorithm = 2;
}
+ message IDP {
+ // internal id of the IDP in ZITADEL
+ string config_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ // id of the user on the IDP
+ string external_user_id = 2 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ // (display) name of the user on the IDP
+ string display_name = 3 [(validate.rules).string = {max_len: 200}];
+ }
string user_name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
@@ -3138,8 +3146,8 @@ message ImportHumanUserRequest {
HashedPassword hashed_password = 6;
bool password_change_required = 7;
bool request_passwordless_registration = 8;
-
string otp_code = 9;
+ repeated IDP idps = 10;
}
message ImportHumanUserResponse {