fix: use preparation filter for org existence check during setup (#11932)

<!--
Please inform yourself about the contribution guidelines on submitting a
PR here:
https://github.com/zitadel/zitadel/blob/main/CONTRIBUTING.md#submit-a-pull-request-pr.
Take note of how PR/commit titles should be written and replace the
template texts in the sections below. Don't remove any of the sections.
It is important that the commit history clearly shows what is changed
and why.
Important: By submitting a contribution you agree to the terms from our
Licensing Policy as described here:
https://github.com/zitadel/zitadel/blob/main/LICENSING.md#community-contributions.
-->

# Which Problems Are Solved

- Creating an organization via `SetUpOrg` failed when a custom domain
was provided in the same request.
- The org-domain setup path checked org existence only against persisted
state, so it could not see the newly created organization before the
batch was pushed.

# How the Problems Are Solved

- Added a preparation-aware org existence check that resolves the org
through the validation filter instead of the direct persisted-state
lookup.
- Reused the transaction-aware preparation filter so later validations
can see earlier in-flight commands from the same `PrepareCommands`
batch.

fixes #11677

---------

Co-authored-by: abhishek kumar gupta <[email protected]>
Co-authored-by: Wim Van Laer <[email protected]>
This commit is contained in:
Abhishek Kumar Gupta
2026-03-31 09:58:42 +02:00
committed by GitHub
co-authored by abhishek kumar gupta Wim Van Laer
parent 21b28b56ac
commit 8e41d288ff
4 changed files with 188 additions and 76 deletions
+20
View File
@@ -327,6 +327,26 @@ func (c *Commands) getOrg(ctx context.Context, orgID string) (*domain.Org, error
return orgWriteModelToOrg(writeModel), nil
}
func checkOrgExistsInPreparation(ctx context.Context, filter preparation.FilterToQueryReducer, orgID string) error {
orgWriteModel := NewOrgWriteModel(orgID)
events, err := filter(ctx, orgWriteModel.Query())
if err != nil {
return err
}
orgWriteModel.AppendEvents(events...)
if err = orgWriteModel.Reduce(); err != nil {
return err
}
if !isOrgStateExists(orgWriteModel.State) {
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-QXPGs", "Errors.Org.NotFound")
}
return nil
}
func (c *Commands) checkOrgExists(ctx context.Context, orgID string) error {
orgWriteModel, err := c.getOrgWriteModelByID(ctx, orgID)
if err != nil {
+2 -1
View File
@@ -35,7 +35,8 @@ func (c *Commands) prepareAddOrgDomain(a *org.Aggregate, addDomain string, userI
return nil, err
}
}
if err := c.checkOrgExists(ctx, a.ID); err != nil {
if err := checkOrgExistsInPreparation(ctx, filter, a.ID); err != nil {
return nil, err
}
+107 -75
View File
@@ -34,6 +34,9 @@ func TestAddDomain(t *testing.T) {
}
agg := org.NewAggregate("test")
noEvents := func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, nil
}
tests := []struct {
name string
@@ -58,19 +61,47 @@ func TestAddDomain(t *testing.T) {
a: agg,
domain: "domain",
claimedUserIDs: []string{"userID1"},
filter: func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewDomainPolicyAddedEvent(ctx, &agg.Aggregate, true, true, true),
}, nil
filter: NewMultiFilter().
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewOrgAddedEvent(ctx, &agg.Aggregate, "org"),
}, nil
}).
Append(noEvents).
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewDomainPolicyAddedEvent(ctx, &agg.Aggregate, true, true, true),
}, nil
}).
Filter(),
eventstore: expectEventstore(),
loginPaths: expectLoginPathsNoCall,
},
want: Want{
Commands: []eventstore.Command{
org.NewDomainAddedEvent(context.Background(), &agg.Aggregate, "domain"),
},
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate,
"org"),
),
),
),
},
},
{
name: "correct when org exists only in the current preparation",
args: args{
a: agg,
domain: "domain",
filter: NewMultiFilter().
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewOrgAddedEvent(ctx, &agg.Aggregate, "org"),
}, nil
}).
Append(noEvents).
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewDomainPolicyAddedEvent(ctx, &agg.Aggregate, true, true, true),
}, nil
}).
Filter(),
eventstore: expectEventstore(),
loginPaths: expectLoginPathsNoCall,
},
want: Want{
@@ -86,41 +117,41 @@ func TestAddDomain(t *testing.T) {
domain: "domain",
claimedUserIDs: []string{"userID1"},
idGenerator: id_mock.ExpectID(t, "newID"),
filter: func() func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
i := 0 //TODO: we should fix this in the future to use some kind of mock struct and expect filter calls
return func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
if i == 2 {
i++
return []eventstore.Event{user.NewHumanAddedEvent(
ctx,
&user.NewAggregate("userID1", "org2").Aggregate,
"username",
"firstname",
"lastname",
"nickname",
"displayname",
language.Und,
domain.GenderUnspecified,
"email",
false,
)}, nil
}
if i == 3 {
i++
return []eventstore.Event{org.NewDomainPolicyAddedEvent(ctx, &agg.Aggregate, false, false, false)}, nil
}
i++
return []eventstore.Event{org.NewDomainPolicyAddedEvent(ctx, &agg.Aggregate, true, false, false)}, nil
}
}(),
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate,
"org"),
),
),
),
filter: NewMultiFilter().
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewOrgAddedEvent(ctx, &agg.Aggregate, "org"),
}, nil
}).
Append(noEvents).
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewDomainPolicyAddedEvent(ctx, &agg.Aggregate, false, false, false),
}, nil
}).
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{user.NewHumanAddedEvent(
ctx,
&user.NewAggregate("userID1", "org2").Aggregate,
"username",
"firstname",
"lastname",
"nickname",
"displayname",
language.Und,
domain.GenderUnspecified,
"email",
false,
)}, nil
}).
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewDomainPolicyAddedEvent(ctx, &org.NewAggregate("org2").Aggregate, false, false, false),
}, nil
}).
Append(noEvents).
Filter(),
eventstore: expectEventstore(),
loginPaths: expectLoginPathsDefaultDomainClaimedURLTemplate(""),
},
want: Want{
@@ -137,21 +168,21 @@ func TestAddDomain(t *testing.T) {
a: agg,
domain: "domain",
claimedUserIDs: []string{"userID1"},
filter: func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewDomainAddedEvent(ctx, &agg.Aggregate, "domain"),
org.NewDomainVerificationAddedEvent(ctx, &agg.Aggregate, "domain", domain.OrgDomainValidationTypeHTTP, nil),
org.NewDomainVerifiedEvent(ctx, &agg.Aggregate, "domain"),
}, nil
},
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate,
"org"),
),
),
),
filter: NewMultiFilter().
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewOrgAddedEvent(ctx, &agg.Aggregate, "org"),
}, nil
}).
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewDomainAddedEvent(ctx, &agg.Aggregate, "domain"),
org.NewDomainVerificationAddedEvent(ctx, &agg.Aggregate, "domain", domain.OrgDomainValidationTypeHTTP, nil),
org.NewDomainVerifiedEvent(ctx, &agg.Aggregate, "domain"),
}, nil
}).
Filter(),
eventstore: expectEventstore(),
loginPaths: expectLoginPathsNoCall,
},
want: Want{
@@ -179,19 +210,20 @@ func TestAddDomain(t *testing.T) {
a: agg,
domain: "domain",
claimedUserIDs: []string{"userID1"},
filter: func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewDomainPolicyAddedEvent(ctx, &agg.Aggregate, true, true, true),
}, nil
},
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate,
"org"),
),
),
),
filter: NewMultiFilter().
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewOrgAddedEvent(ctx, &agg.Aggregate, "org"),
}, nil
}).
Append(noEvents).
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewDomainPolicyAddedEvent(ctx, &agg.Aggregate, true, true, true),
}, nil
}).
Filter(),
eventstore: expectEventstore(),
loginPaths: expectLoginPathsNoCall,
permissionCheck: newMockOrganizationPermissionCheckAllowed(),
},
+59
View File
@@ -18,6 +18,7 @@ import (
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/id"
id_mock "github.com/zitadel/zitadel/internal/id/mock"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/project"
"github.com/zitadel/zitadel/internal/repository/user"
@@ -1845,6 +1846,64 @@ func TestCommandSide_SetUpOrg(t *testing.T) {
},
},
},
{
name: "no admin added, custom domain",
fields: fields{
eventstore: expectEventstore(
expectFilter(), // org already exists check
expectFilter(), // org exists in current preparation
expectFilter(), // custom domain does not exist yet
expectFilter(), // no org domain policy yet
expectFilter(
eventFromEventPusher(
instance.NewDomainPolicyAddedEvent(context.Background(),
&instance.NewAggregate("instance1").Aggregate,
true,
true,
true,
),
),
),
expectPush(
eventFromEventPusher(org.NewOrgAddedEvent(context.Background(),
&org.NewAggregate("orgID").Aggregate,
"Org",
)),
eventFromEventPusher(org.NewDomainAddedEvent(context.Background(),
&org.NewAggregate("orgID").Aggregate, "org.iam-domain",
)),
eventFromEventPusher(org.NewDomainVerifiedEvent(context.Background(),
&org.NewAggregate("orgID").Aggregate,
"org.iam-domain",
)),
eventFromEventPusher(org.NewDomainPrimarySetEvent(context.Background(),
&org.NewAggregate("orgID").Aggregate,
"org.iam-domain",
)),
eventFromEventPusher(org.NewDomainAddedEvent(context.Background(),
&org.NewAggregate("orgID").Aggregate,
"custom.example.com",
)),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "orgID"),
},
args: args{
ctx: http_util.WithRequestedHost(authz.WithInstanceID(context.Background(), "instance1"), "iam-domain"),
setupOrg: &OrgSetup{
Name: "Org",
CustomDomain: "custom.example.com",
},
},
res: res{
createdOrg: &CreatedOrg{
ObjectDetails: &domain.ObjectDetails{
ResourceOwner: "orgID",
},
OrgAdmins: []OrgAdmin{},
},
},
},
{
name: "existing human added",
fields: fields{