diff --git a/cmd/setup/79/07_appname.sql b/cmd/setup/79/07_appname.sql index a4d7b92651..ec58148eb8 100644 --- a/cmd/setup/79/07_appname.sql +++ b/cmd/setup/79/07_appname.sql @@ -1,8 +1,13 @@ -- unique_field: name:projectID (project.NewAddApplicationUniqueConstraint) UPDATE eventstore.unique_constraints uc -SET owners = ARRAY['org:' || a.resource_owner, 'project:' || a.project_id] +SET owners = ( + CASE WHEN uc.owners = '{}' + THEN ARRAY['org:' || a.resource_owner, 'project:' || a.project_id] + ELSE uc.owners + END +) || ARRAY['app:' || a.id] FROM {{.apps}} a WHERE uc.unique_type = 'appname' AND uc.instance_id = a.instance_id - AND uc.owners = '{}' + AND NOT (uc.owners @> ARRAY['app:' || a.id]) AND uc.unique_field IN (a.name || ':' || a.project_id, lower(a.name || ':' || a.project_id)); diff --git a/cmd/setup/79/09_entity_ids.sql b/cmd/setup/79/09_entity_ids.sql index cd741bd139..f0394eb9ae 100644 --- a/cmd/setup/79/09_entity_ids.sql +++ b/cmd/setup/79/09_entity_ids.sql @@ -1,9 +1,14 @@ -- unique_field: entityID (project.NewAddSAMLConfigEntityIDUniqueConstraint) UPDATE eventstore.unique_constraints uc -SET owners = ARRAY['org:' || a.resource_owner, 'project:' || a.project_id] +SET owners = ( + CASE WHEN uc.owners = '{}' + THEN ARRAY['org:' || a.resource_owner, 'project:' || a.project_id] + ELSE uc.owners + END +) || ARRAY['app:' || a.id] FROM {{.apps}} a JOIN {{.apps_saml}} s ON s.app_id = a.id AND s.instance_id = a.instance_id WHERE uc.unique_type = 'entity_ids' AND uc.instance_id = a.instance_id - AND uc.owners = '{}' + AND NOT (uc.owners @> ARRAY['app:' || a.id]) AND uc.unique_field IN (s.entity_id, lower(s.entity_id)); diff --git a/cmd/setup/79_test.go b/cmd/setup/79_test.go index e935373350..5ef876b6f6 100644 --- a/cmd/setup/79_test.go +++ b/cmd/setup/79_test.go @@ -32,9 +32,9 @@ func TestBackfillUniqueConstraintOwnersStmts(t *testing.T) { {"04_org_name.sql", "org_name", "o.name", projection.OrgProjectionTable, nil}, {"05_org_domain.sql", "org_domain", "d.domain", projection.OrgDomainTable, []string{"d.is_verified"}}, {"06_project_names.sql", "project_names", "p.name || p.resource_owner", projection.ProjectProjectionTable, nil}, - {"07_appname.sql", "appname", "a.name || ':' || a.project_id", projection.AppProjectionTable, nil}, + {"07_appname.sql", "appname", "a.name || ':' || a.project_id", projection.AppProjectionTable, []string{"'app:' || a.id"}}, {"08_project_role.sql", "project_role", "r.role_key || ':' || r.project_id", projection.ProjectRoleProjectionTable, nil}, - {"09_entity_ids.sql", "entity_ids", "s.entity_id", projection.AppProjectionTable, []string{projection.AppSAMLTable}}, + {"09_entity_ids.sql", "entity_ids", "s.entity_id", projection.AppProjectionTable, []string{projection.AppSAMLTable, "'app:' || a.id"}}, {"10_project_grant.sql", "project_grant", "g.granted_org_id || ':' || g.project_id", projection.ProjectGrantProjectionTable, []string{"'org:' || g.granted_org_id", "'grant:' || g.grant_id"}}, {"11_project_grant_member.sql", "project_grant_member", "m.project_id || ':' || m.user_id || ':' || m.grant_id", projection.ProjectGrantMemberProjectionTable, []string{"'grant:' || m.grant_id", "m.user_resource_owner", "m.granted_org"}}, {"12_user_grant.sql", "user_grant", "g.resource_owner || ':' || g.user_id || ':' || g.project_id || ':' || g.grant_id", projection.UserGrantProjectionTable, []string{"'grant:' || g.grant_id", "g.resource_owner_user", "g.granted_org"}}, diff --git a/internal/command/project_application.go b/internal/command/project_application.go index 465b12e1e1..34a44b9d9d 100644 --- a/internal/command/project_application.go +++ b/internal/command/project_application.go @@ -139,13 +139,23 @@ func (c *Commands) RemoveApplication(ctx context.Context, projectID, appID, reso projectAgg := ProjectAggregateFromWriteModel(&existingApp.WriteModel) - entityID := "" - samlWriteModel, err := c.getSAMLAppWriteModel(ctx, projectID, appID, resourceOwner) - if err == nil && samlWriteModel.State != domain.AppStateUnspecified && samlWriteModel.State != domain.AppStateRemoved && samlWriteModel.saml { - entityID = samlWriteModel.EntityID + ownerDeleteReady, err := c.isUniqueConstraintOwnerDeleteReady(ctx) + if err != nil { + return nil, err + } + var removedEvent *project.ApplicationRemovedEvent + if ownerDeleteReady { + removedEvent = project.NewApplicationRemovedByOwnerEvent(ctx, projectAgg, appID) + } else { + entityID := "" + samlWriteModel, err := c.getSAMLAppWriteModel(ctx, projectID, appID, resourceOwner) + if err == nil && samlWriteModel.State != domain.AppStateUnspecified && samlWriteModel.State != domain.AppStateRemoved && samlWriteModel.saml { + entityID = samlWriteModel.EntityID + } + removedEvent = project.NewApplicationRemovedEvent(ctx, projectAgg, appID, existingApp.Name, entityID) } - pushedEvents, err := c.eventstore.Push(ctx, project.NewApplicationRemovedEvent(ctx, projectAgg, appID, existingApp.Name, entityID)) + pushedEvents, err := c.eventstore.Push(ctx, removedEvent) if err != nil { return nil, err } diff --git a/internal/command/project_application_test.go b/internal/command/project_application_test.go index a67e6886ed..174fac7ff0 100644 --- a/internal/command/project_application_test.go +++ b/internal/command/project_application_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -678,3 +679,29 @@ func TestCommandSide_RemoveApplication(t *testing.T) { }) } } + +func TestCommandSide_RemoveApplicationOwnerDeleteReady(t *testing.T) { + r := &Commands{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + )), + ), + expectFilter(), + expectPush( + project.NewApplicationRemovedByOwnerEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + ), + ), + )(t), + checkPermission: newMockPermissionCheckAllowed(), + ownerDeleteReady: func(context.Context) (bool, error) { return true, nil }, + } + got, err := r.RemoveApplication(context.Background(), "project1", "app1", "org1") + require.NoError(t, err) + assertObjectDetails(t, &domain.ObjectDetails{ResourceOwner: "org1"}, got) +} diff --git a/internal/eventstore/unique_constraint.go b/internal/eventstore/unique_constraint.go index 0c0af463c7..b3d1a4ab8c 100644 --- a/internal/eventstore/unique_constraint.go +++ b/internal/eventstore/unique_constraint.go @@ -32,6 +32,7 @@ const ( UniqueConstraintOwnerIDP = "idp" UniqueConstraintOwnerProject = "project" UniqueConstraintOwnerGrant = "grant" + UniqueConstraintOwnerApp = "app" // UniqueConstraintOwnersBackfillStep is the setup migration that stamps owners from projections. UniqueConstraintOwnersBackfillStep = "79_backfill_unique_constraint_owners" diff --git a/internal/repository/project/application.go b/internal/repository/project/application.go index 24b2d527c2..2fd85c4db7 100644 --- a/internal/repository/project/application.go +++ b/internal/repository/project/application.go @@ -43,7 +43,7 @@ func (e *ApplicationAddedEvent) Payload() interface{} { } func (e *ApplicationAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { - return []*eventstore.UniqueConstraint{NewAddApplicationUniqueConstraint(e.Name, e.Aggregate().ID).WithOwners(projectOwnerTags(e.Aggregate())...)} + return []*eventstore.UniqueConstraint{NewAddApplicationUniqueConstraint(e.Name, e.Aggregate().ID).WithOwners(appOwnerTags(e.Aggregate(), e.AppID)...)} } func NewApplicationAddedEvent( @@ -91,7 +91,7 @@ func (e *ApplicationChangedEvent) Payload() interface{} { func (e *ApplicationChangedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { return []*eventstore.UniqueConstraint{ NewRemoveApplicationUniqueConstraint(e.oldName, e.Aggregate().ID), - NewAddApplicationUniqueConstraint(e.Name, e.Aggregate().ID).WithOwners(projectOwnerTags(e.Aggregate())...), + NewAddApplicationUniqueConstraint(e.Name, e.Aggregate().ID).WithOwners(appOwnerTags(e.Aggregate(), e.AppID)...), } } @@ -217,6 +217,8 @@ type ApplicationRemovedEvent struct { AppID string `json:"appId,omitempty"` name string entityID string + // removeByOwner deletes appname and entity_ids by app id after setup 79 has stamped that tag. + removeByOwner bool } func (e *ApplicationRemovedEvent) Payload() interface{} { @@ -224,6 +226,11 @@ func (e *ApplicationRemovedEvent) Payload() interface{} { } func (e *ApplicationRemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + if e.removeByOwner { + return []*eventstore.UniqueConstraint{ + eventstore.NewRemoveUniqueConstraintsByOwner(eventstore.UniqueConstraintOwnerApp, e.AppID), + } + } remove := []*eventstore.UniqueConstraint{NewRemoveApplicationUniqueConstraint(e.name, e.Aggregate().ID)} if e.entityID != "" { remove = append(remove, NewRemoveSAMLConfigEntityIDUniqueConstraint(e.entityID)) @@ -250,6 +257,17 @@ func NewApplicationRemovedEvent( } } +// NewApplicationRemovedByOwnerEvent removes every unique constraint tagged with the app id. +func NewApplicationRemovedByOwnerEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + appID string, +) *ApplicationRemovedEvent { + removed := NewApplicationRemovedEvent(ctx, aggregate, appID, "", "") + removed.removeByOwner = true + return removed +} + func ApplicationRemovedEventMapper(event eventstore.Event) (eventstore.Event, error) { e := &ApplicationRemovedEvent{ BaseEvent: *eventstore.BaseEventFromRepo(event), diff --git a/internal/repository/project/project.go b/internal/repository/project/project.go index 848438e00d..3344708905 100644 --- a/internal/repository/project/project.go +++ b/internal/repository/project/project.go @@ -60,6 +60,10 @@ func projectOwnerTags(agg *eventstore.Aggregate) []string { } } +func appOwnerTags(agg *eventstore.Aggregate, appID string) []string { + return append(projectOwnerTags(agg), eventstore.OwnerTag(eventstore.UniqueConstraintOwnerApp, appID)) +} + func grantOwnerTags(agg *eventstore.Aggregate, grantID, grantedOrgID string) []string { return []string{ eventstore.OwnerTag(eventstore.UniqueConstraintOwnerOrg, agg.ResourceOwner), diff --git a/internal/repository/project/saml_config.go b/internal/repository/project/saml_config.go index 06f621ab5e..eefe8dfadd 100644 --- a/internal/repository/project/saml_config.go +++ b/internal/repository/project/saml_config.go @@ -43,7 +43,7 @@ func NewRemoveSAMLConfigEntityIDUniqueConstraint(entityID string) *eventstore.Un } func (e *SAMLConfigAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { - return []*eventstore.UniqueConstraint{NewAddSAMLConfigEntityIDUniqueConstraint(e.EntityID).WithOwners(projectOwnerTags(e.Aggregate())...)} + return []*eventstore.UniqueConstraint{NewAddSAMLConfigEntityIDUniqueConstraint(e.EntityID).WithOwners(appOwnerTags(e.Aggregate(), e.AppID)...)} } func NewSAMLConfigAddedEvent( @@ -104,7 +104,7 @@ func (e *SAMLConfigChangedEvent) UniqueConstraints() []*eventstore.UniqueConstra if e.EntityID != "" { return []*eventstore.UniqueConstraint{ NewRemoveSAMLConfigEntityIDUniqueConstraint(e.oldEntityID), - NewAddSAMLConfigEntityIDUniqueConstraint(e.EntityID).WithOwners(projectOwnerTags(e.Aggregate())...), + NewAddSAMLConfigEntityIDUniqueConstraint(e.EntityID).WithOwners(appOwnerTags(e.Aggregate(), e.AppID)...), } } return nil diff --git a/internal/repository/unique_constraint_owners_test.go b/internal/repository/unique_constraint_owners_test.go index 7ded41ca30..dc9ad63db5 100644 --- a/internal/repository/unique_constraint_owners_test.go +++ b/internal/repository/unique_constraint_owners_test.go @@ -53,7 +53,7 @@ func TestUniqueConstraintOwners(t *testing.T) { { name: "app name", cmd: project.NewApplicationAddedEvent(ctx, agg(project.AggregateType, "project-1", "org-1"), "app-1", "console"), - want: [][]string{{"org:org-1", "project:project-1"}}, + want: [][]string{{"org:org-1", "project:project-1", "app:app-1"}}, }, { name: "project role", @@ -63,7 +63,7 @@ func TestUniqueConstraintOwners(t *testing.T) { { name: "saml entity id", cmd: project.NewSAMLConfigAddedEvent(ctx, agg(project.AggregateType, "project-1", "org-1"), "app-1", "https://sp.example", nil, "", 0, ""), - want: [][]string{{"org:org-1", "project:project-1"}}, + want: [][]string{{"org:org-1", "project:project-1", "app:app-1"}}, }, { name: "project name", diff --git a/internal/v2/eventstore/unique_constraint.go b/internal/v2/eventstore/unique_constraint.go index 96f7cdb498..e33f399014 100644 --- a/internal/v2/eventstore/unique_constraint.go +++ b/internal/v2/eventstore/unique_constraint.go @@ -32,6 +32,7 @@ const ( UniqueConstraintOwnerIDP = "idp" UniqueConstraintOwnerProject = "project" UniqueConstraintOwnerGrant = "grant" + UniqueConstraintOwnerApp = "app" ) func (f UniqueConstraintAction) Valid() bool {