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:
Fabi
2020-09-18 13:26:28 +02:00
committed by GitHub
parent 1d542a0c57
commit 320ddfa46d
141 changed files with 30057 additions and 12535 deletions

View File

@@ -0,0 +1,17 @@
package model
import (
es_models "github.com/caos/zitadel/internal/eventstore/models"
)
type ExternalIDP struct {
es_models.ObjectRoot
IDPConfigID string
UserID string
DisplayName string
}
func (idp *ExternalIDP) IsValid() bool {
return idp.AggregateID != "" && idp.IDPConfigID != "" && idp.UserID != ""
}

View File

@@ -0,0 +1,61 @@
package model
import (
"github.com/caos/zitadel/internal/model"
"time"
)
type ExternalIDPView struct {
UserID string
IDPConfigID string
ExternalUserID string
IDPName string
UserDisplayName string
CreationDate time.Time
ChangeDate time.Time
ResourceOwner string
Sequence uint64
}
type ExternalIDPSearchRequest struct {
Offset uint64
Limit uint64
SortingColumn ExternalIDPSearchKey
Asc bool
Queries []*ExternalIDPSearchQuery
}
type ExternalIDPSearchKey int32
const (
ExternalIDPSearchKeyUnspecified ExternalIDPSearchKey = iota
ExternalIDPSearchKeyExternalUserID
ExternalIDPSearchKeyUserID
ExternalIDPSearchKeyIdpConfigID
ExternalIDPSearchKeyResourceOwner
)
type ExternalIDPSearchQuery struct {
Key ExternalIDPSearchKey
Method model.SearchMethod
Value interface{}
}
type ExternalIDPSearchResponse struct {
Offset uint64
Limit uint64
TotalResult uint64
Result []*ExternalIDPView
Sequence uint64
Timestamp time.Time
}
func (r *ExternalIDPSearchRequest) EnsureLimit(limit uint64) {
if r.Limit == 0 || r.Limit > limit {
r.Limit = limit
}
}
func (r *ExternalIDPSearchRequest) AppendUserQuery(userID string) {
r.Queries = append(r.Queries, &ExternalIDPSearchQuery{Key: ExternalIDPSearchKeyUserID, Method: model.SearchMethodEquals, Value: userID})
}

View File

@@ -15,6 +15,7 @@ type Human struct {
*Email
*Phone
*Address
ExternalIDPs []*ExternalIDP
InitCode *InitUserCode
EmailCode *EmailCode
PhoneCode *PhoneCode
@@ -45,11 +46,11 @@ func (u *Human) SetNamesAsDisplayname() {
}
func (u *Human) IsValid() bool {
return u.Profile != nil && u.FirstName != "" && u.LastName != "" && u.Email != nil && u.Email.IsValid() && u.Phone == nil || (u.Phone != nil && u.Phone.IsValid())
return u.Profile != nil && u.FirstName != "" && u.LastName != "" && u.Email != nil && u.Email.IsValid() && u.Phone == nil || (u.Phone != nil && u.Phone.PhoneNumber != "" && u.Phone.IsValid())
}
func (u *Human) IsInitialState() bool {
return u.Email == nil || !u.IsEmailVerified || u.Password == nil || u.SecretString == ""
return u.Email == nil || !u.IsEmailVerified || (u.ExternalIDPs == nil || len(u.ExternalIDPs) == 0) && (u.Password == nil || u.SecretString == "")
}
func (u *Human) IsOTPReady() bool {
@@ -96,3 +97,12 @@ func (init *InitUserCode) GenerateInitUserCode(generator crypto.Generator) error
init.Expiry = generator.Expiry()
return nil
}
func (u *Human) GetExternalIDP(externalIDP *ExternalIDP) (int, *ExternalIDP) {
for i, idp := range u.ExternalIDPs {
if idp.UserID == externalIDP.UserID {
return i, idp
}
}
return -1, nil
}

View File

@@ -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")

View File

@@ -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)

View File

@@ -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 {

View 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
}

View File

@@ -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 ")
}
})
}
}

View File

@@ -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"

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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)
}
})
}
}

View 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)
}

View 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 ""
}
}

View 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
}

View File

@@ -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{