From 688cc00d485abd4dd13c9bd1c886ea3877b43ddd Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Mon, 29 Mar 2021 07:56:56 +1000 Subject: [PATCH] Hide email address in Sends (#1234) * Add send HideEmail to tables and models * Respect HideEmail setting for Sends * Recreate SendView to include new HideEmail column * Enforce new Send policy * Insert default value for new HideEmail column * Delete c95d7598-71cc-4eab-8b08-aced0045198b.json * Remove unrelated files * Revert disableSendPolicy, add sendOptionsPolicy * Minor style fixes * Update SQL project with Send.HideEmail column * unit test SendOptionsPolicy.DisableHideEmail * Add SendOptionsPolicy to Portal * Make HideEmail nullable, fix migrator script * Remove NOT NULL constraint from HideEmail * Fix style * Make HideEmail nullable * minor fixes to model and error message * Move SendOptionsExemption banner Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com> --- .../Portal/Controllers/PoliciesController.cs | 1 + .../src/Portal/Models/PolicyEditModel.cs | 8 + .../src/Portal/Models/PolicyModel.cs | 5 + .../src/Portal/Views/Policies/Edit.cshtml | 30 +++- src/Api/Controllers/SendsController.cs | 2 +- src/Core/Enums/PolicyType.cs | 1 + .../Models/Api/Request/SendRequestModel.cs | 2 + .../Models/Api/Response/SendResponseModel.cs | 2 + src/Core/Models/Data/SendOptionsPolicyData.cs | 10 ++ src/Core/Models/Table/Send.cs | 1 + src/Core/Resources/SharedResources.en.resx | 18 +++ .../Services/Implementations/SendService.cs | 21 ++- src/Sql/dbo/Stored Procedures/Send_Create.sql | 11 +- src/Sql/dbo/Stored Procedures/Send_Update.sql | 8 +- src/Sql/dbo/Tables/Send.sql | 1 + test/Core.Test/Services/SendServiceTests.cs | 79 ++++++++- ...021-03-22_00_Send_Add_HideEmail_Column.sql | 151 ++++++++++++++++++ 17 files changed, 334 insertions(+), 17 deletions(-) create mode 100644 src/Core/Models/Data/SendOptionsPolicyData.cs create mode 100644 util/Migrator/DbScripts/2021-03-22_00_Send_Add_HideEmail_Column.sql diff --git a/bitwarden_license/src/Portal/Controllers/PoliciesController.cs b/bitwarden_license/src/Portal/Controllers/PoliciesController.cs index 3e7fedc5b..73351a2b6 100644 --- a/bitwarden_license/src/Portal/Controllers/PoliciesController.cs +++ b/bitwarden_license/src/Portal/Controllers/PoliciesController.cs @@ -137,6 +137,7 @@ namespace Bit.Portal.Controllers case PolicyType.TwoFactorAuthentication: case PolicyType.PersonalOwnership: case PolicyType.DisableSend: + case PolicyType.SendOptions: break; case PolicyType.SingleOrg: diff --git a/bitwarden_license/src/Portal/Models/PolicyEditModel.cs b/bitwarden_license/src/Portal/Models/PolicyEditModel.cs index fa6e87037..545caa02b 100644 --- a/bitwarden_license/src/Portal/Models/PolicyEditModel.cs +++ b/bitwarden_license/src/Portal/Models/PolicyEditModel.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Text.Json; using Bit.Core.Enums; using Bit.Core.Models.Table; +using Bit.Core.Models.Data; using Bit.Core.Services; using Microsoft.AspNetCore.Mvc.Rendering; @@ -45,6 +46,9 @@ namespace Bit.Portal.Models case PolicyType.PasswordGenerator: PasswordGeneratorDataModel = JsonSerializer.Deserialize(model.Data, options); break; + case PolicyType.SendOptions: + SendOptionsDataModel = JsonSerializer.Deserialize(model.Data, options); + break; default: throw new ArgumentOutOfRangeException(); } @@ -53,6 +57,7 @@ namespace Bit.Portal.Models public MasterPasswordDataModel MasterPasswordDataModel { get; set; } public PasswordGeneratorDataModel PasswordGeneratorDataModel { get; set; } + public SendOptionsPolicyData SendOptionsDataModel { get; set; } public List Complexities { get; set; } public List DefaultTypes { get; set; } public string EnableCheckboxText { get; set; } @@ -88,6 +93,9 @@ namespace Bit.Portal.Models case PolicyType.PersonalOwnership: case PolicyType.DisableSend: break; + case PolicyType.SendOptions: + existingPolicy.Data = JsonSerializer.Serialize(SendOptionsDataModel, options); + break; default: throw new ArgumentOutOfRangeException(); } diff --git a/bitwarden_license/src/Portal/Models/PolicyModel.cs b/bitwarden_license/src/Portal/Models/PolicyModel.cs index fb3038ff9..a751c4c09 100644 --- a/bitwarden_license/src/Portal/Models/PolicyModel.cs +++ b/bitwarden_license/src/Portal/Models/PolicyModel.cs @@ -52,6 +52,11 @@ namespace Bit.Portal.Models DescriptionKey = "DisableSendDescription"; break; + case PolicyType.SendOptions: + NameKey = "SendOptions"; + DescriptionKey = "SendOptionsDescription"; + break; + default: throw new ArgumentOutOfRangeException(); } diff --git a/bitwarden_license/src/Portal/Views/Policies/Edit.cshtml b/bitwarden_license/src/Portal/Views/Policies/Edit.cshtml index 07dfeb8d3..556feb101 100644 --- a/bitwarden_license/src/Portal/Views/Policies/Edit.cshtml +++ b/bitwarden_license/src/Portal/Views/Policies/Edit.cshtml @@ -32,7 +32,7 @@ @i18nService.T("SingleOrganizationPolicyWarning") } - + @if (Model.PolicyType == PolicyType.RequireSso) { } - + @if (Model.PolicyType == PolicyType.PersonalOwnership) { } - + + @if (Model.PolicyType == PolicyType.SendOptions) + { + + } +
@@ -187,6 +198,19 @@
} + @if (Model.PolicyType == PolicyType.SendOptions) + { +
+

@i18nService.T("Options")

+
+
+ + +
+
+
+ } +
@i18nService.T("Cancel") diff --git a/src/Api/Controllers/SendsController.cs b/src/Api/Controllers/SendsController.cs index 6cb01154f..cb812be2b 100644 --- a/src/Api/Controllers/SendsController.cs +++ b/src/Api/Controllers/SendsController.cs @@ -70,7 +70,7 @@ namespace Bit.Api.Controllers } var sendResponse = new SendAccessResponseModel(send, _globalSettings); - if (send.UserId.HasValue) + if (send.UserId.HasValue && !send.HideEmail.GetValueOrDefault()) { var creator = await _userService.GetUserByIdAsync(send.UserId.Value); sendResponse.CreatorIdentifier = creator.Email; diff --git a/src/Core/Enums/PolicyType.cs b/src/Core/Enums/PolicyType.cs index 0d3bb68ba..72804ca40 100644 --- a/src/Core/Enums/PolicyType.cs +++ b/src/Core/Enums/PolicyType.cs @@ -9,5 +9,6 @@ RequireSso = 4, PersonalOwnership = 5, DisableSend = 6, + SendOptions = 7, } } diff --git a/src/Core/Models/Api/Request/SendRequestModel.cs b/src/Core/Models/Api/Request/SendRequestModel.cs index d64faef17..85fe417b5 100644 --- a/src/Core/Models/Api/Request/SendRequestModel.cs +++ b/src/Core/Models/Api/Request/SendRequestModel.cs @@ -35,6 +35,7 @@ namespace Bit.Core.Models.Api public string Password { get; set; } [Required] public bool? Disabled { get; set; } + public bool? HideEmail { get; set; } public Send ToSend(Guid userId, ISendService sendService) { @@ -125,6 +126,7 @@ namespace Bit.Core.Models.Api existingSend.Password = sendService.HashPassword(Password); } existingSend.Disabled = Disabled.GetValueOrDefault(); + existingSend.HideEmail = HideEmail.GetValueOrDefault(); return existingSend; } } diff --git a/src/Core/Models/Api/Response/SendResponseModel.cs b/src/Core/Models/Api/Response/SendResponseModel.cs index d23ee3af9..4feba289f 100644 --- a/src/Core/Models/Api/Response/SendResponseModel.cs +++ b/src/Core/Models/Api/Response/SendResponseModel.cs @@ -29,6 +29,7 @@ namespace Bit.Core.Models.Api DeletionDate = send.DeletionDate; Password = send.Password; Disabled = send.Disabled; + HideEmail = send.HideEmail.GetValueOrDefault(); SendData sendData; switch (send.Type) @@ -66,5 +67,6 @@ namespace Bit.Core.Models.Api public DateTime RevisionDate { get; set; } public DateTime? ExpirationDate { get; set; } public DateTime DeletionDate { get; set; } + public bool HideEmail { get; set; } } } diff --git a/src/Core/Models/Data/SendOptionsPolicyData.cs b/src/Core/Models/Data/SendOptionsPolicyData.cs new file mode 100644 index 000000000..fe789abb5 --- /dev/null +++ b/src/Core/Models/Data/SendOptionsPolicyData.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Core.Models.Data +{ + public class SendOptionsPolicyData + { + [Display(Name = "DisableHideEmail")] + public bool DisableHideEmail { get; set; } + } +} diff --git a/src/Core/Models/Table/Send.cs b/src/Core/Models/Table/Send.cs index 8de335de5..2660e9e8a 100644 --- a/src/Core/Models/Table/Send.cs +++ b/src/Core/Models/Table/Send.cs @@ -20,6 +20,7 @@ namespace Bit.Core.Models.Table public DateTime? ExpirationDate { get; set; } public DateTime DeletionDate { get; set; } public bool Disabled { get; set; } + public bool? HideEmail { get; set; } public void SetNewId() { diff --git a/src/Core/Resources/SharedResources.en.resx b/src/Core/Resources/SharedResources.en.resx index 478f37d03..02bedd75b 100644 --- a/src/Core/Resources/SharedResources.en.resx +++ b/src/Core/Resources/SharedResources.en.resx @@ -598,6 +598,24 @@ Organization Owners and Administrators are exempt from this policy's enforcement. + + Send Options + 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. + + + Options + + + Do not allow users to hide their email address when creating or editing a Send. + 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. + + + Set options for creating and editing Sends. + 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. + + + Organization Owners and Administrators are exempt from this policy's enforcement. + You must manually disable the Single Sign-On Authentication policy before this policy can be disabled. diff --git a/src/Core/Services/Implementations/SendService.cs b/src/Core/Services/Implementations/SendService.cs index 945efa8e0..8afbdef67 100644 --- a/src/Core/Services/Implementations/SendService.cs +++ b/src/Core/Services/Implementations/SendService.cs @@ -59,7 +59,7 @@ namespace Bit.Core.Services public async Task SaveSendAsync(Send send) { // Make sure user can save Sends - await ValidateUserCanSaveAsync(send.UserId); + await ValidateUserCanSaveAsync(send.UserId, send); if (send.Id == default(Guid)) { @@ -265,7 +265,7 @@ namespace Bit.Core.Services return _passwordHasher.HashPassword(new User(), password); } - private async Task ValidateUserCanSaveAsync(Guid? userId) + private async Task ValidateUserCanSaveAsync(Guid? userId, Send send) { if (!userId.HasValue || (!_currentContext.Organizations?.Any() ?? true)) { @@ -286,6 +286,23 @@ namespace Bit.Core.Services throw new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send."); } } + + if (send.HideEmail.GetValueOrDefault()) + { + foreach (var policy in policies.Where(p => p.Enabled && p.Type == PolicyType.SendOptions && !_currentContext.ManagePolicies(p.OrganizationId))) + { + SendOptionsPolicyData data = null; + if (policy.Data != null) + { + data = JsonConvert.DeserializeObject(policy.Data); + } + + if (data?.DisableHideEmail ?? false) + { + throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send."); + } + } + } } private async Task StorageRemainingForSendAsync(Send send) diff --git a/src/Sql/dbo/Stored Procedures/Send_Create.sql b/src/Sql/dbo/Stored Procedures/Send_Create.sql index 6b550f656..037b069f1 100644 --- a/src/Sql/dbo/Stored Procedures/Send_Create.sql +++ b/src/Sql/dbo/Stored Procedures/Send_Create.sql @@ -12,7 +12,8 @@ @RevisionDate DATETIME2(7), @ExpirationDate DATETIME2(7), @DeletionDate DATETIME2(7), - @Disabled BIT + @Disabled BIT, + @HideEmail BIT AS BEGIN SET NOCOUNT ON @@ -32,7 +33,8 @@ BEGIN [RevisionDate], [ExpirationDate], [DeletionDate], - [Disabled] + [Disabled], + [HideEmail] ) VALUES ( @@ -49,7 +51,8 @@ BEGIN @RevisionDate, @ExpirationDate, @DeletionDate, - @Disabled + @Disabled, + @HideEmail ) IF @UserId IS NOT NULL @@ -61,4 +64,4 @@ BEGIN EXEC [dbo].[User_BumpAccountRevisionDate] @UserId END -- TODO: OrganizationId bump? -END \ No newline at end of file +END diff --git a/src/Sql/dbo/Stored Procedures/Send_Update.sql b/src/Sql/dbo/Stored Procedures/Send_Update.sql index 9bc9ad7ad..6e7ec3e52 100644 --- a/src/Sql/dbo/Stored Procedures/Send_Update.sql +++ b/src/Sql/dbo/Stored Procedures/Send_Update.sql @@ -12,7 +12,8 @@ @RevisionDate DATETIME2(7), @ExpirationDate DATETIME2(7), @DeletionDate DATETIME2(7), - @Disabled BIT + @Disabled BIT, + @HideEmail BIT AS BEGIN SET NOCOUNT ON @@ -32,7 +33,8 @@ BEGIN [RevisionDate] = @RevisionDate, [ExpirationDate] = @ExpirationDate, [DeletionDate] = @DeletionDate, - [Disabled] = @Disabled + [Disabled] = @Disabled, + [HideEmail] = @HideEmail WHERE [Id] = @Id @@ -41,4 +43,4 @@ BEGIN EXEC [dbo].[User_BumpAccountRevisionDate] @UserId END -- TODO: OrganizationId bump? -END \ No newline at end of file +END diff --git a/src/Sql/dbo/Tables/Send.sql b/src/Sql/dbo/Tables/Send.sql index 23d824a25..11232a716 100644 --- a/src/Sql/dbo/Tables/Send.sql +++ b/src/Sql/dbo/Tables/Send.sql @@ -13,6 +13,7 @@ [ExpirationDate] DATETIME2 (7) NULL, [DeletionDate] DATETIME2 (7) NOT NULL, [Disabled] BIT NOT NULL, + [HideEmail] BIT NULL, CONSTRAINT [PK_Send] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [FK_Send_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]), CONSTRAINT [FK_Send_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) diff --git a/test/Core.Test/Services/SendServiceTests.cs b/test/Core.Test/Services/SendServiceTests.cs index 2d7a33167..d24780473 100644 --- a/test/Core.Test/Services/SendServiceTests.cs +++ b/test/Core.Test/Services/SendServiceTests.cs @@ -12,12 +12,15 @@ using Bit.Core.Test.AutoFixture; using Bit.Core.Test.AutoFixture.SendFixtures; using NSubstitute; using Xunit; +using Newtonsoft.Json; namespace Bit.Core.Test.Services { public class SendServiceTests { - private void SaveSendAsync_Setup(SendType sendType, bool canManagePolicies, + // Disable Send policy check + + private void SaveSendAsync_DisableSend_Setup(SendType sendType, bool canManagePolicies, SutProvider sutProvider, Send send, List policies) { send.Id = default; @@ -36,7 +39,7 @@ namespace Bit.Core.Test.Services public async void SaveSendAsync_DisableSend_CantManagePolicies_throws(SendType sendType, SutProvider sutProvider, Send send, List policies) { - SaveSendAsync_Setup(sendType, canManagePolicies: false, sutProvider, send, policies); + SaveSendAsync_DisableSend_Setup(sendType, canManagePolicies: false, sutProvider, send, policies); await Assert.ThrowsAsync(() => sutProvider.Sut.SaveSendAsync(send)); } @@ -47,7 +50,7 @@ namespace Bit.Core.Test.Services public async void SaveSendAsync_DisableSend_DisabledPolicy_CantManagePolicies_success(SendType sendType, SutProvider sutProvider, Send send, List policies) { - SaveSendAsync_Setup(sendType, canManagePolicies: false, sutProvider, send, policies); + SaveSendAsync_DisableSend_Setup(sendType, canManagePolicies: false, sutProvider, send, policies); foreach (var policy in policies.Where(p => p.Type == PolicyType.DisableSend)) { policy.Enabled = false; @@ -64,7 +67,75 @@ namespace Bit.Core.Test.Services public async void SaveSendAsync_DisableSend_CanManagePolicies_success(SendType sendType, SutProvider sutProvider, Send send, List policies) { - SaveSendAsync_Setup(sendType, canManagePolicies: true, sutProvider, send, policies); + SaveSendAsync_DisableSend_Setup(sendType, canManagePolicies: true, sutProvider, send, policies); + + await sutProvider.Sut.SaveSendAsync(send); + + await sutProvider.GetDependency().Received(1).CreateAsync(send); + } + + // SendOptionsPolicy.DisableHideEmail check + + private void SaveSendAsync_DisableHideEmail_Setup(SendType sendType, bool canManagePolicies, + SutProvider sutProvider, Send send, List policies) + { + send.Id = default; + send.Type = sendType; + send.HideEmail = true; + + var dataObj = new SendOptionsPolicyData(); + dataObj.DisableHideEmail = true; + + policies.First().Type = PolicyType.SendOptions; + policies.First().Enabled = true; + policies.First().Data = JsonConvert.SerializeObject(dataObj); + + sutProvider.GetDependency().GetManyByUserIdAsync(send.UserId.Value).Returns(policies); + sutProvider.GetDependency().ManagePolicies(Arg.Any()).Returns(canManagePolicies); + } + + + [Theory] + [InlineUserSendAutoData(SendType.File)] + [InlineUserSendAutoData(SendType.Text)] + public async void SaveSendAsync_DisableHideEmail_CantManagePolicies_throws(SendType sendType, + SutProvider sutProvider, Send send, List policies) + { + SaveSendAsync_DisableHideEmail_Setup(sendType, canManagePolicies: false, sutProvider, send, policies); + + await Assert.ThrowsAsync(() => sutProvider.Sut.SaveSendAsync(send)); + } + + [Theory] + [InlineUserSendAutoData(SendType.File)] + [InlineUserSendAutoData(SendType.Text)] + public async void SaveSendAsync_DisableHideEmail_CantManagePolicies_success(SendType sendType, + SutProvider sutProvider, Send send, List policies) + { + SaveSendAsync_DisableHideEmail_Setup(sendType, canManagePolicies: false, sutProvider, send, policies); + + var policyData = new SendOptionsPolicyData(); + policyData.DisableHideEmail = false; + var policyDataSerialized = JsonConvert.SerializeObject(policyData); + + foreach (var policy in policies.Where(p => p.Type == PolicyType.SendOptions)) + { + policies.First().Enabled = true; + policies.First().Data = policyDataSerialized; + } + + await sutProvider.Sut.SaveSendAsync(send); + + await sutProvider.GetDependency().Received(1).CreateAsync(send); + } + + [Theory] + [InlineUserSendAutoData(SendType.File)] + [InlineUserSendAutoData(SendType.Text)] + public async void SaveSendAsync_DisableHideEmail_CanManagePolicies_success(SendType sendType, + SutProvider sutProvider, Send send, List policies) + { + SaveSendAsync_DisableHideEmail_Setup(sendType, canManagePolicies: true, sutProvider, send, policies); await sutProvider.Sut.SaveSendAsync(send); diff --git a/util/Migrator/DbScripts/2021-03-22_00_Send_Add_HideEmail_Column.sql b/util/Migrator/DbScripts/2021-03-22_00_Send_Add_HideEmail_Column.sql new file mode 100644 index 000000000..b818cd20e --- /dev/null +++ b/util/Migrator/DbScripts/2021-03-22_00_Send_Add_HideEmail_Column.sql @@ -0,0 +1,151 @@ +-- Add HideEmail column +IF COL_LENGTH('[dbo].[Send]', 'HideEmail') IS NULL +BEGIN + ALTER TABLE [dbo].[Send] ADD [HideEmail] BIT NULL; +END +GO + +-- Recreate View +IF OBJECT_ID('[dbo].[SendView]') IS NOT NULL +BEGIN + DROP VIEW [dbo].[SendView] +END +GO + +CREATE VIEW [dbo].[SendView] +AS +SELECT + * +FROM + [dbo].[Send] +GO + +-- Recreate procedures +IF OBJECT_ID('[dbo].[Send_Create]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Send_Create] +END +GO + +CREATE PROCEDURE [dbo].[Send_Create] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Data VARCHAR(MAX), + @Key VARCHAR(MAX), + @Password NVARCHAR(300), + @MaxAccessCount INT, + @AccessCount INT, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @ExpirationDate DATETIME2(7), + @DeletionDate DATETIME2(7), + @Disabled BIT, + @HideEmail BIT +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[Send] + ( + [Id], + [UserId], + [OrganizationId], + [Type], + [Data], + [Key], + [Password], + [MaxAccessCount], + [AccessCount], + [CreationDate], + [RevisionDate], + [ExpirationDate], + [DeletionDate], + [Disabled], + [HideEmail] + ) + VALUES + ( + @Id, + @UserId, + @OrganizationId, + @Type, + @Data, + @Key, + @Password, + @MaxAccessCount, + @AccessCount, + @CreationDate, + @RevisionDate, + @ExpirationDate, + @DeletionDate, + @Disabled, + @HideEmail + ) + + IF @UserId IS NOT NULL + BEGIN + IF @Type = 1 --File + BEGIN + EXEC [dbo].[User_UpdateStorage] @UserId + END + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END + -- TODO: OrganizationId bump? +END +GO + +IF OBJECT_ID('[dbo].[Send_Update]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Send_Update] +END +GO + +CREATE PROCEDURE [dbo].[Send_Update] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Data VARCHAR(MAX), + @Key VARCHAR(MAX), + @Password NVARCHAR(300), + @MaxAccessCount INT, + @AccessCount INT, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @ExpirationDate DATETIME2(7), + @DeletionDate DATETIME2(7), + @Disabled BIT, + @HideEmail BIT +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[Send] + SET + [UserId] = @UserId, + [OrganizationId] = @OrganizationId, + [Type] = @Type, + [Data] = @Data, + [Key] = @Key, + [Password] = @Password, + [MaxAccessCount] = @MaxAccessCount, + [AccessCount] = @AccessCount, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate, + [ExpirationDate] = @ExpirationDate, + [DeletionDate] = @DeletionDate, + [Disabled] = @Disabled, + [HideEmail] = @HideEmail + WHERE + [Id] = @Id + + IF @UserId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END + -- TODO: OrganizationId bump? +END +GO