fix(org): adding unique constrants to not allow an org to be added twice with same id (#10243)

# Which Problems Are Solved

When adding 2 orgs with the same ID, you get a positive response from
the API, later when the org is projected, it errors due to the id
already in use

# How the Problems Are Solved

Check org with orgID specified does not already exist before adding
events

# Additional Changes

Added additional test case for adding same org with same name twice


# Additional Context

- Closes https://github.com/zitadel/zitadel/issues/10127

---------

Co-authored-by: Silvan <27845747+adlerhurst@users.noreply.github.com>
This commit is contained in:
Iraq
2025-07-16 12:07:12 +02:00
committed by GitHub
parent 312b7b6010
commit 870fefe3dc
23 changed files with 172 additions and 31 deletions

View File

@@ -47,14 +47,17 @@ func TestMain(m *testing.M) {
func TestServer_CreateOrganization(t *testing.T) {
idpResp := Instance.AddGenericOAuthProvider(CTX, Instance.DefaultOrg.Id)
tests := []struct {
type test struct {
name string
ctx context.Context
req *v2beta_org.CreateOrganizationRequest
id string
testFunc func(ctx context.Context, t *testing.T)
want *v2beta_org.CreateOrganizationResponse
wantErr bool
}{
}
tests := []test{
{
name: "missing permission",
ctx: Instance.WithAuthorization(CTX, integration.UserTypeOrgOwner),
@@ -73,6 +76,25 @@ func TestServer_CreateOrganization(t *testing.T) {
},
wantErr: true,
},
func() test {
orgName := gofakeit.Name()
return test{
name: "adding org with same name twice",
ctx: CTX,
req: &v2beta_org.CreateOrganizationRequest{
Name: orgName,
Admins: nil,
},
testFunc: func(ctx context.Context, t *testing.T) {
// create org initially
_, err := Client.CreateOrganization(ctx, &v2beta_org.CreateOrganizationRequest{
Name: orgName,
})
require.NoError(t, err)
},
wantErr: true,
}
}(),
{
name: "invalid admin type",
ctx: CTX,
@@ -212,9 +234,34 @@ func TestServer_CreateOrganization(t *testing.T) {
Id: "custom_id",
},
},
func() test {
orgID := gofakeit.Name()
return test{
name: "adding org with same ID twice",
ctx: CTX,
req: &v2beta_org.CreateOrganizationRequest{
Id: &orgID,
Name: gofakeit.Name(),
Admins: nil,
},
testFunc: func(ctx context.Context, t *testing.T) {
// create org initially
_, err := Client.CreateOrganization(ctx, &v2beta_org.CreateOrganizationRequest{
Id: &orgID,
Name: gofakeit.Name(),
})
require.NoError(t, err)
},
wantErr: true,
}
}(),
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.testFunc != nil {
tt.testFunc(tt.ctx, t)
}
got, err := Client.CreateOrganization(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)

View File

@@ -276,6 +276,15 @@ func (c *Commands) SetUpOrg(ctx context.Context, o *OrgSetup, allowInitialMail b
}
}
// because users can choose their own ID, we must check that an org with the same ID does not already exist
existingOrg, err := c.getOrgWriteModelByID(ctx, o.OrgID)
if err != nil {
return nil, err
}
if existingOrg.State.Exists() {
return nil, zerrors.ThrowAlreadyExists(nil, "ORG-laho2n", "Errors.Org.AlreadyExisting")
}
return c.setUpOrgWithIDs(ctx, o, o.OrgID, allowInitialMail, userIDs...)
}
@@ -327,12 +336,13 @@ func (c *Commands) AddOrgWithID(ctx context.Context, name, userID, resourceOwner
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
// because users can choose their own ID, we must check that an org with the same ID does not already exist
existingOrg, err := c.getOrgWriteModelByID(ctx, orgID)
if err != nil {
return nil, err
}
if existingOrg.State != domain.OrgStateUnspecified {
return nil, zerrors.ThrowNotFound(nil, "ORG-lapo2m", "Errors.Org.AlreadyExisting")
if existingOrg.State.Exists() {
return nil, zerrors.ThrowAlreadyExists(nil, "ORG-lapo2n", "Errors.Org.AlreadyExisting")
}
return c.addOrgWithIDAndMember(ctx, name, userID, resourceOwner, orgID, setOrgInactive, claimedUserIDs)

View File

@@ -1293,7 +1293,9 @@ func TestCommandSide_SetUpOrg(t *testing.T) {
{
name: "org name empty, error",
fields: fields{
eventstore: expectEventstore(),
eventstore: expectEventstore(
expectFilter(), // org already exists check
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "orgID"),
},
args: args{
@@ -1326,6 +1328,7 @@ func TestCommandSide_SetUpOrg(t *testing.T) {
name: "userID not existing, error",
fields: fields{
eventstore: expectEventstore(
expectFilter(), // org already exists check
expectFilter(),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "orgID"),
@@ -1348,7 +1351,9 @@ func TestCommandSide_SetUpOrg(t *testing.T) {
{
name: "human invalid, error",
fields: fields{
eventstore: expectEventstore(),
eventstore: expectEventstore(
expectFilter(), // org already exists check
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "orgID", "userID"),
},
args: args{
@@ -1381,6 +1386,7 @@ func TestCommandSide_SetUpOrg(t *testing.T) {
fields: fields{
eventstore: expectEventstore(
expectFilter(), // add human exists check
expectFilter(),
expectFilter(
eventFromEventPusher(
org.NewDomainPolicyAddedEvent(context.Background(),
@@ -1501,10 +1507,82 @@ func TestCommandSide_SetUpOrg(t *testing.T) {
},
},
},
{
name: "org already exists",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
&org.NewAggregate("custom-org-ID").Aggregate, "Org"),
),
),
),
},
args: args{
ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"),
setupOrg: &OrgSetup{
Name: "Org",
OrgID: "custom-org-ID",
},
},
res: res{
err: zerrors.ThrowAlreadyExists(nil, "ORG-laho2n", "Errors.Org.AlreadyExisting"),
},
},
{
name: "org with same id deleted",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
&org.NewAggregate("custom-org-ID").Aggregate, "Org"),
),
org.NewOrgRemovedEvent(
context.Background(), &org.NewAggregate("custom-org-ID").Aggregate,
"Org", []string{}, false, []string{}, []*domain.UserIDPLink{}, []string{}),
),
expectPush(
eventFromEventPusher(org.NewOrgAddedEvent(context.Background(),
&org.NewAggregate("custom-org-ID").Aggregate,
"Org",
)),
eventFromEventPusher(org.NewDomainAddedEvent(context.Background(),
&org.NewAggregate("custom-org-ID").Aggregate, "org.iam-domain",
)),
eventFromEventPusher(org.NewDomainVerifiedEvent(context.Background(),
&org.NewAggregate("custom-org-ID").Aggregate,
"org.iam-domain",
)),
eventFromEventPusher(org.NewDomainPrimarySetEvent(context.Background(),
&org.NewAggregate("custom-org-ID").Aggregate,
"org.iam-domain",
)),
),
),
},
args: args{
ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"),
setupOrg: &OrgSetup{
Name: "Org",
OrgID: "custom-org-ID",
},
},
res: res{
createdOrg: &CreatedOrg{
ObjectDetails: &domain.ObjectDetails{
ResourceOwner: "custom-org-ID",
},
OrgAdmins: []OrgAdmin{},
},
},
},
{
name: "no human added, custom org ID",
fields: fields{
eventstore: expectEventstore(
expectFilter(), // org already exists check
expectPush(
eventFromEventPusher(org.NewOrgAddedEvent(context.Background(),
&org.NewAggregate("custom-org-ID").Aggregate,
@@ -1544,6 +1622,7 @@ func TestCommandSide_SetUpOrg(t *testing.T) {
name: "existing human added",
fields: fields{
eventstore: expectEventstore(
expectFilter(), // org already exists check
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -1616,6 +1695,7 @@ func TestCommandSide_SetUpOrg(t *testing.T) {
fields: fields{
eventstore: expectEventstore(
expectFilter(), // add machine exists check
expectFilter(),
expectFilter(
eventFromEventPusher(
org.NewDomainPolicyAddedEvent(context.Background(),

View File

@@ -43,3 +43,7 @@ const (
func (s OrgState) Valid() bool {
return s > OrgStateUnspecified && s < orgStateMax
}
func (s OrgState) Exists() bool {
return s != OrgStateRemoved && s != OrgStateUnspecified
}

View File

@@ -196,7 +196,7 @@ Errors:
AlreadyExists: Екземплярът вече съществува
NotChanged: Екземплярът не е променен
Org:
AlreadyExists: Името на организацията вече е заето
AlreadyExists: Името или идентификационният номер на организацията вече е зает.
Invalid: Организацията е невалидна
AlreadyDeactivated: Организацията вече е деактивирана
AlreadyActive: Организацията вече е активна

View File

@@ -194,7 +194,7 @@ Errors:
AlreadyExists: Instance již existuje
NotChanged: Instance nezměněna
Org:
AlreadyExists: Název organizace je již obsazen
AlreadyExists: Името или идентификационният номер на организацията вече е зает
Invalid: Organizace je neplatná
AlreadyDeactivated: Organizace je již deaktivována
AlreadyActive: Organizace je již aktivní

View File

@@ -194,7 +194,7 @@ Errors:
AlreadyExists: Instanz exisitiert bereits
NotChanged: Instanz wurde nicht verändert
Org:
AlreadyExists: Organisationsname existiert bereits
AlreadyExists: Der Name oder die ID der Organisation ist bereits vorhanden
Invalid: Organisation ist ungültig
AlreadyDeactivated: Organisation ist bereits deaktiviert
AlreadyActive: Organisation ist bereits aktiv

View File

@@ -195,7 +195,7 @@ Errors:
AlreadyExists: Instance already exists
NotChanged: Instance not changed
Org:
AlreadyExists: Organisation's name already taken
AlreadyExists: Organisation's name or id already taken
Invalid: Organisation is invalid
AlreadyDeactivated: Organisation is already deactivated
AlreadyActive: Organisation is already active

View File

@@ -194,7 +194,7 @@ Errors:
AlreadyExists: La instancia ya existe
NotChanged: La instancia no ha cambiado
Org:
AlreadyExists: El nombre de la organización ya está cogido
AlreadyExists: El nombre o id de la organización ya está tomado
Invalid: El nombre de la organización no es válido
AlreadyDeactivated: La organización ya está desactivada
AlreadyActive: La organización ya está activada

View File

@@ -194,7 +194,7 @@ Errors:
AlreadyExists: L'instance existe déjà
NotChanged: L'instance n'a pas changé
Org:
AlreadyExists: Le nom de l'organisation est déjà pris
AlreadyExists: Le nom de l'organisation ou l'identifiant est déjà pris
Invalid: L'organisation n'est pas valide
AlreadyDeactivated: L'organisation est déjà désactivée
AlreadyActive: L'organisation est déjà active

View File

@@ -194,7 +194,7 @@ Errors:
AlreadyExists: Az instance már létezik
NotChanged: Az instance nem változott
Org:
AlreadyExists: A szervezet neve már foglalt
AlreadyExists: A szervezet neve vagy azonosítója már foglalt
Invalid: A szervezet érvénytelen
AlreadyDeactivated: A szervezet már deaktiválva van
AlreadyActive: A szervezet már aktív

View File

@@ -194,7 +194,7 @@ Errors:
AlreadyExists: Contoh sudah ada
NotChanged: Contoh tidak berubah
Org:
AlreadyExists: Nama organisasi sudah dipakai
AlreadyExists: Nama atau ID organisasi sudah digunakan
Invalid: Organisasi tidak valid
AlreadyDeactivated: Organisasi sudah dinonaktifkan
AlreadyActive: Organisasi sudah aktif

View File

@@ -194,7 +194,7 @@ Errors:
AlreadyExists: L'istanza esiste già
NotChanged: Istanza non modificata
Org:
AlreadyExists: Nome dell'organizzazione già preso
AlreadyExists: Nome o ID dell'organizzazione già utilizzato
Invalid: L'organizzazione non è valida
AlreadyDeactivated: L'organizzazione è già disattivata
AlreadyActive: L'organizzazione è già attiva

View File

@@ -195,7 +195,7 @@ Errors:
AlreadyExists: すでに存在するインスタンス
NotChanged: インスタンスは変更されていません
Org:
AlreadyExists: 組織の名前はすでに使用されています
AlreadyExists: 組織名またはIDはすでに使用されています
Invalid: 無効な組織です
AlreadyDeactivated: 組織はすでに非アクティブです
AlreadyActive: 組織はすでにアクティブです

View File

@@ -195,7 +195,7 @@ Errors:
AlreadyExists: 인스턴스가 이미 존재합니다
NotChanged: 인스턴스가 변경되지 않았습니다
Org:
AlreadyExists: 조직 이름 이미 사용 중입니다
AlreadyExists: 조직 이름 또는 ID가 이미 사용 중입니다
Invalid: 조직이 유효하지 않습니다
AlreadyDeactivated: 조직이 이미 비활성화되었습니다
AlreadyActive: 조직이 이미 활성화되었습니다

View File

@@ -193,7 +193,7 @@ Errors:
AlreadyExists: Инстанцата веќе постои
NotChanged: Инстанцата не е променета
Org:
AlreadyExists: Името на организацијата е веќе зафатено
AlreadyExists: Името или ID-то на организацијата е веќе зафатено
Invalid: Организацијата е невалидна
AlreadyDeactivated: Организацијата е веќе деактивирана
AlreadyActive: Организацијата е веќе активна

View File

@@ -194,7 +194,7 @@ Errors:
AlreadyExists: Instantie bestaat al
NotChanged: Instantie is niet veranderd
Org:
AlreadyExists: Organisatienaam is al in gebruik
AlreadyExists: Organisatienaam of -id is al in gebruik
Invalid: Organisatie is ongeldig
AlreadyDeactivated: Organisatie is al gedeactiveerd
AlreadyActive: Organisatie is al actief

View File

@@ -194,7 +194,7 @@ Errors:
AlreadyExists: Instancja już istnieje
NotChanged: Instancja nie zmieniona
Org:
AlreadyExists: Nazwa organizacji jest już zajęta
AlreadyExists: Nazwa lub identyfikator organizacji jest już zajęty
Invalid: Organizacja jest nieprawidłowa
AlreadyDeactivated: Organizacja jest już deaktywowana
AlreadyActive: Organizacja jest już aktywna

View File

@@ -193,7 +193,7 @@ Errors:
AlreadyExists: Instância já existe
NotChanged: Instância não alterada
Org:
AlreadyExists: Nome da organização já está em uso
AlreadyExists: O nome ou ID da organização já está em uso
Invalid: Organização é inválida
AlreadyDeactivated: Organização já está desativada
AlreadyActive: Organização já está ativa

View File

@@ -195,7 +195,7 @@ Errors:
AlreadyExists: Instanța există deja
NotChanged: Instanța nu a fost schimbată
Org:
AlreadyExists: Numele organizației este deja luat
AlreadyExists: Numele sau ID-ul organizației este deja utilizat
Invalid: Organizația este invalidă
AlreadyDeactivated: Organizația este deja dezactivată
AlreadyActive: Organizația este deja activă

View File

@@ -194,7 +194,7 @@ Errors:
AlreadyExists: Экземпляр уже существует
NotChanged: Экземпляр не изменён
Org:
AlreadyExists: Название организации уже занято
AlreadyExists: Название организации или идентификатор уже занят
Invalid: Организация недействительна
AlreadyDeactivated: Организация уже деактивирована
AlreadyActive: Организация уже активна

View File

@@ -194,7 +194,7 @@ Errors:
AlreadyExists: Instans finns redan
NotChanged: Instans ändrades inte
Org:
AlreadyExists: Organisationens namn är redan taget
AlreadyExists: Organisationens namn eller ID är redan upptaget
Invalid: Organisationen är ogiltigt
AlreadyDeactivated: Organisation är redan avaktiverad
AlreadyActive: Organisationen är redan aktiv

View File

@@ -194,7 +194,7 @@ Errors:
AlreadyExists: 实例已经存在
NotChanged: 实例没有改变
Org:
AlreadyExists: 组织名称已被占用
AlreadyExists: 组织名称或 ID 已被占用
Invalid: 组织无效
AlreadyDeactivated: 组织已停用
AlreadyActive: 组织已处于启用状态