mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
# Which Problems Are Solved - The eventstore did not support intentionally overwriting the resource owner when creating events for aggregates that may be reused across owners. - Resource owner handling was implicit and could not be controlled per command/event type. - We needed a safe way to distinguish between: - keeping the existing aggregate owner, and - explicitly setting a new owner for specific create-like events. # How the Problems Are Solved - Introduced a new eventstore command type with an explicit enforce_owner flag. - Updated eventstore.commands_to_events and eventstore.push so owner assignment is now explicit: - if enforce_owner is true, the command owner is written - if enforce_owner is false, the existing aggregate owner is retained when present - Added EnforceResourceOwnerCommand and wiring so command types can opt in to enforced owner behavior. - Wired the new behavior through the v3 eventstore push path, including compatibility fallback for older command type mapping. - Added migration/setup changes to register and use the new command type and SQL functions. - Added and updated tests for owner overwrite and aggregate ID reuse scenarios. # Additional Changes - Added small migration/setup robustness improvements related to eventstore setup ordering and helper reuse. - Added focused test coverage for enforced owner behavior and sequencing. - Events that currently allow owner changes (implement EnforceResourceOwner) are: - AddedEvent (action) - GroupAddedEvent - StartedEvent (idp intent) - ProjectAddedEvent - HumanAddedEvent - HumanRegisteredEvent - MachineAddedEvent - CreatedEvent (schema user) # Additional Context - Follow-up for eventstore owner-handling correctness in create flows and aggregate ID reuse cases. - No additional issue link was attached for this change. --------- Co-authored-by: abhishek kumar gupta <[email protected]>
137 lines
4.2 KiB
Go
137 lines
4.2 KiB
Go
package eventstore
|
|
|
|
import (
|
|
"context"
|
|
_ "embed"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/zitadel/logging"
|
|
|
|
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
|
"github.com/zitadel/zitadel/internal/api/authz"
|
|
"github.com/zitadel/zitadel/internal/eventstore"
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
|
)
|
|
|
|
type latestSequence struct {
|
|
aggregate *eventstore.Aggregate
|
|
sequence uint64
|
|
}
|
|
|
|
//go:embed sequences_query.sql
|
|
var latestSequencesStmt string
|
|
|
|
func latestSequences(ctx context.Context, tx database.Transaction, commands []eventstore.Command) ([]*latestSequence, error) {
|
|
sequences := commandsToSequences(ctx, commands)
|
|
|
|
conditions, args := sequencesToSql(sequences)
|
|
rows, err := tx.Query(ctx, fmt.Sprintf(latestSequencesStmt, strings.Join(conditions, " UNION ALL ")), args...)
|
|
if err != nil {
|
|
return nil, zerrors.ThrowInternal(err, "V3-5jU5z", "Errors.Internal")
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
if err := scanToSequence(rows, sequences); err != nil {
|
|
return nil, zerrors.ThrowInternal(err, "V3-Ydiwv", "Errors.Internal")
|
|
}
|
|
}
|
|
|
|
if rows.Err() != nil {
|
|
return nil, zerrors.ThrowInternal(rows.Err(), "V3-XApDk", "Errors.Internal")
|
|
}
|
|
return sequences, nil
|
|
}
|
|
|
|
func searchSequenceByCommand(sequences []*latestSequence, command eventstore.Command) *latestSequence {
|
|
return searchSequence(sequences, command.Aggregate().Type, command.Aggregate().ID, command.Aggregate().InstanceID)
|
|
}
|
|
|
|
func searchSequence(sequences []*latestSequence, aggregateType eventstore.AggregateType, aggregateID, instanceID string) *latestSequence {
|
|
for _, sequence := range sequences {
|
|
if sequence.aggregate.Type == aggregateType &&
|
|
sequence.aggregate.ID == aggregateID &&
|
|
sequence.aggregate.InstanceID == instanceID {
|
|
return sequence
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func commandsToSequences(ctx context.Context, commands []eventstore.Command) []*latestSequence {
|
|
sequences := make([]*latestSequence, 0, len(commands))
|
|
|
|
for _, command := range commands {
|
|
if searchSequenceByCommand(sequences, command) != nil {
|
|
continue
|
|
}
|
|
|
|
if command.Aggregate().InstanceID == "" {
|
|
command.Aggregate().InstanceID = authz.GetInstance(ctx).InstanceID()
|
|
}
|
|
sequences = append(sequences, &latestSequence{
|
|
aggregate: command.Aggregate(),
|
|
})
|
|
}
|
|
|
|
return sequences
|
|
}
|
|
|
|
const argsPerCondition = 3
|
|
|
|
func sequencesToSql(sequences []*latestSequence) (conditions []string, args []any) {
|
|
args = make([]interface{}, 0, len(sequences)*argsPerCondition)
|
|
conditions = make([]string, len(sequences))
|
|
|
|
for i, sequence := range sequences {
|
|
conditions[i] = fmt.Sprintf(`(SELECT instance_id, aggregate_type, aggregate_id, "sequence" FROM eventstore.events2 WHERE instance_id = $%d AND aggregate_type = $%d AND aggregate_id = $%d ORDER BY "sequence" DESC LIMIT 1)`,
|
|
i*argsPerCondition+1,
|
|
i*argsPerCondition+2,
|
|
i*argsPerCondition+3,
|
|
)
|
|
args = append(args, sequence.aggregate.InstanceID, sequence.aggregate.Type, sequence.aggregate.ID)
|
|
}
|
|
|
|
return conditions, args
|
|
}
|
|
|
|
func scanToSequence(rows database.Rows, sequences []*latestSequence) error {
|
|
var aggregateType eventstore.AggregateType
|
|
var aggregateID, instanceID string
|
|
var currentSequence uint64
|
|
var resourceOwner string
|
|
|
|
if err := rows.Scan(&instanceID, &resourceOwner, &aggregateType, &aggregateID, ¤tSequence); err != nil {
|
|
return zerrors.ThrowInternal(err, "V3-OIWqj", "Errors.Internal")
|
|
}
|
|
|
|
sequence := searchSequence(sequences, aggregateType, aggregateID, instanceID)
|
|
if sequence == nil {
|
|
logging.WithFields(
|
|
"aggType", aggregateType,
|
|
"aggID", aggregateID,
|
|
"instance", instanceID,
|
|
).Panic("no sequence found")
|
|
// added return for linting
|
|
return nil
|
|
}
|
|
sequence.sequence = currentSequence
|
|
if resourceOwner != "" && sequence.aggregate.ResourceOwner != "" && sequence.aggregate.ResourceOwner != resourceOwner {
|
|
logging.WithFields(
|
|
"current_sequence", sequence.sequence,
|
|
"instance_id", sequence.aggregate.InstanceID,
|
|
"agg_type", sequence.aggregate.Type,
|
|
"agg_id", sequence.aggregate.ID,
|
|
"current_owner", resourceOwner,
|
|
"provided_owner", sequence.aggregate.ResourceOwner,
|
|
).Info("would have set wrong resource owner")
|
|
}
|
|
// set resource owner from previous events
|
|
if resourceOwner != "" {
|
|
sequence.aggregate.ResourceOwner = resourceOwner
|
|
}
|
|
|
|
return nil
|
|
}
|