Files
zitadel/internal/eventstore/event.go
T
6082e59d47 fix(eventstore): allow overwriting resource owner of events (#12261)
# 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]>
2026-06-15 11:24:37 +02:00

181 lines
4.8 KiB
Go

package eventstore
import (
"encoding/json"
"log/slog"
"maps"
"reflect"
"slices"
"time"
"github.com/shopspring/decimal"
"github.com/zitadel/zitadel/internal/zerrors"
)
type action interface {
Aggregate() *Aggregate
// Creator is the userid of the user which created the action
Creator() string
// Type describes the action
Type() EventType
// Revision of the action
Revision() uint16
}
// Command is the intent to store an event into the eventstore
type Command interface {
action
// Payload returns the payload of the event. It represents the changed fields by the event
// valid types are:
// * nil: no payload
// * struct: which can be marshalled to json
// * pointer: to struct which can be marshalled to json
// * []byte: json marshalled data
Payload() any
// UniqueConstraints should be added for unique attributes of an event, if nil constraints will not be checked
UniqueConstraints() []*UniqueConstraint
// Fields should be added for fields which should be indexed for lookup, if nil fields will not be indexed
Fields() []*FieldOperation
}
type EnforceResourceOwnerCommand interface {
Command
// EnforceResourceOwner indicates that the resource owner of the event does not respect the previous events and should be enforced by the eventstore.
EnforceResourceOwner()
}
func ShouldEnforceResourceOwner(cmd Command) bool {
_, ok := cmd.(EnforceResourceOwnerCommand)
return ok
}
// Event is a stored activity
type Event interface {
action
// Sequence of the event in the aggregate
Sequence() uint64
// CreatedAt is the time the event was created at
CreatedAt() time.Time
// Position is the global position of the event
Position() decimal.Decimal
// Unmarshal parses the payload and stores the result
// in the value pointed to by ptr. If ptr is nil or not a pointer,
// Unmarshal returns an error
Unmarshal(ptr any) error
// Deprecated: only use for migration
DataAsBytes() []byte
}
type EventType string
func EventData(event Command) ([]byte, error) {
switch data := event.Payload().(type) {
case nil:
return nil, nil
case []byte:
if json.Valid(data) {
return data, nil
}
return nil, zerrors.ThrowInvalidArgument(nil, "V2-6SbbS", "data bytes are not json")
}
dataType := reflect.TypeOf(event.Payload())
if dataType.Kind() == reflect.Ptr {
dataType = dataType.Elem()
}
if dataType.Kind() == reflect.Struct {
dataBytes, err := json.Marshal(event.Payload())
if err != nil {
return nil, zerrors.ThrowInvalidArgument(err, "V2-xG87M", "could not marshal data")
}
return dataBytes, nil
}
return nil, zerrors.ThrowInvalidArgument(nil, "V2-91NRm", "wrong type of event data")
}
type BaseEventSetter[T any] interface {
Event
SetBaseEvent(*BaseEvent)
*T
}
func GenericEventMapper[T any, PT BaseEventSetter[T]](event Event) (Event, error) {
e := PT(new(T))
e.SetBaseEvent(BaseEventFromRepo(event))
err := event.Unmarshal(e)
if err != nil {
return nil, zerrors.ThrowInternal(err, "ES-Thai6", "unable to unmarshal event")
}
return e, nil
}
func isEventTypes(command Command, types ...EventType) bool {
for _, typ := range types {
if command.Type() == typ {
return true
}
}
return false
}
type logValue struct {
event Event
}
func eventToLogValue(event Event) slog.LogValuer {
return &logValue{
event: event,
}
}
func (lv *logValue) LogValue() slog.Value {
attributes := make([]slog.Attr, 0, 12)
aggregate := lv.event.Aggregate()
attributes = append(attributes,
slog.String("aggregate_id", aggregate.ID),
slog.String("aggregate_type", string(aggregate.Type)),
slog.String("resource_owner", aggregate.ResourceOwner),
slog.String("instance_id", aggregate.InstanceID),
slog.String("version", string(aggregate.Version)),
slog.String("creator", lv.event.Creator()),
slog.String("event_type", string(lv.event.Type())),
slog.Uint64("revision", uint64(lv.event.Revision())),
slog.Uint64("sequence", lv.event.Sequence()),
slog.Time("created_at", lv.event.CreatedAt()),
slog.String("position", lv.event.Position().String()),
)
var m map[string]any
err := lv.event.Unmarshal(&m)
if err != nil {
attributes = append(attributes,
slog.String("msg", "failed to unmarshal event for logging"),
slog.String("err", err.Error()),
)
return slog.GroupValue(attributes...)
}
attributes = append(attributes,
slog.Any("data", mapToLogValue(m)),
)
return slog.GroupValue(attributes...)
}
// mapToLogValue converts a map[string]any to a [slog.Value], handling nested maps recursively.
func mapToLogValue(m map[string]any) slog.Value {
attributes := make([]slog.Attr, 0, len(m))
for _, key := range slices.Sorted(maps.Keys(m)) {
value := m[key]
if nestedMap, ok := value.(map[string]any); ok {
value = mapToLogValue(nestedMap)
}
attributes = append(attributes, slog.Any(key, value))
}
return slog.GroupValue(attributes...)
}