mirror of
https://github.com/bitwarden/server
synced 2025-12-30 15:14:02 +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:
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -484,7 +484,7 @@ jobs:
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Trigger self-host build
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
script: |
|
||||
@@ -525,7 +525,7 @@ jobs:
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Trigger k8s deploy
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
script: |
|
||||
|
||||
8
.github/workflows/test.yml
vendored
8
.github/workflows/test.yml
vendored
@@ -32,6 +32,14 @@ jobs:
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
|
||||
|
||||
- name: Install rust
|
||||
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # stable
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Cache cargo registry
|
||||
uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2.7.7
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
dotnet --info
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -215,6 +215,8 @@ bitwarden_license/src/Sso/wwwroot/assets
|
||||
**/**.swp
|
||||
.mono
|
||||
src/Core/MailTemplates/Mjml/out
|
||||
NativeMethods.g.cs
|
||||
util/RustSdk/rust/target
|
||||
|
||||
src/Admin/Admin.zip
|
||||
src/Api/Api.zip
|
||||
|
||||
@@ -133,6 +133,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Seeder", "util\Seeder\Seede
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbSeederUtility", "util\DbSeederUtility\DbSeederUtility.csproj", "{17A89266-260A-4A03-81AE-C0468C6EE06E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RustSdk", "util\RustSdk\RustSdk.csproj", "{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedWeb.Test", "test\SharedWeb.Test\SharedWeb.Test.csproj", "{AD59537D-5259-4B7A-948F-0CF58E80B359}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeederApi", "util\SeederApi\SeederApi.csproj", "{9F08DFBB-482B-4C9D-A5F4-6BDA6EC2E68F}"
|
||||
@@ -341,6 +342,10 @@ Global
|
||||
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AD59537D-5259-4B7A-948F-0CF58E80B359}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AD59537D-5259-4B7A-948F-0CF58E80B359}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AD59537D-5259-4B7A-948F-0CF58E80B359}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@@ -403,6 +408,7 @@ Global
|
||||
{3631BA42-6731-4118-A917-DAA43C5032B9} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||
{9A612EBA-1C0E-42B8-982B-62F0EE81000A} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
||||
{17A89266-260A-4A03-81AE-C0468C6EE06E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
||||
{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
||||
{AD59537D-5259-4B7A-948F-0CF58E80B359} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||
{9F08DFBB-482B-4C9D-A5F4-6BDA6EC2E68F} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
||||
EndGlobalSection
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<!-- This is a transitive dependency to Sustainsys.Saml2.AspNetCore2 -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
|
||||
|
||||
<PackageReference Include="Sustainsys.Saml2.AspNetCore2" Version="2.10.0" />
|
||||
<PackageReference Include="Sustainsys.Saml2.AspNetCore2" Version="2.11.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -53,6 +53,7 @@ services:
|
||||
- ./.data/postgres/log:/var/log/postgresql
|
||||
profiles:
|
||||
- postgres
|
||||
- ef
|
||||
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
@@ -69,6 +70,7 @@ services:
|
||||
- mysql_dev_data:/var/lib/mysql
|
||||
profiles:
|
||||
- mysql
|
||||
- ef
|
||||
|
||||
mariadb:
|
||||
image: mariadb:10
|
||||
@@ -76,13 +78,13 @@ services:
|
||||
- 4306:3306
|
||||
environment:
|
||||
MARIADB_USER: maria
|
||||
MARIADB_PASSWORD: ${MARIADB_ROOT_PASSWORD}
|
||||
MARIADB_DATABASE: vault_dev
|
||||
MARIADB_RANDOM_ROOT_PASSWORD: "true"
|
||||
volumes:
|
||||
- mariadb_dev_data:/var/lib/mysql
|
||||
profiles:
|
||||
- mariadb
|
||||
- ef
|
||||
|
||||
idp:
|
||||
image: kenchan0130/simplesamlphp:1.19.8
|
||||
@@ -153,5 +155,6 @@ volumes:
|
||||
mssql_dev_data:
|
||||
postgres_dev_data:
|
||||
mysql_dev_data:
|
||||
mariadb_dev_data:
|
||||
rabbitmq_data:
|
||||
redis_data:
|
||||
|
||||
@@ -70,7 +70,7 @@ Foreach ($item in @(
|
||||
@($mysql, "MySQL", "MySqlMigrations", "mySql", 2),
|
||||
# MariaDB shares the MySQL connection string in the server config so they are mutually exclusive in that context.
|
||||
# However they can still be run independently for integration tests.
|
||||
@($mariadb, "MariaDB", "MySqlMigrations", "mySql", 3)
|
||||
@($mariadb, "MariaDB", "MySqlMigrations", "mySql", 4)
|
||||
)) {
|
||||
if (!$item[0] -and !$all) {
|
||||
continue
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
15
src/Billing/Models/OnyxResponseModel.cs
Normal file
15
src/Billing/Models/OnyxResponseModel.cs
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -169,7 +169,7 @@ public class FreshdeskControllerTests
|
||||
[BitAutoData(WebhookKey)]
|
||||
public async Task PostWebhookOnyxAi_success(
|
||||
string freshdeskWebhookKey, FreshdeskOnyxAiWebhookModel model,
|
||||
OnyxAnswerWithCitationResponseModel onyxResponse,
|
||||
OnyxResponseModel onyxResponse,
|
||||
SutProvider<FreshdeskController> sutProvider)
|
||||
{
|
||||
var billingSettings = sutProvider.GetDependency<IOptions<BillingSettings>>().Value;
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models.Business;
|
||||
using Bit.Core.Billing.Organizations.Models;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Models.Business;
|
||||
|
||||
public class OrganizationLicenseTests
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that when the license file is loaded from disk using the current OrganizationLicense class,
|
||||
/// it matches the Organization it was generated for.
|
||||
@@ -33,4 +38,217 @@ public class OrganizationLicenseTests
|
||||
});
|
||||
Assert.True(license.VerifyData(organization, claimsPrincipal, globalSettings));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Known good GetDataBytes output for hash data (forHash: true) for all OrganizationLicense versions.
|
||||
/// These values were verified to be correct on initial implementation and serve as regression baselines.
|
||||
/// NOTE: License versions are now frozen. Use the JWT Token property to add new claims instead of incrementing the version.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<int, string> _knownGoodOrganizationLicenseHashData = new()
|
||||
{
|
||||
{ 1, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|UseDirectory:true|UseGroups:true|UseTotp:true|Version:1" },
|
||||
{ 2, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|UseDirectory:true|UseGroups:true|UsersGetPremium:true|UseTotp:true|Version:2" },
|
||||
{ 3, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|UseDirectory:true|UseEvents:true|UseGroups:true|UsersGetPremium:true|UseTotp:true|Version:3" },
|
||||
{ 4, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseDirectory:true|UseEvents:true|UseGroups:true|UsersGetPremium:true|UseTotp:true|Version:4" },
|
||||
{ 5, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UsersGetPremium:true|UseTotp:true|Version:5" },
|
||||
{ 6, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UsePolicies:true|UsersGetPremium:true|UseTotp:true|Version:6" },
|
||||
{ 7, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UsePolicies:true|UsersGetPremium:true|UseSso:true|UseTotp:true|Version:7" },
|
||||
{ 8, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseSso:true|UseTotp:true|Version:8" },
|
||||
{ 9, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseSso:true|UseTotp:true|Version:9" },
|
||||
{ 10, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSso:true|UseTotp:true|Version:10" },
|
||||
{ 11, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSso:true|UseTotp:true|Version:11" },
|
||||
{ 12, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|ExpirationWithoutGracePeriod:|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSso:true|UseTotp:true|Version:12" },
|
||||
{ 13, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|ExpirationWithoutGracePeriod:|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|SmSeats:5|SmServiceAccounts:8|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePasswordManager:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSecretsManager:true|UseSso:true|UseTotp:true|Version:13" },
|
||||
{ 14, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|ExpirationWithoutGracePeriod:|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|LimitCollectionCreationDeletion:true|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|SmSeats:5|SmServiceAccounts:8|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePasswordManager:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSecretsManager:true|UseSso:true|UseTotp:true|Version:14" },
|
||||
{ 15, "license:organization|AllowAdminAccessToAllCollectionItems:true|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|ExpirationWithoutGracePeriod:|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|LimitCollectionCreationDeletion:true|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|SmSeats:5|SmServiceAccounts:8|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePasswordManager:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSecretsManager:true|UseSso:true|UseTotp:true|Version:15" },
|
||||
{ 16, "license:organization|AllowAdminAccessToAllCollectionItems:true|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|ExpirationWithoutGracePeriod:|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|LimitCollectionCreationDeletion:true|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|SmSeats:5|SmServiceAccounts:8|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePasswordManager:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSecretsManager:true|UseSso:true|UseTotp:true|Version:16" }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Known good GetDataBytes output for signature data (forHash: false) for all OrganizationLicense versions.
|
||||
/// These values were verified to be correct on initial implementation and serve as regression baselines.
|
||||
/// NOTE: License versions are now frozen. Use the JWT Token property to add new claims instead of incrementing the version.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<int, string> _knownGoodOrganizationLicenseSignatureData = new()
|
||||
{
|
||||
{ 1, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:WSyM/Q+vgOuWeF6XBH+RSfUqvf7NDtP3fgNfcbXYqKc=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|UseDirectory:true|UseGroups:true|UseTotp:true|Version:1" },
|
||||
{ 2, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:n4g3leUf/egbnKk+/VgkJTvdxw2YRH6/zGgx89h+J60=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|UseDirectory:true|UseGroups:true|UsersGetPremium:true|UseTotp:true|Version:2" },
|
||||
{ 3, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:zDoMNV/c8YpUypc+FmBoPyj73qOsg4snsMOJDcKFp9k=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|UseDirectory:true|UseEvents:true|UseGroups:true|UsersGetPremium:true|UseTotp:true|Version:3" },
|
||||
{ 4, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:Y2sP9phSZ9GqbCC+PMp1KdnUhjfNaqNg6uzfUydrKZM=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseDirectory:true|UseEvents:true|UseGroups:true|UsersGetPremium:true|UseTotp:true|Version:4" },
|
||||
{ 5, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:PudZKNV7YAWJogm8BJf3wZIL+lESf3qzV/pQlZPPJjY=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UsersGetPremium:true|UseTotp:true|Version:5" },
|
||||
{ 6, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:7SjSYQENeAW4pUnXtsPaux2uipIWNWJz9VIrNW2gVsI=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UsePolicies:true|UsersGetPremium:true|UseTotp:true|Version:6" },
|
||||
{ 7, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:ujf4/zlDXv1g6ktlk9XBj/u3BkRZG+p5I00piGDiWp8=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UsePolicies:true|UsersGetPremium:true|UseSso:true|UseTotp:true|Version:7" },
|
||||
{ 8, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:GEM3AyWbQknnlDtoxyhw0QK7edYS2C/bffX5+p4G9ig=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseSso:true|UseTotp:true|Version:8" },
|
||||
{ 9, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:5SF14wtEieiA9hjj+BTcrggHcx7dLEGbH+HLksvK79o=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseSso:true|UseTotp:true|Version:9" },
|
||||
{ 10, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:NmbIpfiZUNxSvwbaolbUmItQCHIcVCTjfraR/NBlmvE=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSso:true|UseTotp:true|Version:10" },
|
||||
{ 11, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:dGZBQT/PORsuT/W2oRrngcjTboTyfZZVpDZBHshVK6Y=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSso:true|UseTotp:true|Version:11" },
|
||||
{ 12, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|ExpirationWithoutGracePeriod:|Expires:1740787200|Hash:rWecCXB0kuqi/RW3C8u2rLZRDMR49W3W4Q3eL2tZ3j8=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSso:true|UseTotp:true|Version:12" },
|
||||
{ 13, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|ExpirationWithoutGracePeriod:|Expires:1740787200|Hash:15fwM5v5Ba+t7JlD4ToYvtZmAoShWC3DrOD0lM5kXGE=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|SmSeats:5|SmServiceAccounts:8|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePasswordManager:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSecretsManager:true|UseSso:true|UseTotp:true|Version:13" },
|
||||
{ 14, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|ExpirationWithoutGracePeriod:|Expires:1740787200|Hash:2bTNBiH2G/Nzv6UVD1BNJQBGjT9et0UO8ComQofS8uo=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|LimitCollectionCreationDeletion:true|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|SmSeats:5|SmServiceAccounts:8|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePasswordManager:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSecretsManager:true|UseSso:true|UseTotp:true|Version:14" },
|
||||
{ 15, "license:organization|AllowAdminAccessToAllCollectionItems:true|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|ExpirationWithoutGracePeriod:|Expires:1740787200|Hash:3VjOyWJu38N4epIzhDzjRR80zQ651wnYkQCd+DIzeAs=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|LimitCollectionCreationDeletion:true|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|SmSeats:5|SmServiceAccounts:8|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePasswordManager:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSecretsManager:true|UseSso:true|UseTotp:true|Version:15" },
|
||||
{ 16, "license:organization|AllowAdminAccessToAllCollectionItems:true|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|ExpirationWithoutGracePeriod:|Expires:1740787200|Hash:Oo5KFBoX8pMcklJ4oJAqgv77/WA8+gDPxq6+/Fjffwc=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|LimitCollectionCreationDeletion:true|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|SmSeats:5|SmServiceAccounts:8|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePasswordManager:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSecretsManager:true|UseSso:true|UseTotp:true|Version:16" }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Regression test that verifies GetDataBytes output for hash data (forHash: true) remains stable across all OrganizationLicense versions.
|
||||
/// This protects against accidental changes to the data format that would break backward compatibility.
|
||||
/// If this test fails, it means the hash data format has changed and existing licenses may no longer validate.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OrganizationLicense_GetDataBytes_HashData_AllVersions()
|
||||
{
|
||||
// Verify each version produces the expected hash data format
|
||||
for (var version = 1; version <= 16; version++)
|
||||
{
|
||||
var license = CreateDeterministicOrganizationLicense(version);
|
||||
var actualHashData = System.Text.Encoding.UTF8.GetString(license.GetDataBytes(forHash: true));
|
||||
Assert.Equal(_knownGoodOrganizationLicenseHashData[version], actualHashData);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regression test that verifies GetDataBytes output for signature data (forHash: false) remains stable across all OrganizationLicense versions.
|
||||
/// This protects against accidental changes to the data format that would break backward compatibility.
|
||||
/// If this test fails, it means the signature data format has changed and existing licenses may no longer validate.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OrganizationLicense_GetDataBytes_SignatureData_AllVersions()
|
||||
{
|
||||
// Verify each version produces the expected signature data format
|
||||
for (var version = 1; version <= 16; version++)
|
||||
{
|
||||
var license = CreateDeterministicOrganizationLicense(version);
|
||||
var actualSignatureData = System.Text.Encoding.UTF8.GetString(license.GetDataBytes(forHash: false));
|
||||
Assert.Equal(_knownGoodOrganizationLicenseSignatureData[version], actualSignatureData);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that the OrganizationLicense version remains frozen at version 15.
|
||||
/// License versions should no longer be incremented. Use the JWT Token property to add new claims instead.
|
||||
/// If this test fails, it means someone attempted to increment the license version, which is no longer allowed.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OrganizationLicense_CurrentVersion_ShouldRemainFrozen()
|
||||
{
|
||||
const int expectedVersion = 15;
|
||||
var actualVersion = OrganizationLicense.CurrentLicenseFileVersion;
|
||||
|
||||
Assert.True(actualVersion == expectedVersion, $@"
|
||||
ERROR: OrganizationLicense.CurrentLicenseFileVersion has been changed from {expectedVersion} to {actualVersion}
|
||||
|
||||
License versions are now frozen and should not be incremented.
|
||||
|
||||
Instead of incrementing the version:
|
||||
- Use the JWT Token property to add new claims
|
||||
- Add your new capabilities as claims in the Token
|
||||
- This allows for more flexible licensing without breaking backward compatibility
|
||||
|
||||
If you believe you need to change the version for a valid reason, please discuss with the team first.
|
||||
");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a deterministic OrganizationLicense for testing hash values.
|
||||
/// All property values are fixed to ensure reproducible hashes.
|
||||
/// </summary>
|
||||
private static OrganizationLicense CreateDeterministicOrganizationLicense(int version)
|
||||
{
|
||||
var organization = CreateDeterministicOrganization();
|
||||
var subscriptionInfo = CreateDeterministicSubscriptionInfo();
|
||||
var installationId = new Guid("78900000-0000-0000-0000-000000000123");
|
||||
var mockLicensingService = CreateMockLicensingService();
|
||||
|
||||
var license = new OrganizationLicense(organization, subscriptionInfo, installationId, mockLicensingService, version);
|
||||
|
||||
// Override timestamps to deterministic values (constructor sets them to DateTime.UtcNow)
|
||||
license.Issued = new DateTime(2025, 9, 26, 12, 0, 1, DateTimeKind.Utc); // Corresponds to 1759501361 Unix timestamp
|
||||
license.Refresh = new DateTime(2025, 10, 26, 12, 0, 1, DateTimeKind.Utc); // Corresponds to 1762093361 Unix timestamp
|
||||
|
||||
// Recalculate hash with the deterministic Issued/Refresh values
|
||||
license.Hash = Convert.ToBase64String(license.ComputeHash());
|
||||
license.Signature = Convert.ToBase64String(mockLicensingService.SignLicense(license));
|
||||
|
||||
return license;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an Organization with deterministic property values for reproducible testing.
|
||||
/// </summary>
|
||||
private static Organization CreateDeterministicOrganization()
|
||||
{
|
||||
return new Organization
|
||||
{
|
||||
Id = new Guid("12300000-0000-0000-0000-000000000456"),
|
||||
Identifier = "myIdentifier",
|
||||
Name = "myOrg",
|
||||
BillingEmail = "myBillingEmail",
|
||||
Plan = "myPlan",
|
||||
PlanType = PlanType.EnterpriseAnnually2020,
|
||||
Seats = 10,
|
||||
MaxCollections = 2,
|
||||
UsePolicies = true,
|
||||
UseSso = true,
|
||||
UseKeyConnector = true,
|
||||
UseScim = true,
|
||||
UseGroups = true,
|
||||
UseEvents = true,
|
||||
UseDirectory = true,
|
||||
UseTotp = true,
|
||||
Use2fa = true,
|
||||
UseApi = true,
|
||||
UseResetPassword = true,
|
||||
MaxStorageGb = 100,
|
||||
SelfHost = true,
|
||||
UsersGetPremium = true,
|
||||
UseCustomPermissions = true,
|
||||
Enabled = true,
|
||||
LicenseKey = "myLicenseKey",
|
||||
UsePasswordManager = true,
|
||||
UseSecretsManager = true,
|
||||
SmSeats = 5,
|
||||
SmServiceAccounts = 8,
|
||||
UseRiskInsights = false,
|
||||
LimitCollectionCreation = true,
|
||||
LimitCollectionDeletion = true,
|
||||
AllowAdminAccessToAllCollectionItems = true,
|
||||
UseOrganizationDomains = true,
|
||||
UseAdminSponsoredFamilies = false
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a SubscriptionInfo with deterministic dates for reproducible testing.
|
||||
/// </summary>
|
||||
private static SubscriptionInfo CreateDeterministicSubscriptionInfo()
|
||||
{
|
||||
var stripeSubscription = new Subscription
|
||||
{
|
||||
Status = "active",
|
||||
TrialStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
TrialEnd = new DateTime(2024, 2, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
CurrentPeriodStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
CurrentPeriodEnd = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc)
|
||||
};
|
||||
|
||||
return new SubscriptionInfo
|
||||
{
|
||||
UpcomingInvoice = new SubscriptionInfo.BillingUpcomingInvoice
|
||||
{
|
||||
Date = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc)
|
||||
},
|
||||
Subscription = new SubscriptionInfo.BillingSubscription(stripeSubscription)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a mock ILicensingService that returns a deterministic signature.
|
||||
/// </summary>
|
||||
private static ILicensingService CreateMockLicensingService()
|
||||
{
|
||||
var mockService = Substitute.For<ILicensingService>();
|
||||
mockService.SignLicense(Arg.Any<ILicense>())
|
||||
.Returns([0x00, 0x01, 0x02, 0x03]); // Dummy signature for hash testing
|
||||
return mockService;
|
||||
}
|
||||
}
|
||||
|
||||
168
test/Core.Test/Billing/Models/Business/UserLicenseTests.cs
Normal file
168
test/Core.Test/Billing/Models/Business/UserLicenseTests.cs
Normal file
@@ -0,0 +1,168 @@
|
||||
using Bit.Core.Billing.Models.Business;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Business;
|
||||
using NSubstitute;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Models.Business;
|
||||
|
||||
public class UserLicenseTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Known good GetDataBytes output for hash data (forHash: true) for UserLicense version 1.
|
||||
/// This value was verified to be correct on initial implementation and serves as a regression baseline.
|
||||
/// NOTE: License versions are now frozen. Use the JWT Token property to add new claims instead of incrementing the version.
|
||||
/// </summary>
|
||||
private const string _knownGoodUserLicenseHashData = "license:user|Email:test@example.com|Expires:1736208000|Id:12300000-0000-0000-0000-000000000789|LicenseKey:myUserLicenseKey|MaxStorageGb:10|Name:Test User|Premium:true|Trial:false|Version:1";
|
||||
|
||||
/// <summary>
|
||||
/// Known good GetDataBytes output for signature data (forHash: false) for UserLicense version 1.
|
||||
/// This value was verified to be correct on initial implementation and serves as a regression baseline.
|
||||
/// NOTE: License versions are now frozen. Use the JWT Token property to add new claims instead of incrementing the version.
|
||||
/// </summary>
|
||||
private const string _knownGoodUserLicenseSignatureData = "license:user|Email:test@example.com|Expires:1736208000|Hash:oZEopNmWvWQNE3Lnsh/LP2OPo6+IHxjTcpdIse/viQk=|Id:12300000-0000-0000-0000-000000000789|Issued:1758888041|LicenseKey:myUserLicenseKey|MaxStorageGb:10|Name:Test User|Premium:true|Refresh:1735603200|Trial:false|Version:1";
|
||||
|
||||
/// <summary>
|
||||
/// Regression test that verifies GetDataBytes output for hash data (forHash: true) remains stable for UserLicense version 1.
|
||||
/// This protects against accidental changes to the data format that would break backward compatibility.
|
||||
/// If this test fails, it means the hash data format has changed and existing licenses may no longer validate.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void UserLicense_GetDataBytes_HashData_Version1()
|
||||
{
|
||||
var license = CreateDeterministicUserLicense();
|
||||
var actualHashData = System.Text.Encoding.UTF8.GetString(license.GetDataBytes(forHash: true));
|
||||
Assert.Equal(_knownGoodUserLicenseHashData, actualHashData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regression test that verifies GetDataBytes output for signature data (forHash: false) remains stable for UserLicense version 1.
|
||||
/// This protects against accidental changes to the data format that would break backward compatibility.
|
||||
/// If this test fails, it means the signature data format has changed and existing licenses may no longer validate.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void UserLicense_GetDataBytes_SignatureData_Version1()
|
||||
{
|
||||
var license = CreateDeterministicUserLicense();
|
||||
var actualSignatureData = System.Text.Encoding.UTF8.GetString(license.GetDataBytes(forHash: false));
|
||||
Assert.Equal(_knownGoodUserLicenseSignatureData, actualSignatureData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that the UserLicense version remains frozen at version 1.
|
||||
/// License versions should no longer be incremented. Use the JWT Token property to add new claims instead.
|
||||
/// If this test fails, it means someone attempted to add version 2 support, which is no longer allowed.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void UserLicense_CurrentVersion_ShouldRemainFrozen()
|
||||
{
|
||||
const int expectedMaxVersion = 1;
|
||||
|
||||
var user = CreateDeterministicUser();
|
||||
var subscriptionInfo = CreateDeterministicSubscriptionInfo();
|
||||
var mockLicensingService = CreateMockLicensingService();
|
||||
|
||||
// Verify that version 2 is NOT supported (should throw NotSupportedException)
|
||||
var exception = Assert.Throws<NotSupportedException>(() =>
|
||||
new UserLicense(user, subscriptionInfo, mockLicensingService, version: 2));
|
||||
|
||||
// If the exception message changes or we don't get an exception, fail with helpful guidance
|
||||
if (exception == null)
|
||||
{
|
||||
var errorMessage = $@"
|
||||
ERROR: UserLicense now supports version 2 or higher
|
||||
|
||||
License versions are now frozen and should not be incremented.
|
||||
|
||||
Instead of incrementing the version:
|
||||
- Use the JWT Token property to add new claims
|
||||
- Add your new capabilities as claims in the Token
|
||||
- This allows for more flexible licensing without breaking backward compatibility
|
||||
|
||||
If you believe you need to change the version for a valid reason, please discuss with the team first.
|
||||
";
|
||||
Assert.Fail(errorMessage);
|
||||
}
|
||||
|
||||
// Verify we still support version 1
|
||||
var license = new UserLicense(user, subscriptionInfo, mockLicensingService, version: expectedMaxVersion);
|
||||
Assert.NotNull(license);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a deterministic UserLicense for testing hash values.
|
||||
/// All property values are fixed to ensure reproducible hashes.
|
||||
/// </summary>
|
||||
private static UserLicense CreateDeterministicUserLicense()
|
||||
{
|
||||
var user = CreateDeterministicUser();
|
||||
var subscriptionInfo = CreateDeterministicSubscriptionInfo();
|
||||
var mockLicensingService = CreateMockLicensingService();
|
||||
|
||||
var license = new UserLicense(user, subscriptionInfo, mockLicensingService, version: 1);
|
||||
|
||||
// Override timestamps to deterministic values (constructor sets them to DateTime.UtcNow)
|
||||
license.Issued = new DateTime(2025, 9, 26, 12, 0, 41, DateTimeKind.Utc); // Corresponds to 1759502041 Unix timestamp
|
||||
license.Refresh = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc); // Corresponds to 1735603200 Unix timestamp
|
||||
|
||||
// Recalculate hash with the deterministic Issued/Refresh values
|
||||
license.Hash = Convert.ToBase64String(license.ComputeHash());
|
||||
license.Signature = Convert.ToBase64String(mockLicensingService.SignLicense(license));
|
||||
|
||||
return license;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a User with deterministic property values for reproducible testing.
|
||||
/// </summary>
|
||||
private static User CreateDeterministicUser()
|
||||
{
|
||||
return new User
|
||||
{
|
||||
Id = new Guid("12300000-0000-0000-0000-000000000789"),
|
||||
Name = "Test User",
|
||||
Email = "test@example.com",
|
||||
LicenseKey = "myUserLicenseKey",
|
||||
Premium = true,
|
||||
MaxStorageGb = 10,
|
||||
PremiumExpirationDate = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a SubscriptionInfo with deterministic dates for reproducible testing.
|
||||
/// </summary>
|
||||
private static SubscriptionInfo CreateDeterministicSubscriptionInfo()
|
||||
{
|
||||
var stripeSubscription = new Subscription
|
||||
{
|
||||
Status = "active",
|
||||
TrialStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
TrialEnd = new DateTime(2024, 2, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
CurrentPeriodStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
CurrentPeriodEnd = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc)
|
||||
};
|
||||
|
||||
return new SubscriptionInfo
|
||||
{
|
||||
UpcomingInvoice = new SubscriptionInfo.BillingUpcomingInvoice
|
||||
{
|
||||
Date = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc)
|
||||
},
|
||||
Subscription = new SubscriptionInfo.BillingSubscription(stripeSubscription)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a mock ILicensingService that returns a deterministic signature.
|
||||
/// </summary>
|
||||
private static ILicensingService CreateMockLicensingService()
|
||||
{
|
||||
var mockService = Substitute.For<ILicensingService>();
|
||||
mockService.SignLicense(Arg.Any<ILicense>())
|
||||
.Returns([0x00, 0x01, 0x02, 0x03]); // Dummy signature for hash testing
|
||||
return mockService;
|
||||
}
|
||||
}
|
||||
@@ -53,38 +53,6 @@ public class ImportCiphersAsyncCommandTests
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ImportIntoIndividualVaultAsync_WithBulkResourceCreationServiceEnabled_Success(
|
||||
Guid importingUserId,
|
||||
List<CipherDetails> ciphers,
|
||||
SutProvider<ImportCiphersCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IPolicyService>()
|
||||
.AnyPoliciesApplicableToUserAsync(importingUserId, PolicyType.OrganizationDataOwnership)
|
||||
.Returns(false);
|
||||
|
||||
sutProvider.GetDependency<IFolderRepository>()
|
||||
.GetManyByUserIdAsync(importingUserId)
|
||||
.Returns(new List<Folder>());
|
||||
|
||||
var folders = new List<Folder> { new Folder { UserId = importingUserId } };
|
||||
|
||||
var folderRelationships = new List<KeyValuePair<int, int>>();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ICipherRepository>()
|
||||
.Received(1)
|
||||
.CreateAsync_vNext(importingUserId, ciphers, Arg.Any<List<Folder>>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ImportIntoIndividualVaultAsync_WithPolicyRequirementsEnabled_WithOrganizationDataOwnershipPolicyDisabled_Success(
|
||||
Guid importingUserId,
|
||||
@@ -117,42 +85,6 @@ public class ImportCiphersAsyncCommandTests
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ImportIntoIndividualVaultAsync_WithBulkResourceCreationServiceEnabled_WithPolicyRequirementsEnabled_WithOrganizationDataOwnershipPolicyDisabled_Success(
|
||||
Guid importingUserId,
|
||||
List<CipherDetails> ciphers,
|
||||
SutProvider<ImportCiphersCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(importingUserId)
|
||||
.Returns(new OrganizationDataOwnershipPolicyRequirement(
|
||||
OrganizationDataOwnershipState.Disabled,
|
||||
[]));
|
||||
|
||||
sutProvider.GetDependency<IFolderRepository>()
|
||||
.GetManyByUserIdAsync(importingUserId)
|
||||
.Returns(new List<Folder>());
|
||||
|
||||
var folders = new List<Folder> { new Folder { UserId = importingUserId } };
|
||||
|
||||
var folderRelationships = new List<KeyValuePair<int, int>>();
|
||||
|
||||
await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);
|
||||
|
||||
await sutProvider.GetDependency<ICipherRepository>()
|
||||
.Received(1)
|
||||
.CreateAsync_vNext(importingUserId, ciphers, Arg.Any<List<Folder>>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ImportIntoIndividualVaultAsync_ThrowsBadRequestException(
|
||||
List<Folder> folders,
|
||||
@@ -259,66 +191,6 @@ public class ImportCiphersAsyncCommandTests
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ImportIntoOrganizationalVaultAsync_WithBulkResourceCreationServiceEnabled_Success(
|
||||
Organization organization,
|
||||
Guid importingUserId,
|
||||
OrganizationUser importingOrganizationUser,
|
||||
List<Collection> collections,
|
||||
List<CipherDetails> ciphers,
|
||||
SutProvider<ImportCiphersCommand> sutProvider)
|
||||
{
|
||||
organization.MaxCollections = null;
|
||||
importingOrganizationUser.OrganizationId = organization.Id;
|
||||
|
||||
foreach (var collection in collections)
|
||||
{
|
||||
collection.OrganizationId = organization.Id;
|
||||
}
|
||||
|
||||
foreach (var cipher in ciphers)
|
||||
{
|
||||
cipher.OrganizationId = organization.Id;
|
||||
}
|
||||
|
||||
KeyValuePair<int, int>[] collectionRelationships = {
|
||||
new(0, 0),
|
||||
new(1, 1),
|
||||
new(2, 2)
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organization.Id)
|
||||
.Returns(organization);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByOrganizationAsync(organization.Id, importingUserId)
|
||||
.Returns(importingOrganizationUser);
|
||||
|
||||
// Set up a collection that already exists in the organization
|
||||
sutProvider.GetDependency<ICollectionRepository>()
|
||||
.GetManyByOrganizationIdAsync(organization.Id)
|
||||
.Returns(new List<Collection> { collections[0] });
|
||||
|
||||
await sutProvider.Sut.ImportIntoOrganizationalVaultAsync(collections, ciphers, collectionRelationships, importingUserId);
|
||||
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync_vNext(
|
||||
ciphers,
|
||||
Arg.Is<IEnumerable<Collection>>(cols => cols.Count() == collections.Count - 1 &&
|
||||
!cols.Any(c => c.Id == collections[0].Id) && // Check that the collection that already existed in the organization was not added
|
||||
cols.All(c => collections.Any(x => c.Name == x.Name))),
|
||||
Arg.Is<IEnumerable<CollectionCipher>>(c => c.Count() == ciphers.Count),
|
||||
Arg.Is<IEnumerable<CollectionUser>>(cus =>
|
||||
cus.Count() == collections.Count - 1 &&
|
||||
!cus.Any(cu => cu.CollectionId == collections[0].Id) && // Check that access was not added for the collection that already existed in the organization
|
||||
cus.All(cu => cu.OrganizationUserId == importingOrganizationUser.Id && cu.Manage == true)));
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ImportIntoOrganizationalVaultAsync_ThrowsBadRequestException(
|
||||
Organization organization,
|
||||
|
||||
@@ -674,32 +674,6 @@ public class CipherServiceTests
|
||||
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData("")]
|
||||
[BitAutoData("Correct Time")]
|
||||
public async Task ShareManyAsync_CorrectRevisionDate_WithBulkResourceCreationServiceEnabled_Passes(string revisionDateString,
|
||||
SutProvider<CipherService> sutProvider, IEnumerable<CipherDetails> ciphers, Organization organization, List<Guid> collectionIds)
|
||||
{
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id)
|
||||
.Returns(new Organization
|
||||
{
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
MaxStorageGb = 100
|
||||
});
|
||||
|
||||
var cipherInfos = ciphers.Select(c => (c,
|
||||
string.IsNullOrEmpty(revisionDateString) ? null : (DateTime?)c.RevisionDate));
|
||||
var sharingUserId = ciphers.First().UserId.Value;
|
||||
|
||||
await sutProvider.Sut.ShareManyAsync(cipherInfos, organization.Id, collectionIds, sharingUserId);
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).UpdateCiphersAsync_vNext(sharingUserId,
|
||||
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RestoreAsync_UpdatesUserCipher(Guid restoringUserId, CipherDetails cipher, SutProvider<CipherService> sutProvider)
|
||||
@@ -1120,33 +1094,6 @@ public class CipherServiceTests
|
||||
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ShareManyAsync_PaidOrgWithAttachment_WithBulkResourceCreationServiceEnabled_Passes(SutProvider<CipherService> sutProvider,
|
||||
IEnumerable<CipherDetails> ciphers, Guid organizationId, List<Guid> collectionIds)
|
||||
{
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)
|
||||
.Returns(new Organization
|
||||
{
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
MaxStorageGb = 100
|
||||
});
|
||||
ciphers.FirstOrDefault().Attachments =
|
||||
"{\"attachment1\":{\"Size\":\"250\",\"FileName\":\"superCoolFile\","
|
||||
+ "\"Key\":\"superCoolFile\",\"ContainerName\":\"testContainer\",\"Validated\":false}}";
|
||||
|
||||
var cipherInfos = ciphers.Select(c => (c,
|
||||
(DateTime?)c.RevisionDate));
|
||||
var sharingUserId = ciphers.First().UserId.Value;
|
||||
|
||||
await sutProvider.Sut.ShareManyAsync(cipherInfos, organizationId, collectionIds, sharingUserId);
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).UpdateCiphersAsync_vNext(sharingUserId,
|
||||
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
|
||||
}
|
||||
|
||||
private class SaveDetailsAsyncDependencies
|
||||
{
|
||||
public CipherDetails CipherDetails { get; set; }
|
||||
|
||||
@@ -949,36 +949,7 @@ public class CipherRepositoryTests
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task UpdateCiphersAsync_Works(ICipherRepository cipherRepository, IUserRepository userRepository)
|
||||
{
|
||||
var user = await userRepository.CreateAsync(new User
|
||||
{
|
||||
Name = "Test User",
|
||||
Email = $"test+{Guid.NewGuid()}@email.com",
|
||||
ApiKey = "TEST",
|
||||
SecurityStamp = "stamp",
|
||||
});
|
||||
|
||||
var cipher1 = await CreatePersonalCipher(user, cipherRepository);
|
||||
var cipher2 = await CreatePersonalCipher(user, cipherRepository);
|
||||
|
||||
cipher1.Type = CipherType.SecureNote;
|
||||
cipher2.Attachments = "new_attachments";
|
||||
|
||||
await cipherRepository.UpdateCiphersAsync(user.Id, [cipher1, cipher2]);
|
||||
|
||||
var updatedCipher1 = await cipherRepository.GetByIdAsync(cipher1.Id);
|
||||
var updatedCipher2 = await cipherRepository.GetByIdAsync(cipher2.Id);
|
||||
|
||||
Assert.NotNull(updatedCipher1);
|
||||
Assert.NotNull(updatedCipher2);
|
||||
|
||||
Assert.Equal(CipherType.SecureNote, updatedCipher1.Type);
|
||||
Assert.Equal("new_attachments", updatedCipher2.Attachments);
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task CreateAsync_vNext_WithFolders_Works(
|
||||
public async Task CreateAsync_WithFolders_Works(
|
||||
IUserRepository userRepository, ICipherRepository cipherRepository, IFolderRepository folderRepository)
|
||||
{
|
||||
// Arrange
|
||||
@@ -996,7 +967,7 @@ public class CipherRepositoryTests
|
||||
var cipher2 = new Cipher { Id = CoreHelpers.GenerateComb(), Type = CipherType.SecureNote, UserId = user.Id, Data = "" };
|
||||
|
||||
// Act
|
||||
await cipherRepository.CreateAsync_vNext(
|
||||
await cipherRepository.CreateAsync(
|
||||
userId: user.Id,
|
||||
ciphers: [cipher1, cipher2],
|
||||
folders: [folder1, folder2]);
|
||||
@@ -1014,7 +985,7 @@ public class CipherRepositoryTests
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task CreateAsync_vNext_WithCollectionsAndUsers_Works(
|
||||
public async Task CreateAsync_WithCollectionsAndUsers_Works(
|
||||
IOrganizationRepository orgRepository,
|
||||
IOrganizationUserRepository orgUserRepository,
|
||||
ICollectionRepository collectionRepository,
|
||||
@@ -1059,7 +1030,7 @@ public class CipherRepositoryTests
|
||||
};
|
||||
|
||||
// Act
|
||||
await cipherRepository.CreateAsync_vNext(
|
||||
await cipherRepository.CreateAsync(
|
||||
ciphers: [cipher],
|
||||
collections: [collection],
|
||||
collectionCiphers: [collectionCipher],
|
||||
@@ -1084,7 +1055,7 @@ public class CipherRepositoryTests
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task UpdateCiphersAsync_vNext_Works(
|
||||
public async Task UpdateCiphersAsync_Works(
|
||||
IUserRepository userRepository, ICipherRepository cipherRepository)
|
||||
{
|
||||
// Arrange
|
||||
@@ -1110,7 +1081,7 @@ public class CipherRepositoryTests
|
||||
c2.Attachments = expectedNewAttachments;
|
||||
|
||||
// Act
|
||||
await cipherRepository.UpdateCiphersAsync_vNext(user.Id, [c1, c2]);
|
||||
await cipherRepository.UpdateCiphersAsync(user.Id, [c1, c2]);
|
||||
|
||||
// Assert
|
||||
var updated1 = await cipherRepository.GetByIdAsync(c1.Id);
|
||||
|
||||
@@ -26,6 +26,7 @@ public class Program
|
||||
// Create service provider with necessary services
|
||||
var services = new ServiceCollection();
|
||||
ServiceCollectionExtension.ConfigureServices(services);
|
||||
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
// Get a scoped DB context
|
||||
|
||||
58
util/RustSdk/NativeMethods.cs
Normal file
58
util/RustSdk/NativeMethods.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Bit.RustSDK;
|
||||
|
||||
public static partial class NativeMethods
|
||||
{
|
||||
// https://docs.microsoft.com/en-us/dotnet/standard/native-interop/cross-platform
|
||||
// Library path will search
|
||||
// win => __DllName, __DllName.dll
|
||||
// linux, osx => __DllName.so, __DllName.dylib
|
||||
|
||||
static NativeMethods()
|
||||
{
|
||||
NativeLibrary.SetDllImportResolver(typeof(NativeMethods).Assembly, DllImportResolver);
|
||||
}
|
||||
|
||||
static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
|
||||
{
|
||||
if (libraryName != __DllName) return IntPtr.Zero;
|
||||
|
||||
var path = "runtimes/";
|
||||
var extension = "";
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
path += "win-";
|
||||
extension = ".dll";
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
path += "osx-";
|
||||
extension = ".dylib";
|
||||
}
|
||||
else
|
||||
{
|
||||
path += "linux-";
|
||||
extension = ".so";
|
||||
}
|
||||
|
||||
if (RuntimeInformation.ProcessArchitecture == Architecture.X86)
|
||||
{
|
||||
path += "x86";
|
||||
}
|
||||
else if (RuntimeInformation.ProcessArchitecture == Architecture.X64)
|
||||
{
|
||||
path += "x64";
|
||||
}
|
||||
else if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
||||
{
|
||||
path += "arm64";
|
||||
}
|
||||
|
||||
path += "/native/" + __DllName + extension;
|
||||
|
||||
return NativeLibrary.Load(Path.Combine(AppContext.BaseDirectory, path), assembly, searchPath);
|
||||
}
|
||||
}
|
||||
41
util/RustSdk/RustSdk.csproj
Normal file
41
util/RustSdk/RustSdk.csproj
Normal file
@@ -0,0 +1,41 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Bit.RustSDK</RootNamespace>
|
||||
<UserSecretsId>Bit.RustSDK</UserSecretsId>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="rust/target/release/libsdk*.dylib">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
<PackageCopyToOutput>true</PackageCopyToOutput>
|
||||
<Link>runtimes/osx-arm64/native/libsdk.dylib</Link>
|
||||
</Content>
|
||||
<Content Include="./rust/target/release/libsdk*.so">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
<PackageCopyToOutput>true</PackageCopyToOutput>
|
||||
<Link>runtimes/linux-x64/native/libsdk.dylib</Link>
|
||||
</Content>
|
||||
<Content Include="./rust/target/release/libsdk*.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
<PackageCopyToOutput>true</PackageCopyToOutput>
|
||||
<Link>runtimes/windows-x64/native/libsdk.dylib</Link>
|
||||
</Content>
|
||||
|
||||
<!-- This is a work around because this file is compiled by the PreBuild event below, and won't
|
||||
always be detected -->
|
||||
<Compile Remove="NativeMethods.g.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
|
||||
<Exec Command="cargo build --release" WorkingDirectory="$(ProjectDir)/rust" />
|
||||
<ItemGroup>
|
||||
<Compile Include="NativeMethods.g.cs" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
19
util/RustSdk/RustSdkException.cs
Normal file
19
util/RustSdk/RustSdkException.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace Bit.RustSDK;
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when the Rust SDK operations fail
|
||||
/// </summary>
|
||||
public class RustSdkException : Exception
|
||||
{
|
||||
public RustSdkException() : base("An error occurred in the Rust SDK operation")
|
||||
{
|
||||
}
|
||||
|
||||
public RustSdkException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public RustSdkException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
104
util/RustSdk/RustSdkService.cs
Normal file
104
util/RustSdk/RustSdkService.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Bit.RustSDK;
|
||||
|
||||
public class UserKeys
|
||||
{
|
||||
public required string MasterPasswordHash { get; set; }
|
||||
/// <summary>
|
||||
/// Base64 encoded UserKey
|
||||
/// </summary>
|
||||
public required string Key { get; set; }
|
||||
public required string EncryptedUserKey { get; set; }
|
||||
public required string PublicKey { get; set; }
|
||||
public required string PrivateKey { get; set; }
|
||||
}
|
||||
|
||||
public class OrganizationKeys
|
||||
{
|
||||
/// <summary>
|
||||
/// Base64 encoded SymmetricCryptoKey
|
||||
/// </summary>
|
||||
public required string Key { get; set; }
|
||||
|
||||
public required string PublicKey { get; set; }
|
||||
public required string PrivateKey { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Service implementation that provides a C# friendly interface to the Rust SDK
|
||||
/// </summary>
|
||||
public class RustSdkService
|
||||
{
|
||||
private static readonly JsonSerializerOptions CaseInsensitiveOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
public unsafe UserKeys GenerateUserKeys(string email, string password)
|
||||
{
|
||||
var emailBytes = StringToRustString(email);
|
||||
var passwordBytes = StringToRustString(password);
|
||||
|
||||
fixed (byte* emailPtr = emailBytes)
|
||||
fixed (byte* passwordPtr = passwordBytes)
|
||||
{
|
||||
var resultPtr = NativeMethods.generate_user_keys(emailPtr, passwordPtr);
|
||||
|
||||
var result = TakeAndDestroyRustString(resultPtr);
|
||||
|
||||
return JsonSerializer.Deserialize<UserKeys>(result, CaseInsensitiveOptions)!;
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe OrganizationKeys GenerateOrganizationKeys()
|
||||
{
|
||||
var resultPtr = NativeMethods.generate_organization_keys();
|
||||
|
||||
var result = TakeAndDestroyRustString(resultPtr);
|
||||
|
||||
return JsonSerializer.Deserialize<OrganizationKeys>(result, CaseInsensitiveOptions)!;
|
||||
}
|
||||
|
||||
public unsafe string GenerateUserOrganizationKey(string userKey, string orgKey)
|
||||
{
|
||||
var userKeyBytes = StringToRustString(userKey);
|
||||
var orgKeyBytes = StringToRustString(orgKey);
|
||||
|
||||
fixed (byte* userKeyPtr = userKeyBytes)
|
||||
fixed (byte* orgKeyPtr = orgKeyBytes)
|
||||
{
|
||||
var resultPtr = NativeMethods.generate_user_organization_key(userKeyPtr, orgKeyPtr);
|
||||
|
||||
var result = TakeAndDestroyRustString(resultPtr);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static byte[] StringToRustString(string str)
|
||||
{
|
||||
return Encoding.UTF8.GetBytes(str + '\0');
|
||||
}
|
||||
|
||||
private static unsafe string TakeAndDestroyRustString(byte* ptr)
|
||||
{
|
||||
if (ptr == null)
|
||||
{
|
||||
throw new RustSdkException("Pointer is null");
|
||||
}
|
||||
|
||||
var result = Marshal.PtrToStringUTF8((IntPtr)ptr);
|
||||
NativeMethods.free_c_string(ptr);
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
throw new RustSdkException("Failed to convert native result to string");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
21
util/RustSdk/RustSdkServiceFactory.cs
Normal file
21
util/RustSdk/RustSdkServiceFactory.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace Bit.RustSDK;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating Rust SDK service instances
|
||||
/// </summary>
|
||||
public static class RustSdkServiceFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a singleton instance of the Rust SDK service (thread-safe)
|
||||
/// </summary>
|
||||
/// <returns>A singleton IRustSdkService instance</returns>
|
||||
public static RustSdkService CreateSingleton()
|
||||
{
|
||||
return SingletonHolder.Instance;
|
||||
}
|
||||
|
||||
private static class SingletonHolder
|
||||
{
|
||||
internal static readonly RustSdkService Instance = new();
|
||||
}
|
||||
}
|
||||
2938
util/RustSdk/rust/Cargo.lock
generated
Normal file
2938
util/RustSdk/rust/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
util/RustSdk/rust/Cargo.toml
Normal file
33
util/RustSdk/rust/Cargo.toml
Normal file
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "sdk"
|
||||
publish = false
|
||||
|
||||
version = "0.1.0"
|
||||
authors = ["Bitwarden Inc"]
|
||||
edition = "2021"
|
||||
homepage = "https://bitwarden.com"
|
||||
repository = "https://github.com/bitwarden/server"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.22.1"
|
||||
bitwarden-core = { git = "https://github.com/bitwarden/sdk-internal.git", rev = "29c6158636d50141788e41736d15f2f6c7bc7fa8" }
|
||||
bitwarden-crypto = { git = "https://github.com/bitwarden/sdk-internal.git", rev = "29c6158636d50141788e41736d15f2f6c7bc7fa8" }
|
||||
serde = "=1.0.219"
|
||||
serde_json = "=1.0.141"
|
||||
|
||||
[build-dependencies]
|
||||
csbindgen = "=1.9.3"
|
||||
|
||||
# Compile all dependencies with some optimizations when building this crate on debug
|
||||
# This slows down clean builds by about 50%, but the resulting binaries can be orders of magnitude faster
|
||||
# As clean builds won't occur very often, this won't slow down the development process
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 2
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
opt-level = 3
|
||||
9
util/RustSdk/rust/build.rs
Normal file
9
util/RustSdk/rust/build.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
fn main() {
|
||||
csbindgen::Builder::default()
|
||||
.input_extern_file("src/lib.rs")
|
||||
.csharp_dll_name("libsdk")
|
||||
.csharp_namespace("Bit.RustSDK")
|
||||
.csharp_class_accessibility("public")
|
||||
.generate_csharp_file("../NativeMethods.g.cs")
|
||||
.unwrap();
|
||||
}
|
||||
151
util/RustSdk/rust/src/lib.rs
Normal file
151
util/RustSdk/rust/src/lib.rs
Normal file
@@ -0,0 +1,151 @@
|
||||
#![allow(clippy::missing_safety_doc)]
|
||||
use std::ffi::{c_char, CStr, CString};
|
||||
|
||||
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||
|
||||
use bitwarden_crypto::{
|
||||
pbkdf2, AsymmetricCryptoKey, AsymmetricPublicCryptoKey, BitwardenLegacyKeyBytes, HashPurpose,
|
||||
KeyEncryptable, MasterKey, RsaKeyPair, SpkiPublicKeyBytes, SymmetricCryptoKey,
|
||||
UnsignedSharedKey, UserKey,
|
||||
};
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn generate_user_keys(
|
||||
email: *const c_char,
|
||||
password: *const c_char,
|
||||
) -> *const c_char {
|
||||
let email = CStr::from_ptr(email).to_str().unwrap();
|
||||
let password = CStr::from_ptr(password).to_str().unwrap();
|
||||
|
||||
let master_key = derive_master_key(password, email);
|
||||
|
||||
let master_password_hash = master_key
|
||||
.derive_master_key_hash(password.as_bytes(), HashPurpose::ServerAuthorization)
|
||||
.unwrap();
|
||||
|
||||
let (user_key, encrypted_user_key) = master_key.make_user_key().unwrap();
|
||||
|
||||
let keypair = keypair(&user_key.0);
|
||||
|
||||
let json = serde_json::json!({
|
||||
"masterPasswordHash": master_password_hash,
|
||||
"key": user_key.0.to_base64(),
|
||||
"encryptedUserKey": encrypted_user_key.to_string(),
|
||||
"publicKey": keypair.public.to_string(),
|
||||
"privateKey": keypair.private.to_string(),
|
||||
})
|
||||
.to_string();
|
||||
|
||||
let result = CString::new(json).unwrap();
|
||||
|
||||
result.into_raw()
|
||||
}
|
||||
|
||||
fn derive_master_key(email: &str, password: &str) -> MasterKey {
|
||||
let mut hash = pbkdf2(password.as_bytes(), email.as_bytes(), 5000);
|
||||
|
||||
let hash = hash.as_mut_slice();
|
||||
|
||||
hash.try_into().unwrap()
|
||||
}
|
||||
|
||||
fn keypair(key: &SymmetricCryptoKey) -> RsaKeyPair {
|
||||
const RSA_PRIVATE_KEY: &str = "-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX+2hfOQS
|
||||
8HzYUS2oc/jGVTZpv+/Ryuoh9d8ihYX9dd0cYh2tl6KWdFc88lPUH11Oxqy20Rk2
|
||||
e5r/RF6T9yM0Me3NPnaKt+hlhLtfoc0h86LnhD56A9FDUfuI0dVnPcrwNv0YJIo9
|
||||
4LwxtbqBULNvXl6wJ7WAbODrCQy5ZgMVg+iH+gGpwiqsZqHt+KuoHWcN53MSPDfa
|
||||
F4/YMB99U3TziJMOOJask1TEEnakMPln11PczNDazT17DXIxYrbPfutPdh6sLs6A
|
||||
QOajdZijfEvepgnOe7cQ7aeatiOJFrjTApKPGxOVRzEMX4XS4xbyhH0QxQeB6l16
|
||||
l8C0uxIBAgMBAAECggEASaWfeVDA3cVzOPFSpvJm20OTE+R6uGOU+7vh36TX/POq
|
||||
92qBuwbd0h0oMD32FxsXywd2IxtBDUSiFM9699qufTVuM0Q3tZw6lHDTOVG08+tP
|
||||
dr8qSbMtw7PGFxN79fHLBxejjO4IrM9lapjWpxEF+11x7r+wM+0xRZQ8sNFYG46a
|
||||
PfIaty4BGbL0I2DQ2y8I57iBCAy69eht59NLMm27fRWGJIWCuBIjlpfzET1j2HLX
|
||||
UIh5bTBNzqaN039WH49HczGE3mQKVEJZc/efk3HaVd0a1Sjzyn0QY+N1jtZN3jTR
|
||||
buDWA1AknkX1LX/0tUhuS3/7C3ejHxjw4Dk1ZLo5/QKBgQDIWvqFn0+IKRSu6Ua2
|
||||
hDsufIHHUNLelbfLUMmFthxabcUn4zlvIscJO00Tq/ezopSRRvbGiqnxjv/mYxuc
|
||||
vOUBeZtlus0Q9RTACBtw9TGoNTmQbEunJ2FOSlqbQxkBBAjgGEppRPt30iGj/VjA
|
||||
hCATq2MYOa/X4dVR51BqQAFIEwKBgQDBSIfTFKC/hDk6FKZlgwvupWYJyU9Rkyfs
|
||||
tPErZFmzoKhPkQ3YORo2oeAYmVUbS9I2iIYpYpYQJHX8jMuCbCz4ONxTCuSIXYQY
|
||||
UcUq4PglCKp31xBAE6TN8SvhfME9/MvuDssnQinAHuF0GDAhF646T3LLS1not6Vs
|
||||
zv7brwSoGwKBgQC88v/8cGfi80ssQZeMnVvq1UTXIeQcQnoY5lGHJl3K8mbS3TnX
|
||||
E6c9j417Fdz+rj8KWzBzwWXQB5pSPflWcdZO886Xu/mVGmy9RWgLuVFhXwCwsVEP
|
||||
jNX5ramRb0/vY0yzenUCninBsIxFSbIfrPtLUYCc4hpxr+sr2Mg/y6jpvQKBgBez
|
||||
MRRs3xkcuXepuI2R+BCXL1/b02IJTUf1F+1eLLGd7YV0H+J3fgNc7gGWK51hOrF9
|
||||
JBZHBGeOUPlaukmPwiPdtQZpu4QNE3l37VlIpKTF30E6mb+BqR+nht3rUjarnMXg
|
||||
AoEZ18y6/KIjpSMpqC92Nnk/EBM9EYe6Cf4eA9ApAoGAeqEUg46UTlJySkBKURGp
|
||||
Is3v1kkf5I0X8DnOhwb+HPxNaiEdmO7ckm8+tPVgppLcG0+tMdLjigFQiDUQk2y3
|
||||
WjyxP5ZvXu7U96jaJRI8PFMoE06WeVYcdIzrID2HvqH+w0UQJFrLJ/0Mn4stFAEz
|
||||
XKZBokBGnjFnTnKcs7nv/O8=
|
||||
-----END PRIVATE KEY-----";
|
||||
|
||||
let private_key = AsymmetricCryptoKey::from_pem(RSA_PRIVATE_KEY).unwrap();
|
||||
let public_key = private_key.to_public_key().to_der().unwrap();
|
||||
|
||||
let b64 = STANDARD.encode(public_key);
|
||||
|
||||
let p = private_key.to_der().unwrap();
|
||||
|
||||
RsaKeyPair {
|
||||
private: p.encrypt_with_key(key).unwrap(),
|
||||
public: b64,
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn generate_organization_keys() -> *const c_char {
|
||||
let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
|
||||
|
||||
let key = UserKey::new(key);
|
||||
let keypair = key.make_key_pair().expect("Failed to generate key pair");
|
||||
|
||||
let json = serde_json::json!({
|
||||
"key": key.0.to_base64(),
|
||||
"publicKey": keypair.public.to_string(),
|
||||
"privateKey": keypair.private.to_string(),
|
||||
})
|
||||
.to_string();
|
||||
|
||||
let result = CString::new(json).unwrap();
|
||||
|
||||
result.into_raw()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn generate_user_organization_key(
|
||||
user_public_key: *const c_char,
|
||||
organization_key: *const c_char,
|
||||
) -> *const c_char {
|
||||
let user_public_key = CStr::from_ptr(user_public_key).to_str().unwrap().to_owned();
|
||||
let organization_key = CStr::from_ptr(organization_key)
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_owned();
|
||||
|
||||
let user_public_key = STANDARD.decode(user_public_key).unwrap();
|
||||
let organization_key = STANDARD.decode(organization_key).unwrap();
|
||||
|
||||
let encapsulation_key =
|
||||
AsymmetricPublicCryptoKey::from_der(&SpkiPublicKeyBytes::from(user_public_key)).unwrap();
|
||||
|
||||
let encrypted_key = UnsignedSharedKey::encapsulate_key_unsigned(
|
||||
&SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(organization_key)).unwrap(),
|
||||
&encapsulation_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let result = CString::new(encrypted_key.to_string()).unwrap();
|
||||
|
||||
result.into_raw()
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// The `str` pointer must be a valid pointer previously returned by `CString::into_raw`
|
||||
/// and must not have already been freed. After calling this function, the pointer must not be used again.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn free_c_string(str: *mut c_char) {
|
||||
unsafe {
|
||||
drop(CString::from_raw(str));
|
||||
}
|
||||
}
|
||||
56
util/Sdk2/dotnet/NativeMethods.cs
Normal file
56
util/Sdk2/dotnet/NativeMethods.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
internal static unsafe partial class NativeMethods
|
||||
{
|
||||
// https://docs.microsoft.com/en-us/dotnet/standard/native-interop/cross-platform
|
||||
// Library path will search
|
||||
// win => __DllName, __DllName.dll
|
||||
// linux, osx => __DllName.so, __DllName.dylib
|
||||
|
||||
static NativeMethods()
|
||||
{
|
||||
NativeLibrary.SetDllImportResolver(typeof(NativeMethods).Assembly, DllImportResolver);
|
||||
}
|
||||
|
||||
static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
|
||||
{
|
||||
if (libraryName == __DllName)
|
||||
{
|
||||
var path = "runtimes/";
|
||||
var extension = "";
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
path += "win-";
|
||||
extension = ".dll";
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
path += "osx-";
|
||||
extension = ".dylib";
|
||||
}
|
||||
else
|
||||
{
|
||||
path += "linux-";
|
||||
extension = ".so";
|
||||
}
|
||||
|
||||
if (RuntimeInformation.ProcessArchitecture == Architecture.X86)
|
||||
{
|
||||
path += "x86";
|
||||
}
|
||||
else if (RuntimeInformation.ProcessArchitecture == Architecture.X64)
|
||||
{
|
||||
path += "x64";
|
||||
}
|
||||
else if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
||||
{
|
||||
path += "arm64";
|
||||
}
|
||||
|
||||
path += "/native/" + __DllName + extension;
|
||||
|
||||
return NativeLibrary.Load(Path.Combine(AppContext.BaseDirectory, path), assembly, searchPath);
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
@@ -41,4 +41,18 @@ public static class OrgnaizationExtensions
|
||||
Status = OrganizationUserStatusType.Confirmed
|
||||
};
|
||||
}
|
||||
|
||||
public static OrganizationUser CreateSdkOrganizationUser(this Organization organization, User user)
|
||||
{
|
||||
return new OrganizationUser
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = organization.Id,
|
||||
UserId = user.Id,
|
||||
|
||||
Key = "4.rY01mZFXHOsBAg5Fq4gyXuklWfm6mQASm42DJpx05a+e2mmp+P5W6r54WU2hlREX0uoTxyP91bKKwickSPdCQQ58J45LXHdr9t2uzOYyjVzpzebFcdMw1eElR9W2DW8wEk9+mvtWvKwu7yTebzND+46y1nRMoFydi5zPVLSlJEf81qZZ4Uh1UUMLwXz+NRWfixnGXgq2wRq1bH0n3mqDhayiG4LJKgGdDjWXC8W8MMXDYx24SIJrJu9KiNEMprJE+XVF9nQVNijNAjlWBqkDpsfaWTUfeVLRLctfAqW1blsmIv4RQ91PupYJZDNc8nO9ZTF3TEVM+2KHoxzDJrLs2Q==",
|
||||
Type = OrganizationUserType.Admin,
|
||||
Status = OrganizationUserStatusType.Confirmed
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Infrastructure.EntityFramework.Models;
|
||||
using Bit.RustSDK;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Seeder.Factories;
|
||||
|
||||
@@ -66,27 +68,56 @@ public class UserSeeder(Guid mangleId)
|
||||
{
|
||||
var mangleMap = new Dictionary<string, string?>
|
||||
{
|
||||
{expectedUserData.Email, MangleEmail(expectedUserData.Email) },
|
||||
{expectedUserData.Id.ToString(), user.Id.ToString() },
|
||||
{expectedUserData.Kdf.ToString(), user.Kdf.ToString() },
|
||||
{expectedUserData.KdfIterations.ToString(), user.KdfIterations.ToString() }
|
||||
{ expectedUserData.Email, MangleEmail(expectedUserData.Email) },
|
||||
{ expectedUserData.Id.ToString(), user.Id.ToString() },
|
||||
{ expectedUserData.Kdf.ToString(), user.Kdf.ToString() },
|
||||
{ expectedUserData.KdfIterations.ToString(), user.KdfIterations.ToString() }
|
||||
};
|
||||
if (expectedUserData.Key != null)
|
||||
{
|
||||
mangleMap[expectedUserData.Key] = user.Key;
|
||||
}
|
||||
|
||||
if (expectedUserData.PublicKey != null)
|
||||
{
|
||||
mangleMap[expectedUserData.PublicKey] = user.PublicKey;
|
||||
}
|
||||
|
||||
if (expectedUserData.PrivateKey != null)
|
||||
{
|
||||
mangleMap[expectedUserData.PrivateKey] = user.PrivateKey;
|
||||
}
|
||||
|
||||
if (expectedUserData.ApiKey != null)
|
||||
{
|
||||
mangleMap[expectedUserData.ApiKey] = user.ApiKey;
|
||||
}
|
||||
|
||||
return mangleMap;
|
||||
}
|
||||
|
||||
public static (User user, string userKey) CreateSdkUser(IPasswordHasher<Bit.Core.Entities.User> passwordHasher, string email)
|
||||
{
|
||||
var nativeService = RustSdkServiceFactory.CreateSingleton();
|
||||
var keys = nativeService.GenerateUserKeys(email, "asdfasdfasdf");
|
||||
|
||||
var user = new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Email = email,
|
||||
MasterPassword = null,
|
||||
SecurityStamp = "4830e359-e150-4eae-be2a-996c81c5e609",
|
||||
Key = keys.EncryptedUserKey,
|
||||
PublicKey = keys.PublicKey,
|
||||
PrivateKey = keys.PrivateKey,
|
||||
ApiKey = "7gp59kKHt9kMlks0BuNC4IjNXYkljR",
|
||||
|
||||
Kdf = KdfType.PBKDF2_SHA256,
|
||||
KdfIterations = 5_000,
|
||||
};
|
||||
|
||||
user.MasterPassword = passwordHasher.HashPassword(user, keys.MasterPasswordHash);
|
||||
|
||||
return (user, keys.Key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<ProjectReference Include="..\..\src\Core\Core.csproj" />
|
||||
<ProjectReference Include="..\..\src\Infrastructure.EntityFramework\Infrastructure.EntityFramework.csproj" />
|
||||
<ProjectReference Include="..\..\src\SharedWeb\SharedWeb.csproj" />
|
||||
<ProjectReference Include="..\RustSdk\RustSdk.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user