1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-25 12:45:18 +01:00

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>
This commit is contained in:
Thomas Rittson 2021-03-29 07:56:56 +10:00 committed by GitHub
parent 94249747b4
commit 688cc00d48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 334 additions and 17 deletions

View File

@ -137,6 +137,7 @@ namespace Bit.Portal.Controllers
case PolicyType.TwoFactorAuthentication: case PolicyType.TwoFactorAuthentication:
case PolicyType.PersonalOwnership: case PolicyType.PersonalOwnership:
case PolicyType.DisableSend: case PolicyType.DisableSend:
case PolicyType.SendOptions:
break; break;
case PolicyType.SingleOrg: case PolicyType.SingleOrg:

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Text.Json; using System.Text.Json;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Table; using Bit.Core.Models.Table;
using Bit.Core.Models.Data;
using Bit.Core.Services; using Bit.Core.Services;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
@ -45,6 +46,9 @@ namespace Bit.Portal.Models
case PolicyType.PasswordGenerator: case PolicyType.PasswordGenerator:
PasswordGeneratorDataModel = JsonSerializer.Deserialize<PasswordGeneratorDataModel>(model.Data, options); PasswordGeneratorDataModel = JsonSerializer.Deserialize<PasswordGeneratorDataModel>(model.Data, options);
break; break;
case PolicyType.SendOptions:
SendOptionsDataModel = JsonSerializer.Deserialize<SendOptionsPolicyData>(model.Data, options);
break;
default: default:
throw new ArgumentOutOfRangeException(); throw new ArgumentOutOfRangeException();
} }
@ -53,6 +57,7 @@ namespace Bit.Portal.Models
public MasterPasswordDataModel MasterPasswordDataModel { get; set; } public MasterPasswordDataModel MasterPasswordDataModel { get; set; }
public PasswordGeneratorDataModel PasswordGeneratorDataModel { get; set; } public PasswordGeneratorDataModel PasswordGeneratorDataModel { get; set; }
public SendOptionsPolicyData SendOptionsDataModel { get; set; }
public List<SelectListItem> Complexities { get; set; } public List<SelectListItem> Complexities { get; set; }
public List<SelectListItem> DefaultTypes { get; set; } public List<SelectListItem> DefaultTypes { get; set; }
public string EnableCheckboxText { get; set; } public string EnableCheckboxText { get; set; }
@ -88,6 +93,9 @@ namespace Bit.Portal.Models
case PolicyType.PersonalOwnership: case PolicyType.PersonalOwnership:
case PolicyType.DisableSend: case PolicyType.DisableSend:
break; break;
case PolicyType.SendOptions:
existingPolicy.Data = JsonSerializer.Serialize(SendOptionsDataModel, options);
break;
default: default:
throw new ArgumentOutOfRangeException(); throw new ArgumentOutOfRangeException();
} }

View File

@ -52,6 +52,11 @@ namespace Bit.Portal.Models
DescriptionKey = "DisableSendDescription"; DescriptionKey = "DisableSendDescription";
break; break;
case PolicyType.SendOptions:
NameKey = "SendOptions";
DescriptionKey = "SendOptionsDescription";
break;
default: default:
throw new ArgumentOutOfRangeException(); throw new ArgumentOutOfRangeException();
} }

View File

@ -73,6 +73,17 @@
</div> </div>
} }
@if (Model.PolicyType == PolicyType.SendOptions)
{
<div class="callout callout-warning" role="alert">
<h3 class="callout-heading">
<i class="fa fa-warning" *ngIf="icon" aria-hidden="true"></i>
@i18nService.T("Warning")
</h3>
@i18nService.T("SendOptionsExemption")
</div>
}
<div asp-validation-summary="ModelOnly" class="alert alert-danger"></div> <div asp-validation-summary="ModelOnly" class="alert alert-danger"></div>
<div class="form-group"> <div class="form-group">
@ -187,6 +198,19 @@
</div> </div>
} }
@if (Model.PolicyType == PolicyType.SendOptions)
{
<div class="config-section">
<h2>@i18nService.T("Options")</h2>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" asp-for="SendOptionsDataModel.DisableHideEmail">
<label class="form-check-label" asp-for="SendOptionsDataModel.DisableHideEmail"></label>
</div>
</div>
</div>
}
<div class="d-flex"> <div class="d-flex">
<button type="submit" class="btn btn-primary">@i18nService.T("Save")</button> <button type="submit" class="btn btn-primary">@i18nService.T("Save")</button>
<a class="btn btn-outline-secondary ml-1" asp-controller="Policies" asp-action="Index">@i18nService.T("Cancel")</a> <a class="btn btn-outline-secondary ml-1" asp-controller="Policies" asp-action="Index">@i18nService.T("Cancel")</a>

View File

@ -70,7 +70,7 @@ namespace Bit.Api.Controllers
} }
var sendResponse = new SendAccessResponseModel(send, _globalSettings); 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); var creator = await _userService.GetUserByIdAsync(send.UserId.Value);
sendResponse.CreatorIdentifier = creator.Email; sendResponse.CreatorIdentifier = creator.Email;

View File

@ -9,5 +9,6 @@
RequireSso = 4, RequireSso = 4,
PersonalOwnership = 5, PersonalOwnership = 5,
DisableSend = 6, DisableSend = 6,
SendOptions = 7,
} }
} }

View File

@ -35,6 +35,7 @@ namespace Bit.Core.Models.Api
public string Password { get; set; } public string Password { get; set; }
[Required] [Required]
public bool? Disabled { get; set; } public bool? Disabled { get; set; }
public bool? HideEmail { get; set; }
public Send ToSend(Guid userId, ISendService sendService) public Send ToSend(Guid userId, ISendService sendService)
{ {
@ -125,6 +126,7 @@ namespace Bit.Core.Models.Api
existingSend.Password = sendService.HashPassword(Password); existingSend.Password = sendService.HashPassword(Password);
} }
existingSend.Disabled = Disabled.GetValueOrDefault(); existingSend.Disabled = Disabled.GetValueOrDefault();
existingSend.HideEmail = HideEmail.GetValueOrDefault();
return existingSend; return existingSend;
} }
} }

View File

@ -29,6 +29,7 @@ namespace Bit.Core.Models.Api
DeletionDate = send.DeletionDate; DeletionDate = send.DeletionDate;
Password = send.Password; Password = send.Password;
Disabled = send.Disabled; Disabled = send.Disabled;
HideEmail = send.HideEmail.GetValueOrDefault();
SendData sendData; SendData sendData;
switch (send.Type) switch (send.Type)
@ -66,5 +67,6 @@ namespace Bit.Core.Models.Api
public DateTime RevisionDate { get; set; } public DateTime RevisionDate { get; set; }
public DateTime? ExpirationDate { get; set; } public DateTime? ExpirationDate { get; set; }
public DateTime DeletionDate { get; set; } public DateTime DeletionDate { get; set; }
public bool HideEmail { get; set; }
} }
} }

View File

@ -0,0 +1,10 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Models.Data
{
public class SendOptionsPolicyData
{
[Display(Name = "DisableHideEmail")]
public bool DisableHideEmail { get; set; }
}
}

View File

@ -20,6 +20,7 @@ namespace Bit.Core.Models.Table
public DateTime? ExpirationDate { get; set; } public DateTime? ExpirationDate { get; set; }
public DateTime DeletionDate { get; set; } public DateTime DeletionDate { get; set; }
public bool Disabled { get; set; } public bool Disabled { get; set; }
public bool? HideEmail { get; set; }
public void SetNewId() public void SetNewId()
{ {

View File

@ -598,6 +598,24 @@
<data name="DisableSendExemption" xml:space="preserve"> <data name="DisableSendExemption" xml:space="preserve">
<value>Organization Owners and Administrators are exempt from this policy's enforcement.</value> <value>Organization Owners and Administrators are exempt from this policy's enforcement.</value>
</data> </data>
<data name="SendOptions" xml:space="preserve">
<value>Send Options</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="Options" xml:space="preserve">
<value>Options</value>
</data>
<data name="DisableHideEmail" xml:space="preserve">
<value>Do not allow users to hide their email address when creating or editing a Send.</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="SendOptionsDescription" xml:space="preserve">
<value>Set options for creating and editing Sends.</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="SendOptionsExemption" xml:space="preserve">
<value>Organization Owners and Administrators are exempt from this policy's enforcement.</value>
</data>
<data name="DisableRequireSsoError" xml:space="preserve"> <data name="DisableRequireSsoError" xml:space="preserve">
<value>You must manually disable the Single Sign-On Authentication policy before this policy can be disabled.</value> <value>You must manually disable the Single Sign-On Authentication policy before this policy can be disabled.</value>
</data> </data>

View File

@ -59,7 +59,7 @@ namespace Bit.Core.Services
public async Task SaveSendAsync(Send send) public async Task SaveSendAsync(Send send)
{ {
// Make sure user can save Sends // Make sure user can save Sends
await ValidateUserCanSaveAsync(send.UserId); await ValidateUserCanSaveAsync(send.UserId, send);
if (send.Id == default(Guid)) if (send.Id == default(Guid))
{ {
@ -265,7 +265,7 @@ namespace Bit.Core.Services
return _passwordHasher.HashPassword(new User(), password); 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)) 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."); 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<SendOptionsPolicyData>(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<long> StorageRemainingForSendAsync(Send send) private async Task<long> StorageRemainingForSendAsync(Send send)

View File

@ -12,7 +12,8 @@
@RevisionDate DATETIME2(7), @RevisionDate DATETIME2(7),
@ExpirationDate DATETIME2(7), @ExpirationDate DATETIME2(7),
@DeletionDate DATETIME2(7), @DeletionDate DATETIME2(7),
@Disabled BIT @Disabled BIT,
@HideEmail BIT
AS AS
BEGIN BEGIN
SET NOCOUNT ON SET NOCOUNT ON
@ -32,7 +33,8 @@ BEGIN
[RevisionDate], [RevisionDate],
[ExpirationDate], [ExpirationDate],
[DeletionDate], [DeletionDate],
[Disabled] [Disabled],
[HideEmail]
) )
VALUES VALUES
( (
@ -49,7 +51,8 @@ BEGIN
@RevisionDate, @RevisionDate,
@ExpirationDate, @ExpirationDate,
@DeletionDate, @DeletionDate,
@Disabled @Disabled,
@HideEmail
) )
IF @UserId IS NOT NULL IF @UserId IS NOT NULL

View File

@ -12,7 +12,8 @@
@RevisionDate DATETIME2(7), @RevisionDate DATETIME2(7),
@ExpirationDate DATETIME2(7), @ExpirationDate DATETIME2(7),
@DeletionDate DATETIME2(7), @DeletionDate DATETIME2(7),
@Disabled BIT @Disabled BIT,
@HideEmail BIT
AS AS
BEGIN BEGIN
SET NOCOUNT ON SET NOCOUNT ON
@ -32,7 +33,8 @@ BEGIN
[RevisionDate] = @RevisionDate, [RevisionDate] = @RevisionDate,
[ExpirationDate] = @ExpirationDate, [ExpirationDate] = @ExpirationDate,
[DeletionDate] = @DeletionDate, [DeletionDate] = @DeletionDate,
[Disabled] = @Disabled [Disabled] = @Disabled,
[HideEmail] = @HideEmail
WHERE WHERE
[Id] = @Id [Id] = @Id

View File

@ -13,6 +13,7 @@
[ExpirationDate] DATETIME2 (7) NULL, [ExpirationDate] DATETIME2 (7) NULL,
[DeletionDate] DATETIME2 (7) NOT NULL, [DeletionDate] DATETIME2 (7) NOT NULL,
[Disabled] BIT NOT NULL, [Disabled] BIT NOT NULL,
[HideEmail] BIT NULL,
CONSTRAINT [PK_Send] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [PK_Send] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_Send_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]), CONSTRAINT [FK_Send_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]),
CONSTRAINT [FK_Send_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) CONSTRAINT [FK_Send_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id])

View File

@ -12,12 +12,15 @@ using Bit.Core.Test.AutoFixture;
using Bit.Core.Test.AutoFixture.SendFixtures; using Bit.Core.Test.AutoFixture.SendFixtures;
using NSubstitute; using NSubstitute;
using Xunit; using Xunit;
using Newtonsoft.Json;
namespace Bit.Core.Test.Services namespace Bit.Core.Test.Services
{ {
public class SendServiceTests 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<SendService> sutProvider, Send send, List<Policy> policies) SutProvider<SendService> sutProvider, Send send, List<Policy> policies)
{ {
send.Id = default; send.Id = default;
@ -36,7 +39,7 @@ namespace Bit.Core.Test.Services
public async void SaveSendAsync_DisableSend_CantManagePolicies_throws(SendType sendType, public async void SaveSendAsync_DisableSend_CantManagePolicies_throws(SendType sendType,
SutProvider<SendService> sutProvider, Send send, List<Policy> policies) SutProvider<SendService> sutProvider, Send send, List<Policy> policies)
{ {
SaveSendAsync_Setup(sendType, canManagePolicies: false, sutProvider, send, policies); SaveSendAsync_DisableSend_Setup(sendType, canManagePolicies: false, sutProvider, send, policies);
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.SaveSendAsync(send)); await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.SaveSendAsync(send));
} }
@ -47,7 +50,7 @@ namespace Bit.Core.Test.Services
public async void SaveSendAsync_DisableSend_DisabledPolicy_CantManagePolicies_success(SendType sendType, public async void SaveSendAsync_DisableSend_DisabledPolicy_CantManagePolicies_success(SendType sendType,
SutProvider<SendService> sutProvider, Send send, List<Policy> policies) SutProvider<SendService> sutProvider, Send send, List<Policy> 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)) foreach (var policy in policies.Where(p => p.Type == PolicyType.DisableSend))
{ {
policy.Enabled = false; policy.Enabled = false;
@ -64,7 +67,75 @@ namespace Bit.Core.Test.Services
public async void SaveSendAsync_DisableSend_CanManagePolicies_success(SendType sendType, public async void SaveSendAsync_DisableSend_CanManagePolicies_success(SendType sendType,
SutProvider<SendService> sutProvider, Send send, List<Policy> policies) SutProvider<SendService> sutProvider, Send send, List<Policy> 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<ISendRepository>().Received(1).CreateAsync(send);
}
// SendOptionsPolicy.DisableHideEmail check
private void SaveSendAsync_DisableHideEmail_Setup(SendType sendType, bool canManagePolicies,
SutProvider<SendService> sutProvider, Send send, List<Policy> 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<IPolicyRepository>().GetManyByUserIdAsync(send.UserId.Value).Returns(policies);
sutProvider.GetDependency<ICurrentContext>().ManagePolicies(Arg.Any<Guid>()).Returns(canManagePolicies);
}
[Theory]
[InlineUserSendAutoData(SendType.File)]
[InlineUserSendAutoData(SendType.Text)]
public async void SaveSendAsync_DisableHideEmail_CantManagePolicies_throws(SendType sendType,
SutProvider<SendService> sutProvider, Send send, List<Policy> policies)
{
SaveSendAsync_DisableHideEmail_Setup(sendType, canManagePolicies: false, sutProvider, send, policies);
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.SaveSendAsync(send));
}
[Theory]
[InlineUserSendAutoData(SendType.File)]
[InlineUserSendAutoData(SendType.Text)]
public async void SaveSendAsync_DisableHideEmail_CantManagePolicies_success(SendType sendType,
SutProvider<SendService> sutProvider, Send send, List<Policy> 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<ISendRepository>().Received(1).CreateAsync(send);
}
[Theory]
[InlineUserSendAutoData(SendType.File)]
[InlineUserSendAutoData(SendType.Text)]
public async void SaveSendAsync_DisableHideEmail_CanManagePolicies_success(SendType sendType,
SutProvider<SendService> sutProvider, Send send, List<Policy> policies)
{
SaveSendAsync_DisableHideEmail_Setup(sendType, canManagePolicies: true, sutProvider, send, policies);
await sutProvider.Sut.SaveSendAsync(send); await sutProvider.Sut.SaveSendAsync(send);

View File

@ -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