Split user remove and archive actions
Restore RemoveUser as a hard delete operation and surface dependency\nconflicts with a dedicated IAM error.\n\nAdd a new ArchiveUser flow that deactivates profiles while keeping the\nmember in the organization, then expose both actions across Connect, MCP,\nCLI, n8n, console UI, and e2e coverage. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
This commit is contained in:
committed by
Bryan Frimin
parent
a71a7bb56f
commit
1e08a23ddc
@@ -157,6 +157,18 @@ func (e ErrUserManagedBySCIM) Error() string {
|
||||
return fmt.Sprintf("user %q is managed by SCIM and cannot be deleted manually", e.ProfileID)
|
||||
}
|
||||
|
||||
type ErrUserReferencedByRecords struct {
|
||||
ProfileID gid.GID
|
||||
}
|
||||
|
||||
func NewUserReferencedByRecordsError(profileID gid.GID) error {
|
||||
return &ErrUserReferencedByRecords{ProfileID: profileID}
|
||||
}
|
||||
|
||||
func (e ErrUserReferencedByRecords) Error() string {
|
||||
return "cannot remove user because they are referenced by existing records (for example signatures, tasks, assets, or risks)"
|
||||
}
|
||||
|
||||
type ErrLastActiveOwner struct {
|
||||
MembershipID gid.GID
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"go.gearno.de/crypto/uuid"
|
||||
"go.gearno.de/kit/pg"
|
||||
"go.probo.inc/probo/packages/emails"
|
||||
@@ -314,6 +315,73 @@ func (s *OrganizationService) RemoveUser(
|
||||
) error {
|
||||
scope := coredata.NewScopeFromObjectID(organizationID)
|
||||
|
||||
return s.pg.WithTx(
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
profile := coredata.MembershipProfile{}
|
||||
|
||||
if err := profile.LoadByID(ctx, tx, scope, profileID); err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
return NewProfileNotFoundError(profileID)
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot load profile: %w", err)
|
||||
}
|
||||
|
||||
if profile.Source == coredata.ProfileSourceSCIM {
|
||||
return NewUserManagedBySCIMError(profileID)
|
||||
}
|
||||
|
||||
membership := &coredata.Membership{}
|
||||
if err := membership.LoadByIdentityIDAndOrganizationID(ctx, tx, scope, profile.IdentityID, profile.OrganizationID); err != nil {
|
||||
return fmt.Errorf("cannot load membership: %w", err)
|
||||
}
|
||||
|
||||
if membership.Role == coredata.MembershipRoleOwner && profile.State == coredata.ProfileStateActive {
|
||||
profiles := coredata.MembershipProfiles{}
|
||||
|
||||
count, err := profiles.CountActiveOwnerByOrganizationID(ctx, tx, scope, organizationID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot count active owners: %w", err)
|
||||
}
|
||||
|
||||
if count <= 1 {
|
||||
return NewLastActiveOwnerError(profileID)
|
||||
}
|
||||
}
|
||||
|
||||
if err := profile.Delete(ctx, tx, scope, profileID); err != nil {
|
||||
if isUserRemovalDependencyError(err) {
|
||||
return NewUserReferencedByRecordsError(profileID)
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot delete profile: %w", err)
|
||||
}
|
||||
|
||||
if err := membership.Delete(ctx, tx, scope, membership.ID); err != nil {
|
||||
if isUserRemovalDependencyError(err) {
|
||||
return NewUserReferencedByRecordsError(profileID)
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot delete membership: %w", err)
|
||||
}
|
||||
|
||||
if err := webhook.InsertData(ctx, tx, scope, organizationID, coredata.WebhookEventTypeUserDeleted, webhooktypes.NewUser(&profile, membership)); err != nil {
|
||||
return fmt.Errorf("cannot insert webhook event: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (s *OrganizationService) ArchiveUser(
|
||||
ctx context.Context,
|
||||
organizationID gid.GID,
|
||||
profileID gid.GID,
|
||||
) error {
|
||||
scope := coredata.NewScopeFromObjectID(organizationID)
|
||||
|
||||
return s.pg.WithTx(
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
@@ -374,6 +442,19 @@ func (s *OrganizationService) RemoveUser(
|
||||
)
|
||||
}
|
||||
|
||||
func isUserRemovalDependencyError(err error) bool {
|
||||
if errors.Is(err, coredata.ErrResourceInUse) {
|
||||
return true
|
||||
}
|
||||
|
||||
pgErr, ok := errors.AsType[*pgconn.PgError](err)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return pgErr.Code == "23503"
|
||||
}
|
||||
|
||||
func (s *OrganizationService) InviteUser(
|
||||
ctx context.Context,
|
||||
req *CreateInvitationRequest,
|
||||
|
||||
Reference in New Issue
Block a user