Skip to content

[PM-31929] Add deletion days restriction to Send Controls policy#7506

Open
mcamirault wants to merge 2 commits intotools/pm-31884/send-access-controls-policyfrom
tools/pm-31929/send-deletion-days-policy
Open

[PM-31929] Add deletion days restriction to Send Controls policy#7506
mcamirault wants to merge 2 commits intotools/pm-31884/send-access-controls-policyfrom
tools/pm-31929/send-deletion-days-policy

Conversation

@mcamirault
Copy link
Copy Markdown
Contributor

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-31929

📔 Objective

This PR adds a field to the Send Controls policy that allows specifying a deletion date restriction on Sends created in an organization

📸 Screenshots

N/A

@mcamirault mcamirault requested review from a team as code owners April 20, 2026 13:03
@mcamirault mcamirault added the ai-review Request a Claude code review label Apr 20, 2026
@mcamirault mcamirault requested a review from jrmccannon April 20, 2026 13:03
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 20, 2026

🤖 Bitwarden Claude Code Review

Overall Assessment: REQUEST CHANGES

This PR adds a DeletionDays field to the Send Controls policy and propagates the value through a new Send_UpdateDeletionDatesByIds stored procedure / EF implementation, executed from SendControlsSyncPolicyEvent after a policy upsert. Reviewed the policy data model, the sync-event handler, both repository implementations, the SQL migration, and the new test. The MSSQL and EF paths are kept in parity, but there is a units mismatch between the field name and the persisted value, plus behavioral concerns around when the bulk update fires. A human reviewer (mkincaid-bw) has also flagged that the EXEC [dbo].[User_BumpManyAccountRevisionDates] (SELECT ...) construct in the new sproc / migration is not valid syntax for invoking a procedure that takes a table-valued parameter — that comment should be addressed in addition to the items below.

Code Review Details
  • ❌ : Units mismatch — DeletionDays is passed through as deletionHours, producing deletion dates 24× shorter than intended
    • src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEvent.cs:114
    • src/Sql/dbo/Tools/Stored Procedures/Send_UpdateDeletionDatesByIds.sql:12
    • src/Infrastructure.EntityFramework/Tools/Repositories/SendRepository.cs:164
  • ⚠️ : Deletion dates are rewritten even when the SendControls policy is being disabled
    • src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEvent.cs:112-115
  • ❓ : Unclear whether user-set deletion dates earlier than CreationDate + DeletionDays should be preserved (policy intent: max cap vs. hard reset)
    • src/Sql/dbo/Tools/Stored Procedures/Send_UpdateDeletionDatesByIds.sql:9-15

Additional minor observations not posted as inline comments:

  • The migration script file name (2026-04-20_01_SendUpdateDeletionDaysByIds.sql) does not match the procedure name inside it (Send_UpdateDeletionDatesByIds); consider aligning the two.
  • Both new SQL files are missing a trailing newline.
  • SendControlsPolicyRequirement / SendControlsPolicyRequirementFactory do not expose the new DeletionDays field. If clients need to read this requirement (e.g., to constrain the Send creation UI to the policy maximum), the requirement will need to be extended in a follow-up.

}
if (sendControlsPolicyData.DeletionDays != null)
{
await sendRepository.UpdateManyDeletionDatesByIdsAsync(sendIdsChunk, sendControlsPolicyData.DeletionDays.GetValueOrDefault(0));
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.

CRITICAL: Units mismatch — DeletionDays is passed directly as deletionHours.

Details and fix

The policy field is SendControlsPolicyData.DeletionDays (int?), but the repository method is declared as UpdateManyDeletionDatesByIdsAsync(IEnumerable<Guid> ids, int deletionHours) and the stored procedure / EF implementation both do DATEADD(HOUR, @DeletionHours, [CreationDate]) / CreationDate.AddHours(deletionHours).

Tracing the call chain: an admin who sets DeletionDays = 7 ends up with sends whose DeletionDate = CreationDate + 7 hours, i.e., ~7 hours instead of 7 days. This is off by a factor of 24.

The test case even asserts the broken behavior directly: DeletionDays = 48 is asserted to be passed as 48 to a method that interprets it as hours.

Either convert at the call site:

await sendRepository.UpdateManyDeletionDatesByIdsAsync(
    sendIdsChunk,
    sendControlsPolicyData.DeletionDays.GetValueOrDefault(0) * 24);

Or rename the policy field/column to match the unit actually stored (e.g., DeletionHours), and update the DB column name, migration file (2026-04-20_01_SendUpdateDeletionDaysByIds.sql), SendControlsPolicyRequirement, and all callers / UI consumers accordingly.

Recommended: change the SQL/EF implementations to use DATEADD(DAY, @DeletionDays, ...) / .AddDays(...) and rename the parameter to @DeletionDays / deletionDays. That keeps the policy unit semantics consistent with the field name and with how admins will likely reason about Send retention.

Comment on lines +115 to +118
if (sendControlsPolicyData.DeletionDays != null)
{
await sendRepository.UpdateManyDeletionDatesByIdsAsync(sendIdsChunk, sendControlsPolicyData.DeletionDays.GetValueOrDefault(0));
}
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.

⚠️ IMPORTANT: Deletion dates are rewritten even when the policy is being disabled.

Details and fix

The existing enable/disable branch above intentionally checks postUpsertedPolicyState.Enabled — the comment on line 94 notes "If the policy is disabled then we want to re-enable any Sends that were previously disabled." The new DeletionDays branch ignores that guard. If an admin disables the SendControls policy (or its previously-enforced DeletionDays value), every Send in the org still gets DeletionDate reset to CreationDate + DeletionDays. That contradicts the semantics of disabling the policy.

Suggested fix:

if (postUpsertedPolicyState.Enabled && sendControlsPolicyData.DeletionDays != null)
{
    await sendRepository.UpdateManyDeletionDatesByIdsAsync(
        sendIdsChunk,
        sendControlsPolicyData.DeletionDays.Value);
}

Consider also whether only changes to DeletionDays (compared to previousPolicyState) should trigger this update, to avoid rewriting deletion dates on every unrelated save.

Comment on lines +9 to +15
UPDATE
[dbo].[Send]
SET
[DeletionDate] = DATEADD(HOUR, @DeletionHours, [CreationDate]),
[RevisionDate] = GETUTCDATE()
WHERE
[Id] IN (SELECT * FROM @Ids)
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.

QUESTION: Should user-set deletion dates that are earlier than CreationDate + DeletionDays be preserved?

Details

This proc unconditionally overwrites DeletionDate for every Send in the chunk. If a user created a Send with a deliberately short deletion (e.g., CreationDate + 1 day while the policy is DeletionDays = 7), this will extend its lifetime to 7 days — the opposite of what a retention-max policy usually intends.

If DeletionDays is meant as a maximum retention cap, consider:

UPDATE [dbo].[Send]
SET
    [DeletionDate] = DATEADD(DAY, @DeletionDays, [CreationDate]),
    [RevisionDate] = GETUTCDATE()
WHERE
    [Id] IN (SELECT * FROM @Ids)
    AND [DeletionDate] > DATEADD(DAY, @DeletionDays, [CreationDate])

(mirror the same guard in the EF implementation).

If it is instead intended to force all Sends to exactly CreationDate + DeletionDays, please confirm that's the product requirement — that behavior effectively resets users' chosen deletion dates whenever any SendControls policy setting is saved.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 20, 2026

Logo
Checkmarx One – Scan Summary & Detailsbe98a3f8-04e7-4947-86f3-d1cea3d678b8


New Issues (2) Checkmarx found the following issues in this Pull Request
# Severity Issue Source File / Package Checkmarx Insight
1 MEDIUM CSRF src/Api/AdminConsole/Controllers/GroupsController.cs: 275
detailsMethod at line 275 of /src/Api/AdminConsole/Controllers/GroupsController.cs gets a parameter from a user request from orgUserId. This parameter ...
Attack Vector
2 MEDIUM CSRF src/Identity/Controllers/AccountsController.cs: 138
detailsMethod at line 138 of /src/Identity/Controllers/AccountsController.cs gets a parameter from a user request from model. This parameter value flow...
Attack Vector

Fixed Issues (2) Great job! The following issues were fixed in this Pull Request
Severity Issue Source File / Package
MEDIUM CSRF src/Api/AdminConsole/Controllers/GroupsController.cs: 289
MEDIUM CSRF src/Api/Vault/Controllers/CiphersController.cs: 287

Copy link
Copy Markdown
Contributor

@jrmccannon jrmccannon left a comment

Choose a reason for hiding this comment

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

The name of the variable as DeletionHours but using Days is confusing.

Should it be hours or days?

[Id] IN (SELECT * FROM @Ids)

-- Bump account revision dates
EXEC [dbo].[User_BumpManyAccountRevisionDates]
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.

This does not work as expected and oddly, does not generate an error, despite being invalid SQL syntax. Instead you need to do something like this:

-- Near the top of stored proc
    DECLARE @UserIds [dbo].[GuidIdArray]

--rest of stored procedure

    INSERT INTO @UserIds
    SELECT DISTINCT
        UserId
    FROM
        [dbo].[Send]
    WHERE
        [Id] IN (SELECT * FROM @Ids)
        AND [UserId] IS NOT NULL

    EXEC [dbo].[User_BumpManyAccountRevisionDates] @UserIds

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 28, 2026

Codecov Report

❌ Patch coverage is 0% with 24 lines in your changes missing coverage. Please review.
✅ Project coverage is 14.30%. Comparing base (0732302) to head (0d66de0).

Files with missing lines Patch % Lines
...tityFramework/Tools/Repositories/SendRepository.cs 0.00% 12 Missing ⚠️
...ucture.Dapper/Tools/Repositories/SendRepository.cs 0.00% 7 Missing ⚠️
...PolicyEventHandlers/SendControlsSyncPolicyEvent.cs 0.00% 4 Missing ⚠️
...a/Organizations/Policies/SendControlsPolicyData.cs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@                              Coverage Diff                               @@
##           tools/pm-31884/send-access-controls-policy    #7506      +/-   ##
==============================================================================
- Coverage                                       14.30%   14.30%   -0.01%     
==============================================================================
  Files                                            1278     1278              
  Lines                                           55670    55694      +24     
  Branches                                         4317     4319       +2     
==============================================================================
  Hits                                             7966     7966              
- Misses                                          47564    47588      +24     
  Partials                                          140      140              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

JaredScar added a commit that referenced this pull request Apr 30, 2026
…ails in SyncController (#7506) (#7529)

- Added support for retrieving confirmed accepted policies and organization user details based on the feature flag 'PoliciesInAcceptedState'.
- Updated SyncResponseModel to include new properties for these details.
- Enhanced SyncControllerTests to verify behavior with the feature flag enabled and disabled.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Request a Claude code review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants