1
0
mirror of https://github.com/bitwarden/server synced 2025-12-28 06:03:29 +00:00

Merge branch 'arch/seeder-sdk' of github.com:bitwarden/server into arch/seeder-api

# Conflicts:
#	util/Seeder/Factories/UserSeeder.cs
This commit is contained in:
Hinton
2025-10-09 15:53:47 -07:00
42 changed files with 4020 additions and 674 deletions

View File

@@ -66,7 +66,7 @@ public class AccountBillingVNextController(
}
[HttpPost("subscription")]
[RequireFeature(FeatureFlagKeys.PM23385_UseNewPremiumFlow)]
[RequireFeature(FeatureFlagKeys.PM24996ImplementUpgradeFromFreeDialog)]
[InjectUser]
public async Task<IResult> CreateSubscriptionAsync(
[BindNever] User user,

View File

@@ -21,7 +21,7 @@ public class SelfHostedAccountBillingController(
ICreatePremiumSelfHostedSubscriptionCommand createPremiumSelfHostedSubscriptionCommand) : BaseBillingController
{
[HttpPost("license")]
[RequireFeature(FeatureFlagKeys.PM23385_UseNewPremiumFlow)]
[RequireFeature(FeatureFlagKeys.PM24996ImplementUpgradeFromFreeDialog)]
[InjectUser]
public async Task<IResult> UploadLicenseAsync(
[BindNever] User user,

View File

@@ -44,6 +44,15 @@ public class BillingSettings
{
public virtual string ApiKey { get; set; }
public virtual string BaseUrl { get; set; }
public virtual string Path { get; set; }
public virtual int PersonaId { get; set; }
public virtual bool UseAnswerWithCitationModels { get; set; } = true;
public virtual SearchSettings SearchSettings { get; set; } = new SearchSettings();
}
public class SearchSettings
{
public virtual string RunSearch { get; set; } = "auto"; // "always", "never", "auto"
public virtual bool RealTime { get; set; } = true;
}
}

View File

@@ -1,7 +1,4 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
using System.Net.Http.Headers;
using System.Reflection;
using System.Text;
@@ -35,7 +32,7 @@ public class FreshdeskController : Controller
GlobalSettings globalSettings,
IHttpClientFactory httpClientFactory)
{
_billingSettings = billingSettings?.Value;
_billingSettings = billingSettings?.Value ?? throw new ArgumentNullException(nameof(billingSettings));
_userRepository = userRepository;
_organizationRepository = organizationRepository;
_logger = logger;
@@ -101,7 +98,8 @@ public class FreshdeskController : Controller
customFields[_billingSettings.FreshDesk.OrgFieldName] += $"\n{orgNote}";
}
var planName = GetAttribute<DisplayAttribute>(org.PlanType).Name.Split(" ").FirstOrDefault();
var displayAttribute = GetAttribute<DisplayAttribute>(org.PlanType);
var planName = displayAttribute?.Name?.Split(" ").FirstOrDefault();
if (!string.IsNullOrWhiteSpace(planName))
{
tags.Add(string.Format("Org: {0}", planName));
@@ -159,28 +157,22 @@ public class FreshdeskController : Controller
return Ok();
}
// create the onyx `answer-with-citation` request
var onyxRequestModel = new OnyxAnswerWithCitationRequestModel(model.TicketDescriptionText, _billingSettings.Onyx.PersonaId);
var onyxRequest = new HttpRequestMessage(HttpMethod.Post,
string.Format("{0}/query/answer-with-citation", _billingSettings.Onyx.BaseUrl))
{
Content = JsonContent.Create(onyxRequestModel, mediaType: new MediaTypeHeaderValue("application/json")),
};
var (_, onyxJsonResponse) = await CallOnyxApi<OnyxAnswerWithCitationResponseModel>(onyxRequest);
// Get response from Onyx AI
var (onyxRequest, onyxResponse) = await GetAnswerFromOnyx(model);
// the CallOnyxApi will return a null if we have an error response
if (onyxJsonResponse?.Answer == null || !string.IsNullOrEmpty(onyxJsonResponse?.ErrorMsg))
if (onyxResponse?.Answer == null || !string.IsNullOrEmpty(onyxResponse?.ErrorMsg))
{
_logger.LogWarning("Error getting answer from Onyx AI. Freshdesk model: {model}\r\n Onyx query {query}\r\nresponse: {response}. ",
JsonSerializer.Serialize(model),
JsonSerializer.Serialize(onyxRequestModel),
JsonSerializer.Serialize(onyxJsonResponse));
JsonSerializer.Serialize(onyxRequest),
JsonSerializer.Serialize(onyxResponse));
return Ok(); // return ok so we don't retry
}
// add the answer as a note to the ticket
await AddAnswerNoteToTicketAsync(onyxJsonResponse.Answer, model.TicketId);
await AddAnswerNoteToTicketAsync(onyxResponse?.Answer ?? string.Empty, model.TicketId);
return Ok();
}
@@ -206,27 +198,21 @@ public class FreshdeskController : Controller
}
// create the onyx `answer-with-citation` request
var onyxRequestModel = new OnyxAnswerWithCitationRequestModel(model.TicketDescriptionText, _billingSettings.Onyx.PersonaId);
var onyxRequest = new HttpRequestMessage(HttpMethod.Post,
string.Format("{0}/query/answer-with-citation", _billingSettings.Onyx.BaseUrl))
{
Content = JsonContent.Create(onyxRequestModel, mediaType: new MediaTypeHeaderValue("application/json")),
};
var (_, onyxJsonResponse) = await CallOnyxApi<OnyxAnswerWithCitationResponseModel>(onyxRequest);
var (onyxRequest, onyxResponse) = await GetAnswerFromOnyx(model);
// the CallOnyxApi will return a null if we have an error response
if (onyxJsonResponse?.Answer == null || !string.IsNullOrEmpty(onyxJsonResponse?.ErrorMsg))
if (onyxResponse?.Answer == null || !string.IsNullOrEmpty(onyxResponse?.ErrorMsg))
{
_logger.LogWarning("Error getting answer from Onyx AI. Freshdesk model: {model}\r\n Onyx query {query}\r\nresponse: {response}. ",
JsonSerializer.Serialize(model),
JsonSerializer.Serialize(onyxRequestModel),
JsonSerializer.Serialize(onyxJsonResponse));
JsonSerializer.Serialize(onyxRequest),
JsonSerializer.Serialize(onyxResponse));
return Ok(); // return ok so we don't retry
}
// add the reply to the ticket
await AddReplyToTicketAsync(onyxJsonResponse.Answer, model.TicketId);
await AddReplyToTicketAsync(onyxResponse?.Answer ?? string.Empty, model.TicketId);
return Ok();
}
@@ -356,7 +342,32 @@ public class FreshdeskController : Controller
return await CallFreshdeskApiAsync(request, retriedCount++);
}
private async Task<(HttpResponseMessage, T)> CallOnyxApi<T>(HttpRequestMessage request)
async Task<(OnyxRequestModel onyxRequest, OnyxResponseModel onyxResponse)> GetAnswerFromOnyx(FreshdeskOnyxAiWebhookModel model)
{
// TODO: remove the use of the deprecated answer-with-citation models after we are sure
if (_billingSettings.Onyx.UseAnswerWithCitationModels)
{
var onyxRequest = new OnyxAnswerWithCitationRequestModel(model.TicketDescriptionText, _billingSettings.Onyx);
var onyxAnswerWithCitationRequest = new HttpRequestMessage(HttpMethod.Post,
string.Format("{0}/query/answer-with-citation", _billingSettings.Onyx.BaseUrl))
{
Content = JsonContent.Create(onyxRequest, mediaType: new MediaTypeHeaderValue("application/json")),
};
var onyxResponse = await CallOnyxApi<OnyxResponseModel>(onyxAnswerWithCitationRequest);
return (onyxRequest, onyxResponse);
}
var request = new OnyxSendMessageSimpleApiRequestModel(model.TicketDescriptionText, _billingSettings.Onyx);
var onyxSimpleRequest = new HttpRequestMessage(HttpMethod.Post,
string.Format("{0}{1}", _billingSettings.Onyx.BaseUrl, _billingSettings.Onyx.Path))
{
Content = JsonContent.Create(request, mediaType: new MediaTypeHeaderValue("application/json")),
};
var onyxSimpleResponse = await CallOnyxApi<OnyxResponseModel>(onyxSimpleRequest);
return (request, onyxSimpleResponse);
}
private async Task<T> CallOnyxApi<T>(HttpRequestMessage request) where T : class, new()
{
var httpClient = _httpClientFactory.CreateClient("OnyxApi");
var response = await httpClient.SendAsync(request);
@@ -365,7 +376,7 @@ public class FreshdeskController : Controller
{
_logger.LogError("Error calling Onyx AI API. Status code: {0}. Response {1}",
response.StatusCode, JsonSerializer.Serialize(response));
return (null, default);
return new T();
}
var responseStr = await response.Content.ReadAsStringAsync();
var responseJson = JsonSerializer.Deserialize<T>(responseStr, options: new JsonSerializerOptions
@@ -373,11 +384,12 @@ public class FreshdeskController : Controller
PropertyNameCaseInsensitive = true,
});
return (response, responseJson);
return responseJson ?? new T();
}
private TAttribute GetAttribute<TAttribute>(Enum enumValue) where TAttribute : Attribute
private TAttribute? GetAttribute<TAttribute>(Enum enumValue) where TAttribute : Attribute
{
return enumValue.GetType().GetMember(enumValue.ToString()).First().GetCustomAttribute<TAttribute>();
var memberInfo = enumValue.GetType().GetMember(enumValue.ToString()).FirstOrDefault();
return memberInfo != null ? memberInfo.GetCustomAttribute<TAttribute>() : null;
}
}

View File

@@ -1,35 +1,58 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using System.Text.Json.Serialization;
using System.Text.Json.Serialization;
using static Bit.Billing.BillingSettings;
namespace Bit.Billing.Models;
public class OnyxAnswerWithCitationRequestModel
public class OnyxRequestModel
{
[JsonPropertyName("messages")]
public List<Message> Messages { get; set; }
[JsonPropertyName("persona_id")]
public int PersonaId { get; set; } = 1;
[JsonPropertyName("retrieval_options")]
public RetrievalOptions RetrievalOptions { get; set; }
public RetrievalOptions RetrievalOptions { get; set; } = new RetrievalOptions();
public OnyxAnswerWithCitationRequestModel(string message, int personaId = 1)
public OnyxRequestModel(OnyxSettings onyxSettings)
{
PersonaId = onyxSettings.PersonaId;
RetrievalOptions.RunSearch = onyxSettings.SearchSettings.RunSearch;
RetrievalOptions.RealTime = onyxSettings.SearchSettings.RealTime;
}
}
/// <summary>
/// This is used with the onyx endpoint /query/answer-with-citation
/// which has been deprecated. This can be removed once later
/// </summary>
public class OnyxAnswerWithCitationRequestModel : OnyxRequestModel
{
[JsonPropertyName("messages")]
public List<Message> Messages { get; set; } = new List<Message>();
public OnyxAnswerWithCitationRequestModel(string message, OnyxSettings onyxSettings) : base(onyxSettings)
{
message = message.Replace(Environment.NewLine, " ").Replace('\r', ' ').Replace('\n', ' ');
Messages = new List<Message>() { new Message() { MessageText = message } };
RetrievalOptions = new RetrievalOptions();
PersonaId = personaId;
}
}
/// <summary>
/// This is used with the onyx endpoint /chat/send-message-simple-api
/// </summary>
public class OnyxSendMessageSimpleApiRequestModel : OnyxRequestModel
{
[JsonPropertyName("message")]
public string Message { get; set; } = string.Empty;
public OnyxSendMessageSimpleApiRequestModel(string message, OnyxSettings onyxSettings) : base(onyxSettings)
{
Message = message.Replace(Environment.NewLine, " ").Replace('\r', ' ').Replace('\n', ' ');
}
}
public class Message
{
[JsonPropertyName("message")]
public string MessageText { get; set; }
public string MessageText { get; set; } = string.Empty;
[JsonPropertyName("sender")]
public string Sender { get; set; } = "user";

View File

@@ -1,33 +0,0 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using System.Text.Json.Serialization;
namespace Bit.Billing.Models;
public class OnyxAnswerWithCitationResponseModel
{
[JsonPropertyName("answer")]
public string Answer { get; set; }
[JsonPropertyName("rephrase")]
public string Rephrase { get; set; }
[JsonPropertyName("citations")]
public List<Citation> Citations { get; set; }
[JsonPropertyName("llm_selected_doc_indices")]
public List<int> LlmSelectedDocIndices { get; set; }
[JsonPropertyName("error_msg")]
public string ErrorMsg { get; set; }
}
public class Citation
{
[JsonPropertyName("citation_num")]
public int CitationNum { get; set; }
[JsonPropertyName("document_id")]
public string DocumentId { get; set; }
}

View File

@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace Bit.Billing.Models;
public class OnyxResponseModel
{
[JsonPropertyName("answer")]
public string Answer { get; set; } = string.Empty;
[JsonPropertyName("answer_citationless")]
public string AnswerCitationless { get; set; } = string.Empty;
[JsonPropertyName("error_msg")]
public string ErrorMsg { get; set; } = string.Empty;
}

View File

@@ -80,7 +80,13 @@
"onyx": {
"apiKey": "SECRET",
"baseUrl": "https://cloud.onyx.app/api",
"personaId": 7
"path": "/chat/send-message-simple-api",
"useAnswerWithCitationModels": true,
"personaId": 7,
"searchSettings": {
"runSearch": "always",
"realTime": true
}
}
}
}

View File

@@ -135,15 +135,12 @@ public static class AuthenticationSchemes
public static class FeatureFlagKeys
{
/* Admin Console Team */
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";
public const string LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission";
public const string PolicyRequirements = "pm-14439-policy-requirements";
public const string ScimInviteUserOptimization = "pm-16811-optimize-invite-user-flow-to-fail-fast";
public const string EventBasedOrganizationIntegrations = "event-based-organization-integrations";
public const string SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions";
public const string CreateDefaultLocation = "pm-19467-create-default-location";
public const string PM23845_VNextApplicationCache = "pm-24957-refactor-memory-application-cache";
public const string CipherRepositoryBulkResourceCreation = "pm-24951-cipher-repository-bulk-resource-creation-service";
/* Auth Team */
public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence";
@@ -182,9 +179,9 @@ public static class FeatureFlagKeys
public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates";
public const string PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover";
public const string PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings";
public const string PM23385_UseNewPremiumFlow = "pm-23385-use-new-premium-flow";
public const string PM24996ImplementUpgradeFromFreeDialog = "pm-24996-implement-upgrade-from-free-dialog";
public const string PM24032_NewNavigationPremiumUpgradeButton = "pm-24032-new-navigation-premium-upgrade-button";
public const string PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog = "pm-23713-premium-badge-opens-new-premium-upgrade-dialog";
/* Key Management Team */
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
@@ -234,7 +231,6 @@ public static class FeatureFlagKeys
/* Vault Team */
public const string PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge";
public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form";
public const string SecurityTasks = "security-tasks";
public const string CipherKeyEncryption = "cipher-key-encryption";
public const string DesktopCipherForms = "pm-18520-desktop-cipher-forms";
public const string PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk";

View File

@@ -25,7 +25,6 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
private readonly IdentityErrorDescriber _identityErrorDescriber;
private readonly IWebAuthnCredentialRepository _credentialRepository;
private readonly IPasswordHasher<User> _passwordHasher;
private readonly IFeatureService _featureService;
/// <summary>
/// Instantiates a new <see cref="RotateUserAccountKeysCommand"/>
@@ -61,7 +60,6 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
_identityErrorDescriber = errors;
_credentialRepository = credentialRepository;
_passwordHasher = passwordHasher;
_featureService = featureService;
}
/// <inheritdoc />
@@ -103,15 +101,7 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions = new();
if (model.Ciphers.Any())
{
var useBulkResourceCreationService = _featureService.IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation);
if (useBulkResourceCreationService)
{
saveEncryptedDataActions.Add(_cipherRepository.UpdateForKeyRotation_vNext(user.Id, model.Ciphers));
}
else
{
saveEncryptedDataActions.Add(_cipherRepository.UpdateForKeyRotation(user.Id, model.Ciphers));
}
saveEncryptedDataActions.Add(_cipherRepository.UpdateForKeyRotation(user.Id, model.Ciphers));
}
if (model.Folders.Any())

View File

@@ -108,15 +108,7 @@ public class ImportCiphersCommand : IImportCiphersCommand
}
// Create it all
var useBulkResourceCreationService = _featureService.IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation);
if (useBulkResourceCreationService)
{
await _cipherRepository.CreateAsync_vNext(importingUserId, ciphers, newFolders);
}
else
{
await _cipherRepository.CreateAsync(importingUserId, ciphers, newFolders);
}
await _cipherRepository.CreateAsync(importingUserId, ciphers, newFolders);
// push
await _pushService.PushSyncVaultAsync(importingUserId);
@@ -191,15 +183,7 @@ public class ImportCiphersCommand : IImportCiphersCommand
}
// Create it all
var useBulkResourceCreationService = _featureService.IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation);
if (useBulkResourceCreationService)
{
await _cipherRepository.CreateAsync_vNext(ciphers, newCollections, collectionCiphers, newCollectionUsers);
}
else
{
await _cipherRepository.CreateAsync(ciphers, newCollections, collectionCiphers, newCollectionUsers);
}
await _cipherRepository.CreateAsync(ciphers, newCollections, collectionCiphers, newCollectionUsers);
// push
await _pushService.PushSyncVaultAsync(importingUserId);

View File

@@ -33,28 +33,12 @@ public interface ICipherRepository : IRepository<Cipher, Guid>
Task DeleteByUserIdAsync(Guid userId);
Task DeleteByOrganizationIdAsync(Guid organizationId);
Task UpdateCiphersAsync(Guid userId, IEnumerable<Cipher> ciphers);
/// <inheritdoc cref="UpdateCiphersAsync(Guid, IEnumerable{Cipher})"/>
/// <remarks>
/// This version uses the bulk resource creation service to create the temp table.
/// </remarks>
Task UpdateCiphersAsync_vNext(Guid userId, IEnumerable<Cipher> ciphers);
/// <summary>
/// Create ciphers and folders for the specified UserId. Must not be used to create organization owned items.
/// </summary>
Task CreateAsync(Guid userId, IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders);
/// <inheritdoc cref="CreateAsync(Guid, IEnumerable{Cipher}, IEnumerable{Folder})"/>
/// <remarks>
/// This version uses the bulk resource creation service to create the temp tables.
/// </remarks>
Task CreateAsync_vNext(Guid userId, IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders);
Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections,
IEnumerable<CollectionCipher> collectionCiphers, IEnumerable<CollectionUser> collectionUsers);
/// <inheritdoc cref="CreateAsync(IEnumerable{Cipher}, IEnumerable{Collection}, IEnumerable{CollectionCipher}, IEnumerable{CollectionUser})"/>
/// <remarks>
/// This version uses the bulk resource creation service to create the temp tables.
/// </remarks>
Task CreateAsync_vNext(IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections,
IEnumerable<CollectionCipher> collectionCiphers, IEnumerable<CollectionUser> collectionUsers);
Task SoftDeleteAsync(IEnumerable<Guid> ids, Guid userId);
Task SoftDeleteByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId);
Task<DateTime> UnarchiveAsync(IEnumerable<Guid> ids, Guid userId);
@@ -92,10 +76,4 @@ public interface ICipherRepository : IRepository<Cipher, Guid>
/// </summary>
Task<IEnumerable<CipherOrganizationDetailsWithCollections>>
GetManyCipherOrganizationDetailsExcludingDefaultCollectionsAsync(Guid organizationId);
/// <inheritdoc cref="UpdateForKeyRotation(Guid, IEnumerable{Cipher})"/>
/// <remarks>
/// This version uses the bulk resource creation service to create the temp table.
/// </remarks>
UpdateEncryptedDataForKeyRotation UpdateForKeyRotation_vNext(Guid userId,
IEnumerable<Cipher> ciphers);
}

View File

@@ -644,15 +644,7 @@ public class CipherService : ICipherService
cipherIds.Add(cipher.Id);
}
var useBulkResourceCreationService = _featureService.IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation);
if (useBulkResourceCreationService)
{
await _cipherRepository.UpdateCiphersAsync_vNext(sharingUserId, cipherInfos.Select(c => c.cipher));
}
else
{
await _cipherRepository.UpdateCiphersAsync(sharingUserId, cipherInfos.Select(c => c.cipher));
}
await _cipherRepository.UpdateCiphersAsync(sharingUserId, cipherInfos.Select(c => c.cipher));
await _collectionCipherRepository.UpdateCollectionsForCiphersAsync(cipherIds, sharingUserId,
organizationId, collectionIds);

View File

@@ -13,7 +13,6 @@ using Bit.Core.Vault.Models.Data;
using Bit.Core.Vault.Repositories;
using Bit.Infrastructure.Dapper.AdminConsole.Helpers;
using Bit.Infrastructure.Dapper.Repositories;
using Bit.Infrastructure.Dapper.Vault.Helpers;
using Dapper;
using Microsoft.Data.SqlClient;
@@ -383,63 +382,6 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
cmd.ExecuteNonQuery();
}
// Bulk copy data into temp table
using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction))
{
bulkCopy.DestinationTableName = "#TempCipher";
var ciphersTable = ciphers.ToDataTable();
foreach (DataColumn col in ciphersTable.Columns)
{
bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName);
}
ciphersTable.PrimaryKey = new DataColumn[] { ciphersTable.Columns[0] };
await bulkCopy.WriteToServerAsync(ciphersTable);
}
// Update cipher table from temp table
var sql = @"
UPDATE
[dbo].[Cipher]
SET
[Data] = TC.[Data],
[Attachments] = TC.[Attachments],
[RevisionDate] = TC.[RevisionDate],
[Key] = TC.[Key]
FROM
[dbo].[Cipher] C
INNER JOIN
#TempCipher TC ON C.Id = TC.Id
WHERE
C.[UserId] = @UserId
DROP TABLE #TempCipher";
await using (var cmd = new SqlCommand(sql, connection, transaction))
{
cmd.Parameters.Add("@UserId", SqlDbType.UniqueIdentifier).Value = userId;
cmd.ExecuteNonQuery();
}
};
}
/// <inheritdoc />
public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation_vNext(
Guid userId, IEnumerable<Cipher> ciphers)
{
return async (SqlConnection connection, SqlTransaction transaction) =>
{
// Create temp table
var sqlCreateTemp = @"
SELECT TOP 0 *
INTO #TempCipher
FROM [dbo].[Cipher]";
await using (var cmd = new SqlCommand(sqlCreateTemp, connection, transaction))
{
cmd.ExecuteNonQuery();
}
// Bulk copy data into temp table
await BulkResourceCreationService.CreateTempCiphersAsync(connection, transaction, ciphers);
@@ -476,88 +418,6 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
return;
}
using (var connection = new SqlConnection(ConnectionString))
{
connection.Open();
using (var transaction = connection.BeginTransaction())
{
try
{
// 1. Create temp tables to bulk copy into.
var sqlCreateTemp = @"
SELECT TOP 0 *
INTO #TempCipher
FROM [dbo].[Cipher]";
using (var cmd = new SqlCommand(sqlCreateTemp, connection, transaction))
{
cmd.ExecuteNonQuery();
}
// 2. Bulk copy into temp tables.
using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction))
{
bulkCopy.DestinationTableName = "#TempCipher";
var dataTable = BuildCiphersTable(bulkCopy, ciphers);
bulkCopy.WriteToServer(dataTable);
}
// 3. Insert into real tables from temp tables and clean up.
// Intentionally not including Favorites, Folders, and CreationDate
// since those are not meant to be bulk updated at this time
var sql = @"
UPDATE
[dbo].[Cipher]
SET
[UserId] = TC.[UserId],
[OrganizationId] = TC.[OrganizationId],
[Type] = TC.[Type],
[Data] = TC.[Data],
[Attachments] = TC.[Attachments],
[RevisionDate] = TC.[RevisionDate],
[DeletedDate] = TC.[DeletedDate],
[Key] = TC.[Key]
FROM
[dbo].[Cipher] C
INNER JOIN
#TempCipher TC ON C.Id = TC.Id
WHERE
C.[UserId] = @UserId
DROP TABLE #TempCipher";
using (var cmd = new SqlCommand(sql, connection, transaction))
{
cmd.Parameters.Add("@UserId", SqlDbType.UniqueIdentifier).Value = userId;
cmd.ExecuteNonQuery();
}
await connection.ExecuteAsync(
$"[{Schema}].[User_BumpAccountRevisionDate]",
new { Id = userId },
commandType: CommandType.StoredProcedure, transaction: transaction);
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
}
}
public async Task UpdateCiphersAsync_vNext(Guid userId, IEnumerable<Cipher> ciphers)
{
if (!ciphers.Any())
{
return;
}
using (var connection = new SqlConnection(ConnectionString))
{
connection.Open();
@@ -635,54 +495,6 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
return;
}
using (var connection = new SqlConnection(ConnectionString))
{
connection.Open();
using (var transaction = connection.BeginTransaction())
{
try
{
if (folders.Any())
{
using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction))
{
bulkCopy.DestinationTableName = "[dbo].[Folder]";
var dataTable = BuildFoldersTable(bulkCopy, folders);
bulkCopy.WriteToServer(dataTable);
}
}
using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction))
{
bulkCopy.DestinationTableName = "[dbo].[Cipher]";
var dataTable = BuildCiphersTable(bulkCopy, ciphers);
bulkCopy.WriteToServer(dataTable);
}
await connection.ExecuteAsync(
$"[{Schema}].[User_BumpAccountRevisionDate]",
new { Id = userId },
commandType: CommandType.StoredProcedure, transaction: transaction);
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
}
}
public async Task CreateAsync_vNext(Guid userId, IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders)
{
if (!ciphers.Any())
{
return;
}
using (var connection = new SqlConnection(ConnectionString))
{
connection.Open();
@@ -722,75 +534,6 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
return;
}
using (var connection = new SqlConnection(ConnectionString))
{
connection.Open();
using (var transaction = connection.BeginTransaction())
{
try
{
using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction))
{
bulkCopy.DestinationTableName = "[dbo].[Cipher]";
var dataTable = BuildCiphersTable(bulkCopy, ciphers);
bulkCopy.WriteToServer(dataTable);
}
if (collections.Any())
{
using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction))
{
bulkCopy.DestinationTableName = "[dbo].[Collection]";
var dataTable = BuildCollectionsTable(bulkCopy, collections);
bulkCopy.WriteToServer(dataTable);
}
}
if (collectionCiphers.Any())
{
using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction))
{
bulkCopy.DestinationTableName = "[dbo].[CollectionCipher]";
var dataTable = BuildCollectionCiphersTable(bulkCopy, collectionCiphers);
bulkCopy.WriteToServer(dataTable);
}
}
if (collectionUsers.Any())
{
using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction))
{
bulkCopy.DestinationTableName = "[dbo].[CollectionUser]";
var dataTable = BuildCollectionUsersTable(bulkCopy, collectionUsers);
bulkCopy.WriteToServer(dataTable);
}
}
await connection.ExecuteAsync(
$"[{Schema}].[User_BumpAccountRevisionDateByOrganizationId]",
new { OrganizationId = ciphers.First().OrganizationId },
commandType: CommandType.StoredProcedure, transaction: transaction);
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
}
}
public async Task CreateAsync_vNext(IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections,
IEnumerable<CollectionCipher> collectionCiphers, IEnumerable<CollectionUser> collectionUsers)
{
if (!ciphers.Any())
{
return;
}
using (var connection = new SqlConnection(ConnectionString))
{
connection.Open();

View File

@@ -168,16 +168,6 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
}
}
/// <inheritdoc cref="CreateAsync(Guid, IEnumerable{Cipher}, IEnumerable{Folder})"/>
/// <remarks>
/// EF does not use the bulk resource creation service, so we need to use the regular create method.
/// </remarks>
public async Task CreateAsync_vNext(Guid userId, IEnumerable<Core.Vault.Entities.Cipher> ciphers,
IEnumerable<Core.Vault.Entities.Folder> folders)
{
await CreateAsync(userId, ciphers, folders);
}
public async Task CreateAsync(IEnumerable<Core.Vault.Entities.Cipher> ciphers,
IEnumerable<Core.Entities.Collection> collections,
IEnumerable<Core.Entities.CollectionCipher> collectionCiphers,
@@ -216,18 +206,6 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
}
}
/// <inheritdoc cref="CreateAsync(IEnumerable{Cipher}, IEnumerable{Collection}, IEnumerable{CollectionCipher}, IEnumerable{CollectionUser})"/>
/// <remarks>
/// EF does not use the bulk resource creation service, so we need to use the regular create method.
/// </remarks>
public async Task CreateAsync_vNext(IEnumerable<Core.Vault.Entities.Cipher> ciphers,
IEnumerable<Core.Entities.Collection> collections,
IEnumerable<Core.Entities.CollectionCipher> collectionCiphers,
IEnumerable<Core.Entities.CollectionUser> collectionUsers)
{
await CreateAsync(ciphers, collections, collectionCiphers, collectionUsers);
}
public async Task DeleteAsync(IEnumerable<Guid> ids, Guid userId)
{
await ToggleDeleteCipherStatesAsync(ids, userId, CipherStateAction.HardDelete);
@@ -986,15 +964,6 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
}
}
/// <inheritdoc cref="UpdateCiphersAsync(Guid, IEnumerable{Cipher})"/>
/// <remarks>
/// EF does not use the bulk resource creation service, so we need to use the regular update method.
/// </remarks>
public async Task UpdateCiphersAsync_vNext(Guid userId, IEnumerable<Core.Vault.Entities.Cipher> ciphers)
{
await UpdateCiphersAsync(userId, ciphers);
}
public async Task UpdatePartialAsync(Guid id, Guid userId, Guid? folderId, bool favorite)
{
using (var scope = ServiceScopeFactory.CreateScope())
@@ -1107,16 +1076,6 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
return result;
}
/// <inheritdoc cref="UpdateForKeyRotation(Guid, IEnumerable{Cipher})"/>
/// <remarks>
/// EF does not use the bulk resource creation service, so we need to use the regular update method.
/// </remarks>
public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation_vNext(
Guid userId, IEnumerable<Core.Vault.Entities.Cipher> ciphers)
{
return UpdateForKeyRotation(userId, ciphers);
}
public async Task UpsertAsync(CipherDetails cipher)
{
if (cipher.Id.Equals(default))