From f7309c642a0213f796efed2dbd5a3ba50af2840d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 28 May 2026 21:38:57 +0000 Subject: [PATCH] Cancel signature requests on deactivation Disabling a profile now removes pending document signature requests tied to that signatory. This keeps inactive accounts from retaining signature work across manual, archived, and SCIM deactivation flows. Signed-off-by: Cursor Agent Co-authored-by: Bryan FRIMIN --- e2e/console/user_test.go | 108 +++++++++++++++++++++ pkg/coredata/document_version_signature.go | 30 ++++++ pkg/iam/organization_service.go | 12 +++ pkg/iam/scim/service.go | 10 ++ 4 files changed, 160 insertions(+) diff --git a/e2e/console/user_test.go b/e2e/console/user_test.go index 7a76b0d2c..c4617786a 100644 --- a/e2e/console/user_test.go +++ b/e2e/console/user_test.go @@ -321,6 +321,114 @@ func TestUser_ArchiveUser(t *testing.T) { assert.Equal(t, "INACTIVE", archivedUserState) } +func TestUser_DeactivateUserCancelsSignatureRequests(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + signer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner) + + docID, _ := createTestDocument(t, owner) + approveTestDocument(t, owner, docID) + + var versionResult struct { + Node struct { + Versions struct { + Edges []struct { + Node struct { + ID string `json:"id"` + } `json:"node"` + } `json:"edges"` + } `json:"versions"` + } `json:"node"` + } + + err := owner.Execute(` + query($id: ID!) { + node(id: $id) { + ... on Document { + versions(first: 1, orderBy: { field: CREATED_AT, direction: DESC }) { + edges { node { id } } + } + } + } + } + `, map[string]any{"id": docID}, &versionResult) + require.NoError(t, err) + require.NotEmpty(t, versionResult.Node.Versions.Edges) + + documentVersionID := versionResult.Node.Versions.Edges[0].Node.ID + signerProfileID := signer.GetProfileID().String() + + _, err = owner.Do(` + mutation($input: RequestSignatureInput!) { + requestSignature(input: $input) { + documentVersionSignatureEdge { node { id } } + } + } + `, map[string]any{ + "input": map[string]any{ + "documentVersionId": documentVersionID, + "signatoryId": signerProfileID, + }, + }) + require.NoError(t, err) + + assertRequestedSignatureCount(t, owner, documentVersionID, 1) + + var deactivateResult struct { + DeactivateUser struct { + Success bool `json:"success"` + } `json:"deactivateUser"` + } + + err = owner.ExecuteConnect(` + mutation($input: DeactivateUserInput!) { + deactivateUser(input: $input) { + success + } + } + `, map[string]any{ + "input": map[string]any{ + "organizationId": owner.GetOrganizationID().String(), + "profileId": signerProfileID, + }, + }, &deactivateResult) + require.NoError(t, err) + require.True(t, deactivateResult.DeactivateUser.Success) + + assertRequestedSignatureCount(t, owner, documentVersionID, 0) +} + +func assertRequestedSignatureCount( + t *testing.T, + owner *testutil.Client, + documentVersionID string, + expected int, +) { + t.Helper() + + var result struct { + Node struct { + Signatures struct { + TotalCount int `json:"totalCount"` + } `json:"signatures"` + } `json:"node"` + } + + err := owner.Execute(` + query($id: ID!) { + node(id: $id) { + ... on DocumentVersion { + signatures(first: 10, filter: { states: [REQUESTED] }) { + totalCount + } + } + } + } + `, map[string]any{"id": documentVersionID}, &result) + require.NoError(t, err) + assert.Equal(t, expected, result.Node.Signatures.TotalCount) +} + func TestUser_RemoveOwner(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) diff --git a/pkg/coredata/document_version_signature.go b/pkg/coredata/document_version_signature.go index 0b32d4c68..9148aec11 100644 --- a/pkg/coredata/document_version_signature.go +++ b/pkg/coredata/document_version_signature.go @@ -373,6 +373,36 @@ WHERE return nil } +func (pvss *DocumentVersionSignatures) DeleteRequestedBySignatory( + ctx context.Context, + conn pg.Tx, + scope Scoper, + signatoryID gid.GID, +) error { + q := ` +DELETE FROM document_version_signatures +WHERE + %s + AND signed_by_profile_id = @signatory_id + AND state = @state +` + + q = fmt.Sprintf(q, scope.SQLFragment()) + + args := pgx.StrictNamedArgs{ + "signatory_id": signatoryID, + "state": DocumentVersionSignatureStateRequested, + } + maps.Copy(args, scope.SQLArguments()) + + _, err := conn.Exec(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot delete requested document version signatures: %w", err) + } + + return nil +} + func (pvss *DocumentVersionSignaturesWithPeople) LoadByDocumentVersionIDWithPeople( ctx context.Context, conn pg.Querier, diff --git a/pkg/iam/organization_service.go b/pkg/iam/organization_service.go index 1d1020421..500043366 100644 --- a/pkg/iam/organization_service.go +++ b/pkg/iam/organization_service.go @@ -425,6 +425,11 @@ func (s *OrganizationService) ArchiveUser( return fmt.Errorf("cannot expire pending invitations: %w", err) } + signatures := &coredata.DocumentVersionSignatures{} + if err := signatures.DeleteRequestedBySignatory(ctx, tx, scope, profile.ID); err != nil { + return fmt.Errorf("cannot delete requested signatures: %w", err) + } + now := time.Now() if profile.State != coredata.ProfileStateInactive { @@ -1161,6 +1166,13 @@ func (s *OrganizationService) UpdateUserState( return fmt.Errorf("cannot update profile: %w", err) } + if state == coredata.ProfileStateInactive { + signatures := &coredata.DocumentVersionSignatures{} + if err := signatures.DeleteRequestedBySignatory(ctx, tx, scope, profile.ID); err != nil { + return fmt.Errorf("cannot delete requested signatures: %w", err) + } + } + return nil }, ) diff --git a/pkg/iam/scim/service.go b/pkg/iam/scim/service.go index 3a293e8db..59018c373 100644 --- a/pkg/iam/scim/service.go +++ b/pkg/iam/scim/service.go @@ -299,6 +299,11 @@ func (s *Service) CreateUser( ); err != nil { return fmt.Errorf("cannot expire pending invitations: %w", err) } + + signatures := &coredata.DocumentVersionSignatures{} + if err := signatures.DeleteRequestedBySignatory(ctx, tx, scope, profile.ID); err != nil { + return fmt.Errorf("cannot delete requested signatures: %w", err) + } } membership = &coredata.Membership{} @@ -729,6 +734,11 @@ func (s *Service) updateUser( ); err != nil { return fmt.Errorf("cannot expire pending invitations: %w", err) } + + signatures := &coredata.DocumentVersionSignatures{} + if err := signatures.DeleteRequestedBySignatory(ctx, tx, scope, profile.ID); err != nil { + return fmt.Errorf("cannot delete requested signatures: %w", err) + } } needsUpdate := shouldReactivate || shouldDeactivate