mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-03 21:02:15 +00:00
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]>
This commit is contained in:
co-authored by
abhishek kumar gupta
parent
4459a1380c
commit
6082e59d47
+107
-43
@@ -1,55 +1,119 @@
|
||||
CREATE OR REPLACE FUNCTION eventstore.commands_to_events(commands eventstore.command[])
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE eventstore.latest_command AS (
|
||||
instance_id TEXT
|
||||
, aggregate_type TEXT
|
||||
, aggregate_id TEXT
|
||||
, owner TEXT
|
||||
, sequence BIGINT
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION eventstore.commands_to_events(commands eventstore.command2[])
|
||||
RETURNS SETOF eventstore.events2
|
||||
LANGUAGE 'plpgsql'
|
||||
STABLE PARALLEL SAFE
|
||||
ROWS 10
|
||||
AS $$
|
||||
DECLARE
|
||||
"aggregate" RECORD;
|
||||
current_sequence BIGINT;
|
||||
current_owner TEXT;
|
||||
created_at TIMESTAMPTZ;
|
||||
_latest_events eventstore.latest_command[];
|
||||
command eventstore.command2;
|
||||
last_owner TEXT;
|
||||
i INTEGER;
|
||||
BEGIN
|
||||
created_at := statement_timestamp();
|
||||
FOR "aggregate" IN
|
||||
SELECT DISTINCT
|
||||
instance_id
|
||||
, aggregate_type
|
||||
, aggregate_id
|
||||
FROM UNNEST(commands)
|
||||
LOOP
|
||||
SELECT
|
||||
*
|
||||
INTO
|
||||
current_sequence
|
||||
, current_owner
|
||||
FROM eventstore.latest_aggregate_state(
|
||||
"aggregate".instance_id
|
||||
, "aggregate".aggregate_type
|
||||
, "aggregate".aggregate_id
|
||||
);
|
||||
|
||||
RETURN QUERY
|
||||
SELECT array_agg(res) INTO _latest_events
|
||||
FROM (
|
||||
SELECT
|
||||
c.instance_id
|
||||
, c.aggregate_type
|
||||
, c.aggregate_id
|
||||
, c.command_type -- AS event_type
|
||||
, COALESCE(current_sequence, 0) + ROW_NUMBER() OVER () -- AS sequence
|
||||
, c.revision
|
||||
, created_at
|
||||
, c.payload
|
||||
, c.creator
|
||||
, COALESCE(current_owner, c.owner) -- AS owner
|
||||
, EXTRACT(EPOCH FROM created_at) -- AS position
|
||||
, c.ordinality::%s -- AS in_tx_order
|
||||
FROM
|
||||
UNNEST(commands) WITH ORDINALITY AS c
|
||||
WHERE
|
||||
c.instance_id = aggregate.instance_id
|
||||
AND c.aggregate_type = aggregate.aggregate_type
|
||||
AND c.aggregate_id = aggregate.aggregate_id;
|
||||
ROW(
|
||||
c.instance_id,
|
||||
c.aggregate_type,
|
||||
c.aggregate_id,
|
||||
COALESCE(e.owner, c.owner),
|
||||
COALESCE(e.sequence, 0)
|
||||
)::eventstore.latest_command AS res
|
||||
FROM (
|
||||
SELECT DISTINCT ON (c.instance_id, c.aggregate_type, c.aggregate_id)
|
||||
c.instance_id,
|
||||
c.aggregate_type,
|
||||
c.aggregate_id,
|
||||
c.owner,
|
||||
c.ordinality
|
||||
FROM (
|
||||
SELECT *
|
||||
FROM UNNEST(commands) WITH ORDINALITY AS c
|
||||
) AS c
|
||||
ORDER BY
|
||||
c.instance_id,
|
||||
c.aggregate_type,
|
||||
c.aggregate_id,
|
||||
c.ordinality
|
||||
) AS c
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
e.owner,
|
||||
e.sequence
|
||||
FROM eventstore.events2 AS e
|
||||
WHERE e.instance_id = c.instance_id
|
||||
AND e.aggregate_type = c.aggregate_type
|
||||
AND e.aggregate_id = c.aggregate_id
|
||||
ORDER BY e.sequence DESC
|
||||
LIMIT 1
|
||||
) AS e ON TRUE
|
||||
) AS latest;
|
||||
|
||||
FOR command IN SELECT * FROM UNNEST(commands) LOOP
|
||||
IF NOT command.enforce_owner THEN
|
||||
SELECT owner INTO command.owner
|
||||
FROM UNNEST(_latest_events) AS e
|
||||
WHERE e.instance_id = command.instance_id
|
||||
AND e.aggregate_type = command.aggregate_type
|
||||
AND e.aggregate_id = command.aggregate_id;
|
||||
ELSE
|
||||
FOR i IN 1..array_length(_latest_events, 1) LOOP
|
||||
IF _latest_events[i].instance_id = command.instance_id
|
||||
AND _latest_events[i].aggregate_type = command.aggregate_type
|
||||
AND _latest_events[i].aggregate_id = command.aggregate_id
|
||||
THEN
|
||||
_latest_events[i].owner := command.owner;
|
||||
EXIT;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END IF;
|
||||
|
||||
END LOOP;
|
||||
RETURN;
|
||||
|
||||
|
||||
RETURN QUERY SELECT
|
||||
c.instance_id
|
||||
, c.aggregate_type
|
||||
, c.aggregate_id
|
||||
, c.command_type AS event_type
|
||||
, COALESCE(e.sequence, 0) + ROW_NUMBER() OVER (PARTITION BY c.instance_id, c.aggregate_type, c.aggregate_id ORDER BY c.ordinality) AS sequence
|
||||
, c.revision
|
||||
, NOW() AS created_at
|
||||
, c.payload
|
||||
, c.creator
|
||||
, CASE WHEN c.enforce_owner
|
||||
THEN c.owner
|
||||
ELSE COALESCE(e.owner, c.owner)
|
||||
END AS owner
|
||||
, EXTRACT(EPOCH FROM NOW()) AS position
|
||||
, c.ordinality::%s AS in_tx_order
|
||||
FROM UNNEST(commands) WITH ORDINALITY AS c
|
||||
LEFT JOIN unnest(_latest_events) AS e
|
||||
ON c.instance_id = e.instance_id
|
||||
AND c.aggregate_type = e.aggregate_type
|
||||
AND c.aggregate_id = e.aggregate_id
|
||||
ORDER BY
|
||||
in_tx_order;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION eventstore.push(commands eventstore.command2[]) RETURNS SETOF eventstore.events2 VOLATILE AS $$
|
||||
INSERT INTO eventstore.events2
|
||||
SELECT * FROM eventstore.commands_to_events(commands)
|
||||
ORDER BY in_tx_order
|
||||
RETURNING *
|
||||
$$ LANGUAGE SQL;
|
||||
Reference in New Issue
Block a user