Skip to content

Show deprovisioning event and date in the audit log - #635

Open
kayjoosten wants to merge 7 commits into
mainfrom
feature/issue-423-deprovisioning-audit-log
Open

kayjoosten wants to merge 7 commits into
mainfrom
feature/issue-423-deprovisioning-audit-log

Conversation

@kayjoosten

@kayjoosten kayjoosten commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Why is this change needed?

Prior to this change, deprovisioning an identity via the lifecycle API left no trace in the audit log. AuditLogProjector explicitly skipped writing an entry for IdentityForgottenEvent, so RA(A)s had no way to see that, or when, an identity had been deprovisioned.

How does it address the issue?

Maps IdentityForgottenEvent to a new deprovisioned audit log action, and records that entry before the identity's audit trail is anonymized. The actor name is still intentionally anonymized, consistent with every other audit-log entry for a forgotten identity.

The projector also guards against duplicate deprovisioned rows by skipping creation when an entry already exists for the same identity + event + recorded-on timestamp. That makes replay/backfill idempotent for this event.

The RA-side rendering of this new action (translation for "Deprovisioned") is in OpenConext/Stepup-RA#531 — the audit log template there already renders any action generically, so no other RA change was needed.

Retroactive backfill

This PR ships a dedicated one-shot backfill command for historical IdentityForgottenEvents:

bin/console stepup:audit-log:backfill-deprovisioned

Rollout/runbook notes:

  • run it during deployment before re-enabling live deprovisioning traffic
  • keep lifecycle/deprovisioning API access disabled while it runs; the command and live projector do not coordinate atomically
  • rerunning the command is safe: it inserts only missing deprovisioned rows
  • do not use stepup:event:replay for this backfill

Follow-up

IdentityRestoredEvent audit-log handling stays out of scope for this PR and is tracked separately in #637.

Provide links to any relevant tickets, articles or other resources
Closes OpenConext/Stepup-RA#423

Test plan

  • php vendor/bin/phpunit -c ci/qa/phpunit.xml --filter AuditLogProjectorTest
  • php vendor/bin/phpunit -c ci/qa/phpunit.xml src/Surfnet/StepupMiddleware/MiddlewareBundle/Tests/Console/Command/BackfillDeprovisionedAuditLogEntriesCommandTest.php
  • ./ci/qa/phpmd
  • ./ci/qa/phpcs
  • php vendor/bin/phpstan analyse --memory-limit=-1 --no-ansi -c ./ci/qa/phpstan.neon
  • After deploy: run bin/console stepup:audit-log:backfill-deprovisioned in an environment with already-deprovisioned identities and confirm deprovisioned entries appear with correct date/time

@kayjoosten

Copy link
Copy Markdown
Contributor Author

Update after code review: the initial version wrote the `deprovisioned` entry but `AuditLogRepository::createSecondFactorSearchQuery()` filters against an event allowlist that didn't include `IdentityForgottenEvent`, so it was silently dropped from the RA audit log query — the feature was a no-op. Fixed in the latest commit, along with a corrected (previously backwards) code comment and a test that now mocks `findByIdentityId()` the way the real repository behaves (returning the freshly flushed entry, not omitting it).

Also worth calling out explicitly: `stepup:event:replay` is not idempotent (new UUID per replayed entry), so the retroactive backfill described above should only be run once per environment.

@johanib
johanib self-requested a review August 18, 2026 13:05

@johanib johanib left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, focused change — the ordering rationale holds up: AuditLogRepository::save() flushes, so the re-query in applyIdentityForgottenEvent() genuinely picks up the new entry. Ran AuditLogProjectorTest in an isolated worktree, 4/4 green.

One blocker before this can go out, plus two points worth a decision:

  1. Backfill is not idempotent — the stepup:event:replay instruction in the description will duplicate entries on a second run, or when run after new deprovisionings have already been projected live.
  2. The chosen ordering intentionally erases the deprovisioning actor's name; worth confirming that is what the issue reporter wants.
  3. IdentityRestoredEvent has no counterpart action, so the log can end on "deprovisioned" for an identity that is actually live again.

Details inline.

Comment thread src/Surfnet/StepupMiddleware/ApiBundle/Identity/Projector/AuditLogProjector.php Outdated
@johanib

johanib commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Based on the title, I expected this to be easy 😅
The findings above seems genuine on first read.

One thing I don't understand yet: Why does middleware have a stepup:event:replay AND a middleware:event:replay.

Lets discuss this PR before continuing.

@kayjoosten

Copy link
Copy Markdown
Contributor Author

Take a good look at why there are 2 types of replay. Also we need to take a look at how this will work with mega big data. As far as we know atm the replay is not doable in production.

possible thoughts.

  • how can we split the replay
  • Do we need to do the whole replay
  • how do the replays work now
  • are they idempotent

Why is this change needed?
Prior to this change, deprovisioning an identity via the lifecycle API left no
trace in the audit log. AuditLogProjector explicitly skipped writing an entry
for IdentityForgottenEvent, so RA(A)s had no way to see that, or when, an
identity had been deprovisioned.

How does it address the issue?
This change maps IdentityForgottenEvent to a new 'deprovisioned' audit log
action and reorders AuditLogProjector::handle() so the deprovisioning entry
is persisted before the identity's other audit log entries are anonymized;
otherwise the newly created entry would be wiped immediately after. A
projector test covers both the new entry and the existing anonymization
behavior.

Provide links to any relevant tickets, articles or other resources
OpenConext/Stepup-RA#423
Why is this change needed?
IdentityForgottenEvent was mapped to a new 'deprovisioned' audit log
action and persisted, but AuditLogRepository::createSecondFactorSearchQuery()
filters entries against an event allowlist that never included it, so the
new entry was written but silently filtered out of the RA audit log page.
Separately, the comment justifying the projector's insert-before-anonymise
ordering had the mechanism backwards, and the projector test mocked
findByIdentityId() to omit the just-inserted entry, so it couldn't have
caught either issue.

How does it address the issue?
Adds IdentityForgottenEvent::class to the allowlist so the entry is
actually returned to the RA UI. Corrects the ordering comment: inserting
before anonymising means the new entry is included in the same
anonymisation pass as the identity's other entries (its actor name gets
wiped like everything else), not preserved as the old comment claimed.
Updates the test to mock findByIdentityId() the way the real repository
behaves (returning the freshly flushed entry alongside the pre-existing
one), and asserts the new entry's actor name is anonymised too.

Provide links to any relevant tickets, articles or other resources
OpenConext/Stepup-RA#423
Deprovisioning (IdentityForgottenEvent) was added to the audit log after
the fact, so identities forgotten before that change have no
'deprovisioned' entry. The retroactive backfill cannot go through
stepup:event:replay + AuditLogProjector: replaying IdentityForgottenEvent
also re-runs applyIdentityForgottenEvent(), which anonymises every current
audit log entry for the identity - and an identity forgotten, then
restored (UpdateIdentityCommand calls Identity::restore()), then active
again would have its live audit log scrubbed.

stepup:audit-log:backfill-deprovisioned reads IdentityForgottenEvents
from the event store and inserts only the missing entries, without any
anonymisation:

- AuditLogRepository::hasDeprovisionedEntry() keys on identity + event +
  recordedOn (second precision; a restore has to happen between two
  forgets so they cannot share a second), and an in-run key guards
  against re-inserting within a single batch - so the command is
  idempotent and safe to run more than once.
- Entries are persisted in batches of 500 with the entity manager
  cleared between them, keeping memory flat over a large event set.
- A confirmation prompt (bypass with --force or --no-interaction) warns
  to disable the lifecycle API first: the check and insert are not atomic
  with AuditLogProjector, so a deprovisioning projected live during the
  run could otherwise be inserted twice.
- --dry-run reports what it would create.
@kayjoosten

Copy link
Copy Markdown
Contributor Author

Why a new command instead of the existing replay commands:

  • middleware:event:replay wipes and rebuilds all read models from the full event store, too broad and risky just to backfill one gap.
  • stepup:event:replay replays through the live AuditLogProjector, which would re-run its anonymization logic against identities that were later restored (corrupting current audit data), and it doesn't check for existing entries, so reruns would duplicate them.

stepup:audit-log:backfill-deprovisioned avoids both: it only reads IdentityForgottenEvent rows, writes directly (skipping the projector), checks for existing entries first (idempotent), and batches writes so it scales fine.

@kayjoosten
kayjoosten requested a review from johanib September 8, 2026 21:51
name: 'stepup:audit-log:backfill-deprovisioned',
description: 'Creates the missing "deprovisioned" audit log entries for identities forgotten before '
. 'the deprovisioning action was recorded. Idempotent.'
)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put clear upgrade instructions in the upgrade.md (or release notes).

@johanib johanib left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update release notes + @johanib does final check

Deprovisioning entries were only added to the audit log going forward,
so identities forgotten before this change has been deployed have no
'deprovisioned' entry. Add an Unreleased/Deployment action required
section pointing operators at the stepup:audit-log:backfill-deprovisioned
command, following the same convention already used for #628.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Show deprovisioning event in audit log

2 participants