Split inactive profile state

Replace the binary profile ACTIVE/INACTIVE model with PENDING, ACTIVE,
and DEACTIVATED so invited-but-not-yet-activated members remain
assignable to assets, data, and risks instead of being treated like
deactivated users.

Add activated_at/deactivated_at timestamps and Mark* lifecycle helpers,
and update every transition (create, invite/re-invite, activation,
archive, SCIM, SAML, sessions, compliance-portal grant) to the new
states. Expose a multi-state states[] filter across coredata, GraphQL,
MCP, and the console owner pickers, which now request ACTIVE and
PENDING members.

A migration renames the membership_state enum, classifies existing
inactive profiles as PENDING from recent invitation activity, and
backfills the new timestamp columns.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-07-28 17:03:52 +02:00
parent 6b3913b189
commit 4a276e3ef7
29 changed files with 261 additions and 64 deletions

View File

@@ -150,7 +150,7 @@ func (s *Service) CreateUser(
profileState := coredata.ProfileStateActive
if !attrs.Active {
profileState = coredata.ProfileStateInactive
profileState = coredata.ProfileStateDeactivated
}
var externalIdPtr *string
@@ -536,8 +536,8 @@ func (s *Service) updateUser(
previousMembership := *membership
previousUser := webhooktypes.NewUser(&previousProfile, &previousMembership)
shouldReactivate := attrs.Active != nil && *attrs.Active && profile.State == coredata.ProfileStateInactive
shouldDeactivate := attrs.Active != nil && !*attrs.Active && profile.State == coredata.ProfileStateActive
shouldReactivate := attrs.Active != nil && *attrs.Active && profile.State == coredata.ProfileStateDeactivated
shouldDeactivate := attrs.Active != nil && !*attrs.Active && profile.State != coredata.ProfileStateDeactivated
if attrs.FullName != "" {
profile.FullName = attrs.FullName
@@ -748,11 +748,9 @@ func (s *Service) updateUser(
}
if shouldReactivate {
profile.State = coredata.ProfileStateActive
profile.UpdatedAt = now
profile.MarkActive(now)
} else if shouldDeactivate {
profile.State = coredata.ProfileStateInactive
profile.UpdatedAt = now
profile.MarkDeactivated(now)
}
if profile.Source != coredata.ProfileSourceSCIM {
@@ -826,7 +824,15 @@ func applyUserAttributes(
now time.Time,
) {
profile.Source = coredata.ProfileSourceSCIM
profile.State = state
switch {
case state == coredata.ProfileStateActive && profile.State != coredata.ProfileStateActive:
profile.MarkActive(now)
case state == coredata.ProfileStateDeactivated && profile.State != coredata.ProfileStateDeactivated:
profile.MarkDeactivated(now)
default:
profile.State = state
}
profile.FullName = attrs.FullName
profile.Position = &attrs.Title
profile.UserName = &attrs.UserName
@@ -956,15 +962,14 @@ func (s *Service) deactivateProfileInTx(
profile *coredata.MembershipProfile,
membership *coredata.Membership,
) error {
if profile.State == coredata.ProfileStateInactive {
if profile.State == coredata.ProfileStateDeactivated {
return nil
}
previousUser := webhooktypes.NewUser(profile, membership)
now := time.Now()
profile.State = coredata.ProfileStateInactive
profile.UpdatedAt = now
profile.MarkDeactivated(now)
if err := profile.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot deactivate profile: %w", err)