mirror of
https://github.com/bitwarden/server.git
synced 2025-04-13 17:18:14 -05:00
Merge branch 'main' into ac/pm-15621/refactor-delete-command
This commit is contained in:
commit
9e95d875dc
1
.github/renovate.json
vendored
1
.github/renovate.json
vendored
@ -64,7 +64,6 @@
|
||||
"Braintree",
|
||||
"coverlet.collector",
|
||||
"CsvHelper",
|
||||
"FluentAssertions",
|
||||
"Kralizek.AutoFixture.Extensions.MockHttp",
|
||||
"Microsoft.AspNetCore.Mvc.Testing",
|
||||
"Microsoft.Extensions.Logging",
|
||||
|
@ -3,11 +3,16 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
|
||||
<Version>2025.1.5</Version>
|
||||
<Version>2025.2.0</Version>
|
||||
|
||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<!-- Treat it as a test project if the project hasn't set their own value and it follows our test project conventions -->
|
||||
<IsTestProject Condition="'$(IsTestProject)' == '' and ($(MSBuildProjectName.EndsWith('.Test')) or $(MSBuildProjectName.EndsWith('.IntegrationTest')))">true</IsTestProject>
|
||||
<Nullable Condition="'$(Nullable)' == '' and '$(IsTestProject)' == 'true'">annotations</Nullable>
|
||||
<!-- Uncomment the below line when we are ready to enable nullable repo wide -->
|
||||
<!-- <Nullable Condition="'$(Nullable)' == '' and '$(IsTestProject)' != 'true'">enable</Nullable> -->
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
|
@ -248,7 +248,7 @@ public class GroupsControllerTests : IClassFixture<ScimApplicationFactory>, IAsy
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public async Task Post_InvalidDisplayName_BadRequest(string displayName)
|
||||
public async Task Post_InvalidDisplayName_BadRequest(string? displayName)
|
||||
{
|
||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
||||
var model = new ScimGroupRequestModel
|
||||
|
@ -324,7 +324,7 @@ public class UsersControllerTests : IClassFixture<ScimApplicationFactory>, IAsyn
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public async Task Post_InvalidEmail_BadRequest(string email)
|
||||
public async Task Post_InvalidEmail_BadRequest(string? email)
|
||||
{
|
||||
var displayName = "Test User 5";
|
||||
var externalId = "UE";
|
||||
|
@ -266,8 +266,18 @@ public class AccountsController : Controller
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
user = model.ToUser(user);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, e.Message);
|
||||
throw new BadRequestException(ModelState);
|
||||
}
|
||||
|
||||
var result = await _setInitialMasterPasswordCommand.SetInitialMasterPasswordAsync(
|
||||
model.ToUser(user),
|
||||
user,
|
||||
model.MasterPasswordHash,
|
||||
model.Key,
|
||||
model.OrgIdentifier);
|
||||
@ -974,7 +984,6 @@ public class AccountsController : Controller
|
||||
await _userService.ResendNewDeviceVerificationEmail(request.Email, request.Secret);
|
||||
}
|
||||
|
||||
[RequireFeature(FeatureFlagKeys.NewDeviceVerification)]
|
||||
[HttpPost("verify-devices")]
|
||||
[HttpPut("verify-devices")]
|
||||
public async Task SetUserVerifyDevicesAsync([FromBody] SetVerifyDevicesRequestModel request)
|
||||
|
@ -2,6 +2,7 @@
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.Billing.Models.Requests;
|
||||
using Bit.Api.Billing.Models.Responses;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
@ -17,7 +18,6 @@ namespace Bit.Api.Billing.Controllers;
|
||||
[Authorize("Application")]
|
||||
public class OrganizationBillingController(
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService,
|
||||
IOrganizationBillingService organizationBillingService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPaymentService paymentService,
|
||||
@ -282,7 +282,15 @@ public class OrganizationBillingController(
|
||||
var plan = StaticStore.GetPlan(model.PlanType);
|
||||
sale.Organization.PlanType = plan.Type;
|
||||
sale.Organization.Plan = plan.Name;
|
||||
sale.SubscriptionSetup.SkipTrial = true;
|
||||
await organizationBillingService.Finalize(sale);
|
||||
var org = await organizationRepository.GetByIdAsync(organizationId);
|
||||
if (organizationSignup.PaymentMethodType != null)
|
||||
{
|
||||
var paymentSource = new TokenizedPaymentSource(organizationSignup.PaymentMethodType.Value, organizationSignup.PaymentToken);
|
||||
var taxInformation = TaxInformation.From(organizationSignup.TaxInfo);
|
||||
await organizationBillingService.UpdatePaymentMethod(org, paymentSource, taxInformation);
|
||||
}
|
||||
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
using Bit.Api.Models.Request.Organizations;
|
||||
using Bit.Api.Models.Response.Organizations;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
@ -107,7 +106,7 @@ public class OrganizationSponsorshipsController : Controller
|
||||
{
|
||||
var isFreeFamilyPolicyEnabled = false;
|
||||
var (isValid, sponsorship) = await _validateRedemptionTokenCommand.ValidateRedemptionTokenAsync(sponsorshipToken, (await CurrentUser).Email);
|
||||
if (isValid && _featureService.IsEnabled(FeatureFlagKeys.DisableFreeFamiliesSponsorship) && sponsorship.SponsoringOrganizationId.HasValue)
|
||||
if (isValid && sponsorship.SponsoringOrganizationId.HasValue)
|
||||
{
|
||||
var policy = await _policyRepository.GetByOrganizationIdTypeAsync(sponsorship.SponsoringOrganizationId.Value,
|
||||
PolicyType.FreeFamiliesSponsorshipPolicy);
|
||||
|
@ -64,8 +64,9 @@ public class ImportCiphersController : Controller
|
||||
[FromBody] ImportOrganizationCiphersRequestModel model)
|
||||
{
|
||||
if (!_globalSettings.SelfHosted &&
|
||||
(model.Ciphers.Count() > 7000 || model.CollectionRelationships.Count() > 14000 ||
|
||||
model.Collections.Count() > 2000))
|
||||
(model.Ciphers.Count() > _globalSettings.ImportCiphersLimitation.CiphersLimit ||
|
||||
model.CollectionRelationships.Count() > _globalSettings.ImportCiphersLimitation.CollectionRelationshipsLimit ||
|
||||
model.Collections.Count() > _globalSettings.ImportCiphersLimitation.CollectionsLimit))
|
||||
{
|
||||
throw new BadRequestException("You cannot import this much data at once.");
|
||||
}
|
||||
|
@ -424,6 +424,59 @@ public class CiphersController : Controller
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TODO: Move this to its own authorization handler or equivalent service - AC-2062
|
||||
/// </summary>
|
||||
private async Task<bool> CanModifyCipherCollectionsAsync(Guid organizationId, IEnumerable<Guid> cipherIds)
|
||||
{
|
||||
// If the user can edit all ciphers for the organization, just check they all belong to the org
|
||||
if (await CanEditAllCiphersAsync(organizationId))
|
||||
{
|
||||
// TODO: This can likely be optimized to only query the requested ciphers and then checking they belong to the org
|
||||
var orgCiphers = (await _cipherRepository.GetManyByOrganizationIdAsync(organizationId)).ToDictionary(c => c.Id);
|
||||
|
||||
// Ensure all requested ciphers are in orgCiphers
|
||||
if (cipherIds.Any(c => !orgCiphers.ContainsKey(c)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// The user cannot access any ciphers for the organization, we're done
|
||||
if (!await CanAccessOrganizationCiphersAsync(organizationId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
// Select all editable ciphers for this user belonging to the organization
|
||||
var editableOrgCipherList = (await _cipherRepository.GetManyByUserIdAsync(userId, true))
|
||||
.Where(c => c.OrganizationId == organizationId && c.UserId == null && c.Edit && c.ViewPassword).ToList();
|
||||
|
||||
// Special case for unassigned ciphers
|
||||
if (await CanAccessUnassignedCiphersAsync(organizationId))
|
||||
{
|
||||
var unassignedCiphers =
|
||||
(await _cipherRepository.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(
|
||||
organizationId));
|
||||
|
||||
// Users that can access unassigned ciphers can also edit them
|
||||
editableOrgCipherList.AddRange(unassignedCiphers.Select(c => new CipherDetails(c) { Edit = true }));
|
||||
}
|
||||
|
||||
var editableOrgCiphers = editableOrgCipherList
|
||||
.ToDictionary(c => c.Id);
|
||||
|
||||
if (cipherIds.Any(c => !editableOrgCiphers.ContainsKey(c)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TODO: Move this to its own authorization handler or equivalent service - AC-2062
|
||||
/// </summary>
|
||||
@ -579,7 +632,7 @@ public class CiphersController : Controller
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var cipher = await GetByIdAsync(id, userId);
|
||||
if (cipher == null || !cipher.OrganizationId.HasValue ||
|
||||
!await _currentContext.OrganizationUser(cipher.OrganizationId.Value))
|
||||
!await _currentContext.OrganizationUser(cipher.OrganizationId.Value) || !cipher.ViewPassword)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
@ -634,7 +687,7 @@ public class CiphersController : Controller
|
||||
[HttpPost("bulk-collections")]
|
||||
public async Task PostBulkCollections([FromBody] CipherBulkUpdateCollectionsRequestModel model)
|
||||
{
|
||||
if (!await CanEditCiphersAsync(model.OrganizationId, model.CipherIds) ||
|
||||
if (!await CanModifyCipherCollectionsAsync(model.OrganizationId, model.CipherIds) ||
|
||||
!await CanEditItemsInCollections(model.OrganizationId, model.CollectionIds))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.Vault.Models.Request;
|
||||
using Bit.Api.Vault.Models.Response;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Services;
|
||||
@ -20,17 +21,20 @@ public class SecurityTaskController : Controller
|
||||
private readonly IGetTaskDetailsForUserQuery _getTaskDetailsForUserQuery;
|
||||
private readonly IMarkTaskAsCompleteCommand _markTaskAsCompleteCommand;
|
||||
private readonly IGetTasksForOrganizationQuery _getTasksForOrganizationQuery;
|
||||
private readonly ICreateManyTasksCommand _createManyTasksCommand;
|
||||
|
||||
public SecurityTaskController(
|
||||
IUserService userService,
|
||||
IGetTaskDetailsForUserQuery getTaskDetailsForUserQuery,
|
||||
IMarkTaskAsCompleteCommand markTaskAsCompleteCommand,
|
||||
IGetTasksForOrganizationQuery getTasksForOrganizationQuery)
|
||||
IGetTasksForOrganizationQuery getTasksForOrganizationQuery,
|
||||
ICreateManyTasksCommand createManyTasksCommand)
|
||||
{
|
||||
_userService = userService;
|
||||
_getTaskDetailsForUserQuery = getTaskDetailsForUserQuery;
|
||||
_markTaskAsCompleteCommand = markTaskAsCompleteCommand;
|
||||
_getTasksForOrganizationQuery = getTasksForOrganizationQuery;
|
||||
_createManyTasksCommand = createManyTasksCommand;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -71,4 +75,19 @@ public class SecurityTaskController : Controller
|
||||
var response = securityTasks.Select(x => new SecurityTasksResponseModel(x)).ToList();
|
||||
return new ListResponseModel<SecurityTasksResponseModel>(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulk create security tasks for an organization.
|
||||
/// </summary>
|
||||
/// <param name="orgId"></param>
|
||||
/// <param name="model"></param>
|
||||
/// <returns>A list response model containing the security tasks created for the organization.</returns>
|
||||
[HttpPost("{orgId:guid}/bulk-create")]
|
||||
public async Task<ListResponseModel<SecurityTasksResponseModel>> BulkCreateTasks(Guid orgId,
|
||||
[FromBody] BulkCreateSecurityTasksRequestModel model)
|
||||
{
|
||||
var securityTasks = await _createManyTasksCommand.CreateAsync(orgId, model.Tasks);
|
||||
var response = securityTasks.Select(x => new SecurityTasksResponseModel(x)).ToList();
|
||||
return new ListResponseModel<SecurityTasksResponseModel>(response);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,8 @@
|
||||
using Bit.Core.Vault.Models.Api;
|
||||
|
||||
namespace Bit.Api.Vault.Models.Request;
|
||||
|
||||
public class BulkCreateSecurityTasksRequestModel
|
||||
{
|
||||
public IEnumerable<SecurityTaskCreateRequest> Tasks { get; set; }
|
||||
}
|
@ -56,6 +56,11 @@
|
||||
"publicKey": "SECRET",
|
||||
"privateKey": "SECRET"
|
||||
},
|
||||
"importCiphersLimitation": {
|
||||
"ciphersLimit": 40000,
|
||||
"collectionRelationshipsLimit": 80000,
|
||||
"collectionsLimit": 2000
|
||||
},
|
||||
"bitPay": {
|
||||
"production": false,
|
||||
"token": "SECRET",
|
||||
|
@ -10,5 +10,8 @@
|
||||
<ProjectReference Include="..\SharedWeb\SharedWeb.csproj" />
|
||||
<ProjectReference Include="..\Core\Core.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -12,6 +12,7 @@ public class BillingSettings
|
||||
public virtual FreshDeskSettings FreshDesk { get; set; } = new FreshDeskSettings();
|
||||
public virtual string FreshsalesApiKey { get; set; }
|
||||
public virtual PayPalSettings PayPal { get; set; } = new PayPalSettings();
|
||||
public virtual OnyxSettings Onyx { get; set; } = new OnyxSettings();
|
||||
|
||||
public class PayPalSettings
|
||||
{
|
||||
@ -31,4 +32,10 @@ public class BillingSettings
|
||||
public virtual string UserFieldName { get; set; }
|
||||
public virtual string OrgFieldName { get; set; }
|
||||
}
|
||||
|
||||
public class OnyxSettings
|
||||
{
|
||||
public virtual string ApiKey { get; set; }
|
||||
public virtual string BaseUrl { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ using Microsoft.Extensions.Options;
|
||||
namespace Bit.Billing.Controllers;
|
||||
|
||||
[Route("bitpay")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class BitPayController : Controller
|
||||
{
|
||||
private readonly BillingSettings _billingSettings;
|
||||
|
@ -1,6 +1,8 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Web;
|
||||
using Bit.Billing.Models;
|
||||
using Bit.Core.Repositories;
|
||||
@ -142,6 +144,121 @@ public class FreshdeskController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("webhook-onyx-ai")]
|
||||
public async Task<IActionResult> PostWebhookOnyxAi([FromQuery, Required] string key,
|
||||
[FromBody, Required] FreshdeskWebhookModel model)
|
||||
{
|
||||
// ensure that the key is from Freshdesk
|
||||
if (!IsValidRequestFromFreshdesk(key))
|
||||
{
|
||||
return new BadRequestResult();
|
||||
}
|
||||
|
||||
// get ticket info from Freshdesk
|
||||
var getTicketRequest = new HttpRequestMessage(HttpMethod.Get,
|
||||
string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}", model.TicketId));
|
||||
var getTicketResponse = await CallFreshdeskApiAsync(getTicketRequest);
|
||||
|
||||
// check if we have a valid response from freshdesk
|
||||
if (getTicketResponse.StatusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
_logger.LogError("Error getting ticket info from Freshdesk. Ticket Id: {0}. Status code: {1}",
|
||||
model.TicketId, getTicketResponse.StatusCode);
|
||||
return BadRequest("Failed to retrieve ticket info from Freshdesk");
|
||||
}
|
||||
|
||||
// extract info from the response
|
||||
var ticketInfo = await ExtractTicketInfoFromResponse(getTicketResponse);
|
||||
if (ticketInfo == null)
|
||||
{
|
||||
return BadRequest("Failed to extract ticket info from Freshdesk response");
|
||||
}
|
||||
|
||||
// create the onyx `answer-with-citation` request
|
||||
var onyxRequestModel = new OnyxAnswerWithCitationRequestModel(ticketInfo.DescriptionText);
|
||||
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);
|
||||
|
||||
// the CallOnyxApi will return a null if we have an error response
|
||||
if (onyxJsonResponse?.Answer == null || !string.IsNullOrEmpty(onyxJsonResponse?.ErrorMsg))
|
||||
{
|
||||
return BadRequest(
|
||||
string.Format("Failed to get a valid response from Onyx API. Response: {0}",
|
||||
JsonSerializer.Serialize(onyxJsonResponse ?? new OnyxAnswerWithCitationResponseModel())));
|
||||
}
|
||||
|
||||
// add the answer as a note to the ticket
|
||||
await AddAnswerNoteToTicketAsync(onyxJsonResponse.Answer, model.TicketId);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private bool IsValidRequestFromFreshdesk(string key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key)
|
||||
|| !CoreHelpers.FixedTimeEquals(key, _billingSettings.FreshDesk.WebhookKey))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task AddAnswerNoteToTicketAsync(string note, string ticketId)
|
||||
{
|
||||
// if there is no content, then we don't need to add a note
|
||||
if (string.IsNullOrWhiteSpace(note))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var noteBody = new Dictionary<string, object>
|
||||
{
|
||||
{ "body", $"<b>Onyx AI:</b><ul>{note}</ul>" },
|
||||
{ "private", true }
|
||||
};
|
||||
|
||||
var noteRequest = new HttpRequestMessage(HttpMethod.Post,
|
||||
string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}/notes", ticketId))
|
||||
{
|
||||
Content = JsonContent.Create(noteBody),
|
||||
};
|
||||
|
||||
var addNoteResponse = await CallFreshdeskApiAsync(noteRequest);
|
||||
if (addNoteResponse.StatusCode != System.Net.HttpStatusCode.Created)
|
||||
{
|
||||
_logger.LogError("Error adding note to Freshdesk ticket. Ticket Id: {0}. Status: {1}",
|
||||
ticketId, addNoteResponse.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<FreshdeskViewTicketModel> ExtractTicketInfoFromResponse(HttpResponseMessage getTicketResponse)
|
||||
{
|
||||
var responseString = string.Empty;
|
||||
try
|
||||
{
|
||||
responseString = await getTicketResponse.Content.ReadAsStringAsync();
|
||||
var ticketInfo = JsonSerializer.Deserialize<FreshdeskViewTicketModel>(responseString,
|
||||
options: new System.Text.Json.JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
});
|
||||
|
||||
return ticketInfo;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
_logger.LogError("Error deserializing ticket info from Freshdesk response. Response: {0}. Exception {1}",
|
||||
responseString, ex.ToString());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> CallFreshdeskApiAsync(HttpRequestMessage request, int retriedCount = 0)
|
||||
{
|
||||
try
|
||||
@ -166,6 +283,26 @@ public class FreshdeskController : Controller
|
||||
return await CallFreshdeskApiAsync(request, retriedCount++);
|
||||
}
|
||||
|
||||
private async Task<(HttpResponseMessage, T)> CallOnyxApi<T>(HttpRequestMessage request)
|
||||
{
|
||||
var httpClient = _httpClientFactory.CreateClient("OnyxApi");
|
||||
var response = await httpClient.SendAsync(request);
|
||||
|
||||
if (response.StatusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
_logger.LogError("Error calling Onyx AI API. Status code: {0}. Response {1}",
|
||||
response.StatusCode, JsonSerializer.Serialize(response));
|
||||
return (null, default);
|
||||
}
|
||||
var responseStr = await response.Content.ReadAsStringAsync();
|
||||
var responseJson = JsonSerializer.Deserialize<T>(responseStr, options: new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
});
|
||||
|
||||
return (response, responseJson);
|
||||
}
|
||||
|
||||
private TAttribute GetAttribute<TAttribute>(Enum enumValue) where TAttribute : Attribute
|
||||
{
|
||||
return enumValue.GetType().GetMember(enumValue.ToString()).First().GetCustomAttribute<TAttribute>();
|
||||
|
44
src/Billing/Models/FreshdeskViewTicketModel.cs
Normal file
44
src/Billing/Models/FreshdeskViewTicketModel.cs
Normal file
@ -0,0 +1,44 @@
|
||||
namespace Bit.Billing.Models;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
public class FreshdeskViewTicketModel
|
||||
{
|
||||
[JsonPropertyName("spam")]
|
||||
public bool? Spam { get; set; }
|
||||
|
||||
[JsonPropertyName("priority")]
|
||||
public int? Priority { get; set; }
|
||||
|
||||
[JsonPropertyName("source")]
|
||||
public int? Source { get; set; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public int? Status { get; set; }
|
||||
|
||||
[JsonPropertyName("subject")]
|
||||
public string Subject { get; set; }
|
||||
|
||||
[JsonPropertyName("support_email")]
|
||||
public string SupportEmail { get; set; }
|
||||
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string Description { get; set; }
|
||||
|
||||
[JsonPropertyName("description_text")]
|
||||
public string DescriptionText { get; set; }
|
||||
|
||||
[JsonPropertyName("created_at")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("updated_at")]
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("tags")]
|
||||
public List<string> Tags { get; set; }
|
||||
}
|
54
src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs
Normal file
54
src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs
Normal file
@ -0,0 +1,54 @@
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Bit.Billing.Models;
|
||||
|
||||
public class OnyxAnswerWithCitationRequestModel
|
||||
{
|
||||
[JsonPropertyName("messages")]
|
||||
public List<Message> Messages { get; set; }
|
||||
|
||||
[JsonPropertyName("persona_id")]
|
||||
public int PersonaId { get; set; } = 1;
|
||||
|
||||
[JsonPropertyName("prompt_id")]
|
||||
public int PromptId { get; set; } = 1;
|
||||
|
||||
[JsonPropertyName("retrieval_options")]
|
||||
public RetrievalOptions RetrievalOptions { get; set; }
|
||||
|
||||
public OnyxAnswerWithCitationRequestModel(string message)
|
||||
{
|
||||
message = message.Replace(Environment.NewLine, " ").Replace('\r', ' ').Replace('\n', ' ');
|
||||
Messages = new List<Message>() { new Message() { MessageText = message } };
|
||||
RetrievalOptions = new RetrievalOptions();
|
||||
}
|
||||
}
|
||||
|
||||
public class Message
|
||||
{
|
||||
[JsonPropertyName("message")]
|
||||
public string MessageText { get; set; }
|
||||
|
||||
[JsonPropertyName("sender")]
|
||||
public string Sender { get; set; } = "user";
|
||||
}
|
||||
|
||||
public class RetrievalOptions
|
||||
{
|
||||
[JsonPropertyName("run_search")]
|
||||
public string RunSearch { get; set; } = RetrievalOptionsRunSearch.Auto;
|
||||
|
||||
[JsonPropertyName("real_time")]
|
||||
public bool RealTime { get; set; } = true;
|
||||
|
||||
[JsonPropertyName("limit")]
|
||||
public int? Limit { get; set; } = 3;
|
||||
}
|
||||
|
||||
public class RetrievalOptionsRunSearch
|
||||
{
|
||||
public const string Always = "always";
|
||||
public const string Never = "never";
|
||||
public const string Auto = "auto";
|
||||
}
|
30
src/Billing/Models/OnyxAnswerWithCitationResponseModel.cs
Normal file
30
src/Billing/Models/OnyxAnswerWithCitationResponseModel.cs
Normal file
@ -0,0 +1,30 @@
|
||||
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; }
|
||||
}
|
@ -14,19 +14,22 @@ public class CustomerUpdatedHandler : ICustomerUpdatedHandler
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IStripeEventService _stripeEventService;
|
||||
private readonly IStripeEventUtilityService _stripeEventUtilityService;
|
||||
private readonly ILogger<CustomerUpdatedHandler> _logger;
|
||||
|
||||
public CustomerUpdatedHandler(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IReferenceEventService referenceEventService,
|
||||
ICurrentContext currentContext,
|
||||
IStripeEventService stripeEventService,
|
||||
IStripeEventUtilityService stripeEventUtilityService)
|
||||
IStripeEventUtilityService stripeEventUtilityService,
|
||||
ILogger<CustomerUpdatedHandler> logger)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationRepository = organizationRepository ?? throw new ArgumentNullException(nameof(organizationRepository));
|
||||
_referenceEventService = referenceEventService;
|
||||
_currentContext = currentContext;
|
||||
_stripeEventService = stripeEventService;
|
||||
_stripeEventUtilityService = stripeEventUtilityService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -35,25 +38,76 @@ public class CustomerUpdatedHandler : ICustomerUpdatedHandler
|
||||
/// <param name="parsedEvent"></param>
|
||||
public async Task HandleAsync(Event parsedEvent)
|
||||
{
|
||||
var customer = await _stripeEventService.GetCustomer(parsedEvent, true, ["subscriptions"]);
|
||||
if (customer.Subscriptions == null || !customer.Subscriptions.Any())
|
||||
if (parsedEvent == null)
|
||||
{
|
||||
_logger.LogError("Parsed event was null in CustomerUpdatedHandler");
|
||||
throw new ArgumentNullException(nameof(parsedEvent));
|
||||
}
|
||||
|
||||
if (_stripeEventService == null)
|
||||
{
|
||||
_logger.LogError("StripeEventService was not initialized in CustomerUpdatedHandler");
|
||||
throw new InvalidOperationException($"{nameof(_stripeEventService)} is not initialized");
|
||||
}
|
||||
|
||||
var customer = await _stripeEventService.GetCustomer(parsedEvent, true, ["subscriptions"]);
|
||||
if (customer?.Subscriptions == null || !customer.Subscriptions.Any())
|
||||
{
|
||||
_logger.LogWarning("Customer or subscriptions were null or empty in CustomerUpdatedHandler. Customer ID: {CustomerId}", customer?.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
var subscription = customer.Subscriptions.First();
|
||||
|
||||
if (subscription.Metadata == null)
|
||||
{
|
||||
_logger.LogWarning("Subscription metadata was null in CustomerUpdatedHandler. Subscription ID: {SubscriptionId}", subscription.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_stripeEventUtilityService == null)
|
||||
{
|
||||
_logger.LogError("StripeEventUtilityService was not initialized in CustomerUpdatedHandler");
|
||||
throw new InvalidOperationException($"{nameof(_stripeEventUtilityService)} is not initialized");
|
||||
}
|
||||
|
||||
var (organizationId, _, providerId) = _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata);
|
||||
|
||||
if (!organizationId.HasValue)
|
||||
{
|
||||
_logger.LogWarning("Organization ID was not found in subscription metadata. Subscription ID: {SubscriptionId}", subscription.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_organizationRepository == null)
|
||||
{
|
||||
_logger.LogError("OrganizationRepository was not initialized in CustomerUpdatedHandler");
|
||||
throw new InvalidOperationException($"{nameof(_organizationRepository)} is not initialized");
|
||||
}
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(organizationId.Value);
|
||||
|
||||
if (organization == null)
|
||||
{
|
||||
_logger.LogWarning("Organization not found. Organization ID: {OrganizationId}", organizationId.Value);
|
||||
return;
|
||||
}
|
||||
|
||||
organization.BillingEmail = customer.Email;
|
||||
await _organizationRepository.ReplaceAsync(organization);
|
||||
|
||||
if (_referenceEventService == null)
|
||||
{
|
||||
_logger.LogError("ReferenceEventService was not initialized in CustomerUpdatedHandler");
|
||||
throw new InvalidOperationException($"{nameof(_referenceEventService)} is not initialized");
|
||||
}
|
||||
|
||||
if (_currentContext == null)
|
||||
{
|
||||
_logger.LogError("CurrentContext was not initialized in CustomerUpdatedHandler");
|
||||
throw new InvalidOperationException($"{nameof(_currentContext)} is not initialized");
|
||||
}
|
||||
|
||||
await _referenceEventService.RaiseEventAsync(
|
||||
new ReferenceEvent(ReferenceEventType.OrganizationEditedInStripe, organization, _currentContext));
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Globalization;
|
||||
using System.Net.Http.Headers;
|
||||
using Bit.Billing.Services;
|
||||
using Bit.Billing.Services.Implementations;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
@ -34,6 +35,7 @@ public class Startup
|
||||
// Settings
|
||||
var globalSettings = services.AddGlobalSettingsServices(Configuration, Environment);
|
||||
services.Configure<BillingSettings>(Configuration.GetSection("BillingSettings"));
|
||||
var billingSettings = Configuration.GetSection("BillingSettings").Get<BillingSettings>();
|
||||
|
||||
// Stripe Billing
|
||||
StripeConfiguration.ApiKey = globalSettings.Stripe.ApiKey;
|
||||
@ -97,6 +99,10 @@ public class Startup
|
||||
|
||||
// Set up HttpClients
|
||||
services.AddHttpClient("FreshdeskApi");
|
||||
services.AddHttpClient("OnyxApi", client =>
|
||||
{
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", billingSettings.Onyx.ApiKey);
|
||||
});
|
||||
|
||||
services.AddScoped<IStripeFacade, StripeFacade>();
|
||||
services.AddScoped<IStripeEventService, StripeEventService>();
|
||||
@ -112,6 +118,10 @@ public class Startup
|
||||
// Jobs service
|
||||
Jobs.JobsHostedService.AddJobsServices(services);
|
||||
services.AddHostedService<Jobs.JobsHostedService>();
|
||||
|
||||
// Swagger
|
||||
services.AddEndpointsApiExplorer();
|
||||
services.AddSwaggerGen();
|
||||
}
|
||||
|
||||
public void Configure(
|
||||
@ -128,6 +138,11 @@ public class Startup
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(c =>
|
||||
{
|
||||
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Billing API V1");
|
||||
});
|
||||
}
|
||||
|
||||
app.UseStaticFiles();
|
||||
|
@ -73,6 +73,10 @@
|
||||
"region": "US",
|
||||
"userFieldName": "cf_user",
|
||||
"orgFieldName": "cf_org"
|
||||
}
|
||||
},
|
||||
"onyx": {
|
||||
"apiKey": "SECRET",
|
||||
"baseUrl": "https://cloud.onyx.app/api"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,4 +60,12 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
|
||||
Task<ICollection<OrganizationUser>> GetManyByOrganizationWithClaimedDomainsAsync(Guid organizationId);
|
||||
|
||||
Task RevokeManyByIdAsync(IEnumerable<Guid> organizationUserIds);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of OrganizationUsersUserDetails with the specified role.
|
||||
/// </summary>
|
||||
/// <param name="organizationId">The organization to search within</param>
|
||||
/// <param name="role">The role to search for</param>
|
||||
/// <returns>A list of OrganizationUsersUserDetails with the specified role</returns>
|
||||
Task<IEnumerable<OrganizationUserUserDetails>> GetManyDetailsByRoleAsync(Guid organizationId, OrganizationUserType role);
|
||||
}
|
||||
|
@ -1294,6 +1294,12 @@ public class OrganizationService : IOrganizationService
|
||||
}
|
||||
}
|
||||
|
||||
var subscription = await _paymentService.GetSubscriptionAsync(organization);
|
||||
if (subscription?.Subscription?.Status == StripeConstants.SubscriptionStatus.Canceled)
|
||||
{
|
||||
return (false, "You do not have an active subscription. Reinstate your subscription to make changes");
|
||||
}
|
||||
|
||||
if (organization.Seats.HasValue &&
|
||||
organization.MaxAutoscaleSeats.HasValue &&
|
||||
organization.MaxAutoscaleSeats.Value < organization.Seats.Value + seatsToAdd)
|
||||
@ -1301,12 +1307,6 @@ public class OrganizationService : IOrganizationService
|
||||
return (false, $"Seat limit has been reached.");
|
||||
}
|
||||
|
||||
var subscription = await _paymentService.GetSubscriptionAsync(organization);
|
||||
if (subscription?.Subscription?.Status == StripeConstants.SubscriptionStatus.Canceled)
|
||||
{
|
||||
return (false, "Cannot autoscale with a canceled subscription.");
|
||||
}
|
||||
|
||||
return (true, failureReason);
|
||||
}
|
||||
|
||||
@ -1951,6 +1951,11 @@ public class OrganizationService : IOrganizationService
|
||||
|
||||
await RepositoryRevokeUserAsync(organizationUser);
|
||||
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked);
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) && organizationUser.UserId.HasValue)
|
||||
{
|
||||
await _pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RevokeUserAsync(OrganizationUser organizationUser,
|
||||
@ -1958,6 +1963,11 @@ public class OrganizationService : IOrganizationService
|
||||
{
|
||||
await RepositoryRevokeUserAsync(organizationUser);
|
||||
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked, systemUser);
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) && organizationUser.UserId.HasValue)
|
||||
{
|
||||
await _pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RepositoryRevokeUserAsync(OrganizationUser organizationUser)
|
||||
@ -2023,6 +2033,10 @@ public class OrganizationService : IOrganizationService
|
||||
await _organizationUserRepository.RevokeAsync(organizationUser.Id);
|
||||
organizationUser.Status = OrganizationUserStatusType.Revoked;
|
||||
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked);
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) && organizationUser.UserId.HasValue)
|
||||
{
|
||||
await _pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value);
|
||||
}
|
||||
|
||||
result.Add(Tuple.Create(organizationUser, ""));
|
||||
}
|
||||
@ -2050,12 +2064,22 @@ public class OrganizationService : IOrganizationService
|
||||
|
||||
await RepositoryRestoreUserAsync(organizationUser);
|
||||
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) && organizationUser.UserId.HasValue)
|
||||
{
|
||||
await _pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser)
|
||||
{
|
||||
await RepositoryRestoreUserAsync(organizationUser);
|
||||
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored, systemUser);
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) && organizationUser.UserId.HasValue)
|
||||
{
|
||||
await _pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RepositoryRestoreUserAsync(OrganizationUser organizationUser)
|
||||
@ -2147,6 +2171,10 @@ public class OrganizationService : IOrganizationService
|
||||
await _organizationUserRepository.RestoreAsync(organizationUser.Id, status);
|
||||
organizationUser.Status = status;
|
||||
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) && organizationUser.UserId.HasValue)
|
||||
{
|
||||
await _pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value);
|
||||
}
|
||||
|
||||
result.Add(Tuple.Create(organizationUser, ""));
|
||||
}
|
||||
|
@ -1,26 +1,36 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
|
||||
public class KeysRequestModel
|
||||
{
|
||||
[Required]
|
||||
public string PublicKey { get; set; }
|
||||
[Required]
|
||||
public string EncryptedPrivateKey { get; set; }
|
||||
|
||||
public User ToUser(User existingUser)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(existingUser.PublicKey) && !string.IsNullOrWhiteSpace(PublicKey))
|
||||
if (string.IsNullOrWhiteSpace(PublicKey) || string.IsNullOrWhiteSpace(EncryptedPrivateKey))
|
||||
{
|
||||
throw new InvalidOperationException("Public and private keys are required.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(existingUser.PublicKey) && string.IsNullOrWhiteSpace(existingUser.PrivateKey))
|
||||
{
|
||||
existingUser.PublicKey = PublicKey;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(existingUser.PrivateKey))
|
||||
{
|
||||
existingUser.PrivateKey = EncryptedPrivateKey;
|
||||
return existingUser;
|
||||
}
|
||||
else if (PublicKey == existingUser.PublicKey && CoreHelpers.FixedTimeEquals(EncryptedPrivateKey, existingUser.PrivateKey))
|
||||
{
|
||||
return existingUser;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Cannot replace existing key(s) with new key(s).");
|
||||
}
|
||||
|
||||
return existingUser;
|
||||
}
|
||||
}
|
||||
|
@ -297,10 +297,34 @@ public class AuthRequestService : IAuthRequestService
|
||||
return;
|
||||
}
|
||||
|
||||
var admins = await _organizationUserRepository.GetManyByMinimumRoleAsync(
|
||||
var adminEmails = await GetAdminAndAccountRecoveryEmailsAsync(organizationUser.OrganizationId);
|
||||
|
||||
await _mailService.SendDeviceApprovalRequestedNotificationEmailAsync(
|
||||
adminEmails,
|
||||
organizationUser.OrganizationId,
|
||||
user.Email,
|
||||
user.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of emails for admins and custom users with the ManageResetPassword permission.
|
||||
/// </summary>
|
||||
/// <param name="organizationId">The organization to search within</param>
|
||||
private async Task<List<string>> GetAdminAndAccountRecoveryEmailsAsync(Guid organizationId)
|
||||
{
|
||||
var admins = await _organizationUserRepository.GetManyByMinimumRoleAsync(
|
||||
organizationId,
|
||||
OrganizationUserType.Admin);
|
||||
var adminEmails = admins.Select(a => a.Email).Distinct().ToList();
|
||||
await _mailService.SendDeviceApprovalRequestedNotificationEmailAsync(adminEmails, organizationUser.OrganizationId, user.Email, user.Name);
|
||||
|
||||
var customUsers = await _organizationUserRepository.GetManyDetailsByRoleAsync(
|
||||
organizationId,
|
||||
OrganizationUserType.Custom);
|
||||
|
||||
return admins.Select(a => a.Email)
|
||||
.Concat(customUsers
|
||||
.Where(a => a.GetPermissions().ManageResetPassword)
|
||||
.Select(a => a.Email))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ public class SubscriptionSetup
|
||||
public required Plan Plan { get; set; }
|
||||
public required PasswordManager PasswordManagerOptions { get; set; }
|
||||
public SecretsManager? SecretsManagerOptions { get; set; }
|
||||
public bool SkipTrial = false;
|
||||
|
||||
public class PasswordManager
|
||||
{
|
||||
|
@ -379,7 +379,7 @@ public class OrganizationBillingService(
|
||||
["organizationId"] = organizationId.ToString()
|
||||
},
|
||||
OffSession = true,
|
||||
TrialPeriodDays = plan.TrialPeriodDays
|
||||
TrialPeriodDays = subscriptionSetup.SkipTrial ? 0 : plan.TrialPeriodDays
|
||||
};
|
||||
|
||||
return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
||||
|
@ -108,11 +108,10 @@ public static class FeatureFlagKeys
|
||||
public const string IntegrationPage = "pm-14505-admin-console-integration-page";
|
||||
public const string DeviceApprovalRequestAdminNotifications = "pm-15637-device-approval-request-admin-notifications";
|
||||
public const string LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission";
|
||||
public const string PushSyncOrgKeysOnRevokeRestore = "pm-17168-push-sync-org-keys-on-revoke-restore";
|
||||
|
||||
/* Tools Team */
|
||||
public const string ItemShare = "item-share";
|
||||
public const string GeneratorToolsModernization = "generator-tools-modernization";
|
||||
public const string MemberAccessReport = "ac-2059-member-access-report";
|
||||
public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application";
|
||||
public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications";
|
||||
|
||||
@ -155,7 +154,6 @@ public static class FeatureFlagKeys
|
||||
public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss";
|
||||
public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss";
|
||||
public const string SecurityTasks = "security-tasks";
|
||||
public const string DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship";
|
||||
public const string MacOsNativeCredentialSync = "macos-native-credential-sync";
|
||||
public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form";
|
||||
public const string InlineMenuTotp = "inline-menu-totp";
|
||||
@ -174,7 +172,8 @@ public static class FeatureFlagKeys
|
||||
public const string SingleTapPasskeyCreation = "single-tap-passkey-creation";
|
||||
public const string SingleTapPasskeyAuthentication = "single-tap-passkey-authentication";
|
||||
public const string EnablePMAuthenticatorSync = "enable-pm-bwa-sync";
|
||||
public const string P15179_AddExistingOrgsFromProviderPortal = "PM-15179-add-existing-orgs-from-provider-portal";
|
||||
public const string P15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal";
|
||||
public const string AndroidMutualTls = "mutual-tls";
|
||||
|
||||
public static List<string> GetAllKeys()
|
||||
{
|
||||
|
@ -51,7 +51,6 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="8.0.10" />
|
||||
<PackageReference Include="OneOf" Version="3.0.271" />
|
||||
<PackageReference Include="Quartz" Version="3.13.1" />
|
||||
<PackageReference Include="SendGrid" Version="9.29.3" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
||||
@ -73,6 +72,11 @@
|
||||
<PackageReference Include="RabbitMQ.Client" Version="7.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Label="Pinned transitive dependencies">
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Protobuf Include="Billing\Pricing\Protos\password-manager.proto" GrpcServices="Client" />
|
||||
</ItemGroup>
|
||||
|
@ -130,7 +130,7 @@
|
||||
<table class="header" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td valign="middle" class="aligncenter middle logo" style="padding: 20px 0 10px;" align="center">
|
||||
<img src="https://bitwarden.com/images/logo-horizontal-blue.png" alt="" width="250" height="39" />
|
||||
<img src="https://assets.bitwarden.com/email/v1/logo-horizontal-blue.png" alt="" width="250" height="39" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@ -148,13 +148,13 @@
|
||||
<td class="aligncenter social-icons" align="center" style="margin: 0; padding: 15px 0 0 0;" valign="top">
|
||||
<table cellpadding="0" cellspacing="0" style="margin: 0 auto;">
|
||||
<tr>
|
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://twitter.com/bitwarden" target="_blank"><img src="https://bitwarden.com/images/mail-twitter.png" alt="Twitter" width="30" height="30" /></a></td>
|
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://www.reddit.com/r/Bitwarden/" target="_blank"><img src="https://bitwarden.com/images/mail-reddit.png" alt="Reddit" width="30" height="30" /></a></td>
|
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://community.bitwarden.com/" target="_blank"><img src="https://bitwarden.com/images/mail-discourse.png" alt="CommunityForums" width="30" height="30" /></a></td>
|
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://github.com/bitwarden" target="_blank"><img src="https://bitwarden.com/images/mail-github.png" alt="GitHub" width="30" height="30" /></a></td>
|
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw" target="_blank"><img src="https://bitwarden.com/images/mail-youtube.png" alt="Youtube" width="30" height="30" /></a></td>
|
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://www.linkedin.com/company/bitwarden1/" target="_blank"><img src="https://bitwarden.com/images/mail-linkedin.png" alt="LinkedIn" width="30" height="30" /></a></td>
|
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://www.facebook.com/bitwarden/" target="_blank"><img src="https://bitwarden.com/images/mail-facebook.png" alt="Facebook" width="30" height="30" /></a></td>
|
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://twitter.com/bitwarden" target="_blank"><img src="https://assets.bitwarden.com/email/v1/mail-twitter.png" alt="Twitter" width="30" height="30" /></a></td>
|
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://www.reddit.com/r/Bitwarden/" target="_blank"><img src="https://assets.bitwarden.com/email/v1/mail-reddit.png" alt="Reddit" width="30" height="30" /></a></td>
|
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://community.bitwarden.com/" target="_blank"><img src="https://assets.bitwarden.com/email/v1/mail-discourse.png" alt="CommunityForums" width="30" height="30" /></a></td>
|
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://github.com/bitwarden" target="_blank"><img src="https://assets.bitwarden.com/email/v1/mail-github.png" alt="GitHub" width="30" height="30" /></a></td>
|
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw" target="_blank"><img src="https://assets.bitwarden.com/email/v1/mail-youtube.png" alt="Youtube" width="30" height="30" /></a></td>
|
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://www.linkedin.com/company/bitwarden1/" target="_blank"><img src="https://assets.bitwarden.com/email/v1/mail-linkedin.png" alt="LinkedIn" width="30" height="30" /></a></td>
|
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://www.facebook.com/bitwarden/" target="_blank"><img src="https://assets.bitwarden.com/email/v1/mail-facebook.png" alt="Facebook" width="30" height="30" /></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
|
@ -107,10 +107,10 @@
|
||||
|
||||
.footer-text {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.templateColumnContainer{
|
||||
@ -159,7 +159,7 @@
|
||||
<table class="header" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td valign="middle" class="aligncenter middle logo" style="padding: 20px 0 10px;" align="center">
|
||||
<img src="https://bitwarden.com/images/logo-horizontal-blue.png" alt="" width="250" height="39" />
|
||||
<img src="https://assets.bitwarden.com/email/v1/logo-horizontal-blue.png" alt="" width="250" height="39" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@ -177,13 +177,13 @@
|
||||
<td class="aligncenter social-icons" align="center" style="margin: 0; padding: 15px 0 0 0;" valign="top">
|
||||
<table cellpadding="0" cellspacing="0" style="margin: 0 auto;">
|
||||
<tr>
|
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://twitter.com/bitwarden" target="_blank"><img src="https://bitwarden.com/images/mail-twitter.png" alt="Twitter" width="30" height="30" /></a></td>
|
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://www.reddit.com/r/Bitwarden/" target="_blank"><img src="https://bitwarden.com/images/mail-reddit.png" alt="Reddit" width="30" height="30" /></a></td>
|
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://community.bitwarden.com/" target="_blank"><img src="https://bitwarden.com/images/mail-discourse.png" alt="CommunityForums" width="30" height="30" /></a></td>
|
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://github.com/bitwarden" target="_blank"><img src="https://bitwarden.com/images/mail-github.png" alt="GitHub" width="30" height="30" /></a></td>
|
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw" target="_blank"><img src="https://bitwarden.com/images/mail-youtube.png" alt="Youtube" width="30" height="30" /></a></td>
|
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://www.linkedin.com/company/bitwarden1/" target="_blank"><img src="https://bitwarden.com/images/mail-linkedin.png" alt="LinkedIn" width="30" height="30" /></a></td>
|
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://www.facebook.com/bitwarden/" target="_blank"><img src="https://bitwarden.com/images/mail-facebook.png" alt="Facebook" width="30" height="30" /></a></td>
|
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://twitter.com/bitwarden" target="_blank"><img src="https://assets.bitwarden.com/email/v1/mail-twitter.png" alt="Twitter" width="30" height="30" /></a></td>
|
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://www.reddit.com/r/Bitwarden/" target="_blank"><img src="https://assets.bitwarden.com/email/v1/mail-reddit.png" alt="Reddit" width="30" height="30" /></a></td>
|
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://community.bitwarden.com/" target="_blank"><img src="https://assets.bitwarden.com/email/v1/mail-discourse.png" alt="CommunityForums" width="30" height="30" /></a></td>
|
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://github.com/bitwarden" target="_blank"><img src="https://assets.bitwarden.com/email/v1/mail-github.png" alt="GitHub" width="30" height="30" /></a></td>
|
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw" target="_blank"><img src="https://assets.bitwarden.com/email/v1/mail-youtube.png" alt="Youtube" width="30" height="30" /></a></td>
|
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://www.linkedin.com/company/bitwarden1/" target="_blank"><img src="https://assets.bitwarden.com/email/v1/mail-linkedin.png" alt="LinkedIn" width="30" height="30" /></a></td>
|
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://www.facebook.com/bitwarden/" target="_blank"><img src="https://assets.bitwarden.com/email/v1/mail-facebook.png" alt="Facebook" width="30" height="30" /></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
|
@ -70,6 +70,7 @@ public class GlobalSettings : IGlobalSettings
|
||||
public virtual YubicoSettings Yubico { get; set; } = new YubicoSettings();
|
||||
public virtual DuoSettings Duo { get; set; } = new DuoSettings();
|
||||
public virtual BraintreeSettings Braintree { get; set; } = new BraintreeSettings();
|
||||
public virtual ImportCiphersLimitationSettings ImportCiphersLimitation { get; set; } = new ImportCiphersLimitationSettings();
|
||||
public virtual BitPaySettings BitPay { get; set; } = new BitPaySettings();
|
||||
public virtual AmazonSettings Amazon { get; set; } = new AmazonSettings();
|
||||
public virtual ServiceBusSettings ServiceBus { get; set; } = new ServiceBusSettings();
|
||||
@ -521,6 +522,13 @@ public class GlobalSettings : IGlobalSettings
|
||||
public string PrivateKey { get; set; }
|
||||
}
|
||||
|
||||
public class ImportCiphersLimitationSettings
|
||||
{
|
||||
public int CiphersLimit { get; set; }
|
||||
public int CollectionRelationshipsLimit { get; set; }
|
||||
public int CollectionsLimit { get; set; }
|
||||
}
|
||||
|
||||
public class BitPaySettings
|
||||
{
|
||||
public bool Production { get; set; }
|
||||
|
@ -1,16 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization.Infrastructure;
|
||||
|
||||
namespace Bit.Core.Vault.Authorization;
|
||||
|
||||
public class SecurityTaskOperationRequirement : OperationAuthorizationRequirement
|
||||
{
|
||||
public SecurityTaskOperationRequirement(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
|
||||
public static class SecurityTaskOperations
|
||||
{
|
||||
public static readonly SecurityTaskOperationRequirement Update = new(nameof(Update));
|
||||
}
|
65
src/Core/Vault/Commands/CreateManyTasksCommand.cs
Normal file
65
src/Core/Vault/Commands/CreateManyTasksCommand.cs
Normal file
@ -0,0 +1,65 @@
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Vault.Authorization.SecurityTasks;
|
||||
using Bit.Core.Vault.Commands.Interfaces;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Core.Vault.Enums;
|
||||
using Bit.Core.Vault.Models.Api;
|
||||
using Bit.Core.Vault.Repositories;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Bit.Core.Vault.Commands;
|
||||
|
||||
public class CreateManyTasksCommand : ICreateManyTasksCommand
|
||||
{
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ISecurityTaskRepository _securityTaskRepository;
|
||||
|
||||
public CreateManyTasksCommand(
|
||||
ISecurityTaskRepository securityTaskRepository,
|
||||
IAuthorizationService authorizationService,
|
||||
ICurrentContext currentContext)
|
||||
{
|
||||
_securityTaskRepository = securityTaskRepository;
|
||||
_authorizationService = authorizationService;
|
||||
_currentContext = currentContext;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ICollection<SecurityTask>> CreateAsync(Guid organizationId,
|
||||
IEnumerable<SecurityTaskCreateRequest> tasks)
|
||||
{
|
||||
if (!_currentContext.UserId.HasValue)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var tasksList = tasks?.ToList();
|
||||
|
||||
if (tasksList is null || tasksList.Count == 0)
|
||||
{
|
||||
throw new BadRequestException("No tasks provided.");
|
||||
}
|
||||
|
||||
var securityTasks = tasksList.Select(t => new SecurityTask
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
CipherId = t.CipherId,
|
||||
Type = t.Type,
|
||||
Status = SecurityTaskStatus.Pending
|
||||
}).ToList();
|
||||
|
||||
// Verify authorization for each task
|
||||
foreach (var task in securityTasks)
|
||||
{
|
||||
await _authorizationService.AuthorizeOrThrowAsync(
|
||||
_currentContext.HttpContext.User,
|
||||
task,
|
||||
SecurityTaskOperations.Create);
|
||||
}
|
||||
|
||||
return await _securityTaskRepository.CreateManyAsync(securityTasks);
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Core.Vault.Models.Api;
|
||||
|
||||
namespace Bit.Core.Vault.Commands.Interfaces;
|
||||
|
||||
public interface ICreateManyTasksCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates multiple security tasks for an organization.
|
||||
/// Each task must be authorized and the user must have the Create permission
|
||||
/// and associated ciphers must belong to the organization.
|
||||
/// </summary>
|
||||
/// <param name="organizationId">The </param>
|
||||
/// <param name="tasks"></param>
|
||||
/// <returns>Collection of created security tasks</returns>
|
||||
Task<ICollection<SecurityTask>> CreateAsync(Guid organizationId, IEnumerable<SecurityTaskCreateRequest> tasks);
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Vault.Authorization;
|
||||
using Bit.Core.Vault.Authorization.SecurityTasks;
|
||||
using Bit.Core.Vault.Commands.Interfaces;
|
||||
using Bit.Core.Vault.Enums;
|
||||
using Bit.Core.Vault.Repositories;
|
||||
|
9
src/Core/Vault/Models/Api/SecurityTaskCreateRequest.cs
Normal file
9
src/Core/Vault/Models/Api/SecurityTaskCreateRequest.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using Bit.Core.Vault.Enums;
|
||||
|
||||
namespace Bit.Core.Vault.Models.Api;
|
||||
|
||||
public class SecurityTaskCreateRequest
|
||||
{
|
||||
public SecurityTaskType Type { get; set; }
|
||||
public Guid? CipherId { get; set; }
|
||||
}
|
@ -21,4 +21,11 @@ public interface ISecurityTaskRepository : IRepository<SecurityTask, Guid>
|
||||
/// <param name="status">Optional filter for task status. If not provided, returns tasks of all statuses</param>
|
||||
/// <returns></returns>
|
||||
Task<ICollection<SecurityTask>> GetManyByOrganizationIdStatusAsync(Guid organizationId, SecurityTaskStatus? status = null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates bulk security tasks for an organization.
|
||||
/// </summary>
|
||||
/// <param name="tasks">Collection of tasks to create</param>
|
||||
/// <returns>Collection of created security tasks</returns>
|
||||
Task<ICollection<SecurityTask>> CreateManyAsync(IEnumerable<SecurityTask> tasks);
|
||||
}
|
||||
|
@ -21,5 +21,6 @@ public static class VaultServiceCollectionExtensions
|
||||
services.AddScoped<IMarkTaskAsCompleteCommand, MarkTaskAsCompletedCommand>();
|
||||
services.AddScoped<IGetCipherPermissionsForUserQuery, GetCipherPermissionsForUserQuery>();
|
||||
services.AddScoped<IGetTasksForOrganizationQuery, GetTasksForOrganizationQuery>();
|
||||
services.AddScoped<ICreateManyTasksCommand, CreateManyTasksCommand>();
|
||||
}
|
||||
}
|
||||
|
@ -567,4 +567,17 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
||||
new { OrganizationUserIds = JsonSerializer.Serialize(organizationUserIds), Status = OrganizationUserStatusType.Revoked },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<OrganizationUserUserDetails>> GetManyDetailsByRoleAsync(Guid organizationId, OrganizationUserType role)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.QueryAsync<OrganizationUserUserDetails>(
|
||||
"[dbo].[OrganizationUser_ReadManyDetailsByRole]",
|
||||
new { OrganizationId = organizationId, Role = role },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
return results.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -81,7 +81,7 @@ public class DataTableBuilder<T>
|
||||
return true;
|
||||
}
|
||||
|
||||
// Value type properties will implicitly box into the object so
|
||||
// Value type properties will implicitly box into the object so
|
||||
// we need to look past the Convert expression
|
||||
// i => (System.Object?)i.Id
|
||||
if (
|
||||
|
@ -98,7 +98,7 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
|
||||
|
||||
return results
|
||||
.GroupBy(c => c.Id)
|
||||
.Select(g => g.OrderByDescending(og => og.Edit).First())
|
||||
.Select(g => g.OrderByDescending(og => og.Edit).ThenByDescending(og => og.ViewPassword).First())
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Data;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Core.Vault.Enums;
|
||||
@ -46,4 +47,29 @@ public class SecurityTaskRepository : Repository<SecurityTask, Guid>, ISecurityT
|
||||
|
||||
return results.ToList();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ICollection<SecurityTask>> CreateManyAsync(IEnumerable<SecurityTask> tasks)
|
||||
{
|
||||
var tasksList = tasks?.ToList();
|
||||
if (tasksList is null || tasksList.Count == 0)
|
||||
{
|
||||
return Array.Empty<SecurityTask>();
|
||||
}
|
||||
|
||||
foreach (var task in tasksList)
|
||||
{
|
||||
task.SetNewId();
|
||||
}
|
||||
|
||||
var tasksJson = JsonSerializer.Serialize(tasksList);
|
||||
|
||||
await using var connection = new SqlConnection(ConnectionString);
|
||||
await connection.ExecuteAsync(
|
||||
$"[{Schema}].[{Table}_CreateMany]",
|
||||
new { SecurityTasksJson = tasksJson },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
return tasksList;
|
||||
}
|
||||
}
|
||||
|
@ -733,4 +733,25 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
||||
|
||||
await dbContext.UserBumpAccountRevisionDateByOrganizationUserIdsAsync(organizationUserIds);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<OrganizationUserUserDetails>> GetManyDetailsByRoleAsync(Guid organizationId, OrganizationUserType role)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var query = from ou in dbContext.OrganizationUsers
|
||||
join u in dbContext.Users
|
||||
on ou.UserId equals u.Id
|
||||
where ou.OrganizationId == organizationId &&
|
||||
ou.Type == role &&
|
||||
ou.Status == OrganizationUserStatusType.Confirmed
|
||||
select new OrganizationUserUserDetails
|
||||
{
|
||||
Id = ou.Id,
|
||||
Email = ou.Email ?? u.Email,
|
||||
Permissions = ou.Permissions
|
||||
};
|
||||
return await query.ToListAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -52,4 +52,28 @@ public class SecurityTaskRepository : Repository<Core.Vault.Entities.SecurityTas
|
||||
|
||||
return await query.OrderByDescending(st => st.CreationDate).ToListAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ICollection<Core.Vault.Entities.SecurityTask>> CreateManyAsync(
|
||||
IEnumerable<Core.Vault.Entities.SecurityTask> tasks)
|
||||
{
|
||||
var tasksList = tasks?.ToList();
|
||||
if (tasksList is null || tasksList.Count == 0)
|
||||
{
|
||||
return Array.Empty<SecurityTask>();
|
||||
}
|
||||
|
||||
foreach (var task in tasksList)
|
||||
{
|
||||
task.SetNewId();
|
||||
}
|
||||
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var entities = Mapper.Map<List<SecurityTask>>(tasksList);
|
||||
await dbContext.AddRangeAsync(entities);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
return tasksList;
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,10 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="8.0.8" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Label="Pinned transitive dependencies">
|
||||
<PackageReference Include="MessagePack" Version="2.5.192" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Core\Core.csproj" />
|
||||
<ProjectReference Include="..\SharedWeb\SharedWeb.csproj" />
|
||||
|
@ -5,12 +5,40 @@ AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT TOP 1
|
||||
*
|
||||
SELECT
|
||||
[Id],
|
||||
[UserId],
|
||||
[OrganizationId],
|
||||
[Type],
|
||||
[Data],
|
||||
[Attachments],
|
||||
[CreationDate],
|
||||
[RevisionDate],
|
||||
[Favorite],
|
||||
[FolderId],
|
||||
[DeletedDate],
|
||||
[Reprompt],
|
||||
[Key],
|
||||
[OrganizationUseTotp],
|
||||
MAX ([Edit]) AS [Edit],
|
||||
MAX ([ViewPassword]) AS [ViewPassword]
|
||||
FROM
|
||||
[dbo].[UserCipherDetails](@UserId)
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
ORDER BY
|
||||
[Edit] DESC
|
||||
END
|
||||
GROUP BY
|
||||
[Id],
|
||||
[UserId],
|
||||
[OrganizationId],
|
||||
[Type],
|
||||
[Data],
|
||||
[Attachments],
|
||||
[CreationDate],
|
||||
[RevisionDate],
|
||||
[Favorite],
|
||||
[FolderId],
|
||||
[DeletedDate],
|
||||
[Reprompt],
|
||||
[Key],
|
||||
[OrganizationUseTotp]
|
||||
END
|
||||
|
@ -0,0 +1,55 @@
|
||||
CREATE PROCEDURE [dbo].[SecurityTask_CreateMany]
|
||||
@SecurityTasksJson NVARCHAR(MAX)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
CREATE TABLE #TempSecurityTasks
|
||||
(
|
||||
[Id] UNIQUEIDENTIFIER,
|
||||
[OrganizationId] UNIQUEIDENTIFIER,
|
||||
[CipherId] UNIQUEIDENTIFIER,
|
||||
[Type] TINYINT,
|
||||
[Status] TINYINT,
|
||||
[CreationDate] DATETIME2(7),
|
||||
[RevisionDate] DATETIME2(7)
|
||||
)
|
||||
|
||||
INSERT INTO #TempSecurityTasks
|
||||
([Id],
|
||||
[OrganizationId],
|
||||
[CipherId],
|
||||
[Type],
|
||||
[Status],
|
||||
[CreationDate],
|
||||
[RevisionDate])
|
||||
SELECT CAST(JSON_VALUE([value], '$.Id') AS UNIQUEIDENTIFIER),
|
||||
CAST(JSON_VALUE([value], '$.OrganizationId') AS UNIQUEIDENTIFIER),
|
||||
CAST(JSON_VALUE([value], '$.CipherId') AS UNIQUEIDENTIFIER),
|
||||
CAST(JSON_VALUE([value], '$.Type') AS TINYINT),
|
||||
CAST(JSON_VALUE([value], '$.Status') AS TINYINT),
|
||||
CAST(JSON_VALUE([value], '$.CreationDate') AS DATETIME2(7)),
|
||||
CAST(JSON_VALUE([value], '$.RevisionDate') AS DATETIME2(7))
|
||||
FROM OPENJSON(@SecurityTasksJson) ST
|
||||
|
||||
INSERT INTO [dbo].[SecurityTask]
|
||||
(
|
||||
[Id],
|
||||
[OrganizationId],
|
||||
[CipherId],
|
||||
[Type],
|
||||
[Status],
|
||||
[CreationDate],
|
||||
[RevisionDate]
|
||||
)
|
||||
SELECT [Id],
|
||||
[OrganizationId],
|
||||
[CipherId],
|
||||
[Type],
|
||||
[Status],
|
||||
[CreationDate],
|
||||
[RevisionDate]
|
||||
FROM #TempSecurityTasks
|
||||
|
||||
DROP TABLE #TempSecurityTasks
|
||||
END
|
@ -14,10 +14,9 @@ BEGIN
|
||||
WHERE
|
||||
[Id] = @CipherId
|
||||
)
|
||||
|
||||
;WITH [AvailableCollectionsCTE] AS(
|
||||
SELECT
|
||||
SELECT
|
||||
C.[Id]
|
||||
INTO #TempAvailableCollections
|
||||
FROM
|
||||
[dbo].[Collection] C
|
||||
INNER JOIN
|
||||
@ -40,38 +39,33 @@ BEGIN
|
||||
CU.[ReadOnly] = 0
|
||||
OR CG.[ReadOnly] = 0
|
||||
)
|
||||
),
|
||||
[CollectionCiphersCTE] AS(
|
||||
SELECT
|
||||
[CollectionId],
|
||||
[CipherId]
|
||||
FROM
|
||||
[dbo].[CollectionCipher]
|
||||
WHERE
|
||||
[CipherId] = @CipherId
|
||||
-- Insert new collection assignments
|
||||
INSERT INTO [dbo].[CollectionCipher] (
|
||||
[CollectionId],
|
||||
[CipherId]
|
||||
)
|
||||
MERGE
|
||||
[CollectionCiphersCTE] AS [Target]
|
||||
USING
|
||||
@CollectionIds AS [Source]
|
||||
ON
|
||||
[Target].[CollectionId] = [Source].[Id]
|
||||
AND [Target].[CipherId] = @CipherId
|
||||
WHEN NOT MATCHED BY TARGET
|
||||
AND [Source].[Id] IN (SELECT [Id] FROM [AvailableCollectionsCTE]) THEN
|
||||
INSERT VALUES
|
||||
(
|
||||
[Source].[Id],
|
||||
@CipherId
|
||||
)
|
||||
WHEN NOT MATCHED BY SOURCE
|
||||
AND [Target].[CipherId] = @CipherId
|
||||
AND [Target].[CollectionId] IN (SELECT [Id] FROM [AvailableCollectionsCTE]) THEN
|
||||
DELETE
|
||||
;
|
||||
SELECT
|
||||
[Id],
|
||||
@CipherId
|
||||
FROM @CollectionIds
|
||||
WHERE [Id] IN (SELECT [Id] FROM [#TempAvailableCollections])
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM [dbo].[CollectionCipher]
|
||||
WHERE [CollectionId] = [@CollectionIds].[Id]
|
||||
AND [CipherId] = @CipherId
|
||||
);
|
||||
|
||||
-- Delete removed collection assignments
|
||||
DELETE CC
|
||||
FROM [dbo].[CollectionCipher] CC
|
||||
WHERE CC.[CipherId] = @CipherId
|
||||
AND CC.[CollectionId] IN (SELECT [Id] FROM [#TempAvailableCollections])
|
||||
AND CC.[CollectionId] NOT IN (SELECT [Id] FROM @CollectionIds);
|
||||
|
||||
IF @OrgId IS NOT NULL
|
||||
BEGIN
|
||||
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrgId
|
||||
END
|
||||
DROP TABLE #TempAvailableCollections;
|
||||
END
|
||||
|
@ -0,0 +1,16 @@
|
||||
CREATE PROCEDURE [dbo].[OrganizationUser_ReadManyDetailsByRole]
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@Role TINYINT
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
[dbo].[OrganizationUserUserDetailsView]
|
||||
WHERE
|
||||
OrganizationId = @OrganizationId
|
||||
AND Status = 2 -- 2 = Confirmed
|
||||
AND [Type] = @Role
|
||||
END
|
@ -1,6 +1,5 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
|
@ -419,22 +419,32 @@ public class AccountsControllerTests : IDisposable
|
||||
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(true, false)] // User has PublicKey and PrivateKey, and Keys in request are NOT null
|
||||
[BitAutoData(true, true)] // User has PublicKey and PrivateKey, and Keys in request are null
|
||||
[BitAutoData(false, false)] // User has neither PublicKey nor PrivateKey, and Keys in request are NOT null
|
||||
[BitAutoData(false, true)] // User has neither PublicKey nor PrivateKey, and Keys in request are null
|
||||
[BitAutoData(true, "existingPrivateKey", "existingPublicKey", true)] // allow providing existing keys in the request
|
||||
[BitAutoData(true, null, null, true)] // allow not setting the public key when the user already has a key
|
||||
[BitAutoData(false, "newPrivateKey", "newPublicKey", true)] // allow setting new keys when the user has no keys
|
||||
[BitAutoData(false, null, null, true)] // allow not setting the public key when the user has no keys
|
||||
// do not allow single key
|
||||
[BitAutoData(false, "existingPrivateKey", null, false)]
|
||||
[BitAutoData(false, null, "existingPublicKey", false)]
|
||||
[BitAutoData(false, "newPrivateKey", null, false)]
|
||||
[BitAutoData(false, null, "newPublicKey", false)]
|
||||
[BitAutoData(true, "existingPrivateKey", null, false)]
|
||||
[BitAutoData(true, null, "existingPublicKey", false)]
|
||||
[BitAutoData(true, "newPrivateKey", null, false)]
|
||||
[BitAutoData(true, null, "newPublicKey", false)]
|
||||
// reject overwriting existing keys
|
||||
[BitAutoData(true, "newPrivateKey", "newPublicKey", false)]
|
||||
public async Task PostSetPasswordAsync_WhenUserExistsAndSettingPasswordSucceeds_ShouldHandleKeysCorrectlyAndReturn(
|
||||
bool hasExistingKeys,
|
||||
bool shouldSetKeysToNull,
|
||||
User user,
|
||||
SetPasswordRequestModel setPasswordRequestModel)
|
||||
bool hasExistingKeys,
|
||||
string requestPrivateKey,
|
||||
string requestPublicKey,
|
||||
bool shouldSucceed,
|
||||
User user,
|
||||
SetPasswordRequestModel setPasswordRequestModel)
|
||||
{
|
||||
// Arrange
|
||||
const string existingPublicKey = "existingPublicKey";
|
||||
const string existingEncryptedPrivateKey = "existingEncryptedPrivateKey";
|
||||
|
||||
const string newPublicKey = "newPublicKey";
|
||||
const string newEncryptedPrivateKey = "newEncryptedPrivateKey";
|
||||
const string existingEncryptedPrivateKey = "existingPrivateKey";
|
||||
|
||||
if (hasExistingKeys)
|
||||
{
|
||||
@ -447,16 +457,16 @@ public class AccountsControllerTests : IDisposable
|
||||
user.PrivateKey = null;
|
||||
}
|
||||
|
||||
if (shouldSetKeysToNull)
|
||||
if (requestPrivateKey == null && requestPublicKey == null)
|
||||
{
|
||||
setPasswordRequestModel.Keys = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
setPasswordRequestModel.Keys = new KeysRequestModel()
|
||||
setPasswordRequestModel.Keys = new KeysRequestModel
|
||||
{
|
||||
PublicKey = newPublicKey,
|
||||
EncryptedPrivateKey = newEncryptedPrivateKey
|
||||
EncryptedPrivateKey = requestPrivateKey,
|
||||
PublicKey = requestPublicKey
|
||||
};
|
||||
}
|
||||
|
||||
@ -469,44 +479,66 @@ public class AccountsControllerTests : IDisposable
|
||||
.Returns(Task.FromResult(IdentityResult.Success));
|
||||
|
||||
// Act
|
||||
await _sut.PostSetPasswordAsync(setPasswordRequestModel);
|
||||
|
||||
// Assert
|
||||
await _setInitialMasterPasswordCommand.Received(1)
|
||||
.SetInitialMasterPasswordAsync(
|
||||
Arg.Is<User>(u => u == user),
|
||||
Arg.Is<string>(s => s == setPasswordRequestModel.MasterPasswordHash),
|
||||
Arg.Is<string>(s => s == setPasswordRequestModel.Key),
|
||||
Arg.Is<string>(s => s == setPasswordRequestModel.OrgIdentifier));
|
||||
|
||||
// Additional Assertions for User object modifications
|
||||
Assert.Equal(setPasswordRequestModel.MasterPasswordHint, user.MasterPasswordHint);
|
||||
Assert.Equal(setPasswordRequestModel.Kdf, user.Kdf);
|
||||
Assert.Equal(setPasswordRequestModel.KdfIterations, user.KdfIterations);
|
||||
Assert.Equal(setPasswordRequestModel.KdfMemory, user.KdfMemory);
|
||||
Assert.Equal(setPasswordRequestModel.KdfParallelism, user.KdfParallelism);
|
||||
Assert.Equal(setPasswordRequestModel.Key, user.Key);
|
||||
|
||||
if (hasExistingKeys)
|
||||
if (shouldSucceed)
|
||||
{
|
||||
// User Keys should not be modified
|
||||
Assert.Equal(existingPublicKey, user.PublicKey);
|
||||
Assert.Equal(existingEncryptedPrivateKey, user.PrivateKey);
|
||||
}
|
||||
else if (!shouldSetKeysToNull)
|
||||
{
|
||||
// User had no keys so they should be set to the request model's keys
|
||||
Assert.Equal(setPasswordRequestModel.Keys.PublicKey, user.PublicKey);
|
||||
Assert.Equal(setPasswordRequestModel.Keys.EncryptedPrivateKey, user.PrivateKey);
|
||||
await _sut.PostSetPasswordAsync(setPasswordRequestModel);
|
||||
// Assert
|
||||
await _setInitialMasterPasswordCommand.Received(1)
|
||||
.SetInitialMasterPasswordAsync(
|
||||
Arg.Is<User>(u => u == user),
|
||||
Arg.Is<string>(s => s == setPasswordRequestModel.MasterPasswordHash),
|
||||
Arg.Is<string>(s => s == setPasswordRequestModel.Key),
|
||||
Arg.Is<string>(s => s == setPasswordRequestModel.OrgIdentifier));
|
||||
|
||||
// Additional Assertions for User object modifications
|
||||
Assert.Equal(setPasswordRequestModel.MasterPasswordHint, user.MasterPasswordHint);
|
||||
Assert.Equal(setPasswordRequestModel.Kdf, user.Kdf);
|
||||
Assert.Equal(setPasswordRequestModel.KdfIterations, user.KdfIterations);
|
||||
Assert.Equal(setPasswordRequestModel.KdfMemory, user.KdfMemory);
|
||||
Assert.Equal(setPasswordRequestModel.KdfParallelism, user.KdfParallelism);
|
||||
Assert.Equal(setPasswordRequestModel.Key, user.Key);
|
||||
}
|
||||
else
|
||||
{
|
||||
// User had no keys and the request model's keys were null, so they should be set to null
|
||||
Assert.Null(user.PublicKey);
|
||||
Assert.Null(user.PrivateKey);
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostSetPasswordAsync(setPasswordRequestModel));
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostSetPasswordAsync_WhenUserExistsAndHasKeysAndKeysAreUpdated_ShouldThrowAsync(
|
||||
User user,
|
||||
SetPasswordRequestModel setPasswordRequestModel)
|
||||
{
|
||||
// Arrange
|
||||
const string existingPublicKey = "existingPublicKey";
|
||||
const string existingEncryptedPrivateKey = "existingEncryptedPrivateKey";
|
||||
|
||||
const string newPublicKey = "newPublicKey";
|
||||
const string newEncryptedPrivateKey = "newEncryptedPrivateKey";
|
||||
|
||||
user.PublicKey = existingPublicKey;
|
||||
user.PrivateKey = existingEncryptedPrivateKey;
|
||||
|
||||
setPasswordRequestModel.Keys = new KeysRequestModel()
|
||||
{
|
||||
PublicKey = newPublicKey,
|
||||
EncryptedPrivateKey = newEncryptedPrivateKey
|
||||
};
|
||||
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
|
||||
_setInitialMasterPasswordCommand.SetInitialMasterPasswordAsync(
|
||||
user,
|
||||
setPasswordRequestModel.MasterPasswordHash,
|
||||
setPasswordRequestModel.Key,
|
||||
setPasswordRequestModel.OrgIdentifier)
|
||||
.Returns(Task.FromResult(IdentityResult.Success));
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostSetPasswordAsync(setPasswordRequestModel));
|
||||
}
|
||||
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostSetPasswordAsync_WhenUserDoesNotExist_ShouldThrowUnauthorizedAccessException(
|
||||
@ -525,6 +557,7 @@ public class AccountsControllerTests : IDisposable
|
||||
User user,
|
||||
SetPasswordRequestModel model)
|
||||
{
|
||||
model.Keys = null;
|
||||
// Arrange
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
|
||||
_setInitialMasterPasswordCommand.SetInitialMasterPasswordAsync(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
|
||||
|
@ -127,6 +127,7 @@ public class CiphersControllerTests
|
||||
UserId = userId,
|
||||
OrganizationId = Guid.NewGuid(),
|
||||
Type = CipherType.Login,
|
||||
ViewPassword = true,
|
||||
Data = @"
|
||||
{
|
||||
""Uris"": [
|
||||
|
@ -6,7 +6,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Divergic.Logging.Xunit" Version="4.3.0" />
|
||||
<PackageReference Include="FluentAssertions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
||||
<PackageReference Include="RichardSzalay.MockHttp" Version="7.0.0" />
|
||||
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Billing.Controllers;
|
||||
using System.Text.Json;
|
||||
using Bit.Billing.Controllers;
|
||||
using Bit.Billing.Models;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Entities;
|
||||
@ -70,6 +71,159 @@ public class FreshdeskControllerTests
|
||||
_ = mockHttpMessageHandler.Received(1).Send(Arg.Is<HttpRequestMessage>(m => m.Method == HttpMethod.Post && m.RequestUri.ToString().EndsWith($"{model.TicketId}/notes")), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData((string)null, null)]
|
||||
[BitAutoData((string)null)]
|
||||
[BitAutoData(WebhookKey, null)]
|
||||
public async Task PostWebhookOnyxAi_InvalidWebhookKey_results_in_BadRequest(
|
||||
string freshdeskWebhookKey, FreshdeskWebhookModel model,
|
||||
BillingSettings billingSettings, SutProvider<FreshdeskController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IOptions<BillingSettings>>()
|
||||
.Value.FreshDesk.WebhookKey.Returns(billingSettings.FreshDesk.WebhookKey);
|
||||
|
||||
var response = await sutProvider.Sut.PostWebhookOnyxAi(freshdeskWebhookKey, model);
|
||||
|
||||
var statusCodeResult = Assert.IsAssignableFrom<StatusCodeResult>(response);
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, statusCodeResult.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(WebhookKey)]
|
||||
public async Task PostWebhookOnyxAi_invalid_ticketid_results_in_BadRequest(
|
||||
string freshdeskWebhookKey, FreshdeskWebhookModel model, SutProvider<FreshdeskController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IOptions<BillingSettings>>()
|
||||
.Value.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey);
|
||||
|
||||
var mockHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
|
||||
var mockResponse = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest);
|
||||
mockHttpMessageHandler.Send(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>())
|
||||
.Returns(mockResponse);
|
||||
var httpClient = new HttpClient(mockHttpMessageHandler);
|
||||
|
||||
sutProvider.GetDependency<IHttpClientFactory>().CreateClient("FreshdeskApi").Returns(httpClient);
|
||||
|
||||
var response = await sutProvider.Sut.PostWebhookOnyxAi(freshdeskWebhookKey, model);
|
||||
|
||||
var result = Assert.IsAssignableFrom<BadRequestObjectResult>(response);
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(WebhookKey)]
|
||||
public async Task PostWebhookOnyxAi_invalid_freshdesk_response_results_in_BadRequest(
|
||||
string freshdeskWebhookKey, FreshdeskWebhookModel model,
|
||||
SutProvider<FreshdeskController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IOptions<BillingSettings>>()
|
||||
.Value.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey);
|
||||
|
||||
var mockHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
|
||||
var mockResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent("non json content. expect json deserializer to throw error")
|
||||
};
|
||||
mockHttpMessageHandler.Send(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>())
|
||||
.Returns(mockResponse);
|
||||
var httpClient = new HttpClient(mockHttpMessageHandler);
|
||||
|
||||
sutProvider.GetDependency<IHttpClientFactory>().CreateClient("FreshdeskApi").Returns(httpClient);
|
||||
|
||||
var response = await sutProvider.Sut.PostWebhookOnyxAi(freshdeskWebhookKey, model);
|
||||
|
||||
var result = Assert.IsAssignableFrom<BadRequestObjectResult>(response);
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(WebhookKey)]
|
||||
public async Task PostWebhookOnyxAi_invalid_onyx_response_results_in_BadRequest(
|
||||
string freshdeskWebhookKey, FreshdeskWebhookModel model,
|
||||
FreshdeskViewTicketModel freshdeskTicketInfo, SutProvider<FreshdeskController> sutProvider)
|
||||
{
|
||||
var billingSettings = sutProvider.GetDependency<IOptions<BillingSettings>>().Value;
|
||||
billingSettings.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey);
|
||||
billingSettings.Onyx.BaseUrl.Returns("http://simulate-onyx-api.com/api");
|
||||
|
||||
// mocking freshdesk Api request for ticket info
|
||||
var mockFreshdeskHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
|
||||
var mockFreshdeskResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(JsonSerializer.Serialize(freshdeskTicketInfo))
|
||||
};
|
||||
mockFreshdeskHttpMessageHandler.Send(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>())
|
||||
.Returns(mockFreshdeskResponse);
|
||||
var freshdeskHttpClient = new HttpClient(mockFreshdeskHttpMessageHandler);
|
||||
|
||||
// mocking Onyx api response given a ticket description
|
||||
var mockOnyxHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
|
||||
var mockOnyxResponse = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest);
|
||||
mockOnyxHttpMessageHandler.Send(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>())
|
||||
.Returns(mockOnyxResponse);
|
||||
var onyxHttpClient = new HttpClient(mockOnyxHttpMessageHandler);
|
||||
|
||||
sutProvider.GetDependency<IHttpClientFactory>().CreateClient("FreshdeskApi").Returns(freshdeskHttpClient);
|
||||
sutProvider.GetDependency<IHttpClientFactory>().CreateClient("OnyxApi").Returns(onyxHttpClient);
|
||||
|
||||
var response = await sutProvider.Sut.PostWebhookOnyxAi(freshdeskWebhookKey, model);
|
||||
|
||||
var result = Assert.IsAssignableFrom<BadRequestObjectResult>(response);
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(WebhookKey)]
|
||||
public async Task PostWebhookOnyxAi_success(
|
||||
string freshdeskWebhookKey, FreshdeskWebhookModel model,
|
||||
FreshdeskViewTicketModel freshdeskTicketInfo,
|
||||
OnyxAnswerWithCitationResponseModel onyxResponse,
|
||||
SutProvider<FreshdeskController> sutProvider)
|
||||
{
|
||||
var billingSettings = sutProvider.GetDependency<IOptions<BillingSettings>>().Value;
|
||||
billingSettings.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey);
|
||||
billingSettings.Onyx.BaseUrl.Returns("http://simulate-onyx-api.com/api");
|
||||
|
||||
// mocking freshdesk Api request for ticket info (GET)
|
||||
var mockFreshdeskHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
|
||||
var mockFreshdeskResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(JsonSerializer.Serialize(freshdeskTicketInfo))
|
||||
};
|
||||
mockFreshdeskHttpMessageHandler.Send(
|
||||
Arg.Is<HttpRequestMessage>(_ => _.Method == HttpMethod.Get),
|
||||
Arg.Any<CancellationToken>())
|
||||
.Returns(mockFreshdeskResponse);
|
||||
|
||||
// mocking freshdesk api add note request (POST)
|
||||
var mockFreshdeskAddNoteResponse = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest);
|
||||
mockFreshdeskHttpMessageHandler.Send(
|
||||
Arg.Is<HttpRequestMessage>(_ => _.Method == HttpMethod.Post),
|
||||
Arg.Any<CancellationToken>())
|
||||
.Returns(mockFreshdeskAddNoteResponse);
|
||||
var freshdeskHttpClient = new HttpClient(mockFreshdeskHttpMessageHandler);
|
||||
|
||||
|
||||
// mocking Onyx api response given a ticket description
|
||||
var mockOnyxHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
|
||||
onyxResponse.ErrorMsg = string.Empty;
|
||||
var mockOnyxResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(JsonSerializer.Serialize(onyxResponse))
|
||||
};
|
||||
mockOnyxHttpMessageHandler.Send(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>())
|
||||
.Returns(mockOnyxResponse);
|
||||
var onyxHttpClient = new HttpClient(mockOnyxHttpMessageHandler);
|
||||
|
||||
sutProvider.GetDependency<IHttpClientFactory>().CreateClient("FreshdeskApi").Returns(freshdeskHttpClient);
|
||||
sutProvider.GetDependency<IHttpClientFactory>().CreateClient("OnyxApi").Returns(onyxHttpClient);
|
||||
|
||||
var response = await sutProvider.Sut.PostWebhookOnyxAi(freshdeskWebhookKey, model);
|
||||
|
||||
var result = Assert.IsAssignableFrom<OkResult>(response);
|
||||
Assert.Equal(StatusCodes.Status200OK, result.StatusCode);
|
||||
}
|
||||
|
||||
public class MockHttpMessageHandler : HttpMessageHandler
|
||||
{
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
|
@ -8,7 +8,6 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Divergic.Logging.Xunit;
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
@ -577,14 +576,14 @@ public class PayPalControllerTests
|
||||
{
|
||||
var statusCodeActionResult = (IStatusCodeActionResult)result;
|
||||
|
||||
statusCodeActionResult.StatusCode.Should().Be(statusCode);
|
||||
Assert.Equal(statusCode, statusCodeActionResult.StatusCode);
|
||||
}
|
||||
|
||||
private static void Logged(ICacheLogger logger, LogLevel logLevel, string message)
|
||||
{
|
||||
logger.Last.Should().NotBeNull();
|
||||
logger.Last!.LogLevel.Should().Be(logLevel);
|
||||
logger.Last!.Message.Should().Be(message);
|
||||
Assert.NotNull(logger.Last);
|
||||
Assert.Equal(logLevel, logger.Last!.LogLevel);
|
||||
Assert.Equal(message, logger.Last!.Message);
|
||||
}
|
||||
|
||||
private static void LoggedError(ICacheLogger logger, string message)
|
||||
|
@ -2,7 +2,6 @@
|
||||
using Bit.Billing.Services.Implementations;
|
||||
using Bit.Billing.Test.Utilities;
|
||||
using Bit.Core.Settings;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Stripe;
|
||||
@ -36,10 +35,8 @@ public class StripeEventServiceTests
|
||||
var function = async () => await _stripeEventService.GetCharge(stripeEvent);
|
||||
|
||||
// Assert
|
||||
await function
|
||||
.Should()
|
||||
.ThrowAsync<Exception>()
|
||||
.WithMessage($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Charge)}'");
|
||||
var exception = await Assert.ThrowsAsync<Exception>(function);
|
||||
Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Charge)}'", exception.Message);
|
||||
|
||||
await _stripeFacade.DidNotReceiveWithAnyArgs().GetCharge(
|
||||
Arg.Any<string>(),
|
||||
@ -58,7 +55,7 @@ public class StripeEventServiceTests
|
||||
var charge = await _stripeEventService.GetCharge(stripeEvent);
|
||||
|
||||
// Assert
|
||||
charge.Should().BeEquivalentTo(stripeEvent.Data.Object as Charge);
|
||||
Assert.Equivalent(stripeEvent.Data.Object as Charge, charge, true);
|
||||
|
||||
await _stripeFacade.DidNotReceiveWithAnyArgs().GetCharge(
|
||||
Arg.Any<string>(),
|
||||
@ -88,8 +85,8 @@ public class StripeEventServiceTests
|
||||
var charge = await _stripeEventService.GetCharge(stripeEvent, true, expand);
|
||||
|
||||
// Assert
|
||||
charge.Should().Be(apiCharge);
|
||||
charge.Should().NotBeSameAs(eventCharge);
|
||||
Assert.Equal(apiCharge, charge);
|
||||
Assert.NotSame(eventCharge, charge);
|
||||
|
||||
await _stripeFacade.Received().GetCharge(
|
||||
apiCharge.Id,
|
||||
@ -110,10 +107,8 @@ public class StripeEventServiceTests
|
||||
var function = async () => await _stripeEventService.GetCustomer(stripeEvent);
|
||||
|
||||
// Assert
|
||||
await function
|
||||
.Should()
|
||||
.ThrowAsync<Exception>()
|
||||
.WithMessage($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Customer)}'");
|
||||
var exception = await Assert.ThrowsAsync<Exception>(function);
|
||||
Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Customer)}'", exception.Message);
|
||||
|
||||
await _stripeFacade.DidNotReceiveWithAnyArgs().GetCustomer(
|
||||
Arg.Any<string>(),
|
||||
@ -132,7 +127,7 @@ public class StripeEventServiceTests
|
||||
var customer = await _stripeEventService.GetCustomer(stripeEvent);
|
||||
|
||||
// Assert
|
||||
customer.Should().BeEquivalentTo(stripeEvent.Data.Object as Customer);
|
||||
Assert.Equivalent(stripeEvent.Data.Object as Customer, customer, true);
|
||||
|
||||
await _stripeFacade.DidNotReceiveWithAnyArgs().GetCustomer(
|
||||
Arg.Any<string>(),
|
||||
@ -162,8 +157,8 @@ public class StripeEventServiceTests
|
||||
var customer = await _stripeEventService.GetCustomer(stripeEvent, true, expand);
|
||||
|
||||
// Assert
|
||||
customer.Should().Be(apiCustomer);
|
||||
customer.Should().NotBeSameAs(eventCustomer);
|
||||
Assert.Equal(apiCustomer, customer);
|
||||
Assert.NotSame(eventCustomer, customer);
|
||||
|
||||
await _stripeFacade.Received().GetCustomer(
|
||||
apiCustomer.Id,
|
||||
@ -184,10 +179,8 @@ public class StripeEventServiceTests
|
||||
var function = async () => await _stripeEventService.GetInvoice(stripeEvent);
|
||||
|
||||
// Assert
|
||||
await function
|
||||
.Should()
|
||||
.ThrowAsync<Exception>()
|
||||
.WithMessage($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Invoice)}'");
|
||||
var exception = await Assert.ThrowsAsync<Exception>(function);
|
||||
Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Invoice)}'", exception.Message);
|
||||
|
||||
await _stripeFacade.DidNotReceiveWithAnyArgs().GetInvoice(
|
||||
Arg.Any<string>(),
|
||||
@ -206,7 +199,7 @@ public class StripeEventServiceTests
|
||||
var invoice = await _stripeEventService.GetInvoice(stripeEvent);
|
||||
|
||||
// Assert
|
||||
invoice.Should().BeEquivalentTo(stripeEvent.Data.Object as Invoice);
|
||||
Assert.Equivalent(stripeEvent.Data.Object as Invoice, invoice, true);
|
||||
|
||||
await _stripeFacade.DidNotReceiveWithAnyArgs().GetInvoice(
|
||||
Arg.Any<string>(),
|
||||
@ -236,8 +229,8 @@ public class StripeEventServiceTests
|
||||
var invoice = await _stripeEventService.GetInvoice(stripeEvent, true, expand);
|
||||
|
||||
// Assert
|
||||
invoice.Should().Be(apiInvoice);
|
||||
invoice.Should().NotBeSameAs(eventInvoice);
|
||||
Assert.Equal(apiInvoice, invoice);
|
||||
Assert.NotSame(eventInvoice, invoice);
|
||||
|
||||
await _stripeFacade.Received().GetInvoice(
|
||||
apiInvoice.Id,
|
||||
@ -258,10 +251,8 @@ public class StripeEventServiceTests
|
||||
var function = async () => await _stripeEventService.GetPaymentMethod(stripeEvent);
|
||||
|
||||
// Assert
|
||||
await function
|
||||
.Should()
|
||||
.ThrowAsync<Exception>()
|
||||
.WithMessage($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(PaymentMethod)}'");
|
||||
var exception = await Assert.ThrowsAsync<Exception>(function);
|
||||
Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(PaymentMethod)}'", exception.Message);
|
||||
|
||||
await _stripeFacade.DidNotReceiveWithAnyArgs().GetPaymentMethod(
|
||||
Arg.Any<string>(),
|
||||
@ -280,7 +271,7 @@ public class StripeEventServiceTests
|
||||
var paymentMethod = await _stripeEventService.GetPaymentMethod(stripeEvent);
|
||||
|
||||
// Assert
|
||||
paymentMethod.Should().BeEquivalentTo(stripeEvent.Data.Object as PaymentMethod);
|
||||
Assert.Equivalent(stripeEvent.Data.Object as PaymentMethod, paymentMethod, true);
|
||||
|
||||
await _stripeFacade.DidNotReceiveWithAnyArgs().GetPaymentMethod(
|
||||
Arg.Any<string>(),
|
||||
@ -310,8 +301,8 @@ public class StripeEventServiceTests
|
||||
var paymentMethod = await _stripeEventService.GetPaymentMethod(stripeEvent, true, expand);
|
||||
|
||||
// Assert
|
||||
paymentMethod.Should().Be(apiPaymentMethod);
|
||||
paymentMethod.Should().NotBeSameAs(eventPaymentMethod);
|
||||
Assert.Equal(apiPaymentMethod, paymentMethod);
|
||||
Assert.NotSame(eventPaymentMethod, paymentMethod);
|
||||
|
||||
await _stripeFacade.Received().GetPaymentMethod(
|
||||
apiPaymentMethod.Id,
|
||||
@ -332,10 +323,8 @@ public class StripeEventServiceTests
|
||||
var function = async () => await _stripeEventService.GetSubscription(stripeEvent);
|
||||
|
||||
// Assert
|
||||
await function
|
||||
.Should()
|
||||
.ThrowAsync<Exception>()
|
||||
.WithMessage($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Subscription)}'");
|
||||
var exception = await Assert.ThrowsAsync<Exception>(function);
|
||||
Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Subscription)}'", exception.Message);
|
||||
|
||||
await _stripeFacade.DidNotReceiveWithAnyArgs().GetSubscription(
|
||||
Arg.Any<string>(),
|
||||
@ -354,7 +343,7 @@ public class StripeEventServiceTests
|
||||
var subscription = await _stripeEventService.GetSubscription(stripeEvent);
|
||||
|
||||
// Assert
|
||||
subscription.Should().BeEquivalentTo(stripeEvent.Data.Object as Subscription);
|
||||
Assert.Equivalent(stripeEvent.Data.Object as Subscription, subscription, true);
|
||||
|
||||
await _stripeFacade.DidNotReceiveWithAnyArgs().GetSubscription(
|
||||
Arg.Any<string>(),
|
||||
@ -384,8 +373,8 @@ public class StripeEventServiceTests
|
||||
var subscription = await _stripeEventService.GetSubscription(stripeEvent, true, expand);
|
||||
|
||||
// Assert
|
||||
subscription.Should().Be(apiSubscription);
|
||||
subscription.Should().NotBeSameAs(eventSubscription);
|
||||
Assert.Equal(apiSubscription, subscription);
|
||||
Assert.NotSame(eventSubscription, subscription);
|
||||
|
||||
await _stripeFacade.Received().GetSubscription(
|
||||
apiSubscription.Id,
|
||||
@ -417,7 +406,7 @@ public class StripeEventServiceTests
|
||||
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
||||
|
||||
// Assert
|
||||
cloudRegionValid.Should().BeTrue();
|
||||
Assert.True(cloudRegionValid);
|
||||
|
||||
await _stripeFacade.Received(1).GetSubscription(
|
||||
subscription.Id,
|
||||
@ -447,7 +436,7 @@ public class StripeEventServiceTests
|
||||
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
||||
|
||||
// Assert
|
||||
cloudRegionValid.Should().BeTrue();
|
||||
Assert.True(cloudRegionValid);
|
||||
|
||||
await _stripeFacade.Received(1).GetCharge(
|
||||
charge.Id,
|
||||
@ -475,7 +464,7 @@ public class StripeEventServiceTests
|
||||
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
||||
|
||||
// Assert
|
||||
cloudRegionValid.Should().BeTrue();
|
||||
Assert.True(cloudRegionValid);
|
||||
|
||||
await _stripeFacade.Received(1).GetCustomer(
|
||||
invoice.CustomerId,
|
||||
@ -505,7 +494,7 @@ public class StripeEventServiceTests
|
||||
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
||||
|
||||
// Assert
|
||||
cloudRegionValid.Should().BeTrue();
|
||||
Assert.True(cloudRegionValid);
|
||||
|
||||
await _stripeFacade.Received(1).GetInvoice(
|
||||
invoice.Id,
|
||||
@ -535,7 +524,7 @@ public class StripeEventServiceTests
|
||||
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
||||
|
||||
// Assert
|
||||
cloudRegionValid.Should().BeTrue();
|
||||
Assert.True(cloudRegionValid);
|
||||
|
||||
await _stripeFacade.Received(1).GetPaymentMethod(
|
||||
paymentMethod.Id,
|
||||
@ -561,7 +550,7 @@ public class StripeEventServiceTests
|
||||
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
||||
|
||||
// Assert
|
||||
cloudRegionValid.Should().BeTrue();
|
||||
Assert.True(cloudRegionValid);
|
||||
|
||||
await _stripeFacade.Received(1).GetCustomer(
|
||||
customer.Id,
|
||||
@ -592,7 +581,7 @@ public class StripeEventServiceTests
|
||||
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
||||
|
||||
// Assert
|
||||
cloudRegionValid.Should().BeFalse();
|
||||
Assert.False(cloudRegionValid);
|
||||
|
||||
await _stripeFacade.Received(1).GetSubscription(
|
||||
subscription.Id,
|
||||
@ -623,7 +612,7 @@ public class StripeEventServiceTests
|
||||
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
||||
|
||||
// Assert
|
||||
cloudRegionValid.Should().BeTrue();
|
||||
Assert.True(cloudRegionValid);
|
||||
|
||||
await _stripeFacade.Received(1).GetSubscription(
|
||||
subscription.Id,
|
||||
@ -657,7 +646,7 @@ public class StripeEventServiceTests
|
||||
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
||||
|
||||
// Assert
|
||||
cloudRegionValid.Should().BeTrue();
|
||||
Assert.True(cloudRegionValid);
|
||||
|
||||
await _stripeFacade.Received(1).GetSubscription(
|
||||
subscription.Id,
|
||||
|
@ -19,6 +19,7 @@ using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Models.Mail;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
@ -40,8 +41,6 @@ using Xunit;
|
||||
using Organization = Bit.Core.AdminConsole.Entities.Organization;
|
||||
using OrganizationUser = Bit.Core.Entities.OrganizationUser;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
@ -1450,76 +1449,174 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
||||
[OrganizationUser] OrganizationUser organizationUser, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var eventService = sutProvider.GetDependency<IEventService>();
|
||||
|
||||
await sutProvider.Sut.RevokeUserAsync(organizationUser, owner.Id);
|
||||
|
||||
await organizationUserRepository.Received().RevokeAsync(organizationUser.Id);
|
||||
await eventService.Received()
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
.RevokeAsync(organizationUser.Id);
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RevokeUser_WithPushSyncOrgKeysOnRevokeRestoreEnabled_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||
[OrganizationUser] OrganizationUser organizationUser, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore)
|
||||
.Returns(true);
|
||||
|
||||
await sutProvider.Sut.RevokeUserAsync(organizationUser, owner.Id);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
.RevokeAsync(organizationUser.Id);
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked);
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.Received(1)
|
||||
.PushSyncOrgKeysAsync(organizationUser.UserId!.Value);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RevokeUser_WithEventSystemUser_Success(Organization organization, [OrganizationUser] OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
RestoreRevokeUser_Setup(organization, null, organizationUser, sutProvider);
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var eventService = sutProvider.GetDependency<IEventService>();
|
||||
|
||||
await sutProvider.Sut.RevokeUserAsync(organizationUser, eventSystemUser);
|
||||
|
||||
await organizationUserRepository.Received().RevokeAsync(organizationUser.Id);
|
||||
await eventService.Received()
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
.RevokeAsync(organizationUser.Id);
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked, eventSystemUser);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RevokeUser_WithEventSystemUser_WithPushSyncOrgKeysOnRevokeRestoreEnabled_Success(Organization organization, [OrganizationUser] OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
RestoreRevokeUser_Setup(organization, null, organizationUser, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore)
|
||||
.Returns(true);
|
||||
|
||||
await sutProvider.Sut.RevokeUserAsync(organizationUser, eventSystemUser);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
.RevokeAsync(organizationUser.Id);
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked, eventSystemUser);
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.Received(1)
|
||||
.PushSyncOrgKeysAsync(organizationUser.UserId!.Value);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RestoreUser_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var eventService = sutProvider.GetDependency<IEventService>();
|
||||
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
|
||||
|
||||
await organizationUserRepository.Received().RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited);
|
||||
await eventService.Received()
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
.RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited);
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RestoreUser_WithPushSyncOrgKeysOnRevokeRestoreEnabled_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore)
|
||||
.Returns(true);
|
||||
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
.RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited);
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.Received(1)
|
||||
.PushSyncOrgKeysAsync(organizationUser.UserId!.Value);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RestoreUser_WithEventSystemUser_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
RestoreRevokeUser_Setup(organization, null, organizationUser, sutProvider);
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var eventService = sutProvider.GetDependency<IEventService>();
|
||||
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, eventSystemUser);
|
||||
|
||||
await organizationUserRepository.Received().RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited);
|
||||
await eventService.Received()
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
.RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited);
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored, eventSystemUser);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RestoreUser_WithEventSystemUser_WithPushSyncOrgKeysOnRevokeRestoreEnabled_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
RestoreRevokeUser_Setup(organization, null, organizationUser, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore)
|
||||
.Returns(true);
|
||||
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, eventSystemUser);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
.RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited);
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored, eventSystemUser);
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.Received(1)
|
||||
.PushSyncOrgKeysAsync(organizationUser.UserId!.Value);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RestoreUser_RestoreThemselves_Fails(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
organizationUser.UserId = owner.Id;
|
||||
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var eventService = sutProvider.GetDependency<IEventService>();
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
||||
|
||||
Assert.Contains("you cannot restore yourself", exception.Message.ToLowerInvariant());
|
||||
|
||||
await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||
await eventService.DidNotReceiveWithAnyArgs()
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@ -1531,17 +1628,21 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
restoringUser.Type = restoringUserType;
|
||||
RestoreRevokeUser_Setup(organization, restoringUser, organizationUser, sutProvider);
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var eventService = sutProvider.GetDependency<IEventService>();
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, restoringUser.Id));
|
||||
|
||||
Assert.Contains("only owners can restore other owners", exception.Message.ToLowerInvariant());
|
||||
|
||||
await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||
await eventService.DidNotReceiveWithAnyArgs()
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@ -1553,17 +1654,21 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
organizationUser.Status = userStatus;
|
||||
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var eventService = sutProvider.GetDependency<IEventService>();
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
||||
|
||||
Assert.Contains("already active", exception.Message.ToLowerInvariant());
|
||||
|
||||
await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||
await eventService.DidNotReceiveWithAnyArgs()
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@ -1575,8 +1680,6 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
|
||||
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var eventService = sutProvider.GetDependency<IEventService>();
|
||||
|
||||
sutProvider.GetDependency<IPolicyService>()
|
||||
.AnyPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>())
|
||||
@ -1591,9 +1694,15 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
||||
|
||||
Assert.Contains("test@bitwarden.com belongs to an organization that doesn't allow them to join multiple organizations", exception.Message.ToLowerInvariant());
|
||||
|
||||
await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||
await eventService.DidNotReceiveWithAnyArgs()
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@ -1610,8 +1719,6 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, false) });
|
||||
|
||||
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var eventService = sutProvider.GetDependency<IEventService>();
|
||||
|
||||
sutProvider.GetDependency<IPolicyService>()
|
||||
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
|
||||
@ -1626,9 +1733,15 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
||||
|
||||
Assert.Contains("test@bitwarden.com is not compliant with the two-step login policy", exception.Message.ToLowerInvariant());
|
||||
|
||||
await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||
await eventService.DidNotReceiveWithAnyArgs()
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@ -1640,8 +1753,6 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
|
||||
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var eventService = sutProvider.GetDependency<IEventService>();
|
||||
|
||||
sutProvider.GetDependency<IPolicyService>()
|
||||
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
|
||||
@ -1652,8 +1763,11 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
||||
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
|
||||
|
||||
await organizationUserRepository.Received().RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed);
|
||||
await eventService.Received()
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
.RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed);
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
||||
}
|
||||
|
||||
@ -1668,10 +1782,10 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
||||
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
|
||||
secondOrganizationUser.UserId = organizationUser.UserId;
|
||||
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var eventService = sutProvider.GetDependency<IEventService>();
|
||||
|
||||
organizationUserRepository.GetManyByUserAsync(organizationUser.UserId.Value).Returns(new[] { organizationUser, secondOrganizationUser });
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(organizationUser.UserId.Value)
|
||||
.Returns(new[] { organizationUser, secondOrganizationUser });
|
||||
sutProvider.GetDependency<IPolicyService>()
|
||||
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>())
|
||||
.Returns(new[]
|
||||
@ -1688,9 +1802,15 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
||||
|
||||
Assert.Contains("test@bitwarden.com is not compliant with the single organization policy", exception.Message.ToLowerInvariant());
|
||||
|
||||
await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||
await eventService.DidNotReceiveWithAnyArgs()
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@ -1704,11 +1824,8 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
||||
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
|
||||
secondOrganizationUser.UserId = organizationUser.UserId;
|
||||
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var eventService = sutProvider.GetDependency<IEventService>();
|
||||
var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();
|
||||
|
||||
twoFactorIsEnabledQuery
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
|
||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, true) });
|
||||
|
||||
@ -1725,9 +1842,15 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
||||
|
||||
Assert.Contains("test@bitwarden.com belongs to an organization that doesn't allow them to join multiple organizations", exception.Message.ToLowerInvariant());
|
||||
|
||||
await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||
await eventService.DidNotReceiveWithAnyArgs()
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@ -1741,10 +1864,10 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
||||
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
|
||||
secondOrganizationUser.UserId = organizationUser.UserId;
|
||||
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var eventService = sutProvider.GetDependency<IEventService>();
|
||||
|
||||
organizationUserRepository.GetManyByUserAsync(organizationUser.UserId.Value).Returns(new[] { organizationUser, secondOrganizationUser });
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(organizationUser.UserId.Value)
|
||||
.Returns(new[] { organizationUser, secondOrganizationUser });
|
||||
sutProvider.GetDependency<IPolicyService>()
|
||||
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>())
|
||||
.Returns(new[]
|
||||
@ -1768,9 +1891,15 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
||||
|
||||
Assert.Contains("test@bitwarden.com is not compliant with the single organization and two-step login polciy", exception.Message.ToLowerInvariant());
|
||||
|
||||
await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||
await eventService.DidNotReceiveWithAnyArgs()
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@ -1783,8 +1912,6 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
||||
organizationUser.Email = null;
|
||||
|
||||
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var eventService = sutProvider.GetDependency<IEventService>();
|
||||
|
||||
sutProvider.GetDependency<IPolicyService>()
|
||||
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
|
||||
@ -1799,9 +1926,15 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
||||
|
||||
Assert.Contains("test@bitwarden.com is not compliant with the two-step login policy", exception.Message.ToLowerInvariant());
|
||||
|
||||
await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||
await eventService.DidNotReceiveWithAnyArgs()
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@ -1813,22 +1946,22 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
|
||||
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var eventService = sutProvider.GetDependency<IEventService>();
|
||||
var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();
|
||||
|
||||
sutProvider.GetDependency<IPolicyService>()
|
||||
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
|
||||
.Returns(new[] { new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication } });
|
||||
|
||||
twoFactorIsEnabledQuery
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
|
||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, true) });
|
||||
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
|
||||
|
||||
await organizationUserRepository.Received().RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed);
|
||||
await eventService.Received()
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
.RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed);
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
||||
}
|
||||
|
||||
|
@ -109,7 +109,7 @@ public class OrgUserInviteTokenableTests
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
public void Valid_NullOrEmptyOrgUserEmail_ReturnsFalse(string email)
|
||||
public void Valid_NullOrEmptyOrgUserEmail_ReturnsFalse(string? email)
|
||||
{
|
||||
var token = new OrgUserInviteTokenable
|
||||
{
|
||||
|
@ -7,11 +7,13 @@ using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
@ -347,14 +349,24 @@ public class AuthRequestServiceTests
|
||||
User user,
|
||||
OrganizationUser organizationUser1,
|
||||
OrganizationUserUserDetails admin1,
|
||||
OrganizationUserUserDetails customUser1,
|
||||
OrganizationUser organizationUser2,
|
||||
OrganizationUserUserDetails admin2,
|
||||
OrganizationUserUserDetails admin3)
|
||||
OrganizationUserUserDetails admin3,
|
||||
OrganizationUserUserDetails customUser2)
|
||||
{
|
||||
createModel.Type = AuthRequestType.AdminApproval;
|
||||
user.Email = createModel.Email;
|
||||
organizationUser1.UserId = user.Id;
|
||||
organizationUser2.UserId = user.Id;
|
||||
customUser1.Permissions = CoreHelpers.ClassToJsonData(new Permissions
|
||||
{
|
||||
ManageResetPassword = false,
|
||||
});
|
||||
customUser2.Permissions = CoreHelpers.ClassToJsonData(new Permissions
|
||||
{
|
||||
ManageResetPassword = true,
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.DeviceApprovalRequestAdminNotifications)
|
||||
@ -392,6 +404,13 @@ public class AuthRequestServiceTests
|
||||
admin1,
|
||||
]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByRoleAsync(organizationUser1.OrganizationId, OrganizationUserType.Custom)
|
||||
.Returns(
|
||||
[
|
||||
customUser1,
|
||||
]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByMinimumRoleAsync(organizationUser2.OrganizationId, OrganizationUserType.Admin)
|
||||
.Returns(
|
||||
@ -400,6 +419,13 @@ public class AuthRequestServiceTests
|
||||
admin3,
|
||||
]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByRoleAsync(organizationUser2.OrganizationId, OrganizationUserType.Custom)
|
||||
.Returns(
|
||||
[
|
||||
customUser2,
|
||||
]);
|
||||
|
||||
sutProvider.GetDependency<IAuthRequestRepository>()
|
||||
.CreateAsync(Arg.Any<AuthRequest>())
|
||||
.Returns(c => c.ArgAt<AuthRequest>(0));
|
||||
@ -435,7 +461,9 @@ public class AuthRequestServiceTests
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendDeviceApprovalRequestedNotificationEmailAsync(
|
||||
Arg.Is<IEnumerable<string>>(emails => emails.Count() == 2 && emails.Contains(admin2.Email) && emails.Contains(admin3.Email)),
|
||||
Arg.Is<IEnumerable<string>>(emails => emails.Count() == 3 &&
|
||||
emails.Contains(admin2.Email) && emails.Contains(admin3.Email) &&
|
||||
emails.Contains(customUser2.Email)),
|
||||
organizationUser2.OrganizationId,
|
||||
user.Email,
|
||||
user.Name);
|
||||
|
@ -271,7 +271,7 @@ public class CoreHelpersTests
|
||||
[InlineData("ascii.com", "ascii.com")]
|
||||
[InlineData("", "")]
|
||||
[InlineData(null, null)]
|
||||
public void PunyEncode_Success(string text, string expected)
|
||||
public void PunyEncode_Success(string? text, string? expected)
|
||||
{
|
||||
var actual = CoreHelpers.PunyEncode(text);
|
||||
Assert.Equal(expected, actual);
|
||||
@ -435,7 +435,7 @@ public class CoreHelpersTests
|
||||
[InlineData("name@", "name@")] // @ symbol but no domain
|
||||
[InlineData("", "")] // Empty string
|
||||
[InlineData(null, null)] // null
|
||||
public void ObfuscateEmail_Success(string input, string expected)
|
||||
public void ObfuscateEmail_Success(string? input, string? expected)
|
||||
{
|
||||
Assert.Equal(expected, CoreHelpers.ObfuscateEmail(input));
|
||||
}
|
||||
@ -456,7 +456,7 @@ public class CoreHelpersTests
|
||||
[InlineData("user@")]
|
||||
[InlineData("@example.com")]
|
||||
[InlineData("user@ex@ample.com")]
|
||||
public void GetEmailDomain_ReturnsNull(string wrongEmail)
|
||||
public void GetEmailDomain_ReturnsNull(string? wrongEmail)
|
||||
{
|
||||
Assert.Null(CoreHelpers.GetEmailDomain(wrongEmail));
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ public class EncryptedStringAttributeTests
|
||||
[InlineData("Rsa2048_OaepSha256_HmacSha256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Valid Rsa2048_OaepSha256_HmacSha256_B64 as a string
|
||||
[InlineData("6.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Valid Rsa2048_OaepSha1_HmacSha256_B64 as a number
|
||||
[InlineData("Rsa2048_OaepSha1_HmacSha256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")]
|
||||
public void IsValid_ReturnsTrue_WhenValid(string input)
|
||||
public void IsValid_ReturnsTrue_WhenValid(string? input)
|
||||
{
|
||||
var sut = new EncryptedStringAttribute();
|
||||
|
||||
|
@ -47,7 +47,7 @@ public class StrictEmailAttributeTests
|
||||
[InlineData("hellothere@world.com-")] // domain ending in hyphen
|
||||
[InlineData("hellö@world.com")] // unicode at end of local-part
|
||||
[InlineData("héllo@world.com")] // unicode in middle of local-part
|
||||
public void IsValid_ReturnsFalseWhenInvalid(string email)
|
||||
public void IsValid_ReturnsFalseWhenInvalid(string? email)
|
||||
{
|
||||
var sut = new StrictEmailAddressAttribute();
|
||||
|
||||
|
@ -42,7 +42,7 @@ public class StrictEmailAddressListAttributeTests
|
||||
[Theory]
|
||||
[InlineData("single@email.com", false)]
|
||||
[InlineData(null, false)]
|
||||
public void IsValid_ReturnsTrue_WhenValid(string email, bool valid)
|
||||
public void IsValid_ReturnsTrue_WhenValid(string? email, bool valid)
|
||||
{
|
||||
var sut = new StrictEmailAddressListAttribute();
|
||||
|
||||
|
85
test/Core.Test/Vault/Commands/CreateManyTasksCommandTest.cs
Normal file
85
test/Core.Test/Vault/Commands/CreateManyTasksCommandTest.cs
Normal file
@ -0,0 +1,85 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Test.Vault.AutoFixture;
|
||||
using Bit.Core.Vault.Authorization.SecurityTasks;
|
||||
using Bit.Core.Vault.Commands;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Core.Vault.Models.Api;
|
||||
using Bit.Core.Vault.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Vault.Commands;
|
||||
|
||||
[SutProviderCustomize]
|
||||
[SecurityTaskCustomize]
|
||||
public class CreateManyTasksCommandTest
|
||||
{
|
||||
private static void Setup(SutProvider<CreateManyTasksCommand> sutProvider, Guid? userId,
|
||||
bool authorizedCreate = false)
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
||||
sutProvider.GetDependency<IAuthorizationService>()
|
||||
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<object>(),
|
||||
Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>
|
||||
reqs.Contains(SecurityTaskOperations.Create)))
|
||||
.Returns(authorizedCreate ? AuthorizationResult.Success() : AuthorizationResult.Failed());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task CreateAsync_NotLoggedIn_NotFoundException(
|
||||
SutProvider<CreateManyTasksCommand> sutProvider,
|
||||
Guid organizationId,
|
||||
IEnumerable<SecurityTaskCreateRequest> tasks)
|
||||
{
|
||||
Setup(sutProvider, null, true);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CreateAsync(organizationId, tasks));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task CreateAsync_NoTasksProvided_BadRequestException(
|
||||
SutProvider<CreateManyTasksCommand> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
Setup(sutProvider, Guid.NewGuid());
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.CreateAsync(organizationId, null));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task CreateAsync_AuthorizationFailed_NotFoundException(
|
||||
SutProvider<CreateManyTasksCommand> sutProvider,
|
||||
Guid organizationId,
|
||||
IEnumerable<SecurityTaskCreateRequest> tasks)
|
||||
{
|
||||
Setup(sutProvider, Guid.NewGuid());
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CreateAsync(organizationId, tasks));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task CreateAsync_AuthorizationSucceeded_ReturnsSecurityTasks(
|
||||
SutProvider<CreateManyTasksCommand> sutProvider,
|
||||
Guid organizationId,
|
||||
IEnumerable<SecurityTaskCreateRequest> tasks,
|
||||
ICollection<SecurityTask> securityTasks)
|
||||
{
|
||||
Setup(sutProvider, Guid.NewGuid(), true);
|
||||
sutProvider.GetDependency<ISecurityTaskRepository>()
|
||||
.CreateManyAsync(Arg.Any<ICollection<SecurityTask>>())
|
||||
.Returns(securityTasks);
|
||||
|
||||
var result = await sutProvider.Sut.CreateAsync(organizationId, tasks);
|
||||
|
||||
Assert.Equal(securityTasks, result);
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ using System.Security.Claims;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Test.Vault.AutoFixture;
|
||||
using Bit.Core.Vault.Authorization;
|
||||
using Bit.Core.Vault.Authorization.SecurityTasks;
|
||||
using Bit.Core.Vault.Commands;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Core.Vault.Repositories;
|
||||
|
@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
@ -21,9 +20,9 @@
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Events\Events.csproj" />
|
||||
<ProjectReference Include="..\IntegrationTestCommon\IntegrationTestCommon.csproj" />
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Events\Events.csproj" />
|
||||
<ProjectReference Include="..\IntegrationTestCommon\IntegrationTestCommon.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -45,7 +45,7 @@ public class IconLinkTests
|
||||
[InlineData(" ", false)]
|
||||
[InlineData("unusable", false)]
|
||||
[InlineData("ico", true)]
|
||||
public void WithNoRel_IsUsable(string extension, bool expectedResult)
|
||||
public void WithNoRel_IsUsable(string? extension, bool expectedResult)
|
||||
{
|
||||
SetAttributeValue("href", $"/favicon.{extension}");
|
||||
|
||||
|
@ -1,8 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -3,8 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
@ -22,8 +20,8 @@
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Infrastructure.Dapper\Infrastructure.Dapper.csproj" />
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Infrastructure.Dapper\Infrastructure.Dapper.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -252,12 +252,12 @@ public class OrganizationDomainRepositoryTests
|
||||
Txt = "btw+12345"
|
||||
};
|
||||
|
||||
var outside36HoursWindow = 20;
|
||||
var outside36HoursWindow = 50;
|
||||
organizationDomain.SetNextRunDate(outside36HoursWindow);
|
||||
|
||||
await organizationDomainRepository.CreateAsync(organizationDomain);
|
||||
|
||||
var date = DateTimeOffset.UtcNow.Date.AddDays(1);
|
||||
var date = DateTime.UtcNow.AddDays(1);
|
||||
|
||||
// Act
|
||||
var domains = await organizationDomainRepository.GetManyByNextRunDateAsync(date);
|
||||
|
@ -1,7 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<UserSecretsId>6570f288-5c2c-47ad-8978-f3da255079c2</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
@ -223,4 +223,47 @@ public class SecurityTaskRepositoryTests
|
||||
Assert.DoesNotContain(task1, completedTasks, new SecurityTaskComparer());
|
||||
Assert.DoesNotContain(task3, completedTasks, new SecurityTaskComparer());
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task CreateManyAsync(
|
||||
IOrganizationRepository organizationRepository,
|
||||
ICipherRepository cipherRepository,
|
||||
ISecurityTaskRepository securityTaskRepository)
|
||||
{
|
||||
var organization = await organizationRepository.CreateAsync(new Organization
|
||||
{
|
||||
Name = "Test Org",
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
Plan = "Test Plan",
|
||||
BillingEmail = ""
|
||||
});
|
||||
|
||||
var cipher1 = new Cipher { Type = CipherType.Login, OrganizationId = organization.Id, Data = "", };
|
||||
await cipherRepository.CreateAsync(cipher1);
|
||||
|
||||
var cipher2 = new Cipher { Type = CipherType.Login, OrganizationId = organization.Id, Data = "", };
|
||||
await cipherRepository.CreateAsync(cipher2);
|
||||
|
||||
var tasks = new List<SecurityTask>
|
||||
{
|
||||
new()
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
CipherId = cipher1.Id,
|
||||
Status = SecurityTaskStatus.Pending,
|
||||
Type = SecurityTaskType.UpdateAtRiskCredential,
|
||||
},
|
||||
new()
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
CipherId = cipher2.Id,
|
||||
Status = SecurityTaskStatus.Completed,
|
||||
Type = SecurityTaskType.UpdateAtRiskCredential,
|
||||
}
|
||||
};
|
||||
|
||||
var taskIds = await securityTaskRepository.CreateManyAsync(tasks);
|
||||
|
||||
Assert.Equal(2, taskIds.Count);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,55 @@
|
||||
-- SecurityTask_CreateMany
|
||||
CREATE OR ALTER PROCEDURE [dbo].[SecurityTask_CreateMany]
|
||||
@SecurityTasksJson NVARCHAR(MAX)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
CREATE TABLE #TempSecurityTasks
|
||||
(
|
||||
[Id] UNIQUEIDENTIFIER,
|
||||
[OrganizationId] UNIQUEIDENTIFIER,
|
||||
[CipherId] UNIQUEIDENTIFIER,
|
||||
[Type] TINYINT,
|
||||
[Status] TINYINT,
|
||||
[CreationDate] DATETIME2(7),
|
||||
[RevisionDate] DATETIME2(7)
|
||||
)
|
||||
|
||||
INSERT INTO #TempSecurityTasks
|
||||
([Id],
|
||||
[OrganizationId],
|
||||
[CipherId],
|
||||
[Type],
|
||||
[Status],
|
||||
[CreationDate],
|
||||
[RevisionDate])
|
||||
SELECT CAST(JSON_VALUE([value], '$.Id') AS UNIQUEIDENTIFIER),
|
||||
CAST(JSON_VALUE([value], '$.OrganizationId') AS UNIQUEIDENTIFIER),
|
||||
CAST(JSON_VALUE([value], '$.CipherId') AS UNIQUEIDENTIFIER),
|
||||
CAST(JSON_VALUE([value], '$.Type') AS TINYINT),
|
||||
CAST(JSON_VALUE([value], '$.Status') AS TINYINT),
|
||||
CAST(JSON_VALUE([value], '$.CreationDate') AS DATETIME2(7)),
|
||||
CAST(JSON_VALUE([value], '$.RevisionDate') AS DATETIME2(7))
|
||||
FROM OPENJSON(@SecurityTasksJson) ST
|
||||
|
||||
INSERT INTO [dbo].[SecurityTask]
|
||||
([Id],
|
||||
[OrganizationId],
|
||||
[CipherId],
|
||||
[Type],
|
||||
[Status],
|
||||
[CreationDate],
|
||||
[RevisionDate])
|
||||
SELECT [Id],
|
||||
[OrganizationId],
|
||||
[CipherId],
|
||||
[Type],
|
||||
[Status],
|
||||
[CreationDate],
|
||||
[RevisionDate]
|
||||
FROM #TempSecurityTasks
|
||||
|
||||
DROP TABLE #TempSecurityTasks
|
||||
END
|
||||
GO
|
@ -0,0 +1,16 @@
|
||||
CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_ReadManyDetailsByRole]
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@Role TINYINT
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
[dbo].[OrganizationUserUserDetailsView]
|
||||
WHERE
|
||||
OrganizationId = @OrganizationId
|
||||
AND Status = 2 -- 2 = Confirmed
|
||||
AND [Type] = @Role
|
||||
END
|
@ -0,0 +1,118 @@
|
||||
CREATE OR ALTER PROCEDURE [dbo].[CipherDetails_ReadByIdUserId]
|
||||
@Id UNIQUEIDENTIFIER,
|
||||
@UserId UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
[Id],
|
||||
[UserId],
|
||||
[OrganizationId],
|
||||
[Type],
|
||||
[Data],
|
||||
[Attachments],
|
||||
[CreationDate],
|
||||
[RevisionDate],
|
||||
[Favorite],
|
||||
[FolderId],
|
||||
[DeletedDate],
|
||||
[Reprompt],
|
||||
[Key],
|
||||
[OrganizationUseTotp],
|
||||
MAX ([Edit]) AS [Edit],
|
||||
MAX ([ViewPassword]) AS [ViewPassword]
|
||||
FROM
|
||||
[dbo].[UserCipherDetails](@UserId)
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
GROUP BY
|
||||
[Id],
|
||||
[UserId],
|
||||
[OrganizationId],
|
||||
[Type],
|
||||
[Data],
|
||||
[Attachments],
|
||||
[CreationDate],
|
||||
[RevisionDate],
|
||||
[Favorite],
|
||||
[FolderId],
|
||||
[DeletedDate],
|
||||
[Reprompt],
|
||||
[Key],
|
||||
[OrganizationUseTotp]
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE [dbo].[CollectionCipher_UpdateCollections]
|
||||
@CipherId UNIQUEIDENTIFIER,
|
||||
@UserId UNIQUEIDENTIFIER,
|
||||
@CollectionIds AS [dbo].[GuidIdArray] READONLY
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
DECLARE @OrgId UNIQUEIDENTIFIER = (
|
||||
SELECT TOP 1
|
||||
[OrganizationId]
|
||||
FROM
|
||||
[dbo].[Cipher]
|
||||
WHERE
|
||||
[Id] = @CipherId
|
||||
)
|
||||
SELECT
|
||||
C.[Id]
|
||||
INTO #TempAvailableCollections
|
||||
FROM
|
||||
[dbo].[Collection] C
|
||||
INNER JOIN
|
||||
[Organization] O ON O.[Id] = C.[OrganizationId]
|
||||
INNER JOIN
|
||||
[dbo].[OrganizationUser] OU ON OU.[OrganizationId] = O.[Id] AND OU.[UserId] = @UserId
|
||||
LEFT JOIN
|
||||
[dbo].[CollectionUser] CU ON CU.[CollectionId] = C.[Id] AND CU.[OrganizationUserId] = OU.[Id]
|
||||
LEFT JOIN
|
||||
[dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id]
|
||||
LEFT JOIN
|
||||
[dbo].[Group] G ON G.[Id] = GU.[GroupId]
|
||||
LEFT JOIN
|
||||
[dbo].[CollectionGroup] CG ON CG.[CollectionId] = C.[Id] AND CG.[GroupId] = GU.[GroupId]
|
||||
WHERE
|
||||
O.[Id] = @OrgId
|
||||
AND O.[Enabled] = 1
|
||||
AND OU.[Status] = 2 -- Confirmed
|
||||
AND (
|
||||
CU.[ReadOnly] = 0
|
||||
OR CG.[ReadOnly] = 0
|
||||
)
|
||||
-- Insert new collection assignments
|
||||
INSERT INTO [dbo].[CollectionCipher] (
|
||||
[CollectionId],
|
||||
[CipherId]
|
||||
)
|
||||
SELECT
|
||||
[Id],
|
||||
@CipherId
|
||||
FROM @CollectionIds
|
||||
WHERE [Id] IN (SELECT [Id] FROM [#TempAvailableCollections])
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM [dbo].[CollectionCipher]
|
||||
WHERE [CollectionId] = [@CollectionIds].[Id]
|
||||
AND [CipherId] = @CipherId
|
||||
);
|
||||
|
||||
-- Delete removed collection assignments
|
||||
DELETE CC
|
||||
FROM [dbo].[CollectionCipher] CC
|
||||
WHERE CC.[CipherId] = @CipherId
|
||||
AND CC.[CollectionId] IN (SELECT [Id] FROM [#TempAvailableCollections])
|
||||
AND CC.[CollectionId] NOT IN (SELECT [Id] FROM @CollectionIds);
|
||||
|
||||
IF @OrgId IS NOT NULL
|
||||
BEGIN
|
||||
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrgId
|
||||
END
|
||||
DROP TABLE #TempAvailableCollections;
|
||||
END
|
||||
GO
|
Loading…
x
Reference in New Issue
Block a user