Preserve reviewer decisions on access-entry upsert

Subsequent fetches of the same source re-upsert every access entry.
Without a guard, a reviewer's (or an agent's) decision, its note, and
the flags that drove it would be silently replaced on every poll.

The ON CONFLICT DO UPDATE SET clause now refreshes only the columns
that track live source state (email, full_name, role, is_admin, MFA,
etc.) and deliberately omits the verdict bundle -- flags, flag_reasons,
decision, decision_note, decided_by, decided_at -- so the audit trail
stays consistent with whatever the reviewer or agent last recorded.

Tighten the signature from pg.Querier to pg.Tx to match the rest of
the coredata package -- every caller already runs inside WithTx.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
This commit is contained in:
Aurélien Sibiril
2026-04-17 10:46:19 +02:00
parent da80ce3eb1
commit f464bac29d
2 changed files with 463 additions and 15 deletions

View File

@@ -572,9 +572,16 @@ WHERE
return nil
}
// Upsert inserts the entry or refreshes the source-tracking fields from the
// caller. Columns that capture a reviewer's or an agent's verdict -- flags,
// flag_reasons, decision, decision_note, decided_by, decided_at -- are
// intentionally absent from the ON CONFLICT DO UPDATE SET clause, so an
// existing row's verdict survives every subsequent source poll untouched.
// Those columns are written on the initial INSERT (new row) and can only be
// changed afterwards through AccessEntry.Update.
func (e *AccessEntry) Upsert(
ctx context.Context,
conn pg.Querier,
conn pg.Tx,
scope Scoper,
) error {
q := `
@@ -636,20 +643,21 @@ INSERT INTO access_entries (
@updated_at
)
ON CONFLICT (access_review_campaign_id, access_source_id, account_key) DO UPDATE SET
email = EXCLUDED.email,
full_name = EXCLUDED.full_name,
role = EXCLUDED.role,
job_title = EXCLUDED.job_title,
is_admin = EXCLUDED.is_admin,
mfa_status = EXCLUDED.mfa_status,
auth_method = EXCLUDED.auth_method,
account_type = EXCLUDED.account_type,
last_login = EXCLUDED.last_login,
email = EXCLUDED.email,
full_name = EXCLUDED.full_name,
role = EXCLUDED.role,
job_title = EXCLUDED.job_title,
is_admin = EXCLUDED.is_admin,
mfa_status = EXCLUDED.mfa_status,
auth_method = EXCLUDED.auth_method,
account_type = EXCLUDED.account_type,
last_login = EXCLUDED.last_login,
account_created_at = EXCLUDED.account_created_at,
external_id = EXCLUDED.external_id,
incremental_tag = EXCLUDED.incremental_tag,
updated_at = EXCLUDED.updated_at
external_id = EXCLUDED.external_id,
incremental_tag = EXCLUDED.incremental_tag,
updated_at = EXCLUDED.updated_at
`
args := pgx.StrictNamedArgs{
"id": e.ID,
"tenant_id": scope.GetTenantID(),
@@ -680,8 +688,7 @@ ON CONFLICT (access_review_campaign_id, access_source_id, account_key) DO UPDATE
"updated_at": e.UpdatedAt,
}
_, err := conn.Exec(ctx, q, args)
if err != nil {
if _, err := conn.Exec(ctx, q, args); err != nil {
return fmt.Errorf("cannot upsert access entry: %w", err)
}