Grace period (et al) for membership cleaner#479
Conversation
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
| "FirstSeenMissingAt" timestamp NOT NULL, | ||
| "LastCheckedAt" timestamp NOT NULL, | ||
| "CreatedAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| "UpdatedAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, |
There was a problem hiding this comment.
CreatedAt and UpdatedAt are technically not necessary, but I like them to keep things aligned
| ( | ||
| "Id" uuid NOT NULL, | ||
| "UserId" varchar(255) NOT NULL, | ||
| "Status" varchar(50) NOT NULL, -- NotFound, Deactivated |
There was a problem hiding this comment.
This may be overkill, but I like the additional data for why we remove people
| private readonly ILogger<DeactivatedMemberCleanerApplicationService> _logger; | ||
|
|
||
| private readonly StringBuilder _sb = new(); | ||
| private const int GracePeriodDays = 7; |
There was a problem hiding this comment.
7 seems to be our magic grace period number 🤷
|
|
||
| namespace SelfService.Domain.Events; | ||
|
|
||
| public class MemberMarkedAsMissing : IDomainEvent |
There was a problem hiding this comment.
Introduced for later usage.
We should be fully event based for this, I believe
|
|
||
| namespace SelfService.Domain.Events; | ||
|
|
||
| public class MemberRemovedDueToMissingStatus : IDomainEvent |
There was a problem hiding this comment.
Introduced for later usage.
We should be fully event based for this, I believe
There was a problem hiding this comment.
Pull request overview
This PR updates the membership-cleaner workflow to avoid premature deletions due to transient Azure AD issues by tracking “missing” members and only deleting after a 7-day grace period, while also adding RBAC cleanup on deletion.
Changes:
- Add persistence + repository for tracking missing members across cleaner runs (grace period state).
- Update
DeactivatedMemberCleanerApplicationServiceto (1) mark members missing, (2) delete only after grace period, and (3) clean RBAC grants/group memberships when deleting. - Add SQLite-backed tests covering first-detection vs grace-period-expired deletion behavior.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/SelfService/Infrastructure/Persistence/SelfServiceDbContext.cs | Adds DbSet for missing-member tracking. |
| src/SelfService/Infrastructure/Persistence/MissingMemberRepository.cs | New repository for CRUD around missing-member tracking records. |
| src/SelfService/Domain/Models/MissingMemberRecord.cs | New domain model for missing-member state + grace-period checks. |
| src/SelfService/Domain/Models/IMissingMemberRepository.cs | New repository interface. |
| src/SelfService/Domain/Events/MemberRemovedDueToMissingStatus.cs | New domain event type (added in this PR). |
| src/SelfService/Domain/Events/MemberMarkedAsMissing.cs | New domain event type (added in this PR). |
| src/SelfService/Configuration/Domain.cs | Registers IMissingMemberRepository in DI. |
| src/SelfService/Application/DeactivatedMemberCleanerApplicationService.cs | Implements grace-period logic, missing-member bookkeeping, and RBAC cleanup on delete. |
| src/SelfService.Tests/Infrastructure/Persistence/TestMemberCleaner.cs | Adds tests for grace-period behavior. |
| src/SelfService.Tests/Builders/DeactivatedMemberCleanerApplicationServiceBuilder.cs | Updates builder for new cleaner dependencies; mocks RBAC repos. |
| db/migrations/20260616100000_add-missing-member-record-table.sql | Adds SQL migration for MissingMemberRecords. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ( | ||
| "Id" uuid NOT NULL, | ||
| "UserId" varchar(255) NOT NULL, | ||
| "Status" varchar(50) NOT NULL, -- NotFound, Deactivated |
| // Keep heartbeat fresh only while still inside grace period. | ||
| existingRecord.UpdateLastChecked(); | ||
| await _missingMemberRepository.Update(existingRecord); |
| var permissionGrants = await _rbacPermissionGrantRepository.GetAllWithPredicate(pg => | ||
| pg.AssignedEntityId == userId | ||
| ); |
| var groupMembers = await _rbacGroupMemberRepository.GetAllWithPredicate(gm => gm.UserId == userId); | ||
|
|
| public Guid Id { get; set; } | ||
| public string UserId { get; set; } | ||
| public MissingMemberStatus Status { get; set; } |
| // Clean up RBAC permissions | ||
| await CleanupRbacPermissions(member.Id.ToString()); | ||
|
|
💡 Context:
🌟 Changes: