mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 08:27:32 +00:00
feat: Identity brokering (#730)
* feat: add/ remove external idps * feat: external idp add /remove * fix: auth proto * fix: handle login * feat: loginpolicy on authrequest * feat: idp providers on login * feat: link external idp * fix: check login policy on check username * feat: add mapping fields for idp config * feat: use user org id if existing * feat: use user org id if existing * feat: register external user * feat: register external user * feat: user linking * feat: user linking * feat: design external login * feat: design external login * fix: tests * fix: regenerate login design * feat: next step test linking process * feat: next step test linking process * feat: cascade remove external idps on user * fix: tests * fix: tests * feat: external idp requsts on users * fix: generate protos * feat: login styles * feat: login styles * fix: link user * fix: register user on specifig org * fix: user linking * fix: register external, linking auto * fix: remove unnecessary request from proto * fix: tests * fix: new oidc package * fix: migration version * fix: policy permissions * Update internal/ui/login/static/i18n/en.yaml Co-authored-by: Livio Amstutz <livio.a@gmail.com> * Update internal/ui/login/static/i18n/en.yaml Co-authored-by: Livio Amstutz <livio.a@gmail.com> * Update internal/ui/login/handler/renderer.go Co-authored-by: Livio Amstutz <livio.a@gmail.com> * Update internal/ui/login/handler/renderer.go Co-authored-by: Livio Amstutz <livio.a@gmail.com> * fix: pr requests * Update internal/ui/login/handler/link_users_handler.go Co-authored-by: Livio Amstutz <livio.a@gmail.com> * fix: pr requests * fix: pr requests * fix: pr requests * fix: login name size * fix: profile image light * fix: colors * fix: pr requests * fix: remove redirect uri validator * fix: remove redirect uri validator Co-authored-by: Livio Amstutz <livio.a@gmail.com>
This commit is contained in:
@@ -190,16 +190,17 @@ func (es *UserEventstore) CreateUser(ctx context.Context, user *usr_model.User,
|
||||
return model.UserToModel(repoUser), nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) PrepareRegisterUser(ctx context.Context, user *usr_model.User, policy *policy_model.PasswordComplexityPolicy, orgIAMPolicy *org_model.OrgIAMPolicy, resourceOwner string) (*model.User, []*es_models.Aggregate, error) {
|
||||
func (es *UserEventstore) PrepareRegisterUser(ctx context.Context, user *usr_model.User, externalIDP *usr_model.ExternalIDP, policy *policy_model.PasswordComplexityPolicy, orgIAMPolicy *org_model.OrgIAMPolicy, resourceOwner string) (*model.User, []*es_models.Aggregate, error) {
|
||||
if user.Human == nil {
|
||||
return nil, nil, caos_errs.ThrowInvalidArgument(nil, "EVENT-ht8Ux", "Errors.User.Invalid")
|
||||
}
|
||||
|
||||
err := user.CheckOrgIAMPolicy(orgIAMPolicy)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
user.SetNamesAsDisplayname()
|
||||
if !user.IsValid() || user.Password == nil || user.SecretString == "" {
|
||||
if !user.IsValid() || externalIDP == nil && (user.Password == nil || user.SecretString == "") {
|
||||
return nil, nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-9dk45", "Errors.User.Invalid")
|
||||
}
|
||||
id, err := es.idGenerator.Next()
|
||||
@@ -207,7 +208,13 @@ func (es *UserEventstore) PrepareRegisterUser(ctx context.Context, user *usr_mod
|
||||
return nil, nil, err
|
||||
}
|
||||
user.AggregateID = id
|
||||
|
||||
if externalIDP != nil {
|
||||
externalIDP.AggregateID = id
|
||||
if !externalIDP.IsValid() {
|
||||
return nil, nil, errors.ThrowPreconditionFailed(nil, "EVENT-4Dj9s", "Errors.User.ExternalIDP.Invalid")
|
||||
}
|
||||
user.ExternalIDPs = append(user.ExternalIDPs, externalIDP)
|
||||
}
|
||||
err = user.HashPasswordIfExisting(policy, es.PasswordAlg, false)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -218,14 +225,15 @@ func (es *UserEventstore) PrepareRegisterUser(ctx context.Context, user *usr_mod
|
||||
}
|
||||
|
||||
repoUser := model.UserFromModel(user)
|
||||
repoExternalIDP := model.ExternalIDPFromModel(externalIDP)
|
||||
repoInitCode := model.InitCodeFromModel(user.InitCode)
|
||||
|
||||
aggregates, err := UserRegisterAggregate(ctx, es.AggregateCreator(), repoUser, resourceOwner, repoInitCode, orgIAMPolicy.UserLoginMustBeDomain)
|
||||
aggregates, err := UserRegisterAggregate(ctx, es.AggregateCreator(), repoUser, repoExternalIDP, resourceOwner, repoInitCode, orgIAMPolicy.UserLoginMustBeDomain)
|
||||
return repoUser, aggregates, err
|
||||
}
|
||||
|
||||
func (es *UserEventstore) RegisterUser(ctx context.Context, user *usr_model.User, pwPolicy *policy_model.PasswordComplexityPolicy, orgIAMPolicy *org_model.OrgIAMPolicy, resourceOwner string) (*usr_model.User, error) {
|
||||
repoUser, createAggregates, err := es.PrepareRegisterUser(ctx, user, pwPolicy, orgIAMPolicy, resourceOwner)
|
||||
repoUser, createAggregates, err := es.PrepareRegisterUser(ctx, user, nil, pwPolicy, orgIAMPolicy, resourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -691,6 +699,103 @@ func (es *UserEventstore) PasswordCodeSent(ctx context.Context, userID string) e
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) AddExternalIDP(ctx context.Context, externalIDP *usr_model.ExternalIDP) (*usr_model.ExternalIDP, error) {
|
||||
if externalIDP == nil || !externalIDP.IsValid() {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-Ek9s", "Errors.User.ExternalIDP.Invalid")
|
||||
}
|
||||
existingUser, err := es.UserByID(ctx, externalIDP.AggregateID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existingUser.Human == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-Cnk8s", "Errors.User.NotHuman")
|
||||
}
|
||||
repoUser := model.UserFromModel(existingUser)
|
||||
repoExternalIDP := model.ExternalIDPFromModel(externalIDP)
|
||||
aggregates, err := ExternalIDPAddedAggregate(ctx, es.Eventstore.AggregateCreator(), repoUser, repoExternalIDP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = es_sdk.PushAggregates(ctx, es.PushAggregates, repoUser.AppendEvents, aggregates...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
es.userCache.cacheUser(repoUser)
|
||||
if _, idp := model.GetExternalIDP(repoUser.ExternalIDPs, externalIDP.UserID); idp != nil {
|
||||
return model.ExternalIDPToModel(idp), nil
|
||||
}
|
||||
return nil, errors.ThrowInternal(nil, "EVENT-Msi9d", "Errors.Internal")
|
||||
}
|
||||
|
||||
func (es *UserEventstore) BulkAddExternalIDPs(ctx context.Context, userID string, externalIDPs []*usr_model.ExternalIDP) error {
|
||||
if externalIDPs == nil || len(externalIDPs) == 0 {
|
||||
return errors.ThrowPreconditionFailed(nil, "EVENT-Ek9s", "Errors.User.ExternalIDP.MinimumExternalIDPNeeded")
|
||||
}
|
||||
for _, externalIDP := range externalIDPs {
|
||||
if !externalIDP.IsValid() {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-idue3", "Errors.User.ExternalIDP.Invalid")
|
||||
}
|
||||
}
|
||||
existingUser, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existingUser.Human == nil {
|
||||
return errors.ThrowPreconditionFailed(nil, "EVENT-Cnk8s", "Errors.User.NotHuman")
|
||||
}
|
||||
repoUser := model.UserFromModel(existingUser)
|
||||
repoExternalIDPs := model.ExternalIDPsFromModel(externalIDPs)
|
||||
aggregates, err := ExternalIDPAddedAggregate(ctx, es.Eventstore.AggregateCreator(), repoUser, repoExternalIDPs...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = es_sdk.PushAggregates(ctx, es.PushAggregates, repoUser.AppendEvents, aggregates...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
es.userCache.cacheUser(repoUser)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) PrepareRemoveExternalIDP(ctx context.Context, externalIDP *usr_model.ExternalIDP, cascade bool) (*model.User, []*es_models.Aggregate, error) {
|
||||
if externalIDP == nil || !externalIDP.IsValid() {
|
||||
return nil, nil, errors.ThrowPreconditionFailed(nil, "EVENT-Cm8sj", "Errors.User.ExternalIDP.Invalid")
|
||||
}
|
||||
existingUser, err := es.UserByID(ctx, externalIDP.AggregateID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if existingUser.Human == nil {
|
||||
return nil, nil, errors.ThrowPreconditionFailed(nil, "EVENT-E8iod", "Errors.User.NotHuman")
|
||||
}
|
||||
_, existingIDP := existingUser.GetExternalIDP(externalIDP)
|
||||
if existingIDP == nil {
|
||||
return nil, nil, errors.ThrowPreconditionFailed(nil, "EVENT-3Dh7s", "Errors.User.ExternalIDP.NotOnUser")
|
||||
}
|
||||
repoUser := model.UserFromModel(existingUser)
|
||||
repoExternalIDP := model.ExternalIDPFromModel(externalIDP)
|
||||
agg, err := ExternalIDPRemovedAggregate(ctx, es.Eventstore.AggregateCreator(), repoUser, repoExternalIDP, cascade)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return repoUser, agg, err
|
||||
}
|
||||
|
||||
func (es *UserEventstore) RemoveExternalIDP(ctx context.Context, externalIDP *usr_model.ExternalIDP) error {
|
||||
repoUser, aggregates, err := es.PrepareRemoveExternalIDP(ctx, externalIDP, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = es_sdk.PushAggregates(ctx, es.PushAggregates, repoUser.AppendEvents, aggregates...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
es.userCache.cacheUser(repoUser)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) ProfileByID(ctx context.Context, userID string) (*usr_model.Profile, error) {
|
||||
if userID == "" {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-di834", "Errors.User.UserIDMissing")
|
||||
|
@@ -458,6 +458,30 @@ func GetMockManipulateUserWithOTP(ctrl *gomock.Controller, decrypt, verified boo
|
||||
return es
|
||||
}
|
||||
|
||||
func GetMockManipulateUserWithExternalIDP(ctrl *gomock.Controller) *UserEventstore {
|
||||
user := model.Human{
|
||||
Profile: &model.Profile{
|
||||
DisplayName: "DisplayName",
|
||||
},
|
||||
}
|
||||
externalIDP := model.ExternalIDP{
|
||||
IDPConfigID: "IDPConfigID",
|
||||
UserID: "UserID",
|
||||
DisplayName: "DisplayName",
|
||||
}
|
||||
dataUser, _ := json.Marshal(user)
|
||||
dataIDP, _ := json.Marshal(externalIDP)
|
||||
events := []*es_models.Event{
|
||||
{AggregateID: "AggregateID", AggregateVersion: "v1", Sequence: 1, Type: model.UserAdded, Data: dataUser},
|
||||
{AggregateID: "AggregateID", AggregateVersion: "v1", Sequence: 1, Type: model.HumanExternalIDPAdded, Data: dataIDP},
|
||||
}
|
||||
mockEs := mock.NewMockEventstore(ctrl)
|
||||
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
|
||||
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
|
||||
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
|
||||
return GetMockedEventstore(ctrl, mockEs)
|
||||
}
|
||||
|
||||
func GetMockManipulateUserNoEvents(ctrl *gomock.Controller) *UserEventstore {
|
||||
events := []*es_models.Event{}
|
||||
mockEs := mock.NewMockEventstore(ctrl)
|
||||
|
@@ -1967,6 +1967,191 @@ func TestPasswordCodeSent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddExternalIDP(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
type args struct {
|
||||
es *UserEventstore
|
||||
ctx context.Context
|
||||
externalIDP *model.ExternalIDP
|
||||
}
|
||||
type res struct {
|
||||
errFunc func(err error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "add ok",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: authz.NewMockContext("orgID", "userID"),
|
||||
externalIDP: &model.ExternalIDP{
|
||||
ObjectRoot: es_models.ObjectRoot{
|
||||
AggregateID: "AggregateID",
|
||||
},
|
||||
IDPConfigID: "IDPConfigID",
|
||||
UserID: "UserID",
|
||||
DisplayName: "DisplayName",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid idp",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: authz.NewMockContext("orgID", "userID"),
|
||||
externalIDP: &model.ExternalIDP{
|
||||
ObjectRoot: es_models.ObjectRoot{
|
||||
AggregateID: "AggregateID",
|
||||
},
|
||||
UserID: "UserID",
|
||||
DisplayName: "DisplayName",
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "existing user not found",
|
||||
args: args{
|
||||
es: GetMockManipulateUserNoEvents(ctrl),
|
||||
ctx: authz.NewMockContext("orgID", "userID"),
|
||||
externalIDP: &model.ExternalIDP{
|
||||
ObjectRoot: es_models.ObjectRoot{
|
||||
AggregateID: "AggregateID",
|
||||
},
|
||||
IDPConfigID: "IDPConfigID",
|
||||
UserID: "UserID",
|
||||
DisplayName: "DisplayName",
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsNotFound,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := tt.args.es.AddExternalIDP(tt.args.ctx, tt.args.externalIDP)
|
||||
|
||||
if tt.res.errFunc == nil && result.AggregateID == "" {
|
||||
t.Errorf("result has no id")
|
||||
}
|
||||
if tt.res.errFunc == nil && result.IDPConfigID == "" {
|
||||
t.Errorf("result has no idpconfig")
|
||||
}
|
||||
if tt.res.errFunc == nil && result.UserID == "" {
|
||||
t.Errorf("result has no UserID")
|
||||
}
|
||||
if tt.res.errFunc == nil && result == nil {
|
||||
t.Errorf("got wrong result change required: actual: %v ", result)
|
||||
}
|
||||
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveExternalIDP(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
type args struct {
|
||||
es *UserEventstore
|
||||
ctx context.Context
|
||||
externalIDP *model.ExternalIDP
|
||||
}
|
||||
type res struct {
|
||||
errFunc func(err error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "remove ok",
|
||||
args: args{
|
||||
es: GetMockManipulateUserWithExternalIDP(ctrl),
|
||||
ctx: authz.NewMockContext("orgID", "userID"),
|
||||
externalIDP: &model.ExternalIDP{
|
||||
ObjectRoot: es_models.ObjectRoot{
|
||||
AggregateID: "AggregateID",
|
||||
},
|
||||
IDPConfigID: "IDPConfigID",
|
||||
UserID: "UserID",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid idp",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: authz.NewMockContext("orgID", "userID"),
|
||||
externalIDP: &model.ExternalIDP{
|
||||
ObjectRoot: es_models.ObjectRoot{
|
||||
AggregateID: "AggregateID",
|
||||
},
|
||||
UserID: "UserID",
|
||||
DisplayName: "DisplayName",
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remove external idp not existing",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: authz.NewMockContext("orgID", "userID"),
|
||||
externalIDP: &model.ExternalIDP{
|
||||
ObjectRoot: es_models.ObjectRoot{
|
||||
AggregateID: "AggregateID",
|
||||
},
|
||||
IDPConfigID: "IDPConfigID",
|
||||
UserID: "UserID",
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "existing user not found",
|
||||
args: args{
|
||||
es: GetMockManipulateUserNoEvents(ctrl),
|
||||
ctx: authz.NewMockContext("orgID", "userID"),
|
||||
externalIDP: &model.ExternalIDP{
|
||||
ObjectRoot: es_models.ObjectRoot{
|
||||
AggregateID: "AggregateID",
|
||||
},
|
||||
IDPConfigID: "IDPConfigID",
|
||||
UserID: "UserID",
|
||||
DisplayName: "DisplayName",
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsNotFound,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.args.es.RemoveExternalIDP(tt.args.ctx, tt.args.externalIDP)
|
||||
|
||||
if tt.res.errFunc == nil && err != nil {
|
||||
t.Errorf("should not get err, %v", err)
|
||||
}
|
||||
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileByID(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
type args struct {
|
||||
|
96
internal/user/repository/eventsourcing/model/external_idp.go
Normal file
96
internal/user/repository/eventsourcing/model/external_idp.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/caos/logging"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/user/model"
|
||||
)
|
||||
|
||||
type ExternalIDP struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
IDPConfigID string `json:"idpConfigId,omitempty"`
|
||||
UserID string `json:"userId,omitempty"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
}
|
||||
|
||||
func GetExternalIDP(idps []*ExternalIDP, id string) (int, *ExternalIDP) {
|
||||
for i, idp := range idps {
|
||||
if idp.UserID == id {
|
||||
return i, idp
|
||||
}
|
||||
}
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
func ExternalIDPsToModel(externalIDPs []*ExternalIDP) []*model.ExternalIDP {
|
||||
convertedIDPs := make([]*model.ExternalIDP, len(externalIDPs))
|
||||
for i, m := range externalIDPs {
|
||||
convertedIDPs[i] = ExternalIDPToModel(m)
|
||||
}
|
||||
return convertedIDPs
|
||||
}
|
||||
|
||||
func ExternalIDPsFromModel(externalIDPs []*model.ExternalIDP) []*ExternalIDP {
|
||||
convertedIDPs := make([]*ExternalIDP, len(externalIDPs))
|
||||
for i, m := range externalIDPs {
|
||||
convertedIDPs[i] = ExternalIDPFromModel(m)
|
||||
}
|
||||
return convertedIDPs
|
||||
}
|
||||
|
||||
func ExternalIDPFromModel(idp *model.ExternalIDP) *ExternalIDP {
|
||||
if idp == nil {
|
||||
return nil
|
||||
}
|
||||
return &ExternalIDP{
|
||||
ObjectRoot: idp.ObjectRoot,
|
||||
IDPConfigID: idp.IDPConfigID,
|
||||
UserID: idp.UserID,
|
||||
DisplayName: idp.DisplayName,
|
||||
}
|
||||
}
|
||||
|
||||
func ExternalIDPToModel(idp *ExternalIDP) *model.ExternalIDP {
|
||||
return &model.ExternalIDP{
|
||||
ObjectRoot: idp.ObjectRoot,
|
||||
IDPConfigID: idp.IDPConfigID,
|
||||
UserID: idp.UserID,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *Human) appendExternalIDPAddedEvent(event *es_models.Event) error {
|
||||
idp := new(ExternalIDP)
|
||||
err := idp.setData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
idp.ObjectRoot.CreationDate = event.CreationDate
|
||||
u.ExternalIDPs = append(u.ExternalIDPs, idp)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Human) appendExternalIDPRemovedEvent(event *es_models.Event) error {
|
||||
idp := new(ExternalIDP)
|
||||
err := idp.setData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if i, externalIdp := GetExternalIDP(u.ExternalIDPs, idp.UserID); externalIdp != nil {
|
||||
u.ExternalIDPs[i] = u.ExternalIDPs[len(u.ExternalIDPs)-1]
|
||||
u.ExternalIDPs[len(u.ExternalIDPs)-1] = nil
|
||||
u.ExternalIDPs = u.ExternalIDPs[:len(u.ExternalIDPs)-1]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pw *ExternalIDP) setData(event *es_models.Event) error {
|
||||
pw.ObjectRoot.AppendEvent(event)
|
||||
if err := json.Unmarshal(event.Data, pw); err != nil {
|
||||
logging.Log("EVEN-Msi9d").WithError(err).Error("could not unmarshal event data")
|
||||
return caos_errs.ThrowInternal(err, "MODEL-A9osf", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -0,0 +1,89 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAppendExternalIDPAddedEvent(t *testing.T) {
|
||||
type args struct {
|
||||
user *Human
|
||||
externalIDP *ExternalIDP
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *Human
|
||||
}{
|
||||
{
|
||||
name: "append external idp added event",
|
||||
args: args{
|
||||
user: &Human{},
|
||||
externalIDP: &ExternalIDP{IDPConfigID: "IDPConfigID", UserID: "UserID", DisplayName: "DisplayName"},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &Human{ExternalIDPs: []*ExternalIDP{{IDPConfigID: "IDPConfigID", UserID: "UserID", DisplayName: "DisplayName"}}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.args.externalIDP != nil {
|
||||
data, _ := json.Marshal(tt.args.externalIDP)
|
||||
tt.args.event.Data = data
|
||||
}
|
||||
tt.args.user.appendExternalIDPAddedEvent(tt.args.event)
|
||||
if len(tt.args.user.ExternalIDPs) == 0 {
|
||||
t.Error("got wrong result expected external idps on user ")
|
||||
}
|
||||
if tt.args.user.ExternalIDPs[0].UserID != tt.result.ExternalIDPs[0].UserID {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.ExternalIDPs[0].UserID, tt.args.user.ExternalIDPs[0].UserID)
|
||||
}
|
||||
if tt.args.user.ExternalIDPs[0].IDPConfigID != tt.result.ExternalIDPs[0].IDPConfigID {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.ExternalIDPs[0].IDPConfigID, tt.args.user.ExternalIDPs[0].IDPConfigID)
|
||||
}
|
||||
if tt.args.user.ExternalIDPs[0].DisplayName != tt.result.ExternalIDPs[0].DisplayName {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.ExternalIDPs[0].DisplayName, tt.args.user.ExternalIDPs[0].IDPConfigID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendExternalIDPRemovedEvent(t *testing.T) {
|
||||
type args struct {
|
||||
user *Human
|
||||
externalIDP *ExternalIDP
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *Human
|
||||
}{
|
||||
{
|
||||
name: "append external idp removed event",
|
||||
args: args{
|
||||
user: &Human{
|
||||
ExternalIDPs: []*ExternalIDP{
|
||||
{IDPConfigID: "IDPConfigID", UserID: "UserID", DisplayName: "DisplayName"},
|
||||
}},
|
||||
externalIDP: &ExternalIDP{UserID: "UserID"},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &Human{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.args.externalIDP != nil {
|
||||
data, _ := json.Marshal(tt.args.externalIDP)
|
||||
tt.args.event.Data = data
|
||||
}
|
||||
tt.args.user.appendExternalIDPRemovedEvent(tt.args.event)
|
||||
if len(tt.args.user.ExternalIDPs) != 0 {
|
||||
t.Error("got wrong result expected 0 external idps on user ")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -4,8 +4,9 @@ import "github.com/caos/zitadel/internal/eventstore/models"
|
||||
|
||||
//aggregates
|
||||
const (
|
||||
UserAggregate models.AggregateType = "user"
|
||||
UserUserNameAggregate models.AggregateType = "user.username"
|
||||
UserAggregate models.AggregateType = "user"
|
||||
UserUserNameAggregate models.AggregateType = "user.username"
|
||||
UserExternalIDPAggregate models.AggregateType = "user.human.externalidp"
|
||||
)
|
||||
|
||||
// the following consts are for user v1 events
|
||||
@@ -83,6 +84,13 @@ const (
|
||||
HumanPasswordCheckSucceeded models.EventType = "user.human.password.check.succeeded"
|
||||
HumanPasswordCheckFailed models.EventType = "user.human.password.check.failed"
|
||||
|
||||
HumanExternalIDPReserved models.EventType = "user.human.externalidp.reserved"
|
||||
HumanExternalIDPReleased models.EventType = "user.human.externalidp.released"
|
||||
|
||||
HumanExternalIDPAdded models.EventType = "user.human.externalidp.added"
|
||||
HumanExternalIDPRemoved models.EventType = "user.human.externalidp.removed"
|
||||
HumanExternalIDPCascadeRemoved models.EventType = "user.human.externalidp.cascade.removed"
|
||||
|
||||
HumanEmailChanged models.EventType = "user.human.email.changed"
|
||||
HumanEmailVerified models.EventType = "user.human.email.verified"
|
||||
HumanEmailVerificationFailed models.EventType = "user.human.email.verification.failed"
|
||||
|
@@ -19,11 +19,12 @@ type Human struct {
|
||||
*Email
|
||||
*Phone
|
||||
*Address
|
||||
InitCode *InitUserCode `json:"-"`
|
||||
EmailCode *EmailCode `json:"-"`
|
||||
PhoneCode *PhoneCode `json:"-"`
|
||||
PasswordCode *PasswordCode `json:"-"`
|
||||
OTP *OTP `json:"-"`
|
||||
ExternalIDPs []*ExternalIDP `json:"-"`
|
||||
InitCode *InitUserCode `json:"-"`
|
||||
EmailCode *EmailCode `json:"-"`
|
||||
PhoneCode *PhoneCode `json:"-"`
|
||||
PasswordCode *PasswordCode `json:"-"`
|
||||
OTP *OTP `json:"-"`
|
||||
}
|
||||
|
||||
type InitUserCode struct {
|
||||
@@ -52,6 +53,9 @@ func HumanFromModel(user *model.Human) *Human {
|
||||
if user.OTP != nil {
|
||||
human.OTP = OTPFromModel(user.OTP)
|
||||
}
|
||||
if user.ExternalIDPs != nil {
|
||||
human.ExternalIDPs = ExternalIDPsFromModel(user.ExternalIDPs)
|
||||
}
|
||||
return human
|
||||
}
|
||||
|
||||
@@ -72,6 +76,9 @@ func HumanToModel(user *Human) *model.Human {
|
||||
if user.Address != nil {
|
||||
human.Address = AddressToModel(user.Address)
|
||||
}
|
||||
if user.ExternalIDPs != nil {
|
||||
human.ExternalIDPs = ExternalIDPsToModel(user.ExternalIDPs)
|
||||
}
|
||||
if user.InitCode != nil {
|
||||
human.InitCode = InitCodeToModel(user.InitCode)
|
||||
}
|
||||
@@ -169,6 +176,10 @@ func (h *Human) AppendEvent(event *es_models.Event) (err error) {
|
||||
case MFAOTPRemoved,
|
||||
HumanMFAOTPRemoved:
|
||||
h.appendOTPRemovedEvent()
|
||||
case HumanExternalIDPAdded:
|
||||
err = h.appendExternalIDPAddedEvent(event)
|
||||
case HumanExternalIDPRemoved, HumanExternalIDPCascadeRemoved:
|
||||
err = h.appendExternalIDPRemovedEvent(event)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
|
@@ -2,9 +2,10 @@ package eventsourcing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/caos/zitadel/internal/api/authz"
|
||||
iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model"
|
||||
"strings"
|
||||
|
||||
"github.com/caos/zitadel/internal/api/authz"
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
es_sdk "github.com/caos/zitadel/internal/eventstore/sdk"
|
||||
@@ -34,6 +35,14 @@ func UserUserNameUniqueQuery(userName string) *es_models.SearchQuery {
|
||||
SetLimit(1)
|
||||
}
|
||||
|
||||
func UserExternalIDPUniqueQuery(externalIDPUserID string) *es_models.SearchQuery {
|
||||
return es_models.NewSearchQuery().
|
||||
AggregateTypeFilter(model.UserExternalIDPAggregate).
|
||||
AggregateIDFilter(externalIDPUserID).
|
||||
OrderDesc().
|
||||
SetLimit(1)
|
||||
}
|
||||
|
||||
func UserAggregate(ctx context.Context, aggCreator *es_models.AggregateCreator, user *model.User) (*es_models.Aggregate, error) {
|
||||
if user == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-dis83", "Errors.Internal")
|
||||
@@ -148,8 +157,8 @@ func HumanCreateAggregate(ctx context.Context, aggCreator *es_models.AggregateCr
|
||||
return append(uniqueAggregates, agg), nil
|
||||
}
|
||||
|
||||
func UserRegisterAggregate(ctx context.Context, aggCreator *es_models.AggregateCreator, user *model.User, resourceOwner string, initCode *model.InitUserCode, userLoginMustBeDomain bool) ([]*es_models.Aggregate, error) {
|
||||
if user == nil || resourceOwner == "" || initCode == nil {
|
||||
func UserRegisterAggregate(ctx context.Context, aggCreator *es_models.AggregateCreator, user *model.User, externalIDP *model.ExternalIDP, resourceOwner string, initCode *model.InitUserCode, userLoginMustBeDomain bool) ([]*es_models.Aggregate, error) {
|
||||
if user == nil || resourceOwner == "" {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-duxk2", "user, resourceowner, initcode must be set")
|
||||
}
|
||||
|
||||
@@ -157,32 +166,62 @@ func UserRegisterAggregate(ctx context.Context, aggCreator *es_models.AggregateC
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-ekuEA", "user must be type human")
|
||||
}
|
||||
|
||||
aggregates := make([]*es_models.Aggregate, 0)
|
||||
agg, err := UserAggregateOverwriteContext(ctx, aggCreator, user, resourceOwner, user.AggregateID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !userLoginMustBeDomain {
|
||||
validationQuery := es_models.NewSearchQuery().
|
||||
AggregateTypeFilter(org_es_model.OrgAggregate).
|
||||
AggregateIDsFilter()
|
||||
|
||||
validation := addUserNameValidation(user.UserName)
|
||||
agg.SetPrecondition(validationQuery, validation)
|
||||
}
|
||||
agg, err = agg.AppendEvent(model.HumanRegistered, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
agg, err = agg.AppendEvent(model.InitializedHumanCodeAdded, initCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if initCode != nil {
|
||||
agg, err = agg.AppendEvent(model.InitializedHumanCodeAdded, initCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if user.Email != nil && user.EmailAddress != "" && user.IsEmailVerified {
|
||||
agg, err = agg.AppendEvent(model.HumanEmailVerified, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if externalIDP != nil {
|
||||
validationQuery := es_models.NewSearchQuery().
|
||||
AggregateTypeFilter(org_es_model.OrgAggregate, iam_es_model.IAMAggregate).
|
||||
AggregateIDsFilter()
|
||||
|
||||
if !userLoginMustBeDomain {
|
||||
validation := addUserNameAndIDPConfigExistingValidation(user.UserName, externalIDP)
|
||||
agg.SetPrecondition(validationQuery, validation)
|
||||
} else {
|
||||
validation := addIDPConfigExistingValidation(externalIDP)
|
||||
agg.SetPrecondition(validationQuery, validation)
|
||||
}
|
||||
|
||||
agg, err = agg.AppendEvent(model.HumanExternalIDPAdded, externalIDP)
|
||||
uniqueExternalIDPAggregate, err := reservedUniqueExternalIDPAggregate(ctx, aggCreator, resourceOwner, externalIDP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aggregates = append(aggregates, uniqueExternalIDPAggregate)
|
||||
} else if !userLoginMustBeDomain {
|
||||
validationQuery := es_models.NewSearchQuery().
|
||||
AggregateTypeFilter(org_es_model.OrgAggregate).
|
||||
AggregateIDsFilter()
|
||||
validation := addUserNameValidation(user.UserName)
|
||||
agg.SetPrecondition(validationQuery, validation)
|
||||
}
|
||||
|
||||
uniqueAggregates, err := getUniqueUserAggregates(ctx, aggCreator, user.UserName, user.EmailAddress, resourceOwner, userLoginMustBeDomain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(uniqueAggregates, agg), nil
|
||||
aggregates = append(aggregates, uniqueAggregates...)
|
||||
return append(aggregates, agg), nil
|
||||
}
|
||||
|
||||
func getUniqueUserAggregates(ctx context.Context, aggCreator *es_models.AggregateCreator, userName, emailAddress, resourceOwner string, userLoginMustBeDomain bool) ([]*es_models.Aggregate, error) {
|
||||
@@ -726,6 +765,86 @@ func DomainClaimedSentAggregate(aggCreator *es_models.AggregateCreator, user *mo
|
||||
}
|
||||
}
|
||||
|
||||
func ExternalIDPAddedAggregate(ctx context.Context, aggCreator *es_models.AggregateCreator, user *model.User, externalIDPs ...*model.ExternalIDP) ([]*es_models.Aggregate, error) {
|
||||
if externalIDPs == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-Di9os", "Errors.Internal")
|
||||
}
|
||||
aggregates := make([]*es_models.Aggregate, 0)
|
||||
agg, err := UserAggregate(ctx, aggCreator, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
validationQuery := es_models.NewSearchQuery().
|
||||
AggregateTypeFilter(org_es_model.OrgAggregate, iam_es_model.IAMAggregate).
|
||||
AggregateIDsFilter()
|
||||
|
||||
validation := addIDPConfigExistingValidation(externalIDPs...)
|
||||
agg.SetPrecondition(validationQuery, validation)
|
||||
for _, externalIDP := range externalIDPs {
|
||||
agg, err = agg.AppendEvent(model.HumanExternalIDPAdded, externalIDP)
|
||||
uniqueAggregate, err := reservedUniqueExternalIDPAggregate(ctx, aggCreator, "", externalIDP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aggregates = append(aggregates, uniqueAggregate)
|
||||
}
|
||||
return append(aggregates, agg), nil
|
||||
}
|
||||
|
||||
func ExternalIDPRemovedAggregate(ctx context.Context, aggCreator *es_models.AggregateCreator, user *model.User, externalIDP *model.ExternalIDP, cascade bool) ([]*es_models.Aggregate, error) {
|
||||
if externalIDP == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-Mlo0s", "Errors.Internal")
|
||||
}
|
||||
|
||||
aggregates := make([]*es_models.Aggregate, 0)
|
||||
agg, err := UserAggregate(ctx, aggCreator, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cascade {
|
||||
agg, err = agg.AppendEvent(model.HumanExternalIDPCascadeRemoved, externalIDP)
|
||||
} else {
|
||||
agg, err = agg.AppendEvent(model.HumanExternalIDPRemoved, externalIDP)
|
||||
}
|
||||
uniqueReleasedAggregate, err := releasedUniqueExternalIDPAggregate(ctx, aggCreator, externalIDP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aggregates = append(aggregates, uniqueReleasedAggregate)
|
||||
return append(aggregates, agg), nil
|
||||
}
|
||||
|
||||
func reservedUniqueExternalIDPAggregate(ctx context.Context, aggCreator *es_models.AggregateCreator, resourceOwner string, externalIDP *model.ExternalIDP) (*es_models.Aggregate, error) {
|
||||
uniqueExternlIDP := externalIDP.IDPConfigID + externalIDP.UserID
|
||||
aggregate, err := aggCreator.NewAggregate(ctx, uniqueExternlIDP, model.UserExternalIDPAggregate, model.UserVersion, 0)
|
||||
if resourceOwner != "" {
|
||||
aggregate, err = aggCreator.NewAggregate(ctx, uniqueExternlIDP, model.UserExternalIDPAggregate, model.UserVersion, 0, es_models.OverwriteResourceOwner(resourceOwner))
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aggregate, err = aggregate.AppendEvent(model.HumanExternalIDPReserved, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return aggregate.SetPrecondition(UserExternalIDPUniqueQuery(uniqueExternlIDP), isEventValidation(aggregate, model.HumanExternalIDPReserved)), nil
|
||||
}
|
||||
|
||||
func releasedUniqueExternalIDPAggregate(ctx context.Context, aggCreator *es_models.AggregateCreator, externalIDP *model.ExternalIDP) (aggregate *es_models.Aggregate, err error) {
|
||||
uniqueExternlIDP := externalIDP.IDPConfigID + externalIDP.UserID
|
||||
aggregate, err = aggCreator.NewAggregate(ctx, uniqueExternlIDP, model.UserExternalIDPAggregate, model.UserVersion, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aggregate, err = aggregate.AppendEvent(model.HumanExternalIDPReleased, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return aggregate.SetPrecondition(UserExternalIDPUniqueQuery(uniqueExternlIDP), isEventValidation(aggregate, model.HumanExternalIDPReleased)), nil
|
||||
}
|
||||
|
||||
func UsernameChangedAggregates(ctx context.Context, aggCreator *es_models.AggregateCreator, user *model.User, oldUsername string, userLoginMustBeDomain bool) ([]*es_models.Aggregate, error) {
|
||||
aggregates, err := changeUniqueUserNameAggregate(ctx, aggCreator, user.ResourceOwner, oldUsername, user.UserName, userLoginMustBeDomain)
|
||||
if err != nil {
|
||||
@@ -768,41 +887,178 @@ func addUserNameValidation(userName string) func(...*es_models.Event) error {
|
||||
return func(events ...*es_models.Event) error {
|
||||
domains := make([]*org_es_model.OrgDomain, 0)
|
||||
for _, event := range events {
|
||||
switch event.Type {
|
||||
case org_es_model.OrgDomainAdded:
|
||||
domain := new(org_es_model.OrgDomain)
|
||||
domain.SetData(event)
|
||||
domains = append(domains, domain)
|
||||
case org_es_model.OrgDomainVerified:
|
||||
domain := new(org_es_model.OrgDomain)
|
||||
domain.SetData(event)
|
||||
for _, d := range domains {
|
||||
if d.Domain == domain.Domain {
|
||||
d.Verified = true
|
||||
}
|
||||
}
|
||||
case org_es_model.OrgDomainRemoved:
|
||||
domain := new(org_es_model.OrgDomain)
|
||||
domain.SetData(event)
|
||||
for i, d := range domains {
|
||||
if d.Domain == domain.Domain {
|
||||
domains[i] = domains[len(domains)-1]
|
||||
domains[len(domains)-1] = nil
|
||||
domains = domains[:len(domains)-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
domains = handleDomainEvents(domains, event)
|
||||
}
|
||||
return handleCheckDomainAllowedAsUsername(domains, userName)
|
||||
}
|
||||
}
|
||||
|
||||
func handleDomainEvents(domains []*org_es_model.OrgDomain, event *es_models.Event) []*org_es_model.OrgDomain {
|
||||
switch event.Type {
|
||||
case org_es_model.OrgDomainAdded:
|
||||
domain := new(org_es_model.OrgDomain)
|
||||
domain.SetData(event)
|
||||
domains = append(domains, domain)
|
||||
case org_es_model.OrgDomainVerified:
|
||||
domain := new(org_es_model.OrgDomain)
|
||||
domain.SetData(event)
|
||||
for _, d := range domains {
|
||||
if d.Domain == domain.Domain {
|
||||
d.Verified = true
|
||||
}
|
||||
}
|
||||
case org_es_model.OrgDomainRemoved:
|
||||
domain := new(org_es_model.OrgDomain)
|
||||
domain.SetData(event)
|
||||
for i, d := range domains {
|
||||
if d.Domain == domain.Domain {
|
||||
domains[i] = domains[len(domains)-1]
|
||||
domains[len(domains)-1] = nil
|
||||
domains = domains[:len(domains)-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return domains
|
||||
}
|
||||
|
||||
func handleCheckDomainAllowedAsUsername(domains []*org_es_model.OrgDomain, userName string) error {
|
||||
split := strings.Split(userName, "@")
|
||||
if len(split) != 2 {
|
||||
return nil
|
||||
}
|
||||
for _, d := range domains {
|
||||
if d.Verified && d.Domain == split[1] {
|
||||
return errors.ThrowPreconditionFailed(nil, "EVENT-us5Zw", "Errors.User.DomainNotAllowedAsUsername")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addIDPConfigExistingValidation(externalIDPs ...*model.ExternalIDP) func(...*es_models.Event) error {
|
||||
return func(events ...*es_models.Event) error {
|
||||
iamLoginPolicy := new(iam_es_model.LoginPolicy)
|
||||
orgPolicyExisting := false
|
||||
orgLoginPolicy := new(iam_es_model.LoginPolicy)
|
||||
for _, event := range events {
|
||||
switch event.AggregateType {
|
||||
case org_es_model.OrgAggregate:
|
||||
orgPolicyExisting = handleOrgLoginPolicy(event, orgPolicyExisting, orgLoginPolicy)
|
||||
case iam_es_model.IAMAggregate:
|
||||
handleIAMLoginPolicy(event, iamLoginPolicy)
|
||||
}
|
||||
}
|
||||
return handleIDPConfigExisting(iamLoginPolicy, orgLoginPolicy, orgPolicyExisting, externalIDPs...)
|
||||
}
|
||||
}
|
||||
|
||||
func handleIDPConfigExisting(iamLoginPolicy, orgLoginPolicy *iam_es_model.LoginPolicy, orgPolicyExisting bool, externalIDPs ...*model.ExternalIDP) error {
|
||||
if orgPolicyExisting {
|
||||
if !orgLoginPolicy.AllowExternalIdp {
|
||||
return errors.ThrowPreconditionFailed(nil, "EVENT-Wmi9s", "Errors.User.ExternalIDP.NotAllowed")
|
||||
}
|
||||
for _, externalIDP := range externalIDPs {
|
||||
existing := false
|
||||
for _, provider := range orgLoginPolicy.IDPProviders {
|
||||
if provider.IDPConfigID == externalIDP.IDPConfigID {
|
||||
existing = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !existing {
|
||||
return errors.ThrowPreconditionFailed(nil, "EVENT-Ms9it", "Errors.User.ExternalIDP.IDPConfigNotExisting")
|
||||
}
|
||||
}
|
||||
}
|
||||
if !iamLoginPolicy.AllowExternalIdp {
|
||||
return errors.ThrowPreconditionFailed(nil, "EVENT-Ns7uf", "Errors.User.ExternalIDP.NotAllowed")
|
||||
}
|
||||
for _, externalIDP := range externalIDPs {
|
||||
existing := false
|
||||
for _, provider := range iamLoginPolicy.IDPProviders {
|
||||
if provider.IDPConfigID == externalIDP.IDPConfigID {
|
||||
existing = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !existing {
|
||||
return errors.ThrowPreconditionFailed(nil, "EVENT-Ms9it", "Errors.User.ExternalIDP.IDPConfigNotExisting")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addUserNameAndIDPConfigExistingValidation(userName string, externalIDP *model.ExternalIDP) func(...*es_models.Event) error {
|
||||
return func(events ...*es_models.Event) error {
|
||||
domains := make([]*org_es_model.OrgDomain, 0)
|
||||
iamLoginPolicy := new(iam_es_model.LoginPolicy)
|
||||
orgPolicyExisting := false
|
||||
orgLoginPolicy := new(iam_es_model.LoginPolicy)
|
||||
|
||||
for _, event := range events {
|
||||
domains = handleDomainEvents(domains, event)
|
||||
|
||||
switch event.AggregateType {
|
||||
case org_es_model.OrgAggregate:
|
||||
orgPolicyExisting = handleOrgLoginPolicy(event, orgPolicyExisting, orgLoginPolicy)
|
||||
case iam_es_model.IAMAggregate:
|
||||
handleIAMLoginPolicy(event, iamLoginPolicy)
|
||||
}
|
||||
}
|
||||
err := handleCheckDomainAllowedAsUsername(domains, userName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return handleIDPConfigExisting(iamLoginPolicy, orgLoginPolicy, orgPolicyExisting, externalIDP)
|
||||
}
|
||||
}
|
||||
|
||||
func handleOrgLoginPolicy(event *es_models.Event, orgPolicyExisting bool, orgLoginPolicy *iam_es_model.LoginPolicy) bool {
|
||||
switch event.Type {
|
||||
case org_es_model.LoginPolicyAdded:
|
||||
orgPolicyExisting = true
|
||||
orgLoginPolicy.SetData(event)
|
||||
case org_es_model.LoginPolicyChanged:
|
||||
orgLoginPolicy.SetData(event)
|
||||
case org_es_model.LoginPolicyRemoved:
|
||||
orgPolicyExisting = false
|
||||
case org_es_model.LoginPolicyIDPProviderAdded:
|
||||
idp := new(iam_es_model.IDPProvider)
|
||||
idp.SetData(event)
|
||||
orgLoginPolicy.IDPProviders = append(orgLoginPolicy.IDPProviders, idp)
|
||||
case org_es_model.LoginPolicyIDPProviderRemoved, org_es_model.LoginPolicyIDPProviderCascadeRemoved:
|
||||
idp := new(iam_es_model.IDPProvider)
|
||||
idp.SetData(event)
|
||||
for i, provider := range orgLoginPolicy.IDPProviders {
|
||||
if provider.IDPConfigID == idp.IDPConfigID {
|
||||
orgLoginPolicy.IDPProviders[i] = orgLoginPolicy.IDPProviders[len(orgLoginPolicy.IDPProviders)-1]
|
||||
orgLoginPolicy.IDPProviders[len(orgLoginPolicy.IDPProviders)-1] = nil
|
||||
orgLoginPolicy.IDPProviders = orgLoginPolicy.IDPProviders[:len(orgLoginPolicy.IDPProviders)-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return orgPolicyExisting
|
||||
}
|
||||
|
||||
func handleIAMLoginPolicy(event *es_models.Event, iamLoginPolicy *iam_es_model.LoginPolicy) {
|
||||
switch event.Type {
|
||||
case iam_es_model.LoginPolicyAdded, iam_es_model.LoginPolicyChanged:
|
||||
iamLoginPolicy.SetData(event)
|
||||
case iam_es_model.LoginPolicyIDPProviderAdded:
|
||||
idp := new(iam_es_model.IDPProvider)
|
||||
idp.SetData(event)
|
||||
iamLoginPolicy.IDPProviders = append(iamLoginPolicy.IDPProviders, idp)
|
||||
case iam_es_model.LoginPolicyIDPProviderRemoved, iam_es_model.LoginPolicyIDPProviderCascadeRemoved:
|
||||
idp := new(iam_es_model.IDPProvider)
|
||||
idp.SetData(event)
|
||||
for i, provider := range iamLoginPolicy.IDPProviders {
|
||||
if provider.IDPConfigID == idp.IDPConfigID {
|
||||
iamLoginPolicy.IDPProviders[i] = iamLoginPolicy.IDPProviders[len(iamLoginPolicy.IDPProviders)-1]
|
||||
iamLoginPolicy.IDPProviders[len(iamLoginPolicy.IDPProviders)-1] = nil
|
||||
iamLoginPolicy.IDPProviders = iamLoginPolicy.IDPProviders[:len(iamLoginPolicy.IDPProviders)-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
split := strings.Split(userName, "@")
|
||||
if len(split) != 2 {
|
||||
return nil
|
||||
}
|
||||
for _, d := range domains {
|
||||
if d.Verified && d.Domain == split[1] {
|
||||
return errors.ThrowPreconditionFailed(nil, "EVENT-us5Zw", "Errors.User.DomainNotAllowedAsUsername")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@@ -358,6 +358,7 @@ func TestUserRegisterAggregate(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
user *model.User
|
||||
externalIDP *model.ExternalIDP
|
||||
initCode *model.InitUserCode
|
||||
resourceOwner string
|
||||
aggCreator *models.AggregateCreator
|
||||
@@ -365,6 +366,7 @@ func TestUserRegisterAggregate(t *testing.T) {
|
||||
type res struct {
|
||||
eventLen int
|
||||
eventTypes []models.EventType
|
||||
aggLen int
|
||||
errFunc func(err error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
@@ -391,6 +393,29 @@ func TestUserRegisterAggregate(t *testing.T) {
|
||||
res: res{
|
||||
eventLen: 2,
|
||||
eventTypes: []models.EventType{model.HumanRegistered, model.InitializedHumanCodeAdded},
|
||||
aggLen: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user register with erxternalIDP aggregate ok",
|
||||
args: args{
|
||||
ctx: authz.NewMockContext("orgID", "userID"),
|
||||
user: &model.User{
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: "ID"},
|
||||
UserName: "UserName",
|
||||
Human: &model.Human{
|
||||
Profile: &model.Profile{DisplayName: "DisplayName"},
|
||||
Email: &model.Email{EmailAddress: "EmailAddress"},
|
||||
},
|
||||
},
|
||||
externalIDP: &model.ExternalIDP{IDPConfigID: "IDPConfigID"},
|
||||
resourceOwner: "newResourceowner",
|
||||
aggCreator: models.NewAggregateCreator("Test"),
|
||||
},
|
||||
res: res{
|
||||
eventLen: 2,
|
||||
eventTypes: []models.EventType{model.HumanRegistered, model.HumanExternalIDPAdded},
|
||||
aggLen: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -406,25 +431,6 @@ func TestUserRegisterAggregate(t *testing.T) {
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "code nil",
|
||||
args: args{
|
||||
ctx: authz.NewMockContext("orgID", "userID"),
|
||||
resourceOwner: "newResourceowner",
|
||||
user: &model.User{
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: "ID"},
|
||||
UserName: "UserName",
|
||||
Human: &model.Human{
|
||||
Profile: &model.Profile{DisplayName: "DisplayName"},
|
||||
Email: &model.Email{EmailAddress: "EmailAddress"},
|
||||
},
|
||||
},
|
||||
aggCreator: models.NewAggregateCreator("Test"),
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create with init code",
|
||||
args: args{
|
||||
@@ -444,6 +450,7 @@ func TestUserRegisterAggregate(t *testing.T) {
|
||||
res: res{
|
||||
eventLen: 2,
|
||||
eventTypes: []models.EventType{model.HumanRegistered, model.InitializedHumanCodeAdded},
|
||||
aggLen: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -468,16 +475,20 @@ func TestUserRegisterAggregate(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
aggregates, err := UserRegisterAggregate(tt.args.ctx, tt.args.aggCreator, tt.args.user, tt.args.resourceOwner, tt.args.initCode, false)
|
||||
aggregates, err := UserRegisterAggregate(tt.args.ctx, tt.args.aggCreator, tt.args.user, tt.args.externalIDP, tt.args.resourceOwner, tt.args.initCode, false)
|
||||
|
||||
if tt.res.errFunc == nil && len(aggregates[1].Events) != tt.res.eventLen {
|
||||
t.Errorf("got wrong event len: expected: %v, actual: %v ", tt.res.eventLen, len(aggregates[1].Events))
|
||||
if tt.res.errFunc == nil && len(aggregates) != tt.res.aggLen {
|
||||
t.Errorf("got wrong aggregates len: expected: %v, actual: %v ", tt.res.aggLen, len(aggregates))
|
||||
}
|
||||
|
||||
if tt.res.errFunc == nil && len(aggregates[tt.res.aggLen-1].Events) != tt.res.eventLen {
|
||||
t.Errorf("got wrong event len: expected: %v, actual: %v ", tt.res.eventLen, len(aggregates[tt.res.aggLen-1].Events))
|
||||
}
|
||||
for i := 0; i < tt.res.eventLen; i++ {
|
||||
if tt.res.errFunc == nil && aggregates[1].Events[i].Type != tt.res.eventTypes[i] {
|
||||
t.Errorf("got wrong event type: expected: %v, actual: %v ", tt.res.eventTypes[i], aggregates[1].Events[i].Type.String())
|
||||
if tt.res.errFunc == nil && aggregates[tt.res.aggLen-1].Events[i].Type != tt.res.eventTypes[i] {
|
||||
t.Errorf("got wrong event type: expected: %v, actual: %v ", tt.res.eventTypes[i], aggregates[tt.res.aggLen-1].Events[i].Type.String())
|
||||
}
|
||||
if tt.res.errFunc == nil && aggregates[1].Events[i].Data == nil {
|
||||
if tt.res.errFunc == nil && aggregates[tt.res.aggLen-1].Events[i].Data == nil {
|
||||
t.Errorf("should have data in event")
|
||||
}
|
||||
}
|
||||
@@ -2274,3 +2285,136 @@ func TestOTPRemoveAggregate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExternalIDPAddedAggregates(t *testing.T) {
|
||||
type res struct {
|
||||
aggregateCount int
|
||||
isErr func(error) bool
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
aggCreator *models.AggregateCreator
|
||||
user *model.User
|
||||
externalIDP *model.ExternalIDP
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "no user error",
|
||||
args: args{
|
||||
ctx: authz.NewMockContext("org", "user"),
|
||||
aggCreator: models.NewAggregateCreator("test"),
|
||||
user: nil,
|
||||
},
|
||||
res: res{
|
||||
aggregateCount: 0,
|
||||
isErr: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user add external idp successful",
|
||||
args: args{
|
||||
ctx: authz.NewMockContext("org", "user"),
|
||||
aggCreator: models.NewAggregateCreator("test"),
|
||||
user: &model.User{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "AggregateID",
|
||||
Sequence: 5,
|
||||
},
|
||||
},
|
||||
externalIDP: &model.ExternalIDP{
|
||||
IDPConfigID: "IDPConfigID",
|
||||
UserID: "UserID",
|
||||
DisplayName: "DisplayName",
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
aggregateCount: 2,
|
||||
isErr: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ExternalIDPAddedAggregate(tt.args.ctx, tt.args.aggCreator, tt.args.user, tt.args.externalIDP)
|
||||
if tt.res.isErr == nil && err != nil {
|
||||
t.Errorf("no error expected got %T: %v", err, err)
|
||||
}
|
||||
if tt.res.isErr != nil && !tt.res.isErr(err) {
|
||||
t.Errorf("wrong error got %T: %v", err, err)
|
||||
}
|
||||
if tt.res.isErr == nil && len(got) != tt.res.aggregateCount {
|
||||
t.Errorf("ExternalIDPAddedAggregate() aggregate count = %d, wanted count %d", len(got), tt.res.aggregateCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExternalIDPRemovedAggregates(t *testing.T) {
|
||||
type res struct {
|
||||
aggregateCount int
|
||||
isErr func(error) bool
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
aggCreator *models.AggregateCreator
|
||||
user *model.User
|
||||
externalIDP *model.ExternalIDP
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "no user error",
|
||||
args: args{
|
||||
ctx: authz.NewMockContext("org", "user"),
|
||||
aggCreator: models.NewAggregateCreator("test"),
|
||||
user: nil,
|
||||
},
|
||||
res: res{
|
||||
aggregateCount: 0,
|
||||
isErr: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user removed external idp successful",
|
||||
args: args{
|
||||
ctx: authz.NewMockContext("org", "user"),
|
||||
aggCreator: models.NewAggregateCreator("test"),
|
||||
user: &model.User{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "AggregateID",
|
||||
Sequence: 5,
|
||||
},
|
||||
},
|
||||
externalIDP: &model.ExternalIDP{
|
||||
IDPConfigID: "IDPConfigID",
|
||||
UserID: "UserID",
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
aggregateCount: 2,
|
||||
isErr: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ExternalIDPRemovedAggregate(tt.args.ctx, tt.args.aggCreator, tt.args.user, tt.args.externalIDP, false)
|
||||
if tt.res.isErr == nil && err != nil {
|
||||
t.Errorf("no error expected got %T: %v", err, err)
|
||||
}
|
||||
if tt.res.isErr != nil && !tt.res.isErr(err) {
|
||||
t.Errorf("wrong error got %T: %v", err, err)
|
||||
}
|
||||
if tt.res.isErr == nil && len(got) != tt.res.aggregateCount {
|
||||
t.Errorf("ExternalIDPRemovedAggregate() aggregate count = %d, wanted count %d", len(got), tt.res.aggregateCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
117
internal/user/repository/view/external_idp_view.go
Normal file
117
internal/user/repository/view/external_idp_view.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/view/repository"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
global_model "github.com/caos/zitadel/internal/model"
|
||||
usr_model "github.com/caos/zitadel/internal/user/model"
|
||||
"github.com/caos/zitadel/internal/user/repository/view/model"
|
||||
)
|
||||
|
||||
func ExternalIDPByExternalUserIDAndIDPConfigID(db *gorm.DB, table, externalUserID, idpConfigID string) (*model.ExternalIDPView, error) {
|
||||
user := new(model.ExternalIDPView)
|
||||
userIDQuery := &model.ExternalIDPSearchQuery{
|
||||
Key: usr_model.ExternalIDPSearchKeyExternalUserID,
|
||||
Method: global_model.SearchMethodEquals,
|
||||
Value: externalUserID,
|
||||
}
|
||||
idpConfigIDQuery := &model.ExternalIDPSearchQuery{
|
||||
Key: usr_model.ExternalIDPSearchKeyIdpConfigID,
|
||||
Method: global_model.SearchMethodEquals,
|
||||
Value: idpConfigID,
|
||||
}
|
||||
query := repository.PrepareGetByQuery(table, userIDQuery, idpConfigIDQuery)
|
||||
err := query(db, user)
|
||||
if caos_errs.IsNotFound(err) {
|
||||
return nil, caos_errs.ThrowNotFound(nil, "VIEW-Mso9f", "Errors.ExternalIDP.NotFound")
|
||||
}
|
||||
return user, err
|
||||
}
|
||||
|
||||
func ExternalIDPByExternalUserIDAndIDPConfigIDAndResourceOwner(db *gorm.DB, table, externalUserID, idpConfigID, resourceOwner string) (*model.ExternalIDPView, error) {
|
||||
user := new(model.ExternalIDPView)
|
||||
userIDQuery := &model.ExternalIDPSearchQuery{
|
||||
Key: usr_model.ExternalIDPSearchKeyExternalUserID,
|
||||
Method: global_model.SearchMethodEquals,
|
||||
Value: externalUserID,
|
||||
}
|
||||
idpConfigIDQuery := &model.ExternalIDPSearchQuery{
|
||||
Key: usr_model.ExternalIDPSearchKeyIdpConfigID,
|
||||
Method: global_model.SearchMethodEquals,
|
||||
Value: idpConfigID,
|
||||
}
|
||||
resourceOwnerQuery := &model.ExternalIDPSearchQuery{
|
||||
Key: usr_model.ExternalIDPSearchKeyResourceOwner,
|
||||
Method: global_model.SearchMethodEquals,
|
||||
Value: resourceOwner,
|
||||
}
|
||||
query := repository.PrepareGetByQuery(table, userIDQuery, idpConfigIDQuery, resourceOwnerQuery)
|
||||
err := query(db, user)
|
||||
if caos_errs.IsNotFound(err) {
|
||||
return nil, caos_errs.ThrowNotFound(nil, "VIEW-Sf8sd", "Errors.ExternalIDP.NotFound")
|
||||
}
|
||||
return user, err
|
||||
}
|
||||
|
||||
func ExternalIDPsByIDPConfigID(db *gorm.DB, table, idpConfigID string) ([]*model.ExternalIDPView, error) {
|
||||
externalIDPs := make([]*model.ExternalIDPView, 0)
|
||||
orgIDQuery := &usr_model.ExternalIDPSearchQuery{
|
||||
Key: usr_model.ExternalIDPSearchKeyIdpConfigID,
|
||||
Method: global_model.SearchMethodEquals,
|
||||
Value: idpConfigID,
|
||||
}
|
||||
query := repository.PrepareSearchQuery(table, model.ExternalIDPSearchRequest{
|
||||
Queries: []*usr_model.ExternalIDPSearchQuery{orgIDQuery},
|
||||
})
|
||||
_, err := query(db, &externalIDPs)
|
||||
return externalIDPs, err
|
||||
}
|
||||
|
||||
func ExternalIDPsByUserID(db *gorm.DB, table, userID string) ([]*model.ExternalIDPView, error) {
|
||||
externalIDPs := make([]*model.ExternalIDPView, 0)
|
||||
orgIDQuery := &usr_model.ExternalIDPSearchQuery{
|
||||
Key: usr_model.ExternalIDPSearchKeyUserID,
|
||||
Method: global_model.SearchMethodEquals,
|
||||
Value: userID,
|
||||
}
|
||||
query := repository.PrepareSearchQuery(table, model.ExternalIDPSearchRequest{
|
||||
Queries: []*usr_model.ExternalIDPSearchQuery{orgIDQuery},
|
||||
})
|
||||
_, err := query(db, &externalIDPs)
|
||||
return externalIDPs, err
|
||||
}
|
||||
|
||||
func SearchExternalIDPs(db *gorm.DB, table string, req *usr_model.ExternalIDPSearchRequest) ([]*model.ExternalIDPView, uint64, error) {
|
||||
externalIDPs := make([]*model.ExternalIDPView, 0)
|
||||
query := repository.PrepareSearchQuery(table, model.ExternalIDPSearchRequest{Limit: req.Limit, Offset: req.Offset, Queries: req.Queries})
|
||||
count, err := query(db, &externalIDPs)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return externalIDPs, count, nil
|
||||
}
|
||||
|
||||
func PutExternalIDPs(db *gorm.DB, table string, externalIDPs ...*model.ExternalIDPView) error {
|
||||
save := repository.PrepareBulkSave(table)
|
||||
u := make([]interface{}, len(externalIDPs))
|
||||
for i, idp := range externalIDPs {
|
||||
u[i] = idp
|
||||
}
|
||||
return save(db, u...)
|
||||
}
|
||||
|
||||
func PutExternalIDP(db *gorm.DB, table string, idp *model.ExternalIDPView) error {
|
||||
save := repository.PrepareSave(table)
|
||||
return save(db, idp)
|
||||
}
|
||||
|
||||
func DeleteExternalIDP(db *gorm.DB, table, externalUserID, idpConfigID string) error {
|
||||
delete := repository.PrepareDeleteByKeys(table,
|
||||
repository.Key{Key: model.ExternalIDPSearchKey(usr_model.ExternalIDPSearchKeyExternalUserID), Value: externalUserID},
|
||||
repository.Key{Key: model.ExternalIDPSearchKey(usr_model.ExternalIDPSearchKeyIdpConfigID), Value: idpConfigID},
|
||||
)
|
||||
return delete(db)
|
||||
}
|
65
internal/user/repository/view/model/external_idp_query.go
Normal file
65
internal/user/repository/view/model/external_idp_query.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
global_model "github.com/caos/zitadel/internal/model"
|
||||
usr_model "github.com/caos/zitadel/internal/user/model"
|
||||
"github.com/caos/zitadel/internal/view/repository"
|
||||
)
|
||||
|
||||
type ExternalIDPSearchRequest usr_model.ExternalIDPSearchRequest
|
||||
type ExternalIDPSearchQuery usr_model.ExternalIDPSearchQuery
|
||||
type ExternalIDPSearchKey usr_model.ExternalIDPSearchKey
|
||||
|
||||
func (req ExternalIDPSearchRequest) GetLimit() uint64 {
|
||||
return req.Limit
|
||||
}
|
||||
|
||||
func (req ExternalIDPSearchRequest) GetOffset() uint64 {
|
||||
return req.Offset
|
||||
}
|
||||
|
||||
func (req ExternalIDPSearchRequest) GetSortingColumn() repository.ColumnKey {
|
||||
if req.SortingColumn == usr_model.ExternalIDPSearchKeyUnspecified {
|
||||
return nil
|
||||
}
|
||||
return ExternalIDPSearchKey(req.SortingColumn)
|
||||
}
|
||||
|
||||
func (req ExternalIDPSearchRequest) GetAsc() bool {
|
||||
return req.Asc
|
||||
}
|
||||
|
||||
func (req ExternalIDPSearchRequest) GetQueries() []repository.SearchQuery {
|
||||
result := make([]repository.SearchQuery, len(req.Queries))
|
||||
for i, q := range req.Queries {
|
||||
result[i] = ExternalIDPSearchQuery{Key: q.Key, Value: q.Value, Method: q.Method}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (req ExternalIDPSearchQuery) GetKey() repository.ColumnKey {
|
||||
return ExternalIDPSearchKey(req.Key)
|
||||
}
|
||||
|
||||
func (req ExternalIDPSearchQuery) GetMethod() global_model.SearchMethod {
|
||||
return req.Method
|
||||
}
|
||||
|
||||
func (req ExternalIDPSearchQuery) GetValue() interface{} {
|
||||
return req.Value
|
||||
}
|
||||
|
||||
func (key ExternalIDPSearchKey) ToColumnName() string {
|
||||
switch usr_model.ExternalIDPSearchKey(key) {
|
||||
case usr_model.ExternalIDPSearchKeyExternalUserID:
|
||||
return ExternalIDPKeyExternalUserID
|
||||
case usr_model.ExternalIDPSearchKeyUserID:
|
||||
return ExternalIDPKeyUserID
|
||||
case usr_model.ExternalIDPSearchKeyIdpConfigID:
|
||||
return ExternalIDPKeyIDPConfigID
|
||||
case usr_model.ExternalIDPSearchKeyResourceOwner:
|
||||
return ExternalIDPKeyResourceOwner
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
91
internal/user/repository/view/model/external_idps.go
Normal file
91
internal/user/repository/view/model/external_idps.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/caos/logging"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/user/model"
|
||||
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
ExternalIDPKeyExternalUserID = "external_user_id"
|
||||
ExternalIDPKeyUserID = "user_id"
|
||||
ExternalIDPKeyIDPConfigID = "idp_config_id"
|
||||
ExternalIDPKeyResourceOwner = "resource_owner"
|
||||
)
|
||||
|
||||
type ExternalIDPView struct {
|
||||
ExternalUserID string `json:"userID" gorm:"column:external_user_id;primary_key"`
|
||||
IDPConfigID string `json:"idpConfigID" gorm:"column:idp_config_id;primary_key"`
|
||||
UserID string `json:"-" gorm:"column:user_id"`
|
||||
IDPName string `json:"-" gorm:"column:idp_name"`
|
||||
UserDisplayName string `json:"displayName" gorm:"column:user_display_name"`
|
||||
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
|
||||
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
|
||||
ResourceOwner string `json:"-" gorm:"column:resource_owner"`
|
||||
Sequence uint64 `json:"-" gorm:"column:sequence"`
|
||||
}
|
||||
|
||||
func ExternalIDPViewFromModel(externalIDP *model.ExternalIDPView) *ExternalIDPView {
|
||||
return &ExternalIDPView{
|
||||
UserID: externalIDP.UserID,
|
||||
IDPConfigID: externalIDP.IDPConfigID,
|
||||
ExternalUserID: externalIDP.ExternalUserID,
|
||||
IDPName: externalIDP.IDPName,
|
||||
UserDisplayName: externalIDP.UserDisplayName,
|
||||
Sequence: externalIDP.Sequence,
|
||||
CreationDate: externalIDP.CreationDate,
|
||||
ChangeDate: externalIDP.ChangeDate,
|
||||
ResourceOwner: externalIDP.ResourceOwner,
|
||||
}
|
||||
}
|
||||
|
||||
func ExternalIDPViewToModel(externalIDP *ExternalIDPView) *model.ExternalIDPView {
|
||||
return &model.ExternalIDPView{
|
||||
UserID: externalIDP.UserID,
|
||||
IDPConfigID: externalIDP.IDPConfigID,
|
||||
ExternalUserID: externalIDP.ExternalUserID,
|
||||
IDPName: externalIDP.IDPName,
|
||||
UserDisplayName: externalIDP.UserDisplayName,
|
||||
Sequence: externalIDP.Sequence,
|
||||
CreationDate: externalIDP.CreationDate,
|
||||
ChangeDate: externalIDP.ChangeDate,
|
||||
ResourceOwner: externalIDP.ResourceOwner,
|
||||
}
|
||||
}
|
||||
|
||||
func ExternalIDPViewsToModel(externalIDPs []*ExternalIDPView) []*model.ExternalIDPView {
|
||||
result := make([]*model.ExternalIDPView, len(externalIDPs))
|
||||
for i, r := range externalIDPs {
|
||||
result[i] = ExternalIDPViewToModel(r)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (i *ExternalIDPView) AppendEvent(event *models.Event) (err error) {
|
||||
i.Sequence = event.Sequence
|
||||
i.ChangeDate = event.CreationDate
|
||||
switch event.Type {
|
||||
case es_model.HumanExternalIDPAdded:
|
||||
i.setRootData(event)
|
||||
i.CreationDate = event.CreationDate
|
||||
err = i.SetData(event)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *ExternalIDPView) setRootData(event *models.Event) {
|
||||
r.UserID = event.AggregateID
|
||||
r.ResourceOwner = event.ResourceOwner
|
||||
}
|
||||
|
||||
func (r *ExternalIDPView) SetData(event *models.Event) error {
|
||||
if err := json.Unmarshal(event.Data, r); err != nil {
|
||||
logging.Log("EVEN-48sfs").WithError(err).Error("could not unmarshal event data")
|
||||
return caos_errs.ThrowInternal(err, "MODEL-Hs8uf", "Could not unmarshal data")
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -46,6 +46,26 @@ func UserByLoginName(db *gorm.DB, table, loginName string) (*model.UserView, err
|
||||
return user, err
|
||||
}
|
||||
|
||||
func UserByLoginNameAndResourceOwner(db *gorm.DB, table, loginName, resourceOwner string) (*model.UserView, error) {
|
||||
user := new(model.UserView)
|
||||
loginNameQuery := &model.UserSearchQuery{
|
||||
Key: usr_model.UserSearchKeyLoginNames,
|
||||
Method: global_model.SearchMethodListContains,
|
||||
Value: loginName,
|
||||
}
|
||||
resourceOwnerQuery := &model.UserSearchQuery{
|
||||
Key: usr_model.UserSearchKeyResourceOwner,
|
||||
Method: global_model.SearchMethodEquals,
|
||||
Value: resourceOwner,
|
||||
}
|
||||
query := repository.PrepareGetByQuery(table, loginNameQuery, resourceOwnerQuery)
|
||||
err := query(db, user)
|
||||
if caos_errs.IsNotFound(err) {
|
||||
return nil, caos_errs.ThrowNotFound(nil, "VIEW-AD4qs", "Errors.User.NotFound")
|
||||
}
|
||||
return user, err
|
||||
}
|
||||
|
||||
func UsersByOrgID(db *gorm.DB, table, orgID string) ([]*model.UserView, error) {
|
||||
users := make([]*model.UserView, 0)
|
||||
orgIDQuery := &usr_model.UserSearchQuery{
|
||||
|
Reference in New Issue
Block a user