fix(cache): prevent org cache overwrite by other instances (#10012)

# Which Problems Are Solved

A customer reported that randomly certain login flows, such as automatic
redirect to the only configured IdP would not work. During the
investigation it was discovered that they used that same primary domain
on two different instances. As they used the domain for preselecting the
organization, one would always overwrite the other in the cache. Since
The organization and especially it's policies could not be retrieved on
the other instance, it would fallback to the default organization
settings, where the external login and the corresponding IdP were not
configured.

# How the Problems Are Solved

Include the instance id in the cache key for organizations to prevent
overwrites.

# Additional Changes

None

# Additional Context

- found because of a support request
- requires backport to 2.70.x, 2.71.x and 3.x
This commit is contained in:
Livio Spring
2025-06-03 14:48:15 +02:00
committed by GitHub
parent ae1a2e93c1
commit 15902f5bc7
3 changed files with 23 additions and 5 deletions

View File

@@ -12,6 +12,7 @@ import (
"github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/authz"
domain_pkg "github.com/zitadel/zitadel/internal/domain" domain_pkg "github.com/zitadel/zitadel/internal/domain"
es "github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/eventstore/handler/v2"
"github.com/zitadel/zitadel/internal/feature" "github.com/zitadel/zitadel/internal/feature"
"github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/query/projection"
@@ -77,6 +78,8 @@ type Org struct {
ResourceOwner string ResourceOwner string
State domain_pkg.OrgState State domain_pkg.OrgState
Sequence uint64 Sequence uint64
// instanceID is used to create a unique cache key for the org
instanceID string
Name string Name string
Domain string Domain string
@@ -122,7 +125,7 @@ func (q *Queries) OrgByID(ctx context.Context, shouldTriggerBulk bool, id string
ctx, span := tracing.NewSpan(ctx) ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }() defer func() { span.EndWithError(err) }()
if org, ok := q.caches.org.Get(ctx, orgIndexByID, id); ok { if org, ok := q.caches.org.Get(ctx, orgIndexByID, orgCacheKey(authz.GetInstance(ctx).InstanceID(), id)); ok {
return org, nil return org, nil
} }
defer func() { defer func() {
@@ -159,6 +162,7 @@ func (q *Queries) OrgByID(ctx context.Context, shouldTriggerBulk bool, id string
ResourceOwner: foundOrg.Owner, ResourceOwner: foundOrg.Owner,
State: domain_pkg.OrgState(foundOrg.State.State), State: domain_pkg.OrgState(foundOrg.State.State),
Sequence: uint64(foundOrg.Sequence), Sequence: uint64(foundOrg.Sequence),
instanceID: foundOrg.InstanceID,
Name: foundOrg.Name, Name: foundOrg.Name,
Domain: foundOrg.PrimaryDomain.Domain, Domain: foundOrg.PrimaryDomain.Domain,
}, nil }, nil
@@ -195,7 +199,7 @@ func (q *Queries) OrgByPrimaryDomain(ctx context.Context, domain string) (org *O
ctx, span := tracing.NewSpan(ctx) ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }() defer func() { span.EndWithError(err) }()
org, ok := q.caches.org.Get(ctx, orgIndexByPrimaryDomain, domain) org, ok := q.caches.org.Get(ctx, orgIndexByPrimaryDomain, orgCacheKey(authz.GetInstance(ctx).InstanceID(), domain))
if ok { if ok {
return org, nil return org, nil
} }
@@ -430,6 +434,7 @@ func prepareOrgQuery() (sq.SelectBuilder, func(*sql.Row) (*Org, error)) {
OrgColumnResourceOwner.identifier(), OrgColumnResourceOwner.identifier(),
OrgColumnState.identifier(), OrgColumnState.identifier(),
OrgColumnSequence.identifier(), OrgColumnSequence.identifier(),
OrgColumnInstanceID.identifier(),
OrgColumnName.identifier(), OrgColumnName.identifier(),
OrgColumnDomain.identifier(), OrgColumnDomain.identifier(),
). ).
@@ -444,6 +449,7 @@ func prepareOrgQuery() (sq.SelectBuilder, func(*sql.Row) (*Org, error)) {
&o.ResourceOwner, &o.ResourceOwner,
&o.State, &o.State,
&o.Sequence, &o.Sequence,
&o.instanceID,
&o.Name, &o.Name,
&o.Domain, &o.Domain,
) )
@@ -521,15 +527,21 @@ const (
func (o *Org) Keys(index orgIndex) []string { func (o *Org) Keys(index orgIndex) []string {
switch index { switch index {
case orgIndexByID: case orgIndexByID:
return []string{o.ID} return []string{orgCacheKey(o.instanceID, o.ID)}
case orgIndexByPrimaryDomain: case orgIndexByPrimaryDomain:
return []string{o.Domain} return []string{orgCacheKey(o.instanceID, o.Domain)}
case orgIndexUnspecified: case orgIndexUnspecified:
} }
return nil return nil
} }
func orgCacheKey(instanceID, key string) string {
return instanceID + "-" + key
}
func (c *Caches) registerOrgInvalidation() { func (c *Caches) registerOrgInvalidation() {
invalidate := cacheInvalidationFunc(c.org, orgIndexByID, getAggregateID) invalidate := cacheInvalidationFunc(c.org, orgIndexByID, func(aggregate *es.Aggregate) string {
return orgCacheKey(aggregate.InstanceID, aggregate.ID)
})
projection.OrgProjection.RegisterCacheInvalidation(invalidate) projection.OrgProjection.RegisterCacheInvalidation(invalidate)
} }

View File

@@ -50,6 +50,7 @@ var (
` projections.orgs1.resource_owner,` + ` projections.orgs1.resource_owner,` +
` projections.orgs1.org_state,` + ` projections.orgs1.org_state,` +
` projections.orgs1.sequence,` + ` projections.orgs1.sequence,` +
` projections.orgs1.instance_id,` +
` projections.orgs1.name,` + ` projections.orgs1.name,` +
` projections.orgs1.primary_domain` + ` projections.orgs1.primary_domain` +
` FROM projections.orgs1` ` FROM projections.orgs1`
@@ -60,6 +61,7 @@ var (
"resource_owner", "resource_owner",
"org_state", "org_state",
"sequence", "sequence",
"instance_id",
"name", "name",
"primary_domain", "primary_domain",
} }
@@ -242,6 +244,7 @@ func Test_OrgPrepares(t *testing.T) {
"ro", "ro",
domain.OrgStateActive, domain.OrgStateActive,
uint64(20211108), uint64(20211108),
"instance-id",
"org-name", "org-name",
"zitadel.ch", "zitadel.ch",
}, },
@@ -254,6 +257,7 @@ func Test_OrgPrepares(t *testing.T) {
ResourceOwner: "ro", ResourceOwner: "ro",
State: domain.OrgStateActive, State: domain.OrgStateActive,
Sequence: 20211108, Sequence: 20211108,
instanceID: "instance-id",
Name: "org-name", Name: "org-name",
Domain: "zitadel.ch", Domain: "zitadel.ch",
}, },

View File

@@ -18,6 +18,7 @@ type Org struct {
CreationDate time.Time CreationDate time.Time
ChangeDate time.Time ChangeDate time.Time
Owner string Owner string
InstanceID string
} }
func NewOrg(id string) *Org { func NewOrg(id string) *Org {
@@ -60,6 +61,7 @@ func (rm *Org) Reduce(events ...*eventstore.StorageEvent) error {
} }
rm.Sequence = event.Sequence rm.Sequence = event.Sequence
rm.ChangeDate = event.CreatedAt rm.ChangeDate = event.CreatedAt
rm.InstanceID = event.Aggregate.Instance
} }
if err := rm.State.Reduce(events...); err != nil { if err := rm.State.Reduce(events...); err != nil {
return err return err