feat: allow import of federated users in ImportHumanUser (#4675)

Co-authored-by: Fabi <38692350+hifabienne@users.noreply.github.com>
This commit is contained in:
Livio Spring 2022-11-09 09:33:50 +01:00 committed by GitHub
parent a4ddedb5f4
commit eba602e064
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 332 additions and 16 deletions

View File

@ -5408,6 +5408,7 @@ This is an empty response
| password_change_required | bool | - | | | password_change_required | bool | - | |
| request_passwordless_registration | bool | - | | | request_passwordless_registration | bool | - | |
| otp_code | string | - | | | 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<br /> string.max_len: 200<br /> |
| external_user_id | string | id of the user on the IDP | string.min_len: 1<br /> string.max_len: 200<br /> |
| display_name | string | (display) name of the user on the IDP | string.max_len: 200<br /> |
### ImportHumanUserRequest.Phone ### ImportHumanUserRequest.Phone

View File

@ -535,9 +535,9 @@ func (s *Server) importData(ctx context.Context, orgs []*admin_pb.DataOrg) (*adm
if org.HumanUsers != nil { if org.HumanUsers != nil {
for _, user := range org.GetHumanUsers() { for _, user := range org.GetHumanUsers() {
logging.Debugf("import user: %s", user.GetUserId()) logging.Debugf("import user: %s", user.GetUserId())
human, passwordless := management.ImportHumanUserRequestToDomain(user.User) human, passwordless, links := management.ImportHumanUserRequestToDomain(user.User)
human.AggregateID = user.UserId 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 { if err != nil {
errors = append(errors, &admin_pb.ImportDataError{Type: "human_user", Id: user.GetUserId(), Message: err.Error()}) errors = append(errors, &admin_pb.ImportDataError{Type: "human_user", Id: user.GetUserId(), Message: err.Error()})
if isCtxTimeout(ctx) { if isCtxTimeout(ctx) {

View File

@ -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) { 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) initCodeGenerator, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeInitCode, s.userCodeAlg)
if err != nil { if err != nil {
return nil, err return nil, err
@ -247,7 +247,7 @@ func (s *Server) ImportHumanUser(ctx context.Context, req *mgmt_pb.ImportHumanUs
if err != nil { if err != nil {
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }

View File

@ -120,7 +120,7 @@ func AddHumanUserRequestToDomain(req *mgmt_pb.AddHumanUserRequest) *domain.Human
return h 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{ human = &domain.Human{
Username: req.UserName, Username: req.UserName,
} }
@ -153,8 +153,16 @@ func ImportHumanUserRequestToDomain(req *mgmt_pb.ImportHumanUserRequest) (human
if req.HashedPassword != nil && req.HashedPassword.Value != "" && req.HashedPassword.Algorithm != "" { if req.HashedPassword != nil && req.HashedPassword.Value != "" && req.HashedPassword.Algorithm != "" {
human.HashedPassword = domain.NewHashedPassword(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 { func AddMachineUserRequestToCommand(req *mgmt_pb.AddMachineUserRequest, resourceowner string) *command.Machine {

View File

@ -285,7 +285,7 @@ func (h *AddHuman) shouldAddInitCode() bool {
h.Password == "" 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 == "" { if orgID == "" {
return nil, nil, errors.ThrowInvalidArgument(nil, "COMMAND-5N8fs", "Errors.ResourceOwnerMissing") 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 { if err != nil {
return nil, nil, err 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) 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() { if orgID == "" || !human.IsValid() {
return nil, nil, nil, "", errors.ThrowInvalidArgument(nil, "COMMAND-00p2b", "Errors.User.Invalid") 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 { if err != nil {
return nil, nil, nil, "", err 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 != "" { if human.Password != nil && human.Password.SecretString != "" {
human.Password.ChangeRequired = false 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 { if err := human.CheckDomainPolicy(domainPolicy); err != nil {
return nil, nil, err 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)) events = append(events, createAddHumanEvent(ctx, userAgg, human, domainPolicy.UserLoginMustBeDomain))
} }
if link != nil { for _, link := range links {
event, err := c.addUserIDPLink(ctx, userAgg, link) event, err := c.addUserIDPLink(ctx, userAgg, link)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@ -484,7 +488,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.
events = append(events, event) events = append(events, event)
} }
if human.IsInitialState(passwordless, link != nil) { if human.IsInitialState(passwordless, len(links) > 0) {
initCode, err := domain.NewInitUserCode(initCodeGenerator) initCode, err := domain.NewInitUserCode(initCodeGenerator)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err

View File

@ -703,6 +703,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
orgID string orgID string
human *domain.Human human *domain.Human
passwordless bool passwordless bool
links []*domain.UserIDPLink
secretGenerator crypto.Generator secretGenerator crypto.Generator
passwordlessInitCode 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -1460,7 +1590,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
idGenerator: tt.fields.idGenerator, idGenerator: tt.fields.idGenerator,
userPasswordAlg: tt.fields.userPasswordAlg, 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 { if tt.res.err == nil {
assert.NoError(t, err) 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {

View File

@ -3128,6 +3128,14 @@ message ImportHumanUserRequest {
string value = 1; string value = 1;
string algorithm = 2; 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}]; string user_name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
@ -3138,8 +3146,8 @@ message ImportHumanUserRequest {
HashedPassword hashed_password = 6; HashedPassword hashed_password = 6;
bool password_change_required = 7; bool password_change_required = 7;
bool request_passwordless_registration = 8; bool request_passwordless_registration = 8;
string otp_code = 9; string otp_code = 9;
repeated IDP idps = 10;
} }
message ImportHumanUserResponse { message ImportHumanUserResponse {